进一步修复 SQLite 并发锁冲突,统一连接与重试机制。

新增 db_conn 模块、缓存 schema 初始化、positions 页 commit,风控读库自动重试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-24 10:30:26 +08:00
parent 1688452f3f
commit 55d95b4c39
9 changed files with 155 additions and 106 deletions
+77 -54
View File
@@ -2,10 +2,14 @@
from __future__ import annotations
import os
import sqlite3
import time
from datetime import datetime
from typing import Any, Optional
from typing import Any, Callable, Optional, TypeVar
from zoneinfo import ZoneInfo
T = TypeVar("T")
STATUS_NORMAL = "normal"
STATUS_FREEZE_1H = "freeze_1h"
STATUS_FREEZE_4H = "freeze_4h"
@@ -79,6 +83,21 @@ def trading_day_reset_hour() -> int:
_SCHEMA_READY = False
def _db_retry(action: Callable[[], T], *, retries: int = 8, base_delay: float = 0.03) -> T:
last: sqlite3.OperationalError | None = None
for i in range(retries):
try:
return action()
except sqlite3.OperationalError as exc:
if "locked" not in str(exc).lower():
raise
last = exc
time.sleep(base_delay * (2 ** i))
if last is not None:
raise last
raise RuntimeError("db retry failed")
def ensure_account_risk_schema(conn) -> None:
global _SCHEMA_READY
if _SCHEMA_READY:
@@ -209,67 +228,71 @@ def reduce_cooloff_after_journal(conn, *, trading_day: str, now: Optional[dateti
def get_risk_status(conn, *, now: Optional[datetime] = None) -> dict:
ensure_account_risk_schema(conn)
row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone()
td = trading_day_label(now)
stored = str(_row_get(row, "trading_day") or "")
if stored != td:
conn.execute(
"UPDATE account_risk_state SET trading_day=?, manual_close_count=0, daily_frozen=0 WHERE id=1 AND trading_day<>?",
(td, td),
)
def _load() -> dict:
ensure_account_risk_schema(conn)
row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone()
td = trading_day_label(now)
stored = str(_row_get(row, "trading_day") or "")
if stored != td:
conn.execute(
"UPDATE account_risk_state SET trading_day=?, manual_close_count=0, daily_frozen=0 WHERE id=1 AND trading_day<>?",
(td, td),
)
conn.commit()
row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone()
now_ms = _now_ms(now)
daily = int(_row_get(row, "daily_frozen") or 0) == 1
until = _row_get(row, "cooloff_until_ms")
active = count_active_trade_monitors(conn)
mx = max_active_positions()
pos_limit = active >= mx
now_ms = _now_ms(now)
daily = int(_row_get(row, "daily_frozen") or 0) == 1
until = _row_get(row, "cooloff_until_ms")
active = count_active_trade_monitors(conn)
mx = max_active_positions()
pos_limit = active >= mx
if daily:
if daily:
return {
"status": STATUS_DAILY,
"status_label": STATUS_LABELS[STATUS_DAILY],
"can_trade": False,
"can_roll": False,
"reason": "当日日冻结,禁止新开仓",
"active_count": active,
"max_active_positions": mx,
}
if until and int(until) > now_ms:
rem = int((int(until) - now_ms) / 1000)
hours = float(_row_get(row, "cooloff_hours") or cooling_hours_manual())
st = STATUS_FREEZE_1H if hours <= cooling_hours_manual_journal() + 0.01 else STATUS_FREEZE_4H
return {
"status": st,
"status_label": STATUS_LABELS[st],
"can_trade": False,
"can_roll": pos_limit,
"reason": f"冷静期中,剩余约 {rem // 3600}h {(rem % 3600) // 60}m",
"freeze_remaining_sec": rem,
"active_count": active,
"max_active_positions": mx,
}
if pos_limit:
return {
"status": STATUS_FREEZE_POSITION,
"status_label": STATUS_LABELS[STATUS_FREEZE_POSITION],
"can_trade": False,
"can_roll": True,
"reason": f"已达仓位上限 {active}/{mx}",
"active_count": active,
"max_active_positions": mx,
}
return {
"status": STATUS_DAILY,
"status_label": STATUS_LABELS[STATUS_DAILY],
"can_trade": False,
"can_roll": False,
"reason": "当日日冻结,禁止新开仓",
"active_count": active,
"max_active_positions": mx,
}
if until and int(until) > now_ms:
rem = int((int(until) - now_ms) / 1000)
hours = float(_row_get(row, "cooloff_hours") or cooling_hours_manual())
st = STATUS_FREEZE_1H if hours <= cooling_hours_manual_journal() + 0.01 else STATUS_FREEZE_4H
return {
"status": st,
"status_label": STATUS_LABELS[st],
"can_trade": False,
"can_roll": pos_limit,
"reason": f"冷静期中,剩余约 {rem // 3600}h {(rem % 3600) // 60}m",
"freeze_remaining_sec": rem,
"active_count": active,
"max_active_positions": mx,
}
if pos_limit:
return {
"status": STATUS_FREEZE_POSITION,
"status_label": STATUS_LABELS[STATUS_FREEZE_POSITION],
"can_trade": False,
"status": STATUS_NORMAL,
"status_label": STATUS_LABELS[STATUS_NORMAL],
"can_trade": True,
"can_roll": True,
"reason": f"已达仓位上限 {active}/{mx}",
"reason": "可新开仓",
"active_count": active,
"max_active_positions": mx,
}
return {
"status": STATUS_NORMAL,
"status_label": STATUS_LABELS[STATUS_NORMAL],
"can_trade": True,
"can_roll": True,
"reason": "可新开仓",
"active_count": active,
"max_active_positions": mx,
}
return _db_retry(_load)
def assert_can_open(conn) -> Optional[str]: