回测框架设计原则:模块化、可扩展性、事件驱动架构

做高频策略回测,说白了就是在和时间赛跑。我见过太多人一上来就写个巨大的脚本,把所有逻辑揉在一起。结果呢?改个参数要改三处代码,换个交易所就得重写一半逻辑。嗯,今天我们就聊聊怎么避免这种悲剧。

为什么需要设计原则?

先讲个我自己的教训。几年前我接手一个回测项目,代码大概5000行,全在一个文件里。回测逻辑、数据加载、订单管理、绩效统计,全都混在一起。我花了整整两周才搞清楚它到底在干什么。更崩溃的是,我想加一个滑点模型,结果发现订单执行逻辑和行情处理逻辑是耦合的——改一处,崩一片。

所以,设计原则不是理论,是血泪换来的经验。高频策略回测对性能要求极高,但性能不能以牺牲可维护性为代价。你想想看,一个策略从想法到上线,可能要迭代几十次。如果每次改代码都像拆炸弹,那还怎么玩?

模块化:把大象装进冰箱分几步?

模块化,说白了就是「分而治之」。把回测系统拆成独立的模块,每个模块只干一件事,干好一件事。

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

模块名称 职责 我踩过的坑
数据模块 加载、清洗、对齐行情数据 曾经把tick数据和分钟数据混在一个类里,结果时间戳对齐搞了三天
策略模块 接收行情,产生交易信号 信号生成和风控逻辑耦合,导致回测结果和实盘对不上
执行模块 模拟订单簿、撮合、滑点 滑点模型写死在代码里,换策略就得改框架
风控模块 检查仓位、资金、频率限制 风控放在策略里,结果策略改了风控忘了改
绩效模块 计算夏普、最大回撤、胜率等 绩效计算和回测循环混在一起,想加个指标得动核心逻辑

每个模块只通过接口通信。比如策略模块只需要知道「当前价格是多少」,它不需要关心数据是从CSV读的还是从数据库取的。这样,哪天你想换数据源,只需要改数据模块,策略代码一行都不用动。

核心原则:模块之间通过事件或消息传递数据,不要直接调用对方的方法。这是降低耦合的关键。

可扩展性:为未来留个接口

可扩展性,说白了就是「别把路走死」。你永远不知道明天会冒出什么新需求——新的交易所、新的订单类型、新的风控规则。

我建议用「策略模式」和「工厂模式」来设计。举个例子:

# 定义一个滑点模型的接口
class SlippageModel:
    def apply(self, order, market_data):
        raise NotImplementedError

# 具体实现
class FixedSlippage(SlippageModel):
    def apply(self, order, market_data):
        order.price *= (1 + 0.0001)  # 固定万分之一滑点
        return order

class PercentageSlippage(SlippageModel):
    def apply(self, order, market_data):
        spread = market_data.ask - market_data.bid
        order.price += spread * 0.5  # 按买卖价差的一半滑点
        return order

你看,新加一个滑点模型,只需要继承 SlippageModel 并实现 apply 方法。框架代码完全不用改。我曾经在项目中用这种方式,一周内接了三个不同交易所的回测需求,每个交易所的撮合逻辑不同,但核心框架没动过一行。

小技巧:用配置文件来管理策略参数和模块选择。比如在YAML里写 slippage_model: percentage,框架自动加载对应的类。这样改策略连代码都不用重启。

事件驱动架构:让数据自己跑起来

高频策略回测,本质上就是「行情来了 -> 策略响应 -> 产生订单 -> 模拟撮合 -> 更新状态」这样一个循环。事件驱动架构正好匹配这个流程。

我习惯用「事件总线」来解耦各个模块。每个模块只关心自己感兴趣的事件,不关心事件是谁发的。

核心事件类型:

  • MarketDataEvent:新的tick或K线数据到达
  • SignalEvent:策略产生了买卖信号
  • OrderEvent:需要提交的订单
  • FillEvent:订单被部分或全部成交
  • RiskCheckEvent:风控检查触发

事件驱动的优势很明显:

  • 模块之间完全解耦,加一个新模块只需要注册它关心的事件
  • 容易做并行处理——不同事件可以交给不同线程处理
  • 回测和实盘可以用同一套事件处理逻辑,只是数据源不同

注意:事件驱动虽然灵活,但要注意事件风暴问题。如果某个事件触发了另一个事件,然后又触发更多事件,容易形成循环。我建议在事件总线上加一个最大递归深度限制,或者用队列来异步处理。

框架核心流程图

下面这张图展示了整个回测框架的数据流。我画的时候特意把模块之间的边界标清楚了,你一看就明白。

数据模块 行情数据加载/清洗 事件总线 策略模块 信号生成 风控模块 仓位/频率检查 执行模块 订单簿/撮合 绩效模块 指标计算 MarketDataEvent 行情推送 SignalEvent 风控检查 RiskCheckEvent OrderEvent FillEvent 成交数据 数据流方向 | 事件通过总线中转,模块间不直接调用

这张图里,所有模块都只和事件总线打交道。数据模块产生行情事件,策略模块消费它并产生信号事件,风控模块检查后放行,执行模块撮合后产生成交事件,绩效模块最后统计。每个环节都可以独立替换或升级。

避坑指南:我踩过的三个大坑

讲完理论,分享几个实战中容易翻车的地方:

  • 坑一:模块拆分太细。 我曾经把数据清洗拆成三个模块——去重、对齐、插值。结果为了处理一个tick数据,事件在总线里转了三圈,性能直接崩了。模块化不是越细越好,找到合适的粒度是关键。
  • 坑二:事件命名不规范。 团队里有人用 on_data,有人用 handle_tick,还有人用 process_market。调试的时候简直灾难。我建议统一用 事件类型_动作 的命名方式,比如 market_data_receivedorder_filled
  • 坑三:忽略事件顺序。 高频回测中,tick数据是严格按时间顺序的。如果事件处理是异步的,可能会出现后一个tick先处理完的情况。我后来在事件总线上加了一个时间戳排序器,确保事件按时间顺序消费。

我的建议:刚开始做框架时,先别追求完美。用最简单的模块划分跑通一个策略,然后逐步重构。我见过太多人花三个月设计框架,结果策略还没跑就放弃了。先跑起来,再优化。

好了,模块化、可扩展性、事件驱动,这三个原则是回测框架的基石。你想想看,如果一开始就把这些想清楚,后面加策略、换数据、改参数,都会轻松很多。嗯,下一章我们聊聊具体的数据处理——毕竟,没有干净的数据,再好的策略也是白搭。


专注资料整理