第七章:回测系统搭建

做量化交易,最怕什么?

怕策略在实盘里跑着跑着就崩了。怕历史回测漂亮得像朵花,一上实盘就变成豆腐渣。

我做了这么多年做市商系统,见过太多人栽在回测这个环节。说白了,回测系统就是你的策略试金石。今天咱们就聊聊怎么搭一个靠谱的回测系统。

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

回测的第一步,得有数据。而且得是干净的数据。

我个人习惯把历史数据分成三个层级:

  • Tick级数据:每笔成交的细节,适合高频策略
  • 快照级数据:每秒或每百毫秒的市场状态
  • K线数据:1分钟、5分钟等聚合数据

做做市商策略,我建议至少用快照级数据。为什么?因为做市商吃的是微观结构的饭,K线太粗了,很多细节都丢了。

数据清洗的坑

我曾经遇到过一个问题:某交易所的数据里,偶尔会出现时间戳倒流的情况。如果你不做处理,回测时就会算出穿越时空的收益——听起来很酷,但实盘里根本不存在。

数据回放的逻辑其实不复杂:

class DataReplayer:
    def __init__(self, data_path):
        self.data = self.load_data(data_path)
        self.current_idx = 0
    
    def next_tick(self):
        """获取下一个数据点"""
        if self.current_idx < len(self.data):
            tick = self.data[self.current_idx]
            self.current_idx += 1
            return tick
        return None
    
    def reset(self):
        self.current_idx = 0

嗯,代码看着简单,但实际用起来要注意性能。我见过有人用Python列表存几千万条tick数据,回测一次跑半小时。后来改成内存映射文件,速度提升了十几倍。

7.2 事件驱动回测框架:核心引擎

回测框架的核心是什么?是事件驱动。

你想想看,实盘交易时,市场在不停地产生事件:新的报价来了,订单成交了,撤单成功了...回测系统就得模拟这个过程。

我的经验

事件驱动框架里,最容易被忽略的是事件的时间戳对齐。不同数据源的时间可能差几毫秒,但就是这几毫秒,可能导致你的策略在回测里赚钱,实盘里亏钱。

一个典型的事件驱动框架包含这几个组件:

  • 事件队列:按时间排序的事件列表
  • 事件处理器:处理每种事件的逻辑
  • 策略模块:你的交易策略
  • 风控模块:检查订单是否合规
  • 成交引擎:模拟订单如何成交

下面是我常用的框架结构:

class EventEngine:
    def __init__(self):
        self.events = []
        self.strategies = []
        self.risk_manager = RiskManager()
        self.execution_engine = ExecutionEngine()
    
    def run(self):
        while self.events:
            event = heapq.heappop(self.events)
            self.process_event(event)
    
    def process_event(self, event):
        if event.type == 'TICK':
            for strategy in self.strategies:
                strategy.on_tick(event.data)
        elif event.type == 'ORDER':
            self.execution_engine.handle_order(event.data)

这里有个细节:事件队列要用优先队列(heapq),保证事件按时间顺序处理。我见过有人用普通列表,然后每次排序——那性能,啧啧,不忍直视。

7.3 成交模拟:最考验功力的地方

回测里最难的是什么?是模拟成交。

实盘里,你挂一个买单,可能瞬间就被吃掉了,也可能挂一天都没人理。回测里怎么模拟这个过程?

注意

千万不要假设「挂单就能成交」。我见过太多回测系统这么干,结果实盘时发现滑点大得吓人。

我常用的成交模型分三种:

模型类型 适用场景 精度
立即成交模型 流动性极好的品种
队列模型 做市商策略
订单簿重建模型 高频策略

做做市商策略,我建议用队列模型。它假设订单簿上每个价位都有一个队列,你的订单排在队尾。只有前面的订单都成交了,才轮到你。

class QueueExecutionModel:
    def __init__(self, queue_depth=10):
        self.queue_depth = queue_depth
    
    def can_fill(self, order, market_data):
        """判断订单是否能成交"""
        # 计算订单在队列中的位置
        position = self.get_queue_position(order, market_data)
        # 如果位置小于队列深度,认为能成交
        return position < self.queue_depth

这个模型虽然简单,但比「立即成交」靠谱多了。我曾在实盘里验证过,误差在5%以内。

7.4 绩效指标计算:用数字说话

回测跑完了,怎么评价策略好不好?

别只看总收益。我见过有人回测收益翻倍,但最大回撤80%,这种策略你敢用吗?

夏普比率(Sharpe Ratio)

夏普比率衡量的是「每承担一单位风险,能获得多少超额收益」。

def calculate_sharpe(returns, risk_free_rate=0.03):
    excess_returns = returns - risk_free_rate / 252
    sharpe = np.mean(excess_returns) / np.std(excess_returns)
    return sharpe * np.sqrt(252)  # 年化

夏普比率大于1算不错,大于2就很好了。但要注意:它假设收益是正态分布的,而实际市场里可不是这样。

索提诺比率(Sortino Ratio)

索提诺比率和夏普类似,但它只考虑下行风险。说白了,上涨的波动是好事,下跌的波动才是风险。

def calculate_sortino(returns, risk_free_rate=0.03):
    excess_returns = returns - risk_free_rate / 252
    downside_returns = excess_returns[excess_returns < 0]
    downside_std = np.std(downside_returns)
    sortino = np.mean(excess_returns) / downside_std
    return sortino * np.sqrt(252)

我个人更看重索提诺比率。为什么?因为做市商策略的收益曲线通常比较平滑,夏普比率可能很高,但索提诺更能反映真实的风险。

最大回撤(Max Drawdown)

最大回撤就是「从最高点跌到最低点的最大幅度」。

def calculate_max_drawdown(equity_curve):
    peak = np.maximum.accumulate(equity_curve)
    drawdown = (equity_curve - peak) / peak
    max_dd = np.min(drawdown)
    return max_dd

做市商策略的最大回撤一般控制在5%以内。如果超过10%,你得好好想想是不是策略有问题。

避坑指南

我曾经犯过一个错误:用全部历史数据计算绩效指标,结果策略在某个极端行情里表现极差,但被其他时间段的数据「平均」掉了。后来我改用滚动窗口计算,每个时间段的绩效都单独看,这才发现问题。

7.5 回测系统的整体架构

说了这么多,咱们看看回测系统的整体架构:

回测系统架构图 数据层 历史Tick数据 | 快照数据 | K线数据 | 数据清洗与对齐 事件驱动引擎 事件队列 | 事件分发 | 时间管理 | 性能优化 策略与风控层 策略模块 | 风控检查 | 订单管理 | 仓位管理 成交与绩效层 成交模拟 | 绩效计算 | 报告生成 | 可视化

这个架构图看着简单,但每一层都有很多细节。我建议你从数据层开始搭,一步步往上。别想着一次搞定,回测系统是迭代出来的。

最后说一句

回测系统不是一次性的工作。随着你对市场的理解加深,你会发现需要加更多功能。比如我后来加了「市场冲击模型」和「手续费动态计算」,回测结果才更接近实盘。

好了,回测系统的基本框架就这些。记住:回测是工具,不是目的。别在回测里追求完美,那反而会让你在实盘里摔跟头。

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