进一步修复 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
+4 -9
View File
@@ -34,6 +34,7 @@ from kline_store import ensure_kline_tables
from kline_stream import kline_hub, sse_format
from kline_chart import generate_review_kline_chart, fetch_market_klines, MARKET_PERIODS
from market import get_price as market_get_price, set_ths_refresh_token, get_quote_source_label
from db_conn import connect_db
from strategy.strategy_db import init_strategy_tables
from install_trading import install_trading
from vnpy_bridge import try_init_vnpy
@@ -155,17 +156,9 @@ def expire_old_plans():
conn.commit()
conn.close()
# —————————————— 设置读写 ——————————————
def get_db():
conn = sqlite3.connect(DB_PATH, timeout=30)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA busy_timeout=30000")
try:
conn.execute("PRAGMA journal_mode=WAL")
except sqlite3.OperationalError:
pass
return conn
return connect_db()
def get_setting(key: str, default: str = "") -> str:
@@ -315,6 +308,8 @@ def init_db():
updated_at TEXT NOT NULL)''')
ensure_kline_tables(conn)
init_strategy_tables(conn)
from risk.account_risk_lib import ensure_account_risk_schema
ensure_account_risk_schema(conn)
conn.commit()
conn.close()
+19
View File
@@ -0,0 +1,19 @@
"""SQLite 连接统一配置(WAL + busy_timeout,降低并发锁冲突)。"""
from __future__ import annotations
import os
import sqlite3
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "futures.db")
def connect_db(path: str | None = None) -> sqlite3.Connection:
db_path = path or DB_PATH
conn = sqlite3.connect(db_path, timeout=30, check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA busy_timeout=30000")
try:
conn.execute("PRAGMA journal_mode=WAL")
except sqlite3.OperationalError:
pass
return conn
+3 -3
View File
@@ -8,6 +8,8 @@ from typing import Optional
from contract_specs import get_contract_spec
from db_conn import connect_db
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "futures.db")
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
DEFAULT_JSON = os.path.join(DATA_DIR, "fee_rates.json")
@@ -32,9 +34,7 @@ def product_from_code(ths_code: str) -> str:
def _get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
return connect_db()
def get_fee_multiplier() -> float:
+4 -1
View File
@@ -282,6 +282,7 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
@login_required
def positions():
conn = get_db()
try:
init_strategy_tables(conn)
mode = get_trading_mode(get_setting)
ctp_st = ctp_status(mode)
@@ -298,7 +299,7 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
roll_count = conn.execute(
"SELECT COUNT(*) AS n FROM roll_groups WHERE status='active'"
).fetchone()["n"]
conn.close()
conn.commit()
sizing = get_sizing_mode(get_setting)
return render_template(
"trade.html",
@@ -316,6 +317,8 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
sizing_mode_label="以损定仓" if sizing == MODE_RISK else "固定张数",
risk_percent=get_risk_percent(get_setting),
)
finally:
conn.close()
@app.route("/recommend")
@login_required
+4 -3
View File
@@ -11,6 +11,7 @@ from zoneinfo import ZoneInfo
import requests
from symbols import ths_to_codes
from db_conn import connect_db
from kline_store import ensure_kline_tables, get_cached_entry, save_bars
logger = logging.getLogger(__name__)
@@ -234,7 +235,7 @@ def fetch_market_klines(
if db_path and chart_sym and not force_remote:
try:
conn = sqlite3.connect(db_path)
conn = connect_db(db_path)
cached = get_cached_entry(conn, chart_sym, p)
conn.close()
if cached and cached.get("fresh"):
@@ -251,7 +252,7 @@ def fetch_market_klines(
source = "remote"
if db_path and chart_sym:
try:
conn = sqlite3.connect(db_path)
conn = connect_db(db_path)
ensure_kline_tables(conn)
save_bars(conn, chart_sym, p, remote_bars)
meta = conn.execute(
@@ -264,7 +265,7 @@ def fetch_market_klines(
logger.warning("kline cache write failed %s %s: %s", chart_sym, p, exc)
elif not bars and db_path and chart_sym:
try:
conn = sqlite3.connect(db_path)
conn = connect_db(db_path)
cached = get_cached_entry(conn, chart_sym, p)
conn.close()
if cached and cached.get("bars"):
+2 -2
View File
@@ -102,8 +102,8 @@ class KlineStreamHub:
if is_trading_session() and sub.period in FAST_PERIODS:
return True
try:
import sqlite3
conn = sqlite3.connect(db_path)
from db_conn import connect_db
conn = connect_db(db_path)
ensure_kline_tables(conn)
meta = load_meta(conn, chart_sym, sub.period)
conn.close()
+24 -1
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,6 +228,7 @@ def reduce_cooloff_after_journal(conn, *, trading_day: str, now: Optional[dateti
def get_risk_status(conn, *, now: Optional[datetime] = None) -> dict:
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)
@@ -218,6 +238,7 @@ def get_risk_status(conn, *, now: Optional[datetime] = None) -> dict:
"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)
@@ -271,6 +292,8 @@ def get_risk_status(conn, *, now: Optional[datetime] = None) -> dict:
"max_active_positions": mx,
}
return _db_retry(_load)
def assert_can_open(conn) -> Optional[str]:
rs = get_risk_status(conn)
+1 -1
View File
@@ -165,6 +165,6 @@
document.addEventListener('DOMContentLoaded', function () {
pollPositions();
pollTimer = setInterval(pollPositions, 2000);
pollTimer = setInterval(pollPositions, 3000);
});
})();
+8
View File
@@ -116,7 +116,13 @@ CREATE TABLE IF NOT EXISTS ctp_sim_positions (
"""
_TABLES_READY = False
def init_strategy_tables(conn) -> None:
global _TABLES_READY
if _TABLES_READY:
return
for sql in (
ROLL_GROUPS_SQL,
ROLL_LEGS_SQL,
@@ -129,3 +135,5 @@ def init_strategy_tables(conn) -> None:
conn.execute(sql)
if not conn.execute("SELECT id FROM ctp_sim_account WHERE id=1").fetchone():
conn.execute("INSERT INTO ctp_sim_account (id, balance, available) VALUES (1, 100000, 100000)")
conn.commit()
_TABLES_READY = True