← 返回教程库

延迟和容量:两个回测里根本不存在的约束

最后更新 2026-08-19
📚 量化与 AI 投研 📖 E 线 · 实盘边界 ⏱ 约 23 分钟 R2 · 注意风险
你将学到
  • 说得出延迟的几个来源分别在流程的哪一段产生,并按量级把它们排出先后,知道该优化哪一档、不该碰哪一档
  • 用日均成交额、参与率容忍度、建仓天数和目标权重,一步步倒推出一套规则的组合规模上限
  • 判断自己手上的回测引擎对资金规模是否敏感,以及在不敏感的引擎里做容量测试为什么必然得到「无限大」

同一天,同一只标的,你的回测记了一笔成交,你的实盘什么也没发生。

你去查代码。信号那一行的条件判断是对的,下单那一行也确实被调用了,日志里能看到请求发出去,也能看到回来的响应。没有异常,没有报错,没有分支走错。你把两边的输入数据逐行比对,一模一样。

然后你会陷入一种很特别的困惑:代码是对的,结果是不一样的,而且你找不到任何一行可以指着说「就是这儿」。

因为差异根本不在代码里,在代码之外——在你的回测世界所依赖的两条从来没人写下来的假设上。

回测世界的三条物理定律

拿一个回测引擎跑出来的每一笔成交,背后都站着三条谁也没声明过的规矩:

数据是瞬间到达的。 循环走到某一根 K 线,那根 K 线的全部数值就在你手上,包括收盘价。

订单是瞬间成交的。 你在这一根上决定买,成交就发生在这一根,没有等待,没有部分成交,没有撤单重挂。

资金是无限的。 你的下单量不影响价格,也不影响能不能成交。把初始资金乘以一千,收益率一分不变。

这三条不是哪个作者写进去的,恰恰相反——它们是「没写」造成的。 一个循环变量 for bar in bars,天然就意味着「这一根的信息在这一根可用」;一个 fill_price = bar.open 的赋值,天然就意味着「想成交就成交」。要打破这三条,你得主动写代码去打破;不写,它们就默认成立。

实盘把三条全推翻了。前两条对应的是延迟,第三条对应的是容量。这篇就讲这两件事:延迟到底从哪几个地方来、各自多大量级;容量怎么从公开数据倒推出一个数量级。

至于「代码上要补哪些东西才能对接真实通道」,那是 从回测走到实盘,代码上要补哪些东西 的活;「跑起来之后该盯什么」,是 实盘跑起来之后,该盯着什么 的活。这篇只负责让你先看清这两道墙有多厚。

延迟:不是慢一点,是中间插进了一段你没测过的价格

先说人话

你在手机上点了一份外卖,从按下确认到吃进嘴里,中间有几段完全不同的时间。

商家看到订单要一会儿;做菜要一会儿;骑手接单、赶到店、取餐要一会儿;路上要一会儿。这几段的长短差得很远——做一碗面可能几分钟,跨半个城的路程可能小半个钟头。

现在假设你在做一件更讲究时机的事:你听说某家店在做限时特价,赶紧下单。你按下确认那一刻,特价还在;等订单真的落到商家那儿,特价已经结束了。 你不是「慢了一点吃到饭」,你是根本没吃到你以为自己买的那个东西

这两种理解的差别就是延迟的全部要害。多数人第一反应是「晚一点而已」,实际上它的后果是:你的决策依据的是一个已经过期的世界,而中间这段过期的时间里,价格自己动了。

投资层:延迟贵不贵,看它和什么比

先把一个投资概念说清楚:持有期,就是你从建仓到平仓打算拿多久。这是判断延迟严重程度的唯一尺子。

一套打算拿几周的规则,中间插进去几秒钟的延迟,几乎什么也不改变——几秒钟里价格能走的那点距离,相对于你几周要赚的那段,小到可以忽略。而一套打算在同一天内进出的规则,几秒钟就可能吃掉它全部的目标空间。

所以「我的系统延迟大不大」这个问题本身是问错的。正确的问法是:我的延迟,和我打算赚的那段价格移动,是不是一个量级。

第二件事更要命:延迟的代价不是均匀分布的。 市场平静的时候,几秒钟里价格几乎不动,延迟等于零成本;市场剧烈的时候,同样几秒钟价格能走出平时半天的距离。而你的规则最想成交的那些时刻——突破、放量、消息落地——恰好全都是剧烈的时刻。

换句话说,延迟专挑你最在乎的那几笔收费。 你按全年平均去估它,会得到一个安慰性的小数字;按真正贡献收益的那几笔去估,是另一回事。这一点和 滑点 是连着的:延迟是原因,滑点是你在成交记录里看到的那个结果。

第三件事:延迟会把你的信号变成一张过期的票。你的规则说「价格突破某条线就买」,那是在信号生成的那一刻成立的判断。等你的单子真的到了市场,价格可能已经在线上面很远的地方了。这时候你执行的,已经不是你测过的那套规则了。

有意思的是,freqtrade 直接给了一个配置项来处理这件事:一个入场信号在多少秒之后就不再被使用。这个选项的存在本身就是一份承认——信号有保质期,过了就该扔,而不是硬着头皮去追。

系统层:延迟的几个来源,以及它们完全不在一个量级上

把「下单慢了」拆开,你会发现它是好几段完全不同性质的时间加起来的。逐段看:

来源一:行情到达你手里

K 线收盘的那一刻,这根 K 线并不存在于你的程序里。它要先在交易所那边生成,再通过某个通道传给你。

freqtrade 的主循环里有一个细节把这件事挑明了:它在计算「睡到下一根 K 线开始」的时候,故意在时间点上加了一个小偏移量,代码注释写的是「加偏移是为了确保新 K 线已经被发出」。也就是说,作者明确知道「K 线时间到了」和「K 线数据拿得到」是两回事,于是宁可多等一小会儿,也不要拿到一根残缺的。

同一个系统里还有一条判断,决定某个品种的 K 线数据要不要重新拉——它比较的是「上次刷新时间加上一个周期」和「当前这根活跃 K 线的开始时间」。效果是:一个周期之内,最多只拉一次。 这条规则很合理(省流量、省限频额度),但它的代价你得看清楚:在两次刷新之间,你手上那份数据是静止的。你以为自己在看行情,其实你在看一张快照。

还有一层是通道本身。这个框架支持通过实时推送通道拿数据,文档里同时写了一句:如果推送连接失败或者被关掉,会退回到轮询接口。 这句话在故障排查时很重要——推送和轮询的时效性根本不是一个量级,而这个降级是自动发生的,你的策略代码完全感知不到。一个静默的降级,能让你的延迟量级跳一档,而它只体现在连接层。

来源二:你自己的计算

这一段最容易被低估,因为它是你自己写的,你会本能地觉得「我的代码很快」。

看一眼一个真实交易机器人每一轮循环里要干的事:读出当前持仓、算出这一轮可交易的品种清单、给清单里所有品种下载 K 线、逐个品种调用你的指标计算和信号判断、更新所有挂单的状态、检查超时、检查止损和退出条件、检查是否还有仓位额度、最后才轮到判断要不要开新仓。

这一整串是串行的,而且其中好几步的耗时随品种数量线性增长。 你的池子从十个变成一百个,指标计算那一段就长十倍。

这里有个连带效应特别值得注意:那套按成交额筛品种的插件支持用一段回看窗口来算成交额(而不是只看当下的快照),文档专门配了一条性能警告——如果把它放在筛选链的第一位,它要为所有可交易品种下载 K 线,非常耗时耗资源,建议先用别的过滤器把范围缩小。

看出这里的循环了吗?你为了把容量估得更准(用一段窗口的成交额而不是瞬时值),代价是让你的循环变慢,也就是延迟变大。 这两件事在工程上是直接打架的,而它们在回测里都不存在。

还有一条落差,官方文档自己用警告框标出来了:回测中每个回调每根 K 线最多被调用一次,而实盘中大多数回调每轮循环都会被调用一次,这会导致回测与实盘结果不一致。 这句话的含义比它看上去严重——你的自定义止损、自定义退出逻辑,在回测里一根 K 线只有一次判断机会,在实盘里一根 K 线内可能被问很多次。同一段代码,两个世界里的调用频率不同,行为自然就不同。

来源三:网络与限频排队

网络往返本身通常是这几段里最小的一档。真正吃时间的不是「传过去」,是排队

交易所和券商都对调用频率有限制。freqtrade 的文档里有一条警告写得很直白:限频这个配置项存的是两次请求之间的间隔毫秒数,而不是每秒允许多少次请求。 文档专门为此加了一条警告,说明很多人把这两者理解反了——而理解反了的后果是,你以为自己设的是上限,其实设的是下限。

限频的真实影响是这样的:你有一百个品种要更新,接口一次只能问一个,两次之间还必须隔一段固定的间隔。那么最后一个品种拿到的数据,比第一个品种老了整整九十九个间隔。 你的策略却在同一轮里把这一百份数据当成同一时刻的截面来比较、排序、选股。

这是一种没人提醒你的时间错位:不是数据错了,是同一份数据里不同的行,时间戳并不相同。品种越多、限频越紧,这个错位越大。

来源四:下单之后,成交之前

单子发出去不等于成交了。

这一段最好的证据不是文档里的某句描述,而是一个配置项的存在本身:freqtrade 有一组「未成交超时」的设置,规定一个未成交的入场单等多久之后撤掉、一个未成交的出场单等多久之后撤掉并按新价重挂。还配了一个计数——出场单连续超时多少次之后,触发紧急退出。

这一整套东西在回测里完全没有存在的必要,因为回测里没有「挂着不成」这种状态。它在实盘里是必需品,说明什么?说明**「单子挂在那儿一直不成」是常态,不是异常。**

这里还有一个耦合,藏在配置说明的一句备注里:如果你把超时的单位设成秒,那么主循环的间隔必须小于等于这个超时值。理由很直接:超时检查是在循环里做的,循环多久转一圈,你能表达的最小超时就是多久。 这句话的推广意义很大——你的轮询周期,是你所有时间类逻辑的分辨率下限。 你写「三秒不成就撤」,而循环三十秒转一圈,那你实际执行的是「三十秒不成就撤」,代码不会报错,也不会有任何提示。

来源五:钱和货的交收,量级比上面所有加起来还大

前面四段全是秒级以内的事。这一段是天级的,而且最常被忽略。

qlib 的持仓模块里有一个可选的结算机制,它的文档串写得非常清楚:开启现金延迟结算之后,这一步里卖出拿到的现金,在这一步里不能用——举的例子就是「你不能卖掉一只股票拿到现金,然后在同一步里去买另一只」。代码上的做法也很朴素:卖出的钱进一个单独的暂存字段,等到提交结算的时候才并进可用现金里。

Vibe-Trading 那边把这件事做成了各市场引擎的差异:A 股引擎的说明里明确写了当天买入的份额不能当天卖出;印度那个引擎标注的是交割制度带来的同样限制;而韩国那个引擎的注释专门说明了它不做同一根 K 线内的拦截,因为那个市场允许当日卖出。

这一层的重要性在于量级。 前面四段你优化到极致,省下来的是秒;而交收制度让你的钱在账上躺一天甚至更久。一个把资金周转算错的组合,损失的不是几个基点,是整整一轮机会。

而且这件事直接和 调仓频率 挂钩:你的回测里如果假设卖出的钱当天可以立刻买入,那你测出来的那个换手率,在实盘里根本达不到。你以为自己在测一套高频调仓的规则,实际上测的是一套只有在无摩擦世界里才成立的规则。

把这几段排个序

按量级从大到小:

来源 大致量级 你能改吗
交收与资金可用性 改不了,是制度
决策周期本身(多久判断一次) 分钟到小时 能改,但改了要重测
主循环耗时 + 限频排队 秒级,随品种数增长 能改,收益明显
行情到达(推送 vs 轮询) 差一个量级,看通道 能改,注意静默降级
网络往返本身 这几段里最小的一档 基本不用碰

这张表最大的用处是告诉你别优化错地方。 很多人一上来就折腾网络、换机房、优化数据结构,而他手上跑的是一套持有期以周计、还带着交收限制的规则。省下来的那点时间,相对于制度性的等待,连零头都算不上。

反过来,如果你的规则确实吃日内的短促移动,那么决策周期和主循环耗时就是你的天花板,而且它们是硬天花板——你没法让一个几十秒转一圈的循环去捕捉几秒钟的机会,无论代码写得多漂亮。

想知道自己到底处在哪一档,不需要搭什么监控体系,先记三个时间戳就够:信号生成的时刻、下单请求发出的时刻、成交回报到达的时刻。 把这三个数存下来,两两相减,看的不是平均值,是尾部——最慢的那百分之几发生在什么时候。多半就是行情最急的那几天。至于怎么把它变成日常盯盘的一部分,实盘跑起来之后,该盯着什么 那篇有系统的说法。

容量:从日均成交额倒推你最多能装多少钱

延迟说完,换另一堵墙。

容量这个概念本身——为什么存在上限、为什么成本涨得比规模快、为什么公开的好方法会自己失效——流动性与策略容量 那篇讲得很完整,这里不重复。这一节只干一件它没干的事:给你一条能拿现成数据算出来的估算路径。

先想清楚在估什么

搬家的时候,你要判断一辆货车能不能一次拉完。你不会去量车厢容积——你会去看楼道最窄的那个转角。 决定「能不能一次搞定」的从来不是最宽裕的那个环节,是最紧的那个。

组合的容量也一样。它不是你所有持仓容量的加总,是被最难交易的那一只卡住的。

五步倒推

第一步:给每只标的量一个日均成交额。

两个细节决定这一步准不准。用中位数,不用平均数——一个品种的日均成交额很容易被少数几天的暴量抬起来,而你真正需要保守的恰恰是清淡的那些日子。只算你真会交易的那个时段——如果你只在开盘后的一段时间里下单,那全天成交额对你没有意义,你能吃到的只有那一段。

第二步:把你的冲击容忍度翻译成参与率上限。

参与率就是「你的成交额占同期该品种总成交额的比例」。先问自己一句:为了建这个仓,我最多愿意在成交均价上多付多少? 这个「多付多少」是你自己定的,不是市场给的。定完之后,用你的成本模型把它反解成一个参与率——冲击成本 那篇里的平方关系告诉你这两者不是线性的,参与率的容忍度会比你直觉的小得多。

这一步本身没有正确答案,它是个偏好。但把它显式写出来这个动作非常有价值:多数人从来没定过这个数,于是他们的容量假设是「无穷大」——不是他们相信无穷大,是他们从来没想过要定。

第三步:单只标的、单日能吃的量 = 日均成交额 × 参与率上限。

第四步:乘上你愿意花的建仓天数,得到单只标的的仓位上限。

这一步藏着一个取舍。天数拉长,能装的钱变多,但你暴露在「价格自己跑掉」的风险里也更久——今天想买的东西,可能不等你买完就已经涨上去了。这正是延迟和容量在这里合流的地方:容量约束的标准解法是拆单拉长时间,而拉长时间等于主动增加延迟。两头不能同时占。

第五步:除以这只标的在你组合里的目标权重,得到组合规模上限;然后对所有标的取最小值。

举个不带数字的走法:某只标的算出来单只能装 X,而它在你组合里占一成的权重,那么组合规模上限就是十倍的 X。每只标的都这么算一遍,最小的那个就是你的答案——楼道最窄的转角。

三处必须修正,否则这个数偏乐观

修正一:用退出侧算,别用建仓侧。 买入可以慢慢来,卖出常常由不得你慢。而你必须卖的那些日子,恰恰是市场恐慌、成交额萎缩的日子。所以第一步的日均成交额,最好用市场承压时段的读数,而不是全期读数。用风平浪静时的市场厚度,去估一个多半会在风浪里发生的动作,是个乐观得离谱的假设。

修正二:把换手折进去。 容量吃掉的是成交量,不是持仓量。同样的持仓,一年换两次和一周换两次,年度吞吐量差几十倍。所以正确的做法是拿你的年度成交总额去比市场的年度成交额,而不是拿持仓比。 很多人漏掉这一步,于是给一套高换手规则算出了一个低换手规则的容量。

修正三:逐日算,然后取一个保守的分位数。 用全期数据算一次得到的是一个平均意义上的容量,而你需要知道的是「最差的那些日子里我还剩多少余地」。把这个估算在历史上的每一天各做一遍,得到一条随时间变化的容量曲线,然后取低分位。这条曲线的形状本身也很有信息量——如果它在某几段时间塌陷得特别厉害,那几段就是你的规则真正的脆弱点。

反向验算:一个更快的自查

正着估很费事,反着验只要一分钟:拿你现在(或者打算投入)的资金规模,按你的持仓权重和调仓频率,算出你在每只标的上的日均参与率。然后问一句:这个数落在我第二步定的容忍带里吗?

超出了,你不需要再算什么容量,你已经知道答案了。

系统层:容量约束在代码里长什么样

qlib 提供了一个成交量限额。 它接受两种口径:一种是随时间累积的量(比如日内累计成交量),当作限额时要减掉已经成交的部分;另一种是实时值(比如买一档的挂单量),可以直接当上限用。买卖两侧还能分别设,多个限制取最严的那个。

关键在超出之后发生什么:订单数量会被直接裁掉,相当于市场只吃下了一部分。

这个「裁掉」的行为,是做容量测试最有用的一个开关。因为被裁掉的那部分交易在回测里就消失了,收益也跟着变。 于是你可以这么用:把限额从很松逐步收紧,看收益曲线在哪一档开始明显变形。变形的那一档,就是你的规则开始撞到容量墙的地方。这不是在配参数,这是在做压力测试。

freqtrade 那套按成交额筛品种的插件,示范的是估算侧的工程做法。 它支持用一段回看窗口内的成交额来排序和过滤,而不是只看当下的快照——这正是上面第一步「用一段时间的稳定读数」的代码形态。它还同时提供了下限和上限两个门槛:低于下限的太清淡不碰,高于上限的也可以剔掉。

上限这个设计值得多看一眼。 为什么要剔掉成交额最高的?因为最热闹的那几个品种往往也是最拥挤的——所有人都在同一个地方,共享同一个出口。成交额高不等于你出得来,它只说明平时有很多人在换手;而你需要跑的那一刻,那些人可能全都站在你这一边。

至于第三点,就有点扎心了:那条性能警告说明,认真估容量本身是要付出计算代价的,而这份代价恰好落在你最不想它变慢的那个主循环里。这大概也是为什么这一步在多数人的系统里干脆被跳过——不是不知道该做,是做了会疼。

一个必须先确认的前提

上面所有的容量测试,都有一个隐含前提:你的引擎对资金规模是敏感的。

而多数引擎不敏感。它们的滑点函数只接受价格和方向两个参数,压根没有「数量」这一项;它们的成本模型全是按比例收的线性项。一堆线性项加起来还是线性的,结果就是:把初始资金乘以一千,收益率一分不变。

所以有一件事必须排在容量测试之前做:先确认你的引擎对规模有反应。 方法极其简单——把初始资金放大若干倍,重跑,看收益率变没变。一点没变,说明你的引擎认为市场是个无限深的水池,在这样的引擎里做任何容量测试,结论永远是「无限大」。

要让它有反应,你至少得装上一个跟参与率挂钩的非线性项,或者打开成交量裁剪。这两条路的语义不一样:一条说「你能买到,但更贵」,另一条说「你根本买不到这么多」。现实里两件事同时发生。这一层怎么和撮合规则配合,回测里的成交假设 那篇排了一把从宽松到严格的尺子。

两堵墙其实是同一堵

写到这儿可以把它们合起来看了。

延迟和容量看着是两件事——一个是时间,一个是钱——但它们的根源是同一个:你不是这个市场的观察者,你是它的参与者。

回测把你放在观察者的位置上:数据流过,你在旁边判断,你的判断不影响任何东西。实盘里你是参与者:你要排队才能看到数据(延迟),你的动作会改变别人看到的数据(容量)。

而且这两堵墙是互相顶着的。容量的标准解法是拆单、拉长成交时间,用时间换厚度;可拉长时间正好是在增加延迟暴露。反过来,你为了压低延迟而一次性打完,付出的就是更高的参与率。你手上其实只有一个旋钮,两端各写着一个你不想要的东西。

理解到这一层,回测那条曲线该怎么读就清楚了:它回答的是「这套逻辑成不成立」,它结构上回答不了「这套逻辑能装多少钱、跑多快」。 后两个问题不在这类工具的能力范围内,不是换个参数就能问出来的。想看一个真实框架的主循环具体长什么样,freqtrade 的一轮循环里到底发生了什么 那篇是逐段拆开的。

⚠️ 风险提示

本文讲的是量化系统在延迟与资金容量两个方向上的工程约束与估算思路,属于投资教育内容,不构成任何投资建议、买卖信号或收益承诺,不推荐任何标的、平台、券商、交易所或框架。文中所有关于开源项目的描述,均来自对指定快照版本源码与文档的阅读,不是运行结果,也不保证在你手上那个版本、你的市场、你的通道上同样成立。文中刻意不给任何具体的时间数值和资金规模数值——这些数字随市场、品种、通道、制度和你自己的规则而变,任何看起来精确的数都是假的精确,具体口径请以你自己的实测和现行交易规则为准。容量估算给出的是数量级,不是许可证;估算通过也不等于你就该把那么多钱投进去。 完整风险提示见 免责声明

小结

  • 回测的三条隐含定律——数据瞬间到、订单瞬间成、资金无限大——不是谁写进去的,是「没写」造成的。要打破它们,得主动写代码去打破。
  • 延迟的要害不是「慢了一点」,是你的信号和你的成交之间插进了一段你从没测过的价格移动。而且它专挑行情最急、你最在乎的那几笔收费。
  • 判断延迟严不严重,唯一的尺子是它和你打算赚的那段价格移动是不是一个量级。持有期以周计的规则,秒级延迟无关紧要;日内规则,主循环周期就是硬天花板。
  • 延迟的几个来源量级差得极远:交收制度是天级、决策周期是分钟到小时级、主循环加限频排队是秒级、网络往返最小。 别去优化最小的那一档。
  • 有两个陷阱藏在细节里:限频配的是两次请求的间隔而不是每秒次数,它会让同一轮拉回来的多个品种在时间上错位;轮询周期是你所有时间类逻辑的分辨率下限,写了更细的超时也执行不出来。
  • 容量估算的路径:日均成交额取中位数并限定在你真会交易的时段 → 把冲击容忍度翻译成参与率上限 → 相乘得单日可吃量 → 乘建仓天数得单只仓位上限 → 除以目标权重得组合上限 → 对所有标的取最小值
  • 三处必须修正:用退出侧的市场厚度(承压时段而非全期)、把换手折进去(容量吃的是成交量不是持仓量)、逐日算取低分位(你要知道最差那几天还剩多少余地)。
  • 反向验算比正着估快得多:拿你现在的资金规模倒推出实际参与率,看它落不落在你定的容忍带里。
  • 做容量测试之前先确认一件事:把初始资金放大若干倍重跑,收益率变了吗。 一点没变,说明你的引擎认为市场无限深,在它上面测出来的容量永远是无穷大。
  • 延迟和容量是同一堵墙的两面:你不是市场的观察者,是参与者。而且拆单降冲击就得拉长时间,拉长时间就得多担延迟——只有一个旋钮,两端都写着你不想要的东西。

一句话说完这篇:你在回测里买卖的那个市场,是个消息立刻传到、单子立刻成交、钱怎么加都不嫌多的地方;而真实的市场里,消息要排队、单子要等、你买得越多东西越贵——所以那条曲线告诉你的是这套想法对不对,从来不是这套想法能装多少钱、跑得多快。

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

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