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:
@@ -284,6 +284,9 @@ async def fleet_status(_tok: Annotated[str, Depends(require_fleet_token)]) -> di
|
||||
"risk_perp_unit": _pick("risk_perp_unit", 1.0),
|
||||
"risk_option_unit": _pick("risk_option_unit", 2.0),
|
||||
"risk_exit_unit": _pick("risk_exit_unit", 15.0),
|
||||
"martingale_enabled": st.get("martingale_enabled"),
|
||||
"martingale_doubles": st.get("martingale_doubles"),
|
||||
"risk_effective_loss_pct": st.get("risk_effective_loss_pct"),
|
||||
},
|
||||
"position": {
|
||||
"status": pos.get("status") or ("open" if pos.get("has_position") else "flat"),
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user