4、订单簿数据结构:订单簿的构成(bids/asks)、深度图绘制、订单簿快照与增量更新。
做市交易的核心,说白了就是跟订单簿打交道。你每天盯着屏幕看的那一列列价格和数量,背后就是订单簿在支撑。我个人习惯把订单簿比作一个「实时拍卖场」——买单和卖单在这里不断碰撞,最终形成成交。
4.1 订单簿的构成:bids 和 asks
订单簿由两部分组成:bids(买单)和asks(卖单)。bids 按价格从高到低排列,asks 按价格从低到高排列。为什么这样排?因为买方想用更低的价格买,卖方想用更高的价格卖,所以价格越高的买单越优先成交,价格越低的卖单越优先成交。
我举个例子,你打开任何一个交易所的 BTC/USDT 交易对,看到的买一、卖一就是订单簿的「第一层」。买一价格最高,卖一价格最低,两者之间的差值就是「价差」(spread)。
sortedcontainers 库或者直接维护两个列表。
来看一个简单的订单簿结构定义:
from dataclasses import dataclass
from typing import List, Tuple
@dataclass
class OrderBookLevel:
price: float
quantity: float
@dataclass
class OrderBook:
bids: List[OrderBookLevel] # 买单,按价格降序
asks: List[OrderBookLevel] # 卖单,按价格升序
timestamp: int # 快照时间戳
def best_bid(self) -> float:
return self.bids[0].price if self.bids else 0.0
def best_ask(self) -> float:
return self.asks[0].price if self.asks else 0.0
def spread(self) -> float:
return self.best_ask() - self.best_bid()
嗯,这里要注意:价格精度是个坑。我在项目中遇到过,不同交易所对价格的小数位数要求不一样。比如 Binance 的 BTC/USDT 价格精度是 2 位小数,但有些山寨币可能是 8 位。如果你用浮点数直接比较,很容易出问题。我建议统一用 Decimal 类型,或者至少做一次四舍五入。
4.2 深度图绘制:把订单簿变成可视化
光看数字不够直观,深度图(Depth Chart)能让你一眼看出市场的买卖力量对比。深度图的核心逻辑是:累计数量。从最优价格开始,向两侧累加每个价格档位的数量。
绘制步骤其实很简单:
- 从买一价格开始,向左(更低价格)累加 bids 的数量
- 从卖一价格开始,向右(更高价格)累加 asks 的数量
- 把价格作为 X 轴,累计数量作为 Y 轴,画两条曲线
我曾经用 Matplotlib 画过一张深度图,但后来发现 Plotly 更适合做交互式展示。这里给一个简单的实现:
import plotly.graph_objects as go
def plot_depth(order_book: OrderBook, symbol: str = "BTC/USDT"):
# 计算累计深度
bid_prices = [level.price for level in order_book.bids]
bid_volumes = []
cum_vol = 0
for level in order_book.bids:
cum_vol += level.quantity
bid_volumes.append(cum_vol)
ask_prices = [level.price for level in order_book.asks]
ask_volumes = []
cum_vol = 0
for level in order_book.asks:
cum_vol += level.quantity
ask_volumes.append(cum_vol)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=bid_prices, y=bid_volumes,
mode='lines', name='Bids',
fill='tozeroy', line=dict(color='green')
))
fig.add_trace(go.Scatter(
x=ask_prices, y=ask_volumes,
mode='lines', name='Asks',
fill='tozeroy', line=dict(color='red')
))
fig.update_layout(
title=f"{symbol} 深度图",
xaxis_title="价格",
yaxis_title="累计数量",
template="plotly_white"
)
return fig
你想想看,深度图其实就是一个「压力测试」工具。比如你想卖 100 个 BTC,从深度图上就能看出价格会滑多少点。我在做市策略里,经常用深度图来估算「市场冲击成本」。
4.3 订单簿快照与增量更新
这是做市系统里最容易踩坑的地方。交易所通常提供两种数据:快照(Snapshot)和增量(Incremental Update)。
快照就是某一时刻订单簿的完整状态。增量则是后续的变化——哪些价格档位被新增、修改或删除了。
为什么需要增量?因为全量推送太费带宽了。一个深度 100 层的订单簿,每秒推送 10 次快照,数据量相当可观。所以交易所一般让你先拿一次快照,然后只推送增量。
增量更新的数据结构通常长这样:
@dataclass
class OrderBookDelta:
bids: List[Tuple[float, float]] # (价格, 数量),数量为0表示删除
asks: List[Tuple[float, float]]
timestamp: int
def apply_delta(order_book: OrderBook, delta: OrderBookDelta) -> OrderBook:
# 处理 bids
for price, qty in delta.bids:
if qty == 0:
# 删除该价格档位
order_book.bids = [l for l in order_book.bids if l.price != price]
else:
# 更新或新增
found = False
for i, level in enumerate(order_book.bids):
if level.price == price:
order_book.bids[i].quantity = qty
found = True
break
if not found:
order_book.bids.append(OrderBookLevel(price, qty))
# 重新排序
order_book.bids.sort(key=lambda x: x.price, reverse=True)
# 处理 asks(逻辑类似,按价格升序)
for price, qty in delta.asks:
if qty == 0:
order_book.asks = [l for l in order_book.asks if l.price != price]
else:
found = False
for i, level in enumerate(order_book.asks):
if level.price == price:
order_book.asks[i].quantity = qty
found = True
break
if not found:
order_book.asks.append(OrderBookLevel(price, qty))
order_book.asks.sort(key=lambda x: x.price)
order_book.timestamp = delta.timestamp
return order_book
这里有个细节:数量为 0 表示删除该价格档位。为什么不是直接传一个「删除指令」?因为交易所为了简化协议,统一用数量来标识——数量为 0 就是删除,大于 0 就是新增或修改。
4.4 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张流程图:
这张图把本章的核心内容串起来了。你从订单簿出发,左边是 bids 和 asks 的构成,右边是深度图的绘制方法,下面是快照与增量更新的维护逻辑。三者缺一不可。
好了,订单簿的数据结构就讲到这里。记住三个关键词:有序、累计、增量。下次你看到深度图的时候,脑子里应该能浮现出背后那一行行代码是怎么跑的。
公众号:蓝海资料掘金营,微信deep3321