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,