halo 的技术博客

返回

回测的悖论:越认真做,越不信结果#

做了十年量化回测系统,我有一个越来越坚定的信念:一个回测结果越好看,越应该怀疑它。不是怀疑策略,是怀疑回测本身有没有漏洞。

回测是量化投资里最容易被滥用的工具。同样是年化 30% 的策略,一个用了严谨的 tick 级回测,一个用了日频 OHLC 加 naive 滑点——这两个 30% 完全不是一个概念。后者在实盘中可能连 5% 都做不到。

以下是这些年我踩过和看过别人踩过的 12 个坑,按「坑害程度」从高到低排列。

坑 1:存活偏差——你见不到死了的股票#

这是最基础也最容易被忽视的偏差。你用今天的股票池回测三年前的策略——三年前那些股票里可能有一半已经退市了。用现存的股票回测 = 系统性高估收益。

正确做法:维护每个交易日的历史成分股池(point-in-time universe),用当时的可用股票进行回测,而不是用回测时的全量股票。

坑 2:前视偏差——你「不小心」用了未来的信息#

这个坑的隐蔽程度远超想象。最常见的场景:

  • 财报发布日期用的是数据库的「录入日期」而不是「实际公告日」
  • 用了季报发布后才可能计算出的财务比率
  • 用全量数据的均值和标准差做标准化(「未来」数据也在里面)

正确做法:实现严格的 point-in-time 数据层。每一时刻只能用截至该时刻已知的信息。这要求数据处理 pipeline 支持 time-travel 查询。

坑 3:交易成本建模太粗糙#

回测里「买入价 = 当天收盘价 + 固定 2bp」这种模型在实盘中是灾难。交易成本分三层:

  • 显性成本:佣金、印花税、交易所费用
  • 市场冲击:你的单子本身推动价格。越是成交不活跃的品种,冲击越严重
  • 机会成本:流动性不够导致部分挂单没成交,错过信号窗口

Almgren-Chriss 模型给出了一个不错的代理:永久冲击 + 临时冲击的两组分分解。但更加重要的是认识问题而非使用什么模型——在实际买卖中,小额买卖的成本几乎可以忽略不计,但大单成本是交易致命伤,一个回撤可能让人心如刀绞。

坑 4:多资产同步问题#

假设你有一个跨品种套利策略:买入 A 股票的同时卖出 B 股票。在日频回测里这很简单——同一天执行两笔。但在分钟/tick 级回测里呢?

A 的最后一笔交易在 10:30:00.123,B 的最后一笔在 10:30:00.789。你以哪个时间为准?如果在这 666ms 的间隔里发生了跨资产的信息传导,你的模拟价格就不对了。

正确做法:至少用 1 秒分桶做时间对齐。高频场景下考虑 Barndorff-Nielsen 的多变量同步方案。

坑 5:因子时序对齐错误#

这是前视偏差的一个特例,但因为它太常见所以单列。

例如你的因子定义是「过去 20 日收益率的负相关性」。你在回测里这样写:

factor = df['close'].pct_change(20).shift(-1)
python

这里 shift(-1) 的逻辑是什么?你可能想表达「用 20 日前的价格算收益率」——但实际 data engineer 在凌晨 2 点刷新数据库的时候,这笔数据包含了当天收盘价。而你的策略在 9:30 就跑了。

正确做法:建立 factor computation date 的概念。每个因子值都有一个 as_of_date,标记它是用截至哪一天的数据计算的。

坑 6:复权因子误用#

A 股的前复权和后复权容易混淆。多数数据商提供的是前复权价格(比例复权),但你在计算收益率的时候,前复权价格本身会变化——今天的前复权历史价格可能和昨天的前复权历史价格不一样,因为复权因子变了。

正确做法:存未复权的 OHLCV + 复权因子表。计算收益率时先还原真实价格再算。

坑 7:过拟合在回测里看起来像「稳健」#

你跑了 500 个参数组合,选了最好的那个,回测曲线平滑向上。这在统计学上叫「p-hacking by another name」。如果你的参数空间维度很高且没有使用严格的样本外检验,回测最优参数基本上就是噪音。

正确做法

  • 严格的时间序列交叉验证(不是随机切分!)
  • Walk-forward analysis:每 N 个月用滚动窗口重新做参数选择
  • 记录整个参数空间的「平坦度」——最优参数周围的表现是否一致。如果差一个参数步长夏普比率就从 2.5 掉到 0.8,那大概率是过拟合

坑 8:无风险利率没有复权#

这个坑在长回测中特别明显。2015 年的一年期定期存款利率和 2025 年差出 200bp。如果你在 10 年回测里固定用一个无风险利率,早期的超额收益被系统性地低估或高估。

坑 9:回测引擎的撮合逻辑#

你的回测引擎怎么处理「想买入 1000 股但当时只有 300 股在卖一价位」?是等下一个 tick 继续买入剩余 700,还是全部取消?还是用后面的价位?每种选择对你的成交均价和滑点影响完全不同。

至少要实现

  • 多档撮合(match at multiple price levels)
  • 部分成交(partial fill handling)
  • 成交量加权平均价格(VWAP)基准对比

坑 10:回测不包含暂停/停牌#

A 股里每年都有大量停牌事件。如果你的回测假设所有股票一直可交易,那你的策略相当于在「市场永远 open」的假想世界里跑——实盘中遇到停牌,你想平仓都平不掉。

坑 11:基准没有做风险因子归因#

年化 20% 听起来不错,但你的超额收益里有多少来自「无脑满仓小市值」?如果你跑一个 Fama-French 五因子归因,发现所有 alpha 都被 HML 和 SMB 解释了——那你就没有 alpha,你只是赌了风格因子。

在回测报告的首页放归因分析,这不是 optional 的。

坑 12:幸存者偏差之外还有「选择偏差」#

你选择回测的品种本身就是一种偏差。为什么你选了这 500 只而不是那 500 只?如果选取规则本身和收益正相关(比如「选择流通市值最大的 500 只」= 排除了退市风险高的 micro-cap),那你的回测结果有一部分来自选股规则本身的幸存者偏差。

总结:好回测的三个标准#

  1. 可重复:同一个人在不同时间跑同一个回测,结果数字误差在 1bp 以内
  2. 诚实:所有假设都在报告里写明。包括滑点模型、数据源、复权方式、存活偏差处理
  3. 保守:在不确定的参数上倾向悲观估计。宁可低估收益,不高估收益

最后一句话:如果一个回测结果让你想马上上线,把它放到一边冷静三天再回来看。三天后你一定会找到问题

回测系统架构概览

Walk-forward分析示意图

量化回测系统设计:12 个你一定会踩的坑
https://blog.halo26812.eu.org/blog/backtesting-system-design-decisions
Author halo
Published at 2026年8月6日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨