做市引擎架构:事件驱动、异步IO与任务调度

做市引擎的架构设计,说白了就是解决一个核心问题:如何在极短的时间内,处理来自多个交易所的海量数据,并做出正确的交易决策

我个人习惯把做市引擎比作一个繁忙的机场塔台。你想想看,塔台要同时监控几十架飞机的状态,接收飞行员的各种请求,还要给出起飞降落的指令。任何一个环节卡顿,都可能出大问题。做市引擎也一样——它要同时监听多个交易所的行情、订单簿、成交回报,还要管理自己的订单状态。

这一章,我们就来拆解这个塔台的核心架构。

事件驱动架构:一切皆事件

做市引擎的本质,是一个事件驱动的状态机

什么叫事件驱动?说白了就是:没事别找我,有事我再叫你。引擎平时处于空闲状态,只有当某个事件发生时(比如收到一条新的行情数据、某个订单被成交了),它才会被唤醒,执行相应的处理逻辑。

我在项目中遇到过最典型的反例:有人用轮询的方式去检查交易所的订单状态。每10毫秒查一次,结果交易所的API有频率限制,直接把他封了。这就是典型的「没事找事」——明明可以用事件驱动,非要主动去问。

事件驱动架构的核心组件包括:

  • 事件源:产生事件的地方,比如WebSocket连接、REST API回调、定时器
  • 事件总线:负责事件的发布和订阅,是事件的中转站
  • 事件处理器:消费事件并执行具体业务逻辑的模块

嗯,这里要注意:事件处理器之间不能互相阻塞。如果一个处理器在处理事件时卡住了,其他事件就得排队等着。这就是为什么我们需要异步IO。

核心原则:事件处理器必须是「非阻塞」的。任何可能耗时的操作(比如网络请求、数据库写入)都应该异步执行。

异步IO:别让CPU闲着

做市引擎要同时连接多个交易所,每个交易所都有行情推送和交易接口。如果用同步的方式,一个连接卡住了,其他连接都得等。这显然不行。

Python的异步IO(asyncio)就是解决这个问题的利器。它允许我们在单线程内并发处理多个IO操作,当一个操作在等待网络响应时,CPU可以切换到另一个操作继续执行。

我曾经踩过一个坑:刚开始用asyncio时,我在一个协程里调用了time.sleep(),结果整个事件循环都卡住了。后来才明白,在异步代码里要用await asyncio.sleep(),而不是time.sleep()。前者会主动让出控制权,后者会阻塞整个线程。

一个典型的异步IO架构如下:

import asyncio
import aiohttp

class ExchangeConnection:
    """交易所连接管理器"""
    
    def __init__(self, exchange_name, ws_url):
        self.name = exchange_name
        self.ws_url = ws_url
        self.session = None
        self.ws = None
        
    async def connect(self):
        """建立WebSocket连接"""
        self.session = aiohttp.ClientSession()
        self.ws = await self.session.ws_connect(self.ws_url)
        print(f"{self.name} 连接成功")
        
    async def listen(self, callback):
        """监听行情推送"""
        async for msg in self.ws:
            if msg.type == aiohttp.WSMsgType.TEXT:
                # 将原始数据解析为事件
                event = self.parse(msg.data)
                # 回调事件处理器
                await callback(event)
                
    async def close(self):
        """关闭连接"""
        if self.ws:
            await self.ws.close()
        if self.session:
            await self.session.close()

async def main():
    # 同时连接两个交易所
    binance = ExchangeConnection("Binance", "wss://stream.binance.com/ws")
    okx = ExchangeConnection("OKX", "wss://ws.okx.com/v5/public")
    
    await asyncio.gather(
        binance.connect(),
        okx.connect()
    )
    
    # 同时监听两个交易所的行情
    await asyncio.gather(
        binance.listen(handle_binance_event),
        okx.listen(handle_okx_event)
    )

asyncio.run(main())

避坑指南:我曾经在异步代码里混用了同步的requests库,结果导致事件循环频繁阻塞。后来全部换成aiohttp,性能提升了10倍以上。记住:在异步代码里,所有网络操作都要用异步库

内存队列:解耦生产者和消费者

事件驱动架构里,事件的生产者和消费者往往是不同步的。行情数据可能一秒钟来几百条,但策略处理的速度可能跟不上。这时候就需要一个缓冲区——内存队列

我习惯用Python的asyncio.Queue来做这件事。它天然支持异步操作,而且线程安全。

内存队列的作用有三个:

  • 解耦:生产者只管往队列里塞数据,消费者只管从队列里取数据,互不干扰
  • 削峰填谷:行情爆发时,队列可以缓冲数据,防止系统过载
  • 优先级控制:可以给不同的事件设置不同的优先级,比如成交回报的优先级应该高于行情数据
import asyncio
from enum import Enum, auto

class EventPriority(Enum):
    HIGH = auto()   # 成交回报、错误事件
    NORMAL = auto() # 订单簿更新
    LOW = auto()    # 行情Ticker

class Event:
    """事件基类"""
    def __init__(self, event_type, data, priority=EventPriority.NORMAL):
        self.type = event_type
        self.data = data
        self.priority = priority

class EventQueue:
    """带优先级的事件队列"""
    
    def __init__(self):
        # 为每个优先级创建一个队列
        self.queues = {
            EventPriority.HIGH: asyncio.Queue(),
            EventPriority.NORMAL: asyncio.Queue(),
            EventPriority.LOW: asyncio.Queue()
        }
        
    async def put(self, event):
        """放入事件"""
        await self.queues[event.priority].put(event)
        
    async def get(self):
        """获取最高优先级的事件"""
        for priority in [EventPriority.HIGH, EventPriority.NORMAL, EventPriority.LOW]:
            if not self.queues[priority].empty():
                return await self.queues[priority].get()
        # 所有队列都为空,等待
        return await self.queues[EventPriority.NORMAL].get()

注意:内存队列不是万能的。如果生产者的速度长期大于消费者的速度,队列会无限增长,最终导致内存溢出。我建议给队列设置最大长度,超过限制时丢弃低优先级的事件,或者触发降级策略。

任务调度:谁先谁后?

有了事件队列,接下来就是任务调度。说白了就是:这么多事件,先处理哪个?

做市引擎的任务调度,我一般分为三个层次:

调度层次 说明 实现方式
事件优先级调度 根据事件类型决定处理顺序 多级队列 + 优先级选择
策略级调度 多个做市策略之间的资源分配 时间片轮转或权重分配
交易所级调度 控制对交易所API的请求频率 令牌桶算法或滑动窗口

我个人最常用的是令牌桶算法来控制对交易所的请求频率。为什么?因为交易所都有API限流,你发太快了会被封。令牌桶可以平滑请求速率,既保证吞吐量,又不会触发限流。

import asyncio
import time

class TokenBucket:
    """令牌桶限流器"""
    
    def __init__(self, rate, capacity):
        """
        rate: 每秒生成的令牌数
        capacity: 桶的最大容量
        """
        self.rate = rate
        self.capacity = capacity
        self.tokens = capacity
        self.last_refill = time.monotonic()
        self.lock = asyncio.Lock()
        
    async def acquire(self):
        """获取一个令牌,如果没有则等待"""
        while True:
            async with self.lock:
                self._refill()
                if self.tokens >= 1:
                    self.tokens -= 1
                    return True
            # 等待一段时间再试
            await asyncio.sleep(0.001)
            
    def _refill(self):
        """补充令牌"""
        now = time.monotonic()
        elapsed = now - self.last_refill
        self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
        self.last_refill = now

# 使用示例
rate_limiter = TokenBucket(rate=10, capacity=20)  # 每秒10个请求,突发20个

async def send_order(exchange, order):
    await rate_limiter.acquire()
    return await exchange.place_order(order)

整体架构图

说了这么多,我们来画一张整体的架构图。这张图展示了事件从产生到被消费的完整流程:

做市引擎事件驱动架构 交易所层 Binance WebSocket OKX WebSocket Bybit WebSocket REST API 事件总线层 事件发布器 事件订阅管理器 事件序列化/反序列化 内存队列层 高优先级队列 普通优先级队列 低优先级队列 任务调度层 策略A调度器 策略B调度器 令牌桶限流器 超时管理器

这张图展示了数据流的完整路径:交易所的行情数据通过WebSocket进入事件总线,被封装成事件对象后放入不同优先级的内存队列,最后由任务调度器根据优先级和限流策略分发给各个策略处理器。

我的经验:刚开始做这个架构时,我犯了一个错误——把所有事件都放在一个队列里。结果有一次行情爆发,成交回报被堵在队列后面,导致订单状态更新延迟了整整2秒。后来改成多级优先级队列,这个问题就解决了。

总结

做市引擎的架构,核心就是三件事:

  • 事件驱动:让系统只在需要时工作,避免无谓的轮询
  • 异步IO:充分利用CPU时间,不让网络等待成为瓶颈
  • 内存队列+任务调度:解耦各个模块,让系统有弹性、可扩展

这个架构看起来简单,但实际落地时有很多细节。比如事件丢失怎么办?队列满了怎么处理?多个策略之间如何公平调度?这些我们会在后面的章节里逐一展开。

嗯,这一章就到这里。记住一句话:好的架构不是设计出来的,是踩坑踩出来的


专注资料整理