🚀 AI 一键生成 joinquant 策略代码

JoinQuant 回测中如何处理已退市股票?避免幸存者偏差的编程实践

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

问题描述

在回测过程中,如果我持有的股票退市了,该怎么处理?如何避免幸存者偏差?

解决方案

在聚宽(JoinQuant)平台进行回测时,处理退市股票和避免幸存者偏差是构建稳健策略的关键环节。

以下是关于这两个问题的详细解答及代码实现方案。

1. 如何避免幸存者偏差?

什么是幸存者偏差?
如果在回测时,只使用当前还在上市的股票作为股票池,那么你就人为地剔除了历史上那些因为业绩差而退市的股票。这会导致回测结果虚高,因为你实际上是在用“胜利者”的数据去验证历史。

在聚宽中如何避免?
聚宽的数据已经包含了退市股票的数据,要避免幸存者偏差,关键在于不要使用固定的股票列表,而是使用动态的股票池

  • 错误做法:手动写死一个列表,如 g.stocks = ['000001.XSHE', '600519.XSHG', ...](这些是现在还活着的股票)。
  • 正确做法:使用 API 获取历史当时存在的股票。例如使用 get_index_stocksget_all_securities 配合 date 参数。

2. 持有的股票退市了该怎么处理?

在回测引擎中,如果股票退市,其价格数据将停止更新,你将无法在二级市场将其卖出。为了防止这种情况,策略应当包含过滤机制

  1. 选股阶段过滤:在买入前,剔除 ST 股、*ST 股、以及名称中带有“退”字的股票。
  2. 持仓监控:检查持仓股票的退市日期,在退市前强制卖出。

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

核心逻辑解析

  1. 动态股票池 (get_index_stocks):

    • 代码中使用 get_index_stocks('000300.XSHG', date=context.current_dt)
    • 原理:这个函数会返回回测当时那个日期的指数成分股。如果 2016 年某只股票在沪深 300 里,后来退市了,回测运行到 2016 年时,它依然会被选入。这就是还原历史真实情况,避免了只选“现在还活着的股票”的幸存者偏差。
  2. 基础过滤 (get_current_data):

    • current_data[stock].paused: 剔除停牌股,避免资金被锁。
    • current_data[stock].is_st: 剔除 ST 股,通常 ST 股风险较高,且容易退市。
    • '退' in current_data[stock].name: 显式剔除处于退市整理期的股票。
  3. 高级退市防御 (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 就能获取到数据。聚宽的数据清洗原则是保留历史真实数据。