feat: 持仓监控后台 SSE 推送与浏览器缓存,刷新不再阻塞读柜台。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+89
-18
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user