fix: SimNow 登录封禁(错误75)时冷却退避,停止自动重连

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-25 16:50:09 +08:00
parent 5a6c89c662
commit 4aebc2df49
3 changed files with 74 additions and 3 deletions
+1
View File
@@ -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` |
+17 -2
View File
@@ -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;
+56 -1
View File
@@ -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