diff --git a/backend/api/routes/config.py b/backend/api/routes/config.py index 3775cab..73de433 100644 --- a/backend/api/routes/config.py +++ b/backend/api/routes/config.py @@ -150,6 +150,12 @@ SMART_ENTRY_CONFIG_DEFAULTS = { "category": "strategy", "description": "下单后确认成交等待时间(秒)。", }, + "ALGO_ORDER_TIMEOUT_SEC": { + "value": 45, + "type": "number", + "category": "strategy", + "description": "Algo 条件单(止损/止盈)单次请求超时(秒)。币安接口高负载时易超时,可调大至 60。", + }, "ENTRY_MAX_DRIFT_PCT_TRENDING": { "value": 0.006, # 0.6% "type": "number", diff --git a/backend/config_manager.py b/backend/config_manager.py index b48aedf..7c3fc27 100644 --- a/backend/config_manager.py +++ b/backend/config_manager.py @@ -911,6 +911,8 @@ class ConfigManager: 'ENTRY_CONFIRM_TIMEOUT_SEC': eff_get('ENTRY_CONFIRM_TIMEOUT_SEC', 30), 'ENTRY_MAX_DRIFT_PCT_TRENDING': eff_get('ENTRY_MAX_DRIFT_PCT_TRENDING', 0.006), 'ENTRY_MAX_DRIFT_PCT_RANGING': eff_get('ENTRY_MAX_DRIFT_PCT_RANGING', 0.3), + # Algo 条件单(止损/止盈)单次请求超时(秒),币安接口高负载时易超时,网络不稳可调大至 60 + 'ALGO_ORDER_TIMEOUT_SEC': eff_get('ALGO_ORDER_TIMEOUT_SEC', 45), # 持仓详细监控日志开关(用于排查问题时观察每次检查的当前价/目标价/ROE 等) 'POSITION_DETAILED_LOG_ENABLED': eff_get('POSITION_DETAILED_LOG_ENABLED', False), diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index ee22128..d75a547 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -2270,6 +2270,10 @@ class BinanceClient: continue logger.error(f"{symbol} ❌ 创建 Algo 条件单失败: 请求超时({algo_timeout}秒,已重试)") logger.error(f" 参数: {params}") + logger.error( + f" 💡 可能原因: (1) 网络/币安 API 慢,可调大 ALGO_ORDER_TIMEOUT_SEC(当前 {algo_timeout}s)" + f" (2) 条件单已触发平仓,持仓消失后挂单会卡住或 -4509,见 docs/bian 条件订单交易更新推送 EXPIRED" + ) return None except BinanceAPIException as e: error_code = e.code if hasattr(e, 'code') else None @@ -2592,8 +2596,8 @@ class BinanceClient: logger.info(f"{symbol} ✓ 重试成功(移除 positionSide)") return retry_order - logger.error(f"{symbol} ❌ 保护单挂单失败") - logger.error(f" 参数: {params}") + logger.error(f"{symbol} ❌ 保护单挂单失败(详见上方「创建 Algo 条件单失败」日志)") + logger.error(f" 类型: {trigger_type} 触发价: {stop_price_str}") if not one_way_only: logger.error(f" 对冲模式: {dual}(None 表示无法读取持仓模式,请检查网络或 API 权限)") return None diff --git a/trading_system/config.py b/trading_system/config.py index 91db1e4..9951f0e 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -469,7 +469,7 @@ CONNECTION_RETRIES = int(os.getenv('CONNECTION_RETRIES', '3')) # 连接重试 # 仅用于 get_open_positions / get_recent_trades 等只读接口的单次等待时间,不影响下单/止损止盈的快速失败 # 调大此值会延长单次请求最大等待时间,在同步/查询持仓时可能阻塞事件循环,影响实时性;保持 60 秒,通过增加重试+退避应对偶发超时 READ_ONLY_REQUEST_TIMEOUT = int(os.getenv('READ_ONLY_REQUEST_TIMEOUT', '60')) -# 创建 Algo 条件单(止损/止盈)单次请求超时(秒),币安 Algo 接口在高负载时易超时,默认 45 秒;网络不稳可调大至 60 +# 创建 Algo 条件单(止损/止盈)单次请求超时(秒),币安 Algo 接口在高负载时易超时;可由 TRADING_CONFIG['ALGO_ORDER_TIMEOUT_SEC'] 或环境变量覆盖 ALGO_ORDER_TIMEOUT_SEC = int(os.getenv('ALGO_ORDER_TIMEOUT_SEC', '45')) # 获取持仓时过滤掉名义价值低于此值的仓位(USDT),与币安仪表板不一致时可调低或设为 0 POSITION_MIN_NOTIONAL_USDT = float(os.getenv('POSITION_MIN_NOTIONAL_USDT', '1.0')) diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 57c4611..1a2aa39 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -1481,6 +1481,9 @@ class PositionManager: 目的: - 服务重启/网络波动时仍有交易所级别保护 - 用户在币安界面能看到止损/止盈委托 + 流程(与 docs/bian 条件订单交易更新推送 一致): + - 先检查持仓存在 → 取消旧保护单 → 再次检查持仓(避免条件单已触发平仓后仍挂单导致 -4509/超时)→ 再挂 SL/TP。 + - 条件单触发平仓后,交易所会将同 symbol 其余条件单置为 EXPIRED,见 ALGO_UPDATE。 """ try: enabled = bool(config.TRADING_CONFIG.get("EXCHANGE_SLTP_ENABLED", True)) @@ -1595,6 +1598,18 @@ class PositionManager: except Exception as e: logger.warning(f"[账号{self.account_id}] {symbol} 取消旧保护单异常: {e},继续尝试挂新单") + # 取消后再次确认持仓仍存在(docs/bian:条件单触发平仓后,交易所会 EXPIRED 其他条件单;若先触发了 TP/SL 平仓,此处再挂会 -4509 或请求卡住) + try: + positions = await self._get_open_positions() + live_position = next((p for p in positions if p.get('symbol') == symbol), None) + if not live_position or abs(float(live_position.get('positionAmt', 0) or 0)) <= 0: + logger.info( + f"[账号{self.account_id}] {symbol} 取消旧保护单后持仓已不存在(可能已被条件单平仓),跳过挂新单" + ) + return + except Exception as e: + logger.debug(f"{symbol} 取消后复检持仓失败: {e},继续尝试挂单") + # 获取当前价格(如果未提供):优先 WS 缓存(bookTicker/ticker24h)→ 持仓 markPrice → REST ticker if current_price is None: try: