feat: 持仓监控后台 SSE 推送与浏览器缓存,刷新不再阻塞读柜台。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-25 13:49:44 +08:00
parent f31164076f
commit bbcc5607ad
4 changed files with 304 additions and 97 deletions
+89 -18
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import json
import logging
import threading
from datetime import datetime
from typing import Any, Callable, Optional
@@ -28,6 +29,7 @@ from recommend_store import (
refresh_recommend_cache,
)
from recommend_stream import recommend_hub, start_recommend_worker
from position_stream import position_hub, start_position_worker
from ctp_reconnect import start_ctp_reconnect_worker
from ctp_premarket_connect import start_ctp_premarket_connect_worker
from ctp_fee_worker import start_ctp_fee_worker
@@ -70,6 +72,7 @@ from ctp_symbol import ths_to_vnpy_symbol
from vnpy_bridge import (
ctp_connect,
ctp_get_account,
ctp_get_tick_price,
ctp_list_active_orders,
ctp_list_positions,
ctp_status,
@@ -290,8 +293,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
tp = float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None
open_time = (mon.get("open_time") or "") if mon else ""
holding = _holding_duration(open_time, now_iso) if open_time else ""
mark = None
if codes:
mark = ctp_get_tick_price(mode, sym)
if (mark is None or mark <= 0) and codes:
mark = fetch_price(
sym,
codes.get("market_code", ""),
@@ -382,6 +385,45 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
})
return rows
def _build_trading_live_payload(conn) -> dict:
mode = get_trading_mode(get_setting)
ctp_st = ctp_status(mode)
_sync_trade_monitors_with_ctp(conn, mode)
rows = _build_trading_live_rows(conn)
pending_orders = _build_pending_orders(conn, mode)
capital = _capital(conn)
risk = get_risk_status(conn, active_count=_effective_active_position_count(conn, mode))
return {
"ok": True,
"rows": rows,
"pending_orders": pending_orders,
"capital": capital,
"ctp_status": ctp_st,
"trading_mode_label": trading_mode_label(get_setting),
"risk_status": risk,
"trading_session": is_trading_session(),
}
def _refresh_trading_live_snapshot() -> dict:
conn = get_db()
try:
init_strategy_tables(conn)
payload = _build_trading_live_payload(conn)
conn.commit()
return payload
finally:
conn.close()
def _push_position_snapshot_async() -> None:
def _run() -> None:
try:
payload = _refresh_trading_live_snapshot()
position_hub.broadcast("positions", payload)
except Exception as exc:
logger.debug("push position snapshot: %s", exc)
threading.Thread(target=_run, daemon=True).start()
@app.route("/trade")
@login_required
def trade_page():
@@ -466,29 +508,52 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
@app.route("/api/trading/live")
@login_required
def api_trading_live():
cached = position_hub.get_snapshot()
if cached:
return jsonify(cached)
conn = get_db()
try:
init_strategy_tables(conn)
mode = get_trading_mode(get_setting)
ctp_st = ctp_status(mode)
_sync_trade_monitors_with_ctp(conn, mode)
rows = _build_trading_live_rows(conn)
pending_orders = _build_pending_orders(conn, mode)
capital = _capital(conn)
risk = get_risk_status(conn, active_count=_effective_active_position_count(conn, mode))
payload = _build_trading_live_payload(conn)
conn.commit()
return jsonify({
"rows": rows,
"pending_orders": pending_orders,
"capital": capital,
"ctp_status": ctp_st,
"trading_mode_label": trading_mode_label(get_setting),
"risk_status": risk,
"trading_session": is_trading_session(),
})
position_hub.set_snapshot(payload)
return jsonify(payload)
finally:
conn.close()
@app.route("/api/trading/stream")
@login_required
def api_trading_stream():
from queue import Empty
def generate():
q = position_hub.subscribe()
try:
snap = position_hub.get_snapshot()
if snap:
yield sse_format("positions", snap)
else:
payload = _refresh_trading_live_snapshot()
position_hub.set_snapshot(payload)
yield sse_format("positions", payload)
while True:
try:
msg = q.get(timeout=25)
yield sse_format(msg["event"], msg["data"])
except Empty:
yield ": heartbeat\n\n"
finally:
position_hub.unsubscribe(q)
return Response(
generate(),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
)
@app.route("/api/trading/monitor/upsert", methods=["POST"])
@login_required
def api_trading_monitor_upsert():
@@ -749,6 +814,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
)
conn.commit()
conn.close()
_push_position_snapshot_async()
return jsonify({"ok": True, "message": "已平仓并记入交易记录(手动平仓)"})
except ValueError as exc:
conn.close()
@@ -1018,6 +1084,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
conn.commit()
send_wechat_msg(f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}")
conn.close()
_push_position_snapshot_async()
return jsonify({"ok": True, "result": result, "lots": lots, "message": "委托已提交柜台,限价单需成交后才会显示持仓"})
except (ValueError, RuntimeError) as exc:
conn.close()
@@ -1501,6 +1568,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
notify_fn=send_wechat_msg,
interval=1,
)
start_position_worker(
refresh_fn=_refresh_trading_live_snapshot,
interval=1,
)
start_ctp_fee_worker(
get_mode_fn=lambda: get_trading_mode(get_setting),
get_setting_fn=get_setting,