fix(risk): stop stale 4h cooloff after 1h journal expires

Anchor last_close on journal save, ignore leftover stored until when 1h window ended, and clear expired cooloff on trading-day rollover.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-18 22:04:14 +08:00
parent 9c778e0232
commit c73944581c
3 changed files with 105 additions and 31 deletions
+43 -28
View File
@@ -146,26 +146,26 @@ def _cooloff_hours_value(row) -> float:
def _resolved_cooloff_until_ms(row, now_ms: int) -> Optional[int]:
"""取仍有效的冷静期结束时刻(多源时用最短未过期时间,避免旧 4h 覆盖复盘后的 1h)"""
raw_until = _cooloff_until_ms(row)
last = _row_get(row, "last_close_at_ms")
"""冷静期结束时刻 = last_close + cooloff_hours1h 档过期后忽略旧 4h stored"""
hours = _cooloff_hours_value(row)
candidates: list[int] = []
if raw_until is not None:
try:
candidates.append(_normalize_epoch_ms(int(raw_until), now_ms))
except (TypeError, ValueError):
pass
if last is not None:
try:
last_i = _normalize_epoch_ms(int(last), now_ms)
candidates.append(last_i + int(hours * 3600 * 1000))
except (TypeError, ValueError):
pass
active = [c for c in candidates if c > now_ms]
if not active:
journal_h = cooling_hours_manual_journal()
last_raw = _row_get(row, "last_close_at_ms")
stored_raw = _cooloff_until_ms(row)
last_ms = (
_normalize_epoch_ms(int(last_raw), now_ms) if last_raw is not None else None
)
if last_ms is not None:
end_ms = last_ms + int(hours * 3600 * 1000)
if end_ms > now_ms:
return end_ms
if hours <= journal_h + 1e-6:
return None
if stored_raw is None:
return None
return min(active)
stored_ms = _normalize_epoch_ms(int(stored_raw), now_ms)
return stored_ms if stored_ms > now_ms else None
def _freeze_tier_from_remaining_ms(remaining_ms: int) -> str:
@@ -195,14 +195,26 @@ def _sync_trading_day(conn, trading_day: str, now: Optional[datetime] = None) ->
td = (trading_day or "").strip()
stored = str(_row_get(row, "trading_day") or "").strip()
if stored != td:
now_ms = _now_ms(now)
cooloff_active = _resolved_cooloff_until_ms(row, now_ms)
conn.execute(
"""UPDATE account_risk_state SET
trading_day=?,
manual_close_count=0,
daily_frozen=0,
cooloff_until_ms=?,
cooloff_hours=?,
last_close_at_ms=?,
pending_journal_trade_id=NULL,
updated_at=?
WHERE id=1""",
(td, (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")),
(
td,
cooloff_active,
_row_get(row, "cooloff_hours") if cooloff_active else None,
_row_get(row, "last_close_at_ms") if cooloff_active else None,
(now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"),
),
)
row = _load_state(conn)
return row
@@ -270,8 +282,7 @@ def _cooloff_until_ms(row) -> Optional[int]:
def _journal_can_reduce_cooloff(row, pending, now_ms: int) -> bool:
if int(_row_get(row, "daily_frozen") or 0) == 1:
return False
until_ms = _cooloff_until_ms(row)
if until_ms is None or until_ms <= now_ms:
if _resolved_cooloff_until_ms(row, now_ms) is None:
return False
journal_h = cooling_hours_manual_journal()
cooloff_h = float(_row_get(row, "cooloff_hours") or cooling_hours_manual())
@@ -297,11 +308,9 @@ def _journal_cooloff_until_ms(row, now_ms: int, journal_hours: float) -> int:
else:
base_ms = now_ms
until_from_close = base_ms + journal_ms
until_ms = until_from_close if until_from_close > now_ms else now_ms + journal_ms
current_until = _resolved_cooloff_until_ms(row, now_ms)
if current_until is not None and until_ms > current_until:
until_ms = current_until
return until_ms
if until_from_close > now_ms:
return until_from_close
return now_ms + journal_ms
def _set_daily_frozen(conn, *, trading_day: str, now: Optional[datetime] = None) -> None:
@@ -445,10 +454,16 @@ def on_journal_saved(
hours=journal_h,
now=now,
)
anchor_ms = until_ms - int(journal_h * 3600 * 1000)
conn.execute(
"UPDATE account_risk_state SET pending_journal_trade_id=NULL, updated_at=? WHERE id=1",
((now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"),),
"""UPDATE account_risk_state SET
pending_journal_trade_id=NULL,
last_close_at_ms=?,
updated_at=?
WHERE id=1""",
(int(anchor_ms), (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")),
)
return
def apply_manual_close_journal_cooloff(