1. 订单簿基础:限价单与市价单、买卖盘口、订单簿数据结构
大家好,欢迎来到《订单簿微观结构与算法交易》的第一章。
说实话,订单簿这东西,看着简单,但坑特别多。我刚开始做量化的时候,以为订单簿不就是一堆买单卖单嘛,结果第一次实盘就被盘口数据给坑了——嗯,这事后面再细说。
这一章,咱们把订单簿最核心的三个东西讲清楚:订单类型、盘口结构、数据怎么存。你把这些搞明白了,后面讲做市、讲套利、讲高频策略,你才能听得懂。
1.1 限价单 vs 市价单:两种最基本的玩法
先问个问题:你想买100股茅台,你怎么办?
两种选择:
- 市价单(Market Order):不管价格,立马成交。你下单的时候,系统直接按当前最优卖价给你吃掉。
- 限价单(Limit Order):你指定一个价格,比如1500块。只有有人愿意1500卖给你,才成交。否则就挂在那等。
我个人习惯,做策略的时候,市价单用来抢流动性,限价单用来提供流动性。你想想看,如果你做高频,市价单就是你的子弹,打出去就要见血;限价单呢,更像是钓鱼,挂在那等鱼上钩。
核心区别一句话:
- 市价单:成交优先,价格不确定(可能滑点)
- 限价单:价格确定,成交不确定(可能不成交)
我在项目中遇到过一件事:有个同事用市价单做回测,回测里每次都按最优价成交,结果实盘一跑,滑点直接吃掉2个tick,策略直接亏钱。后来我帮他改成限价单+被动挂单,虽然成交率低了点,但每笔交易的成本稳定多了。
避坑指南: 我曾经在回测里忽略了一个细节——市价单在流动性不足的时候,会吃掉多个档位的订单。比如你买1000手,卖一只有200手,剩下的800手会继续吃卖二、卖三……价格越吃越高。这个叫「冰山效应」,后面我们会专门讲。
1.2 买卖盘口:订单簿的「脸面」
订单簿长什么样?说白了,就是一张表,左边是买单,右边是卖单。
举个例子,某时刻的盘口数据:
| 买价 | 买量 | 卖价 | 卖量 |
|---|---|---|---|
| 1500.10 | 200 | 1500.20 | 150 |
| 1500.00 | 500 | 1500.30 | 300 |
| 1499.90 | 800 | 1500.40 | 600 |
| 1499.80 | 1200 | 1500.50 | 900 |
这里面有几个关键概念:
- 最优买价(Bid):当前愿意出的最高买入价,这里是1500.10
- 最优卖价(Ask):当前愿意卖的最低卖出价,这里是1500.20
- 买卖价差(Spread):Ask - Bid = 0.10,这就是你来回交易一次的成本
- 盘口深度(Depth):每个价位上的挂单量,比如买一挂了200手
你想想看,如果价差只有0.01,说明流动性好;如果价差有1块钱,说明这个品种没人玩,你一进去就是被收割的命。
个人经验: 我一般看盘口,第一眼先看价差,第二眼看买一卖一的挂单量。如果买一只有10手,卖一有1000手,那说明卖方压力大,价格可能要跌。这个叫「订单簿不平衡」,后面章节会专门讲怎么量化。
1.3 订单簿数据结构:怎么存才高效?
好了,现在你知道订单簿长什么样了。但问题是——计算机里怎么存?
你想想看,每秒可能有几千笔订单进来,有的撤单,有的改价,有的成交。如果存得不好,你的策略还没算完,行情已经变了。
我常用的数据结构有两种:
1.3.1 数组/列表(适合小规模)
最简单的方式,用两个列表分别存买单和卖单,按价格排序。
# 伪代码示例
bids = [(1500.10, 200), (1500.00, 500), (1499.90, 800)]
asks = [(1500.20, 150), (1500.30, 300), (1500.40, 600)]
# 获取最优买卖价
best_bid = bids[0][0] # 1500.10
best_ask = asks[0][0] # 1500.20
spread = best_ask - best_bid
这种方式的优点是简单,缺点是插入和删除慢。如果你每秒处理几百笔订单,还行;但如果是高频场景,每秒几万笔,那就扛不住了。
1.3.2 红黑树/有序字典(适合高频)
真正的高频系统,一般用红黑树(Red-Black Tree)或者跳表(Skip List)。Python里可以用 sortedcontainers 库,或者自己实现一个。
from sortedcontainers import SortedDict
# 买单:价格从高到低排序
bids = SortedDict({1500.10: 200, 1500.00: 500, 1499.90: 800})
# 卖单:价格从低到高排序
asks = SortedDict({1500.20: 150, 1500.30: 300, 1500.40: 600})
# 获取最优买卖价
best_bid = bids.peekitem(-1)[0] # 最后一个元素,最高价
best_ask = asks.peekitem(0)[0] # 第一个元素,最低价
红黑树的插入、删除、查找都是 O(log n),比列表的 O(n) 快得多。我在做期货高频策略的时候,用的就是这种结构。
注意: 别用Python自带的dict来存订单簿,因为dict是无序的。你每次都要排序,性能直接爆炸。我曾经见过一个新手,用dict存盘口,然后每次更新都sort一遍,回测跑了一晚上没跑完……
1.4 订单簿的微观结构:一张图看懂
下面这张图,是我自己画的,把订单簿的核心结构展示出来。你看一眼,基本就全明白了。
这张图里,左边是买单,价格从高到低排列;右边是卖单,价格从低到高排列。中间那个空隙,就是价差。你想想看,如果价差为0,那就是成交了。
1.5 实战中的订单簿更新逻辑
最后,咱们聊聊订单簿怎么更新。真实行情里,你会收到三种消息:
- 新增订单:有人挂了一个新单子,插入到对应位置
- 撤单:有人把挂着的单子撤了,删除对应记录
- 成交:买卖双方匹配成功,从订单簿里移除
我一般用事件驱动的方式来处理:
class OrderBook:
def __init__(self):
self.bids = SortedDict() # 买单
self.asks = SortedDict() # 卖单
def update(self, side, price, volume):
"""side: 'buy' 或 'sell', volume=0 表示撤单"""
if side == 'buy':
if volume == 0:
del self.bids[price]
else:
self.bids[price] = volume
elif side == 'sell':
if volume == 0:
del self.asks[price]
else:
self.asks[price] = volume
def best_bid(self):
return self.bids.peekitem(-1) if self.bids else None
def best_ask(self):
return self.asks.peekitem(0) if self.asks else None
小技巧: 我习惯在更新订单簿的时候,顺便记录一下时间戳。这样后面做回测的时候,可以精确到微秒级别,分析订单簿的微观变化。这个在讲「订单簿重建」的时候会详细展开。
好了,这一章的内容就到这里。订单簿的基础,说白了就是:知道怎么挂单、怎么看盘口、怎么存数据。你把这些搞熟了,后面讲做市策略、讲套利逻辑,你才能跟得上。
记住一句话:订单簿是市场的「心电图」。读懂了它,你就读懂了市场。