This commit is contained in:
薇薇安 2026-01-22 13:26:01 +08:00
parent 0ecdff4530
commit 5717614f61
4 changed files with 176 additions and 42 deletions

View File

@ -26,9 +26,11 @@ async def _ensure_exchange_sltp_for_symbol(symbol: str, account_id: int = 1):
该接口用于手动补挂不依赖 trading_system 的监控任务 该接口用于手动补挂不依赖 trading_system 的监控任务
""" """
# 从 accounts 表读取账号私有API密钥 # 从 accounts 表读取账号私有API密钥
api_key, api_secret, use_testnet = Account.get_credentials(int(account_id or 1)) account_id_int = int(account_id or 1)
api_key, api_secret, use_testnet = Account.get_credentials(account_id_int)
if not api_key or not api_secret: if not api_key or not api_secret:
raise HTTPException(status_code=400, detail="API密钥未配置") logger.error(f"[account_id={account_id_int}] API密钥未配置")
raise HTTPException(status_code=400, detail=f"API密钥未配置account_id={account_id_int}")
# 导入交易系统的BinanceClient复用其精度/持仓模式处理) # 导入交易系统的BinanceClient复用其精度/持仓模式处理)
try: try:
@ -286,7 +288,8 @@ async def ensure_all_positions_sltp(
# 先拿当前持仓symbol列表 # 先拿当前持仓symbol列表
api_key, api_secret, use_testnet = Account.get_credentials(account_id) api_key, api_secret, use_testnet = Account.get_credentials(account_id)
if not api_key or not api_secret: if not api_key or not api_secret:
raise HTTPException(status_code=400, detail="API密钥未配置") logger.error(f"[account_id={account_id}] API密钥未配置")
raise HTTPException(status_code=400, detail=f"API密钥未配置account_id={account_id}")
try: try:
from binance_client import BinanceClient from binance_client import BinanceClient
@ -365,7 +368,8 @@ async def get_realtime_account_data(account_id: int = 1):
logger.info(f" - 使用测试网: {use_testnet}") logger.info(f" - 使用测试网: {use_testnet}")
if not api_key or not api_secret: if not api_key or not api_secret:
error_msg = "API密钥未配置请在配置界面设置该账号的BINANCE_API_KEY和BINANCE_API_SECRET" error_msg = f"API密钥未配置account_id={account_id}请在配置界面设置该账号的BINANCE_API_KEY和BINANCE_API_SECRET"
logger.error(f"[account_id={account_id}] API密钥未配置")
logger.error(f"{error_msg}") logger.error(f"{error_msg}")
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
@ -563,11 +567,11 @@ async def get_realtime_positions(account_id: int = Depends(get_account_id)):
try: try:
api_key, api_secret, use_testnet = Account.get_credentials(account_id) api_key, api_secret, use_testnet = Account.get_credentials(account_id)
logger.info(f"尝试获取实时持仓数据 (testnet={use_testnet})") logger.info(f"尝试获取实时持仓数据 (testnet={use_testnet}, account_id={account_id})")
if not api_key or not api_secret: if not api_key or not api_secret:
error_msg = "API密钥未配置" error_msg = f"API密钥未配置account_id={account_id}"
logger.warning(error_msg) logger.warning(f"[account_id={account_id}] {error_msg}")
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail=error_msg detail=error_msg
@ -737,8 +741,8 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
api_key, api_secret, use_testnet = Account.get_credentials(account_id) api_key, api_secret, use_testnet = Account.get_credentials(account_id)
if not api_key or not api_secret: if not api_key or not api_secret:
error_msg = "API密钥未配置" error_msg = f"API密钥未配置account_id={account_id}"
logger.warning(error_msg) logger.warning(f"[account_id={account_id}] {error_msg}")
raise HTTPException(status_code=400, detail=error_msg) raise HTTPException(status_code=400, detail=error_msg)
# 导入必要的模块 # 导入必要的模块
@ -1048,8 +1052,8 @@ async def sync_positions(account_id: int = Depends(get_account_id)):
api_key, api_secret, use_testnet = Account.get_credentials(account_id) api_key, api_secret, use_testnet = Account.get_credentials(account_id)
if not api_key or not api_secret: if not api_key or not api_secret:
error_msg = "API密钥未配置" error_msg = f"API密钥未配置account_id={account_id}"
logger.warning(error_msg) logger.warning(f"[account_id={account_id}] {error_msg}")
raise HTTPException(status_code=400, detail=error_msg) raise HTTPException(status_code=400, detail=error_msg)
# 导入必要的模块 # 导入必要的模块

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from 'react' import React, { useState, useEffect, useRef } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { api, getCurrentAccountId } from '../services/api' import { api } from '../services/api'
import './GlobalConfig.css' import './GlobalConfig.css'
import './ConfigPanel.css' // ConfigPanel import './ConfigPanel.css' // ConfigPanel
@ -23,7 +23,6 @@ const GlobalConfig = ({ currentUser }) => {
// //
const [configs, setConfigs] = useState({}) const [configs, setConfigs] = useState({})
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [currentAccountId, setCurrentAccountId] = useState(getCurrentAccountId())
const [configMeta, setConfigMeta] = useState(null) const [configMeta, setConfigMeta] = useState(null)
// //
@ -217,8 +216,16 @@ const GlobalConfig = ({ currentUser }) => {
const loadConfigs = async () => { const loadConfigs = async () => {
try { try {
const data = await api.getConfigs() // 使 account
setConfigs(data) if (isAdmin && configMeta?.global_strategy_account_id) {
const globalAccountId = parseInt(String(configMeta.global_strategy_account_id), 10) || 1
const data = await api.getGlobalConfigs(globalAccountId)
setConfigs(data)
} else {
// configMeta 使
const data = await api.getConfigs()
setConfigs(data)
}
} catch (error) { } catch (error) {
console.error('Failed to load configs:', error) console.error('Failed to load configs:', error)
} }
@ -288,8 +295,13 @@ const GlobalConfig = ({ currentUser }) => {
loadAccounts() loadAccounts()
// //
if (isAdmin) { if (isAdmin) {
loadConfigMeta().catch(() => {}) // // configMeta configs loadConfigs global_strategy_account_id
loadConfigs().catch(() => {}) // loadConfigMeta()
.then(() => {
// configMeta configs
loadConfigs().catch(() => {})
})
.catch(() => {}) //
loadSystemStatus().catch(() => {}) // loadSystemStatus().catch(() => {}) //
loadBackendStatus().catch(() => {}) // loadBackendStatus().catch(() => {}) //
@ -451,7 +463,14 @@ const GlobalConfig = ({ currentUser }) => {
} }
}).filter(Boolean) }).filter(Boolean)
const response = await api.updateConfigsBatch(configItems) //
let response
if (isAdmin && configMeta?.global_strategy_account_id) {
const globalAccountId = parseInt(String(configMeta.global_strategy_account_id), 10) || 1
response = await api.updateGlobalConfigsBatch(configItems, globalAccountId)
} else {
response = await api.updateConfigsBatch(configItems)
}
setMessage(response.message || `已应用${preset.name}`) setMessage(response.message || `已应用${preset.name}`)
if (response.note) { if (response.note) {
setTimeout(() => { setTimeout(() => {
@ -490,7 +509,14 @@ const GlobalConfig = ({ currentUser }) => {
} }
const buildConfigSnapshot = async (includeSecrets) => { const buildConfigSnapshot = async (includeSecrets) => {
const data = await api.getConfigs() // 使
let data
if (isAdmin && configMeta?.global_strategy_account_id) {
const globalAccountId = parseInt(String(configMeta.global_strategy_account_id), 10) || 1
data = await api.getGlobalConfigs(globalAccountId)
} else {
data = await api.getConfigs()
}
const now = new Date() const now = new Date()
const categoryMap = { const categoryMap = {
@ -695,7 +721,8 @@ const GlobalConfig = ({ currentUser }) => {
const globalStrategyAccountId = configMeta?.global_strategy_account_id const globalStrategyAccountId = configMeta?.global_strategy_account_id
? parseInt(String(configMeta.global_strategy_account_id), 10) ? parseInt(String(configMeta.global_strategy_account_id), 10)
: 1 : 1
const isGlobalStrategyAccount = isAdmin && currentAccountId === globalStrategyAccountId // account
const isGlobalStrategyAccount = isAdmin
// render 使 useMemo // render 使 useMemo
let currentPreset = null let currentPreset = null

View File

@ -217,6 +217,35 @@ export const api = {
} }
return response.json() return response.json()
}, },
// 全局配置:获取全局策略账号的配置(管理员专用,不依赖当前 account
getGlobalConfigs: async (globalAccountId) => {
const response = await fetch(buildUrl('/api/config'), {
headers: withAuthHeaders({ 'X-Account-Id': String(globalAccountId || 1) })
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '获取全局配置失败' }))
throw new Error(error.detail || '获取全局配置失败')
}
return response.json()
},
// 全局配置:批量更新全局策略账号的配置(管理员专用)
updateGlobalConfigsBatch: async (configs, globalAccountId) => {
const response = await fetch(buildUrl('/api/config/batch'), {
method: 'POST',
headers: withAuthHeaders({
'Content-Type': 'application/json',
'X-Account-Id': String(globalAccountId || 1)
}),
body: JSON.stringify(configs)
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.detail || '更新全局配置失败')
}
return response.json()
},
getConfigs: async () => { getConfigs: async () => {
const response = await fetch(buildUrl('/api/config'), { headers: withAccountHeaders() }); const response = await fetch(buildUrl('/api/config'), { headers: withAccountHeaders() });
if (!response.ok) { if (!response.ok) {

View File

@ -1189,6 +1189,7 @@ class PositionManager:
min_hold_sec = int(config.TRADING_CONFIG.get('MIN_HOLD_TIME_SEC', 1800) or 1800) min_hold_sec = int(config.TRADING_CONFIG.get('MIN_HOLD_TIME_SEC', 1800) or 1800)
entry_time = position_info.get('entryTime') entry_time = position_info.get('entryTime')
hold_time_sec = 0 hold_time_sec = 0
hold_time_minutes = 0
if entry_time: if entry_time:
try: try:
if isinstance(entry_time, datetime): if isinstance(entry_time, datetime):
@ -1196,14 +1197,22 @@ class PositionManager:
else: else:
# 兼容:如果是时间戳或字符串 # 兼容:如果是时间戳或字符串
hold_time_sec = int(time.time() - (float(entry_time) if isinstance(entry_time, (int, float)) else 0)) hold_time_sec = int(time.time() - (float(entry_time) if isinstance(entry_time, (int, float)) else 0))
hold_time_minutes = hold_time_sec / 60.0
except Exception: except Exception:
hold_time_sec = 0 hold_time_sec = 0
hold_time_minutes = 0
# 如果持仓时间不足,禁止平仓(除非是手动平仓) # 如果持仓时间不足,禁止平仓(除非是手动平仓)
if hold_time_sec < min_hold_sec: if hold_time_sec < min_hold_sec:
logger.debug( remaining_sec = min_hold_sec - hold_time_sec
f"{symbol} [持仓时间锁] 持仓时间 {hold_time_sec}s < 最小要求 {min_hold_sec}s" remaining_minutes = remaining_sec / 60.0
f"禁止自动平仓(强制波段持仓纪律)" logger.warning(
f"{symbol} [持仓时间锁] ⚠ 持仓时间不足,禁止自动平仓: "
f"已持仓 {hold_time_minutes:.1f} 分钟 ({hold_time_sec}s) < "
f"最小要求 {min_hold_sec/60:.1f} 分钟 ({min_hold_sec}s) | "
f"还需等待 {remaining_minutes:.1f} 分钟 ({remaining_sec}s) | "
f"入场时间: {entry_time} | "
f"强制波段持仓纪律,避免分钟级平仓"
) )
continue # 跳过这个持仓,不触发任何平仓逻辑 continue # 跳过这个持仓,不触发任何平仓逻辑
@ -1240,7 +1249,12 @@ class PositionManager:
except Exception as e: except Exception as e:
logger.debug(f"从Redis重新加载配置失败: {e}") logger.debug(f"从Redis重新加载配置失败: {e}")
# 检查是否启用移动止损默认False需要显式启用
use_trailing = config.TRADING_CONFIG.get('USE_TRAILING_STOP', False) use_trailing = config.TRADING_CONFIG.get('USE_TRAILING_STOP', False)
if use_trailing:
logger.debug(f"{symbol} [移动止损] 已启用,将检查移动止损逻辑")
else:
logger.debug(f"{symbol} [移动止损] 已禁用USE_TRAILING_STOP=False跳过移动止损检查")
if use_trailing: if use_trailing:
trailing_activation = config.TRADING_CONFIG.get('TRAILING_STOP_ACTIVATION', 0.01) # 相对于保证金 trailing_activation = config.TRADING_CONFIG.get('TRAILING_STOP_ACTIVATION', 0.01) # 相对于保证金
trailing_protect = config.TRADING_CONFIG.get('TRAILING_STOP_PROTECT', 0.01) # 相对于保证金 trailing_protect = config.TRADING_CONFIG.get('TRAILING_STOP_PROTECT', 0.01) # 相对于保证金
@ -1327,13 +1341,38 @@ class PositionManager:
# 直接比较当前盈亏百分比与止损目标(基于保证金) # 直接比较当前盈亏百分比与止损目标(基于保证金)
if pnl_percent_margin <= -stop_loss_pct_margin: if pnl_percent_margin <= -stop_loss_pct_margin:
logger.warning(
f"{symbol} 触发止损(基于保证金): "
f"当前盈亏={pnl_percent_margin:.2f}% of margin <= 止损目标=-{stop_loss_pct_margin:.2f}% of margin | "
f"当前价={current_price:.4f}, 止损价={stop_loss:.4f}"
)
# 确定平仓原因 # 确定平仓原因
exit_reason = 'trailing_stop' if position_info.get('trailingStopActivated') else 'stop_loss' exit_reason = 'trailing_stop' if position_info.get('trailingStopActivated') else 'stop_loss'
# 计算持仓时间
entry_time = position_info.get('entryTime')
hold_time_minutes = 0
if entry_time:
try:
if isinstance(entry_time, datetime):
hold_time_sec = int((get_beijing_time() - entry_time).total_seconds())
else:
hold_time_sec = int(time.time() - (float(entry_time) if isinstance(entry_time, (int, float)) else 0))
hold_time_minutes = hold_time_sec / 60.0
except Exception:
hold_time_minutes = 0
# 详细诊断日志:记录平仓时的所有关键信息
logger.warning("=" * 80)
logger.warning(f"{symbol} [平仓诊断日志] ===== 触发止损平仓 =====")
logger.warning(f" 平仓原因: {exit_reason}")
logger.warning(f" 入场价格: {entry_price:.6f} USDT")
logger.warning(f" 当前价格: {current_price:.4f} USDT")
logger.warning(f" 止损价格: {stop_loss:.4f} USDT")
logger.warning(f" 持仓数量: {quantity:.4f}")
logger.warning(f" 持仓时间: {hold_time_minutes:.1f} 分钟")
logger.warning(f" 入场时间: {entry_time}")
logger.warning(f" 当前盈亏: {pnl_percent_margin:.2f}% of margin")
logger.warning(f" 止损目标: -{stop_loss_pct_margin*100:.2f}% of margin")
logger.warning(f" 亏损金额: {abs(pnl_amount):.4f} USDT")
if position_info.get('trailingStopActivated'):
logger.warning(f" 移动止损: 已激活(从初始止损 {position_info.get('initialStopLoss', 'N/A')} 调整)")
logger.warning("=" * 80)
# 更新数据库 # 更新数据库
if DB_AVAILABLE: if DB_AVAILABLE:
trade_id = position_info.get('tradeId') trade_id = position_info.get('tradeId')
@ -2370,6 +2409,7 @@ class PositionManager:
min_hold_sec = int(config.TRADING_CONFIG.get('MIN_HOLD_TIME_SEC', 1800) or 1800) min_hold_sec = int(config.TRADING_CONFIG.get('MIN_HOLD_TIME_SEC', 1800) or 1800)
entry_time = position_info.get('entryTime') entry_time = position_info.get('entryTime')
hold_time_sec = 0 hold_time_sec = 0
hold_time_minutes = 0
if entry_time: if entry_time:
try: try:
if isinstance(entry_time, datetime): if isinstance(entry_time, datetime):
@ -2377,18 +2417,31 @@ class PositionManager:
else: else:
# 兼容:如果是时间戳或字符串 # 兼容:如果是时间戳或字符串
hold_time_sec = int(time.time() - (float(entry_time) if isinstance(entry_time, (int, float)) else 0)) hold_time_sec = int(time.time() - (float(entry_time) if isinstance(entry_time, (int, float)) else 0))
hold_time_minutes = hold_time_sec / 60.0
except Exception: except Exception:
hold_time_sec = 0 hold_time_sec = 0
hold_time_minutes = 0
# 如果持仓时间不足,禁止平仓(除非是手动平仓) # 如果持仓时间不足,禁止平仓(除非是手动平仓)
if hold_time_sec < min_hold_sec: if hold_time_sec < min_hold_sec:
logger.debug( remaining_sec = min_hold_sec - hold_time_sec
f"{symbol} [持仓时间锁] 持仓时间 {hold_time_sec}s < 最小要求 {min_hold_sec}s" remaining_minutes = remaining_sec / 60.0
f"禁止自动平仓(强制波段持仓纪律)" logger.warning(
f"{symbol} [实时监控-持仓时间锁] ⚠ 持仓时间不足,禁止自动平仓: "
f"已持仓 {hold_time_minutes:.1f} 分钟 ({hold_time_sec}s) < "
f"最小要求 {min_hold_sec/60:.1f} 分钟 ({min_hold_sec}s) | "
f"还需等待 {remaining_minutes:.1f} 分钟 ({remaining_sec}s) | "
f"入场时间: {entry_time} | "
f"强制波段持仓纪律,避免分钟级平仓"
) )
return # 不触发任何平仓逻辑 return # 不触发任何平仓逻辑
# 检查是否启用移动止损默认False需要显式启用
use_trailing = config.TRADING_CONFIG.get('USE_TRAILING_STOP', False) use_trailing = config.TRADING_CONFIG.get('USE_TRAILING_STOP', False)
if use_trailing:
logger.debug(f"{symbol} [实时监控-移动止损] 已启用,将检查移动止损逻辑")
else:
logger.debug(f"{symbol} [实时监控-移动止损] 已禁用USE_TRAILING_STOP=False跳过移动止损检查")
if use_trailing: if use_trailing:
trailing_activation = config.TRADING_CONFIG.get('TRAILING_STOP_ACTIVATION', 0.01) # 相对于保证金 trailing_activation = config.TRADING_CONFIG.get('TRAILING_STOP_ACTIVATION', 0.01) # 相对于保证金
trailing_protect = config.TRADING_CONFIG.get('TRAILING_STOP_PROTECT', 0.01) # 相对于保证金 trailing_protect = config.TRADING_CONFIG.get('TRAILING_STOP_PROTECT', 0.01) # 相对于保证金
@ -2484,12 +2537,23 @@ class PositionManager:
if pnl_percent_margin <= -stop_loss_pct_margin: if pnl_percent_margin <= -stop_loss_pct_margin:
should_close = True should_close = True
exit_reason = 'trailing_stop' if position_info.get('trailingStopActivated') else 'stop_loss' exit_reason = 'trailing_stop' if position_info.get('trailingStopActivated') else 'stop_loss'
logger.warning(
f"{symbol} [实时监控] ⚠⚠⚠ 触发止损(基于保证金): " # 详细诊断日志:记录平仓时的所有关键信息
f"当前盈亏={pnl_percent_margin:.2f}% of margin <= 止损目标=-{stop_loss_pct_margin:.2f}% of margin | " logger.warning("=" * 80)
f"当前价={current_price_float:.4f}, 止损价={stop_loss:.4f} | " logger.warning(f"{symbol} [实时监控-平仓诊断日志] ===== 触发止损平仓 =====")
f"保证金={margin:.4f} USDT, 亏损金额={pnl_amount:.4f} USDT" logger.warning(f" 平仓原因: {exit_reason}")
) logger.warning(f" 入场价格: {entry_price:.6f} USDT")
logger.warning(f" 当前价格: {current_price_float:.6f} USDT")
logger.warning(f" 止损价格: {stop_loss:.4f} USDT")
logger.warning(f" 持仓数量: {quantity:.4f}")
logger.warning(f" 持仓时间: {hold_time_minutes:.1f} 分钟")
logger.warning(f" 入场时间: {entry_time}")
logger.warning(f" 当前盈亏: {pnl_percent_margin:.2f}% of margin")
logger.warning(f" 止损目标: -{stop_loss_pct_margin:.2f}% of margin")
logger.warning(f" 亏损金额: {abs(pnl_amount):.4f} USDT")
if position_info.get('trailingStopActivated'):
logger.warning(f" 移动止损: 已激活(从初始止损 {position_info.get('initialStopLoss', 'N/A')} 调整)")
logger.warning("=" * 80)
# 检查止盈(基于保证金收益比) # 检查止盈(基于保证金收益比)
if not should_close: if not should_close:
@ -2523,11 +2587,21 @@ class PositionManager:
if pnl_percent_margin >= take_profit_pct_margin: if pnl_percent_margin >= take_profit_pct_margin:
should_close = True should_close = True
exit_reason = 'take_profit' exit_reason = 'take_profit'
logger.info(
f"{symbol} [实时监控] 触发止盈(基于保证金): " # 详细诊断日志:记录平仓时的所有关键信息
f"当前盈亏={pnl_percent_margin:.2f}% of margin >= 止盈目标={take_profit_pct_margin:.2f}% of margin | " logger.info("=" * 80)
f"当前价={current_price_float:.4f}, 止盈价={take_profit:.4f}" logger.info(f"{symbol} [实时监控-平仓诊断日志] ===== 触发止盈平仓 =====")
) logger.info(f" 平仓原因: {exit_reason}")
logger.info(f" 入场价格: {entry_price:.6f} USDT")
logger.info(f" 当前价格: {current_price_float:.6f} USDT")
logger.info(f" 止盈价格: {take_profit:.4f} USDT")
logger.info(f" 持仓数量: {quantity:.4f}")
logger.info(f" 持仓时间: {hold_time_minutes:.1f} 分钟")
logger.info(f" 入场时间: {entry_time}")
logger.info(f" 当前盈亏: {pnl_percent_margin:.2f}% of margin")
logger.info(f" 止盈目标: {take_profit_pct_margin:.2f}% of margin")
logger.info(f" 盈利金额: {pnl_amount:.4f} USDT")
logger.info("=" * 80)
# 如果触发止损止盈,执行平仓 # 如果触发止损止盈,执行平仓
if should_close: if should_close: