问题描述
解决方案
这是一个非常关键的量化交易问题。在回测中出现“未来数据”(Look-ahead Bias)通常是因为信息获取的时间点与实际发生的时间点不匹配造成的。
针对你的问题,核心原因在于财务数据的“报告期”与“公告日”存在时间差。
1. 为什么回测用到了未来数据?
在现实市场中,上市公司的财务报表(如一季报)虽然统计的是截至 3 月 31 日(报告期)的数据,但实际发布给公众的时间(公告日)通常在 4 月份。
-
错误的做法(导致未来函数): 按照**报告期(Report Date)**取数。
- 场景:回测时间走到 4 月 1 日。
- 逻辑:策略去读取“报告期为 3 月 31 日”的财报数据。
- 结果:策略在 4 月 1 日就拿到了实际上 4 月 25 日才发布的净利润数据,并据此交易。这就是典型的利用未来数据作弊。
-
正确的做法: 按照**公告日(Announcement Date)**取数。
- 场景:回测时间走到 4 月 1 日。
- 逻辑:策略查询“截至当前日期已公告”的最新财报。
- 结果:由于一季报尚未公告,策略只能读到去年的年报数据。直到回测时间走到 4 月 25 日(公告日),策略才能读取到一季报数据。
2. 财务数据应该按公告日还是报告期取?
结论:在回测和实盘策略中,必须按“公告日(announce_time)”取数据。
只有在进行纯粹的财务分析(不涉及回测交易,仅研究企业历史经营状况)时,才可能会用到报告期。
3. QMT 中的具体实现
在 QMT 的 Python API 中,获取财务数据的核心函数是 ContextInfo.get_financial_data。该函数有一个关键参数 report_type。
-
report_type='announce_time'(默认值/推荐):- 按照数据的公告日期为区分取数据。
- 这是防止未来函数的正确设置。
- 逻辑:若某公司当年 4 月 26 日发布上年度年报,则在 4 月 26 日之前,取到的都是更早一期的财报数据。
-
report_type='report_time'(慎用):- 按照报告期取数据。
- 如果在回测中使用此模式,极大概率会导致未来数据泄露。
代码示例
以下代码展示了如何在 QMT 中正确获取财务数据以避免未来函数:
# -*- coding: gbk -*-
def init(ContextInfo):
# 设置股票池
ContextInfo.set_universe(['600000.SH'])
# 财务字段:净利润 (ASHAREINCOME.net_profit_incl_min_int_inc)
ContextInfo.field = 'ASHAREINCOME.net_profit_incl_min_int_inc'
def handlebar(ContextInfo):
# 获取当前K线的时间
current_bar_time = ContextInfo.get_bar_timetag(ContextInfo.barpos)
# 转换为字符串格式,用于财务数据查询接口
current_date_str = timetag_to_datetime(current_bar_time, '%Y%m%d')
# ------------------------------------------------------------------
# 正确做法:按公告日取数 (report_type='announce_time')
# ------------------------------------------------------------------
# 即使当前是4月10日,如果一季报没出,这里取到的还是去年年报的数据,符合真实交易场景
fin_data_safe = ContextInfo.get_financial_data(
[ContextInfo.field], # 字段列表
['600000.SH'], # 股票列表
current_date_str, # 开始时间
current_date_str, # 结束时间
report_type='announce_time' # 【关键】设置为公告日
)
# ------------------------------------------------------------------
# 错误做法:按报告期取数 (report_type='report_time')
# ------------------------------------------------------------------
# 如果当前是4月10日,这里可能会直接取到3月31日的一季报数据(实际上当时还没发),导致未来函数
fin_data_future = ContextInfo.get_financial_data(
[ContextInfo.field],
['600000.SH'],
current_date_str,
current_date_str,
report_type='report_time' # 【危险】回测中不要使用
)
# 打印对比(仅在回测日志中查看)
if ContextInfo.is_new_bar():
print(f"日期: {current_date_str}")
if not fin_data_safe.empty:
print(f" [安全数据-公告日] 净利润: {fin_data_safe.iloc[0][0]}")
if not fin_data_future.empty:
print(f" [未来数据-报告期] 净利润: {fin_data_future.iloc[0][0]}")
4. 总结
- 现象:回测收益率异常高,或者买入了当时实际上看起来并不好的股票(但在未来发布了利好财报)。
- 原因:使用了
report_type='report_time'或者在处理数据时错误地对齐了时间轴。 - 修正:在调用
ContextInfo.get_financial_data时,务必保持默认参数report_type='announce_time'或显式指定它。
Q&A
Q: 如果我使用 get_market_data_ex 获取行情数据,会有未来函数吗?
A: 通常不会。get_market_data_ex 获取的是开高低收等行情数据,这些数据在收盘时即已确定。但如果在日线级别的回测中,你在当天的 handlebar(即当天收盘前)使用了当天的 close 价格来决定是否在当天开盘买入,这属于逻辑上的未来函数。正确的做法是使用昨日的收盘价决定今日的交易,或者使用当天的收盘价决定明日的交易。
Q: 为什么有时候按公告日取数,数据会有滞后?
A: 这不是滞后,而是市场的真实情况。例如,2022 年年报可能在 2023 年 4 月 28 日才发布。在 2023 年 1 月到 4 月 27 日期间,市场上可用的最新经审计数据确实只有 2022 年的三季报或 2021 年的年报。策略必须基于“当时已知”的信息运行。
Q: 财务数据中的 m_timetag 和 m_anntime 字段有什么用?
A: 在 QMT 的财务数据表中,m_timetag 通常代表报告截止日(如 3月31日),而 m_anntime 代表公告日(如 4月25日)。如果你直接读取底层数据表进行处理,必须使用 m_anntime 作为数据生效的时间戳来对齐行情。