模块化设计:拆开才能看清楚

做回测框架,我踩过最大的坑是什么?

就是一开始把所有逻辑揉在一个文件里。策略逻辑、数据读取、订单管理、绩效计算……全搅在一起。改一个地方,崩一片。那感觉,就像在厨房里炒菜,结果锅铲、调料、食材全混在一个碗里——你根本分不清哪个环节出了问题。

所以,模块化设计是我第一个要强调的原则。

什么是模块化?

说白了,就是把一个大的系统,拆成若干个独立的小模块。每个模块只干一件事,干好一件事。

在直盘做市回测框架里,我习惯这样拆分:

  • 数据模块:负责读取、清洗、对齐行情数据。它不关心策略怎么用这些数据。
  • 策略模块:接收数据,产生交易信号。它不关心订单怎么执行。
  • 执行模块:接收信号,生成订单,模拟撮合。它不关心绩效怎么算。
  • 风控模块:检查订单是否合规,比如最大持仓、最大亏损。它独立于策略逻辑。
  • 绩效模块:计算夏普比率、最大回撤、胜率等指标。它只读数据,不修改任何状态。

核心原则:每个模块只通过接口(函数签名或类方法)与其他模块通信。内部实现可以随时替换,不影响外部。

举个例子。我早期做的一个做市策略,需要从不同交易所拿数据。一开始我把数据清洗逻辑直接写在策略类里。后来换了一个数据源,好家伙,策略代码改得面目全非。后来我学乖了,把数据模块独立出来,策略只认标准化的数据结构。换数据源?改一行配置就行。

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

回测和实盘最大的区别是什么?

实盘是事件驱动的。行情来了,处理行情;订单成交了,处理成交。回测如果写成顺序执行,就完全模拟不了这种异步感。

我个人习惯用事件驱动架构来做回测框架。核心思想很简单:

  1. 系统里有一个事件队列,所有发生的事情都变成事件放进去。
  2. 有一个事件循环,不断从队列里取事件。
  3. 每个事件对应一个处理器,处理器根据事件类型做相应操作。

在直盘做市场景下,常见的事件类型有:

事件类型 触发时机 处理内容
TickEvent 每收到一个报价 更新市场状态,触发策略
OrderEvent 策略发出订单 检查风控,送入撮合引擎
FillEvent 订单成交 更新持仓,记录交易
BarEvent K线闭合 触发基于K线的策略逻辑
SignalEvent 策略产生信号 转换为订单事件

我的经验:事件队列最好用优先级队列或时间排序队列。因为回测里,事件是有时间顺序的。乱序处理会导致成交逻辑出错。我曾经因为用了普通队列,导致一个订单在错误的时间点被撮合,回测结果虚高了好几个百分点。

你想想看,如果不用事件驱动,而是顺序执行:先读一天的数据,然后跑策略,然后生成订单,然后撮合……那你怎么处理「订单在下一个tick才成交」这种场景?做市策略里,订单的成交时机直接影响盈亏。所以事件驱动不是可选项,是必选项。

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

这个原则,我是在一次惨痛教训后才真正理解的。

有一次,我写了一个做市策略,直接在策略类里写死了数据路径和字段名。后来数据供应商改了字段命名规则,我不得不把策略代码翻了个底朝天。更糟糕的是,我想换一个数据源做对比测试,发现策略代码和数据解析代码完全耦合在一起,根本拆不开。

数据与策略分离,核心就三点:

  • 数据模块只负责提供数据,不关心策略怎么用。
  • 策略模块只接收标准化数据,不关心数据从哪里来、什么格式。
  • 两者通过一个明确的接口通信,比如一个DataHandler基类。

具体到代码层面,我一般这样设计:

class DataHandler(ABC):
    """数据处理器基类,所有数据源都继承这个"""
    
    @abstractmethod
    def get_latest_tick(self, symbol: str) -> Tick:
        pass
    
    @abstractmethod
    def get_latest_bar(self, symbol: str) -> Bar:
        pass
    
    @abstractmethod
    def update(self) -> bool:
        """推进数据,返回是否还有数据"""
        pass


class CSVDataHandler(DataHandler):
    """从CSV文件读取数据"""
    # 实现具体的数据读取逻辑


class LiveDataHandler(DataHandler):
    """从交易所API获取实时数据"""
    # 实现实盘数据逻辑

策略模块只认DataHandler这个接口。你给它传CSVDataHandler,它就做回测;你给它传LiveDataHandler,它就做实盘。代码一行都不用改。

注意:数据与策略分离,不代表数据模块可以随意设计。接口要稳定,字段要明确。我建议在项目初期就定义好标准的数据结构(比如Tick、Bar、OrderBook),所有模块都基于这些结构通信。中途改接口,代价很大。

三个原则的关系

这三个原则不是孤立的。它们互相支撑:

  • 模块化让每个组件独立,方便替换和测试。
  • 事件驱动让模块之间通过事件通信,而不是直接调用,进一步降低耦合。
  • 数据与策略分离是模块化在数据层面的具体体现。

说白了,这三个原则共同指向一个目标:让回测框架可扩展、可维护、可信任

下面这张图,是我自己画的一个框架结构图,你可以看看各个模块是怎么配合的:

直盘做市回测框架 - 模块化事件驱动架构 数据模块 CSV / API / 数据库 标准化Tick/Bar数据 事件队列 TickEvent / OrderEvent FillEvent / SignalEvent 策略模块 做市策略逻辑 产生SignalEvent 执行模块 订单生成与撮合 产生FillEvent 风控模块 最大持仓 / 最大亏损 订单合规检查 绩效模块 夏普 / 回撤 / 胜率 绩效报告生成 事件循环(Event Loop) 从队列取事件 → 分发到对应处理器 推送数据 分发事件 SignalEvent OrderEvent 检查 FillEvent 成交数据

嗯,这张图基本概括了这三个原则在框架里的落地方式。数据模块在最左边,独立提供数据;事件队列在中间,负责调度;策略、执行、风控、绩效各司其职,互不干扰。

最后说一句。这三个原则听起来简单,但真正做起来,需要你在写每一行代码时都问自己:「这个逻辑应该放在哪个模块?这个数据应该通过事件传递还是直接调用?」

养成这个习惯,你的回测框架会越来越健壮。我在项目中遇到过太多次因为耦合导致的问题,后来发现,90%的坑都可以通过这三个原则提前规避。

避坑指南:我曾经在事件循环里直接调用了策略模块的方法,导致策略状态被意外修改。后来强制规定:所有模块之间的通信必须通过事件队列,不允许直接调用。这个规则虽然严格,但让系统的稳定性提升了一个档次。