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
+93
View File
@@ -149,3 +149,96 @@ def test_compute_risk_sizing_respects_basis(tmp_path, monkeypatch) -> None:
assert r2.k is not None and r2.k > 1.0
assert r2.option_ask == 10.0
db.close()
def test_consecutive_loss_days_and_martingale(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("MODE", "SIM")
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from app.models.db import Database
from app.strategy.risk_sizing import consecutive_loss_days, resolve_martingale
db = Database(tmp_path / "mg.db")
sh = ZoneInfo("Asia/Shanghai")
def day_ms(ymd: str, hour: int = 16) -> int:
dt = datetime.strptime(ymd, "%Y-%m-%d").replace(
hour=hour, tzinfo=sh
)
return int(dt.astimezone(timezone.utc).timestamp() * 1000)
# 插入:盈利日打断后连亏 3 天(有成交日序列,跳过无成交日)
rows = [
("g1", day_ms("2026-07-28"), 10.0),
("g2", day_ms("2026-07-29"), -5.0),
("g3", day_ms("2026-07-30"), -3.0),
("g4", day_ms("2026-07-31"), -1.0),
]
for gid, ms, pnl in rows:
db.execute(
"""INSERT INTO groups(
group_id, status, realized_pnl, close_at_ms, open_at_ms
) VALUES(?,?,?,?,?)""",
(gid, "closed", pnl, ms, ms - 3600_000),
)
assert consecutive_loss_days(db) == 3
db.set_setting("sizing_mode", "risk_based")
db.set_setting("risk_loss_mode", "percent")
db.set_setting("risk_loss_pct", "2")
db.set_setting("martingale_enabled", "true")
db.set_setting("martingale_start_after_loss_days", "2")
db.set_setting("martingale_max_doubles", "3")
mg = resolve_martingale(db, base_pct=2.0)
assert mg["eligible"] is True
assert mg["loss_days"] == 3
# 连亏3天、start=2 → doubles = min(3-2+1, 3) = 2 → 2%*4 = 8%
assert mg["doubles"] == 2
assert abs(float(mg["effective_pct"]) - 8.0) < 1e-9
db.set_setting("risk_loss_pct", "3.1")
mg2 = resolve_martingale(db, base_pct=3.1)
assert mg2["eligible"] is False
assert mg2["doubles"] == 0
assert abs(float(mg2["effective_pct"]) - 3.1) < 1e-9
db.close()
def test_martingale_doubles_capped(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("MODE", "SIM")
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from app.models.db import Database
from app.strategy.risk_sizing import resolve_martingale
db = Database(tmp_path / "mg_cap.db")
sh = ZoneInfo("Asia/Shanghai")
def day_ms(ymd: str) -> int:
dt = datetime.strptime(ymd, "%Y-%m-%d").replace(hour=12, tzinfo=sh)
return int(dt.astimezone(timezone.utc).timestamp() * 1000)
for i, ymd in enumerate(
["2026-07-26", "2026-07-27", "2026-07-28", "2026-07-29", "2026-07-30"]
):
db.execute(
"""INSERT INTO groups(
group_id, status, realized_pnl, close_at_ms, open_at_ms
) VALUES(?,?,?,?,?)""",
(f"c{i}", "closed", -1.0, day_ms(ymd), day_ms(ymd) - 1000),
)
db.set_setting("sizing_mode", "risk_based")
db.set_setting("risk_loss_mode", "percent")
db.set_setting("martingale_enabled", "true")
db.set_setting("martingale_start_after_loss_days", "2")
db.set_setting("martingale_max_doubles", "3")
mg = resolve_martingale(db, base_pct=2.0)
# 连亏5、start2 → raw=4cap=3 → 2%*8=16%
assert mg["doubles"] == 3
assert abs(float(mg["effective_pct"]) - 16.0) < 1e-9
db.close()