← 返回教程库

回测里成本那一格,别急着填一个"对的数"

最后更新 2026-08-19
📚 量化与 AI 投研 📖 E 线 · 回测引擎 ⏱ 约 16 分钟 R2 · 注意风险
你将学到
  • 动手设成本模型时,知道费率、滑点、冲击成本这三格各自填什么,以及每一格填完拿哪个可执行的检查去验
  • 会跑"死亡成本线"这个动作:零成本基线起步按倍数逐档加码,结果目录与汇总表怎么组织,余量怎么算怎么读
  • 知道按时间收的成本该挂在引擎的哪一步、天数按什么口径算、为什么要用独立累计器

你把策略跑通了,图也画出来了。现在光标停在配置文件里那一行上:成本,等号后面空着。

这一篇不解释成本是怎么回事。成本一共由哪几件事构成、为什么高频策略最先死,回测里的交易成本假设 讲完了;滑点从哪来、回测为什么系统性低估它,滑点 讲完了;自己把价格推走的那部分按什么规律收、为什么它就是策略容量,冲击成本:自己把价格买高了 讲完了;费率那一档凭什么不属于你,回测按贵宾价算账,实盘按你的真实价算账 讲完了。

这一篇一个机制都不重讲,只回答一件事:你手上那个配置文件,每一格具体填什么、填完拿什么去验。 至于"这笔单子算不算成交了"归 回测引擎凭什么认为你成交了,引擎在一根 K 线内部走了哪几步归 一个回测引擎内部到底在做什么

第一格:费率怎么填

动作一,费率和策略参数放在不同的配置区块,最好是不同文件。 费率属于环境不属于策略,不进搜索空间。物理上分开的作用,是让"把它塞进优化器"这个动作需要多一步手工操作——让容易犯的错写起来别扭,这是最便宜的防线。

动作二,买入和卖出各给一个字段,哪怕现在两边填同一个数。 qlib 的交易所对象就是把开仓成本率和平仓成本率拆成两个独立参数的。你现在填一样,这个结构保住的是以后改对的可能;只留一个字段,以后想改就得动数据结构。

动作三,最低收费单独建一格,没有这条规则也要显式填零,不许留空。 这一格的后果和别的格不同:qlib 在算"这些现金最多能买多少"的时候,会先判断现金够不够付那笔最低收费,不够就把成交数量直接置零并写一条 debug 日志。也就是说它决定一笔单子下不下得出去,不只是贵一点。

动作四,在费率那一行旁边写一条注释,注明这个数从哪来:合同截图、券商官网、还是框架默认值。三个月后你自己分不清这两者,而这份结果能不能拿给别人看,取决于这条注释。怎么确定自己那一档,回测按贵宾价算账,实盘按你的真实价算账 有完整算法。

填完的自查:grep 一遍参数搜索空间的定义,确认里面不含任何成本字段。一分钟能做完,但这是唯一能挡住"优化器把费率调到零"的检查。

第二格:滑点怎么填

先决定要不要做成随场景变化的。判据是一个能直接跑出来的量:把回测里每一笔成交的时刻打上标签——落在开盘收盘几分钟、消息落地当天、止损触发这三类里的算一桶,其余算另一桶,看两桶各占多少笔。两类场景的真实滑点差多远,滑点 讲透了;这里只用结果做决定:成交时刻散在日历上的,固定比例够用;明显扎堆在第一桶里的,这一格得加形状。

加形状有两条路,都不难写:

一,拿振幅当标尺。 最高价减最低价再除以前收盘,滑点比例正比于它。数据现成,日线就够。但必须取前一根、或者前面若干根的平均,不能取当根——当根振幅是收盘之后才知道的量,拿它决定当天的成交成本,等于把未来信息塞进了下单那一步。

二,拿平均真实波幅这类现成的波动度量当标尺,逻辑一样,它天然用历史窗口算,不容易碰前视问题。

实现上抄不到现成的:三套系统各自把滑点做成什么形状,滑点 有对照表,结论是没有一套按波动缩放,这一层得你自己加。

写完之后三条评审判据,每条都能当场核:

  • 搜一遍有没有"从收益里减掉滑点"这种写法。 滑点要落在成交价那个字段上,不落在结果上。
  • 看滑点因子有没有带符号的方向变量,买入卖出取反号。写成对称随机数,长期期望是零,等于白建。
  • 看"这笔合不合法"的判断用的是哪个价。 涨跌停价带、最小变动价位、可用现金这几处比较,都必须用滑点之后的价。这一条最容易在评审里被放过去,因为它看起来只是一行比较语句的参数写错了。

第三格:冲击成本怎么填

判据是一个自查动作:把回测里每一笔的成交量,除以那一天该品种的真实成交量,把这个比值的分布画出来。分布整体贴着零,这一格填零是诚实的;尾巴上有一批高得离谱的,那批交易现实里做不出来,这一格必须建。这个比值的规律和它为什么是容量天花板,冲击成本:自己把价格买高了 讲完了,这里只用它的分布做决定。几行代码就能跑完,和 拿到一份数据,先跑哪几项自查 里的那些动作是一类的,建议固定进流程。

要建的话,qlib 的交易所对象里有这个参数,它默认是关的(源码默认值和文档推荐值并不相同,具体以你手上那个版本为准)。打开之后,工程上有两件事要抄进操作手册:

一,别纠结这一格填得准不准。 它的作用是让曲线在规模变大的时候开始往下弯,弯的斜率对不对是第二位的事。源码作者自己在这段上方留了一条 TODO,说这个调整后的成本率可能偏高。

二,开了这一格,每份结果都必须标注跑的是多大资金。 这一格是规模进入模型的唯一入口:关着的时候标注也没意义,因为不同规模跑出来是同一条曲线;开着而不标注,读的人会默认它适用于任何规模。把资金规模写进结果目录名,并在汇总表里单列一列,和档位并排。

主菜:死亡成本线怎么扫

三格都填完,你仍然不知道自己填对没有。这一节的做法绕过这个问题:不去找那个对的数,去找让这套规则死掉的那个数。

这个数怎么定义、拿到之后怎么用

把成本当变量从零往上扫,收益曲线塌掉的那一档,就是这套规则的死亡成本线

给它起名字不是修辞。"曲线看着塌了"是一句观感,写不进结论;一个有名字的数可以被减、被比、被别人复核。

拿到之后第一个动作是减法:死亡成本线,减去你的真实成本,剩下的那段是余量。

减数必须是你自己那一档真实成本,不是框架默认值,也不是你觉得"比较保守"的那个数。用错减数,整个余量都是假的,而且是往乐观方向假。

余量怎么读:

  • 余量宽,比如真实成本只有死亡线的几分之一。这套规则对成本假设不敏感,那三格填糙一点不影响结论,注意力可以挪到别处。这个结论只在当前换手水平上成立,换手一改要重算。
  • 余量窄,真实成本已经贴着死亡线。它不是"风险高",它是。区别在于应对方式完全不同:风险高可以减仓缓解,脆减仓没用——余量是个比值,仓位变小它不变。
  • 死亡线低于真实成本,没什么好讨论的。这套规则在你的环境里本来就是负的,回测显示为正只说明成本填小了。

余量还有一个用处:它能被第三方从两头查。 你说"我这套年化二十个点",对方没有任何办法核。你说"这套规则的死亡成本线在往返千分之几这个量级,我的真实成本大约是它的几分之一",对方立刻可以从两个方向盘问你——去核那个量级的费率在市场上普遍不普遍,或者反过来问你换手多少、为什么这个换手对应这条线。能被两头盘问的数,才是能拿出去说的数。

四步跑法

第一步,跑一次严格零成本的基线。 费率零、滑点零、冲击成本零、最低收费零,全关。这一档是你的参照系:后面每加一档,你读的是"这一档吃掉了百分之多少",没有基准就只能读绝对值。它还顺带是个诊断——零成本都不赚钱,成本这三格怎么填都没意义,问题在别处。

第二步,档位按倍数排,不按加法排。 以你估计的真实成本为基准,跑零倍、半倍、一倍、两倍、四倍、八倍。用倍数是因为你要找的是量级:死亡线"落在两倍到四倍之间"和"落在二点七倍",对决策没有任何区别,而后者的精度是假的。

第三步,把换手相关的参数和成本档位做成一张二维表。 一行是一组换手参数(持有期、信号阈值之类),一列是一个成本档位,格子里放同一个指标。死亡线会随换手挪,只扫成本这一个维度看不见这件事。表填完你常会看到:零成本那一列里表现最好的参数组,往往是死亡线最低的那一批——最佳行和最结实的行不是同一行,而只跑零成本那一列,你连它们不是同一行都看不见。换手这个变量还牵动什么,调仓频率 讲得更全。

第四步,把输出组织成能横着读的形状。 这一步看着像杂务,但它决定前三步跑出来的东西你能不能真读出结论。四条:

  1. 结果目录用档位命名,不用时间戳。 时间戳只回答"这是哪次跑的",你现在要问的是"这是哪一档"。目录名里带上档位倍数,排序出来天然就是你要的顺序。
  2. 每一档把完整的成本配置原样复制一份进结果目录。 别只记"两倍"。三个月后你还记得两倍,但记不得当时的基准是多少,而没有基准的倍数是个废数。
  3. 汇总表至少四列:收益、成交笔数、成本总金额、资金规模。 只看收益,塌方到底是"每笔更贵了"还是"笔数本身变了"你读不出来。成本抬高会让一批边缘小单根本下不出去,笔数是个会跟着动的量,单独列出来,塌方的原因当场就分开了。
  4. 所有档位画在同一张图上,横轴用档位倍数并取对数刻度。 档位是按倍数排的,线性横轴会把低档位挤成一团,而塌方点经常就落在那一团里。每档单出一张图是最常见的浪费——你要看的是形状,形状只存在于叠图里。

每一档必须整跑,不能事后减

有人想省事:跑一遍,然后按"每笔成本乘以笔数"从收益里扣掉,一次算出所有档位。

这是错的。成本不是从结果里减掉的一个数,它前置地改变了持仓本身。 三处,都在源码里摆着。

其一,成本决定这笔单下不下得出去。 前面那条 qlib 的现金约束:现金不够付最低收费,成交数量直接置零。成本抬高之后,一批边缘小单从"成交"变成"没成交",持仓路径整个变了。

其二,成本决定你能开多大的仓。 Vibe-Trading 的引擎在调仓那一步,先按目标权重把这批开仓单全部"定价"出来,每一单算出保证金加佣金作为占用;总占用超过可用资金时,它不是砍掉几单,而是对整批做一次缩放:

planned = _plans(1.0)
if sum(order.cost for order in planned) > self.capital + 1e-9:
    low, high = 0.0, 1.0
    for _ in range(50):
        mid = (low + high) / 2.0
        ...

在零到一之间二分一个缩放系数,把整批订单按同一比例缩到刚好装得下。佣金是算在占用里的——成本一调高,这个系数就变小,所有仓位一起变瘦。这个效应在事后做减法的模型里完全不存在。

其三,成本自己会随成交量变。 开了冲击成本那一格之后,成本率是参与率的函数,而参与率取决于你实际成交了多少:仓位一缩,参与率降,成本率跟着降。这是个反馈回路,只有重跑才能收敛。

结论很简单:每一档成本,重跑一次。 慢就慢点,这是唯一诚实的做法。

🚧 避坑

死亡成本线是个诊断量,不是优化目标。 我见过一种用法:跑完发现死亡线太低,于是回头改规则,直到死亡线抬上去为止。这个动作看起来是在提高鲁棒性,实际上你已经把成本档位变成了一个搜索维度——你在拿历史数据挑那些"恰好在这段历史上对成本不敏感"的规则,而这种不敏感很可能和收益一样是历史巧合。扫成本的目的是知道自己站在哪,不是把自己挪到一个好看的位置上。 死亡线低就是低,那说明这个方向不适合你的成本环境,换方向,别改数。

一张填表清单

动手的时候按这个顺序过一遍,每一格都要有明确答案,不许留"默认"。

这一格 该填什么 怎么自查 什么时候可以省
费率 你合同上的那一档,买卖分开两个字段,旁边写来源注释 grep 搜索空间定义,确认不含成本字段 永远不能省
最低收费 有这条规则就建,没有就显式填零 看引擎里有没有"现金不够付最低收费则置零"这条分支 你所在市场没有这条规则
滑点 至少一个带方向的固定比例;成交扎堆在异动时刻的加波动缩放 成交时刻分桶统计;核合法性判断用的是滑点后的价 永远不能省,可以简化
冲击成本 参与率分布尾部不可忽略时建,并标注资金规模 画每笔成交量占当日真实成交量的比值分布 分布整体贴零,且策略不会选中清淡品种
持仓型成本 融资利息、永续合约资金费率、期货展期 看它挂在推进时间那一步还是下单函数里 你的品种没有这类费用

按时间收的那一格,在按次收的引擎里怎么记

最后一行最容易被漏,不是因为作者忘了,是因为引擎里没有它该挂的地方:按次收的成本天然挂在成交事件上,一笔单子成交触发一次扣费,代码路径现成;持仓型成本没有成交事件可挂,你一动不动它照样在扣。所以得自己给它找挂点。四条做法:

挂点在推进时间的那一步,不在下单函数里。 引擎每往前推一个结算周期,遍历一遍当前持仓,按市值或者名义本金乘上对应的日费率,扣一笔现金。写进下单函数是最常见的错法,那样只有交易的那天会扣,而它恰恰属于不交易也要扣的那一类。

天数口径单独确认,别拿 K 线根数当天数。 这类费用通常按自然日计,周末假期照收,而你的 K 线序列里没有非交易日。用"走过几根 K 线"当天数,一年下来会少算相当一截,而且偏差是单向的。正确做法是取相邻两根 K 线的实际日期差,让引擎把跳过的那些天补回来。

用一个独立的累计器,别混进"每笔成本"那本账。 两个理由。一是报表上要能分开看,两个总额加在一起那个数没有解释力。二是回到扫档位那件事——死亡成本线扫的是按次那一档。持仓型成本抬高对收益曲线的作用接近整体平移,按次成本抬高会改变"哪些单子还下得出去",作用是塌方。两种形状混进同一格参数,扫出来的死亡线会失真,失真方向还取决于你的持有期,没法事后校正。

留意这类费率可能是负的。 有些按周期结算的费用在特定市场状态下方向会反过来,持仓反而收到钱。实现里如果写了"成本取绝对值"或者"成本不小于零"这类保护,就把这种情况静默抹掉了。断言它非负是你自己加的假设,不是市场的。

⚠️ 风险提示

本文讲的是回测系统中成本模型的设置方法与验证手段,不构成任何投资建议、买卖信号或收益承诺,也不推荐任何框架、平台、券商或标的。文中所有关于开源项目的描述,均来自对指定快照版本源码与文档的阅读,不是运行程序得到的结果,也不保证在你的版本、你的数据、你的市场上同样成立。文中刻意不给任何费率、税率、滑点比例的具体数值——这些全部由你的券商合同、交易所规则与现行监管政策决定,并且会随时变动,请以官方口径和你自己的账单为准。还要强调一句:把成本模型建得再细,回测依然不等于实盘,它只是少了一处失真,并不会因此获得预测未来的能力。完整边界见 免责声明

小结

  • 费率格四个动作:与策略参数分文件放、买卖两个字段、最低收费单独建、来源写成注释;填完 grep 一遍搜索空间定义。
  • 滑点格先分桶统计成交时刻决定要不要加波动缩放;加的话标尺取前一根或多根均值以避开前视;评审只核三件事——落在成交价上、带方向符号、合法性判断用滑点后的价。
  • 冲击成本格的判据是参与率分布,不是你有多少钱;开了这一格就必须在结果里标注资金规模,因为它是规模进入模型的唯一入口。
  • 主菜是扫档位:零成本基线起步,按倍数逐档加码,找出塌方那一档也就是死亡成本线,再减去你的真实成本得到余量。余量宽说明结实,余量窄说明脆——脆减仓没用,因为余量是比值。
  • 扫的时候把换手参数做成二维表的另一维,因为最佳行和最结实的行常常不是同一行。
  • 每一档整跑重来,不能事后从收益里减:成本决定单子下不下得出、决定整批订单被缩放到多小,开了冲击成本之后它还和成交量互为因果。
  • 输出按档位而不是时间戳组织,每档存完整成本配置,汇总表带上成交笔数和资金规模,所有档位叠在一张对数横轴的图上。
  • 按时间收的那一格挂在推进时间那一步,天数按自然日算,用独立累计器,别断言它非负。
  • 死亡成本线是诊断量不是优化目标,跑出来低就换方向,别回头改规则去把这条线抬上来。

一句话说完这篇:别纠结那一格该填多少,把它从零一路加上去,看你这套东西在哪一档咽气,再回头看看你自己那一档离它有多远。

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

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