Fix options PnL backfill matching when the same contract is traded twice.

Match exchange history by sheets and open time so an earlier close is not overwritten with the later trade's PnL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-06 09:04:30 +08:00
parent 25ed46e3f2
commit f19500bcd9
4 changed files with 189 additions and 22 deletions
+33 -9
View File
@@ -142,11 +142,13 @@ def _created_at_ms(created_at: Any) -> int | None:
def _group_key_for_closed_trade(row: Any) -> str:
inst = str(row["inst_id"] or "").strip()
ord_id = str(row["close_ord_id"] or "").strip() if "close_ord_id" in row.keys() else ""
if ord_id:
return f"{inst}|ord:{ord_id}"
closed = str(row["closed_at"] or "").strip()
return f"{inst}|close:{(closed[:16] if closed else '')}"
close_prefix = closed[:16] if closed else ""
ord_id = str(row["close_ord_id"] or "").strip() if "close_ord_id" in row.keys() else ""
# 即使 close_ord_id/posId 相同,也要按平仓时间拆开(OKX 可能复用 posId)
if ord_id:
return f"{inst}|ord:{ord_id}|close:{close_prefix}"
return f"{inst}|close:{close_prefix}"
def backfill_closed_options_realized_pnl_from_history(
@@ -170,7 +172,8 @@ def backfill_closed_options_realized_pnl_from_history(
rows = conn.execute(
"""
SELECT id, inst_id, sheets, premium_paid, realized_pnl, created_at, closed_at, close_ord_id
SELECT id, inst_id, sheets, premium_paid, realized_pnl, close_quote,
created_at, closed_at, close_ord_id
FROM options_trades
WHERE status = 'closed'
ORDER BY id DESC
@@ -193,13 +196,26 @@ def backfill_closed_options_realized_pnl_from_history(
inst = str(group[0]["inst_id"] or "").strip()
open_candidates = [_created_at_ms(r["created_at"]) for r in group]
open_ms = min((x for x in open_candidates if x is not None), default=None)
close_info = resolve_option_close_from_history(by_inst.get(inst) or [], open_ms=open_ms)
close_candidates = [_created_at_ms(r["closed_at"]) for r in group]
close_ms = max((x for x in close_candidates if x is not None), default=None)
sheets_hint = None
try:
sheets_hint = sum(float(_safe_float(r["sheets"]) or 0.0) for r in group) or None
except (TypeError, ValueError):
sheets_hint = None
close_info = resolve_option_close_from_history(
by_inst.get(inst) or [],
open_ms=open_ms,
close_ms=close_ms,
sheets=sheets_hint,
)
if not close_info:
continue
ex_pnl = _safe_float(close_info.get("realized_pnl"))
if ex_pnl is None:
continue
close_quote = _safe_float(close_info.get("close_quote"))
matched_pos = str(close_info.get("pos_id") or "").strip() or None
total_paid = 0.0
for r in group:
total_paid += float(_safe_float(r["premium_paid"]) or 0.0)
@@ -215,7 +231,14 @@ def backfill_closed_options_realized_pnl_from_history(
share = round(float(ex_pnl) / len(group), 4)
allocated += share
local = _safe_float(r["realized_pnl"])
if local is not None and abs(local - share) < 1e-6:
local_close = _safe_float(r["close_quote"])
local_ord = str(r["close_ord_id"] or "").strip()
pnl_ok = local is not None and abs(local - share) < 1e-6
quote_ok = close_quote is None or (
local_close is not None and abs(local_close - float(close_quote)) < 1e-6
)
ord_ok = (not matched_pos) or (local_ord == matched_pos)
if pnl_ok and quote_ok and ord_ok:
continue
prem_recv = round(paid + share, 4)
conn.execute(
@@ -223,10 +246,11 @@ def backfill_closed_options_realized_pnl_from_history(
UPDATE options_trades
SET realized_pnl = ?,
premium_received = ?,
close_quote = COALESCE(?, close_quote)
close_quote = COALESCE(?, close_quote),
close_ord_id = COALESCE(?, close_ord_id)
WHERE id = ?
""",
(share, prem_recv, close_quote, int(r["id"])),
(share, prem_recv, close_quote, matched_pos, int(r["id"])),
)
updated += 1
return updated