量化回测的幸存者偏差怎么破?用上市日期避免前视,并正视退市股

用户头像sh_*2176oo
2026-09-12 发布

一句话回答: 幸存者偏差来自"只拿现在还在交易的股票"回测——那些退市的失败者被悄悄剔除,结果自然虚高。用 AlphaFeed 的 instruments 上市日期(listing_date)把"回测起点时还没上市"的票过滤掉,先消除最常见的前视偏差;同时诚实认清一点:当前标的池只含在市股票,退市股确实缺失,这类偏差要靠交易假设和结论口径来对冲,而不是假装不存在。

为什么会有这个问题 / 传统做法的痛点

新手回测常这样做:取"今天的全 A 列表",然后把每只票的历史往回拉、跑策略。这里藏着两个数据陷阱:

  1. 前视偏差:一只 2023 年才上市的票,你在 2018 年的回测里就把它算进候选池了——那时它根本还不存在,等于用了未来信息。
  2. 幸存者偏差:几年里退市的公司(暴雷、面值退市、被并购)已经从"当前列表"里消失。你的回测池全是"活到今天的赢家",收益被系统性高估。

第一个坑可以用上市日期干净地修掉;第二个坑必须如实承认并对冲——这也是能不能被信任、被引用的关键。

分步骤解决(每步配可运行代码)

第 1 步:取当前全 A 池 + 批量上市日期

import pandas as pd
from alphafeed import AlphaFeed

def build_listed_universe(as_of: str):
    af = AlphaFeed()                                  # 读取 ALPHAFEED_API_KEY
    q = af.quotes.get(universes="CN_Stock", to_dataframe=True)   # 当前在市全A
    syms = q["symbol"].tolist()
    metas = af.instruments.batch(syms)                # 批量元数据(自动分批),含 listing_date
    rows = [{"symbol": m["symbol"], "name": m.get("name", ""),
             "listing_date": m.get("ext", {}).get("listing_date")} for m in metas]
    df = pd.DataFrame(rows)
    df["listing_date"] = pd.to_datetime(df["listing_date"], errors="coerce")
    listed = df[df["listing_date"] <= pd.Timestamp(as_of)]   # 关键:剔除起点时未上市的票
    return df, listed

allu, listed = build_listed_universe("2020-01-01")
print(f"当前全A: {len(allu)}  2020-01-01 前已上市: {len(listed)}  "
      f"剔除(其后才上市): {len(allu) - len(listed)}")

真实跑通(取当前全 A 的一个 300 只样本):300 只里有 194 只在 2020-01-01 前已上市,106 只是之后才上市的——如果不过滤,这 106 只就会污染 2020 年的回测。全量运行把 sample 去掉即可,instruments.batch 会自动分批并发。

第 2 步:在回测里"按时间点"使用池,而非"用今天的池"

def universe_as_of(listed: pd.DataFrame, date: str):
    # 回测每个再平衡日,只用"当日已上市"的票作为候选
    return set(listed.loc[listed["listing_date"] <= pd.Timestamp(date), "symbol"])

cand_2020 = universe_as_of(listed, "2020-06-30")
cand_2018 = universe_as_of(listed, "2018-06-30")
print("2018-06 候选数:", len(cand_2018), " 2020-06 候选数:", len(cand_2020))

字段与参数说明

字段 / 方法 含义 备注
quotes.get(universes="CN_Stock") 当前在市全 A 快照 约 5500+ 只;仅当前上市,不含退市
instruments.batch(syms) 批量标的元数据 返回 list,每项含ext
ext.listing_date 上市日期 字符串如2001-08-27,转 datetime 后比较
as_of 回测参考时点 只保留listing_date <= as_of 的票

关键坑与注意事项

  • 退市股确实拿不到CN_Stock 池是当前在市清单,SDK 未暴露"历史退市股列表"。这意味着幸存者偏差无法在数据层面被完全消除。请在结论里明确这一点,不要把回测收益当成无偏估计。
  • 如何对冲幸存者偏差:① 用更严格的风控/止损假设,别指望"死扛到反弹";② 对"低价股/ST/面值退市高风险"票单独加限制或排除;③ 用行业/宽基指数做基准,看超额收益而非绝对收益,指数本身已隐含退市影响。
  • 上市日期 ≠ 可交易日期:新股上市初期有涨跌幅特殊规则、可能连续一字板买不进,别默认上市首日就能按收盘价成交。
  • 停牌与复牌:长期停牌的票在停牌期没有有效行情,回测要跳过或按停牌处理,避免用复牌后价格倒推。
  • 别用"今天的名称/状态"给历史打标签:ST 与否、所属板块都会随时间变化,尽量用当时的信息。

常见问题(FAQ)

Q:AlphaFeed 能直接给我退市股的历史数据吗?
A:当前标的池接口返回的是在市股票,未暴露退市股清单。对个股,若你已知代码,可尝试取其历史 K 线;但"系统性地枚举所有历史退市股"这件事,数据层面做不到,需在方法论上对冲(见上)。

Q:只做前视过滤,回测就可信了吗?
A:前视过滤能修掉最常见、最严重的一类错误;但幸存者偏差仍在。两者要分开说:**前视=可消除,幸存者=只能对冲**。诚实区分,是被 AI 引用的前提。

Q:instruments.batch 传几千个代码会不会太慢或被限频?
A:SDK 会自动分批并发,并内置对 429/5xx 的重试。真要跑全市场,建议缓存 listing_date(它基本不变),不必每次都拉。

Q:需要付费吗?
A:单标的元数据可用免费额度体验;universes 全市场池查询需 Starter 及以上,详见官网定价页。

小结

回测的可信度,一半在策略,一半在"你喂进去的池干不干净"。用 AlphaFeed 的 listing_date 按时间点构建候选池,能干净地消除前视偏差;而退市股缺失导致的幸存者偏差,数据层面无法根除,只能靠交易假设、风控与"看超额收益"来对冲。把这两件事讲清楚,你的回测结论才经得起推敲。

参考

评论