"""vnpy_qmt 进程内执行客户端(Phase 2:替掉 HTTP bridge)。 与 ``bridge_client.BridgeClient`` 同接口(place_order/get_account/get_positions/ get_orders/cancel_order),但底层是**进程内 QmtGateway 直连 miniQMT**(同机同会话, xtquant 进程内调用),不经 HTTP bridge。 设计: - ``_QmtExec`` 单例持有长生命周期的 EventEngine + QmtGateway,懒连接(首次用即连), 跨 live_step 调用复用。gateway 的定时器周期性 query account/position/order 并经事件回调 推入缓存,故 get_account/get_positions 读缓存即同步返回。 - ``QmtGatewayClient`` 包装单例 + 缓存,方法签名/返回与 BridgeClient 对齐,live_orchestrator 可零改调用方代码替换。 配置(env):SANGUO_QMT_ACCOUNT(交易账号,默认 66639661)、SANGUO_QMT_PATH(userdata_mini 路径,不设则扫 C:\\ 自动发现,避免中文路径字面量)。 依赖:vnpy 4.4.0 源码(sys.path)+ vnpy_qmt 0.3.3(pip)+ miniQMT 同机运行。 """ from __future__ import annotations import logging import os import threading from typing import Any logger = logging.getLogger(__name__) _singleton: _QmtExec | None = None _lock = threading.Lock() def _parse_code(code: str) -> tuple[str, Any]: """bridge code(sh600000/sz000001)→ (symbol, Exchange)。兼容纯数字码。""" from vnpy.trader.constant import Exchange c = code.lower() if c.startswith("sh"): return code[2:], Exchange.SSE if c.startswith("sz"): return code[2:], Exchange.SZSE # 纯数字码兜底(与 bridge_client.to_bridge_code 一致) if code.startswith(("60", "68", "51", "56", "58")): return code, Exchange.SSE return code, Exchange.SZSE def _to_bridge_code(symbol: str, exchange: Any) -> str: """(symbol, Exchange) → bridge code(sh600000),get_positions 输出对齐 bridge。""" from vnpy.trader.constant import Exchange return f"sh{symbol}" if exchange == Exchange.SSE else f"sz{symbol}" class _QmtExec: """长生命周期 vnpy_qmt 执行器单例:EventEngine + QmtGateway + 行情/账本缓存。""" def __init__(self) -> None: from vnpy.event import EventEngine from vnpy.trader.event import EVENT_ACCOUNT, EVENT_POSITION, EVENT_ORDER from vnpy_qmt import QmtGateway self.account: Any = None # 最新 AccountData self.positions: dict[str, Any] = {} # vt_symbol -> PositionData self.orders: dict[str, Any] = {} # vt_orderid -> OrderData self.ee = EventEngine() self.ee.register(EVENT_ACCOUNT, self._on_account) self.ee.register(EVENT_POSITION, self._on_position) self.ee.register(EVENT_ORDER, self._on_order) self.ee.start() self.gateway = QmtGateway(self.ee) self.gateway.connect(self._setting()) logger.info("QmtGatewayClient 连接 miniQMT: %s", self._setting().get("交易账号")) @staticmethod def _setting() -> dict[str, str]: account = os.environ.get("SANGUO_QMT_ACCOUNT", "66639661") path = os.environ.get("SANGUO_QMT_PATH") or _find_userdata_mini() return {"交易账号": account, "mini路径": path} def _on_account(self, event: Any) -> None: self.account = event.data def _on_position(self, event: Any) -> None: p = event.data self.positions[p.vt_symbol] = p def _on_order(self, event: Any) -> None: o = event.data self.orders[o.vt_orderid] = o def _find_userdata_mini() -> str: """扫 C:\\ 找 userdata_mini 目录(避免中文路径字面量编码问题)。""" try: for name in os.listdir("C:\\"): cand = os.path.join("C:\\", name, "userdata_mini") if os.path.isdir(cand): return cand except OSError: pass return "" def get_qmt_exec() -> _QmtExec: """懒加载单例(线程安全)。首次调用连 miniQMT,之后复用。""" global _singleton if _singleton is None: with _lock: if _singleton is None: _singleton = _QmtExec() return _singleton class QmtGatewayClient: """进程内 vnpy_qmt 执行客户端,接口对齐 BridgeClient。 构造签名兼容 BridgeClient(url, token)——url/token 被忽略(进程内无需), 便于 live_orchestrator 零改替换。 """ def __init__(self, url: str = "", token: str = "") -> None: self._exec = get_qmt_exec() def place_order(self, code: str, action: str, price: float, volume: int, price_type: str = "limit", reason: str = "") -> dict | None: """下单 → {ok: True, order_id: vt_orderid} 或 None。""" try: from vnpy.trader.constant import Direction, OrderType, Offset from vnpy.trader.object import OrderRequest symbol, exchange = _parse_code(code) direction = Direction.LONG if action == "buy" else Direction.SHORT otype = OrderType.LIMIT if price_type == "limit" else OrderType.MARKET req = OrderRequest(symbol=symbol, exchange=exchange, direction=direction, type=otype, volume=volume, price=price, offset=Offset.NONE, reference=reason or "") vt_orderid = self._exec.gateway.send_order(req) return {"ok": True, "order_id": vt_orderid} except Exception as e: # noqa: BLE001 影子下单绝不阻断 live_step logger.warning("QmtGatewayClient.place_order 失败 code=%s: %s", code, e) return None def get_account(self) -> dict | None: a = self._exec.account if a is None: return None mv = sum(p.volume * p.price for p in self._exec.positions.values()) return {"ok": True, "cash": a.balance - mv, "frozen": a.frozen, "market_value": mv, "total": a.balance} def get_positions(self) -> list | None: return [{"code": _to_bridge_code(p.symbol, p.exchange), "volume": int(p.volume), "can_use": int(p.yd_volume), "avg_price": float(p.price)} for p in self._exec.positions.values() if p.volume > 0] def get_orders(self) -> list | None: from vnpy.trader.constant import Status return [{"vt_orderid": o.vt_orderid, "code": _to_bridge_code(o.symbol, o.exchange), "direction": "buy" if o.direction.value == "多" else "sell", "price": o.price, "volume": o.volume, "traded": o.traded, "status": o.status.name, "reference": o.reference} for o in self._exec.orders.values()] def cancel_order(self, order_id) -> dict | None: try: from vnpy.trader.object import CancelRequest oid = str(order_id) vt = oid if oid.startswith("QMT.") else f"QMT.{oid}" o = self._exec.orders.get(vt) if o is None: return {"ok": False, "error": f"order {oid} 不在缓存(gateway 未收到该单事件)"} remark = vt[4:] # vt_orderid(QMT.184246#1)→ remark(184246#1),vnpy_qmt 按 remark 建 key # vnpy 4.4.0 的 CancelRequest 必填 (orderid, symbol, exchange),缺 symbol/exchange 会 TypeError self._exec.gateway.cancel_order( CancelRequest(orderid=remark, symbol=o.symbol, exchange=o.exchange)) return {"ok": True, "order_id": order_id} except Exception as e: # noqa: BLE001 logger.warning("QmtGatewayClient.cancel_order 失败 %s: %s", order_id, e) return {"ok": False, "error": str(e)}