2、回测系统架构设计:系统模块划分、数据流设计、事件驱动架构介绍
好,咱们进入正题。上一章聊了回测系统的整体定位,这一章咱们把骨架搭起来。
做回测系统,说白了就是模拟一个微型市场。你的策略在里面跑,看它能不能赚钱。我刚开始做的时候,以为写个循环就完事了。结果呢?数据对不上,成交逻辑混乱,回测结果跟实盘差十万八千里。后来我明白了——架构设计没做好,后面全是坑。
2.1 系统模块划分:各司其职
一个健壮的回测系统,至少需要这几个模块。我习惯把它们分成四层:
| 层级 | 模块名称 | 核心职责 |
|---|---|---|
| 数据层 | 数据引擎 | 加载、清洗、对齐历史行情数据 |
| 逻辑层 | 策略引擎 | 运行你的交易策略,生成信号 |
| 执行层 | 模拟撮合引擎 | 处理订单、模拟成交、管理持仓 |
| 分析层 | 绩效分析器 | 计算盈亏、夏普比率、最大回撤等 |
每个模块只做一件事。数据引擎不管策略逻辑,策略引擎不关心订单怎么成交。这样改起来才方便。
2.2 数据流设计:钱是怎么流动的
数据流是系统的血脉。我画了一张图,你看一眼就明白了:
数据流其实就两条线:
- 正向流: 数据引擎 → 策略引擎 → 撮合引擎 → 绩效分析器
- 反向流: 撮合引擎把成交结果回传给策略引擎,更新持仓状态
你想想看,如果正向流和反向流混在一起,代码会乱成什么样?我见过一个项目,订单状态更新和行情处理写在一个函数里,调试时根本分不清是哪个环节出了问题。
datetime 加一个自增ID作为唯一标识。
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 |
定时触发(如每秒) | 当前时间 |
嗯,这里要注意:SignalEvent 和 OrderEvent 很多人会混在一起。我建议分开。为什么?因为信号可能被风控拦截,也可能被拆分成多个订单。分开处理,逻辑更清晰。
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
你看,代码其实不复杂。但架构设计对了,后面加功能就顺了。
好了,这一章就聊到这儿。架构设计是地基,地基稳了,后面盖楼才放心。下一章咱们开始动手写代码,从数据引擎开始。