← 返回目录
p1.3动手⏱ 约 16 分钟

价格序列的四个陷阱

复权、除权、停牌、退市——它们不在你的代码里,在数据里

🔎 最后验证 2026-07📚 来源:四个陷阱于 2026-07-31 在 ECS 实测(样本股 2005-01-04~2026-07-29 共 5238 个交易日),输出见 code/outputs/stdout.txt🧰 baostock、pandas 3.0.5、Python 3.12.13
为什么学这个

前两节我们把代码里的坑堵住了:未来函数有两把尺子守着,数据源有降级链兜着。

这一节的四个坑不一样——它们不在你的代码里,在数据里

先给你一个数字:同一只股票、同一段 21 年,用不复权价算,累计涨 3524%;用后复权价算,累计涨 16993%

同一只标的,同一段时间,两个都是"真实的历史价格"。差了将近 5 倍。

代码写得再干净,口径拿错,结论一样是错的。

💡 打个比方

量身高,有人光脚量,有人穿鞋量,有人踮着脚量。三个数字都是"真实测出来的"。

问题不在于哪个尺子准,而在于:你有没有搞清楚自己用的是哪一种,以及它和你要回答的问题匹不匹配。

陷阱一:复权口径

同一只样本股(2005-01-04 ~ 2026-07-29,5238 个交易日),三种口径:

口径首日收盘末日收盘期间累计涨幅
不复权36.451321.003524.1%
后复权59.2710131.0916993.0%
前复权7.731321.0016993.0%

用不复权价算收益,比用后复权少算 371.6%——少掉的全是被除权"吃掉"的分红。

注意前复权和后复权的累计涨幅完全相同(都是 16993.0%),因为它们只是同一条乘数序列选了不同的锚点。那为什么回测要用后复权?

⚠️ 避坑

因为前复权序列会变。

前复权把今天的价格当锚点,往前推算历史。所以每发生一次新分红,整条历史序列都要重算一遍——你昨天存下来的 CSV,今天就对不上了。

这会直接击穿判据七(可复现):同一段代码、同一个标的,今天跑和下个月跑,结果不一样,而你查不出原因。

后复权把历史当锚点,历史一旦发生就不再改变。回测用后复权,展示给人看用前复权(因为前复权的价格和行情软件上看到的一致)。这是行业惯例,也是本课的默认。

陷阱二:除权日的假跌幅

除权日当天,不复权价格会凭空跳空下跌——那不是跌,是分红从股价里划出去了。

实测:两种口径日收益差超过 0.5% 的交易日共 26 天,几乎全是除权日。最大的几例:

日期不复权当日收益后复权当日收益
2006-05-25-56.69%-11.96%-44.73pp
2005-08-05-15.96%+1.77%-17.73pp
2014-06-25-13.05%-1.72%-11.33pp
2015-07-17-9.26%+1.58%-10.84pp
2011-07-01-9.88%+0.22%-10.10pp

看 2005-08-05 和 2015-07-17 这两行:真实是涨的,不复权口径下却是大跌

26 天合计,不复权序列平白多记了 125.3 个百分点的"跌幅"。

后果不只是收益算错:

  • 止损规则会被这些假跌幅误触发
  • 动量因子会把除权日误判成暴跌
  • 风控指标(回撤、下行波动)全部被污染

🔧 动手做:用不复权价算一次收益,见识满屏"假崩盘"(5 分钟)

python price_traps.py,对比同一只股票的不复权价和后复权价。

你会看到(实测):同一只股 21 年,不复权累计 3524%、后复权 16993%——不复权少算了近 13000 个百分点。更要命的是除权日的假跌幅:2006-05-25 那天,不复权显示 -56.69%,而后复权只是 -11.96%——那 44 个百分点的"暴跌"根本不存在,是分红除权造成的账面假象。全样本这样的假跌幅有 26 天、合计 125pp。

想明白:复权/除权/停牌/退市这些坑不在你的代码里,在数据里——代码写得再干净,拿错口径的价格,结论一样是错的。算收益必须用复权价;看到某天"暴跌 50%",先查是不是除权日。

陷阱三:停牌——数据里有这一行,但那天不能交易

这个坑最阴。我原本以为停牌日会在数据里"缺席",实测结果是:

指数交易日 5238 天,该股有数据 5238 天,缺 0 天
⚠️ 但 tradestatus=0(停牌)的行有 68 行 —— 数据在,交易不在
   其中 68/68 天的日收益恰好为 0(价格=前收盘,是占位不是行情)

数据源一天都没少给你,但其中 68 天是占位符。价格是前一天的收盘价,收益恰好为 0。

如果你不看 tradestatus 这个字段,这 68 天会被当成 68 个"平盘日":

含停牌日的年化波动率 31.90%  vs  剔除后 32.11%  (相差 0.21pp)

波动率被稀释 0.21 个百分点——每一个假的 0 收益,都在悄悄把你的风险指标往下压

而更严重的是回测撮合:停牌日你的引擎会照常"成交",现实里你根本下不了单。P4 章的撮合层必须显式处理这件事。

三种处理方式,三种错误

做法错在哪
① 直接丢弃停牌日停牌期间的风险凭空消失,回撤被低估
② 填成 0 收益等于假设停牌期间"什么都没发生"——而停牌往往正因为出了事
③ 用复牌首日补记最接近真实,但会造成单日巨幅收益,须在风控里单独处理

本课选 ③,并在引擎里给复牌首日打标记。没有完美选项,只有"你知道自己选了哪个"。

陷阱四:退市与幸存者偏差

baostock 全量股票 5540 只:在市 5203 只,已退市/停止上市 337  (6.1%)

2019 年以来各年退市数量:
  2019: 11    2020: 16    2021: 20
  2022: 43    2023: 45    2024: 52    2025: 30    2026: 19

退市数量在系统性上升:2022 年起每年 40+ 只,是 2015-2018 年均值(约 4.75 只/年)的 7 倍以上。

如果你的股票池是"今天还在交易的股票",你就自动删掉了 337 只死掉的样本。这些样本恰好是表现最差的那一批——它们不是随机消失的。

后果:任何全市场回测都会系统性高估收益。而且这个偏差不会体现在任何指标上,它只是让你的整个样本变得比历史更好。

P5 章做选股时,必须用当时的成分股名单,而不是今天的。这是幸存者偏差在数据层的解法。

❓ 测验
你的回测用了前复权价格,代码、数据源、随机种子全部固定。三个月后重跑同一段代码,结果和上次不一样。最可能的原因是?
✏️ 填空
停牌日的数据往往不会缺席,而是以前收盘价占位。要识别它们,必须检查数据源的 ___ 字段(baostock 里为 tradestatus)。

📌 免责:本课为技术教学。本节需要一只长期分红的个股来演示除权机制(指数不除权),代码中的样本股仅因分红次数多、跳空明显而被选中,与其投资价值无关,不构成任何投资建议。

✅ 小结

四个陷阱,四个实测数字:

  • 复权:不复权 3524% vs 后复权 16993%,差近 5 倍;回测一律用后复权(前复权会变,击穿可复现)
  • 除权:26 天假跌幅合计 125.3pp,其中最大一天 -56.69%(真实是 -11.96%)
  • 停牌:68 行数据在、交易不在,68/68 收益恰为 0,波动率被稀释 0.21pp
  • 退市:337 只已退市(6.1%),2022 年起每年 40+,用"今天还在的股票"回测必然高估

它们的共同点:代码是干净的,错在数据口径。所以 p0.3 那两把尺子抓不到它们——这类错误只能靠知道它们存在,以及下一节要做的数据体检来防。

下一节讲交易日历与多频率对齐:不同市场、不同频率的数据怎么对齐才不出错,以及为什么"用 pandas 的 date_range 生成交易日"是一个会让你付出代价的偷懒。

下一节 → 交易日历与多频率对齐
🔎 来源与核验· 5 条,点开核对
本节每个关键论断都对应一个可追溯的来源 —— 这是本课程"靠谱、不过时"的底线。
「样本股 2005-01-04~2026-07-29(5238 个交易日):不复权累计 3524.1%,后复权 16993.0%,前复权 16993.0%;不复权比后复权少算 371.6%」
📚 本节 code/price_traps.py,2026-07-31 于 ECS 实测(baostock adjustflag=1/2/3),输出见 code/outputs/stdout.txt✓ 已核验 2026-07
「两口径日收益差 >0.5% 的交易日 26 天,合计假跌幅 125.3pp;最大一例 2006-05-25 不复权 -56.69% vs 后复权 -11.96%」
📚 同上实测✓ 已核验 2026-07
「该股 tradestatus=0(停牌)68 行,数据未缺席;68/68 日收益恰为 0;含停牌日年化波动率 31.90% vs 剔除后 32.11%」
📚 同上实测✓ 已核验 2026-07
「baostock query_stock_basic 全量股票 5540 只:在市 5203、已退市/停止上市 337(6.1%);2022 年 43 只、2023 年 45 只、2024 年 52 只」
📚 同上实测(type=1 过滤指数与基金)✓ 已核验 2026-07
「回测应使用后复权:前复权以当前价为锚点,新分红会导致历史序列整体重算,破坏可复现性」
📚 复权机制通识 + baostock/akshare 复权口径文档✓ 已核验 2026-07
退市统计以 baostock 数据口径为准,不同数据源对「停止上市」的定义可能略有差异。
智图软件的赞赏码
都看到这了,打个赏呗!
接下来 · p1.4
交易日历与多频率对齐
继续读下一节 →