From 0ac03cc72666deebd9c59fc995125614fa7844a2 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 7 Aug 2026 17:25:03 +0800 Subject: [PATCH] Align OO Call/Put position cards with per-leg mark, size, leverage. Co-authored-by: Cursor --- backend/app/live/live_pnl.py | 5 +- backend/app/sim/matcher.py | 77 ++++- frontend/src/api/client.ts | 6 + frontend/src/pages/Plan.tsx | 540 ++++++++++++++++++++++------------- 4 files changed, 417 insertions(+), 211 deletions(-) diff --git a/backend/app/live/live_pnl.py b/backend/app/live/live_pnl.py index 03db58e..74181ec 100644 --- a/backend/app/live/live_pnl.py +++ b/backend/app/live/live_pnl.py @@ -72,13 +72,16 @@ def enrich_live_unrealized( logger.warning("live unrealized exchange overlay failed: %s", e) option_upl = float(base.get("option_upl") or 0.0) # 期权净盈亏(本地) + option2_upl = float(base.get("option2_upl") or 0.0) # 期期 Put 腿 # 盯盘/达标:离场费 ≈ 入场费 → 合计扣 已付×2 est_close = float(fees_paid) - net_pnl = perp_upl + option_upl - fees_paid * 2.0 + funding + # 期期无永续:perp_upl 一般为 0;仍加 option2 + net_pnl = perp_upl + option_upl + option2_upl - fees_paid * 2.0 + funding out = dict(base) out["perp_upl"] = perp_upl out["option_upl"] = option_upl + out["option2_upl"] = option2_upl out["fees_paid"] = fees_paid out["funding_usdt"] = funding out["est_close_fees"] = est_close diff --git a/backend/app/sim/matcher.py b/backend/app/sim/matcher.py index a07c5f8..00479fd 100644 --- a/backend/app/sim/matcher.py +++ b/backend/app/sim/matcher.py @@ -2012,15 +2012,23 @@ class Matcher: index_px = snap.perp.mark_px qty = float(pos.get("option_qty_eth") or 0) qty2 = float(pos.get("option2_qty_eth") or qty) + contracts1 = float(pos.get("option_qty_contracts") or 0) + contracts2 = float(pos.get("option2_qty_contracts") or 0) prem1 = float(pos.get("initial_premium") or 0) prem2 = float(pos.get("initial_premium2") or 0) call_id = str(pos.get("option_inst_id") or "") put_id = str(pos.get("option2_inst_id") or "") + entry1 = float(pos.get("option_entry_px") or 0) + entry2 = float(pos.get("option2_entry_px") or 0) oq1 = self._quote_held_option(call_id) if call_id else None oq2 = self._quote_held_option(put_id) if put_id else None option_upl = 0.0 option2_upl = 0.0 fees = 0.0 + opt_mark = None + opt_bid_sz = None + opt2_mark = None + opt2_bid_sz = None if oq1 and oq1.bid is not None and qty > 0: bid = float(oq1.bid) of = option_fill( @@ -2032,6 +2040,13 @@ class Matcher: ) fees += of.fee option_upl = bid * qty - prem1 + opt_mark = bid + opt_bid_sz = float(oq1.bid_sz) if oq1.bid_sz is not None else None + elif oq1: + opt_mark = oq1.bid or oq1.mark_px + opt_bid_sz = float(oq1.bid_sz) if oq1.bid_sz is not None else None + if opt_mark is not None and qty > 0: + option_upl = float(opt_mark) * qty - prem1 if oq2 and oq2.bid is not None and qty2 > 0: bid = float(oq2.bid) of = option_fill( @@ -2043,15 +2058,51 @@ class Matcher: ) fees += of.fee option2_upl = bid * qty2 - prem2 + opt2_mark = bid + opt2_bid_sz = float(oq2.bid_sz) if oq2.bid_sz is not None else None + elif oq2: + opt2_mark = oq2.bid or oq2.mark_px + opt2_bid_sz = float(oq2.bid_sz) if oq2.bid_sz is not None else None + if opt2_mark is not None and qty2 > 0: + option2_upl = float(opt2_mark) * qty2 - prem2 g = None gid = pos.get("group_id") if gid: g = self.db.fetchone("SELECT * FROM groups WHERE group_id=?", (gid,)) paid = float(g["fees"] or 0) if g else 0.0 - net = option_upl + option2_upl - paid - fees + # SIM:浮盈 − 估平仓费;有入场费时离场按入场估算(对齐永期口径) + if paid > 1e-12: + est_close = paid + net = option_upl + option2_upl - paid * 2.0 + else: + est_close = fees + net = option_upl + option2_upl - fees entry_idx = float(pos.get("entry_index_px") or 0) move = abs(float(index_px) - entry_idx) if index_px is not None and entry_idx else 0.0 move_pct = (move / entry_idx * 100.0) if entry_idx > 0 else 0.0 + from ..strategy.selection import option_leverage as _opt_lev + + opt_lev = ( + _opt_lev(entry_idx, entry1) if entry_idx > 0 and entry1 > 0 else None + ) + opt2_lev = ( + _opt_lev(entry_idx, entry2) if entry_idx > 0 and entry2 > 0 else None + ) + if opt_lev is not None: + opt_lev = round(float(opt_lev), 1) + if opt2_lev is not None: + opt2_lev = round(float(opt2_lev), 1) + expiry_ymd = str(g["expiry_ymd"]) if g and g["expiry_ymd"] else None + expiry_ms = None + if expiry_ymd and len(expiry_ymd) == 6: + try: + from ..exchange.expiry import expiry_ms_from_ymd + + expiry_ms = expiry_ms_from_ymd(expiry_ymd) + except Exception: + expiry_ms = None + strike1 = float(g["strike"]) if g and g["strike"] is not None else None + strike2 = float(pos.get("strike2") or 0) or None return { "has_position": True, "hedge_mode": "option_option", @@ -2062,23 +2113,33 @@ class Matcher: "option2_upl": option2_upl, "net_pnl": net, "fees_paid": paid, - "est_close_fees": fees, + "est_close_fees": est_close, "index_px": float(index_px) if index_px is not None else None, "entry_index_px": entry_idx, "move_points": move, "move_pct": move_pct, - "initial_premium": prem1 + prem2, + "initial_premium": prem1, + "initial_premium2": prem2, "option_inst_id": call_id, "option2_inst_id": put_id, "option_side": "call", "option2_side": "put", "option_qty_eth": qty, "option2_qty_eth": qty2, - "option_entry_px": float(pos.get("option_entry_px") or 0), - "option2_entry_px": float(pos.get("option2_entry_px") or 0), - "strike": float(g["strike"]) if g and g["strike"] is not None else None, - "strike2": float(pos.get("strike2") or 0) or None, - "expiry_ymd": g["expiry_ymd"] if g else None, + "option_qty_contracts": contracts1, + "option2_qty_contracts": contracts2, + "option_entry_px": entry1, + "option2_entry_px": entry2, + "option_mark_px": float(opt_mark) if opt_mark is not None else None, + "option_bid_sz": float(opt_bid_sz) if opt_bid_sz is not None else None, + "option2_mark_px": float(opt2_mark) if opt2_mark is not None else None, + "option2_bid_sz": float(opt2_bid_sz) if opt2_bid_sz is not None else None, + "option_leverage": float(opt_lev) if opt_lev is not None else None, + "option2_leverage": float(opt2_lev) if opt2_lev is not None else None, + "strike": strike1, + "strike2": strike2, + "expiry_ymd": expiry_ymd, + "expiry_ms": expiry_ms, "open_at_ms": int(g["open_at_ms"]) if g and g["open_at_ms"] else None, "perp_side": None, "perp_qty_eth": 0.0, diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index cb0ad2c..5ea5fb2 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -320,10 +320,16 @@ export type PlanState = { premium_gap?: number; hedge_mode?: string; option2_inst_id?: string; + option2_side?: string; option2_upl?: number; option2_entry_px?: number; option2_qty_eth?: number; + option2_qty_contracts?: number; + option2_mark_px?: number | null; + option2_bid_sz?: number | null; + option2_leverage?: number | null; strike2?: number | null; + initial_premium2?: number; }; residuals?: { group_id: string; diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index f733110..0ed321e 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -652,217 +652,353 @@ export default function PlanPage() {
{open ? ( <> - {plan?.hedge_mode === "option_option" || - pos?.hedge_mode === "option_option" || - pos?.option2_inst_id ? ( -
-
-
- - {pos?.option_inst_id || "Call"} - - - 看涨 Call - -
-
-
- 期期 · Call - 组 {pos?.group_id} - - 数量 {fmt(pos?.option_qty_eth, 1)} ETH - -
-
-
- 开仓价 - - {fmt(pos?.option_entry_px)} - -
-
- 浮盈亏 - +
+
+
+ {pos?.option_inst_id || "Call"} + + 看涨 Call + +
+
- {fmt(pos?.option_upl)} + {countdown} +
+
+
+ 期期 · Call + + 到期 {pos?.expiry_ymd || "—"} + + + 行权{" "} + {pos?.strike != null + ? Math.round(Number(pos.strike)) + : "—"} + + + {fmt(pos?.option_qty_eth, 2)} ETH ·{" "} + {fmt(pos?.option_qty_contracts, 0)} 张 + + + 杠杆 {fmt(pos?.option_leverage, 0)}x
-
- 行权价 - - {fmt(pos?.strike, 0)} - -
-
- 持仓时长 - - {holdDurationLabel} - +
+
+ 开仓均价 + + {fmtExPx("option", pos?.option_entry_px)} + +
+
+ 买一 + + {fmtBidLiqEx( + "option", + pos?.option_mark_px, + pos?.option_bid_sz, + )} + +
+
+ + {plan?.mode === "LIVE" ? "净盈亏" : "浮盈亏"} + + + {fmt(pos?.option_upl)} + +
+
+ 初始权利金 + + {fmt(pos?.initial_premium)} + +
+
+
+ 开仓时间 + {openTimeLabel} +
+
+ 持仓时长 + + {holdDurationLabel} + +
+
-
- ) : ( -
-
-
- {pos?.perp_inst_id || "ETH-USDT-SWAP"} - - {pos?.perp_side === "long" ? "做多" : "做空"} - -
-
-
- 永续持仓 - 组 {pos?.group_id} - - 数量 {fmt(pos?.perp_qty_eth, 2)} ETH - - - {fmt(pos?.leverage ?? plan?.leverage, 0)}x - -
-
-
- 开仓价 - - {fmtExPx("perp", pos?.perp_entry_px)} - -
-
- 可平价 - - {fmtExPx("perp", pos?.perp_mark_px)} - -
-
- 浮盈亏 - - {fmt(pos?.perp_upl)} - -
-
- 保证金 - {fmt(pos?.perp_margin)} -
-
- 名义价值 - {fmt(pos?.perp_notional)} -
-
- 波动比例 - {fmt(movePct, 2)}% -
-
-
- 开仓时间 - {openTimeLabel} -
-
- 持仓时长 - {holdDurationLabel} -
-
-
-
- )} -
-
-
- - {plan?.hedge_mode === "option_option" || - pos?.option2_inst_id - ? pos?.option2_inst_id || "Put" - : pos?.option_inst_id || "期权"} - - - {plan?.hedge_mode === "option_option" || - pos?.option2_inst_id - ? "看跌 Put" - : pos?.option_side === "call" - ? "看涨 Call" - : "看跌 Put"} - -
-
- {countdown} -
-
-
- 期权持仓 - 到期 {pos?.expiry_ymd || "—"} - - 行权{" "} - {pos?.strike != null - ? Math.round(Number(pos.strike)) - : "—"} - - - {fmt(pos?.option_qty_eth, 2)} ETH · {fmt(pos?.option_qty_contracts, 0)} 张 - - - 杠杆 {fmt(pos?.option_leverage, 0)}x - -
-
-
- 开仓均价 - - {fmtExPx("option", pos?.option_entry_px)} - -
-
- 买一 - - {fmtBidLiqEx( - "option", - pos?.option_mark_px, - pos?.option_bid_sz, - )} - -
-
- - {plan?.mode === "LIVE" ? "净盈亏" : "浮盈亏"} - - - {fmt(pos?.option_upl)} - -
-
- 初始权利金 - {fmt(pos?.initial_premium)} -
-
-
- 开仓时间 - {openTimeLabel} +
+
+
+ {pos?.option2_inst_id || "Put"} + + 看跌 Put + +
+
+ {countdown} +
-
- 持仓时长 - {holdDurationLabel} +
+ 期期 · Put + + 到期 {pos?.expiry_ymd || "—"} + + + 行权{" "} + {pos?.strike2 != null + ? Math.round(Number(pos.strike2)) + : "—"} + + + {fmt(pos?.option2_qty_eth, 2)} ETH ·{" "} + {fmt(pos?.option2_qty_contracts, 0)} 张 + + + 杠杆 {fmt(pos?.option2_leverage, 0)}x + +
+
+
+ 开仓均价 + + {fmtExPx("option", pos?.option2_entry_px)} + +
+
+ 买一 + + {fmtBidLiqEx( + "option", + pos?.option2_mark_px, + pos?.option2_bid_sz, + )} + +
+
+ + {plan?.mode === "LIVE" ? "净盈亏" : "浮盈亏"} + + + {fmt(pos?.option2_upl)} + +
+
+ 初始权利金 + + {fmt(pos?.initial_premium2)} + +
+
+
+ 开仓时间 + {openTimeLabel} +
+
+ 持仓时长 + + {holdDurationLabel} + +
+
-
-
+ + ) : ( + <> +
+
+
+ {pos?.perp_inst_id || "ETH-USDT-SWAP"} + + {pos?.perp_side === "long" ? "做多" : "做空"} + +
+
+
+ 永续持仓 + 组 {pos?.group_id} + + 数量 {fmt(pos?.perp_qty_eth, 2)} ETH + + + {fmt(pos?.leverage ?? plan?.leverage, 0)}x + +
+
+
+ 开仓价 + + {fmtExPx("perp", pos?.perp_entry_px)} + +
+
+ 可平价 + + {fmtExPx("perp", pos?.perp_mark_px)} + +
+
+ 浮盈亏 + + {fmt(pos?.perp_upl)} + +
+
+ 保证金 + + {fmt(pos?.perp_margin)} + +
+
+ 名义价值 + + {fmt(pos?.perp_notional)} + +
+
+ 波动比例 + + {fmt(movePct, 2)}% + +
+
+
+ 开仓时间 + {openTimeLabel} +
+
+ 持仓时长 + + {holdDurationLabel} + +
+
+
+
+ +
+
+
+ {pos?.option_inst_id || "期权"} + + {pos?.option_side === "call" + ? "看涨 Call" + : "看跌 Put"} + +
+
+ {countdown} +
+
+
+ 期权持仓 + + 到期 {pos?.expiry_ymd || "—"} + + + 行权{" "} + {pos?.strike != null + ? Math.round(Number(pos.strike)) + : "—"} + + + {fmt(pos?.option_qty_eth, 2)} ETH ·{" "} + {fmt(pos?.option_qty_contracts, 0)} 张 + + + 杠杆 {fmt(pos?.option_leverage, 0)}x + +
+
+
+ 开仓均价 + + {fmtExPx("option", pos?.option_entry_px)} + +
+
+ 买一 + + {fmtBidLiqEx( + "option", + pos?.option_mark_px, + pos?.option_bid_sz, + )} + +
+
+ + {plan?.mode === "LIVE" ? "净盈亏" : "浮盈亏"} + + + {fmt(pos?.option_upl)} + +
+
+ 初始权利金 + + {fmt(pos?.initial_premium)} + +
+
+
+ 开仓时间 + {openTimeLabel} +
+
+ 持仓时长 + + {holdDurationLabel} + +
+
+
+
+ + )} ) : isOo ? ( <>