Count expiry settlements as martingale losses.
Even small-profit expiry closes count toward consecutive loss days; only non-expiry profitable days break the streak. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -269,10 +269,26 @@ def resolve_budget(db: Database | None = None) -> tuple[float | None, str, float
|
||||
MARTINGALE_MAX_BASE_PCT = 3.0
|
||||
|
||||
|
||||
def _is_expiry_close_reason(reason: str | None) -> bool:
|
||||
r = str(reason or "").strip().lower()
|
||||
return r in ("expiry", "到期", "到期结算", "到期结算全平")
|
||||
|
||||
|
||||
def _martingale_day_pnl_contrib(realized_pnl: float, close_reason: str | None) -> float:
|
||||
"""
|
||||
倍投连亏日口径:到期结算无论实际盈亏(含小盈利)一律按亏损计入;
|
||||
其它平仓按真实 realized_pnl。
|
||||
"""
|
||||
if _is_expiry_close_reason(close_reason):
|
||||
return -1.0
|
||||
return float(realized_pnl or 0.0)
|
||||
|
||||
|
||||
def consecutive_loss_days(db: Database | None = None) -> int:
|
||||
"""
|
||||
按上海日历「平仓日」汇总净盈亏,从最近有平仓的一天往前数连续亏损天数。
|
||||
某日净盈亏 < 0 计为亏损日;无平仓的日历日不计入、不打断(按有成交日序列)。
|
||||
按上海日历「平仓日」汇总倍投口径盈亏,从最近有平仓的一天往前数连续亏损天数。
|
||||
某日合计 < 0 计为亏损日;到期结算组无论盈亏均按亏损计入。
|
||||
无平仓的日历日不计入、不打断(按有成交日序列)。
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timezone
|
||||
@@ -280,7 +296,7 @@ def consecutive_loss_days(db: Database | None = None) -> int:
|
||||
|
||||
database = db or get_db()
|
||||
rows = database.fetchall(
|
||||
"""SELECT realized_pnl, close_at_ms FROM groups
|
||||
"""SELECT realized_pnl, close_at_ms, close_reason FROM groups
|
||||
WHERE status='closed' AND close_at_ms IS NOT NULL
|
||||
ORDER BY close_at_ms ASC"""
|
||||
)
|
||||
@@ -300,7 +316,10 @@ def consecutive_loss_days(db: Database | None = None) -> int:
|
||||
.astimezone(sh)
|
||||
.strftime("%Y-%m-%d")
|
||||
)
|
||||
day_pnl[day] += float(r["realized_pnl"] or 0)
|
||||
day_pnl[day] += _martingale_day_pnl_contrib(
|
||||
float(r["realized_pnl"] or 0),
|
||||
r["close_reason"],
|
||||
)
|
||||
if not day_pnl:
|
||||
return 0
|
||||
streak = 0
|
||||
|
||||
@@ -242,3 +242,45 @@ def test_martingale_doubles_capped(tmp_path, monkeypatch) -> None:
|
||||
assert mg["doubles"] == 3
|
||||
assert abs(float(mg["effective_pct"]) - 16.0) < 1e-9
|
||||
db.close()
|
||||
|
||||
|
||||
def test_expiry_settle_counts_as_loss_even_if_profit(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
|
||||
|
||||
db = Database(tmp_path / "mg_exp.db")
|
||||
sh = ZoneInfo("Asia/Shanghai")
|
||||
|
||||
def day_ms(ymd: str) -> int:
|
||||
dt = datetime.strptime(ymd, "%Y-%m-%d").replace(hour=16, tzinfo=sh)
|
||||
return int(dt.astimezone(timezone.utc).timestamp() * 1000)
|
||||
|
||||
# 达标盈利打断;随后两天均为到期小盈利 → 仍计连亏 2
|
||||
rows = [
|
||||
("e0", day_ms("2026-07-28"), 20.0, "fixed_usdt"),
|
||||
("e1", day_ms("2026-07-29"), 3.5, "expiry"),
|
||||
("e2", day_ms("2026-07-30"), 1.2, "expiry"),
|
||||
]
|
||||
for gid, ms, pnl, reason in rows:
|
||||
db.execute(
|
||||
"""INSERT INTO groups(
|
||||
group_id, status, realized_pnl, close_at_ms, open_at_ms, close_reason
|
||||
) VALUES(?,?,?,?,?,?)""",
|
||||
(gid, "closed", pnl, ms, ms - 3600_000, reason),
|
||||
)
|
||||
assert consecutive_loss_days(db) == 2
|
||||
|
||||
# 再来一天达标盈利 → 连亏清零
|
||||
db.execute(
|
||||
"""INSERT INTO groups(
|
||||
group_id, status, realized_pnl, close_at_ms, open_at_ms, close_reason
|
||||
) VALUES(?,?,?,?,?,?)""",
|
||||
("e3", "closed", 15.0, day_ms("2026-07-31"), day_ms("2026-07-31") - 1000, "fixed_usdt"),
|
||||
)
|
||||
assert consecutive_loss_days(db) == 0
|
||||
db.close()
|
||||
|
||||
@@ -816,7 +816,7 @@ export default function SettingsPage() {
|
||||
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
|
||||
{riskLossPct > 3
|
||||
? "亏损幅度超过 3% 时不可启用倍投。"
|
||||
: "连续亏损日达阈值后,按基础幅度翻倍(不改保存的幅度值)。最多可设翻倍次数。"}
|
||||
: "连续亏损日达阈值后翻倍。到期结算无论盈亏(含小盈利)都按亏损日计;达标平仓盈利才打断连亏。"}
|
||||
</p>
|
||||
</div>
|
||||
{martingaleOn && riskLossPct <= 3 ? (
|
||||
|
||||
Reference in New Issue
Block a user