From 9c8b92d2bddd06d76f5a77dd7901aaf6d1ff6462 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 25 Jun 2026 16:57:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=99=BB=E5=BD=95=E5=86=B7=E5=8D=B4?= =?UTF-8?q?=E6=9C=9F=E9=97=B4=E4=B8=8D=E5=86=8D=E6=98=BE=E7=A4=BA=20CTP=20?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E4=B8=AD=EF=BC=8C=E4=BC=98=E5=8C=96=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E7=8A=B6=E6=80=81=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- ctp_premarket_connect.py | 6 +++- install_trading.py | 8 +++++ static/js/trade.js | 73 +++++++++++++++++++++++++++++++++------- vnpy_bridge.py | 4 ++- 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/ctp_premarket_connect.py b/ctp_premarket_connect.py index 2af9dd4..a4f22bd 100644 --- a/ctp_premarket_connect.py +++ b/ctp_premarket_connect.py @@ -47,7 +47,11 @@ def start_ctp_premarket_connect_worker( ): mode = get_mode_fn() st = ctp_status(mode) - if not st.get("connected") and not st.get("connecting"): + if ( + not st.get("connected") + and not st.get("connecting") + and int(st.get("login_cooldown_sec") or 0) <= 0 + ): info = ctp_start_connect(mode, force=False) if info.get("started"): logger.info( diff --git a/install_trading.py b/install_trading.py index 19dacd4..b4167dd 100644 --- a/install_trading.py +++ b/install_trading.py @@ -1549,6 +1549,14 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se "status": st, "account": acc, }) + if info.get("cooldown"): + return jsonify({ + "ok": False, + "cooldown": True, + "error": st.get("last_error") or "CTP 登录冷却中", + "status": st, + "account": acc, + }), 400 return jsonify({ "ok": False, "error": st.get("last_error") or "CTP 连接未启动", diff --git a/static/js/trade.js b/static/js/trade.js index b066168..6cf246c 100644 --- a/static/js/trade.js +++ b/static/js/trade.js @@ -30,7 +30,7 @@ var selectedMaxLots = null; var recommendMaxByProduct = {}; var recommendMaxByCode = {}; - var POS_CACHE_KEY = 'qihuo_trading_live_v2'; + var POS_CACHE_KEY = 'qihuo_trading_live_v3'; function runWhenReady(fn) { if (document.readyState === 'loading') { @@ -148,9 +148,11 @@ if (cap && data.capital != null) cap.textContent = Number(data.capital).toFixed(2); var connected = data.ctp_status && data.ctp_status.connected; var connecting = data.ctp_status && data.ctp_status.connecting; + var cooldownSec = (data.ctp_status && data.ctp_status.login_cooldown_sec) || 0; + if (cooldownSec > 0) connecting = false; ctpConnected = !!connected; isTradingSession = !!data.trading_session; - updateCtpBadge(!!connected, !!connecting); + syncCtpBadgeFromStatus(data.ctp_status || { connected: connected, connecting: connecting }); if (!connected && !connecting && data.ctp_status && data.ctp_status.last_error) { showCtpError(data.ctp_status.last_error); if (isCtpLoginBanError(data.ctp_status.last_error)) { @@ -184,6 +186,11 @@ list.innerHTML = '
CTP 连接中,请稍候…
'; return; } + if (cooldownSec > 0 || (data.ctp_status && data.ctp_status.last_error)) { + var err = (data.ctp_status && data.ctp_status.last_error) || 'CTP 未连接'; + list.innerHTML = '
' + err + '
'; + return; + } list.innerHTML = '
CTP 未连接,正在尝试自动重连…
'; tryAutoCtpReconnect(); return; @@ -305,6 +312,16 @@ updateRRDisplay(); } + function syncCtpBadgeFromStatus(st) { + if (!st) return; + var connected = !!st.connected; + var connecting = !!st.connecting; + if ((st.login_cooldown_sec || 0) > 0) { + connecting = false; + } + updateCtpBadge(connected, connecting); + } + function updateCtpBadge(connected, connecting) { var ctpBadge = document.getElementById('ctp-badge'); var btnConnect = document.getElementById('btn-ctp-connect'); @@ -336,7 +353,7 @@ .then(function (d) { var st = d.status || {}; if (st.connected) { - updateCtpBadge(true, false); + syncCtpBadgeFromStatus(st); showCtpError(''); if (d.account && d.account.available != null) { var avail = document.getElementById('avail-display'); @@ -345,13 +362,18 @@ pollPositions(); return true; } + if ((st.login_cooldown_sec || 0) > 0) { + syncCtpBadgeFromStatus(st); + if (st.last_error) showCtpError(st.last_error); + return false; + } if (st.connecting && Date.now() < deadline) { - updateCtpBadge(false, true); + syncCtpBadgeFromStatus(st); return new Promise(function (resolve) { setTimeout(function () { resolve(tick()); }, 2000); }); } - updateCtpBadge(false, false); + syncCtpBadgeFromStatus(st); if (st.last_error) { showCtpError(st.last_error); if (isCtpLoginBanError(st.last_error)) { @@ -380,22 +402,28 @@ }) .then(function (r) { return r.json(); }) .then(function (d) { - if (d.status && d.status.connected) { - updateCtpBadge(true, false); + var st = d.status || {}; + if (st.connected) { + syncCtpBadgeFromStatus(st); showCtpError(''); pollPositions(); return d; } - if (d.connecting || (d.status && d.status.connecting)) { + if ((st.login_cooldown_sec || 0) > 0 || d.cooldown) { + syncCtpBadgeFromStatus(st); + showCtpError(st.last_error || d.error || 'CTP 登录冷却中'); + return d; + } + if (d.connecting || st.connecting) { return waitForCtpConnected(70000).then(function (ok) { if (!ok && d.error) showCtpError(d.error); - else if (!ok && d.status && d.status.last_error) showCtpError(d.status.last_error); + else if (!ok && st.last_error) showCtpError(st.last_error); return d; }); } if (!d.ok) { - updateCtpBadge(false, false); - var err = d.error || (d.status && d.status.last_error) || '连接失败'; + syncCtpBadgeFromStatus(st); + var err = d.error || st.last_error || '连接失败'; showCtpError(err); } return d; @@ -1051,6 +1079,24 @@ }); } + function initCtpOnLoad() { + fetch('/api/ctp/status') + .then(function (r) { return r.json(); }) + .then(function (d) { + var st = d.status || {}; + syncCtpBadgeFromStatus(st); + if (st.last_error) showCtpError(st.last_error); + if (st.connected) { + pollPositions(); + return; + } + if ((st.login_cooldown_sec || 0) > 0) return; + if (isCtpLoginBanError(st.last_error)) return; + if (!st.connecting) requestCtpConnect(false); + }) + .catch(function () {}); + } + runWhenReady(function () { setPriceType('limit'); if (isFixedMode() && lotsCalc) { @@ -1059,11 +1105,14 @@ } var cached = loadPosCache(); if (cached) { + if (cached.ctp_status) { + cached.ctp_status = Object.assign({}, cached.ctp_status, { connecting: false }); + } applyPositionsData(cached); } pollPositions(); connectPositionStream(); - requestCtpConnect(false); + initCtpOnLoad(); connectRecommendStream(); fetch('/api/recommend/list') .then(function (r) { return r.json(); }) diff --git a/vnpy_bridge.py b/vnpy_bridge.py index eb118c0..496d66e 100644 --- a/vnpy_bridge.py +++ b/vnpy_bridge.py @@ -251,10 +251,12 @@ class CtpBridge: self.ping() st = _setting_for_mode(mode) missing = [k for k in ("用户名", "密码", "交易服务器") if not st.get(k)] + cooldown = self.login_cooldown_remaining() + connecting = bool(self._connect_in_progress and cooldown <= 0) return { "vnpy_installed": self.available(), "connected": self._connected_mode == mode, - "connecting": self._connect_in_progress, + "connecting": connecting, "connected_mode": self._connected_mode, "mode_label": _mode_label(mode), "missing_config": missing,