:root{--brand-1:#18794e;--brand-2:#3dd68c;--brand-3:#2bb673;--brand-soft:#3dd68c22;} .hero .kicker{color:var(--brand-2)} .nav .ep{color:var(--brand-2)} .sec h2 .num{color:var(--brand-2)} .quote::before{background:var(--brand-1)} .quote .q-text .em{color:var(--brand-2)} .quote .q-water .pg{color:var(--brand-2)} .widget .w-title{color:var(--brand-2)} .takeaway h3{color:var(--brand-2)} .takeaway ol li::marker{color:var(--brand-2)} .next .nx-label{color:var(--brand-2)} .formula .eq{color:var(--brand-2)} a{color:var(--brand-2)} .comp.c1 .tag{background:var(--brand-soft);color:var(--brand-2)} /* 事件循环演示组件 */ .eveng .stage{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin:14px 0 10px} .eveng .col{background:var(--bg-mute);border:1px solid var(--divider);border-radius:8px;padding:10px;min-height:120px} .eveng .col h5{margin:0 0 8px;font-size:12px;color:var(--brand-2);font-weight:700;letter-spacing:.05em;text-align:center} .eveng .ev{background:var(--bg-soft);border:1px solid var(--divider);border-radius:6px;padding:6px 8px;margin:5px 0;font-size:12px;color:var(--t2);cursor:pointer;transition:all .2s;font-family:var(--mono)} .eveng .ev:hover{border-color:var(--brand-1);color:var(--t1)} .eveng .ev.active{border-color:var(--brand-2);background:var(--brand-soft);color:var(--brand-2);box-shadow:0 0 0 2px var(--brand-soft)} .eveng .queue{min-height:60px} .eveng .arrow{text-align:center;color:var(--t3);font-size:18px;margin:2px 0} .eveng .log{background:#0a0e14;border:1px solid var(--divider);border-radius:8px;padding:10px 12px;margin-top:12px;font-family:var(--mono);font-size:12px;color:var(--t2);min-height:90px;max-height:160px;overflow:auto;line-height:1.7} .eveng .log .line{margin:2px 0} .eveng .log .ts{color:var(--t3)} .eveng .log .ev-n{color:var(--brand-2);font-weight:600} .eveng .log .ev-h{color:var(--cyan-1)} .eveng .controls{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0} .eveng .controls button{font-size:12.5px} .eveng .controls button.on{background:var(--brand-soft);color:var(--brand-2);border-color:var(--brand-1)} .eveng .hint{font-size:12px;color:var(--t3);margin-top:8px;text-align:center}
量化交易实战 · 第 9 期 · 第二季开篇

你的策略还在轮询?vnpy 早就事件驱动了

vnpy EventEngine 事件引擎 · event-driven engine

轮询是"每隔 1 秒问一次:行情到了吗"——又慢又浪费。vnpy 用事件驱动:行情一推过来就触发一个 Event,引擎把它丢进队列,按类型分发给所有订阅的处理器。策略、风控、下单、日志全是事件处理器。一个 Event 转完整个交易链路,这才是实盘该有的样子。
⏱ 约 9 分钟 🎯 写实盘策略、想懂框架的人 📦 源:vnpy §2

01轮询 vs 事件驱动:一个天上一个地下

新手写策略最直觉的写法是 while True: get_price(); sleep(1)——这是轮询。它有两个致命问题:延迟最高 1 秒,且 99% 的循环都在白跑。

事件驱动反过来:行情主动找你。交易所推一个 tick,框架包成 Event(EVENT_TICK, tick),丢进队列。引擎从队列取出,看类型是 EVENT_TICK,分发给所有订阅了 tick 的处理器——你的策略、风控、日志、UI 各自收到,各自干活。延迟是毫秒级,CPU 不空转。

vnpy 的 EventEngine 就是干这个的。它不是什么高深东西——一个队列 + 一个分发线程 + 一张"事件类型→处理器列表"的注册表。但它把整个框架解耦了:行情源、策略、风控、订单、UI 互不认识,只通过事件通信。这就是为什么 vnpy 能同时跑 CTP、币安、回测,互不干扰。

轮询是你找行情,
事件驱动是行情找你。
灏天文库 · 量化交易实战 P.27

02事件循环演示:点事件看它怎么转

下面模拟 vnpy 的 EventEngine。左边是事件源(点一下生成一个事件),中间是事件队列,右边是注册的处理器。点"自动跑"看事件被取出、分发、各处理器响应的全过程。

⚙️ EventEngine 事件循环演示
点左侧事件源生成事件,看它进队列→被引擎取出→分发给订阅的处理器。
事件源(点我)
EVENT_TICK · 行情
EVENT_ORDER · 委托
EVENT_TRADE · 成交
EVENT_TIMER · 定时
EVENT_LOG · 日志
事件队列
处理器(订阅)
策略 on_tick / on_order
风控 check_risk
日志 write_log
UI update_widget
提示:事件按类型分发——EVENT_TICK 只触发订阅了 tick 的处理器,EVENT_ORDER 触发订阅了 order 的。

03注册表:事件类型怎么找到处理器

事件驱动的核心是一张注册表:Dict[EventType, List[Handler]]。处理器启动时调用 engine.register(type, handler) 把自己挂到对应类型下。引擎取出事件后,handlers = registry[event.type],挨个调用。

Event(type, data) → queue.put() → engine.run() → registry[type] → for h in handlers: h(event)

关键设计:一个事件类型可以有多个处理器。一个 tick 进来,策略算指标、风控查仓位、UI 刷新 K 线——全靠这一份事件,互不阻塞。这就是 vnpy 能"行情来了同时干三件事"的底层。

另一个细节:处理器在同一线程顺序执行(vnpy 默认)。好处是无需加锁、逻辑简单;坏处是一个处理器卡住会拖慢所有事件。所以处理器要快——重活扔到策略自己的工作线程,引擎线程只做分发。

框架解耦的秘诀:
大家只认事件,不认彼此。
灏天文库 · 量化交易实战 P.28

04带走这套清单

✅ 事件驱动 6 条可执行规则

  1. 别在策略里轮询:用框架的事件回调(on_tick/on_bar),让行情找你。
  2. 处理器要快:引擎线程只做分发,重活(指标计算、下单)扔工作线程。
  3. 一个事件多处理器:tick 同时给策略、风控、UI,靠注册表解耦。
  4. 事件类型要规划好:EVENT_TICK/ORDER/TRADE/TIMER/LOG 分清楚,别混。
  5. 队列要监控:积压说明处理器太慢,是性能瓶颈的信号。
  6. 定时事件用 EVENT_TIMER:vnpy 用它做秒级/分钟级任务,别自己 sleep。
事件驱动不是炫技,
是实盘低延迟的底线。
灏天文库 · 量化交易实战 P.29