2、订单簿数据获取:交易所API对接(REST/WebSocket)
做量化交易,第一道坎就是怎么把订单簿数据搞到手。
我刚开始做这个的时候,以为不就是调个接口嘛,简单。结果一上手就踩坑——数据对不上、时间戳乱跳、增量更新丢了几个事件,整个订单簿就歪了。嗯,今天咱们就把这块彻底讲透。
2.1 REST vs WebSocket:怎么选?
交易所通常提供两种接口:REST和WebSocket。说白了,一个是你主动去问,一个是交易所主动告诉你。
| 特性 | REST | WebSocket |
|---|---|---|
| 数据获取方式 | 请求-响应 | 订阅-推送 |
| 延迟 | 高(100ms+) | 低(1-10ms) |
| 适合场景 | 历史数据、快照 | 实时行情、增量更新 |
| 连接开销 | 每次请求新建连接 | 长连接,持续推送 |
我个人习惯是:REST拿快照,WebSocket收增量。这样既保证了初始数据的完整性,又能拿到低延迟的实时更新。
小技巧: 启动时先用REST拉一次全量快照,然后订阅WebSocket增量流。这样本地订单簿就能快速对齐。
2.2 数据清洗与对齐
数据拿到手,别急着用。交易所返回的数据经常有坑。
我曾经遇到过一个情况:某交易所的卖一价突然跳了个负数。你想想看,价格还能是负的?明显是数据异常。所以清洗是必须的。
清洗规则我一般这么定:
- 价格检查: 价格必须大于0,不能是NaN或Infinity
- 数量检查: 数量必须大于等于0(有些交易所允许0表示撤单)
- 时间戳检查: 时间戳不能是未来的,也不能比上次更新还早
- 去重: 同一个价格位,如果连续收到相同数据,跳过
def clean_orderbook_update(data):
"""清洗订单簿增量数据"""
cleaned = []
for entry in data:
price = float(entry['price'])
size = float(entry['size'])
# 价格异常直接扔掉
if price <= 0 or price != price: # NaN检查
continue
# 数量为0表示删除该价位
if size < 0:
continue
cleaned.append({
'price': price,
'size': size,
'side': entry['side'],
'timestamp': entry['timestamp']
})
return cleaned
注意: 不同交易所的时间戳格式不一样。有的是毫秒,有的是微秒,还有的是字符串。对齐时一定要统一成一种格式,否则本地订单簿会乱套。
2.3 快照与增量更新
订单簿重建的核心逻辑就四个字:快照+增量。
流程是这样的:
- 通过REST获取当前全量快照(bids和asks列表)
- 订阅WebSocket增量流,每条增量包含:价格、数量、方向(买/卖)
- 收到增量后,在本地订单簿中找到对应价格位,更新数量
- 如果数量为0,删除该价位
这里有个关键点:增量更新的顺序必须严格按时间戳执行。我见过有人因为多线程处理,导致增量乱序,订单簿直接崩了。
核心原则: 增量更新是幂等的。同一个增量重复应用两次,结果应该不变。这样才能应对网络重连后的数据补发。
2.4 本地订单簿重建
本地订单簿说白了就是一个有序字典。买盘按价格降序排列,卖盘按价格升序排列。
我一般用红黑树或者跳表来实现,但Python里直接用sortedcontainers库就够用了。
from sortedcontainers import SortedDict
class OrderBook:
def __init__(self):
self.bids = SortedDict(lambda k: -k) # 买盘,价格降序
self.asks = SortedDict() # 卖盘,价格升序
def apply_snapshot(self, snapshot):
"""应用全量快照"""
self.bids.clear()
self.asks.clear()
for bid in snapshot['bids']:
self.bids[float(bid[0])] = float(bid[1])
for ask in snapshot['asks']:
self.asks[float(ask[0])] = float(ask[1])
def apply_update(self, update):
"""应用增量更新"""
price = float(update['price'])
size = float(update['size'])
side = update['side']
book = self.bids if side == 'buy' else self.asks
if size == 0:
# 删除该价位
if price in book:
del book[price]
else:
# 更新数量
book[price] = size
def get_top(self, n=10):
"""获取前n档行情"""
top_bids = list(self.bids.items())[:n]
top_asks = list(self.asks.items())[:n]
return {'bids': top_bids, 'asks': top_asks}
避坑指南: 我曾经因为快照和增量之间的时间差,导致本地订单簿少了几个价位。解决办法是:收到快照后,把快照时间戳之后的所有增量重新应用一遍。这叫「快照对齐」。
2.5 整体架构图
下面这张图展示了订单簿数据获取的完整流程。从交易所到本地重建,每一步都不能少。
你看,整个流程其实不复杂。但每个环节都有细节要注意。数据清洗别偷懒,增量顺序别搞乱,快照对齐别忘记。做到这三点,本地订单簿基本就稳了。
总结一下: 订单簿数据获取的核心就是「REST拿快照,WebSocket收增量,本地做合并」。这套模式几乎所有交易所都通用,学会了就能举一反三。
公众号:蓝海资料掘金营,微信deep3321