# Copyright (c) 2025-2026 马建军. All rights reserved. # 专有软件 — 未经授权禁止复制、传播、转售。 # 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md """Local HTTP client for the isolated CTP worker process.""" from __future__ import annotations import json import os import time import urllib.error import urllib.request from typing import Any, Optional DEFAULT_BASE_URL = "http://127.0.0.1:6601" DEFAULT_TIMEOUT_SEC = 2.5 STATUS_TIMEOUT_SEC = 5.0 MUTATION_TIMEOUT_SEC = 8.0 class CtpWorkerUnavailable(RuntimeError): """Raised when the local CTP worker cannot be reached.""" def ctp_role() -> str: return (os.getenv("QIHUO_CTP_ROLE", "client") or "client").strip().lower() def is_worker_role() -> bool: return ctp_role() == "worker" def worker_base_url() -> str: return (os.getenv("QIHUO_CTP_WORKER_URL", DEFAULT_BASE_URL) or DEFAULT_BASE_URL).rstrip("/") def worker_token() -> str: token = (os.getenv("QIHUO_CTP_WORKER_TOKEN", "") or "").strip() if token: return token # Localhost-only default keeps old deployments working; PM2 sets a shared token. return "qihuo-local-ctp" def _request( method: str, path: str, payload: Optional[dict[str, Any]] = None, *, timeout: float = DEFAULT_TIMEOUT_SEC, ) -> dict[str, Any]: url = f"{worker_base_url()}{path}" body = None headers = { "Accept": "application/json", "X-Qihuo-CTP-Token": worker_token(), } if payload is not None: body = json.dumps(payload, ensure_ascii=False).encode("utf-8") headers["Content-Type"] = "application/json" req = urllib.request.Request(url, data=body, headers=headers, method=method.upper()) try: with urllib.request.urlopen(req, timeout=timeout) as resp: raw = resp.read().decode("utf-8", errors="replace") except (urllib.error.URLError, TimeoutError, OSError) as exc: raise CtpWorkerUnavailable(f"CTP worker unavailable: {exc}") from exc if not raw: return {} try: data = json.loads(raw) except json.JSONDecodeError as exc: raise CtpWorkerUnavailable(f"CTP worker returned invalid JSON: {raw[:120]}") from exc if not isinstance(data, dict): raise CtpWorkerUnavailable("CTP worker returned non-object JSON") if data.get("ok") is False: raise RuntimeError(str(data.get("error") or "CTP worker request failed")) return data def get(path: str, *, timeout: float = DEFAULT_TIMEOUT_SEC) -> dict[str, Any]: return _request("GET", path, timeout=timeout) def post( path: str, payload: Optional[dict[str, Any]] = None, *, timeout: float = DEFAULT_TIMEOUT_SEC, ) -> dict[str, Any]: return _request("POST", path, payload or {}, timeout=timeout) def health() -> dict[str, Any]: try: return get("/health", timeout=1.0) except Exception as exc: return { "ok": False, "worker_online": False, "error": str(exc), "ts": time.time(), } def status(mode: str) -> dict[str, Any]: try: data = get(f"/ctp/status?mode={mode}", timeout=STATUS_TIMEOUT_SEC) return dict(data.get("status") or {}) except Exception as exc: return { "connected": False, "connecting": False, "worker_online": False, "last_error": f"CTP worker 离线或重启中:{exc}", } def connect(mode: str, *, force: bool = False) -> dict[str, Any]: data = post( "/ctp/connect", {"mode": mode, "force": bool(force)}, timeout=MUTATION_TIMEOUT_SEC, ) return dict(data.get("status") or data) def start_connect(mode: str, *, force: bool = False, scheduled: bool = False) -> dict[str, Any]: return post( "/ctp/start_connect", {"mode": mode, "force": bool(force), "scheduled": bool(scheduled)}, timeout=MUTATION_TIMEOUT_SEC, ) def disconnect(*, set_disabled_hint: bool = False) -> None: post( "/ctp/disconnect", {"set_disabled_hint": bool(set_disabled_hint)}, timeout=MUTATION_TIMEOUT_SEC, ) def account(mode: str) -> dict[str, Any]: data = get(f"/ctp/account?mode={mode}") return dict(data.get("account") or {}) def positions( mode: str, *, refresh_if_empty: bool = True, refresh_margin: bool = False, ) -> list[dict[str, Any]]: data = post( "/ctp/positions", { "mode": mode, "refresh_if_empty": bool(refresh_if_empty), "refresh_margin": bool(refresh_margin), }, ) return list(data.get("positions") or []) def trades(mode: str, *, refresh: bool = False) -> list[dict[str, Any]]: data = post("/ctp/trades", {"mode": mode, "refresh": bool(refresh)}) return list(data.get("trades") or []) def active_orders(mode: str) -> list[dict[str, Any]]: data = get(f"/ctp/active_orders?mode={mode}") return list(data.get("orders") or []) def tick_price(mode: str, symbol: str) -> Optional[float]: data = post("/ctp/tick_price", {"mode": mode, "symbol": symbol}) value = data.get("price") return float(value) if value not in (None, "") else None def tick_detail(mode: str, symbol: str) -> dict[str, Any]: data = post("/ctp/tick_detail", {"mode": mode, "symbol": symbol}) return dict(data.get("detail") or {}) def estimate_margin_one_lot( mode: str, symbol: str, price: float, *, direction: str = "long", ) -> Optional[float]: data = post( "/ctp/estimate_margin_one_lot", {"mode": mode, "symbol": symbol, "price": price, "direction": direction}, ) value = data.get("margin") return float(value) if value not in (None, "") else None def contract_spec(mode: str, symbol: str) -> Optional[dict[str, Any]]: data = post("/ctp/contract_spec", {"mode": mode, "symbol": symbol}) spec = data.get("spec") return dict(spec) if isinstance(spec, dict) else None def send_order(payload: dict[str, Any]) -> dict[str, Any]: return post("/ctp/order", payload, timeout=MUTATION_TIMEOUT_SEC) def cancel_order(mode: str, vt_orderid: str) -> bool: data = post( "/ctp/cancel", {"mode": mode, "vt_orderid": vt_orderid}, timeout=MUTATION_TIMEOUT_SEC, ) return bool(data.get("cancelled")) def bridge_action(action: str, payload: Optional[dict[str, Any]] = None) -> dict[str, Any]: return post( f"/ctp/bridge/{action}", payload or {}, timeout=MUTATION_TIMEOUT_SEC, )