From 67f22626e9418d62f4dcc3cc653bffe3c4e5f4df Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 30 Jul 2026 14:58:30 +0800 Subject: [PATCH] Show risk params box on control cards and modal. Co-authored-by: Cursor --- backend/app/api/fleet.py | 6 ++ backend/app/strategy/engine.py | 2 + control/frontend/src/pages/Monitor.tsx | 107 +++++++++++++++++++++---- control/frontend/src/styles.css | 24 ++++++ 4 files changed, 125 insertions(+), 14 deletions(-) diff --git a/backend/app/api/fleet.py b/backend/app/api/fleet.py index 1ca26c3..feab568 100644 --- a/backend/app/api/fleet.py +++ b/backend/app/api/fleet.py @@ -223,13 +223,19 @@ async def fleet_status(_tok: Annotated[str, Depends(require_fleet_token)]) -> di "exit_mode": st.get("exit_mode"), "exit_target_usdt": st.get("exit_target_usdt"), "net_profit_target": st.get("net_profit_target"), + "premium_exit_multiple": st.get("premium_exit_multiple"), "leverage": st.get("leverage"), + "min_option_leverage": st.get("min_option_leverage"), "perp_margin_mode": st.get("perp_margin_mode"), "perp_qty_eth": st.get("perp_qty_eth"), "option_qty_eth": st.get("option_qty_eth"), "sizing_mode": st.get("sizing_mode"), "risk_last_k": st.get("risk_last_k"), "risk_sizing_locked": st.get("risk_sizing_locked"), + "risk_loss_pct": st.get("risk_loss_pct"), + "risk_perp_unit": st.get("risk_perp_unit"), + "risk_option_unit": st.get("risk_option_unit"), + "risk_exit_unit": st.get("risk_exit_unit"), }, "position": { "status": pos.get("status") or ("open" if pos.get("has_position") else "flat"), diff --git a/backend/app/strategy/engine.py b/backend/app/strategy/engine.py index e711fc9..5efe660 100644 --- a/backend/app/strategy/engine.py +++ b/backend/app/strategy/engine.py @@ -127,6 +127,7 @@ class StrategyEngine: risk_perp_unit = self.ledger.get_setting_float("risk_perp_unit", 1.0) 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_loss_pct = self.ledger.get_setting_float("risk_loss_pct", 1.0) risk_last_k = self.ledger.get_setting_float("risk_last_k", 0.0) risk_preview: dict[str, Any] | None = None pos_status = str(upl.get("status") or "flat") @@ -225,6 +226,7 @@ class StrategyEngine: "risk_perp_unit": risk_perp_unit, "risk_option_unit": risk_option_unit, "risk_exit_unit": risk_exit_unit, + "risk_loss_pct": risk_loss_pct, "risk_last_k": risk_last_k if risk_last_k > 0 else None, "risk_sizing_preview": risk_preview, "risk_sizing_locked": bool(trade_locked and sizing_mode == "risk_based"), diff --git a/control/frontend/src/pages/Monitor.tsx b/control/frontend/src/pages/Monitor.tsx index 647cd67..3421365 100644 --- a/control/frontend/src/pages/Monitor.tsx +++ b/control/frontend/src/pages/Monitor.tsx @@ -50,6 +50,95 @@ function hasOpenPosition(position: Record): boolean { return OPEN_STATUSES.has(st); } +function unitLabel(v: unknown): string { + const n = Number(v); + if (!Number.isFinite(n)) return "—"; + if (Number.isInteger(n)) return String(n); + return n.toFixed(2).replace(/\.?0+$/, ""); +} + +function leveragePair(strat: Record): string { + const lev = Number(strat.leverage); + const opt = Number(strat.min_option_leverage); + const a = Number.isFinite(lev) ? `${Math.round(lev)}x` : "—"; + const b = Number.isFinite(opt) ? `${Math.round(opt)}x` : "—"; + return `${a}/${b}`; +} + +type RiskLines = { + riskBased: boolean; + sizing: string; + lossPct: string | null; + exit: string; + openRatio: string | null; + leverage: string; +}; + +function riskLines(strat: Record): RiskLines { + const riskBased = + strat.sizing_mode === "risk_based" || strat.risk_based === true; + const exitMode = String(strat.exit_mode || "fixed_usdt"); + let exit: string; + if (riskBased) { + exit = `基数${unitLabel(strat.risk_exit_unit ?? 15)}`; + } else if (exitMode === "premium_multiple") { + exit = `权利金×${fmt(strat.premium_exit_multiple ?? 1, 2)}`; + } else { + exit = `固定 ${fmt(strat.net_profit_target ?? strat.exit_target_usdt, 2)}U`; + } + const lossN = Number(strat.risk_loss_pct); + const lossPct = + riskBased && Number.isFinite(lossN) + ? `${fmt(lossN, lossN % 1 === 0 ? 0 : 2)}%` + : null; + const openRatio = riskBased + ? `${unitLabel(strat.risk_perp_unit ?? 1)}:${unitLabel(strat.risk_option_unit ?? 2)}` + : null; + return { + riskBased, + sizing: riskBased ? "以损定仓" : "手动仓位", + lossPct, + exit, + openRatio, + leverage: leveragePair(strat), + }; +} + +function RiskParamsBox({ strat }: { strat: Record }) { + const r = riskLines(strat); + return ( +
+
风控参数
+
+
+
定仓
+
{r.sizing}
+
+ {r.lossPct ? ( +
+
风险比例
+
{r.lossPct}
+
+ ) : null} +
+
出场
+
{r.exit}
+
+ {r.openRatio ? ( +
+
开仓比例
+
{r.openRatio}
+
+ ) : null} +
+
杠杆
+
{r.leverage}
+
+
+
+ ); +} + /** 页面级快照:整页刷新前保留上次卡片,避免空白等待 */ let cachedNodes: NodeCard[] = []; let cachedSseSec = 1; @@ -374,6 +463,7 @@ export default function MonitorPage() { + {n.fleet_error ?
{n.fleet_error}
: null} {n.error ?
{n.error}
: null}
e.stopPropagation()}> @@ -447,6 +537,7 @@ export default function MonitorPage() {

策略详情

+
状态
@@ -471,11 +562,8 @@ export default function MonitorPage() {
-
杠杆 / 保证金
-
- {fmt(detail.strat.leverage, 1)}x /{" "} - {String(detail.strat.perp_margin_mode || "—")} -
+
保证金
+
{String(detail.strat.perp_margin_mode || "—")}
名义 永续/期权
@@ -488,15 +576,6 @@ export default function MonitorPage() {
出场目标
{fmt(detail.strat.exit_target_usdt, 2)} USDT
-
-
定仓
-
- {String(detail.strat.sizing_mode || "—")} - {detail.strat.risk_last_k != null - ? ` · k=${fmt(detail.strat.risk_last_k, 2)}` - : ""} -
-
指数
{fmt(detail.index_px, 2)}
diff --git a/control/frontend/src/styles.css b/control/frontend/src/styles.css index 7d56a1c..5dc82b1 100644 --- a/control/frontend/src/styles.css +++ b/control/frontend/src/styles.css @@ -290,6 +290,30 @@ input { font-size: 0.92rem; } +.risk-box { + border: 1px solid rgba(46, 229, 154, 0.45); + background: rgba(46, 229, 154, 0.08); + border-radius: 8px; + padding: 10px 12px; +} + +.risk-box-title { + font-size: 0.78rem; + color: var(--pnl-pos); + font-weight: 700; + margin-bottom: 6px; + letter-spacing: 0.02em; +} + +.risk-kv { + margin: 0; +} + +.risk-kv dd { + color: var(--pnl-pos); + font-weight: 700; +} + .node-actions { display: flex; flex-wrap: wrap;