← 返回教程库

一个开源回测框架摆在你面前,从哪一行开始读

最后更新 2026-08-19
📚 量化与 AI 投研 📖 E 线 · 回测引擎 ⏱ 约 22 分钟 R1 · 低风险
你将学到
  • 能按「测试 → 数据结构 → 主循环 → 策略示例」的顺序读一个陌生的回测框架,并说出每一步分别能看出什么
  • 能在任意一个框架里定位到三个关键答案:成交价取的是哪根K线的哪个价、手续费和滑点在哪一步扣、有没有防前视偏差的机制
  • 知道什么时候该停止读源码——判断自己已经读够了的标准是什么

你把一个星标好几万的回测框架 clone 下来,ls 一下,几十个目录,几千个文件。README 写得很客气,教你装环境、跑一个示例、改配置文件。你照着跑通了,屏幕上吐出一条净值曲线和一堆统计数字。

然后你盯着那条曲线,心里冒出一个问题:这条线是怎么算出来的?

具体点说:它认为你在哪个价成交的?手续费扣在哪一步?我写的那个指标,它有没有偷偷用到当天收盘以后才知道的数据?

这些问题,文档不会回答你。文档只回答「怎么用」,不回答「它替你假设了什么」。而对一个用回测做决策的人来说,后者才是全部。

所以你迟早要打开源码。问题是——从哪一行开始读?

绝大多数人的做法是:打开 README 链的那个「架构介绍」,看两屏,遇到第一个不认识的抽象类,弃。或者更惨的:从主循环那个一千多行的文件从上往下读,读到第三百行的时候已经忘了第五十行那个变量是干嘛的。

这一篇给你一套顺序。这套顺序不针对某一个框架,换成任何一个回测引擎都能用。

先说人话:这像接手一家还在营业的店

想象你顶下了一家已经开了三年的小面馆。老板把钥匙给你,人就走了。店还在营业,明天早上照样有人来吃面。

你有一整间屋子的东西要摸清:菜单、供货商联系方式、后厨流程、收银机、三年的账本、员工的排班表。你能从哪儿开始?

最糟的开法是从后厨的操作台开始。 你站在那儿看着一排调料罐和五口锅,能看出的信息量约等于零——你不知道哪口锅对应哪碗面,也不知道那罐深色的东西是酱油还是醋。锅和调料是实现,脱离了「要做出什么」,它们什么也说明不了。

最省力的开法是先翻账本和小票。 一张小票上写着:牛肉面一碗,加蛋,二十八块,收现金三十找两块。这一张小票同时告诉你四件事——菜单上有这个东西、它卖多少钱、加料怎么算钱、这家店收不收现金。一张记录完整的凭据,比十页流程说明书信息密度高得多,因为它是「具体的输入」和「具体的输出」绑在一起的。

第二步该看的是单据本身的格式。 小票上有没有「桌号」这一栏?有,说明这店做堂食;没有,说明可能只做外带。有没有「折扣」这一栏?没有的话,就算老板嘴上说「熟客可以打折」,这家店的系统里也根本表达不了折扣这件事——账本记不下来的东西,实际上就是不存在的。

第三步才是看流程。 早上几点开门、几点备料、客人进门先干什么。流程是把前面那些单据串起来的骨架。

最后才轮到看老员工怎么颠勺。 那是具体手艺,跟你能不能接管这家店,关系其实最小。

这个顺序——凭据 → 单据格式 → 流程 → 手艺——原样搬到读回测框架上,就是:测试 → 数据结构 → 主循环 → 策略示例。

对投资的人来说,这件事为什么非做不可

你可能觉得,我又不打算改这个框架,我只想用它跑跑我的想法,何必读源码。

问题在于:回测结果里那些让你做决定的数字,一大半不是你的策略决定的,是框架的默认假设决定的。

同一套买卖规则,喂给两个不同的引擎,年化能差出好几倍。差在哪儿?差在它认为你成交在开盘价还是收盘价、差在它有没有算冲击成本、差在它算指标的时候那个窗口有没有多吃一根未来的K线。这些东西不写在你的策略文件里,写在框架里。你不读,就等于把「这次实验的对照条件」整个外包给了一个陌生人。

更要命的是,这类偏差全都朝一个方向错——朝好看的方向。原因很直白:框架作者要让示例策略跑出漂亮曲线,用户才愿意留下;而且宽松的假设写起来简单,严格的假设要多处理一堆边界情况。所以默认值的漂移方向天然是乐观的。至于这些偏差一共有哪几类、各自怎么发生,回测到底是在做什么那篇有个总账。

还有第三个理由,比前两个都实在:读框架的源码,是学交易系统设计最便宜的路。 一个被几千人用了好几年的回测引擎,它每一处看起来奇怪的判断,背后通常都埋着一个真实踩过的坑。你自己搭一套要踩三年的坑,人家的代码注释里两行就写完了。

好了,说回怎么读。

第一站:读测试,不是读实现

这是整篇最想让你记住的一条。

测试文件是这个框架唯一一份「输入—输出」都写死的文档,而且它保证是对的——因为它一旦不对,持续集成就红了。README 可能过期,架构图可能是三年前画的,注释可能跟代码脱节,只有测试不会撒谎,它天天在跑。

而且回测引擎这类程序,测试的可读性异常地高。原因是它的输入天然就是一张表:几根K线,每根有开高低收和量。作者要测「止损有没有在正确的那根K线上触发」,就得手工造一小段K线,把想触发的那根压低到止损线以下,然后写明:我期望这次回测产出一笔交易,离场原因是止损,进场在第几根、离场在第几根。

freqtrade 就是这么干的。它的回测测试目录里有一个专门做功能性验证的文件,里面几十上百个用例,每一个都长成同一个形状:一张手写的小K线表(每行标着开、高、低、收、量,外加几列进出场信号的零一标记),配上这次要用的止损设置和几个开关,最后跟一个期望结果的列表——每笔期望交易写明离场原因、在第几根进、在第几根出。表格上方还有一行注释,直接写着这个用例想验证什么,比如「跌到止损位」「先触发止损还是先触发目标」。

你就读这个文件。 不需要看懂 Backtesting 这个类内部的任何一行,你只看这几十个用例,就能把这个引擎的行为边界摸得七七八八:

  • 信号出现在哪根K线,交易开在哪根K线?——用例里注释直接写了,信号在上一根、开仓在下一根。这一条比任何架构图都值钱,它就是这个引擎防前视的核心约定。
  • 一根K线同时触发了止损和止盈目标,它算哪个?——去找那个用例,期望结果写着谁赢。
  • 止损用的是最低价还是收盘价?——找那个「收盘只跌了一点、最低价跌穿了」的用例,看它期不期望成交。
  • 移动止损怎么抬?加仓减仓怎么算成本?挂单超时不成交怎么办?——每一类都有一组用例。

这个测试容器本身也值得多看两眼:它是个字段不多的具名元组,把「这次回测的全部输入」和「期望的全部输出」摆在同一个结构里。能被这样写成一张表的东西,说明这个引擎的行为是确定性的——同样的输入必然得到同样的输出,没有随机、没有外部状态。这个性质你在别的框架里未必有,值得第一时间确认。

顺带说一句,另一类测试也很好用:那种「造几笔典型交易当作夹具」的文件。它把一笔进行中的交易、一笔已平仓的交易、一笔挂着没成交的委托,各造一个出来。你读这几个夹具,就知道这个系统认为一笔交易「正常」的样子长什么样——哪些字段必填、哪些能为空、一笔单子有几种状态。

怎么找到这些文件?很简单,tests 目录下带 backtest、engine、simulat 这类词的文件,从最短的那个开始读。测试文件越短越该先读,短说明它测的是原子行为。

qlib 那边的组织方式不太一样,它的测试按模块分了子目录,回测相关的、数据层的、算子的各占一块,另有几个从数据准备一路跑到出报告的整链路测试。整链路那种别一上来就读,信息太杂;先读单点的。

第二站:读数据结构,它决定了这个系统能表达什么

翻完测试,你手上有了一堆具体行为,但还是散的。接下来要找的是骨架。

骨架不在主循环里,在数据结构里。回到面馆那个比喻:小票上没有「折扣」这一栏,这家店就表达不了折扣。同理——

去找这个框架里「一笔交易」和「一个持仓」是怎么定义的。 通常在名字带 model、trade、position、order、decision 的文件里。找到那个类,把它的字段从头到尾看一遍。这件事花不了十分钟,但它能一次性回答一大批问题:

  • 一笔交易只有一个进场价,还是有一串委托挂在下面? 前者意味着这个引擎压根不支持分批建仓——你想测「跌了再补一点」的想法,它表达不了。后者意味着它把「交易」和「委托」拆成了两层,那你就得留心两者的成本是分开算还是合并算。
  • 有没有「已成交数量」和「委托数量」两个不同的字段? 有,说明这个系统认真对待部分成交;没有,说明它假设你挂多少成交多少——这是个很强的假设,在流动性差的品种上直接不成立。
  • 有没有「离场原因」这个字段? 有的话去看它的取值有哪些。这个枚举列表本质上是这个引擎认可的全部平仓路径,一眼看完你就知道它支不支持你想要的那种离场方式。
  • 手续费是一个字段还是两个? 进场费和出场费分开存,说明它允许买卖费率不同;只有一个,说明它假设对称。
  • 时间字段有几个? 委托时间、成交时间、更新时间分开存的,说明这个系统认真对待「下单到成交之间有间隔」这件事。只有一个时间的,基本可以判定它假设瞬时成交。

freqtrade 那个交易模型就是典型的「交易挂一串委托」的两层结构,委托那一层把委托量、已成交量、剩余量、均价、成本分成不同字段存着,还专门有一组以 safe 开头的取值方法——存在这类方法本身就是个信号:它说明这些字段在真实世界里经常是空的,交易所返回的数据没那么规矩。这种从代码形态反推现实约束的读法,比读注释收获大。

qlib 的口径又不同。它的委托对象是个轻量的数据类,带方向枚举,还有个方法专门把各种五花八门的方向表示(正负数、字符串、数组)统一成内部枚举。光看这个方法你就知道它的上游是什么样——它接的是模型打分,打分是个连续数,正负决定买卖。这跟一个接人写规则的框架,气质完全不一样。它的持仓对象则明确区分了「股票市值」和「现金」,还有一组跟结算相关的方法,说明它认真处理了「卖出的钱当天不能用」这类规则。

读数据结构的收益是最高的:字段是有限的,一眼看得完,而每一个字段的有无都是一次设计表态。一个系统不会做它的数据结构里存不下的事。

第三站:读主循环,骨架全在那儿

现在才轮到那个一千多行的大文件。但你已经不是空着手进去了——你知道它要产出什么样的交易对象,也知道它在几十个具体场景下该给出什么答案。

主循环通常好认:名字里带 loop、run、backtest、execute、step 的那个方法。找到之后,别读它的实现,先读它调用的方法名,把顺序抄下来。 一屏之内你就能得到这个引擎的动作序列。

freqtrade 的回测类里,方法名几乎是自解释的:判断这根K线数据合不合法、处理已有交易的离场、检查要不要调整仓位、处理挂着没成交的委托要不要撤要不要改价、判断还有没有空仓位、检查有没有新的进场信号、真正开仓。这个顺序跟它实盘那套循环几乎是同构的,交易机器人开机之后,它一整天到底在忙什么那篇把实盘那一侧讲得更细。

顺序本身就是信息。 「先处理离场、再处理进场」意味着同一根K线上腾出来的仓位当根就能用;反过来的实现,钱要等下一根才能周转。这个差别在高换手策略上能差出很多。

qlib 走的是另一条路:它把执行拆成了可以嵌套的层级,外层执行器把一个交易决定往内层传,内层可以按更细的时间粒度再拆一遍。这个结构你只要看几个方法名和参数就能感觉到——凡是出现「内层决定」「层级基础设施」这类概念的,都是为多时间粒度嵌套准备的。结构复杂度不是坏事,它对应的是这个框架想解决的问题:日频给出组合权重,日内再拆成一笔笔委托去执行。

读主循环时给自己定个规矩:只读控制流,不读计算细节。 遇到一个算价格的函数,记下它的名字和调用位置,不要跳进去。跳进去你就出不来了。等你把整条骨架抄完,再回头挑三个函数跳进去——就是下面这三个。

至于这条骨架每一步内部到底在算什么,那是一个回测引擎内部到底在做什么的活,这篇只管教你怎么自己找到它。

第四站:策略示例,最后才看

框架自带的示例策略,是最多人第一个打开的文件,也是最该最后打开的。

原因很简单:示例策略是写给「已经理解了这个框架」的人看的。它里面每一个函数名、每一个返回值的含义,都是前面三站里那套约定的产物。你不懂约定去读它,读到的只是一堆你不知道为什么这么写的代码,然后你会照抄——照抄一段你不理解其前提的代码,是把别人的假设不打折扣地继承过来。

反过来,走完前三站再读示例,你会读出完全不同的东西。你会注意到它在哪一步做了平移、注意到它某个地方用了看似多余的判断(那多半是防某个坑)、注意到它有意避开了某类写法。示例策略这时候变成了一份「作者认为的正确用法」的对照答案,而你手上已经有题目了。

怎么把自己的规则写成这个框架能接住的形状,是把策略写成能被测试的样子那篇的事。

无论读哪个框架,都要找到答案的三个问题

上面是顺序。下面是目标——你读这一趟,至少得带走三个答案。这三个问题跨框架通用,找不到答案就等于没读。

问题一:成交价,是哪根K线的哪个价

这是最重要的一个,因为它直接决定收益。

具体要问清楚三件事:信号出现在第几根、成交发生在第几根、成交取的是那根的哪个价。

找法有两条。第一条,回到测试。 前面说的那些功能用例,进场那一根和信号那一根差几格,答案直接摆在期望结果里。第二条,在源码里搜取价相关的函数名——带 price、rate、deal、fill 这类词的方法,通常就那么两三个。找到之后看它的入参:它拿到的是当前这根K线的哪几个字段?

这里有个判断标准,很容易记:如果一个取价函数能拿到当前这根K线的收盘价、最高价、最低价,那它在原理上就有能力偷看未来——因为在这根K线走完之前,这三个数都还不存在。能不能偷看,取决于它实际用了哪个。用开盘价一般安全,用收盘价成交就要打问号,用最高最低价来「择优」成交,基本可以判定这个回测不可信。

qlib 把这件事做成了一个可配置项,成交价从行情表达式里取,默认取什么、能改成什么,以你手上那个版本的文档为准。这种设计的好处是灵活,坏处是默认值悄悄决定了你的结论,而大多数人从来不改默认值。

成交价背后那一整套「凭什么认为你成交了」的假设,回测引擎凭什么认为你成交了回测里的成交假设分别从工程侧和机制侧讲过,这里只教你在源码里怎么定位它。

问题二:成本,扣在哪一步

第二个问题比第一个更容易漏,因为成本这件事在代码里往往是分散的。

要找的是三样东西,而且要分清它们分别在哪一步生效:

手续费:一般好找,交易对象上就有费率字段。要确认的是它进出场是不是分开算、是按成交额还是按成交量算、有没有最低收费。

滑点:这个最容易找不到,因为很多框架根本没有。找法是搜 slip、impact 这类词,搜不到就基本可以确认它没建模——那意味着你的每一笔都成交在理论价上。

冲击成本和容量限制:找那种「按当根成交量给成交数量封顶」的逻辑。qlib 的撮合模块里就有一处按成交量做裁剪的处理,还有一个按现金上限反推能买多少的函数。有没有这类裁剪,决定了你的回测认不认「你自己的单子会推动价格」这件事。

还要问一个位置问题:成本是在成交那一刻扣的,还是在算收益的时候统一减的? 这两种做法在单笔上等价,在复利上不等价——前者会减少你下一笔的可用资金,后者不会。换手越高,这个差越大。

怎么把成本设到一个不骗自己的水平,是成本模型该怎么设才不骗自己的话题,机制侧则见交易成本假设

问题三:有没有防前视偏差的机制

前两个问题问的是「它怎么算」,这一个问的是「它防不防你算错」。

前视偏差就是在模拟的那个时刻用到了当时还拿不到的信息。它的可怕之处在于完全不报错,而且结果会好得让你舍不得怀疑。机制层面的展开见前视偏差

在源码里,防这个的机制通常长这么几种样子:

信号平移。 用第几根的信号、在第几根成交,这个约定是否被强制。找法还是回到测试用例。

数据切片。 看主循环喂给策略的是什么:是整段历史的完整表格,还是截到当前时刻为止的一段?喂整段的框架,防前视的责任就完全落在策略作者身上——你自己写指标的时候一不小心用了个居中的窗口、或者做了个全局的标准化,就漏了。

专门的检测工具。 少数框架会做这个。freqtrade 就在优化相关的目录下单开了一块做分析,里面分了两类:一类查前视,一类查「递归」——也就是同一个指标因为喂进去的历史长度不同而算出不同的值。一个框架专门造工具来防的坑,说明它在真实用户里出现得非常频繁,这块单独讲在一个框架专门造工具来防的坑。递归这一类的机制本身,指标启动长度讲得更细。

时点数据。 更严格的框架会在数据层就解决:财务这类会被追溯修订的数据,存的时候连「什么时候才能知道这个数」一起存。qlib 的测试目录里就有专门针对这件事的用例。在数据层解决这个问题,比在策略层小心翼翼地防,可靠一个数量级。

如果三种一个都没有——那不代表这个框架不能用,只代表这道防线整个落在你自己身上,你得自己在策略侧加。知道了这一点,比不知道强太多。

目录结构给你的第一印象

最后说个省时间的小技巧:进一个新仓库,先只看目录名,别看文件。

回测框架的目录长相高度趋同,一眼能认出几块:数据层(下载、转换、存储)、执行层(撮合、账户、持仓)、策略层(接口、示例)、优化层(参数搜索)、报告层(统计、绘图)、接口层(命令行、通知、接口服务)。

目录的粗细比例,就是这个框架的重心。 freqtrade 里跟交易所对接、跟通知服务对接的部分占了很大篇幅,回测反倒只是其中一块——这说明它的重心在实盘执行,回测是它的一个功能而不是它的全部。qlib 那边最重的是数据层和模型层,回测那一块相对紧凑,还专门有一块做强化学习——这说明它的重心是「用机器学习做投资研究」,回测是用来验证研究结论的。

这个判断要在读代码之前做完。 它决定了你该不该继续读下去:你要找的是一个能对接真实券商的执行框架,还是一个能管住几千只票的因子研究平台?重心不对的框架,你读得再懂也用不上。

另外,测试目录的丰满程度也是个很直接的质量指标。测试目录跟源码目录一样厚的项目,和只有零星几个测试的项目,你要用哪一个来跑决定你真金白银的实验,答案不用想。

什么时候可以停下来

读源码最大的风险不是读不懂,是读上瘾。你会不知不觉从「搞清楚它怎么算成交价」滑到「这个缓存机制设计得真妙」,一晚上过去,你什么策略也没验证。

给自己一个明确的停止标准:当你能对着一段自己的规则,说出这个框架会在哪根K线、按哪个价、扣多少成本地成交,并且能预测出一个大致的方向,你就读够了。

再补一个更硬的验收动作:照着这个框架的测试写法,自己造一小段假K线,把你的规则跑一遍,然后先手算一遍预期结果,再看引擎给的答案对不对。 对得上,说明你对它的理解是准的;对不上,那个差就是你还没搞懂的地方——而这个差,比你再读五百行源码值钱得多。

剩下的部分,等你真的需要改它的时候再读。读源码是为了消除假设,不是为了通读。

⚠️ 风险提示

本文讲的是阅读开源回测框架源码的方法,不构成任何投资建议、软件推荐或收益承诺。文中对具体开源项目的所有描述,均来自对上述指定快照版本源码与目录结构的阅读,未实际运行任何程序,也不保证在你的版本、你的配置下同样成立——具体行为请以你手上那个版本和你自己的核对为准。文中不给任何阈值、默认值或参数数值,此类内容全部随版本变化。读懂一个回测框架,只是让你知道它替你做了什么假设,它不会让你的策略变得更赚钱,历史回测结果也不预示未来收益。 完整风险提示见免责声明

小结

  • 读一个陌生的回测框架,顺序是测试 → 数据结构 → 主循环 → 策略示例。从架构文档或者示例策略开始,是最费劲收益最低的两条路。
  • 测试是唯一保证正确的文档,因为它天天在跑。回测引擎的测试尤其好读——输入是一小张手写K线表,输出是期望的交易列表,行为边界全摆在明面上。
  • 数据结构决定了这个系统能表达什么。 一笔交易有几个字段、委托和交易分不分层、有没有已成交量、有没有离场原因枚举——每个字段的有无都是一次设计表态,而系统不会做它存不下的事。
  • 读主循环只读控制流,把调用顺序抄下来就够了,别跳进具体计算函数。顺序本身就是信息:先离场还是先进场,直接影响资金周转。
  • 无论读哪个框架,必须带走三个答案:成交价取的是哪根K线的哪个价、成本在哪一步扣(尤其滑点有没有建模)、有没有防前视偏差的机制。三个问题都找不到答案,等于没读。
  • 判断读够了的标准,是你能预测它对你的规则会给出什么结果。验收方式是自己造一小段假K线,先手算再对答案。

一句话说完这篇:别从头读代码,先去翻它的测试——那里写着「给它这几根K线,它就该做这几笔交易」,看完这个,你就知道它是怎么替你花钱的。

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

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