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() {