diff --git a/app.py b/app.py
index 2b25e8f..0c081c5 100644
--- a/app.py
+++ b/app.py
@@ -205,7 +205,9 @@ def require_nav(key: str):
@app.context_processor
def inject_globals():
- return {"nav_items": get_nav_items(get_setting)}
+ trade_js = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static", "js", "trade.js")
+ asset_v = str(int(os.path.getmtime(trade_js))) if os.path.isfile(trade_js) else "0"
+ return {"nav_items": get_nav_items(get_setting), "asset_v": asset_v}
def _trading_mode() -> str:
@@ -1714,9 +1716,16 @@ def settings():
save_ctp_settings_from_form(request.form, set_setting)
try:
- from vnpy_bridge import get_bridge
+ from vnpy_bridge import get_bridge, _persist_last_error
- get_bridge().mark_disconnected()
+ b = get_bridge()
+ b.mark_disconnected()
+ if (request.form.get("simnow_password") or "").strip() or (
+ request.form.get("ctp_live_password") or ""
+ ).strip():
+ b._clear_login_cooldown()
+ b._last_error = ""
+ _persist_last_error("")
except Exception:
pass
flash("CTP 配置已保存,请在持仓监控页重连 CTP")
diff --git a/static/js/trade.js b/static/js/trade.js
index 6cf246c..f2cb456 100644
--- a/static/js/trade.js
+++ b/static/js/trade.js
@@ -394,7 +394,6 @@
return Promise.resolve({});
}
ctpConnectInflight = true;
- updateCtpBadge(false, true);
return fetch('/api/ctp/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -415,6 +414,7 @@
return d;
}
if (d.connecting || st.connecting) {
+ updateCtpBadge(false, true);
return waitForCtpConnected(70000).then(function (ok) {
if (!ok && d.error) showCtpError(d.error);
else if (!ok && st.last_error) showCtpError(st.last_error);
@@ -1086,13 +1086,7 @@
var st = d.status || {};
syncCtpBadgeFromStatus(st);
if (st.last_error) showCtpError(st.last_error);
- if (st.connected) {
- pollPositions();
- return;
- }
- if ((st.login_cooldown_sec || 0) > 0) return;
- if (isCtpLoginBanError(st.last_error)) return;
- if (!st.connecting) requestCtpConnect(false);
+ if (st.connected) pollPositions();
})
.catch(function () {});
}
diff --git a/templates/trade.html b/templates/trade.html
index b1e20c3..37b2052 100644
--- a/templates/trade.html
+++ b/templates/trade.html
@@ -161,5 +161,5 @@ window.TRADE_SIZING_MODE = {{ sizing_mode|tojson }};
window.TRADE_FIXED_LOTS = {{ fixed_lots|tojson }};
window.TRADE_FIXED_AMOUNT = {{ fixed_amount|tojson }};
-
+
{% endblock %}
diff --git a/vnpy_bridge.py b/vnpy_bridge.py
index 496d66e..3269221 100644
--- a/vnpy_bridge.py
+++ b/vnpy_bridge.py
@@ -24,6 +24,48 @@ CONNECT_WAIT_SEC = 60
CONNECT_POLL_INTERVAL_SEC = 0.5
LOGIN_BAN_COOLDOWN_SEC = 45 * 60
LOGIN_FAIL_COOLDOWN_SEC = 5 * 60
+CTP_COOLDOWN_UNTIL_KEY = "ctp_login_cooldown_until"
+CTP_LAST_ERROR_KEY = "ctp_last_error"
+
+
+def _persist_login_cooldown(seconds: float) -> None:
+ from fee_specs import get_setting, set_setting
+
+ new_until = time.time() + max(0.0, seconds)
+ try:
+ old = float(get_setting(CTP_COOLDOWN_UNTIL_KEY, "0") or 0)
+ except (TypeError, ValueError):
+ old = 0.0
+ if new_until > old:
+ set_setting(CTP_COOLDOWN_UNTIL_KEY, str(new_until))
+
+
+def _persisted_login_cooldown_remaining() -> int:
+ from fee_specs import get_setting
+
+ try:
+ until = float(get_setting(CTP_COOLDOWN_UNTIL_KEY, "0") or 0)
+ return max(0, int(until - time.time()))
+ except (TypeError, ValueError):
+ return 0
+
+
+def _clear_persisted_login_cooldown() -> None:
+ from fee_specs import set_setting
+
+ set_setting(CTP_COOLDOWN_UNTIL_KEY, "0")
+
+
+def _persist_last_error(msg: str) -> None:
+ from fee_specs import set_setting
+
+ set_setting(CTP_LAST_ERROR_KEY, (msg or "").strip())
+
+
+def _load_persisted_last_error() -> str:
+ from fee_specs import get_setting
+
+ return (get_setting(CTP_LAST_ERROR_KEY, "") or "").strip()
_position_refresh_callback: Optional[Callable[[], None]] = None
@@ -138,6 +180,7 @@ class CtpBridge:
self._connect_lock = threading.Lock()
self._connect_in_progress = False
self._login_cooldown_until: float = 0.0
+ self._restore_persisted_state()
self._commission_waiters: dict[int, threading.Event] = {}
self._commission_lists: dict[int, list] = {}
self._commission_hooked = False
@@ -180,10 +223,18 @@ class CtpBridge:
def connect_in_progress(self) -> bool:
return self._connect_in_progress
+ def _restore_persisted_state(self) -> None:
+ err = _load_persisted_last_error()
+ if err:
+ self._last_error = err
+ db_remain = _persisted_login_cooldown_remaining()
+ if db_remain > 0:
+ self._login_cooldown_until = time.monotonic() + db_remain
+
def login_cooldown_remaining(self) -> int:
- """距允许再次登录的剩余秒数。"""
- remain = int(self._login_cooldown_until - time.monotonic())
- return max(0, remain)
+ """距允许再次登录的剩余秒数(内存 + 数据库,重启后仍有效)。"""
+ mem = max(0, int(self._login_cooldown_until - time.monotonic()))
+ return max(mem, _persisted_login_cooldown_remaining())
def _is_login_cooldown_active(self) -> bool:
return self.login_cooldown_remaining() > 0
@@ -192,6 +243,11 @@ class CtpBridge:
until = time.monotonic() + max(0.0, seconds)
if until > self._login_cooldown_until:
self._login_cooldown_until = until
+ _persist_login_cooldown(seconds)
+
+ def _clear_login_cooldown(self) -> None:
+ self._login_cooldown_until = 0.0
+ _clear_persisted_login_cooldown()
def _apply_login_failure_cooldown(self, ctp_logs: list[str]) -> None:
text = "\n".join(ctp_logs)
@@ -253,6 +309,7 @@ class CtpBridge:
missing = [k for k in ("用户名", "密码", "交易服务器") if not st.get(k)]
cooldown = self.login_cooldown_remaining()
connecting = bool(self._connect_in_progress and cooldown <= 0)
+ last_error = self._last_error or _load_persisted_last_error()
return {
"vnpy_installed": self.available(),
"connected": self._connected_mode == mode,
@@ -260,8 +317,8 @@ class CtpBridge:
"connected_mode": self._connected_mode,
"mode_label": _mode_label(mode),
"missing_config": missing,
- "last_error": self._last_error,
- "login_cooldown_sec": self.login_cooldown_remaining(),
+ "last_error": last_error,
+ "login_cooldown_sec": cooldown,
"broker_id": st.get("经纪商代码", ""),
"td_address": st.get("交易服务器", ""),
}
@@ -336,6 +393,8 @@ class CtpBridge:
if self._wait_connected(mode, ctp_logs):
self._connected_mode = mode
self._last_error = ""
+ _persist_last_error("")
+ self._clear_login_cooldown()
logger.info("CTP 已连接 [%s] td_login=%s accounts=%s",
mode, self._td_logged_in(),
len(self._engine.get_all_accounts() or []))
@@ -354,6 +413,7 @@ class CtpBridge:
self._apply_login_failure_cooldown(ctp_logs)
hint = _format_ctp_failure(ctp_logs, td_address=setting.get("交易服务器", ""))
self._last_error = hint
+ _persist_last_error(hint)
logger.warning("CTP 连接失败 [%s]: %s | logs=%s", mode, hint, ctp_logs[-5:])
raise RuntimeError(hint)
finally: