fix: 重启后立即读库展示持仓,CTP异步重连不再阻塞
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-3
@@ -11,7 +11,7 @@ from vnpy_bridge import ctp_try_auto_reconnect
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
RECONNECT_INTERVAL_SEC = 30
|
RECONNECT_INTERVAL_SEC = 10
|
||||||
|
|
||||||
|
|
||||||
def _auto_reconnect_enabled() -> bool:
|
def _auto_reconnect_enabled() -> bool:
|
||||||
@@ -26,7 +26,6 @@ def start_ctp_reconnect_worker(*, get_mode_fn: Callable[[], str], interval: int
|
|||||||
"""定时检测 CTP 连接,断线后自动重连。"""
|
"""定时检测 CTP 连接,断线后自动重连。"""
|
||||||
|
|
||||||
def _loop() -> None:
|
def _loop() -> None:
|
||||||
time.sleep(5)
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if _auto_reconnect_enabled():
|
if _auto_reconnect_enabled():
|
||||||
@@ -35,6 +34,6 @@ def start_ctp_reconnect_worker(*, get_mode_fn: Callable[[], str], interval: int
|
|||||||
logger.debug("CTP 连接正常 [%s]", mode)
|
logger.debug("CTP 连接正常 [%s]", mode)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("CTP reconnect worker: %s", exc)
|
logger.warning("CTP reconnect worker: %s", exc)
|
||||||
time.sleep(max(15, interval))
|
time.sleep(max(5, interval))
|
||||||
|
|
||||||
threading.Thread(target=_loop, daemon=True, name="ctp-reconnect-worker").start()
|
threading.Thread(target=_loop, daemon=True, name="ctp-reconnect-worker").start()
|
||||||
|
|||||||
+75
-23
@@ -78,6 +78,7 @@ from vnpy_bridge import (
|
|||||||
ctp_status,
|
ctp_status,
|
||||||
execute_order,
|
execute_order,
|
||||||
get_bridge,
|
get_bridge,
|
||||||
|
set_position_refresh_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -128,9 +129,18 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _ctp_positions(mode: str, *, refresh_if_empty: bool = True) -> list:
|
def _ctp_positions(
|
||||||
|
mode: str,
|
||||||
|
*,
|
||||||
|
refresh_if_empty: bool = True,
|
||||||
|
refresh_margin: bool = False,
|
||||||
|
) -> list:
|
||||||
try:
|
try:
|
||||||
return ctp_list_positions(mode, refresh_if_empty=refresh_if_empty)
|
return ctp_list_positions(
|
||||||
|
mode,
|
||||||
|
refresh_if_empty=refresh_if_empty,
|
||||||
|
refresh_margin=refresh_margin,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -431,6 +441,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
mode: str,
|
mode: str,
|
||||||
capital: float,
|
capital: float,
|
||||||
now_iso: str,
|
now_iso: str,
|
||||||
|
fast: bool = False,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
if not mon and not ctp:
|
if not mon and not ctp:
|
||||||
return None
|
return None
|
||||||
@@ -463,14 +474,16 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
holding = _holding_duration(open_time, now_iso) if open_time else ""
|
holding = _holding_duration(open_time, now_iso) if open_time else ""
|
||||||
|
|
||||||
mark = None
|
mark = None
|
||||||
if ctp_status(mode).get("connected"):
|
if not fast and ctp_status(mode).get("connected"):
|
||||||
mark = ctp_get_tick_price(mode, sym)
|
mark = ctp_get_tick_price(mode, sym)
|
||||||
if (mark is None or mark <= 0) and codes:
|
if not fast and (mark is None or mark <= 0) and codes:
|
||||||
mark = fetch_price(
|
mark = fetch_price(
|
||||||
sym,
|
sym,
|
||||||
codes.get("market_code", ""),
|
codes.get("market_code", ""),
|
||||||
codes.get("sina_code", ""),
|
codes.get("sina_code", ""),
|
||||||
)
|
)
|
||||||
|
if mark is None or mark <= 0:
|
||||||
|
mark = entry if entry else None
|
||||||
close_est = float(mark) if mark and mark > 0 else entry
|
close_est = float(mark) if mark and mark > 0 else entry
|
||||||
if float_pnl is None and mark and entry:
|
if float_pnl is None and mark and entry:
|
||||||
pos_tmp = calc_position_metrics(
|
pos_tmp = calc_position_metrics(
|
||||||
@@ -564,7 +577,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
"trailing_r_locked": int(mon.get("trailing_r_locked") or 0) if mon else 0,
|
"trailing_r_locked": int(mon.get("trailing_r_locked") or 0) if mon else 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _build_trading_live_rows(conn) -> list[dict]:
|
def _build_trading_live_rows(conn, *, fast: bool = False) -> list[dict]:
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
tz = ZoneInfo("Asia/Shanghai")
|
tz = ZoneInfo("Asia/Shanghai")
|
||||||
now_iso = datetime.now(tz).strftime("%Y-%m-%dT%H:%M")
|
now_iso = datetime.now(tz).strftime("%Y-%m-%dT%H:%M")
|
||||||
@@ -583,7 +596,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
if key not in monitor_by_key:
|
if key not in monitor_by_key:
|
||||||
monitor_by_key[key] = mon
|
monitor_by_key[key] = mon
|
||||||
|
|
||||||
ctp_list: list[dict] = _ctp_positions(mode) if ctp_status(mode).get("connected") else []
|
ctp_list: list[dict] = (
|
||||||
|
_ctp_positions(mode, refresh_if_empty=not fast, refresh_margin=not fast)
|
||||||
|
if ctp_status(mode).get("connected") else []
|
||||||
|
)
|
||||||
ctp_by_key: dict[str, dict] = {}
|
ctp_by_key: dict[str, dict] = {}
|
||||||
for p in ctp_list:
|
for p in ctp_list:
|
||||||
if int(p.get("lots") or 0) <= 0:
|
if int(p.get("lots") or 0) <= 0:
|
||||||
@@ -617,6 +633,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
try:
|
try:
|
||||||
row = _compose_position_row(
|
row = _compose_position_row(
|
||||||
conn, mon=mon, ctp=ctp, mode=mode, capital=capital, now_iso=now_iso,
|
conn, mon=mon, ctp=ctp, mode=mode, capital=capital, now_iso=now_iso,
|
||||||
|
fast=fast,
|
||||||
)
|
)
|
||||||
if row:
|
if row:
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
@@ -647,6 +664,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
try:
|
try:
|
||||||
row = _compose_position_row(
|
row = _compose_position_row(
|
||||||
conn, mon=mon, ctp=ctp, mode=mode, capital=capital, now_iso=now_iso,
|
conn, mon=mon, ctp=ctp, mode=mode, capital=capital, now_iso=now_iso,
|
||||||
|
fast=fast,
|
||||||
)
|
)
|
||||||
if row:
|
if row:
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
@@ -663,11 +681,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
deduped.append(row)
|
deduped.append(row)
|
||||||
return deduped
|
return deduped
|
||||||
|
|
||||||
def _build_trading_live_payload(conn) -> dict:
|
def _build_trading_live_payload(conn, *, fast: bool = False) -> dict:
|
||||||
mode = get_trading_mode(get_setting)
|
mode = get_trading_mode(get_setting)
|
||||||
ctp_st = ctp_status(mode)
|
ctp_st = ctp_status(mode)
|
||||||
|
if not fast:
|
||||||
_ensure_monitors_from_ctp(conn, mode)
|
_ensure_monitors_from_ctp(conn, mode)
|
||||||
rows = _build_trading_live_rows(conn)
|
rows = _build_trading_live_rows(conn, fast=fast)
|
||||||
pending_orders = _build_pending_orders(conn, mode)
|
pending_orders = _build_pending_orders(conn, mode)
|
||||||
capital = _capital(conn)
|
capital = _capital(conn)
|
||||||
risk = get_risk_status(conn, active_count=_effective_active_position_count(conn, mode))
|
risk = get_risk_status(conn, active_count=_effective_active_position_count(conn, mode))
|
||||||
@@ -682,26 +701,54 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
"trading_session": is_trading_session(),
|
"trading_session": is_trading_session(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _refresh_trading_live_snapshot() -> dict:
|
def _refresh_trading_live_snapshot(*, fast: bool = False) -> dict:
|
||||||
|
mode = get_trading_mode(get_setting)
|
||||||
|
if not fast and ctp_status(mode).get("connected"):
|
||||||
|
try:
|
||||||
|
get_bridge().refresh_positions()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("refresh positions before snapshot: %s", exc)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
init_strategy_tables(conn)
|
init_strategy_tables(conn)
|
||||||
payload = _build_trading_live_payload(conn)
|
payload = _build_trading_live_payload(conn, fast=fast)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return payload
|
return payload
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def _push_position_snapshot_async() -> None:
|
def _push_position_snapshot_async(*, fast: bool = False) -> None:
|
||||||
def _run() -> None:
|
def _run() -> None:
|
||||||
try:
|
try:
|
||||||
payload = _refresh_trading_live_snapshot()
|
payload = _refresh_trading_live_snapshot(fast=fast)
|
||||||
position_hub.broadcast("positions", payload)
|
position_hub.broadcast("positions", payload)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("push position snapshot: %s", exc)
|
logger.debug("push position snapshot: %s", exc)
|
||||||
|
|
||||||
threading.Thread(target=_run, daemon=True).start()
|
threading.Thread(target=_run, daemon=True).start()
|
||||||
|
|
||||||
|
def _bootstrap_trading_runtime() -> None:
|
||||||
|
"""进程启动:立刻读库展示持仓,并异步连 CTP。"""
|
||||||
|
set_position_refresh_callback(
|
||||||
|
lambda: _push_position_snapshot_async(fast=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _warm() -> None:
|
||||||
|
try:
|
||||||
|
payload = _refresh_trading_live_snapshot(fast=True)
|
||||||
|
position_hub.set_snapshot(payload)
|
||||||
|
position_hub.broadcast("positions", payload)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("bootstrap position snapshot: %s", exc)
|
||||||
|
|
||||||
|
threading.Thread(target=_warm, daemon=True, name="position-bootstrap").start()
|
||||||
|
try:
|
||||||
|
from vnpy_bridge import ctp_start_connect
|
||||||
|
mode = get_trading_mode(get_setting)
|
||||||
|
ctp_start_connect(mode, force=False)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("bootstrap ctp connect: %s", exc)
|
||||||
|
|
||||||
@app.route("/trade")
|
@app.route("/trade")
|
||||||
@login_required
|
@login_required
|
||||||
def trade_page():
|
def trade_page():
|
||||||
@@ -788,7 +835,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
init_strategy_tables(conn)
|
init_strategy_tables(conn)
|
||||||
payload = _build_trading_live_payload(conn)
|
payload = _build_trading_live_payload(conn, fast=True)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
position_hub.set_snapshot(payload)
|
position_hub.set_snapshot(payload)
|
||||||
return jsonify(payload)
|
return jsonify(payload)
|
||||||
@@ -807,7 +854,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
if snap:
|
if snap:
|
||||||
yield sse_format("positions", snap)
|
yield sse_format("positions", snap)
|
||||||
else:
|
else:
|
||||||
payload = _refresh_trading_live_snapshot()
|
payload = _refresh_trading_live_snapshot(fast=True)
|
||||||
position_hub.set_snapshot(payload)
|
position_hub.set_snapshot(payload)
|
||||||
yield sse_format("positions", payload)
|
yield sse_format("positions", payload)
|
||||||
while True:
|
while True:
|
||||||
@@ -1322,20 +1369,24 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
"status": st,
|
"status": st,
|
||||||
"account": acc,
|
"account": acc,
|
||||||
})
|
})
|
||||||
try:
|
return jsonify({
|
||||||
st = ctp_connect(mode, force=force)
|
"ok": False,
|
||||||
acc = _ctp_account(mode)
|
"error": st.get("last_error") or "CTP 连接未启动",
|
||||||
return jsonify({"ok": True, "status": st, "account": acc})
|
"status": st,
|
||||||
except Exception as exc:
|
"account": acc,
|
||||||
st = ctp_status(mode)
|
}), 400
|
||||||
return jsonify({"ok": False, "error": str(exc), "status": st}), 400
|
|
||||||
|
|
||||||
@app.route("/api/ctp/status")
|
@app.route("/api/ctp/status")
|
||||||
@login_required
|
@login_required
|
||||||
def api_ctp_status():
|
def api_ctp_status():
|
||||||
mode = get_trading_mode(get_setting)
|
mode = get_trading_mode(get_setting)
|
||||||
st = ctp_status(mode)
|
st = ctp_status(mode)
|
||||||
acc = _ctp_account(mode) if st.get("connected") else {}
|
acc = {}
|
||||||
|
if st.get("connected"):
|
||||||
|
try:
|
||||||
|
acc = _ctp_account(mode)
|
||||||
|
except Exception:
|
||||||
|
acc = {}
|
||||||
return jsonify({"ok": True, "status": st, "account": acc})
|
return jsonify({"ok": True, "status": st, "account": acc})
|
||||||
|
|
||||||
@app.route("/api/account_snapshot")
|
@app.route("/api/account_snapshot")
|
||||||
@@ -1777,9 +1828,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
interval=1,
|
interval=1,
|
||||||
)
|
)
|
||||||
start_position_worker(
|
start_position_worker(
|
||||||
refresh_fn=_refresh_trading_live_snapshot,
|
refresh_fn=lambda: _refresh_trading_live_snapshot(fast=False),
|
||||||
interval=1,
|
interval=1,
|
||||||
)
|
)
|
||||||
|
_bootstrap_trading_runtime()
|
||||||
start_ctp_fee_worker(
|
start_ctp_fee_worker(
|
||||||
get_mode_fn=lambda: get_trading_mode(get_setting),
|
get_mode_fn=lambda: get_trading_mode(get_setting),
|
||||||
get_setting_fn=get_setting,
|
get_setting_fn=get_setting,
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ def start_position_worker(
|
|||||||
"""后台定时刷新持仓快照并 SSE 广播。"""
|
"""后台定时刷新持仓快照并 SSE 广播。"""
|
||||||
|
|
||||||
def _loop() -> None:
|
def _loop() -> None:
|
||||||
time.sleep(3)
|
|
||||||
while True:
|
while True:
|
||||||
sleep_sec = idle_interval
|
sleep_sec = idle_interval
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -111,6 +111,10 @@
|
|||||||
|
|
||||||
function savePosCache(data) {
|
function savePosCache(data) {
|
||||||
try {
|
try {
|
||||||
|
if (!data || !data.rows || !data.rows.length) {
|
||||||
|
var prev = loadPosCache();
|
||||||
|
if (prev && prev.rows && prev.rows.length) return;
|
||||||
|
}
|
||||||
sessionStorage.setItem(POS_CACHE_KEY, JSON.stringify(data));
|
sessionStorage.setItem(POS_CACHE_KEY, JSON.stringify(data));
|
||||||
} catch (e) { /* quota */ }
|
} catch (e) { /* quota */ }
|
||||||
}
|
}
|
||||||
@@ -947,6 +951,7 @@
|
|||||||
}
|
}
|
||||||
pollPositions();
|
pollPositions();
|
||||||
connectPositionStream();
|
connectPositionStream();
|
||||||
|
requestCtpConnect(false);
|
||||||
connectRecommendStream();
|
connectRecommendStream();
|
||||||
fetch('/api/recommend/list')
|
fetch('/api/recommend/list')
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
|
|||||||
+28
-10
@@ -6,7 +6,7 @@ import os
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import Any, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
from locale_fix import ensure_process_locale
|
from locale_fix import ensure_process_locale
|
||||||
|
|
||||||
@@ -19,6 +19,23 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
GATEWAY_NAME = "CTP"
|
GATEWAY_NAME = "CTP"
|
||||||
|
|
||||||
|
_position_refresh_callback: Optional[Callable[[], None]] = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_position_refresh_callback(fn: Optional[Callable[[], None]]) -> None:
|
||||||
|
global _position_refresh_callback
|
||||||
|
_position_refresh_callback = fn
|
||||||
|
|
||||||
|
|
||||||
|
def _fire_position_refresh_callback() -> None:
|
||||||
|
fn = _position_refresh_callback
|
||||||
|
if not fn:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
threading.Thread(target=fn, daemon=True, name="ctp-position-refresh").start()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("position refresh callback: %s", exc)
|
||||||
|
|
||||||
_bridge: Optional["CtpBridge"] = None
|
_bridge: Optional["CtpBridge"] = None
|
||||||
_bridge_lock = threading.Lock()
|
_bridge_lock = threading.Lock()
|
||||||
|
|
||||||
@@ -231,6 +248,7 @@ class CtpBridge:
|
|||||||
self.refresh_positions()
|
self.refresh_positions()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("initial position query: %s", exc)
|
logger.debug("initial position query: %s", exc)
|
||||||
|
_fire_position_refresh_callback()
|
||||||
return
|
return
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
finally:
|
finally:
|
||||||
@@ -796,7 +814,7 @@ class CtpBridge:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("refresh_positions: %s", exc)
|
logger.debug("refresh_positions: %s", exc)
|
||||||
|
|
||||||
def list_positions(self, *, refresh_if_empty: bool = True, refresh_margin: bool = True) -> list[dict[str, Any]]:
|
def list_positions(self, *, refresh_if_empty: bool = True, refresh_margin: bool = False) -> list[dict[str, Any]]:
|
||||||
if self._engine and self._connected_mode and refresh_margin:
|
if self._engine and self._connected_mode and refresh_margin:
|
||||||
self.refresh_positions()
|
self.refresh_positions()
|
||||||
out = self._collect_positions()
|
out = self._collect_positions()
|
||||||
@@ -958,23 +976,23 @@ def ctp_start_connect(mode: str, *, force: bool = False) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def ctp_try_auto_reconnect(mode: str) -> bool:
|
def ctp_try_auto_reconnect(mode: str) -> bool:
|
||||||
"""断线时静默重连;已连接且 ping 正常则直接返回 True。"""
|
"""断线时静默异步重连;已连接且 ping 正常则直接返回 True。"""
|
||||||
b = get_bridge()
|
b = get_bridge()
|
||||||
if not b.available():
|
if not b.available():
|
||||||
return False
|
return False
|
||||||
if b.connect_in_progress():
|
if b.connect_in_progress():
|
||||||
return False
|
return True
|
||||||
st = _setting_for_mode(mode)
|
st = _setting_for_mode(mode)
|
||||||
if not st.get("用户名") or not st.get("密码") or not st.get("交易服务器"):
|
if not st.get("用户名") or not st.get("密码") or not st.get("交易服务器"):
|
||||||
return False
|
return False
|
||||||
if b.connected_mode == mode and b.ping():
|
if b.connected_mode == mode and b.ping():
|
||||||
return True
|
return True
|
||||||
try:
|
info = b.start_connect_async(mode, force=False)
|
||||||
b.connect(mode, force=False)
|
return bool(
|
||||||
return b.connected_mode == mode
|
info.get("connected")
|
||||||
except Exception as exc:
|
or info.get("connecting")
|
||||||
logger.info("CTP 自动重连失败: %s", exc)
|
or info.get("started")
|
||||||
return False
|
)
|
||||||
|
|
||||||
|
|
||||||
def ctp_status(mode: str) -> dict[str, Any]:
|
def ctp_status(mode: str) -> dict[str, Any]:
|
||||||
|
|||||||
Reference in New Issue
Block a user