From f1bc8413df8c058a2de066697539d5e459dfc3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Fri, 23 Jan 2026 17:20:55 +0800 Subject: [PATCH] a --- trading_system/position_manager.py | 122 ++++++++++++++++++++--------- 1 file changed, 83 insertions(+), 39 deletions(-) diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 15c1038..80d918f 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -1895,9 +1895,11 @@ class PositionManager: # 使用 try-except 包裹,确保异常被正确处理 try: # 计算持仓持续时间和策略类型 - # exit_reason 细分:优先看币安平仓订单类型,其次用价格接近止损/止盈价做兜底 + # exit_reason 细分:优先用价格匹配和特征判断,其次看币安订单类型 exit_reason = "sync" exit_time_ts = None + is_reduce_only = False + try: if latest_close_order and isinstance(latest_close_order, dict): otype = str( @@ -1905,65 +1907,107 @@ class PositionManager: or latest_close_order.get("origType") or "" ).upper() - # 检查订单的 reduceOnly 字段:如果是 true,说明是自动平仓,不应该标记为 manual is_reduce_only = latest_close_order.get("reduceOnly", False) - if "TRAILING" in otype: - exit_reason = "trailing_stop" - elif "TAKE_PROFIT" in otype: - exit_reason = "take_profit" - elif "STOP" in otype: - # STOP / STOP_MARKET 通常对应止损触发 - exit_reason = "stop_loss" - elif otype in ("MARKET", "LIMIT"): - # 如果是 reduceOnly 订单,说明是自动平仓(可能是保护单触发的),先标记为 sync,后续用价格判断 - if is_reduce_only: - exit_reason = "sync" # 临时标记,后续用价格判断 - else: - exit_reason = "manual" # 非 reduceOnly 的 MARKET/LIMIT 订单才是真正的手动平仓 - ms = latest_close_order.get("updateTime") or latest_close_order.get("time") try: if ms: exit_time_ts = int(int(ms) / 1000) except Exception: exit_time_ts = None - except Exception: - # 保持默认 sync pass - # 价格兜底:如果能明显命中止损/止盈价,则覆盖 exit_reason - # 这对于保护单触发的 MARKET 订单特别重要(币安的保护单触发后会生成 MARKET 订单) + # ⚠️ 关键改进:优先使用价格匹配和特征判断(提高准确性) + # 这对于保护单触发的 MARKET 订单特别重要(币安的保护单触发后会生成 MARKET 订单,但 reduceOnly 可能不准确) try: - def _close_to(a: float, b: float, max_pct: float = 0.05) -> bool: # 从2%放宽到5%,以应对极端滑点下的同步识别 + def _close_to(a: float, b: float, max_pct: float = 0.10) -> bool: # 从5%放宽到10%,以应对极端滑点 if a <= 0 or b <= 0: return False return abs((a - b) / b) <= max_pct ep = float(exit_price or 0) - if ep > 0: - sl = trade.get("stop_loss_price") - tp = trade.get("take_profit_price") - tp1 = trade.get("take_profit_1") - tp2 = trade.get("take_profit_2") - # 优先检查止损(因为止损更关键) - if sl is not None and _close_to(ep, float(sl), max_pct=0.05): + entry_price_val = float(trade.get("entry_price", 0) or 0) + sl = trade.get("stop_loss_price") + tp = trade.get("take_profit_price") + tp1 = trade.get("take_profit_1") + tp2 = trade.get("take_profit_2") + + # 计算持仓时间和亏损比例(用于特征判断) + entry_time = trade.get("entry_time") + duration_minutes = None + if entry_time and exit_time_ts: + try: + duration_minutes = (exit_time_ts - int(entry_time)) / 60.0 + except Exception: + pass + + pnl_percent = float(trade.get("pnl_percent", 0) or 0) + + # 1. 优先检查止损价格匹配(提高容忍度到10%) + if sl is not None and entry_price_val > 0 and ep > 0: + sl_val = float(sl) + # 价格匹配:平仓价接近止损价 + if _close_to(ep, sl_val, max_pct=0.10): exit_reason = "stop_loss" - # 然后检查止盈 - elif tp is not None and _close_to(ep, float(tp), max_pct=0.05): + # 方向匹配:BUY时平仓价 < 止损价,SELL时平仓价 > 止损价 + elif (trade.get("side") == "BUY" and ep < sl_val) or (trade.get("side") == "SELL" and ep > sl_val): + # 如果价格在止损方向,且亏损比例较大,更可能是止损触发 + if pnl_percent < -5.0: # 亏损超过5% + exit_reason = "stop_loss" + logger.info(f"{trade.get('symbol')} [同步] 价格方向匹配止损,且亏损{pnl_percent:.2f}%,标记为止损") + + # 2. 检查止盈价格匹配 + if exit_reason == "sync" and ep > 0: + if tp is not None and _close_to(ep, float(tp), max_pct=0.10): exit_reason = "take_profit" - elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.05): + elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.10): exit_reason = "take_profit" - elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.05): + elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.10): exit_reason = "take_profit" - # 如果之前标记为 sync 且是 reduceOnly 订单,但价格不匹配止损/止盈,可能是其他自动平仓(如移动止损) - elif exit_reason == "sync" and is_reduce_only: - # 检查是否是移动止损:如果价格接近入场价,可能是移动止损触发的 - entry_price_val = float(trade.get("entry_price", 0) or 0) - if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01): - exit_reason = "trailing_stop" - except Exception: + + # 3. 特征判断:如果价格不匹配,但满足止损特征,也标记为止损 + if exit_reason == "sync" and entry_price_val > 0 and ep > 0: + # 特征1:持仓时间短(< 30分钟)且亏损 + if duration_minutes and duration_minutes < 30 and pnl_percent < -5.0: + # 特征2:价格在止损方向 + if sl is not None: + sl_val = float(sl) + if (trade.get("side") == "BUY" and ep < sl_val) or (trade.get("side") == "SELL" and ep > sl_val): + exit_reason = "stop_loss" + logger.info(f"{trade.get('symbol')} [同步] 特征判断:持仓{duration_minutes:.1f}分钟,亏损{pnl_percent:.2f}%,价格在止损方向,标记为止损") + + # 4. 如果之前标记为 sync 且是 reduceOnly 订单,但价格不匹配止损/止盈,可能是其他自动平仓(如移动止损) + if exit_reason == "sync" and is_reduce_only: + # 检查是否是移动止损:如果价格接近入场价,可能是移动止损触发的 + if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01): + exit_reason = "trailing_stop" + + # 5. 最后才看币安订单类型(作为兜底) + if exit_reason == "sync" and latest_close_order and isinstance(latest_close_order, dict): + otype = str( + latest_close_order.get("type") + or latest_close_order.get("origType") + or "" + ).upper() + + if "TRAILING" in otype: + exit_reason = "trailing_stop" + elif "TAKE_PROFIT" in otype: + exit_reason = "take_profit" + elif "STOP" in otype: + exit_reason = "stop_loss" + elif otype in ("MARKET", "LIMIT"): + # 只有在价格和特征都不匹配,且不是 reduceOnly 时,才标记为手动平仓 + if not is_reduce_only: + # 再次检查:如果亏损很大,更可能是止损触发(币安API可能不准确) + if pnl_percent < -10.0: + exit_reason = "stop_loss" # 大额亏损,更可能是止损 + logger.warning(f"{trade.get('symbol')} [同步] 大额亏损{pnl_percent:.2f}%,即使reduceOnly=false也标记为止损") + else: + exit_reason = "manual" + except Exception as e: + logger.warning(f"判断平仓原因失败: {e}") pass # 持仓持续时间(分钟):优先用币安订单时间,否则用当前时间