From 5eb169a6360c6a265d21e80b620323a44cf53d6b Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 29 Jul 2026 23:52:43 +0800 Subject: [PATCH] Show live risk-sizing estimate on plan page instead of stale manual qty. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When 以损定仓 is on, state() overlays next-open k/qty/exit from market preview. Co-authored-by: Cursor --- backend/app/strategy/engine.py | 27 +++++++++++++++++++ frontend/src/pages/Plan.tsx | 49 +++++++++++++++++++++++++++++----- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/backend/app/strategy/engine.py b/backend/app/strategy/engine.py index 46fcaa4..780feb8 100644 --- a/backend/app/strategy/engine.py +++ b/backend/app/strategy/engine.py @@ -128,6 +128,32 @@ class StrategyEngine: risk_option_unit = self.ledger.get_setting_float("risk_option_unit", 2.0) risk_exit_unit = self.ledger.get_setting_float("risk_exit_unit", 15.0) risk_last_k = self.ledger.get_setting_float("risk_last_k", 0.0) + risk_preview: dict[str, Any] | None = None + # 以损定仓:监控页展示「下一次开仓」实时估算,避免仍显示上次手填/过期名义 + if sizing_mode == "risk_based": + try: + from .risk_sizing import preview_risk_sizing + + risk_preview = preview_risk_sizing(self.db) + 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: + opt_qty = float(risk_preview["option_qty_eth"]) + if risk_preview.get("net_profit_target") is not None: + net_target = float(risk_preview["net_profit_target"]) + exit_amt = net_target + if risk_preview.get("k") is not None: + risk_last_k = float(risk_preview["k"]) + if risk_preview.get("perp_unit") is not None: + risk_perp_unit = float(risk_preview["perp_unit"]) + if risk_preview.get("option_unit") is not None: + risk_option_unit = float(risk_preview["option_unit"]) + if risk_preview.get("exit_unit") is not None: + risk_exit_unit = float(risk_preview["exit_unit"]) + except Exception: + logger.exception("risk sizing preview for state() failed") + risk_preview = {"ok": False, "detail": "以损定仓预览失败"} rest_until = row["rest_until_ms"] rest_left = 0 if rest_until: @@ -172,6 +198,7 @@ class StrategyEngine: "risk_option_unit": risk_option_unit, "risk_exit_unit": risk_exit_unit, "risk_last_k": risk_last_k if risk_last_k > 0 else None, + "risk_sizing_preview": risk_preview, "min_option_hours": min_hours, "min_option_leverage": min_opt_lev, "atm_open_offset_enabled": atm_off_on, diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index d9e8090..028e117 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -145,14 +145,23 @@ export default function PlanPage() { const pos = plan?.position; const open = !!pos?.has_position; const exitMode = plan?.exit_mode ?? "fixed_usdt"; - const exitTarget = plan?.exit_target_usdt ?? plan?.net_profit_target ?? 15; + const riskBased = + plan?.risk_based === true || plan?.sizing_mode === "risk_based"; + const riskLiveOk = !riskBased || plan?.risk_sizing_preview?.ok === true; + const displayPerpQty = riskLiveOk ? (plan?.perp_qty_eth ?? 1) : null; + const displayOptQty = riskLiveOk ? (plan?.option_qty_eth ?? 2) : null; + const exitTarget = riskLiveOk + ? (plan?.exit_target_usdt ?? plan?.net_profit_target ?? 15) + : null; const netPnl = pos?.net_pnl ?? 0; const movePct = pos?.move_pct ?? 0; const exitRuleLabel = exitMode === "premium_multiple" ? `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}` : plan?.risk_based || plan?.sizing_mode === "risk_based" - ? `以损定仓 · 固定 ${fmt(exitTarget)} U(基数${fmt(plan?.risk_exit_unit ?? 15)})` + ? exitTarget != null + ? `以损定仓 · 固定 ${fmt(exitTarget)} U(基数${fmt(plan?.risk_exit_unit ?? 15)})` + : `以损定仓 · 待估算(基数${fmt(plan?.risk_exit_unit ?? 15)})` : `固定 ${fmt(plan?.net_profit_target ?? 15)} U`; const phaseLabel = PHASE_ZH[plan?.phase || ""] || plan?.phase || "—"; const atmRule = plan?.fixed_direction_enabled @@ -206,7 +215,9 @@ export default function PlanPage() { {open ? fmt(netPnl) : "—"} - 目标 {fmt(exitTarget)} U + + 目标 {exitTarget != null ? `${fmt(exitTarget)} U` : "—"} +
到期 @@ -237,7 +248,11 @@ export default function PlanPage() {

开仓 - 永续 {fmt(plan?.perp_qty_eth ?? 1, 2)} ETH / {fmt(plan?.leverage, 0)}x + 永续{" "} + {displayPerpQty != null + ? `${fmt(displayPerpQty, 2)} ETH` + : "—"}{" "} + / {fmt(plan?.leverage, 0)}x · 期权 {fmt(plan?.option_qty_eth ?? 2, 2)} ETH 名义 · 剩余≥ {fmt(plan?.min_option_hours, 0)}h · 期权杠杆≥ {fmt(plan?.min_option_leverage, 0)}x · {atmRule} @@ -354,11 +369,27 @@ export default function PlanPage() { {plan?.risk_last_k ? ` · k=${fmt(plan.risk_last_k, 1)}` : ""} + {plan?.risk_sizing_preview?.budget != null + ? ` · 预算${fmt(plan.risk_sizing_preview.budget, 2)}U` + : ""} + {plan?.risk_sizing_preview?.max_loss != null + ? ` · 估亏${fmt(plan.risk_sizing_preview.max_loss, 2)}U` + : ""} + {plan?.risk_sizing_preview?.ok === false + ? ` · ${String(plan.risk_sizing_preview.detail || "预览失败")}` + : ""} {" · "} ) : null} - 永续{fmt(plan?.perp_qty_eth ?? 1, 2)}ETH/期权 - {fmt(plan?.option_qty_eth ?? 2, 2)}ETH · 剩余≥ + 永续 + {displayPerpQty != null + ? `${fmt(displayPerpQty, 2)}ETH` + : "—"} + /期权 + {displayOptQty != null + ? `${fmt(displayOptQty, 2)}ETH` + : "—"}{" "} + · 剩余≥ {fmt(plan?.min_option_hours, 0)}h · 期权杠杆≥ {fmt(plan?.min_option_leverage, 0)}x {plan?.fixed_direction_enabled @@ -451,7 +482,11 @@ export default function PlanPage() { {open ? fmt(netPnl) : "—"} {" / "} - {open || exitMode === "fixed_usdt" ? fmt(exitTarget) : "—"} + {open || exitMode === "fixed_usdt" + ? exitTarget != null + ? fmt(exitTarget) + : "—" + : "—"}