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
+76
View File
@@ -61,6 +61,9 @@ KEYS = (
"risk_perp_unit",
"risk_option_unit",
"risk_exit_unit",
"martingale_enabled",
"martingale_start_after_loss_days",
"martingale_max_doubles",
)
@@ -104,6 +107,9 @@ class StrategySettingsBody(BaseModel):
risk_perp_unit: float | None = Field(default=None, ge=0.01, le=100)
risk_option_unit: float | None = Field(default=None, ge=0.01, le=100)
risk_exit_unit: float | None = Field(default=None, ge=0.1, le=1_000_000)
martingale_enabled: bool | None = None
martingale_start_after_loss_days: int | None = Field(default=None, ge=1, le=30)
martingale_max_doubles: int | None = Field(default=None, ge=1, le=10)
def _as_bool(raw: str | None, default: bool) -> bool:
@@ -298,6 +304,29 @@ def _read_settings() -> dict:
"risk_perp_unit": float(db.get_setting("risk_perp_unit", "1") or 1),
"risk_option_unit": float(db.get_setting("risk_option_unit", "2") or 2),
"risk_exit_unit": float(db.get_setting("risk_exit_unit", "15") or 15),
"martingale_enabled": _as_bool(
db.get_setting(
"martingale_enabled", str(s.martingale_enabled)
),
s.martingale_enabled,
),
"martingale_start_after_loss_days": int(
float(
db.get_setting(
"martingale_start_after_loss_days",
str(s.martingale_start_after_loss_days),
)
or s.martingale_start_after_loss_days
)
),
"martingale_max_doubles": int(
float(
db.get_setting(
"martingale_max_doubles", str(s.martingale_max_doubles)
)
or s.martingale_max_doubles
)
),
"risk_sizing_preview": _risk_preview_safe(),
"exchange": rt.exchange,
"perp_inst_id": rt.perp_inst_id,
@@ -378,6 +407,9 @@ async def put_strategy_settings(
"risk_loss_usdt",
"risk_capital_source",
"risk_manual_capital_usdt",
"martingale_enabled",
"martingale_start_after_loss_days",
"martingale_max_doubles",
)
hit = [k for k in locked_keys if k in data]
if hit:
@@ -439,6 +471,50 @@ async def put_strategy_settings(
detail="以损定仓选用亏损幅度时,须填写 risk_loss_pct > 0",
)
# 倍投:仅以损定仓 + 亏损幅度% + 基础幅度≤3%;条件不满足则强制关闭
from ..strategy.risk_sizing import MARTINGALE_MAX_BASE_PCT
loss_mode_final = str(
data.get(
"risk_loss_mode",
db.get_setting("risk_loss_mode", "percent") or "percent",
)
).strip().lower()
pct_final = data.get("risk_loss_pct")
if pct_final is None:
pct_final = float(db.get_setting("risk_loss_pct", "1") or 1)
else:
pct_final = float(pct_final)
existing_mg = _as_bool(
db.get_setting("martingale_enabled", str(s.martingale_enabled)),
s.martingale_enabled,
)
want_mg = (
bool(data["martingale_enabled"])
if "martingale_enabled" in data
else existing_mg
)
mg_eligible = (
sizing_mode == "risk_based"
and loss_mode_final in ("percent", "pct", "%", "幅度")
and float(pct_final) <= MARTINGALE_MAX_BASE_PCT + 1e-12
)
if want_mg and not mg_eligible:
explicit_on = "martingale_enabled" in data and bool(data["martingale_enabled"])
if explicit_on:
if sizing_mode != "risk_based":
reason = "倍投模式仅可在以损定仓下开启"
elif loss_mode_final not in ("percent", "pct", "%", "幅度"):
reason = "倍投模式仅可在「亏损幅度%」下开启"
else:
reason = (
f"以损定仓亏损幅度超过 {MARTINGALE_MAX_BASE_PCT:g}% 时不可启用倍投"
)
raise HTTPException(status_code=400, detail=reason)
data["martingale_enabled"] = False
elif not mg_eligible:
data["martingale_enabled"] = False
for k, v in data.items():
if k in KEYS:
db.set_setting(k, str(v))