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:
@@ -524,10 +524,14 @@ async def put_strategy_settings(
|
|||||||
)
|
)
|
||||||
).strip().lower()
|
).strip().lower()
|
||||||
if sizing_mode == "risk_based":
|
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("perp_qty_eth", None)
|
||||||
data.pop("option_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(
|
loss_mode = str(
|
||||||
data.get(
|
data.get(
|
||||||
"risk_loss_mode",
|
"risk_loss_mode",
|
||||||
|
|||||||
@@ -703,26 +703,42 @@ def apply_risk_sizing_to_ledger(
|
|||||||
if not r.ok:
|
if not r.ok:
|
||||||
return r
|
return r
|
||||||
|
|
||||||
# 以损定仓强制 fixed_usdt;出场 = exit_unit × k
|
exit_mode = str(
|
||||||
database.set_setting("exit_mode", "fixed_usdt")
|
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("perp_qty_eth", str(r.perp_qty_eth))
|
||||||
database.set_setting("option_qty_eth", str(r.option_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_k", str(r.k))
|
||||||
database.set_setting(
|
database.set_setting(
|
||||||
"risk_last_max_loss",
|
"risk_last_max_loss",
|
||||||
f"{r.max_loss:.2f}" if r.max_loss is not None else "",
|
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(
|
logger.info(
|
||||||
"risk_sizing applied k=%.1f basis=%s sizing_ask=%.4f actual_ask=%.4f "
|
"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.k or 0,
|
||||||
r.leverage_basis or "?",
|
r.leverage_basis or "?",
|
||||||
r.option_ask or 0,
|
r.option_ask or 0,
|
||||||
r.actual_option_ask or 0,
|
r.actual_option_ask or 0,
|
||||||
r.perp_qty_eth or 0,
|
r.perp_qty_eth or 0,
|
||||||
r.option_qty_eth or 0,
|
r.option_qty_eth or 0,
|
||||||
r.net_profit_target or 0,
|
exit_log,
|
||||||
r.max_loss or 0,
|
r.max_loss or 0,
|
||||||
r.budget 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)
|
r = compute_risk_sizing(index_px=float(idx), option_ask=float(ask), db=database)
|
||||||
perp_u, opt_u, exit_u = read_risk_units(ledger)
|
perp_u, opt_u, exit_u = read_risk_units(ledger)
|
||||||
mg = resolve_martingale(database, ledger=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(
|
out.update(
|
||||||
{
|
{
|
||||||
"ok": r.ok,
|
"ok": r.ok,
|
||||||
@@ -778,7 +808,10 @@ def preview_risk_sizing(db: Database | None = None) -> dict[str, Any]:
|
|||||||
"max_loss": r.max_loss,
|
"max_loss": r.max_loss,
|
||||||
"perp_qty_eth": r.perp_qty_eth,
|
"perp_qty_eth": r.perp_qty_eth,
|
||||||
"option_qty_eth": r.option_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,
|
"index_px": r.index_px,
|
||||||
"option_ask": r.option_ask,
|
"option_ask": r.option_ask,
|
||||||
"actual_option_ask": r.actual_option_ask,
|
"actual_option_ask": r.actual_option_ask,
|
||||||
|
|||||||
@@ -208,10 +208,13 @@ function riskLines(strat: Record<string, unknown>): RiskLines {
|
|||||||
const minH = Number(
|
const minH = Number(
|
||||||
strat.oo_min_option_hours ?? strat.min_option_hours,
|
strat.oo_min_option_hours ?? strat.min_option_hours,
|
||||||
);
|
);
|
||||||
|
const ampFilt = strat.oo_amplitude_filter_enabled === true;
|
||||||
const amp =
|
const amp =
|
||||||
Number.isFinite(ampN) && Number.isFinite(ampH)
|
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 =
|
const callQ =
|
||||||
strat.risk_sizing_preview &&
|
strat.risk_sizing_preview &&
|
||||||
typeof strat.risk_sizing_preview === "object" &&
|
typeof strat.risk_sizing_preview === "object" &&
|
||||||
@@ -246,13 +249,15 @@ function riskLines(strat: Record<string, unknown>): RiskLines {
|
|||||||
isOo: true,
|
isOo: true,
|
||||||
sizing: riskBased ? "以损定仓(A)" : "手动开仓(B)",
|
sizing: riskBased ? "以损定仓(A)" : "手动开仓(B)",
|
||||||
sizingShort: riskBased ? "A" : "B",
|
sizingShort: riskBased ? "A" : "B",
|
||||||
lossPct: Number.isFinite(ampN)
|
lossPct: ampFilt && Number.isFinite(ampN)
|
||||||
? `≤${fmt(ampN, ampN % 1 === 0 ? 0 : 1)}%`
|
? `≤${fmt(ampN, ampN % 1 === 0 ? 0 : 1)}%`
|
||||||
: null,
|
: null,
|
||||||
exit,
|
exit,
|
||||||
openRatio,
|
openRatio,
|
||||||
riskOrOpen: `${amp}/${openRatio}/${minHours}`,
|
riskOrOpen: `${amp}/${openRatio}/${minHours}`,
|
||||||
riskOrOpenTitle: "振幅上限·回看 / Call:Put 数量 / 最短剩余(h)",
|
riskOrOpenTitle: ampFilt
|
||||||
|
? "振幅上限·回看 / Call:Put 数量 / 最短剩余(h)"
|
||||||
|
: "振幅回看 / Call:Put 数量 / 最短剩余(h)",
|
||||||
minHours,
|
minHours,
|
||||||
leverage,
|
leverage,
|
||||||
leverageTitle: "期期单腿最低杠杆",
|
leverageTitle: "期期单腿最低杠杆",
|
||||||
@@ -261,7 +266,12 @@ function riskLines(strat: Record<string, unknown>): RiskLines {
|
|||||||
|
|
||||||
const exitMode = String(strat.exit_mode || "fixed_usdt");
|
const exitMode = String(strat.exit_mode || "fixed_usdt");
|
||||||
let exit: string;
|
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 =
|
exit =
|
||||||
strat.risk_exit_unit != null && Number.isFinite(Number(strat.risk_exit_unit))
|
strat.risk_exit_unit != null && Number.isFinite(Number(strat.risk_exit_unit))
|
||||||
? `基数${unitLabel(strat.risk_exit_unit)}`
|
? `基数${unitLabel(strat.risk_exit_unit)}`
|
||||||
|
|||||||
@@ -216,7 +216,11 @@ export default function PlanPage() {
|
|||||||
}${fmt(ooRatio, 1)})`
|
}${fmt(ooRatio, 1)})`
|
||||||
: `净盈≥预算×${fmt(ooRatio, 1)}(达标只平盈利腿)`
|
: `净盈≥预算×${fmt(ooRatio, 1)}(达标只平盈利腿)`
|
||||||
: exitMode === "premium_multiple"
|
: 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
|
: riskBased
|
||||||
? exitTarget != null
|
? exitTarget != null
|
||||||
? `${riskLocked ? "锁定 " : "固定 "}${fmt(exitTarget)} U(基数${fmt(plan?.risk_exit_unit ?? 15)})`
|
? `${riskLocked ? "锁定 " : "固定 "}${fmt(exitTarget)} U(基数${fmt(plan?.risk_exit_unit ?? 15)})`
|
||||||
|
|||||||
@@ -376,8 +376,7 @@ export default function SettingsPage() {
|
|||||||
try {
|
try {
|
||||||
const body: Record<string, unknown> = {
|
const body: Record<string, unknown> = {
|
||||||
fee_rate: fee,
|
fee_rate: fee,
|
||||||
exit_mode:
|
exit_mode: oo ? "fixed_usdt" : exitMode,
|
||||||
oo || sizingMode === "risk_based" ? "fixed_usdt" : exitMode,
|
|
||||||
premium_exit_multiple: premMult,
|
premium_exit_multiple: premMult,
|
||||||
rest_seconds: rest,
|
rest_seconds: rest,
|
||||||
live_order_interval_sec: orderInterval,
|
live_order_interval_sec: orderInterval,
|
||||||
@@ -1029,10 +1028,23 @@ export default function SettingsPage() {
|
|||||||
pk != null
|
pk != null
|
||||||
? Number((riskOptUnit * pk).toFixed(4))
|
? Number((riskOptUnit * pk).toFixed(4))
|
||||||
: riskPreview.option_qty_eth;
|
: riskPreview.option_qty_eth;
|
||||||
|
const premEst = Number(riskPreview.premium_est);
|
||||||
const exit =
|
const exit =
|
||||||
pk != null
|
exitMode === "premium_multiple"
|
||||||
? Number((riskExitUnit * pk).toFixed(2))
|
? Number.isFinite(premEst)
|
||||||
: riskPreview.net_profit_target;
|
? 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)
|
const budS = Number.isFinite(bud)
|
||||||
? bud.toFixed(2)
|
? bud.toFixed(2)
|
||||||
: "—";
|
: "—";
|
||||||
@@ -1076,7 +1088,7 @@ export default function SettingsPage() {
|
|||||||
} · Put≈${
|
} · Put≈${
|
||||||
riskPreview.put_qty_eth ?? "—"
|
riskPreview.put_qty_eth ?? "—"
|
||||||
} · 出场=预算×${ooRewardRatio}${mgS}`
|
} · 出场=预算×${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>
|
||||||
</div>
|
</div>
|
||||||
@@ -1120,9 +1132,72 @@ export default function SettingsPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="field">
|
||||||
<label htmlFor="riskExitU">
|
<label htmlFor="riskExitU">
|
||||||
出场资金(k=1;改后按比例联动)
|
出场基数(k=1;改后按比例联动)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="riskExitU"
|
id="riskExitU"
|
||||||
@@ -1178,6 +1253,8 @@ export default function SettingsPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
@@ -1399,10 +1476,7 @@ export default function SettingsPage() {
|
|||||||
<select
|
<select
|
||||||
id="exitMode"
|
id="exitMode"
|
||||||
className="mono"
|
className="mono"
|
||||||
disabled={sizingMode === "risk_based"}
|
value={exitMode}
|
||||||
value={
|
|
||||||
sizingMode === "risk_based" ? "fixed_usdt" : exitMode
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setExitMode(
|
setExitMode(
|
||||||
e.target.value === "premium_multiple"
|
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>
|
<option value="premium_multiple">权利金倍数</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{sizingMode === "risk_based" || exitMode === "fixed_usdt" ? (
|
{exitMode === "fixed_usdt" ? (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="netTarget">
|
<label htmlFor="netTarget">
|
||||||
{sizingMode === "risk_based"
|
{sizingMode === "risk_based"
|
||||||
? "出场资金(k=1 基数)"
|
? "出场基数(k=1)"
|
||||||
: "净盈利出场目标(USDT)"}
|
: "净盈利出场目标(USDT)"}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -1635,6 +1713,10 @@ export default function SettingsPage() {
|
|||||||
k,权利金再便宜也不放大仓位;实际杠杆按盘口卖一定仓,便宜时
|
k,权利金再便宜也不放大仓位;实际杠杆按盘口卖一定仓,便宜时
|
||||||
k 会变大。
|
k 会变大。
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
出场资金:可选「基数×k」或「权利金倍数」(如盈利 1
|
||||||
|
倍/2 倍权利金);开仓后按真实权利金锁定目标。
|
||||||
|
</li>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
Reference in New Issue
Block a user