e5a586f903
Move business code under modules/, env template to config/, PM2 single qihuo process, and _legacy shims for old imports. Co-authored-by: Cursor <cursoragent@cursor.com>
117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
|
# 专有软件 — 未经授权禁止复制、传播、转售。
|
|
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
|
|
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
|
|
|
|
"""CTP 按计划自动连接:盘前 30 分钟检查;交易时段断线后台重连;不自动强制断开。"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import threading
|
|
import time
|
|
from typing import Callable
|
|
|
|
from modules.market.market_sessions import (
|
|
in_premarket_connect_window,
|
|
in_postmarket_grace_window,
|
|
is_trading_session,
|
|
should_keep_ctp_connected,
|
|
)
|
|
from modules.ctp.vnpy_bridge import ctp_start_connect, ctp_status
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
CHECK_INTERVAL_SEC = 60
|
|
TRADING_CHECK_INTERVAL_SEC = 15
|
|
PREMARKET_CHECK_INTERVAL_SEC = 30
|
|
DEFAULT_MINUTES_BEFORE = 30
|
|
DEFAULT_MINUTES_AFTER = 30
|
|
|
|
|
|
def premarket_minutes_before() -> int:
|
|
try:
|
|
return max(5, int(os.getenv("CTP_PREMARKET_MINUTES", str(DEFAULT_MINUTES_BEFORE))))
|
|
except (TypeError, ValueError):
|
|
return DEFAULT_MINUTES_BEFORE
|
|
|
|
|
|
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",
|
|
"yes",
|
|
)
|
|
|
|
|
|
def should_auto_connect_now(*, minutes_before: int | None = None) -> bool:
|
|
"""是否应保持/发起 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(
|
|
*,
|
|
get_mode_fn: Callable[[], str],
|
|
get_setting_fn: Callable[[str, str], str] | None = None,
|
|
interval: int = CHECK_INTERVAL_SEC,
|
|
) -> None:
|
|
"""盘前 30 分钟:未连接则自动连;已连接则不重复发起。不自动强制断开。"""
|
|
|
|
def _loop() -> None:
|
|
time.sleep(10)
|
|
while True:
|
|
sleep_sec = max(30, interval)
|
|
try:
|
|
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")
|
|
and int(st.get("login_cooldown_sec") or 0) <= 0
|
|
):
|
|
info = ctp_start_connect(mode, force=False, scheduled=True)
|
|
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,
|
|
mins_b,
|
|
)
|
|
if is_trading_session():
|
|
sleep_sec = TRADING_CHECK_INTERVAL_SEC
|
|
elif in_premarket_connect_window(minutes_before=mins_b):
|
|
sleep_sec = PREMARKET_CHECK_INTERVAL_SEC
|
|
except Exception as exc:
|
|
logger.warning("CTP scheduled connect worker: %s", exc)
|
|
time.sleep(sleep_sec)
|
|
|
|
threading.Thread(target=_loop, daemon=True, name="ctp-premarket-connect").start()
|