"""xtquant 网关封装:单例 trader,连接 miniQMT 客户端。 照搬 check_xtquant.py 验证过的 xtquant 调用模式: - XtQuantTrader(userdata, session_id) -> start() -> connect() - StockAccount(account_id) -> subscribe(account) - query_stock_asset / query_stock_positions / order_stock 启动时连接 miniQMT;连不上不崩溃,bridge /health 报 disconnected。 所有 xtquant import 延迟到 connect() 内部(Mac/NAS 无 xtquant 时模块仍可加载)。 """ import logging import os from typing import Any logger = logging.getLogger(__name__) # ===== 配置(环境变量优先,fallback 到 check_xtquant.py 默认值)===== MINIQMT_USERDATA = os.environ.get( "MINIQMT_USERDATA", r"D:\国金QMT交易端模拟\userdata_mini", ) ACCOUNT_ID = os.environ.get("ACCOUNT_ID", "66639661") SESSION_ID = int(os.environ.get("BRIDGE_SESSION_ID", "20260710")) # ===== 代码格式转换 ===== def to_xtquant_code(code: str) -> str: """sanguo 格式(sh600000/sz000001) -> xtquant 格式(600000.SH/000001.SZ)。""" code = code.strip().lower() if "." in code: return code.upper() if code.startswith("sh"): return f"{code[2:]}.SH" if code.startswith("sz"): return f"{code[2:]}.SZ" raise ValueError(f"无法识别的股票代码格式: {code}") def to_sanguo_code(code: str) -> str: """xtquant 格式(600000.SH/000001.SZ) -> sanguo 格式(sh600000/sz000001)。""" code = code.strip() if "." not in code: raise ValueError(f"无法识别的 xtquant 代码格式: {code}") symbol, market = code.split(".", 1) return f"{market.lower()}{symbol}" # ===== 网关 ===== class XtGateway: """xtquant 单例网关,封装 connect/query/place_order。 connected 属性供 bridge /health 查询;未连接时 query/place_order 抛 RuntimeError。 """ def __init__(self) -> None: self._xt: Any = None self._account: Any = None self._connected: bool = False self._last_query_ok: bool = False # 上次 query 是否成功(供 /health 探活参考) @property def connected(self) -> bool: """miniQMT 是否已连接。""" return self._connected def connect(self) -> bool: """连接 miniQMT 客户端。失败记日志不崩溃,返回 False。""" try: return self._open_session() except ImportError as e: logger.error("xtquant import 失败(检查 site-packages): %s", e) self._connected = False return False except Exception as e: logger.error("xtquant 连接异常: %s", e) self._connected = False return False def _open_session(self) -> bool: """建 XtQuantTrader + start + connect + subscribe(延迟 import xtquant)。 从 connect() 抽出,供 reconnect() 复用。raise 异常由调用方捕获。 """ from xtquant import xttrader from xtquant.xttype import StockAccount xt = xttrader.XtQuantTrader(MINIQMT_USERDATA, SESSION_ID) xt.start() ret = xt.connect() if ret != 0: logger.error( "xtquant connect 返回 %s(miniQMT 未登录或路径错误)", ret, ) self._connected = False return False account = StockAccount(ACCOUNT_ID) try: xt.subscribe(account) except Exception as e: logger.warning("subscribe 异常(可忽略,继续): %s", e) self._xt = xt self._account = account self._connected = True logger.info("xtquant 连接成功 account=%s", ACCOUNT_ID) return True def reconnect(self) -> bool: """重连:stop 旧 trader(若有),再 _open_session 重建连接。 miniQMT 客户端重启后旧 XtQuantTrader 连接失效,必须重建。 """ if self._xt is not None and hasattr(self._xt, "stop"): try: self._xt.stop() except Exception as e: logger.warning("旧 trader stop 异常(忽略,继续重建): %s", e) self._xt = None self._account = None self._connected = False logger.info("开始重连 miniQMT ...") return self.connect() def is_alive(self) -> bool: """轻量探活:query_stock_asset 返回非空 = True。 供 /health 调用检测真实连接状态(_connected 标志可能假阳性)。 不触发 reconnect(保持轻量),由调用方决定是否重连。 """ try: if not self._connected: return False asset = self._xt.query_stock_asset(self._account) alive = asset is not None self._last_query_ok = alive return alive except Exception as e: logger.debug("is_alive 探活异常: %s", e) self._last_query_ok = False return False def _retry_with_reconnect(self, fn: Any, fail_msg: str) -> Any: """执行 fn(),失败(异常)时 reconnect 一次再重试。 重连仍失败则 raise(由调用方按原逻辑降级返回空/False)。 Args: fn: 无参可调用,执行实际 query/order。 fail_msg: 日志标识(如 "query_account")。 """ try: result = fn() self._last_query_ok = True return result except Exception as e: self._last_query_ok = False logger.warning("%s 首次失败,尝试重连: %s", fail_msg, e) if not self.reconnect(): raise RuntimeError(f"重连失败,放弃 {fail_msg}: {e}") from e result = fn() # 重试一次(不再重连) self._last_query_ok = True return result def query_account(self) -> dict[str, float]: """查资金:{cash, frozen, market_value, total}(断线自动重连重试一次)。""" def _do() -> dict[str, float]: if not self._connected: raise RuntimeError("xtquant 未连接") asset = self._xt.query_stock_asset(self._account) if asset is None: raise RuntimeError("query_stock_asset 返回空(账户ID/权限问题或断线)") return { "cash": float(asset.cash), "frozen": float(asset.frozen_cash), "market_value": float(asset.market_value), "total": float(asset.total_asset), } return self._retry_with_reconnect(_do, "query_account") def query_positions(self) -> list[dict[str, Any]]: """查持仓:[{code, volume, can_use, avg_price}],code 已转 sanguo 格式。 None(断线)触发重连重试,空列表 [](真没持仓)是正常结果。 """ def _do() -> list[dict[str, Any]]: if not self._connected: raise RuntimeError("xtquant 未连接") positions = self._xt.query_stock_positions(self._account) if positions is None: raise RuntimeError("query_stock_positions 返回 None(疑似断线)") return [ { "code": to_sanguo_code(p.stock_code), "volume": int(p.volume), "can_use": int(p.can_use_volume), "avg_price": float(p.avg_price), } for p in positions ] return self._retry_with_reconnect(_do, "query_positions") def place_order( self, code: str, action: str, price: float, volume: int, price_type: str = "limit", ) -> int: """下单,返回 order_id(>0 = 报单成功)。 断线(调用抛异常)自动重连重试一次;order_id<=0 是 broker 拒单 (如非交易日/资金不足),不重试直接返回。 Args: code: sanguo 格式(sh600000/sz000001),内部转 xtquant 格式。 action: "buy" / "sell"。 price: 委托价格(市价单忽略)。 volume: 委托数量(股)。 price_type: "limit"(限价 FIX_PRICE) / "market"(市价最新 LATEST_PRICE)。 Raises: ValueError: action/price_type/code 不合法。 RuntimeError: xtquant 未连接 / 重连失败。 """ def _do() -> int: if not self._connected: raise RuntimeError("xtquant 未连接") from xtquant import xtconstant order_type = ( xtconstant.STOCK_BUY if action == "buy" else xtconstant.STOCK_SELL ) xt_price_type = ( xtconstant.FIX_PRICE if price_type == "limit" else xtconstant.LATEST_PRICE ) xt_code = to_xtquant_code(code) order_id = self._xt.order_stock( self._account, xt_code, order_type, volume, xt_price_type, price, "sanguo_bridge", "", ) return int(order_id) return self._retry_with_reconnect(_do, "place_order") def query_orders(self) -> list[dict[str, Any]]: """查委托列表:[{order_id, code, status, status_name, volume, traded, price}]。""" def _do() -> list[dict[str, Any]]: if not self._connected: raise RuntimeError("xtquant 未连接") orders = self._xt.query_stock_orders(self._account) if not orders: return [] # xtquant order_status 实测映射(50=挂单,56=已成,57=拒单,54=撤单) _STATUS = {48: "unknown", 50: "pending", 51: "reporting", 52: "reported", 53: "reported", 54: "canceled", 55: "canceled", 56: "filled", 57: "rejected"} result = [] for o in orders: try: st = getattr(o, "order_status", None) code = getattr(o, "stock_code", None) result.append({ "order_id": getattr(o, "order_id", None), "code": to_sanguo_code(code) if code else None, "status": st, "status_name": _STATUS.get(st, str(st)), "status_msg": getattr(o, "status_msg", "") or "", "volume": int(getattr(o, "order_volume", 0) or 0), "traded": int(getattr(o, "traded_volume", 0) or 0), "price": float(getattr(o, "price", 0) or 0), }) except Exception as e: logger.warning("query_orders 解析异常: %s", e) return result return self._retry_with_reconnect(_do, "query_orders") def cancel_order(self, order_id: int) -> int: """撤单,返回 order_id(>0=撤单请求受理,实际状态查 /orders)。""" def _do() -> int: if not self._connected: raise RuntimeError("xtquant 未连接") return int(self._xt.cancel_order_stock(self._account, int(order_id))) return self._retry_with_reconnect(_do, "cancel_order") # 模块级单例(bridge.py 启动时调 connect) gateway = XtGateway()