4. 交易成本建模:滑点、手续费、冲击成本的精确计算
做量化交易的朋友,十有八九都经历过这种事:回测曲线漂亮得不行,年化50%,夏普3.0,结果一上实盘就蔫了。为什么?说白了,就是交易成本没算对。
我入行第三年的时候,就栽过这个大跟头。当时一个统计套利策略,回测跑了两年,收益曲线跟教科书一样平滑。结果实盘第一天,直接亏了0.8%。我盯着账户愣了半天,最后发现——滑点设少了,手续费算错了,冲击成本压根没考虑。
嗯,从那以后,我再也不敢小看交易成本建模了。
4.1 交易成本的三大构成
交易成本不是简单的一个「千分之一」就能打发的。我个人习惯把它拆成三块:
- 手续费:交易所和券商收的固定费用。这个最透明,但不同品种差异很大。
- 滑点:你下单时的预期价格和实际成交价之间的差值。流动性越差,滑点越大。
- 冲击成本:你的订单本身对市场价格造成的影响。大单尤其明显。
你想想看,这三块加起来,有时候能吃掉你策略利润的30%-50%。不建模?那回测就是自欺欺人。
核心观点:回测中忽略交易成本,等于在沙滩上建城堡。看起来很美,潮水一来就没了。
4.2 手续费的精确计算
手续费其实是最容易处理的。但要注意,不同交易所、不同品种的费率结构完全不同。
| 品种 | 费率结构 | 典型费率 |
|---|---|---|
| A股 | 佣金+印花税+过户费 | 万2.5 ~ 万3 |
| 期货 | 按手数固定或按成交额比例 | 3元/手 ~ 万分之0.5 |
| 加密货币 | Maker/Taker 费率 | 0.02% ~ 0.1% |
| 外汇 | 点差模式 | 0.5 ~ 3 个点 |
我在项目中遇到过最坑的情况:某个期货品种,交易所收万分之0.5,但期货公司额外加收万分之1.5。回测时我只算了交易所的,结果实盘手续费直接翻了3倍。所以我的建议是——永远以实际交割单为准。
代码实现也很简单:
def calc_commission(trade_value, rate=0.00025, min_fee=5):
"""
计算A股手续费
trade_value: 成交金额
rate: 佣金费率,默认万2.5
min_fee: 最低佣金,默认5元
"""
fee = trade_value * rate
fee = max(fee, min_fee) # 不低于最低佣金
# 印花税卖出时收,千分之一
stamp_tax = trade_value * 0.001 if is_sell else 0
# 过户费,万分之0.2
transfer_fee = trade_value * 0.00002
return fee + stamp_tax + transfer_fee
小技巧:很多券商对散户有「最低5元」的限制。如果你的策略频繁做小单,这个5元会吃掉很大比例。我建议在回测中把这个逻辑写进去,别偷懒。
4.3 滑点建模:从固定值到动态模型
滑点是最容易被低估的成本。很多新手直接设一个「万分之五」的固定滑点,然后就觉得万事大吉了。但实际情况远没那么简单。
为什么会这样?因为滑点跟市场流动性、波动率、订单大小都有关系。固定滑点模型说白了就是「一刀切」,在流动性好的时候高估了成本,在流动性差的时候又低估了。
我个人习惯用动态滑点模型:
def calc_slippage(volume, avg_volume, volatility, spread):
"""
动态滑点计算
volume: 你的订单量
avg_volume: 该品种日均成交量
volatility: 近期波动率
spread: 当前买卖价差
"""
# 订单量占比越大,滑点越高
volume_ratio = volume / avg_volume
# 波动率越大,滑点越高
vol_factor = 1 + volatility * 10
# 基础滑点 = 半个价差
base_slippage = spread / 2
# 动态调整
slippage = base_slippage * (1 + volume_ratio * 5) * vol_factor
return slippage
你看,这个模型考虑了三个因素:订单相对大小、市场波动、当前价差。比固定滑点靠谱多了。
我曾经用这个模型回测一个高频策略,结果发现滑点成本比固定模型高了40%。当时团队里有人不信,觉得我算错了。后来我们拉了一周实盘数据一对比——嗯,动态模型跟实际滑点误差不到5%。从那以后,全组都改用动态滑点了。
避坑指南:我曾经见过有人把滑点设成0,理由是「我用限价单,不会滑点」。这是典型的误区。限价单确实不会滑点,但可能不成交。不成交的机会成本,有时候比滑点更可怕。
4.4 冲击成本:大单的隐形杀手
冲击成本是交易成本建模里最难的一块。它指的是你的订单本身对市场价格造成的影响。小单可以忽略,但大单不行。
我记得有一次帮一个私募朋友做策略优化。他的策略在回测里年化30%,但实盘只有12%。我一看他的交易记录——平均每笔订单占了该品种日成交量的3%。这个量级,冲击成本至少吃掉一半利润。
冲击成本的计算,业界常用的是平方根模型:
def calc_impact(volume, avg_volume, volatility, price):
"""
平方根冲击成本模型
volume: 订单量
avg_volume: 日均成交量
volatility: 日波动率
price: 当前价格
"""
# 订单量占比
participation = volume / avg_volume
# 冲击成本 = 价格 * 波动率 * sqrt(参与率)
impact = price * volatility * (participation ** 0.5)
return impact
这个模型背后的逻辑是:市场深度跟成交量的平方根成正比。你想想看,一个占日成交量1%的订单,冲击成本大约是波动率的10%。如果波动率是2%,那冲击成本就是0.2%。对于高频策略来说,这个数字已经很大了。
更精确的做法是用Almgren-Chriss模型,它把冲击成本拆成两部分:
- 永久冲击:你的订单改变了市场供需平衡,价格回不去了
- 临时冲击:订单执行过程中的价格偏离,之后会恢复
这个模型比较复杂,但如果你做的是大资金管理,我强烈建议你花时间研究一下。我自己在管理千万级资金时,就是用这个模型来拆单执行的。
经验之谈:冲击成本跟你的交易频率成反比。高频策略单笔小,冲击成本低但手续费占比高;低频策略单笔大,冲击成本高但手续费占比低。找到平衡点,才是盈利的关键。
4.5 综合成本模型:把三块拼起来
好了,现在我们把三块成本拼成一个完整的模型:
class TradingCostModel:
def __init__(self, commission_rate, min_commission, spread_model, impact_model):
self.commission_rate = commission_rate
self.min_commission = min_commission
self.spread_model = spread_model # 滑点模型
self.impact_model = impact_model # 冲击模型
def total_cost(self, order):
"""
计算一笔订单的总交易成本
order: 包含 volume, price, side, symbol 等信息
"""
# 手续费
comm = self.calc_commission(order)
# 滑点
slippage = self.spread_model(order)
# 冲击成本
impact = self.impact_model(order)
# 总成本
total = comm + slippage + impact
return total
这个类的好处是:你可以随时替换不同的子模型。比如回测时用简单模型,实盘前换成复杂模型。灵活得很。
我个人习惯在回测中至少跑三组成本参数:
- 乐观估计:手续费按最低算,滑点设0.5个tick
- 中性估计:手续费按平均算,滑点设1个tick
- 悲观估计:手续费按最高算,滑点设2个tick,加冲击成本
如果策略在悲观估计下还能赚钱,那实盘基本就稳了。如果只有乐观估计能赚钱——嗯,我建议你再想想。
实用建议:别在回测里用「完美成交」假设。我见过太多人回测时每笔都按收盘价成交,实盘发现根本买不到。至少加一个「成交概率模型」,比如流动性好的品种成交概率90%,差的只有60%。
4.6 知识体系总览
下面这张图,是我自己整理的成本建模知识体系。你可以把它当成一个检查清单,看看自己漏了哪一块:
这张图把整个知识体系串起来了。从上到下,从三大成本到具体子项,再到综合模型和实践建议。你写代码的时候,可以对着这张图检查自己有没有遗漏。
最后说一句:交易成本建模没有「完美」的答案。不同策略、不同资金量、不同市场环境,适合的模型都不一样。我的建议是——先跑起来,再优化。别想着一步到位,先加一个简单的成本模型,然后根据实盘数据不断调整。这才是工程化的思路。