问题描述
解决方案
在量化回测中,未来函数(Look-ahead Bias) 是指在策略回测过程中使用了当时在现实中无法获取的数据(例如在开盘时使用了当天的收盘价,或者在财报发布前使用了财报数据)。这会导致回测结果虚高,实盘惨败。
在 JoinQuant 聚宽平台上,可以通过以下几种方式和最佳实践来避免使用未来数据:
1. 开启防未来数据选项(最推荐)
聚宽提供了一个专门的 API 选项,开启后系统会自动检测并拦截大部分常见的未来数据调用。建议在 initialize 函数中加入:
def initialize(context):
# 开启避免未来数据模式
# 如果尝试获取未来数据(如在当前时间点获取未发生的行情),系统会抛出 FutureDataError 异常
set_option("avoid_future_data", True)
2. 正确获取行情数据
在获取价格数据(开高低收、成交量)时,需注意时间点的对齐:
-
使用
history或attribute_history:
这两个函数默认获取的是前一个单位时间的数据。- 例如:在日频回测的
handle_data(默认 9:30 运行)中调用history(1, '1d', 'close'),获取的是昨天的收盘价,这是安全的。 - 注意:不要在日频策略中试图获取当天的
close,因为在盘中你是无法预知收盘价的。
- 例如:在日频回测的
-
使用
get_price:
如果使用get_price,必须严格控制end_date。- 错误做法:
end_date设置为context.current_dt且包含当天数据。 - 正确做法:
end_date设置为context.previous_date(前一个交易日)。
- 错误做法:
3. 正确获取财务数据
在使用 get_fundamentals 查询财务数据(如市值、PE、净利润)时,不要直接指定季度(statDate),而应该指定查询日期(date)。
- 错误做法(含未来函数):
直接查询statDate='2023q1'。因为 2023 年一季报可能在 4 月底才发布,如果在 4 月 1 日的回测中直接查 Q1 数据,就是使用了未来数据。 - 正确做法:
使用date=context.current_dt。q = query(valuation.pe_ratio).filter(valuation.code == '000001.XSHE') # 系统会自动返回在 context.current_dt 这个时间点之前已经公告的最新数据 df = get_fundamentals(q, date=context.current_dt)
4. 使用真实价格模式(动态复权)
传统的前复权是基于当前(回测结束或最新日期)的复权因子对历史价格进行调整。这意味着历史回测时使用了未来的分红配股信息。
- 建议:开启真实价格模式。
def initialize(context): # 开启动态复权模式,回测中使用当天的真实价格,避免复权因子带来的未来信息 set_option('use_real_price', True)
5. 避免使用全天统计数据进行盘中决策
- 错误:在盘中(如 10:00)使用当天的最高价(High)或最低价(Low)来判断是否交易。因为在 10:00 时,你无法确定今天的最高价是否就是当前的最高价。
- 正确:使用
get_current_data()获取当前的快照数据(如当前最新价last_price),或者使用分钟级数据的history。
综合策略代码示例
下面是一个标准的、避免了未来数据的策略框架示例:
# -*- coding: utf-8 -*-
from jqdata import *
def initialize(context):
# 1. 开启防未来数据检测
set_option("avoid_future_data", True)
# 2. 开启动态复权(真实价格)
set_option('use_real_price', True)
# 设定基准
set_benchmark('000300.XSHG')
# 设定手续费
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
g.security = '000001.XSHE'
# 每天开盘时运行
run_daily(market_open, time='09:30')
def market_open(context):
security = g.security
# 3. 获取行情数据:使用 attribute_history 获取过去5天的数据(不含今天)
# 这在 9:30 是安全的,因为获取的是昨天及之前的数据
h = attribute_history(security, 5, '1d', ['close'])
# 计算5日均线
ma5 = h['close'].mean()
# 4. 获取当前时间点的快照数据(非未来数据)
current_data = get_current_data()
current_price = current_data[security].last_price
# 5. 获取财务数据:指定 date=context.current_dt
# 系统会查找在此日期前已发布的最新财报,避免引用未发布的财报
q = query(valuation.pe_ratio).filter(valuation.code == security)
fund = get_fundamentals(q, date=context.current_dt)
if fund is not None and len(fund) > 0:
pe = fund['pe_ratio'][0]
# 简单的策略逻辑
if current_price > ma5 and pe < 10:
order_value(security, context.portfolio.available_cash)
log.info("买入 %s" % security)
elif current_price < ma5 and context.portfolio.positions[security].closeable_amount > 0:
order_target(security, 0)
log.info("卖出 %s" % security)
Q&A
Q: 为什么使用了 set_option("avoid_future_data", True) 后回测报错了?
A: 这说明您的代码中确实尝试获取了未来数据。例如,您可能在 get_price 中设置了包含当天的 end_date,或者在 get_fundamentals 中直接指定了尚未发布的季度。请根据报错信息调整代码逻辑。
Q: get_current_data() 会包含未来数据吗?
A: 不会。get_current_data() 获取的是当前回测时间点的快照数据(如最新价、涨跌停价、是否停牌)。在日级回测中,如果在 9:30 调用,它返回的是开盘时的状态;如果在盘中调用,返回的是当时的最新状态,符合实盘逻辑。
Q: 为什么说前复权数据包含未来信息?
A: 前复权是利用“现在”的复权因子去调整“过去”的价格。如果在 2015 年回测,使用了基于 2023 年计算出来的复权因子,就等于在 2015 年预知了未来会发生分红配股。虽然对价格走势形态影响不大,但在计算具体买入股数时会产生偏差。使用 set_option('use_real_price', True) 可以解决这个问题。