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
|
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:
|
def consecutive_loss_days(db: Database | None = None) -> int:
|
||||||
"""
|
"""
|
||||||
按上海日历「平仓日」汇总净盈亏,从最近有平仓的一天往前数连续亏损天数。
|
按上海日历「平仓日」汇总倍投口径盈亏,从最近有平仓的一天往前数连续亏损天数。
|
||||||
某日净盈亏 < 0 计为亏损日;无平仓的日历日不计入、不打断(按有成交日序列)。
|
某日合计 < 0 计为亏损日;到期结算组无论盈亏均按亏损计入。
|
||||||
|
无平仓的日历日不计入、不打断(按有成交日序列)。
|
||||||
"""
|
"""
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -280,7 +296,7 @@ def consecutive_loss_days(db: Database | None = None) -> int:
|
|||||||
|
|
||||||
database = db or get_db()
|
database = db or get_db()
|
||||||
rows = database.fetchall(
|
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
|
WHERE status='closed' AND close_at_ms IS NOT NULL
|
||||||
ORDER BY close_at_ms ASC"""
|
ORDER BY close_at_ms ASC"""
|
||||||
)
|
)
|
||||||
@@ -300,7 +316,10 @@ def consecutive_loss_days(db: Database | None = None) -> int:
|
|||||||
.astimezone(sh)
|
.astimezone(sh)
|
||||||
.strftime("%Y-%m-%d")
|
.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:
|
if not day_pnl:
|
||||||
return 0
|
return 0
|
||||||
streak = 0
|
streak = 0
|
||||||
|
|||||||
@@ -242,3 +242,45 @@ def test_martingale_doubles_capped(tmp_path, monkeypatch) -> None:
|
|||||||
assert mg["doubles"] == 3
|
assert mg["doubles"] == 3
|
||||||
assert abs(float(mg["effective_pct"]) - 16.0) < 1e-9
|
assert abs(float(mg["effective_pct"]) - 16.0) < 1e-9
|
||||||
db.close()
|
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" }}>
|
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
|
||||||
{riskLossPct > 3
|
{riskLossPct > 3
|
||||||
? "亏损幅度超过 3% 时不可启用倍投。"
|
? "亏损幅度超过 3% 时不可启用倍投。"
|
||||||
: "连续亏损日达阈值后,按基础幅度翻倍(不改保存的幅度值)。最多可设翻倍次数。"}
|
: "连续亏损日达阈值后翻倍。到期结算无论盈亏(含小盈利)都按亏损日计;达标平仓盈利才打断连亏。"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{martingaleOn && riskLossPct <= 3 ? (
|
{martingaleOn && riskLossPct <= 3 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user