# 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` 维护迭代索引表,每迭代一行: | 迭代 | 日期 | 状态 | 关联文档 | |------|------|------|---------| | 迭代名 | 起止日期 | ✅ 已完成 / 讨论中 | 文档链接 |