Files
qmt_bridge/docs/设计/整体设计方案_v3.md

570 lines
26 KiB
Markdown

# 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 |