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

前视偏差:回测年化 80%,实盘第一个月就亏,问题常出在这

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

一个挺常见的场景:你把一套规则写成程序,拿过去五年的历史数据跑了一遍——这个动作叫回测,就是「假装回到过去,按这套规则走一遍,看看会赚会亏」。结果出来,年化 80%,回撤还小得离谱。你盯着那条一路向上的资金曲线看了半天,越看越觉得自己发现了什么。

然后你上真钱。第一个月,亏。

这时候大多数人的第一反应是「市场风格变了」「运气不好,再等等」。但在量化圈里,遇到这种「历史上好得不真实、现实里立刻现原形」的落差,有一类原因的嫌疑最大,而且它排在幸存者偏差、过拟合前面——你的程序,在算历史的时候偷看了未来。

先说人话:它到底是件什么事

想象你在看一场昨天已经踢完的球赛录像。你一边看回放,一边在旁边的本子上写「第 70 分钟这边会进球,押它」。写完对答案,全中。你把这个本子拿给朋友看,说我预测比赛的准确率是 100%。

朋友当然会笑你。可换个包装,这件事在电脑里发生的时候,一点都不好笑,因为你自己也看不出来。程序不会跳出来说「警告:你用了明天的数据」,它只会安静地把那条资金曲线画得更漂亮一点。你回头检查代码,每一行看起来都合情合理。

再换个更贴近的说法:拿着今天的报纸,去买昨天开奖的彩票。中奖是必然的,可这个「必然」在明天早上一文不值——因为明天的报纸,今天没有。

前视偏差(look-ahead bias,也常被叫作「未来函数」)说的就是这件事:在计算某个时刻的决策时,用上了那个时刻还不可能知道的信息。它的杀伤力不在于错得离谱,而在于错得舒服——它只会让你的历史成绩变好,从来不会变差。一个只往好处错的错误,是最难被主动发现的。

它在真实投资里怎么发生

财报:报告期和公布日差着一两个月

这是最经典、也最容易中招的一处。

一份「2019 年二季度」的财报,报告期是二季度,可它躺到你面前的时间,是七月甚至更晚。如果你在做历史分析时,把这份财报的数据直接贴回到六月三十号那一格上,那么你的程序在六月三十号做决策时,就已经知道了一个当时全市场都还不知道的数字。

更麻烦的是,财报数据会被修订。同一个报告期的同一个指标,先公布一个版本,过几天又改一版,一年后年报出来再对一次——这不是罕见情况,是常态。而市面上大多数数据库只保存「最新的那一版」。你今天下载下来的历史数据,是被后来所有修订反复擦洗过的干净版本;当年真正摆在投资者面前的,是另一份。用干净版本去检验一套依赖财报的方法,等于让它在一个从未存在过的世界里做题。

qlib 的 PIT 数据文件把这件事拆成了两列,看一眼就明白问题出在哪(以下取自其文档快照中的示例数据):

(20190713, 201902, 0.        , 1040)
(20190718, 201902, 0.175322  , 4294967295)

第一列是公布日,第二列是报告期,第三列是数值。同一个报告期 201902,七月十三号那次给出的是一个值,七月十八号又给出了另一个值。两列不是一回事,混用就是前视。

停牌复牌:那一段「没有价格」的日子

一只股票停牌两个月,复牌当天补跌 40%。历史数据里,这两个月要么是空的,要么被填成了停牌前的价格。如果你的程序在停牌期间还在「按昨日价格」计算持仓市值、还在下单,那这一段收益是凭空来的——现实里那两个月你连卖都卖不掉。反过来,若你的规则恰好能在复牌前一天精准跑掉,八成是数据对齐的时候出了岔子,而不是规则聪明。

指数成分调整:名单是什么时候的名单

你想检验「只买沪深 300 成分股」的效果,于是拿今天的成分股名单,回到三年前开始跑。可三年前的名单不是这一份——这三年里被调进指数的公司,往往正是这三年涨得好的那些。这份名单本身就携带了未来的信息,你还没开始交易,胜负就已经被安排好了。

涨跌停:能不能成交,也是信息

A 股这类有涨跌幅限制的市场里还有一层更隐蔽的。判断「这一笔到底能不能成交」,需要先算出当天的涨跌停价带。Vibe-Trading 的 A 股撮合模块在注释里专门记了一笔:早先的实现用当天的收盘价去推算涨跌停判断,这是前视——因为撮合发生在开盘那一刻,当天收盘价那时还不存在。后果是双向的:开盘就封死、尾盘却打开的票,被程序放行成交了;开盘能买、收盘才封死的票,反而被拒之门外。

它现在改用严格属于历史的来源——上一交易日的收盘价(pre_close)或前结算价(pre_settle)。同一个价格,差一天,性质完全不同。

系统层:同一个概念,三种完全不同的解法

这才是真正值得慢慢看的部分。前视偏差是个概念,不是一段代码;不同的人站在不同位置,会用完全不同的方式对付它。把三种解法摆在一起读,你才会明白它到底是什么。

视角 解法 一句话说清
freqtrade lookahead-analysis 工具 事后检测:跑两遍,比对结果对不对得上
qlib Point-In-Time 数据库 源头堵死:数据库只肯给你当时能拿到的那一版
Vibe-Trading 撮合与统计层的守则 结构约束:让「拿到未来数据」这件事在代码结构上做不到
你的日常 ——(没有解法) 用今天的消息,解释昨天的决策

下面逐行展开。只列表格没有意义,关键在于它们各自是怎么做到的

解法一 · 事后检测:不读你的代码,只看你的结果对不对得上

freqtrade 的思路很有意思:它压根不去分析你的策略代码写了什么。它的判断依据是——如果你的计算没偷看未来,那么把数据从后面截掉一段,前面那部分的计算结果应该一模一样。

具体做法分两步。先用完整时间段跑一次回测,作为基准;然后针对每一笔交易,把数据只截到那笔交易发生的那一根 K 线(再多留一根,因为最后一根不会成交),单独重跑一次。两次结果一比对,问题就藏不住了。核心的比对逻辑短得出乎意料:

# 把完整回测的结果,裁到和截断回测同样的行范围
cut_full_df = full_df.loc[cut_df.index]
compare_df = cut_full_df.compare(cut_df)

同样的日期、同样的行,值却不一样——那就只有一种解释:完整数据里的某一列,用到了截断后不存在的那些行。哪一列不同,就报哪一列的名字。

这套检查还会强制关掉一批干扰项,比如禁用结果缓存、把订单类型强制成市价单、关掉保护机制、把回测钱包设得极大。原因不是这些设置本身有问题,而是它们会让「成交与否」受到资金和撮合的干扰,从而制造假阳性——本来没偷看未来,却因为钱不够而没成交,工具会误以为是偏差。(这些强制项的具体数值属于实现细节,可能随版本变化,看的时候以你手上那个版本的文档为准。)

上游文档里还列了几个最常见的犯错姿势,值得抄在手边:把序列往回移负数格(等于直接读未来);在计算指标的函数里用行号定位某一行;求平均、求最大最小时忘了限定只看过去的一段窗口,结果算的是整段历史的平均值;某些指标库的参数取了极端值,内部会退化成隐式引用未来。这些写法都不会报错。

解法二 · 源头堵死:数据库只肯给你「当时」那一版

qlib 走的是另一条路——既然前视多半源于数据本身,那就让数据层根本给不出未来的东西。

它的 PIT(Point-In-Time,直译「时点」)文件为每个字段存四列:公布日、报告期、数值,以及一个指向「这条数据下一次被修订到哪里」的位置。查询的时候,你给一个时间点,它翻出的是那个时间点之前最后一次公布的版本,而不是今天这个被修订过的最终版。

它的测试文件把这个机制写得比任何文档都清楚。同一只股票的同一个季度指标,在几个连续日期上查出来是这样的:

2019-07-15    0.000000
2019-07-16    0.000000
2019-07-17    0.000000
2019-07-18    0.175322
2019-07-19    0.175322

七月十五号那天,数据库给你的是先前那一版的值;直到十八号新版本公布,查询结果才跟着变。它没有替你「修正」历史,它替你保留了历史的无知。 这正是要点:回测的目的不是算出正确答案,是复现当时的信息状态。

同一份代码里还有一道更硬的闸门。当你写出一个引用未来期数据的表达式时,它不会默默算给你,而是直接抛错:

if end_ws > 0:
    raise ValueError(
        "PIT database does not support referring to future period ..."
    )

事后检测和源头堵死的区别,在这里体现得最清楚:前者告诉你「你已经错了」,后者让你「错不出来」。

需要提醒的是,这类时点数据库通常只覆盖季度、年度这类会被修订的基本面数据(其文档在已知限制里也是这么写的)。价格、成交量这些不修订的数据,不走这套机制,它们的前视风险在别处。

解法三 · 结构约束:让偷看在写法上就不可能

第三种解法既不检测也不建库,而是把「只能看过去」写进模块的设计里。

Vibe-Trading 的风险统计模块在文件开头就声明:所有统计量都只在已经过去的收益序列上计算,因此前视在结构上不可能发生。这句话的分量在于,它不是靠人自觉,而是靠这个模块压根不接收未来的输入——想犯错都没有入口。前面说的涨跌停价带也是同一个思路:可用的价格来源被限定成一份白名单,全是严格属于历史的字段,当天的收盘价不在其中。

这种做法的成本是灵活性。你不能随手取一个数来用,每加一个输入都得先回答「这个东西在那一刻存在吗」。但换来的是一整类错误的消失。

解法四 · 你的日常:其实你每天都在犯

把前面三种解法摆完,再看这一行,你大概会有点不舒服。

「早知道那天就该卖了。」 「这波行情其实很明显,我当时就是没敢下手。」 「按我这套逻辑,去年那几只票都能抓到。」

这些话有个共同结构:用今天才知道的结果,去评价昨天在信息不全时做的决策。 它和程序里那个 bug 是同一件事,只不过发生在你脑子里,而且没有任何工具能扫描出来。

你在复盘时看到的那根「明显」的启动阳线,之所以明显,是因为它右边的走势你已经看过了。把右边遮住再看一遍,绝大多数「明显」会瞬间消失。人不会因为这种偏差亏钱亏在当下,而是亏在它让你系统性地高估自己的判断力——于是下一次,你会下更重的注。

真要对抗它,方法和 qlib 那套是一个道理:复盘的时候强迫自己只用「当时能拿到的信息」重演一遍决策,判断决策质量而不是判断结果好坏。这也是投资决策日志的价值所在——它冻结了你当时的信息状态。

为什么它极难自查

这是本篇最想让你记住的部分。前视偏差之所以在量化里被称为顽疾,不是因为它复杂,而是因为它有一组极其难缠的性质。

它只往好处错。 一个会让结果变差的 bug,你会主动去查;一个只让结果变好的 bug,你的第一反应是相信它。这是人性问题,不是技术问题。检验一套方法时,收益率越离谱,你越应该先查前视,而不是先算仓位——可现实中,几乎所有人的顺序是反的。

它不报错。 程序照常跑完,数字照常输出,没有任何异常。你没法靠「哪里崩了」找到它。

一个函数调用就能引入。 求个平均值忘了限定窗口、取一行数据用错了索引方式、循环时边界多算了一格——这些在别的场景下完全无害的写法,在时间序列上就是灾难。而且写下它的那一刻,你的注意力多半在别的地方。

数据在到你手上之前就可能已经被污染。 你的代码可以一行前视都没有,但如果数据供应商把修订后的财报回填到了原始日期上、把今天的指数成分表当作历史名单,那你从起点就输了。这类污染发生在你的代码之外,任何代码级检查都查不到。

检测工具本身有边界。 freqtrade 的文档很坦白地写了这一点:这类工具只能验证它实际跑到的那些交易信号。如果你的策略有好几种触发条件,而检查期间某一种压根没触发过,那它就没被验证——报告会显示「没有偏差」,这是假阴性,比不检查更危险,因为它给了你一张假的安心条。反过来,某些机器学习目标列会被误报成有偏差,这是假阳性。所以工具的输出是线索,不是判决。

发现之后往往救不回来。 这一条最扎心:偏差通常正是那份漂亮收益的来源。把偷看未来的那部分去掉,收益一起没了。上游文档对此的说法很直接——如果你在网上找到一套有偏差的策略,想保留收益、只去掉偏差,大多数时候你不会走运。

🚧 避坑

「工具报告没有偏差」不等于「没有偏差」。 检测只覆盖它跑到的信号、只覆盖它比对的那些列,覆盖不到的部分一律沉默。把这类报告当成通行证,等于把「没查出来」误读成「查过了没有」。收益高得反常的时候,正确的姿势是继续怀疑,而不是找一张证明来结束怀疑。

失效边界与常见误用

把前视偏差和别的问题混为一谈。 幸存者偏差说的是「样本里只剩下活下来的」,过拟合说的是「参数被历史噪音磨得太合身」。三者都能让历史成绩虚高,但成因、检查方法、补救方式完全不同。用查前视的工具去治过拟合,不会有任何效果。想系统看这几类问题的分界,可以回到 量化与 AI 投研卷 顺着读。

以为消除了前视,方法就成立了。 去掉偷看之后的成绩才是起点,不是终点。一套不偷看未来但也不赚钱的规则,仍然是不赚钱的规则。

把「看得见的历史」当成「用得上的历史」。 历史数据里有价格,不代表当时那个价格上有足够的量能让你成交。这不算严格意义上的前视,但造成的落差是同一种——回测里能做到的事,现实里做不到。

只查代码,不查数据。 前面说了,很多污染发生在数据源头。拿到一份新数据,第一件该问的事是「这里面的每个数字,是哪一天真正可得的」,而不是急着开始计算。

在脑子里放过自己。 程序里的前视你会去修,复盘时的前视你往往会享受。后者对长期收益的伤害,未必比前者小。这一层和风险控制是连着的,风险管理卷R 里讲的仓位纪律,很大一部分是在对冲「我以为我看得准」这件事。

小结

  • 前视偏差 = 在计算某个时刻的决策时,用了那个时刻还拿不到的信息;它只会让历史成绩变好,所以最难被主动发现。
  • 投资里最常见的入口:财报的报告期与公布日错位且数据会被修订、停牌区间的价格填充、用今天的指数成分名单回测过去、用当日收盘价判断当日能否成交。
  • 三种解法各在一层——事后检测(截断数据重跑,比对结果是否一致)、源头堵死(时点数据库只给当时那一版,并拒绝引用未来期的表达式)、结构约束(模块只接收历史输入,想犯错都没入口)。堵在越前面的层,代价越大,但换来的确定性也越高。
  • 检测工具的报告是线索不是判决:跑不到的信号不会被验证,「没查出来」不等于「没有」。
  • 它在你脑子里也有一份副本——复盘时用今天的结果评价昨天的决策,是同一个错误的人类版本。

一句话记住这篇:别用今天才知道的事,去证明你昨天有多聪明。

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

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

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