2. 回测框架设计原则:模块化设计、事件驱动架构、数据与逻辑分离

做市策略的回测,说白了就是一场「模拟战争」。你想想看,真金白银投进去之前,总得先在沙盘上推演几轮吧?我做了这么多年量化,见过太多人一上来就写个几百行的脚本,跑完发现bug一堆,改起来想死的心都有。

为什么会这样?因为回测框架的设计,从一开始就没想清楚。今天我就跟你聊聊,我个人习惯遵循的三个核心原则。嗯,这三个原则帮我少踩了无数坑。

2.1 模块化设计:拆开,别揉在一起

模块化,说白了就是「各司其职」。我见过最糟糕的代码,是把数据读取、策略逻辑、订单管理、绩效统计全部塞进一个函数里。你想想看,想改个手续费计算方式,结果把策略逻辑也带崩了。

我个人习惯把回测框架拆成这几个模块:

  • 数据模块:负责行情数据的加载、清洗、对齐。只输出标准化的DataFrame。
  • 策略模块:接收行情数据,输出交易信号。它不关心数据从哪来,也不关心订单怎么执行。
  • 执行模块:接收信号,模拟撮合、成交、滑点、手续费。它只负责「模拟交易所」。
  • 分析模块:接收成交记录,计算夏普比率、最大回撤、胜率等指标。

核心原则:每个模块只做一件事,且只通过接口通信。改一个模块,不影响其他模块。

我在项目中遇到过一件事:有个同事把数据清洗逻辑写在了策略类里。后来换了数据源,他得改策略类里的清洗代码,结果策略逻辑也被改坏了。嗯,从那以后,我强制要求数据模块必须独立。

2.2 事件驱动架构:让系统「活」起来

做市策略跟传统趋势策略不一样。趋势策略可能一天只交易几次,但做市策略每秒都可能产生信号。如果用循环遍历K线的方式,根本扛不住高频场景。

事件驱动架构,就是让系统「主动响应」而不是「被动轮询」。我常用的设计是这样的:

class EventEngine:
    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 put(self, event):
        # 事件来了,通知所有注册的处理器
        for handler in self.handlers.get(event.type, []):
            handler(event)

你看,核心就三个动作:注册事件产生事件分发事件。行情来了是一个事件,订单成交了是一个事件,定时器到了也是一个事件。

我曾经犯过一个错误:把所有事件处理逻辑写在一个大循环里,结果行情快了就卡死。后来改成事件驱动,每个处理器独立运行,系统瞬间流畅了。说白了,这就是「异步」的朴素实现。

小技巧:做市策略里,建议把「订单簿更新」和「成交回报」拆成两个独立事件。因为它们的处理逻辑完全不同,混在一起容易出bug。

2.3 数据与逻辑分离:别让数据「绑架」策略

这个原则,我是在一次惨痛教训后才真正理解的。当时我写了一个回测,数据直接硬编码在策略类里。后来想换一个时间区间,得改策略代码。更糟的是,数据格式一变,策略逻辑全废了。

数据与逻辑分离,核心就一句话:策略不应该知道数据是怎么来的

我建议的做法:

  • 数据模块输出统一的接口,比如 get_bars(symbol, start, end, freq)
  • 策略模块只调用这个接口,不关心背后是CSV、数据库还是API
  • 数据清洗、对齐、重采样等操作,全部在数据模块内部完成

举个例子:

# 不好的做法:策略里直接读CSV
class MyStrategy:
    def run(self):
        df = pd.read_csv('data.csv')
        # ... 策略逻辑

# 好的做法:策略只依赖接口
class MyStrategy:
    def __init__(self, data_provider):
        self.data = data_provider

    def run(self):
        bars = self.data.get_bars('BTCUSDT', '2024-01-01', '2024-06-01', '1s')
        # ... 策略逻辑

你想想看,如果以后数据源从CSV换成数据库,或者换成实时行情,策略代码一行都不用改。这就是「分离」带来的好处。

注意:数据与逻辑分离,不代表数据模块可以随意设计。接口一定要稳定,一旦确定就不要轻易改。我曾经因为改了接口参数名,导致三个策略同时报错,改了一下午。

2.4 三个原则如何协同工作?

这三个原则不是孤立的。模块化是骨架,事件驱动是血脉,数据与逻辑分离是灵魂。我画了一张图,帮你理解它们的关系:

回测框架三大设计原则 数据模块 行情加载、清洗、对齐 输出标准化DataFrame (数据与逻辑分离) 事件引擎 注册、产生、分发事件 行情事件、成交事件 (事件驱动架构) 策略模块 接收行情、生成信号 不关心数据来源 (模块化设计) 执行模块 模拟撮合、滑点、手续费 (模块化设计) 分析模块 成交回报事件 图:三大原则在回测框架中的协作关系

从图上你能看到:数据模块把清洗好的行情推给事件引擎,事件引擎分发给策略模块。策略模块生成信号后,交给执行模块模拟撮合。执行模块再把成交结果作为新事件,反馈给事件引擎。整个过程环环相扣,但每个模块都是独立的。

我建议你在搭建框架时,先画一张类似的图。把模块边界、事件流向、数据接口都标清楚。嗯,这一步花不了多少时间,但能帮你省下后面大量的调试时间。

2.5 避坑指南

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

  • 模块间耦合太紧:我曾经把数据模块和策略模块写在同一个类里,结果想换数据源,得重写整个类。后来强制用接口隔离,才解决。
  • 事件类型定义太粗:一开始我只定义了「行情事件」和「成交事件」,后来发现还需要「定时事件」「风控事件」。建议一开始就把事件类型规划好,留扩展余地。
  • 数据格式不统一:不同数据源的时间戳格式、列名都不一样。我建议在数据模块入口处,统一转换成内部标准格式。别等到策略模块再去处理。

一句话总结:模块化让你改得动,事件驱动让你跑得快,数据与逻辑分离让你活得久。这三个原则,是做市策略回测框架的基石。

好了,这一章就聊到这儿。记住,框架设计不是一次性的工作,它会在你不断迭代策略的过程中,慢慢完善。别追求完美,先跑起来再说。