← 返回量化与 AI 投研概念库

回测偏差地图:好看的历史成绩,是在哪一道工序上做出来的

最后更新 2026-08-18
R2 · 注意风险

假设你已经把偷看未来的问题查干净了。工具跑过,报告是绿的,代码里没有一处引用到明天的数据。你重新跑了一遍——年化还是很好看。

然后你上真钱,还是亏。

这时候大多数人会开始怀疑人生,或者怀疑市场。但更值得怀疑的是另一件事:回测偏差」从来不是一个东西,它是一族东西。 它们分别卡在流程的不同位置上,症状高度相似(历史好看、现实难看),成因却毫不相干。你修好了其中一种,剩下六种一动不动地待在原地。

这篇不新讲某一种偏差,而是把它们排回各自的原位——看清楚哪一道工序会掺进什么水,你才知道下一次该往哪儿查。

先说人话:满意度 98% 的那家餐厅

一家餐厅门口挂着牌子:顾客满意度 98%。

这个数字可以在四个完全不同的地方被做出来,而牌子上只写着一个数:

  • 问了谁。 只问了这个月还在来的老顾客。吃过一次再没来的那些人,压根不在样本里。
  • 什么时候问的。 上完甜点、结账之前,趁着心情最好的那五分钟问的。
  • 问了几轮。 这个月做了五十次小调查,把最好看的那一次印上了牌子,其余四十九次扔了。
  • 算没算全成本。 满意度里不含停车费、不含排队四十分钟、不含服务费。

四个问题,四个不同的动手脚位置。你只盯着「98% 这个数是不是算错了」,永远查不出问题——因为算术全对。问题不在算术,在于这个数被生产出来的每一道工序上。

回测就是这么一条生产线。你看到的那条向上的资金曲线,是它的最终产品。

投资层:这条生产线一共有几道工序

先把话说清楚:回测就是把一套事先定死的规则拿到历史数据上重走一遍,看「如果当初这么做,账户会变成什么样」(术语卡见 回测,这里只用它最朴素的意思)。想更完整地理解这一步到底在算什么,可以看 回测到底在算什么

拆开看,它至少有这么八道工序,缺一不可:

工序 它在干什么 这一道会掺进的水 错了会怎样
一、圈样本 决定哪些标的进入检验范围 幸存者偏差 名单本身就是赢家名单
二、备数据 把每个字段填进每一天 前视偏差、数据修订 用了当时拿不到的数字
三、算指标 从价格算出均线、波动这类中间量 启动期偏差 回测算出的值和实盘算出的不是同一个
四、出信号 把指标变成买卖决定 (本身不掺水,是上游的下游) 前三道的错误在这里第一次变成钱
五、撮合 判断这一笔能不能成、成在什么价 成交假设、滑点、流动性 现实里根本轮不到你成交
六、扣成本 手续费、税、冲击 成本假设 交易越频繁,误差越致命
七、出统计 算收益、回撤、跟谁比 基准选择 赢了大盘还是赢了通胀,两回事
八、回头调参 改一改再跑一遍 过拟合、数据窥探 这一道在流水线之外,它放大前七道所有的噪音

这张表最值得记的是最后一行。前七道是「这一次运行内部」的问题,第八道是「你一共跑了多少次」的问题——它不属于任何一次运行,所以任何一次运行内部的检查都发现不了它。后面会专门说。

系统层:每道工序在真实系统里长什么样

工序一 · 圈样本:名单是不是赢家名单

这是最早发生、也最容易被跳过的一道。你要检验「买沪深 300 成分股会怎样」,得先有一份成分名单。问题是,哪一天的名单

Vibe-Trading 的因子评测工具在这件事上做得挺露骨——它不藏,直接把「这份样本有毛病」写进了输出的元数据里:

panel["_meta"] = {
    "universe": "sp500",
    "survivorship_bias": True,
    ...
}

配套的注释交代得很清楚:它拿到的标普 500 名单是当前的成员表,不是历史某一天的快照;那段时间里退市、被并购、被调出指数的公司,是被静默排除掉的,因此统计结果会被系统性地抬高。

它对沪深 300 那条路径处理得更细:如果能拿到带日期的成分归属表,就按每一天的真实成员去裁剪横截面;拿不到,就退回一份静态名单,同时把 survivorship_bias 标成真、把这次运行标成「降级」。而这个标记会一路往上传,最后出现在评测摘要里给用户看。

这个设计值得单拎出来讲一句:它没有假装问题不存在,也没有把警告塞进日志末尾让你自己去翻,而是把「样本有偏」做成了报告结构的一部分。 一个成绩单如果不告诉你它的样本是怎么圈的,那这个成绩单就是不完整的——这句话对量化系统成立,对你在雪球上看到的任何一张收益截图同样成立。展开看 幸存者偏差

工序二 · 备数据:每个数字,那天真的拿得到吗

第二道是前视偏差的主战场,前面已经单独写过一整篇,这里只补它在地图上的位置感。

要点是:这一道的错误不一定发生在你的代码里。财报的报告期和公布日差着一两个月、数据库只保存被后来反复修订过的最终版、指数成分表被今天的版本覆盖了历史——这些污染在数据到你手上之前就已经完成了。qlib 用时点数据库(Point-In-Time 数据)来堵这个口子,思路是让数据层只肯给出「查询时点之前最后一次公布的那一版」。

放在地图上看,它的特殊之处在于:它是唯一一道你可以完全不写错一行代码、却照样中招的工序。

工序三 · 算指标:回测算出的值,实盘算不出来

这一道最容易被完全忽略,因为它听起来不像偏差,像技术细节。

问题出在递归上。有一类指标的每一个值都依赖它前一个值——比如指数移动平均(EMA,一种给近期数据更高权重的均线,见 均线与 EMA),今天的值是「昨天的值」和「今天的价格」加权而来。这意味着它没有一个干净的起点,历史喂得越长,值越收敛。

freqtrade 的文档举了个傻到不能再傻却一看就懂的例子:一个叫 steps 的量,第一行是 0,之后每行加一。喂一千根 K 线(K 线就是一根一根的价格柱),最后一根算出来是 999;只喂五百根,同一根 K 线上算出来是 499。同一个位置、同一个公式,值不一样。

麻烦在于回测和实盘拿到的数据长度天然不同:回测按你指定的时间段一次性吃下整段历史,实盘只能拿到交易所愿意给的最近那一截。于是同一个指标,在两个场景下就不是同一个数了。

freqtrade 为此单独做了一个检查命令,做法是拿不同长度的「预热数据」各算一遍指标,然后比对最后一行的差异,输出一张方差表。它的文档说了一句很实在的话:目标不是把方差压到绝对为零——因为像 EMA 这种天生递归的指标,要零方差得预热到荒谬的长度——而是压到小到不会改变你的进出场判断为止。

这一道的偏差方向是双向的,这是它和前两道最大的区别。它不必然让你的历史成绩变好,它只是让「回测里发生的事」和「实盘里会发生的事」不是同一件事。别小看这个区别:只往好处错的偏差你会舍不得查,双向错的偏差你会觉得「误差不大算了」——两种心态,同样致命。展开看 指标的启动期

顺带记一句这类检查的边界:它只比对最后一行的指标值,至于这点差异会不会真的改变某一笔交易,它不回答。工具给的是线索,不是判决。

工序五 · 撮合:你以为的成交,是一串默认假设

(第四道出信号本身不掺水,跳过——它是前三道的下游,错误在这里第一次变成账面上的钱。)

回测最大的信息缺口在这里:它不知道一根 K 线内部发生了什么。 一根日线只告诉你开、高、低、收四个数,中间那六个多小时价格怎么走的、什么时候走到哪儿、有没有量,全是空白。可撮合恰恰需要这些信息。

信息不够,就只能做假设。freqtrade 的文档很难得地把自己的假设列成了一张清单,摘几条最有代表性的(原文更长,建议直接去看它的回测文档):

  • 入场按当根 K 线的开盘价成交,除非你自己写了定价逻辑;
  • 只要请求价落在这根 K 线的高低区间之内,就视为按请求价全额成交,不计滑点
  • 信号触发的出场,按下一根 K 线的开盘价成交;
  • 止损恰好在止损价成交,哪怕这根 K 线的最低价比止损价还低;
  • 一根 K 线内部,假定「先到低点、后到高点」,因为这样对止损更保守,优先保住本金。

看完这几条,你应该有点后背发凉。这里面的每一条单独看都合情合理,合在一起却构成了一个比现实友好得多的世界:没有滑点、想成交就成交、止损永远精准生效。

必须说句公道话:这些假设不是 bug,是没有 K 线内部信息时的无奈取舍,而且它们中的一部分(比如低点先于高点)是刻意往保守方向偏的。同一份文档也很坦白地写着:回测永远不能替代先用模拟盘跑一遍。真正危险的不是这些假设存在,而是你不知道自己已经默认了它们

同一份文档里还藏着一个更妙的例子:交易所对最小下单量和价格精度是有硬限制的,而回测拿到的是今天的限制,历史上的限制拿不到。于是在价格远高于今天的那段历史里,用今天的最小下单量折算出来的最小金额会被算得虚高——一条本该无关紧要的现实约束,也可能是错的。这个细节的教育意义在于:回测里「现实的部分」,同样是一份快照。

A 股这边还多一层。Vibe-Trading 的 A 股撮合模块要先算出当天的涨跌停价带才能判断能不能成交,而这个价带必须从严格属于历史的价格推出来——用当天的收盘价去推,就成了偷看未来。同一个价格,差一天,性质完全变了。

这一道展开看:回测里的成交假设滑点流动性与策略容量

工序六 · 扣成本:它不是一个数,是一族假设

qlib 的交易所模块把成本拆成了几个独立参数:开仓一档费率、平仓另一档费率,外加一个最低成本兜底(都是当前默认值,可配置)。

为什么买和卖的费率要分开?因为它们本来就不对称——印花税在 A 股是卖出单边征收的(印花税就是卖出时按成交额缴的一笔税),所以卖出那一侧天然更贵。最低成本兜底则对应券商的最低佣金:单子太小的时候,费用不按比例走,按下限走,小单会被吃掉得离谱。

把这三件事写成三个参数,本身就是在说一件事:成本不是「乘一个万分之几」这么简单。 而且交易越频繁,这一族假设的误差被放大得越厉害——一套一年调两次仓的规则,成本假设错一倍无伤大雅;一套一天进出三次的规则,成本假设错一点点就能把全部超额吃光。展开看 回测里的交易成本假设

工序七 · 出统计:赢了,可是赢了谁

这一道最短,但漏掉它,前面七道白做:年化 20% 到底算好还是不好,取决于同期你不动脑子买指数能拿多少。基准选错了,整份成绩单的含义就变了。见 基准与超额收益

工序八 · 回头调参:唯一一道不在流水线上的

前面七道都在问「这一次运行有没有算错」。第八道问的是另一件完全不同的事:这份数据,你一共问了多少次。

你跑了一遍,年化 12%。把参数从 14 改成 20,跑第二遍,18%。再改成 22,跑第三遍,23%。你留下了 22 那一版,前两版随手扔了。

注意这里没有任何一次运行是「算错」的——三次都算得完全正确。可你最后交出去的那个 23%,是从三次尝试里挑出来的最大值,它比这套规则的真实水平高。挑得越多,虚得越厉害。这就是数据窥探:反复用同一份历史找答案,找到的很可能只是这份历史独有的巧合。而当你把这种挑选做到极致——参数多到能把历史上每一个小波折都照顾到——就成了过拟合:规则记住的不是市场的规律,是这段历史的噪音。

有意思的是,freqtrade 的参数搜索模块把「别给自己太多自由度」直接写进了搜索空间的设计:小数参数被限制在有限的精度上,理由写在文档里——比这更精细的取值,通常只会得到过拟合的结果。它宁可让你搜不到那个更漂亮的数,也不让你有机会骗自己。

这一道是整张地图上最贵的一道,因为:

  • 没有任何工具能扫出它。 查前视的工具读的是一次运行的结果,而这里的问题存在于「你扔掉的那四十九次」里,那些运行早就没了。
  • 它的入口是你的手,不是你的代码。 你每按一次回车,都在往里加一点。
  • 它伪装成勤奋。 反复调参在感觉上像是在认真工作。

对付它的办法全是流程纪律,不是代码:留出一段从不参与调参的数据(样本内与样本外)、让检验窗口滚着往前走(滚动前进验证)、看参数附近是一片平地还是一根孤峰(参数高原与参数尖峰)、把成交顺序打乱重算很多遍看结果散不散(蒙特卡洛检验)。展开看 过拟合数据窥探,术语卡在 过拟合。

为什么必须当成一张图,而不是一份清单

排完八道工序,真正的收获在这里。

第一,它们的「错误方向」不一样。 幸存者偏差、前视偏差、过拟合、数据窥探这四种,只往好处错——它们从不让你的历史成绩变难看。成交假设和成本假设是系统性偏乐观——默认无滑点、默认按请求价成交、默认成本是个固定比例。启动期偏差是双向的。这个分类不是学术洁癖,它直接决定查错的优先级:一份好得反常的成绩单,优先怀疑「只往好处错」的那四种;一份和实盘差距忽大忽小、方向不定的成绩单,先去看启动期和撮合。

第二,修好一种,剩下的纹丝不动。 时点数据库能让你拿不到未来的财报,但挡不住你跑一百遍挑最好那次;查前视的工具能告诉你哪一列偷看了,但对「你的样本只剩赢家」一无所知;把成本调到很保守,也救不了一个成交假设本身就不成立的策略——现实里那一笔压根轮不到你成交,成本设多高都没意义。每一道工序需要一把自己的钥匙。

第三,它们是乘起来的,不是加起来的。 样本只留赢家(第一道)+ 成交默认无滑点(第五道)+ 从三十次尝试里挑最好(第八道),三个各自「不算太离谱」的偏差叠在一起,能把一套毫无价值的规则,包装成一条漂亮得让你睡不着觉的曲线。而报告上只有一个数字,看不出它是三样东西的乘积。

第四,越靠前的工序,错了越贵。 第一道圈错样本,后面七道做得再精细也是在一个不存在的世界里做题;第八道调参调过头,至少前七道的结构还在,换一份数据重来还有救。所以自查的顺序应该是从前往后,而不是从你最熟悉的那道开始。

🚧 避坑

「我已经查过前视了」是这张地图上最常见的自我安慰。 前视偏差因为有现成的工具、有醒目的名字,成了最容易被查、也最容易被当成终点的一种。查完前视只是过了第二道工序。样本是怎么圈的、指标预热够不够、成交凭什么算成、成本怎么扣的、这份数据你一共问了多少次——五个问题一个都没回答,成绩单的可信度并没有提高多少。

一份按顺序来的自查表

顺序有讲究:从最便宜、最能一票否决的问起。前面的问题答不好,后面的问题就不必问了。

  1. 这份样本是怎么圈出来的? 名单是哪一天的名单?期间退市、被并购、被调出的标的,还在不在里面?
  2. 每个字段,那一天真的拿得到吗? 尤其是财报这类会被修订的数据——你手上的是当年那一版,还是被后来擦洗干净的最终版?
  3. 指标预热够不够? 有没有递归型指标?回测喂进去的历史长度,实盘拿得到吗?
  4. 成交假设你能不能说出来? 按什么价成交、凭什么认为能成交、滑点算了没有、遇到涨跌停或停牌怎么处理。说不出来,等于你在用一套自己不知道的规则。
  5. 成本是怎么扣的? 买卖两侧一样吗?有没有最低佣金?换手率越高,这一条越要命。
  6. 跟谁比? 同期指数拿多少?
  7. 这份数据你一共问了多少次? 诚实回答。有没有一段数据从头到尾没参与过调参?
🚧 避坑

别把「回测本来就不准,看个大概」当成结论。 这句话听着谦虚,实际上是放弃:如果你不知道自己的成绩单在哪一道工序上被美化了多少,那这份成绩单既不能证明规则可行,也不能证明它不可行——它什么都没告诉你,你却因为看过它而更有信心了。一个不能证伪的检验,比不检验更危险。

失效边界与常见误用

以为偏差都会让成绩变好。 启动期偏差、A 股撮合的价带判断,方向都是双向的。「这个偏差让我的成绩变差了,所以真实情况更好」——这个推论同样站不住,因为你不知道另外几道工序往哪边偏了多少。

把成本调高当成万能保险。 成本保守和成交假设保守是两件事。你可以把手续费设得很高,同时仍然默认「只要价格在区间内就一定能全额成交」——后者才是大多数策略在现实里破功的地方。

把工具的绿灯当成通行证。 每一个检查工具都有它明确的覆盖边界:查前视的只验它实际跑到的信号,查启动期的只比对最后一行的指标值。没查出来,不等于没有。

修完偏差就以为规则成立了。 去掉所有水分之后的成绩才是起点。一套干净但不赚钱的规则,仍然是不赚钱的规则;而更常见的情况是,水分去掉之后,那份漂亮的收益也一起没了——因为水分本来就是它的来源。

只在电脑里查,不在脑子里查。 你复盘一次失败的交易,只挑那些印证了你结论的历史片段来看,这是数据窥探的人类版;你回忆自己的历史战绩,记住的多半是赚钱那几笔,这是幸存者偏差的人类版。这层和心理账户是连着的,行为金融学 那篇讲得更系统。

小结

  • 回测偏差不是一个概念,是一族卡在不同工序上的问题:圈样本(幸存者)、备数据(前视)、算指标(启动期)、撮合(成交假设、滑点)、扣成本、出统计(基准)、回头调参(过拟合、数据窥探)。
  • 第八道「调参」不在流水线内,它问的是「你一共跑了多少次」,因此任何一次运行内部的检查都发现不了它,只能靠流程纪律。
  • 各种偏差的错误方向不同:幸存者、前视、过拟合、数据窥探只往好处错;成交与成本假设系统性偏乐观;启动期是双向的。成绩单好得反常时,先查前四种。
  • 它们乘起来:三个「不算太离谱」的偏差叠在一起,足以把无价值的规则包装成漂亮曲线。
  • 自查从前往后:样本怎么圈的 → 字段哪天可得 → 指标预热够不够 → 成交假设说得出来吗 → 成本怎么扣 → 跟谁比 → 这份数据问了多少次。
  • 修好一种不代表修好了别的。每道工序有自己的钥匙。

一句话记住这篇:一份漂亮的历史成绩,可以在七八个不同的地方被做出来;你只查了其中一个,就等于什么都没查。

本文所引源码与文档均来自上述快照版本,是阅读代码与文档得出的机制说明,不是运行结果;具体行为请以你手上那个版本为准。回 量化与 AI 投研概念库 逐个看这几种偏差各自怎么发生,或从 量化与 AI 投研卷 顺着读;本站内容为投资教育,不构成任何投资建议,边界见 免责声明

内容有错、看不懂、或想看下一篇?告诉我们 →

本文为投资教育整理,不构成任何投资建议;不荐个股/基金、不预测点位、不承诺收益。关键数据与结论请结合权威来源自行验证,并见风险免责声明