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:
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user