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:
dekun
2026-08-07 15:08:44 +08:00
parent 930e6c26c5
commit 04e5c11027
3 changed files with 66 additions and 5 deletions
+23 -4
View File
@@ -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
+42
View File
@@ -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()
+1 -1
View File
@@ -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 ? (