一个框架专门造工具来防的坑,说明它真的很常见
- 能从"框架愿意为一个坑付多大工程代价"反推出这个坑有多普遍,学会用这个视角评估任何工具链
- 能说清这两个检测工具的共同套路——截一段、重算、比对——以及为什么它们不读你的代码
- 能说出每个工具官方承认的覆盖盲区,不再把"检测通过"当成免检通行证
你在翻一个开源交易框架的目录结构,一层层点进去,在 optimize 下面看到一个叫 analysis 的文件夹。点开,里面只有三个文件,加起来几百行。
你以为是某种绩效分析——算夏普、画曲线之类的。结果不是。这三个文件干的是另一件事:它们专门用来查这个框架自己的用户写出来的策略,有没有在偷偷作弊。
一个交易框架,主业是撮合、下单、管仓位。它为什么要分出人力,去写一套「怀疑用户」的代码?
这个问题的答案,比这两个工具本身有意思得多。
装一台 X 光机,要付多少钱
先离开代码,想一个工厂。
一条食品包装线的尽头,有时候会装一台金属异物检测机。这台机器不便宜,占地方,需要专人定期用标准试块去校准,还会拖慢整条线的节拍——每个包装袋都得从它肚子里过一遍。更烦的是它会误报:袋子里的锡纸封条、某些矿物质含量高的原料,都可能让它响。响一次就得停线、开袋、人工复检,然后确认虚惊一场。
现在你是这个厂的老板。你会在什么情况下决定装它?
不会是因为「万一呢」。为「万一」买单的老板活不长。你会装它,只可能是因为异物进袋子这件事,在你这个行业里发生得足够频繁,频繁到你算过账:装机器的成本,低于不装的代价。
反过来推也成立。你走进一个陌生的车间,看见产线尽头蹲着这么一台机器,你立刻就知道了一件关于这个行业的事实——不用问任何人。防护措施的存在本身,是关于风险频率的一手证据。
这个推理方式,比这篇文章里所有的技术细节都值钱。你以后看任何一个工具链,都可以这么读它:它在哪里加了护栏,哪里就流过血。
这两个坑的共同长相
回到框架。那三个文件对付的是两样东西:一个叫前视偏差,一个叫指标递归启动期。
这两个概念本身,站内各有一篇讲机制的,这里不重复:前视偏差讲的是程序在算历史的时候用上了当时不可能知道的信息;指标递归启动期讲的是某些指标的值依赖它前面所有的值,喂进去的历史长度不同,算出来的数就不同。想搞清它们是什么、怎么发生的,先去读那两篇。
这篇只关心一件事:这两样东西身上有什么共同点,逼得一个框架必须专门造工具?
共同点有三条,每一条都很要命。
第一,它们不报错。 程序不崩,日志不红,测试全绿。回测(拿历史数据把一套规则走一遍,看看假设中的表现)照样跑完,照样吐出一条资金曲线。软件工程里绝大多数 bug 会通过「东西坏了」来通知你,这两个不会——它们通过「东西变好了」来通知你,而人对这种通知天生没有警觉。
第二,它们只往一个方向错。 偷看未来只会让历史成绩变好;启动数据喂得太足只会让指标在回测里比实盘更准。没有哪一次是因为这两个毛病而让回测显得更差的。一个只会让你高兴的错误,等于没有报警器。
第三,它们藏在正确的语法后面。 你写的每一行单独看都合法、合理、有注释。问题出在数据的时间结构上,不在语法上。这就是为什么静态检查(不运行程序、只读代码找问题的那类工具)在这里基本无能为力——你没法靠读一行 .mean() 就断定它是不是漏了未来数据,那取决于它作用在哪个窗口上、那个窗口又是怎么切出来的。
三条加在一起,结论只有一个:这两个坑必须靠「跑起来对答案」来查,不能靠「读代码找茬」来查。 于是就有了那三个文件。
同一个动作,查两种病
翻开那三个文件,会发现一件挺优雅的事:两个检测工具共用同一个基座(源码里那个 base_analysis.py,定义了一个用来装「一次运行的全部中间结果」的容器)。它们的技术动作是同一个:
截一段数据,重新算一遍,把两次的结果摆在一起比。
就这一句。所有的复杂度都在「截哪一段」和「比哪一列」上。
用生活里的话说,这叫对答案。你怀疑一个学生抄了后面的答案,最直接的办法不是审查他的草稿纸——他草稿纸写得比谁都工整——而是把后面那几页撕掉,让他重做一遍,看两次答案一不一样。一样,说明他真的会;不一样,说明他刚才在看不该看的东西。
两个工具的区别,只在于「撕掉哪几页」。
查偷看未来:撕掉后面的页
查前视偏差的那个工具,撕的是时间上靠后的数据。
它的流程大致是这样:先拿完整时间段跑一遍回测,把结果存下来当基准。这份基准里有一笔笔交易,每笔都有开仓时间和平仓时间。然后它挑出其中一笔,重新构造一个数据范围——从头开始,但在这笔交易开仓的那根K线处就截断,多给一根用于成交,再算一遍。
如果这笔交易在截断后的那次运行里仍然出现在同一个时间点上,说明它不依赖后面的数据,清白。如果它消失了或者挪位置了——那么它当初之所以成立,靠的是当时还没发生的信息。平仓信号同理,按平仓时间再截一次。
除了「交易还在不在」,它还会做更细的一层比对:把完整运行算出的指标数据裁剪到和截断运行同样的行范围,然后逐列比数值。哪一列在两次运行里对不上,哪一列就被点名。这一层更狠,因为它能在交易层面还没暴露之前就把可疑的指标抓出来。
一段说明这个思路的示意(不是源码,只是把动作写清楚):
基准运行: 用 [起点 ...................... 终点] 跑一遍,记下每笔交易
对某笔交易:用 [起点 ..... 这笔交易开仓处] 再跑一遍
比较: 这笔交易还在原来的位置上吗?
同一时间段的指标值,两次算出来一样吗?
不一样 → 它用到了截断掉的那段数据 → 偷看了未来
查启动期:撕掉前面的页
查递归启动期的那个工具,撕的是时间上靠前的数据。
它连回测都不跑,只算指标——因为它要问的问题跟交易没关系:同一个指标,在同一个时刻,喂给它的历史长度不一样,算出来的数会差多少?
所以它先用一段很长的历史算一遍当基准,再用一组由短到长的预热长度各算一遍(这组长度是可配置的,具体取值以你手上版本的文档为准),然后只比最后一行——也就是最新那根K线上的指标值——报出每种预热长度相对基准偏离了百分之几。
这张表读起来很直观:某个指标在预热很短的时候偏离得离谱,随着预热变长慢慢收敛到接近零。你要做的判断是:收敛到多少,才小到不会改变你的进出决策? 这个数没有标准答案,取决于你的规则对指标值有多敏感——如果你的条件是「指标穿过某条线」,那么在那条线附近的一点点偏离就足以翻转结论;如果你的条件是「指标处在一个宽区间里」,同样的偏离可能毫无影响。
这里有一个细节值得单独拎出来说,因为它体现了框架的态度:如果策略没有申报自己需要多少预热数据(或者申报了一个不合法的值),这个命令直接抛错,拒绝往下跑,并在错误信息里明说「这会在某些指标上导致递归问题」。
不是警告,不是用个默认值兜底,是罢工。一个框架肯为一件事罢工,说明这件事在它的经验里坑过太多人。
造这两个工具,框架付了什么代价
现在回到开头那个「装 X 光机要花多少钱」的问题。把源码里那些不起眼的细节列一遍,你会对代价有个体感。
算力代价是数量级的。 查前视偏差的工具,每检查一笔交易,要额外跑两遍回测(开仓截一次、平仓截一次)。检查 N 笔交易,就是 2N 次回测,再加上一次基准。这不是「慢一点」,这是「慢两个数量级」。所以源码里有个上限参数,检查到一定笔数就停——不是因为查够了,是因为再查下去没人等得起。
它得先把自己的嘴捂上。 源码里有一对成对出现的调用,一个在检查开始时把日志级别压下去,一个在检查结束后恢复。原因很实在:几十遍回测会把终端刷成瀑布,真正有用的那几行「发现偏差」会被淹掉。为了一个功能专门去改全局日志行为,这在工程上是很不情愿才会做的事。
它得强行接管一堆设置。 官方文档里明写了这个命令会强制关掉结果缓存、强制放大钱包和仓位上限、强制关掉保护机制、强制把订单类型改成市价。理由不是这些设置本身有问题,而是它们会制造假阳性——本来没偷看未来,却因为钱不够、格子满了、限价没成交而在截断运行里消失,工具就会误判成偏差。(这些强制项的具体内容属于实现细节,会随版本变化。)
换句话说:为了让检测结果可信,工具不得不把被测环境改造成一个不真实的环境。 这是个很深刻的取舍,你在任何检测系统里都会碰到。
它得处理各种恶心的边角情况。 比如回测结尾会把还开着的仓位强制平掉,这类平仓不是策略决定的,拿去比对必然对不上,所以源码里专门跳过它们,还留了注释说明「跳过是为了避免假阳性」。再比如某笔交易的平仓时间恰好是整段数据的终点,也要单独排除。这些分支不写,工具就会天天喊狼来了。
它还被接进了图形界面和接口层。 两个分析都能作为后台任务从网页界面触发,也能通过接口调用、轮询进度、取结果——因为它们跑得实在太久,不能占着命令行等。为一个「自查工具」做到这一步,等于承认它是日常工作流的一部分,而不是偶尔用一次的诊断棒。
把这几项加起来:一个开源项目的维护者,愿意为「用户可能写出偷看未来的策略」这件事,付出数量级的算力开销、全局日志改造、环境强制接管、一堆边角特判,以及界面和接口的集成。
你现在应该能自己回答开篇那个问题了。他们不是因为「万一呢」。他们是因为见过太多人拿着一条完美的资金曲线来提问,然后发现问题都出在同一个地方。
关于这个框架整体是什么、边界在哪,另有一篇freqtrade 是什么,又不是什么;想看它平时那套主循环怎么转,看交易机器人开机之后在忙什么。
官方自己承认它们查不出什么
这一节是本篇最重要的部分,请慢一点读。
一个成熟项目的文档,最值钱的往往不是「本工具能做什么」,而是「本工具做不到什么」。这两个命令的文档里都有专门的「注意事项」小节,坦白得近乎自嘲。
查前视的工具,只能验证它实际跑到的那些信号。 如果你的策略有好几种不同的入场条件,而检查覆盖的那段时间里某一种压根没触发过,那这一种就没被验证过。报告依然会显示「未发现偏差」。
这是假阴性——比不检查更危险。不检查的人心里还有个疙瘩,拿到一张「未发现偏差」报告的人,疙瘩没了。「没查出来」和「查过了没有」,是两句完全不同的话,而报告只会用同一行字表达。
它也会误报。 文档里点名了两种情况:一是用限价单配合自定义价格回调时,成交可能被延后,工具会误以为是偏差(这正是它强行改成市价单的原因,而如果你手动允许限价,误报就会回来);二是某些机器学习目标列会被稳定地误标成有偏差,文档直接写了「这些不是偏差,可以放心忽略」。
还有一种连环误报:入场信号有偏差时,跟它共用同一个指标的出场信号大概率也会被标红。文档给的建议很实用——先修入场,再看出场,不要对着一张全红的表逐条改。
查递归启动期的工具,只比最后一行。 它告诉你某个指标在不同预热长度下差了百分之几,但它完全不告诉你这个差异有没有真的改变过任何一笔交易。文档原话就是这个意思:影响不影响你的进出,不在输出里。一个偏离 5% 的指标可能毫无影响,一个偏离 0.1% 的指标可能正好卡在你的阈值线上把信号翻转——这两种情况在那张表里长得一模一样。
它也只覆盖指标计算那一段。 如果你把某些指标算在了信号生成的环节里而不是指标环节里,这个工具压根不会算到它。文档明说了这一点。这条限制很容易中招,因为「把某个计算顺手写在信号函数里」是几乎每个人都干过的事。
它对标的和精度还挑食。 文档建议用价格较高、波动适中的品种来做这个分析,理由是低价品种的四舍五入误差会污染百分比结果。这提醒你:这类数值比对的结论,天然带着浮点精度的噪音底。
最后还有一条最容易被忽略的:查前视的那个工具,如果基准运行里的交易笔数太少,它会直接取消整个检查并打印一行提示。这时候你收到的不是「没有偏差」,而是「没查」——但如果你只扫一眼有没有红色报错,两者看上去差不多。
那么,你该怎么用它们
把上面这些拼起来,得到的结论不是「这些工具不靠谱」,而是一个更准确的定位:
它们是体温计,不是药,更不是健康证明。
体温计的价值在于,它能把一个你完全无法凭感觉发现的异常变成一个数字。这已经很了不起了——毕竟前视偏差最恐怖的地方就是「你自己看不出来」。但体温正常不等于没病,这句话不需要论证。
具体到用法上,有三条我认为值得记住:
第一,把「未发现偏差」读成「在这次覆盖到的范围内未发现偏差」。 每次看报告,先问自己:这次检查覆盖了几笔交易?我的哪几种入场条件在这段时间里真的触发过?没触发的那些,等于没查。
第二,工具的输出是线索,不是判决。 报了红,先看是不是文档里点名的那几种已知误报;报了绿,也别把它当结束怀疑的理由。真正该触发怀疑的信号从来不是工具的输出,而是收益好得反常这件事本身。
第三,这类检测查的是「实现层面的错」,治不了「方法层面的错」。 你的规则被历史数据磨得过于合身(过拟合)、你的样本里只剩活下来的(这类问题在回测偏差总览里有分类)、你的样本外检验做得不老实,这三样,任何前视检测工具都一个都查不出来。它们和前视偏差都能让历史成绩虚高,但成因和治法完全不同。用体温计去治骨折,不会有任何效果。
给不写代码的你
如果你不打算自己写策略,只是会遇到别人拿量化结果来跟你说话,这篇里有一样东西是直接能用的。
下次有人给你看一条漂亮的历史曲线,你不需要懂任何技术,只需要问两个问题:
「这套东西查过前视偏差吗?怎么查的?」 答不上来、或者回答是「我代码检查过没问题」的,你就知道他大概率没做过这件事——因为这件事没法靠读代码做到,而真做过的人会自然而然地提到「跑了几遍」「比对了什么」。
「查的时候覆盖了多少笔交易、几种入场条件?」 这个问题会让对方意识到你知道「覆盖率」这回事。真做过的人会给你一个具体的数;没做过的人会开始解释别的。
你不是在挑刺,你是在确认对方的怀疑有没有落到实处。一个自己都没怀疑过自己的历史成绩,不值得你替他怀疑。
想更系统地读一个开源回测框架的源码,看怎么读一个开源回测框架的源码;想知道回测引擎在这些检测之下究竟每一步在做什么,看一个回测引擎内部到底在做什么。
一句话说完这篇:有人肯花大价钱在生产线尽头装一台会误报、会拖慢产线的检测机,唯一合理的解释是,他们真的经常在袋子里发现不该有的东西——而那台机器再贵,也只能告诉你它照到的那几个袋子,剩下的还是得靠你自己心里有数。
本文为投资教育整理,关键数据与结论请结合下列权威来源验证。