回测里的交易成本假设:改一个小数点,一半策略当场死掉
有个实验你可以在脑子里做一遍。
你手上有一套规则,跑了回测——也就是拿历史数据假装重走一遍,看看按这套规则会赚会亏。结果年化 30%,看着挺稳。现在别改规则,一个字都别动,只把「每买卖一次要交多少钱」这个设定往上抬一点点,从万分之三抬到千分之一。再跑一遍。
如果这套规则一年只进出几次,曲线基本不动。如果它一周进出好几次,那条向上的曲线很可能直接被压平,甚至翻到零线以下。
同一套规则、同一段历史、同一份数据,只因为一个你几乎没认真看过的参数,结论从「能做」变成「不能做」。这不是极端情况,这是量化里最常见的一种死法——而且死得特别安静,因为默认参数一般是系统替你填的,你从头到尾没做过任何决定。
先说人话:成本假设到底是什么东西
想象你在机场换外币。牌子上写着两个价:银行买你的美元给一个价,卖给你美元给另一个价,中间差一截。你换过去再换回来,钱没动地方,但少了一块。柜台还要收一笔固定的手续费,不管你换多少都收。
现在假设你要做一件事:靠汇率的小波动来回换,赚差价。你在纸上算,一年来回换两百次,每次赚千分之三,看起来很美。可你只要把中间那截差价和每次的固定手续费算进去,两百次一乘,赚的还不够填。
这就是交易成本假设的全部含义:你在纸面上推演的时候,到底把「换一次要掉多少肉」算成了多少。 算少了,纸面收益就是假的;而人在做纸面推演的时候,天然倾向于把它算少——因为掉的那块肉不显眼,它不是一笔转账,是从每一次操作里悄悄扣走的。
再换个更狠的比喻。成本不是从你的收益里减掉一个数,它是乘在你操作次数上的一个数。你操作得越勤,它被放大得越厉害。一个只有微弱优势的规则,配上高频操作,就像一个只赚一分钱的生意配上一块钱的快递费——生意本身没错,运费把它吃了。
投资层:这几个假设,各自在遮什么
回测里的「成本」通常不是一个数,是好几件性质完全不同的事被打包在一起。拆开看,你才知道自己漏了哪一件。
第一件:费率用哪一档
券商和交易所的收费往往是分档的——交易量大的客户便宜,散户贵;有的地方还分「主动成交」和「挂单等成交」两种价。回测系统必须替你选一档。
问题在于,它选哪一档,你的成绩就变成哪一档。用大机构的费率去检验一套散户能不能做的规则,结论天然偏乐观。反过来也有人为了「保守」把费率设得离谱地高,然后得出「什么都做不了」的结论,同样没用。
这里还有一个特别容易被忽略的:费率是按次收的,一次往返要收两遍。买的时候一遍,卖的时候一遍。所以你看到的「单次成本万分之几」,在一笔完整的买卖里要翻倍。有些系统在文档里会专门强调这一点,就是因为漏算一半的人太多。
第二件:算不算滑点
滑点说的是你想按某个价成交、实际成交价却差了一点这件事。回测里最省事的做法是假设它不存在——你在图上看到某根 K 线里有这个价,就当你按这个价成交了。
这个假设有多离谱,取决于你交易什么。买一只成交活跃的大盘股,差个一两跳;碰上开盘几分钟、或者一只本来就没什么人交易的品种,差的可能不是一两跳。而回测不会告诉你这一点,它只会安静地按你要的价格给你记账。
第三件:自己把价格买高了没有
这一件比滑点更隐蔽。滑点还只是「市场本来就在动」,冲击成本说的是你这笔单子本身把价格推走了。你要买的量越大、这个品种平时成交越不活跃,推得越狠。
几乎所有回测系统在这一层都有同一个盲点:成本被写成一个固定比例,跟你下多大的单没有关系。 你买一万块和买一个亿,每股的成本被算成一样。这直接导致一件事——回测里你的策略规模可以无限放大而成绩不变,现实里它有个天花板。你在纸面上看到的「这套方法能管一个亿」,多半是这个盲点的产物。
第四件:有没有最低收费
很多市场的佣金是「按比例收,但不低于某个数」。这条规则对大单毫无影响,对小单是毁灭性的:单子越小,那个固定的最低收费摊到每股上就越贵,实际成本率会往上飙。
后果是,一套「分散买很多只、每只买一点」的方法,在纸面上看不出问题,落到现实里每一笔都在踩最低收费。如果回测系统没建这个模型,你永远发现不了。
第五件:买和卖收得一样吗
有些市场对卖出单边征税,有些不。这意味着买入和卖出的成本天然不对称。如果回测只用一个数字同时代表买和卖,那么你的每一次往返都在系统性地少算一头。
系统层:三套系统各自怎么建模
把三套真实系统的做法摆在一起看,你会发现「交易成本」这件事根本没有标准答案,每一套都在不同的地方做了不同的妥协。
| 系统 | 费率从哪来 | 滑点 | 成本随下单量变吗 |
|---|---|---|---|
| freqtrade | 交易所默认费率(最低档) | 明确假设为零 | 不变 |
| qlib | 配置里给开仓/平仓两个费率 + 最低收费 | 不单独建模 | 不变(可选限量) |
| Vibe-Trading | 按市场分引擎,各自一套费用结构 | 固定比例 | 不变 |
三行看完,第三列全是「不变」。这不是巧合,下面会说为什么。
freqtrade:先承认自己用的是最好的价
它的文档在讲基本概念时就把话说死了:所有收益计算都含费用,而回测、参数搜索、模拟盘这三种模式一律使用交易所的默认费率,也就是最低那一档(这是文档明确写的口径);只有真实下单时,才用交易所实际收的那个数。
有意思的是它的取值逻辑。源码里那段拿到「挂单成交」和「主动成交」两个费率之后,取的是两者里更大的那个,日志会直说这是「最坏情况」:
self.fee = max(fee for fee in fees if fee is not None)
所以它同时做了两个方向相反的选择:在挂单/主动这一维取最保守,在客户分档这一维取最优惠。你要是不知道这一点,就会误以为「默认值已经很保守了」。
它也留了口子:命令行可以直接给一个费率覆盖默认值,而且文档专门注明这个值会被应用两次——进场一次、出场一次。这句注明本身就说明了漏算一半是个常见错误。
关于滑点,它在「回测所做的假设」那一节里写得毫不含糊:只要价格落在那根 K 线的最高最低之间,所有订单都按你要的价格全部成交,没有滑点。同一节里还有一条特别值得记住的细则:止损被触发时,成交价按止损价算,但实际亏损会比止损价多出两倍费用。也就是说,你以为止损把亏损锁在某个位置,费用还会在那之外再啃一口。
这套写法的价值不在于它建模有多精细,而在于它把假设一条条列出来了。列出来的假设是可以被质疑的,没列出来的假设才是真正杀人的那种。
qlib:把最低收费认真建进去
qlib 的交易所模块把成本拆成三个参数:开仓费率、平仓费率、最低收费。(这几个都是配置项,默认值随版本而定,官方示例配置里给的值和源码默认值本身就不一样,用之前务必看你手上那份。)
拆成开仓、平仓两个数,正是为了表达前面说的第五件事——买和卖可以不一样。在对卖出单边征税的市场里,平仓那一档天然更贵,用一个数糊过去就是系统性低估。
更值得看的是最低收费的处理。它在计算「这些现金最多能买多少」的时候,专门算了一个临界价:
critical_price = self.min_cost / cost_ratio + self.min_cost
if cash >= critical_price:
max_trade_amount = cash / (1 + cost_ratio) / trade_price
else:
max_trade_amount = (cash - self.min_cost) / trade_price
这段的意思是:金额大到一定程度,成本就是按比例算;金额没到那条线,成本就是那个固定的最低收费,得先把它整个从现金里扣掉再算能买多少。换句话说,小单的成本率不是常数,是随金额变化的。 一个把资金切成很多小份的方法,在这条逻辑下会被如实地惩罚——这正是很多简化版回测看不到的东西。
至于下单量的影响,它提供了一个可选的成交量上限设置,可以写成「不超过当日累计成交量的某个比例」之类的表达式。但要注意:这个参数默认是不开的。 不开的时候,你想买多少买多少,价格和成本都不受影响。所以「能不能买到这么多」这件事,在默认配置下根本不在检验范围内。
Vibe-Trading:不同市场,不同的费用结构
它的做法是按市场分成不同的引擎,每个引擎自己声明一套费用结构,因为不同市场的收费方式压根不是同一种东西:
- A 股引擎:佣金按比例收但设了最低收费门槛、过户费按比例、印花税只在卖出时收、再叠加一个滑点比例。四项加起来才是一笔交易的真实代价。
- 加密永续引擎:区分挂单费率和主动成交费率,另外有按固定周期结算的资金费率——这是一笔持仓越久扣越多的费用,跟你交易多少次无关。
- 期货引擎:按合约品种查手续费和保证金比例,还要乘上合约乘数。
(以上各项的默认数值都写在代码里且是可配置项,实际以你的券商、交易所和当时的政策为准,不要拿代码默认值当事实。)
这份清单本身就是干货:「交易成本」在不同市场是完全不同的几件事。 持有加密永续合约,你就算一动不动,资金费率也在按周期扣;做 A 股,你不动就不花钱,但每次卖出都要交税。用同一套成本假设跨市场套用,等于用汽油车的油耗算电车的电费。
它的滑点处理是一个固定比例,按方向推价格:买的时候往上推一点,卖的时候往下推一点。比「假设没有滑点」诚实,但仍然和下单量无关。
三套系统的共同盲点
回到那张表的第三列。三套系统,没有一套让成本随下单量增长。
这不是它们偷懒,而是这件事真的很难建模——你要预测「我这笔单子会把价格推走多少」,需要的信息(挂单簿的深度、当时的对手方)在日线级别的历史数据里根本不存在。所以工程上的通行做法是把它简化成常数,然后把这个简化留给你自己去意识到。
于是有了一条很实用的判断:回测告诉你的是「这套逻辑在成本为常数的世界里成立不成立」,它不告诉你「这套逻辑能管多少钱」。 后面那个问题,回测结构上就回答不了。
为什么一改假设就死掉一大片
有了前面的拆解,这件事就很好理解了。
成本是乘在换手上的。 一套一年调仓两次的方法,一年只交四次费;一套一周调两次的方法,一年交两百多次。同样把单次成本抬高万分之五,前者一年多掉万分之二十,后者多掉一个百分点还不止。所以每次「改成本参数就死一片」的时候,死掉的一定是操作最勤的那批——不是因为它们的逻辑更差,而是因为它们放大倍数更高。
微弱优势经不起任何加价。 很多短周期规则的单次期望本来就只有千分之几。往返成本一旦逼近这个量级,剩下的就是零。这类规则不是「不太赚」,是在成本线的两侧非黑即白:成本略低于阈值,它是印钞机;略高于阈值,它是碎纸机。而你选的那个费率参数,决定了它落在哪一侧。
成本还会吃掉复利的根。 每次少赚一点,看着不多,但它是从每一次的本金基数上扣的。扣掉的部分不参与后面的滚动,长期看差距会比你直觉估的大得多。
所以真正该做的不是「找一个正确的成本值」,而是把成本当变量扫一遍:从乐观的一档一直加到明显悲观的一档,看那条收益曲线是平缓下沉还是断崖式塌方。断崖式塌方意味着这套方法的全部生存空间都押在「成本必须足够低」上,而这恰恰是你最控制不了的一件事——费率会变,市场活跃度会变,你的资金规模一变冲击成本也变。这一遍扫描用 滑点与手续费敏感度 就能粗算:把单次成本和年换手次数填进去,几档成本下的收益差距会直接列出来。
顺带说一句,这也解释了为什么很多公开分享的漂亮曲线在你手上复现不出来:分享者用的可能是机构费率、零滑点、无限容量,而你三项全占反面。这中间的落差不需要任何人撒谎就能产生。
别把「我已经设了手续费」当成「我已经算了成本」。 手续费只是四五件事里最容易算的那一件,也是唯一一件明码标价的。滑点、自己把价格推走的那部分、小单踩最低收费、买卖不对称,这几项加起来往往比手续费本身还大,而且全都不会在任何一张账单上出现。一套只设了手续费的回测,对成本的估计是系统性偏低的——注意是系统性,也就是说它每一笔都偏低,不会正负抵消。
失效边界与常见误用
把成本假设当成「调参」的一部分。 有人会一路往下调费率,直到曲线好看为止。这已经不是检验了,这是在给自己编故事。成本参数应该由你的真实交易环境决定,定完就锁住,不参与任何搜索。
只测一个成本值。 单点结果没有信息量,你不知道自己站在悬崖边上还是平地上。至少测三档:乐观、你实际的、明显悲观的。
忽略持仓型成本。 融资利息、加密永续的资金费率、期货的展期,这些跟你交易多少次无关,只跟你持有多久有关。一套长期持有的方法可能交易成本极低,持仓成本却不低。两者是两笔账。
用小资金的成本假设去推大资金的结论。 前面说过,三套系统的成本都不随规模变化。所以「这套方法年化多少」这个结论,只在某个隐含的资金规模内成立,而那个规模从来没人写出来过。
把成本问题和别的回测毛病混在一起。 成本假设偏乐观,和用了当时拿不到的信息(前视偏差)、和参数被历史噪音磨得过分合身,是三种完全不同的病。三者都让成绩虚高,但检查方法和补救方式毫无交集。把它们分清楚,才知道该修哪儿。想按体系顺着读,可以回到量化与 AI 投研卷。
以为算准成本就万事大吉。 扣掉真实成本之后还剩下的优势,才是起点,不是终点。一套扣完成本还剩一点点的方法,仍然要面对它到底是真优势还是历史巧合这个更根本的问题。
小结
- 回测里的「成本」不是一个数,至少是五件事:费率用哪一档、算不算滑点、算不算自己把价格推走、有没有最低收费、买卖是不是对称。
- 成本是乘在操作次数上的,不是从总收益里减一次。操作越勤,同样的假设偏差被放大得越厉害——所以每次收紧成本假设,最先倒下的永远是换手最高的那批。
- 三套真实系统的做法各不相同:一套明确声明「零滑点、用交易所最低档费率」并把假设逐条列出;一套把开仓/平仓拆成两个费率并认真建了最低收费的临界逻辑;一套按市场分别建模,A 股、加密永续、期货各有各的费用结构。
- 三套系统有个共同盲点:成本率不随下单量变化。这意味着回测能回答「这套逻辑成不成立」,回答不了「它能管多少钱」。
- 正确的做法不是找一个「对的」成本值,而是把成本当变量扫一遍。断崖式塌方的曲线,说明这套方法的命押在一个你控制不了的参数上。
一句话记住这篇:纸上赚的那点钱,先看看够不够付来回的路费。
本文所引源码与文档均来自上述快照版本,是阅读代码得出的机制说明,不是运行结果;具体数值和行为请以你手上那个版本、你的券商与当时政策为准。回量化与 AI 投研概念库看其它概念各自在说什么;本站内容为投资教育,不构成任何投资建议,边界见免责声明。
本文为投资教育整理,关键数据与结论请结合下列权威来源验证。
- freqtrade 源码快照 b3404c9(2026-08-18):docs/bot-basics.md 的 Fee handling 一节、docs/backtesting.md 的 Assumptions made by backtesting 与 --fee 说明、freqtrade/optimize/backtesting.py 的 set_fee ↗
- qlib 源码快照 79633dd(2026-07-23):qlib/backtest/exchange.py(open_cost / close_cost / min_cost / volume_threshold)、docs/component/workflow.rst ↗
- Vibe-Trading 源码快照 3a752d5(2026-08-04):agent/backtest/engines/china_a.py、crypto.py、china_futures.py、base.py ↗