
1. MiniQMT 与大 QMT 的能力边界先把账算清楚聊桥接方案之前得先把 MiniQMT 和大 QMT 这两个东西的定位掰开说清楚不然选型就是空中楼阁。很多人包括半年前的我一开始都是冲着 MiniQMT 去的因为它轻、它干净、它给了一个 xtquant 库Python 里几行代码就能连上去下单。听起来很美好但真把它当生产环境的主力用一段时间你会发现它的能力边界比你想象中窄很多。而大 QMT 是完整版客户端界面、策略编辑器、行情、交易、风控全都在里面功能上是“全套”代价是它默认不给你一个干净的编程接口你得自己想办法“搭桥”把外部程序接进去。这就是“桥接”这个词的来源。所谓大 QMT 桥接本质上是QMT 客户端本体负责与柜台通信、持有登录态和交易权限而你的策略程序不在客户端里跑而是在外面运行通过某种通道把下单、撤单、查持仓的指令递给客户端再把客户端收到的委托回报、成交回报拿回来。这个“通道”怎么做就是四种方案的分水岭。1.1 两者到底差在哪些地方先从最实际的角度对比一下。MiniQMT 你可以理解成 QMT 官方切出来的一个“编程模式子集”它的核心卖点就是那个xtquant包里面有xtdata行情和xttrader交易两个模块。你写个 Python 脚本连上userdata_mini目录就能下单。但它的短板也很明确策略生命周期完全靠你自己的进程管理官方不保证你的进程挂了之后会怎样行情订阅在断线重连上需要你自己兜底更重要的是MiniQMT 用户侧的权限、账户类型、支持的品种很多时候是有限制的一些高级的下单类型、算法单、组合交易能力在 MiniQMT 这套接口里要么没有要么需要额外申请。大 QMT 就不一样了。它是完整的交易终端登录、风控、资金校验、涨跌停校验、可撤单判断这些原本就由客户端处理。它的策略编辑器支持内置 Python 策略和 VBA客户端本身是“活”的7×24 挂着也没问题。缺点也很直观你很难把外部一套成熟的 Python 工程体系比如你自己写的调度框架、风控中间件、日志系统、回测引擎直接塞进它的策略编辑器里跑。内置策略的调试体验、依赖管理、异常隔离都远不如你自己在外部跑一个进程来得舒服。1.2 我为什么决定换掉 MiniQMT说几个我踩过的具体场景。第一我的策略体系是多进程的有做行情的、有做信号计算的、有做下单执行的彼此通过消息通信。这套东西放在 MiniQMT 上意味着我每一个进程都要自己维护一个到userdata_mini的连接session_id 要小心不要冲突任何一个进程崩溃重连行情订阅就要重新拉一遍。第二我需要跨语言一部分风控逻辑是同事用别的语言写的它没法直接调 Python 的 xtquant。第三日志和监控。我需要把所有委托、成交、拒单都汇总到一个中心化的地方做审计MiniQMT 那种“谁连谁拿回报”的模式导致回报分散在各个进程里汇总很痛苦。这三点叠加起来结论就一个我需要一个中心化的交易网关所有指令都经过它所有回报都由它统一分发。这个网关骑在谁身上骑在大 QMT 身上。那么网关和 QMT 之间怎么通信就成了我接下来要横评的核心问题。而最终我选了 HTTP API 这条路原因后面会一层层展开。2. 四种桥接方案的横评各自的算盘与代价在动手之前我把市面上能想到的桥接方式都过了一遍归纳成四类。这里说的“主流”是指在实盘圈子里真正有人用、并且能跑起来的方案不是纸上谈兵。每一类我都尽量说清楚它的实现思路、适用场景以及它到底会在哪个环节咬你一口。2.1 方案一QMT 内置 Python 策略 文件/消息队列中转这是最“原生”的做法。你在 QMT 客户端里写一段内置 Python 策略让它常驻运行然后在策略里监听一个本地的中转介质——可以是文件、可以是 Redis 的某个 list、也可以是本地 socket。外部程序把指令写进去内置策略读出来调用 QMT 自己的下单函数执行成交回报再由内置策略写回另一个通道外部程序去读。它的最大好处是不依赖 xtquant 的外部连接机制因为策略本身就活在客户端进程里下单权限、账户状态、风控校验都是现成的。你不用担心connect()失败、不用担心 session 冲突。但代价是什么呢你的核心执行逻辑被绑死在客户端的策略编辑器里。那段代码不好版本管理不好单元测试出异常了客户端可能直接把策略停掉而你人在外部完全不知情。更麻烦的是性能文件轮询的延迟天然就在几十到几百毫秒Redis 虽然快但你在策略编辑器里跑一个 Redis 客户端本身就是个负担稍有不慎就阻塞整个策略线程。我实测过文件轮询那一版行情剧烈波动时指令从写出到真正报出去抖动可以到 300 毫秒以上对于做短周期的东西这个抖动是不可接受的。2.2 方案二VBA / Excel 桥这是更老的一派玩法。QMT 客户端支持 VBA早年不少人用 Excel 做界面通过 VBA 调 QMT 的下单接口形成一个“Excel 下单器”。现在还有人这么用尤其是做一些手工半自动的场景——比如表格里维护一组标的点个按钮批量报单。它的优点是上手门槛极低会点 Excel 就能搞。但作为生产桥接方案它的硬伤太明显Excel 本身不是为高可靠通信设计的进程崩溃、宏被禁用、文件被占用、弹窗阻塞任何一个都能让你的下单链路断掉。而且 VBA 的性能和并发能力撑不起多账户、高频次的下单需求。我现在的判断很直接这套方案适合做辅助工具和应急手工操作不适合做策略的主执行通道。如果你的策略需要 7×24 稳定运行趁早别往这条路上走。2.3 方案三自建 Socket / RPC 中间件这是很多有一定工程能力的人会走的一步。思路是在 QMT 那台机器上跑一个 Python 进程这个进程用 xtquant 连上 QMT然后自己起一个 TCP 服务或者 gRPC 服务对外暴露下单、撤单、查询的方法。外部程序作为客户端连上来发请求、收响应。从性能上讲这是四种方案里最优的。二进制协议、长连接、低延迟指令往返可以做到毫秒级。gRPC 还自带流式推送委托回报可以实时推给订阅方。但它的复杂度也是最高的你要自己定义协议、自己处理粘包/半包、自己做心跳和断线重连、自己做序列化。长连接一旦断了你要保证重连期间没有指令丢失要么做重发队列要么做幂等。我这边的实际情况是团队里能维护这套东西的人不多一旦我休假别人接手这个 socket 层会很吃力。工程上的最优很多时候不等于运维上的最优。2.4 方案四HTTP API 封装最后就是 HTTP API。核心思路和方案三其实是一样的——中间跑一个 Python 服务内部用 xtquant 连 QMT——区别只在于对外的协议换成了 HTTP JSON。外部程序无论是 Python、Java、Go 还是脚本只要能发 HTTP 请求就能下单。委托回报通过轮询接口或者 SSE/WebSocket 推送出去。它的性能和方案三比当然有差距每次请求都要建连除非用 keep-alive、都要序列化 JSON、都要走一遍 HTTP 解析。但现代 HTTP 客户端的连接池足够成熟实测下来在局域网内的往返延迟完全够用。而它换来的东西太值了调试极其简单curl 一条命令就能验证链路语言无关任何能联网的程序都能接入可观测性好每个接口的请求量、耗时、错误率都能被标准的监控体系直接采集扩展方便要加一个查询接口加个路由就行不需要动协议定义。2.5 一张表看清四种方案的取舍维度内置策略队列VBA/Excel自建Socket/RPCHTTP API实现难度低极低高中局域网往返延迟几十至几百毫秒不稳定毫秒级毫秒至十几毫秒跨语言支持差差中需生成客户端极好调试便利性差一般差极好客户端崩溃影响直接影响直接影响无无运维复杂度低低高中监控接入难难需自建天然支持适合场景简单辅助手工操作极低延迟需求绝大多数生产场景看这张表就很清楚了。如果你的策略是低频、批量、对延迟不敏感HTTP API 是性价比最高的如果你的策略真的对每一毫秒都敏感那方案三自建 socket 才值得你付出那份运维成本。我最终 All In HTTP API不是因为它是性能最好的而是因为它是“总拥有成本”最低的而且在多数场景下性能已经绰绰有余。3. All In HTTP API整体架构与选型理由方案定了接下来是把它落地成一套真能跑的东西。这一节先说架构和选型下一节再讲代码级实操。3.1 进程模型与部署拓扑我把整套东西分成三层。最底层是 QMT 客户端它跑在一台常开的 Windows 机器上负责登录、连接柜台、持有交易权限。中间层是一个 Python 服务我用的 FastAPI它通过 xtquant 连上 QMT 的userdata_mini目录对外暴露 HTTP 接口。这一层是整套系统的核心我把它叫做“交易网关”。最上层是任意数量的客户端——策略进程、风控模块、监控看板、手工下单页面它们全都是 HTTP 消费方。这个分层有个关键好处交易权限和执行细节全部收敛到网关这一层客户端只关心“我要下什么单”。客户端崩了、重启了网关不受影响网关要升级客户端无感。而且因为网关只有一个进程连着 QMTsession_id 冲突的问题彻底消失了。这一点在我早期踩坑时特别有感触——多个进程各自连 QMTsession 冲突导致连接莫名其妙掉线查半天查不出来就是因为没做中心化。3.2 为什么是 HTTP 而不是 gRPC / Socket有人会问既然都决定自建中间层了为什么不直接上 gRPC我的理由有三点。第一调试成本。HTTP JSON 可以让人肉直接看懂抓包、curl、Postman 全都开箱即用gRPC 得配 proto、得有专用工具排查问题时多一层心智负担。第二生态兼容。我的客户端里有一部分是运营同事用低代码平台搭的人家只认 HTTP。第三性能不是瓶颈。我做过压测局域网内单连接 HTTP 往返平均 3 到 8 毫秒用连接池之后稳定在这个区间。而我策略的信号频率是秒级的这个延迟占比可以忽略。除非你的策略是毫秒级套利否则没必要为了那几毫秒去背 gRPC 的复杂度。3.3 技术栈与依赖版本我用的组合是Python 3.8 以上xtquant 对版本有一定要求建议跟着 QMT 官方文档走FastAPI 提供 HTTP 服务uvicorn 做 ASGI 服务器pydantic 做请求体校验xtquant 做底层通道。监控方面用 Prometheus 的 Python 客户端埋点每个接口记录 count 和 latency 的 histogram。日志用 loguru 或者标准 logging 都行关键是把每一笔委托的 order_id、请求来源、时间戳、账户、标的、方向、数量、价格全部结构化打出来方便事后审计和对账。这套东西没有任何冷门依赖全部是主流开源件维护起来人才好找这是我很看重的一点。4. HTTP API 桥接的落地实操这一节是干货最密集的部分。我会把网关的核心实现拆成几块讲代码都是可以直接参考改的骨架。4.1 xtquant 封装层连接、回调与线程模型先解决最底层。xtquant 的连接对象是XtQuantTrader初始化时要传入 QMT 的userdata_mini目录路径和一个 session_id。session_id 必须全局唯一我一般用随机数import random from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback from xtquant.xttype import StockAccount from xtquant import xtconstant class TradeCallback(XtQuantTraderCallback): def on_disconnected(self): print(connection lost, need reconnect) def on_stock_order(self, order): # 委托状态变化缓存并推送 push_event(order, serialize(order)) def on_stock_trade(self, trade): # 成交回报 push_event(trade, serialize(trade)) def on_order_error(self, err): push_event(order_error, serialize(err)) def on_cancel_error(self, err): push_event(cancel_error, serialize(err)) def on_account_status(self, status): push_event(account_status, serialize(status)) def build_trader(userdata_path): session_id random.randint(100000, 999999) trader XtQuantTrader(userdata_path, session_id) trader.register_callback(TradeCallback()) trader.start() connect_result trader.connect() if connect_result ! 0: raise RuntimeError(fconnect failed: {connect_result}) account StockAccount(你的资金账号, STOCK) trader.subscribe(account) return trader, account这里有几个坑必须提前说。第一回调是在 xtquant 内部的线程里执行的你在回调里做任何耗时操作比如写数据库、发 HTTP都会阻塞后续回报所以我的做法是回调只往一个内存队列里塞数据由单独的消费线程慢慢处理。第二connect()的返回值要检查0 才是成功非 0 的情况要区分是路径错、QMT 没启动、还是 session 冲突。第三subscribe(account)之后账户状态变化才会推过来漏了这一步你就收不到on_account_status。4.2 接口设计下单、撤单、查持仓、查委托对外接口我保持极简核心就四个。下单接口的请求体用 pydantic 定义字段包括标的代码、方向、数量、价格、价格类型再加一个客户端生成幂等键from pydantic import BaseModel from typing import Optional class OrderReq(BaseModel): code: str # 如 600519.SH side: str # buy / sell volume: int price: Optional[float] None price_type: str limit # limit / latest / market remark: str # 客户端幂等键 app.post(/api/order) def place_order(req: OrderReq): order_type xtconstant.STOCK_BUY if req.side buy else xtconstant.STOCK_SELL pt xtconstant.FIX_PRICE if req.price_type limit else xtconstant.LATEST_PRICE order_id trader.order_stock( account, req.code, order_type, req.volume, pt, req.price or 0, http_api, req.remark ) if order_id 0: raise HTTPException(400, forder rejected: {order_id}) return {order_id: order_id}order_stock的返回值是 order_id负数代表报单失败。注意这里的失败是“报单请求失败”不代表委托被交易所拒绝交易所层面的拒绝会通过on_order_error回调异步返回所以你的客户端必须同时监听回报。撤单用cancel_order_stock(account, order_id)查询用query_stock_positions、query_stock_orders、query_stock_asset、query_stock_trades这几个方法我把它们分别包成 GET 接口。参数选择上有个经验限价单优先用 FIX_PRICE最新价单用 LATEST_PRICE市价单要看你所在市场是否支持A 股主板一般用限价对手价更稳。价格类型选错会导致报单被柜台直接打回这个在初期很容易翻车。4.3 委托回报与事件推送回报这块我做了两条路。第一条是轮询GET /api/orders?sincexxx返回某个时间点之后的所有委托变化客户端自己维护游标。第二条是 SSE客户端连一个/api/events的长连接网关把队列里的事件实时推出去。轮询适合简单场景SSE 适合需要实时反应的。两条路都基于同一个内存队列和同一份状态缓存保证一致性。这里要特别注意状态机。一笔委托会经历“已报”“部成”“已成”“已撤”“废单”等多个状态你不能只看最后一帧。我在网关里维护了一个以 order_id 为主键的字典每次收到回报就更新客户端拿到的永远是最新状态。实测下来这套机制很稳即使客户端中途断开重连靠轮询也能把丢失的中间状态补齐。4.4 客户端调用与压测记录客户端这边Python 用requests.Session()复用连接其他语言用各自的标准 HTTP 客户端配连接池。压测我的做法是起 20 个并发客户端每个循环发下单请求到测试账户注意用模拟环境记录 P50、P95、P99 延迟。实测结果是单请求平均 4 到 6 毫秒P99 在 20 毫秒以内QPS 稳定在 200 以上没有明显劣化。这个数据对我秒级频率的策略来说余量非常充足。要提醒的是压测一定要在模拟账户上做别拿实盘练手这个不用多说。5. 常见问题与排查技巧实录跑了大半年坑踩得够多我把高频问题整理出来方便你少走弯路。5.1 连接与鉴权类问题最常见的两个报错是 401 和 500。401 基本都是鉴权问题——我在网关前面加了一层 token 校验客户端忘了带 header、或者 token 过期就会返回 401。排查的第一反应就是看请求头里的 Authorization 对不对这是最容易被忽略的低级错误。500 则复杂一些通常意味着网关内部抛异常了可能是 xtquant 没连上、账户没订阅、或者请求参数触发了未处理的路径。我的经验是在网关里做全局异常捕获把所有未处理异常统一打成 500 并附带 trace_id然后用 trace_id 去日志里捞完整堆栈比盲目重启有效得多。5.2 报单与回报类问题有一类问题特别隐蔽接口返回了 order_id看起来成功了但回报迟迟不来。这种情况八成是回调没注册或者回调队列被写满了。我早期把耗时的落库操作直接放在回调里结果回报一多队列堆积后续回报全卡住。改成“回调只入队、消费线程再处理”之后就再没出过这个问题。另一类是报单被柜台拒绝比如资金不足、超出涨跌停、非交易时段、标的停牌这些都会走on_order_error客户端务必把这个回调接起来并告警不然你会以为单子下出去了实际根本没成交。5.3 稳定性与资源占用问题网关是常驻进程内存泄漏和线程堆积是隐性杀手。我的做法是给回调队列设上限超过就告警并落盘而不是无限增长。另外xtquant 的连接在长时间运行后偶发掉线是正常的要监听on_disconnected在里面触发重连逻辑并在重连成功后重新subscribe。还有个细节QMT 客户端的登录态有时会因为网络波动失效网关本身是感知不到的所以我在客户端侧加了一个轻量的心跳接口网关每次被调用时顺便检查账户状态发现异常就打日志告警。5.4 问题速查表现象可能原因处理方式返回 401token 缺失/过期检查请求头 Authorization返回 500网关内部异常用 trace_id 捞日志堆栈connect 返回非 0QMT 未启动/路径错/session 冲突检查 userdata_mini 路径与 session_id有 order_id 但无回报回调未注册/队列阻塞检查回调注册与队列消费报单被拒无感知on_order_error 未接补上错误回调并告警长时间后掉线网络波动/QMT 登录态失效监听 on_disconnected 并重连行情订阅丢失重连后未重新订阅重连成功后重新 subscribe这张表建议贴在工位上出问题先对一遍能省掉大量重复排查的时间。6. 这套网关后续还能怎么扩展跑通基础版本之后我在它上面又叠了几层顺手分享一下方向都是我自己实际在用的。第一层是统一风控前置所有下单请求先过一遍风控规则单笔上限、单日累计、标的黑名单、频率限制不通过直接在下单接口里拒掉这样风控就跟策略解耦了改规则不用碰策略代码。第二层是多账户路由网关维护一个账户路由表请求里带账户标识网关负责分发到对应的 xtquant 连接配合前面的订阅机制一套网关可以管多个账户。第三层是审计与对账所有请求和回报落结构化日志每天收盘后跑一遍对账脚本把网关记录和柜台流水核对差异自动告警。还有个小技巧值得单说幂等键一定要用起来。客户端在网络抖动时可能重发请求如果网关不做幂等就可能下出两笔单。我的做法是网关侧维护一个近期 remark 的集合重复 remark 直接返回首次的 order_id不做第二次报单。这个逻辑看起来简单但真出问题的时候它是能救命的。HTTP API 这条路我走到现在最大的感受就是它的价值不在某个单点性能而在于把交易的复杂度收敛到了一个可以被调试、被监控、被交接的点上。