🚀 AI 一键生成 qmt 策略代码

QMT 策略如何避免回测中的未来函数(Look-ahead Bias)?

qmt | 发布时间: 2025/12/17
以下内容由 EasyQuant 生成。

问题描述

在做策略回测时,如何确保我没有用到未来的数据,让回测结果更真实?

解决方案

在量化策略回测中,**“未来函数”(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_exget_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 的逻辑。因为在实盘中,你无法预知当天的最低价是多少,除非你挂单在那个价格等待成交(但回测很难模拟挂单排队)。建议以 openclose 价格成交。
  • 全样本标准化: 如果你在回测开始前,先把所有历史数据拿来进行 Z-Score 标准化,然后再进行回测,这是严重的未来数据。标准化必须使用滚动窗口(Rolling Window),只利用过去 N 天的数据计算均值和方差。

5. 总结检查清单

在运行回测前,请检查以下几点:

  1. 交易时机: 策略是否是“今日信号,明日开盘成交”?如果是“今日成交”,是否确保了没有利用今日收盘价作为成交价?
  2. 财务数据: get_financial_data 是否使用了 announce_time 模式?
  3. 数据引用: 是否只引用了 barpos 及其之前的数据,没有引用 barpos + 1 的数据?
  4. 偷价行为: 是否设置了合理的滑点(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),则是合理的(利用开盘价判断,开盘即买入)。