From 5717614f613744234f5135f11a807776e094a856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Thu, 22 Jan 2026 13:26:01 +0800 Subject: [PATCH] a --- backend/api/routes/account.py | 26 ++--- frontend/src/components/GlobalConfig.jsx | 45 +++++++-- frontend/src/services/api.js | 29 ++++++ trading_system/position_manager.py | 118 ++++++++++++++++++----- 4 files changed, 176 insertions(+), 42 deletions(-) diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py index 50d28b6..e00f01f 100644 --- a/backend/api/routes/account.py +++ b/backend/api/routes/account.py @@ -26,9 +26,11 @@ async def _ensure_exchange_sltp_for_symbol(symbol: str, account_id: int = 1): 该接口用于“手动补挂”,不依赖 trading_system 的监控任务。 """ # 从 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: - 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(复用其精度/持仓模式处理) try: @@ -286,7 +288,8 @@ async def ensure_all_positions_sltp( # 先拿当前持仓symbol列表 api_key, api_secret, use_testnet = Account.get_credentials(account_id) 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: from binance_client import BinanceClient @@ -365,7 +368,8 @@ async def get_realtime_account_data(account_id: int = 1): logger.info(f" - 使用测试网: {use_testnet}") 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}") raise HTTPException( status_code=400, @@ -563,11 +567,11 @@ async def get_realtime_positions(account_id: int = Depends(get_account_id)): try: 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: - error_msg = "API密钥未配置" - logger.warning(error_msg) + error_msg = f"API密钥未配置(account_id={account_id})" + logger.warning(f"[account_id={account_id}] {error_msg}") raise HTTPException( status_code=400, 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) if not api_key or not api_secret: - error_msg = "API密钥未配置" - logger.warning(error_msg) + error_msg = f"API密钥未配置(account_id={account_id})" + logger.warning(f"[account_id={account_id}] {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) if not api_key or not api_secret: - error_msg = "API密钥未配置" - logger.warning(error_msg) + error_msg = f"API密钥未配置(account_id={account_id})" + logger.warning(f"[account_id={account_id}] {error_msg}") raise HTTPException(status_code=400, detail=error_msg) # 导入必要的模块 diff --git a/frontend/src/components/GlobalConfig.jsx b/frontend/src/components/GlobalConfig.jsx index 3b1e5cb..90a886f 100644 --- a/frontend/src/components/GlobalConfig.jsx +++ b/frontend/src/components/GlobalConfig.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react' import { Link } from 'react-router-dom' -import { api, getCurrentAccountId } from '../services/api' +import { api } from '../services/api' import './GlobalConfig.css' import './ConfigPanel.css' // 复用 ConfigPanel 的样式 @@ -23,7 +23,6 @@ const GlobalConfig = ({ currentUser }) => { // 预设方案相关 const [configs, setConfigs] = useState({}) const [saving, setSaving] = useState(false) - const [currentAccountId, setCurrentAccountId] = useState(getCurrentAccountId()) const [configMeta, setConfigMeta] = useState(null) // 配置快照相关 @@ -217,8 +216,16 @@ const GlobalConfig = ({ currentUser }) => { const loadConfigs = async () => { try { - const data = await api.getConfigs() - setConfigs(data) + // 管理员全局配置:使用全局策略账号的配置,不依赖当前 account + 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) { console.error('Failed to load configs:', error) } @@ -288,8 +295,13 @@ const GlobalConfig = ({ currentUser }) => { loadAccounts() // 只有管理员才加载配置和系统状态 if (isAdmin) { - loadConfigMeta().catch(() => {}) // 静默失败 - loadConfigs().catch(() => {}) // 静默失败 + // 先加载 configMeta,再加载 configs(因为 loadConfigs 需要 global_strategy_account_id) + loadConfigMeta() + .then(() => { + // configMeta 加载完成后,再加载 configs + loadConfigs().catch(() => {}) + }) + .catch(() => {}) // 静默失败 loadSystemStatus().catch(() => {}) // 静默失败 loadBackendStatus().catch(() => {}) // 静默失败 @@ -451,7 +463,14 @@ const GlobalConfig = ({ currentUser }) => { } }).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}`) if (response.note) { setTimeout(() => { @@ -490,7 +509,14 @@ const GlobalConfig = ({ currentUser }) => { } 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 categoryMap = { @@ -695,7 +721,8 @@ const GlobalConfig = ({ currentUser }) => { const globalStrategyAccountId = configMeta?.global_strategy_account_id ? parseInt(String(configMeta.global_strategy_account_id), 10) : 1 - const isGlobalStrategyAccount = isAdmin && currentAccountId === globalStrategyAccountId + // 管理员全局配置页面:不依赖当前 account,直接管理全局策略账号 + const isGlobalStrategyAccount = isAdmin // 简单计算:当前预设(直接在 render 时计算,不使用 useMemo) let currentPreset = null diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 3b5b63a..34c3405 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -217,6 +217,35 @@ export const api = { } 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 () => { const response = await fetch(buildUrl('/api/config'), { headers: withAccountHeaders() }); if (!response.ok) { diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 3c0b2c8..f02b3f6 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -1189,6 +1189,7 @@ class PositionManager: min_hold_sec = int(config.TRADING_CONFIG.get('MIN_HOLD_TIME_SEC', 1800) or 1800) entry_time = position_info.get('entryTime') hold_time_sec = 0 + hold_time_minutes = 0 if entry_time: try: if isinstance(entry_time, datetime): @@ -1196,14 +1197,22 @@ class PositionManager: 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_sec = 0 + hold_time_minutes = 0 # 如果持仓时间不足,禁止平仓(除非是手动平仓) if hold_time_sec < min_hold_sec: - logger.debug( - f"{symbol} [持仓时间锁] 持仓时间 {hold_time_sec}s < 最小要求 {min_hold_sec}s," - f"禁止自动平仓(强制波段持仓纪律)" + remaining_sec = min_hold_sec - hold_time_sec + remaining_minutes = remaining_sec / 60.0 + 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 # 跳过这个持仓,不触发任何平仓逻辑 @@ -1240,7 +1249,12 @@ class PositionManager: except Exception as e: logger.debug(f"从Redis重新加载配置失败: {e}") + # 检查是否启用移动止损(默认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: trailing_activation = config.TRADING_CONFIG.get('TRAILING_STOP_ACTIVATION', 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: - 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' + + # 计算持仓时间 + 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: 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) entry_time = position_info.get('entryTime') hold_time_sec = 0 + hold_time_minutes = 0 if entry_time: try: if isinstance(entry_time, datetime): @@ -2377,18 +2417,31 @@ class PositionManager: 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_sec = 0 + hold_time_minutes = 0 # 如果持仓时间不足,禁止平仓(除非是手动平仓) if hold_time_sec < min_hold_sec: - logger.debug( - f"{symbol} [持仓时间锁] 持仓时间 {hold_time_sec}s < 最小要求 {min_hold_sec}s," - f"禁止自动平仓(强制波段持仓纪律)" + remaining_sec = min_hold_sec - hold_time_sec + remaining_minutes = remaining_sec / 60.0 + 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 # 不触发任何平仓逻辑 + # 检查是否启用移动止损(默认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: trailing_activation = config.TRADING_CONFIG.get('TRAILING_STOP_ACTIVATION', 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: should_close = True 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 | " - f"当前价={current_price_float:.4f}, 止损价={stop_loss:.4f} | " - f"保证金={margin:.4f} USDT, 亏损金额={pnl_amount:.4f} USDT" - ) + + # 详细诊断日志:记录平仓时的所有关键信息 + logger.warning("=" * 80) + logger.warning(f"{symbol} [实时监控-平仓诊断日志] ===== 触发止损平仓 =====") + 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: @@ -2523,11 +2587,21 @@ class PositionManager: if pnl_percent_margin >= take_profit_pct_margin: should_close = True exit_reason = 'take_profit' - logger.info( - f"{symbol} [实时监控] 触发止盈(基于保证金): " - f"当前盈亏={pnl_percent_margin:.2f}% of margin >= 止盈目标={take_profit_pct_margin:.2f}% of margin | " - f"当前价={current_price_float:.4f}, 止盈价={take_profit:.4f}" - ) + + # 详细诊断日志:记录平仓时的所有关键信息 + logger.info("=" * 80) + 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: