问题描述
解决方案
在量化策略回测中,**“未来函数”(Look-ahead Bias)**是指在策略进行决策时,利用了当时那个时间点实际上无法获取的数据。这会导致回测结果虚高,实盘却严重亏损。
在 QMT 平台中,要确保回测的真实性,需要从撮合机制理解、行情数据获取、财务数据处理以及代码逻辑四个方面进行严格控制。
以下是具体的操作指南和注意事项:
1. 理解 QMT 的回测撮合机制
QMT 的 Python 策略主要通过 handlebar(ContextInfo) 函数驱动。
-
默认机制(安全): 在日线回测中,
handlebar在每一根 K 线结束时被调用。此时,你拥有了这根 K 线的 Open, High, Low, Close (OHLC)。- 信号生成: 你利用当天的收盘价计算指标,产生买卖信号。
- 订单执行: 默认情况下,QMT 会将交易指令发送到下一根 K 线的开盘时刻(Open)进行撮合。
- 结论: 这种“今日收盘决策,明日开盘执行”的模式是避免未来数据的最标准做法。
-
风险点(quickTrade 参数):
- 如果在
passorder函数中设置quickTrade=1,或者使用do_order(),意味着你试图在当前 K 线立即成交。 - 危险场景: 如果你基于当天的
Close(收盘价)计算信号,并在当天就以Close价格成交,这就是典型的用到未来数据(因为在盘中你不可能知道收盘价是多少)。 - 修正: 如果必须在当天成交(例如收盘前5分钟),必须确保你的逻辑是基于盘中实时数据,而非全天收盘数据。
- 如果在
2. 行情数据的正确获取
在使用 ContextInfo.get_market_data_ex 或 get_history_data 时,要注意数据的时间范围。
错误的做法
在计算指标时,获取了包含“明天”或“未来”的数据进行归一化或计算移动平均。
正确的做法
始终基于当前回测进度 ContextInfo.barpos 来截取数据。
# -*- coding: gbk -*-
def handlebar(ContextInfo):
# 获取当前 K 线索引
index = ContextInfo.barpos
# 获取当前主图品种
code = ContextInfo.stockcode + '.' + ContextInfo.market
# 【正确】获取截止到当前 K 线的数据(count 设为一定长度,不要设为 -1 获取全部)
# 注意:在 handlebar 运行结束时,当前 bar 的数据是已知的
data = ContextInfo.get_market_data_ex(
['close'],
[code],
period=ContextInfo.period,
count=20, # 获取过去20根
end_time='', # 默认为当前进度时间
subscribe=False
)
if code in data:
df = data[code]
# 确保数据长度足够
if len(df) < 20:
return
# 计算指标,例如 MA20
ma20 = df['close'].mean()
# 获取上一根 K 线的收盘价(用于判断交叉)
# 这里的 iloc[-1] 是当前 bar,iloc[-2] 是上一根 bar
current_close = df['close'].iloc[-1]
prev_close = df['close'].iloc[-2]
3. 财务数据的“公告日”原则
这是量化回测中最容易忽视的未来数据陷阱。
- 报告期 (Report Date): 财报所属的时间段(如 2023年一季报,报告期是 2023-03-31)。
- 公告日 (Announcement Date): 财报实际对外发布的时间(如 2023年一季报可能在 2023-04-25 发布)。
如果在 2023-04-01 就使用了一季报的数据进行选股,就是使用了未来数据。
QMT 中的正确用法
在使用 ContextInfo.get_financial_data 时,务必注意 report_type 参数。
report_type='announce_time'(默认值/推荐): 系统会根据公告日期来返回数据。即在 4月1日时,系统只会给你看 2022年年报的数据,只有到了 4月25日,系统才会给你看 2023年一季报的数据。这是真实的。report_type='report_time'(慎用): 系统会根据报告期返回数据。这会导致你在 4月1日就能看到 4月25日才发布的数据,造成回测虚假繁荣。
# -*- coding: gbk -*-
def handlebar(ContextInfo):
# ... 省略部分代码
# 【正确】使用默认的 announce_time 或显式指定
# 这样能确保在回测历史时间点上,只获取到当时已经公告的财报
fin_data = ContextInfo.get_financial_data(
['ASHAREINCOME.net_profit_incl_min_int_inc'], # 净利润
['600000.SH'],
startDate=ContextInfo.get_bar_timetag(ContextInfo.barpos - 10), # 示例时间
endDate=ContextInfo.get_bar_timetag(ContextInfo.barpos),
report_type='announce_time' # 关键参数
)
4. 避免特定的逻辑陷阱
- 最高/最低价成交: 不要编写类似
if price <= low: buy的逻辑。因为在实盘中,你无法预知当天的最低价是多少,除非你挂单在那个价格等待成交(但回测很难模拟挂单排队)。建议以open或close价格成交。 - 全样本标准化: 如果你在回测开始前,先把所有历史数据拿来进行
Z-Score标准化,然后再进行回测,这是严重的未来数据。标准化必须使用滚动窗口(Rolling Window),只利用过去 N 天的数据计算均值和方差。
5. 总结检查清单
在运行回测前,请检查以下几点:
- 交易时机: 策略是否是“今日信号,明日开盘成交”?如果是“今日成交”,是否确保了没有利用今日收盘价作为成交价?
- 财务数据:
get_financial_data是否使用了announce_time模式? - 数据引用: 是否只引用了
barpos及其之前的数据,没有引用barpos + 1的数据? - 偷价行为: 是否设置了合理的滑点(
ContextInfo.set_slippage)和手续费?虽然这不是未来数据,但忽略滑点会高估策略表现。
Q&A: 常见问题解答
Q: QMT 回测时,ContextInfo.get_market_data_ex 如果不填 end_time,默认是什么时间?
A: 在回测模式下,如果不填 end_time,默认是当前回测运行到的时间点(即 ContextInfo.barpos 对应的时间)。这保证了你不会取到比当前回测进度更晚的数据。
Q: 我想在收盘前 5 分钟交易,如何避免未来数据?
A: 你需要使用分钟线(如 5分钟或 1分钟周期)进行回测,或者在日线策略中使用 timetag_to_datetime 判断时间,并配合 do_order。但最稳健的方法是使用分钟周期回测,在每天的最后一根分钟 K 线(如 14:55)产生信号并下单。
Q: 为什么我的策略回测收益率极高,但实盘不行?
A: 除了未来数据(如提前知道财报、提前知道涨跌停)外,常见原因还包括:未计算滑点和手续费、成交量超过了市场实际流动性(买入量超过了当时的盘口挂单量)、使用了复权数据但未考虑除权日的实际买卖限制等。
Q: quickTrade=1 在回测中到底能不能用?
A: 可以用,但必须非常小心。它表示“立即下单”。如果在日线级别回测,且逻辑是 if close > ma20: passorder(..., quickTrade=1),这就是利用未来数据(因为盘中不知道收盘价)。如果逻辑是 if open > ma20: passorder(..., quickTrade=1),则是合理的(利用开盘价判断,开盘即买入)。