Add PostgreSQL production backend to eliminate SQLite lock contention.

Support DATABASE_URL with connection pooling, pg_dump backups, SQLite migration script, and deploy_postgres.sh with docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-01 08:11:42 +08:00
parent 39eac983ff
commit 52aca456e9
23 changed files with 1208 additions and 150 deletions
+151 -25
View File
@@ -488,6 +488,71 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
)
if ctp_positions:
return
_ensure_monitors_from_sticky_state(conn, mode)
def _ensure_monitors_from_sticky_state(conn, mode: str) -> None:
"""vnpy 持仓空窗但账户仍有保证金时,恢复本地 active 监控。"""
if not ctp_status(mode).get("connected"):
return
margin_raw = ctp_account_margin_used(mode)
if margin_raw is None or float(margin_raw or 0) <= 0:
return
if count_active_trade_monitors(conn) > 0:
return
capital = _capital(conn)
for p in trading_state.get_positions() or []:
lots = int(p.get("lots") or 0)
if lots <= 0:
continue
direction = p.get("direction") or "long"
ths = _ctp_pos_to_ths_code(p) or (p.get("symbol") or "")
if not ths:
continue
existing = _find_or_revive_monitor(conn, ths, direction)
if existing:
_sync_monitor_from_ctp(
conn, int(existing["id"]), ths, direction, mode, ctp=p,
capital=capital,
)
continue
sl, tp, trailing_be, initial_sl = _restore_sl_tp_from_closed(conn, ths, direction)
mid = _upsert_open_monitor(
conn,
sym=ths,
direction=direction,
lots=lots,
price=float(p.get("avg_price") or 0),
sl=sl,
tp=tp,
trailing_be=trailing_be,
ctp_open_time=(p.get("open_time") or "").strip() or None,
monitor_type="ctp_sync",
)
if initial_sl is not None and sl is not None:
conn.execute(
"UPDATE trade_order_monitors SET initial_stop_loss=? WHERE id=?",
(initial_sl, mid),
)
if count_active_trade_monitors(conn) > 0:
return
today = datetime.now().strftime("%Y-%m-%d")
for r in conn.execute(
"SELECT * FROM trade_order_monitors WHERE status='closed' "
"AND open_time LIKE ? ORDER BY id DESC LIMIT 5",
(f"{today}%",),
).fetchall():
mon = dict(r)
if int(mon.get("lots") or 0) <= 0:
continue
revived = _revive_closed_monitor(
conn, mon.get("symbol") or "", mon.get("direction") or "long",
)
if revived:
logger.info(
"保证金占用下恢复监控 id=%s sym=%s",
revived.get("id"), revived.get("symbol"),
)
break
def _restore_recent_pending_monitors(conn, mode: str) -> None:
"""重启或 vnpy 委托缓存丢失时,恢复当日最近一笔可能仍有效的开仓挂单。"""
@@ -1729,8 +1794,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
seen.add(rk)
deduped.append(row)
if not deduped and ctp_status(mode).get("connected") and monitor_by_pk:
margin_used = float(ctp_account_margin_used(mode) or 0)
if not deduped and ctp_status(mode).get("connected"):
margin_raw = ctp_account_margin_used(mode)
margin_used = float(margin_raw or 0) if margin_raw is not None else 0.0
has_margin_hint = margin_raw is not None and margin_used > 0
has_active_mon = any(
int(m.get("lots") or 0) > 0 for m in monitor_by_pk.values()
)
@@ -1741,7 +1808,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
)
except Exception:
pass
if margin_used > 0 or has_active_mon or since_connect < 300:
if has_margin_hint or has_active_mon or since_connect < 300:
if not monitor_by_pk and has_margin_hint:
_ensure_monitors_from_sticky_state(conn, mode)
monitor_by_pk = _monitors_by_position_key(conn)
for mon in monitor_by_pk.values():
lots = int(mon.get("lots") or 0)
if lots <= 0:
@@ -1776,6 +1846,41 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
except Exception as exc:
logger.warning("compose monitor fallback row failed: %s", exc)
if not deduped and ctp_status(mode).get("connected"):
for r in conn.execute(
"SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC"
).fetchall():
mon = dict(r)
lots = int(mon.get("lots") or 0)
if lots <= 0:
continue
sym = (mon.get("symbol") or "").strip()
direction = (mon.get("direction") or "long").strip().lower()
rk = _monitor_position_key(mon)
if rk in seen:
continue
if fast:
mon = _overlay_sl_tp_readonly(conn, mon, sym, direction) or mon
try:
row = _compose_position_row(
conn,
mon=mon,
ctp=None,
mode=mode,
capital=capital,
now_iso=now_iso,
fast=fast,
)
if not row:
continue
row_key = row.get("key") or row.get("position_key") or rk
if row_key in seen:
continue
seen.add(row_key)
deduped.append(row)
except Exception as exc:
logger.warning("compose active monitor row failed: %s", exc)
return deduped
def _build_trading_live_payload(conn, *, fast: bool = False) -> dict:
@@ -1787,9 +1892,14 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
capital = _capital(conn)
if ctp_st.get("connected") and (not fast or _has_pending_monitors(conn)):
_reconcile_pending(conn, mode, capital=capital)
if ctp_st.get("connected") and not fast:
_ensure_monitors_from_ctp(conn, mode)
_sync_trade_monitors_with_ctp(conn, mode)
if ctp_st.get("connected"):
if not fast:
_ensure_monitors_from_ctp(conn, mode)
_sync_trade_monitors_with_ctp(conn, mode)
elif count_active_trade_monitors(conn) == 0:
margin_raw = ctp_account_margin_used(mode)
if margin_raw is not None and float(margin_raw) > 0:
_ensure_monitors_from_sticky_state(conn, mode)
rows = _build_trading_live_rows(conn, fast=fast)
active_orders = _build_active_orders(
conn, mode=mode, capital=capital, now_iso=now_iso,
@@ -1803,12 +1913,16 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
active_count=_effective_active_position_count(conn, mode),
equity=capital,
)
margin_used = (
ctp_account_margin_used(mode) if ctp_st.get("connected") else None
)
return {
"ok": True,
"rows": rows,
"active_orders": active_orders,
"pending_orders": pending_orders,
"capital": capital,
"margin_used": margin_used,
"ctp_status": ctp_st,
"trading_mode_label": trading_mode_label(get_setting),
"risk_status": risk,
@@ -1841,18 +1955,34 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
payload = _build_trading_live_payload(conn, fast=fast)
commit_retry(conn)
prev = position_hub.get_snapshot()
active_n = int((payload.get("risk_status") or {}).get("active_count") or 0)
if (
prev
and ctp_status(mode).get("connected")
and not (payload.get("rows") or [])
and (prev.get("rows") or [])
):
margin_used = float(ctp_account_margin_used(mode) or 0)
if margin_used > 0 or trading_state.sync_state == "syncing":
margin_raw = payload.get("margin_used")
if margin_raw is None:
margin_raw = ctp_account_margin_used(mode)
margin_used = float(margin_raw or 0) if margin_raw is not None else 0.0
if (
(margin_raw is not None and margin_used > 0)
or trading_state.sync_state == "syncing"
or active_n > 0
):
payload = dict(payload)
payload["rows"] = prev["rows"]
payload["sync_state"] = "syncing"
payload["sync_label"] = "同步中…"
if trading_state.sync_state == "syncing":
payload["sync_state"] = "syncing"
payload["sync_label"] = "同步中…"
elif (
ctp_status(mode).get("connected")
and not (payload.get("rows") or [])
and active_n > 0
):
payload = dict(payload)
payload["rows"] = _build_trading_live_rows(conn, fast=fast)
return payload
finally:
conn.close()
@@ -2143,15 +2273,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
@app.route("/api/trading/live")
@login_required
def api_trading_live():
conn = get_db()
try:
init_strategy_tables(conn)
payload = _build_trading_live_payload(conn, fast=True)
commit_retry(conn)
position_hub.set_snapshot(payload)
return jsonify(payload)
finally:
conn.close()
payload = _refresh_trading_live_snapshot(fast=True)
return jsonify(payload)
@app.route("/api/trading/stream")
@login_required
@@ -3183,7 +3306,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
risk_percent, capital_snapshot, plan_margin, target_lots, first_lots, remainder_lots,
dca_legs, leg_amounts_json, grid_prices_json, first_order_done, avg_entry_price,
lots_open, opened_at, period
) VALUES ('active',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?,?)""",
) VALUES ('active',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?,?) RETURNING id""",
(
sym, sym_name or (codes.get("name", sym) if codes else sym), plan["direction"],
plan["stop_loss"], plan["add_upper"], plan["take_profit"],
@@ -3193,7 +3316,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
price, plan["first_lots"], now, plan["period"],
),
)
plan_id = cur.lastrowid
row = cur.fetchone()
plan_id = int(row["id"] if isinstance(row, dict) else row[0])
conn.commit()
conn.close()
send_wechat_msg(f"趋势回调首仓 {sym} {plan['first_lots']}")
@@ -3325,13 +3449,14 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
"""INSERT INTO roll_groups (
order_monitor_id, symbol, direction, initial_take_profit, initial_stop_loss,
current_stop_loss, risk_percent, leg_count, status, created_at, updated_at
) VALUES (?,?,?,?,?,?,?,1,'active',?,?)""",
) VALUES (?,?,?,?,?,?,?,1,'active',?,?) RETURNING id""",
(
mon_id, sym, mon["direction"], mon["take_profit"], mon["stop_loss"],
new_sl, risk_budget, now, now,
),
)
gid = int(cur.lastrowid)
row = cur.fetchone()
gid = int(row["id"] if isinstance(row, dict) else row[0])
leg_n = 1
if pending_leg_id:
conn.execute(
@@ -3417,13 +3542,14 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
"""INSERT INTO roll_groups (
order_monitor_id, symbol, direction, initial_take_profit, initial_stop_loss,
current_stop_loss, risk_percent, leg_count, status, created_at, updated_at
) VALUES (?,?,?,?,?,?,?,0,'active',?,?)""",
) VALUES (?,?,?,?,?,?,?,0,'active',?,?) RETURNING id""",
(
mon_id, mon["symbol"], mon["direction"], mon["take_profit"], mon["stop_loss"],
preview["new_stop_loss"], risk_budget, now, now,
),
)
gid = int(cur.lastrowid)
row = cur.fetchone()
gid = int(row["id"] if isinstance(row, dict) else row[0])
leg_n = int(conn.execute(
"SELECT COUNT(*) AS n FROM roll_legs WHERE roll_group_id=? AND status=?",
(gid, LEG_STATUS_FILLED),