一、事件驱动架构概述:什么是事件驱动?为什么回测引擎需要事件驱动?与向量化回测的对比
好,咱们直接切入正题。今天要聊的,是回测引擎最核心的底层逻辑——事件驱动架构。很多朋友刚开始学量化时,接触的都是向量化回测,觉得又快又方便。但一旦你想做更精细的策略,比如处理逐笔成交数据、模拟真实撮合逻辑,你就会发现向量化那套东西根本不够用。
我个人习惯把事件驱动比作「现实世界的模拟器」。你想想看,真实交易中,市场是一秒一秒在变的,订单是一笔一笔进来的,你的策略也是一步一步响应的。事件驱动,就是要把这种「发生→响应」的链条,原封不动地搬到代码里。
1.1 什么是事件驱动?
说白了,事件驱动就是一种编程范式。它的核心思想是:程序不主动轮询,而是等待事件发生,然后触发对应的处理函数。
举个例子。你在写一个GUI程序,用户点了一下按钮,程序就弹出一个对话框。这就是事件驱动——按钮点击是事件,弹窗是响应。回测引擎里也一样:
- 事件:比如新的Bar数据来了、订单成交了、止损触发了
- 处理器:比如更新持仓、计算指标、发送新订单
嗯,这里要注意:事件驱动不是「顺序执行」,而是「按需响应」。你的策略不会主动去问「市场变了吗?」,而是市场变了之后,系统告诉你「嘿,有数据来了,你处理一下」。
核心要点:事件驱动 = 事件源 + 事件队列 + 事件处理器。三者缺一不可。
1.2 为什么回测引擎需要事件驱动?
这个问题我当年也困惑过。我记得第一次写回测引擎时,直接用向量化方式算了一堆K线,然后逐根判断买卖点。结果发现,策略里加了止损、加仓、分批止盈之后,代码变得一团乱麻。
为什么会这样?因为真实交易中,事件是异步的、无序的、不可预测的。比如:
- 你挂了一个限价单,可能下一秒就成交,也可能三天后才成交
- 你设了止损单,价格先触发了止损,然后又反弹了——但你的单子已经出去了
- 你同时监控多个品种,每个品种的数据到达时间不一样
向量化回测处理不了这些。它假设所有数据都是整齐排列的,所有操作都是同步完成的。但现实不是这样。
我曾经在做一个多品种套利策略时,就踩过这个坑。用向量化回测跑出来年化30%,实盘一跑直接亏了两个月。后来发现,问题出在回测里我假设两个品种的Bar是同时到达的,但实盘里它们差了好几秒,导致套利信号完全失效。
避坑指南:我曾经以为向量化回测够用了,直到被实盘打脸。如果你策略里涉及止损、限价单、多品种同步、逐笔成交,请老老实实用事件驱动。
1.3 事件驱动 vs 向量化回测
咱们直接上对比,这样最清楚。
| 对比维度 | 向量化回测 | 事件驱动回测 |
|---|---|---|
| 执行方式 | 一次性计算所有数据 | 逐事件处理,模拟真实时间流 |
| 订单处理 | 假设立即成交,无滑点模拟 | 支持限价单、止损单、部分成交 |
| 数据粒度 | 通常只支持Bar级别 | 支持Tick、Bar、自定义事件 |
| 策略复杂度 | 适合简单条件策略 | 适合复杂多条件、多品种策略 |
| 性能 | 极快(毫秒级跑完几年数据) | 较慢(逐事件处理,开销大) |
| 实盘一致性 | 低(理想化假设太多) | 高(更贴近真实交易环境) |
你可能会问:「那向量化回测是不是就没用了?」也不是。我个人习惯是:先用向量化做快速筛选,再用事件驱动做精细验证。向量化适合跑几百个策略找灵感,事件驱动适合把筛选出来的几个策略做实盘级测试。
说白了,向量化是「快但糙」,事件驱动是「慢但准」。两者不是替代关系,而是互补关系。
1.4 事件驱动的核心流程
下面这张图,是我自己画的事件驱动回测引擎的核心流程。你看一眼就能明白整体架构。
这张图里,最关键的其实是「事件循环」那个模块。它就像一个调度员,从队列里取出一个事件,然后问所有处理器:「谁要处理这个事件?」。处理器说「我要」,就执行对应的逻辑。处理过程中可能产生新事件(比如订单成交),再放回队列尾部。
嗯,这里有个细节:事件队列必须是FIFO(先进先出)的,这样才能保证时间顺序不乱。我见过有人用优先级队列,结果事件顺序乱了,回测结果完全不对。
小技巧:事件驱动回测里,时间戳是命根子。每个事件都必须带精确的时间戳,事件队列必须严格按时间排序。我曾经因为时间戳精度不够(只到秒),导致同一秒内多个事件的顺序错乱,查了三天bug。
1.5 一个简单的事件驱动回测代码骨架
光说不练假把式。我写一个最简版本的事件驱动回测框架,你感受一下结构。
class Event:
"""事件基类"""
def __init__(self, event_type, timestamp):
self.type = event_type # 'BAR', 'TICK', 'ORDER', 'FILL'
self.timestamp = timestamp
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)
if event.type in self.handlers:
for handler in self.handlers[event.type]:
handler(event)
# 使用示例
engine = EventEngine()
def on_bar(event):
print(f"处理Bar事件: {event.timestamp}")
engine.register_handler('BAR', on_bar)
engine.put(Event('BAR', '2024-01-01 10:00:00'))
engine.run()
这段代码虽然简单,但已经包含了事件驱动的三个核心要素:事件定义、事件队列、事件分发。你可以在on_bar里写你的策略逻辑,比如计算均线、判断买卖点、发送订单。
当然,真实的生产级引擎要复杂得多。比如要处理订单状态机、要支持多品种、要考虑性能优化。但万变不离其宗,核心就是上面这个骨架。
1.6 什么时候该用事件驱动?
我总结了几条经验,你对照一下自己的策略:
- 必须用事件驱动:策略涉及止损单、限价单、分批止盈、多品种套利、逐笔Tick数据
- 建议用事件驱动:策略有复杂的条件组合(比如A条件触发后,B条件在5分钟内有效)
- 向量化就够了:简单的均线交叉、固定周期调仓、单品种日线级别策略
说白了,如果你的策略逻辑可以写成「如果今天收盘价>20日均线,明天开盘买入」,那向量化完全够用。但如果你要写「如果价格突破前高且成交量放大,挂一个限价单在突破价下方0.5%处,同时设一个2%的止损」,那还是老老实实用事件驱动吧。
好,这一章就到这里。事件驱动的核心思想其实不复杂,就是「发生→响应」的循环。下一章我们会深入事件队列的实现细节,包括如何处理不同优先级的事件、如何保证时间顺序、以及如何做性能优化。