From 24a860ad3fc57a00765690aea69a0eb249934946 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 7 Aug 2026 17:08:05 +0800 Subject: [PATCH] Split OO budget per leg and size Call/Put independently. Each half of the risk budget buys its own qty from ask; equal qty no longer forced. Co-authored-by: Cursor --- backend/app/live/binance_executor.py | 43 ++++++--- backend/app/live/executor.py | 49 ++++++---- backend/app/sim/matcher.py | 30 +++--- backend/app/strategy/engine.py | 8 +- backend/app/strategy/open_capacity.py | 19 ++-- backend/app/strategy/risk_sizing.py | 110 +++++++++++++--------- backend/tests/test_oo_selection_sizing.py | 23 ++++- docs/期期对冲说明.md | 2 +- frontend/src/api/client.ts | 1 + frontend/src/pages/Plan.tsx | 13 ++- frontend/src/pages/Settings.tsx | 22 +++-- 11 files changed, 210 insertions(+), 110 deletions(-) diff --git a/backend/app/live/binance_executor.py b/backend/app/live/binance_executor.py index 5482ab6..c5d35bd 100644 --- a/backend/app/live/binance_executor.py +++ b/backend/app/live/binance_executor.py @@ -743,18 +743,19 @@ class BinanceLiveExecutor(Matcher): s = live_settings() client = self._client() - opt_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth) + call_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth) + put_qty = self.ledger.get_setting_float("oo_put_qty_eth", call_qty) call_ct = self._ct_mult(call_inst_id) put_ct = self._ct_mult(put_inst_id) - call_contracts = contracts_for_eth(opt_qty, call_ct) - put_contracts = contracts_for_eth(opt_qty, put_ct) + call_contracts = contracts_for_eth(call_qty, call_ct) + put_contracts = contracts_for_eth(put_qty, put_ct) stamp_opening_intent( self.db, group_id=group_id, option_inst_id=call_inst_id, option_side="call", perp_side=f"oo_put:{put_inst_id}", - option_qty_eth=opt_qty, + option_qty_eth=call_qty, option_qty_contracts=float(call_contracts), entry_index_px=entry_index_px, ) @@ -771,8 +772,8 @@ class BinanceLiveExecutor(Matcher): if call_fill.sz and call_fill.sz > 0 else float(call_contracts) ) - opt_qty = eth_from_contracts(call_contracts, call_ct) - put_contracts = contracts_for_eth(opt_qty, put_ct) + call_qty = eth_from_contracts(call_contracts, call_ct) + put_contracts = contracts_for_eth(put_qty, put_ct) try: put_fill = client.place_option_market( symbol=put_inst_id, side="BUY", quantity=put_contracts @@ -798,9 +799,9 @@ class BinanceLiveExecutor(Matcher): ) of_px = float(call_fill.avg_px) pf_px = float(put_fill.avg_px) - qty2 = eth_from_contracts(put_contracts, put_ct) - call_prem = of_px * opt_qty - put_prem = pf_px * qty2 + put_qty = eth_from_contracts(put_contracts, put_ct) + call_prem = of_px * call_qty + put_prem = pf_px * put_qty now = int(time.time() * 1000) with self.db._lock: self.db._conn.execute( @@ -834,8 +835,24 @@ class BinanceLiveExecutor(Matcher): ), ) for leg, inst, contracts, fill_px, fee, ts, q in ( - ("option", call_inst_id, call_contracts, of_px, getattr(call_fill, "fee", 0), now, opt_qty), - ("option2", put_inst_id, put_contracts, pf_px, getattr(put_fill, "fee", 0), now + 1, qty2), + ( + "option", + call_inst_id, + call_contracts, + of_px, + getattr(call_fill, "fee", 0), + now, + call_qty, + ), + ( + "option2", + put_inst_id, + put_contracts, + pf_px, + getattr(put_fill, "fee", 0), + now + 1, + put_qty, + ), ): self.db._conn.execute( """INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts, @@ -870,13 +887,13 @@ class BinanceLiveExecutor(Matcher): ( group_id, call_inst_id, - opt_qty, + call_qty, call_contracts, of_px, entry_index_px, call_prem, put_inst_id, - qty2, + put_qty, put_contracts, pf_px, float(put_strike), diff --git a/backend/app/live/executor.py b/backend/app/live/executor.py index b331b84..f7749b0 100644 --- a/backend/app/live/executor.py +++ b/backend/app/live/executor.py @@ -771,18 +771,19 @@ class OkxLiveExecutor(Matcher): s = live_settings() client = self._client() - opt_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth) + call_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth) + put_qty = self.ledger.get_setting_float("oo_put_qty_eth", call_qty) call_ct = self._ct_mult(call_inst_id) put_ct = self._ct_mult(put_inst_id) - call_contracts = contracts_for_eth(opt_qty, call_ct) - put_contracts = contracts_for_eth(opt_qty, put_ct) + call_contracts = contracts_for_eth(call_qty, call_ct) + put_contracts = contracts_for_eth(put_qty, put_ct) stamp_opening_intent( self.db, group_id=group_id, option_inst_id=call_inst_id, option_side="call", perp_side=f"oo_put:{put_inst_id}", - option_qty_eth=opt_qty, + option_qty_eth=call_qty, option_qty_contracts=float(call_contracts), entry_index_px=entry_index_px, ) @@ -804,8 +805,9 @@ class OkxLiveExecutor(Matcher): if call_fill.sz and call_fill.sz > 0 else float(int(round(call_contracts))) ) - opt_qty = eth_from_contracts(call_contracts, call_ct) - put_contracts = contracts_for_eth(opt_qty, put_ct) + call_qty = eth_from_contracts(call_contracts, call_ct) + # Put 用独立定仓数量,不跟 Call 成交量对齐 + put_contracts = contracts_for_eth(put_qty, put_ct) try: put_fill = client.place_market( inst_id=put_inst_id, @@ -838,10 +840,10 @@ class OkxLiveExecutor(Matcher): ) of_px = float(call_fill.avg_px) pf_px = float(put_fill.avg_px) - call_prem = of_px * opt_qty - put_prem = pf_px * eth_from_contracts(put_contracts, put_ct) - # 等量:以 Call 成交名义为准 - qty2 = eth_from_contracts(put_contracts, put_ct) + call_qty = eth_from_contracts(call_contracts, call_ct) + put_qty = eth_from_contracts(put_contracts, put_ct) + call_prem = of_px * call_qty + put_prem = pf_px * put_qty now = int(time.time() * 1000) with self.db._lock: self.db._conn.execute( @@ -874,11 +876,26 @@ class OkxLiveExecutor(Matcher): put_prem, ), ) - for leg, inst, contracts, fill_px, fee, ts in ( - ("option", call_inst_id, call_contracts, of_px, getattr(call_fill, "fee", 0), now), - ("option2", put_inst_id, put_contracts, pf_px, getattr(put_fill, "fee", 0), now + 1), + for leg, inst, contracts, fill_px, fee, ts, q in ( + ( + "option", + call_inst_id, + call_contracts, + of_px, + getattr(call_fill, "fee", 0), + now, + call_qty, + ), + ( + "option2", + put_inst_id, + put_contracts, + pf_px, + getattr(put_fill, "fee", 0), + now + 1, + put_qty, + ), ): - q = opt_qty if leg == "option" else qty2 self.db._conn.execute( """INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts, base_px, fill_px, fee, slip, notional, ts_ms, exec_mode) @@ -912,13 +929,13 @@ class OkxLiveExecutor(Matcher): ( group_id, call_inst_id, - opt_qty, + call_qty, call_contracts, of_px, entry_index_px, call_prem, put_inst_id, - qty2, + put_qty, put_contracts, pf_px, float(put_strike), diff --git a/backend/app/sim/matcher.py b/backend/app/sim/matcher.py index 384edee..a07c5f8 100644 --- a/backend/app/sim/matcher.py +++ b/backend/app/sim/matcher.py @@ -426,22 +426,23 @@ class Matcher: return OpenResult(ok=False, detail="期期 Call/Put 卖一不可用") fee_rate = self._fee_rate() - opt_qty = self.ledger.get_setting_float("option_qty_eth", 0.1) - if opt_qty < 0.1 - 1e-12: + call_qty = self.ledger.get_setting_float("option_qty_eth", 0.1) + put_qty = self.ledger.get_setting_float("oo_put_qty_eth", call_qty) + if call_qty < 0.1 - 1e-12 or put_qty < 0.1 - 1e-12: return OpenResult(ok=False, detail="期期名义 qty 无效") call_ct = self._ct_mult(call_inst_id) put_ct = self._ct_mult(put_inst_id) - call_contracts = contracts_for_eth(opt_qty, call_ct) - put_contracts = contracts_for_eth(opt_qty, put_ct) + call_contracts = contracts_for_eth(call_qty, call_ct) + put_contracts = contracts_for_eth(put_qty, put_ct) cf = option_fill( action="open", bid=float(getattr(cq, "bid", None) or 0), ask=float(cq.ask), - qty_eth=opt_qty, + qty_eth=call_qty, fee_rate=fee_rate, ) - call_prem = cf.fill_px * opt_qty + call_prem = cf.fill_px * call_qty call_cost = cf.notional + cf.fee try: self.ledger.apply_cash( @@ -460,10 +461,10 @@ class Matcher: action="open", bid=float(getattr(pq2, "bid", None) or 0), ask=ask2, - qty_eth=opt_qty, + qty_eth=put_qty, fee_rate=fee_rate, ) - put_prem = pf.fill_px * opt_qty + put_prem = pf.fill_px * put_qty put_cost = pf.notional + pf.fee try: self.ledger.apply_cash( @@ -479,7 +480,7 @@ class Matcher: action="close", bid=bid, ask=float(cq.ask), - qty_eth=opt_qty, + qty_eth=call_qty, fee_rate=fee_rate, ) self.ledger.apply_cash( @@ -532,7 +533,7 @@ class Matcher: "open", "long", call_inst_id, - opt_qty, + call_qty, call_contracts, cf.base_px, cf.fill_px, @@ -553,7 +554,7 @@ class Matcher: "open", "long", put_inst_id, - opt_qty, + put_qty, put_contracts, pf.base_px, pf.fill_px, @@ -576,7 +577,7 @@ class Matcher: group_id, call_inst_id, "call", - opt_qty, + call_qty, call_contracts, cf.fill_px, entry_index_px, @@ -585,7 +586,7 @@ class Matcher: "option_option", put_inst_id, "put", - opt_qty, + put_qty, put_contracts, pf.fill_px, float(put_strike), @@ -614,7 +615,8 @@ class Matcher: "put_inst_id": put_inst_id, "call_strike": float(call_strike), "put_strike": float(put_strike), - "option_qty_eth": float(opt_qty), + "option_qty_eth": float(call_qty), + "option2_qty_eth": float(put_qty), "initial_premium": total_prem, "fees": cf.fee + pf.fee, "open_sequence": ["call", "put"], diff --git a/backend/app/strategy/engine.py b/backend/app/strategy/engine.py index ef60579..0f3b35b 100644 --- a/backend/app/strategy/engine.py +++ b/backend/app/strategy/engine.py @@ -116,6 +116,7 @@ class StrategyEngine: fixed_perp = "long" perp_qty = self.ledger.get_setting_float("perp_qty_eth", s.perp_qty_eth) opt_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth) + oo_put_qty = self.ledger.get_setting_float("oo_put_qty_eth", opt_qty) sizing_mode = ( sm if ( @@ -161,8 +162,12 @@ class StrategyEngine: if risk_preview.get("ok"): if risk_preview.get("perp_qty_eth") is not None: perp_qty = float(risk_preview["perp_qty_eth"]) - if risk_preview.get("option_qty_eth") is not None: + if risk_preview.get("call_qty_eth") is not None: + opt_qty = float(risk_preview["call_qty_eth"]) + elif risk_preview.get("option_qty_eth") is not None: opt_qty = float(risk_preview["option_qty_eth"]) + if risk_preview.get("put_qty_eth") is not None: + oo_put_qty = float(risk_preview["put_qty_eth"]) if risk_preview.get("net_profit_target") is not None: net_target = float(risk_preview["net_profit_target"]) exit_amt = net_target @@ -243,6 +248,7 @@ class StrategyEngine: "perp_margin_mode": perp_mm, "perp_qty_eth": perp_qty, "option_qty_eth": opt_qty, + "oo_put_qty_eth": oo_put_qty, "sizing_mode": sizing_mode, "risk_based": sizing_mode == "risk_based", "hedge_mode": ( diff --git a/backend/app/strategy/open_capacity.py b/backend/app/strategy/open_capacity.py index 54e3b6f..dea111a 100644 --- a/backend/app/strategy/open_capacity.py +++ b/backend/app/strategy/open_capacity.py @@ -152,16 +152,17 @@ def assess_open_capacity( ca = float(snap.call.ask) if pa is None and snap.put and snap.put.ask: pa = float(snap.put.ask) - except Exception: - pass - cush = float( - ledger.get_setting_float("oo_budget_cushion", s.oo_budget_cushion) - or s.oo_budget_cushion - ) - cush = min(1.0, max(0.5, cush)) + except Exception: + pass if ca is not None and pa is not None and ca > 0 and pa > 0: - # 与定仓一致:按预留后的权利金需求估资金门 - premium_need = (ca + pa) * opt_qty * (1.0 + fee_rate) * cush + call_q = float(opt_qty) + put_q = float( + ledger.get_setting_float("oo_put_qty_eth", call_q) or call_q + ) + # 与定仓一致:两腿各自权利金 + premium_need = ( + (ca * call_q + pa * put_q) * (1.0 + fee_rate) + ) else: premium_need = None margin_need = 0.0 diff --git a/backend/app/strategy/risk_sizing.py b/backend/app/strategy/risk_sizing.py index c3c4e5e..442e7cc 100644 --- a/backend/app/strategy/risk_sizing.py +++ b/backend/app/strategy/risk_sizing.py @@ -507,7 +507,9 @@ class OoSizingResult: detail: str budget: float | None = None spend: float | None = None - qty_eth: float | None = None + qty_eth: float | None = None # 兼容:Call 数量 + call_qty_eth: float | None = None + put_qty_eth: float | None = None call_ask: float | None = None put_ask: float | None = None call_premium: float | None = None @@ -517,6 +519,7 @@ class OoSizingResult: capital_base: float | None = None cushion: float | None = None reward_ratio: float | None = None + leg_budget: float | None = None # 单腿权利金预算(B/2×cushion) def compute_oo_sizing( @@ -530,7 +533,8 @@ def compute_oo_sizing( reward_ratio: float = 2.0, ) -> OoSizingResult: """ - 期期 1:1:预算 B 预留后平分两腿权利金;qty 一位小数向下取整; + 期期:总预算 B 平分给 Call/Put(各约 B/2,再乘 cushion 预留); + 两腿按各自卖一独立定仓 qty=floor_1dp(腿预算/ask),数量可以不同; 出场目标 = B × reward_ratio(按全额预算)。 """ if budget is None or budget <= 0 or not math.isfinite(budget): @@ -539,53 +543,56 @@ def compute_oo_sizing( return OoSizingResult(ok=False, detail="期期缺少有效卖一") cush = min(1.0, max(0.5, float(cushion))) ratio = max(0.5, float(reward_ratio)) - spend = float(budget) * cush - # 粗估两腿开仓费(按指数名义近似) - fee_est = 0.0 + # 各腿:总预算一半 × 预留 + leg_raw = float(budget) / 2.0 + leg_budget = leg_raw * cush + # 单腿开仓费粗估(从该腿预算里扣) + fee_one = 0.0 if index_px and index_px > 0 and fee_rate > 0: - fee_est = float(index_px) * float(fee_rate) * 2.0 - spend_prem = max(0.0, spend - fee_est) - if spend_prem <= 1e-9: - return OoSizingResult(ok=False, detail="期期预留后可用权利金不足") - leg = spend_prem / 2.0 - # 等量:受较贵腿限制 - q_call = floor_k_1dp(leg / float(call_ask)) - q_put = floor_k_1dp(leg / float(put_ask)) - qty = min(q_call, q_put) - if qty < 0.1 - 1e-12: + fee_one = float(index_px) * float(fee_rate) + leg_spend = max(0.0, leg_budget - fee_one) + if leg_spend <= 1e-9: + return OoSizingResult(ok=False, detail="期期单腿预留后可用权利金不足") + + def _leg_qty(ask: float) -> tuple[float, float]: + q = floor_k_1dp(leg_spend / float(ask)) + while q >= 0.1 - 1e-12: + prem = float(ask) * q + if prem <= leg_spend + 1e-6: + return round(q, 1), prem + q = round(q - 0.1, 1) + return 0.0, 0.0 + + q_call, cp = _leg_qty(float(call_ask)) + q_put, pp = _leg_qty(float(put_ask)) + if q_call < 0.1 - 1e-12 or q_put < 0.1 - 1e-12: return OoSizingResult( ok=False, detail=( - f"期期定仓 qty<{0.1}(call可{q_call} put可{q_put})," - f"预算 {budget:.2f}U 不足" + f"期期定仓失败:Call可{q_call} Put可{q_put}(各腿预算约" + f"{leg_budget:.2f}U),总预算 {budget:.2f}U 不足" ), budget=_round2(float(budget)), + leg_budget=_round2(leg_budget), ) - # 若仍略超 spend_prem,再降一档 - while qty >= 0.1 - 1e-12: - cp = float(call_ask) * qty - pp = float(put_ask) * qty - if cp + pp <= spend_prem + 1e-6: - return OoSizingResult( - ok=True, - detail="ok", - budget=_round2(float(budget)), - spend=_round2(spend), - qty_eth=round(qty, 1), - call_ask=_round2(float(call_ask)), - put_ask=_round2(float(put_ask)), - call_premium=_round2(cp), - put_premium=_round2(pp), - max_loss=_round2(cp + pp + fee_est), - net_profit_target=_round2(float(budget) * ratio), - cushion=cush, - reward_ratio=ratio, - ) - qty = round(qty - 0.1, 1) + spend = leg_budget * 2.0 return OoSizingResult( - ok=False, - detail=f"期期无法在预算 {budget:.2f}U 内找到合规 qty", + ok=True, + detail="ok", budget=_round2(float(budget)), + spend=_round2(spend), + qty_eth=round(q_call, 1), + call_qty_eth=round(q_call, 1), + put_qty_eth=round(q_put, 1), + call_ask=_round2(float(call_ask)), + put_ask=_round2(float(put_ask)), + call_premium=_round2(cp), + put_premium=_round2(pp), + max_loss=_round2(cp + pp + fee_one * 2.0), + net_profit_target=_round2(float(budget) * ratio), + cushion=cush, + reward_ratio=ratio, + leg_budget=_round2(leg_budget), ) @@ -624,24 +631,29 @@ def apply_oo_sizing_to_ledger( ) if not r.ok: return r + call_q = float(r.call_qty_eth or r.qty_eth or 0) + put_q = float(r.put_qty_eth or r.qty_eth or 0) database.set_setting("exit_mode", "fixed_usdt") database.set_setting("perp_qty_eth", "0") - database.set_setting("option_qty_eth", str(r.qty_eth)) + database.set_setting("option_qty_eth", str(call_q)) + database.set_setting("oo_put_qty_eth", str(put_q)) database.set_setting("net_profit_target", str(r.net_profit_target)) - database.set_setting("risk_last_k", str(r.qty_eth)) + database.set_setting("risk_last_k", str(call_q)) database.set_setting( "risk_last_max_loss", f"{r.max_loss:.2f}" if r.max_loss is not None else "", ) logger.info( - "oo_sizing applied qty=%.1f call_ask=%.4f put_ask=%.4f exit=%.2f " - "max_loss=%.2f budget=%.2f", - r.qty_eth or 0, + "oo_sizing applied call_qty=%.1f put_qty=%.1f call_ask=%.4f put_ask=%.4f " + "exit=%.2f max_loss=%.2f budget=%.2f leg=%.2f", + call_q, + put_q, r.call_ask or 0, r.put_ask or 0, r.net_profit_target or 0, r.max_loss or 0, r.budget or 0, + r.leg_budget or 0, ) # attach capital for callers return OoSizingResult( @@ -649,7 +661,9 @@ def apply_oo_sizing_to_ledger( detail=r.detail, budget=r.budget, spend=r.spend, - qty_eth=r.qty_eth, + qty_eth=call_q, + call_qty_eth=call_q, + put_qty_eth=put_q, call_ask=r.call_ask, put_ask=r.put_ask, call_premium=r.call_premium, @@ -659,6 +673,7 @@ def apply_oo_sizing_to_ledger( capital_base=_round2(capital) if capital is not None else None, cushion=r.cushion, reward_ratio=r.reward_ratio, + leg_budget=r.leg_budget, ) @@ -862,6 +877,9 @@ def _preview_oo_sizing( "sizing_ok": bool(r.ok), "detail": "ok" if r.ok else str(r.detail or "期期数量未估出"), "option_qty_eth": r.qty_eth if r.ok else None, + "call_qty_eth": r.call_qty_eth if r.ok else None, + "put_qty_eth": r.put_qty_eth if r.ok else None, + "leg_budget": r.leg_budget if r.ok else None, "call_ask": r.call_ask, "put_ask": r.put_ask, "call_premium": r.call_premium if r.ok else None, diff --git a/backend/tests/test_oo_selection_sizing.py b/backend/tests/test_oo_selection_sizing.py index 4e8a056..50ecf90 100644 --- a/backend/tests/test_oo_selection_sizing.py +++ b/backend/tests/test_oo_selection_sizing.py @@ -46,6 +46,25 @@ def test_select_oo_pair_same_expiry(tmp_path=None) -> None: def test_compute_oo_sizing_1_1_and_reward() -> None: + r = compute_oo_sizing( + budget=100.0, + call_ask=5.0, + put_ask=10.0, # Put 更贵 → 数量更少 + fee_rate=0.0, + index_px=2000.0, + cushion=1.0, # 测纯平分,不预留 + reward_ratio=2.0, + ) + assert r.ok + # 各 50U:Call 50/5=10,Put 50/10=5 + assert r.call_qty_eth == 10.0 + assert r.put_qty_eth == 5.0 + assert abs(float(r.net_profit_target or 0) - 200.0) < 1e-9 + assert abs(float(r.call_premium or 0) - 50.0) < 1e-6 + assert abs(float(r.put_premium or 0) - 50.0) < 1e-6 + + +def test_compute_oo_sizing_equal_asks() -> None: r = compute_oo_sizing( budget=100.0, call_ask=5.0, @@ -56,7 +75,9 @@ def test_compute_oo_sizing_1_1_and_reward() -> None: reward_ratio=2.0, ) assert r.ok - assert r.qty_eth == 9.2 + # 各腿 46U / 5 = 9.2 + assert r.call_qty_eth == 9.2 + assert r.put_qty_eth == 9.2 assert abs(float(r.net_profit_target or 0) - 200.0) < 1e-9 assert float(r.call_premium or 0) + float(r.put_premium or 0) <= 92.0 + 1e-6 diff --git a/docs/期期对冲说明.md b/docs/期期对冲说明.md index 6b65db9..b525c56 100644 --- a/docs/期期对冲说明.md +++ b/docs/期期对冲说明.md @@ -23,7 +23,7 @@ ## 定仓 - 仅支持以损定仓 + 亏损幅度 %(含倍投)。 -- 预算 B 预留余地后两腿 **1:1** 平分权利金;数量 **一位小数向下取整**。 +- 预算 B 平分给 Call/Put(各约 B/2,再乘 cushion);两腿按各自卖一 **独立** 定仓(数量可不同)。 - 出场目标按全额 B × 盈亏比(例 B=100、比=2 → 目标 200U)。 ## 平仓 diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index f12fe12..cb0ad2c 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -367,6 +367,7 @@ export type PlanState = { oo_min_option_hours?: number; oo_min_leverage?: number; oo_reward_ratio?: number; + oo_put_qty_eth?: number; sizing_mode?: "manual" | "risk_based"; risk_based?: boolean; ledger: { equity: number; available: number; reserved: number }; diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index 42b0067..f733110 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -223,7 +223,7 @@ export default function PlanPage() { : `待估算(基数${fmt(plan?.risk_exit_unit ?? 15)})` : `固定 ${fmt(plan?.net_profit_target ?? 15)} U`; const riskRatioLabel = isOo - ? "Call:Put 1:1" + ? "预算平分 Call/Put" : `比例${Number(plan?.risk_perp_unit ?? 1)}:${Number(plan?.risk_option_unit ?? 2)}`; const sizingModeLabel = riskBased ? [ @@ -268,7 +268,16 @@ export default function PlanPage() { "虚值Call@高·Put@低", `剩余≥${fmt(plan?.oo_min_option_hours ?? 24, 0)}h`, `杠杆≥${fmt(plan?.oo_min_leverage ?? 200, 0)}x`, - displayOptQty != null ? `单腿${fmt(displayOptQty, 1)}ETH` : null, + displayOptQty != null + ? `Call${fmt(displayOptQty, 1)}/Put${fmt( + Number( + plan?.risk_sizing_preview?.put_qty_eth ?? + plan?.oo_put_qty_eth ?? + displayOptQty, + ), + 1, + )}ETH` + : null, ] .filter(Boolean) .join(" · "); diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 9e72cf2..2c61a1c 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -756,7 +756,7 @@ export default function SettingsPage() { />

目标盈利 = 以损预算 × 盈亏比(例预算 100U、比 2 → 目标 - 200U)。两腿 1:1 平分预算。振幅/选约参数见「选约」页。 + 200U)。预算平分给 Call/Put,各按卖一独立定仓。

) : null} @@ -785,7 +785,7 @@ export default function SettingsPage() {
- 以损定仓(期期固定 · Call/Put 1:1) + 以损定仓(期期固定 · 预算平分,按各自卖一定仓)
)} @@ -1048,11 +1048,19 @@ export default function SettingsPage() { ? ` · 倍投待命(连亏${Number(mg.loss_days) || 0}天)` : ""; return isOo - ? `预算=${budS}U · 估亏=${mxS}U · 出场=预算×${ooRewardRatio}${ - riskPreview.option_qty_eth != null - ? ` · 单腿≈${riskPreview.option_qty_eth}ETH` - : "" - }${mgS}` + ? `预算=${budS}U · 各腿≈${ + riskPreview.leg_budget != null + ? Number(riskPreview.leg_budget).toFixed(2) + : budS !== "—" + ? (Number(budS) / 2).toFixed(2) + : "—" + }U · Call≈${ + riskPreview.call_qty_eth ?? + riskPreview.option_qty_eth ?? + "—" + } · Put≈${ + riskPreview.put_qty_eth ?? "—" + } · 出场=预算×${ooRewardRatio}${mgS}` : `k=${pk ?? "—"} · 预算=${budS}U · 估亏=${mxS}U · 永续=${perp ?? "—"} · 期权=${opt ?? "—"} · 出场=${exit ?? "—"}${basisS}${mgS}`; })()}