Fix OO expiry Put PnL: overlay wrong near-zero settlement fills with intrinsic.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+29
-16
@@ -82,10 +82,14 @@ def _infer_settle_index(g: dict, fills: list) -> float | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _overlay_expiry_zero_fills(
|
def _overlay_expiry_intrinsic_fills(
|
||||||
g: dict, fills: list, settle_index: float | None
|
g: dict, fills: list, settle_index: float | None
|
||||||
) -> list:
|
) -> list:
|
||||||
"""到期 close 价为 0 且已有结算指数时,用内在价值覆盖展示(响应层,不写库)。"""
|
"""到期且已有结算指数:close 成交按内在价值覆盖展示/盈亏(响应层)。
|
||||||
|
|
||||||
|
交易所账单偶发落成近 0 价(如 0.2),与内在价值(如 42.2)严重不符时
|
||||||
|
若只覆盖 fill≈0,Put 仍会按错误价算成巨亏。
|
||||||
|
"""
|
||||||
if settle_index is None or settle_index <= 0:
|
if settle_index is None or settle_index <= 0:
|
||||||
return fills
|
return fills
|
||||||
if str(g.get("close_reason") or "") != "expiry":
|
if str(g.get("close_reason") or "") != "expiry":
|
||||||
@@ -102,17 +106,21 @@ def _overlay_expiry_zero_fills(
|
|||||||
px = float(f.get("fill_px") or 0)
|
px = float(f.get("fill_px") or 0)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
px = 0.0
|
px = 0.0
|
||||||
if px <= 1e-9:
|
leg = str(f.get("leg") or "")
|
||||||
leg = str(f.get("leg") or "")
|
if leg == "option":
|
||||||
if leg == "option":
|
strike = g.get("strike")
|
||||||
strike = g.get("strike")
|
side = str(g.get("option_side") or "").lower()
|
||||||
side = str(g.get("option_side") or "").lower()
|
else:
|
||||||
else:
|
strike = g.get("strike2")
|
||||||
strike = g.get("strike2")
|
side = str(g.get("option2_side") or "put").lower()
|
||||||
side = str(g.get("option2_side") or "put").lower()
|
if strike is not None:
|
||||||
if strike is not None:
|
try:
|
||||||
try:
|
intrinsic = float(
|
||||||
intrinsic = _intrinsic(side, float(settle_index), float(strike))
|
_intrinsic(side, float(settle_index), float(strike))
|
||||||
|
)
|
||||||
|
# 与内在价值偏差超过 0.5 USDT(或相对 5%)则覆盖
|
||||||
|
tol = max(0.5, abs(intrinsic) * 0.05)
|
||||||
|
if abs(px - intrinsic) > tol:
|
||||||
qty = float(f.get("qty_eth") or 0)
|
qty = float(f.get("qty_eth") or 0)
|
||||||
f["fill_px"] = intrinsic
|
f["fill_px"] = intrinsic
|
||||||
f["base_px"] = intrinsic
|
f["base_px"] = intrinsic
|
||||||
@@ -120,11 +128,16 @@ def _overlay_expiry_zero_fills(
|
|||||||
f["slip"] = 0.0
|
f["slip"] = 0.0
|
||||||
f["_overlay_intrinsic"] = True
|
f["_overlay_intrinsic"] = True
|
||||||
changed = True
|
changed = True
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
out.append(f)
|
out.append(f)
|
||||||
return out if changed else fills
|
return out if changed else fills
|
||||||
|
|
||||||
|
|
||||||
|
# 兼容旧测试名
|
||||||
|
_overlay_expiry_zero_fills = _overlay_expiry_intrinsic_fills
|
||||||
|
|
||||||
|
|
||||||
def _intrinsic(side: str, settle_index: float, strike: float) -> float:
|
def _intrinsic(side: str, settle_index: float, strike: float) -> float:
|
||||||
s = str(side or "").lower()
|
s = str(side or "").lower()
|
||||||
if s in ("call", "c"):
|
if s in ("call", "c"):
|
||||||
@@ -264,7 +277,7 @@ def _enrich_group(g: dict, fills: list) -> dict:
|
|||||||
is_oo = _is_oo_group(g)
|
is_oo = _is_oo_group(g)
|
||||||
g["is_oo"] = is_oo
|
g["is_oo"] = is_oo
|
||||||
settle = _infer_settle_index(g, fills)
|
settle = _infer_settle_index(g, fills)
|
||||||
view_fills = _overlay_expiry_zero_fills(g, fills, settle)
|
view_fills = _overlay_expiry_intrinsic_fills(g, fills, settle)
|
||||||
overlaid = any(
|
overlaid = any(
|
||||||
isinstance(f, dict) and f.get("_overlay_intrinsic") for f in view_fills
|
isinstance(f, dict) and f.get("_overlay_intrinsic") for f in view_fills
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -76,3 +76,57 @@ def test_public_fallback_and_overlay(monkeypatch) -> None:
|
|||||||
view = _overlay_expiry_zero_fills(g, fills, 1877.8)
|
view = _overlay_expiry_zero_fills(g, fills, 1877.8)
|
||||||
assert view[0]["fill_px"] == 0.0 # call OTM
|
assert view[0]["fill_px"] == 0.0 # call OTM
|
||||||
assert abs(view[1]["fill_px"] - (1920 - 1877.8)) < 1e-9
|
assert abs(view[1]["fill_px"] - (1920 - 1877.8)) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_overlay_wrong_near_zero_put_fill() -> None:
|
||||||
|
"""账单误写成 0.2 时,应按内在价值 42.2 覆盖,Put 显示盈利。"""
|
||||||
|
g = {
|
||||||
|
"hedge_mode": "option_option",
|
||||||
|
"option_side": "call",
|
||||||
|
"option2_side": "put",
|
||||||
|
"strike": 1940.0,
|
||||||
|
"strike2": 1920.0,
|
||||||
|
"close_reason": "expiry",
|
||||||
|
}
|
||||||
|
fills = [
|
||||||
|
{
|
||||||
|
"leg": "option",
|
||||||
|
"action": "open",
|
||||||
|
"fill_px": 12.0,
|
||||||
|
"qty_eth": 7.5,
|
||||||
|
"fee": 0.045,
|
||||||
|
"slip": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"leg": "option2",
|
||||||
|
"action": "open",
|
||||||
|
"fill_px": 12.8,
|
||||||
|
"qty_eth": 7.0,
|
||||||
|
"fee": 0.045,
|
||||||
|
"slip": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"leg": "option",
|
||||||
|
"action": "close",
|
||||||
|
"fill_px": 0.0,
|
||||||
|
"qty_eth": 7.5,
|
||||||
|
"fee": 0,
|
||||||
|
"slip": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"leg": "option2",
|
||||||
|
"action": "close",
|
||||||
|
"fill_px": 0.2,
|
||||||
|
"qty_eth": 7.0,
|
||||||
|
"fee": 0.0007,
|
||||||
|
"slip": 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
from app.sim.pnl import summarize_fills_pnl
|
||||||
|
|
||||||
|
view = _overlay_expiry_zero_fills(g, fills, 1877.8)
|
||||||
|
assert abs(view[3]["fill_px"] - 42.2) < 1e-9
|
||||||
|
s = summarize_fills_pnl(view)
|
||||||
|
# Put: (42.2 - 12.8) * 7 = 205.8
|
||||||
|
assert abs(float(s["option2_pnl"] or 0) - 205.8) < 1e-6
|
||||||
|
assert float(s["option2_pnl"] or 0) > 0
|
||||||
|
|||||||
@@ -5,6 +5,18 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-08-11 — 期期到期盈亏:错误近零成交按内在价值覆盖
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
到期 close 成交与内在价值偏差过大(如 Put 账单 0.2 vs 内在 42.2)时,详情按内在价值重算腿盈亏;不再只覆盖 fill=0。
|
||||||
|
|
||||||
|
### 审计
|
||||||
|
|
||||||
|
G-20260810-01:结算指数 1877.8、Put 内在 42.2 正确,但成交仍用 0.2 → Put 显示 −88。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-08-11 — 期期到期结算指数:公开行情回退 + 零成交覆盖
|
## 2026-08-11 — 期期到期结算指数:公开行情回退 + 零成交覆盖
|
||||||
|
|
||||||
### 变更
|
### 变更
|
||||||
|
|||||||
@@ -86,14 +86,16 @@ if REPAIR_FILLS:
|
|||||||
).fetchone()
|
).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
continue
|
continue
|
||||||
if abs(float(row["fill_px"] or 0)) > 1e-9:
|
old_px = float(row["fill_px"] or 0)
|
||||||
|
tol = max(0.5, abs(iv) * 0.05)
|
||||||
|
if abs(old_px - iv) <= tol:
|
||||||
continue
|
continue
|
||||||
qty = float(row["qty_eth"] or 0)
|
qty = float(row["qty_eth"] or 0)
|
||||||
con.execute(
|
con.execute(
|
||||||
"UPDATE fills SET base_px=?, fill_px=?, notional=?, slip=0 WHERE id=?",
|
"UPDATE fills SET base_px=?, fill_px=?, notional=?, slip=0 WHERE id=?",
|
||||||
(iv, iv, iv * qty, row["id"]),
|
(iv, iv, iv * qty, row["id"]),
|
||||||
)
|
)
|
||||||
updated.append((leg, iv, qty))
|
updated.append((leg, old_px, iv, qty))
|
||||||
# 重算 realized
|
# 重算 realized
|
||||||
fills = list(con.execute("SELECT * FROM fills WHERE group_id=?", (GROUP,)))
|
fills = list(con.execute("SELECT * FROM fills WHERE group_id=?", (GROUP,)))
|
||||||
opt = {}
|
opt = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user