Skip to content

执行算法回测框架:回测环境搭建、历史数据回放、订单模拟执行、绩效指标计算

做量化交易的朋友都知道,策略写出来是一回事,能不能在真实市场里跑通是另一回事。我见过太多人,回测曲线漂亮得像教科书,一上实盘就亏得亲妈都不认识。问题出在哪?说白了,就是回测框架太粗糙,没有把执行层面的细节模拟到位。

今天我们就来聊聊,怎么搭建一个靠谱的执行算法回测框架。我个人习惯把这件事拆成四个模块:环境搭建、数据回放、订单模拟、绩效计算。一个一个来。

16.1 回测环境搭建:别让基础设施拖后腿

回测环境,说白了就是你的实验台。我刚开始做回测时,直接用单线程跑历史数据,结果一个月的 tick 数据要跑半小时。后来改成向量化计算,速度提升了上百倍。嗯,这里要注意,回测环境的核心就两个:速度和真实性。

速度方面,我建议用 NumPy 和 Pandas 做数据处理,用 Numba 加速循环计算。真实性方面,你得模拟交易所的撮合逻辑、网络延迟、手续费等。

核心要点:回测环境不是越复杂越好,而是要在速度和真实性之间找到平衡点。我个人习惯先做一个轻量级版本,跑通逻辑后再逐步增加细节。

下面是一个简单的回测环境骨架:

import numpy as np
import pandas as pd
from dataclasses import dataclass
from typing import List, Dict

@dataclass
class BacktestConfig:
    initial_capital: float = 1_000_000
    commission_rate: float = 0.0003  # 万三
    slippage_model: str = 'fixed'    # 固定滑点
    slippage_bps: float = 1.0        # 1个基点
    market_impact_model: str = 'linear'  # 线性冲击模型

class BacktestEngine:
    def __init__(self, config: BacktestConfig):
        self.config = config
        self.capital = config.initial_capital
        self.positions = {}
        self.trades = []
        self.equity_curve = []

    def run(self, data: pd.DataFrame, strategy):
        """主回测循环"""
        for timestamp, bar in data.iterrows():
            # 生成信号
            signal = strategy.on_bar(bar)
            # 执行订单
            if signal is not None:
                self.execute_order(signal, bar)
            # 记录权益
            self.equity_curve.append(self.get_equity(bar))

    def execute_order(self, signal, bar):
        """订单执行逻辑"""
        # 计算滑点
        slippage = self.calc_slippage(bar)
        # 计算冲击成本
        impact = self.calc_market_impact(signal, bar)
        # 实际成交价格
        exec_price = bar['close'] * (1 + slippage + impact)
        # ... 省略具体撮合逻辑
        pass

我的经验:回测环境里最容易忽略的是「数据对齐」。我曾经因为时间戳没对齐,导致回测结果比实盘好了30%。后来我强制要求所有数据都用 UTC 时间,并且精确到毫秒级。

16.2 历史数据回放:让市场重演

数据回放,说白了就是让历史行情重新走一遍。你想想看,如果回测时用的数据质量不行,那结果还有什么意义?我见过有人用日线数据回测高频策略,这不是开玩笑吗?

数据回放有几种常见模式:

  • Bar 回放:按 K 线周期推进,适合中低频策略
  • Tick 回放:逐笔成交数据,适合高频策略
  • Snapshot 回放:定时快照,适合做市策略

我个人习惯用 Bar 回放做初步验证,用 Tick 回放做精细优化。为什么?因为 Bar 回放速度快,能快速排除明显不行的策略;Tick 回放虽然慢,但能暴露很多执行层面的问题。

下面是一个数据回放器的实现:

class DataReplayer:
    def __init__(self, data: pd.DataFrame, mode='bar'):
        self.data = data
        self.mode = mode
        self.current_idx = 0

    def next(self):
        """获取下一帧数据"""
        if self.current_idx >= len(self.data):
            return None

        if self.mode == 'bar':
            bar = self.data.iloc[self.current_idx]
            self.current_idx += 1
            return bar
        elif self.mode == 'tick':
            # Tick 模式下,一次返回一个 tick
            tick = self.data.iloc[self.current_idx]
            self.current_idx += 1
            return tick

    def reset(self):
        """重置回放位置"""
        self.current_idx = 0

    def set_speed(self, multiplier):
        """设置回放速度倍数"""
        # 实际应用中可以通过跳过数据点来实现加速
        pass

避坑指南:我曾经在回放数据时忽略了「幸存者偏差」。用现在的股票池回测过去,结果当然好看,但实盘时那些退市的股票早就没了。记住,回放的数据必须是在那个时间点真实存在的。

16.3 订单模拟执行:把纸上谈兵变成实战

订单模拟执行,是整个回测框架里最考验功力的部分。为什么?因为真实市场的撮合逻辑远比想象中复杂。你想想看,你的订单发出去后,会遇到什么情况?

  • 对手盘深度不够,只能部分成交
  • 价格波动太快,成交价和预期差很远
  • 大单进场,直接把价格打穿

我建议至少模拟以下几种订单类型:

订单类型 模拟逻辑 适用场景
市价单 按当前最优价成交,考虑滑点 追求成交速度
限价单 挂在指定价格,等待对手盘 控制成交成本
冰山订单 只显示部分数量,隐藏真实意图 大单交易
TWA订单 按时间加权平均价格执行 降低冲击成本

下面是一个订单模拟器的核心逻辑:

class OrderSimulator:
    def __init__(self, order_book: pd.DataFrame):
        self.order_book = order_book  # 订单簿数据
        self.filled_orders = []

    def simulate_market_order(self, side: str, quantity: float):
        """模拟市价单执行"""
        remaining = quantity
        total_cost = 0
        total_filled = 0

        # 从最优价开始逐档吃单
        for level in self.order_book[side]:
            if remaining <= 0:
                break
            # 当前档位的可成交量
            available = min(level['volume'], remaining)
            # 成交价格 = 档位价格 + 滑点
            exec_price = level['price'] * (1 + self.slippage)
            # 累计成交
            total_cost += available * exec_price
            total_filled += available
            remaining -= available

        # 记录成交信息
        return {
            'filled_quantity': total_filled,
            'avg_price': total_cost / total_filled if total_filled > 0 else 0,
            'unfilled': remaining
        }

    def simulate_limit_order(self, price: float, quantity: float):
        """模拟限价单执行"""
        # 检查是否有对手盘愿意在这个价格成交
        # 如果有,则成交;否则挂单等待
        pass

关键点:订单模拟的精度直接决定了回测的可信度。我见过有人用「收盘价+随机滑点」来模拟成交,结果回测夏普比率2.5,实盘只有0.8。后来改用逐笔订单簿模拟,差距缩小到了10%以内。

16.4 绩效指标计算:用数据说话

回测跑完了,怎么评价执行算法的好坏?光看收益率肯定不行。我一般会从三个维度来评估:成交率、滑点、冲击成本。

16.4.1 成交率

成交率,说白了就是你的订单有多少被实际执行了。对于限价单来说,成交率尤其重要。我见过一个做市策略,理论年化收益20%,但因为成交率只有60%,实际收益直接腰斩。

def calc_fill_rate(trades: List[Dict]) -> float:
    """计算成交率"""
    total_orders = len(trades)
    filled_orders = sum(1 for t in trades if t['filled_quantity'] > 0)
    return filled_orders / total_orders if total_orders > 0 else 0

16.4.2 滑点

滑点,就是你的实际成交价和预期价格之间的差距。我习惯用基点(bps)来衡量:

def calc_slippage(trades: List[Dict], benchmark_prices: pd.Series) -> float:
    """计算平均滑点(bps)"""
    total_slippage = 0
    count = 0

    for trade in trades:
        expected_price = benchmark_prices[trade['timestamp']]
        actual_price = trade['avg_price']
        slippage_bps = (actual_price - expected_price) / expected_price * 10000
        total_slippage += slippage_bps
        count += 1

    return total_slippage / count if count > 0 else 0

16.4.3 冲击成本

冲击成本,说白了就是你的订单对市场价格造成的影响。大单进场,价格会被推高(买入)或压低(卖出),这部分成本就是冲击成本。

def calc_market_impact(trades: List[Dict], order_book: pd.DataFrame) -> float:
    """计算市场冲击成本"""
    total_impact = 0
    count = 0

    for trade in trades:
        # 成交前后的价差
        pre_trade_price = order_book.loc[trade['timestamp'] - pd.Timedelta('1s'), 'mid_price']
        post_trade_price = order_book.loc[trade['timestamp'] + pd.Timedelta('1s'), 'mid_price']
        impact_bps = (post_trade_price - pre_trade_price) / pre_trade_price * 10000
        total_impact += impact_bps
        count += 1

    return total_impact / count if count > 0 else 0

我的经验:这三个指标要结合起来看。比如,一个策略成交率很高,但滑点也很大,那说明它可能是在用市价单「硬吃」,成本控制得不好。反过来,成交率低但滑点小,说明限价单挂得太保守了。

16.5 知识体系总览

说了这么多,我画了一张图来总结整个执行算法回测框架的核心逻辑。你一看就明白了:

flowchart TD
    A[回测环境搭建<br/>速度优化 · 真实性模拟]
    B[历史数据回放<br/>Bar回放 · Tick回放]
    C[订单模拟执行<br/>市价单 · 限价单 · 冰山]
    D{执行}
    E[绩效指标计算<br/>成交率 · 滑点 · 冲击成本]

    A -.-> D
    B -.-> D
    C -.-> D
    D ==> E
    E -.反馈优化.-> A
    E -.反馈优化.-> B
    E -.反馈优化.-> C

    style A fill:#3498db,color:#fff
    style B fill:#2ecc71,color:#fff
    style C fill:#e74c3c,color:#fff
    style D fill:#f39c12,color:#fff
    style E fill:#9b59b6,color:#fff

从图上你能看到,整个框架是一个闭环。环境搭建是基础,数据回放提供原料,订单模拟是核心引擎,绩效计算给出评价。最后,绩效结果会反馈回来,指导你优化执行算法。

我个人觉得,这个框架最妙的地方在于它的可扩展性。你可以随时替换某个模块,比如把固定滑点模型换成更复杂的 Almgren-Chriss 模型,或者把 Bar 回放换成 Tick 回放。框架本身不需要大改。

最后提醒一句:回测框架再完美,也只是对真实市场的近似模拟。我见过太多人在回测里赚得盆满钵满,一上实盘就亏。记住,回测的目的是发现策略的缺陷,而不是证明策略有多牛。