一句话结论:A股历史数据库里共有5497只非北交所股票,当前在交易的5225只,272只已退市——幸存率95.1%。只用今天还活着的股票做10年回测,等于把最差的那批从样本里删掉了。加上未来函数、交易成本和滑点,四重失真叠加,回测漂亮是必然的,不是你的策略真的好。
第59课讲了样本外检验,这一课讲让回测失真的四个根本原因,以及青柠如何应对。
一、未来函数(Look-Ahead Bias)
什么是未来函数
最常见的回测bug:用今天收盘价算出的信号,决定今天买入——但收盘价要到当天结束才能知道。
# 错误写法:当日收盘价 > 20日均线就买
df['signal'] = (df['close'] > df['close'].rolling(20).mean())
# 问题:计算信号时已经"看到"了今天的收盘价
# 正确写法:用昨日收盘价的信号,今日开盘执行
df['signal'] = (df['close'].shift(1) > df['close'].rolling(20).mean().shift(1))
换句话说:你在下午3点前不可能知道当天3点整的收盘价,但大量策略代码在回测里假装知道。
A股的常见变体
| 变体 | 描述 | 现实情况 |
|---|---|---|
| 用收盘价生成当日信号 | 最典型 | 收盘价知道时,当天已无法交易 |
| 财报数据提前用 | 一季报3月31日结束,但5月才披露 | 不能在4月1日就用 |
| 技术指标当天计算当天买 | SAR、布林带等 | 当天值要等收盘后才有 |
青柠怎么应对
青柠回测引擎的执行规则:信号日收盘确认,次日开盘买入。这一条就消除了最常见的未来函数错误。所有fwd(5, 20)计算的也是次日开盘价到第5/20日收盘的真实持仓收益。
二、幸存者偏差(Survivorship Bias)
实测:A股消失了多少股票
我们查询了青柠引擎数据库(市场ID=1,排除北交所),统计了历史上出现过的全部A股:
| 指标 | 数量 | 说明 |
|---|---|---|
| 历史上曾出现的A股 | 5,497只 | 数据库中有过交易记录的全部A股 |
| 当前仍在交易的股票 | 5,225只 | 最近一个交易日有成交 |
| 已退市或消失的股票 | 272只 | 差值,即幸存者偏差的来源 |
| 幸存率 | 95.1% | 那4.9%退市的通常是最差的 |
从上市年份来看,2011年上市的2309只中已有217只退市(9.4%),而2020年后上市的股票退市率极低,因为退市往往在上市后若干年才发生。
为什么这很重要
退市的股票往往是经营恶化、股价长期低迷的,正是被策略"筛掉"后表现最差的那批。只用今天活着的股票回测,等于告诉策略"你已经知道这些股票会活到今天了"——这是实盘中不可能有的信息。
学术估计:幸存者偏差使策略年化收益虚高100-300个基点(1%-3%)。
青柠怎么应对
青柠数据库保留了所有历史上有过记录的股票,包括已退市的。with_bars() 函数从daily_bars里拉数据,不会只取当前股票——退市前有过数据的股票,在其历史区间内照样参与回测。
三、交易成本(Transaction Costs)
A股的真实成本
一笔A股交易来回的成本构成:
| 成本类型 | 买入 | 卖出 | 备注 |
|---|---|---|---|
| 佣金 | ~0.025% | ~0.025% | 最低佣金约5元 |
| 印花税 | 0 | 0.1% | 只在卖出时收 |
| 买卖价差 | ~0.05% | ~0.05% | 取决于流动性 |
| 合计估算 | ~0.075% | ~0.175% | 来回约0.15% |
换手率越高,成本越高
| 年换手次数 | 年化成本 | 若毛收益10%则净收益 | 成本占毛收益比 |
|---|---|---|---|
| 1次 | 0.15% | 9.85% | 1.5% |
| 5次 | 0.75% | 9.25% | 7.5% |
| 10次 | 1.50% | 8.50% | 15.0% |
| 20次 | 3.00% | 7.00% | 30.0% |
| 50次 | 7.50% | 2.50% | 75.0% |
换手20次/年(每月换1-2次)成本就达3%,消耗掉年化10%毛收益的30%。高频策略的净收益比想象中差得多。
青柠的处理方式
课程中所有数据实验默认对每笔交易扣0.15%来回成本,输出r20_net和e20_net(净收益)。第56课关于过拟合的分析也指出:某些回测漂亮的策略,扣完成本就翻负。
四、滑点(Slippage)
什么是滑点
滑点是回测假设的成交价 vs 实盘真实成交价之间的差距。
| 场景 | 回测假设 | 实盘现实 |
|---|---|---|
| 普通股票买入 | 次日开盘价成交 | 实际可能更高(开盘竞价) |
| 大资金买入 | 开盘价100%成交 | 冲击市价,实际均价更高 |
| A股涨停板 | 次日涨停开盘买入 | 完全买不到(一字板) |
| 止损平仓 | 设定的止损价成交 | 跳空低开,实际更低 |
A股独有:一字涨停板
A股的T+1+涨跌停制度,在滑点问题上有一个极端场景:一字涨停板。信号日之后的次日,某股票直接以涨停价开盘且全天不打开,全天无法买入。青柠引擎对此的处理是:检测到次日开盘价=最高价=最低价且高于前日收盘4%以上,记为NULL(无法买入,不计入统计)。
这不是小概率事件——每课实验的meta文件里都有limit_up字段,统计一字板比例,通常在0.4%-1%之间。
如何减少滑点影响
- – 选流动性好的股票(日成交额>1000万)
- – 避免在集合竞价时立刻买入
- – 回测时用"次日开盘价"而不是"当日收盘价"作为成交价假设(青柠默认)
- – 成本压力测试:把假设成本翻2-3倍,看策略是否还盈利
五、常见误区
误区1:「我已经加了手续费,所以没问题」
只加佣金不加印花税和滑点,还是少算了。印花税0.1%单向,大多数情况下是最大的单项成本。
误区2:「回测期间策略很好,实盘就会一样好」
实盘面临的是未来(样本外)数据,参数不会再最优,还要加上真实滑点和成本。
误区3:「退市的都是垃圾股,我的策略不会选到」
你不能提前知道哪只股票会退市。幸存者偏差不是说你的策略选了退市股,而是回测时的股票池本身就是事后筛过的。
六、在青柠中实操
青柠从设计上已消除了上述大部分陷阱:
- 1. 未来函数:信号确认→次日开盘执行,不允许当日买当日卖
- 2. 幸存者偏差:历史数据库保留所有股票,包括已退市
- 3. 交易成本:每笔默认扣0.15%来回成本,可在设置中调整
- 4. 一字板滑点:自动检测并记为NULL,不虚计收益
你还能做的是:用"参数实验室"(第57课)做成本压力测试——把成本调到2倍,看策略净收益是否还为正。
七、常见问题
Q:数据库里的272只消失股票,是全部退市了吗?
不完全是。部分可能是更名后symbol变化,或因转移到北交所导致记录断裂。但绝大多数是真实退市。
Q:策略信号数量里的NULL是什么意思?
NULL代表由于一字涨停板无法买入,这些交易在统计胜率和收益时被排除,不计入也不计出。
Q:成本0.15%是怎么算的?
买入佣金0.025% + 买卖价差0.05% + 卖出佣金0.025% + 印花税0.1% ≈ 0.20%,保守一点取0.15%(价差较低时)。
口径说明
本课数据来自青柠量化引擎,股票池为A股(非ST、排除北交所4/8/9开头),价格均为后复权。幸存者偏差统计基于截至2026-10-18的数据库快照。本文内容仅供量化学习参考,不构成投资建议。
读到这里,不如自己验证一次
文章里的逻辑,你可以在青柠量化里用真实数据重新跑一遍。免费,零代码。