17 KiB
17 KiB
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 接口不暴露
- 新接口设计不需要账号参数;如需多账号,部署多桥实例而非加参数
- 2026-08-21 已清理:
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 部署流程
- 修改
src/下代码 - 编译 + ASCII 检查(见下文验证清单)
- 复制到
C:\Programs\GJQMT_BIG\python\(可用deploy_elevated.bat或直接 Copy-Item + danger-full-access) - 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 同步为可靠路径; 自动推送留待网络环境/格式确认后再打通
- 沙箱网络代理对 Python urllib 伪造 201 空响应(
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 迭代记录流程
- 迭代启动: 讨论设计(遵循"讨论什么记什么"原则,未讨论的不写死)
- 设计定稿: 迭代设计文档标记为"设计定稿",移入
docs/迭代记录/ - 实现完成: 设计文档状态改为"✅ 已完成",补充实现记录
- 登记索引: 在
docs/迭代记录/index.md添加一行: 迭代名、日期、状态、关联文档
9.3 迭代记录文档模板
每次迭代在 docs/迭代记录/ 下新建 <迭代名>.md,建议结构:
# <迭代名>
> 版本: x.y (日期)
> 状态: 讨论中 / 设计定稿 / ✅ 已完成
> 关联: docs/设计/整体设计方案_v3.md 相关章节
> 本次迭代: <一句话范围>
## 一、已确认的设计决策
## 二、实现记录(完成后补)
## 三、待讨论事项(讨论中保留)
9.4 索引表(index.md)
docs/迭代记录/index.md 维护迭代索引表,每迭代一行:
| 迭代 | 日期 | 状态 | 关联文档 |
|---|---|---|---|
| 迭代名 | 起止日期 | ✅ 已完成 / 讨论中 | 文档链接 |