3、订单簿数据结构:使用Python字典/列表模拟订单簿、价格-数量映射关系。
做量化交易这几年,我每天打交道最多的数据结构就是订单簿。说白了,订单簿就是交易所里那个实时滚动的「挂单大屏」——左边是卖单,右边是买单,价格从高到低排得整整齐齐。
你想想看,一个订单簿里最核心的信息是什么?其实就是两样东西:价格和数量。谁在什么价位上挂了多少钱,这就是全部秘密。
3.1 订单簿的核心结构
我个人习惯把订单簿拆成三个部分来理解:
- 买盘(Bids):按价格从高到低排列,最高价在最前面
- 卖盘(Asks):按价格从低到高排列,最低价在最前面
- 时间戳:记录当前快照的时刻
为什么买盘要降序、卖盘要升序?因为买方当然希望用最低价买,但挂单时得按市场规则来——想立刻成交就得挂高价。所以买盘最高价就是「最佳买价」,卖盘最低价就是「最佳卖价」。这个价差,就是咱们常说的 spread。
3.2 用Python字典模拟订单簿
Python里最直观的模拟方式,就是用字典做价格-数量的映射。我在项目中遇到过好几次,新手喜欢用列表存价格和数量,结果查价的时候慢得不行。字典的哈希查找,才是正经路子。
来看一个最简单的例子:
# 模拟一个BTC/USDT的订单簿快照
order_book = {
'bids': { # 买盘:价格 -> 数量
50000.0: 1.5,
49990.0: 2.3,
49980.0: 0.8,
49970.0: 3.1,
49960.0: 1.2
},
'asks': { # 卖盘:价格 -> 数量
50010.0: 0.9,
50020.0: 1.7,
50030.0: 2.5,
50040.0: 1.1,
50050.0: 3.0
},
'timestamp': 1700000000
}
嗯,这里要注意:字典的键是价格,值是挂单数量。为什么用价格做键?因为订单簿的核心操作就是「查某个价位的挂单量」,字典的O(1)查询速度刚好满足。
3.3 价格-数量映射关系的设计要点
我曾经踩过一个坑:直接用浮点数做字典键。浮点数在计算机里是不精确的,比如 50000.1 和 50000.10000000001 会被当成两个不同的键。后来我统一用整数表示价格,比如把价格乘以10000存成整数,或者直接用Decimal类型。
这里给出一个更健壮的实现:
from decimal import Decimal
class OrderBook:
def __init__(self):
self.bids = {} # 买盘
self.asks = {} # 卖盘
self.last_update = None
def update(self, side: str, price: float, quantity: float):
"""更新订单簿"""
# 用Decimal避免浮点精度问题
p = Decimal(str(price))
q = Decimal(str(quantity))
if side == 'buy':
if q == 0:
self.bids.pop(p, None) # 数量为0表示撤单
else:
self.bids[p] = q
elif side == 'sell':
if q == 0:
self.asks.pop(p, None)
else:
self.asks[p] = q
self.last_update = time.time()
def get_best_bid(self) -> tuple:
"""获取最佳买价"""
if not self.bids:
return None
best_price = max(self.bids.keys())
return (best_price, self.bids[best_price])
def get_best_ask(self) -> tuple:
"""获取最佳卖价"""
if not self.asks:
return None
best_price = min(self.asks.keys())
return (best_price, self.asks[best_price])
3.4 订单簿的深度聚合
光有价格-数量映射还不够,咱们经常需要看「累计深度」——比如在某个价格以下总共挂了多少买单。这个在计算市场冲击成本时特别有用。
举个例子:你想买入10个BTC,不能只看最佳卖价那0.9个,得把后面几个价位的单子都吃掉。这时候就需要算累计量:
def calculate_depth(order_book: dict, side: str, levels: int = 5):
"""计算指定深度的累计挂单量"""
if side == 'bids':
prices = sorted(order_book[side].keys(), reverse=True)
else:
prices = sorted(order_book[side].keys())
cumulative = 0
depth_data = []
for i, price in enumerate(prices[:levels]):
qty = order_book[side][price]
cumulative += qty
depth_data.append({
'price': price,
'quantity': qty,
'cumulative': cumulative
})
return depth_data
# 使用示例
depth = calculate_depth(order_book, 'asks', levels=3)
for level in depth:
print(f"价格: {level['price']:.2f}, "
f"数量: {level['quantity']:.4f}, "
f"累计: {level['cumulative']:.4f}")
3.5 订单簿的可视化结构
为了让你更直观地理解,我画了一张订单簿的结构图。这张图展示了买盘和卖盘如何构成一个完整的市场深度:
3.6 实战:从交易所API获取订单簿
光说不练假把式。咱们用币安的公开API拉一份真实的订单簿看看:
import requests
import json
def fetch_order_book(symbol: str, limit: int = 10):
"""从币安获取订单簿数据"""
url = f"https://api.binance.com/api/v3/depth"
params = {
'symbol': symbol.upper(),
'limit': limit
}
response = requests.get(url, params=params)
data = response.json()
# 转换成咱们的字典格式
order_book = {
'bids': {},
'asks': {},
'timestamp': data['lastUpdateId']
}
# 注意:交易所返回的是字符串,需要转成数值
for bid in data['bids']:
price = float(bid[0])
qty = float(bid[1])
if qty > 0: # 忽略空单
order_book['bids'][price] = qty
for ask in data['asks']:
price = float(ask[0])
qty = float(ask[1])
if qty > 0:
order_book['asks'][price] = qty
return order_book
# 拉取BTC/USDT的订单簿
book = fetch_order_book('btcusdt', limit=5)
print(f"最佳买价: {max(book['bids'].keys()):.2f}")
print(f"最佳卖价: {min(book['asks'].keys()):.2f}")
print(f"买盘数据: {book['bids']}")
print(f"卖盘数据: {book['asks']}")
运行这段代码,你就能看到真实的订单簿长什么样了。你会发现,真实市场的挂单量分布其实很不均匀——有些价位上堆着大单,有些价位上空空如也。这些信息,就是咱们做策略的「矿藏」。
好了,这一章咱们把订单簿的数据结构讲透了。从字典模拟到API实战,从价格映射到深度聚合,这些都是后续分析的基础。记住一句话:订单簿就是市场的「心电图」,读懂它,你就能感知到资金的流向和博弈的节奏。