From 4aebc2df4922ef9231fa49b40486a33054b365d9 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 25 Jun 2026 16:50:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20SimNow=20=E7=99=BB=E5=BD=95=E5=B0=81?= =?UTF-8?q?=E7=A6=81(=E9=94=99=E8=AF=AF75)=E6=97=B6=E5=86=B7=E5=8D=B4?= =?UTF-8?q?=E9=80=80=E9=81=BF=EF=BC=8C=E5=81=9C=E6=AD=A2=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E9=87=8D=E8=BF=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- docs/SIMNOW.md | 1 + static/js/trade.js | 19 ++++++++++++++-- vnpy_bridge.py | 57 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/docs/SIMNOW.md b/docs/SIMNOW.md index 1bcbdcf..7e3bd25 100644 --- a/docs/SIMNOW.md +++ b/docs/SIMNOW.md @@ -182,6 +182,7 @@ python scripts/test_simnow.py | 端口探测失败 | 服务器出网或防火墙问题,`nc -zv 180.168.146.187 10201` | | 报错 **4097** / 握手失败 | `pip install -U vnpy vnpy_ctp`,`.env` 设 `SIMNOW_ENV=实盘` | | **不合法的登录** | 投资者代码/密码错,或未在快期改过一次密码 | +| **连续登录失败次数超限(75)** | 短时间失败太多次被临时封禁;等待 30~60 分钟,快期验证密码后再连,勿反复点连接 | | 快期能登、脚本不能 | 多为网络或前置地址,换 SimNow 官网其他组前置试 | | 连上后进程崩溃 `locale::facet::_S_create_c_locale` | **必须**安装 `zh_CN.GB18030`:`sed -i '/^# zh_CN.GB18030/s/^# //' /etc/locale.gen && locale-gen zh_CN.GB18030` | diff --git a/static/js/trade.js b/static/js/trade.js index 0089c2f..b066168 100644 --- a/static/js/trade.js +++ b/static/js/trade.js @@ -20,6 +20,7 @@ var priceType = 'limit'; var lastCtpReconnectAt = 0; var lastCtpUnreachableAt = 0; + var lastCtpLoginBanAt = 0; var ctpReconnecting = false; var ctpConnectInflight = false; var isTradingSession = false; @@ -128,6 +129,15 @@ if (hint) hint.textContent = msg || ''; } + function isCtpLoginBanError(msg) { + return !!(msg && ( + msg.indexOf('登录被禁止') >= 0 || + msg.indexOf('连续登录失败') >= 0 || + msg.indexOf('登录冷却') >= 0 || + msg.indexOf('错误码 75') >= 0 + )); + } + function isCtpUnreachableError(msg) { return !!(msg && (msg.indexOf('不可达') >= 0 || msg.indexOf('Connection refused') >= 0 || msg.indexOf('timed out') >= 0)); } @@ -143,7 +153,9 @@ updateCtpBadge(!!connected, !!connecting); if (!connected && !connecting && data.ctp_status && data.ctp_status.last_error) { showCtpError(data.ctp_status.last_error); - if (isCtpUnreachableError(data.ctp_status.last_error)) { + if (isCtpLoginBanError(data.ctp_status.last_error)) { + lastCtpLoginBanAt = Date.now(); + } else if (isCtpUnreachableError(data.ctp_status.last_error)) { lastCtpUnreachableAt = Date.now(); } } @@ -342,7 +354,9 @@ updateCtpBadge(false, false); if (st.last_error) { showCtpError(st.last_error); - if (isCtpUnreachableError(st.last_error)) { + if (isCtpLoginBanError(st.last_error)) { + lastCtpLoginBanAt = Date.now(); + } else if (isCtpUnreachableError(st.last_error)) { lastCtpUnreachableAt = Date.now(); } } @@ -497,6 +511,7 @@ if (ctpReconnecting || ctpConnectInflight) return; var now = Date.now(); if (now - lastCtpReconnectAt < 60000) return; + if (lastCtpLoginBanAt && now - lastCtpLoginBanAt < 2700000) return; if (lastCtpUnreachableAt && now - lastCtpUnreachableAt < 300000) return; lastCtpReconnectAt = now; ctpReconnecting = true; diff --git a/vnpy_bridge.py b/vnpy_bridge.py index 3695995..eb118c0 100644 --- a/vnpy_bridge.py +++ b/vnpy_bridge.py @@ -22,6 +22,8 @@ GATEWAY_NAME = "CTP" CONNECT_WAIT_SEC = 60 CONNECT_POLL_INTERVAL_SEC = 0.5 +LOGIN_BAN_COOLDOWN_SEC = 45 * 60 +LOGIN_FAIL_COOLDOWN_SEC = 5 * 60 _position_refresh_callback: Optional[Callable[[], None]] = None @@ -94,6 +96,11 @@ def _format_ctp_failure(ctp_logs: list[str], *, td_address: str = "") -> str: "tcp://180.168.146.187:10201 / 10211,并确认服务器能访问该地址。" ) text = "\n".join(ctp_logs) + if "连续登录失败" in text or "登录被禁止" in text or "代码:75" in text: + return ( + "CTP 登录被临时禁止:连续失败次数过多(错误码 75)。" + "请等待约 30~60 分钟后再试,先用快期确认投资者代码与密码正确,期间勿反复点「连接」。" + ) if "4097" in text or "Decrypt handshake" in text or "shake hand" in text.lower(): return ( "CTP 握手失败(4097):vnpy_ctp 与 SimNow 前置加密不匹配。" @@ -130,6 +137,7 @@ class CtpBridge: self._last_error: str = "" self._connect_lock = threading.Lock() self._connect_in_progress = False + self._login_cooldown_until: float = 0.0 self._commission_waiters: dict[int, threading.Event] = {} self._commission_lists: dict[int, list] = {} self._commission_hooked = False @@ -172,6 +180,33 @@ class CtpBridge: def connect_in_progress(self) -> bool: return self._connect_in_progress + def login_cooldown_remaining(self) -> int: + """距允许再次登录的剩余秒数。""" + remain = int(self._login_cooldown_until - time.monotonic()) + return max(0, remain) + + def _is_login_cooldown_active(self) -> bool: + return self.login_cooldown_remaining() > 0 + + def _set_login_cooldown(self, seconds: float) -> None: + until = time.monotonic() + max(0.0, seconds) + if until > self._login_cooldown_until: + self._login_cooldown_until = until + + def _apply_login_failure_cooldown(self, ctp_logs: list[str]) -> None: + text = "\n".join(ctp_logs) + if "连续登录失败" in text or "登录被禁止" in text or "代码:75" in text: + self._set_login_cooldown(LOGIN_BAN_COOLDOWN_SEC) + elif any("登录失败" in m or "不合法的登录" in m for m in ctp_logs): + self._set_login_cooldown(LOGIN_FAIL_COOLDOWN_SEC) + + def _login_cooldown_message(self) -> str: + remain = self.login_cooldown_remaining() + return ( + f"CTP 登录冷却中,请 {remain // 60} 分 {remain % 60} 秒后再试" + f"(避免连续失败被 SimNow 封禁)" + ) + def _close_gateway(self) -> None: """关闭 CTP 网关,避免半连接状态下重连卡在「连接登录」。""" if not self._engine: @@ -186,7 +221,11 @@ class CtpBridge: time.sleep(0.6) def _login_rejected(self, ctp_logs: list[str]) -> bool: - return any("登录失败" in m or "不合法的登录" in m for m in ctp_logs) + return any( + kw in m + for m in ctp_logs + for kw in ("登录失败", "不合法的登录", "登录被禁止", "连续登录失败") + ) def _wait_connected(self, mode: str, ctp_logs: list[str] | None = None) -> bool: """等待账户回报或交易通道登录成功。""" @@ -220,6 +259,7 @@ class CtpBridge: "mode_label": _mode_label(mode), "missing_config": missing, "last_error": self._last_error, + "login_cooldown_sec": self.login_cooldown_remaining(), "broker_id": st.get("经纪商代码", ""), "td_address": st.get("交易服务器", ""), } @@ -227,6 +267,10 @@ class CtpBridge: def connect(self, mode: str, *, force: bool = False) -> None: if self._connect_in_progress: raise RuntimeError("CTP 正在连接中,请稍候") + if self._is_login_cooldown_active() and not force: + msg = self._login_cooldown_message() + self._last_error = msg + raise RuntimeError(msg) if not self._engine: raise RuntimeError(self._last_error or "vnpy 引擎未初始化") if self._connected_mode == mode and not force: @@ -305,6 +349,7 @@ class CtpBridge: self._ee.unregister(EVENT_LOG, _on_log) self._close_gateway() + self._apply_login_failure_cooldown(ctp_logs) hint = _format_ctp_failure(ctp_logs, td_address=setting.get("交易服务器", "")) self._last_error = hint logger.warning("CTP 连接失败 [%s]: %s | logs=%s", mode, hint, ctp_logs[-5:]) @@ -318,6 +363,14 @@ class CtpBridge: return {"started": False, "connecting": False, "connected": True} if self._connect_in_progress: return {"started": False, "connecting": True, "connected": False} + if self._is_login_cooldown_active() and not force: + self._last_error = self._login_cooldown_message() + return { + "started": False, + "connecting": False, + "connected": False, + "cooldown": True, + } def _run() -> None: try: @@ -1092,6 +1145,8 @@ def ctp_try_auto_reconnect(mode: str) -> bool: return False if b.connect_in_progress(): return False + if b.login_cooldown_remaining() > 0: + return False st = _setting_for_mode(mode) if not st.get("用户名") or not st.get("密码") or not st.get("交易服务器"): return False