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:
+119
-25
@@ -141,11 +141,11 @@ def _normalize_epoch_ms(ms: int, ref_now_ms: Optional[int] = None) -> int:
|
||||
return corrected
|
||||
|
||||
|
||||
def _sanitize_last_close_ms(last_ms: int, now_ms: int) -> int:
|
||||
"""平仓时刻不得显著晚于当前时间(脏数据/时区混用)。"""
|
||||
def _sanitize_last_close_ms(last_ms: int, now_ms: int) -> Optional[int]:
|
||||
"""平仓时刻须不晚于当前(允许 1 分钟时钟偏差);显著未来视为无效锚点。"""
|
||||
slack_ms = 60 * 1000
|
||||
if last_ms > now_ms + slack_ms:
|
||||
return now_ms
|
||||
return None
|
||||
return last_ms
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ def _cooloff_hours_value(row) -> float:
|
||||
|
||||
|
||||
def _resolved_cooloff_until_ms(row, now_ms: int) -> Optional[int]:
|
||||
"""冷静期结束 = last_close + cooloff_hours;剩余不得超过配置时长。"""
|
||||
"""冷静期结束 = last_close + cooloff_hours;无效/已过期锚点不再重启计时。"""
|
||||
hours = _cooloff_hours_value(row)
|
||||
journal_h = cooling_hours_manual_journal()
|
||||
duration_ms = _cooloff_duration_ms(hours)
|
||||
@@ -166,27 +166,42 @@ def _resolved_cooloff_until_ms(row, now_ms: int) -> Optional[int]:
|
||||
stored_raw = _cooloff_until_ms(row)
|
||||
|
||||
if last_raw is not None:
|
||||
last_ms = _sanitize_last_close_ms(
|
||||
_normalize_epoch_ms(int(last_raw), now_ms), now_ms
|
||||
)
|
||||
end_ms = last_ms + duration_ms
|
||||
max_end_ms = now_ms + duration_ms
|
||||
if end_ms > max_end_ms:
|
||||
end_ms = max_end_ms
|
||||
if end_ms > now_ms:
|
||||
return end_ms
|
||||
try:
|
||||
last_ms = _sanitize_last_close_ms(
|
||||
_normalize_epoch_ms(int(last_raw), now_ms), now_ms
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
last_ms = None
|
||||
if last_ms is not None:
|
||||
end_ms = last_ms + duration_ms
|
||||
if end_ms > now_ms:
|
||||
return end_ms
|
||||
if hours <= journal_h + 1e-6:
|
||||
return None
|
||||
|
||||
if stored_raw is None:
|
||||
return None
|
||||
stored_ms = _normalize_epoch_ms(int(stored_raw), now_ms)
|
||||
max_end_ms = now_ms + duration_ms
|
||||
if stored_ms > max_end_ms:
|
||||
stored_ms = max_end_ms
|
||||
return stored_ms if stored_ms > now_ms else None
|
||||
|
||||
|
||||
def _clear_inactive_cooloff(
|
||||
conn,
|
||||
*,
|
||||
now: Optional[datetime] = None,
|
||||
) -> None:
|
||||
"""冷静期已结束或锚点无效时清库,避免重启后误读旧冻结。"""
|
||||
conn.execute(
|
||||
"""UPDATE account_risk_state SET
|
||||
cooloff_until_ms=NULL,
|
||||
cooloff_hours=NULL,
|
||||
last_close_at_ms=NULL,
|
||||
updated_at=?
|
||||
WHERE id=1""",
|
||||
((now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"),),
|
||||
)
|
||||
|
||||
|
||||
def _freeze_tier_from_remaining_ms(remaining_ms: int, hours: float) -> str:
|
||||
journal_h = cooling_hours_manual_journal()
|
||||
rh = remaining_ms / 3600000.0
|
||||
@@ -301,6 +316,67 @@ def _set_cooloff_until(
|
||||
)
|
||||
|
||||
|
||||
def _ms_trading_day_label(ms: int) -> str:
|
||||
dt = datetime.fromtimestamp(ms / 1000, tz=_app_tz())
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _parse_journal_close_ms(raw: Any) -> Optional[int]:
|
||||
if raw is None:
|
||||
return None
|
||||
s = str(raw).strip()
|
||||
if not s:
|
||||
return None
|
||||
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M:%S", "%Y-%m-%d %H:%M"):
|
||||
try:
|
||||
dt = datetime.strptime(s[:19] if len(s) > 16 else s, fmt)
|
||||
return _now_ms(dt)
|
||||
except ValueError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _latest_journaled_manual_close_ms(conn, trading_day: str) -> Optional[int]:
|
||||
"""当日最近一条已复盘的手动平仓时刻(journal 有说明)。"""
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"""SELECT close_datetime FROM journal_entries
|
||||
WHERE early_exit_trigger='手动平仓'
|
||||
AND early_exit_note IS NOT NULL AND TRIM(early_exit_note) <> ''
|
||||
ORDER BY close_datetime DESC"""
|
||||
).fetchall()
|
||||
except Exception:
|
||||
return None
|
||||
td = (trading_day or "").strip()
|
||||
best: Optional[int] = None
|
||||
for row in rows:
|
||||
ms = _parse_journal_close_ms(_row_get(row, "close_datetime"))
|
||||
if ms is None:
|
||||
continue
|
||||
if td and _ms_trading_day_label(ms) != td:
|
||||
continue
|
||||
if best is None or ms > best:
|
||||
best = ms
|
||||
return best
|
||||
|
||||
|
||||
def _journaled_manual_cooloff_expired(
|
||||
conn, *, trading_day: str, now_ms: int, pending: Any
|
||||
) -> bool:
|
||||
"""当日手动平仓已复盘且 1h 冷静期结束,且无待复盘的新平仓。"""
|
||||
if pending is not None:
|
||||
try:
|
||||
if int(pending) != 0:
|
||||
return False
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
close_ms = _latest_journaled_manual_close_ms(conn, trading_day)
|
||||
if close_ms is None:
|
||||
return False
|
||||
journal_ms = _cooloff_duration_ms(cooling_hours_manual_journal())
|
||||
return close_ms + journal_ms <= now_ms
|
||||
|
||||
|
||||
def _cooloff_until_ms(row) -> Optional[int]:
|
||||
raw = _row_get(row, "cooloff_until_ms")
|
||||
try:
|
||||
@@ -317,39 +393,46 @@ def _repair_stale_cooloff_row(
|
||||
resolved_until_ms: Optional[int],
|
||||
now: Optional[datetime] = None,
|
||||
) -> None:
|
||||
"""脏数据(未来 last_close / 超长 until)读时写回修正。"""
|
||||
"""脏数据读时写回:过期/无效则清库,否则对齐 until / last_close。"""
|
||||
last_raw = _row_get(row, "last_close_at_ms")
|
||||
stored_raw = _cooloff_until_ms(row)
|
||||
if last_raw is None and stored_raw is None:
|
||||
return
|
||||
if resolved_until_ms is None:
|
||||
if last_raw is not None or stored_raw is not None:
|
||||
_clear_inactive_cooloff(conn, now=now)
|
||||
return
|
||||
dirty = False
|
||||
new_last: Optional[int] = None
|
||||
if last_raw is not None:
|
||||
try:
|
||||
norm = _normalize_epoch_ms(int(last_raw), now_ms)
|
||||
sanitized = _sanitize_last_close_ms(norm, now_ms)
|
||||
new_last = sanitized
|
||||
if sanitized != int(last_raw):
|
||||
if sanitized is None:
|
||||
dirty = True
|
||||
else:
|
||||
new_last = sanitized
|
||||
if sanitized != int(last_raw):
|
||||
dirty = True
|
||||
except (TypeError, ValueError):
|
||||
new_last = None
|
||||
dirty = True
|
||||
if stored_raw is not None:
|
||||
stored_norm = _normalize_epoch_ms(int(stored_raw), now_ms)
|
||||
if resolved_until_ms is None:
|
||||
dirty = True
|
||||
elif abs(stored_norm - int(resolved_until_ms)) > 60 * 1000:
|
||||
if abs(stored_norm - int(resolved_until_ms)) > 60 * 1000:
|
||||
dirty = True
|
||||
if not dirty:
|
||||
return
|
||||
conn.execute(
|
||||
"""UPDATE account_risk_state SET
|
||||
cooloff_until_ms=?,
|
||||
cooloff_hours=?,
|
||||
last_close_at_ms=?,
|
||||
updated_at=?
|
||||
WHERE id=1""",
|
||||
(
|
||||
resolved_until_ms,
|
||||
new_last if resolved_until_ms else None,
|
||||
_row_get(row, "cooloff_hours"),
|
||||
new_last,
|
||||
(now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
),
|
||||
)
|
||||
@@ -382,6 +465,8 @@ def _journal_cooloff_until_ms(row, now_ms: int, journal_hours: float) -> int:
|
||||
_normalize_epoch_ms(int(last_close_ms), now_ms), now_ms
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
base_ms = None
|
||||
if base_ms is None:
|
||||
base_ms = now_ms
|
||||
else:
|
||||
base_ms = now_ms
|
||||
@@ -625,7 +710,16 @@ def compute_account_risk_status(
|
||||
row = _sync_trading_day(conn, trading_day, now=now)
|
||||
now_ms = _now_ms(now)
|
||||
daily_frozen = int(_row_get(row, "daily_frozen") or 0) == 1
|
||||
pending = _row_get(row, "pending_journal_trade_id")
|
||||
cooloff_until_ms = _resolved_cooloff_until_ms(row, now_ms)
|
||||
if (
|
||||
not daily_frozen
|
||||
and cooloff_until_ms is not None
|
||||
and _journaled_manual_cooloff_expired(
|
||||
conn, trading_day=trading_day, now_ms=now_ms, pending=pending
|
||||
)
|
||||
):
|
||||
cooloff_until_ms = None
|
||||
if not daily_frozen:
|
||||
_repair_stale_cooloff_row(
|
||||
conn, row, now_ms=now_ms, resolved_until_ms=cooloff_until_ms, now=now
|
||||
@@ -668,7 +762,7 @@ def compute_account_risk_status(
|
||||
else None,
|
||||
"manual_close_count": manual_close_count,
|
||||
"daily_frozen": daily_frozen,
|
||||
"pending_journal_trade_id": _row_get(row, "pending_journal_trade_id"),
|
||||
"pending_journal_trade_id": pending,
|
||||
"freeze_remaining_sec": freeze_remaining_sec if not can_trade else 0,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user