arbitrage-engine/backend/fix_historical_pnl.py
root 2f9dce483c 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
2026-03-01 09:40:00 +00:00

104 lines
4.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
fix_historical_pnl.py — 修正历史paper_trades中虚高的pnl_r
问题TP/SL_BE场景用了硬编码倍数(1.5R/2.25R)实际应为0.75R/1.125R
修复:用(exit_price - entry_price) / risk_distance 重算
"""
import os, sys
sys.path.insert(0, os.path.dirname(__file__))
from db import get_sync_conn
FEE_RATE = 0.0005 # Taker 0.05%
def fix():
with get_sync_conn() as conn:
cur = conn.cursor()
# 读取所有已平仓记录
cur.execute("""
SELECT id, direction, entry_price, exit_price, tp1_price, tp2_price,
sl_price, tp1_hit, status, pnl_r, atr_at_entry
FROM paper_trades
WHERE status NOT IN ('active', 'tp1_hit')
ORDER BY id
""")
rows = cur.fetchall()
fixed = 0
for row in rows:
pid, direction, entry, exit_p, tp1, tp2, sl, tp1_hit, status, old_pnl, atr_entry = row
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
# 实盘模拟TP/SL以限价单价格成交
new_exit = exit_p # 默认不变
if status == "tp":
new_exit = tp2 # TP以TP2价成交
if direction == "LONG":
tp1_r = (tp1 - entry) / risk_distance
tp2_r = (tp2 - entry) / risk_distance
else:
tp1_r = (entry - tp1) / risk_distance
tp2_r = (entry - tp2) / risk_distance
new_pnl = 0.5 * tp1_r + 0.5 * tp2_r
elif status == "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 = (sl - entry) / risk_distance
else:
new_pnl = (entry - sl) / risk_distance
elif status == "timeout":
new_exit = exit_p # 超时市价平仓保持原exit
if direction == "LONG":
move = exit_p - entry
else:
move = entry - exit_p
new_pnl = move / risk_distance
if tp1_hit:
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:
new_pnl = (entry - exit_p) / risk_distance
else:
continue
# 扣手续费
fee_r = (2 * FEE_RATE * entry) / risk_distance if risk_distance > 0 else 0
new_pnl -= fee_r
new_pnl = round(new_pnl, 4)
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()
# 汇总
cur.execute("SELECT COUNT(*), SUM(pnl_r) FROM paper_trades WHERE status NOT IN ('active','tp1_hit')")
total, total_pnl = cur.fetchone()
print(f"\n修正了 {fixed}/{len(rows)} 笔交易")
print(f"修正后总计: {total}笔, 总pnl={total_pnl:+.2f}R")
if __name__ == "__main__":
fix()