第一章:订单簿重建与维护

做市系统的核心,说白了就是一本「电子账本」——订单簿。

你想想看,每秒几万笔交易进来,订单簿要是乱了,整个系统就废了。我当年刚入行时,就遇到过订单簿数据不一致导致爆仓的事故。嗯,从那以后,我对订单簿重建这块特别较真。

1.1 从撮合引擎日志重建订单簿

交易所的撮合引擎会输出日志,记录每一笔成交、每一个撤单。我们要做的,就是把这些日志「回放」一遍,还原出当时的订单簿状态。

我个人习惯用「事件驱动」的方式来做这件事。每个日志条目就是一个事件,按时间顺序依次处理。

核心思路: 日志是「增量」的,订单簿是「全量」的。用增量事件拼出全量快照。

举个例子,日志里可能有这么几种事件:

  • NEW_ORDER — 新订单进来
  • CANCEL_ORDER — 订单撤销
  • TRADE — 订单成交(部分或全部)
  • AMEND_ORDER — 修改订单(价格或数量)

重建时,我维护一个内存里的 OrderBook 对象。每来一个事件,就更新它。代码大概长这样:

class OrderBookReconstructor:
    def __init__(self):
        self.bids = {}  # 价格 -> 订单列表
        self.asks = {}
    
    def apply_event(self, event):
        if event.type == 'NEW_ORDER':
            self._add_order(event)
        elif event.type == 'CANCEL_ORDER':
            self._remove_order(event.order_id)
        elif event.type == 'TRADE':
            self._reduce_order(event.order_id, event.quantity)
        # ... 其他事件类似

这里有个坑:日志可能乱序。比如撤单事件比下单事件先到。我曾经因为这个吃过亏,后来加了个「事件缓冲队列」,等所有事件按时间戳排好序再处理。

注意: 日志时间戳必须是单调递增的。如果交易所的时钟不同步,你重建出来的订单簿可能跟真实情况差几毫秒。做市策略对这几毫秒很敏感。

1.2 增量更新机制

全量重建太慢了。你想想看,如果每次重启都要回放几亿条日志,黄花菜都凉了。

所以我们需要「增量更新」。说白了就是:只记录变化的部分,不重复计算不变的部分。

我常用的做法是维护一个「增量快照链」:

  1. 每 N 毫秒生成一个全量快照
  2. 两次快照之间,只记录增量事件
  3. 恢复时,加载最近的全量快照,再回放之后的增量事件

举个例子:

时间 操作 说明
T0 全量快照 保存当前所有订单
T0 ~ T1 增量事件 只记录新增/撤销/成交
T1 全量快照 再次保存全量

恢复时,加载 T1 的快照,然后回放 T1 之后的增量事件。这样速度能快几十倍。

小技巧: 快照不要存得太频繁。我一般设 1 秒一次。太频繁了浪费 IO,太稀疏了恢复慢。你可以根据自己系统的吞吐量调这个参数。

1.3 快照与回放技术

快照怎么存?我见过三种方案:

  • 内存快照:直接序列化内存里的订单簿对象。快,但重启就没了。
  • 文件快照:写到磁盘上。慢一点,但持久化。
  • 数据库快照:存到关系型数据库里。最慢,但方便查询。

我个人习惯用「内存 + 文件」双写。内存快照用于快速恢复,文件快照用于容灾。你想想看,万一机器断电了,内存快照就没了,但文件快照还在。

回放技术这块,有个关键点:回放速度要可控

我做过一个回放引擎,支持三种模式:

  1. 实时回放:按原始时间戳间隔回放,用于模拟真实环境
  2. 加速回放:跳过空闲期,只处理有事件的时间段
  3. 全速回放:不等待,一口气处理完所有事件

代码实现上,我用了「生产者-消费者」模式:

class ReplayEngine:
    def __init__(self, events, mode='realtime'):
        self.events = events
        self.mode = mode
        self.current_index = 0
    
    def run(self):
        while self.current_index < len(self.events):
            event = self.events[self.current_index]
            self._apply_event(event)
            
            if self.mode == 'realtime':
                time.sleep(event.interval)  # 按原始间隔等待
            elif self.mode == 'fast':
                # 跳过空闲期
                pass
            # 'fullspeed' 模式不等待
            
            self.current_index += 1

这里有个经验:全速回放时,CPU 占用会很高。我建议在回放线程里加个 time.sleep(0),让出 CPU 给其他线程。

避坑指南: 我曾经在回放时忘了处理「重复事件」。比如日志里同一个订单被记录了两次。结果订单簿里出现了重复的订单,导致撮合逻辑出错。后来我加了个「去重缓存」,用订单 ID 做 key,确保每个事件只处理一次。

1.4 知识体系总览

下面这张图,是我自己画的订单簿重建与维护的整体流程。你看一眼就能明白各个模块的关系:

订单簿重建与维护知识体系 撮合引擎日志 事件解析与排序 增量更新机制 快照生成 事件缓冲队列 去重缓存 内存快照 文件快照 订单簿重建完成

从这张图你能看到,整个流程是:日志进来 → 解析排序 → 分两条路走(增量更新 + 快照)→ 最终合并重建出完整的订单簿。

嗯,第一章的内容就到这。订单簿重建是做市系统的地基,地基不稳,上层策略再牛也白搭。我见过太多团队在这上面栽跟头了。


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