Fix false freeze after restart from stale account_risk_state.

Clear expired cooloff on read, never restart timer from invalid future anchors, and reconcile with journaled manual closes when the 1h window already ended.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-18 22:14:58 +08:00
parent 9330e356fc
commit ce172a7cee
3 changed files with 188 additions and 31 deletions
+66 -5
View File
@@ -33,6 +33,16 @@ def _mem_conn():
return conn
def _mem_conn_with_journal():
conn = _mem_conn()
conn.execute(
"""CREATE TABLE IF NOT EXISTS journal_entries (
close_datetime TEXT, early_exit_trigger TEXT, early_exit_note TEXT
)"""
)
return conn
def _local_ms(dt_naive: datetime) -> int:
return int(dt_naive.replace(tzinfo=APP_TZ).timestamp() * 1000)
@@ -257,6 +267,58 @@ class AccountRiskLibTests(unittest.TestCase):
st = compute_account_risk_status(conn, trading_day="2026-06-18", now=now)
self.assertEqual(st["status"], STATUS_NORMAL)
self.assertTrue(st["can_trade"])
row = conn.execute(
"SELECT cooloff_until_ms, cooloff_hours, last_close_at_ms FROM account_risk_state WHERE id=1"
).fetchone()
self.assertIsNone(row["cooloff_until_ms"])
self.assertIsNone(row["last_close_at_ms"])
def test_corrupted_anchor_cleared_when_journaled_manual_expired(self):
"""上一版误把 last_close 写成近期时刻时,已复盘且 1h 已过的仍应显示正常。"""
conn = _mem_conn_with_journal()
now = datetime(2026, 6, 18, 22, 30, 0)
now_ms = _local_ms(now)
bad_last = now_ms - 60 * 1000
conn.execute(
"""UPDATE account_risk_state SET
trading_day='2026-06-18',
manual_close_count=1,
cooloff_until_ms=?,
cooloff_hours=1,
last_close_at_ms=?,
pending_journal_trade_id=NULL,
daily_frozen=0
WHERE id=1""",
(bad_last + 3600 * 1000, bad_last),
)
conn.execute(
"INSERT INTO journal_entries (close_datetime, early_exit_trigger, early_exit_note) VALUES (?,?,?)",
("2026-06-18 17:56:00", "手动平仓", "按计划离场"),
)
st = compute_account_risk_status(conn, trading_day="2026-06-18", now=now)
self.assertEqual(st["status"], STATUS_NORMAL)
self.assertTrue(st["can_trade"])
def test_future_last_close_does_not_restart_cooloff(self):
"""脏数据 last_close 在未来时,不应重启 1h/4h 冻结。"""
conn = _mem_conn()
now = datetime(2026, 6, 18, 22, 30, 0)
now_ms = _local_ms(now)
future_close = now_ms + 49 * 60 * 1000
conn.execute(
"""UPDATE account_risk_state SET
trading_day='2026-06-18',
manual_close_count=1,
cooloff_until_ms=?,
cooloff_hours=1,
last_close_at_ms=?,
daily_frozen=0
WHERE id=1""",
(future_close + 3600 * 1000, future_close),
)
st = compute_account_risk_status(conn, trading_day="2026-06-18", now=now)
self.assertEqual(st["status"], STATUS_NORMAL)
self.assertTrue(st["can_trade"])
def test_active_4h_countdown_matches_tier(self):
conn = _mem_conn()
@@ -313,11 +375,8 @@ class AccountRiskLibTests(unittest.TestCase):
(future_close + 4 * 3600 * 1000, future_close),
)
st = compute_account_risk_status(conn, trading_day="2026-06-18", now=now)
self.assertEqual(st["status"], STATUS_FREEZE_4H)
self.assertLessEqual(st["freeze_remaining_sec"], 4 * 3600 + 2)
self.assertGreater(st["freeze_remaining_sec"], 3 * 3600 + 58 * 60)
row = conn.execute("SELECT last_close_at_ms, cooloff_until_ms FROM account_risk_state WHERE id=1").fetchone()
self.assertLessEqual(int(row["last_close_at_ms"]), now_ms + 60 * 1000)
self.assertEqual(st["status"], STATUS_NORMAL)
self.assertTrue(st["can_trade"])
def test_legacy_naive_utc_ms_countdown_normalized(self):
conn = _mem_conn()
@@ -366,6 +425,8 @@ class AccountRiskLibTests(unittest.TestCase):
later = datetime(2026, 6, 14, 13, 0, 0)
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=later)
self.assertEqual(st["status"], STATUS_NORMAL)
row = conn.execute("SELECT cooloff_until_ms FROM account_risk_state WHERE id=1").fetchone()
self.assertIsNone(row["cooloff_until_ms"])
def test_trading_day_reset_clears_daily_frozen(self):
conn = _mem_conn()