订单簿数据结构:Level 2 数据解析,Bid/Ask 队列
做市商的核心工作,说白了就是跟订单簿打交道。你每天盯着屏幕上跳动的数字,其实就是在看买卖双方的博弈。Level 2 数据,就是这场博弈的「底牌」——虽然看不到对手是谁,但能看到他们的挂单意图。
我个人习惯把订单簿比作一个「排队系统」。买方排一队,卖方排一队。谁的价格好,谁就排在前面。这个比喻虽然简单,但能帮你理解后面所有的逻辑。
什么是 Level 2 数据?
很多新手一开始只盯着「买一」「卖一」看,也就是所谓的 Level 1 数据。但做市商要是只看这个,那跟裸奔没什么区别。
Level 2 数据,展示的是订单簿的完整深度。它告诉你:
- 在每一个价格档位上,有多少人在等着买
- 在每一个价格档位上,有多少人在等着卖
- 这些订单是限价单还是市价单(部分交易所会区分)
举个例子,BTC/USDT 的订单簿可能是这样的:
| 价格 | Bid 数量 | Ask 数量 | 价格 |
|---|---|---|---|
| 30000 | 5.2 BTC | 3.1 BTC | 30001 |
| 29999 | 8.7 BTC | 4.5 BTC | 30002 |
| 29998 | 12.3 BTC | 6.8 BTC | 30003 |
你看,左边是买单队列(Bid),右边是卖单队列(Ask)。每个价格档位上的挂单量,就是所谓的「深度」。
Bid/Ask 队列的结构
队列的本质,就是一个按价格排序的列表。买单从高到低排,卖单从低到高排。为什么?因为买方当然希望用更低的价格买,但为了成交,他必须出价更高才能排到前面。反过来,卖方希望卖得更贵,但为了成交,他必须降价。
嗯,这里要注意:队列里的每个「档位」,其实是一个聚合数据。交易所会把同一价格的所有订单合并成一个「档位」。你看到的 5.2 BTC,可能是 10 个订单的总和,也可能是 1 个大户的挂单。
我在项目中遇到过这样的情况:某个价格档位突然出现巨量挂单,看起来像是「支撑位」。结果一查,是某个机构在拆单挂单,故意制造假象。所以,只看总量是不够的,还要看订单的「微观结构」。
如何解析 Level 2 数据?
不同的交易所,数据格式略有不同。但核心逻辑是一样的。我们以 Binance 的 WebSocket 数据为例:
{
"e": "depthUpdate", // 事件类型
"E": 123456789, // 事件时间
"s": "BTCUSDT", // 交易对
"U": 157, // 第一个更新ID
"u": 160, // 最后一个更新ID
"b": [ // 买单队列
["30000", "5.2"], // [价格, 数量]
["29999", "8.7"]
],
"a": [ // 卖单队列
["30001", "3.1"],
["30002", "4.5"]
]
}
解析的时候,我建议你注意两点:
- 增量更新 vs 全量快照:大部分交易所会先给你一个全量快照,然后推送增量更新。增量更新只告诉你「哪个价格变了,变成多少」。你需要自己维护一个本地的订单簿副本。
- 更新ID的连续性:每个更新都有一个 ID。如果发现 ID 不连续,说明你漏掉了数据。这时候必须重新请求全量快照,否则你的本地订单簿会「漂移」。
队列的「微观特征」
做市商不仅要看队列的「静态分布」,还要看它的「动态变化」。我个人总结了几个关键指标:
- 队列斜率:从买一到买五,挂单量的变化趋势。如果斜率很陡,说明流动性集中在某个价格附近。
- 队列厚度:某个价格区间内的总挂单量。厚度越大,价格越难突破。
- 队列更新频率:如果某个档位的挂单量频繁变化,说明有人在「试盘」或者「撤单钓鱼」。
你想想看,如果一个价格档位上的挂单量突然从 10 BTC 变成 0.1 BTC,然后又变回 10 BTC,这正常吗?大概率是有人在「挂单诱多」或者「挂单诱空」。做市商要是跟着这种假信号走,很容易被割韭菜。
用 Python 构建本地订单簿
下面是一个简单的实现思路。我习惯用字典来存储订单簿,价格作为 key,数量作为 value:
class OrderBook:
def __init__(self):
self.bids = {} # 买单队列
self.asks = {} # 卖单队列
def apply_snapshot(self, snapshot):
"""应用全量快照"""
self.bids = {float(price): float(qty)
for price, qty in snapshot['b']}
self.asks = {float(price): float(qty)
for price, qty in snapshot['a']}
def apply_update(self, update):
"""应用增量更新"""
for price, qty in update['b']:
price = float(price)
qty = float(qty)
if qty == 0:
# 数量为0,说明这个档位被撤单了
self.bids.pop(price, None)
else:
self.bids[price] = qty
for price, qty in update['a']:
price = float(price)
qty = float(qty)
if qty == 0:
self.asks.pop(price, None)
else:
self.asks[price] = qty
def get_top_n(self, n=5):
"""获取前N档深度"""
sorted_bids = sorted(self.bids.items(),
reverse=True)[:n]
sorted_asks = sorted(self.asks.items())[:n]
return sorted_bids, sorted_asks
这段代码看起来简单,但实际生产环境中要考虑的事情很多。比如:
- 数据延迟:WebSocket 推送的数据可能有几十毫秒的延迟
- 内存管理:如果交易对很多,订单簿会占用大量内存
- 并发安全:多个线程同时更新订单簿时,要加锁
订单簿的「可视化」
光看数字是不够的。我建议你把订单簿画出来,直观感受一下市场的「呼吸」。下面这张图展示了典型的订单簿结构:
这张图里,每个档位的「宽度」代表了挂单量的大小。你看买一只有 5.2 BTC,但买三有 12.3 BTC。这说明在 29998 这个价位上,有更强的支撑。如果价格跌到 29998,可能会遇到大量买单「接盘」。
反过来看卖单,卖三有 6.8 BTC,是卖单里最厚的。这说明 30003 是一个重要的「阻力位」。做市商在做策略时,这些位置就是天然的「止损点」和「止盈点」。
总结一下
Level 2 数据解析,说白了就是两件事:
- 把交易所推送的原始数据,转换成你本地能用的订单簿结构
- 从订单簿的「静态分布」和「动态变化」中,提取出有交易价值的信号
我个人觉得,做市商的核心竞争力不在于算法有多复杂,而在于对订单簿的理解有多深。你能从队列的细微变化中读出市场的「情绪」,那你就能比别人早一步行动。
嗯,今天就先聊到这里。记住,订单簿是你的「战场地图」,别只看地图上的「山川河流」,还要看「风吹草动」。