回测框架设计原则:模块化设计、事件驱动架构、数据与策略分离、可扩展性

做回测框架,说白了就是在搭积木。我见过太多人一上来就写一个巨大的脚本,把数据加载、策略逻辑、订单管理全塞在一个文件里。结果呢?改一个参数要翻三页代码,换个策略得重写一半逻辑。嗯,今天我们就聊聊怎么把回测框架设计得既灵活又抗造。

模块化设计:拆得开,才能拼得爽

我个人习惯,把回测框架拆成几个独立的模块。每个模块只干一件事,干好它。比如数据模块只管拿数据、清洗数据;策略模块只管生成信号;风控模块只管检查订单是否合规。

为什么要这么拆?我在项目中遇到过一件事:有一次需要把回测从日线数据切换到分钟线数据。如果数据模块和策略模块耦合在一起,那改动量简直不敢想。但因为我们一开始就拆开了,只需要换数据模块的接口实现,策略代码一行没动。

模块化设计的好处,说白了就是「高内聚、低耦合」。每个模块内部逻辑要完整,但对外暴露的接口要简单。举个例子:

# 数据模块接口
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事件。每个模块只关心自己感兴趣的事件,互不干扰。

我的经验:事件类型不要定义得太细,也不要太粗。我一般用5-8种核心事件:BAR、TICK、ORDER、TRADE、POSITION、ACCOUNT、TIMER。够用,不乱。

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

这个原则,说白了就是「策略不应该知道数据从哪里来」。你想想看,如果策略代码里写死了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对象,再也没出过这种问题。

注意:数据与策略分离,不是让策略完全不知道数据长什么样。策略需要知道数据有哪些字段(比如open、close),但不需要知道数据存在哪里、怎么读的。

可扩展性:给未来留点余地

做回测框架,最怕的就是「这个功能我暂时用不到,先不做了」。结果三个月后要用,发现整个框架得重构。可扩展性,就是让框架能轻松地加新功能,而不破坏已有的东西。

怎么做到?我总结了三个小技巧:

  1. 用插件机制:策略、数据源、风控规则都可以做成插件。想加新的?写个类,注册一下就行。
  2. 用配置驱动:不要硬编码参数。把回测参数、策略参数、风控参数都放到配置文件里。
  3. 用接口抽象:每个模块都定义抽象基类,新模块只要实现这些接口就能接入。

举个例子,可扩展的数据源设计:

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方法,完事。框架其他代码完全不用动。

避坑指南:我曾经为了「省事」,把策略参数直接写在代码里。结果要跑100组参数优化时,只能手动改代码。后来改成用配置文件,一行命令跑完所有组合。嗯,从那以后我再也不硬编码参数了。

一张图看懂回测框架设计

下面这张SVG图,把四个设计原则的关系画清楚了。你仔细看看,模块化是骨架,事件驱动是血脉,数据与策略分离是规矩,可扩展性是目标。

回测框架设计原则 数据模块 DataFeed 策略模块 Strategy 风控模块 RiskManager 事件引擎 (EventEngine) BAR | TICK | ORDER | TRADE | POSITION 推送数据 注册/响应 检查订单 四大设计原则 模块化设计:各司其职 事件驱动:自动响应 数据与策略分离:解耦 可扩展性:预留接口

你看这张图,数据模块、策略模块、风控模块都挂在事件引擎上。它们之间没有直接连线,全部通过事件来通信。这就是事件驱动架构的精髓——模块之间松耦合,但又能协同工作。

一个小建议:刚开始做框架时,别追求完美。先把核心流程跑通,再逐步加模块。我见过有人花了一个月设计框架,结果代码一行没写。先跑起来,再优化,这才是正道。

好了,这四个原则讲完了。模块化让你拆得开,事件驱动让系统自己动,数据与策略分离让你换数据源不慌,可扩展性给未来留余地。你想想看,一个回测框架如果做到了这四点,是不是怎么改都不怕?


专注资料整理