第一章 事件驱动引擎:事件循环机制、事件类型定义、事件队列管理
大家好,我是老张。今天咱们聊聊回测系统里最核心的骨架——事件驱动引擎。
说实话,我刚开始做量化交易系统时,也踩过不少坑。最早我写回测,就是简单的循环遍历K线,每个tick算一下持仓盈亏。结果呢?策略稍微复杂点,比如同时处理多个品种、不同时间周期的信号,代码就乱成一锅粥了。
后来我才明白,回测系统本质上就是个事件驱动的状态机。你想想看,真实交易中,我们不就是根据行情事件、订单事件、时间事件来做决策的吗?回测也一样。
1.1 事件循环机制——系统的“心脏”
事件循环,说白了就是一个永不停止的while循环。它不停地问:有没有新事件?有就处理,没有就等着。
我习惯把事件循环比作餐厅的传菜员。厨师(数据源)做好菜(事件),传菜员(事件循环)端给对应的客人(事件处理器)。客人吃完了,传菜员再回来等下一道菜。
核心代码其实很简单:
class EventLoop:
def __init__(self):
self._running = False
self._queue = EventQueue()
def run(self):
self._running = True
while self._running:
event = self._queue.get()
if event is None:
continue
self._dispatch(event)
def stop(self):
self._running = False
嗯,这里要注意:get()方法一定要有超时机制。我曾经在早期版本里用了阻塞队列,结果回测跑完想停都停不下来,只能强制杀进程。后来改成了带超时的非阻塞获取,优雅多了。
关键点:事件循环必须支持优雅关闭。别让用户等得心焦。
1.2 事件类型定义——给事件“贴标签”
事件类型,就是给每个事件一个身份标识。我见过有人用字符串,有人用枚举。我个人强烈推荐用枚举,原因很简单:
- 字符串容易拼写错误,比如
"MARKET_DATA"写成"MARKET_DATA "(多了个空格),排查起来想死的心都有 - 枚举有IDE自动补全,写起来爽
- 枚举可以附加额外信息,比如优先级
来看看我项目里常用的定义:
from enum import Enum, auto
class EventType(Enum):
# 行情事件
TICK = auto() # 逐笔成交
BAR = auto() # K线
ORDER_BOOK = auto() # 深度行情
# 订单事件
ORDER_NEW = auto() # 新订单
ORDER_FILL = auto() # 成交
ORDER_CANCEL = auto() # 撤单
ORDER_REJECT = auto() # 拒单
# 系统事件
TIMER = auto() # 定时器
SHUTDOWN = auto() # 关闭信号
ERROR = auto() # 错误事件
为什么要有 ERROR 事件?我记得有一次回测跑了一整天,结果因为某个数据源异常,整个系统直接崩溃了。从那以后,我强制要求所有异常都必须包装成事件,由事件循环统一处理。这样至少能优雅地记录日志、保存中间状态。
小技巧:事件类型最好按功能分组,比如行情类、订单类、系统类。这样在事件分发时,可以快速路由到对应的处理器。
1.3 事件队列管理——别让事件“堵车”
事件队列,就是事件的缓冲区。它解决了一个核心问题:生产者和消费者的速度不匹配。
比如行情数据源可能一秒产生1000个tick,但策略处理一个tick需要5毫秒。如果没有队列,策略根本跟不上。有了队列,数据源只管往队列里塞,策略按自己的节奏处理。
我常用的队列实现有两种:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| FIFO队列 | 先进先出,保证时序 | 普通回测,按时间顺序处理 |
| 优先级队列 | 按优先级出队 | 需要紧急处理的事件(如风控信号) |
大多数情况下,FIFO就够了。但如果你做的是高频回测,我建议用优先级队列。为什么?因为风控事件必须优先处理。我曾经见过一个策略,因为行情事件堆积太多,风控信号被堵在后面,结果回测爆仓了——虽然只是模拟,但看着也心疼啊。
来看看队列的核心实现:
import heapq
from threading import Lock
class EventQueue:
def __init__(self, priority=False):
self._queue = []
self._priority = priority
self._lock = Lock()
def put(self, event):
with self._lock:
if self._priority:
# 优先级队列,按事件类型的优先级入队
heapq.heappush(self._queue, (event.type.value, event))
else:
self._queue.append(event)
def get(self, timeout=0.1):
with self._lock:
if self._queue:
if self._priority:
return heapq.heappop(self._queue)[1]
else:
return self._queue.pop(0)
return None
避坑指南:我曾经在队列里直接用了Python的list做FIFO,pop(0)的时间复杂度是O(n)。回测数据量一大,性能直接崩了。后来改用collections.deque,O(1)的复杂度,舒服多了。
1.4 完整的事件驱动流程
把上面三个部分串起来,就是完整的事件驱动流程。我画了张图,方便你理解:
流程其实很清晰:
- 数据源产生事件(行情、订单、定时等)
- 事件进入队列,等待处理
- 事件循环从队列中取出事件
- 根据事件类型,路由到对应的处理器
- 处理器更新状态,或触发策略决策
- 策略决策产生新事件(如订单),重新进入队列
你看,这就是个闭环。数据进来,策略出去,周而复始。
核心思想:事件驱动引擎的本质,就是把“什么时候做什么事”这个复杂问题,拆解成“发生什么事就处理什么事”的简单模型。每个事件都是独立的,互不干扰。这样你的策略逻辑就清晰了,调试也方便。
1.5 实际项目中的经验
最后分享几个我在实战中总结的经验:
- 事件要轻量:事件对象里只放必要的数据,别把整个DataFrame塞进去。我见过有人把100MB的行情数据当事件传,结果队列直接OOM。
- 队列要有容量限制:设置最大长度,超过就丢弃旧事件或阻塞生产者。不然内存会爆。
- 日志要详细:每个事件处理前后都打日志。回测出问题时,日志就是你的救命稻草。
- 性能测试不能少:用真实数据量压一下,看看事件循环能不能扛住。我一般要求100万事件/秒的处理能力。
好了,事件驱动引擎的核心就这些。说白了,它就是回测系统的骨架。骨架搭好了,后面加策略、加指标、加风控,都顺理成章。
下一章咱们聊聊数据源和回测引擎的对接,到时候见。
公众号:蓝海资料掘金营,微信deep3321