冲击成本:你这笔单子自己把价格买高了
有个现象,手动下过几次大单的人多半撞见过。
你看上一个不太热闹的品种,挂单买入。第一笔成交价和屏幕上那个数字一样,第二笔就贵了一点点,等单子全部成交,回头一算平均成交价,比你按下确认键那一刻的价高出一截。这期间没有任何新闻,没有大资金进场,行情本身安安静静。
价格是被你自己推上去的。
买十万块的时候,这一截小到你根本注意不到;买一千万的时候,它可能比你付出的手续费还多几倍。而这件事,正是「回测里那条漂亮曲线,为什么钱一多就不灵了」背后最硬的一个原因——比参数没调好、比市场风格变化都硬得多,因为它不是运气问题,是算术问题。
先说人话:它到底是件什么事
想象一个只有三个摊位的早市,都卖同一种西红柿。
你去买五斤,第一家摊主按牌价卖给你,交易结束,你走人,市场毫无变化。
现在换成你要买五百斤。第一家的存货被你买光了,你只好去第二家。第二家摊主刚看着你把隔壁扫空,他不傻,价格往上抬了一点。你还是不够,去第三家——这时候全场都知道有个人急着要大量西红柿,而且没别处可去了。第三家的报价,跟你进场时看到的那个牌价已经不是一回事。
你最后算总账:五百斤的平均单价,比牌价贵了一截。贵出来的这一截,没有任何人向你收,也不会出现在任何一张收据上,但它确确实实从你口袋里走了。
这就是冲击成本的全部含义:你不是在一个固定价格上买东西,你是在一张有厚度的价目表上,从便宜的那一层往贵的那一层一层层往上吃。你吃得越深,平均价越难看。
有一个必须当场分清的东西:它和滑点不是一回事。滑点说的是「价格自己在动,你慢了一步,成交价和你想要的差了一点」——你不下单,市场照样在动。冲击成本说的是「价格因为你才动」——你不下单,它就不会发生。判断标准只有一条:这笔额外支出,是市场本来就有的,还是你自己造出来的? 是你造出来的,就是冲击成本。
卖出的时候方向反过来,性质一模一样:你把手里的货一层层往下砸,砸得越深,平均卖价越低。
投资层:这笔钱按什么规律收
菜市场的比喻讲完了,接下来是它在真实交易里的几条脾气。这几条决定了你的钱能不能变大。
它不看你买了多少钱,它看你占了别人多大的比例
这是最反直觉的一条。买一百万,在一个每天成交几十亿的品种上,什么都不会发生;同样一百万,砸到一个每天只成交几百万的冷门品种上,你就是当天市场的主角,价格由你说了算——而这从来不是好事。
所以真正决定冲击大小的,不是你下单的绝对金额,而是你这笔量占了这个品种当天正常成交量的多大一块。业内把这个比值叫「参与率」,说人话就是:这一天的买卖里,有多大一部分是你自己。这个比例越高,你越是在跟自己抢货。
顺带解释了一件很多人想不通的事:为什么专业机构宁可放弃看起来最诱人的小盘品种,去做那些「平庸」的大品种。不是他们眼光差,是那些机会对他们不存在——想拿到有意义的仓位,光是建仓就会把价格推到没有性价比。流动性在这里不是一个抽象名词,它是一道实实在在的门槛。
它至少收两遍,而且出场那遍通常更贵
买的时候推高一次,卖的时候砸低一次。一次完整的往返,你要付两遍。
更难受的是这两遍并不对称。你想买的时候,可以慢慢等、可以挑清淡的日子分批进;你想卖的时候,往往是因为出了事——止损被触发、风险指标报警、或者干脆是市场在跌。而你想跑的时候,通常也正是所有人都想跑的时候。这时候买盘那一侧本来就薄,你的卖单会砸出比平时深得多的坑。
所以有一条经验值得记住:建仓时估算的冲击成本,几乎必然低估了平仓时的真实代价,因为你是拿风平浪静时的市场厚度,去估一个多半会在风浪里发生的动作。
它在你最赚钱的时候变得最贵
这一条是所有做大了的人共同的痛。
假设你的方法真的有效,那么会发生什么?你赚钱,本金变大;你信心增强,仓位加重;你可能还开始管别人的钱。规模每上一个台阶,你的参与率就跟着上一个台阶,冲击成本也跟着上一个台阶。
换句话说,一套方法的成功本身,会系统性地侵蚀它继续成功的条件。 这跟市场变没变、对手变没变都没关系,纯粹是你自己长大了。这也是为什么很多公开的漂亮业绩没法简单外推——那份业绩是在某个你不知道的资金规模上做出来的,而那个规模从来没写在成绩单上。
它跟你的操作频率成正比
冲击成本是按次收的。一年调仓两次的方法,一年付两轮;一周调两次的方法,一年付一百多轮。同样的单笔冲击,后者被放大到几十倍。
这和回测里的交易成本假设里说的手续费是同一个乘法结构,但有个关键区别:手续费的费率是明码标价、不随你规模变的;冲击成本的「费率」会随着你的规模自己往上爬。两者叠加,高频加大额是最难受的那个组合。
系统层:三套真实系统怎么对待它
把这件事在真实回测系统里的处理摆出来,你会看到一个挺让人不安的共同点。
| 系统 | 怎么处理「你自己推走价格」 | 结果 |
|---|---|---|
| freqtrade | 不建模,文档明确声明零滑点 | 下单量对成交价完全没影响 |
| qlib | 有专门的冲击成本参数,按参与率的平方加价 | 当前默认值为零,不主动开就等于没有 |
| Vibe-Trading | 按市场给不同的固定滑点比例 | 体现了市场差异,但与你下单多大无关 |
freqtrade:把假设一条条写出来,包括这一条
它在「回测所做的假设」那一节里写得毫不含糊:只要价格落在那根 K 线的最高最低之间,所有订单都按你要求的价格全部成交,没有滑点。
这句话翻译成人话就是:在这个回测世界里,你可以是当天唯一的买家,买多少都不影响价格。 你买一手和买一万手,成交价一模一样。
它确实处理了交易所的最小/最大下单量限制,但文档同时坦白了一件事:它没有历史上的限制数据,只能拿当前的规则去套过去,因此可能出现拿历史价格换算出离谱门槛的情况。这是关于「能不能下这笔单」的约束,不是关于「下了这笔单价格会怎么动」的约束——两回事。
这套做法的价值不在于精确,而在于它把假设写出来了。写出来的假设可以被你质疑,没写出来的才是真正吃人的那种。
qlib:唯一把它写成公式的,而且是平方
qlib 的交易所模块里有个单独的冲击成本参数,源码注释直接把它标注为「市场冲击成本率」。它的计算方式值得一个字一个字地看:
adj_cost_ratio = self.impact_cost * (trade_val / total_trade_val) ** 2
分母是这个品种当天的总成交额,分子是你这一笔的成交额,两者相除就是前面说的参与率。关键在末尾那个平方。
平方意味着什么?你的参与率从一成翻到两成,冲击成本率不是翻一倍,是翻四倍。而你实际掏出去的钱等于成本率乘以成交额,成交额本身也翻了一倍,所以总账翻了八倍。规模翻一倍,这笔看不见的支出翻八倍——这就是「大资金做不了」这句话的数学形式。
算出来的这个比率,会被加到手续费率上一起扣掉:
cost_ratio = self.open_cost + adj_cost_ratio
注意它的实现选择:它没有去改成交价,而是把冲击折算成一笔额外的费用。 结果上等价,但意味着你在成交记录里看不到「价格被推走了」这件事,只会看到成本变高了。源码里还留了一条诚实的备注,说这个比率有可能被高估,因为成交量在后面的步骤里还会被裁剪。
真正要命的是它的默认值:这个参数当前默认为零,可配置。 也就是说,除非你主动去打开它,否则你跑出来的所有结果里,冲击成本是不存在的。绝大多数人不会去改自己不知道存在的参数。
它还提供了另一条完全不同的路:可以配置一个成交量上限表达式(比如「不超过当日累计成交量的某个比例」),超出的部分会被直接砍掉。源码注释说得很直白,这是在模拟「单子太大,被交易所直接拒掉」。
这两条路的区别值得停下来想一想:一条说「你可以买到,但会更贵」,另一条说「你根本买不到这么多」。 现实里两件事同时发生,而且都得你自己主动开。
Vibe-Trading:承认市场之间有差别,但不看你下多大
它按市场分成不同的引擎,每个引擎自带一套滑点比例。引擎的文档串里明确写着,美股这类高流动性市场用较低的比例,港股用较高的——这至少承认了「不同市场的厚度不一样」。推价的方式是按方向的固定比例:买的时候往上推一点,卖的时候往下推一点。
def apply_slippage(self, price: float, direction: int) -> float:
return price * (1 + direction * self.slippage_rate)
比「假设不存在」诚实一档,但你从这段里看不到 amount 这个词——推多少,跟你买多少没有关系。 买一手和买一万手,被推走的幅度一样。
共同点:这个成本默认是零
三套系统,一套明说不建模,一套建了模但默认关着,一套建的是与下单量无关的固定比例。
这不是它们偷懒。想算准「我这笔单会把价格推走多少」,需要的信息是当时挂单簿有多厚、对手方是谁,而这些东西在日线级别的历史数据里根本不存在。工程上没有更好的选择,只能简化,然后把这个简化留给你自己去意识到。
于是有了一条很实用的判断:回测能回答「这套逻辑成不成立」,它结构上回答不了「这套逻辑能管多少钱」。 后一个问题,不是你换个参数就能问出来的,是这类工具的能力边界之外。
为什么它就是「策略容量」本身
把前面的东西串起来,一个概念自然就浮出来了。
你的方法每笔交易有一个平均优势,比如千分之几。冲击成本随着规模上升而上升。当规模大到冲击成本追平那个优势的时候,这套方法就不赚钱了——这个规模就是它的容量。
容量不是一个可以靠努力突破的东西,它是被三样东西夹出来的:你交易的品种有多活跃、你交易得有多频繁、你的优势有多厚。这三样里任何一个变差,容量都会往下掉。
由此可以推出几件很有用的事:
做冷门品种的方法,容量天生就小。 优势可能很真实,但那块市场装不下多少钱。这也解释了为什么有些方法只适合个人做——不是因为机构做不出来,是因为对机构来说,做出来也不值得。
换手越高,容量越小。 每次操作都要付一遍冲击,操作频率越高,同样规模下年度总冲击越大。所以短周期方法的容量普遍远小于长周期方法,即便前者的单次胜率好看得多。
容量还会自己缩水。 一套方法被越多人用,市场上朝同一个方向的单子就越多,这些单子彼此推着价格走。你不是唯一在推价的人,你只是同时在被别人推。
面对容量约束,现实中的做法是把大单拆开、拖长时间成交,用时间换厚度。但这里有个跑不掉的矛盾:拖得越久,冲击越小,但你暴露在「价格自己跑掉」的风险里越久。 你今天想买的东西,可能不等你买完就已经涨上去了。压缩时间就得多付冲击,拉长时间就得多担风险,两头没法同时占——这是执行这件事的核心难题,不是可以通过更聪明的算法消灭的。
别把「我的回测扣了手续费」当成「我算过成本」。 手续费是唯一会出现在账单上的那一项,也正因为如此,它是所有成本里你最容易记得的一项。冲击成本从头到尾不会被任何人向你收取,它藏在你的成交均价里——你只会觉得「今天买得有点贵」,而永远不会把它算成一笔支出。一套只扣了手续费的回测,对成本的低估是系统性的:每一笔都低估,不会正负抵消,而且低估的幅度随你的资金规模一起长大。
失效边界与常见误用
拿小资金的实盘结果去外推大资金。 你用几万块跑通了一套方法,感觉和回测吻合得很好,于是相信它可以线性放大。这个推论在冲击成本这一层是错的——你恰恰是在冲击成本几乎为零的那个区间里做的验证,而那个区间外的行为,你一次也没观察到。
把冲击成本和滑点混着说。 两者的补救方式完全不同:滑点靠改进下单方式(比如用限价单而不是市价单)能缓解一部分,冲击成本是你自身体量造成的,换下单方式只能改变它怎么发生,改变不了它存在。想清楚哪一部分是市场在动、哪一部分是你在动,才知道该修哪儿。
以为分散到很多个品种就能绕开它。 分散确实能降低单个品种上的参与率,但你的调仓笔数也跟着变多了,每一笔都要付最低那一档的固定代价,还可能踩到各种最低收费门槛。分散是把一个大问题拆成很多个小问题,不是消灭问题。
只看历史平均成交量。 一个品种平时很活跃,不代表你需要出场的那一天它还活跃。市场紧张的时候,成交量和挂单厚度会一起蒸发,而那往往正是你必须动手的时候。用平时的活跃度去估极端时刻的冲击,是一个乐观得离谱的假设。这一层和仓位纪律是连着的,风险管理卷R里讲的仓位上限,很大一部分就是在给「我到底跑不跑得掉」留余量。
以为冲击成本只是「大机构的烦恼」。 只要你交易的品种足够冷清,几万块也能成为当天的主要成交方。判断标准从来不是你有多少钱,是你占了多大比例。想沿着流动性这条线继续往下看,流动性与策略容量那篇讲得更完整;想按体系顺着读,可以回到量化与 AI 投研卷。
小结
- 冲击成本 = 价格因为你的单子而移动,你在一张有厚度的价目表上一层层往上吃,吃得越深平均价越差。它不出现在任何账单上,只藏在成交均价里。
- 决定它大小的不是你下单的金额,是你占了这个品种当天正常成交量的多大比例。同样一笔钱,砸在活跃品种上没声音,砸在冷清品种上你就是全场主角。
- 它一次往返收两遍,出场那遍通常更贵;它随你的规模一起长大;它按你的操作次数累加。
- 三套真实回测系统里:一套明说不建模、一套写成了公式但当前默认值为零、一套用与下单量无关的固定比例。默认情况下,回测世界里你的单子对价格没有任何影响。
- 唯一把它写成公式的那套,用的是参与率的平方——规模翻一倍,这笔支出翻八倍。这就是「小资金能做、大资金做不了」的算术根据,也是策略容量的来源。
- 容量约束没法靠努力突破,只能靠拆单和拉长时间来缓解,而拉长时间又会把你暴露在价格跑掉的风险里。两头不能同时占。
一句话记住这篇:你买得越多,越是在跟自己抢货;抢到最后,赚的那点差价全给了自己。
本文所引源码与文档均来自上述快照版本,是阅读代码得出的机制说明,不是运行结果;具体参数与行为请以你手上那个版本为准。回量化与 AI 投研概念库看其它概念各自在说什么;本站内容为投资教育,不构成任何投资建议,边界见免责声明。
本文为投资教育整理,关键数据与结论请结合下列权威来源验证。
- qlib 源码快照 79633dd(2026-07-23):qlib/backtest/exchange.py 的 impact_cost 参数、_get_trade_info 中的 adj_cost_ratio 计算、_clip_amount_by_volume 与 volume_threshold 文档串 ↗
- freqtrade 源码快照 b3404c9(2026-08-18):docs/backtesting.md 的 Assumptions made by backtesting 与 Trading limits in backtesting 两节 ↗
- Vibe-Trading 源码快照 3a752d5(2026-08-04):agent/backtest/engines/global_equity.py、china_a.py 的 apply_slippage ↗