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

回测到底在算什么:它替你重放了历史,也替你做了一堆假设

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

你第一次拿到一份回测报告的时候,大概率是这个感受:年化多少、最大回撤多少、胜率多少、盈亏比多少、平均持仓几天……密密麻麻一整屏数字。你盯着看了一会儿,心里冒出来的其实是个挺土的问题——这些数字,到底是怎么算出来的?

这个问题看着幼稚,却是整个量化里最该先问的一个。回测报告不是市场给你的答案,它是一个程序,按照一堆事先约好的规矩,把历史"演"了一遍之后交上来的作业。它算什么、不算什么、遇到说不清的地方按哪条规矩填——全是人定的。你不知道这些约定,就没办法判断那份报告哪几行可以信、哪几行只是纸上的。

站内术语卡 回测 给了一句话的定义,够你听懂别人在说什么。这篇要做的是另一件事:把程序内部那层循环拆开,让你看见"重放一遍历史"这句话,落到代码里到底是几个动作。

先说人话:它在放录像,不是在推演

想象你在看一场已经踢完的球赛录像。你可以暂停、可以倒回去、可以一帧一帧看,但有一件事你做不到——你的观看不会改变球员的动作。录像里的一切早就定死了,变的只有你。

回测就是这样一场重放。历史行情是那盘录像,你的规则是你在旁边做的动作。程序做的事是:把录像按时间往前推一格,问一句"按你的规矩,这一刻你会干什么",把你的动作记在一个虚拟账本上,然后推下一格。推到头,把账本汇总成一堆指标。

这里藏着一个很多人从没意识到的要点:回测模拟的是你,不是市场。 市场那部分是录好的,不管你在里面买了一万块还是一个亿,录像里的价格一动不动。这不是程序偷懒,是历史数据的性质决定的——那些价格是在一个"没有你"的世界里形成的,你事后再怎么演,也变不出一个"有你参与"的历史。

换个更家常的比方。复盘一局已经下完的棋,你可以在第三十手换个下法,然后自己想象对手会怎么应。可对手真正的应手,你其实并不知道——你只是在拿自己的想象补全对面那一半。回测比这个严谨,因为它明写了自己在假设什么;但性质是同一个性质。

投资层:这份报告只做了三件事

对你的决策来说,一份回测报告的可信度,全部落在三件事上。

第一件是定价:这一笔按什么价成交。 你的规则说"今天收盘后信号触发,买入",那到底按哪个价买?今天的收盘价?明天的开盘价?明天的成交均价?换一个答案,同一套规则的结果可能天差地别。

第二件是记账:扣多少钱。 手续费、印花税、买卖价差、你自己的单子把价格推高的那部分。这几项单独看都不大,乘上你一年交易多少次之后,能把一条向上的曲线压成一条向下的。

第三件是统计:把那条账户曲线揉成指标。 这一步反而是最没有争议的——公式是公开的,谁算都一样。

所以你拿到一份报告,正确的看法顺序其实是反的:先翻它的定价和记账设置,最后才看收益。 大多数人的顺序恰好相反,先被年化数字勾住,然后一路往下找理由说服自己。真正决定这份报告值不值钱的,是前两件事里那些没人跟你商量过的默认值。

系统层:一根 K 线里,程序其实什么都不知道

日线数据一天只给四个价:开盘、最高、最低、收盘。可这一天里,价格实际走了成千上万步。四个数把这一整天压扁了。

于是就出现了回测最根本的那个困境:这一天里,是先到最高点还是先到最低点? 数据里没写。而这个顺序,直接决定你的止损有没有被打掉、你的止盈有没有先兑现。

freqtrade 的文档对此说得很直白:它没法知道一根 K 线内部价格是怎么走的,所以只能定一套约定,并且把这套约定完整列出来。挑几条对理解最有帮助的(措辞按机制概括,具体细则以你手上那个版本的文档为准):

  • 开仓按这根 K 线的开盘价成交,除非你自己写了定价逻辑;
  • 只要你要的价格落在这根 K 线的最高最低区间之内,就当作按那个价全额成交,不计滑点;
  • 卖出信号的平仓,发生在下一根 K 线的开盘价上;
  • 止损就按止损价成交,哪怕这根 K 线的最低价比止损价还低;
  • 同一根 K 线里既碰到止损又碰到止盈,判定顺序是固定写死的,而且假设低点先于高点发生——这条的用意是先保护资金,宁可判成止损;
  • 追踪止损那部分反过来:假设高点先发生,先把止损位抬上去,再拿抬高后的止损位去和低点比。

把这一串读完你会有个感觉:这些不是"市场就是这样",而是"程序被规定成这样"。它们全是为了在信息不足时给出一个确定的答案。有些偏保守(止损优先),有些偏宽松(不计滑点)。而策略这东西有个讨厌的特性——它会自动往宽松的那些缝里钻。你不是故意的,但你反复调参数、留下那个成绩最好的版本,这个过程本身就在挑选"最会利用假设"的那一版。

freqtrade 给这件事留了一个很聪明的自查口子:用更细的周期数据去还原 K 线内部。你在跑日线或小时线回测的同时,让它加载分钟级数据,在有信号、有持仓的那些时段按细粒度重新走一遍。文档里的提示值得抄下来——如果换成细数据之后结果变化很大,说明你的策略正在吃回测假设的便宜。

它文档里举的极端例子很能说明问题:假设同一根整点 K 线上,好几个品种同时出信号,而你同时只允许持有一笔。粗粒度下,程序只能按这根 K 线整体处理,一根 K 线内只成得了第一笔,仓位要等这根走完才腾出来;细粒度下,第一笔可能几分钟就平掉,位置立刻让给第二个品种。同一套规则、同一段行情,交易清单完全不是一回事。

至于逐格重放这件事在代码里长什么样,Vibe-Trading 的回测引擎在模块开头一句话交代得很清楚:加载数据 → 生成信号 → 预先算好目标仓位 → 逐根 K 线执行并施加市场规则 → 出指标。而它在对齐信号时定了一条硬约定:

Signal is shifted by 1 bar (next-bar-open semantics)

信号整体往后挪一格,用下一根的开盘价成交。这一格看着不起眼,却是回测里最重要的一格——不挪,你就是拿这根 K 线自己算出来的信号,在这根 K 线自己身上成交,等于用当天的结果决定当天的动作。这类错误单独成篇,见 前视偏差

三个你从没同意过、却一直生效的假设

假设一:你的单子不影响价格

这是最隐蔽的一条。回测里你砸进去一千万,成交价一分不差;现实里同样一千万砸进一只日成交额只有几千万的票,价格自己就往上跑了。

qlib 的交易所层把这件事做成了两个可选参数:一个是成交量上限(限制你在某个品种上单笔能吃掉多少),一个是市场冲击成本率。要点在于——这两项默认都是不开启或为零。也就是说,你不主动打开,回测默认你是个不存在的人:想买多少买多少,价格纹丝不动。

它的文档注释里对冲击成本给了一句推荐取值的说明。我不打算把那个数字搬过来当结论,因为它随品种、随资金量、随市场差得极远,抄一个数比不填还危险。真正该记住的是机制:回测默认的"你"是没有体重的,而现实里的你有。 这一层展开在 回测里的交易成本假设

假设二:你想成交就能成交

停牌、涨跌停、流动性枯竭,这三件事在现实里天天发生,在回测里默认统统不存在。

qlib 的做法是把这些约定写进数据规范里。它的行情表有一条注释说得很干脆:某一天的收盘价字段为空,就把这只股票在那天视为停牌。涨跌停则交给一个可配置的表达式,算出来为真的那天就是不可交易。这里有个细节值得咂摸——如果你不设这个涨跌停配置,它会打一条警告出来,大意是"打到涨跌停的票也会被照常买卖"。

请注意这条警告的方向:默认状态下的回测,比现实宽容。 不是它想骗你,是"限制"这件事需要额外的数据和额外的配置,而一个刚上手的人不会去填。于是那些现实中你根本买不进的一字涨停板,在你的回测里被顺利买进了,并且贡献了最漂亮的那几笔收益。

流动性这一层同理:历史数据告诉你那天那个价格上有成交,但没告诉你那个价格上一共只有多少量。你要的量超过了,剩下的部分现实里根本吃不到。这块展开在 回测里的成交假设

假设三:费率是固定的、而且是已知的

qlib 的交易所层把开仓和平仓的费率分开设置——这个设计对 A 股特别对症,因为卖出那侧多一道印花税,两边天然不对称。它还带一个最低费用参数,模拟"再小的单子也要收一个起步价"(这些都是当前默认值,可配置,具体数以你手上那个版本为准)。freqtrade 那边则是默认取交易所的挂牌费率,也允许你在命令里手动指定一个值覆盖掉。

机制清楚了,问题也就出来了:你是拿今天的费率,去跑十年前的历史。 印花税调整过、交易所费率结构变过、券商佣金一路降过,而回测里这条线是平的。对一年交易几次的方法,这点误差可以忽略;对一天来回好几趟的方法,它可能就是全部盈亏。

freqtrade 文档里还有一段关于交易限额的说明,性质相同但更少被注意到:交易所的最小下单量、价格精度这些规则,程序只能拿到当前的版本,历史上的拿不到。它举的例子是:某个品种最小下单量按币计是固定的,可换算成钱要乘以当时的价格——今天的价位下折合二十几美元,而回测区间里价格曾经高一倍多,那时候的最小金额就是五十美元上下。程序拿今天的限额去过滤历史订单,本身就是一处轻微的信息错位。

为什么回测的结果,天然往好看的方向偏

把上面几段串起来,你会发现偏差不是偶然,是结构性的。

说不清的地方,默认往宽松填。 不计滑点、不计冲击、不设涨跌停限制——每一条单独看都只是"没配置",合起来就是一个对你格外友好的平行世界。

你会反复改,改到好看为止。 这是人的问题不是程序的问题。同一段历史被你翻来覆去测了两百遍,最后留下的那一版,很难说清有多少是规律、多少是这段历史特有的噪音。这就是 过拟合

数据里可能藏着当时拿不到的信息。 财报的公布日、指数成分名单、被修订过的历史数值,任何一处错位都会让成绩凭空变好。

一条曲线只是一次抽样。 就算前面所有假设都干净,你也只是看到了历史这一次的走法。把一次结果当成对未来的期望,是概率思维上的基本错误,期望值与概率思维 那篇讲得更系统。

🚧 避坑

回测报告的精度是假的精度。 一份报告告诉你年化 23.7%,小数点后那一位会让你不自觉地相信整个数字很扎实。可这个数字的上游,是一串你从没逐条检查过的假设:按哪个价成交、算不算滑点、涨跌停管不管、费率取的哪一年。任何一条改一下,那个 23.7 就变成别的样子。把它读成"二十几"都算乐观,它准确的含义是——在这一堆假设成立的前提下,历史这一次走出来的一个数。

失效边界与常见误用

把回测当预测。 回测是一次检验,它能做的是排除:一套规则连历史都过不去,就不用往下想了。反过来不成立——历史过得去,不代表未来行。这两个方向的强弱完全不对称,很多人却把它当成同一件事。

只看收益,不看配置。 拿到别人的回测结果,第一件该问的不是"年化多少",而是"成交价用的哪个字段、费率填的多少、滑点算没算、停牌涨跌停处理了没有"。这几个答案不清楚,那个年化数字没有任何意义。

换个成交价字段就天差地别,却当没看见。 同一套规则,成交价从开盘价换成收盘价、或换成成交均价,结果如果变化剧烈,说明这套规则的收益主要来自执行时机而不是判断本身。而执行时机恰恰是现实中最不由你控制的部分。这是个极好用的压力测试,可惜很少有人主动做。

拿回测结果反推仓位。 "历史最大回撤只有 8%,那我可以上三倍杠杆。" 这句话的危险在于,那个 8% 是在上述所有宽容假设下算出来的,而杠杆是实打实的。回撤数字的不确定性,比收益数字的不确定性更该被认真对待。

忘了回测跑的是你写下的规则,不是你脑子里的规则。 你脑子里那套方法有很多说不出口的临场判断——"那种情况我肯定不会买"。这些例外不在代码里,所以不在回测里;同样,它们在你真正下单的那一刻,也未必还在你脑子里。

小结

  • 回测在做的事:把历史行情当录像逐格重放,每一格问一次"按你的规矩现在该干什么",记进一个虚拟账本,最后把账本揉成指标。市场那半是录好的,模拟的只有你。
  • 一份报告的可信度,全落在定价(按什么价成交)和记账(扣多少费)这两件事上;统计那一步反而没什么争议。
  • 一根 K 线内部的价格顺序数据里没有,程序只能按一套写死的约定填。这套约定有的偏保守、有的偏宽松,而策略在反复调参中会自动往宽松的缝里钻。
  • 三个默认生效的假设:你的单子不影响价格、你想成交就能成交、费率固定且等于今天的费率。这三条现实里都不成立,而且每一条都往对你有利的方向偏
  • 检验的方向是不对称的:回测能证伪,不能证实。历史过不去就别做了;历史过得去,也只是"还没被排除"。

一句话记住这篇:回测不是在预测未来,是把历史按一堆你必须亲自过目的规矩重演一遍——报告好不好看,一半取决于市场,一半取决于你允许程序对自己有多宽容。

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

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

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