一、系统架构设计:核心组件与通信机制
做量化回测系统,说白了就是在模拟真实交易环境。我刚开始做的时候,也踩过不少坑——最典型的就是把所有逻辑揉在一个大脚本里,改个参数要翻半天代码。后来才明白,好的架构设计,才是回测系统能跑得稳、改得动的关键。
今天咱们就聊聊事件驱动回测系统的核心架构。我会把每个模块拆开来讲,再告诉你它们之间怎么配合。
1.1 整体架构概览
事件驱动回测系统,核心思想就一句话:一切皆事件。行情来了是事件,订单成交了是事件,风控触发了也是事件。系统里各个模块不直接调用对方,而是通过事件引擎来沟通。
我画了一张架构图,你先感受下整体脉络:
你看,事件引擎在正中间,其他四个模块围着它转。这种设计的好处很明显——模块之间解耦了,你想换策略或者换数据源,都不影响其他部分。
1.2 事件引擎——系统的"心脏"
事件引擎是整个系统的调度中心。它维护一个事件队列,按照时间顺序依次处理每个事件。我个人习惯把事件引擎设计成单例模式,整个系统只有一个实例在跑。
核心职责:
- 接收各模块发来的事件
- 按时间戳排序放入队列
- 依次派发给对应的处理器
这里有个关键点:事件必须按时间顺序处理。我在项目中遇到过因为事件乱序导致回测结果完全不对的情况,排查了两天才发现是队列排序出了问题。
# 事件引擎的核心骨架
class EventEngine:
def __init__(self):
self.queue = [] # 事件队列
self.handlers = {} # 事件类型 -> 处理器列表
def register_handler(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)
self.queue.sort(key=lambda e: e.timestamp) # 按时间排序
def run(self):
"""启动事件循环"""
while self.queue:
event = self.queue.pop(0)
self._dispatch(event)
def _dispatch(self, event):
"""派发事件到对应处理器"""
handlers = self.handlers.get(event.type, [])
for handler in handlers:
handler(event)
小技巧:事件队列用优先队列(heapq)实现,比每次排序效率高得多。我后来重构时改成了heapq,回测速度提升了30%。
1.3 策略模块——你的交易大脑
策略模块是用户写策略的地方。它接收行情事件,计算信号,然后生成交易指令。说白了,这就是你交易思路的代码化表达。
我建议把策略基类设计成接口,只暴露两个方法:on_bar() 和 on_tick()。用户只需要实现这两个方法就行,其他事情不用管。
class BaseStrategy:
def __init__(self, name):
self.name = name
self.event_engine = None
def on_bar(self, bar):
"""处理K线数据"""
raise NotImplementedError
def on_tick(self, tick):
"""处理Tick数据"""
raise NotImplementedError
def send_signal(self, signal):
"""发送交易信号"""
self.event_engine.put(signal)
嗯,这里要注意:策略里不要做任何耗时操作。我曾经见过有人在策略里写数据库查询,结果回测速度慢得像蜗牛。策略就该只做计算,其他事情交给别的模块。
1.4 数据模块——喂给策略的"粮食"
数据模块负责把原始数据转换成策略能理解的事件。它从CSV、数据库或者API读取数据,然后按时间顺序推送给事件引擎。
| 数据源类型 | 适用场景 | 注意事项 |
|---|---|---|
| CSV文件 | 小规模回测、研究阶段 | 注意日期格式统一 |
| 数据库 | 中大规模回测 | 做好索引优化查询 |
| 实时API | 模拟盘/实盘 | 处理断线重连 |
数据模块还有一个重要职责——数据清洗。比如处理停牌、复权、缺失值等。我习惯在数据模块里做一次性的清洗,而不是让策略去处理这些脏活。
1.5 风控模块——系统的"刹车"
风控模块很多人会忽略,但我觉得它恰恰是最重要的。它负责检查每一笔交易是否合规,比如仓位是否超限、是否在禁止交易时段等。
我曾经踩过的坑:一开始没做风控,回测结果看起来很美。结果实盘第一天就触发了交易所的风控限制,直接爆仓。从那以后,我回测系统里必加风控模块。
风控模块的检查逻辑通常包括:
- 单笔交易金额上限
- 总仓位比例限制
- 交易品种黑名单
- 交易时间窗口
- 最大回撤阈值
1.6 执行模块——把想法变成现实
执行模块负责模拟订单的成交过程。它接收风控通过后的订单,然后根据市场流动性、滑点等因素模拟成交。
这里有个容易忽略的点:成交模拟的精度。简单的做法是直接按当前价成交,但这样会高估收益。我建议至少考虑以下因素:
- 滑点:按固定比例或随机比例
- 成交量限制:大单可能无法完全成交
- 延迟:从下单到成交的时间差
class ExecutionModule:
def execute_order(self, order):
"""模拟订单执行"""
# 计算滑点
slippage = self.calc_slippage(order.volume)
# 模拟成交价
fill_price = order.price * (1 + slippage)
# 模拟成交量(考虑流动性)
fill_volume = min(order.volume, self.available_liquidity)
# 生成成交回报
return Fill(order.id, fill_price, fill_volume)
1.7 模块间通信机制
模块之间怎么通信?说白了就是通过事件。每个模块只跟事件引擎打交道,不直接调用其他模块。
举个例子,策略模块想下单:
- 策略生成信号事件,发给事件引擎
- 事件引擎把信号事件派发给风控模块
- 风控检查通过后,生成订单事件
- 事件引擎把订单事件派发给执行模块
- 执行模块模拟成交,生成成交回报事件
- 成交回报再通过事件引擎通知策略
你想想看,这种设计的好处是什么?每个模块只需要关心自己收到的和发出的事件,不需要知道其他模块的存在。想加一个新模块?注册几个事件处理器就行。
事件类型定义示例:
MARKET_DATA- 行情数据事件SIGNAL- 交易信号事件ORDER- 订单事件FILL- 成交回报事件RISK_ALERT- 风控警告事件
好了,这就是事件驱动回测系统的核心架构。每个模块各司其职,通过事件引擎串联起来。这种设计我用了好几年,改起来方便,扩展性也好。你动手搭建的时候,可以先从事件引擎开始,其他模块慢慢加进去。