进一步修复 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_stream import kline_hub, sse_format
from kline_chart import generate_review_kline_chart, fetch_market_klines, MARKET_PERIODS 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 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 strategy.strategy_db import init_strategy_tables
from install_trading import install_trading from install_trading import install_trading
from vnpy_bridge import try_init_vnpy from vnpy_bridge import try_init_vnpy
@@ -155,17 +156,9 @@ def expire_old_plans():
conn.commit() conn.commit()
conn.close() conn.close()
# —————————————— 设置读写 ——————————————
def get_db(): def get_db():
conn = sqlite3.connect(DB_PATH, timeout=30) return connect_db()
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA busy_timeout=30000")
try:
conn.execute("PRAGMA journal_mode=WAL")
except sqlite3.OperationalError:
pass
return conn
def get_setting(key: str, default: str = "") -> str: def get_setting(key: str, default: str = "") -> str:
@@ -315,6 +308,8 @@ def init_db():
updated_at TEXT NOT NULL)''') updated_at TEXT NOT NULL)''')
ensure_kline_tables(conn) ensure_kline_tables(conn)
init_strategy_tables(conn) init_strategy_tables(conn)
from risk.account_risk_lib import ensure_account_risk_schema
ensure_account_risk_schema(conn)
conn.commit() conn.commit()
conn.close() 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 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") 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") DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
DEFAULT_JSON = os.path.join(DATA_DIR, "fee_rates.json") 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(): def _get_db():
conn = sqlite3.connect(DB_PATH) return connect_db()
conn.row_factory = sqlite3.Row
return conn
def get_fee_multiplier() -> float: 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 @login_required
def positions(): def positions():
conn = get_db() conn = get_db()
try:
init_strategy_tables(conn) init_strategy_tables(conn)
mode = get_trading_mode(get_setting) mode = get_trading_mode(get_setting)
ctp_st = ctp_status(mode) 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( roll_count = conn.execute(
"SELECT COUNT(*) AS n FROM roll_groups WHERE status='active'" "SELECT COUNT(*) AS n FROM roll_groups WHERE status='active'"
).fetchone()["n"] ).fetchone()["n"]
conn.close() conn.commit()
sizing = get_sizing_mode(get_setting) sizing = get_sizing_mode(get_setting)
return render_template( return render_template(
"trade.html", "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 "固定张数", sizing_mode_label="以损定仓" if sizing == MODE_RISK else "固定张数",
risk_percent=get_risk_percent(get_setting), risk_percent=get_risk_percent(get_setting),
) )
finally:
conn.close()
@app.route("/recommend") @app.route("/recommend")
@login_required @login_required
+4 -3
View File
@@ -11,6 +11,7 @@ from zoneinfo import ZoneInfo
import requests import requests
from symbols import ths_to_codes from symbols import ths_to_codes
from db_conn import connect_db
from kline_store import ensure_kline_tables, get_cached_entry, save_bars from kline_store import ensure_kline_tables, get_cached_entry, save_bars
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -234,7 +235,7 @@ def fetch_market_klines(
if db_path and chart_sym and not force_remote: if db_path and chart_sym and not force_remote:
try: try:
conn = sqlite3.connect(db_path) conn = connect_db(db_path)
cached = get_cached_entry(conn, chart_sym, p) cached = get_cached_entry(conn, chart_sym, p)
conn.close() conn.close()
if cached and cached.get("fresh"): if cached and cached.get("fresh"):
@@ -251,7 +252,7 @@ def fetch_market_klines(
source = "remote" source = "remote"
if db_path and chart_sym: if db_path and chart_sym:
try: try:
conn = sqlite3.connect(db_path) conn = connect_db(db_path)
ensure_kline_tables(conn) ensure_kline_tables(conn)
save_bars(conn, chart_sym, p, remote_bars) save_bars(conn, chart_sym, p, remote_bars)
meta = conn.execute( meta = conn.execute(
@@ -264,7 +265,7 @@ def fetch_market_klines(
logger.warning("kline cache write failed %s %s: %s", chart_sym, p, exc) logger.warning("kline cache write failed %s %s: %s", chart_sym, p, exc)
elif not bars and db_path and chart_sym: elif not bars and db_path and chart_sym:
try: try:
conn = sqlite3.connect(db_path) conn = connect_db(db_path)
cached = get_cached_entry(conn, chart_sym, p) cached = get_cached_entry(conn, chart_sym, p)
conn.close() conn.close()
if cached and cached.get("bars"): 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: if is_trading_session() and sub.period in FAST_PERIODS:
return True return True
try: try:
import sqlite3 from db_conn import connect_db
conn = sqlite3.connect(db_path) conn = connect_db(db_path)
ensure_kline_tables(conn) ensure_kline_tables(conn)
meta = load_meta(conn, chart_sym, sub.period) meta = load_meta(conn, chart_sym, sub.period)
conn.close() conn.close()
+24 -1
View File
@@ -2,10 +2,14 @@
from __future__ import annotations from __future__ import annotations
import os import os
import sqlite3
import time
from datetime import datetime from datetime import datetime
from typing import Any, Optional from typing import Any, Callable, Optional, TypeVar
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
T = TypeVar("T")
STATUS_NORMAL = "normal" STATUS_NORMAL = "normal"
STATUS_FREEZE_1H = "freeze_1h" STATUS_FREEZE_1H = "freeze_1h"
STATUS_FREEZE_4H = "freeze_4h" STATUS_FREEZE_4H = "freeze_4h"
@@ -79,6 +83,21 @@ def trading_day_reset_hour() -> int:
_SCHEMA_READY = False _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: def ensure_account_risk_schema(conn) -> None:
global _SCHEMA_READY global _SCHEMA_READY
if _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 get_risk_status(conn, *, now: Optional[datetime] = None) -> dict:
def _load() -> dict:
ensure_account_risk_schema(conn) ensure_account_risk_schema(conn)
row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone() row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone()
td = trading_day_label(now) 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<>?", "UPDATE account_risk_state SET trading_day=?, manual_close_count=0, daily_frozen=0 WHERE id=1 AND trading_day<>?",
(td, td), (td, td),
) )
conn.commit()
row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone() row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone()
now_ms = _now_ms(now) now_ms = _now_ms(now)
@@ -271,6 +292,8 @@ def get_risk_status(conn, *, now: Optional[datetime] = None) -> dict:
"max_active_positions": mx, "max_active_positions": mx,
} }
return _db_retry(_load)
def assert_can_open(conn) -> Optional[str]: def assert_can_open(conn) -> Optional[str]:
rs = get_risk_status(conn) rs = get_risk_status(conn)
+1 -1
View File
@@ -165,6 +165,6 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
pollPositions(); 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: def init_strategy_tables(conn) -> None:
global _TABLES_READY
if _TABLES_READY:
return
for sql in ( for sql in (
ROLL_GROUPS_SQL, ROLL_GROUPS_SQL,
ROLL_LEGS_SQL, ROLL_LEGS_SQL,
@@ -129,3 +135,5 @@ def init_strategy_tables(conn) -> None:
conn.execute(sql) conn.execute(sql)
if not conn.execute("SELECT id FROM ctp_sim_account WHERE id=1").fetchone(): 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.execute("INSERT INTO ctp_sim_account (id, balance, available) VALUES (1, 100000, 100000)")
conn.commit()
_TABLES_READY = True