diff --git a/backend/app/api/trades.py b/backend/app/api/trades.py index bf1d279..bb858ba 100644 --- a/backend/app/api/trades.py +++ b/backend/app/api/trades.py @@ -16,6 +16,51 @@ def _row(r: Any) -> dict: return dict(r) +def _expiry_settle_info(g: dict, fills: list) -> dict | None: + """到期结算口径:期权价 = 内在价值(指数 vs 行权价),非盘口。""" + if str(g.get("close_reason") or "") != "expiry": + return None + strike = g.get("strike") + side = str(g.get("option_side") or "").lower() + settle_index = g.get("settle_index_px") + if settle_index is None and strike is not None: + 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": + continue + if abs(float(f.get("slip") or 0)) > 1e-12: + continue + px = float(f.get("fill_px") or 0) + k = float(strike) + if side in ("call", "c"): + settle_index = k + px + elif side in ("put", "p"): + settle_index = k - px + break + intrinsic = None + if settle_index is not None and strike is not None: + s = float(settle_index) + k = float(strike) + if side in ("call", "c"): + intrinsic = max(s - k, 0.0) + elif side in ("put", "p"): + intrinsic = max(k - s, 0.0) + formula = ( + "Call: max(指数−行权价, 0)" + if side in ("call", "c") + else "Put: max(行权价−指数, 0)" + if side in ("put", "p") + else "" + ) + return { + "settle_index_px": float(settle_index) if settle_index is not None else None, + "strike": float(strike) if strike is not None else None, + "intrinsic": intrinsic, + "formula": formula, + "perp_note": "永续仍按市价平仓(非指数交割)", + } + + def _enrich_group(g: dict, fills: list) -> dict: summary = summarize_fills_pnl(fills) # LIVE:优先 groups.realized_pnl(已按交易所回写,含资金费) @@ -31,6 +76,11 @@ def _enrich_group(g: dict, fills: list) -> dict: elif g.get("realized_pnl") is not None: g["net_pnl"] = float(g["realized_pnl"]) g.update(hold_timing(g, fills)) + info = _expiry_settle_info(g, 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"] return g diff --git a/backend/app/models/db.py b/backend/app/models/db.py index c190ace..04cf465 100644 --- a/backend/app/models/db.py +++ b/backend/app/models/db.py @@ -42,7 +42,8 @@ CREATE TABLE IF NOT EXISTS groups ( slip_cost REAL DEFAULT 0, note TEXT, exec_mode TEXT, - funding_usdt REAL + funding_usdt REAL, + settle_index_px REAL ); CREATE TABLE IF NOT EXISTS fills ( @@ -152,6 +153,7 @@ class Database: for table, col, decl in ( ("groups", "exec_mode", "TEXT"), ("groups", "funding_usdt", "REAL"), + ("groups", "settle_index_px", "REAL"), ("fills", "exec_mode", "TEXT"), ("fills", "fee_ccy", "TEXT"), ): diff --git a/backend/app/sim/matcher.py b/backend/app/sim/matcher.py index e29b13b..14f6568 100644 --- a/backend/app/sim/matcher.py +++ b/backend/app/sim/matcher.py @@ -566,10 +566,20 @@ class Matcher: ).fetchone() fees = float(g["fees"] or 0) + pf.fee + of.fee slip = float(g["slip_cost"] or 0) + pf.slip + of.slip + settle_index = float(spot) if is_expiry and spot is not None else None self.db._conn.execute( """UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?, - fees=?, slip_cost=?, note=NULL WHERE group_id=?""", - ("closed", now, reason, net_after_all_fees, fees, slip, group_id), + fees=?, slip_cost=?, note=NULL, settle_index_px=? WHERE group_id=?""", + ( + "closed", + now, + reason, + net_after_all_fees, + fees, + slip, + settle_index, + group_id, + ), ) self.db._conn.execute( """UPDATE positions SET diff --git a/docs/更新说明.md b/docs/更新说明.md index 4ff5f5f..92dd5fc 100644 --- a/docs/更新说明.md +++ b/docs/更新说明.md @@ -5,6 +5,16 @@ --- +## 2026-07-29 — 到期结算展示:指数 / 行权价 / 内在价值 + +### 变更 + +1. 到期平仓期权价本就是 **内在价值**(`max(指数−K,0)` / Put 对称),不是盘口;详情页补 **结算指数、行权价、内在价值**。 +2. 成交行标注「期权到期结算」;组表落库 `settle_index_px`。 +3. 说明:永续到期时仍按市价平(与策略文档一致)。 + +--- + ## 2026-07-29 — 持仓卡显示开仓时间 / 持仓时长 ### 变更 diff --git a/frontend/src/labels.ts b/frontend/src/labels.ts index 73adf4c..5b0d037 100644 --- a/frontend/src/labels.ts +++ b/frontend/src/labels.ts @@ -52,10 +52,18 @@ export function positionSidesZh( return `${sideZh(perp)}/${sideZh(option)}`; } -export function fillDescZh(leg: string, action: string, side: string): string { +export function fillDescZh( + leg: string, + action: string, + side: string, + closeReason?: string | null, +): string { const l = LEG_ZH[leg] || leg; const a = ACTION_ZH[action] || action; const s = SIDE_ZH[side] || side; + if (leg === "option" && action === "close" && closeReason === "expiry") { + return "期权到期结算"; + } // 期权买入开仓:「期权开多」;永续:「永续开多/开空」 if (leg === "option" && action === "open") return "期权开多"; if (leg === "option" && action === "close") return "期权平多"; diff --git a/frontend/src/pages/Trades.tsx b/frontend/src/pages/Trades.tsx index f475524..e86eddb 100644 --- a/frontend/src/pages/Trades.tsx +++ b/frontend/src/pages/Trades.tsx @@ -15,6 +15,14 @@ type PnlSummary = { net_pnl: number | null; }; +type ExpirySettle = { + settle_index_px: number | null; + strike: number | null; + intrinsic: number | null; + formula: string; + perp_note: string; +}; + type Group = { group_id: string; status: string; @@ -31,6 +39,9 @@ type Group = { hold_close_at_ms?: number | null; hold_ms?: number | null; hold_basis?: string | null; + strike?: number | null; + settle_index_px?: number | null; + expiry_settle?: ExpirySettle | null; pnl_summary?: PnlSummary; }; @@ -276,14 +287,46 @@ export default function TradesPage() {
- 成交价为成交均价(未预先扣费);手续费单独列出。净盈亏 = 期权盈亏 + - 永续盈亏 − 全部手续费。 + {selectedGroup.close_reason === "expiry" + ? "到期结算:期权按「指数 vs 行权价」的内在价值入账(非盘口);永续仍按市价平。净盈亏 = 期权盈亏 + 永续盈亏 − 全部手续费。" + : "成交价为成交均价(未预先扣费);手续费单独列出。净盈亏 = 期权盈亏 + 永续盈亏 − 全部手续费。"}
+ {selectedGroup.expiry_settle ? ( +