This commit is contained in:
薇薇安 2026-01-23 17:20:55 +08:00
parent 0c489bfdee
commit f1bc8413df

View File

@ -1895,9 +1895,11 @@ class PositionManager:
# 使用 try-except 包裹,确保异常被正确处理 # 使用 try-except 包裹,确保异常被正确处理
try: try:
# 计算持仓持续时间和策略类型 # 计算持仓持续时间和策略类型
# exit_reason 细分:优先看币安平仓订单类型,其次用价格接近止损/止盈价做兜底 # exit_reason 细分:优先用价格匹配和特征判断,其次看币安订单类型
exit_reason = "sync" exit_reason = "sync"
exit_time_ts = None exit_time_ts = None
is_reduce_only = False
try: try:
if latest_close_order and isinstance(latest_close_order, dict): if latest_close_order and isinstance(latest_close_order, dict):
otype = str( otype = str(
@ -1905,65 +1907,107 @@ class PositionManager:
or latest_close_order.get("origType") or latest_close_order.get("origType")
or "" or ""
).upper() ).upper()
# 检查订单的 reduceOnly 字段:如果是 true说明是自动平仓不应该标记为 manual
is_reduce_only = latest_close_order.get("reduceOnly", False) 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") ms = latest_close_order.get("updateTime") or latest_close_order.get("time")
try: try:
if ms: if ms:
exit_time_ts = int(int(ms) / 1000) exit_time_ts = int(int(ms) / 1000)
except Exception: except Exception:
exit_time_ts = None exit_time_ts = None
except Exception: except Exception:
# 保持默认 sync
pass pass
# 价格兜底:如果能明显命中止损/止盈价,则覆盖 exit_reason # ⚠️ 关键改进:优先使用价格匹配和特征判断(提高准确性)
# 这对于保护单触发的 MARKET 订单特别重要(币安的保护单触发后会生成 MARKET 订单 # 这对于保护单触发的 MARKET 订单特别重要(币安的保护单触发后会生成 MARKET 订单,但 reduceOnly 可能不准确)
try: 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: if a <= 0 or b <= 0:
return False return False
return abs((a - b) / b) <= max_pct return abs((a - b) / b) <= max_pct
ep = float(exit_price or 0) ep = float(exit_price or 0)
if ep > 0: entry_price_val = float(trade.get("entry_price", 0) or 0)
sl = trade.get("stop_loss_price") sl = trade.get("stop_loss_price")
tp = trade.get("take_profit_price") tp = trade.get("take_profit_price")
tp1 = trade.get("take_profit_1") tp1 = trade.get("take_profit_1")
tp2 = trade.get("take_profit_2") tp2 = trade.get("take_profit_2")
# 优先检查止损(因为止损更关键)
if sl is not None and _close_to(ep, float(sl), max_pct=0.05): # 计算持仓时间和亏损比例(用于特征判断)
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" exit_reason = "stop_loss"
# 然后检查止盈 # 方向匹配BUY时平仓价 < 止损价SELL时平仓价 > 止损价
elif tp is not None and _close_to(ep, float(tp), max_pct=0.05): 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" 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" 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" exit_reason = "take_profit"
# 如果之前标记为 sync 且是 reduceOnly 订单,但价格不匹配止损/止盈,可能是其他自动平仓(如移动止损)
elif exit_reason == "sync" and is_reduce_only: # 3. 特征判断:如果价格不匹配,但满足止损特征,也标记为止损
# 检查是否是移动止损:如果价格接近入场价,可能是移动止损触发的 if exit_reason == "sync" and entry_price_val > 0 and ep > 0:
entry_price_val = float(trade.get("entry_price", 0) or 0) # 特征1持仓时间短< 30分钟且亏损
if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01): if duration_minutes and duration_minutes < 30 and pnl_percent < -5.0:
exit_reason = "trailing_stop" # 特征2价格在止损方向
except Exception: 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 pass
# 持仓持续时间(分钟):优先用币安订单时间,否则用当前时间 # 持仓持续时间(分钟):优先用币安订单时间,否则用当前时间