diff --git a/backend/app/api/fleet.py b/backend/app/api/fleet.py index a753667..d37e087 100644 --- a/backend/app/api/fleet.py +++ b/backend/app/api/fleet.py @@ -300,9 +300,15 @@ async def fleet_status(_tok: Annotated[str, Depends(require_fleet_token)]) -> di "perp_margin_mode": st.get("perp_margin_mode"), "perp_qty_eth": st.get("perp_qty_eth"), "option_qty_eth": st.get("option_qty_eth"), + "oo_put_qty_eth": ( + st.get("oo_put_qty_eth") + if st.get("oo_put_qty_eth") is not None + else _sf("oo_put_qty_eth", 0.0) or None + ), "sizing_mode": st.get("sizing_mode"), "risk_last_k": st.get("risk_last_k"), "risk_sizing_locked": st.get("risk_sizing_locked"), + "risk_sizing_preview": st.get("risk_sizing_preview"), "risk_loss_pct": _pick("risk_loss_pct", 1.0), "risk_perp_unit": _pick("risk_perp_unit", 1.0), "risk_option_unit": _pick("risk_option_unit", 2.0), @@ -310,6 +316,21 @@ async def fleet_status(_tok: Annotated[str, Depends(require_fleet_token)]) -> di "martingale_enabled": st.get("martingale_enabled"), "martingale_doubles": st.get("martingale_doubles"), "risk_effective_loss_pct": st.get("risk_effective_loss_pct"), + "oo_amplitude_pct": _pick( + "oo_amplitude_pct", float(settings.oo_amplitude_pct) + ), + "oo_amplitude_hours": _pick( + "oo_amplitude_hours", float(settings.oo_amplitude_hours) + ), + "oo_min_option_hours": _pick( + "oo_min_option_hours", float(settings.oo_min_option_hours) + ), + "oo_min_leverage": _pick( + "oo_min_leverage", float(settings.oo_min_leverage) + ), + "oo_reward_ratio": _pick( + "oo_reward_ratio", float(settings.oo_reward_ratio) + ), "hedge_mode": ( hm if ( diff --git a/control/frontend/src/pages/Monitor.tsx b/control/frontend/src/pages/Monitor.tsx index 00bc09e..c7af323 100644 --- a/control/frontend/src/pages/Monitor.tsx +++ b/control/frontend/src/pages/Monitor.tsx @@ -175,24 +175,93 @@ function closeReasonZh(v: string): string { type RiskLines = { riskBased: boolean; + isOo: boolean; sizing: string; sizingShort: string; lossPct: string | null; exit: string; openRatio: string | null; - /** 表格「风险/开仓」列:如 5%/1:2/12;手动为 —/名义比/时长 */ + /** 表格「风险/开仓」列:永期 5%/1:2/12;期期 ≤2%·12h/C:P/≥24 */ riskOrOpen: string; + riskOrOpenTitle: string; minHours: string; leverage: string; + leverageTitle: string; }; +function isOptionOption(strat: Record): boolean { + return ( + String(strat.hedge_mode || "") + .trim() + .toLowerCase() === "option_option" + ); +} + function riskLines(strat: Record): RiskLines { + const isOo = isOptionOption(strat); const riskBased = strat.sizing_mode === "risk_based" || strat.risk_based === true; + + if (isOo) { + const ampN = Number(strat.oo_amplitude_pct); + const ampH = Number(strat.oo_amplitude_hours); + const minH = Number( + strat.oo_min_option_hours ?? strat.min_option_hours, + ); + const amp = + Number.isFinite(ampN) && Number.isFinite(ampH) + ? `≤${fmt(ampN, ampN % 1 === 0 ? 0 : 1)}%·${fmt(ampH, 0)}h` + : "≤—"; + const callQ = + strat.risk_sizing_preview && + typeof strat.risk_sizing_preview === "object" && + (strat.risk_sizing_preview as Record).call_qty_eth != + null + ? (strat.risk_sizing_preview as Record).call_qty_eth + : strat.option_qty_eth; + const putQ = + strat.risk_sizing_preview && + typeof strat.risk_sizing_preview === "object" && + (strat.risk_sizing_preview as Record).put_qty_eth != null + ? (strat.risk_sizing_preview as Record).put_qty_eth + : strat.oo_put_qty_eth ?? strat.option_qty_eth; + const openRatio = `C${unitLabel(callQ)}:P${unitLabel(putQ)}`; + const minHours = Number.isFinite(minH) ? `≥${unitLabel(minH)}` : "≥—"; + const ratioN = Number(strat.oo_reward_ratio); + const target = strat.exit_target_usdt ?? strat.net_profit_target; + let exit: string; + if (target != null && Number.isFinite(Number(target))) { + exit = `目标${fmt(target, 2)}U`; + } else if (Number.isFinite(ratioN)) { + exit = `预算×${fmt(ratioN, ratioN % 1 === 0 ? 0 : 2)}`; + } else { + exit = "预算×—"; + } + const ooLev = Number(strat.oo_min_leverage); + const leverage = Number.isFinite(ooLev) + ? `≥${Math.round(ooLev)}x` + : "≥—x"; + return { + riskBased, + isOo: true, + sizing: riskBased ? "以损定仓(A)" : "手动开仓(B)", + sizingShort: riskBased ? "A" : "B", + lossPct: Number.isFinite(ampN) + ? `≤${fmt(ampN, ampN % 1 === 0 ? 0 : 1)}%` + : null, + exit, + openRatio, + riskOrOpen: `${amp}/${openRatio}/${minHours}`, + riskOrOpenTitle: "振幅上限·回看 / Call:Put 数量 / 最短剩余(h)", + minHours, + leverage, + leverageTitle: "期期单腿最低杠杆", + }; + } + const exitMode = String(strat.exit_mode || "fixed_usdt"); let exit: string; if (riskBased) { - // 必须读策略机 risk_exit_unit,禁止写死 15 exit = strat.risk_exit_unit != null && Number.isFinite(Number(strat.risk_exit_unit)) ? `基数${unitLabel(strat.risk_exit_unit)}` @@ -224,14 +293,17 @@ function riskLines(strat: Record): RiskLines { const riskOrOpen = `${lossPct || "—"}/${openRatio}/${minHours}`; return { riskBased, + isOo: false, sizing: riskBased ? "以损定仓(A)" : "手动开仓(B)", sizingShort: riskBased ? "A" : "B", lossPct, exit, openRatio, riskOrOpen, + riskOrOpenTitle: "风险比例 / 开仓比例 / 最小剩余时长(h)", minHours, leverage: leveragePair(strat), + leverageTitle: "永续杠杆 / 期权最低杠杆", }; } @@ -316,25 +388,44 @@ function RiskParamsBox({ strat }: { strat: Record }) {
定仓
{r.sizing}
- {r.lossPct != null ? ( -
-
风险比例
-
{r.lossPct}
-
- ) : null} + {r.isOo ? ( + <> +
+
振幅
+
{r.lossPct || "—"}
+
+
+
Call:Put
+
{r.openRatio || "—"}
+
+
+
剩余
+
{r.minHours}
+
+ + ) : ( + <> + {r.lossPct != null ? ( +
+
风险比例
+
{r.lossPct}
+
+ ) : null} + {r.openRatio ? ( +
+
开仓比例
+
{r.openRatio}
+
+ ) : null} + + )}
出场
{r.exit}
- {r.openRatio ? ( -
-
开仓比例
-
{r.openRatio}
-
- ) : null}
-
杠杆
-
{r.leverage}
+
{r.isOo ? "期权杠杆" : "杠杆"}
+
{r.leverage}
@@ -832,12 +923,14 @@ export default function MonitorPage() { {s.rounds ?? "—"} {risk.riskOrOpen} {risk.exit} - {risk.leverage} + + {risk.leverage} + e.stopPropagation()} @@ -996,16 +1089,20 @@ export default function MonitorPage() { 定仓 - {r.lossPct != null ? 风险比例 : null} + {r.lossPct != null ? ( + {r.isOo ? "振幅" : "风险比例"} + ) : null} 出场 - {r.openRatio ? 开仓比例 : null} - 杠杆 + {r.openRatio ? ( + {r.isOo ? "Call:Put" : "开仓比例"} + ) : null} + {r.isOo ? "期权杠杆" : "杠杆"} 状态 模式/交易所 轮次 组 ID - 保证金 - 名义永续/期权 + {!r.isOo ? 保证金 : null} + {r.isOo ? "数量 Call/Put" : "名义永续/期权"} 出场目标 指数 合约对 @@ -1017,7 +1114,7 @@ export default function MonitorPage() { {r.lossPct != null ? {r.lossPct} : null} {r.exit} {r.openRatio ? {r.openRatio} : null} - {r.leverage} + {r.leverage} {detailRunning ? "运行中" : "已停"} · {detail.phase} @@ -1036,13 +1133,27 @@ export default function MonitorPage() { "—", )} - {String(detail.strat.perp_margin_mode || "—")} + {!r.isOo ? ( + + {String(detail.strat.perp_margin_mode || "—")} + + ) : null} - {fmt(detail.strat.perp_qty_eth, 4)} /{" "} - {fmt(detail.strat.option_qty_eth, 4)} ETH + {r.isOo + ? `${fmt(detail.strat.option_qty_eth, 2)} / ${fmt( + detail.strat.oo_put_qty_eth ?? + detail.strat.option_qty_eth, + 2, + )} ETH` + : `${fmt(detail.strat.perp_qty_eth, 4)} / ${fmt( + detail.strat.option_qty_eth, + 4, + )} ETH`} {fmt(detail.strat.exit_target_usdt, 2)} USDT - {fmtExPx("perp", detail.index_px)} + + {fmtExPx("perp", detail.index_px)} + {detail.pair ? `${detail.pair.expiry_ymd || "?"} @ ${