退市股去哪了
如果你取的是「当前 A 股清单」,那么期间内退市的标的根本不在表里。这些标的当年往往是跌得最惨的一批,把它们排除,等于把亏损从样本里删掉——长期策略的收益会被系统性高估。
ZVT 项目研究站 · 数据避坑
数据在库里,不等于数据能直接用。A 股有六类结构性问题会静默污染结论:停牌与零成交、涨跌停不可成交、ST 与退市、成分股变动、缺失与重复记录、复权口径。这一页把每类问题拆成「现象 → 怎么发现 → 怎么处理 → ZVT 是否代劳」,你可以当成取数后的验收清单。
先看这张表建立全局观,下面几节再逐个展开。
| 问题 | 在数据里的现象 | 对回测的后果 | 处理方式 | ZVT 代劳? |
|---|---|---|---|---|
| 停牌与零成交 | 该标的在交易日历上「缺行」,或成交量为 0 | 把停牌日当交易日,持仓价格凭空延续 | 按交易日历补行并标记停牌;停牌期间不允许成交 | 否 |
| 涨跌停 | 价格等于涨跌停价、成交极稀薄 | 在买不到的价格上买入,收益虚高 | 用前收盘价与涨跌停规则判断可成交性 | 否 |
| ST 与退市 | 当前清单里没有已退市标的 | 幸存者偏差,长期收益被高估 | 保留退市记录或用历史成分股;标记 ST 期间 | 否 |
| 成分股变动 | 指数/板块成分只有当前快照 | 股票池穿越,等于提前知道谁被纳入 | 按历史时点取成分(框架有指数成分表,需自己按时间取) | 部分(提供数据,不自动按时间切) |
| 缺失与重复 | 同一天多行、或关键字段为空 | 统计口径被重复行放大 | 按 entity_id+timestamp 去重;空值单独处理 | 否 |
| 复权口径 | 默认表与后复权表价格量级不同 | 因子与成交价错配,长期收益失真 | 全流程锁定一个口径(见「K 线与复权」页) | 部分(提供口径,需你选对) |
停牌不会报错,只会让某一列悄悄少几行。发现它的标准动作是「和交易日历对齐」。
框架里有交易日历相关的表(聚宽一侧提供 jq_trade_day_recorder)。没有日历表时,至少用指数日线(如沪深 300)的日期序列当作全市场交易日基准。
df = Stock1dHfqKdata.query_data(code='000338', index='timestamp')
missing = set(calendar_dates) - set(df.index) # 疑似停牌日说明:这一步不依赖任何框架特性,纯集合差。预期输出:缺失日期列表——通常就是停牌日。
拿到缺失日期后,在选股与成交判断里先剔除这些日期:停牌期间既不能买也不能卖。不处理的话,回测会假设你在停牌日以某价格成交。
部分数据在停牌日仍有行但成交量为 0。判断条件建议加一条:volume > 0。两条一起用,能覆盖绝大多数情形。
涨停买不到、跌停卖不掉——这是 ZVT 回测里最常见的「看着能赚」的来源。
| 情形 | 为什么不可成交 | 怎么发现 | 处理方式 | 注意点 |
|---|---|---|---|---|
| 一字涨停 | 开盘即封板,全天只有封单 | 开盘价=收盘价=最高价=最低价,且等于涨停价 | 当天不可买入,信号顺延或作废 | 顺延到下一个可成交日的价格可能已经更高 |
| 盘中涨停 | 封板期间挂单排队 | 最高价触及涨停价 | 保守做法:触及涨停即视为不可买入 | 严格做法要看封单量与成交笔数,成本高 |
| 跌停 | 卖出同样排不上队 | 最低价触及跌停价 | 跌停日不可卖出,止损会失效 | 止损回测必须处理这一点,否则回撤被低估 |
| ST 股涨跌幅限制不同 | 不同板块涨跌停幅度不一样(如 ST、科创板、创业板) | 按标的类型与所属板块区分阈值 | 按板块维护不同的涨跌停比例 | 统一按 ±10% 判断会出错 |
| 新股上市初期 | 上市首日与之后的涨跌幅规则不同 | 结合上市日期判断 | 把上市初期单独处理或直接排除 | 新股上市初期的数据特征与常态差异大 |
把这两件事放在一起讲,因为它们造成的是同一种错误:你在用「后来才知道的信息」筛标的——而这正是 ZVT 不会替你拦住的那个错误。
如果你取的是「当前 A 股清单」,那么期间内退市的标的根本不在表里。这些标的当年往往是跌得最惨的一批,把它们排除,等于把亏损从样本里删掉——长期策略的收益会被系统性高估。
排除 ST 是一种常见的、也需要明说的假设:它确实降低了踩雷概率,但同时也排除了「重组翻身」这一类标的。无论选哪种做法,都要在研究报告里写明规则与生效时点,而不是事后挑一个好看的版本。
动态股票池的正确形态是「每个时点各一份名单」:按调仓日重建池子,而不是用一份静态名单跑到底。框架提供了标的清单、指数成分、板块成分这类原料,但按时间切片要你自己做。
这三项检查成本极低,但能拦住一大批「数据脏」导致的问题。建议做成脚本,每次 ZVT 写库后自动跑一遍。
| 检查项 | 怎么写 | 合格线 | 不合格时怎么办 |
|---|---|---|---|
| 行数是否等于交易日数 | 与交易日历取交集比较(停牌日需排除) | 缺行只出现在停牌日 | 补写缺失区间,或确认是否停牌 |
| 是否有重复记录 | 按 entity_id + timestamp 分组计数,看是否有 count>1 | 全为 1 | 按 provider 找出来源,去重并固定单一来源 |
| 关键字段空值率 | 对价格、成交量、财务关键列算 isnull().mean() | 非停牌日价格不应为空 | 空值多的列不要直接用于因子 |
| 时间范围是否符合预期 | 看 min / max 时间戳 | 与标的上市日期、你的需求区间一致 | 补齐区间;确认是否上市前数据 |
| 最近更新时间 | 看最大 timestamp 与当天日期差 | 差 1 个交易日以内 | 重新跑增量更新;检查数据源是否失效 |
| 跨源一致性 | 同一标的两家 provider 的收盘价做差 | 差异在可解释范围内 | 固定一家为基准,差异写进研究记录 |
前半部分回答数据层事实,后半部分是通用研究规范,框架不提供现成功能。
不会。它负责把数据取下来并按统一 schema 存好;停牌日往往表现为「缺行」,涨跌停日则是有行但不可成交。这两件事都需要你在查询与策略层显式处理。
最可靠的做法是与交易日历比对:交易日历有、行情表没有的那几天基本就是停牌。另一个补充条件是成交量为 0。两者结合足够覆盖绝大多数情况。
取决于数据源与取数方式。如果标的还在源的历史接口里,用 code 直接取通常可以拿到上市期间的行情;但如果你的股票池来自「当前清单」,这些标的就不会出现在池子里。这是两件事,别混起来。
没有标准答案,但必须有明确规则并按规则执行。常见做法是:调仓日处于 ST 状态的标的排除,且规则在回测开始前固定。事后修改规则会让结果不可信。
先按 provider 拆开,确认是不是两家源各写了一条;如果是同一源重复,按 entity_id+timestamp 去重并保留最新写入。治本的做法是固定一个 provider 作为研究基准。
日频研究每天收盘后增量更新一次即可(框架是增量写入,重复运行不会重复拉取)。关键是加一个「最大时间戳距今超过 N 个交易日」的告警,而不是靠人记得跑。
ZVT 没有内置数据体检工具。本站建议把上面六项做成一个小脚本,每次写库后自动跑一遍——这也是把「数据能用」和「数据在库里」区分开的最省力方式。