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
This commit is contained in:
root 2026-03-01 09:40:00 +00:00
parent d351949a7c
commit 2f9dce483c
3 changed files with 40 additions and 24 deletions

View File

@ -28,16 +28,18 @@ def fix():
for row in rows: for row in rows:
pid, direction, entry, exit_p, tp1, tp2, sl, tp1_hit, status, old_pnl, atr_entry = row 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 continue
risk_distance = 2.0 * 0.7 * atr_entry risk_distance = 2.0 * 0.7 * atr_entry
if risk_distance <= 0: if risk_distance <= 0:
continue continue
# 重算pnl_r # 实盘模拟TP/SL以限价单价格成交
new_exit = exit_p # 默认不变
if status == "tp": if status == "tp":
# 半仓TP1 + 半仓TP2 new_exit = tp2 # TP以TP2价成交
if direction == "LONG": if direction == "LONG":
tp1_r = (tp1 - entry) / risk_distance tp1_r = (tp1 - entry) / risk_distance
tp2_r = (tp2 - entry) / risk_distance tp2_r = (tp2 - entry) / risk_distance
@ -46,19 +48,20 @@ def fix():
tp2_r = (entry - tp2) / risk_distance tp2_r = (entry - tp2) / risk_distance
new_pnl = 0.5 * tp1_r + 0.5 * tp2_r new_pnl = 0.5 * tp1_r + 0.5 * tp2_r
elif status == "sl_be": elif status == "sl_be":
# TP1半仓锁定 + SL_BE半仓归零 new_exit = sl # SL_BE以SL价成交成本价附近
if direction == "LONG": if direction == "LONG":
tp1_r = (tp1 - entry) / risk_distance tp1_r = (tp1 - entry) / risk_distance
else: else:
tp1_r = (entry - tp1) / risk_distance tp1_r = (entry - tp1) / risk_distance
new_pnl = 0.5 * tp1_r new_pnl = 0.5 * tp1_r
elif status == "sl": elif status == "sl":
# 全仓止损 new_exit = sl # SL以SL价成交
if direction == "LONG": if direction == "LONG":
new_pnl = (exit_p - entry) / risk_distance new_pnl = (sl - entry) / risk_distance
else: else:
new_pnl = (entry - exit_p) / risk_distance new_pnl = (entry - sl) / risk_distance
elif status == "timeout": elif status == "timeout":
new_exit = exit_p # 超时市价平仓保持原exit
if direction == "LONG": if direction == "LONG":
move = exit_p - entry move = exit_p - entry
else: else:
@ -68,6 +71,7 @@ def fix():
tp1_r = abs(tp1 - entry) / risk_distance tp1_r = abs(tp1 - entry) / risk_distance
new_pnl = max(new_pnl, 0.5 * tp1_r) new_pnl = max(new_pnl, 0.5 * tp1_r)
elif status == "signal_flip": elif status == "signal_flip":
new_exit = exit_p # 信号翻转市价平仓
if direction == "LONG": if direction == "LONG":
new_pnl = (exit_p - entry) / risk_distance new_pnl = (exit_p - entry) / risk_distance
else: else:
@ -80,9 +84,10 @@ def fix():
new_pnl -= fee_r new_pnl -= fee_r
new_pnl = round(new_pnl, 4) new_pnl = round(new_pnl, 4)
if abs(new_pnl - old_pnl) > 0.001: need_update = abs(new_pnl - old_pnl) > 0.001 or (new_exit and exit_p and abs(new_exit - exit_p) > 0.0001)
print(f" #{pid} {status:10s} {direction:5s}: {old_pnl:+.4f}R → {new_pnl:+.4f}R (Δ{new_pnl-old_pnl:+.4f})") if need_update:
cur.execute("UPDATE paper_trades SET pnl_r = %s WHERE id = %s", (new_pnl, pid)) 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 fixed += 1
conn.commit() conn.commit()

View File

@ -64,24 +64,26 @@ def check_and_close(symbol_upper: str, price: float):
# 统一计算risk_distance (1R基准距离) # 统一计算risk_distance (1R基准距离)
risk_distance = 2.0 * 0.7 * atr_entry if atr_entry > 0 else 1 risk_distance = 2.0 * 0.7 * atr_entry if atr_entry > 0 else 1
# === 实盘模拟TP/SL视为限价单以挂单价成交非市价 ===
if direction == "LONG": if direction == "LONG":
if price <= sl: if price <= sl:
closed = True closed = True
exit_price = sl # 限价止损单以SL价成交
if tp1_hit: if tp1_hit:
new_status = "sl_be" new_status = "sl_be"
# TP1半仓已锁定盈利 = 0.5 * (tp1 - entry) / risk_distance
tp1_r = (tp1 - entry_price) / risk_distance if risk_distance > 0 else 0 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: else:
new_status = "sl" 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: elif not tp1_hit and price >= tp1:
new_sl = entry_price * 1.0005 new_sl = entry_price * 1.0005
cur.execute("UPDATE paper_trades SET tp1_hit=TRUE, sl_price=%s, status='tp1_hit' WHERE id=%s", cur.execute("UPDATE paper_trades SET tp1_hit=TRUE, sl_price=%s, status='tp1_hit' WHERE id=%s",
(new_sl, pid)) (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: elif tp1_hit and price >= tp2:
closed = True closed = True
exit_price = tp2 # 限价止盈单以TP2价成交
new_status = "tp" new_status = "tp"
tp1_r = (tp1 - entry_price) / risk_distance if risk_distance > 0 else 0 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 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 else: # SHORT
if price >= sl: if price >= sl:
closed = True closed = True
exit_price = sl # 限价止损单以SL价成交
if tp1_hit: if tp1_hit:
new_status = "sl_be" new_status = "sl_be"
tp1_r = (entry_price - tp1) / risk_distance if risk_distance > 0 else 0 tp1_r = (entry_price - tp1) / risk_distance if risk_distance > 0 else 0
pnl_r = 0.5 * tp1_r pnl_r = 0.5 * tp1_r
else: else:
new_status = "sl" 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: elif not tp1_hit and price <= tp1:
new_sl = entry_price * 0.9995 new_sl = entry_price * 0.9995
cur.execute("UPDATE paper_trades SET tp1_hit=TRUE, sl_price=%s, status='tp1_hit' WHERE id=%s", cur.execute("UPDATE paper_trades SET tp1_hit=TRUE, sl_price=%s, status='tp1_hit' WHERE id=%s",
(new_sl, pid)) (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: elif tp1_hit and price <= tp2:
closed = True closed = True
exit_price = tp2 # 限价止盈单以TP2价成交
new_status = "tp" new_status = "tp"
tp1_r = (entry_price - tp1) / risk_distance if risk_distance > 0 else 0 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 tp2_r = (entry_price - tp2) / risk_distance if risk_distance > 0 else 0
pnl_r = 0.5 * tp1_r + 0.5 * tp2_r pnl_r = 0.5 * tp1_r + 0.5 * tp2_r
# 时间止损60分钟 # 时间止损60分钟(市价平仓,用当前价)
if not closed and (now_ms - entry_ts > 60 * 60 * 1000): if not closed and (now_ms - entry_ts > 60 * 60 * 1000):
closed = True closed = True
exit_price = price # 超时是市价平仓
new_status = "timeout" new_status = "timeout"
if direction == "LONG": if direction == "LONG":
move = price - entry_price move = price - entry_price
@ -129,10 +134,10 @@ def check_and_close(symbol_upper: str, price: float):
cur.execute( cur.execute(
"UPDATE paper_trades SET status=%s, exit_price=%s, exit_ts=%s, pnl_r=%s WHERE id=%s", "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( 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)" f"status={new_status} pnl={pnl_r:+.2f}R (fee={fee_r:.3f}R)"
) )

View File

@ -544,16 +544,18 @@ def paper_check_positions(symbol: str, current_price: float, now_ms: int):
pnl_r = 0.0 pnl_r = 0.0
risk_distance = 2.0 * 0.7 * atr_entry if atr_entry > 0 else 1 risk_distance = 2.0 * 0.7 * atr_entry if atr_entry > 0 else 1
# === 实盘模拟TP/SL视为限价单以挂单价成交 ===
if direction == "LONG": if direction == "LONG":
if current_price <= sl: if current_price <= sl:
closed = True closed = True
exit_price = sl
if tp1_hit: if tp1_hit:
new_status = "sl_be" new_status = "sl_be"
tp1_r = (tp1 - entry_price) / risk_distance if risk_distance > 0 else 0 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
else: else:
new_status = "sl" 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: elif not tp1_hit and current_price >= tp1:
# TP1触发移动止损到成本价 # TP1触发移动止损到成本价
new_sl = entry_price * 1.0005 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}") logger.info(f"[{symbol}] 📝 TP1触发 LONG @ {current_price:.2f}, SL移至成本{new_sl:.2f}")
elif tp1_hit and current_price >= tp2: elif tp1_hit and current_price >= tp2:
closed = True closed = True
exit_price = tp2
new_status = "tp" new_status = "tp"
tp1_r = (tp1 - entry_price) / risk_distance if risk_distance > 0 else 0 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 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 else: # SHORT
if current_price >= sl: if current_price >= sl:
closed = True closed = True
exit_price = sl
if tp1_hit: if tp1_hit:
new_status = "sl_be" new_status = "sl_be"
tp1_r = (entry_price - tp1) / risk_distance if risk_distance > 0 else 0 tp1_r = (entry_price - tp1) / risk_distance if risk_distance > 0 else 0
pnl_r = 0.5 * tp1_r pnl_r = 0.5 * tp1_r
else: else:
new_status = "sl" 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: elif not tp1_hit and current_price <= tp1:
new_sl = entry_price * 0.9995 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)) 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}") logger.info(f"[{symbol}] 📝 TP1触发 SHORT @ {current_price:.2f}, SL移至成本{new_sl:.2f}")
elif tp1_hit and current_price <= tp2: elif tp1_hit and current_price <= tp2:
closed = True closed = True
exit_price = tp2
new_status = "tp" new_status = "tp"
tp1_r = (entry_price - tp1) / risk_distance if risk_distance > 0 else 0 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 tp2_r = (entry_price - tp2) / risk_distance if risk_distance > 0 else 0
pnl_r = 0.5 * tp1_r + 0.5 * tp2_r pnl_r = 0.5 * tp1_r + 0.5 * tp2_r
# 时间止损60分钟 # 时间止损60分钟(市价平仓)
if not closed and (now_ms - entry_ts > 60 * 60 * 1000): if not closed and (now_ms - entry_ts > 60 * 60 * 1000):
closed = True closed = True
exit_price = current_price
new_status = "timeout" new_status = "timeout"
if direction == "LONG": if direction == "LONG":
move = current_price - entry_price move = current_price - entry_price
@ -607,9 +613,9 @@ def paper_check_positions(symbol: str, current_price: float, now_ms: int):
cur.execute( cur.execute(
"UPDATE paper_trades SET status=%s, exit_price=%s, exit_ts=%s, pnl_r=%s WHERE id=%s", "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() conn.commit()