模块化设计:拆开才能看清楚
做回测框架,我踩过最大的坑是什么?
就是一开始把所有逻辑揉在一个文件里。策略逻辑、数据读取、订单管理、绩效计算……全搅在一起。改一个地方,崩一片。那感觉,就像在厨房里炒菜,结果锅铲、调料、食材全混在一个碗里——你根本分不清哪个环节出了问题。
所以,模块化设计是我第一个要强调的原则。
什么是模块化?
说白了,就是把一个大的系统,拆成若干个独立的小模块。每个模块只干一件事,干好一件事。
在直盘做市回测框架里,我习惯这样拆分:
- 数据模块:负责读取、清洗、对齐行情数据。它不关心策略怎么用这些数据。
- 策略模块:接收数据,产生交易信号。它不关心订单怎么执行。
- 执行模块:接收信号,生成订单,模拟撮合。它不关心绩效怎么算。
- 风控模块:检查订单是否合规,比如最大持仓、最大亏损。它独立于策略逻辑。
- 绩效模块:计算夏普比率、最大回撤、胜率等指标。它只读数据,不修改任何状态。
核心原则:每个模块只通过接口(函数签名或类方法)与其他模块通信。内部实现可以随时替换,不影响外部。
举个例子。我早期做的一个做市策略,需要从不同交易所拿数据。一开始我把数据清洗逻辑直接写在策略类里。后来换了一个数据源,好家伙,策略代码改得面目全非。后来我学乖了,把数据模块独立出来,策略只认标准化的数据结构。换数据源?改一行配置就行。
事件驱动架构:让系统自己动起来
回测和实盘最大的区别是什么?
实盘是事件驱动的。行情来了,处理行情;订单成交了,处理成交。回测如果写成顺序执行,就完全模拟不了这种异步感。
我个人习惯用事件驱动架构来做回测框架。核心思想很简单:
- 系统里有一个事件队列,所有发生的事情都变成事件放进去。
- 有一个事件循环,不断从队列里取事件。
- 每个事件对应一个处理器,处理器根据事件类型做相应操作。
在直盘做市场景下,常见的事件类型有:
| 事件类型 | 触发时机 | 处理内容 |
|---|---|---|
| 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),所有模块都基于这些结构通信。中途改接口,代价很大。
三个原则的关系
这三个原则不是孤立的。它们互相支撑:
- 模块化让每个组件独立,方便替换和测试。
- 事件驱动让模块之间通过事件通信,而不是直接调用,进一步降低耦合。
- 数据与策略分离是模块化在数据层面的具体体现。
说白了,这三个原则共同指向一个目标:让回测框架可扩展、可维护、可信任。
下面这张图,是我自己画的一个框架结构图,你可以看看各个模块是怎么配合的:
嗯,这张图基本概括了这三个原则在框架里的落地方式。数据模块在最左边,独立提供数据;事件队列在中间,负责调度;策略、执行、风控、绩效各司其职,互不干扰。
最后说一句。这三个原则听起来简单,但真正做起来,需要你在写每一行代码时都问自己:「这个逻辑应该放在哪个模块?这个数据应该通过事件传递还是直接调用?」
养成这个习惯,你的回测框架会越来越健壮。我在项目中遇到过太多次因为耦合导致的问题,后来发现,90%的坑都可以通过这三个原则提前规避。
避坑指南:我曾经在事件循环里直接调用了策略模块的方法,导致策略状态被意外修改。后来强制规定:所有模块之间的通信必须通过事件队列,不允许直接调用。这个规则虽然严格,但让系统的稳定性提升了一个档次。