一个能跑通的回测和一个可信的回测之间,隔着三类系统性错误。它们不会抛异常,不会让曲线崩掉,恰恰相反——它们全都会让结果变得更漂亮。这也是它们难以被自己发现的原因。
一、幸存者偏差:只在活下来的公司上做测试
假设你用「当前标普 500 成分股」的列表去回测一套 2015 年至今的策略。这个列表里的每一家公司,都通过了一道极强的筛选:它们在 2026 年还在指数里。破产的、被收购的、因为市值萎缩被剔除的,全都不在名单上。
于是你实际测的不是「一套策略在美股上的表现」,而是「一套策略在已知会成功的公司上的表现」。这两件事的差距不是几个百分点。美国上市公司的历史退市率相当可观,大量标的因为并购、私有化、破产、不满足上市条件而退出交易;把它们排除掉,等于事先剔除了策略最应该被检验的那部分样本——恰恰是止损规则要处理的那部分。
怎么避免: 标的池必须包含在回测区间内存在过、但如今已退市的标的,并且要用它们退市当时的真实价格序列,而不是把序列在退市日截断后当作正常结束。我们的标的库保留已退市标的且不做幸存者过滤,行情接口在返回时会显式标注该标的的退市状态。
一个反直觉的推论: 幸存者偏差对买入并持有基准的抬高,往往比对策略本身更大。所以在有偏样本上,策略的超额收益会被系统性低估——它同时污染了两边,只是污染程度不同。
二、前视偏差:用了当时还不知道的信息
前视偏差(look-ahead bias)比幸存者偏差隐蔽得多,因为它常常只差一格。
最常见的一格:用今天的数据算今天的信号。 比如「突破 22 日新高入场」,如果 22 日最高价的窗口包含了今天,那么「今天收盘价 ≥ 22 日最高价」这个条件几乎恒成立——今天的最高价本来就在窗口里。正确的写法是窗口只到昨天为止:
hh = rolling_max(high, 22)
prev_hh = np.roll(hh, 1) # 向后挪一格,窗口截止到昨日
prev_hh[0] = np.nan
entry = (close > ma) & (close >= prev_hh)
这一格的差别,能把一套毫无价值的规则变成一条完美的收益曲线。
其他几种常见形态:
- 用收盘价决策、又用收盘价成交。 严格说来,你要等到收盘才知道收盘价,因此不可能以该价格成交。可接受的近似是收盘价决策、次日开盘成交;如果坚持用收盘价成交,至少要在滑点里为此留出余量。
- 复权因子的追溯改写。 后复权序列会在每次分红、拆股之后被整体改写。用今天下载的复权序列回测三年前的策略,等于用了三年前不存在的调整信息。
- 数据修订。 财务数据、成交量、甚至部分交易所的日线都可能事后被修正。回测里用的是修正后的版本,实盘当时拿到的是修正前的。
- 指数成分的生效时间。 「纳入标普 500」是先公告、后生效的。用生效日之前的日期去假设你已经知道这件事,就是把公告的价格效应白送给了策略。
怎么自查: 对每一个指标问一句「计算它的那一刻,这个数在现实里已经存在了吗」。这一句能挡掉九成的前视偏差。
三、过拟合:把噪声当成规律
这是三者中最难对付的,因为它不是一个 bug,而是一个流程问题——每一步看起来都很合理。
吊灯止损策略有六个可调参数。假设你只在一个不算大的网格上搜索:
sma_period 6 个取值
breakout_period 5 个取值
atr_period 3 个取值
entry_mult 6 个取值
exit_mult 5 个取值
adx_threshold 4 个取值
--------------------------
组合数 = 6 × 5 × 3 × 6 × 5 × 4 = 10800
一万多个组合里必然有一个表现最好。问题在于:即使底层数据是纯粹的随机游走,最好的那个组合看起来也会相当漂亮。 你搜索的组合越多,「最优结果」中来自运气的成分就越大。当你报告最优组合的收益率时,你报告的是一个被选择过的极值,而不是一个期望。
更隐蔽的形式是「手工过拟合」:你没有跑网格搜索,只是看了结果不满意,把 SMA 从 100 改成 120,好一点;再改成 150,更好一点。这与自动搜索没有本质区别,只是你没有记录尝试次数,因此连自己搜索了多少次都不知道。
四条能实际降低过拟合的做法:
- 留出样本外区间。 在前 70% 的数据上选参数,在后 30% 上只跑一次。如果样本外的结果显著变差,说明选出的是噪声。注意「只跑一次」是字面意思——看过样本外结果再回去调参,样本外就变成样本内了。
- 要平台,不要尖峰。 把参数的邻域一起看:如果
sma_period取 100 时年化 24%,取 90 和 110 时只有 8%,这个 24% 是运气。真实的规律在参数空间里表现为一片平缓的高地,而不是一根针。 - 减少参数,而不是增加。 每加一个过滤器(ADX、成交量、周几效应)都会让样本内结果变好,也都会让样本外结果变差得更快。一个参数需要被加入的门槛,应该是「它有一个说得通的市场机制」,而不是「加了以后曲线更好看」。
- 跨标的验证。 同一组参数在 20 个标的上跑,看结论是否一致。只在一个标的上成立的参数,是那个标的这段历史的特征,不是策略的特征。
四、还有几件同样会让结论失真的事
- 交易成本与滑点。 一套年化 15% 的策略,如果一年交易 200 次,单边成本 0.1% 就吃掉 20 个百分点。高换手策略必须把成本作为核心参数而不是事后修正项。
- 数据口径混用。 用单交易所口径的收盘价算突破、用合并口径算成交量过滤,这种混搭会产生现实中不存在的信号。实测两种口径的收盘价偏差可达 7%,详见《合并成交量与单场成交量》。
- 回测区间太短。 一段只覆盖单边上涨的历史,测不出任何风控规则的价值。我们的合并日线自 2024-07-01 起可用,这是数据源的客观边界;在这样的区间上得到的结论,其适用范围也就到这里为止——把它说成「五年验证」是不诚实的。
- 幸存的是策略本身。 你今天在研究趋势跟踪,是因为它在过去几十年被写进了很多书。那些同样被认真研究过、但没有效果的策略,你根本没机会听说。这一层偏差无法用数据修正,只能靠意识到它的存在来打折。
收尾:一个不太舒服的标准
判断一个回测结论是否可信,有一个简单粗暴的检验:把你为了得到这个结论所做过的全部尝试次数写下来。 如果写不出来,或者写出来发现是几百次,那么这条结论的置信度远低于你以为的水平。
回测的价值不在于找到最好的参数,而在于排除掉明显行不通的想法。把它当成一个证伪工具,它非常有用;把它当成一个寻优工具,它几乎必然会骗你。
本文仅用于量化技术研究与教育目的,不构成任何投资建议。历史回测结果不代表未来收益,交易风险由投资者自行承担。