问题描述
就比如格力电器的例子,如果回测当天是 2023-05-01,当天是能看到 董事会预案的结果但看不到除权除息日的结果的(因为相对回测当天还未发生),但实际上bonus_ratio_rmb 已经是 实施方案公告 和 除权除息日都过去了且确定的结果了,而不是董事会预案的结果,因为已经被更新修改了?
解决方案
核心解答:是的,如果不加限制地查询,极易引入未来函数!
您理解得非常准确。在量化回测中,分红数据(如 bonus_ratio_rmb)是未来函数的重灾区。以您提到的格力电器为例,如果您在 2023-05-01(仅有董事会预案)去查询该报告期的分红数据,如果不做严格的日期过滤,您获取到的很可能是数据库中已经被更新为“最终实施方案”的数据。这就意味着您在预案日“提前”知道了未来除权除息日的确定结果,这就是典型的未来函数。
为什么会产生这种未来函数?
在底层数据库设计中,一次分红事件通常对应数据表(如 finance.STK_DIVIDEND)中的一条记录。这条记录会随着事件的推进(董事会预案 -> 股东大会决议 -> 实施公告 -> 除权除息)被不断更新(Update)。
如果您在回测时,仅仅通过 code(股票代码)和 report_date(报告期)去查询,数据库返回的是当前这条记录的最新状态。因此,在 2023-05-01 的回测时点,您拿到了未来才确定的 bonus_ratio_rmb。
如何在 JoinQuant 中规避分红数据的未来函数?
为了保证回测的真实性,JoinQuant 提供了多种机制来规避未来函数,您需要按照以下规范进行操作:
1. 严格使用 pub_date(公告日)进行过滤
当您使用 finance.run_query 查询分红等财务/事件数据时,绝对不能只按报告期查询,必须加上 pub_date 的限制。您需要确保查询的 pub_date 小于等于回测的当前逻辑时间(context.current_dt)。
from jqdata import *
def handle_data(context, data):
# 正确做法:限制公告日必须在回测当前时间之前
q = query(finance.STK_DIVIDEND).filter(
finance.STK_DIVIDEND.code == '000651.XSHE',
finance.STK_DIVIDEND.pub_date <= context.current_dt.date()
).order_by(finance.STK_DIVIDEND.pub_date.desc()).limit(1)
df = finance.run_query(q)
if not df.empty:
# 这样拿到的是截至回测当天,市场上真实披露的最新分红进度数据
print(df['bonus_ratio_rmb'].values[0])
2. 开启聚宽的防未来函数模式
JoinQuant 提供了全局的防未来函数开关。在 initialize 函数中开启此选项,系统会在您调用 API 时自动检测并剔除未来数据(对于包含时间参数的 API)。
def initialize(context):
# 开启避免未来数据模式
set_option("avoid_future_data", True)
注意:avoid_future_data 主要针对 get_price、get_fundamentals 等标准 API,对于 finance.run_query 的自定义查询,仍需您自己把控 pub_date。
3. 依赖系统自动处理(推荐)
如果您只是想在策略中正确处理分红除权对账户资金和持仓的影响,您完全不需要自己去查分红表。JoinQuant 的回测引擎已经内置了完善的处理机制:
-
开启动态复权(真实价格模式):
set_option('use_real_price', True)开启后,每到新的一天,如果持仓中有股票发生了送股或者分红,对应的股数/现金会自动在账户(
context.portfolio)中体现,完全符合真实交易逻辑,绝无未来函数。 -
使用事件回调
on_event:
如果您需要在分红发生时执行特定逻辑,可以使用DividendsEvent事件回调,系统会在真实的除权除息日触发该事件。
总结
在量化回测中,“预案”不等于“实施”。直接查询分红比例字段极易引入未来函数。正确的做法是:要么严格利用 pub_date 限制查询范围,要么直接信任并使用量化平台底层的真实价格撮合与自动分红派息机制。