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:
parent
d351949a7c
commit
2f9dce483c
@ -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()
|
||||||
|
|||||||
@ -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)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user