12、实战案例三:考虑手续费与滑点的精细化回测
做回测最怕什么?
怕回测曲线漂亮得像假图,实盘一跑就崩。
我见过太多人,回测年化50%,实盘直接亏到怀疑人生。原因很简单——他们没把手续费和滑点算进去。说白了,回测里的「完美成交」在真实市场里根本不存在。今天我们就来把这个坑填上。
12.1 为什么手续费和滑点如此重要?
先看一个真实案例。我去年帮一个团队复盘他们的做市策略,回测数据显示年化收益28%,最大回撤只有3%。结果实盘跑了两个月,收益直接缩水到9%,回撤却飙到了11%。
问题出在哪?
他们用的回测框架,手续费设的是万分之二,滑点直接没考虑。而实际交易中,做市商频繁挂单撤单,手续费累积起来相当可观。再加上行情波动时的滑点,利润就这么被吃掉了。
核心结论:手续费和滑点不是「细节」,它们是决定策略生死的关键变量。忽略它们,回测就是自欺欺人。
12.2 精细化回测的三大要素
我个人习惯把精细化回测拆成三块:
- 手续费模型——不仅要算成交手续费,还要算挂单未成交的撤单费(如果有的话)
- 滑点模型——模拟真实成交时的价格偏移
- 流动性模型——不同深度下的成交质量差异
这三块缺一不可。你想想看,如果只考虑手续费不考虑滑点,那遇到流动性枯竭的时候,你的回测结果还是虚高的。
12.3 构建精细化回测框架
嗯,这里我们直接上代码。我习惯用Python写回测引擎,结构清晰,方便调试。
class FineGrainedBacktest:
def __init__(self, fee_rate=0.0002, slippage_model='fixed',
slippage_bps=1.0, depth_data=None):
"""
精细化回测引擎
参数:
- fee_rate: 手续费率,默认万二
- slippage_model: 滑点模型,'fixed'固定滑点,'depth'基于深度
- slippage_bps: 固定滑点时的基点偏移
- depth_data: 深度数据,用于动态滑点计算
"""
self.fee_rate = fee_rate
self.slippage_model = slippage_model
self.slippage_bps = slippage_bps
self.depth_data = depth_data
self.trade_log = []
def calculate_fee(self, trade_value):
"""计算手续费"""
return trade_value * self.fee_rate
def calculate_slippage(self, order_side, order_size, current_price):
"""计算滑点后的实际成交价"""
if self.slippage_model == 'fixed':
# 固定滑点:买入时价格上移,卖出时价格下移
slippage = current_price * (self.slippage_bps / 10000)
if order_side == 'buy':
return current_price + slippage
else:
return current_price - slippage
elif self.slippage_model == 'depth':
# 基于深度的滑点:根据订单大小和盘口深度计算
# 这里简化处理,实际需要逐档扫描
return self._depth_based_slippage(order_side, order_size, current_price)
def execute_trade(self, order_side, order_size, current_price, timestamp):
"""执行一笔交易,返回实际成交信息"""
# 1. 计算滑点后的价格
actual_price = self.calculate_slippage(order_side, order_size, current_price)
# 2. 计算成交金额
trade_value = order_size * actual_price
# 3. 计算手续费
fee = self.calculate_fee(trade_value)
# 4. 记录交易日志
self.trade_log.append({
'timestamp': timestamp,
'side': order_side,
'size': order_size,
'price': actual_price,
'fee': fee,
'slippage_bps': abs(actual_price - current_price) / current_price * 10000
})
return {
'actual_price': actual_price,
'fee': fee,
'net_value': trade_value - fee if order_side == 'sell' else trade_value + fee
}
我的经验:刚开始做回测时,我总用固定滑点模型。后来发现,在流动性好的币对(比如BTC/USDT),固定滑点1bps就够;但在小币种上,有时候10bps都不止。建议至少准备两套滑点模型,根据交易对流动性切换。
12.4 实战:做市策略的精细化回测
我们拿一个经典的做市策略来演示——双边挂单,赚取买卖价差。
def market_making_backtest(price_data, depth_data, strategy_params):
"""
做市策略精细化回测
策略逻辑:
- 在买一价下方挂买单,在卖一价上方挂卖单
- 每次成交后,立即补单
- 考虑手续费和滑点
"""
# 初始化回测引擎
bt = FineGrainedBacktest(
fee_rate=0.0002,
slippage_model='depth',
depth_data=depth_data
)
# 策略参数
spread_bps = strategy_params.get('spread_bps', 5) # 价差宽度,单位bps
order_size = strategy_params.get('order_size', 0.1) # 每单数量
inventory_target = strategy_params.get('inventory_target', 0) # 目标库存
# 回测主循环
pnl = 0
inventory = 0
trade_count = 0
for i in range(1, len(price_data)):
current_price = price_data[i]
timestamp = i
# 计算挂单价格
half_spread = current_price * (spread_bps / 20000)
bid_price = current_price - half_spread
ask_price = current_price + half_spread
# 模拟成交判断(简化版)
# 实际中需要根据盘口深度判断是否成交
if price_data[i] <= bid_price and inventory < inventory_target + order_size:
# 买单成交
result = bt.execute_trade('buy', order_size, bid_price, timestamp)
inventory += order_size
pnl -= result['net_value']
trade_count += 1
elif price_data[i] >= ask_price and inventory > inventory_target - order_size:
# 卖单成交
result = bt.execute_trade('sell', order_size, ask_price, timestamp)
inventory -= order_size
pnl += result['net_value']
trade_count += 1
# 计算最终持仓价值
final_inventory_value = inventory * price_data[-1]
total_pnl = pnl + final_inventory_value
return {
'total_pnl': total_pnl,
'trade_count': trade_count,
'final_inventory': inventory,
'trade_log': bt.trade_log
}
12.5 对比分析:精细化 vs 粗糙回测
我曾经用同一组数据跑过对比,结果触目惊心:
| 指标 | 粗糙回测(无手续费滑点) | 精细化回测(万二手续费+深度滑点) | 差异 |
|---|---|---|---|
| 总收益 | +12.3% | +4.7% | -61.8% |
| 最大回撤 | 2.1% | 5.8% | +176% |
| 夏普比率 | 3.2 | 1.1 | -65.6% |
| 交易次数 | 1,247 | 1,247 | 0% |
| 平均每笔成本 | 0 | 0.031% | — |
看到没?收益直接腰斩,回撤翻倍。这就是为什么我说「粗糙回测是毒药」。
避坑指南:我曾经犯过一个低级错误——在回测中把手续费设成固定值,没考虑不同交易所的费率结构。结果换了个交易所跑,收益直接变负。记住:不同交易所的费率差异很大,做市商还有maker/taker之分,一定要按实际费率来。
12.6 知识体系总览
下面这张图,是我做精细化回测时的核心逻辑框架:
12.7 几个实用建议
最后,分享几个我踩坑后总结的经验:
- 手续费要分maker/taker——做市商大部分是maker单,费率低;但被吃单时就是taker,费率高。别混为一谈。
- 滑点模型要动态调整——波动率大的时候,滑点会放大。我习惯用ATR(平均真实波幅)来动态调整滑点参数。
- 别忘了资金费率——永续合约做市还要考虑资金费率,有时候这一项就能吃掉所有利润。
- 回测周期要覆盖不同行情——单边行情和震荡行情下的手续费/滑点影响完全不同。
一个小技巧:我每次跑完精细化回测,都会把交易日志导出来,按「手续费占比」和「滑点占比」排序。如果某笔交易的成本超过预期收益的30%,我就会去检查是不是参数设置有问题。这个习惯帮我发现了不少策略漏洞。
好了,精细化回测的核心就这些。记住一句话:回测不精细,实盘两行泪。把手续费和滑点算清楚,你的策略才算真正经得起考验。
无相订单流研究社 微信Lucian808555