diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 6187bfa..4d3b9d1 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -183,6 +183,8 @@ class PositionManager: # 自动平仓去抖/限流(避免止损触发后反复下单/刷屏) self._last_auto_close_attempt_ms: Dict[str, int] = {} self._last_auto_close_fail_log_ms: Dict[str, int] = {} + # 「当前价已触发止损价,无法挂止损单→立即市价平仓」去重,避免同一 symbol 短时间多次打日志/重复平仓 + self._sl_protection_close_triggered_at: Dict[str, float] = {} # symbol -> time.time() async def _get_open_positions(self, force_rest: bool = False) -> List[Dict]: """优先使用 User Data Stream 持仓缓存(Redis),无缓存或未启动时走 REST。多账号时必须传 account_id 读对应缓存。""" @@ -203,6 +205,18 @@ class PositionManager: return bal return await self.client.get_account_balance() + def _should_skip_sl_protection_close(self, symbol: str, cooldown_sec: float = 60.0) -> bool: + """同一 symbol 在 cooldown_sec 内已触发过「价格已触发止损→立即市价平仓」则跳过,避免重复日志与重复平仓。""" + now = time.time() + to_remove = [s for s, t in self._sl_protection_close_triggered_at.items() if now - t > cooldown_sec] + for s in to_remove: + del self._sl_protection_close_triggered_at[s] + return symbol in self._sl_protection_close_triggered_at + + def _mark_sl_protection_close_triggered(self, symbol: str) -> None: + """标记该 symbol 已触发保护平仓(用于去重)。""" + self._sl_protection_close_triggered_at[symbol] = time.time() + @staticmethod def _pct_like_to_ratio(v: float) -> float: """ @@ -1887,10 +1901,12 @@ class PositionManager: if side == "BUY": # 做多:当前价 <= 止损价,说明已触发止损 if current_price_val <= stop_loss_val: + if self._should_skip_sl_protection_close(symbol): + return + self._mark_sl_protection_close_triggered(symbol) entry_price_str = f"{entry_price_val:.8f}" if entry_price_val is not None else 'N/A' - logger.error( - f"{symbol} ⚠️ 当前价格({current_price_val:.8f})已触发止损价({stop_loss_val:.8f}),无法挂止损单,立即执行市价平仓保护!" - f" | 入场价: {entry_price_str}" + logger.warning( + f"{symbol} 当前价格({current_price_val:.8f})已触发止损价({stop_loss_val:.8f}),无法挂止损单,立即执行市价平仓保护 | 入场价: {entry_price_str}" ) # 立即执行市价平仓 await self.close_position(symbol, reason='stop_loss') @@ -1908,10 +1924,12 @@ class PositionManager: elif side == "SELL": # 做空:当前价 >= 止损价,说明已触发止损 if current_price_val >= stop_loss_val: + if self._should_skip_sl_protection_close(symbol): + return + self._mark_sl_protection_close_triggered(symbol) entry_price_str = f"{entry_price_val:.8f}" if entry_price_val is not None else 'N/A' - logger.error( - f"{symbol} ⚠️ 当前价格({current_price_val:.8f})已触发止损价({stop_loss_val:.8f}),无法挂止损单,立即执行市价平仓保护!" - f" | 入场价: {entry_price_str}" + logger.warning( + f"{symbol} 当前价格({current_price_val:.8f})已触发止损价({stop_loss_val:.8f}),无法挂止损单,立即执行市价平仓保护 | 入场价: {entry_price_str}" ) # 立即执行市价平仓 await self.close_position(symbol, reason='stop_loss') @@ -1943,7 +1961,10 @@ class PositionManager: # 检查是否是 -2021 (立即触发) error_msg = str(e) if "-2021" in error_msg or "immediately trigger" in error_msg: - logger.error(f"{symbol} ⚠️ 止损单会立即触发(-2021),视为已触发止损,立即执行市价平仓") + if self._should_skip_sl_protection_close(symbol): + return + self._mark_sl_protection_close_triggered(symbol) + logger.warning(f"{symbol} 止损单会立即触发(-2021),视为已触发止损,立即执行市价平仓") await self.close_position(symbol, reason='stop_loss') return @@ -1964,7 +1985,10 @@ class PositionManager: except Exception as retry_e: retry_msg = str(retry_e) if "-2021" in retry_msg or "immediately trigger" in retry_msg: - logger.error(f"{symbol} ⚠️ 重试挂止损单会立即触发(-2021),立即执行市价平仓") + if self._should_skip_sl_protection_close(symbol): + return + self._mark_sl_protection_close_triggered(symbol) + logger.warning(f"{symbol} 重试挂止损单会立即触发(-2021),立即执行市价平仓") await self.close_position(symbol, reason='stop_loss') return logger.error(f"{symbol} 重试挂止损单失败: {retry_e}") @@ -1986,7 +2010,10 @@ class PositionManager: except Exception as e: error_msg = str(e) if "-2021" in error_msg or "immediately trigger" in error_msg: - logger.error(f"{symbol} ⚠️ 止损单会立即触发(-2021),视为已触发止损,立即执行市价平仓") + if self._should_skip_sl_protection_close(symbol): + return + self._mark_sl_protection_close_triggered(symbol) + logger.warning(f"{symbol} 止损单会立即触发(-2021),视为已触发止损,立即执行市价平仓") await self.close_position(symbol, reason='stop_loss') return logger.error(f"{symbol} 挂止损单失败(API/网络): {e}") @@ -2041,15 +2068,14 @@ class PositionManager: should_close = True if should_close: + if self._should_skip_sl_protection_close(symbol): + return + self._mark_sl_protection_close_triggered(symbol) entry_price_str = f"{entry_price_val:.8f}" if entry_price_val is not None else 'N/A' - logger.error("=" * 80) - logger.error(f"{symbol} ⚠️ 止损单挂单失败,但当前价格已触发止损,立即执行市价平仓保护!") - logger.error(f" 当前价格: {current_price_val:.8f}") - logger.error(f" 止损价格: {stop_loss_val:.8f}") - logger.error(f" 入场价格: {entry_price_str}") - logger.error(f" 持仓方向: {side}") - logger.error(f" 价格偏离: {abs(current_price_val - stop_loss_val):.8f} ({abs(current_price_val - stop_loss_val)/stop_loss_val*100:.2f}%)") - logger.error("=" * 80) + logger.warning( + f"{symbol} 止损单挂单失败,当前价格已触发止损,立即执行市价平仓保护 | " + f"当前价: {current_price_val:.8f}, 止损价: {stop_loss_val:.8f}, 入场价: {entry_price_str}" + ) # 立即执行市价平仓 if await self.close_position(symbol, reason='stop_loss'): logger.info(f"{symbol} ✓ 止损平仓成功(止损单挂单失败后的保护措施)")