最近做了一件有点笨但很值的事:把手上五个能实盘跑通的 QMT 策略、两份券商手册、官网 innerApi 十几页文档,再加上在真实客户端里跑的六组探针脚本,全部交叉对照了一遍。
先说最反直觉的结论:官方文档是删减版,最该信的是你自己跑通的代码。
这篇讲「环境、运行机制和下单」,下一篇讲「品种判定、交易日判断、行情取数、交易字段,以及怎么让客户端自己回答语法问题」。
一、权威性顺序:把官网放到最后
写 QMT 代码遇到文档互相打架时,按这个顺序裁决:
+--------------------+--------------------------------------------------+
| 优先级 | 来源 |
+--------------------+--------------------------------------------------+
| 1(最高) | 你自己实盘跑通的代码 |
+--------------------+--------------------------------------------------+
| 2 | 券商 / 迅投的本地手册(PDF、DOCX) |
+--------------------+--------------------------------------------------+
| 3(最低) | 官网 dict.thinktrader.net/innerApi |
+--------------------+--------------------------------------------------+官网为什么排最后?因为它删掉了四样东西:
is_typed_stock(判品种,附完整证券分类表)can_cancel_order(判能否撤单)do_order(立刻发出上一根 K 线的信号)run_time的第 4 个参数market
二、先搞清你在什么 Python 里写代码
这点比想象中重要 —— QMT 内置的是 Python 3.6.8:
+--------------------+--------------------+------------------------------+
| 项 | 实测值 | 影响 |
+--------------------+--------------------+------------------------------+
| Python | 3.6.8 | 不能用 3.7+ 语法 |
+--------------------+--------------------+------------------------------+
| pandas | 0.22.0 | 极旧,禁 pandas 1.x API |
+--------------------+--------------------+------------------------------+
| numpy | 1.19.1 | 可用 |
+--------------------+--------------------+------------------------------+
| TA-Lib | 可用 | 直接 import 就能用 |
+--------------------+--------------------+------------------------------+用不了的:海象运算符 :=、dataclasses、list[str] 这种泛型下标、f-string 的 = 调试语法。
能用的:f-string 本身、类型注解、% 格式化。
一个容易踩的点:pandas 0.22 太老了,Series[-1] 的位置索引回退居然还生效,所以 get_market_data(...)['close'][-1] 这种写法能跑。换到新 pandas 上会直接报错 —— 这是版本红利,不是正确写法。新代码建议改用 get_market_data_ex,它的返回结构固定为 {code: DataFrame}。
三、运行机制怎么选
QMT 有三套驱动机制,选错是最常见的设计级错误:
+------------------------+----------------------------+--------------------------+
| 机制 | 触发方式 | 适用场景 |
+------------------------+----------------------------+--------------------------+
| handlebar(C) | 历史 K 线逐根 + 盘中 tick | 回测;需要逐 K 线语义的 |
| | | 实盘 |
+------------------------+----------------------------+--------------------------+
| ContextInfo.run_time | 固定时间间隔 | 盘中定时判断(实盘首选) |
+------------------------+----------------------------+--------------------------+
| subscribe_quote | 订阅品种 tick 到达 | 需要比 3 秒更快、或只关 |
| | | 心少数品种 |
+------------------------+----------------------------+--------------------------+几条要记住的边界:
handlebar的股票 tick 大约 3 秒一个,频率受主图品种影响。想更快只能用订阅。run_time在回测里完全不触发。 回测必须走handlebar,否则你会以为策略没信号。run_time没有停止方法,随策略一起结束;需要精细控制就用schedule_run。- 订阅有数量上限(非 VIP 受限),而且超限不报错,客户端会拿前值填充。
init 里订阅 + handlebar 空实现 + run_time 做状态刷新与兜底。
定时器有两种 API:
# 旧 API —— 实盘代码大量在用,可靠
C.run_time("my_func", "5nSecond", "2000-01-01 00:00:00", "SH")
# ^ 传历史时间 = 立刻启动
# 新 API —— 支持分组、可取消、可限定次数
import datetime as dt
tid = C.schedule_run(on_timer, '20231231235959', -1, dt.timedelta(minutes=1), 'my_timer')
C.cancel_schedule_run('my_timer') # 按组取消
C.cancel_schedule_run(tid) # 按号取消period 单位是 nMilliSecond / nSecond / nDay。注意部分周期在首次运行前会先等一个 period,别把第一次触发的时间算错。
生命周期函数一共这几个:
init(C) # 启动一次。执行完之前部分接口还不可用
after_init(C) # init 之后、handlebar 之前一次
handlebar(C) # 每根 K 线 / 每个 tick,必须定义
stop(C) # 策略停止前 —— 此时交易连接已断开,禁止报单/撤单
account_callback / order_callback / deal_callback / position_callback
orderError_callback
# 仅实盘模式生效,且必须先在 init 里 C.set_account(账号)一个容易忽略的点:init 里很多接口还没准备好。 实测 get_trading_dates 在 init 里拿不到东西,要放到 after_init。
四、八条铁律
违反任何一条,代码大概率「能编译但不工作」,而且报错信息毫无指向性。
1. 首行统一写 # -- coding: gbk --
三种写法等价:#coding:gbk / #coding=gbk / # -- coding: gbk --。真正的坑不在写法,而在声明和文件实际字节编码不一致:
+----------------------------+--------------------------------------------+
| 场景 | 结果 |
+----------------------------+--------------------------------------------+
| GBK 字节 + 声明 gbk | 一切正常 |
+----------------------------+--------------------------------------------+
| UTF-8 字节 + 声明 gbk | 不报错,中文静默变乱码(最难查) |
+----------------------------+--------------------------------------------+
| GBK 字节 + 声明 utf-8 | 直接 SyntaxError(反而好查) |
+----------------------------+--------------------------------------------+2. 跨 bar、跨 tick 的状态,绝对不能存在 ContextInfo 上。
每个 bar 开始前 ContextInfo 会被深拷贝并回滚,存上去的变量下一个 bar 就没了。
class G: pass
g = G()
def init(C):
g.waiting = {}
def handlebar(C):
g.waiting['x'] = 1 # 这样才保留得住3. quickTrade 必须和调用位置匹配,否则静默漏单。
+------------------------------------------------+--------------+
| 调用位置 | quickTrade |
+------------------------------------------------+--------------+
| handlebar 内,想等 K 线走完再下单 | 0 |
+------------------------------------------------+--------------+
| handlebar 内,想盘中立刻下单 | 1 |
+------------------------------------------------+--------------+
| init / after_init / 定时器回调 / 订阅回调 / 回 | 必须 2 |
| 报回调 | |
+------------------------------------------------+--------------+日线及以上周期在 handlebar 里传 0,等于全天不报单。这是「策略跑了但一笔没成交」的头号原因。
4. QMT 是单线程,所有策略共用一个线程。
禁止 while True、禁止长 sleep。任何一个策略卡住线程,其它策略一起停摆。
5. 交易接口是异步的,下单后立刻查询查不到。
get_trade_detail_data 读的是客户端本地缓存,不是现查柜台。有交易主推的柜台 50ms 刷一次,没有的 1~6 秒一次。推论:卖出后立刻查可用资金不会变多;下单后立刻查委托可能查不到。 必须自己维护委托台账。
6. init 和 handlebar 都必须存在。
哪怕你的逻辑全在定时器里,也要写一个空的 handlebar。
7. 行情、订阅、交易日接口只能写成 C.xxx()。
实测这些名字在全局命名空间里根本不存在:get_market_data、get_market_data_ex、get_local_data、get_full_tick、subscribe_quote、subscribe_whole_quote、unsubscribe_quote、get_trading_dates。它们只挂在 ContextInfo 上。唯一双形态的是 get_stock_list_in_sector。
所以官方示例都写 ContextInfo.get_market_data_ex(...) —— 这不是编码风格,是必须。
8. 交易类内置函数只接受位置参数。
get_trade_detail_data(accountID=acc, ...) 会直接抛 did not match C++ signature。这些是 pybind11 暴露的 C++ 函数,参数名不可用。
五、下单:先用对函数,再对参数
推荐只用 passorder。它是唯一覆盖股票 / ETF / 可转债 / 逆回购 / 打新 / 两融的下单函数,也是唯一支持投资备注的常规函数。
passorder(opType, orderType, accountid, orderCode, prType, price, volume,
strategyName, quickTrade, userOrderId, ContextInfo)它其实有 5 个重载(8 / 9 / 9 / 10 / 11 参)。这不是从文档抄的 —— 是让客户端自己吐出来的:故意用关键字参数去调用,C++ 报错时会把全部候选签名列出来,比任何文档都准。
passorder(int, int, str, str, int, double, double, object) # 8 参
passorder(int, int, str, str, int, double, double, int, object) # 9 参,第 8 位 int
passorder(int, int, str, str, int, double, double, str, object) # 9 参,第 8 位 str
passorder(int, int, str, str, int, double, double, str, int, object) # 10 参
passorder(int, int, str, str, int, double, double, str, int, str, object) # 11 参关键:第 8 位是「多态」的 —— 传 int 就当 quickTrade,传 str 就当 strategyName。所以:
passorder(23, 1101, acc, '600901.SH', 14, -1, 100, C) # 8 参,quickTrade 等同 0
passorder(23, 1101, acc, '600901.SH', 14, -1, 100, 2, C) # 9 参:int = quickTrade
passorder(23, 1101, acc, '600901.SH', 14, -1, 100, '策略名', C) # 9 参:str = strategyName
passorder(23, 1101, acc, '600901.SH', 14, -1, 100, '策略名', 2, C) # 10 参:策略名 + quickTrade
passorder(23, 1101, acc, '600901.SH', 14, -1, 100, '策略名', 2, '备注', C) # 11 参:+ 投资备注只有投资备注 userOrderId 必须用 11 参,而且必须在有 strategyName 的前提下(不需要就传 '')—— 重载表里没有「跳过策略名直接给备注」的组合。
顺便一个反直觉的发现:orderError_callback 不是「下单失败一定通知你」的保障。 我总共试了 11 种非法调用 —— prType=11 + price=0(文档明确记为必然废单)的 8 / 9 / 10 / 11 参,再加上坏账号、坏代码、坏 orderType、坏 opType、零数量、空备注、空策略名 —— 全部「返回正常、不抛异常」,那个回调一次都没触发。废单信息是打在客户端的「策略信号」栏里的。
要诚实补一句范围:这些都是在「模拟 / 研究」运行模式下测的,而官方文档说交易类回调仅实盘模式生效。所以准确的说法是「模拟/研究模式下它不会被调用」,实盘模式我没测。但无论哪种模式,都不该指望它兜底。所以下单是否成功,只能靠自己维护台账 + 轮询委托来确认。
我最初是靠统计本地代码得出「9 参第 8 位只能是 quickTrade、要写策略名必须凑满 11 参」的结论,那是错的 —— 本地代码碰巧在第 8 位只传过整数。让客户端自己说话,比统计语料可靠。
常用的价格类型:
+------------+--------------------------+--------------------------------+
| 值 | 含义 | 备注 |
+------------+--------------------------+--------------------------------+
| 5 | 最新价 | |
+------------+--------------------------+--------------------------------+
| 11 | 指定价 | 只有 11 / 49 会真正使用 price |
+------------+--------------------------+--------------------------------+
| 12 | 涨跌停价 | 追单托底常用 |
+------------+--------------------------+--------------------------------+
| 13 / 14 | 本方一档 / 对手价 | 14 是追单首选 |
+------------+--------------------------+--------------------------------+
| 42~45 | 五档即成剩撤 / 对手方最 | 上交所股票适用,不支持模拟交易 |
| | 优 | |
+------------+--------------------------+--------------------------------+实盘里跑得最顺的组合是:对手价 14 首发 → 未成交撤单 → 涨跌停价 12 追单,形成二级追单。
六、撤单与查询
cancel(orderId, accountId, accountType, ContextInfo) # 返回 bool
orderId = get_last_order_id(accountId, 'STOCK', 'ORDER') # 返回字符串,找不到是 '-1'
obj = get_value_by_order_id(orderId, accountId, 'STOCK', 'ORDER')
can_cancel_order(orderId, accountId, 'STOCK') # 判能否撤单⚠ get_last_order_id 有延迟,而且委托废单时它返回的是「上一次成功下单的委托号」。 所以它不能当「本次下单是否成功」的判据 —— 这是很多自动追单逻辑出错的地方。实测它返回的是字符串 '-1',所以判空要写 == '-1',写成 == -1 永远不成立。
查资金、持仓、委托、成交:
get_trade_detail_data(accountID, accountType, datatype[, strategyName])
# datatype : ACCOUNT / POSITION / POSITION_STATISTICS / ORDER / DEAL / TASK
# accountType : STOCK / CREDIT / FUTURE / STOCK_OPTION,大小写不敏感⚠ 第 4 个参数是按 strategyName(passorder 第 8 参)过滤,不是按 m_strRemark(投资备注)。而绝大多数台账去重都用 m_strRemark 做 key —— 这两个不是同一个东西,不要指望用第 4 参做逐笔去重。
另外要记住:get_trade_detail_data 读的是本地缓存。想让状态「立刻更新」是做不到的,只能轮询 + 超时。
七、常见陷阱清单
+-----+-----------------------------+------------------------------------+
| # | 现象 | 根因 / 对策 |
+-----+-----------------------------+------------------------------------+
| 1 | 跑了一整天一笔委托都没有 | quickTrade 与调用位置不匹配;回调 |
| | | 内要传 2 |
+-----+-----------------------------+------------------------------------+
| 2 | 对手价下单报「对手价无效」 | 全推行情只有一档,客户端要改成五档 |
+-----+-----------------------------+------------------------------------+
| 3 | 变量莫名被重置 | 存到 ContextInfo 上了,改用模块级 |
| | | 对象 |
+-----+-----------------------------+------------------------------------+
| 4 | 中文日志乱码 | 声明编码 ≠ 文件实际字节编码 |
+-----+-----------------------------+------------------------------------+
| 5 | 委托成功但卖不出去 | 用了 m_nVolume,应该用 m_nCanUseVo |
| | | lume |
+-----+-----------------------------+------------------------------------+
| 6 | 数量被废单 | 没按品种最小单位取整 |
+-----+-----------------------------+------------------------------------+
| 7 | 委托重复 / 超单 | 没有「待报」台账 |
+-----+-----------------------------+------------------------------------+
| 8 | 判方向判反 | 用了 m_nDirection,股票恒为 48 |
+-----+-----------------------------+------------------------------------+
| 9 | 数据全是重复值 | 超过订阅上限,客户端用前值填充 |
+-----+-----------------------------+------------------------------------+
| 10 | 定时器在回测里不触发 | run_time 回测无效,回测要走 handle |
| | | bar |
+-----+-----------------------------+------------------------------------+八、交付前自检清单
写完一版策略,过一遍这十二条再上实盘:
- 首行有编码声明,且与文件实际字节编码一致
init/handlebar都存在- 所有跨 bar、跨 tick 的状态都在模块级对象里,没塞进
ContextInfo - 每个下单调用的
quickTrade都与调用位置匹配(回调内必须是 2) init里调了C.set_account(账号)- 价格参数:非「指定价 / 盘后定价」时填了占位值(-1 或 0)
- 数量按品种取了正确的最小单位
- 卖出数量 ≤ 可卖数量
- 所有内置函数都是位置传参
- 没有
while True、没有秒级以上sleep - 有委托台账,且「未查到委托」时暂停该品种的后续报单
stop里没有报单、撤单
小结
QMT 的坑其实高度集中在这五处:编码、ContextInfo 回滚、quickTrade、字段名、单位换算。把它们前置到「写之前就知道」,比事后对着毫无指向性的报错瞎猜要省力得多。
下一篇讲另外半边:怎么判品种(以及一个会静默返回空列表的板块名陷阱)、三套行情数据源怎么分开用、交易对象字段里那个会让止盈止损彻底失效的「哨兵值」,以及我认为最有用的一招 —— 怎么让客户端自己回答「这个语法到底行不行」。

💬 评论 (0)
还没有评论,来抢沙发吧。