问题描述
解决方案
在聚宽(JoinQuant)平台进行回测时,处理退市股票和避免幸存者偏差是构建稳健策略的关键环节。
以下是关于这两个问题的详细解答及代码实现方案。
1. 如何避免幸存者偏差?
什么是幸存者偏差?
如果在回测时,只使用当前还在上市的股票作为股票池,那么你就人为地剔除了历史上那些因为业绩差而退市的股票。这会导致回测结果虚高,因为你实际上是在用“胜利者”的数据去验证历史。
在聚宽中如何避免?
聚宽的数据已经包含了退市股票的数据,要避免幸存者偏差,关键在于不要使用固定的股票列表,而是使用动态的股票池。
- 错误做法:手动写死一个列表,如
g.stocks = ['000001.XSHE', '600519.XSHG', ...](这些是现在还活着的股票)。 - 正确做法:使用 API 获取历史当时存在的股票。例如使用
get_index_stocks或get_all_securities配合date参数。
2. 持有的股票退市了该怎么处理?
在回测引擎中,如果股票退市,其价格数据将停止更新,你将无法在二级市场将其卖出。为了防止这种情况,策略应当包含过滤机制:
- 选股阶段过滤:在买入前,剔除 ST 股、*ST 股、以及名称中带有“退”字的股票。
- 持仓监控:检查持仓股票的退市日期,在退市前强制卖出。
3. 代码实现
以下是一个完整的策略代码示例,展示了如何动态获取股票池(避免幸存者偏差)以及如何过滤高风险和即将退市的股票。
# -*- coding: utf-8 -*-
from jqdata import *
import datetime
def initialize(context):
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 每天开盘前运行选股和风控
run_daily(before_market_open, time='09:00')
# 每天开盘时运行交易
run_daily(market_open, time='09:30')
def before_market_open(context):
# 1. 获取动态股票池(避免幸存者偏差的关键)
# 获取当前日期沪深300的成分股,这个列表是随时间动态变化的
# 历史上退市的股票,在它们还在指数里的时候,会被包含进来
security_list = get_index_stocks('000300.XSHG', date=context.current_dt)
# 2. 过滤高风险股票(ST、退市整理、停牌)
g.buy_list = filter_risk_stocks(context, security_list)
def market_open(context):
# 卖出不在待买入列表中的股票
for stock in context.portfolio.positions:
if stock not in g.buy_list:
order_target(stock, 0)
log.info("卖出不再符合条件的股票: %s" % stock)
# 买入逻辑(示例:等权重买入)
if len(g.buy_list) > 0:
position_count = len(g.buy_list)
value_per_stock = context.portfolio.available_cash / position_count
for stock in g.buy_list:
order_value(stock, value_per_stock)
def filter_risk_stocks(context, security_list):
"""
过滤掉 ST、*ST、退市整理期、停牌、以及即将退市的股票
"""
# 获取当前单位时间的涨跌停价, 是否停牌等信息
current_data = get_current_data()
valid_stocks = []
for stock in security_list:
# 1. 过滤停牌股票
if current_data[stock].paused:
continue
# 2. 过滤 ST 和 *ST 股票
# is_st 属性包含 ST 和 *ST
if current_data[stock].is_st:
continue
# 3. 过滤名称中带有 '退' 字的股票 (退市整理期)
if '退' in current_data[stock].name:
continue
# 4. 过滤即将退市的股票 (高级风控)
# 获取股票信息
info = get_security_info(stock)
# 如果 end_date 不是 2200-01-01,说明该股票有确定的退市日期
if info.end_date < datetime.date(2200, 1, 1):
# 如果距离退市日期不足 30 天,则剔除
# 注意:context.current_dt 是 datetime 对象,info.end_date 是 date 对象
days_to_delist = (info.end_date - context.current_dt.date()).days
if days_to_delist < 30:
log.info("股票 %s 即将退市(剩余%s天),剔除。" % (stock, days_to_delist))
continue
valid_stocks.append(stock)
return valid_stocks
核心逻辑解析
-
动态股票池 (
get_index_stocks):- 代码中使用
get_index_stocks('000300.XSHG', date=context.current_dt)。 - 原理:这个函数会返回回测当时那个日期的指数成分股。如果 2016 年某只股票在沪深 300 里,后来退市了,回测运行到 2016 年时,它依然会被选入。这就是还原历史真实情况,避免了只选“现在还活着的股票”的幸存者偏差。
- 代码中使用
-
基础过滤 (
get_current_data):current_data[stock].paused: 剔除停牌股,避免资金被锁。current_data[stock].is_st: 剔除 ST 股,通常 ST 股风险较高,且容易退市。'退' in current_data[stock].name: 显式剔除处于退市整理期的股票。
-
高级退市防御 (
get_security_info):get_security_info(stock).end_date: 聚宽数据库中记录了股票的退市日期。- 如果
end_date小于 2200 年,说明该股票在未来某一天会退市。 - 策略判断
(info.end_date - context.current_dt.date()).days < 30,即如果距离退市不到 30 天,坚决不买入(或卖出),从而避免持有至退市导致资产归零或无法交易。
Q&A
Q: 为什么使用 get_all_securities 也可以避免幸存者偏差?
A: get_all_securities(date=context.current_dt) 会返回指定日期当天所有上市的股票。只要传入了 date 参数,它就不会只返回当前存在的股票,而是返回历史上那一刻存在的股票(包含后来退市的)。
Q: 如果我持有的股票突然停牌了,复牌后直接退市怎么办?
A: 这是极端的黑天鹅事件。在回测中,如果股票一直停牌直到退市,资金确实会被锁死。但在实际操作中,通常会有 ST 戴帽等前兆。上述代码中的 is_st 检查通常能提前将这类股票过滤掉。
Q: get_price 获取的数据包含退市股票吗?
A: 包含。只要在查询的时间段内该股票是上市交易的,get_price 就能获取到数据。聚宽的数据清洗原则是保留历史真实数据。