fix: CTP重连前探测前置可达性,失败时关闭网关并明确报错
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,8 +18,8 @@ SERVICE_NAME="qihuo"
|
||||
|
||||
# SimNow 前置候选(按优先级;部署时自动 nc 探测)
|
||||
SIMNOW_FRONTS=(
|
||||
"182.254.243.31:30001:30011"
|
||||
"180.168.146.187:10201:10211"
|
||||
"180.168.146.187:10202:10212"
|
||||
"180.168.146.187:10130:10131"
|
||||
"218.202.237.33:10203:10213"
|
||||
)
|
||||
|
||||
+5
-5
@@ -139,15 +139,15 @@ tcp://IP:端口
|
||||
pm2 restart qihuo
|
||||
```
|
||||
|
||||
**云服务器若 `180.168.146.187` 端口超时**,可改用备用前置(交易时段):
|
||||
**云服务器网络说明**:`182.254.243.31` 段前置已停用(Connection refused),请勿再使用。官方前置为 `180.168.146.187:10201/10211`。若服务器 `nc -zv 180.168.146.187 10201` 超时,属于**出网/防火墙**问题,需联系云厂商放行或换能访问 SimNow 的网络,无法仅靠改代码解决。
|
||||
|
||||
旧文档备用地址(已失效,仅作排查参考):
|
||||
|
||||
```env
|
||||
SIMNOW_TD_ADDRESS=tcp://182.254.243.31:30001
|
||||
SIMNOW_MD_ADDRESS=tcp://182.254.243.31:30011
|
||||
# 勿用 — 已 dead
|
||||
# SIMNOW_TD_ADDRESS=tcp://182.254.243.31:30001
|
||||
```
|
||||
|
||||
服务器上用 `nc -zv 182.254.243.31 30001` 验证连通后再配置。
|
||||
|
||||
### 3. 网页端连接 CTP
|
||||
|
||||
1. 登录本系统
|
||||
|
||||
+38
-8
@@ -19,7 +19,9 @@
|
||||
var lastQuotePrice = null;
|
||||
var priceType = 'limit';
|
||||
var lastCtpReconnectAt = 0;
|
||||
var lastCtpUnreachableAt = 0;
|
||||
var ctpReconnecting = false;
|
||||
var ctpConnectInflight = false;
|
||||
var isTradingSession = false;
|
||||
var hasSlTpMonitoring = false;
|
||||
var ctpConnected = false;
|
||||
@@ -121,6 +123,15 @@
|
||||
} catch (e) { /* quota */ }
|
||||
}
|
||||
|
||||
function showCtpError(msg) {
|
||||
var hint = document.querySelector('.ctp-install-hint');
|
||||
if (hint) hint.textContent = msg || '';
|
||||
}
|
||||
|
||||
function isCtpUnreachableError(msg) {
|
||||
return !!(msg && (msg.indexOf('不可达') >= 0 || msg.indexOf('Connection refused') >= 0 || msg.indexOf('timed out') >= 0));
|
||||
}
|
||||
|
||||
function applyPositionsData(data) {
|
||||
if (!list || !data) return;
|
||||
var cap = document.getElementById('cap-display');
|
||||
@@ -130,6 +141,12 @@
|
||||
ctpConnected = !!connected;
|
||||
isTradingSession = !!data.trading_session;
|
||||
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)) {
|
||||
lastCtpUnreachableAt = Date.now();
|
||||
}
|
||||
}
|
||||
var riskBadge = document.getElementById('risk-badge');
|
||||
if (riskBadge && data.risk_status) {
|
||||
riskBadge.textContent = data.risk_status.status_label || '';
|
||||
@@ -300,7 +317,7 @@
|
||||
}
|
||||
|
||||
function waitForCtpConnected(maxMs) {
|
||||
var deadline = Date.now() + (maxMs || 35000);
|
||||
var deadline = Date.now() + (maxMs || 70000);
|
||||
function tick() {
|
||||
return fetch('/api/ctp/status')
|
||||
.then(function (r) { return r.json(); })
|
||||
@@ -308,6 +325,7 @@
|
||||
var st = d.status || {};
|
||||
if (st.connected) {
|
||||
updateCtpBadge(true, false);
|
||||
showCtpError('');
|
||||
if (d.account && d.account.available != null) {
|
||||
var avail = document.getElementById('avail-display');
|
||||
if (avail) avail.textContent = Number(d.account.available).toFixed(2);
|
||||
@@ -323,8 +341,10 @@
|
||||
}
|
||||
updateCtpBadge(false, false);
|
||||
if (st.last_error) {
|
||||
var hint = document.querySelector('.ctp-install-hint');
|
||||
if (hint) hint.textContent = st.last_error;
|
||||
showCtpError(st.last_error);
|
||||
if (isCtpUnreachableError(st.last_error)) {
|
||||
lastCtpUnreachableAt = Date.now();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
@@ -334,6 +354,10 @@
|
||||
}
|
||||
|
||||
function requestCtpConnect(force) {
|
||||
if (!force && ctpConnectInflight) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
ctpConnectInflight = true;
|
||||
updateCtpBadge(false, true);
|
||||
return fetch('/api/ctp/connect', {
|
||||
method: 'POST',
|
||||
@@ -344,24 +368,29 @@
|
||||
.then(function (d) {
|
||||
if (d.status && d.status.connected) {
|
||||
updateCtpBadge(true, false);
|
||||
showCtpError('');
|
||||
pollPositions();
|
||||
return d;
|
||||
}
|
||||
if (d.connecting || (d.status && d.status.connecting)) {
|
||||
return waitForCtpConnected(35000).then(function (ok) {
|
||||
if (!ok && d.error) alert(d.error);
|
||||
else if (!ok && d.status && d.status.last_error) alert(d.status.last_error);
|
||||
return waitForCtpConnected(70000).then(function (ok) {
|
||||
if (!ok && d.error) showCtpError(d.error);
|
||||
else if (!ok && d.status && d.status.last_error) showCtpError(d.status.last_error);
|
||||
return d;
|
||||
});
|
||||
}
|
||||
if (!d.ok) {
|
||||
updateCtpBadge(false, false);
|
||||
alert(d.error || (d.status && d.status.last_error) || '连接失败');
|
||||
var err = d.error || (d.status && d.status.last_error) || '连接失败';
|
||||
showCtpError(err);
|
||||
}
|
||||
return d;
|
||||
})
|
||||
.catch(function () {
|
||||
updateCtpBadge(false, false);
|
||||
})
|
||||
.finally(function () {
|
||||
ctpConnectInflight = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -465,9 +494,10 @@
|
||||
}
|
||||
|
||||
function tryAutoCtpReconnect() {
|
||||
if (ctpReconnecting) return;
|
||||
if (ctpReconnecting || ctpConnectInflight) return;
|
||||
var now = Date.now();
|
||||
if (now - lastCtpReconnectAt < 60000) return;
|
||||
if (lastCtpUnreachableAt && now - lastCtpUnreachableAt < 300000) return;
|
||||
lastCtpReconnectAt = now;
|
||||
ctpReconnecting = true;
|
||||
requestCtpConnect(false).finally(function () {
|
||||
|
||||
+113
-28
@@ -19,6 +19,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
GATEWAY_NAME = "CTP"
|
||||
|
||||
CONNECT_WAIT_SEC = 60
|
||||
CONNECT_POLL_INTERVAL_SEC = 0.5
|
||||
|
||||
_position_refresh_callback: Optional[Callable[[], None]] = None
|
||||
|
||||
|
||||
@@ -79,8 +82,38 @@ def _mode_label(mode: str) -> str:
|
||||
return "SimNow" if mode == "simulation" else "期货公司实盘"
|
||||
|
||||
|
||||
def _format_ctp_failure(ctp_logs: list[str]) -> str:
|
||||
def _parse_tcp_address(address: str) -> tuple[str, int]:
|
||||
raw = (address or "").strip()
|
||||
if raw.startswith("tcp://"):
|
||||
raw = raw[6:]
|
||||
if ":" not in raw:
|
||||
raise ValueError(f"无效 TCP 地址: {address}")
|
||||
host, port_s = raw.rsplit(":", 1)
|
||||
return host, int(port_s)
|
||||
|
||||
|
||||
def probe_tcp_address(address: str, timeout: float = 5.0) -> tuple[bool, str]:
|
||||
"""探测 CTP 前置 TCP 是否可达。"""
|
||||
import socket
|
||||
|
||||
try:
|
||||
host, port = _parse_tcp_address(address)
|
||||
with socket.create_connection((host, port), timeout=timeout):
|
||||
return True, ""
|
||||
except Exception as exc:
|
||||
return False, str(exc)
|
||||
|
||||
|
||||
def _format_ctp_failure(ctp_logs: list[str], *, td_address: str = "") -> str:
|
||||
"""根据 CTP 网关日志拼出可读错误。"""
|
||||
if td_address:
|
||||
ok, err = probe_tcp_address(td_address, timeout=4.0)
|
||||
if not ok:
|
||||
return (
|
||||
f"SimNow 交易前置不可达:{td_address}({err})。"
|
||||
"182.254.243.31 已停用,请改 .env 为官方前置 "
|
||||
"tcp://180.168.146.187:10201 / 10211,并确认服务器能访问该地址。"
|
||||
)
|
||||
text = "\n".join(ctp_logs)
|
||||
if "4097" in text or "Decrypt handshake" in text or "shake hand" in text.lower():
|
||||
return (
|
||||
@@ -160,6 +193,35 @@ class CtpBridge:
|
||||
def connect_in_progress(self) -> bool:
|
||||
return self._connect_in_progress
|
||||
|
||||
def _close_gateway(self) -> None:
|
||||
"""关闭 CTP 网关,避免半连接状态下重连卡在「连接登录」。"""
|
||||
if not self._engine:
|
||||
return
|
||||
try:
|
||||
gw = self._engine.get_gateway(GATEWAY_NAME)
|
||||
if gw:
|
||||
gw.close()
|
||||
except Exception as exc:
|
||||
logger.debug("gateway close: %s", exc)
|
||||
self._connected_mode = None
|
||||
time.sleep(0.6)
|
||||
|
||||
def _wait_connected(self, mode: str) -> bool:
|
||||
"""等待账户回报或交易通道登录成功。"""
|
||||
if not self._engine:
|
||||
return False
|
||||
loops = max(1, int(CONNECT_WAIT_SEC / CONNECT_POLL_INTERVAL_SEC))
|
||||
for _ in range(loops):
|
||||
try:
|
||||
if self._engine.get_all_accounts():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
if self._td_logged_in():
|
||||
return True
|
||||
time.sleep(CONNECT_POLL_INTERVAL_SEC)
|
||||
return False
|
||||
|
||||
def status(self, mode: str) -> dict[str, Any]:
|
||||
if self._connected_mode == mode:
|
||||
self.ping()
|
||||
@@ -199,14 +261,7 @@ class CtpBridge:
|
||||
try:
|
||||
with self._connect_lock:
|
||||
if force and self._connected_mode:
|
||||
try:
|
||||
gw = self._engine.get_gateway(GATEWAY_NAME)
|
||||
if gw:
|
||||
gw.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._connected_mode = None
|
||||
time.sleep(0.8)
|
||||
self._close_gateway()
|
||||
elif self._connected_mode and self._connected_mode != mode:
|
||||
try:
|
||||
self._engine.close()
|
||||
@@ -214,6 +269,8 @@ class CtpBridge:
|
||||
pass
|
||||
self._connected_mode = None
|
||||
time.sleep(1)
|
||||
elif not (self._connected_mode == mode and self.ping()):
|
||||
self._close_gateway()
|
||||
|
||||
ctp_logs: list[str] = []
|
||||
from vnpy.trader.event import EVENT_LOG
|
||||
@@ -222,7 +279,7 @@ class CtpBridge:
|
||||
msg = getattr(event.data, "msg", "") or str(event.data)
|
||||
if msg:
|
||||
ctp_logs.append(str(msg))
|
||||
if len(ctp_logs) > 20:
|
||||
if len(ctp_logs) > 40:
|
||||
ctp_logs.pop(0)
|
||||
logger.info("CTP | %s", msg)
|
||||
|
||||
@@ -236,27 +293,36 @@ class CtpBridge:
|
||||
setting.get("交易服务器"),
|
||||
setting.get("柜台环境", "实盘"),
|
||||
)
|
||||
td_addr = setting.get("交易服务器", "")
|
||||
ok, err = probe_tcp_address(td_addr, timeout=5.0)
|
||||
if not ok:
|
||||
raise RuntimeError(
|
||||
f"SimNow 交易前置不可达:{td_addr}({err})。"
|
||||
"请更新 .env 中 SIMNOW_TD_ADDRESS 为官网最新地址,"
|
||||
"并在服务器执行 nc -zv 验证出网。"
|
||||
)
|
||||
self._engine.connect(setting, GATEWAY_NAME)
|
||||
for _ in range(60):
|
||||
accounts = self._engine.get_all_accounts()
|
||||
if accounts:
|
||||
self._connected_mode = mode
|
||||
self._last_error = ""
|
||||
logger.info("CTP 已连接 [%s] account=%s", mode, len(accounts))
|
||||
self._install_position_margin_hook()
|
||||
self._schedule_fee_sync(mode)
|
||||
try:
|
||||
self.refresh_positions()
|
||||
except Exception as exc:
|
||||
logger.debug("initial position query: %s", exc)
|
||||
_fire_position_refresh_callback()
|
||||
return
|
||||
time.sleep(0.5)
|
||||
if self._wait_connected(mode):
|
||||
self._connected_mode = mode
|
||||
self._last_error = ""
|
||||
logger.info("CTP 已连接 [%s] td_login=%s accounts=%s",
|
||||
mode, self._td_logged_in(),
|
||||
len(self._engine.get_all_accounts() or []))
|
||||
self._install_position_margin_hook()
|
||||
self._schedule_fee_sync(mode)
|
||||
try:
|
||||
self.refresh_positions()
|
||||
except Exception as exc:
|
||||
logger.debug("initial position query: %s", exc)
|
||||
_fire_position_refresh_callback()
|
||||
return
|
||||
finally:
|
||||
self._ee.unregister(EVENT_LOG, _on_log)
|
||||
|
||||
hint = _format_ctp_failure(ctp_logs)
|
||||
self._close_gateway()
|
||||
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:])
|
||||
raise RuntimeError(hint)
|
||||
finally:
|
||||
self._connect_in_progress = False
|
||||
@@ -1040,12 +1106,20 @@ def ctp_try_auto_reconnect(mode: str) -> bool:
|
||||
if not b.available():
|
||||
return False
|
||||
if b.connect_in_progress():
|
||||
return True
|
||||
return False
|
||||
st = _setting_for_mode(mode)
|
||||
if not st.get("用户名") or not st.get("密码") or not st.get("交易服务器"):
|
||||
return False
|
||||
if b.connected_mode == mode and b.ping():
|
||||
return True
|
||||
td = st.get("交易服务器", "")
|
||||
ok, err = probe_tcp_address(td, timeout=4.0)
|
||||
if not ok:
|
||||
b._last_error = (
|
||||
f"SimNow 交易前置不可达:{td}({err})。"
|
||||
"请更新 SIMNOW_TD_ADDRESS 并确认服务器出网。"
|
||||
)
|
||||
return False
|
||||
info = b.start_connect_async(mode, force=False)
|
||||
return bool(
|
||||
info.get("connected")
|
||||
@@ -1055,7 +1129,18 @@ def ctp_try_auto_reconnect(mode: str) -> bool:
|
||||
|
||||
|
||||
def ctp_status(mode: str) -> dict[str, Any]:
|
||||
return get_bridge().status(mode)
|
||||
st = get_bridge().status(mode)
|
||||
if not st.get("connected") and not st.get("connecting"):
|
||||
setting = _setting_for_mode(mode)
|
||||
td = setting.get("交易服务器", "")
|
||||
if td:
|
||||
ok, err = probe_tcp_address(td, timeout=3.0)
|
||||
st["td_reachable"] = ok
|
||||
if not ok and not st.get("last_error"):
|
||||
st["last_error"] = (
|
||||
f"SimNow 交易前置不可达:{td}({err})"
|
||||
)
|
||||
return st
|
||||
|
||||
|
||||
def ctp_get_account(mode: str) -> dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user