chore: init qmt_bridge repo (HTTP+WS bridge, MCP endpoint, docs, references)

This commit is contained in:
Docker
2026-08-26 16:53:15 +08:00
commit 22a5b8ca04
210 changed files with 68176 additions and 0 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,253 @@
# QMT Bridge 接口清单 & sfgrid 满足度对照
> 更新日期: 2026-08-26
> 适用范围: `qmt_bridge`(大 QMT 内策略桥,端口 8610)
> 对照对象: `C:\Users\Docker\Development\sfgrid`(网格策略客户端,当前走 miniQMT xtquant SDK 直连)
> 接口规范权威来源: `docs/api_spec/openapi.yaml`(主动维护,可同步 Apifox
---
## 〇、背景与结论速览
- **sfgrid 当前接入方式**: `core/qmt_real.py` 直接封装 `xtquant` SDK`XtQuantTrader` + `xtdata`),**不是** HTTP 桥。
- **目标**: 让 sfgrid 可以改走 qmt_bridge(或桥为其提供等价能力)。
- **一句话结论**: 桥的**查询类接口已满足** sfgrid 的持仓/资金/委托/成交/行情/涨跌停价/全量tick需求;**交易闭环(下单、撤单、异步回报推送)尚未实现**,是 sfgrid 迁移的主要缺口。
---
## 一、当前桥已实现接口(✅ 12 个)
### 1.1 通用
#### GET /health —— 健康检查
- **功能**: 检查桥是否存活,返回运行模式、端口、当前资金账号、桥版本。
- **返回**:
```json
{
"status": "ok",
"connect_info": {"mode": "big_qmt", "http_port": 8610, "ws_port": 8610, "account": "8882874667"},
"last_error": null,
"bridge_version": "2",
"hot_reload": true
}
```
### 1.2 data 命名空间(行情/数据)
#### GET /data/kline —— K线查询
- **功能**: 查询历史/实时 K 线,支持周期合成,按字段筛选。**查询前自动先下载基础周期数据**。
- **参数** (Query):
| 参数 | 类型 | 默认 | 说明 |
|------|------|------|------|
| code | str | 必填 | 股票代码,`600519.SH` 或 `600519` |
| period | str | `1d` | `tick/1m/5m/15m/30m/60m/1h/1d/1w/1mon` |
| start | str | `""` | 起始 `YYYYMMDD` |
| end | str | `""` | 结束 `YYYYMMDD` |
| count | int | `-1` | 条数,`-1`=全部;与 start/end 互斥 |
| fields | str | `open,high,low,close,volume,amount` | 逗号分隔,**不传=返回完整 OHLCV**;传了则只返回指定字段(白名单: `open,high,low,close,volume,amount,settle,openInterest` |
- **返回**(实测 2026-08-21, 600900.SH:
```json
{
"code": "600900.SH", "period": "1d", "count": 14,
"data": [{"time": "20260803", "open": 28.99, "high": 29.2, "low": 28.75,
"close": 29.05, "volume": 1018010.0, "amount": 2949045835.0}, ...]
}
```
- **底层**: 先 `download_history_data`(合成周期自动下载基础周期: 3m←1m, 15m/30m/60m←5m, 1w/1mon←1d),再 `ContextInfo.get_market_data_ex`。
- **fields 行为**(实测): 不传→完整 OHLCV;`fields=close`→只 close`fields=open,high`→只 open/high。
#### GET /data/quote —— 实时行情快照(单票)
- **功能**: 查询指定股票的**完整实时快照**(最新价、开高低收、昨收、成交量额、五档盘口等)。
- **参数** (Query): `code`(必填)。
- **底层**: `ContextInfo.get_full_tick([code])`。sfgrid 的 `getLastPrice()` 就是从 `get_full_tick` 里取 `lastPrice` 字段。
#### GET /data/tick —— 全推 tick 快照(批量)【2026-08-26 新增】
- **功能**: 透传 `get_full_tick(codes)`,返回多个代码的最新分笔快照。客户端按需主动拉取(WS 只推增量,不做 prime 推送)。
- **参数** (Query): `codes`(必填,逗号分隔,如 `600000.SH,000001.SZ`)。
- **返回**:
```json
{"ok": true, "data": {"600000.SH": {"timetag": "20260826 09:25:02", "lastPrice": 9.13, ...}}}
```
- **底层**: `ContextInfo.get_full_tick(codes)`。
#### GET /data/instrument —— 合约详细信息(含涨跌停价)
- **功能**: 查询合约详细信息,**含涨跌停价**(sfgrid `dailyUpStop`/`dailyDownStop` 所需)。
- **参数** (Query): `code`(必填)。
- **返回**(实测 600519.SH: `ExchangeID/InstrumentID/InstrumentName/OpenDate/PreClose/UpStopPrice/DownStopPrice/PriceTick/...`
- **底层**: `ContextInfo.get_instrument_detail(code)`。
#### GET /data/calendar/trading_dates —— 交易日历
- **功能**: 返回 [start, end] 区间交易日列表。可用于市场活跃判断。
- **参数** (Query): `start`、`end`YYYYMMDD,可空)。
- **返回**: `{"market":"SH","count":N,"dates":["20260803",...]}`
- **底层**: `ContextInfo.get_trading_dates`(大 QMT 签名 `(stockcode,start,end,count)`)。
#### 旧路径兼容(别名)
- `/kline` → `/data/kline`、`/quote` → `/data/quote`(保留,旧脚本零改动)。
### 1.3 订阅与推送(WS 单通道)【2026-08-26 新增】
> 设计详见 `docs/迭代记录/WS单通道推送设计.md`(已完成)。单通道 `/ws`,消息按 `type` 区分。
#### POST /data/subscribe —— 订阅数据(全量tick)
- **功能**: 订阅数据推送类型。当前支持 `whole`(全量tick增量推送)。
- **请求体**:
```json
{"type": "whole", "codes": ["SH", "SZ"]}
```
- **返回**: `{"ok": true, "sub_id": 7322507885117440}`snowflake 类唯一 ID
- **推送**: 订阅成功后,WS 连接 `ws://host:8610/ws` 收到增量推送:
```json
{"type": "whole", "data": {"600000.SH": {"lastPrice": 9.13, ...}}}
```
- **底层**: `ContextInfo.subscribe_whole_quote(codes, callback)`。
#### POST /data/unsubscribe —— 退订数据
- **功能**: 按 sub_id 退订某类型订阅。
- **请求体**: `{"sub_id": 7322507885117440}`
- **返回**: `{"ok": true}`
#### WS 端点 /ws —— 单通道推送
- **功能**: 客户端建立 WS 连接接收推送(心跳 30s ping/pong)。
- **推送消息**: `{"type":"whole","data":{code:tick_dict}}`(增量,只含变化品种)。
- **账号交易通知**trade_result/order_update/position_update/asset_update: 预留命名,**尚未实现**。
### 1.4 trade 命名空间(交易查询)
> 数据来自 QMT `get_trade_detail_data`**由策略线程(adjust/handlebar)定时刷新缓存**(当前 300ms),HTTP 线程只读缓存。账号为桥绑定账号(单账号策略),**无需 account/account_type 参数**。
#### GET /trade/positions —— 持仓查询【2026-08-26 增强】
- **功能**: 当前账号**全部持仓**,每行 = 语义化字段(xt 风格,sfgrid 可直接消费)+ 原始 `m_*` 字段超集 + 汇总 summary。
- **不做过滤**: 不提供 code/status 等过滤参数,客户端从全量中自取(sfgrid `getStockPosition` 本就以全部持仓为缓存)。
- **返回**:
```json
{"ok": true, "data": {"positions": [{"stock_code": "600519.SH", "stock_name": "贵州茅台",
"volume": 100, "available": 100, "frozen_volume": 0, "on_road_volume": 0,
"yesterday_volume": 0, "avg_price": 1500.0, "price": 1510.0,
"market_value": 151000.0, "open_price": 1500.0, "profit": 1000.0,
"profit_pct": 0.67, "direction": 48, "m_strInstrumentID": "600519", ...}],
"summary": {"count": 1, "total_market_value": 151000.0, "total_profit": 1000.0}}}
```
- **字段映射**: `m_strInstrumentID` → `stock_code`(补全后缀)、`m_nVolume` → `volume`、`m_nCanUseVolume` → `available`、`m_dOpenPrice` → `avg_price`/`open_price`、`m_dFloatProfit` → `profit`、市值/盈亏缺失时计算兜底。
- **底层**: 策略线程缓存(`get_trade_detail_data('POSITION')`300ms 刷新),HTTP 线程只读。
#### GET /trade/asset —— 资金/资产查询
- **返回**: `{"ok":true,"data":[{m_dBalance, m_dAvailable, m_dAssetBalance, m_dFrozenCash, ...}]}`
#### GET /trade/orders —— 委托查询(支持过滤)
- **功能**: 当日委托,支持三类过滤。
- **参数** (Query):
| 参数 | 说明 |
|------|------|
| code | 客户端按代码过滤(600519 或 600519.SH|
| status | `active`(排除已撤54/废单57)或数字状态码 |
| strategy_name | **服务端**按 passorder 策略名过滤(get_trade_detail_data 第4参数)|
- **返回**: `{"ok":true,"data":[{m_strOrderSysID, m_strInstrumentID, m_nOrderStatus, m_nVolumeTraded, m_dLimitPrice, ...}]}`
#### GET /trade/trades —— 成交查询
- **返回**: `{"ok":true,"data":[{m_strTradeID, m_strOrderSysID, m_strInstrumentID, m_dPrice, m_nVolume, m_strTradeTime, ...}]}`
---
## 二、sfgrid 实际需要的能力(代码调用点)
sfgrid 通过 `core/qmt_real.py` 的 `qmtv` 单例调用以下方法:
| # | qmtv 方法 | sfgrid 用途 | 调用位置 |
|---|-----------|------------|---------|
| 1 | `init_qmtv()` + `connect()` | 初始化并连接 QMT | `core/ui/tinker/app.py`、`core/ui/flet/app_v2.py` |
| 2 | `getAllPositions()` | 全部持仓展示/计算 | `core/ui/*/data_store.py`、`app_v2.py` |
| 3 | `getStockPosition(code)` | 单票持仓查询 | 同上 |
| 4 | `queryTodayOrders()` | 当日委托列表 | 同上 |
| 5 | `queryTodayTrades()` | 当日成交列表 | 同上 |
| 6 | `queryPendingOrder(code, tag)` | **按 strategy_name 过滤未成交挂单**(网格恢复核心) | `core/sfgrid/sfgrid_strategy.py` |
| 7 | `orderAsync(code, vol, type, price, priceType, remark, strategy_name)` | 下单(带 remark + strategy_name | 同上 |
| 8 | `xt_trader.cancel_order_stock_async(account, order_id)` | 撤单(用 order_id | 同上 |
| 9 | `dailyUpStop(code)` / `dailyDownStop(code)` | 涨跌停价(网格边界校验) | 同上 |
| 10 | `getLastPrice(code)` | 最新价兜底 | `data_store.py`、`app_v2.py` |
| 11 | `getInstrumentName(code)` | 股票名称 | 同上 |
| 12 | `isMarketActive`(属性) | 市场是否活跃(交易门禁) | `sfgrid_strategy.py` |
| 13 | `subscribe_whole_quote(['SH','SZ'], cb)` + `_on_market_data` | 全市场 tick 推送 | `core/qmt_real.py` |
| 14 | 回调 `on_order_stock_async_response` / `on_stock_trade` / `on_order_error` | 下单回报/成交/失败**异步推送** | `sfgrid_strategy.py` 事件订阅 |
**sfgrid 网格逻辑对桥的关键要求**:
1. 下单必须透传 **`remark`**(如 `BUY_3`)和 **`strategy_name`** —— 用于关联网格档位、恢复挂单。
2. 挂单查询必须支持**按 strategy_name 过滤**`queryPendingOrder(code, tag)` 等价)。
3. 撤单依赖 **order_id**(订单系统号),不是 remark。
4. 回报必须**异步推送**(下单成功/成交/失败),驱动网格状态机。
---
## 三、对照结论:已满足 / 未满足
### ✅ 已满足(桥可直接支撑)
| sfgrid 需求 | 桥接口 | 备注 |
|-------------|--------|------|
| 全部持仓 | `GET /trade/positions` | **已增强(2026-08-26**:语义化字段(stock_code/volume/available/avg_price/market_value/profit...+ summarysfgrid 无需再映射 |
| 资金资产 | `GET /trade/asset` | 同上 |
| 当日委托 | `GET /trade/orders` | 同上 |
| 当日成交 | `GET /trade/trades` | 同上 |
| 最新价 | `GET /data/quote` 或 `GET /data/tick`(取 `lastPrice` | 返回完整快照(含五档)|
| K线 | `GET /data/kline` | 桥已可用 |
| 涨跌停价 | `GET /data/instrument``UpStopPrice`/`DownStopPrice`| 已实现(实测 600519 → 1438.67/1177.09|
| 股票名称 | `GET /data/instrument``InstrumentName`| 已实现 |
| 全市场 tick 订阅 | `POST /data/subscribe` + WS `/ws``type=whole`| **2026-08-26 已实现**,增量推送 |
### ⚠️ 部分满足(可用但缺细节)
| sfgrid 需求 | 现状 | 缺口 |
|-------------|------|------|
| 市场活跃 `isMarketActive` | 无对应概念 | 桥没有暴露(可用 trading_dates + 时间判断)|
### ❌ 未满足(sfgrid 迁移必须补)
| sfgrid 需求 | 缺失的桥能力 | 建议实现 |
|-------------|-------------|---------|
| 下单 `orderAsync` | **`POST /trade/order`** | passorder 走 **adjust() 队列**(策略线程),透传 remark/strategy_name/价格类型 |
| 撤单 `cancel_order_stock_async` | **`POST /trade/cancel`** | cancel(队列→adjust),按 order_id 撤单 |
| 异步回报推送 | WS 只有 `whole`(行情)| WS 推送 `trade_result`submitted/order_update/dealt/order_error),账号交易通知预留未实现 |
| 下单状态查询(轮询兜底) | 无 | `GET /trade/order/status?rid=...` |
---
## 四、建议的 sfgrid → 桥 接口映射(未来实现)
| sfgrid 调用 | 桥端点(建议) | 说明 |
|-------------|---------------|------|
| `getAllPositions()` | `GET /trade/positions` | ✅ 已实现(语义化字段 + summary,直接可用)|
| `getStockPosition(code)` | `GET /trade/positions` 后客户端按 code 取 | ✅ 已实现(桥返回全部持仓,客户端过滤即可;已确认不做服务端过滤)|
| `queryTodayOrders()` | `GET /trade/orders` | ✅ 已实现 |
| `queryTodayTrades()` | `GET /trade/trades` | ✅ 已实现 |
| `queryPendingOrder(code, tag)` | `GET /trade/orders?strategy_name=xxx&status=active` | ✅ 已实现(服务端 strategy_name 过滤)|
| `orderAsync(...)` | `POST /trade/order` | **需新增**body 含 code/volume/opType/price/prType/remark/strategyName |
| `cancel(...)` | `POST /trade/cancel` | **需新增**body 含 orderId |
| `dailyUpStop/DownStop` | `GET /data/instrument?code=` | ✅ 已实现 |
| `getLastPrice(code)` | `GET /data/quote?code=` 或 `GET /data/tick?codes=` | ✅ 已实现 |
| `getInstrumentName(code)` | `GET /data/instrument?code=` | ✅ 已实现 |
| `subscribe_whole_quote` | `POST /data/subscribe` + WS `/ws` | ✅ 已实现(type=whole 增量推送)|
| 回报推送 | WS `trade_result`(账号通知)| **需实现**(预留命名)|
| `isMarketActive` | `GET /data/calendar/trading_dates` + 时间判断 | 建议扩展 |
---
## 五、官方接口依据(涨跌停价数据来源)
**涨跌停价不在行情快照里,而在合约详细信息里。**
- 官方接口: `ContextInfo.get_instrument_detail(code)`(根据代码获取合约详细信息)
- 涨跌停字段: `UpStopPrice`(涨停)/ `DownStopPrice`(跌停)
- 其他常用字段: `InstrumentName`、`OpenDate` 等
**对桥的启示**: `GET /data/instrument` 已实现,透传 `UpStopPrice`/`DownStopPrice`,同时满足 sfgrid 的 `dailyUpStop`/`dailyDownStop` 和 `getInstrumentName`。
---
## 六、桥的已知限制(文档留痕)
1. **线程约束**: `get_trade_detail_data`/`passorder`/`cancel` 只能在**策略线程**调用,HTTP worker 线程禁止直调(查询已用缓存/队列方案解决;下单/撤单将来必须走 adjust 队列)。`download_history_data`、`get_full_tick`、`subscribe_whole_quote` 在 HTTP 线程调用已验证可行。
2. **字段名**: 桥返回 QMT 原生 `m_*` 字段,sfgrid 消费时需自行映射(如 `m_strInstrumentID` → `stock_code`)。
3. **WS 推送**: 当前已实现 `whole`(全量tick增量);**账号交易通知**trade_result/order_update/position_update/asset_update)预留命名未实现。
4. **周期**: `/kline` 周期白名单 `tick/1m/5m/15m/30m/60m/1h/1d/1w/1mon`,合成周期自动先下载基础周期。
5. **单账号**: 桥是单账号(QMT 一个策略实例=一个账号),HTTP 接口无 account 参数;多账号=多桥实例(不同端口)。
6. **端口**: 桥监听 **8610**HTTP + WS 共端口)。
+167
View File
@@ -0,0 +1,167 @@
# xtquant_big_convert 实现原理调研(基于协议线索 + 官方API还原)
调研日期:2026-08-19
仓库:https://github.com/litaolemo/xtquant_big_convert
⚠️ 注意:本机出网受限(GitHub/镜像/Bing/Baidu 全部不通),无法抓取真实源码。
本文基于方案文档中的协议线索 + 迅投官方API文档(已核实)还原实现原理。
协议线索:请求 RPUSH bigqmt:rpc:queue:{账号} / 响应 BLPOP bigqmt:rpc:respq:{账号}:{reqId}
---
## 一、它解决什么问题
**目标**:让外部 Python 程序(没有 MiniQMT/XtQuantServer 权限)直接调用"大QMT"的交易和行情能力。
**背景约束**(官方文档核实):
- xtquant 外部库的 XtQuantTrader / xtdata **本质是和 MiniQMT 建立连接**(官方原文)
- 大QMT(完整交易端)不提供 MiniQMT 的 userdata_mini 连接通道 → 外部 xtquant 直连大QMT 失败
- 但大QMT 的策略编辑器(内置 Python 3.6)能跑策略,能调 passorder / get_trade_detail_data
**解法**:把"桥"跑在大QMT 进程内(策略里),外部程序通过 Redis 发 RPC 请求,桥在 QMT 策略线程里执行并回写结果。
---
## 二、核心架构
```
┌─ 外部Python进程(无QMT权限) ─┐ ┌─ Redis ─┐ ┌─ 大QMT进程内(策略) ─┐
│ 你的量化脚本 / sfgrid(TS) │ │ │ │ qmt_big_convert.py │
│ │ ①RPUSH │ │ ②BLPOP │ │
│ redis_client.rpush( ├───────►│ queue ├───────►│ bridge_loop(): │
│ 'bigqmt:rpc:queue:{acct}',│ │ {acct} │ │ 解析请求 │
│ json.dumps(req)) │ │ │ │ passorder(...) │
│ │ │ │ │ get_trade_detail │
│ ⑤BLPOP(respq) ←─────────────┼────────┤ respq │◄───────┤ ④RPUSH 结果 │
│ reqId → 结果 │ │ {acct}: │ │ │
└──────────────────────────────┘ │ {reqId} │ └──────────────────────┘
└─────────┘
```
**关键**:Redis 是"队列桥接"的中枢,不是 HTTP server。这正好绕开官方"QMT 内置 Python 不能多线程"的约束——QMT 侧不需要开监听线程,用一个循环/定时器消费队列即可。
---
## 三、Redis 协议设计(从 key 命名还原)
### 3.1 请求通道
```
Key: bigqmt:rpc:queue:{accountID}
Type: LIST
操作: 外部进程 RPUSH, QMT桥 BLPOP
Value: JSON 请求体
```
### 3.2 响应通道
```
Key: bigqmt:rpc:respq:{accountID}:{reqId}
Type: LIST
操作: QMT桥 RPUSH, 外部进程 BLPOP(带超时)
Value: JSON 响应体
```
### 3.3 推断的请求/响应结构(基于常见 RPC 桥设计 + 协议命名)
```jsonc
// 请求(外部 → QMT)
{
"reqId": "uuid或自增id", // 用于关联响应队列
"method": "order | cancel | query_position | query_asset | kline | ...",
"params": {
// order: {"code":"000001.SZ","opType":23,"volume":100,"priceType":5,"price":-1,...}
// kline: {"code":"600000.SH","period":"1d","count":100}
}
}
// 响应(QMT → 外部)
{
"reqId": "同上",
"code": 0, // 0成功, 非0错误
"data": {...} | [...], // 查询结果 / 下单结果
"msg": "error message"
}
```
---
## 四、QMT 侧桥程序的关键实现
### 4.1 入口:策略生命周期
```python
#coding:gbk
import json, redis # 或自实现极简RESP客户端
def init(ContextInfo):
ContextInfo.run_time("bridge", "200nMilliSecond", "2019-01-01 00:00:00")
# 或:
# 起一个 while 循环线程(若实测线程可用)
def bridge(ContextInfo):
# 定时/循环:BLPOP 队列
req = redis_client.blpop('bigqmt:rpc:queue:%s' % ACCOUNT, timeout=0.1)
if req:
result = dispatch(req) # 在策略上下文执行
redis_client.rpush('bigqmt:rpc:respq:%s:%s' % (ACCOUNT, req['reqId']),
json.dumps(result))
def dispatch(req):
method = req['method']
if method == 'order':
return passorder(req['params']) # 必须传 ContextInfo
elif method == 'cancel':
return cancel(...)
elif method == 'query':
return get_trade_detail_data(...)
elif method == 'kline':
return ContextInfo.get_market_data_ex(...)
```
### 4.2 必须处理的点(官方文档核实)
1. **passorder 无返回** → 下单结果要 order_callback/deal_callback 或轮询委托回写
2. **回调仅实盘模式 + 需 set_account** → init 里 `ContextInfo.set_account(account)`
3. **状态不能挂 ContextInfo**(会回滚) → 用模块级全局 class
4. **行情函数**:内置 Python 用 `ContextInfo.get_market_data_ex`(不能直接在 init 跑)
5. **定时器用 run_time**(官方无 adjust)
---
## 五、为什么它和你的 HTTP 桥方案是"同构"的
| 维度 | xtquant_big_convert | 你的 qmt_http_bridge 方案 |
|------|--------------------|--------------------------|
| 桥的位置 | 大QMT 策略内 | 大QMT 策略内 |
| 外部入口 | Redis LIST(RPUSH/BLPOP) | HTTP server |
| 队列桥接 | Redis 队列 | 内存 ORDER_QUEUE |
| 结果回写 | Redis respq + reqId | ORDER_RESULTS[rid] |
| QMT 侧执行 | 策略线程(定时消费) | adjust() 定时消费 |
| 解决的问题 | 外部无 xtquant 权限 | 外部脚本零改动复用8610 |
**本质相同**:都是"大QMT 进程内桥 + 队列 + 策略线程执行 + 结果回写"。
---
## 六、Redis vs HTTP 的取舍(你的方案该参考什么)
### xtquant_big_convert 用 Redis 的动机(推断)
- 官方"不能多线程"→ 无法开 HTTP server 线程
- Redis BLPOP 天然适配单线程轮询:一个 run_time 定时器就能消费
- 外部 redis-py 客户端成熟,不碰 QMT
### 但它引入的问题
1. **需要 Redis 服务** → 多一个运维组件(虽然轻量)
2. **QMT 内置 Python 3.6 是否有 redis 库?** → 大概率没有,要自己实现极简 RESP 客户端(纯 socket,~100行)
3. **BLPOP 阻塞语义** → 若桥循环里阻塞,会卡住策略主线程,需要 timeout 参数
### 对你 HTTP 桥方案的启示
- 如果实测 QMT 里**线程可用** → HTTP 方案可行,但必须加请求超时保护(慢客户端卡死策略)
- 如果**线程不可用** → 你的 HTTP 方案需要改成"单线程事件循环",或者借鉴 Redis 思路:QMT 侧只做定时轮询,HTTP 监听放外部进程(方向B)
---
## 七、待验证项(拿到真实源码后核对)
- [ ] 桥是否用 redis-py 还是自写 RESP 客户端
- [ ] 请求/响应 JSON 的确切字段名
- [ ] 下单结果如何回写(回调 or 轮询)
- [ ] 是否处理了 order_callback / deal_callback
- [ ] 定时器用的 run_time 还是线程
- [ ] 是否支持行情(kline)还是只做交易
- [ ] 是否内置了 token/鉴权
+190
View File
@@ -0,0 +1,190 @@
# QMT 官方接口核实报告(迅投知识库 dict.thinktrader.net)
核实日期:2026-08-19
资料来源:迅投官方知识库 http://dict.thinktrader.net(所有页面已存为 docs_ref/*.txt 供查阅)
---
## 一、结论速览
| # | 方案假设 | 官方文档核实结果 | 影响 |
|---|---------|----------------|------|
| 1 | `adjust()` 是 QMT 定时回调 | ❌ **不存在**。官方定时回调是 `ContextInfo.run_time(funcName, period, startTime)`(支持 `500nMilliSecond`)或新版 `ContextInfo.schedule_run` | 骨架代码要改 |
| 2 | HTTP 线程(daemon)+ 队列桥接 | ❌ **官方明确:内置 Python 无法使用多线程和多进程,所有策略在同一线程执行** | 方案核心架构要重新设计 |
| 3 | `xtdata.get_market_data_ex` 任意线程直调 | ⚠️ **xtdata 模块主动获取接口是 `get_market_data`,不是 `get_market_data_ex`**(后者是 ContextInfo 方法)。且 xtdata 文档写明"本质是和 MiniQmt 建立连接" | 函数名要改;能否连大 QMT 需实测 |
| 4 | `passorder` 下单 → 结果回写 | ✅ 确认 `passorder(opType, orderType, accountid, orderCode, prType, price, volume, strategyName, quickTrade, userOrderId, ContextInfo)`,**返回无**,真实状态靠 `order_callback`/`deal_callback` 主推(仅实盘模式生效,需先 `set_account`) | 你的判断正确,补充细节 |
| 5 | 下单须在策略线程执行 | ✅ 正确,必须。所有交易函数依赖 ContextInfo 和策略上下文 | 成立 |
| 6 | Python 3.6.8 | ✅ 官方确认:"内置了 3.6 版本的 python 运行环境" | 成立 |
| 7 | `# coding:gbk` | ✅ 官方明确要求 | 成立 |
| 8 | `cancel(orderId, accountId, ContextInfo)` | ⚠️ 实际签名是 `cancel(orderId, accountId, accountType, ContextInfo)`,**有 accountType 参数** | 修正 |
---
## 二、逐项核实详情
### 2.1 内置 Python 运行环境(快速开始页)
> "QMT 极速策略交易系统...内置了 **3.6 版本**的 python 运行环境,提供行情数据与交易下单两大核心功能。"
- 确认方案 4.3 的"Python 3.6.8"判断正确(官方口径 3.6)。
- 三种运行机制(官方明确):
1. 逐 K 线驱动 `handlebar`
2. 事件驱动 `subscribe`(订阅推送)
3. **定时任务 `run_time`(固定间隔触发)**
### 2.2 ⚠️ 关于线程(使用须知页)—— 最关键发现
> "**QMT中,python 无法使用多线程和多进程,而且所有策略都在同一线程中执行**,所以策略中应该尽量避免阻塞类的写法,否则会影响其他策略的执行。"
**这是对整个方案的颠覆性约束**:
- 方案假设的"init() 里启动 HTTP 服务线程(daemon)"**很可能直接失败**——多线程不可用
- 队列桥接(HTTP线程入队 → adjust 出队)的双线程前提不成立
- 需要找到"单线程事件循环"形态的替代架构(见下文第三节)
### 2.3 系统函数(系统函数页)—— 没有 adjust
- 官方系统函数列表:`ContextInfo`对象、`init``after_init``handlebar``ContextInfo.schedule_run``ContextInfo.cancel_schedule_run``ContextInfo.run_time``stop``ContextInfo.is_last_bar``ContextInfo.is_new_bar` 等。
- **全文没有 `adjust` 函数**。
- 替代:定时回调用
```python
# 500ms 定时器
ContextInfo.run_time("f", "500nMilliSecond", "2019-10-14 13:20:00")
# 或新版
ContextInfo.schedule_run(func, time_point, repeat_times, interval, name)
```
### 2.4 交易下单函数(交易函数页)
**passorder 完整签名**:
```python
passorder(opType, orderType, accountid, orderCode, prType, price, volume,
strategyName, quickTrade, userOrderId, ContextInfo)
```
- **返回:无**(不是布尔/错误码!下单是否成功只能靠回调或查询委托)
- opType:23=股票买入, 24=股票卖出(期货另有开平仓)
- orderType:1101=按股数(深沪通用), 1102=沪市按股, 1202=按金额(对账号组)
- prType:5=最新价, 11=限价, 14=模型价(跟价), 12=市价
- quickTrade:0=逐K线生效(默认), 1=最后一根K线立即, **2=调用即下单(不判bar状态)**
- userOrderId:用户自设委托ID,会进入 order/deal 对象的 **m_strRemark** 字段
- **编译器界面执行的下单函数不会产生实际委托**(必须策略交易界面运行)
- 下单真实结果靠 `get_trade_detail_data` 查询或回调主推
**cancel 实际签名**:
```python
cancel(orderId, accountId, accountType, ContextInfo)
```
- accountType: 'FUTURE'/'STOCK'/'CREDIT'/'HUGANGTONG'/'SHENGANGTONG'/'STOCK_OPTION'
- 返回 bool(是否发出撤单信号)
- 注意:撤单参数是 **orderId(委托号, m_strOrderSysID)**,不是 userOrderId
**其他下单**:algo_passorder(拆单)、smart_algo_passorder(VWAP等,需权限)、cancel_task/pause_task/resume_task(任务级)、get_basket/set_basket(篮子)。
### 2.5 交易查询函数(交易函数页)
```python
get_trade_detail_data(accountID, strAccountType, strDatatype[, strategyName])
```
- strDatatype: 'ACCOUNT'/'POSITION'/'POSITION_STATISTICS'/'ORDER'/'DEAL'/'TASK'
- 返回 list 对象,字段以 m_ 开头,如:
- 账号: `m_dBalance`(总资产) `m_dAssureAsset`(净资产) `m_dAvailable`(可用) `m_dInstrumentValue`(市值) `m_dPositionProfit`(盈亏)
- 持仓: `m_strInstrumentID` `m_nVolume`(持仓量) `m_nCanUseVolume`(可用) `m_dOpenPrice`(成本) `m_dInstrumentValue` `m_dPositionProfit`
- 委托: `m_strOrderSysID`(委托号) `m_nOrderStatus`(状态) `m_strRemark`(userOrderId) `m_nVolumeTraded`
- 成交: `m_strOrderSysID` `m_dPrice` `m_nVolume` `m_dTradeAmount`
**get_value_by_order_id(orderId, accountID, accountType)** 按委托号取委托/成交信息。
**get_last_order_id(accountID, accountType, 'order'/'deal')** 取最新委托号。
### 2.6 成交回报主推回调(回调函数页)
| 回调 | 签名 | 说明 |
|------|------|------|
| account_callback | (ContextInfo, accountInfo) | 资金账号状态变化 |
| task_callback | (ContextInfo, taskInfo) | 任务状态变化 |
| **order_callback** | (ContextInfo, orderInfo) | **委托状态变化主推** |
| **deal_callback** | (ContextInfo, dealInfo) | **成交状态变化主推** |
| position_callback | (ContextInfo, positionInfo) | 持仓状态变化 |
| orderError_callback | (ContextInfo, orderArgs, errMsg) | 异常下单 |
**重要提示(官方)**:
- 回调**仅在实盘运行模式下生效**
- 需要先在 init 里调用 **`ContextInfo.set_account(account)`**
- order 对象的 `m_strRemark` = userOrderId, `m_strOrderSysID` = 委托号
### 2.7 行情数据(行情函数页 + XtQuant 文档)
**内置 Python(策略内)**:
```python
ContextInfo.get_market_data_ex(fields=[], stock_code=[], period='1d',
start_time='', end_time='', count=-1, dividend_type='follow',
fill_data=True, subscribe=True)
```
- 返回 {stock_code: pd.DataFrame},index 为 time,columns 为 fields(open/high/low/close/volume/amount...)
- 注意:该函数**不建议在 init 中运行**(init 中只能取本地数据)
- 周期: 'tick' '1m' '5m' '15m' '30m' '1h' '1d' '1w' '1mon' '1q' '1hy' '1y'
- 其他: `ContextInfo.get_full_tick`(全推) `ContextInfo.subscribe_quote`(订阅) `ContextInfo.get_history_data`(不推荐) `ContextInfo.get_local_data`(不推荐)
**XtQuant 原生(外部 Python)**:
```python
xtdata.get_market_data(field_list=[], stock_list=[], period='1d',
start_time='', end_time='', count=-1, dividend_type='none', fill_data=True)
```
- **注意:xtdata 的主动获取接口是 `get_market_data`(不是 `get_market_data_ex`!)** `get_market_data_ex` 是内置 Python 的 ContextInfo 方法
- xtdata 返回:period 为 K 线时 {field: pd.DataFrame}(index 为 stock_list, columns 为 time_list)——与内置 Python 的返回结构**不同**(内置是 {stock: df},xtdata 是 {field: df})!
- **运行逻辑(官方原文)**: "xtdata提供和MiniQmt的交互接口,本质是**和MiniQmt建立连接**,由MiniQmt处理行情数据请求"
### 2.8 XtQuant 交易模块(xttrader 文档)
- 外部 Python 可用 `XtQuantTrader(path, session_id)` 连接,**文档明确绑定 MiniQMT**(`userdata_mini` 路径)
- 完整 API:order_stock(同步)/order_stock_async(异步)、cancel_order_stock、query_stock_asset/orders/trades/positions、回调 on_stock_order/on_stock_trade/on_order_error 等
- **这印证了方案判断:外部 xtquant 依赖 miniQMT,大 QMT 不适用,必须走桥策略**
### 2.9 ContextInfo 使用注意(使用须知页)
> "由于底层机制的限制,ContextInfo 中存储的变量值将会回滚...请避免在其中存储任何变量。"
- 官方推荐用 `class G(): pass; g = G()` 全局对象存状态
- 桥策略的队列/结果字典等必须用模块级全局变量,不能挂 ContextInfo
---
## 三、对方案的修正建议(基于官方文档)
### 3.1 核心架构问题:单线程约束
官方明确"python 无法使用多线程"。这意味着方案的核心机制(HTTP 线程收请求 + 队列 + adjust 线程执行)需要重新设计。可选方向:
**方向 A:单线程异步 HTTP(推荐验证)**
- 用 `asyncore`/`selectors` 实现非阻塞 HTTP,或
- 用 `BaseHTTPRequestHandler` 但**不开线程**——利用 run_time 定时器轮询处理 socket
- 但 HTTP 长连接/慢客户端会阻塞整个策略,风险仍在
**方向 B:桥只做"指令中继",HTTP 放外部(推荐)**
- 把 HTTP 服务放在**外部独立进程**(Linux 侧或 Windows 侧 Python 3.14),不占用 QMT 策略线程
- QMT 策略内只保留一个轻量"指令执行器":run_time 定时轮询一个本地指令源(文件/命名管道/端口)
- 外部 HTTP 桥收到 /order → 写指令文件/管道 → QMT 定时器取指令 → passorder → 回写结果文件
- 这样 QMT 内零线程、零阻塞,完全符合官方约束;HTTP 的并发/超时/安全都在外部处理
**方向 C:验证"线程是否真的不可用"**
- 官方文档说不可用,但社区有说法"能起线程但会阻塞主线程/不稳定"
- 建议在国金 GJQMT 上做个 10 分钟实测:init 里 `threading.Thread(target=...).start()` 看是否报错、是否影响 handlebar
- **如果实测线程能跑**,原方案(方向A变体)可保留,但必须加超时保护,避免慢请求卡死策略
### 3.2 函数修正清单
1. ~~adjust()~~ → `ContextInfo.run_time("on_bridge_timer", "500nMilliSecond", "...")`
2. ~~xtdata.get_market_data_ex~~ → 内置 Python 用 `ContextInfo.get_market_data_ex`;若用 xtdata 模块则 `xtdata.get_market_data`
3. ~~cancel(orderId, accountId, ContextInfo)~~ → `cancel(orderId, accountId, 'STOCK', ContextInfo)`
4. 下单状态:passorder **无返回** → 必须 `order_callback` + `deal_callback`(实盘模式 + set_account)或轮询 `get_trade_detail_data`
5. 状态存储:不能挂 ContextInfo → 用模块级全局 class 实例
### 3.3 必须实测的三个点(写码前)
1. **线程可用性**:init 里起 daemon 线程是否真的失败(决定方向A还是B)
2. **set_account**:实盘模式回调是否需要、如何配置账号
3. **xtdata 在大 QMT 内能否 import 并取数**:若大 QMT 无 miniQMT 的 userdata_mini 连接,xtdata 可能连不上——则行情一律走 `ContextInfo.get_market_data_ex`
### 3.4 仍成立的设计
- 队列桥接思路(若线程可用)、复权/周期参数、GBK 编码、8610 端口复用、脚本零改动目标
- passorder 必须在策略上下文执行(官方:下单函数需要 ContextInfo,且编译器环境不下单)
- 实盘模式登录才能收完整回报(官方:回调仅实盘生效)
+569
View File
@@ -0,0 +1,569 @@
# 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 |
+119
View File
@@ -0,0 +1,119 @@
# MCP 服务(桥内嵌 /mcp 端点)
> 版本: 1.1 (2026-08-27)
> 状态: ✅ 已完成
> 关联: docs/设计/整体设计方案_v3.md(单端口 8610 统一方案)、docs/api_spec/openapi.yaml
> 本次迭代: 在现有 qmt_bridge HTTP 服务器上新增 MCP(Model Context Protocol)端点,使 AI 助手(Claude Desktop / Cursor 等)可直接通过 MCP 调用桥的行情/交易能力
---
## 一、已确认的设计决策
### 1.1 结论:可行,采用"桥内嵌 /mcp 端点"方案
MCP 本质是 **JSON-RPC 2.0 应用协议**,官方两种传输:
- **stdio**:子进程管道,适合"MCP 客户端拉起独立进程"
- **Streamable HTTP**(2025-03-26 起取代旧 SSE):POST JSON-RPC,单请求-响应
桥已经手写 raw-socket HTTP 服务器,并已证明可在同一端口处理 WebSocket 长连接(Upgrade 分流)。因此:
- **选定方案 A(桥内嵌)**:新增 `bridge_mcp_server.py`,在 `POST /mcp` 上实现 JSON-RPC 2.0 分发,工具直接复用 `bridge_data_adapter` / `bridge_util`
- **放弃方案 B(独立 MCP 进程 + 官方 mcp SDK)**:官方 SDK 要求 **Python ≥ 3.10**,QMT 内置是 **Python 3.6.8 纯标准库**(不能装包);方案 B 需额外进程经 HTTP 8610 转发,多一跳且偏离项目"薄桥/零依赖"理念。
- **传输选型**:Streamable HTTP 的"非流式"子集(单请求-响应 `application/json`),覆盖主流 MCP 客户端。流式(SSE)留待后续。
### 1.2 约束核对(逐条过项目规范)
| 硬约束 | 影响 | 结论 |
|--------|------|------|
| 源码 GBK + 纯 ASCII | 新模块同样遵守,无新问题 | ✅ |
| QMT 函数线程约束 | MCP tools/call 跑在 HTTP worker 线程,全部复用现有 adapter 安全模式 | ✅ |
| 单账号 | MCP 工具无 account 参数,与 HTTP 一致 | ✅ |
| 单端口 8610 | `POST /mcp` 复用 8610,不新开端口 | ✅ |
| 热重载 | `qmt_strategy_entry.py``_BRIDGE_MODULES` 需加 `bridge_mcp_server` | ✅ |
| 部署 | `deploy_elevated.bat` 需加 `bridge_mcp_server.py` | ✅ |
### 1.3 协议实现范围(MCP 最小集)
| MCP 方法 | 实现 | 说明 |
|----------|------|------|
| `initialize` | ✅ | 返回协议版本 + `tools` capability,记录协商版本 |
| `notifications/initialized` | ✅ | 空响应(通知类) |
| `ping` | ✅ | 返回空 result |
| `tools/list` | ✅ | 从内置工具注册表返回 `{tools: [...]}`(含 inputSchema) |
| `tools/call` | ✅ | 校验参数 → 调 adapter → 返回 `{content:[{type:"text",text:json}]}` |
> 暂不实现: `resources/*`、`prompts/*`、`logging/*`、`completions/*`、流式(SSE)。纯工具型最小实现与主流客户端兼容。
### 1.4 首批工具(9 个,与 HTTP 接口 1:1)
| 工具名 | 对应 HTTP | 底层 |
|--------|-----------|------|
| `qmt_kline` | GET /data/kline | get_market_data_ex(worker 线程直调 ✅) |
| `qmt_quote` | GET /data/quote | get_full_tick |
| `qmt_tick` | GET /data/tick | get_full_tick(批量) |
| `qmt_instrument` | GET /data/instrument | get_instrument_detail |
| `qmt_trading_dates` | GET /data/calendar/trading_dates | get_trading_dates |
| `qmt_positions` | GET /trade/positions | 读 TRADE_CACHE(策略线程刷新 ✅) |
| `qmt_asset` | GET /trade/asset | 读 TRADE_CACHE |
| `qmt_orders` | GET /trade/orders | 读 TRADE_CACHE(+ 可选过滤) |
| `qmt_trades` | GET /trade/trades | 读 TRADE_CACHE |
后续迭代: `qmt_order`(下单,队列→adjust)、`qmt_cancel`(撤单)、`qmt_subscribe`(订阅)。
### 1.5 工具实现与 HTTP 共用一个内部函数层
为免工具逻辑与 HTTP handler 双写,`bridge_mcp_server.py` 内置"工具名 → 参数校验 → 结果 dict"的映射,直接 import `bridge_data_adapter` 的函数(get_market_data_ex / get_full_tick / get_instrument_detail / get_trading_dates / get_trade_data / get_positions),参数语义与 HTTP 完全一致(code 自动补后缀等)。
> 说明: 本次迭代不把 HTTP handler 重构为共用同一函数层(避免动既有稳定代码);但工具内部复用的 adapter 已是同一函数,行为一致。文档(openapi.yaml)仍是唯一事实来源,工具 schema 与之保持 1:1。
### 1.6 鉴权与 CORS
- 鉴权复用桥 TOKEN 机制: `X-Token` header 或 `?token=`(MCP 客户端如 Claude Desktop 支持自定义 header)。
- 响应带 CORS 头(`Access-Control-Allow-Origin: *` 等),兼容浏览器类 MCP 客户端 / 调试工具。
- `OPTIONS /mcp` 预检返回 204。
### 1.7 错误语义
- 非法 JSON / 非法 JSON-RPC: `-32700` parse error / `-32600` invalid request(HTTP 200, MCP 规范要求 JSON-RPC 错误也走 200)。
- 未知方法: `-32601` method not found。
- 工具参数错误: `-32602` invalid params(带 detail)。
- 工具执行异常: `-32603` internal error(带 detail)。
- 非 JSON-RPC body(如纯文本): 400 + `{"detail": "..."}`(桥统一错误格式)。
---
## 二、实现记录(已完成)
### 2.1 新增文件
- `src/bridge_mcp_server.py`: JSON-RPC 2.0 分发 + 9 个工具(复用 bridge_data_adapter)+ raw-socket 响应 + CORS 头
- `tests/test_mcp.py`: mock 测试(纯标准库,FakeDF 代替 pandas,本机即可跑),覆盖 initialize / tools/list / 9 个 tools/call / 错误路径 / notification 202 / OPTIONS 204
### 2.2 改动文件
- `src/bridge_http_server.py`: `POST /mcp` 路由 + `OPTIONS /mcp` CORS 预检(204)
- `src/qmt_strategy_entry.py`: `_BRIDGE_MODULES` 热重载列表加入 `bridge_mcp_server`
- `deploy_elevated.bat`: 部署文件列表加入 `bridge_mcp_server.py`(7 → 8 个 .py)
- `README.md` / `docs/项目规范.md`(新增 3.8 节)/ `docs/迭代记录/index.md`: 文档同步
### 2.3 实现要点
- **工具与 HTTP 1:1**: qmt_kline / qmt_quote / qmt_tick / qmt_instrument / qmt_trading_dates / qmt_positions / qmt_asset / qmt_orders / qmt_trades
- **协议**: `initialize`(协商 protocolVersion,回显 2025-06-18/2025-03-26/2024-11-05)、`notifications/initialized`(202 空 body)、`ping``tools/list``tools/call`
- **错误**: JSON-RPC 错误走 HTTP 200(parse -32700 / invalid request -32600 / method not found -32601 / invalid params -32602 / internal -32603);非法 body 走 400 `{"detail":...}`
- **鉴权/CORS**: 复用 TOKEN(X-Token);`OPTIONS /mcp` 预检 204;响应带 `Access-Control-Allow-Origin: *`
- **线程约束**: 全部工具经 bridge_data_adapter → 与 HTTP 端点同模式(行情 worker 直调,交易读缓存),无新 QMT 调用形态
### 2.4 验证结果
- 编译 + ASCII 检查: 全部通过(纯 ASCII,0 非 ASCII 字节)
- `python tests/test_mcp.py`: 17 项断言全部 OK(initialize、9 工具、错误路径、202、204)
- 回归: `test_subscription.py` / `test_sub_no_ws.py` / `test_ws_push.py` 全部通过(路由改动无影响)
---
## 三、待讨论事项
- 是否需要 SSE 流式(长连接占线程池 worker,参考 WS 的处理;默认不做)
- 是否需要 `qmt_order` / `qmt_cancel`(依赖交易接口后续迭代)
- 目标 MCP 客户端确认(Claude Desktop / Cursor / DSH 等)
@@ -0,0 +1,146 @@
# WS 单通道推送设计
> 版本: 3.1 (2026-08-25)
> 状态: ✅ **已完成**(2026-08-26 实现并真实验证通过)
> 关联: docs/设计/整体设计方案_v3.md 第五节(订阅与推送)
> 本次迭代: **通用接口定义 + 全量 tick 订阅(whole**
>
> 实现记录:
> - 新增 `src/bridge_subscription.py`(订阅管理:snowflake sub_id、订阅表、QMT 回调→WS 推送)
> - `bridge_http_server.py` 新增 `POST /data/subscribe`、`POST /data/unsubscribe`、`GET /data/tick`
> - `bridge_ws_server.py` 新增 `broadcast_all`
> - `bridge_main.py` stop 清理订阅;`qmt_strategy_entry.py` 热重载模块列表更新
> - 桥端口迁移至 **8610**
> - 真实验证通过:订阅 whole → WS 收到真实增量推送(单批最多 26673 代码)、tick 快照、退订正常
## 一、通道定义(已确认)
**只保留一个 WS 通道**,不再区分 data/trade 双通道。
- WS 端点:`ws://host:8610/ws`(单通道)
- 所有推送(行情数据 + 交易事件)走同一条连接
- 消息用 `type` 字段区分类型
- 客户端按 `type` 解析分发到不同处理逻辑
> 变更记录:v1 曾设计双通道(/data/ws + /trade/ws),讨论后改为单通道。
## 二、订阅流程(已确认)
**客户端通过 HTTP 提交订阅请求,订阅成功后建立 WS 连接接收推送。**
```
1. 客户端 → HTTP 提交订阅请求(含订阅类型 + 内容)
2. 服务端订阅成功,记录:订阅ID + 订阅类型
3. 服务端 → 客户端:返回 订阅ID
4. 客户端 建立 WS 连接 (ws://host:8610/ws)
5. 开始接收数据推送(按 type 分发)
6. 以后:客户端用 订阅ID 取消某类型的订阅
```
已确认要点:
- **订阅/退订统一走 HTTP**(请求-响应,返回明确结果)
- 服务端记录:**订阅ID + 订阅类型**(+ 订阅内容,如 codes)
- 订阅响应返回:**订阅ID**
- **去掉 client_id**(单客户端场景,客户端不需要自报身份)
- **统一按单客户端处理**:不搞多客户端归属路由
- **WS 连接:无 client_id**,推送发到唯一 WS 连接
- **按订阅过滤推送**:用户订阅了什么类型,才推送什么类型;
没订阅的数据类型不推送;退订某类型后该类型停止推送
- **不做 prime 推送**WS 只推增量(subscribe_whole_quote 回调数据);
客户端需要当前快照时,**主动调用透传 get_full_tick 的 HTTP 接口**(另设)
- **心跳机制:沿用现有**(服务端每 30s ping,客户端回 pong,已有实现)
- **退订用订阅ID**(HTTP 退订接口)
## 三、订阅接口 URI(已确认风格 B)
**一个订阅接口,type 在 body 里**
| 方法 | URI | body | 返回 |
|------|-----|------|------|
| POST | `/data/subscribe` | `{"type":"whole","codes":["SH","SZ"]}` | `{"ok":true,"sub_id":123}` |
| POST | `/data/unsubscribe` | `{"sub_id":123}` | `{"ok":true}` |
- 数据订阅类型未来扩展(whole/stock/kline)只需加 type,不改 URI
- **sub_id 生成:snowflake 类唯一 ID 算法**(时间戳 + 序列号,全局唯一、趋势递增)
- **错误格式:沿用项目规范 3.6**——`{"detail":"..."}` + 4xx/5xx
(如 400 unsupported type / codes required、404 sub_id not found、503 订阅失败)
## 四、两类推送(已确认架构)
| 类别 | 是否需要订阅 | 推送方式 |
|------|-------------|---------|
| **数据订阅**whole 全量tick 等)| 需要订阅,按订阅过滤 | 订阅了什么才推什么 |
| **账号交易通知**(成交/订单/持仓/资金)| **不需要订阅**,统一回传 | 账号级事件,连上 WS 即收 |
## 四之二、get_full_tick 透传接口(已确认新增)
WS 只推增量(subscribe_whole_quote 回调数据),**不做 prime 快照推送**;
客户端需要当前 tick 快照时,主动调用本接口:
| 方法 | URI | 参数 | 返回 |
|------|-----|------|------|
| GET | `/data/tick` | `codes=600000.SH,000001.SZ`(逗号分隔)| `{"ok":true,"data":{code: tick_dict}}` |
- 底层:透传 `ContextInfo.get_full_tick(codes)`(现有 `bridge_data_adapter.get_full_tick`
- 支持批量代码;与单票 `/data/quote` 并存(/data/tick 批量,/data/quote 保留兼容)
## 五、本次迭代范围(聚焦)
**本次实现**
- 通用接口定义:订阅接口(HTTP)、退订接口(HTTP)、WS 单通道、心跳
- **数据订阅:全量 ticktype=whole**——订阅/退订/增量推送
- **get_full_tick 透传接口**`/data/tick`,客户端按需拉快照)
**本次不做(预留,保留命名,不实现)**
- 数据订阅其他类型:单票行情(预留)
- 账号交易通知:成交/订单/持仓/资金回调推送
## 六、type 枚举(初步定义)
### 数据订阅类(按订阅推送)
| type | 含义 | 本次 |
|------|------|------|
| `whole` | 全量 tick(增量推送,只含变化的品种)| ✅ 实现 |
### 账号交易通知类(统一回传,本次不实现)
| type | 含义 | 触发 |
|------|------|------|
| `trade_result` | 成交回报 | deal_callback |
| `order_update` | 订单状态 | order_callback |
| `position_update` | 持仓变更 | position_callback |
| `asset_update` | 资金变更 | account_callback |
### 通用
| type | 含义 |
|------|------|
| `pong` | 心跳响应(现有)|
> 已确认:**不加** `subscribed`/`unsubscribed`HTTP 响应已确认订阅结果);
> **不加** `error`(订阅期错误由 HTTP 4xx/5xx 返回,运行期错误服务端内部消化,客户端无需感知)
## 七、whole 推送消息结构(已确认)
`get_full_tick` / `subscribe_whole_quote` 回调数据结构一致(官方文档确认):
```json
{"type":"whole","data":{"600000.SH":{"timetag":"20231106 15:00:04","lastPrice":2.533,"open":2.528,"high":2.538,"low":2.521,"lastClose":2.513,"amount":1442588037.0,"volume":5701929,"pvolume":5701929,"stockStatus":5}}}
```
- `data``{code: tick_dict}`code 为完整代码(600000.SH
- 增量推送只含**变化的品种**
- tick_dict 字段透传 QMT 原始字段(timetag/lastPrice/open/high/low/lastClose/amount/volume/pvolume/stockStatus...
## 八、改造点(已确认)
| 文件 | 改动 |
|------|------|
| `bridge_http_server.py` | 新增路由:`POST /data/subscribe``POST /data/unsubscribe``GET /data/tick` |
| `bridge_ws_server.py` | 新增 `broadcast_all`(推送 whole 到唯一连接);现有心跳保留 |
| `bridge_subscription.py`(新)| 订阅管理:订阅表(sub_id → type/codes)、snowflake sub_id、订阅/退订逻辑、QMT 订阅回调 → 推送 |
| `bridge_main.py` | 初始化订阅管理器;adjust 里驱动 |
| `bridge_data_adapter.py` | `get_full_tick` 已存在(批量支持)|
## 九、参考资料(仅背景,非设计结论)
- docs/设计/整体设计方案_v3.md 第五节:订阅与推送的总体规划
- reference/xtquant_big_convert 的 quote_subscription_manager.py:订阅管理的参考实现
+12
View File
@@ -0,0 +1,12 @@
# 迭代记录索引
> 本目录存放每次迭代的设计/实现记录。每完成一次迭代,在下方表格登记一行。
> 迭代规范见 `docs/项目规范.md` 第九节。
## 迭代索引表
| # | 迭代 | 日期 | 状态 | 关联文档 |
|---|------|------|------|---------|
| 1 | WS 单通道推送(一期:通用接口 + 全量tick订阅 whole | 2026-08-25 ~ 2026-08-26 | ✅ 已完成 | [WS单通道推送设计.md](./WS单通道推送设计.md) |
| 2 | 持仓查询接口(GET /trade/positions 增强:语义化字段 + summary | 2026-08-26 | ✅ 已完成 | [持仓查询接口.md](./持仓查询接口.md) |
| 3 | MCP 服务(桥内嵌 /mcp 端点:JSON-RPC tools | 2026-08-27 | ✅ 已完成 | [MCP服务.md](./MCP服务.md) |
+86
View File
@@ -0,0 +1,86 @@
# 持仓查询接口(GET /trade/positions 增强)
> 版本: 1.0 (2026-08-26)
> 状态: ✅ 已完成
> 关联: docs/设计/整体设计方案_v3.md 第四节(trade 命名空间)、docs/桥接口清单与sfgrid满足度对照.md
> 本次迭代: **持仓查询接口返回全部持仓信息(语义化字段 + 汇总),不做过滤筛选,不兼容旧调用模式**
## 一、已确认的设计决策
1. **接口**: `GET /trade/positions`(复用现有路由,不改 URI
2. **返回全部持仓信息**: 每一行 = 语义化字段(xt 风格,供 sfgrid 等消费方直接使用)+ 完整原始 m_* 字段(透传,不丢弃)
3. **不做过滤筛选**: 不加 code/status 等过滤参数;客户端需要单票时自行从全量中取(sfgrid 的 getStockPosition 本就以全部持仓为缓存)
4. **不兼容旧调用模式**: 不再承诺只返回 m_* 原始字段;新返回为超集(m_* 仍在,新增语义字段),旧调用方读 m_* 不受影响,但返回结构/示例以本设计为准
5. **增加汇总 summary**: count(持仓只数)、total_market_value(总市值)、total_profit(总浮动盈亏)
6. **字段映射**QMT POSITION 行 → 语义字段,字段缺失时安全降级):
| 语义字段 | 来源(m_* 候选,按序探测) | 说明 |
|----------|------------------------|------|
| stock_code | m_strInstrumentID | 完整代码(带交易所后缀,如 600519.SH) |
| stock_name | m_strInstrumentName | 股票名称 |
| volume | m_nVolume | 总持仓量 |
| available | m_nCanUseVolume | 可用持仓量 |
| frozen_volume | m_nFrozenVolume | 冻结数量 |
| on_road_volume | m_nOnRoadVolume | 在途数量 |
| yesterday_volume | m_nYesterdayVolume | 昨仓数量 |
| avg_price | m_dOpenPrice / m_dCostPrice | 持仓成本价 |
| price | m_dLastPrice / m_dSettlementPrice / m_dOpenPrice | 最新价(缺失时兜底成本价) |
| market_value | m_dMarketValue / m_dInstrumentValue / volume*price 计算 | 持仓市值 |
| open_price | m_dOpenPrice | 开仓价(与 avg_price 同源) |
| profit | m_dFloatProfit / 计算 (price-avg_price)*volume | 浮动盈亏 |
| profit_pct | 计算 | 盈亏比例 % |
| direction | m_nDirection | 方向(默认 48=多) |
7. **JSON 安全**: 所有值经 `_json_safe` 处理(QMT 可能返回 numpy 类型)
## 二、实现记录(已完成)
- `src/bridge_data_adapter.py` 新增 `get_positions()`:
-`bridge_util.get_trade_cache("position")` 读缓存(策略线程 300ms 刷新,HTTP 线程只读)
- 每行先 `_obj_to_dict` 得到 m_* 字段,再补充语义字段(含缺失字段探测与计算兜底)
- 计算 summary: count / total_market_value / total_profit
- 返回 `{"positions": [...], "summary": {...}}`
- `src/bridge_http_server.py` `_api_trade`: `dtype == "position"` 时调用新 `get_positions()`,返回 `{"ok":true,"data":{...}}`
- `tests/test_trade.py`: 更新持仓断言(语义字段 + summary)
- `docs/api_spec/openapi.yaml`: `/trade/positions` 响应示例更新为语义字段 + summary
- 重新生成 `docs/api_spec/openapi.json`
## 三、返回结构(示例)
```json
{
"ok": true,
"data": {
"positions": [
{
"stock_code": "600519.SH",
"stock_name": "贵州茅台",
"volume": 100,
"available": 100,
"frozen_volume": 0,
"on_road_volume": 0,
"yesterday_volume": 0,
"avg_price": 1500.0,
"price": 1510.0,
"market_value": 151000.0,
"open_price": 1500.0,
"profit": 1000.0,
"profit_pct": 0.67,
"direction": 48,
"m_strInstrumentID": "600519.SH",
"m_nVolume": 100,
"m_nCanUseVolume": 100,
"m_dOpenPrice": 1500.0,
"m_dFloatProfit": 1000.0
}
],
"summary": {
"count": 1,
"total_market_value": 151000.0,
"total_profit": 1000.0
}
}
}
```
> 说明: 语义字段名以 xtquant 风格为准(sfgrid `qmt_real.py`/`data_store.py` 消费的 `volume/avg_price/stock_code/instrument_name` 等);原始 `m_*` 字段作为超集保留在同一行,避免旧调用方(如已部署脚本)因删字段而失效。
+286
View File
@@ -0,0 +1,286 @@
# 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` 维护迭代索引表,每迭代一行:
| 迭代 | 日期 | 状态 | 关联文档 |
|------|------|------|---------|
| 迭代名 | 起止日期 | ✅ 已完成 / 讨论中 | 文档链接 |