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
+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