From 432fc85a798c998de2c79cb8ccf47c153401d0a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Thu, 26 Feb 2026 11:50:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=A7=BB=E5=8A=A8=E6=AD=A2?= =?UTF-8?q?=E6=8D=9F=E5=8D=95=E7=8B=AC=E7=9A=84=E6=97=A5=E5=BF=97=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trading_system/position_manager.py | 117 +++++++++++++++++++++++++---- 1 file changed, 104 insertions(+), 13 deletions(-) diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 3c3a302..694b8e4 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -97,6 +97,46 @@ def _log_trade_db_failure(symbol, entry_order_id, side, quantity, entry_price, a logger.warning(f"写入落库失败日志失败: {e}") +_TRAILING_LOG_PATH: Optional[Path] = None + +def _get_trailing_stop_log_path() -> Optional[Path]: + """保本/移动止损专用日志路径:logs/profit_protection.log,便于 tail/grep 确认是否执行。""" + global _TRAILING_LOG_PATH + if _TRAILING_LOG_PATH is not None: + return _TRAILING_LOG_PATH + try: + root = Path(__file__).parent.parent + log_dir = root / "logs" + log_dir.mkdir(parents=True, exist_ok=True) + _TRAILING_LOG_PATH = log_dir / "profit_protection.log" + return _TRAILING_LOG_PATH + except Exception: + return None + +def _log_trailing_stop_event(account_id: int, symbol: str, event: str, **kwargs): + """将保本/移动止损相关事件写入专用日志(一行 JSON),便于确认是否正常执行。""" + path = _get_trailing_stop_log_path() + if not path: + return + try: + rec = { + "ts": datetime.utcnow().isoformat() + "Z", + "account_id": account_id, + "symbol": symbol, + "event": event, + } + for k, v in kwargs.items(): + if v is not None and k not in rec: + if isinstance(v, float): + rec[k] = round(v, 6) + else: + rec[k] = v + with open(path, "a", encoding="utf-8") as f: + f.write(json.dumps(rec, ensure_ascii=False) + "\n") + except Exception as e: + logger.debug(f"写入保本/移动止损日志失败: {e}") + + # User Data Stream 缓存(持仓/余额优先读 WS,减少 REST) try: from .user_data_stream import get_stream_instance, get_positions_from_cache, get_balance_from_cache @@ -1481,8 +1521,10 @@ class PositionManager: logger.warning(f"[账号{self.account_id}] {symbol} 同步跳过: 止损或止盈为空 stop_loss={stop_loss} take_profit={take_profit}") return + sync_type = "保本/移动止损同步" if (position_info.get("breakevenStopSet") or position_info.get("trailingStopActivated")) else "SL/TP同步" logger.info( - f"[账号{self.account_id}] {symbol} 开始同步止损/止盈至交易所: SL={float(stop_loss):.4f} TP={float(take_profit):.4f}" + f"[账号{self.account_id}] {symbol} 开始同步止损/止盈至交易所 [{sync_type}]: " + f"止损触发价={float(stop_loss):.4f} 止盈触发价={float(take_profit):.4f}" ) # 验证止损价格是否合理。保本/移动止损时:多单止损可≥入场价、空单止损可≤入场价,不得被改回亏损价 @@ -1541,7 +1583,15 @@ class PositionManager: await self.client.cancel_open_algo_orders_by_order_types( symbol, {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET"} ) - logger.info(f"[账号{self.account_id}] {symbol} 已取消旧保护单,准备挂新单") + logger.info( + f"[账号{self.account_id}] {symbol} 已取消旧保护单,准备挂新单 [{sync_type}] " + f"条件价: 止损={float(stop_loss):.4f} 止盈={float(take_profit):.4f}" + ) + if sync_type == "保本/移动止损同步": + _log_trailing_stop_event( + self.account_id, symbol, "sync_to_exchange", + stop_loss=float(stop_loss), take_profit=float(take_profit), msg="准备挂新单" + ) except Exception as e: logger.warning(f"[账号{self.account_id}] {symbol} 取消旧保护单异常: {e},继续尝试挂新单") @@ -1756,7 +1806,10 @@ class PositionManager: except Exception: pass if sl_order: - logger.info(f"[账号{self.account_id}] {symbol} ✓ 止损单已成功挂到交易所: {sl_order.get('algoId', 'N/A')}") + logger.info( + f"[账号{self.account_id}] {symbol} ✓ 止损单已成功挂到交易所: {sl_order.get('algoId', 'N/A')} " + f"触发价={float(stop_loss):.4f}" + ) else: if sl_failed_due_to_gte: logger.warning(f"{symbol} 条件单被拒(持仓未就绪或已平),跳过交易所止损单,将依赖 WebSocket 监控") @@ -1873,7 +1926,10 @@ class PositionManager: except Exception: pass if tp_order: - logger.info(f"[账号{self.account_id}] {symbol} ✓ 止盈单已成功挂到交易所: {tp_order.get('algoId', 'N/A')}") + logger.info( + f"[账号{self.account_id}] {symbol} ✓ 止盈单已成功挂到交易所: {tp_order.get('algoId', 'N/A')} " + f"触发价={float(take_profit):.4f}" + ) else: logger.warning(f"{symbol} ⚠️ 止盈单挂单失败,将依赖WebSocket监控") @@ -1889,10 +1945,15 @@ class PositionManager: if position_info.get("exchangeSlOrderId") or position_info.get("exchangeTpOrderId"): logger.info( - f"[账号{self.account_id}] {symbol} 已挂币安保护单: " - f"SL={position_info.get('exchangeSlOrderId') or '-'} " - f"TP={position_info.get('exchangeTpOrderId') or '-'}" + f"[账号{self.account_id}] {symbol} 已挂币安保护单 [{sync_type}]: " + f"SL={position_info.get('exchangeSlOrderId') or '-'} 触发价={float(stop_loss):.4f} | " + f"TP={position_info.get('exchangeTpOrderId') or '-'} 触发价={float(take_profit):.4f}" ) + if sync_type == "保本/移动止损同步": + _log_trailing_stop_event( + self.account_id, symbol, "sync_to_exchange_ok", + stop_loss=float(stop_loss), take_profit=float(take_profit), msg="已挂币安保护单" + ) logger.info(f"[账号{self.account_id}] {symbol} 止损/止盈同步至交易所完成") else: logger.warning(f"[账号{self.account_id}] {symbol} 同步结束但未挂上保护单(止损或止盈挂单均失败),将依赖 WebSocket 监控") @@ -2041,10 +2102,13 @@ class PositionManager: position_info['stopLoss'] = breakeven position_info['breakevenStopSet'] = True logger.info(f"{symbol} [定时检查] 盈利{pnl_percent_margin:.2f}%≥{lock_pct*100:.0f}%,止损已移至含手续费保本价 {breakeven:.4f}") + _log_trailing_stop_event(self.account_id, symbol, "breakeven_set", breakeven=breakeven, pnl_pct=pnl_percent_margin, source="定时检查") logger.info(f"{symbol} [定时检查] 尝试将保本止损同步至交易所") try: await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price) + _log_trailing_stop_event(self.account_id, symbol, "breakeven_sync_ok", breakeven=breakeven, source="定时检查") except Exception as sync_e: + _log_trailing_stop_event(self.account_id, symbol, "breakeven_sync_fail", breakeven=breakeven, error=str(sync_e), source="定时检查") logger.warning( f"{symbol} [定时检查] 同步保本止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, @@ -2058,15 +2122,18 @@ class PositionManager: f"{symbol} 移动止损激活: 止损移至含手续费保本价 {breakeven:.4f} (入场: {entry_price:.4f}) " f"(盈利: {pnl_percent_margin:.2f}% of margin)" ) + _log_trailing_stop_event(self.account_id, symbol, "trailing_activated", breakeven=breakeven, pnl_pct=pnl_percent_margin, source="定时检查") logger.info(f"{symbol} [定时检查] 尝试将移动止损同步至交易所") try: await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price) + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_ok", new_stop_loss=breakeven, source="定时检查") logger.info(f"{symbol} [定时检查] 已同步移动止损至交易所") except Exception as sync_e: + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_fail", new_stop_loss=breakeven, error=str(sync_e), source="定时检查") logger.warning( f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) + ) else: # 盈利超过阈值后,止损移至保护利润位(基于保证金) # 如果已经部分止盈,使用剩余仓位计算 @@ -2089,14 +2156,17 @@ class PositionManager: f"(保护{trailing_protect*100:.1f}% of remaining margin = {protect_amount:.4f} USDT, " f"剩余数量: {remaining_quantity:.4f})" ) + _log_trailing_stop_event(self.account_id, symbol, "trailing_update", new_stop_loss=new_stop_loss, source="定时检查-剩余仓位") try: await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price) + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_ok", new_stop_loss=new_stop_loss, source="定时检查") logger.info(f"{symbol} [定时检查] 已同步移动止损至交易所") except Exception as sync_e: + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_fail", new_stop_loss=new_stop_loss, error=str(sync_e), source="定时检查") logger.warning( f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) + ) else: new_stop_loss = entry_price + (remaining_pnl - protect_amount) / remaining_quantity new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL')) @@ -2108,14 +2178,17 @@ class PositionManager: f"(保护{trailing_protect*100:.1f}% of remaining margin = {protect_amount:.4f} USDT, " f"剩余数量: {remaining_quantity:.4f})" ) + _log_trailing_stop_event(self.account_id, symbol, "trailing_update", new_stop_loss=new_stop_loss, source="定时检查-剩余仓位") try: await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price) + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_ok", new_stop_loss=new_stop_loss, source="定时检查") logger.info(f"{symbol} [定时检查] 已同步移动止损至交易所") except Exception as sync_e: + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_fail", new_stop_loss=new_stop_loss, error=str(sync_e), source="定时检查") logger.warning( f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) + ) else: # 未部分止盈,使用原始仓位计算;保护金额至少覆盖手续费 protect_amount = max(margin * trailing_protect, self._min_protect_amount_for_fees(margin, leverage)) @@ -2129,14 +2202,17 @@ class PositionManager: f"{symbol} 移动止损更新: {new_stop_loss:.4f} " f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)" ) + _log_trailing_stop_event(self.account_id, symbol, "trailing_update", new_stop_loss=new_stop_loss, source="定时检查") try: await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price) + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_ok", new_stop_loss=new_stop_loss, source="定时检查") logger.info(f"{symbol} [定时检查] 已同步移动止损至交易所") except Exception as sync_e: + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_fail", new_stop_loss=new_stop_loss, error=str(sync_e), source="定时检查") logger.warning( f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) + ) else: # 做空:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量 # 注意:对于做空,止损价应该高于开仓价,所以用加法 @@ -2151,14 +2227,17 @@ class PositionManager: f"{symbol} 移动止损更新: {new_stop_loss:.4f} " f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)" ) + _log_trailing_stop_event(self.account_id, symbol, "trailing_update", new_stop_loss=new_stop_loss, source="定时检查") try: await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price) + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_ok", new_stop_loss=new_stop_loss, source="定时检查") logger.info(f"{symbol} [定时检查] 已同步移动止损至交易所") except Exception as sync_e: + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_fail", new_stop_loss=new_stop_loss, error=str(sync_e), source="定时检查") logger.warning( f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) + ) # 检查止损(使用更新后的止损价,基于保证金收益比) # ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行 @@ -4171,10 +4250,13 @@ class PositionManager: logger.info( f"[账号{self.account_id}] {symbol} [实时监控] 盈利{pnl_percent_margin:.2f}%≥{lock_pct*100:.0f}%,止损已移至含手续费保本价 {breakeven:.4f}(留住盈利)" ) + _log_trailing_stop_event(self.account_id, symbol, "breakeven_set", breakeven=breakeven, pnl_pct=pnl_percent_margin, source="实时监控") logger.info(f"[账号{self.account_id}] {symbol} [实时监控] 尝试将保本止损同步至交易所") try: await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) + _log_trailing_stop_event(self.account_id, symbol, "breakeven_sync_ok", breakeven=breakeven, source="实时监控") except Exception as sync_e: + _log_trailing_stop_event(self.account_id, symbol, "breakeven_sync_fail", breakeven=breakeven, error=str(sync_e), source="实时监控") logger.warning( f"[账号{self.account_id}] {symbol} [实时监控] 同步保本止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, @@ -4198,16 +4280,19 @@ class PositionManager: f"[账号{self.account_id}] {symbol} [实时监控] 移动止损激活: 止损移至保护利润位 {new_stop_loss:.4f} " f"(盈利: {pnl_percent_margin:.2f}% of margin, 保护: {trailing_protect*100:.1f}% of margin)" ) + _log_trailing_stop_event(self.account_id, symbol, "trailing_activated", new_stop_loss=new_stop_loss, pnl_pct=pnl_percent_margin, source="实时监控") # 同步至交易所:取消原止损单并按新止损价重挂,使移动止损也有交易所保护 logger.info(f"[账号{self.account_id}] {symbol} [实时监控] 尝试将移动止损同步至交易所") try: await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_ok", new_stop_loss=new_stop_loss, source="实时监控") logger.info(f"[账号{self.account_id}] {symbol} [实时监控] 已同步移动止损至交易所") except Exception as sync_e: + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_fail", new_stop_loss=new_stop_loss, error=str(sync_e), source="实时监控") logger.warning( f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) + ) else: # ⚠️ 优化:如果分步止盈第一目标已触发,移动止损不再更新剩余仓位的止损价 # 原因:分步止盈第一目标触发后,剩余50%仓位止损已移至成本价(保本),等待第二目标 @@ -4227,10 +4312,13 @@ class PositionManager: f"[账号{self.account_id}] {symbol} [实时监控] 移动止损更新: {new_stop_loss:.4f} " f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)" ) + _log_trailing_stop_event(self.account_id, symbol, "trailing_update", new_stop_loss=new_stop_loss, source="实时监控") try: await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_ok", new_stop_loss=new_stop_loss, source="实时监控") logger.info(f"[账号{self.account_id}] {symbol} [实时监控] 已同步移动止损至交易所") except Exception as sync_e: + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_fail", new_stop_loss=new_stop_loss, error=str(sync_e), source="实时监控") logger.warning( f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, @@ -4244,10 +4332,13 @@ class PositionManager: f"[账号{self.account_id}] {symbol} [实时监控] 移动止损更新: {new_stop_loss:.4f} " f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)" ) + _log_trailing_stop_event(self.account_id, symbol, "trailing_update", new_stop_loss=new_stop_loss, source="实时监控") try: await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_ok", new_stop_loss=new_stop_loss, source="实时监控") logger.info(f"[账号{self.account_id}] {symbol} [实时监控] 已同步移动止损至交易所") except Exception as sync_e: + _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_fail", new_stop_loss=new_stop_loss, error=str(sync_e), source="实时监控") logger.warning( f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False,