Add CTP auto-connect toggle to stop off-hours reconnect attempts.

When disabled, disconnect immediately and skip auto-reconnect, premarket connect, and TCP probes that fail outside SimNow trading hours.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-26 18:53:49 +08:00
parent 3a150dd3d6
commit 631aa2c0ab
9 changed files with 231 additions and 18 deletions
+23 -1
View File
@@ -1788,9 +1788,24 @@ def settings():
return redirect(url_for("settings")) return redirect(url_for("settings"))
flash("交易模式已保存") flash("交易模式已保存")
elif action == "ctp": elif action == "ctp":
from ctp_settings import save_ctp_auto_connect, is_ctp_auto_connect_enabled
from ctp_settings import save_ctp_settings_from_form from ctp_settings import save_ctp_settings_from_form
from vnpy_bridge import ctp_disconnect
was_enabled = is_ctp_auto_connect_enabled(get_setting)
auto_enabled = save_ctp_auto_connect(request.form, set_setting)
save_result = save_ctp_settings_from_form(request.form, set_setting) save_result = save_ctp_settings_from_form(request.form, set_setting)
if not auto_enabled:
ctp_disconnect(set_disabled_hint=True)
elif not was_enabled and auto_enabled:
try:
from vnpy_bridge import get_bridge
from trading_context import get_trading_mode
mode = get_trading_mode(get_setting)
get_bridge().reconnect_after_settings_saved(mode)
except Exception as exc:
app.logger.debug("CTP connect after enable auto: %s", exc)
pwd_updated = save_result.get("passwords_updated") or [] pwd_updated = save_result.get("passwords_updated") or []
pwd_empty = save_result.get("passwords_submitted_empty") or [] pwd_empty = save_result.get("passwords_submitted_empty") or []
simnow_pwd_len = len((request.form.get("simnow_password") or "").strip()) simnow_pwd_len = len((request.form.get("simnow_password") or "").strip())
@@ -1816,6 +1831,12 @@ def settings():
pwd_note = "实盘交易密码未改(提交为空)" pwd_note = "实盘交易密码未改(提交为空)"
else: else:
pwd_note = "" pwd_note = ""
if not auto_enabled:
flash("CTP 配置已保存;自动连接已关闭,所有 CTP 连接已断开")
return redirect(url_for("settings"))
if not was_enabled:
flash("CTP 配置已保存;自动连接已开启,正在连接…")
return redirect(url_for("settings"))
flash_msg = "CTP 配置已保存,正在使用新地址重连…" flash_msg = "CTP 配置已保存,正在使用新地址重连…"
if pwd_note: if pwd_note:
flash_msg = f"CTP 配置已保存;{pwd_note},正在重连…" flash_msg = f"CTP 配置已保存;{pwd_note},正在重连…"
@@ -1864,7 +1885,7 @@ def settings():
ctp_st = ctp_status(get_trading_mode(get_setting)) ctp_st = ctp_status(get_trading_mode(get_setting))
except Exception: except Exception:
pass pass
from ctp_settings import get_ctp_settings_for_ui from ctp_settings import get_ctp_settings_for_ui, is_ctp_auto_connect_enabled
return render_template( return render_template(
"settings.html", "settings.html",
@@ -1873,6 +1894,7 @@ def settings():
quote_label=get_quote_source_label(ctp_connected=bool(ctp_st.get("connected"))), quote_label=get_quote_source_label(ctp_connected=bool(ctp_st.get("connected"))),
ctp_status=ctp_st, ctp_status=ctp_st,
ctp_cfg=get_ctp_settings_for_ui(), ctp_cfg=get_ctp_settings_for_ui(),
ctp_auto_connect=is_ctp_auto_connect_enabled(get_setting),
trading_mode=get_setting("trading_mode", "simulation"), trading_mode=get_setting("trading_mode", "simulation"),
position_sizing_mode=get_setting("position_sizing_mode", "fixed"), position_sizing_mode=get_setting("position_sizing_mode", "fixed"),
fixed_lots=get_setting("fixed_lots", "1"), fixed_lots=get_setting("fixed_lots", "1"),
+11 -2
View File
@@ -12,6 +12,7 @@ import threading
import time import time
from typing import Callable from typing import Callable
from ctp_settings import is_ctp_auto_connect_enabled
from market_sessions import in_premarket_connect_window from market_sessions import in_premarket_connect_window
from vnpy_bridge import ctp_start_connect, ctp_status from vnpy_bridge import ctp_start_connect, ctp_status
@@ -39,6 +40,7 @@ def _minutes_before_open() -> int:
def start_ctp_premarket_connect_worker( def start_ctp_premarket_connect_worker(
*, *,
get_mode_fn: Callable[[], str], get_mode_fn: Callable[[], str],
get_setting_fn: Callable[[str, str], str] | None = None,
interval: int = CHECK_INTERVAL_SEC, interval: int = CHECK_INTERVAL_SEC,
) -> None: ) -> None:
"""在交易开始前若干分钟自动发起 CTP 连接。""" """在交易开始前若干分钟自动发起 CTP 连接。"""
@@ -47,8 +49,15 @@ def start_ctp_premarket_connect_worker(
time.sleep(10) time.sleep(10)
while True: while True:
try: try:
if _premarket_enabled() and in_premarket_connect_window( gs = get_setting_fn
minutes_before=_minutes_before_open(), if gs is None:
from fee_specs import get_setting as gs
if (
is_ctp_auto_connect_enabled(gs)
and _premarket_enabled()
and in_premarket_connect_window(
minutes_before=_minutes_before_open(),
)
): ):
mode = get_mode_fn() mode = get_mode_fn()
st = ctp_status(mode) st = ctp_status(mode)
+13 -2
View File
@@ -12,6 +12,7 @@ import threading
import time import time
from typing import Callable from typing import Callable
from ctp_settings import is_ctp_auto_connect_enabled
from vnpy_bridge import ctp_try_auto_reconnect from vnpy_bridge import ctp_try_auto_reconnect
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -27,13 +28,23 @@ def _auto_reconnect_enabled() -> bool:
) )
def start_ctp_reconnect_worker(*, get_mode_fn: Callable[[], str], interval: int = RECONNECT_INTERVAL_SEC) -> None: def start_ctp_reconnect_worker(
*,
get_mode_fn: Callable[[], str],
get_setting_fn: Callable[[str, str], str] | None = None,
interval: int = RECONNECT_INTERVAL_SEC,
) -> None:
"""定时检测 CTP 连接,断线后自动重连。""" """定时检测 CTP 连接,断线后自动重连。"""
def _loop() -> None: def _loop() -> None:
while True: while True:
try: try:
if _auto_reconnect_enabled(): gs = get_setting_fn
if gs is None:
from fee_specs import get_setting as gs
if not is_ctp_auto_connect_enabled(gs):
pass
elif _auto_reconnect_enabled():
mode = get_mode_fn() mode = get_mode_fn()
if ctp_try_auto_reconnect(mode): if ctp_try_auto_reconnect(mode):
logger.debug("CTP 连接正常 [%s]", mode) logger.debug("CTP 连接正常 [%s]", mode)
+25
View File
@@ -34,6 +34,30 @@ LIVE_FIELDS: tuple[tuple[str, str, str, str], ...] = (
PASSWORD_DB_KEYS = frozenset({"simnow_password", "ctp_live_password"}) PASSWORD_DB_KEYS = frozenset({"simnow_password", "ctp_live_password"})
CTP_AUTO_CONNECT_KEY = "ctp_auto_connect"
CTP_DISABLED_HINT = "CTP 自动连接已关闭(非交易时段建议关闭,避免反复连接失败)"
def is_ctp_auto_connect_enabled(get_setting=None) -> bool:
"""系统设置:是否允许 CTP 连接(含自动重连、盘前连接、手动连接)。"""
if get_setting is None:
from fee_specs import get_setting as _gs
get_setting = _gs
val = (get_setting(CTP_AUTO_CONNECT_KEY, "1") or "1").strip().lower()
return val in ("1", "true", "yes", "on")
def save_ctp_auto_connect(form: Any, set_setting: Callable[[str, str], None]) -> bool:
enabled = (form.get("ctp_auto_connect") or "").strip().lower() in (
"1",
"on",
"true",
"yes",
)
set_setting(CTP_AUTO_CONNECT_KEY, "1" if enabled else "0")
return enabled
def _get_db_setting(key: str, default: str = "") -> str: def _get_db_setting(key: str, default: str = "") -> str:
from fee_specs import get_setting from fee_specs import get_setting
@@ -85,6 +109,7 @@ def get_ctp_settings_for_ui() -> dict[str, Any]:
if db_key in PASSWORD_DB_KEYS: if db_key in PASSWORD_DB_KEYS:
ui[f"{db_key}_set"] = bool(ui[db_key]) ui[f"{db_key}_set"] = bool(ui[db_key])
ui[db_key] = "" ui[db_key] = ""
ui["ctp_auto_connect"] = is_ctp_auto_connect_enabled()
return ui return ui
+24 -5
View File
@@ -34,6 +34,7 @@ from recommend_store import (
) )
from recommend_stream import recommend_hub, schedule_recommend_refresh, start_recommend_worker from recommend_stream import recommend_hub, schedule_recommend_refresh, start_recommend_worker
from position_stream import position_hub, start_position_worker from position_stream import position_hub, start_position_worker
from ctp_settings import is_ctp_auto_connect_enabled
from ctp_reconnect import start_ctp_reconnect_worker from ctp_reconnect import start_ctp_reconnect_worker
from ctp_premarket_connect import start_ctp_premarket_connect_worker from ctp_premarket_connect import start_ctp_premarket_connect_worker
from ctp_fee_worker import start_ctp_fee_worker from ctp_fee_worker import start_ctp_fee_worker
@@ -1562,9 +1563,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
threading.Thread(target=_warm, daemon=True, name="position-bootstrap").start() threading.Thread(target=_warm, daemon=True, name="position-bootstrap").start()
try: try:
from vnpy_bridge import ctp_start_connect if is_ctp_auto_connect_enabled(get_setting):
mode = get_trading_mode(get_setting) from vnpy_bridge import ctp_start_connect
ctp_start_connect(mode, force=False) mode = get_trading_mode(get_setting)
ctp_start_connect(mode, force=False)
except Exception as exc: except Exception as exc:
logger.debug("bootstrap ctp connect: %s", exc) logger.debug("bootstrap ctp connect: %s", exc)
@@ -1617,6 +1619,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
risk_percent=get_risk_percent(get_setting), risk_percent=get_risk_percent(get_setting),
max_margin_pct=get_max_margin_pct(get_setting), max_margin_pct=get_max_margin_pct(get_setting),
pending_order_timeout_min=get_pending_order_timeout_min(get_setting), pending_order_timeout_min=get_pending_order_timeout_min(get_setting),
ctp_auto_connect=is_ctp_auto_connect_enabled(get_setting),
recommend_rows=rec_cache.get("rows") or [], recommend_rows=rec_cache.get("rows") or [],
recommend_updated_at=rec_cache.get("updated_at"), recommend_updated_at=rec_cache.get("updated_at"),
product_categories=PRODUCT_CATEGORIES, product_categories=PRODUCT_CATEGORIES,
@@ -2258,7 +2261,17 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
@login_required @login_required
def api_ctp_connect(): def api_ctp_connect():
from vnpy_bridge import ctp_start_connect from vnpy_bridge import ctp_start_connect
from ctp_settings import CTP_DISABLED_HINT
if not is_ctp_auto_connect_enabled(get_setting):
mode = get_trading_mode(get_setting)
st = ctp_status(mode)
return jsonify({
"ok": False,
"disabled": True,
"error": CTP_DISABLED_HINT,
"status": st,
}), 400
mode = get_trading_mode(get_setting) mode = get_trading_mode(get_setting)
body = request.get_json(silent=True) or {} body = request.get_json(silent=True) or {}
force = bool(body.get("force")) force = bool(body.get("force"))
@@ -2723,8 +2736,14 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
get_sizing_mode_fn=lambda: get_sizing_mode(get_setting), get_sizing_mode_fn=lambda: get_sizing_mode(get_setting),
get_fixed_lots_fn=lambda: get_fixed_lots(get_setting), get_fixed_lots_fn=lambda: get_fixed_lots(get_setting),
) )
start_ctp_reconnect_worker(get_mode_fn=lambda: get_trading_mode(get_setting)) start_ctp_reconnect_worker(
start_ctp_premarket_connect_worker(get_mode_fn=lambda: get_trading_mode(get_setting)) get_mode_fn=lambda: get_trading_mode(get_setting),
get_setting_fn=get_setting,
)
start_ctp_premarket_connect_worker(
get_mode_fn=lambda: get_trading_mode(get_setting),
get_setting_fn=get_setting,
)
start_sl_tp_guard_worker( start_sl_tp_guard_worker(
db_path=DB_PATH, db_path=DB_PATH,
get_mode_fn=lambda: get_trading_mode(get_setting), get_mode_fn=lambda: get_trading_mode(get_setting),
+62 -5
View File
@@ -33,6 +33,7 @@
var hasSlTpMonitoring = false; var hasSlTpMonitoring = false;
var ctpConnected = false; var ctpConnected = false;
var ctpConnecting = false; var ctpConnecting = false;
var ctpAutoConnectEnabled = window.CTP_AUTO_CONNECT !== false;
var positionsRendered = false; var positionsRendered = false;
var selectedMaxLots = null; var selectedMaxLots = null;
var recommendMaxByProduct = {}; var recommendMaxByProduct = {};
@@ -183,6 +184,10 @@
ctpConnecting = !!connecting; ctpConnecting = !!connecting;
isTradingSession = !!data.trading_session; isTradingSession = !!data.trading_session;
syncCtpBadgeFromStatus(data.ctp_status || { connected: connected, connecting: connecting }); syncCtpBadgeFromStatus(data.ctp_status || { connected: connected, connecting: connecting });
if (data.ctp_status && typeof data.ctp_status.auto_connect_enabled === 'boolean') {
ctpAutoConnectEnabled = data.ctp_status.auto_connect_enabled;
updateCtpConnectButtonState();
}
if (syncBadge) { if (syncBadge) {
if (data.sync_label && connected) { if (data.sync_label && connected) {
syncBadge.hidden = false; syncBadge.hidden = false;
@@ -200,6 +205,8 @@
} else if (isCtpUnreachableError(data.ctp_status.last_error)) { } else if (isCtpUnreachableError(data.ctp_status.last_error)) {
lastCtpUnreachableAt = Date.now(); lastCtpUnreachableAt = Date.now();
} }
} else if (!connected && data.ctp_status && data.ctp_status.disabled_hint) {
showCtpError(data.ctp_status.disabled_hint);
} }
var riskBadge = document.getElementById('risk-badge'); var riskBadge = document.getElementById('risk-badge');
if (riskBadge && data.risk_status) { if (riskBadge && data.risk_status) {
@@ -235,14 +242,20 @@
list.innerHTML = '<div class="empty-hint text-loss">' + err + '</div>'; list.innerHTML = '<div class="empty-hint text-loss">' + err + '</div>';
return; return;
} }
if (!ctpAutoConnectEnabled) {
var offHint = (data.ctp_status && data.ctp_status.disabled_hint) ||
'CTP 自动连接已关闭,请在系统设置中开启';
list.innerHTML = '<div class="empty-hint text-muted">' + offHint + '</div>';
return;
}
list.innerHTML = '<div class="empty-hint">CTP 未连接,正在尝试自动重连…</div>'; list.innerHTML = '<div class="empty-hint">CTP 未连接,正在尝试自动重连…</div>';
tryAutoCtpReconnect(); if (ctpAutoConnectEnabled) tryAutoCtpReconnect();
return; return;
} }
list.innerHTML = '<div class="empty-hint">暂无持仓。</div>'; list.innerHTML = '<div class="empty-hint">暂无持仓。</div>';
return; return;
} }
if (!connected) { if (!connected && ctpAutoConnectEnabled) {
tryAutoCtpReconnect(); tryAutoCtpReconnect();
} }
list.innerHTML = rows.map(buildPosCard).join(''); list.innerHTML = rows.map(buildPosCard).join('');
@@ -350,11 +363,28 @@
updateCtpBadge(connected, connecting); updateCtpBadge(connected, connecting);
} }
function updateCtpConnectButtonState() {
var btnConnect = document.getElementById('btn-ctp-connect');
var hint = document.getElementById('ctp-auto-hint');
if (hint) {
hint.textContent = ctpAutoConnectEnabled
? '断线自动重连 · 开盘前 30 分钟自动连接'
: 'CTP 自动连接已关闭(系统设置可开启)';
}
if (btnConnect && !ctpAutoConnectEnabled) {
btnConnect.disabled = true;
btnConnect.title = '请先在系统设置 → CTP 连接 中开启自动连接';
}
}
function updateCtpBadge(connected, connecting) { function updateCtpBadge(connected, connecting) {
var ctpBadge = document.getElementById('ctp-badge'); var ctpBadge = document.getElementById('ctp-badge');
var btnConnect = document.getElementById('btn-ctp-connect'); var btnConnect = document.getElementById('btn-ctp-connect');
if (ctpBadge) { if (ctpBadge) {
if (connecting) { if (!ctpAutoConnectEnabled && !connected) {
ctpBadge.textContent = 'CTP 已关闭';
ctpBadge.className = 'badge planned';
} else if (connecting) {
ctpBadge.textContent = 'CTP 连接中'; ctpBadge.textContent = 'CTP 连接中';
ctpBadge.className = 'badge planned'; ctpBadge.className = 'badge planned';
} else { } else {
@@ -363,11 +393,17 @@
} }
} }
if (btnConnect) { if (btnConnect) {
if (connecting) { if (!ctpAutoConnectEnabled) {
btnConnect.textContent = connected ? '重连 CTP' : '连接 CTP';
btnConnect.disabled = true;
btnConnect.title = '请先在系统设置 → CTP 连接 中开启自动连接';
} else if (connecting) {
btnConnect.textContent = '连接中…'; btnConnect.textContent = '连接中…';
btnConnect.disabled = true; btnConnect.disabled = true;
btnConnect.title = '';
} else { } else {
btnConnect.disabled = false; btnConnect.disabled = false;
btnConnect.title = '';
btnConnect.textContent = connected ? '重连 CTP' : '连接 CTP'; btnConnect.textContent = connected ? '重连 CTP' : '连接 CTP';
} }
} }
@@ -418,6 +454,10 @@
} }
function requestCtpConnect(force) { function requestCtpConnect(force) {
if (!force && !ctpAutoConnectEnabled) {
showCtpError('CTP 自动连接已关闭,请在系统设置中开启');
return Promise.resolve({ ok: false, disabled: true });
}
if (!force && ctpConnectInflight) { if (!force && ctpConnectInflight) {
return Promise.resolve({}); return Promise.resolve({});
} }
@@ -449,6 +489,13 @@
return d; return d;
}); });
} }
if (d.disabled || st.auto_connect_enabled === false) {
ctpAutoConnectEnabled = false;
updateCtpConnectButtonState();
syncCtpBadgeFromStatus(st);
showCtpError(st.disabled_hint || d.error || 'CTP 自动连接已关闭');
return d;
}
if (!d.ok) { if (!d.ok) {
syncCtpBadgeFromStatus(st); syncCtpBadgeFromStatus(st);
var err = d.error || st.last_error || '连接失败'; var err = d.error || st.last_error || '连接失败';
@@ -564,6 +611,7 @@
} }
function tryAutoCtpReconnect() { function tryAutoCtpReconnect() {
if (!ctpAutoConnectEnabled) return;
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;
@@ -1517,14 +1565,23 @@
.then(function (r) { return r.json(); }) .then(function (r) { return r.json(); })
.then(function (d) { .then(function (d) {
var st = d.status || {}; var st = d.status || {};
if (typeof st.auto_connect_enabled === 'boolean') {
ctpAutoConnectEnabled = st.auto_connect_enabled;
}
updateCtpConnectButtonState();
syncCtpBadgeFromStatus(st); syncCtpBadgeFromStatus(st);
if (st.last_error) showCtpError(st.last_error); if (st.disabled_hint) {
showCtpError(st.disabled_hint);
} else if (st.last_error) {
showCtpError(st.last_error);
}
if (st.connected) pollPositions(); if (st.connected) pollPositions();
}) })
.catch(function () {}); .catch(function () {});
} }
runWhenReady(function () { runWhenReady(function () {
updateCtpConnectButtonState();
setPriceType('limit'); setPriceType('limit');
if (isFixedMode() && lotsCalc) { if (isFixedMode() && lotsCalc) {
lotsCalc.value = String(window.TRADE_FIXED_LOTS || 1); lotsCalc.value = String(window.TRADE_FIXED_LOTS || 1);
+17 -1
View File
@@ -200,11 +200,13 @@
{% call settings_card('ctp', 'CTP 连接', 'settings-ctp-wrap') %} {% call settings_card('ctp', 'CTP 连接', 'settings-ctp-wrap') %}
<p class="hint" style="margin-bottom:.85rem"> <p class="hint" style="margin-bottom:.85rem">
投资者代码、密码、前置地址在此维护(优先于 <code>.env</code>)。保存后将自动断开并用新地址重连 CTP。 投资者代码、密码、前置地址在此维护(优先于 <code>.env</code>)。保存后将自动断开并用新地址重连 CTP(须开启下方自动连接)
{% if ctp_status.connected %} {% if ctp_status.connected %}
<span class="badge profit" style="margin-left:.35rem">已连接</span> <span class="badge profit" style="margin-left:.35rem">已连接</span>
{% elif ctp_status.connecting %} {% elif ctp_status.connecting %}
<span class="badge planned" style="margin-left:.35rem">连接中</span> <span class="badge planned" style="margin-left:.35rem">连接中</span>
{% elif ctp_status.disabled_hint %}
<span class="text-muted" style="display:block;margin-top:.35rem">{{ ctp_status.disabled_hint }}</span>
{% elif ctp_status.last_error %} {% elif ctp_status.last_error %}
<span class="text-loss" style="display:block;margin-top:.35rem">{{ ctp_status.last_error }}</span> <span class="text-loss" style="display:block;margin-top:.35rem">{{ ctp_status.last_error }}</span>
{% endif %} {% endif %}
@@ -213,6 +215,20 @@
<form action="{{ url_for('settings') }}" method="post" id="ctp-settings-form"> <form action="{{ url_for('settings') }}" method="post" id="ctp-settings-form">
<input type="hidden" name="action" value="ctp"> <input type="hidden" name="action" value="ctp">
<div class="settings-ctp-auto card" style="margin-bottom:.85rem;padding:.75rem 1rem">
<label class="settings-ctp-auto-label" style="display:flex;align-items:flex-start;gap:.65rem;cursor:pointer;margin:0">
<input type="checkbox" name="ctp_auto_connect" value="1" {% if ctp_auto_connect %}checked{% endif %}
style="margin-top:.2rem;width:auto">
<span>
<strong>CTP 自动连接</strong>
<span class="hint" style="display:block;margin:.25rem 0 0;font-size:.78rem;line-height:1.55">
开启:盘前自动连接、断线重连、持仓页可连 CTP。关闭:立即断开所有 CTP 连接,不再尝试重连。
SimNow 非交易时段前置常不可用(与快期相同),建议收盘后关闭。
</span>
</span>
</label>
</div>
<div class="settings-ctp-cards-row"> <div class="settings-ctp-cards-row">
<div class="settings-ctp-fold card is-collapsed" data-ctp-fold="simnow"> <div class="settings-ctp-fold card is-collapsed" data-ctp-fold="simnow">
<button type="button" class="settings-ctp-fold-head" aria-expanded="false"> <button type="button" class="settings-ctp-fold-head" aria-expanded="false">
+6 -2
View File
@@ -19,8 +19,11 @@
{% endif %} {% endif %}
</div> </div>
<div class="trade-top-bar-actions"> <div class="trade-top-bar-actions">
<button type="button" class="btn-primary btn-ctp-sm" id="btn-ctp-connect">{% if ctp_status.connected %}重连 CTP{% else %}连接 CTP{% endif %}</button> <button type="button" class="btn-primary btn-ctp-sm" id="btn-ctp-connect"
<span class="text-muted trade-top-hint">断线自动重连 · 开盘前 30 分钟自动连接</span> {% if not ctp_auto_connect %}disabled title="请先在系统设置 → CTP 连接 中开启自动连接"{% endif %}>
{% if ctp_status.connected %}重连 CTP{% else %}连接 CTP{% endif %}
</button>
<span class="text-muted trade-top-hint" id="ctp-auto-hint">{% if ctp_auto_connect %}断线自动重连 · 开盘前 30 分钟自动连接{% else %}CTP 自动连接已关闭{% endif %}</span>
</div> </div>
</div> </div>
@@ -228,6 +231,7 @@ window.TRADE_FIXED_LOTS = {{ fixed_lots|tojson }};
window.TRADE_FIXED_AMOUNT = {{ fixed_amount|tojson }}; window.TRADE_FIXED_AMOUNT = {{ fixed_amount|tojson }};
window.PRODUCT_CATEGORIES = {{ product_categories | default([]) | tojson }}; window.PRODUCT_CATEGORIES = {{ product_categories | default([]) | tojson }};
window.__RECOMMEND_ROWS__ = {{ recommend_rows | default([]) | tojson }}; window.__RECOMMEND_ROWS__ = {{ recommend_rows | default([]) | tojson }};
window.CTP_AUTO_CONNECT = {{ ctp_auto_connect | tojson }};
</script> </script>
<script src="{{ url_for('static', filename='js/trade.js') }}?v={{ asset_v }}"></script> <script src="{{ url_for('static', filename='js/trade.js') }}?v={{ asset_v }}"></script>
{% endblock %} {% endblock %}
+50
View File
@@ -546,6 +546,12 @@ class CtpBridge:
} }
def connect(self, mode: str, *, force: bool = False) -> None: def connect(self, mode: str, *, force: bool = False) -> None:
from ctp_settings import CTP_DISABLED_HINT, is_ctp_auto_connect_enabled
if not is_ctp_auto_connect_enabled():
self._last_error = CTP_DISABLED_HINT
_persist_last_error(CTP_DISABLED_HINT)
raise RuntimeError(CTP_DISABLED_HINT)
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: if self._is_login_cooldown_active() and not force:
@@ -644,6 +650,18 @@ class CtpBridge:
def start_connect_async(self, mode: str, *, force: bool = False) -> dict[str, Any]: def start_connect_async(self, mode: str, *, force: bool = False) -> dict[str, Any]:
"""后台连接,不阻塞 HTTP 请求。""" """后台连接,不阻塞 HTTP 请求。"""
from ctp_settings import CTP_DISABLED_HINT, is_ctp_auto_connect_enabled
if not is_ctp_auto_connect_enabled():
self._last_error = CTP_DISABLED_HINT
_persist_last_error(CTP_DISABLED_HINT)
return {
"started": False,
"connecting": False,
"connected": False,
"disabled": True,
"error": CTP_DISABLED_HINT,
}
if self._connected_mode == mode and self.ping() and not force: if self._connected_mode == mode and self.ping() and not force:
return {"started": False, "connecting": False, "connected": True} return {"started": False, "connecting": False, "connected": True}
if self._connect_in_progress: if self._connect_in_progress:
@@ -787,9 +805,13 @@ class CtpBridge:
def reconnect_after_settings_saved(self, mode: str) -> dict[str, Any]: def reconnect_after_settings_saved(self, mode: str) -> dict[str, Any]:
"""保存前置/账号后关闭旧连接,并用数据库中的新配置重连。""" """保存前置/账号后关闭旧连接,并用数据库中的新配置重连。"""
from ctp_settings import is_ctp_auto_connect_enabled
self._close_gateway() self._close_gateway()
self._last_error = "" self._last_error = ""
_persist_last_error("") _persist_last_error("")
if not is_ctp_auto_connect_enabled():
return {"started": False, "connecting": False, "connected": False, "disabled": True}
return self.start_connect_async(mode, force=True) return self.start_connect_async(mode, force=True)
def _schedule_fee_sync(self, mode: str) -> None: def _schedule_fee_sync(self, mode: str) -> None:
@@ -1623,6 +1645,20 @@ def vnpy_available() -> bool:
return get_bridge().available() return get_bridge().available()
def ctp_disconnect(*, set_disabled_hint: bool = False) -> None:
"""主动断开 CTP 并清理内存状态。"""
from ctp_settings import CTP_DISABLED_HINT
b = get_bridge()
b._close_gateway()
if set_disabled_hint:
b._last_error = CTP_DISABLED_HINT
_persist_last_error(CTP_DISABLED_HINT)
else:
b._last_error = ""
_persist_last_error("")
def ctp_connect(mode: str, *, force: bool = False) -> dict[str, Any]: def ctp_connect(mode: str, *, force: bool = False) -> dict[str, Any]:
b = get_bridge() b = get_bridge()
b.connect(mode, force=force) b.connect(mode, force=force)
@@ -1639,6 +1675,10 @@ def ctp_start_connect(mode: str, *, force: bool = False) -> dict[str, Any]:
def ctp_try_auto_reconnect(mode: str) -> bool: def ctp_try_auto_reconnect(mode: str) -> bool:
"""断线时静默异步重连;已连接且交易通道正常则不再重复 connect。""" """断线时静默异步重连;已连接且交易通道正常则不再重复 connect。"""
from ctp_settings import is_ctp_auto_connect_enabled
if not is_ctp_auto_connect_enabled():
return False
b = get_bridge() b = get_bridge()
if not b.available(): if not b.available():
return False return False
@@ -1673,7 +1713,17 @@ def ctp_try_auto_reconnect(mode: str) -> bool:
def ctp_status(mode: str) -> dict[str, Any]: def ctp_status(mode: str) -> dict[str, Any]:
from ctp_settings import CTP_DISABLED_HINT, is_ctp_auto_connect_enabled
auto = is_ctp_auto_connect_enabled()
st = get_bridge().status(mode) st = get_bridge().status(mode)
st["auto_connect_enabled"] = auto
if not auto:
st["disabled_hint"] = CTP_DISABLED_HINT
if not st.get("connected") and not st.get("connecting"):
st["last_error"] = ""
st["td_reachable"] = None
return st
if not st.get("connected") and not st.get("connecting"): if not st.get("connected") and not st.get("connecting"):
setting = _setting_for_mode(mode) setting = _setting_for_mode(mode)
td = setting.get("交易服务器", "") td = setting.get("交易服务器", "")