Caloogy 文章
English

交易成本建模:手续费、滑点与市场冲击

日期
2025 年 6 月 8 日
系列
量化
阅读
4 分钟
主题
QuantQuant devTransaction costSlippage

一句话版本

一个回测里年化 70% 的策略,到了实盘可能一分钱都赚不到。差距往往来自交易成本:手续费、买卖价差、以及你自己的订单把价格推向不利方向的市场冲击。 前两项大致是固定比例,最后一项会随着交易规模变大:经验上,冲击成本大约和"交易量占市场成交量的比例"的平方根成正比。这意味着一个策略存在容量上限:资金越多,单位资金的成本越高,到某个规模后收益就被吃光了。

免责

本文仅为技术科普,不构成任何投资建议。文中代码使用模拟数据,参数仅作示意。


第一层:通俗理解

1.1 菜市场里的成本

你去菜市场买菜:

成本 菜市场的对应 交易中的对应
手续费 停车费 券商、交易所佣金
价差 摊主收购价和卖出价的差 买一价和卖一价的差,每次交易付一半
市场冲击 你一下要买走一半的白菜,摊主马上涨价 大单把价格推高(买)或压低(卖)

前两项不管买多少,比例基本不变;第三项买得越多越贵。

1.2 为什么回测常常低估成本


第二层:成本模型

2.1 单边成本的组成

cost=fee⏟手续费+12 spread⏟半价差+Y⋅σ⋅QV⏟市场冲击\text{cost} = \underbrace{\text{fee}}_{\text{手续费}} + \underbrace{\tfrac{1}{2}\,\text{spread}}_{\text{半价差}} + \underbrace{Y \cdot \sigma \cdot \sqrt{\frac{Q}{V}}}_{\text{市场冲击}}

符号 含义
σ\sigma 资产的日波动率
QQ 你的交易量(金额或股数)
VV 日均成交量(ADV)
YY 经验系数,通常在 0.5 到 1 左右,要用自己的成交数据校准

2.2 平方根法则

"冲击 ∝ Q/V\sqrt{Q/V}"是市场微观结构研究中最稳健的经验规律之一,在股票、期货、期权、加密货币等不同市场中都被反复观察到。

直觉上:交易量翻 4 倍,单位冲击只翻 2 倍,但总冲击成本(单位冲击 × 交易量)翻 8 倍。所以:

2.3 降低成本的手段

手段 原理
降低换手 信号平滑、设置调仓阈值(变化不大就不调)
拆单执行 TWAP、VWAP 等算法把大单拆成小单,分散到一段时间
限价单 挂单等待成交,赚取而不是支付价差(但有不成交的风险)
选择流动性好的标的 价差小、ADV 大
成本感知的组合优化 在优化目标中直接减去预期成本

第三层:代码:成本如何吃掉一个策略

3.1 一个高换手策略

模拟一个日频、几乎每天换方向的策略,它有一定的真实预测力(毛夏普约 3.5,这在现实中极少见,这里是为了看清成本的作用):

import numpy as np, pandas as pd
rng = np.random.default_rng(5)
n = 252 * 4
ret = pd.Series(rng.normal(0, 0.015, n))
# 一个带有真实预测力的日频信号
signal = pd.Series(0.25 * ret.shift(-1).fillna(0) / 0.015 + rng.normal(0, 1, n))
pos = np.sign(signal).shift(1).fillna(0)          # 昨天的信号,今天持仓(+1 / -1)
gross = pos * ret

def cost_per_trade(trade_value, adv, sigma, fee_bps=1, half_spread_bps=2, Y=1.0):
    """单边成本(占成交额的比例)= 手续费 + 半价差 + 平方根冲击"""
    impact = Y * sigma * np.sqrt(trade_value / adv)
    return fee_bps / 1e4 + half_spread_bps / 1e4 + impact

turnover = pos.diff().abs().fillna(0)               # 每天换手(仓位变化量)
def net_returns(capital, adv=5e8, sigma=0.015):
    c = cost_per_trade(capital * turnover.clip(lower=1e-12), adv, sigma)
    return gross - turnover * c

def sharpe(x): return x.mean() / x.std() * np.sqrt(252)
print("日均换手:", round(turnover.mean(), 2))
print("毛夏普:", round(sharpe(gross), 2))
for cap in [0, 1e5, 1e6, 1e7, 1e8]:
    print(f"资金 {cap:>12,.0f}: 净夏普 {sharpe(net_returns(cap)):.2f}  年化净收益 {net_returns(cap).mean()*252*100:.1f}%")

关于模拟信号

代码中的信号用到了 ret.shift(-1)(明天的收益),这是为了人为制造一个已知强度的预测力,便于观察成本的影响。真实策略中绝不能这样写,那正是前视偏差(见《回测的七宗罪》)。

输出:

日均换手: 0.99
毛夏普: 3.49
资金            0: 净夏普 3.17  年化净收益 73.3%
资金      100,000: 净夏普 2.84  年化净收益 65.8%
资金    1,000,000: 净夏普 2.14  年化净收益 49.7%
资金   10,000,000: 净夏普 -0.06  年化净收益 -1.3%
资金  100,000,000: 净夏普 -5.75  年化净收益 -162.6%

3.2 解读

资金 发生了什么
0(只算手续费和价差) 毛夏普 3.49 → 3.17,固定成本已吃掉一部分
10 万 冲击开始出现,但影响不大
100 万 净夏普降到 2.14
1000 万 净收益归零:这大约就是这个策略在这个标的上的容量
1 亿 大幅亏损:每天都在用自己的订单把价格推向不利方向

(年化 −162% 是因为这里用简单收益相加计算年化,又假设每天全仓换向;现实中早在这之前就会停止交易。)

关键结论:策略的收益不是一个数字,而是资金规模的函数。脱离资金规模谈收益是没有意义的。

3.3 估算容量的简单方法

令每笔交易的预期收益等于成本:

预期 alpha=fee+12spread+YσQ/V⇒Qmax⁡=V(α−fee−12spreadYσ)2.\text{预期 alpha} = \text{fee} + \tfrac{1}{2}\text{spread} + Y\sigma\sqrt{Q/V} \quad\Rightarrow\quad Q_{\max} = V\left(\frac{\alpha - \text{fee} - \tfrac{1}{2}\text{spread}}{Y\sigma}\right)^2.

它给出了单笔交易规模的上限,再乘以标的数量和换手频率,就能粗略估计策略总容量。

3.4 实务要点

要点 说明
用自己的数据校准 记录每笔实盘的"决策价格"和"实际成交均价",差值就是真实成本(执行落差,implementation shortfall)
按标的区分 大盘股、小盘股、主流币、小币种的成本差异巨大
考虑时段 开盘、收盘、重大新闻前后,价差和冲击都会变大
加密货币的特殊成本 资金费率、提币费、链上 gas、在去中心化交易所上被三明治攻击的损失(见《MEV与三明治攻击》)
成本模型保守一点 宁可高估成本,也不要低估

总结

层次 要点
组成 手续费 + 半价差 + 市场冲击
平方根法则 冲击 ∝ σ√(Q/V);总冲击随规模超线性增长
容量 收益是资金规模的函数;示例策略约 1000 万时收益归零
实务 用实盘成交数据校准;降低换手;拆单执行;成本模型宁可保守