Allow risk-based 永期 exits via premium multiple as well as base×k.

Settings UI picks 基数 or 权利金×n; sizing preview and control monitor follow.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-07 18:13:11 +08:00
parent ab612df818
commit 06f612aa52
5 changed files with 161 additions and 28 deletions
+6 -2
View File
@@ -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",
+39 -6
View File
@@ -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,
+15 -5
View File
@@ -208,10 +208,13 @@ function riskLines(strat: Record<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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)}`
+5 -1
View File
@@ -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)}`
+96 -14
View File
@@ -376,8 +376,7 @@ export default function SettingsPage() {
try {
const body: Record<string, unknown> = {
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}`;
})()}
</div>
</div>
@@ -1120,9 +1132,72 @@ export default function SettingsPage() {
}}
/>
</div>
<div className="field">
<label htmlFor="riskExitKind"></label>
<select
id="riskExitKind"
className="mono"
value={
exitMode === "premium_multiple"
? "premium_multiple"
: "fixed_usdt"
}
onChange={(e) =>
setExitMode(
e.target.value === "premium_multiple"
? "premium_multiple"
: "fixed_usdt",
)
}
>
<option value="fixed_usdt">×k</option>
<option value="premium_multiple"></option>
</select>
</div>
{exitMode === "premium_multiple" ? (
<div className="field">
<label htmlFor="riskPremMult">
</label>
<select
id="riskPremMultQuick"
className="mono"
value={
[1, 1.5, 2, 3].some(
(x) => Math.abs(x - Number(premMult)) < 1e-9,
)
? String(Number(premMult))
: "custom"
}
onChange={(e) => {
if (e.target.value === "custom") return;
setPremMult(Number(e.target.value));
}}
>
<option value="1">1 </option>
<option value="1.5">1.5 </option>
<option value="2">2 </option>
<option value="3">3 </option>
<option value="custom"></option>
</select>
<input
id="riskPremMult"
className="mono"
style={{ marginTop: 6 }}
type="number"
step="0.1"
min="0.1"
value={premMult}
onChange={(e) =>
setPremMult(Number(e.target.value))
}
/>
</div>
) : (
<>
<div className="field">
<label htmlFor="riskExitU">
k=1
k=1
</label>
<input
id="riskExitU"
@@ -1178,6 +1253,8 @@ export default function SettingsPage() {
}}
/>
</div>
</>
)}
</>
) : null}
</>
@@ -1399,10 +1476,7 @@ export default function SettingsPage() {
<select
id="exitMode"
className="mono"
disabled={sizingMode === "risk_based"}
value={
sizingMode === "risk_based" ? "fixed_usdt" : exitMode
}
value={exitMode}
onChange={(e) =>
setExitMode(
e.target.value === "premium_multiple"
@@ -1411,15 +1485,19 @@ export default function SettingsPage() {
)
}
>
<option value="fixed_usdt">USDT</option>
<option value="fixed_usdt">
{sizingMode === "risk_based"
? "基数(×k"
: "固定净盈利(USDT"}
</option>
<option value="premium_multiple"></option>
</select>
</div>
{sizingMode === "risk_based" || exitMode === "fixed_usdt" ? (
{exitMode === "fixed_usdt" ? (
<div className="field">
<label htmlFor="netTarget">
{sizingMode === "risk_based"
? "出场资金k=1 基数"
? "出场基数k=1"
: "净盈利出场目标(USDT"}
</label>
<input
@@ -1635,6 +1713,10 @@ export default function SettingsPage() {
k便便
k
</li>
<li>
×k 1
/2
</li>
</>
)}
<li>