回测框架设计原则:模块化设计、事件驱动、数据与策略分离

做市策略的回测,说白了就是一场「模拟考试」。你想想看,真金白银冲进去之前,总得先知道自己的策略靠不靠谱吧?我做了这么多年量化,见过太多人上来就写个脚本跑回测,结果换到实盘就崩了。问题出在哪?框架设计没想清楚。

今天咱们聊聊回测框架的三个核心设计原则。嗯,这三个原则是我自己踩坑踩出来的。刚开始做回测时,我也喜欢把所有代码揉在一个文件里,后来发现改个参数都要翻半天,别提多痛苦了。

模块化设计:拆开才能复用

模块化,说白了就是「各司其职」。一个回测框架,至少应该拆成这几个模块:

  • 数据模块:负责获取、清洗、存储行情数据
  • 策略模块:实现具体的做市逻辑,比如报价、风控
  • 执行模块:模拟订单簿、撮合、滑点
  • 分析模块:计算收益、夏普、最大回撤等指标

我个人习惯,每个模块只干一件事。比如数据模块,它只管把 tick 数据喂进来,至于策略怎么用,它不关心。这样好处很明显——哪天你想换数据源,改一个模块就行,其他代码动都不用动。

核心原则:每个模块的输入输出要清晰,模块之间通过接口通信,不要直接调用内部函数。

我在项目中遇到过一件事:有个同事把数据清洗逻辑直接写在策略类里。结果换了个交易所的数据,策略代码改得面目全非。嗯,这就是耦合太紧的代价。

事件驱动:让系统自己动起来

做市策略和普通策略不一样。普通策略可能每天收盘后算一次信号,但做市策略是高频的,每笔 tick 都可能触发报价更新。这时候,事件驱动就派上用场了。

事件驱动的核心思想很简单:系统不主动轮询,而是等事件来了再响应。比如:

  • 新 tick 数据到达 → 触发策略更新报价
  • 订单成交 → 触发仓位更新和风控检查
  • 定时器到期 → 触发统计报告生成

你想想看,如果每来一个 tick 都去轮询一遍所有模块,CPU 全浪费在空转上了。事件驱动的好处就是「按需干活」,效率高得多。

小技巧:用 Python 的 queue.Queue 或者 asyncio 的事件循环来实现事件总线。我个人偏好用队列,调试起来更直观。

我曾经犯过一个错:把所有事件处理都放在主线程里,结果一个耗时操作卡住了整个回测。后来改成异步事件处理,每个事件类型有自己的处理器,问题就解决了。

数据与策略分离:别让数据绑架你的逻辑

这个原则,我愿称之为「回测框架的命根子」。为什么?因为做市策略对数据极其敏感。同样的策略,用不同频率的数据跑,结果可能天差地别。

数据与策略分离,具体来说:

  1. 数据层:只负责提供标准化的行情接口,比如 get_tick()get_orderbook()
  2. 策略层:只关心业务逻辑,比如「当买卖价差超过 0.01 时,挂单吃单」
  3. 两者之间通过事件通信:数据来了,触发策略;策略下单,触发执行

这样做的好处,我举个例子你就明白了。假设你原来用 1 秒的快照数据做回测,后来想换成 tick 级数据。如果数据和策略混在一起,你得改策略代码。但如果分离了,你只需要换数据模块,策略代码一行都不用改。

注意:数据与策略分离,不代表它们完全独立。策略需要知道数据的格式和频率,否则没法用。我的做法是定义一个「数据契约」——比如所有 tick 数据必须包含时间戳、价格、成交量三个字段。策略按契约来写,数据模块按契约来提供。

我记得有一次,团队里有人把数据清洗逻辑写进了策略的回调函数里。结果数据源换了格式,策略直接报错。嗯,这就是典型的「数据绑架策略」。

一张图看懂框架设计

下面这张图,是我自己画的一个简化版回测框架结构。你看一眼就明白了:

数据模块 行情数据 · 清洗 · 存储 事件总线 队列 · 分发 · 调度 策略模块 报价逻辑 · 风控 · 订单 执行模块 订单簿 · 撮合 · 滑点 分析模块 收益 · 风险 · 归因 tick事件 策略事件 订单事件 成交事件 报告事件 图例: 数据模块 事件总线 策略模块 执行模块 分析模块 箭头方向:事件流动方向,从数据产生到最终分析报告

你看,数据模块产生 tick 事件,通过事件总线发给策略模块。策略模块根据逻辑生成订单事件,再发给执行模块。执行模块撮合后产生成交事件,最后分析模块汇总出报告。整个过程,每个模块只关心自己的事。

代码示例:一个极简的事件驱动回测骨架

光说不练假把式。我写个最简单的例子,你感受一下:

# 事件基类
class Event:
    def __init__(self, event_type, data):
        self.type = event_type
        self.data = data

# 事件总线
class EventBus:
    def __init__(self):
        self.handlers = {}
    
    def register(self, event_type, handler):
        if event_type not in self.handlers:
            self.handlers[event_type] = []
        self.handlers[event_type].append(handler)
    
    def emit(self, event):
        for handler in self.handlers.get(event.type, []):
            handler(event)

# 数据模块
class DataModule:
    def __init__(self, bus):
        self.bus = bus
    
    def feed_tick(self, tick):
        event = Event('tick', tick)
        self.bus.emit(event)

# 策略模块
class StrategyModule:
    def __init__(self, bus):
        self.bus = bus
        bus.register('tick', self.on_tick)
    
    def on_tick(self, event):
        # 做市逻辑:如果价差大于0.01,挂单
        tick = event.data
        if tick['ask'] - tick['bid'] > 0.01:
            order = {'side': 'buy', 'price': tick['bid'] + 0.005}
            self.bus.emit(Event('order', order))

# 执行模块
class ExecutionModule:
    def __init__(self, bus):
        self.bus = bus
        bus.register('order', self.on_order)
    
    def on_order(self, event):
        # 模拟撮合
        print(f"执行订单: {event.data}")

# 组装
bus = EventBus()
data = DataModule(bus)
strategy = StrategyModule(bus)
execution = ExecutionModule(bus)

# 模拟数据
data.feed_tick({'bid': 100.00, 'ask': 100.02})
data.feed_tick({'bid': 100.01, 'ask': 100.03})

这个例子虽然简单,但骨架是完整的。你想想看,如果以后要加风控模块,只需要注册一个 'order' 事件的处理器,在策略和执行之间插一脚就行。模块化、事件驱动、数据与策略分离,全都有了。

避坑指南:我曾经在事件总线上踩过一个坑——事件处理顺序。如果多个处理器注册了同一个事件,它们的执行顺序可能影响结果。我的建议是:给每个处理器加一个优先级字段,按优先级排序执行。

总结一下

这三个原则,说白了就是「高内聚、低耦合」在回测框架里的具体落地。模块化让你能拆开改,事件驱动让系统自动响应,数据与策略分离让你换数据不换逻辑。嗯,我做了这么多年,发现凡是能稳定跑上几个月的回测框架,都离不开这三个原则。

你如果刚开始搭框架,别急着写代码。先画张图,把模块边界划清楚。磨刀不误砍柴工,框架设计好了,后面写代码就是填坑的事。

专注资料整理