fix: CTP登录冷却持久化到数据库,取消页面自动连并刷新JS缓存
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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")
|
||||
|
||||
+2
-8
@@ -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 () {});
|
||||
}
|
||||
|
||||
@@ -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 }};
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/trade.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/trade.js') }}?v={{ asset_v }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
+65
-5
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user