# QMT Bridge 整体设计方案(统一单端口 8610) 版本: v3(2026-08-19, 2026-08-26 整合环境调研/最终方案) 状态: 实施中(WS 单通道推送一期已完成,见设计变更记录) --- ## 一、设计目标 1. **复刻旧桥接口**:把 `miniqmt_bridge/bridge.py`(FastAPI, 8610)的 9 个接口 1:1 覆盖,旧脚本零改动 2. **新增订阅与推送**:HTTP 订阅(单票/全量)+ WS 推送(行情 + 交易结果) 3. **统一单端口**:HTTP + WebSocket 都走 8610(性能无影响,技术问题可解,见下) 4. **跑在大 QMT 内**:策略编辑器加载,Python 3.6.8,纯标准库零依赖 5. **薄桥原则**: 桥只做协议转换 + 透传,QMT 统一处理业务逻辑。不追求大并发时**不做订阅合并/缓存/过滤等自研逻辑**,订阅、回调、数据处理全部透传给大 QMT(官方:多策略订阅同品种计数不累加,即 QMT 已内置合并) 6. **实施路径(2026-08-20 定)**: 最小骨架先行(`/health` + `/data/kline` + `/data/quote` + WS 连接/心跳),验证整条链路;订阅端点、交易接口、WS 推送后续增量添加 ### 1.1 命名空间(2026-08-19 定稿) | 命名空间 | 内容 | 官方依据 | |---------|------|---------| | **data** | 行情、K线、板块、日历、合约信息、快照、订阅 | `xtquant.xtdata`(行情中心) | | **trade** | 持仓、资金、委托、成交、下单、撤单 | `xtquant.xttrader`(交易中心) | | 通用 | 健康检查 | - | **新旧并存**: 旧桥无前缀路径保留(内部转发),新接口用 `/data/*` `/trade/*`。 --- ## 一之二、QMT 环境调研(设计依据,2026-08-19 实测) > 整合自 `QMT内置Python环境与pip扩展调研.md`(已删除,内容并入本设计文档)。 > QMT 安装路径: `C:\Programs\GJQMT_BIG`(桌面快捷方式指向 `bin.x64\XtItClient.exe`) ### 本机两个 QMT 实例 | | `C:\Programs\GJQMT` | `C:\Programs\GJQMT_BIG`(★ 桥运行目标)| |---|---|---| | 身份 | miniQMT 版(有 XtMiniQmt.exe + userdata_mini) | 大 QMT 完整版(纯交易端)| | Python | 极简嵌入式(pythonw + python36.dll,无 site-packages) | 完整 3.6.8 + 101 包 | | 桥 | ❌ 不在此跑(miniQMT 关停中) | ✅ **策略编辑器 + 桥都跑这里** | ### 内置 Python 环境(实测) | 项 | 值 | |----|-----| | 解释器 | `C:\Programs\GJQMT_BIG\bin.x64\pythonw.exe`(嵌入式,无 python.exe/pip.exe)| | 版本 | **Python 3.6.8** (Dec 19 2022) 64位 | | 发行包 | **101 个**(site-packages 实测)| | pip | **21.0.1**(内置,可导入)| | threading | **实测可用**(线程能起)| | 调用方式 | `pythonw.exe -c "..."`(GUI 子系统,**无 stdout/stderr**,需重定向到文件/StringIO)| ### 已内置关键包(桥方案相关) - **通信**: redis 3.5.3 ✅、pyzmq 18.0.1 ✅、requests 2.24.0、tornado 6.0.2 - **科学计算**: numpy 1.19.1、pandas 0.22.0、scipy 1.5.2 - **ML**: scikit-learn 0.23.2、tensorflow 1.8.0、torch 1.0.1、talib 0.4.17 - **数据库**: pymongo 3.11.3、pymssql 2.1.4 ### pip 扩展能力(实测可用) - 结论: **可以用 pip 安装外部包**(pythonw 无控制台需先设 stdout/stderr 代理,否则报 `'NoneType' object has no attribute 'write'`) - 实测: `pip.main(["install","--target",target,"flask==2.0.3"])` → exit 0,安装成功并可 import - **方式 A**: `--target` 装到独立目录(推荐,不污染 QMT);**方式 B**: 直接装进 site-packages(需管理员) - ⚠️ 注意: 只能装支持 cp36 的包;SSL 旧,HTTPS 可能失败(用 `--trusted-host` 或国内镜像);C 扩展包需 cp36 wheel ### 对本方案的意义 1. **零依赖方案成立**: 纯标准库即可实现桥(当前实现);redis/pyzmq 内置可作备选传输 2. **HTTP 桥可扩展**: 可 pip 装 flask/websockets 等 3. **线程实测可用**: 多线程在 QMT 内 OK(线程池方案成立) 4. **策略可上 ML**: talib/tensorflow/torch 内置 --- ## 二、总体架构 ``` ┌──────────────────── 大 QMT 进程(策略) ────────────────────┐ │ qmt_bridge.py (统一入口, 薄桥) │ │ ├─ init(ContextInfo) │ │ │ ├─ set_account / GIL 调优 │ │ │ ├─ 启动 8610 统一服务器(HTTP + WS 共端口) │ │ │ └─ run_time("adjust", 5ms) 注册定时器 │ │ │ │ │ ├─ HTTP server (ThreadingMixIn, 8610) │ │ │ ├─ /data/* 查询 + 订阅 │ │ │ ├─ /trade/* 交易查询 │ │ │ └─ Upgrade: websocket → WS 帧循环 (RFC6455) │ │ │ │ │ ├─ WS 推送 (单通道 /ws, 按 type 区分) │ │ │ ├─ 行情推送: type=whole (全量tick增量, 已实现) │ │ │ └─ 交易结果推送: type=trade_result (预留未实现) │ │ │ │ │ ├─ 订阅管理 (bridge_subscription.py, 单客户端) │ │ ├─ adjust() 定时器 → 下单/撤单队列执行 (passorder/cancel) │ │ └─ 回调: order_callback / deal_callback → 回写 + WS 推送 │ └────────────────────────────────────────────────────────────┘ │ 8610 (HTTP + WS 同端口) ▼ 外部: sfgrid / monitor_signals / 策略脚本 / AI 助手 ``` > 注: 调度周期由 v3 初稿的 500ms 更新为 **5ms**(见"二之二、架构决策与性能实测")。 --- ## 二之二、架构决策与性能实测(2026-08-20 定稿,整合自最终方案) ### 架构决策:自研 HTTP 桥(线程池 + 高频调度),不用 big_convert 经过多轮实测,最终确定:**在 QMT 策略内跑自研 HTTP 桥**,TS 客户端直接 fetch。 **为什么不用 big_convert**: - big_convert 的 ZMQ 方案成熟,但要写 TS ZMQ library(协议复杂,2-4 周) - HTTP 方案 TS 直接 fetch,零协议成本 - **关键突破**:发现"线程池 + 高频 run_time 调度"能让 QMT 内 HTTP 变快 ### 性能实测数据(5ms 调度 + 8 线程池) | 调度周期 | 平均延迟 | |---------|---------| | 500ms(默认) | 500-2500ms ❌ | | 50ms | 62.6ms | | **5ms** | **28.2ms(最快 7ms)** ✅ | | 接口 | 速度 | 真实数据 | |------|------|---------| | /health | 24-57ms | - | | /data/quote | 29-34ms | ✅ 真实行情 | | /data/kline | 正常 | ✅ 真实K线 | ### 关键技术发现(QMT 环境特性) 1. **线程创建慢,复用快** → 用线程池(预建 8 线程) 2. **run_time 调度周期决定线程调度频率** → 周期越短,线程响应越快 - 500ms 周期 → 线程每 500ms 才被调度(慢) - 5ms 周期 → 线程每 5ms 被调度(快) 3. **但周期太小有风险**(big_convert 警告):adjust 热循环占 GIL,饿死后台线程 - 5ms = 200次/秒,远低于危险的 2500次/秒,实测稳定 4. **行情读取线程安全**(后台线程直接调 get_full_tick 快) 5. **交易查询需要主线程**(实测: get_trade_detail_data 在 HTTP 线程报 `'NoneType' object has no attribute 'request_id'` → 用策略线程缓存/队列方案) ### 最终架构 ``` TS 客户端(DSH 插件) │ HTTP fetch(简单) ▼ QMT 策略内 HTTP 桥 ├─ 线程池 8 线程(预建,复用) ├─ run_time 5ms 调度 ├─ 单端口 8610 └─ data/trade 接口 + WS 单通道推送 │ ContextInfo/passorder ▼ 大QMT ``` --- ## 三、单端口(8610)方案分析 ### 3.1 原理:WebSocket 握手就是 HTTP ``` 客户端: GET /ws HTTP/1.1 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: xxx ``` 服务端: 请求头含 `Upgrade: websocket` → 回 101 → 进入 WS 帧循环 否则 → 普通 HTTP 处理 ### 3.2 性能影响:无 | 维度 | 分析 | |------|------| | 连接模型 | HTTP 已是 ThreadingMixIn(每连接线程);WS 也是每连接线程 → 合并后线程模型不变 | | 握手成本 | WS 握手 = 一次 HTTP 请求,同成本 | | 分流开销 | `Upgrade` 头判断是 O(1) | | GIL | 同进程,合并不改变竞争;已 setswitchinterval(0.001) | | 吞吐 | 单端口 accept + 双 handler,与双端口等价 | ### 3.3 潜在技术问题与解法 | 问题 | 影响 | 解法 | |------|------|------| | HTTP 短连接 vs WS 长连接超时差异 | HTTP server 的 socket 配置可能误伤 WS | WS 升级后 socket 单独 settimeout(2)(_client_loop 已有) | | 鉴权统一 | WS 握手也能带 header | 统一走 HTTP 层 token 检查(Upgrade 前) | | 连接泄漏 | WS 断开要清理 | _mark_dead 已有(引用计数拆除) | ### 3.4 结论:单端口可行 ✅ --- ## 四、HTTP 接口(8610,复刻旧桥 9 接口 + data/trade 前缀) > 详细规格见 `旧桥接口规格对照.md`;此处列结构。 > **命名空间**: 大 QMT/xtquant 官方即分 data(行情) / trade(交易)两套,故接口加前缀。 ### 4.0 命名空间设计(官方依据) | 命名空间 | 内容 | 依据 | |---------|------|------| | **data** | 行情、K线、板块、日历、合约信息、快照、订阅 | 官方 `xtquant.xtdata`(行情中心) | | **trade** | 持仓、资金、委托、成交、下单、撤单 | 官方 `xtquant.xttrader`(交易中心) | | 通用 | 健康检查 | - | **新旧并存策略**: 旧桥无前缀路径(`/kline` 等)保留,内部转发到新前缀;新接口统一用 `/data/*` `/trade/*`。 ### data 命名空间(行情/数据) | 方法 | 新前缀路径 | 旧兼容路径 | 底层大QMT函数 | |------|-----------|-----------|--------------| | GET | /data/kline | /kline | ContextInfo.get_market_data_ex | | GET | /data/kline/batch | /kline/batch | 同上(多代码) | | POST | /data/kline/download | /kline/download | 全局 download_history_data | | GET | /data/instrument | /instrument | ContextInfo.get_instrument_detail | | GET | /data/calendar/trading_dates | /calendar/trading_dates | ContextInfo.get_trading_dates | | GET | /data/sectors | /sectors | ContextInfo.get_sector_list | | GET | /data/sectors/stocks | /sectors/stocks | ContextInfo.get_stock_list_in_sector | | GET | /data/stocks/delisted | /stocks/delisted | 板块差集 + 本地目录 + instrument | | GET | /data/quote | /quote | ContextInfo.get_full_tick(实时快照) | ### trade 命名空间(交易) | 方法 | 路径 | 底层大QMT函数 | |------|------|--------------| | GET | /trade/positions | get_trade_detail_data(ACCOUNT,'position') | | GET | /trade/asset | get_trade_detail_data(ACCOUNT,'account') | | GET | /trade/orders | get_trade_detail_data(ACCOUNT,'order') | | GET | /trade/trades | get_trade_detail_data(ACCOUNT,'deal') | | POST | /trade/order | passorder(队列→adjust) | | POST | /trade/cancel | cancel(队列→adjust) | | GET | /trade/order/status | ORDER_RESULTS 查询(轮询兜底,推荐用 WS 收结果) | ### 订阅端点(HTTP,2026-08-19 定稿) | 方法 | 路径 | body | 返回 | |------|------|------|------| | POST | /data/subscribe | `{"code":"600519.SH","period":"1d","client_id":"..."}` | `{"ok":true,"sub_id":123}` | | POST | /data/subscribe_whole | `{"codes":["SH","SZ"],"client_id":"..."}` | `{"ok":true,"sub_id":124}` | | POST | /data/unsubscribe | `{"sub_id":123}` | `{"ok":true}` | > 订阅是 HTTP 请求-响应;行情推送走 WS(需先建立 WS 连接并带同 client_id)。详见第五节。 ### 通用 | 方法 | 路径 | 说明 | |------|------|------| | GET | /health | 健康检查 | 错误格式统一: `{"detail": "..."}` + 400/404/500/503(与旧桥一致) ### 4.1 K 线周期合成规则(官方,直接影响 /kline 实现) **基础周期(实际存储)**: `tick`、`1m`、`5m`、`1d` **合成周期**: - `3m` ← 由 1m 合成 - `10m, 15m, 30m, 60m, 2h, 3h, 4h` ← 由 5m 合成 - `2d, 3d, 5d, 1w(周), 1mon(月), 1q(季), 1hy(半年), 1y(年)` ← 由 1d 合成 **三条规则**: 1. 取合成周期**历史** → 必须先下载基础周期(如取 15m 先下载 5m) 2. 取**实时** → 可直接订阅原始周期 3. 基础+合成混用 → 只下载基础周期一次(5m+15m 只需下载 5m) **/kline 实现映射**(旧桥已遵循): | 请求周期 | 底层动作 | |---------|---------| | tick/1m/5m/1d | 直接 download + get_market_data_ex | | 3m | 先下载 1m → 取 3m | | 10m/15m/30m/60m/2h/3h/4h | 先下载 5m → 取合成周期 | | 1w/1mon/1q/1hy/1y | 先下载 1d → 取合成周期 | --- ## 五、订阅与推送(HTTP 订阅 + WS 回调推送) ### 5.0 架构原则(2026-08-19 定稿):按协议特性划分 **不按 data/trade 划分协议,而是按"请求-响应 vs 推送"划分**: | 协议 | 职责 | 承载 | |------|------|------| | **HTTP**(请求-响应) | 所有主动操作:查询、**订阅/退订**、下单请求、下载 | `/data/*` `/trade/*` 全部查询 + 订阅端点 | | **WebSocket**(双向长连接) | 所有**异步回调/推送**:行情、交易结果 | 行情 tick、trade_result、心跳 | **理由**: - 订阅是"一次性动作 + 明确返回",天然适合 HTTP(返回订阅号、幂等重试) - WS 连接专一化:客户端只收推送,不用发请求(除心跳),连接管理简单 - 交易结果异步推送(替代 HTTP 轮询 order/status) - 符合薄桥原则:桥只做"HTTP 请求→QMT 调用"和"QMT 回调→WS 推送" ``` HTTP (8610) WebSocket (8610 同端口) ├─ POST /data/subscribe 订阅单票 ─► 行情推送 {type:"quote",...} ├─ POST /data/subscribe_whole 全量 ──► 行情推送 {type:"whole",...} ├─ POST /data/unsubscribe 退订 ├─ GET /data/kline 查询K线 ├─ POST /trade/order 下单 ────► 交易结果 {type:"trade_result",rid,...} ├─ POST /trade/cancel 撤单 ────► 交易结果 {type:"trade_result",rid,...} └─ ... 其余查询/操作 只有:行情推送 + 交易结果 + 心跳 ``` ### 5.1 订阅关联机制(HTTP 订阅 ↔ WS 推送) **核心问题**: HTTP 订阅后,行情推给哪个 WS 连接? **采用方案:client_id 绑定** ``` 1. 客户端建立 WS 连接,握手带 client_id(或首个消息声明): ws://host:8610/ws?client_id=my-app-1 (或 X-Client-Id 头) 2. 客户端 HTTP 订阅时带 client_id: POST /data/subscribe body: {"code":"600519.SH","period":"1d","client_id":"my-app-1"} 3. 桥记录: 订阅(sub_id) → client_id → WS 连接 QMT 回调 → 查订阅表 → 按 client_id 找 WS 连接 → 推送 ``` **关联表**: ``` _SUBS[key] = {qmt_subid, refcount, clients: {client_id}} _WS_CONN[conn] = {client_id, alive, subs: set(key)} ``` - 同一 client_id 可多个 WS 连接(负载/重连),推送广播到该 client_id 的所有连接 - 无 WS 连接时订阅仍生效(数据缓存),连接建立后补推? (MVP: 不补推,客户端重连后重新订阅) ### 5.2 HTTP 订阅端点 | 方法 | 路径 | body | 返回 | |------|------|------|------| | POST | `/data/subscribe` | `{"code":"600519.SH","period":"1d","client_id":"..."}` | `{"ok":true,"sub_id":123}` | | POST | `/data/subscribe_whole` | `{"codes":["SH","SZ"],"client_id":"..."}` | `{"ok":true,"sub_id":124}` | | POST | `/data/unsubscribe` | `{"sub_id":123}` 或 `{"code":"...","period":"...","client_id":"..."}` | `{"ok":true}` | - 单票订阅透传 → `ContextInfo.subscribe_quote(code, period, cb)` - 全量订阅透传 → `ContextInfo.subscribe_whole_quote(codes, cb)` - 返回 `sub_id`(QMT 订阅号或桥生成),用于退订 - 错误: `{"detail":"..."}` + 400/404/503 ### 5.3 单票订阅 vs 全量订阅(QMT 官方定义) **前提确认(实盘验证)**: 大 QMT 的 `ContextInfo` **有**订阅逻辑,与 miniQMT 同名但形态不同。 依据 xtquant_big_convert 实盘验证(`quote_subscription_manager.py` 第 48-51 行): > "Verified against the real environment: `ContextInfo.subscribe_whole_quote(code_list, callback)` returns an int subscription id (`<0` on failure) and pushes INCREMENTAL `{code: tick}` batches on a **dedicated quote thread**; `ContextInfo.unsubscribe_quote(sub_id)` cancels it." | 方法 | 大 QMT 行为 | |------|------------| | `ContextInfo.subscribe_whole_quote(codes, cb)` | 全量订阅,返回 int 订阅号,失败 <0 | | `ContextInfo.subscribe_quote(code, period, cb)` | 单票订阅(官方文档确认) | | `ContextInfo.unsubscribe_quote(sub_id)` | 反订阅 | | `ContextInfo.get_full_tick(codes)` | 取当前快照(全推增量前的 prime) | 大 QMT vs miniQMT: | | miniQMT(xtdata) | 大 QMT(ContextInfo) | |---|---|---| | 调用 | `xtdata.subscribe_quote(...)` | `ContextInfo.subscribe_quote(...)` | | 位置 | 外部 Python | 策略进程内 | | 回调线程 | 独立行情线程 | 专用 quote 线程(不阻塞主线程) | | 连接 | 连 miniQMT 58610 | 无需连接,策略内直接用 | | | **单票订阅** | **全量订阅** | |---|---|---| | QMT 函数 | `ContextInfo.subscribe_quote(code, period, cb)` | `ContextInfo.subscribe_whole_quote(code_list, cb)` | | 数据 | 指定周期 **K 线**(分笔/1m/5m/1d) | **分笔快照**(最新值,无历史) | | 推送时机 | 该股行情更新 | **增量推送有变化的品种** | | 五档盘口 | 有(视行情权限) | **默认无,只有最新价**(需开全推行情级别) | | 数量限制 | 非VIP ~300 个(按周期累加计数) | 不占单股订阅数,无品种上限 | | 数据形态 | `{code: DataFrame}`(K线) | `{code: {lastPrice, volume, amount, ...}}`(tick dict) | | 典型场景 | 网格策略盯指定票 | 全市场监控 / 异动扫描 | **重要官方细节**: - 单票订阅同一品种不同周期**累加计数**(如订阅 1m+5m+1d 算 3 个) - 多策略订阅同一品种**计数不累加**(QMT 内部去重) - 全推数据服务器对交易所数据**即时转发,打包增量**下发 - 全推默认无五档(需修改行情源的全推行情级别);若要五档需客户端确认权限 ### 5.4 WS 推送协议(服务端 → 客户端) 客户端建立 WS 连接后,主要**收推送**(除心跳外不发请求): ```json // 行情推送 - 单票(HTTP 订阅后) {"type":"quote","code":"600519.SH","data":{"close":[1500.0,1510.0],"volume":[...],"_index":["20260818","20260819"]}} // 行情推送 - 全量(HTTP 订阅后,增量tick) {"type":"whole","data":{"600519.SH":{"lastPrice":1500.0,"volume":12345,"amount":...,"askPrice":[...],"bidPrice":[...]}}} // 交易结果推送(HTTP 下单后,替代轮询) {"type":"trade_result","rid":"r123","status":"submitted","code":"600519.SH","opType":23,"volume":100} {"type":"trade_result","rid":"r123","status":"order_update","orderSysId":"SYS1","orderStatus":50,"volumeTraded":50} {"type":"trade_result","rid":"r123","status":"dealt","price":1500.0,"volume":100,"amount":150000.0} {"type":"trade_result","rid":"r123","status":"order_error","error":"..."} // 心跳响应 {"type":"pong","ts":...} ``` 客户端 → 服务端(仅心跳,可选): ```json {"action":"ping"} ``` ### 5.5 服务端实现 ``` ┌─ 单票订阅(subscribe_quote) ──► QMT回调 → {type:"quote"} 推送 HTTP POST /data/subscribe ──► 订阅表 └─ 全量订阅(subscribe_whole_quote) ─► QMT回调 → {type:"whole"} 推送 HTTP POST /trade/order ──► 队列 → adjust → passorder └─ order_callback/deal_callback → {type:"trade_result"} 推送 ``` **订阅表(桥内状态)**: ``` _SUBS[key] = {qmt_subid, refcount, clients: {client_id}} key: - 单票: ("quote", code.upper(), period) - 全量: ("whole", tuple(sorted(codes.upper()))) ``` **QMT 回调 → WS 推送转换**: - 单票回调 `data = {code: DataFrame}` → `{"type":"quote","code":code,"data":{col:[vals],"_index":[...]}}` - 全量回调 `data = {code: tick_dict}` → `{"type":"whole","data":{code: tick_dict}}` - 交易回调(ORDER_RESULTS 更新后)→ `{"type":"trade_result","rid":remark,...}` 推给该 rid 关联的 client_id **分发**: - QMT 回调 → 按订阅表找 client_id 集合 → 推给该 client_id 的所有 WS 连接 - 无 WS 连接在线时:行情丢弃(增量);交易结果存 ORDER_RESULTS(可 HTTP 查) ### 5.6 边界与注意 - **订阅上限**: 单票超 ~300 时拒绝新订阅,HTTP 返回 `{"detail":"sub failed/limit"}` + 503 - **全量五档**: 默认只有最新价;若客户端要五档,需 QMT 行情源开启全推行情级别(文档提示) - **初次订阅耗时**: 单票订阅"初次订阅耗时长",客户端应容忍订阅后首次数据延迟 - **周期**: 单票仅 4 种基本周期(tick/1m/5m/1d)+ L2(有权限);全量只有分笔 - **并发推送**: QMT 回调可能频繁触发,推送用短帧、不阻塞回调;广播失败(客户端断开)静默清理 - **订阅后先推快照(全量订阅必须,实盘验证)**: 全推回调是增量的,客户端订阅后第一次要先 `get_full_tick([codes])` 拿当前快照,再进入增量推送(xtquant_big_convert 实盘验证了此设计) ### 5.7 分时以上 K 线订阅的处理(重要设计决策) **依据 xtquant_big_convert 实盘实现**: 分时以上 K 线(1m/5m/1d)**不做 QMT 推送订阅**,而是**轮询 `get_market_data_ex`**。 源码证据(xtquant_compat.py `subscribe_quote`): - K 线周期的"订阅"回调 = 拉一次 `get_market_data_ex` 历史 - 服务端 `save_quote_subscription` 只登记状态,不建立 K 线推送通道 - 客户端靠自己的定时器反复拉最新 K 线 **原因**: - K 线是聚合数据,1m 一分钟一根、1d 一天一根,轮询成本极低 - QMT 单票 subscribe_quote 有 ~300 上限,不值得为 K 线占配额 - 全推只有分笔,给不了 K 线 **我们的桥对应设计**: | 数据 | 方式 | 通道 | |------|------|------| | 分笔/tick(实时) | QMT 全推真推送 | WS(HTTP 订阅后推送) | | 1m/5m/1d K线 | 轮询 `get_market_data_ex` | HTTP /kline(客户端主动拉) | | K线推送式(可选) | 桥内定时器轮询 → 推 WS | WS (type=kline, 桥内周期拉取) | **决策**: MVP 阶段 **K 线走 HTTP /kline 轮询**(简单、不占配额);若客户端需要推送式 K 线,再加桥内周期轮询推送(第 3 行)。 --- ## 六、模块结构(薄桥版) **决策**: 保持**单文件**(`qmt_bridge.py`),因为: - QMT 策略编辑器加载单文件最简单(一个文件一个策略) - 薄桥逻辑简单,拆多个文件反而增加 QMT 目录同步负担 - 后续需要再拆(WS 复杂化时) ``` qmt_bridge.py 统一入口 + HTTP handler + WS 订阅 + 下单队列(薄桥,全部透传) ``` ### 6.1 单文件内部结构 ``` qmt_bridge.py ├─ 配置区 PORT/ACCOUNT/ACCOUNT_TYPE/TOKEN/WS... ├─ QMT 生命周期 init / handlebar / adjust / stop ├─ HTTP handler BaseHTTPRequestHandler │ ├─ /data/* 行情查询 + 订阅端点(旧路径兼容转发) │ ├─ /trade/* 交易查询/下单 │ └─ Upgrade: websocket → WS 逻辑 ├─ WS 推送 握手/帧/心跳(只收心跳,专注推送行情 + trade_result) ├─ 订阅表 HTTP 订阅 → 关联 client_id → QMT subscribe(透传) ├─ 下单队列 adjust() 里执行 passorder/cancel(唯一非透传: 下单必须策略线程) └─ 回调 order_callback / deal_callback(回写 ORDER_RESULTS + WS 推送) ``` ### 6.2 薄桥下"唯一需要桥自己逻辑"的地方 | 功能 | 是否透传 | 说明 | |------|---------|------| | 行情查询(/data/*) | ✅ 透传 | 直接调 ContextInfo | | 订阅(HTTP) | ✅ 透传 | 直接 subscribe_quote/whole_quote,按 client_id 记录关联 | | WS 推送 | ⚠️ 桥内关联 | QMT 回调 → 查订阅表 → 推给 client_id 的 WS 连接 | | 持仓/资金/委托/成交查询(/trade/*) | ✅ 透传 | 直接 get_trade_detail_data | | **下单/撤单** | ⚠️ **队列** | passorder 必须在策略线程(adjust)执行,HTTP 线程不能直调(官方约束) | | 状态回写 | ⚠️ 回调+推送 | passorder 无返回,靠 order_callback/deal_callback 回写 + WS trade_result 推送 | --- ## 六之二、实现计划(薄桥 MVP) ### Phase 1: 最小骨架(2026-08-20 定,本次实现) 1. 单文件 `qmt_bridge.py`:HTTP handler + `/health` + `/data/kline` + `/data/quote` 2. WS 连接打通(Upgrade + 帧循环 + 心跳,暂不推送内容) 3. 旧路径兼容(`/kline` → `/data/kline`,`/quote` → `/data/quote`) 4. 本地 mock 测试(最小骨架 3 接口 + WS 连接) ### Phase 1.5: 增量(骨架验证后) 5. data 其余查询接口(/kline/batch /kline/download /instrument /calendar /sectors /sectors/stocks /stocks/delisted) 6. trade 查询接口(/positions /asset /orders /trades) 7. 下单/撤单队列 + adjust + 回调回写 ### Phase 1.6: 订阅与推送(骨架 + 查询稳定后) 8. HTTP 订阅端点(/data/subscribe /subscribe_whole /unsubscribe)+ client_id 关联 9. WS 行情推送(type=quote / type=whole) 10. WS 交易结果推送(type=trade_result) ### Phase 2: 真实环境验证 11. 部署到 QMT(GJQMT_BIG),跑通待实测项 12. 模拟盘验证下单/撤单/回报 ### Phase 3: 扩展(按需) 13. 交易类接口细化(两融/期权/新股) 14. 推送式 K 线(桥内轮询) 15. Redis/MQ 中转(海量客户端时) --- ## 七、端口决策记录 | 方案 | 结论 | 理由 | |------|------|------| | 双端口(8610 HTTP + 8611 WS) | 备选 | 实现简单,但多开一个端口 | | **单端口(8610 统一)** | **选定** | 无性能影响,技术问题可解,部署/安全/客户端更简单 | --- ## 八、待实测项(真实 QMT) 1. `ContextInfo.get_market_data_ex` 返回结构确认(index=时间? columns=字段?) 2. `download_history_data` 全局函数是否可用 3. `/stocks/delisted` 的大QMT 本地数据目录结构 4. `get_sector_list` 是否原生(还是 fallback) 5. WS 单端口升级在 ThreadingMixIn 下是否正常 6. HTTP 订阅后,`subscribe_quote`/`subscribe_whole_quote` 回调能否正确触达(真实行情环境) 7. 下单后 `order_callback`/`deal_callback` 是否实盘模式才触发(决定 trade_result 推送的可靠性) --- ## 九、设计变更记录 | 日期 | 变更 | |------|------| | 2026-08-19 | v3 定稿:薄桥 + data/trade 前缀 + 单端口 8610 | | 2026-08-19 | **协议划分变更**: HTTP 负责请求/订阅/下单,WS 只做推送(行情 + 交易结果)。订阅从 WS 移入 HTTP,新增 /data/subscribe 等端点;WS 新增 trade_result 推送,替代 order/status 轮询 | | 2026-08-20 | **架构决策**: 自研 HTTP 桥(线程池 8 线程 + run_time 5ms 调度),不用 big_convert;整合自最终方案.md(详见二之二) | | 2026-08-26 | **WS 单通道推送(一期)完成**: 单通道 /ws + 全量tick订阅(type=whole),订阅/退订走 HTTP(/data/subscribe,/data/unsubscribe),新增 /data/tick 透传 get_full_tick;详情见 docs/迭代记录/WS单通道推送设计.md |