From d324d303322fc18a146ef982bc09b7d089eeb7c9 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 30 Jun 2026 22:14:39 +0800 Subject: [PATCH] Schedule CTP disconnect 30 minutes after day and night session close. Co-authored-by: Cursor --- ctp_premarket_connect.py | 77 +++++++++++++++++++++++++++--------- market_sessions.py | 37 +++++++++++++++++ scripts/test_ctp_schedule.py | 44 +++++++++++++++++++++ templates/trade.html | 2 +- 4 files changed, 141 insertions(+), 19 deletions(-) create mode 100644 scripts/test_ctp_schedule.py diff --git a/ctp_premarket_connect.py b/ctp_premarket_connect.py index c56b644..9f8ad94 100644 --- a/ctp_premarket_connect.py +++ b/ctp_premarket_connect.py @@ -3,7 +3,7 @@ # 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。 # 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md -"""交易前自动连接 CTP(默认开盘前 30 分钟)。""" +"""CTP 按计划自动连接/断开:盘前 30 分钟连,日盘/夜盘收盘后 30 分钟断。""" from __future__ import annotations import logging @@ -12,13 +12,19 @@ import threading import time from typing import Callable -from market_sessions import in_premarket_connect_window, is_trading_session -from vnpy_bridge import ctp_start_connect, ctp_status +from market_sessions import ( + in_premarket_connect_window, + in_postmarket_grace_window, + is_trading_session, + should_keep_ctp_connected, +) +from vnpy_bridge import ctp_disconnect, ctp_start_connect, ctp_status logger = logging.getLogger(__name__) CHECK_INTERVAL_SEC = 60 DEFAULT_MINUTES_BEFORE = 30 +DEFAULT_MINUTES_AFTER = 30 def premarket_minutes_before() -> int: @@ -28,7 +34,14 @@ def premarket_minutes_before() -> int: return DEFAULT_MINUTES_BEFORE -def _premarket_enabled() -> bool: +def postmarket_minutes_after() -> int: + try: + return max(5, int(os.getenv("CTP_POSTMARKET_MINUTES", str(DEFAULT_MINUTES_AFTER)))) + except (TypeError, ValueError): + return DEFAULT_MINUTES_AFTER + + +def _scheduled_connect_enabled() -> bool: return (os.getenv("CTP_PREMARKET_CONNECT", "true") or "true").strip().lower() in ( "1", "true", @@ -36,14 +49,25 @@ def _premarket_enabled() -> bool: ) +def _scheduled_disconnect_enabled() -> bool: + return (os.getenv("CTP_POSTMARKET_DISCONNECT", "true") or "true").strip().lower() in ( + "1", + "true", + "yes", + ) + + def should_auto_connect_now(*, minutes_before: int | None = None) -> bool: - """交易时段内,或距下一段开盘 <= minutes_before 且尚未开盘。""" - mins = premarket_minutes_before() if minutes_before is None else minutes_before - if is_trading_session(): - return True - if not _premarket_enabled(): - return False - return in_premarket_connect_window(minutes_before=mins) + """是否应保持/发起 CTP 连接(供重连、权限判断复用)。""" + mins_b = premarket_minutes_before() if minutes_before is None else minutes_before + mins_a = postmarket_minutes_after() + if not _scheduled_connect_enabled() and not is_trading_session(): + if not in_postmarket_grace_window(minutes_after=mins_a): + return False + return should_keep_ctp_connected( + minutes_before=mins_b, + minutes_after=mins_a, + ) def start_ctp_premarket_connect_worker( @@ -52,16 +76,20 @@ def start_ctp_premarket_connect_worker( get_setting_fn: Callable[[str, str], str] | None = None, interval: int = CHECK_INTERVAL_SEC, ) -> None: - """在交易开始前若干分钟自动发起 CTP 连接。""" + """盘前自动连接;日盘/夜盘收盘宽限结束后自动断开。""" def _loop() -> None: time.sleep(10) while True: sleep_sec = max(30, interval) try: - if should_auto_connect_now(): - mode = get_mode_fn() - st = ctp_status(mode) + mins_b = premarket_minutes_before() + mins_a = postmarket_minutes_after() + keep = should_auto_connect_now() + mode = get_mode_fn() + st = ctp_status(mode) + + if keep: if ( not st.get("connected") and not st.get("connecting") @@ -71,18 +99,31 @@ def start_ctp_premarket_connect_worker( if info.get("started"): if is_trading_session(): logger.info("交易时段内自动连接 CTP [%s]", mode) + elif in_postmarket_grace_window(minutes_after=mins_a): + logger.info( + "盘后宽限期内保持/恢复 CTP 连接 [%s](收盘后 %d 分钟内)", + mode, + mins_a, + ) else: logger.info( "盘前自动连接 CTP [%s](开盘前 %d 分钟)", mode, - premarket_minutes_before(), + mins_b, ) if not is_trading_session() and in_premarket_connect_window( - minutes_before=premarket_minutes_before(), + minutes_before=mins_b, ): sleep_sec = 30 + elif _scheduled_disconnect_enabled() and st.get("connected"): + ctp_disconnect() + logger.info( + "盘后自动断开 CTP [%s](日盘/夜盘结束 %d 分钟后)", + mode, + mins_a, + ) except Exception as exc: - logger.warning("CTP premarket connect worker: %s", exc) + logger.warning("CTP scheduled connect worker: %s", exc) time.sleep(sleep_sec) threading.Thread(target=_loop, daemon=True, name="ctp-premarket-connect").start() diff --git a/market_sessions.py b/market_sessions.py index 2795f02..22c1626 100644 --- a/market_sessions.py +++ b/market_sessions.py @@ -246,3 +246,40 @@ def in_premarket_connect_window( if mins is None: return False return 0 < mins <= float(minutes_before) + + +def in_postmarket_grace_window( + now: Optional[datetime] = None, + *, + minutes_after: int = 30, +) -> bool: + """日盘 15:00 或夜盘 02:30 收盘后 minutes_after 分钟内(仍保持连接,便于收尾)。""" + if is_trading_session(now): + return False + d = _normalize_dt(now) + t = _minutes_of_day(d) + wd = d.weekday() + ma = max(1, int(minutes_after)) + day_close = 15 * 60 + night_close = 2 * 60 + 30 + # 日盘收盘 15:00 后宽限(周一至周五) + if wd < 5 and day_close <= t < day_close + ma: + return True + # 夜盘收盘 02:30 后宽限(含周六凌晨结束周五夜盘) + if night_close <= t < night_close + ma: + return True + return False + + +def should_keep_ctp_connected( + now: Optional[datetime] = None, + *, + minutes_before: int = 30, + minutes_after: int = 30, +) -> bool: + """是否处于应连接 CTP 的窗口:交易时段 + 盘前 + 盘后宽限。""" + if is_trading_session(now): + return True + if in_postmarket_grace_window(now, minutes_after=minutes_after): + return True + return in_premarket_connect_window(now, minutes_before=minutes_before) diff --git a/scripts/test_ctp_schedule.py b/scripts/test_ctp_schedule.py new file mode 100644 index 0000000..b505b36 --- /dev/null +++ b/scripts/test_ctp_schedule.py @@ -0,0 +1,44 @@ +"""Verify CTP scheduled connect/disconnect windows.""" +from datetime import datetime +from zoneinfo import ZoneInfo + +from market_sessions import ( + in_premarket_connect_window, + in_postmarket_grace_window, + should_keep_ctp_connected, +) + +TZ = ZoneInfo("Asia/Shanghai") + + +def chk(label, dt, exp_keep, exp_pre=None, exp_post=None): + keep = should_keep_ctp_connected(dt) + pre = in_premarket_connect_window(dt) + post = in_postmarket_grace_window(dt) + auto = keep + ok = keep == exp_keep + if exp_pre is not None: + ok = ok and pre == exp_pre + if exp_post is not None: + ok = ok and post == exp_post + print( + f"{'OK' if ok else 'FAIL'} {label} {dt.strftime('%a %H:%M')} " + f"keep={keep} pre={pre} post={post} auto={auto}" + ) + return ok + + +cases = [ + ("日盘交易中", datetime(2026, 6, 30, 10, 0, tzinfo=TZ), True, False, False), + ("上午小节休盘前10分", datetime(2026, 6, 30, 10, 20, tzinfo=TZ), True, True, False), + ("日盘盘前28分", datetime(2026, 6, 30, 8, 32, tzinfo=TZ), True, True, False), + ("日盘盘前31分", datetime(2026, 6, 30, 8, 29, tzinfo=TZ), False, False, False), + ("日盘收盘后15分", datetime(2026, 6, 30, 15, 15, tzinfo=TZ), True, False, True), + ("日盘收盘后35分", datetime(2026, 6, 30, 15, 35, tzinfo=TZ), False, False, False), + ("夜盘盘前28分", datetime(2026, 6, 30, 20, 32, tzinfo=TZ), True, True, False), + ("夜盘交易中", datetime(2026, 6, 30, 22, 0, tzinfo=TZ), True, False, False), + ("夜盘收盘后15分", datetime(2026, 7, 1, 2, 45, tzinfo=TZ), True, False, True), + ("夜盘收盘后35分", datetime(2026, 7, 1, 3, 5, tzinfo=TZ), False, False, False), +] +failed = sum(0 if chk(*c) else 1 for c in cases) +print("failed", failed) diff --git a/templates/trade.html b/templates/trade.html index f746d2d..bfcf52c 100644 --- a/templates/trade.html +++ b/templates/trade.html @@ -40,7 +40,7 @@ {% if not ctp_auto_connect %}disabled title="请先在系统设置 → CTP 连接 中开启自动连接"{% endif %}> {% if ctp_status.connected %}重连 CTP{% else %}连接 CTP{% endif %} - {% if ctp_auto_connect %}断线自动重连 · 开盘前 30 分钟自动连接{% else %}自动连接已关闭 · 开盘前 30 分钟仍会按计划连接{% endif %} + {% if ctp_auto_connect %}断线自动重连 · 开盘前 30 分钟连接 · 日盘/夜盘收盘后 30 分钟断开{% else %}手动连接已关闭 · 仍按交易时段计划自动连/断(盘前 30 分连、收盘 30 分后断){% endif %}