diff --git a/backend/fix_historical_pnl.py b/backend/fix_historical_pnl.py new file mode 100644 index 0000000..27ddec7 --- /dev/null +++ b/backend/fix_historical_pnl.py @@ -0,0 +1,98 @@ +#!/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 exit_p is None or 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 + if status == "tp": + # 半仓TP1 + 半仓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": + # TP1半仓锁定 + SL_BE半仓归零 + 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": + # 全仓止损 + if direction == "LONG": + new_pnl = (exit_p - entry) / risk_distance + else: + new_pnl = (entry - exit_p) / risk_distance + elif status == "timeout": + 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": + 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) + + 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)) + 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()