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:
@@ -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"),
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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("交易服务器", "")
|
||||||
|
|||||||
Reference in New Issue
Block a user