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
@@ -0,0 +1,168 @@
# FormulaServer 直连快速路径(58600
## 这是什么
大 QMT 的 `58600` 端口是 **FormulaServer** —— QMT 内置的 C++ 行情/参考数据服务。端口取自
QMT 安装目录的 `config/formulaserver/formulaserver.ini`
```ini
[server_formula]
address = 0.0.0.0:58600
```
QMT 自带 Python 里就有它的官方客户端:`bin.x64/Lib/site-packages/qmt_api`。协议是
**BSON over TCP**,帧格式在 `qmt_api/net/RPCBase.py`
```
| packLen(uint32 BE) | seq(uint32 BE) | cmd(uint16 BE) | tag(uint16 BE) | BSON body |
```
`cmd = 3``NET_CMD_RPC`),body 是 `{"func": <名字>, "params": {...}}`
响应 `{"status": 0, "params": {...}}``status != 0``params` 里带 `ErrorID`/`ErrorMsg`
`tag & 7` 标记 zlib 压缩,`tag >> 8` 的低 4 位是 seq 的高位。
## 为什么值得接
原来所有只读请求都要绕一整圈:
```
客户端 → redis/zmq → QMT python 策略线程 → ContextInfo → 原路返回
```
这不只是慢,还要和策略自己抢 QMT 主线程的 GIL —— zmq 传输约 30% 的请求会撞上 ~500ms 的
调度尖峰,就是这么来的。
直连 FormulaServer 完全绕开策略进程:
| 路径 | p50 |
|------|-----|
| redis RPC | ~13ms |
| zmq RPC | ~0.7ms30% 撞 500ms GIL 尖峰)|
| **FormulaServer 直连** | **0.07ms**,无 GIL 竞争 |
穿过完整客户端栈(`BigQmtRpcClient.call`)实测 **0.145ms/次**,比 redis 快约 90 倍。
## 能力边界
**FormulaServer 只有行情/参考数据。** 实测所有账户/交易类方法一律返回
`ErrorID 200005 未找到该服务`
```
getAsset / getPositions / getAccountDetail / passorder -> 200005
getFullTick / getQuote -> 200005
```
所以它是**只读快速路径,不是 RPC 桥的替代品**。交易、账户查询、持仓、委托、成交、
五档盘口全部仍然走 RPC。
### 已接入的方法(10 个)
| 我们的方法 | FormulaServer func |
|---|---|
| `get_instrument` / `get_instrument_detail` / `get_instrumentdetail` | `getInstrumentDetail` |
| `get_last_volume` | `getLastVolume` |
| `get_total_share` | `getTotalShare` |
| `get_contract_multiplier` | `getContractMultiplier` |
| `get_main_contract` | `getMainContract` |
| `get_weight_in_index` | `getWeightInIndex` |
| `get_stock_list_in_sector` | `getStockListInSector` |
| `get_market_data_ex` | `getMarketData` |
### 刻意不接的方法,以及原因
宁可慢,不能悄悄给错数据。以下几项参数语义与我们的调用方不一致:
- **`get_trading_dates`** —— FormulaServer 要的是**股票代码**。实测:
```
{'stockCode': 'SH', ...} -> {'result': []} # 静默空
{'stockCode': '000001.SZ', ...} -> ['20260630', '20260701', ...]
```
而 `market_bigqmt.get_trading_dates(market, ...)` 的调用方传的是市场代码。传错了不报错、
只给空列表,交易日历错了后果太重。
- **`get_divid_factors`** —— 我们是 `(stock_code, start_time, end_time)` 区间,
FormulaServer 是 `(stockCode, date)` 单日。
- **`get_risk_free_rate`** —— 我们传 `index=-1`FormulaServer 要 `timetag`。语义不同。
- **复权 K 线** —— 实测 `dividendType` 传 `none` 和 `front` 返回**完全相同**的价格,
说明复权没有生效。因此只有 `dividend_type="none"`(或空)才走直连,其他复权类型直接
判为 unroutable 回退 RPC。否则策略要前复权、拿到的却是不复权价格,且毫无提示。
### 字段名坑
FormulaServer 的 `getInstrumentDetail` 返回 **`FloatVolumn` / `TotalVolumn`**(官方拼写错误),
而原生 xtdata SDK 用的是 `FloatVolume` / `TotalVolume`。下游代码按 SDK 拼写读,直接透传会
静默读到 `None`。所以 `_instrument_result` 做了别名归一化,两种拼写都保留。
### 尚未验证
`qmt_api/api.py` 的 `getMarketData` 支持 `fields=['quoter']`,注释说会返回
`askPrice/askVol/bidPrice/bidVol`level1 五档 / level2 十档)。**如果这在盘中可用,
`get_full_tick` 也能走直连** —— 这是热路径,收益很大。
但收盘时段实测返回空,无法确认。需要**盘中**再测一次:
```python
c.request('getMarketData', {'fields': ['quoter'], 'stockCodes': ['000001.SZ'],
'startTime': '', 'endTime': '', 'period': 'tick',
'dividendType': 'none', 'count': -1})
```
在确认之前不要接 —— 没验证就上映射,正是订单方向判定踩过的坑。
## 失败行为
**任何失败都自动回退 RPC**,所以连不上 58600 的客户端行为与改动前完全一致:
| 情况 | 行为 |
|---|---|
| 方法不在映射表 | `supports()` 返回 False,直接走 RPC |
| 参数 translate 不了 | `Unroutable`,走 RPC,**不**触发熔断(这是单次调用的问题) |
| 服务连不上 / IO 失败 | `Unroutable` + 熔断 `failure_cooldown_seconds`(默认 30s),期间全部走 RPC |
| 服务端回 `200005` | 该方法永久标记 unimplemented,只停这一个方法,不影响其他 |
| socket 断了(QMT 重启) | 自动重连重试一次 |
## 依赖
**不需要装任何东西。** BSON 编解码内置了无依赖实现;如果环境里有 pymongo 的 `bson`
或 QMT 的 `xtquant.xtbson`,会优先用(更快、更久经考验)。两条路径的输出实测逐字节一致,
测试里有对拍用例。
## 配置
客户端侧,默认开启,通常不用写:
```python
BIGQMT_FORMULA_SERVER_CONFIG = {
"enabled": True, # 或环境变量 BIGQMT_FORMULA_ENABLED=0 关闭
# "host": "127.0.0.1", # 绑的是 0.0.0.0,跨机可达(需放行防火墙)
# "port": 58600, # 不写则从 qmt_root 的 ini 读,再退回 58600
# "qmt_root": r"D:\国金证券QMT交易端",
# "timeout_seconds": 3.0,
# "methods": ["get_instrument"], # 只路由白名单
# "failure_cooldown_seconds": 30.0,
}
```
也可以写在 `BIGQMT_REDIS_CONFIG["formula_server"]` 里,后者优先级更高。
## 排查
```python
client._formula_router().stats()
# {'enabled': True, 'hits': 202, 'misses': 0, 'available': True,
# 'unimplemented': [], 'methods': [...]}
```
启动时会打一行:
```
[bigqmt_formula] active at 127.0.0.1:58600 (10 methods routed direct)
```
熔断时:
```
[bigqmt_formula] unavailable, falling back to RPC for 30s: connect 127.0.0.1:58600 failed: ...
```