🚀 AI 一键生成 joinquant 策略代码

JoinQuant 回测严谨性指南:如何正确处理退市股票以避免幸存者偏差

joinquant | 发布时间: 2025/12/11
以下内容由 EasyQuant 生成。

问题描述

做历史回测时,如何获取那些已经退市的股票数据,来避免只用当前正常交易的股票产生偏差?

解决方案

在聚宽(JoinQuant)平台进行历史回测时,平台默认已经包含了退市股票的数据(涵盖2005年以来的A股数据)。

要避免幸存者偏差(Survivorship Bias),关键在于如何构建股票池(Universe)。你不能使用“当前”的股票列表去回测历史,而必须使用“历史当时”的股票列表。

以下是具体的方法和代码示例:

核心方法

  1. 使用 get_index_stocks 获取历史成分股(推荐)
    这是最常用的方法。通过传入 date 参数,你可以获取某一指数在历史某一天的真实成分股列表。如果某只股票当时在指数内,即使现在已退市,也会被包含在内。

  2. 使用 get_all_securities 获取历史全市场股票
    通过指定 date 参数,可以获取在该日期处于上市状态的所有股票列表。

  3. 直接查询数据
    对于已知的退市股票代码(如 000003.XSHE),可以直接使用 get_priceattribute_history 获取其在市期间的行情数据。


代码实现示例

以下是一个完整的策略示例,展示如何在回测中动态获取历史成分股(包含当时的退市股)并获取其数据。

# -*- coding: utf-8 -*-
from jqdata import *

def initialize(context):
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    
    # 每天开盘前运行,更新股票池
    run_daily(before_market_open, time='09:00')

def before_market_open(context):
    # 【关键步骤】获取“当前回测日期”的沪深300成分股
    # 如果 context.current_dt 是 2015年某天,这里返回的就是2015年那天的成分股
    # 其中可能包含后来退市的股票
    current_date = context.current_dt.date()
    g.security_list = get_index_stocks('000300.XSHG', date=current_date)
    
    # 打印日志验证(可选)
    # log.info(f"日期: {current_date}, 股票池数量: {len(g.security_list)}")

def handle_data(context, data):
    # 获取股票池中所有股票的收盘价
    # 如果股票池中包含退市股,只要在它退市前,get_price 都能正常返回数据
    # panel=False 返回 DataFrame
    df = get_price(g.security_list, 
                   end_date=context.current_dt, 
                   frequency='daily', 
                   fields=['close'], 
                   count=1, 
                   panel=False)
    
    # 简单的逻辑示例:买入股票池中的前5只股票
    buy_list = g.security_list[:5]
    
    # 资金分配
    if len(buy_list) > 0:
        cash_per_stock = context.portfolio.available_cash / len(buy_list)
        for stock in buy_list:
            # 过滤掉停牌或数据缺失的股票
            if stock in df['close'].index and not isnan(df['close'][stock][0]):
                order_value(stock, cash_per_stock)

详细说明

1. 为什么这样能避免偏差?

如果你在代码中写死股票列表(例如 g.stocks = ['000001.XSHE', ...]),或者使用不带日期的 get_index_stocks('000300.XSHG')(这会默认取今天的成分股),那么你回测2015年时,用的就是2023年的“幸存者”名单,表现差被剔除或退市的股票就被忽略了,导致回测虚高。

使用 date=context.current_dt,平台会去查询历史数据库,还原当时的真实市场情况。

2. 如何获取全市场历史股票?

如果你不局限于指数成分股,想全市场选股,可以使用 get_all_securities

# 获取回测当日还在上市的所有股票信息
all_stocks_info = get_all_securities(types=['stock'], date=context.current_dt)
# 提取股票代码列表
g.security_list = list(all_stocks_info.index)

3. 退市后的数据表现

  • 退市前: 数据正常获取。
  • 退市后: 如果你在策略中请求该股票退市日期之后的数据:
    • get_price 会返回 NaN(空值)。
    • attribute_history 会返回 NaN
    • get_current_data()[stock].paused 会返回 True(表示停止交易)。

常见问题 Q&A

Q: 我需要专门去下载退市股票的数据包吗?
A: 不需要。聚宽的云端数据库已经内置了所有历史退市股票的数据,只要通过 API 正确调用即可。

Q: 如果我持有的股票在回测过程中退市了,系统怎么处理?
A: 在聚宽回测引擎中,如果持仓股票退市,通常无法再进行卖出操作(因为没有后续行情撮合),这笔资产会留在账户中直到回测结束。在编写策略时,建议通过 get_current_data() 检查股票状态,或利用 get_all_securities 中的 end_date(退市日期)提前进行风控平仓。

Q: get_price 获取退市股票数据时需要设置 skip_paused 吗?
A: 视情况而定。

  • skip_paused=False (默认):如果股票已退市,查询退市后的日期会用 NaN 填充。
  • skip_paused=True:会跳过退市后的日期,只返回有交易数据的行。如果请求的时间段完全在退市后,可能返回空 DataFrame。

Q: 如何查询某只股票具体的退市日期?
A: 使用 get_security_info 函数:

info = get_security_info('000003.XSHE')
print(info.end_date) # 输出退市日期,未退市则为 2200-01-01