Fix OO expiry settle index: persist spot and never invent strike from OTM fill.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-11 16:44:56 +08:00
parent 4a5d19e30f
commit 3264dd4381
6 changed files with 124 additions and 17 deletions
+39 -14
View File
@@ -25,6 +25,7 @@ def _is_oo_group(g: dict) -> bool:
def _infer_settle_index(g: dict, fills: list) -> float | None:
"""优先库内 settle_index_px;否则用「实值腿」成交反推。虚值 fill≈0 时禁止推成行权价。"""
settle_index = g.get("settle_index_px")
if settle_index is not None:
try:
@@ -33,25 +34,44 @@ def _infer_settle_index(g: dict, fills: list) -> float | None:
return v
except (TypeError, ValueError):
pass
strike = g.get("strike")
side = str(g.get("option_side") or "").lower()
if strike is None:
return None
candidates: list[float] = []
for raw in fills:
f = dict(raw) if not isinstance(raw, dict) else raw
if str(f.get("leg")) != "option" or str(f.get("action")) != "close":
if str(f.get("action") or "") != "close":
continue
leg = str(f.get("leg") or "")
if leg not in ("option", "option2"):
continue
if abs(float(f.get("slip") or 0)) > 1e-12:
continue
px = float(f.get("fill_px") or 0)
k = float(strike)
try:
px = float(f.get("fill_px") or 0)
except (TypeError, ValueError):
continue
# 虚值到期 fill=0:k+0 / k-0 会得到行权价,不是真实结算指数
if px <= 1e-9:
continue
if leg == "option":
strike = g.get("strike")
side = str(g.get("option_side") or "").lower()
else:
strike = g.get("strike2")
side = str(g.get("option2_side") or "put").lower()
if strike is None:
continue
try:
k = float(strike)
except (TypeError, ValueError):
continue
if side in ("call", "c"):
return k + px
if side in ("put", "p"):
return k - px
break
return None
candidates.append(k + px)
elif side in ("put", "p"):
candidates.append(k - px)
if not candidates:
return None
# 多腿一致时取平均;实值腿通常只有一条
return round(sum(candidates) / len(candidates), 4)
def _intrinsic(side: str, settle_index: float, strike: float) -> float:
s = str(side or "").lower()
@@ -112,7 +132,7 @@ def _expiry_settle_info(g: dict, fills: list) -> dict | None:
def _close_index_px(g: dict, fills: list) -> float | None:
"""平仓时标的指数:优先 settle_index_px否则永续平仓价近似"""
"""平仓时标的指数:优先库内 settle;到期才用实值腿反推;否则永续平仓价。"""
raw = g.get("settle_index_px")
if raw is not None:
try:
@@ -121,6 +141,11 @@ def _close_index_px(g: dict, fills: list) -> float | None:
return v
except (TypeError, ValueError):
pass
# 仅到期:期权平仓价=内在价值,可反推指数;中途卖出的权利金不能当指数
if str(g.get("close_reason") or "") == "expiry":
inferred = _infer_settle_index(g, fills)
if inferred is not None and inferred > 0:
return inferred
for row in fills:
f = dict(row) if not isinstance(row, dict) else row
if str(f.get("leg") or "") == "perp" and str(f.get("action") or "") == "close":
+8 -1
View File
@@ -1082,16 +1082,23 @@ class BinanceLiveExecutor(Matcher):
)
summary = summarize_fills_pnl(list(fill_rows))
net = float(summary.get("net_pnl") or 0.0)
settle_px = None
if reason == "expiry":
try:
settle_px = self._close_spot_px(get_session().snapshot())
except Exception:
settle_px = None
with self.db._lock:
self.db._conn.execute(
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?,
note=? WHERE group_id=?""",
note=?, settle_index_px=COALESCE(?, settle_index_px) WHERE group_id=?""",
(
"closed",
int(time.time() * 1000),
reason,
net,
f"oo full close {reason} exchange_flat_or_settle",
float(settle_px) if settle_px is not None else None,
group_id,
),
)
+8 -1
View File
@@ -1129,16 +1129,23 @@ class OkxLiveExecutor(Matcher):
)
summary = summarize_fills_pnl(list(fill_rows))
net = float(summary.get("net_pnl") or 0.0)
settle_px = None
if reason == "expiry":
try:
settle_px = self._close_spot_px(get_session().snapshot())
except Exception:
settle_px = None
with self.db._lock:
self.db._conn.execute(
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?,
note=? WHERE group_id=?""",
note=?, settle_index_px=COALESCE(?, settle_index_px) WHERE group_id=?""",
(
"closed",
int(time.time() * 1000),
reason,
net,
f"oo full close {reason} exchange_flat_or_settle",
float(settle_px) if settle_px is not None else None,
group_id,
),
)
+8 -1
View File
@@ -1000,9 +1000,15 @@ class Matcher:
now += 1
with self.db._lock:
settle_px = None
if reason == "expiry":
try:
settle_px = self._close_spot_px(get_session().snapshot())
except Exception:
settle_px = None
self.db._conn.execute(
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?,
fees=COALESCE(fees,0)+?, note=?
fees=COALESCE(fees,0)+?, note=?, settle_index_px=COALESCE(?, settle_index_px)
WHERE group_id=?""",
(
"closed",
@@ -1011,6 +1017,7 @@ class Matcher:
float(total_pnl),
float(total_fees),
f"oo full close {reason}",
float(settle_px) if settle_px is not None else None,
group_id,
),
)