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
|
@app.context_processor
|
||||||
def inject_globals():
|
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:
|
def _trading_mode() -> str:
|
||||||
@@ -1714,9 +1716,16 @@ def settings():
|
|||||||
|
|
||||||
save_ctp_settings_from_form(request.form, set_setting)
|
save_ctp_settings_from_form(request.form, set_setting)
|
||||||
try:
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
flash("CTP 配置已保存,请在持仓监控页重连 CTP")
|
flash("CTP 配置已保存,请在持仓监控页重连 CTP")
|
||||||
|
|||||||
+2
-8
@@ -394,7 +394,6 @@
|
|||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
}
|
}
|
||||||
ctpConnectInflight = true;
|
ctpConnectInflight = true;
|
||||||
updateCtpBadge(false, true);
|
|
||||||
return fetch('/api/ctp/connect', {
|
return fetch('/api/ctp/connect', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -415,6 +414,7 @@
|
|||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
if (d.connecting || st.connecting) {
|
if (d.connecting || st.connecting) {
|
||||||
|
updateCtpBadge(false, true);
|
||||||
return waitForCtpConnected(70000).then(function (ok) {
|
return waitForCtpConnected(70000).then(function (ok) {
|
||||||
if (!ok && d.error) showCtpError(d.error);
|
if (!ok && d.error) showCtpError(d.error);
|
||||||
else if (!ok && st.last_error) showCtpError(st.last_error);
|
else if (!ok && st.last_error) showCtpError(st.last_error);
|
||||||
@@ -1086,13 +1086,7 @@
|
|||||||
var st = d.status || {};
|
var st = d.status || {};
|
||||||
syncCtpBadgeFromStatus(st);
|
syncCtpBadgeFromStatus(st);
|
||||||
if (st.last_error) showCtpError(st.last_error);
|
if (st.last_error) showCtpError(st.last_error);
|
||||||
if (st.connected) {
|
if (st.connected) pollPositions();
|
||||||
pollPositions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((st.login_cooldown_sec || 0) > 0) return;
|
|
||||||
if (isCtpLoginBanError(st.last_error)) return;
|
|
||||||
if (!st.connecting) requestCtpConnect(false);
|
|
||||||
})
|
})
|
||||||
.catch(function () {});
|
.catch(function () {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,5 +161,5 @@ window.TRADE_SIZING_MODE = {{ sizing_mode|tojson }};
|
|||||||
window.TRADE_FIXED_LOTS = {{ fixed_lots|tojson }};
|
window.TRADE_FIXED_LOTS = {{ fixed_lots|tojson }};
|
||||||
window.TRADE_FIXED_AMOUNT = {{ fixed_amount|tojson }};
|
window.TRADE_FIXED_AMOUNT = {{ fixed_amount|tojson }};
|
||||||
</script>
|
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
+65
-5
@@ -24,6 +24,48 @@ CONNECT_WAIT_SEC = 60
|
|||||||
CONNECT_POLL_INTERVAL_SEC = 0.5
|
CONNECT_POLL_INTERVAL_SEC = 0.5
|
||||||
LOGIN_BAN_COOLDOWN_SEC = 45 * 60
|
LOGIN_BAN_COOLDOWN_SEC = 45 * 60
|
||||||
LOGIN_FAIL_COOLDOWN_SEC = 5 * 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
|
_position_refresh_callback: Optional[Callable[[], None]] = None
|
||||||
|
|
||||||
@@ -138,6 +180,7 @@ class CtpBridge:
|
|||||||
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._login_cooldown_until: float = 0.0
|
||||||
|
self._restore_persisted_state()
|
||||||
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
|
||||||
@@ -180,10 +223,18 @@ 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 _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:
|
def login_cooldown_remaining(self) -> int:
|
||||||
"""距允许再次登录的剩余秒数。"""
|
"""距允许再次登录的剩余秒数(内存 + 数据库,重启后仍有效)。"""
|
||||||
remain = int(self._login_cooldown_until - time.monotonic())
|
mem = max(0, int(self._login_cooldown_until - time.monotonic()))
|
||||||
return max(0, remain)
|
return max(mem, _persisted_login_cooldown_remaining())
|
||||||
|
|
||||||
def _is_login_cooldown_active(self) -> bool:
|
def _is_login_cooldown_active(self) -> bool:
|
||||||
return self.login_cooldown_remaining() > 0
|
return self.login_cooldown_remaining() > 0
|
||||||
@@ -192,6 +243,11 @@ class CtpBridge:
|
|||||||
until = time.monotonic() + max(0.0, seconds)
|
until = time.monotonic() + max(0.0, seconds)
|
||||||
if until > self._login_cooldown_until:
|
if until > self._login_cooldown_until:
|
||||||
self._login_cooldown_until = 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:
|
def _apply_login_failure_cooldown(self, ctp_logs: list[str]) -> None:
|
||||||
text = "\n".join(ctp_logs)
|
text = "\n".join(ctp_logs)
|
||||||
@@ -253,6 +309,7 @@ class CtpBridge:
|
|||||||
missing = [k for k in ("用户名", "密码", "交易服务器") if not st.get(k)]
|
missing = [k for k in ("用户名", "密码", "交易服务器") if not st.get(k)]
|
||||||
cooldown = self.login_cooldown_remaining()
|
cooldown = self.login_cooldown_remaining()
|
||||||
connecting = bool(self._connect_in_progress and cooldown <= 0)
|
connecting = bool(self._connect_in_progress and cooldown <= 0)
|
||||||
|
last_error = self._last_error or _load_persisted_last_error()
|
||||||
return {
|
return {
|
||||||
"vnpy_installed": self.available(),
|
"vnpy_installed": self.available(),
|
||||||
"connected": self._connected_mode == mode,
|
"connected": self._connected_mode == mode,
|
||||||
@@ -260,8 +317,8 @@ class CtpBridge:
|
|||||||
"connected_mode": self._connected_mode,
|
"connected_mode": self._connected_mode,
|
||||||
"mode_label": _mode_label(mode),
|
"mode_label": _mode_label(mode),
|
||||||
"missing_config": missing,
|
"missing_config": missing,
|
||||||
"last_error": self._last_error,
|
"last_error": last_error,
|
||||||
"login_cooldown_sec": self.login_cooldown_remaining(),
|
"login_cooldown_sec": cooldown,
|
||||||
"broker_id": st.get("经纪商代码", ""),
|
"broker_id": st.get("经纪商代码", ""),
|
||||||
"td_address": st.get("交易服务器", ""),
|
"td_address": st.get("交易服务器", ""),
|
||||||
}
|
}
|
||||||
@@ -336,6 +393,8 @@ class CtpBridge:
|
|||||||
if self._wait_connected(mode, ctp_logs):
|
if self._wait_connected(mode, ctp_logs):
|
||||||
self._connected_mode = mode
|
self._connected_mode = mode
|
||||||
self._last_error = ""
|
self._last_error = ""
|
||||||
|
_persist_last_error("")
|
||||||
|
self._clear_login_cooldown()
|
||||||
logger.info("CTP 已连接 [%s] td_login=%s accounts=%s",
|
logger.info("CTP 已连接 [%s] td_login=%s accounts=%s",
|
||||||
mode, self._td_logged_in(),
|
mode, self._td_logged_in(),
|
||||||
len(self._engine.get_all_accounts() or []))
|
len(self._engine.get_all_accounts() or []))
|
||||||
@@ -354,6 +413,7 @@ class CtpBridge:
|
|||||||
self._apply_login_failure_cooldown(ctp_logs)
|
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
|
||||||
|
_persist_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:])
|
||||||
raise RuntimeError(hint)
|
raise RuntimeError(hint)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
Reference in New Issue
Block a user