回测框架设计原则:模块化设计、事件驱动架构、数据与策略分离、可扩展性
做回测框架,说白了就是在搭积木。我见过太多人一上来就写一个巨大的脚本,把数据加载、策略逻辑、订单管理全塞在一个文件里。结果呢?改一个参数要翻三页代码,换个策略得重写一半逻辑。嗯,今天我们就聊聊怎么把回测框架设计得既灵活又抗造。
模块化设计:拆得开,才能拼得爽
我个人习惯,把回测框架拆成几个独立的模块。每个模块只干一件事,干好它。比如数据模块只管拿数据、清洗数据;策略模块只管生成信号;风控模块只管检查订单是否合规。
为什么要这么拆?我在项目中遇到过一件事:有一次需要把回测从日线数据切换到分钟线数据。如果数据模块和策略模块耦合在一起,那改动量简直不敢想。但因为我们一开始就拆开了,只需要换数据模块的接口实现,策略代码一行没动。
模块化设计的好处,说白了就是「高内聚、低耦合」。每个模块内部逻辑要完整,但对外暴露的接口要简单。举个例子:
# 数据模块接口
class DataFeed:
def get_bars(self, symbol, start, end):
"""返回OHLCV数据"""
pass
# 策略模块接口
class Strategy:
def on_bar(self, bar):
"""每根K线触发一次"""
pass
# 风控模块接口
class RiskManager:
def check_order(self, order):
"""返回True/False"""
pass
你看,每个模块的职责清清楚楚。数据模块不知道策略怎么用数据,策略也不知道风控怎么检查订单。它们只通过接口交流。
事件驱动架构:让系统自己动起来
回测的本质是什么?是模拟市场在时间轴上的推进。事件驱动架构就是让系统在「事件发生」时自动响应,而不是我们手动去轮询。
我刚开始做回测时,用的是循环驱动:
for bar in bars:
strategy.on_bar(bar)
# 处理订单...
# 更新持仓...
这样写简单,但扩展性很差。你想加个定时任务?加个外部信号?都得往循环里塞代码。后来我改成了事件驱动:
class EventEngine:
def __init__(self):
self.handlers = {}
def register(self, event_type, handler):
self.handlers.setdefault(event_type, []).append(handler)
def emit(self, event):
for handler in self.handlers.get(event.type, []):
handler(event)
# 使用示例
engine = EventEngine()
engine.register('BAR', strategy.on_bar)
engine.register('ORDER', risk_manager.check_order)
engine.register('TRADE', portfolio.update)
为什么会这样?因为事件驱动把「什么时候做什么事」的决定权交给了事件本身。市场来了新K线,触发BAR事件;订单被成交,触发TRADE事件。每个模块只关心自己感兴趣的事件,互不干扰。
数据与策略分离:别让数据绑架你的策略
这个原则,说白了就是「策略不应该知道数据从哪里来」。你想想看,如果策略代码里写死了CSV文件路径,那换数据库怎么办?换API怎么办?
我建议的做法是:数据模块把数据包装成统一的格式,策略只认这个格式。比如:
# 统一的数据格式
class BarData:
def __init__(self, symbol, open, high, low, close, volume, timestamp):
self.symbol = symbol
self.open = open
self.high = high
self.low = low
self.close = close
self.volume = volume
self.timestamp = timestamp
# 策略只依赖BarData,不关心数据来源
class MyStrategy(Strategy):
def on_bar(self, bar: BarData):
if bar.close > bar.open:
self.buy(bar.symbol, 100)
我曾经踩过一个坑:有个策略直接用了CSV的列名,比如 row['close']。后来数据源换了,列名变成了 row['Close'],整个策略跑不起来。从那以后,我强制所有数据都转成统一的BarData对象,再也没出过这种问题。
可扩展性:给未来留点余地
做回测框架,最怕的就是「这个功能我暂时用不到,先不做了」。结果三个月后要用,发现整个框架得重构。可扩展性,就是让框架能轻松地加新功能,而不破坏已有的东西。
怎么做到?我总结了三个小技巧:
- 用插件机制:策略、数据源、风控规则都可以做成插件。想加新的?写个类,注册一下就行。
- 用配置驱动:不要硬编码参数。把回测参数、策略参数、风控参数都放到配置文件里。
- 用接口抽象:每个模块都定义抽象基类,新模块只要实现这些接口就能接入。
举个例子,可扩展的数据源设计:
from abc import ABC, abstractmethod
class DataSource(ABC):
@abstractmethod
def load_data(self, symbol, start, end):
pass
class CSVDataSource(DataSource):
def load_data(self, symbol, start, end):
# 从CSV读取
pass
class DatabaseDataSource(DataSource):
def load_data(self, symbol, start, end):
# 从数据库读取
pass
class APIDataSource(DataSource):
def load_data(self, symbol, start, end):
# 从API获取
pass
想加个新的数据源?写个类继承DataSource,实现load_data方法,完事。框架其他代码完全不用动。
一张图看懂回测框架设计
下面这张SVG图,把四个设计原则的关系画清楚了。你仔细看看,模块化是骨架,事件驱动是血脉,数据与策略分离是规矩,可扩展性是目标。
你看这张图,数据模块、策略模块、风控模块都挂在事件引擎上。它们之间没有直接连线,全部通过事件来通信。这就是事件驱动架构的精髓——模块之间松耦合,但又能协同工作。
好了,这四个原则讲完了。模块化让你拆得开,事件驱动让系统自己动,数据与策略分离让你换数据源不慌,可扩展性给未来留余地。你想想看,一个回测框架如果做到了这四点,是不是怎么改都不怕?