4. 事件循环与调度器:主循环设计、事件分发机制、定时器事件、异步事件处理

好,咱们今天聊聊回测引擎的心脏——事件循环与调度器。说实话,我见过太多回测系统跑着跑着就崩了,或者时序错乱,十有八九是这块没设计好。你想想看,回测的本质是什么?就是在模拟市场的时间流逝,把历史数据当成实时流喂给策略。这个模拟过程,全靠事件循环撑着。

4.1 主循环设计:别让CPU空转

主循环说白了就是一个while True。但怎么设计这个循环,学问大了。我个人习惯把主循环分成三个层次:

  • 时间推进层:决定下一个事件什么时候发生
  • 事件获取层:从队列里拿出下一个要处理的事件
  • 事件处理层:把事件分发给对应的处理器

我在项目中遇到过一个问题:有人把主循环写成死等,CPU占用直接拉满。低延迟回测最忌讳这个。正确的做法是让循环在无事可做时休眠,或者用条件变量等待。

核心原则:主循环不应该主动轮询,而应该被事件唤醒。这是事件驱动架构的精髓。

class EventLoop:
    def __init__(self):
        self._running = False
        self._event_queue = PriorityQueue()
        self._timer_heap = []  # 最小堆,存定时器事件
        
    def run(self):
        self._running = True
        while self._running:
            # 1. 先检查定时器
            self._process_timers()
            # 2. 从队列取事件,设置超时
            try:
                event = self._event_queue.get(timeout=0.001)
                self._dispatch(event)
            except Empty:
                continue  # 没事件就继续检查定时器
                
    def stop(self):
        self._running = False

4.2 事件分发机制:路由的艺术

事件来了,发给谁?这就是分发机制要解决的问题。我见过最粗暴的做法是用if-else链,事件类型一多,代码就成了一团浆糊。

更好的做法是用注册表模式。每个处理器声明自己关心什么事件,分发器维护一张映射表。嗯,这里要注意:事件类型应该用枚举或者字符串常量,别用魔法数字。

我的经验:事件分发器的性能瓶颈往往不在查找,而在事件对象的创建和销毁。建议用对象池复用事件对象,减少GC压力。

class EventDispatcher:
    def __init__(self):
        self._handlers = defaultdict(list)
        
    def register(self, event_type, handler):
        self._handlers[event_type].append(handler)
        
    def dispatch(self, event):
        handlers = self._handlers.get(event.type, [])
        for handler in handlers:
            handler(event)
            
    # 支持优先级
    def register_with_priority(self, event_type, handler, priority=0):
        heapq.heappush(self._handlers[event_type], (priority, handler))

4.3 定时器事件:时间就是金钱

回测里定时器事件太常见了:每天收盘前5分钟清仓、每周一重新平衡、每月初调参数……这些都需要精确的定时触发。

我曾经踩过一个坑:用time.sleep来做定时,结果回测速度慢得像蜗牛。后来改用最小堆管理定时器,效率提升了几十倍。

定时器事件的设计要点:

  • 绝对时间 vs 相对时间:我建议统一用绝对时间(时间戳),避免时区问题
  • 一次性 vs 周期性:周期性定时器要记录下次触发时间,而不是简单重复
  • 精度控制:回测不需要纳秒级精度,毫秒级就够了。别为了精度牺牲性能
class TimerEvent:
    def __init__(self, trigger_time, callback, interval=None):
        self.trigger_time = trigger_time  # 绝对时间戳
        self.callback = callback
        self.interval = interval  # None表示一次性,否则是周期
        
    def __lt__(self, other):
        return self.trigger_time < other.trigger_time
        
class TimerScheduler:
    def __init__(self):
        self._heap = []
        
    def add_timer(self, timer_event):
        heapq.heappush(self._heap, timer_event)
        
    def process_due_timers(self, current_time):
        while self._heap and self._heap[0].trigger_time <= current_time:
            timer = heapq.heappop(self._heap)
            timer.callback()
            # 如果是周期性定时器,重新加入堆
            if timer.interval:
                timer.trigger_time += timer.interval
                heapq.heappush(self._heap, timer)

4.4 异步事件处理:别让一个事件卡死全局

回测里最怕什么?一个事件处理时间过长,导致后续事件全部延迟。比如策略里做了个复杂的计算,或者数据库查询超时。这时候就需要异步处理。

我个人建议用线程池来处理耗时操作,但要注意线程安全。事件循环本身是单线程的,分发事件很快,但处理事件可以扔到线程池里跑。

注意:异步处理会改变事件顺序。如果两个事件有因果关系,必须保证它们在同一线程中按序处理。我一般把市场数据事件和策略信号事件放在主线程,把日志、存储等非关键事件异步处理。

import concurrent.futures

class AsyncEventLoop(EventLoop):
    def __init__(self, max_workers=4):
        super().__init__()
        self._executor = concurrent.futures.ThreadPoolExecutor(max_workers)
        self._async_events = set()  # 标记哪些事件类型需要异步
        
    def mark_async(self, event_type):
        self._async_events.add(event_type)
        
    def _dispatch(self, event):
        if event.type in self._async_events:
            # 异步处理,不阻塞主循环
            self._executor.submit(self._handle_event, event)
        else:
            # 同步处理,保证顺序
            self._handle_event(event)
            
    def _handle_event(self, event):
        handlers = self._handlers.get(event.type, [])
        for handler in handlers:
            handler(event)

4.5 整体架构图

下面这张图展示了事件循环与调度器的完整工作流程。我画这张图的时候,特意把数据流向标清楚了,方便你理解各个组件怎么配合。

事件主循环 优先级事件队列 事件分发器 市场数据处理器 策略信号处理器 定时器处理器 定时器最小堆 异步线程池 主循环组件 数据存储 分发逻辑 处理器

避坑指南:我曾经在定时器事件里直接调用了数据库写入操作,结果回测速度从每秒10万笔掉到每秒1000笔。后来把所有IO操作都扔到异步线程池里,速度才恢复。记住:事件循环里只做计算,不做IO。

好了,事件循环与调度器的核心设计就这些。你想想看,其实整个回测引擎就是围绕这个循环转的。把主循环设计好了,事件分发清楚了,定时器管理精确了,异步处理到位了,你的回测系统就稳了八成。剩下的就是填业务逻辑了。

专注资料整理