回测框架设计原则:模块化设计、事件驱动架构、数据与策略分离
做量化回测这些年,我踩过最大的坑是什么?
就是一开始把代码写得「太死」。策略逻辑和数据获取混在一起,换一个品种就得重写一大片。后来我学乖了——回测框架的设计,核心就三个原则:模块化、事件驱动、数据与策略分离。今天咱们一个一个聊透。
一、模块化设计:搭积木,别砌墙
模块化,说白了就是把系统拆成独立的小零件。每个零件只管自己的事,互相之间通过接口通信。
我个人习惯把回测框架拆成这样几个模块:
- 数据模块:负责获取、清洗、存储行情数据
- 策略模块:只做一件事——根据数据生成交易信号
- 风控模块:检查信号是否合规,比如仓位限制、止损条件
- 执行模块:把信号变成真实的订单操作
- 绩效模块:计算收益率、夏普比率、最大回撤等指标
为什么要这么拆?我在项目中遇到过一件事:有一次需要把回测从日线级别改成分钟级别。如果数据模块和策略模块耦合在一起,那改动量几乎是全局的。但因为是模块化的,我只改了数据模块的采样频率,策略那边完全没动。
二、事件驱动架构:让系统自己动起来
你想想看,真实交易中,市场是不断产生新数据的。一根K线走完,一个订单成交,一条新闻发布——这些都是事件。
事件驱动架构,就是让系统对这些事件做出响应。而不是用一个大循环去轮询「有没有新数据?」。
我建议这样设计事件流:
- 市场事件:新K线到达、Tick更新
- 信号事件:策略计算出买卖信号
- 订单事件:系统发出委托单
- 成交事件:订单被撮合,产生成交记录
每个事件都带着时间戳和必要数据,在模块之间传递。这样做的好处很明显——回测逻辑和真实交易逻辑可以共用同一套代码。我在做实盘对接时,几乎没改事件处理部分的代码。
三、数据与策略分离:别让策略「挑食」
这个原则我吃过亏。早期写策略时,我直接把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读的,还是从数据库查的,策略完全不知道,也不需要知道。
四、三个原则如何协同工作?
这三个原则不是孤立的。它们合在一起,才构成一个健壮的回测框架。
我画了一张图,帮你理清它们的关系:
从图上你能看到:数据模块产生市场事件,通过事件总线推送给策略模块。策略模块生成信号事件,再推送给执行和风控模块。每个模块都是独立的,替换任何一个都不会影响其他模块。
五、避坑指南
最后分享几个我踩过的坑:
- 别过度设计:一开始就搞几十个模块,反而让系统臃肿。我建议先拆成5-6个核心模块,后续再按需拆分。
- 事件别太细:每个Tick都发一个事件?没必要。通常K线级别的事件就够了,否则性能扛不住。
- 接口要稳定:数据模块的接口一旦定下来,尽量别改。否则所有策略都得跟着改,那叫「牵一发动全身」。
嗯,这三个原则就聊到这儿。记住它们,你的回测框架至少不会走偏。