4. 订单簿管理模块:订单簿深度维护、买卖盘口价差计算、订单簿不平衡度指标、订单簿快照与增量更新

做市商系统里,订单簿管理模块可以说是最核心的「心脏」了。我刚开始做量化交易那会儿,总觉得订单簿不就是一堆买单卖单排排队嘛,有什么难的?直到有一次实盘环境里,因为订单簿更新延迟了 200 毫秒,导致我们做市策略报了一堆废单……嗯,从那以后,我对这个模块的敬畏心就上来了。

说白了,订单簿管理模块要干四件事:维护深度、算价差、算不平衡度、处理快照与增量。咱们一个一个来拆解。

4.1 订单簿深度维护

订单簿深度,就是看某个价格水平上到底有多少挂单。我习惯用一个价格-数量映射表来维护,买盘和卖盘各一张表。

举个例子,BTC/USDT 的买盘可能是这样的:

价格 (USDT) 数量 (BTC) 累计深度
30000 1.5 1.5
29990 2.0 3.5
29980 0.8 4.3

这里有个坑:价格精度问题。不同交易所的价格精度不一样,有的支持小数点后两位,有的支持八位。我曾经因为精度没对齐,导致订单簿里同一个价格出现了两条记录,直接让价差计算崩了。

避坑指南: 维护订单簿深度时,一定要用 Decimal 类型,别用 float。float 的精度问题在价格计算上会要人命。

4.2 买卖盘口价差计算

价差(Spread)就是最优卖价和最优买价的差值。公式很简单:

spread = ask_price_1 - bid_price_1

但实际项目中,我一般会算三个指标:

  • 绝对价差:就是上面那个差值,单位是 USDT 或 BTC
  • 相对价差:绝对价差 / 中间价,用百分比表示
  • 加权价差:考虑深度后的价差,比如前 5 档的平均价差

为什么要算加权价差?你想想看,如果最优档只有 0.001 个 BTC,那这个价差其实没什么参考价值。真正能成交的,往往是前几档的加权结果。

我个人习惯: 在监控面板上同时展示绝对价差和相对价差。绝对价差看绝对值变化,相对价差看市场流动性变化。如果相对价差突然放大,往往意味着市场要出幺蛾子了。

4.3 订单簿不平衡度指标

这个指标很有意思,它能告诉你当前市场的买卖力量对比。我常用的公式是:

imbalance = (bid_volume - ask_volume) / (bid_volume + ask_volume)

结果范围在 -1 到 1 之间:

  • 接近 1:买盘远大于卖盘,市场看涨情绪浓
  • 接近 -1:卖盘远大于买盘,市场看跌情绪浓
  • 接近 0:买卖力量均衡,市场处于震荡

但这里要注意:不平衡度要看深度档位。只看最优档容易误判。我记得有一次,最优买盘只有 0.5 BTC,但第二档到第五档加起来有 50 BTC,这时候只看最优档就会得出「买盘弱」的错误结论。

所以我一般会算三个维度的不平衡度:

  1. 第 1 档不平衡度:反应最直接的买卖意愿
  2. 前 5 档不平衡度:反应短期流动性分布
  3. 全深度不平衡度:反应整体市场情绪
小技巧: 把不平衡度做成时间序列曲线,配合价格走势一起看。你会发现,价格拐点往往出现在不平衡度极端值之后。

4.4 订单簿快照与增量更新

这是订单簿管理里最考验性能的部分。交易所通常提供两种数据:

  • 快照(Snapshot):某一时刻的完整订单簿
  • 增量(Incremental):订单簿的变化事件(新增、删除、修改)

我建议的架构是:以快照为基准,用增量做实时更新。具体流程如下:

1. 首次连接:获取全量快照,建立初始订单簿
2. 持续接收:增量事件流,逐条更新订单簿
3. 定期校验:每隔 N 秒或 N 条增量后,重新获取快照做对比
4. 异常恢复:发现不一致时,丢弃当前订单簿,重新拉取快照

这里有个关键点:增量事件的顺序不能乱。如果先处理了「删除」事件,再处理「新增」事件,但实际顺序是反的,那订单簿就乱套了。我一般会用队列加时间戳来保证顺序。

我曾经踩过的坑: 增量事件里有个「修改」操作,但有些交易所的修改操作是「先删后增」的原子操作。如果没处理好,会导致同一价格出现两条记录。解决方案是:收到修改事件时,先按价格+方向查找,如果存在就更新数量,不存在就新增。

4.5 整体架构图

下面这张图是我自己项目里用的订单簿管理模块架构,你可以参考一下:

订单簿管理模块架构图 交易所数据源 WebSocket / REST API 数据预处理 去重 / 排序 / 校验 订单簿核心引擎 快照管理 / 增量更新 深度维护 / 状态同步 价差计算模块 绝对价差 / 相对价差 加权价差 / 实时监控 不平衡度计算模块 多档位不平衡度 时间序列 / 异常检测 深度维护模块 价格-数量映射表 累计深度 / 分层管理 输出接口 实时监控面板 | 告警系统 | 策略引擎 | 日志存储

从架构图可以看出,数据从交易所进来后,先做预处理,然后交给核心引擎维护订单簿状态。三个计算模块并行工作,最后统一输出到监控、告警和策略系统。

4.6 代码示例:订单簿核心类

下面是一个简化版的订单簿管理类,包含了核心功能:

class OrderBook:
    def __init__(self, symbol):
        self.symbol = symbol
        self.bids = {}  # 买盘: {price: volume}
        self.asks = {}  # 卖盘: {price: volume}
        self.last_snapshot_id = 0
        
    def apply_snapshot(self, snapshot):
        """应用全量快照"""
        self.bids = {item[0]: item[1] for item in snapshot['bids']}
        self.asks = {item[0]: item[1] for item in snapshot['asks']}
        self.last_snapshot_id = snapshot['id']
        
    def apply_incremental(self, events):
        """应用增量事件"""
        for event in events:
            price = event['price']
            volume = event['volume']
            side = event['side']  # 'bid' or 'ask'
            
            if volume == 0:
                # 删除该价格档位
                if side == 'bid':
                    self.bids.pop(price, None)
                else:
                    self.asks.pop(price, None)
            else:
                # 新增或更新
                if side == 'bid':
                    self.bids[price] = volume
                else:
                    self.asks[price] = volume
                    
    def get_spread(self):
        """计算买卖价差"""
        if not self.bids or not self.asks:
            return None
        best_bid = max(self.bids.keys())
        best_ask = min(self.asks.keys())
        return best_ask - best_bid
        
    def get_imbalance(self, levels=5):
        """计算不平衡度"""
        bid_vol = sum(sorted(self.bids.values(), reverse=True)[:levels])
        ask_vol = sum(sorted(self.asks.values())[:levels])
        total = bid_vol + ask_vol
        if total == 0:
            return 0
        return (bid_vol - ask_vol) / total
提示: 实际生产环境中,建议用红黑树或跳表来维护价格排序,而不是每次都排序。Python 里可以用 sortedcontainers 库,性能会好很多。

好了,订单簿管理模块的核心内容就这些。记住一句话:订单簿是市场的镜子,维护好这面镜子,你的做市策略才能看清路


专注资料整理