Add martingale mode for risk-based percent sizing.

Enable in settings (default off): after N consecutive loss days, double the effective risk_loss_pct up to a configurable max; blocked when base pct > 3%.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-07 09:55:29 +08:00
parent 3d1f9f3d50
commit 0f552eb50e
9 changed files with 476 additions and 10 deletions
+24
View File
@@ -322,6 +322,27 @@ export type PlanState = {
liquidity_ok?: boolean;
liquidity_detail?: string | null;
}[];
risk_perp_unit?: number;
risk_option_unit?: number;
risk_exit_unit?: number;
risk_last_k?: number | null;
risk_sizing_preview?: {
ok?: boolean;
locked?: boolean;
detail?: string;
budget?: number;
k?: number;
[key: string]: unknown;
};
risk_sizing_locked?: boolean;
martingale_enabled?: boolean;
martingale_eligible?: boolean;
martingale_doubles?: number;
martingale_loss_days?: number;
risk_effective_loss_pct?: number;
risk_loss_pct?: number;
sizing_mode?: "manual" | "risk_based";
risk_based?: boolean;
ledger: { equity: number; available: number; reserved: number };
mode?: "SIM" | "LIVE";
sim?: boolean;
@@ -364,6 +385,9 @@ export type StrategySettings = {
risk_option_unit?: number;
risk_exit_unit?: number;
risk_sizing_preview?: Record<string, unknown>;
martingale_enabled?: boolean;
martingale_start_after_loss_days?: number;
martingale_max_doubles?: number;
exchange?: string;
};
+7
View File
@@ -223,6 +223,13 @@ export default function PlanPage() {
: plan?.risk_sizing_preview?.budget != null
? `预算${fmt(plan.risk_sizing_preview.budget, 2)}U`
: null,
plan?.martingale_enabled &&
(plan?.martingale_doubles ?? 0) > 0 &&
!riskLocked
? `倍投×${2 ** Number(plan.martingale_doubles)}(${fmt(plan.risk_effective_loss_pct ?? plan.risk_loss_pct ?? 0, 2)}%)`
: plan?.martingale_enabled && !riskLocked
? "倍投开"
: null,
!riskLocked && plan?.risk_sizing_preview?.ok === false
? String(plan.risk_sizing_preview.detail || "预览失败")
: null,
+123 -9
View File
@@ -105,6 +105,9 @@ export default function SettingsPage() {
const [riskPerpUnit, setRiskPerpUnit] = useState(1);
const [riskOptUnit, setRiskOptUnit] = useState(2);
const [riskExitUnit, setRiskExitUnit] = useState(15);
const [martingaleOn, setMartingaleOn] = useState(false);
const [martingaleStartAfter, setMartingaleStartAfter] = useState(2);
const [martingaleMaxDoubles, setMartingaleMaxDoubles] = useState(3);
const [riskPreview, setRiskPreview] = useState<Record<string, unknown> | null>(
null,
);
@@ -215,6 +218,9 @@ export default function SettingsPage() {
setRiskPerpUnit(s.risk_perp_unit ?? 1);
setRiskOptUnit(s.risk_option_unit ?? 2);
setRiskExitUnit(s.risk_exit_unit ?? 15);
setMartingaleOn(s.martingale_enabled === true);
setMartingaleStartAfter(s.martingale_start_after_loss_days ?? 2);
setMartingaleMaxDoubles(s.martingale_max_doubles ?? 3);
setRiskPreview(
s.risk_sizing_preview && typeof s.risk_sizing_preview === "object"
? s.risk_sizing_preview
@@ -376,6 +382,13 @@ export default function SettingsPage() {
risk_perp_unit: riskPerpUnit,
risk_option_unit: riskOptUnit,
risk_exit_unit: riskExitUnit,
martingale_enabled:
sizingMode === "risk_based" &&
riskLossMode === "percent" &&
riskLossPct <= 3 &&
martingaleOn,
martingale_start_after_loss_days: martingaleStartAfter,
martingale_max_doubles: martingaleMaxDoubles,
exchange,
};
// 以损定仓不提交手填名义/出场,避免禁用输入框脏值导致 422
@@ -719,13 +732,14 @@ export default function SettingsPage() {
id="riskLossMode"
className="mono"
value={riskLossMode}
onChange={(e) =>
setRiskLossMode(
onChange={(e) => {
const mode =
e.target.value === "absolute"
? "absolute"
: "percent",
)
}
: "percent";
setRiskLossMode(mode);
if (mode !== "percent") setMartingaleOn(false);
}}
>
<option value="percent"> %</option>
<option value="absolute">USDT</option>
@@ -778,11 +792,93 @@ export default function SettingsPage() {
step="0.01"
min="0.01"
value={riskLossPct}
onChange={(e) =>
setRiskLossPct(Number(e.target.value))
}
onChange={(e) => {
const v = Number(e.target.value);
setRiskLossPct(v);
if (v > 3) setMartingaleOn(false);
}}
/>
</div>
<div className="field">
<label htmlFor="mgOn"></label>
<select
id="mgOn"
className="mono"
value={martingaleOn ? "on" : "off"}
disabled={riskLossPct > 3}
onChange={(e) =>
setMartingaleOn(e.target.value === "on")
}
>
<option value="off"></option>
<option value="on"></option>
</select>
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
{riskLossPct > 3
? "亏损幅度超过 3% 时不可启用倍投。"
: "连续亏损日达阈值后,按基础幅度翻倍(不改保存的幅度值)。最多可设翻倍次数。"}
</p>
</div>
{martingaleOn && riskLossPct <= 3 ? (
<>
<div className="field">
<label htmlFor="mgStart">
</label>
<input
id="mgStart"
className="mono"
type="number"
step="1"
min="1"
max="30"
value={martingaleStartAfter}
onChange={(e) =>
setMartingaleStartAfter(
Number(e.target.value),
)
}
/>
</div>
<div className="field">
<label htmlFor="mgMax"></label>
<input
id="mgMax"
className="mono"
type="number"
step="1"
min="1"
max="10"
value={martingaleMaxDoubles}
onChange={(e) =>
setMartingaleMaxDoubles(
Number(e.target.value),
)
}
/>
<p
className="hint"
style={{ margin: "0.35rem 0 0" }}
>
{riskLossPct}%{" "}
{martingaleStartAfter} {" "}
{martingaleMaxDoubles} {" "}
{[0, 1, 2, 3]
.filter((d) => d <= martingaleMaxDoubles)
.map(
(d) =>
`${Number(
(
riskLossPct *
2 ** d
).toPrecision(6),
)}%`,
)
.join(" → ")}
</p>
</div>
</>
) : null}
</>
) : (
<div className="field">
@@ -835,7 +931,25 @@ export default function SettingsPage() {
? "选约杠杆"
: "";
const basisS = basis ? ` · ${basis}` : "";
return `k=${pk ?? "—"} · 预算=${budS}U · 估亏=${mxS}U · 永续=${perp ?? "—"} · 期权=${opt ?? "—"} · 出场=${exit ?? "—"}${basisS}`;
const mg =
riskPreview.martingale &&
typeof riskPreview.martingale === "object"
? (riskPreview.martingale as Record<
string,
unknown
>)
: null;
const mgD = Number(mg?.doubles);
const mgS =
mg != null &&
mg.enabled === true &&
Number.isFinite(mgD) &&
mgD > 0
? ` · 倍投×${2 ** mgD}(连亏${Number(mg.loss_days) || 0}天·有效${Number(mg.effective_pct)}%)`
: mg != null && mg.enabled === true
? ` · 倍投待命(连亏${Number(mg.loss_days) || 0}天)`
: "";
return `k=${pk ?? "—"} · 预算=${budS}U · 估亏=${mxS}U · 永续=${perp ?? "—"} · 期权=${opt ?? "—"} · 出场=${exit ?? "—"}${basisS}${mgS}`;
})()}
</div>
</div>