问题描述
解决方案
在 QMT(Quant Trader)平台中,策略运行缓慢通常是由数据获取方式不当、循环计算过多或平台参数设置不合理导致的。
以下是针对 QMT Python 策略性能优化的完整指南,分为平台设置优化、数据接口优化和代码逻辑优化三个维度。
QMT 策略性能优化指南
1. 平台设置优化(最快见效)
在修改代码之前,首先检查策略编辑器的“基本信息”设置,这是最常见的性能瓶颈来源。
1.1 开启“快速计算”
- 原理:默认情况下(值为 0),QMT 会从历史数据的第一根 K 线开始,对每一根 K 线都执行一次
handlebar函数。如果历史数据很长,启动会非常慢。 - 优化方法:在策略编辑器的【基本信息】中,将 “快速计算” 设置为一个合理的整数(例如
100或200)。 - 效果:策略启动时,只会从当前时刻往前推 N 根 K 线开始计算,极大缩短启动时间。
1.2 调整“刷新间隔”
- 原理:默认情况下,行情每变动一个 Tick(分笔),
handlebar就会被触发一次。对于高频策略这是必须的,但对于分钟级或日线级策略,这会造成大量无用计算。 - 优化方法:在【基本信息】中设置 “刷新间隔”(例如
3000毫秒)。或者在代码中控制逻辑执行频率。
2. 数据接口优化(核心瓶颈)
QMT 的底层是 C++,Python API 是通过层级调用的。频繁、细碎的数据交互会严重拖慢速度。
2.1 使用 get_market_data_ex 替代旧接口
- 建议:尽量使用
ContextInfo.get_market_data_ex()。 - 原因:官方文档明确指出旧版
get_market_data已不建议使用。新版接口在处理批量数据时效率更高,且返回格式更统一(Pandas DataFrame)。
2.2 批量获取数据(向量化)
- 错误做法:在循环中逐个获取股票数据。
# ❌ 低效写法 for stock in stock_list: data = ContextInfo.get_market_data_ex(..., [stock], ...) # 处理逻辑... - 优化做法:一次性获取所有股票的数据,利用 Pandas 进行矩阵运算。
# ✅ 高效写法 data_map = ContextInfo.get_market_data_ex(..., stock_list, ...) # 此时 data_map 包含所有股票数据,直接处理
2.3 避免在 handlebar 中重复下载历史数据
- 问题:
get_history_data或download_history_data如果放在handlebar中且没有条件限制,会在每个 Tick 都执行下载/读取 IO 操作。 - 优化:
- 静态历史数据在
init()中一次性获取。 - 动态数据仅在
ContextInfo.is_new_bar()为 True 时获取。
- 静态历史数据在
3. 代码逻辑优化(Python 特性)
3.1 减少 handlebar 的计算负载
handlebar 是高频回调函数。
- 利用
is_last_bar:如果是实盘运行,历史 K 线的handlebar回放通常不需要执行下单逻辑或复杂的信号计算。def handlebar(ContextInfo): # 如果不是最后一根K线(即正在回放历史),直接跳过复杂逻辑 if not ContextInfo.is_last_bar(): return # 下面写实盘逻辑 ...
3.2 使用 Pandas/Numpy 替代 For 循环
Python 的原生循环非常慢。凡是涉及价格计算(如均线、RSI、相关性),务必使用 pandas 或 numpy 的内置函数。
- 示例:计算 100 只股票的均线。
- 低效:遍历 list,手动累加除以 N。
- 高效:
df['close'].rolling(window=10).mean()。
3.3 减少日志输出
- 问题:
print()函数是同步 IO 操作,在大量 Tick 推送时,频繁打印会显著阻塞策略运行。 - 优化:仅打印关键交易信息,或使用 Python 的
logging模块并设置较高的日志级别。
3.4 避免重复创建对象
- 优化:将固定的股票池列表、常量定义、大对象的初始化放在
init(ContextInfo)中,而不是handlebar(ContextInfo)中。
4. 优化代码示例
以下是一个应用了上述优化技巧的框架示例:
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
# 1. 在 init 中做一次性设置,避免在 handlebar 中重复操作
ContextInfo.stock_list = ['600000.SH', '000001.SZ', '600519.SH']
ContextInfo.set_universe(ContextInfo.stock_list)
# 2. 预先定义好需要的字段,避免反复创建列表
ContextInfo.fields = ['close', 'open', 'high', 'low']
# 3. 账号设置
ContextInfo.account_id = '您的账号'
ContextInfo.set_account(ContextInfo.account_id)
def handlebar(ContextInfo):
# 4. 【关键优化】如果是回测模式或历史K线回放,且不需要逐K线计算信号,可以直接跳过
# 如果设置了“快速计算”,这里的负担会小很多
if not ContextInfo.is_last_bar():
return
# 5. 【关键优化】使用 get_market_data_ex 批量获取数据
# count=-1 表示获取所有订阅的数据,或者指定 count=200 用于计算指标
data_map = ContextInfo.get_market_data_ex(
ContextInfo.fields,
ContextInfo.stock_list,
period='1d',
count=20,
dividend_type='front'
)
# 6. 【关键优化】避免循环,尽量使用逻辑判断
for stock in ContextInfo.stock_list:
if stock not in data_map:
continue
df = data_map[stock]
if df.empty:
continue
# 使用 pandas 快速计算均线,而不是手写循环
ma5 = df['close'].rolling(5).mean().iloc[-1]
current_price = df['close'].iloc[-1]
# 简单的交易逻辑
if current_price > ma5:
# 检查是否已有持仓等逻辑...
pass
Q&A: 常见性能问题解答
Q: 为什么我的策略在回测时很快,一到实盘就卡顿?
A: 回测时数据是本地读取的,速度极快。实盘时 handlebar 是由实时 Tick 驱动的。如果你的策略逻辑处理时间超过了两个 Tick 之间的间隔(例如 500ms),就会导致积压和卡顿。请检查是否在 handlebar 里做了耗时的网络请求或复杂的数学运算。
Q: get_market_data 和 get_market_data_ex 真的有很大区别吗?
A: 是的。get_market_data 是旧版接口,在处理多标的数据时性能较差。get_market_data_ex 经过优化,且直接对接 Pandas 格式,减少了数据转换的开销。
Q: “快速计算”设置为 0 会发生什么?
A: 设置为 0 意味着全量计算。假设你在日线上运行,且主图品种有 10 年历史,策略启动时会从 10 年前第一天开始,把每一天的 handlebar 都跑一遍。这通常是策略启动慢的罪魁祸首。
Q: 我可以使用多线程来加速吗?
A: 在 QMT 的 Python 环境中,不建议使用 threading 或 multiprocessing 进行复杂的计算或下单操作。因为 QMT 的底层 API 并非完全线程安全,且 Python 的 GIL 锁限制了多线程的计算效率。对于计算密集型任务,优先考虑向量化(Numpy);对于 IO 密集型任务,尽量减少频率。