问题描述
get_stock_list是现在去筛选好的股,对于过去的时间已经是结果了,这时候再回测,就等于用了未来函数?
解决方案
核心解答
是的,完全正确。 如果您在“现在”这个时间点筛选好了一个固定的股票列表(例如现在的上证50成分股,或者现在某个概念板块的股票),然后把这个固定的列表直接写死在代码里,去回测过去几年的历史数据,这绝对属于使用了未来函数(具体来说是引入了“幸存者偏差”和“未来数据”)。
为什么这算未来函数?
在量化回测中,最核心的原则是:在历史的任意一个时间点 $T$,策略只能使用 $T$ 时刻及之前能够获取到的信息。
如果您用今天的选股结果去回测5年前的市场:
- 成分股变动:5年前的指数成分股和今天完全不同。很多今天表现优秀的股票,在5年前可能根本不在该指数里;而5年前在指数里但后来暴跌被剔除的股票,被您完美避开了。
- 幸存者偏差:您现在能选出来的股票,意味着它们至少“存活”到了今天(没有退市)。这无形中剔除了历史上那些已经退市的失败公司,导致回测收益被严重高估。
JoinQuant (聚宽) 中的正确做法
为了避免这种未来函数,JoinQuant 平台在设计 API 时已经做好了防范机制。在 JoinQuant 中,获取股票池通常使用以下函数:
get_index_stocks(index_symbol, date=None):获取指数成分股get_industry_stocks(industry_code, date=None):获取行业成分股get_concept_stocks(concept_code, date=None):获取概念成分股
关键点在于 date 参数:
- 在回测模块中,如果您不传入
date参数(即date=None),系统会自动将date设定为回测当前的逻辑时间(context.current_dt)。 - 这意味着,当回测引擎运行到 2018 年 1 月 1 日时,
get_index_stocks('000300.XSHG')返回的是 2018 年 1 月 1 日当天的沪深300成分股,而不是今天的成分股。
代码示例对比
❌ 错误做法(包含未来函数)
在策略外部或者初始化时写死了一个现在的股票列表:
def initialize(context):
# 错误!这是写死了现在的股票,回测过去时就是未来函数
g.my_stocks = ['000001.XSHE', '600519.XSHG', '000858.XSHE']
def handle_data(context, data):
# 每天都在交易这几只固定的股票
for stock in g.my_stocks:
order_target_value(stock, 10000)
✅ 正确做法(动态获取,无未来函数)
在定时运行函数或 handle_data 中,动态获取当天的成分股:
def initialize(context):
# 开启避免未来数据模式(强烈建议)
set_option("avoid_future_data", True)
# 每天开盘前动态更新股票池
run_daily(update_stock_list, time='09:00')
def update_stock_list(context):
# 正确!在回测中,这里会自动获取 context.current_dt 当天的沪深300成分股
g.my_stocks = get_index_stocks('000300.XSHG')
log.info(f"当前日期的股票池数量: {len(g.my_stocks)}")
def handle_data(context, data):
# 交易当天真实的成分股
for stock in g.my_stocks:
# 交易逻辑...
pass
总结建议
- 永远不要在回测中硬编码(Hardcode)当前的选股结果。
- 充分利用 JoinQuant 提供的
get_index_stocks等 API 动态获取历史当天的成分股。 - 在
initialize中加入set_option("avoid_future_data", True),让平台帮您拦截可能引入未来数据的 API 调用。