fix: SimNow 登录封禁(错误75)时冷却退避,停止自动重连
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+56
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user