订单簿数据结构:从原始数据到交易信号的核心

做市策略的核心是什么?说白了,就是读懂订单簿。我做了这么多年量化,见过太多人把精力花在策略参数调优上,结果连订单簿的基本结构都没搞明白。嗯,今天我们就来把这个基础打扎实。

订单簿的构成要素

订单簿,你可以把它想象成一个实时更新的拍卖现场。买家和卖家都在喊价,只不过这个现场是数字化的。

一个标准的订单簿包含以下几个核心要素:

  • 价格档位(Price Level):每个价格就是一个档位,比如3.250、3.251这样
  • 委托量(Volume):该价格上挂着的总订单数量
  • 订单数量(Order Count):该价格上有多少个独立的订单
  • 买卖方向(Side):买单(Bid)还是卖单(Ask)
  • 层级深度(Depth Level):从最优价格开始算,第几档

关键点:最优买卖价(Best Bid & Best Ask)之间的价差,就是我们常说的Spread。这是做市商吃饭的本钱。

我在项目中遇到过一个问题:有些交易所会把同价格的不同订单合并显示,有些则分开。这个细节会直接影响你计算订单簿深度的方式。千万别想当然。

Level2行情数据解析

Level2行情,说白了就是比普通行情更细的数据。普通行情只给你看最优的5档或10档,Level2能给你看全量数据。

常见的Level2数据格式有两种:

数据格式 特点 常见交易所
增量快照(Snapshot + Incremental) 先发全量快照,后续只发变化 Binance、OKX
全量推送(Full Order Book) 每次推送都是完整订单簿 部分传统交易所

我个人习惯用增量快照模式。为什么?因为数据量小,延迟低。你想想看,如果每秒推送100次全量数据,光网络带宽就够你受的。

解析Level2数据时,有几个坑要注意:

  • 时间戳对齐:交易所的时间戳和你的本地时间可能有偏差
  • 序列号校验:确保你没有漏掉任何一条增量消息
  • 数据校验:我见过有些交易所偶尔会推送错误数据,需要做合法性检查

避坑指南:我曾经因为没做序列号校验,导致订单簿数据错乱了整整3分钟。那3分钟里,策略一直在用错误的数据做决策,亏了不少钱。从那以后,序列号校验成了我的标配。

订单簿的更新与维护

拿到数据后,怎么维护一个实时准确的订单簿?这里我分享一个我常用的数据结构:

class OrderBook:
    def __init__(self):
        self.bids = {}  # 价格 -> 委托量
        self.asks = {}  # 价格 -> 委托量
        self.best_bid = None
        self.best_ask = None
        self.sequence = 0
    
    def update(self, data):
        # 增量更新逻辑
        for change in data['changes']:
            price = change['price']
            volume = change['volume']
            side = change['side']
            
            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
        
        # 更新最优价格
        self.best_bid = max(self.bids.keys()) if self.bids else None
        self.best_ask = min(self.asks.keys()) if self.asks else None

这个代码看起来简单,但实际生产环境中要考虑的东西多着呢。比如:

  • 内存管理:全量订单簿可能有几千个价格档位,用字典存储要注意内存
  • 排序效率:每次更新后都要重新找最优价格,用堆(Heap)结构会更高效
  • 并发安全:如果你的策略是多线程的,记得加锁

小技巧:我建议用SortedDict或者跳表(Skip List)来维护订单簿。这样查找最优价格的时间复杂度能从O(n)降到O(log n)。对于高频场景,这个优化很关键。

订单簿的深度聚合

深度聚合,就是把原始订单簿数据加工成更有用的信息。比如:

  • 累计深度:从最优价格开始,累计到第N档的总委托量
  • 加权价格:考虑委托量后的平均价格
  • 买卖压力比:买单总深度 vs 卖单总深度
  • 价格冲击成本:吃掉一定量订单需要滑多少点

为什么要做深度聚合?你想想看,原始订单簿里几千个价格档位,你不可能全用上。我们需要提炼出关键特征,才能生成交易信号。

下面是我常用的一个深度聚合函数:

def aggregate_depth(order_book, levels=10):
    """
    聚合订单簿深度
    levels: 聚合的档位数
    """
    bid_depth = []
    ask_depth = []
    
    # 按价格排序
    sorted_bids = sorted(order_book.bids.items(), reverse=True)
    sorted_asks = sorted(order_book.asks.items())
    
    cum_bid_vol = 0
    cum_ask_vol = 0
    
    for i in range(min(levels, len(sorted_bids))):
        price, vol = sorted_bids[i]
        cum_bid_vol += vol
        bid_depth.append({
            'price': price,
            'volume': vol,
            'cum_volume': cum_bid_vol
        })
    
    for i in range(min(levels, len(sorted_asks))):
        price, vol = sorted_asks[i]
        cum_ask_vol += vol
        ask_depth.append({
            'price': price,
            'volume': vol,
            'cum_volume': cum_ask_vol
        })
    
    return bid_depth, ask_depth

这个函数返回的数据,可以直接用来计算各种指标。比如买卖压力比:

def pressure_ratio(bid_depth, ask_depth):
    total_bid = bid_depth[-1]['cum_volume']
    total_ask = ask_depth[-1]['cum_volume']
    return total_bid / total_ask if total_ask > 0 else float('inf')

实战经验:我在做市策略中,最喜欢用的一个指标是「深度斜率」。它反映了订单簿在不同价格区间的流动性分布。斜率陡峭的地方,说明流动性集中,做市风险小;斜率平缓的地方,说明流动性分散,做市要小心。

最后,我画了一张图来总结订单簿数据结构的核心逻辑:

订单簿数据结构核心逻辑 交易所Level2行情 数据解析:快照 + 增量更新 订单簿核心数据结构 Bids字典 | Asks字典 | 最优价格 | 序列号 深度聚合:累计深度 | 买卖压力比 交易信号生成 注意事项 • 序列号校验 • 时间戳对齐 • 内存管理 • 并发安全 • 数据合法性 • 排序效率

这张图把整个流程串起来了。从交易所拿到原始数据,经过解析、维护、聚合,最后生成交易信号。每一步都有坑,每一步都需要优化。

好了,订单簿数据结构这块就讲到这里。记住一句话:订单簿是你的眼睛,数据质量决定了你能不能看清市场。下一章我们会聊如何基于订单簿数据生成具体的做市信号。