前阵子给自己的 AI 助手写了个 Skill,取名 qmt-strategy-review。它就干一件事:丢一段 QMT 策略代码进去,对照官方文档逐条查 API 签名、状态管理、单线程陷阱和实盘可用性,吐一份能直接用来决策改不改、改什么的审阅报告——不是那种”你这里有个错误”的空泛评论,是带着改法和风险等级的。
这个 Skill 很快上架,但目前只在我本地的 Skill 目录里跑。等市场能搜到了我再更这篇。
为什么要写这个 Skill
其实就是一句话:直接跟 AI 聊天审 QMT 代码,每次都要重复那些前置背景,烦。
QMT 这套东西有几个特点:API 签名散在十几页文档里、ContextInfo 逐 K 线回滚是单线程里最容易踩的隐蔽坑、order_shares 这种函数只在回测里有效但文档藏在角落、编码声明和实际编码不一致会导致盘中静默挂掉。每次我把一段策略代码贴给普通 AI,它要么漏查这些特定约束,要么让我把文档片段再贴一遍。
把这些查档路径和核查规则固化成 Skill 之后,下次丢代码进去就不用再说”记得查 ContextInfo 回滚机制,翻《常见问题》那页”——Skill 自己知道要查哪几页文档、按什么顺序对、哪些严重错误必须标红。一次调到位,后续对话都能复用。
这也是为什么我只让它审工程层:策略逻辑好不好、参数选得对不对,那是交易系统的问题;而”这个函数实盘会静默失效”、”这个状态变量会被 K 线回滚吞掉”——这些是任何 QMT 策略不管做什么都绕不开的技术约束,适合做成一次性跑通的 Skill。
真实案例:官方日内回转示例策略
我把官方那份日内回转实盘示例丢进去,跑出来的报告是这样:
第一部分:评审范围说明
本次只检查代码编写与代码实现层:单线程阻塞、数据层效率、ContextInfo 误用与状态管理(逐 K 线回滚、深拷贝成本)、API 签名与枚举值(均对照 QMT 官方文档当次核实)、编码声明与乱码风险、Python 代码层错误、回放与重复下单防护、日志写法。
- 不检查策略逻辑:MACD 参数 12/26/9 是否合理、信号是否有超额、回转规则赚不赚钱,属于策略逻辑范畴,本节不评价、不预测。
- 不收集、不存储策略代码:评审只在当前对话内完成,不落盘、不复制、不写入任何记忆或日志文件,阶段一连改后文件的副本都不生成。
先说结论里最要紧的一句:这个文件头部自己写了“回测模型示例(非实盘交易策略)”,这句话比任何评审意见都关键——它用的 order_shares 官方明确标注“仅回测可用”。你的目标是回测还是实盘,决定了下面哪些条目必须改。
第二部分:修改意见
汇总表
| 编号 | 级别 | 位置 | 问题一句话 |
|---|---|---|---|
| 严1 | 严重 | L46/L80/L88/L98/L107 | order_shares 官方标注“仅回测可用”,模拟盘与实盘调用不产生任何委托 |
| 严2 | 严重 | L17/L52/L84/L93/L97/L106 | 持仓记账 MarketPosition 挂在 ContextInfo 上,盘中逐 K 线回退会算错仓位 |
| 警1 | 警告 | L39 | get_market_data 已标注【不推荐】且返回类型随参数变化,代码强依赖 DataFrame 形态 |
| 警2 | 警告 | L59/L76 | 收盘回转窗口只有 14:55 这一分钟,14:56 起直接 return,漏一分钟就隔夜裸仓 |
| 警3 | 警告 | L20/L79/L80 | Lots 单位前后不一致:资金检查按 100 手算,实际只买 100 股 |
| 提1 | 提示 | L12/L35 | talib 依赖需本地确认;每根 bar 都 print 日期,回测日志爆量 |
| 提2 | 提示 | L36/L37 | flage / singleemited 是死变量,画图代码被注释后无人使用 |
| 提3 | 提示 | L51/L56/L116 | day 状态只写不读,原意被注释掉了 |
| 提4 | 提示 | L24 | 账号配置挂在 ContextInfo 上(官方未列为只读,但属于该迁走的配置) |
| 提5 | 提示 | L39 | get_universe 在官方文档页面未检索到定义,只能以本地实测为准 |
总体结论:回测用途成立,不能直接上实盘。 严1 决定了这份代码只能在“回测”按钮下工作——切到模拟信号或实盘模式,5 处下单全部静默失效,策略看起来在跑、日志也在打,但一笔委托都不会发出去;严2 则是一旦真的在盘中跑起来,同一根 K 线内的多次记账会被回退,收盘回转会按错误仓位补单。最大风险就是这两条叠在一起。
逐条详述
严1(严重)order_shares 只在回测里有效
- 现象:L46、L80、L88、L98、L107 五处下单都用
order_shares。 - 原因:官方文档把
order_shares放在「其他交易函数(仅回测可用)」章节,章节开头原文是“以下函数仅回测生效,实盘和模拟盘交易均不可用”。也就是说:回测模式下它按ContextInfo.capital虚拟撮合;一旦你用「模拟信号」或「实盘」模式启动,这五处调用不会产生任何委托,也不会有报错提示——最危险的一种失效方式。 - 依据:QMT 官方文档《交易函数》- 其他交易函数(仅回测可用)。
- 签名核对:
order_shares(stockcode, shares[, style, price], ContextInfo[, accId]),shares正数买、负数卖,'fix'为指定价(官方示例即写小写'fix')——你现有的参数顺序是对的,问题不在写法,在于函数的用途边界。 - 改法(未落盘):要上模拟/实盘,换成
passorder(23/24, 1101, account, code, prType, price, volume, '策略名', quickTrade, 备注, C),quickTrade=0配合 K 线走完再下单。若只是想少改,可保留order_shares专供回测,另写一个_order()包装函数按运行模式分支。 - 回归:模拟信号模式下确认「策略信号」栏有记录;回测结果数字与改前一致。
严2(严重)持仓记账挂在 ContextInfo 上
- 现象:
ContextInfo.MarketPosition(L17 建立,L52/L84/L93/L97/L106/L113 读写)、ContextInfo.first、ContextInfo.day全部是挂在 ContextInfo 上的自定义属性。 - 原因:官方《使用须知》原文——“由于底层机制的限制,ContextInfo 中存储的变量值将会回滚……在完全理解 ContextInfo 机制之前,请避免在其中存储任何变量”,并给了推荐写法
class G(): pass+ 全局实例。机制细节(《常见问题》- 系统对象 ContextInfo 逐 k 线保存的机制):每次handlebar前深拷贝,若新分笔不是该 K 线第一个分笔,就回退到深拷贝——只有 K 线结束时最后一个分笔的修改才有效。回测里每根 bar 只调用一次,所以侥幸不出事;盘中每个 tick 都调用,MarketPosition += Lots这类累加会被吞掉,14:55 回转时算出来的仓位是错的,可能多买或少卖。 - 依据:QMT 官方文档《使用须知》、《常见问题》- ContextInfo 逐 k 线保存机制。
- 改法(未落盘):把这些状态整体搬到模块级单例
class G(): pass; g = G(),g.pos、g.first、g.day直接用,不再经过 ContextInfo。 - 回归:回测结果应与改前完全一致(回测下每 bar 一次调用,搬迁不改语义);盘中打印
g.pos确认同一根 K 线内多次触发时数值不再回退。
警1(警告)get_market_data 返回形态不稳定
- 现象:
df = get_market_data(['close'], stock_code=..., start_time, end_time, period=...),随后依赖df.iloc[-1, 0]、df.iloc[-35:, 0]、df.empty——即假定“行=时间、第 0 列=close 的 DataFrame”。 - 原因:官方页面标题即标【不推荐】,并明确“返回值根据传入的参数情况,会返回不同类型”:单股票+单字段+单时间点可能返回 float,多字段单股票返回 Series,多股票返回 index 为代码的 DataFrame,多字段多时间点返回 Panel(新版 pandas 已移除 Panel)。你这里是单股票单字段多时间点,按文档应落进 DataFrame 分支,但文档没有明确该分支的 index/columns 语义,
iloc的写法属于对未定义形状的依赖;换客户端版本或改参数就可能AttributeError: 'float' object has no attribute 'iloc'。 - 依据:QMT 官方文档《行情函数》- get_market_data(不推荐)。
- 改法(未落盘):迁到
get_market_data_ex(['close'], [code], period=..., count=35),返回稳定{code: DataFrame(index=时间, columns=字段)},取df['close'].values即可,顺带省掉 start_time/end_time 的字符串拼装。 - 回归:取数长度 35、最后一根收盘价与主图一致。
警2(警告)回转窗口只有一分钟
- 现象:L59
if int(date[-8:-6] + date[-5:-3]) > 1455: return,14:56 起直接返回;L76 的if date[-8:-3] != '14:55'决定只有 14:55:00–14:55:59 这一分钟走回转分支。 - 原因:设计意图是“收盘前把仓位调回原仓位”,但实际可执行窗口只有一分钟。若这一分钟没有 bar(停牌、断线重连、客户端重启、数据缺口),回转永不执行,仓位带过夜,与你注释里的设计目标相反。另外这段用字符串切片拼
'1455'再转 int,格式一变就静默失效。 - 依据:QMT 官方文档《变量约定》- timetag_to_datetime 格式化用法(建议直接解析时间对象,而不是切字符串)。
- 改法(未落盘):改成“时间窗口”而非“某一分钟”,例如
>= 145000到150000之间都允许回转,且回转完成后置一个g.done_today标记避免重复;时间用datetime.strptime或直接比较HHMMSS整数。 - 回归:故意让 14:55 那根 bar 缺失(改窗口起点到 14:50 模拟),确认回转仍会执行且只执行一次。
警3(警告)Lots 单位不一致
- 现象:L20
Lots = 100 # 设定交易手数;L79 资金检查用price * Lots * 100(按 100 手 = 10000 股算);L80 下单order_shares(code, Lots, ...)只买 100 股;L84 记账也+= Lots。 - 原因:资金门槛按万股价校验,实际只下百股,检查形同虚设;如果以后把
Lots当“手数”调成 10,程序会买 10 股(不足 1 手,直接废单)。 - 改法:统一为“股数”一个口径,变量名改
LotShares = 100,资金检查用price * LotShares;或统一为“手数”,下单时传Lots * 100。 - 回归:改完后打印一次下单股数与资金占用,与手算一致。
提1(提示)外部依赖与日志量:import talib 是否可用取决于客户端“下载 Python 库”是否补全(官方《使用须知》- 下载 python 库),官方文档未承诺默认随附,请在本地确认;另外官方《引用函数》页面提供 MA/MACD/KDJ 等指标引用函数,若不想依赖 talib 可改用它。L35 每根 bar 都 print('日期', date),回测几千根 bar 会让日志非常庞大,建议只在信号触发时打印。
提2(提示)死变量:flage(L36)与 singleemited(L37)只在被赋值的分支里使用或根本没人读,画图代码注释掉后就成了死代码,删掉更清爽。
提3(提示)day 状态只写不读:L51、L56 往 ContextInfo.day 写值,L116-117 才是原本“更新过去日期数据”的意图,但已被注释;现在这个状态不参与任何判断,属于遗留物。
提4(提示)accountID 挂 ContextInfo:静态扫描把它判成“写只读属性”,我核对官方《变量约定》未发现 accountID 的只读定义(聚宽迁移示例里官方自己就示范过给 C 挂账号字段),因此这条降级为写法建议:账号属于配置,放模块级更合适,也更符合“ContextInfo 不存变量”的官方指引。
提5(提示)get_universe:官方《行情函数》页面只收录了 set_universe(stock_list) 的调用示例(与你 L18 的写法一致),get_universe() 的原型在三张页面里都没检索到,无法为你核实签名;它在你本地能跑就继续用,但更稳妥的写法是用模块级变量存 code:
1 | # [qmt-strategy-review] ---- 候选替换(未应用,确认 提5 后启用)---- |
已核实无误(正面确认):C.stockcode + '.' + C.market 拼接格式正确;C.period(L40 传入 period)是只读属性、取值 '1m' 等与文档一致;timetag_to_datetime(ContextInfo.get_bar_timetag(ContextInfo.barpos), '%Y-%m-%d %H:%M:%S') 与官方《变量约定》示例逐字一致;get_trade_detail_data(accountid, 'STOCK', 'ACCOUNT'/'POSITION') 参数顺序与《交易函数》一致;set_universe 用法与官方示例一致。全篇无 sleep、无线程库、无 count=-1 无界取数、无网络/文件外接。
第三部分:代码节选 + 注释(原样,逻辑零改动)
1 | # coding:gbk |
要不要我落地改? 请回编号,例如「改 通1、严1、警2」或「全改」。动之前我会先复制 日内回转.py.bak,只改你点到的条目,改完重跑静态审查并对比严重项数量。另外有两种典型组合,可以直接说:
- 只做回测 → 只需「通1 + 警2 + 提1(日志限流)」,其余保留现状即可。
- 要上模拟/实盘 → 必改「通1 + 严1 + 严2 + 警2 + 警3」,这是一次结构性改造(passorder + 委托状态机 + 模块级状态),我会给完整骨架。
⚠️ 实盘风险提示:涉及真实下单。请先在 QMT「模拟信号模式」或「模拟柜台」中充分验证后,再切换实盘账号;股票受 2% 价格笼子限制,数量超可用会产生废单。
现状与后续
qmt-strategy-review 目前只装在我自己的本地 Skill 目录里,配合 QMT 官方 API 文档在跑。等它上架到 Skill 市场能搜到了我再回来补个链接。
策略代码全文和审阅完整版报告请在 WTSolutions 公众号后台发送“代码评审”获取。
风险提示
- 本文是技术分享,不构成任何投资建议、荐股意见或收益承诺。
- 文中评审意见已标注官方文档依据,但 AI 也会看错页面或漏掉更新;请以 QMT 官方文档与你本地客户端的实际行为为准。
passorder等交易接口实盘模式下真实发出委托,任何策略在上实盘前必须先在 QMT「模拟信号模式」或「模拟柜台」跑通。- 因使用本文内容造成的盈亏由使用者自行承担。量化交易涉及真实资金风险,入市需谨慎。
- QMT、迅投为迅投公司或其关联公司的商标,本文为社区非官方整理,与官方无隶属关系。