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

做量化回测这些年,我踩过最大的坑是什么?

就是一开始把代码写得「太死」。策略逻辑和数据获取混在一起,换一个品种就得重写一大片。后来我学乖了——回测框架的设计,核心就三个原则:模块化、事件驱动、数据与策略分离。今天咱们一个一个聊透。

一、模块化设计:搭积木,别砌墙

模块化,说白了就是把系统拆成独立的小零件。每个零件只管自己的事,互相之间通过接口通信。

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

  • 数据模块:负责获取、清洗、存储行情数据
  • 策略模块:只做一件事——根据数据生成交易信号
  • 风控模块:检查信号是否合规,比如仓位限制、止损条件
  • 执行模块:把信号变成真实的订单操作
  • 绩效模块:计算收益率、夏普比率、最大回撤等指标

为什么要这么拆?我在项目中遇到过一件事:有一次需要把回测从日线级别改成分钟级别。如果数据模块和策略模块耦合在一起,那改动量几乎是全局的。但因为是模块化的,我只改了数据模块的采样频率,策略那边完全没动。

核心要点:每个模块只暴露必要的接口,内部实现随便改。这就是「高内聚、低耦合」。

二、事件驱动架构:让系统自己动起来

你想想看,真实交易中,市场是不断产生新数据的。一根K线走完,一个订单成交,一条新闻发布——这些都是事件

事件驱动架构,就是让系统对这些事件做出响应。而不是用一个大循环去轮询「有没有新数据?」。

我建议这样设计事件流:

  1. 市场事件:新K线到达、Tick更新
  2. 信号事件:策略计算出买卖信号
  3. 订单事件:系统发出委托单
  4. 成交事件:订单被撮合,产生成交记录

每个事件都带着时间戳和必要数据,在模块之间传递。这样做的好处很明显——回测逻辑和真实交易逻辑可以共用同一套代码。我在做实盘对接时,几乎没改事件处理部分的代码。

小技巧:用Python的queue.Queue来管理事件队列。生产者(数据模块)往里放事件,消费者(策略模块)从里取事件。天然线程安全。

三、数据与策略分离:别让策略「挑食」

这个原则我吃过亏。早期写策略时,我直接把DataFrame塞进策略类里。结果换一个数据源,策略里所有列名都得改一遍。

正确的做法是:数据模块提供标准化的接口,比如get_ohlcv(symbol, start, end, freq)。策略只调用这个接口,不管数据是从CSV来的、从数据库来的、还是从API实时拉的。

来看一个简单的代码示例:

# 数据模块接口
class DataProvider:
    def get_ohlcv(self, symbol, start, end, freq):
        """返回标准化的OHLCV DataFrame"""
        pass

# 策略模块只依赖接口
class MovingAverageStrategy:
    def __init__(self, data_provider: DataProvider):
        self.data = data_provider
        
    def generate_signals(self, symbol):
        df = self.data.get_ohlcv(symbol, '2024-01-01', '2024-12-31', '1d')
        # 计算均线,生成信号
        df['ma5'] = df['close'].rolling(5).mean()
        df['ma20'] = df['close'].rolling(20).mean()
        df['signal'] = 0
        df.loc[df['ma5'] > df['ma20'], 'signal'] = 1
        df.loc[df['ma5'] < df['ma20'], 'signal'] = -1
        return df[['signal']]

你看,策略里只调用了get_ohlcv方法。至于数据是从本地CSV读的,还是从数据库查的,策略完全不知道,也不需要知道。

注意:数据与策略分离,不代表策略可以随意假设数据质量。我曾经因为数据里有空值,导致策略在回测中表现完美,实盘却一塌糊涂。所以数据模块一定要做好清洗和校验。

四、三个原则如何协同工作?

这三个原则不是孤立的。它们合在一起,才构成一个健壮的回测框架。

我画了一张图,帮你理清它们的关系:

回测框架三大原则协同关系 数据模块 获取·清洗·存储 标准化接口 get_ohlcv() 事件总线 市场事件·信号事件 订单事件·成交事件 策略模块 生成交易信号 只依赖接口,不依赖实现 执行模块 订单管理·撮合 风控模块 仓位·止损·合规检查 绩效模块 夏普·回撤·收益率 模块化设计 + 事件驱动 + 数据与策略分离 每个模块独立开发、独立测试、独立替换 事件总线解耦模块间依赖,数据接口标准化 策略只需关心「何时买卖」,不用管数据从哪来

从图上你能看到:数据模块产生市场事件,通过事件总线推送给策略模块。策略模块生成信号事件,再推送给执行和风控模块。每个模块都是独立的,替换任何一个都不会影响其他模块。

一句话总结:模块化让系统好改,事件驱动让系统活起来,数据与策略分离让策略可复用。这三个原则,是我做回测框架的「铁三角」。

五、避坑指南

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

  • 别过度设计:一开始就搞几十个模块,反而让系统臃肿。我建议先拆成5-6个核心模块,后续再按需拆分。
  • 事件别太细:每个Tick都发一个事件?没必要。通常K线级别的事件就够了,否则性能扛不住。
  • 接口要稳定:数据模块的接口一旦定下来,尽量别改。否则所有策略都得跟着改,那叫「牵一发动全身」。

嗯,这三个原则就聊到这儿。记住它们,你的回测框架至少不会走偏。


专注资料整理