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:
+151
-25
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user