6.3 KiB
FormulaServer 直连快速路径(58600)
这是什么
大 QMT 的 58600 端口是 FormulaServer —— QMT 内置的 C++ 行情/参考数据服务。端口取自
QMT 安装目录的 config/formulaserver/formulaserver.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.7ms(30% 撞 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 也能走直连 —— 这是热路径,收益很大。
但收盘时段实测返回空,无法确认。需要盘中再测一次:
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,会优先用(更快、更久经考验)。两条路径的输出实测逐字节一致,
测试里有对拍用例。
配置
客户端侧,默认开启,通常不用写:
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"] 里,后者优先级更高。
排查
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: ...