1. 订单簿基础:限价单与市价单、买卖盘口、订单簿数据结构
做量化交易这些年,我见过不少新手一上来就盯着K线图猛看,却忽略了最底层的东西——订单簿。说实话,订单簿才是市场最真实的脉搏。K线是二手加工品,订单簿才是原始数据流。今天我们就从最基础的东西聊起。
1.1 两种最基础的订单类型
先说说限价单和市价单。这两个概念看似简单,但我在项目中见过太多人把它们用错了。
限价单(Limit Order):你指定一个价格,只有市场达到这个价格时才会成交。说白了,就是「我只愿意在这个价位买/卖,贵了我不买,便宜了我不卖」。
市价单(Market Order):你不管价格,直接按当前最优价格成交。说白了就是「我现在就要成交,价格无所谓」。
嗯,这里要注意一个关键区别:限价单提供流动性,市价单消耗流动性。这个区别在后面的课程里会反复出现。
核心要点:限价单 = 挂单等待,市价单 = 立即吃掉。两者共同构成了订单簿的「供给」和「需求」两端。
1.2 买卖盘口:Bid 与 Ask
订单簿的核心结构其实很简单,就两个队列:
- 买盘(Bid):所有想买入的人挂的限价单,按价格从高到低排列
- 卖盘(Ask):所有想卖出的人挂的限价单,按价格从低到高排列
为什么买盘从高到低?你想想看,谁出价高谁就先成交,这是很自然的逻辑。卖盘反过来,谁卖得便宜谁先成交。
我刚开始做高频策略时,犯过一个低级错误——把买卖盘的方向搞反了。结果策略一直在反向交易,亏了不少手续费才反应过来。从那以后,我每次写订单簿相关的代码,第一件事就是确认 Bid 和 Ask 的方向。
1.3 订单簿的数据结构
在实际开发中,订单簿的数据结构怎么设计?我个人的习惯是用两个有序字典(OrderedDict)或者红黑树来实现。为什么?因为订单簿的核心操作就三个:
- 新增订单:插入到对应价格层级
- 撤销订单:从对应价格层级删除
- 成交:从最优价格层级移除
这三个操作都需要 O(log n) 或 O(1) 的时间复杂度。用数组?不行,插入删除太慢。用哈希表?也不行,没法按价格排序。
下面是我常用的一个简化版订单簿实现:
class OrderBook:
def __init__(self):
self.bids = {} # 价格 -> 数量
self.asks = {} # 价格 -> 数量
self.bid_prices = [] # 排序后的买价列表
self.ask_prices = [] # 排序后的卖价列表
def update_bid(self, price, volume):
if volume == 0:
# 撤销订单
if price in self.bids:
del self.bids[price]
self.bid_prices.remove(price)
else:
# 新增或更新
self.bids[price] = volume
if price not in self.bid_prices:
self.bid_prices.append(price)
self.bid_prices.sort(reverse=True) # 从高到低
def update_ask(self, price, volume):
if volume == 0:
if price in self.asks:
del self.asks[price]
self.ask_prices.remove(price)
else:
self.asks[price] = volume
if price not in self.ask_prices:
self.ask_prices.append(price)
self.ask_prices.sort() # 从低到高
@property
def best_bid(self):
return self.bid_prices[0] if self.bid_prices else None
@property
def best_ask(self):
return self.ask_prices[0] if self.ask_prices else None
@property
def spread(self):
if self.best_bid and self.best_ask:
return self.best_ask - self.best_bid
return None
实战经验:这个实现只适合教学和回测。实盘交易中,我建议用 sortedcontainers 库的 SortedDict,或者直接用 Redis 的有序集合。我在一个低延迟项目中试过纯 Python 实现,结果订单簿更新一次要 0.5 毫秒,完全扛不住高频场景。
1.4 订单簿的层级结构
一个完整的订单簿,通常包含多个价格层级。每个层级上可能有多个订单,但价格相同。举个例子:
| 层级 | 买盘价格 | 买盘数量 | 卖盘价格 | 卖盘数量 |
|---|---|---|---|---|
| 1 | 100.00 | 500 | 100.10 | 300 |
| 2 | 99.90 | 800 | 100.20 | 600 |
| 3 | 99.80 | 1200 | 100.30 | 900 |
| 4 | 99.70 | 400 | 100.40 | 200 |
| 5 | 99.60 | 600 | 100.50 | 1000 |
这个表格里,最优买价是 100.00,最优卖价是 100.10,价差(spread)是 0.10。如果你下了一个市价买单,会先吃掉 100.10 的 300 股,如果还不够,继续吃 100.20 的 600 股,以此类推。
避坑指南:我曾经在回测中忽略了「部分成交」的情况。市价单可能只成交一部分,剩下的部分会变成限价单挂在订单簿上。这个细节如果不处理,回测结果会和实盘差很多。
1.5 订单簿的视觉化
为了让你更直观地理解订单簿的结构,我画了一张图:
这张图展示了一个典型的订单簿结构。左边是买盘,价格从高到低排列;右边是卖盘,价格从低到高排列。中间的价差就是市场流动性的直接体现。
1.6 订单簿的实时更新机制
实盘中的订单簿是动态变化的。每一秒都有成千上万的订单进来、撤销、成交。我处理过的一个高频数据源,每秒推送超过 10 万条订单簿更新消息。
订单簿的更新消息通常包含以下几种类型:
- 新增:一个新的限价单进入订单簿
- 撤销:一个已有的限价单被取消
- 成交:一个限价单被市价单吃掉(部分或全部)
- 修改:一个限价单的价格或数量被修改(有些交易所不支持)
我个人习惯用「快照 + 增量更新」的方式来维护订单簿。先获取一个完整的订单簿快照,然后根据后续的增量消息逐条更新。这样做的好处是,即使某条增量消息丢失了,也能通过重新获取快照来恢复。
关键点:订单簿的深度(Depth)指的是每个价格层级上的挂单总量。深度越厚,说明这个价格附近的流动性越好。我经常用「深度图」来判断市场的支撑位和阻力位。
1.7 常见的数据源与格式
不同交易所的订单簿数据格式略有不同,但核心结构大同小异。以币安为例,它的订单簿数据长这样:
{
"lastUpdateId": 1027024,
"bids": [
["4.00000000", "431.00000000"], // [价格, 数量]
["3.90000000", "152.00000000"],
["3.80000000", "100.00000000"]
],
"asks": [
["4.20000000", "245.00000000"],
["4.30000000", "320.00000000"],
["4.40000000", "150.00000000"]
]
}
这个格式很直观。bids 数组里每个元素是 [价格, 数量],按价格从高到低排列。asks 同理,按价格从低到高排列。
小技巧:处理订单簿数据时,我建议把价格和数量都转成浮点数或 Decimal 类型。字符串比较在 Python 里是按字典序的,100.0 和 99.9 比较会出错。我曾经因为这个 bug 排查了整整一个下午。
好了,订单簿的基础知识就讲到这里。这些概念虽然简单,但它们是后续所有流动性信号和微观结构分析的基础。下一节我们会深入讨论如何从订单簿中提取有用的交易信号。
公众号:蓝海数据掘金营,微信deep3321