fix: SimNow 登录封禁(错误75)时冷却退避,停止自动重连
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -182,6 +182,7 @@ python scripts/test_simnow.py
|
|||||||
| 端口探测失败 | 服务器出网或防火墙问题,`nc -zv 180.168.146.187 10201` |
|
| 端口探测失败 | 服务器出网或防火墙问题,`nc -zv 180.168.146.187 10201` |
|
||||||
| 报错 **4097** / 握手失败 | `pip install -U vnpy vnpy_ctp`,`.env` 设 `SIMNOW_ENV=实盘` |
|
| 报错 **4097** / 握手失败 | `pip install -U vnpy vnpy_ctp`,`.env` 设 `SIMNOW_ENV=实盘` |
|
||||||
| **不合法的登录** | 投资者代码/密码错,或未在快期改过一次密码 |
|
| **不合法的登录** | 投资者代码/密码错,或未在快期改过一次密码 |
|
||||||
|
| **连续登录失败次数超限(75)** | 短时间失败太多次被临时封禁;等待 30~60 分钟,快期验证密码后再连,勿反复点连接 |
|
||||||
| 快期能登、脚本不能 | 多为网络或前置地址,换 SimNow 官网其他组前置试 |
|
| 快期能登、脚本不能 | 多为网络或前置地址,换 SimNow 官网其他组前置试 |
|
||||||
| 连上后进程崩溃 `locale::facet::_S_create_c_locale` | **必须**安装 `zh_CN.GB18030`:`sed -i '/^# zh_CN.GB18030/s/^# //' /etc/locale.gen && locale-gen zh_CN.GB18030` |
|
| 连上后进程崩溃 `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
@@ -20,6 +20,7 @@
|
|||||||
var priceType = 'limit';
|
var priceType = 'limit';
|
||||||
var lastCtpReconnectAt = 0;
|
var lastCtpReconnectAt = 0;
|
||||||
var lastCtpUnreachableAt = 0;
|
var lastCtpUnreachableAt = 0;
|
||||||
|
var lastCtpLoginBanAt = 0;
|
||||||
var ctpReconnecting = false;
|
var ctpReconnecting = false;
|
||||||
var ctpConnectInflight = false;
|
var ctpConnectInflight = false;
|
||||||
var isTradingSession = false;
|
var isTradingSession = false;
|
||||||
@@ -128,6 +129,15 @@
|
|||||||
if (hint) hint.textContent = msg || '';
|
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) {
|
function isCtpUnreachableError(msg) {
|
||||||
return !!(msg && (msg.indexOf('不可达') >= 0 || msg.indexOf('Connection refused') >= 0 || msg.indexOf('timed out') >= 0));
|
return !!(msg && (msg.indexOf('不可达') >= 0 || msg.indexOf('Connection refused') >= 0 || msg.indexOf('timed out') >= 0));
|
||||||
}
|
}
|
||||||
@@ -143,7 +153,9 @@
|
|||||||
updateCtpBadge(!!connected, !!connecting);
|
updateCtpBadge(!!connected, !!connecting);
|
||||||
if (!connected && !connecting && data.ctp_status && data.ctp_status.last_error) {
|
if (!connected && !connecting && data.ctp_status && data.ctp_status.last_error) {
|
||||||
showCtpError(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();
|
lastCtpUnreachableAt = Date.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,7 +354,9 @@
|
|||||||
updateCtpBadge(false, false);
|
updateCtpBadge(false, false);
|
||||||
if (st.last_error) {
|
if (st.last_error) {
|
||||||
showCtpError(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();
|
lastCtpUnreachableAt = Date.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,6 +511,7 @@
|
|||||||
if (ctpReconnecting || ctpConnectInflight) return;
|
if (ctpReconnecting || ctpConnectInflight) return;
|
||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
if (now - lastCtpReconnectAt < 60000) return;
|
if (now - lastCtpReconnectAt < 60000) return;
|
||||||
|
if (lastCtpLoginBanAt && now - lastCtpLoginBanAt < 2700000) return;
|
||||||
if (lastCtpUnreachableAt && now - lastCtpUnreachableAt < 300000) return;
|
if (lastCtpUnreachableAt && now - lastCtpUnreachableAt < 300000) return;
|
||||||
lastCtpReconnectAt = now;
|
lastCtpReconnectAt = now;
|
||||||
ctpReconnecting = true;
|
ctpReconnecting = true;
|
||||||
|
|||||||
+56
-1
@@ -22,6 +22,8 @@ GATEWAY_NAME = "CTP"
|
|||||||
|
|
||||||
CONNECT_WAIT_SEC = 60
|
CONNECT_WAIT_SEC = 60
|
||||||
CONNECT_POLL_INTERVAL_SEC = 0.5
|
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
|
_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,并确认服务器能访问该地址。"
|
"tcp://180.168.146.187:10201 / 10211,并确认服务器能访问该地址。"
|
||||||
)
|
)
|
||||||
text = "\n".join(ctp_logs)
|
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():
|
if "4097" in text or "Decrypt handshake" in text or "shake hand" in text.lower():
|
||||||
return (
|
return (
|
||||||
"CTP 握手失败(4097):vnpy_ctp 与 SimNow 前置加密不匹配。"
|
"CTP 握手失败(4097):vnpy_ctp 与 SimNow 前置加密不匹配。"
|
||||||
@@ -130,6 +137,7 @@ class CtpBridge:
|
|||||||
self._last_error: str = ""
|
self._last_error: str = ""
|
||||||
self._connect_lock = threading.Lock()
|
self._connect_lock = threading.Lock()
|
||||||
self._connect_in_progress = False
|
self._connect_in_progress = False
|
||||||
|
self._login_cooldown_until: float = 0.0
|
||||||
self._commission_waiters: dict[int, threading.Event] = {}
|
self._commission_waiters: dict[int, threading.Event] = {}
|
||||||
self._commission_lists: dict[int, list] = {}
|
self._commission_lists: dict[int, list] = {}
|
||||||
self._commission_hooked = False
|
self._commission_hooked = False
|
||||||
@@ -172,6 +180,33 @@ class CtpBridge:
|
|||||||
def connect_in_progress(self) -> bool:
|
def connect_in_progress(self) -> bool:
|
||||||
return self._connect_in_progress
|
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:
|
def _close_gateway(self) -> None:
|
||||||
"""关闭 CTP 网关,避免半连接状态下重连卡在「连接登录」。"""
|
"""关闭 CTP 网关,避免半连接状态下重连卡在「连接登录」。"""
|
||||||
if not self._engine:
|
if not self._engine:
|
||||||
@@ -186,7 +221,11 @@ class CtpBridge:
|
|||||||
time.sleep(0.6)
|
time.sleep(0.6)
|
||||||
|
|
||||||
def _login_rejected(self, ctp_logs: list[str]) -> bool:
|
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:
|
def _wait_connected(self, mode: str, ctp_logs: list[str] | None = None) -> bool:
|
||||||
"""等待账户回报或交易通道登录成功。"""
|
"""等待账户回报或交易通道登录成功。"""
|
||||||
@@ -220,6 +259,7 @@ class CtpBridge:
|
|||||||
"mode_label": _mode_label(mode),
|
"mode_label": _mode_label(mode),
|
||||||
"missing_config": missing,
|
"missing_config": missing,
|
||||||
"last_error": self._last_error,
|
"last_error": self._last_error,
|
||||||
|
"login_cooldown_sec": self.login_cooldown_remaining(),
|
||||||
"broker_id": st.get("经纪商代码", ""),
|
"broker_id": st.get("经纪商代码", ""),
|
||||||
"td_address": st.get("交易服务器", ""),
|
"td_address": st.get("交易服务器", ""),
|
||||||
}
|
}
|
||||||
@@ -227,6 +267,10 @@ class CtpBridge:
|
|||||||
def connect(self, mode: str, *, force: bool = False) -> None:
|
def connect(self, mode: str, *, force: bool = False) -> None:
|
||||||
if self._connect_in_progress:
|
if self._connect_in_progress:
|
||||||
raise RuntimeError("CTP 正在连接中,请稍候")
|
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:
|
if not self._engine:
|
||||||
raise RuntimeError(self._last_error or "vnpy 引擎未初始化")
|
raise RuntimeError(self._last_error or "vnpy 引擎未初始化")
|
||||||
if self._connected_mode == mode and not force:
|
if self._connected_mode == mode and not force:
|
||||||
@@ -305,6 +349,7 @@ class CtpBridge:
|
|||||||
self._ee.unregister(EVENT_LOG, _on_log)
|
self._ee.unregister(EVENT_LOG, _on_log)
|
||||||
|
|
||||||
self._close_gateway()
|
self._close_gateway()
|
||||||
|
self._apply_login_failure_cooldown(ctp_logs)
|
||||||
hint = _format_ctp_failure(ctp_logs, td_address=setting.get("交易服务器", ""))
|
hint = _format_ctp_failure(ctp_logs, td_address=setting.get("交易服务器", ""))
|
||||||
self._last_error = hint
|
self._last_error = hint
|
||||||
logger.warning("CTP 连接失败 [%s]: %s | logs=%s", mode, hint, ctp_logs[-5:])
|
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}
|
return {"started": False, "connecting": False, "connected": True}
|
||||||
if self._connect_in_progress:
|
if self._connect_in_progress:
|
||||||
return {"started": False, "connecting": True, "connected": False}
|
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:
|
def _run() -> None:
|
||||||
try:
|
try:
|
||||||
@@ -1092,6 +1145,8 @@ def ctp_try_auto_reconnect(mode: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
if b.connect_in_progress():
|
if b.connect_in_progress():
|
||||||
return False
|
return False
|
||||||
|
if b.login_cooldown_remaining() > 0:
|
||||||
|
return False
|
||||||
st = _setting_for_mode(mode)
|
st = _setting_for_mode(mode)
|
||||||
if not st.get("用户名") or not st.get("密码") or not st.get("交易服务器"):
|
if not st.get("用户名") or not st.get("密码") or not st.get("交易服务器"):
|
||||||
return False
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user