12、实战案例三:考虑手续费与滑点的精细化回测

做回测最怕什么?

怕回测曲线漂亮得像假图,实盘一跑就崩。

我见过太多人,回测年化50%,实盘直接亏到怀疑人生。原因很简单——他们没把手续费和滑点算进去。说白了,回测里的「完美成交」在真实市场里根本不存在。今天我们就来把这个坑填上。

12.1 为什么手续费和滑点如此重要?

先看一个真实案例。我去年帮一个团队复盘他们的做市策略,回测数据显示年化收益28%,最大回撤只有3%。结果实盘跑了两个月,收益直接缩水到9%,回撤却飙到了11%。

问题出在哪?

他们用的回测框架,手续费设的是万分之二,滑点直接没考虑。而实际交易中,做市商频繁挂单撤单,手续费累积起来相当可观。再加上行情波动时的滑点,利润就这么被吃掉了。

核心结论:手续费和滑点不是「细节」,它们是决定策略生死的关键变量。忽略它们,回测就是自欺欺人。

12.2 精细化回测的三大要素

我个人习惯把精细化回测拆成三块:

  1. 手续费模型——不仅要算成交手续费,还要算挂单未成交的撤单费(如果有的话)
  2. 滑点模型——模拟真实成交时的价格偏移
  3. 流动性模型——不同深度下的成交质量差异

这三块缺一不可。你想想看,如果只考虑手续费不考虑滑点,那遇到流动性枯竭的时候,你的回测结果还是虚高的。

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 知识体系总览

下面这张图,是我做精细化回测时的核心逻辑框架:

精细化回测核心逻辑 输入数据 策略参数 市场环境 精细化回测引擎 手续费模型 maker/taker费率 滑点模型 固定/深度滑点 流动性模型 盘口深度扫描 输出:真实收益 / 实际回撤 / 交易成本明细 粗糙回测:虚高收益 精细化回测:真实可靠 结论:忽略手续费和滑点的回测,都是耍流氓

12.7 几个实用建议

最后,分享几个我踩坑后总结的经验:

  • 手续费要分maker/taker——做市商大部分是maker单,费率低;但被吃单时就是taker,费率高。别混为一谈。
  • 滑点模型要动态调整——波动率大的时候,滑点会放大。我习惯用ATR(平均真实波幅)来动态调整滑点参数。
  • 别忘了资金费率——永续合约做市还要考虑资金费率,有时候这一项就能吃掉所有利润。
  • 回测周期要覆盖不同行情——单边行情和震荡行情下的手续费/滑点影响完全不同。

一个小技巧:我每次跑完精细化回测,都会把交易日志导出来,按「手续费占比」和「滑点占比」排序。如果某笔交易的成本超过预期收益的30%,我就会去检查是不是参数设置有问题。这个习惯帮我发现了不少策略漏洞。

好了,精细化回测的核心就这些。记住一句话:回测不精细,实盘两行泪。把手续费和滑点算清楚,你的策略才算真正经得起考验。


交易系统化学习资料 微信Strategy888888