diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index c0e5f05..b2fdea3 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -524,10 +524,14 @@ async def put_strategy_settings( ) ).strip().lower() if sizing_mode == "risk_based": - data["exit_mode"] = "fixed_usdt" + # 永期以损:允许 exit_mode=fixed_usdt(基数×k)或 premium_multiple(权利金×倍) + # 期期仍强制 fixed(目标由预算×盈亏比写入) + if hedge_mode == "option_option": + data["exit_mode"] = "fixed_usdt" data.pop("perp_qty_eth", None) data.pop("option_qty_eth", None) - data.pop("net_profit_target", None) + if str(data.get("exit_mode") or db.get_setting("exit_mode", "") or "").strip().lower() != "premium_multiple": + data.pop("net_profit_target", None) loss_mode = str( data.get( "risk_loss_mode", diff --git a/backend/app/strategy/risk_sizing.py b/backend/app/strategy/risk_sizing.py index 442e7cc..30ceb48 100644 --- a/backend/app/strategy/risk_sizing.py +++ b/backend/app/strategy/risk_sizing.py @@ -703,26 +703,42 @@ def apply_risk_sizing_to_ledger( if not r.ok: return r - # 以损定仓强制 fixed_usdt;出场 = exit_unit × k - database.set_setting("exit_mode", "fixed_usdt") + exit_mode = str( + ledger.get_setting_str("exit_mode", "fixed_usdt") or "fixed_usdt" + ).strip().lower() + s = get_settings() + prem_mult = float( + ledger.get_setting_float("premium_exit_multiple", s.premium_exit_multiple) + or s.premium_exit_multiple + ) database.set_setting("perp_qty_eth", str(r.perp_qty_eth)) database.set_setting("option_qty_eth", str(r.option_qty_eth)) - database.set_setting("net_profit_target", str(r.net_profit_target)) database.set_setting("risk_last_k", str(r.k)) database.set_setting( "risk_last_max_loss", f"{r.max_loss:.2f}" if r.max_loss is not None else "", ) + if exit_mode == "premium_multiple": + database.set_setting("exit_mode", "premium_multiple") + # 预估展示用:估权利金×倍数;开仓后以真实 initial_premium 锁定 + est = float(r.premium_est or 0) * max(0.0, prem_mult) + if est > 0: + database.set_setting("net_profit_target", f"{est:.4f}") + exit_log = f"prem×{prem_mult:g}≈{est:.2f}" + else: + database.set_setting("exit_mode", "fixed_usdt") + database.set_setting("net_profit_target", str(r.net_profit_target)) + exit_log = f"{r.net_profit_target or 0:.4f}" logger.info( "risk_sizing applied k=%.1f basis=%s sizing_ask=%.4f actual_ask=%.4f " - "perp=%.4f opt=%.4f exit=%.4f max_loss=%.4f budget=%.4f", + "perp=%.4f opt=%.4f exit=%s max_loss=%.4f budget=%.4f", r.k or 0, r.leverage_basis or "?", r.option_ask or 0, r.actual_option_ask or 0, r.perp_qty_eth or 0, r.option_qty_eth or 0, - r.net_profit_target or 0, + exit_log, r.max_loss or 0, r.budget or 0, ) @@ -766,6 +782,20 @@ def preview_risk_sizing(db: Database | None = None) -> dict[str, Any]: r = compute_risk_sizing(index_px=float(idx), option_ask=float(ask), db=database) perp_u, opt_u, exit_u = read_risk_units(ledger) mg = resolve_martingale(database, ledger=ledger) + s = get_settings() + exit_mode = str( + ledger.get_setting_str("exit_mode", s.exit_mode) or s.exit_mode + ).strip().lower() + prem_mult = float( + ledger.get_setting_float("premium_exit_multiple", s.premium_exit_multiple) + or s.premium_exit_multiple + ) + exit_target = r.net_profit_target + exit_label = "基数×k" + if exit_mode == "premium_multiple": + exit_label = f"权利金×{prem_mult:g}" + if r.ok and r.premium_est is not None: + exit_target = round(float(r.premium_est) * max(0.0, prem_mult), 2) out.update( { "ok": r.ok, @@ -778,7 +808,10 @@ def preview_risk_sizing(db: Database | None = None) -> dict[str, Any]: "max_loss": r.max_loss, "perp_qty_eth": r.perp_qty_eth, "option_qty_eth": r.option_qty_eth, - "net_profit_target": r.net_profit_target, + "net_profit_target": exit_target, + "exit_mode": exit_mode if exit_mode in ("fixed_usdt", "premium_multiple") else "fixed_usdt", + "premium_exit_multiple": prem_mult, + "exit_label": exit_label, "index_px": r.index_px, "option_ask": r.option_ask, "actual_option_ask": r.actual_option_ask, diff --git a/control/frontend/src/pages/Monitor.tsx b/control/frontend/src/pages/Monitor.tsx index c7af323..c17a097 100644 --- a/control/frontend/src/pages/Monitor.tsx +++ b/control/frontend/src/pages/Monitor.tsx @@ -208,10 +208,13 @@ function riskLines(strat: Record): RiskLines { const minH = Number( strat.oo_min_option_hours ?? strat.min_option_hours, ); + const ampFilt = strat.oo_amplitude_filter_enabled === true; const amp = Number.isFinite(ampN) && Number.isFinite(ampH) - ? `≤${fmt(ampN, ampN % 1 === 0 ? 0 : 1)}%·${fmt(ampH, 0)}h` - : "≤—"; + ? ampFilt + ? `≤${fmt(ampN, ampN % 1 === 0 ? 0 : 1)}%·${fmt(ampH, 0)}h` + : `${fmt(ampH, 0)}h` + : "—"; const callQ = strat.risk_sizing_preview && typeof strat.risk_sizing_preview === "object" && @@ -246,13 +249,15 @@ function riskLines(strat: Record): RiskLines { isOo: true, sizing: riskBased ? "以损定仓(A)" : "手动开仓(B)", sizingShort: riskBased ? "A" : "B", - lossPct: Number.isFinite(ampN) + lossPct: ampFilt && Number.isFinite(ampN) ? `≤${fmt(ampN, ampN % 1 === 0 ? 0 : 1)}%` : null, exit, openRatio, riskOrOpen: `${amp}/${openRatio}/${minHours}`, - riskOrOpenTitle: "振幅上限·回看 / Call:Put 数量 / 最短剩余(h)", + riskOrOpenTitle: ampFilt + ? "振幅上限·回看 / Call:Put 数量 / 最短剩余(h)" + : "振幅回看 / Call:Put 数量 / 最短剩余(h)", minHours, leverage, leverageTitle: "期期单腿最低杠杆", @@ -261,7 +266,12 @@ function riskLines(strat: Record): RiskLines { const exitMode = String(strat.exit_mode || "fixed_usdt"); let exit: string; - if (riskBased) { + if (riskBased && exitMode === "premium_multiple") { + exit = + strat.premium_exit_multiple != null + ? `权利金×${fmt(strat.premium_exit_multiple, 2)}` + : "权利金×—"; + } else if (riskBased) { exit = strat.risk_exit_unit != null && Number.isFinite(Number(strat.risk_exit_unit)) ? `基数${unitLabel(strat.risk_exit_unit)}` diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index a804849..455bd2f 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -216,7 +216,11 @@ export default function PlanPage() { }${fmt(ooRatio, 1)})` : `净盈≥预算×${fmt(ooRatio, 1)}(达标只平盈利腿)` : exitMode === "premium_multiple" - ? `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}` + ? riskBased + ? exitTarget != null + ? `${riskLocked ? "锁定 " : ""}净盈≥${fmt(exitTarget)} U(权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)})` + : `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}(开仓后锁定)` + : `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}` : riskBased ? exitTarget != null ? `${riskLocked ? "锁定 " : "固定 "}${fmt(exitTarget)} U(基数${fmt(plan?.risk_exit_unit ?? 15)})` diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index acc65a7..eebe968 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -376,8 +376,7 @@ export default function SettingsPage() { try { const body: Record = { fee_rate: fee, - exit_mode: - oo || sizingMode === "risk_based" ? "fixed_usdt" : exitMode, + exit_mode: oo ? "fixed_usdt" : exitMode, premium_exit_multiple: premMult, rest_seconds: rest, live_order_interval_sec: orderInterval, @@ -1029,10 +1028,23 @@ export default function SettingsPage() { pk != null ? Number((riskOptUnit * pk).toFixed(4)) : riskPreview.option_qty_eth; + const premEst = Number(riskPreview.premium_est); const exit = - pk != null - ? Number((riskExitUnit * pk).toFixed(2)) - : riskPreview.net_profit_target; + exitMode === "premium_multiple" + ? Number.isFinite(premEst) + ? Number( + ( + premEst * Number(premMult || 1) + ).toFixed(2), + ) + : riskPreview.net_profit_target + : pk != null + ? Number((riskExitUnit * pk).toFixed(2)) + : riskPreview.net_profit_target; + const exitDisp = + exitMode === "premium_multiple" + ? `权利金×${Number(premMult || 1)}≈${exit ?? "—"}` + : String(exit ?? "—"); const budS = Number.isFinite(bud) ? bud.toFixed(2) : "—"; @@ -1076,7 +1088,7 @@ export default function SettingsPage() { } · Put≈${ riskPreview.put_qty_eth ?? "—" } · 出场=预算×${ooRewardRatio}${mgS}` - : `k=${pk ?? "—"} · 预算=${budS}U · 估亏=${mxS}U · 永续=${perp ?? "—"} · 期权=${opt ?? "—"} · 出场=${exit ?? "—"}${basisS}${mgS}`; + : `k=${pk ?? "—"} · 预算=${budS}U · 估亏=${mxS}U · 永续=${perp ?? "—"} · 期权=${opt ?? "—"} · 出场=${exitDisp}${basisS}${mgS}`; })()} @@ -1120,9 +1132,72 @@ export default function SettingsPage() { }} /> +
+ + +
+ {exitMode === "premium_multiple" ? ( +
+ + + + setPremMult(Number(e.target.value)) + } + /> +
+ ) : ( + <>
+ + )} ) : null} @@ -1399,10 +1476,7 @@ export default function SettingsPage() { - {sizingMode === "risk_based" || exitMode === "fixed_usdt" ? ( + {exitMode === "fixed_usdt" ? (
+
  • + 出场资金:可选「基数×k」或「权利金倍数」(如盈利 1 + 倍/2 倍权利金);开仓后按真实权利金锁定目标。 +
  • )}