3. 事件驱动引擎:让回测系统“活”起来

做量化回测,最怕什么?

我最怕的是策略逻辑和市场数据搅在一起,改一个参数就得重写整个流程。后来我接触了事件驱动架构,说白了,就是把系统拆成一个个“反应单元”——市场来了新数据,系统就触发一个事件;策略想下单,也发一个事件。各模块各干各的,互不干扰。

这一章,我们就来亲手搭建这个核心引擎。

3.1 事件循环机制:系统的“心跳”

事件驱动引擎的核心,是一个永不停止的循环。它就像人的心跳,一下一下地检查有没有新事件需要处理。

核心逻辑:

  1. 从事件队列里取出一个事件
  2. 判断事件类型
  3. 交给对应的处理器去执行
  4. 处理完,继续取下一个

我在早期版本里犯过一个低级错误——循环里忘了加退出条件。结果回测跑完了,程序还在空转,CPU 直接拉满。嗯,这里要注意:事件循环必须能优雅地终止。

class EventLoop:
    def __init__(self):
        self.queue = Queue()
        self.running = False

    def start(self):
        self.running = True
        while self.running:
            event = self.queue.get()
            if event is None:  # 退出信号
                break
            self._dispatch(event)

    def stop(self):
        self.running = False
        self.queue.put(None)  # 塞一个退出事件

你想想看,这个 None 信号是不是很巧妙?它既能让循环退出,又不会阻塞在 queue.get() 上。

3.2 事件类型定义:三类核心事件

一个回测系统里,事件其实就三大类。我习惯把它们定义成枚举,清晰又好扩展。

事件类型 触发时机 携带数据
市场事件 新 K 线/ tick 到达 时间、开盘价、收盘价、成交量等
信号事件 策略产生买卖信号 标的、方向、置信度
订单事件 下单、成交、撤单 订单 ID、价格、数量、状态

我的习惯:每个事件都带一个 timestamp 字段。为什么?因为回测里时间就是一切。没有时间戳,你根本没法对齐市场数据和策略信号。

from enum import Enum, auto
from dataclasses import dataclass
from datetime import datetime

class EventType(Enum):
    MARKET = auto()
    SIGNAL = auto()
    ORDER = auto()

@dataclass
class Event:
    type: EventType
    timestamp: datetime
    data: dict

我曾经遇到过一个坑:信号事件和订单事件的时间戳不一致,导致回测结果完全对不上。后来我强制所有事件的时间戳都从同一个时钟源获取,问题才解决。

3.3 事件队列管理:别让事件“堵车”

事件队列,说白了就是一个先进先出的管道。但这里有个关键问题:优先级

市场事件必须优先处理,因为它驱动着整个回测的推进。订单事件可以稍微排后,毕竟成交确认晚一两个 tick 影响不大。

避坑指南:我曾经用过一个简单的 list 当队列,结果事件一多,插入和删除的性能直线下降。后来换成了 collections.deque,性能提升了 10 倍不止。

from collections import deque
from heapq import heappush, heappop

class PriorityEventQueue:
    def __init__(self):
        self._market_queue = deque()
        self._signal_queue = deque()
        self._order_queue = deque()

    def put(self, event: Event):
        if event.type == EventType.MARKET:
            self._market_queue.append(event)
        elif event.type == EventType.SIGNAL:
            self._signal_queue.append(event)
        else:
            self._order_queue.append(event)

    def get(self) -> Event:
        # 市场事件优先
        if self._market_queue:
            return self._market_queue.popleft()
        if self._signal_queue:
            return self._signal_queue.popleft()
        if self._order_queue:
            return self._order_queue.popleft()
        return None

你可能会问:为什么不用一个统一的优先级队列?嗯,我试过。但实际跑起来发现,市场事件和信号事件的优先级差距太大,混在一起反而增加了排序开销。拆成三个独立队列,反而更清爽。

3.4 完整的事件驱动流程

把上面三个部分串起来,就是一套完整的事件驱动引擎。我画了一张流程图,帮你理清关系。

市场数据源 市场事件 事件队列 事件循环 事件分发器 市场处理器 策略处理器 订单处理器 新事件反馈回队列

整个流程其实就三步:

  1. 数据进来:市场数据源产生市场事件,塞进队列
  2. 循环调度:事件循环不断从队列里取事件,交给分发器
  3. 分发处理:分发器根据事件类型,扔给对应的处理器

处理器处理完后,可能会产生新的事件(比如策略处理器产生信号事件),再塞回队列。这样就形成了一个闭环。

性能优化小技巧:如果回测的数据量很大(比如 10 年 tick 级数据),可以考虑用多线程。一个线程专门负责从数据源读取并生成事件,另一个线程负责事件循环和处理。但要注意线程安全问题,我一般用 queue.Queue 来保证。

好了,事件驱动引擎的核心骨架已经搭好了。下一节我们会在这个骨架上,填上具体的策略逻辑和订单管理模块。到时候你会发现,这套架构的扩展性真的非常强。


公众号:蓝海资料掘金营,微信deep3321