Backfill OO expiry settle display via public ETHUSDT and intrinsic overlay.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-11 16:58:02 +08:00
parent 3264dd4381
commit 77d2effb6d
5 changed files with 375 additions and 17 deletions
+65
View File
@@ -0,0 +1,65 @@
"""公开行情辅助:补历史到期结算指数展示(不发明成交现金)。"""
from __future__ import annotations
import json
import logging
import urllib.error
import urllib.request
from typing import Any
logger = logging.getLogger(__name__)
_CACHE: dict[int, float] = {}
_CACHE_MAX = 256
def looks_binance_option(inst_id: str | None) -> bool:
return "USD_UM" in str(inst_id or "")
def eth_usdt_close_at_ms(ts_ms: int | None) -> float | None:
"""币安 ETHUSDT 1m K 线收盘价(近似期权结算指数)。失败返回 None。"""
if ts_ms is None:
return None
try:
ms = int(ts_ms)
except (TypeError, ValueError):
return None
if ms <= 0:
return None
minute = (ms // 60_000) * 60_000
cached = _CACHE.get(minute)
if cached is not None:
return cached
url = (
"https://api.binance.com/api/v3/klines"
f"?symbol=ETHUSDT&interval=1m&startTime={minute}&limit=1"
)
try:
with urllib.request.urlopen(url, timeout=4) as resp:
raw = resp.read().decode("utf-8", "replace")
rows = json.loads(raw)
if not rows:
return None
close_px = float(rows[0][4])
if close_px <= 0:
return None
if len(_CACHE) >= _CACHE_MAX:
_CACHE.clear()
_CACHE[minute] = close_px
return close_px
except (urllib.error.URLError, TimeoutError, ValueError, TypeError, IndexError) as e:
logger.debug("eth_usdt_close_at_ms failed ms=%s: %s", minute, e)
return None
def maybe_public_settle_index(g: dict[str, Any]) -> float | None:
"""库内无结算价时,币安期权到期组用公开 ETHUSDT 收盘近似。"""
if str(g.get("close_reason") or "") != "expiry":
return None
inst = g.get("option_inst_id") or g.get("option2_inst_id")
if not looks_binance_option(str(inst) if inst else None):
return None
ts = g.get("close_at_ms") or g.get("hold_close_at_ms")
return eth_usdt_close_at_ms(ts if ts is not None else None)
+86 -14
View File
@@ -25,7 +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_px;否则用「实值腿」成交反推;再否则公开指数近似"""
settle_index = g.get("settle_index_px")
if settle_index is not None:
try:
@@ -68,10 +68,62 @@ def _infer_settle_index(g: dict, fills: list) -> float | 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)
if candidates:
return round(sum(candidates) / len(candidates), 4)
try:
from .public_index import maybe_public_settle_index
pub = maybe_public_settle_index(g)
if pub is not None and pub > 0:
return round(float(pub), 4)
except Exception:
pass
return None
def _overlay_expiry_zero_fills(
g: dict, fills: list, settle_index: float | None
) -> list:
"""到期 close 价为 0 且已有结算指数时,用内在价值覆盖展示(响应层,不写库)。"""
if settle_index is None or settle_index <= 0:
return fills
if str(g.get("close_reason") or "") != "expiry":
return fills
out: list = []
changed = False
for raw in fills:
f = dict(raw) if not isinstance(raw, dict) else dict(raw)
if str(f.get("action") or "") == "close" and str(f.get("leg") or "") in (
"option",
"option2",
):
try:
px = float(f.get("fill_px") or 0)
except (TypeError, ValueError):
px = 0.0
if px <= 1e-9:
leg = str(f.get("leg") or "")
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 not None:
try:
intrinsic = _intrinsic(side, float(settle_index), float(strike))
qty = float(f.get("qty_eth") or 0)
f["fill_px"] = intrinsic
f["base_px"] = intrinsic
f["notional"] = intrinsic * qty
f["slip"] = 0.0
f["_overlay_intrinsic"] = True
changed = True
except (TypeError, ValueError):
pass
out.append(f)
return out if changed else fills
def _intrinsic(side: str, settle_index: float, strike: float) -> float:
s = str(side or "").lower()
@@ -211,15 +263,26 @@ def _option_leverage_for_leg(
def _enrich_group(g: dict, fills: list) -> dict:
is_oo = _is_oo_group(g)
g["is_oo"] = is_oo
summary = summarize_fills_pnl(fills)
# LIVE:优先 groups.realized_pnl(已按交易所回写,含资金费)
if str(g.get("exec_mode") or "").upper() == "LIVE" and g.get("realized_pnl") is not None:
settle = _infer_settle_index(g, fills)
view_fills = _overlay_expiry_zero_fills(g, fills, settle)
overlaid = any(
isinstance(f, dict) and f.get("_overlay_intrinsic") for f in view_fills
)
summary = summarize_fills_pnl(view_fills)
# LIVE 且未做内在价值覆盖:优先 groups.realized_pnl(含资金费)
if (
not overlaid
and str(g.get("exec_mode") or "").upper() == "LIVE"
and g.get("realized_pnl") is not None
):
summary = dict(summary)
summary["net_pnl"] = float(g["realized_pnl"])
if g.get("funding_usdt") is not None:
summary["funding_usdt"] = float(g["funding_usdt"])
summary["pnl_source"] = "live_exchange"
# 期期 SIM:若成交汇总缺腿但组上已有 realized_pnl,用组值兜底
elif overlaid:
summary = dict(summary)
summary["pnl_source"] = "expiry_intrinsic_overlay"
elif (
is_oo
and g.get("realized_pnl") is not None
@@ -240,17 +303,20 @@ def _enrich_group(g: dict, fills: list) -> dict:
prem2 = float(g.get("initial_premium2") or 0) if is_oo else 0.0
g["total_initial_premium"] = prem1 + prem2 if is_oo else prem1
g.update(hold_timing(g, fills))
info = _expiry_settle_info(g, fills)
if settle is not None and g.get("settle_index_px") is None:
g["settle_index_px"] = float(settle)
info = _expiry_settle_info(g, view_fills)
if info:
g["expiry_settle"] = info
if g.get("settle_index_px") is None and info.get("settle_index_px") is not None:
g["settle_index_px"] = info["settle_index_px"]
mp = _move_points(g, fills)
mp = _move_points(g, view_fills)
g["move_points"] = mp
g["close_index_px"] = _close_index_px(g, fills)
g["close_index_px"] = _close_index_px(g, view_fills)
g["option_leverage"] = _option_leverage_for_leg(g, fills, leg="option")
if is_oo:
g["option2_leverage"] = _option_leverage_for_leg(g, fills, leg="option2")
g["_view_fills"] = view_fills
return g
@@ -265,7 +331,9 @@ async def list_groups(_user: Annotated[str, Depends(require_user)]) -> dict:
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC",
(g["group_id"],),
)
groups.append(_enrich_group(g, fills))
gr = _enrich_group(g, fills)
gr.pop("_view_fills", None)
groups.append(gr)
return {"groups": groups}
@@ -281,9 +349,13 @@ async def group_detail(
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,)
)
gr = _enrich_group(_row(g), fills)
view_fills = gr.pop("_view_fills", None) or fills
return {
"group": gr,
"fills": [_row(x) for x in fills],
"fills": [
{k: v for k, v in (dict(x) if not isinstance(x, dict) else x).items() if k != "_overlay_intrinsic"}
for x in view_fills
],
"pnl_summary": gr.get("pnl_summary"),
}
+33 -3
View File
@@ -2,22 +2,28 @@
from __future__ import annotations
from app.api.trades import _infer_settle_index
from app.api.trades import _infer_settle_index, _overlay_expiry_zero_fills
def test_otm_call_zero_fill_does_not_become_strike() -> None:
def test_otm_call_zero_fill_does_not_become_strike(monkeypatch) -> None:
g = {
"hedge_mode": "option_option",
"option_side": "call",
"option2_side": "put",
"option_inst_id": "ETH-USD-260811-1920-C", # OKX 样式:不走公开回退
"strike": 1920.0,
"strike2": 1890.0,
"settle_index_px": None,
"close_reason": "expiry",
"close_at_ms": 1,
}
fills = [
{"leg": "option", "action": "close", "fill_px": 0.0, "slip": 0},
{"leg": "option2", "action": "close", "fill_px": 0.0, "slip": 0},
]
monkeypatch.setattr(
"app.api.public_index.maybe_public_settle_index", lambda _g: None
)
assert _infer_settle_index(g, fills) is None
@@ -34,7 +40,6 @@ def test_itm_put_fill_infers_settle_near_1875() -> None:
{"leg": "option", "action": "close", "fill_px": 0.0, "slip": 0},
{"leg": "option2", "action": "close", "fill_px": 45.0, "slip": 0},
]
# put intrinsic 45 → settle = 1920 - 45 = 1875
assert _infer_settle_index(g, fills) == 1875.0
@@ -46,3 +51,28 @@ def test_stored_settle_wins() -> None:
}
fills = [{"leg": "option", "action": "close", "fill_px": 0.0, "slip": 0}]
assert _infer_settle_index(g, fills) == 1875.2
def test_public_fallback_and_overlay(monkeypatch) -> None:
g = {
"hedge_mode": "option_option",
"option_side": "call",
"option2_side": "put",
"option_inst_id": "ETH-USD_UM-260811-1940-C",
"strike": 1940.0,
"strike2": 1920.0,
"settle_index_px": None,
"close_reason": "expiry",
"close_at_ms": 1786435200000,
}
fills = [
{"leg": "option", "action": "close", "fill_px": 0.0, "qty_eth": 7, "slip": 0},
{"leg": "option2", "action": "close", "fill_px": 0.0, "qty_eth": 7, "slip": 0},
]
monkeypatch.setattr(
"app.api.public_index.maybe_public_settle_index", lambda _g: 1877.8
)
assert _infer_settle_index(g, fills) == 1877.8
view = _overlay_expiry_zero_fills(g, fills, 1877.8)
assert view[0]["fill_px"] == 0.0 # call OTM
assert abs(view[1]["fill_px"] - (1920 - 1877.8)) < 1e-9