2、回测系统架构设计:系统模块划分、数据流设计、事件驱动架构介绍

好,咱们进入正题。上一章聊了回测系统的整体定位,这一章咱们把骨架搭起来。

做回测系统,说白了就是模拟一个微型市场。你的策略在里面跑,看它能不能赚钱。我刚开始做的时候,以为写个循环就完事了。结果呢?数据对不上,成交逻辑混乱,回测结果跟实盘差十万八千里。后来我明白了——架构设计没做好,后面全是坑。

2.1 系统模块划分:各司其职

一个健壮的回测系统,至少需要这几个模块。我习惯把它们分成四层:

层级 模块名称 核心职责
数据层 数据引擎 加载、清洗、对齐历史行情数据
逻辑层 策略引擎 运行你的交易策略,生成信号
执行层 模拟撮合引擎 处理订单、模拟成交、管理持仓
分析层 绩效分析器 计算盈亏、夏普比率、最大回撤等

每个模块只做一件事。数据引擎不管策略逻辑,策略引擎不关心订单怎么成交。这样改起来才方便。

我的经验: 模块之间通过接口通信,不要直接调用内部方法。我曾经把数据引擎和策略引擎写在一起,结果换数据源时,策略代码也得跟着改。那叫一个痛苦。

2.2 数据流设计:钱是怎么流动的

数据流是系统的血脉。我画了一张图,你看一眼就明白了:

数据引擎 加载Tick/Bar数据 行情推送 策略引擎 生成买卖信号 订单请求 撮合引擎 模拟成交、管理持仓 成交回执 绩效分析器 计算盈亏、指标 持仓更新 交易记录 图例说明: 数据层 逻辑层 执行层 分析层 数据流方向:行情 → 策略 → 撮合 → 绩效,同时有回执反馈

数据流其实就两条线:

  • 正向流: 数据引擎 → 策略引擎 → 撮合引擎 → 绩效分析器
  • 反向流: 撮合引擎把成交结果回传给策略引擎,更新持仓状态

你想想看,如果正向流和反向流混在一起,代码会乱成什么样?我见过一个项目,订单状态更新和行情处理写在一个函数里,调试时根本分不清是哪个环节出了问题。

小技巧: 每个数据包都带上时间戳和序列号。这样即使数据流乱了,也能回溯排查。我习惯用 datetime 加一个自增ID作为唯一标识。

2.3 事件驱动架构:让系统活起来

回测系统不是流水线,不能按顺序一步步走。市场是随机的,你得让系统能响应各种事件。

事件驱动架构,说白了就是:发生什么事,就触发什么处理逻辑。核心组件就三个:

  1. 事件队列: 存放所有待处理的事件。比如行情到达、订单成交、定时器触发等。
  2. 事件分发器: 从队列里取出事件,分发给对应的处理器。
  3. 事件处理器: 每个模块注册自己关心的事件。比如策略引擎只处理行情事件和成交事件。

我举个例子你就明白了:

# 伪代码,展示事件驱动核心逻辑
class EventEngine:
    def __init__(self):
        self.queue = []
        self.handlers = {}  # 事件类型 -> 处理函数列表
    
    def register(self, event_type, handler):
        if event_type not in self.handlers:
            self.handlers[event_type] = []
        self.handlers[event_type].append(handler)
    
    def put(self, event):
        self.queue.append(event)
    
    def run(self):
        while self.queue:
            event = self.queue.pop(0)
            for handler in self.handlers.get(event.type, []):
                handler(event)

# 使用示例
engine = EventEngine()
engine.register('tick', strategy.on_tick)
engine.register('fill', portfolio.on_fill)
engine.put(TickEvent(symbol='BTC-30DEC23-50000-C', price=150.0, time=...))
engine.run()

为什么要用事件驱动?我跟你讲三个理由:

  • 解耦: 每个模块只关心自己注册的事件。加一个新策略,不需要改撮合引擎的代码。
  • 可扩展: 想加一个风控模块?注册一个 order 事件就行。想加日志记录?注册所有事件。
  • 模拟真实环境: 实盘交易系统就是事件驱动的。回测用同样的架构,策略迁移时改动最小。
注意: 事件队列不要用无限循环。回测时你得控制时间推进。我习惯用一个 Clock 模块来驱动事件,每次只处理当前时间戳之前的事件。否则回测会跑飞。

2.4 事件类型定义:别漏了关键事件

我列一下回测系统里最常用的事件类型。你照着这个清单来,基本不会漏:

事件类型 触发时机 包含数据
MarketDataEvent 新行情数据到达 时间、品种、最新价、买卖盘口
SignalEvent 策略生成买卖信号 方向、数量、价格、策略ID
OrderEvent 信号转化为订单 订单类型、限价、有效期
FillEvent 订单部分或全部成交 成交价、成交量、手续费
TimerEvent 定时触发(如每秒) 当前时间

嗯,这里要注意:SignalEventOrderEvent 很多人会混在一起。我建议分开。为什么?因为信号可能被风控拦截,也可能被拆分成多个订单。分开处理,逻辑更清晰。

避坑指南: 我曾经把信号和订单合并成一个事件,结果想加一个「订单数量限制」的风控时,发现信号已经包含了订单信息,改起来牵一发动全身。后来我花了整整两天重构。所以,一开始就分开,别偷懒。

2.5 时间推进机制:别让回测跑偏

事件驱动架构里,时间推进是个大问题。你不能让系统自己跑,得控制它按历史数据的时间线走。

我常用的方法有两种:

  • 基于Bar的推进: 每次处理一个Bar(比如1分钟K线),处理完再推进到下一个Bar。简单,适合低频策略。
  • 基于Tick的推进: 每次处理一个Tick,适合高频做市策略。但数据量大,性能要求高。

我个人习惯用 混合模式:平时按Bar推进,但在Bar内部,如果有事件(比如订单成交),就暂停时间,处理完再继续。这样既保证了性能,又不会漏掉细节。

class TimeEngine:
    def __init__(self, data):
        self.data = data  # 按时间排序的历史数据
        self.current_index = 0
    
    def next_tick(self):
        """推进到下一个时间点"""
        if self.current_index < len(self.data):
            event = self.data[self.current_index]
            self.current_index += 1
            return event
        return None
    
    def get_current_time(self):
        return self.data[self.current_index - 1].time if self.current_index > 0 else None

你看,代码其实不复杂。但架构设计对了,后面加功能就顺了。

我的建议: 刚开始做回测系统,别追求完美。先搭一个最小可用版本,跑通一条策略。然后再慢慢加模块、优化性能。我见过太多人一开始就想做「通用回测框架」,结果半年过去了,连一个策略都没跑通。

好了,这一章就聊到这儿。架构设计是地基,地基稳了,后面盖楼才放心。下一章咱们开始动手写代码,从数据引擎开始。