进一步修复 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:
|
||||
|
||||
+37
-34
@@ -282,40 +282,43 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
|
||||
@login_required
|
||||
def positions():
|
||||
conn = get_db()
|
||||
init_strategy_tables(conn)
|
||||
mode = get_trading_mode(get_setting)
|
||||
ctp_st = ctp_status(mode)
|
||||
capital = _capital(conn)
|
||||
risk = get_risk_status(conn)
|
||||
ctp_acc = _ctp_account(mode) if ctp_st.get("connected") else {}
|
||||
recommend_rows = list_product_recommendations(capital, _main_price)
|
||||
active_trend = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
monitor_count = conn.execute(
|
||||
"SELECT COUNT(*) AS n FROM trade_order_monitors WHERE status='active'"
|
||||
).fetchone()["n"]
|
||||
roll_count = conn.execute(
|
||||
"SELECT COUNT(*) AS n FROM roll_groups WHERE status='active'"
|
||||
).fetchone()["n"]
|
||||
conn.close()
|
||||
sizing = get_sizing_mode(get_setting)
|
||||
return render_template(
|
||||
"trade.html",
|
||||
trading_mode=mode,
|
||||
trading_mode_label=trading_mode_label(get_setting),
|
||||
capital=capital,
|
||||
risk_status=risk,
|
||||
ctp_status=ctp_st,
|
||||
ctp_account=ctp_acc,
|
||||
recommend_rows=recommend_rows,
|
||||
active_trend=dict(active_trend) if active_trend else None,
|
||||
monitor_count=monitor_count,
|
||||
roll_count=roll_count,
|
||||
sizing_mode=sizing,
|
||||
sizing_mode_label="以损定仓" if sizing == MODE_RISK else "固定张数",
|
||||
risk_percent=get_risk_percent(get_setting),
|
||||
)
|
||||
try:
|
||||
init_strategy_tables(conn)
|
||||
mode = get_trading_mode(get_setting)
|
||||
ctp_st = ctp_status(mode)
|
||||
capital = _capital(conn)
|
||||
risk = get_risk_status(conn)
|
||||
ctp_acc = _ctp_account(mode) if ctp_st.get("connected") else {}
|
||||
recommend_rows = list_product_recommendations(capital, _main_price)
|
||||
active_trend = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
monitor_count = conn.execute(
|
||||
"SELECT COUNT(*) AS n FROM trade_order_monitors WHERE status='active'"
|
||||
).fetchone()["n"]
|
||||
roll_count = conn.execute(
|
||||
"SELECT COUNT(*) AS n FROM roll_groups WHERE status='active'"
|
||||
).fetchone()["n"]
|
||||
conn.commit()
|
||||
sizing = get_sizing_mode(get_setting)
|
||||
return render_template(
|
||||
"trade.html",
|
||||
trading_mode=mode,
|
||||
trading_mode_label=trading_mode_label(get_setting),
|
||||
capital=capital,
|
||||
risk_status=risk,
|
||||
ctp_status=ctp_st,
|
||||
ctp_account=ctp_acc,
|
||||
recommend_rows=recommend_rows,
|
||||
active_trend=dict(active_trend) if active_trend else None,
|
||||
monitor_count=monitor_count,
|
||||
roll_count=roll_count,
|
||||
sizing_mode=sizing,
|
||||
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()
|
||||
|
||||
+77
-54
@@ -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]:
|
||||
|
||||
+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