4、回测框架搭建:用Python实现一个简单的回测引擎

回测引擎,说白了就是你的策略时光机。

它能让你穿越到过去,看看你的套利策略在历史上表现如何。我个人习惯把回测引擎比作「交易实验室」——你在里面做实验,失败了也不会亏真金白银。

4.1 回测引擎的核心架构

先别急着写代码。我们得搞清楚,一个回测引擎到底长什么样?

我画了一张图,你看一眼就明白了:

回测引擎核心架构 数据模块 行情数据加载与清洗 策略模块 信号生成与逻辑判断 执行模块 订单生成与撮合 风控模块 仓位限制、止损检查 绩效模块 收益率、夏普比率计算 日志模块 交易记录与异常追踪 主循环:逐K线/逐Tick推进

嗯,这张图里最核心的就是那个「主循环」。它像钟表一样,一格一格地往前走。每走一格,就触发一次数据更新、策略判断、交易执行。

4.2 数据模块:喂给引擎的「原料」

回测的第一步,是拿到干净的数据。我在项目中遇到过最坑的事——数据里有未来函数,回测曲线漂亮得像假的一样,实盘直接崩了。

所以,数据清洗是底线。我们至少要做三件事:

  • 去重:同一时间戳的重复数据,只保留一条
  • 补全:缺失的时间点,用前值填充或插值
  • 对齐:不同品种的时间戳必须对齐到同一频率

重要原则:永远不要在回测中使用未来数据。你的策略在t时刻,只能看到t时刻及之前的数据。

来看一个简单的数据加载函数:

import pandas as pd
import numpy as np

def load_data(file_path, freq='1min'):
    """
    加载并清洗行情数据
    freq: 重采样频率,默认1分钟
    """
    df = pd.read_csv(file_path, parse_dates=['time'])
    df.set_index('time', inplace=True)
    
    # 去重:按时间戳去重,保留第一条
    df = df[~df.index.duplicated(keep='first')]
    
    # 补全缺失时间点
    full_idx = pd.date_range(start=df.index.min(), 
                             end=df.index.max(), freq=freq)
    df = df.reindex(full_idx, method='ffill')
    
    # 删除开盘前和收盘后的数据(假设9:30-15:00交易)
    df = df.between_time('09:30', '15:00')
    
    return df

这段代码看起来简单,但够用了。你想想看,真实场景下数据量动辄几百万行,性能优化是后话,先把逻辑跑通再说。

4.3 策略模块:把你的想法写成代码

策略模块是回测引擎的灵魂。我建议把策略设计成一个「黑盒」——给它数据,它返回信号。

一个标准的策略接口长这样:

class BaseStrategy:
    """策略基类,所有策略都继承它"""
    
    def __init__(self, params=None):
        self.params = params or {}
        self.position = 0  # 当前持仓
        self.signals = []  # 信号列表
    
    def on_data(self, data):
        """
        每来一条新数据,调用一次
        data: 当前时刻的行情数据(DataFrame的一行)
        返回: 1(买入), -1(卖出), 0(持有)
        """
        raise NotImplementedError("子类必须实现on_data方法")
    
    def reset(self):
        """回测开始前重置状态"""
        self.position = 0
        self.signals = []

举个例子,一个简单的均值回归策略:

class MeanReversionStrategy(BaseStrategy):
    """均值回归策略:价格偏离均线太多就反向开仓"""
    
    def __init__(self, params=None):
        super().__init__(params)
        self.window = params.get('window', 20)  # 均线周期
        self.threshold = params.get('threshold', 2.0)  # 标准差倍数
        self.prices = []  # 缓存历史价格
    
    def on_data(self, data):
        price = data['close']
        self.prices.append(price)
        
        if len(self.prices) < self.window:
            return 0  # 数据不够,不交易
        
        # 计算均线和标准差
        ma = np.mean(self.prices[-self.window:])
        std = np.std(self.prices[-self.window:])
        
        if std == 0:
            return 0
        
        z_score = (price - ma) / std
        
        # 信号判断
        if z_score > self.threshold and self.position <= 0:
            return -1  # 卖出
        elif z_score < -self.threshold and self.position >= 0:
            return 1   # 买入
        else:
            return 0   # 持有

小技巧:写策略时,先把逻辑用伪代码写一遍。我曾经直接上手写代码,结果逻辑漏洞百出,debug花了两天。先想清楚再动手,效率反而更高。

4.4 执行模块:模拟真实交易

策略发出了信号,接下来就是「执行」。执行模块要考虑几个现实问题:

  • 滑点:你看到的价格和实际成交价之间有差距
  • 手续费:每笔交易都要交钱
  • 成交限制:大单可能无法全部成交

我习惯用一个简单的「订单簿」来模拟:

class ExecutionEngine:
    def __init__(self, slippage=0.0001, commission=0.0003):
        self.slippage = slippage  # 滑点比例
        self.commission = commission  # 手续费比例
        self.trades = []  # 成交记录
    
    def execute(self, signal, price, volume):
        """
        执行交易
        signal: 1买入, -1卖出
        price: 当前价格
        volume: 交易数量
        返回: 实际成交价格和数量
        """
        # 模拟滑点:买入价格上浮,卖出价格下浮
        if signal == 1:
            exec_price = price * (1 + self.slippage)
        else:
            exec_price = price * (1 - self.slippage)
        
        # 计算手续费
        fee = exec_price * volume * self.commission
        
        # 记录交易
        self.trades.append({
            'price': exec_price,
            'volume': volume,
            'fee': fee,
            'signal': signal
        })
        
        return exec_price, volume, fee

注意:滑点和手续费参数不要拍脑袋设。我建议从你的券商那里拿到真实数据,或者至少用市场平均水平。设得太乐观,回测好看但实盘亏钱;设得太悲观,可能错过好策略。

4.5 主循环:让一切转起来

好了,各个模块都准备好了。现在把它们组装起来,跑一个完整的回测:

def run_backtest(data, strategy, execution, initial_capital=100000):
    """
    运行回测
    data: 历史行情数据(DataFrame)
    strategy: 策略实例
    execution: 执行引擎实例
    initial_capital: 初始资金
    """
    capital = initial_capital
    position = 0
    equity_curve = []  # 权益曲线
    
    # 重置策略状态
    strategy.reset()
    
    for idx, row in data.iterrows():
        # 1. 获取信号
        signal = strategy.on_data(row)
        
        # 2. 执行交易
        if signal != 0:
            # 简单仓位管理:每次交易固定数量
            volume = 100  # 假设每次交易100股
            price = row['close']
            
            exec_price, exec_volume, fee = execution.execute(
                signal, price, volume
            )
            
            # 更新持仓和资金
            if signal == 1:  # 买入
                cost = exec_price * exec_volume + fee
                if capital >= cost:
                    capital -= cost
                    position += exec_volume
            else:  # 卖出
                revenue = exec_price * exec_volume - fee
                capital += revenue
                position -= exec_volume
        
        # 3. 记录当前权益
        current_equity = capital + position * row['close']
        equity_curve.append(current_equity)
    
    # 计算绩效指标
    returns = pd.Series(equity_curve).pct_change().dropna()
    total_return = (equity_curve[-1] - initial_capital) / initial_capital
    sharpe = np.sqrt(252) * returns.mean() / returns.std() if returns.std() > 0 else 0
    
    return {
        'equity_curve': equity_curve,
        'total_return': total_return,
        'sharpe_ratio': sharpe,
        'trades': execution.trades
    }

这段代码虽然简单,但已经包含了回测引擎的核心逻辑。你想想看,实际生产环境里,我们还会加入多线程、内存管理、异常处理等等。但核心骨架,就是上面这些。

4.6 绩效评估:回测结果怎么看?

回测跑完了,一堆数字摆在你面前。怎么看?我一般关注这几个指标:

指标 含义 合格线
总收益率 回测期间的总收益比例 至少跑赢基准
夏普比率 每单位风险获得的超额收益 > 1.0 算不错
最大回撤 账户从峰值跌到谷底的最大幅度 < 20% 可接受
胜率 盈利交易占总交易的比例 > 40% 算正常
盈亏比 平均盈利 / 平均亏损 > 1.5 较好

我的经验:不要只看总收益率。我曾经见过一个策略年化50%,但最大回撤40%。这种策略你敢实盘吗?回撤控制比收益率更重要。

4.7 避坑指南

最后,分享几个我踩过的坑:

  • 幸存者偏差:只用现在还在交易的品种做回测,退市的都被忽略了。结果看起来很好,但实际可能很差。
  • 过拟合:参数调得太多,策略只适合历史数据。换一个时间段就失效。我建议参数不超过3个。
  • 未来信息泄露:比如用当天的收盘价计算当天的信号,这在实盘中根本做不到。一定要确保信号只基于历史数据。
  • 忽略交易成本:高频套利策略,手续费和滑点可能是利润的杀手。我曾经有一个策略,算上手续费后直接从盈利变成亏损。

嗯,回测引擎就讲到这里。代码虽然简单,但足够你开始做实验了。记住,回测只是起点,不是终点。真正的考验,永远在实盘里。


公众号:蓝海资料掘金营,微信deep3321