8. 回测框架搭建:向量化回测引擎设计,考虑交易成本与滑点

回测,说白了就是让策略在历史数据上跑一遍。但这里有个坑——很多人跑出来的曲线漂亮得不行,实盘一上就崩。为什么?因为没把交易成本和滑点算进去。

我个人习惯,回测框架一定要从第一天就考虑这两个东西。否则你看到的收益率,全是幻觉。

8.1 向量化 vs 事件驱动:选哪个?

回测引擎分两种:向量化和事件驱动。我刚开始做的时候,两种都写过。后来发现,对于基差套利这种策略,向量化完全够用,而且快得多。

向量化回测的核心思想:用数组运算代替循环。你想想看,一次算完所有信号,一次算完所有持仓,一次算完所有收益。速度比事件驱动快几个数量级。

但要注意,向量化有个前提——你的策略逻辑必须能用矩阵运算表达。基差套利恰好满足这个条件。

核心区别:
  • 向量化:一次性处理所有数据,速度快,适合批量策略验证
  • 事件驱动:逐笔模拟,更真实,但慢,适合高频或复杂逻辑

8.2 交易成本怎么算?

交易成本不只是手续费。我见过有人只算万二的佣金,结果实盘亏得底掉。真正的交易成本包括三块:

成本类型 说明 典型值(商品期货)
手续费 交易所+期货公司收取 万分之0.5 ~ 万分之3
滑点 实际成交价与信号价的偏差 0.5 ~ 2个tick
冲击成本 大单对市场价格的推动 视流动性而定

嗯,这里要注意。滑点是最容易被低估的。我在项目中遇到过,明明信号价差是10个点,结果一开仓,实际成交价差只有8个点。两个点就这么没了。

我的经验:回测时滑点至少设1个tick。如果是流动性差的合约,设2个tick都不为过。宁可回测难看点,也别实盘被割。

8.3 向量化回测引擎的核心设计

直接上代码吧。这是我常用的一个简化版向量化回测框架。注意看交易成本和滑点是怎么嵌入的。

import numpy as np
import pandas as pd

class VectorizedBacktester:
    def __init__(self, data, signal_func, 
                 fee_rate=0.0003,  # 手续费率
                 slippage=1,       # 滑点(tick数)
                 tick_size=1):     # 最小变动价位
        self.data = data
        self.signal_func = signal_func
        self.fee_rate = fee_rate
        self.slippage = slippage
        self.tick_size = tick_size
        
    def run(self):
        # 生成信号(向量化)
        signals = self.signal_func(self.data)
        
        # 计算持仓(考虑滑点后的实际成交价)
        # 开仓价 = 信号价 + 滑点 * 方向
        entry_price = self.data['close'] + \
                      self.slippage * self.tick_size * signals
        
        # 计算交易成本
        trade_cost = entry_price * self.fee_rate * abs(signals.diff())
        
        # 计算收益(考虑成本)
        returns = signals.shift(1) * \
                  (self.data['close'].pct_change()) - trade_cost
        
        # 累计收益
        cumulative_returns = (1 + returns).cumprod()
        
        return cumulative_returns

你看,核心就几行。但关键在 entry_price 那行——滑点直接加在成交价上。方向为正(做多)就加,为负(做空)就减。这才是真实成交价。

8.4 滑点模型:别用固定值

固定滑点虽然简单,但不够真实。我后来改成了动态滑点模型——根据流动性来调整。

def dynamic_slippage(volume, avg_volume, base_slippage=1):
    """
    动态滑点模型
    volume: 当前成交量
    avg_volume: 平均成交量
    base_slippage: 基础滑点(tick数)
    """
    # 流动性越差,滑点越大
    liquidity_ratio = volume / avg_volume
    if liquidity_ratio < 0.5:
        return base_slippage * 2
    elif liquidity_ratio < 1.0:
        return base_slippage * 1.5
    else:
        return base_slippage

为什么会这样?你想想看,流动性差的时候,你的单子很难找到对手盘。我曾经在某个冷门合约上吃过亏,回测时滑点设1个tick,实盘直接滑了3个tick。从那以后,我再也不敢用固定滑点了。

8.5 完整的回测流程

一个完整的向量化回测,应该包含以下步骤:

  1. 数据准备:清洗、对齐、处理缺失值
  2. 信号生成:计算基差、生成开平仓信号
  3. 成本计算:手续费 + 滑点 + 冲击成本
  4. 收益计算:考虑成本后的净值曲线
  5. 绩效评估:夏普比率、最大回撤、胜率等
避坑指南:我曾经犯过一个低级错误——在计算收益时忘了考虑信号延迟。信号是收盘后生成的,但实际成交是第二天开盘。差一天,结果天差地别。记得用 shift(1) 把信号往后挪一天。

8.6 绩效评估指标

跑完回测,不能只看收益率曲线。我一般会看这几个指标:

指标 公式 合格线
年化收益率 累计收益^(252/天数) - 1 > 10%
夏普比率 (收益率 - 无风险利率) / 波动率 > 1.5
最大回撤 峰值到谷底的最大跌幅 < 15%
胜率 盈利交易次数 / 总交易次数 > 50%

注意,这些指标都是在扣除交易成本之后算的。如果扣除成本后夏普比率还能到1.5以上,这个策略才值得上实盘。

8.7 回测的常见陷阱

最后,说几个我踩过的坑:

  • 前视偏差:用未来数据生成信号。比如用当天的收盘价算当天的信号,这是作弊。
  • 幸存者偏差:只回测现在还活着的合约。退市的合约也要包含进去。
  • 过度优化:参数调得刚刚好,换段时间就失效。我一般会做样本外测试。
我的建议:回测只是第一步。真正靠谱的策略,要经过样本内、样本外、以及实盘模拟三个阶段。别急着上真金白银。

好了,向量化回测框架的核心就这些。说白了,就是要把交易成本和滑点当成策略的一部分来设计。你想想看,如果回测时都不考虑这些,实盘怎么可能赚钱?

向量化回测引擎核心流程 数据准备 信号生成(基差计算) 交易成本计算(手续费 + 滑点) 收益计算(净值曲线) 绩效评估(夏普/回撤) 清洗、对齐 价差计算 成本嵌入 向量化运算 指标计算 ⚠ 避免前视偏差 ✔ 动态滑点模型 ⚠ 考虑信号延迟 ✔ 样本外测试

框架搭好了,下一步就是往里面填策略逻辑。记住,回测的目的是发现bug,不是证明策略牛逼。能发现问题的回测,才是好回测。