Isolate CTP in worker process and improve strategy roll UX.

Split vn.py into qihuo-ctp worker with IPC client bridge, keep CTP connected during breaks with cached account fallback, speed up strategy page loads, and allow off-session breakout roll submissions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-01 12:35:47 +08:00
parent 08d55411aa
commit 9cd81a3ea7
17 changed files with 2214 additions and 227 deletions
+290 -6
View File
@@ -14,9 +14,11 @@ import time
from collections import deque
from typing import Any, Callable, Optional
import ctp_ipc_client
from locale_fix import ensure_process_locale
ensure_process_locale()
if ctp_ipc_client.is_worker_role():
ensure_process_locale()
from ctp_settings import live_setting_dict, simnow_setting_dict
from ctp_symbol import ths_to_vnpy_symbol, to_vnpy_exchange
@@ -34,6 +36,10 @@ CTP_COOLDOWN_UNTIL_KEY = "ctp_login_cooldown_until"
CTP_LAST_ERROR_KEY = "ctp_last_error"
def _use_ctp_worker_client() -> bool:
return not ctp_ipc_client.is_worker_role()
def _persist_login_cooldown(seconds: float) -> None:
from fee_specs import get_setting, set_setting
@@ -163,7 +169,7 @@ def _fire_position_refresh_callback_debounced(*, min_interval: float = 0.35) ->
def _fire_position_refresh_burst() -> None:
"""连接后持仓回报可能分批到达,分多次触发快照刷新。"""
_fire_position_refresh_callback()
for delay in (1.5, 4.0, 10.0, 18.0):
for delay in (0.4, 0.9, 1.5, 3.0, 6.0, 12.0, 20.0):
threading.Timer(delay, _fire_position_refresh_callback).start()
@@ -183,10 +189,11 @@ def _schedule_after_instruments_ready(bridge: "CtpBridge") -> None:
bridge._ensure_instrument_margin_hooks()
with _ctp_td_lock:
bridge.request_position_snapshot(force=True)
time.sleep(2.0)
time.sleep(0.8)
with _ctp_td_lock:
bridge.calibrate_trading_state()
_fire_position_refresh_callback()
_fire_position_refresh_burst()
n = len(bridge._collect_positions())
logger.info("CTP 合约加载完成,持仓 %s 条,已刷新快照", n)
except Exception as exc:
@@ -217,7 +224,7 @@ _bridge: Optional["CtpBridge"] = None
_bridge_lock = threading.Lock()
_ctp_td_lock = threading.RLock()
POSITION_QUERY_MIN_INTERVAL_SEC = 5.0
POSITION_QUERY_RETRY_DELAYS_SEC = (22.0, 50.0, 95.0)
POSITION_QUERY_RETRY_DELAYS_SEC = (1.5, 4.0, 9.0, 18.0, 35.0)
TRADE_QUERY_MIN_INTERVAL_SEC = 10.0
@@ -337,6 +344,7 @@ class CtpBridge:
self._trade_query_event = threading.Event()
self._last_trade_query_ts: float = 0.0
self._last_connect_ok_ts: float = 0.0
self._connect_started_ts: float = 0.0
self._tick_hooked = False
self._position_hooked = False
self._order_hooked = False
@@ -704,6 +712,16 @@ class CtpBridge:
cooldown = self.login_cooldown_remaining()
connecting = bool(self._connect_in_progress and cooldown <= 0)
last_error = self._last_error or _load_persisted_last_error()
if (
connecting
and self._connect_started_ts > 0
and time.time() - self._connect_started_ts > CONNECT_WAIT_SEC + 10
and not last_error
):
last_error = (
f"CTP 连接进行中已超过 {CONNECT_WAIT_SEC}s"
"可能前置不可达或柜台响应慢"
)
return {
"vnpy_installed": self.available(),
"connected": self._connected_mode == mode,
@@ -746,6 +764,7 @@ class CtpBridge:
raise ValueError(f"{_mode_label(mode)}:未配置交易服务器地址")
self._connect_in_progress = True
self._connect_started_ts = time.time()
try:
with _ctp_td_lock:
with self._connect_lock:
@@ -806,6 +825,10 @@ class CtpBridge:
self.calibrate_trading_state()
except Exception as exc:
logger.debug("post-connect calibrate: %s", exc)
try:
self.request_position_snapshot(force=True)
except Exception as exc:
logger.debug("post-connect position query: %s", exc)
self._ensure_instrument_margin_hooks()
_fire_position_refresh_burst()
_schedule_position_query_retries(self)
@@ -823,6 +846,7 @@ class CtpBridge:
raise RuntimeError(hint)
finally:
self._connect_in_progress = False
self._connect_started_ts = 0.0
def start_connect_async(
self, mode: str, *, force: bool = False, scheduled: bool = False,
@@ -859,13 +883,39 @@ class CtpBridge:
except Exception as exc:
logger.warning("CTP 后台连接失败: %s", exc)
def _watchdog() -> None:
deadline = CONNECT_WAIT_SEC + 25
time.sleep(deadline)
if not self._connect_in_progress:
return
logger.warning(
"CTP 连接 watchdog 超时 %.0fs,重置连接状态 [%s]",
deadline,
mode,
)
self._connect_in_progress = False
self._connect_started_ts = 0.0
hint = (
f"CTP 连接超时(>{deadline:.0f}s),可能前置不可达或柜台无响应。"
"请检查 SimNow 前置地址与账号,勿频繁重试。"
)
self._last_error = hint
_persist_last_error(hint)
try:
self._close_gateway()
except Exception as exc:
logger.debug("watchdog gateway close: %s", exc)
threading.Thread(target=_run, daemon=True, name="ctp-connect-async").start()
threading.Thread(target=_watchdog, daemon=True, name="ctp-connect-watchdog").start()
return {"started": True, "connecting": True, "connected": False}
def ensure_connected(self, mode: str) -> None:
if self._connected_mode == mode and self.ping():
return
self.connect(mode)
if self._connect_in_progress:
raise RuntimeError("CTP 连接中,请稍候")
raise RuntimeError("请先连接 CTP")
def require_connected(self, mode: str) -> None:
"""报单前检查:须已连接,不在此发起阻塞式 connect。"""
@@ -2125,8 +2175,170 @@ class CtpBridge:
return False
def get_bridge() -> CtpBridge:
class CtpBridgeProxy:
"""Client-side stand-in for CtpBridge, forwarding calls to qihuo-ctp."""
_engine = None
@property
def connected_mode(self) -> Optional[str]:
st = ctp_ipc_client.health().get("status") or {}
return st.get("connected_mode")
@property
def last_error(self) -> str:
st = ctp_ipc_client.health().get("status") or {}
return str(st.get("last_error") or "")
@property
def _last_connect_ok_ts(self) -> float:
st = ctp_ipc_client.health().get("status") or {}
try:
return float(st.get("last_connect_ok_ts") or 0)
except (TypeError, ValueError):
return 0.0
def available(self) -> bool:
return bool(ctp_ipc_client.health().get("worker_online"))
def status(self, mode: str) -> dict[str, Any]:
return ctp_ipc_client.status(mode)
def ping(self) -> bool:
return bool(ctp_ipc_client.health().get("worker_online"))
def connect(self, mode: str, *, force: bool = False) -> dict[str, Any]:
return ctp_ipc_client.connect(mode, force=force)
def start_connect_async(
self,
mode: str,
*,
force: bool = False,
scheduled: bool = False,
) -> dict[str, Any]:
return ctp_ipc_client.start_connect(mode, force=force, scheduled=scheduled)
def connect_in_progress(self) -> bool:
data = ctp_ipc_client.bridge_action("connect_in_progress")
return bool(data.get("result"))
def login_cooldown_remaining(self) -> int:
st = ctp_ipc_client.health().get("status") or {}
try:
return int(st.get("login_cooldown_sec") or 0)
except (TypeError, ValueError):
return 0
def ensure_connected(self, mode: str) -> None:
if not self.status(mode).get("connected"):
raise RuntimeError("CTP worker 未连接,请重连后再操作")
def require_connected(self, mode: str) -> None:
self.ensure_connected(mode)
def get_account(self) -> dict[str, Any]:
mode = self.connected_mode or "simulation"
return ctp_ipc_client.account(mode)
def list_positions(
self,
*,
refresh_if_empty: bool = True,
refresh_margin: bool = False,
) -> list[dict[str, Any]]:
mode = self.connected_mode or "simulation"
return ctp_ipc_client.positions(
mode,
refresh_if_empty=refresh_if_empty,
refresh_margin=refresh_margin,
)
def list_active_orders(self) -> list[dict[str, Any]]:
mode = self.connected_mode or "simulation"
return ctp_ipc_client.active_orders(mode)
def list_trades(self, *, refresh: bool = False) -> list[dict[str, Any]]:
mode = self.connected_mode or "simulation"
return ctp_ipc_client.trades(mode, refresh=refresh)
def get_tick_price(self, ths_code: str, *, mode: str = "") -> Optional[float]:
return ctp_ipc_client.tick_price(mode or self.connected_mode or "simulation", ths_code)
def get_tick_detail(self, ths_code: str, *, mode: str = "") -> dict[str, Any]:
return ctp_ipc_client.tick_detail(mode or self.connected_mode or "simulation", ths_code)
def estimate_margin_one_lot(
self,
ths_code: str,
price: float,
*,
direction: str = "long",
) -> Optional[float]:
return ctp_ipc_client.estimate_margin_one_lot(
self.connected_mode or "simulation",
ths_code,
price,
direction=direction,
)
def lookup_contract_spec(self, ths_code: str) -> Optional[dict]:
return ctp_ipc_client.contract_spec(self.connected_mode or "simulation", ths_code)
def send_order(self, **payload: Any) -> str:
data = ctp_ipc_client.send_order(payload)
return str(data.get("order_id") or "")
def cancel_order(self, vt_orderid: str) -> bool:
return ctp_ipc_client.cancel_order(self.connected_mode or "simulation", vt_orderid)
def calibrate_trading_state(self) -> Any:
return ctp_ipc_client.bridge_action("calibrate_trading_state").get("result")
def request_position_snapshot(self, *, force: bool = False) -> Any:
return ctp_ipc_client.bridge_action(
"request_position_snapshot",
{"force": bool(force)},
).get("result")
def subscribe_symbol(self, symbol: str) -> Any:
return ctp_ipc_client.bridge_action("subscribe_symbol", {"symbol": symbol}).get("result")
def refresh_positions(self) -> Any:
return ctp_ipc_client.bridge_action("refresh_positions").get("result")
def reconnect_after_settings_saved(self, mode: str) -> Any:
return ctp_ipc_client.bridge_action(
"reconnect_after_settings_saved",
{"mode": mode},
).get("result")
def query_all_commissions(self, *, mode: str = "") -> list[dict]:
data = ctp_ipc_client.bridge_action("query_all_commissions", {"mode": mode})
return list(data.get("result") or [])
def query_instrument_commission(self, symbol: str, *, mode: str = "") -> dict:
data = ctp_ipc_client.bridge_action(
"query_instrument_commission",
{"symbol": symbol, "mode": mode or self.connected_mode or "simulation"},
)
return dict(data.get("result") or {})
def get_kline_bars_1m(self, ths_code: str, *, mode: str) -> list[dict]:
data = ctp_ipc_client.bridge_action(
"get_kline_bars_1m",
{"symbol": ths_code, "mode": mode},
)
return list(data.get("result") or [])
def _close_gateway(self) -> None:
ctp_ipc_client.disconnect()
def get_bridge():
global _bridge
if _use_ctp_worker_client():
return CtpBridgeProxy()
with _bridge_lock:
if _bridge is None:
_bridge = CtpBridge()
@@ -2134,10 +2346,14 @@ def get_bridge() -> CtpBridge:
def try_init_vnpy(_settings: dict | None = None) -> bool:
if _use_ctp_worker_client():
return bool(ctp_ipc_client.health().get("worker_online"))
return get_bridge().available()
def vnpy_available() -> bool:
if _use_ctp_worker_client():
return bool(ctp_ipc_client.health().get("worker_online"))
return get_bridge().available()
@@ -2156,6 +2372,9 @@ def _ctp_connect_permitted(*, scheduled: bool = False) -> bool:
def ctp_disconnect(*, set_disabled_hint: bool = False) -> None:
"""主动断开 CTP 并清理内存状态。"""
if _use_ctp_worker_client():
ctp_ipc_client.disconnect(set_disabled_hint=set_disabled_hint)
return
from ctp_settings import CTP_DISABLED_HINT
b = get_bridge()
@@ -2169,6 +2388,8 @@ def ctp_disconnect(*, set_disabled_hint: bool = False) -> None:
def ctp_connect(mode: str, *, force: bool = False) -> dict[str, Any]:
if _use_ctp_worker_client():
return ctp_ipc_client.connect(mode, force=force)
b = get_bridge()
b.connect(mode, force=force)
return b.status(mode)
@@ -2176,6 +2397,8 @@ def ctp_connect(mode: str, *, force: bool = False) -> dict[str, Any]:
def ctp_start_connect(mode: str, *, force: bool = False, scheduled: bool = False) -> dict[str, Any]:
"""非阻塞发起连接,供 Web API 使用。"""
if _use_ctp_worker_client():
return ctp_ipc_client.start_connect(mode, force=force, scheduled=scheduled)
b = get_bridge()
info = b.start_connect_async(mode, force=force, scheduled=scheduled)
st = b.status(mode)
@@ -2184,6 +2407,13 @@ def ctp_start_connect(mode: str, *, force: bool = False, scheduled: bool = False
def ctp_try_auto_reconnect(mode: str) -> bool:
"""断线时静默异步重连;已连接且交易通道正常则不再重复 connect。"""
if _use_ctp_worker_client():
info = ctp_ipc_client.start_connect(mode, force=False, scheduled=True)
return bool(
info.get("connected")
or info.get("connecting")
or info.get("started")
)
if not _ctp_connect_permitted(scheduled=True):
return False
b = get_bridge()
@@ -2222,6 +2452,10 @@ def ctp_try_auto_reconnect(mode: str) -> bool:
def ctp_status(mode: str) -> dict[str, Any]:
from ctp_settings import CTP_DISABLED_HINT, is_ctp_auto_connect_enabled
if _use_ctp_worker_client():
st = ctp_ipc_client.status(mode)
st["auto_connect_enabled"] = is_ctp_auto_connect_enabled()
return st
auto = is_ctp_auto_connect_enabled()
st = get_bridge().status(mode)
st["auto_connect_enabled"] = auto
@@ -2245,6 +2479,8 @@ def ctp_status(mode: str) -> dict[str, Any]:
def ctp_get_account(mode: str) -> dict[str, Any]:
if _use_ctp_worker_client():
return ctp_ipc_client.account(mode)
b = get_bridge()
b.ensure_connected(mode)
return b.get_account()
@@ -2269,6 +2505,18 @@ def ctp_sum_position_margins(
def ctp_account_margin_used(mode: str) -> Optional[float]:
"""账户实际占用保证金 ≈ 权益 − 可用(与顶栏柜台资金一致)。"""
if _use_ctp_worker_client():
try:
acc = ctp_ipc_client.account(mode)
balance = float(acc.get("balance") or 0)
available = float(acc.get("available") or 0)
if balance <= 0:
return None
used = balance - available
return round(used, 2) if used > 0 else None
except Exception as exc:
logger.debug("ctp_account_margin_used ipc: %s", exc)
return None
b = get_bridge()
if b.connected_mode != mode or not b.ping():
return None
@@ -2291,6 +2539,12 @@ def ctp_list_positions(
refresh_if_empty: bool = True,
refresh_margin: bool = False,
) -> list[dict[str, Any]]:
if _use_ctp_worker_client():
return ctp_ipc_client.positions(
mode,
refresh_if_empty=refresh_if_empty,
refresh_margin=refresh_margin,
)
b = get_bridge()
if b.connected_mode != mode or not b.ping():
return []
@@ -2298,18 +2552,24 @@ def ctp_list_positions(
def ctp_list_active_orders(mode: str) -> list[dict[str, Any]]:
if _use_ctp_worker_client():
return ctp_ipc_client.active_orders(mode)
b = get_bridge()
b.ensure_connected(mode)
return b.list_active_orders()
def ctp_cancel_order(mode: str, vt_orderid: str) -> bool:
if _use_ctp_worker_client():
return ctp_ipc_client.cancel_order(mode, vt_orderid)
b = get_bridge()
b.ensure_connected(mode)
return b.cancel_order(vt_orderid)
def ctp_list_trades(mode: str, *, refresh: bool = False) -> list[dict[str, Any]]:
if _use_ctp_worker_client():
return ctp_ipc_client.trades(mode, refresh=refresh)
b = get_bridge()
if b.connected_mode != mode or not b.ping():
return []
@@ -2318,6 +2578,8 @@ def ctp_list_trades(mode: str, *, refresh: bool = False) -> list[dict[str, Any]]
def ctp_get_tick_price(mode: str, ths_code: str) -> Optional[float]:
"""CTP 柜台最新价(需已连接并订阅)。"""
if _use_ctp_worker_client():
return ctp_ipc_client.tick_price(mode, ths_code)
b = get_bridge()
if b.connected_mode != mode:
return None
@@ -2329,6 +2591,8 @@ def ctp_get_tick_price(mode: str, ths_code: str) -> Optional[float]:
def ctp_get_tick_detail(mode: str, ths_code: str) -> dict[str, Any]:
if _use_ctp_worker_client():
return ctp_ipc_client.tick_detail(mode, ths_code)
b = get_bridge()
if b.connected_mode != mode:
return {}
@@ -2346,6 +2610,13 @@ def ctp_estimate_margin_one_lot(
*,
direction: str = "long",
) -> Optional[float]:
if _use_ctp_worker_client():
return ctp_ipc_client.estimate_margin_one_lot(
mode,
ths_code,
price,
direction=direction,
)
b = get_bridge()
if b.connected_mode != mode or not b.ping():
return None
@@ -2357,6 +2628,8 @@ def ctp_estimate_margin_one_lot(
def ctp_lookup_contract_spec(mode: str, ths_code: str) -> Optional[dict]:
if _use_ctp_worker_client():
return ctp_ipc_client.contract_spec(mode, ths_code)
b = get_bridge()
if b.connected_mode != mode or not b.ping():
return None
@@ -2390,6 +2663,17 @@ def execute_order(
order_type: str = "limit",
) -> dict[str, Any]:
"""统一下单:simulation=SimNowlive=期货公司 CTP。"""
if _use_ctp_worker_client():
return ctp_ipc_client.send_order({
"mode": mode,
"offset": offset,
"symbol": symbol,
"direction": direction,
"lots": lots,
"price": price,
"settings": settings or {},
"order_type": order_type,
})
del conn, settings
if mode not in ("simulation", "live"):
raise ValueError("未知交易模式")