一句话版本
一个回测里年化 70% 的策略,到了实盘可能一分钱都赚不到。差距往往来自交易成本:手续费、买卖价差、以及你自己的订单把价格推向不利方向的市场冲击。 前两项大致是固定比例,最后一项会随着交易规模变大:经验上,冲击成本大约和"交易量占市场成交量的比例"的平方根成正比。这意味着一个策略存在容量上限:资金越多,单位资金的成本越高,到某个规模后收益就被吃光了。
免责
本文仅为技术科普,不构成任何投资建议。文中代码使用模拟数据,参数仅作示意。
第一层:通俗理解
1.1 菜市场里的成本
你去菜市场买菜:
| 成本 | 菜市场的对应 | 交易中的对应 |
|---|---|---|
| 手续费 | 停车费 | 券商、交易所佣金 |
| 价差 | 摊主收购价和卖出价的差 | 买一价和卖一价的差,每次交易付一半 |
| 市场冲击 | 你一下要买走一半的白菜,摊主马上涨价 | 大单把价格推高(买)或压低(卖) |
前两项不管买多少,比例基本不变;第三项买得越多越贵。
1.2 为什么回测常常低估成本
- 回测按"收盘价"成交,现实中你下单时价格已经变了;
- 忽略了价差(尤其是小盘股、加密货币小币种,价差可能很大);
- 忽略了冲击:回测里买 1000 万和买 1 万,成交价一样;
- 高换手策略:每天调仓,成本每天都要付一次。
第二层:成本模型
2.1 单边成本的组成
| 符号 | 含义 |
|---|---|
| 资产的日波动率 | |
| 你的交易量(金额或股数) | |
| 日均成交量(ADV) | |
| 经验系数,通常在 0.5 到 1 左右,要用自己的成交数据校准 |
2.2 平方根法则
"冲击 ∝ "是市场微观结构研究中最稳健的经验规律之一,在股票、期货、期权、加密货币等不同市场中都被反复观察到。
直觉上:交易量翻 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 估算容量的简单方法
令每笔交易的预期收益等于成本:
它给出了单笔交易规模的上限,再乘以标的数量和换手频率,就能粗略估计策略总容量。
3.4 实务要点
| 要点 | 说明 |
|---|---|
| 用自己的数据校准 | 记录每笔实盘的"决策价格"和"实际成交均价",差值就是真实成本(执行落差,implementation shortfall) |
| 按标的区分 | 大盘股、小盘股、主流币、小币种的成本差异巨大 |
| 考虑时段 | 开盘、收盘、重大新闻前后,价差和冲击都会变大 |
| 加密货币的特殊成本 | 资金费率、提币费、链上 gas、在去中心化交易所上被三明治攻击的损失(见《MEV与三明治攻击》) |
| 成本模型保守一点 | 宁可高估成本,也不要低估 |
总结
| 层次 | 要点 |
|---|---|
| 组成 | 手续费 + 半价差 + 市场冲击 |
| 平方根法则 | 冲击 ∝ σ√(Q/V);总冲击随规模超线性增长 |
| 容量 | 收益是资金规模的函数;示例策略约 1000 万时收益归零 |
| 实务 | 用实盘成交数据校准;降低换手;拆单执行;成本模型宁可保守 |