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
+53 -9
View File
@@ -158,7 +158,10 @@ def option_history_row_key(
pos_id = (pos_id or "").strip()
if source == "live":
return f"live:{inst_id}:{pos_id or close_ms or '0'}"
# OKX 可能对同合约多次开平复用 posId,必须带上平仓时间区分
if pos_id:
if close_ms:
return f"ex:{pos_id}:{int(close_ms)}"
return f"ex:{pos_id}"
return f"ex:{inst_id}:{close_ms or 0}"
@@ -1396,28 +1399,69 @@ def resolve_option_close_from_history(
hist_rows: list[dict[str, Any]],
*,
open_ms: int | None = None,
close_ms: int | None = None,
sheets: float | int | None = None,
) -> dict[str, Any] | None:
"""从 positions-history 选取最近一条有效平仓/结算记录."""
best: dict[str, Any] | None = None
best_utime = -1
"""从 positions-history 选取匹配的平仓记录.
同合约多次开平时,优先按开仓时间(cTime≈open_ms)对齐,再按平仓时间/张数;
无锚点时取开仓后最晚一条(供刚平掉的持仓同步)。
"""
candidates: list[tuple[int, dict[str, Any]]] = []
for row in hist_rows:
u_ms = _safe_float(row.get("uTime"))
if u_ms is None or u_ms <= 0:
continue
if open_ms is not None and u_ms < int(open_ms) - 60_000:
u_i = int(u_ms)
# 本地时间偶发与交易所差整时区时,放宽到 12h,主要靠 cTime/张数精配
if open_ms is not None and u_i < int(open_ms) - 12 * 3600_000:
continue
if u_ms > best_utime:
best = row
best_utime = int(u_ms)
if not best:
candidates.append((u_i, row))
if not candidates:
return None
has_ctime = any(_safe_float(row.get("cTime")) is not None for _, row in candidates)
want_sheets = _safe_float(sheets)
def _score(item: tuple[int, dict[str, Any]]) -> tuple:
u_i, row = item
c_ms = _safe_float(row.get("cTime"))
parts: list[float] = []
# 张数优先:同合约多笔时最稳,且不受本地/交易所时区偏差影响
if want_sheets is not None:
hist_sheets = _safe_float(row.get("closeTotalPos"))
if hist_sheets is None:
hist_sheets = _safe_float(row.get("openMaxPos"))
parts.append(
abs(float(hist_sheets) - float(want_sheets))
if hist_sheets is not None
else 1e12
)
if open_ms is not None and c_ms is not None:
parts.append(float(abs(int(c_ms) - int(open_ms))))
if close_ms is not None:
parts.append(float(abs(u_i - int(close_ms))))
if not parts:
parts.append(float(-u_i))
# 同距时偏向更晚平仓
parts.append(float(-u_i))
return tuple(parts)
if open_ms is None and close_ms is None and want_sheets is None:
u_i, best = max(candidates, key=lambda item: item[0])
elif open_ms is not None and close_ms is None and want_sheets is None and not has_ctime:
# 兼容旧调用:只有 open_ms 时仍取最晚一条
u_i, best = max(candidates, key=lambda item: item[0])
else:
u_i, best = min(candidates, key=_score)
realized = _safe_float(best.get("realizedPnl"))
if realized is None:
realized = _safe_float(best.get("pnl"))
return {
"close_quote": _safe_float(best.get("closeAvgPx")),
"realized_pnl": realized,
"close_ms": best_utime,
"close_ms": u_i,
"pos_id": str(best.get("posId") or "").strip() or None,
}