287 lines
17 KiB
Markdown
287 lines
17 KiB
Markdown
# QMT Bridge 项目规范
|
|
|
|
> 版本: 1.1 (2026-08-26)
|
|
> 用途: **新会话快速参考**。接手本项目前请先读本文件 + `README.md` + `docs/设计/整体设计方案_v3.md`。
|
|
> 本文件沉淀了历次会话验证过的技术决策与约束,是项目的"宪法"。
|
|
|
|
---
|
|
|
|
## 一、项目定位
|
|
|
|
**大 QMT 薄桥**:跑在国金 QMT(GJQMT_BIG)策略进程内的 HTTP + WebSocket 服务,把 QMT 的行情/交易能力暴露为 RESTful 接口和 WS 推送,供外部系统(sfgrid 网格策略、监控脚本、AI 助手)调用。
|
|
|
|
- 环境: QMT 内置 Python **3.6.8**,**纯标准库零依赖**(不能装第三方包)
|
|
- 端口: **8610**(HTTP + WS 共端口)
|
|
- 账号: 从 `C:\Programs\GJQMT_BIG\python\bridge_local_config.py` 读 `ACCOUNT_ID`
|
|
- 外部消费者: **sfgrid**(`C:\Users\Docker\Development\sfgrid`,当前走 miniQMT SDK,计划迁移到桥)
|
|
|
|
---
|
|
|
|
## 二、项目结构规范
|
|
|
|
```
|
|
qmt_bridge/
|
|
├── README.md # 项目说明(入口文档)
|
|
├── docs/
|
|
│ ├── 项目规范.md # 本文件
|
|
│ ├── api_spec/openapi.yaml # 接口规范(唯一事实来源,中文,主动维护)
|
|
│ ├── 设计/整体设计方案_v3.md # 主设计文档(唯一设计依据)
|
|
│ ├── 研究/ # 调研类文档(xtquant_big_convert调研、官方接口核实)
|
|
│ ├── 迭代记录/ # 迭代完成记录(WS单通道推送设计等)
|
|
│ ├── 桥接口清单与sfgrid满足度对照.md # sfgrid 需求 vs 桥能力
|
|
│ └── ...
|
|
├── src/ # 桥源码(部署时复制到 QMT python 目录)
|
|
│ ├── bridge_main.py # QMT 生命周期入口
|
|
│ ├── bridge_http_server.py # HTTP server + 路由(含 /mcp 路由)
|
|
│ ├── bridge_ws_server.py # WebSocket(心跳 + 推送 broadcast_all)
|
|
│ ├── bridge_data_adapter.py # QMT API 透传适配层
|
|
│ ├── bridge_subscription.py # 订阅管理(snowflake sub_id + QMT 订阅回调→WS 推送)
|
|
│ ├── bridge_mcp_server.py # MCP 端点(JSON-RPC tools,POST /mcp)
|
|
│ ├── bridge_util.py # 共享状态 + 工具
|
|
│ └── qmt_strategy_entry.py # QMT 策略入口(粘贴进 QMT 编辑器,含热重载)
|
|
├── tools/
|
|
│ ├── push_apifox.py # 生成 openapi.json + Apifox 手动导入指引
|
|
│ ├── fetch_apifox_doc.ps1 # 沙箱外抓取 Apifox 文档(UAC 提权)
|
|
│ └── fetch_qmt_docs.ps1 # 沙箱外抓取 QMT 官方文档(UAC 提权)
|
|
├── tests/
|
|
│ ├── test_trade.py # 交易接口 mock 测试
|
|
│ ├── test_subscription.py # 订阅/退订/tick mock 测试
|
|
│ ├── test_ws_push.py # WS 推送链路 mock 测试
|
|
│ ├── test_ws_real.py # 真实 QMT WS 推送验证
|
|
│ └── test_sub_no_ws.py # 无 WS 连接时订阅行为测试
|
|
├── reference/ # 参考材料(xtquant_big_convert 等,只读)
|
|
└── deploy_elevated.bat # 部署脚本(自动提权)
|
|
```
|
|
|
|
### 命名规范
|
|
- 桥模块: `bridge_*.py`(部署目标),入口: `qmt_strategy_entry.py`(永不改,只转发)
|
|
- 文档: `docs/` 下中文命名,`api_spec/` 放接口规范,`设计/` 放设计文档,`研究/` 放调研文档,`迭代记录/` 放迭代完成记录
|
|
- 诊断/一次性脚本: 用完即删,不留 `diag_*.py` 垃圾
|
|
|
|
---
|
|
|
|
## 三、设计约束(硬性,踩过坑)
|
|
|
|
### 3.1 源码编码约束 ⚠️ 最重要
|
|
- **源码文件头 `# coding:gbk`,内容必须纯 ASCII**(0-127)
|
|
- **不允许在 .py 源码里写 UTF-8 中文字符**(QMT Python 3.6 按 GBK 解析,UTF-8 中文会 SyntaxError)
|
|
- 需要中文时用 `\uXXXX` 转义(运行时输出正确)
|
|
- 检查方法: 统计文件非 ASCII 字节,必须为 0
|
|
|
|
### 3.2 QMT 函数线程约束
|
|
| 函数 | 能否在 HTTP worker 线程调用 | 处理方案 |
|
|
|------|---------------------------|---------|
|
|
| `get_trade_detail_data` | ❌ 不能(报 `NoneType.request_id`) | **策略线程缓存**(adjust 刷新,HTTP 读缓存)|
|
|
| `passorder` / `cancel` | ❌ 不能 | **adjust 队列**(下单请求入队,adjust 执行)|
|
|
| `download_history_data` | ✅ 可以(实测) | 直接调用 |
|
|
| `get_market_data_ex` / `get_full_tick` / `get_instrument_detail` / `get_trading_dates` | ✅ 可以 | 直接调用 |
|
|
| `subscribe_whole_quote` | ✅ 可以(实测,2026-08-26 验证) | 直接调用,回调→WS 推送 |
|
|
| `get_trade_detail_data(acct, type, 'ORDER', strategy_name)` | ✅ 可以(带第4参数实测 OK) | 服务端过滤 |
|
|
|
|
**核心原则**: 任何 QMT 函数若在 HTTP 线程调用报错,一律改为"策略线程执行 + 缓存/队列"模式。
|
|
|
|
### 3.3 大 QMT 与 miniQMT 字段差异
|
|
- 大 QMT `get_trade_detail_data('ORDER')` 返回对象**没有 `m_strStrategyName`/`m_strRemark` 字段**(miniQMT 有)
|
|
- 原因: 策略名/备注是**下单终端本地属性**,跨终端不可见(参考项目 C8 约束)
|
|
- 结论: 按策略名过滤必须用**服务端参数**(get_trade_detail_data 第 4 参数),不能靠字段
|
|
- `m_strInstrumentID` **不带后缀**(如 `600719`,不是 `600719.SH`)
|
|
- `get_full_tick` 返回 `{code: tick_dict}`,tick 字段含 timetag/lastPrice/open/high/low/amount/volume/pvolume/stockStatus 等
|
|
|
|
### 3.4 账号机制(单账号,重要结论)
|
|
- **QMT 一个策略实例 = 一个资金账号**(界面选定账号后注入全局变量 `account`,策略内引用)
|
|
- `ContextInfo.set_account(account)` 在 init 中调用: 设定账号 + **订阅交易主推回调**(order/deal/account 回调的前提;init 后调用不再订阅)
|
|
- **passorder 的 account 传空时,用最后一次 set_account 的账号**
|
|
- 查询函数 `get_trade_detail_data(accountID, type, dtype, strategyName)` 的 **accountID 必填**
|
|
- **多账户支持**: 官方机制是**每个策略实例绑一个账号** → 要支持多账号 = 部署**多个桥实例**(多个策略,各选账号,各用不同端口 8610/8618...)
|
|
- **结论: 桥本身天然单账号,HTTP 接口的 `account`/`account_type` 参数已移除(冗余)**
|
|
- 2026-08-21 已清理: `get_trade_data`/`_submit_trade_request`/`_drain_trade_requests`/`_api_trade` 的 account + account_type 参数全部删除
|
|
- account/account_type 均为桥绑定配置(bridge_local_config.py),HTTP 接口不暴露
|
|
- 新接口设计**不需要**账号参数;如需多账号,部署多桥实例而非加参数
|
|
|
|
### 3.5 K 线数据规则
|
|
- 查询前**必须先下载**(`download_history_data`),否则返回残缺数据(OHLC 全同/volume=0)
|
|
- 合成周期需下载基础周期: `3m←1m`; `10m~4h←5m`; `2d~1y←1d`
|
|
- `get_market_data_ex` 的 fields 参数传**完整字段列表**(`["open","high","low","close","volume","amount"]`),传 `[]` 只返回 close
|
|
- DataFrame 取值用 `iat[i,j]`(位置),不用 `loc`(大 QMT index 可能非唯一)
|
|
|
|
### 3.6 WS 订阅与推送(单通道,2026-08-26 新增)
|
|
- **单通道 `/ws`**,消息按 `type` 区分(`whole` 全量tick、`pong` 心跳)
|
|
- **订阅/退订走 HTTP**: `POST /data/subscribe`(type 在 body)、`POST /data/unsubscribe`(sub_id)
|
|
- **sub_id**: snowflake 类唯一 ID(时间戳 << 12 | 序列号)
|
|
- **无 client_id**(单客户端设计): 推送发到唯一 WS 连接
|
|
- **推送过滤**: 订阅了什么类型才推什么;没订阅的不推
|
|
- **不做 prime 推送**: 客户端需要快照时调 `GET /data/tick`(透传 get_full_tick)
|
|
- **订阅但无 WS 连接**: QMT 推送继续,数据被静默丢弃(不缓冲/补推)
|
|
- 详细设计见 `docs/迭代记录/WS单通道推送设计.md`
|
|
|
|
### 3.7 接口设计规范
|
|
- 错误格式统一: `{"detail": "..."}` + 4xx/5xx
|
|
- 返回格式: 查询类 `{"ok": true, "data": [...]}`,K线 `{"code","period","count","data"}`
|
|
- fields 参数: **不传=默认完整 OHLCV**,传了=按白名单过滤(不要默认 close)
|
|
- 旧路径兼容: `/kline` → `/data/kline` 等前缀别名保留
|
|
- **必填参数校验**: code 等必填参数**在代码转换前先校验原始值非空**,返回 400 `{"detail":"code required"}`
|
|
- 踩坑: `_code_to_full("")` 返回 `".SH"`,会绕过 `if not code` 校验 → 必须校验原始参数
|
|
|
|
### 3.8 MCP 端点(2026-08-27 新增)
|
|
- **`POST /mcp`**: MCP(Model Context Protocol)端点,JSON-RPC 2.0 应用协议,与 HTTP/WS 同端口 8610
|
|
- **为什么桥内嵌**: QMT 内置 Python 3.6.8 纯标准库,官方 mcp SDK 要求 **Python ≥ 3.10**;桥手写 JSON-RPC(仅 json/socket/threading),复用现有 adapter
|
|
- **实现范围**: `initialize` / `notifications/initialized` / `ping` / `tools/list` / `tools/call`,暂不做 resources/prompts/SSE 流式
|
|
- **工具与 HTTP 1:1**: 9 个工具(qmt_kline/quote/tick/instrument/trading_dates/positions/asset/orders/trades),直接复用 bridge_data_adapter → QMT 线程约束与 HTTP 完全一致
|
|
- **响应规则**: JSON-RPC 错误走 HTTP 200(规范要求);notification 走 HTTP 202 空 body;非法 body 走 400 `{"detail":...}`
|
|
- **鉴权/CORS**: 鉴权复用 TOKEN(X-Token 头);响应带 CORS 头,`OPTIONS /mcp` 返回 204 预检
|
|
- **热重载**: `qmt_strategy_entry.py` 的 `_BRIDGE_MODULES` 已含 `bridge_mcp_server`
|
|
- 详细设计见 `docs/迭代记录/MCP服务.md`
|
|
|
|
---
|
|
|
|
## 四、部署规范
|
|
|
|
### 4.1 部署流程
|
|
1. 修改 `src/` 下代码
|
|
2. **编译 + ASCII 检查**(见下文验证清单)
|
|
3. 复制到 `C:\Programs\GJQMT_BIG\python\`(可用 `deploy_elevated.bat` 或直接 Copy-Item + danger-full-access)
|
|
4. QMT 策略编辑器: **停止 → 运行**(热重载生效,不用重启 QMT)
|
|
- ⚠️ 若端口被残留占用(监听但不响应),需**重启 QMT 进程**彻底释放
|
|
|
|
### 4.2 热重载机制
|
|
- `qmt_strategy_entry.py` 顶部 `_reload_bridge()`: 重跑时先 stop 旧桥(释放端口) → 清 `sys.modules` 缓存 → 重新 import
|
|
- **前提**: QMT 编辑器里粘贴的是**新版 `qmt_strategy_entry.py` 内容**(不是旧版转发器)
|
|
- QMT 进程不退出,模块会被缓存;不清理则改代码不生效
|
|
- 新模块加入时需同步更新 `_BRIDGE_MODULES` 热重载列表(如 bridge_subscription)
|
|
|
|
### 4.3 部署验证清单
|
|
```
|
|
1. python -c "import py_compile; py_compile.compile(f, doraise=True)" # 编译
|
|
2. 统计非 ASCII 字节 = 0 # 编码
|
|
3. python tests/test_trade.py 等 mock 测试 # 本地 mock
|
|
4. QMT 停止→运行 → curl 各端点 # 真实验证
|
|
```
|
|
|
|
### 4.4 沙箱/权限
|
|
- 当前会话(pwsh)默认 `workspace-write`:**不能写 `C:\Programs\`**(连 cmd.exe 都拒绝)
|
|
- 需要写 QMT 目录时用 `sandbox_permissions: danger-full-access`(会弹审批,用户批准)
|
|
- 外网访问(api.apifox.com 等)同样需要 danger-full-access
|
|
- 沙箱 DNS 白名单限制部分域名 → 可用**UAC 提权脚本在沙箱外执行**(如 tools/fetch_*.ps1)
|
|
|
|
---
|
|
|
|
## 五、接口文档规范
|
|
|
|
### 5.1 唯一事实来源
|
|
- **`docs/api_spec/openapi.yaml`** 是接口规范的唯一权威,中文,主动维护
|
|
- 每次改接口 → 同步更新 YAML + 重新生成 openapi.json
|
|
|
|
### 5.2 桥提供 OpenAPI 端点(2026-08-25 新增)
|
|
- 桥提供 `GET /openapi.yaml` 和 `GET /openapi.json`(从 QMT 部署目录读取)
|
|
- 用途: **Apifox URL 同步**(Apifox 主动拉取)
|
|
- 更新 openapi 文件后需**部署到 QMT python 目录**(桥每次请求时读文件,无需重启)
|
|
|
|
### 5.3 导入 Apifox
|
|
- 项目: QMT_HTTP_BRIDGE (ID **8742354**)
|
|
- 手动导入: Apifox → 导入数据 → OpenAPI/Swagger → 文件导入 `docs/api_spec/openapi.yaml`
|
|
- 或 URL 导入: `http://<桥IP>:8610/openapi.yaml`(需 Apifox 能访问到桥,可能要内网穿透)
|
|
- **开放 API 自动推送未打通**: `POST /v1/projects/8742354/import-openapi`
|
|
- 沙箱网络代理对 Python urllib 伪造 201 空响应(`text/html` + 0 字节,请求未真正到达)
|
|
- PowerShell 直连返回 422(body 格式未确认,错误体为空)
|
|
- 结论: 以**手动导入 / URL 同步**为可靠路径; 自动推送留待网络环境/格式确认后再打通
|
|
|
|
### 5.4 OpenAPI 内容规范
|
|
- info.title 用中文("QMT桥接服务接口")
|
|
- 每个接口: 中文 summary + 中文 description + 参数表 + 实测响应示例
|
|
- 参数默认值必须与代码一致(如 fields 默认 OHLCV)
|
|
- 示例数据用真实实测值(600519/600900 等)
|
|
|
|
---
|
|
|
|
## 六、关键已知信息(避免重复踩坑)
|
|
|
|
| 主题 | 结论 |
|
|
|------|------|
|
|
| 涨跌停价 | `ContextInfo.get_instrument_detail` 的 `UpStopPrice`/`DownStopPrice`,**不在** get_full_tick 里 |
|
|
| 交易日历 | 大 QMT 签名 `get_trading_dates(stockcode, start, end, count, period)`;实现兼容 (SH,start,end,count) → (start,end) → (SH,start,end) |
|
|
| 账号机制 | **一个策略实例=一个账号**;`set_account` 订阅回调;桥单账号,HTTP `account` 参数冗余 |
|
|
| 市场活跃判断 | sfgrid 用"120秒无行情"看门狗;桥可用 trading_dates + 时间判断 |
|
|
| passorder 参数 | 第10参数 userOrderId = 订单 m_strRemark(sfgrid 用它存 "类型,网格,代码") |
|
|
| sfgrid 策略名 | 固定 `"SFGRID"`;remark 格式 `"{类型},{网格索引},{股票代码}"` |
|
|
| 订单状态码 | 54=已撤, 57=废单; `status=active` 排除这两个 |
|
|
| 下单必须带 | strategyName + userOrderId(remark),否则无法回查归属 |
|
|
| 全量tick订阅 | `subscribe_whole_quote` 增量推送 `{code: tick_dict}`;订阅走 HTTP,推送走 WS `/ws` |
|
|
| 参考项目 | `reference/xtquant_big_convert` 有大量实盘验证过的坑(务必参考) |
|
|
|
|
---
|
|
|
|
## 七、待办/路线图
|
|
|
|
- [ ] `/trade/order` 下单(passorder, 队列→adjust, 透传 strategyName + userOrderId)
|
|
- [ ] `/trade/cancel` 撤单(cancel, 队列→adjust)
|
|
- [ ] `/trade/order/status` 下单状态查询
|
|
- [ ] WS 推送 trade_result(账号交易通知: 成交/订单/持仓/资金,预留命名未实现)
|
|
- [ ] `/trade/orders` 按 remark 过滤(暂缓,依赖下单后 remark 可见)
|
|
- [ ] MCP `qmt_order` / `qmt_cancel`(依赖交易接口迭代)
|
|
- [ ] MCP SSE 流式传输(长连接占线程池 worker,参考 WS 处理;默认不做)
|
|
- [ ] sfgrid 迁移到桥(替换 qmt_real.py 的 SDK 直连)
|
|
|
|
---
|
|
|
|
## 八、会话历史关键决策(留痕)
|
|
|
|
| 日期 | 决策 |
|
|
|------|------|
|
|
| 2026-08-20 | trade 查询线程问题修复: get_trade_detail_data 不能在 HTTP 线程调 → 策略线程缓存 |
|
|
| 2026-08-20 | 新增 /data/instrument(涨跌停价,get_instrument_detail) |
|
|
| 2026-08-20 | 新增 /data/calendar/trading_dates |
|
|
| 2026-08-20 | K线修复: 先下载后查询 + fields 完整列表 + iat 取值 + 默认完整 OHLCV |
|
|
| 2026-08-21 | 热重载: qmt_strategy_entry 清模块缓存 + 释放端口,改代码只需停止→运行 |
|
|
| 2026-08-21 | /trade/orders 过滤: code(客户端) + status(客户端) + strategy_name(服务端第4参数) |
|
|
| 2026-08-21 | 移除代码内 OpenAPI/Swagger,改用 docs/api_spec/openapi.yaml + Apifox 导入 |
|
|
| 2026-08-21 | Apifox 自动推送未打通(沙箱代理伪造201/PowerShell 422),定为手动导入;文档已验证中文接口可导入 |
|
|
| 2026-08-21 | code 必填校验修复: 转换前先校验原始参数(kline/quote/instrument 缺 code 返回400) |
|
|
| 2026-08-21 | trading_dates 签名修复: 大QMT 需 (SH,start,end,count),兼容多种签名 |
|
|
| 2026-08-21 | 账号机制确认: QMT 一个策略实例=一个账号,桥单账号,HTTP account 参数冗余 |
|
|
| 2026-08-21 | 移除 HTTP 接口的 account/account_type 冗余参数,OpenAPI 同步更新 |
|
|
| 2026-08-25 | 桥新增 /openapi.yaml + /openapi.json 端点(供 Apifox URL 同步) |
|
|
| 2026-08-26 | WS 单通道推送一期完成: 单通道 /ws + 全量tick订阅(whole),HTTP 订阅/退订,/data/tick 透传 |
|
|
| 2026-08-26 | 桥端口迁移: 8617 → 8610(残留监听问题,需重启 QMT 进程释放) |
|
|
| 2026-08-26 | 持仓查询接口增强: GET /trade/positions 返回全部持仓(语义化字段 stock_code/volume/available/avg_price/market_value/profit + summary),不做过滤;详见 docs/迭代记录/持仓查询接口.md |
|
|
| 2026-08-27 | MCP 端点一期完成: POST /mcp(JSON-RPC 2.0,initialize/ping/tools/list/tools/call),9 个工具与 HTTP 接口 1:1,复用 adapter(QMT 线程约束一致);详见 docs/迭代记录/MCP服务.md |
|
|
|
|
---
|
|
|
|
## 九、迭代规范
|
|
|
|
### 9.1 迭代记录管理
|
|
|
|
**每一次迭代的设计文档/记录都放在 `docs/迭代记录/` 目录下**,并在 `docs/迭代记录/index.md` 中登记索引。
|
|
|
|
### 9.2 迭代记录流程
|
|
|
|
1. **迭代启动**: 讨论设计(遵循"讨论什么记什么"原则,未讨论的不写死)
|
|
2. **设计定稿**: 迭代设计文档标记为"设计定稿",移入 `docs/迭代记录/`
|
|
3. **实现完成**: 设计文档状态改为"✅ 已完成",补充实现记录
|
|
4. **登记索引**: 在 `docs/迭代记录/index.md` 添加一行: 迭代名、日期、状态、关联文档
|
|
|
|
### 9.3 迭代记录文档模板
|
|
|
|
每次迭代在 `docs/迭代记录/` 下新建 `<迭代名>.md`,建议结构:
|
|
|
|
```markdown
|
|
# <迭代名>
|
|
|
|
> 版本: x.y (日期)
|
|
> 状态: 讨论中 / 设计定稿 / ✅ 已完成
|
|
> 关联: docs/设计/整体设计方案_v3.md 相关章节
|
|
> 本次迭代: <一句话范围>
|
|
|
|
## 一、已确认的设计决策
|
|
## 二、实现记录(完成后补)
|
|
## 三、待讨论事项(讨论中保留)
|
|
```
|
|
|
|
### 9.4 索引表(index.md)
|
|
|
|
`docs/迭代记录/index.md` 维护迭代索引表,每迭代一行:
|
|
|
|
| 迭代 | 日期 | 状态 | 关联文档 |
|
|
|------|------|------|---------|
|
|
| 迭代名 | 起止日期 | ✅ 已完成 / 讨论中 | 文档链接 |
|