2. 订单簿数据获取:Level 1、Level 2、Level 3数据的区别,交易所API对接(以Binance为例),数据清洗与对齐
做量化交易,第一关就是数据。没有数据,再牛的策略也是空中楼阁。
我个人习惯把订单簿数据比作「市场的X光片」。Level 1、Level 2、Level 3,就是不同清晰度的片子。你想想看,普通玩家看个大概,专业玩家要看到肌肉纹理,做市商甚至要看到毛细血管。
2.1 三个Level,到底差在哪?
先别急着写代码。咱们得先搞清楚,交易所到底给了我们什么。
| 数据层级 | 包含内容 | 典型用途 | 数据量级 |
|---|---|---|---|
| Level 1 (L1) | 最优买卖价(Best Bid/Ask)、最新成交价、24h成交量 | 散户看盘、简单指标计算 | 极低,每秒几次 |
| Level 2 (L2) | 前N档(通常5-50档)买卖挂单的价格与数量 | 订单簿形态分析、买卖压力测算 | 中等,每秒几十到几百次 |
| Level 3 (L3) | 全量订单簿,包含每个挂单的订单ID、时间戳 | 做市商策略、高频套利、订单流分析 | 极高,每秒数千到数万次 |
说白了,L1就是告诉你「现在有人愿意用100块买,101块卖」。L2告诉你「100块有10个币要买,99块有50个币要买...」。L3则告诉你「订单ID为123456的张三在100.01挂了1个币,订单ID为123457的李四...」。
核心区别: L1是快照,L2是有限深度的快照,L3是完整的增量流。做买卖压力测算,L2是起步门槛,L3是进阶武器。
2.2 交易所API对接:以Binance为例
我在项目中遇到过不少新手,一上来就对着文档猛抄代码,结果连WebSocket和REST都分不清。嗯,这里要注意,两者用途完全不同。
- REST API: 拉取静态数据,比如历史K线、当前订单簿快照。适合初始化。
- WebSocket: 订阅实时数据流,比如订单簿增量更新。适合持续监控。
我个人建议,做订单簿分析,一定要用WebSocket。为什么?因为REST拉取的快照,在你拿到手的那一刻就已经过时了。你想想看,高频交易的世界里,毫秒级的延迟都可能让你亏钱。
下面是一个Binance L2订单簿WebSocket订阅的示例。代码不长,但坑不少。
import asyncio
import json
import websockets
# Binance 现货市场 WebSocket 地址
BINANCE_WS_URL = "wss://stream.binance.com:9443/ws"
async def subscribe_depth(symbol="btcusdt", level=20):
"""
订阅订单簿深度数据
level: 5, 10, 20 等,Binance L2 最多支持 1000 档
"""
# 构建订阅参数
stream_name = f"{symbol}@depth{level}@100ms"
subscribe_msg = {
"method": "SUBSCRIBE",
"params": [stream_name],
"id": 1
}
async with websockets.connect(BINANCE_WS_URL) as ws:
# 发送订阅请求
await ws.send(json.dumps(subscribe_msg))
# 等待订阅确认
response = await ws.recv()
print(f"订阅确认: {response}")
# 持续接收数据
while True:
data = await ws.recv()
# 这里先不做处理,直接打印
print(f"收到数据: {data[:200]}...")
# 运行
# asyncio.run(subscribe_depth("btcusdt", 20))
避坑指南: 我曾经在Binance的文档里踩过一个坑——它的L2数据更新频率是100ms一次,但如果你订阅的档位太多(比如1000档),实际推送频率可能会降低。所以,别贪多,够用就行。做买卖压力测算,20档通常就足够了。
2.3 数据清洗与对齐:脏活累活,但必须干
数据拿到手了,你以为就能直接用了吗?太天真了。交易所推过来的数据,就像没洗过的土豆,上面全是泥。
常见的「泥」有哪些?
- 乱序: WebSocket数据包可能后发先至。你收到的第100个数据包,时间戳可能比第99个还早。
- 重复: 网络抖动可能导致同一个事件被推送两次。
- 缺失: 偶尔会丢包,导致订单簿状态不一致。
- 快照与增量不匹配: 这是最要命的。你拿增量去更新一个旧快照,结果订单簿对不上了。
怎么解决?我有一套固定的流程,分享给你。
- 建立本地订单簿: 先通过REST API拉取一个完整的快照,作为基准。
- 维护事件队列: 所有WebSocket过来的增量事件,先按时间戳排好队,不急着处理。
- 校验与去重: 检查每个事件的`lastUpdateId`或`U`字段,确保它是连续的。跳号了?说明丢了数据,重新拉取快照。
- 应用增量: 按顺序把增量事件应用到本地订单簿上。更新、插入、删除,一个都不能错。
- 定期校验: 每隔一段时间(比如5分钟),重新拉取一次快照,跟本地订单簿做对比。不一致?立即重置。
警告: 千万别跳过「校验」这一步。我曾经有一次因为网络波动,本地订单簿跟交易所差了整整3档,结果策略连续下了5笔废单,亏了2000 USDT。从那以后,我再也不敢偷懒了。
下面是一个简单的数据对齐逻辑示例,展示了如何处理增量更新。
class OrderBook:
def __init__(self, snapshot):
# 初始化:从REST快照构建本地订单簿
self.bids = {price: qty for price, qty in snapshot['bids']}
self.asks = {price: qty for price, qty in snapshot['asks']}
self.last_update_id = snapshot['lastUpdateId']
def apply_update(self, update):
"""
应用增量更新
update 结构: {'bids': [[price, qty], ...], 'asks': [[price, qty], ...], 'U': ..., 'u': ...}
"""
# 校验:确保增量是连续的
if update['U'] != self.last_update_id + 1:
print("数据不连续!需要重新拉取快照")
return False
# 更新买单
for price, qty in update['bids']:
qty = float(qty)
if qty == 0:
# 数量为0,表示删除该价位
self.bids.pop(price, None)
else:
self.bids[price] = qty
# 更新卖单(逻辑同上)
for price, qty in update['asks']:
qty = float(qty)
if qty == 0:
self.asks.pop(price, None)
else:
self.asks[price] = qty
# 更新最后ID
self.last_update_id = update['u']
return True
这段代码看起来简单,但实际生产环境中,你还要处理并发、内存管理、数据持久化等问题。不过,核心逻辑就是这个——校验、更新、再校验。
2.4 本章知识体系总览
为了让你更直观地理解整个数据获取流程,我画了一张图。它把从交易所到本地订单簿的完整链路串起来了。
这张图把整个流程分成了六个阶段。从交易所出发,经过数据获取、原始数据流、清洗对齐,最终构建出可靠的本地订单簿,然后才能用于买卖压力测算。每一步都有坑,每一步都值得你花时间去打磨。
好了,数据获取这块就聊到这儿。记住,数据质量决定了策略的天花板。别在数据上省钱,也别在清洗上偷懒。这是我从亏钱经历里换来的教训,希望你能少走弯路。