问题描述
解决方案
在 QMT(Quant Trader)平台中,策略回测速度慢通常由平台参数设置不当或代码逻辑效率低下导致。QMT 的运行机制是逐 K 线(Bar-by-Bar)驱动 handlebar 函数,如果该函数内部包含大量重复计算或 I/O 操作,回测效率会显著下降。
以下是针对 QMT 平台的具体提速方案,分为设置优化和代码优化两部分。
一、 平台参数设置优化(立竿见影)
在 QMT 策略编辑器的界面右侧或“基本信息”中,有几个关键参数直接影响速度:
-
开启“快速计算” (Quick Calculation)
- 原理:默认情况下(值为 0),QMT 会从主图品种的第一根 K 线开始运行模型。如果你的策略只需要最近 100 根 K 线的数据来计算均线,但主图有 10 年数据,全量计算会极其浪费时间。
- 操作:将“快速计算”设置为你需要的数据长度(例如
100或200)。这样平台只会计算最后 N 根 K 线,极大缩短初始化时间。 - 注意:此设置主要影响实盘或模拟盘的初始化速度,回测时如果需要完整历史绩效,需谨慎使用,但在调试阶段非常有用。
-
调整“刷新间隔”
- 原理:控制策略运行的频率。
- 操作:如果策略不需要每根 K 线都运行,可以设置刷新间隔。但在回测模式下,通常由行情驱动,此项更多影响实盘。
-
减少回测对象和周期
- 操作:在“回测参数”中,尽量缩小“开始时间”和“结束时间”的范围。如果只是验证逻辑,不需要跑 10 年数据。
- 股票池:
ContextInfo.set_universe()中的股票数量越多,循环次数越多,速度越慢。调试时建议只用几只代表性股票。
二、 代码逻辑优化(核心关键)
QMT 的 handlebar(ContextInfo) 函数会在每一根 K 线(或 Tick)到来时被调用一次。如果在 handlebar 里写了低效代码,回测时间会呈指数级增长。
1. 避免在 handlebar 中重复获取全量历史数据
错误做法:在每一根 K 线通过 get_market_data 获取过去几年的所有数据。
优化做法:只获取计算指标所需的最小窗口数据。
# --- 低效代码 ---
def handlebar(ContextInfo):
# 每次都取了全量历史数据,极慢
data = ContextInfo.get_market_data(['close'], stock_code=['000001.SZ'], count=-1)
# --- 高效代码 ---
def handlebar(ContextInfo):
# 只取计算 MA60 所需的最近 60 根 K 线
data = ContextInfo.get_market_data_ex(['close'], ['000001.SZ'], period='1d', count=60)
2. 移除或限制 print 输出 (I/O 瓶颈)
原理:控制台输出(I/O 操作)是非常耗时的。如果在 handlebar 中每根 K 线都 print 数据,回测速度会降低 10 倍以上。
优化:
- 注释掉所有非必要的
print。 - 或者只在特定条件下打印(如只在买卖点打印)。
def handlebar(ContextInfo):
# print(ContextInfo.barpos) # <--- 务必注释掉这行
if condition_buy:
# 仅在交易时打印
print(f"Buy at {ContextInfo.barpos}")
passorder(...)
3. 向量化计算代替循环 (Vectorization)
QMT 集成了 pandas 和 numpy。尽量使用这些库的整列计算功能,而不是用 for 循环遍历价格列表。
4. 将静态计算移至 init
原理:init(ContextInfo) 只在策略启动时运行一次。
优化:如果有些数据(如股票池的基本面数据、行业分类、固定的参数配置)在回测期间不会变,务必放在 init 中,不要放在 handlebar 里。
def init(ContextInfo):
# 在初始化中设定好股票池,不要在 handlebar 里反复 set
ContextInfo.set_universe(['000001.SZ', '600000.SH'])
# 预加载一些静态数据
ContextInfo.my_static_data = { ... }
def handlebar(ContextInfo):
# 直接使用
data = ContextInfo.my_static_data
5. 使用 get_market_data_ex 替代 get_market_data
根据文档,get_market_data 已不建议使用,建议使用 get_market_data_ex。新接口在处理多股数据结构时通常更高效,且返回的是标准的 Pandas DataFrame/Dict 结构,便于直接进行向量化运算。
三、 进阶技巧:定时器优化
如果你的策略是日线级别,但运行在分钟周期上(例如为了更精细的撮合),可以使用 ContextInfo.run_time 定时器,让核心逻辑只在特定时间(如收盘前)运行,而不是每分钟都跑一遍逻辑。
def init(ContextInfo):
# 设定每天 14:55:00 运行一次 my_strategy_logic
ContextInfo.run_time("my_strategy_logic", "1nDay", "2019-10-14 14:55:00", "SH")
def my_strategy_logic(ContextInfo):
# 这里写核心交易逻辑
print("Running daily logic...")
def handlebar(ContextInfo):
# handlebar 留空或仅处理极简逻辑
pass
四、 总结检查清单
如果回测依然慢,请按以下顺序检查:
- Print 语句:是否在
handlebar里打印了大量日志?(最常见原因) - 数据获取:是否在
handlebar里使用了count=-1获取全量历史数据? - 循环嵌套:是否存在多重
for循环遍历股票池和 K 线? - 网络请求:策略中是否包含访问外部网站(如爬虫)的代码?(这会阻塞回测)
- 图形绘制:是否调用了大量的
paint函数?(大量绘图也会占用资源)
Q&A: QMT 策略回测常见问题
Q: 为什么我的策略在实盘运行很快,回测却很慢?
A: 实盘通常是“当前帧”触发,只处理最新的 Tick 或 K 线。而回测需要从历史开始,模拟成千上万次 handlebar 的调用。如果在 handlebar 中写了全量数据获取(如获取过去5年的数据),实盘只跑一次没感觉,回测跑几千次就会极慢。
Q: ContextInfo.get_market_data_ex 和 get_market_data 哪个更快?
A: 推荐使用 get_market_data_ex。它是新版接口,针对数据结构进行了优化,且更符合 Pandas 的使用习惯,能更好地配合向量化计算来提升速度。
Q: 如何在不降低回测精度的情况下减少计算量?
A: 使用“缓存”思想。例如计算 MA10,不需要每次取 10 个数计算。可以维护一个全局变量,新来一个价格,减去最早的一个,加上最新的一个(增量计算),或者利用 Pandas 的 rolling 函数在获取数据后一次性计算好,而不是在循环中计算。
Q: QMT 支持多进程或多线程回测吗?
A: QMT 的 Python 策略主要运行在单线程环境中。虽然 Python 支持多线程,但受限于 GIL 和 QMT 底层 C++ 的交互机制,直接在策略中开多线程可能会导致数据竞争或报错。建议通过优化算法复杂度来提速。