进一步修复 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_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
@@ -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 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
@@ -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
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -165,6 +165,6 @@
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
pollPositions();
|
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:
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user