进一步修复 SQLite 并发锁冲突,统一连接与重试机制。
新增 db_conn 模块、缓存 schema 初始化、positions 页 commit,风控读库自动重试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -165,6 +165,6 @@
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
pollPositions();
|
||||
pollTimer = setInterval(pollPositions, 2000);
|
||||
pollTimer = setInterval(pollPositions, 3000);
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user