From 2f9dce483cab777eff042711ceda2859a8df1fda Mon Sep 17 00:00:00 2001 From: root Date: Sun, 1 Mar 2026 09:40:00 +0000 Subject: [PATCH] fix: simulate limit orders for TP/SL (match real trading) - TP/SL now exit at order price (limit order), not market price - SL exits at sl_price, TP1 at tp1_price, TP2 at tp2_price - Only timeout and signal_flip use market price (current price) - Updated fix_historical_pnl.py to also correct exit_price - This eliminates fake slippage in paper trading stats --- backend/fix_historical_pnl.py | 25 +++++++++++++++---------- backend/paper_monitor.py | 23 ++++++++++++++--------- backend/signal_engine.py | 16 +++++++++++----- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/backend/fix_historical_pnl.py b/backend/fix_historical_pnl.py index 27ddec7..460d13d 100644 --- a/backend/fix_historical_pnl.py +++ b/backend/fix_historical_pnl.py @@ -28,16 +28,18 @@ def fix(): for row in rows: pid, direction, entry, exit_p, tp1, tp2, sl, tp1_hit, status, old_pnl, atr_entry = row - if exit_p is None or entry is None or atr_entry is None or atr_entry <= 0: + if entry is None or atr_entry is None or atr_entry <= 0: continue risk_distance = 2.0 * 0.7 * atr_entry if risk_distance <= 0: continue - # 重算pnl_r + # 实盘模拟:TP/SL以限价单价格成交 + new_exit = exit_p # 默认不变 + if status == "tp": - # 半仓TP1 + 半仓TP2 + new_exit = tp2 # TP以TP2价成交 if direction == "LONG": tp1_r = (tp1 - entry) / risk_distance tp2_r = (tp2 - entry) / risk_distance @@ -46,19 +48,20 @@ def fix(): tp2_r = (entry - tp2) / risk_distance new_pnl = 0.5 * tp1_r + 0.5 * tp2_r elif status == "sl_be": - # TP1半仓锁定 + SL_BE半仓归零 + new_exit = sl # SL_BE以SL价成交(成本价附近) if direction == "LONG": tp1_r = (tp1 - entry) / risk_distance else: tp1_r = (entry - tp1) / risk_distance new_pnl = 0.5 * tp1_r elif status == "sl": - # 全仓止损 + new_exit = sl # SL以SL价成交 if direction == "LONG": - new_pnl = (exit_p - entry) / risk_distance + new_pnl = (sl - entry) / risk_distance else: - new_pnl = (entry - exit_p) / risk_distance + new_pnl = (entry - sl) / risk_distance elif status == "timeout": + new_exit = exit_p # 超时市价平仓,保持原exit if direction == "LONG": move = exit_p - entry else: @@ -68,6 +71,7 @@ def fix(): tp1_r = abs(tp1 - entry) / risk_distance new_pnl = max(new_pnl, 0.5 * tp1_r) elif status == "signal_flip": + new_exit = exit_p # 信号翻转市价平仓 if direction == "LONG": new_pnl = (exit_p - entry) / risk_distance else: @@ -80,9 +84,10 @@ def fix(): new_pnl -= fee_r new_pnl = round(new_pnl, 4) - if abs(new_pnl - old_pnl) > 0.001: - print(f" #{pid} {status:10s} {direction:5s}: {old_pnl:+.4f}R → {new_pnl:+.4f}R (Δ{new_pnl-old_pnl:+.4f})") - cur.execute("UPDATE paper_trades SET pnl_r = %s WHERE id = %s", (new_pnl, pid)) + need_update = abs(new_pnl - old_pnl) > 0.001 or (new_exit and exit_p and abs(new_exit - exit_p) > 0.0001) + if need_update: + print(f" #{pid} {status:10s} {direction:5s}: pnl {old_pnl:+.4f}R → {new_pnl:+.4f}R | exit {exit_p} → {new_exit}") + cur.execute("UPDATE paper_trades SET pnl_r = %s, exit_price = %s WHERE id = %s", (new_pnl, new_exit, pid)) fixed += 1 conn.commit() diff --git a/backend/paper_monitor.py b/backend/paper_monitor.py index a85a241..5f7df4b 100644 --- a/backend/paper_monitor.py +++ b/backend/paper_monitor.py @@ -64,24 +64,26 @@ def check_and_close(symbol_upper: str, price: float): # 统一计算risk_distance (1R基准距离) risk_distance = 2.0 * 0.7 * atr_entry if atr_entry > 0 else 1 + # === 实盘模拟:TP/SL视为限价单,以挂单价成交(非市价) === if direction == "LONG": if price <= sl: closed = True + exit_price = sl # 限价止损单以SL价成交 if tp1_hit: new_status = "sl_be" - # TP1半仓已锁定盈利 = 0.5 * (tp1 - entry) / risk_distance tp1_r = (tp1 - entry_price) / risk_distance if risk_distance > 0 else 0 - pnl_r = 0.5 * tp1_r + pnl_r = 0.5 * tp1_r # TP1半仓已锁定 else: new_status = "sl" - pnl_r = (price - entry_price) / risk_distance if risk_distance > 0 else -1.0 + pnl_r = (exit_price - entry_price) / risk_distance if risk_distance > 0 else -1.0 elif not tp1_hit and price >= tp1: new_sl = entry_price * 1.0005 cur.execute("UPDATE paper_trades SET tp1_hit=TRUE, sl_price=%s, status='tp1_hit' WHERE id=%s", (new_sl, pid)) - logger.info(f"[{symbol_upper}] ✅ TP1触发 LONG @ {price:.4f}, SL→{new_sl:.4f}") + logger.info(f"[{symbol_upper}] ✅ TP1触发 LONG @ {tp1:.4f}, SL→{new_sl:.4f}") elif tp1_hit and price >= tp2: closed = True + exit_price = tp2 # 限价止盈单以TP2价成交 new_status = "tp" tp1_r = (tp1 - entry_price) / risk_distance if risk_distance > 0 else 0 tp2_r = (tp2 - entry_price) / risk_distance if risk_distance > 0 else 0 @@ -89,28 +91,31 @@ def check_and_close(symbol_upper: str, price: float): else: # SHORT if price >= sl: closed = True + exit_price = sl # 限价止损单以SL价成交 if tp1_hit: new_status = "sl_be" tp1_r = (entry_price - tp1) / risk_distance if risk_distance > 0 else 0 pnl_r = 0.5 * tp1_r else: new_status = "sl" - pnl_r = (entry_price - price) / risk_distance if risk_distance > 0 else -1.0 + pnl_r = (entry_price - exit_price) / risk_distance if risk_distance > 0 else -1.0 elif not tp1_hit and price <= tp1: new_sl = entry_price * 0.9995 cur.execute("UPDATE paper_trades SET tp1_hit=TRUE, sl_price=%s, status='tp1_hit' WHERE id=%s", (new_sl, pid)) - logger.info(f"[{symbol_upper}] ✅ TP1触发 SHORT @ {price:.4f}, SL→{new_sl:.4f}") + logger.info(f"[{symbol_upper}] ✅ TP1触发 SHORT @ {tp1:.4f}, SL→{new_sl:.4f}") elif tp1_hit and price <= tp2: closed = True + exit_price = tp2 # 限价止盈单以TP2价成交 new_status = "tp" tp1_r = (entry_price - tp1) / risk_distance if risk_distance > 0 else 0 tp2_r = (entry_price - tp2) / risk_distance if risk_distance > 0 else 0 pnl_r = 0.5 * tp1_r + 0.5 * tp2_r - # 时间止损:60分钟 + # 时间止损:60分钟(市价平仓,用当前价) if not closed and (now_ms - entry_ts > 60 * 60 * 1000): closed = True + exit_price = price # 超时是市价平仓 new_status = "timeout" if direction == "LONG": move = price - entry_price @@ -129,10 +134,10 @@ def check_and_close(symbol_upper: str, price: float): cur.execute( "UPDATE paper_trades SET status=%s, exit_price=%s, exit_ts=%s, pnl_r=%s WHERE id=%s", - (new_status, price, now_ms, round(pnl_r, 4), pid) + (new_status, exit_price, now_ms, round(pnl_r, 4), pid) ) logger.info( - f"[{symbol_upper}] 📝 平仓: {direction} @ {price:.4f} " + f"[{symbol_upper}] 📝 平仓: {direction} @ {exit_price:.4f} " f"status={new_status} pnl={pnl_r:+.2f}R (fee={fee_r:.3f}R)" ) diff --git a/backend/signal_engine.py b/backend/signal_engine.py index 7e83c89..cc7dde5 100644 --- a/backend/signal_engine.py +++ b/backend/signal_engine.py @@ -544,16 +544,18 @@ def paper_check_positions(symbol: str, current_price: float, now_ms: int): pnl_r = 0.0 risk_distance = 2.0 * 0.7 * atr_entry if atr_entry > 0 else 1 + # === 实盘模拟:TP/SL视为限价单,以挂单价成交 === if direction == "LONG": if current_price <= sl: closed = True + exit_price = sl if tp1_hit: new_status = "sl_be" tp1_r = (tp1 - entry_price) / risk_distance if risk_distance > 0 else 0 pnl_r = 0.5 * tp1_r else: new_status = "sl" - pnl_r = (current_price - entry_price) / risk_distance if risk_distance > 0 else -1.0 + pnl_r = (exit_price - entry_price) / risk_distance if risk_distance > 0 else -1.0 elif not tp1_hit and current_price >= tp1: # TP1触发,移动止损到成本价 new_sl = entry_price * 1.0005 @@ -561,6 +563,7 @@ def paper_check_positions(symbol: str, current_price: float, now_ms: int): logger.info(f"[{symbol}] 📝 TP1触发 LONG @ {current_price:.2f}, SL移至成本{new_sl:.2f}") elif tp1_hit and current_price >= tp2: closed = True + exit_price = tp2 new_status = "tp" tp1_r = (tp1 - entry_price) / risk_distance if risk_distance > 0 else 0 tp2_r = (tp2 - entry_price) / risk_distance if risk_distance > 0 else 0 @@ -568,27 +571,30 @@ def paper_check_positions(symbol: str, current_price: float, now_ms: int): else: # SHORT if current_price >= sl: closed = True + exit_price = sl if tp1_hit: new_status = "sl_be" tp1_r = (entry_price - tp1) / risk_distance if risk_distance > 0 else 0 pnl_r = 0.5 * tp1_r else: new_status = "sl" - pnl_r = (entry_price - current_price) / risk_distance if risk_distance > 0 else -1.0 + pnl_r = (entry_price - exit_price) / risk_distance if risk_distance > 0 else -1.0 elif not tp1_hit and current_price <= tp1: new_sl = entry_price * 0.9995 cur.execute("UPDATE paper_trades SET tp1_hit=TRUE, sl_price=%s, status='tp1_hit' WHERE id=%s", (new_sl, pid)) logger.info(f"[{symbol}] 📝 TP1触发 SHORT @ {current_price:.2f}, SL移至成本{new_sl:.2f}") elif tp1_hit and current_price <= tp2: closed = True + exit_price = tp2 new_status = "tp" tp1_r = (entry_price - tp1) / risk_distance if risk_distance > 0 else 0 tp2_r = (entry_price - tp2) / risk_distance if risk_distance > 0 else 0 pnl_r = 0.5 * tp1_r + 0.5 * tp2_r - # 时间止损:60分钟 + # 时间止损:60分钟(市价平仓) if not closed and (now_ms - entry_ts > 60 * 60 * 1000): closed = True + exit_price = current_price new_status = "timeout" if direction == "LONG": move = current_price - entry_price @@ -607,9 +613,9 @@ def paper_check_positions(symbol: str, current_price: float, now_ms: int): cur.execute( "UPDATE paper_trades SET status=%s, exit_price=%s, exit_ts=%s, pnl_r=%s WHERE id=%s", - (new_status, current_price, now_ms, round(pnl_r, 4), pid) + (new_status, exit_price, now_ms, round(pnl_r, 4), pid) ) - logger.info(f"[{symbol}] 📝 模拟平仓: {direction} @ {current_price:.2f} status={new_status} pnl={pnl_r:+.2f}R (fee={fee_r:.3f}R)") + logger.info(f"[{symbol}] 📝 模拟平仓: {direction} @ {exit_price:.2f} status={new_status} pnl={pnl_r:+.2f}R (fee={fee_r:.3f}R)") conn.commit()