周三上午 9:47,某沪上 私募 的执行交易员告诉你:你在沪深300 ETF(510300)上挂的那笔 4.130 元、10000 份的买入限价委托已经躺了 10 分钟没成交,明明屏幕上的 BBO 在这十分钟里触碰 4.130 元至少一打次。他想知道为什么。答案在 L1 报价下面那一层——委托簿(order book)本身。4.130 元上排在你前面的是 180,000 份其它资金的挂单,过去 10 分钟买一价(买卖价差最优买侧)的成交速率约 800 份 / 秒。按这个算术,你的 10000 份要 225 秒才能轮到——而排在你前面的那 18 万份,撤单的概率远高于成交的概率。本课要教的,是量化人必须掌握的 LOB 知识:读 tick 数据、判断挂单是否会成交、计算队列不平衡(queue-imbalance)与订单流(order flow)信号——所有 L3 的 alpha 故事都挂在这上面。
数据模型与优先规则
限价委托簿——简称 LOB——是两个梯子。买侧 委托簿 装买盘挂单,按价格 降序 排列,最高的买价位于顶端,它就是 买一价(best bid)。卖侧装卖盘挂单,按 升序 排列,最低的卖价位于顶端,它就是 卖一价(best ask)。每一侧是从价位(price level)到挂单时间优先队列的映射。
两条优先规则按以下次序生效:
1. price priority — 更优价格的挂单总是先成交(最高买价 / 最低卖价获胜)
2. time priority — 同一价位上,先到的挂单先成交(FIFO at the level)
连续竞价中委托簿维护的不变量是 best_bid < best_ask。买入价格 ≥ best_ask 的委托是 可成交(marketable)的——它会立即匹配,而不会驻留在簿中。卖出同理对称。委托簿在连续竞价里 既不锁定(best_bid == best_ask)也 不交叉(best_bid > best_ask)。开盘前、收盘后的集合竞价(call auction)调用期里两种状态都允许——挂单只累积、不撮合,直到集合竞价结束的统一价撮合。
研究级(research-grade)委托簿在 Python 中的标准内存表示:
from collections import deque
from dataclasses import dataclass
from typing import Deque, Dict, Tuple
@dataclass
class Order:
order_id: int
size: int
arrival_seq: int
Bid = Dict[float, Deque[Order]] # price -> FIFO queue of resting orders
Ask = Dict[float, Deque[Order]] # price -> FIFO queue of resting orders
Order 数据类承载三个字段:order_id(交易所分配的唯一标识)、size(当前驻留挂单量——部分成交时原地递减)、arrival_seq(该挂单进入队列时的委托序号,用于 FIFO 优先级判定)。选择 deque 而非 list 是有意为之:成交时从头出队,deque 是 O(1)、list 是 O(n);做市商一个 session 几万次成交跑下来差异是数量级的。
事件流重建
委托簿不存储;它从交易所发布的事件流 派生。三类标准事件驱动每一次状态转换:
1. add(order_id, side, price, size, ts, arrival_seq)
— 把新挂单 append 进 (side, price) 的 deque;如果该价位之前为空,可能改善 BBO
2. cancel(order_id, ts)
— 按 id 从所在 deque 移除该挂单;若该价位变空,价位本身消失
3. trade(buy_order_id, sell_order_id, price, size, ts)
— 在成交价位的驻留侧 deque **头部** 消耗对应 size
把三者绑在一起的规则:**trade 事件总是从成交价位的驻留侧 deque 的头部消耗 size**。这一句话就是「时间优先(time priority)」的定义本身。如果 trade 事件让某挂单的 size 归零,该挂单被移出 deque、同时从 order_index: Dict[int, Tuple[str, float]] 这张「order_id 到 (side, price)」的旁路表里弹出。部分成交则保留在 deque 头部,size 递减。
策略可提交的四种标准委托类型,及其对委托簿的影响:
1. market order — 无价格;从对手方 BBO 向外走簿(walks the book)直到 size 满足
2. marketable limit order — 价位至少触及对手方;像带价格保护下限的 market order
3. passive limit order — 价位严格在 BBO 内侧;变为 `add` 事件,挂入簿中等待
4. stop / stop-limit order — 由交易所内部持有直到 stop 价被触发;触发后再提交 market 或 limit order
以及每个研究级(research-grade)回测必须建模的三个时间在效(time-in-force)标志:
1. IOC — immediate-or-cancel;立刻吃可吃的、剩余 size 立刻撤销;永不驻留
2. FOK — fill-or-kill;原子;要么全部立刻成交,要么全部撤销零成交
3. GTC — good-till-cancelled;驻留在簿中直到成交或用户主动撤销
桌面(desk)层面的操作规则:研究回测用 IOC market order 模拟激进成交、GTC limit order 模拟被动驻留。GTD(good-till-date)、OPG(at-the-open)、CLO(at-the-close)是交易所特定的调度标志,存在于实盘系统中,研究级回测里抽象掉。
某些场所发布第四类事件 modify(order_id, new_size_or_price)。按交易所约定,要么按 cancel + add 处理(挂单失去队列位置),要么按「严格 size 递减保留队列位置」处理。规则要记住:size 严格递减保留队列位置;其它任何修改都是 cancel-and-re-add——挂单回到新价位的队尾。SSE / SZSE 的 Level-2 逐笔委托里存在「委托类型」字段并发布撤单事件;做市商(market maker)关心这条规则的程度,约等于基金经理关心一个基点。
510300 上的 10 事件跟踪
class LimitOrderBook:
"""研究级内存版 LOB;消费 {add, cancel, trade} 事件流。"""
def __init__(self):
self.bids: Dict[float, Deque[Order]] = {}
self.asks: Dict[float, Deque[Order]] = {}
self.order_index: Dict[int, Tuple[str, float]] = {} # order_id -> (side, price)
def add(self, order_id: int, side: str, price: float, size: int, arrival_seq: int) -> None:
"""把驻留挂单 append 到 (side, price) 的 deque。"""
book = self.bids if side == "buy" else self.asks
book.setdefault(price, deque()).append(Order(order_id, size, arrival_seq))
self.order_index[order_id] = (side, price)
def cancel(self, order_id: int) -> None:
"""按 id 移除;若价位变空则丢弃该价位。"""
side, price = self.order_index.pop(order_id)
book = self.bids if side == "buy" else self.asks
book[price] = deque(o for o in book[price] if o.order_id != order_id)
if not book[price]:
del book[price]
def trade(self, side: str, price: float, size: int) -> None:
"""在成交价位的驻留侧 deque 头部消耗 size。"""
book = self.bids if side == "buy" else self.asks
remaining = size
while remaining > 0 and book[price]:
head = book[price][0]
take = min(head.size, remaining)
head.size -= take
remaining -= take
if head.size == 0:
book[price].popleft()
self.order_index.pop(head.order_id, None)
def best_bid(self) -> float: return max(self.bids) if self.bids else float("nan")
def best_ask(self) -> float: return min(self.asks) if self.asks else float("nan")
def mid(self) -> float: return 0.5 * (self.best_bid() + self.best_ask())
def spread(self) -> float: return self.best_ask() - self.best_bid()
510300 上从空簿开始的 10 事件追踪:
event best_bid | best_ask | spread | top-bid-depth | top-ask-depth
1. add(order_id=201, side='buy', price=4.130, size=30000, seq=1) 4.130 | nan | nan | 30000 | 0
2. add(order_id=202, side='sell', price=4.135, size=20000, seq=2) 4.130 | 4.135 | 0.005 | 30000 | 20000
3. add(order_id=203, side='buy', price=4.129, size=50000, seq=3) 4.130 | 4.135 | 0.005 | 30000 | 20000
4. add(order_id=204, side='sell', price=4.135, size=15000, seq=4) 4.130 | 4.135 | 0.005 | 30000 | 35000
5. add(order_id=205, side='buy', price=4.130, size=10000, seq=5) 4.130 | 4.135 | 0.005 | 40000 | 35000
6. trade(side='sell', price=4.135, size=20000, ts=...) 4.130 | 4.135 | 0.005 | 40000 | 15000
7. cancel(order_id=201) 4.130 | 4.135 | 0.005 | 10000 | 15000
8. add(order_id=206, side='sell', price=4.132, size=8000, seq=8) 4.130 | 4.132 | 0.002 | 10000 | 8000
9. trade(side='sell', price=4.132, size=8000, ts=...) 4.130 | 4.135 | 0.005 | 10000 | 15000
10. add(order_id=207, side='buy', price=4.131, size=20000, seq=10) 4.131 | 4.135 | 0.004 | 20000 | 15000
逐事件验算:事件 6——20000 份的可成交买委托吃掉卖侧 4.135 元价位 deque 头部的 order_id=202(size=20000),该挂单全部成交并弹出;order_id=204 成为新头部,余 15000 份。事件 9——8000 份的可成交买委托吃掉 4.132 元上唯一的挂单 order_id=206,该价位消失,best_ask 回到 4.135。
L1 / L2 / L3 feed 档位
feed 发布的 委托簿状态 决定了下游能做什么微观结构分析。每档暴露的字段、支撑的分析、不能支撑的分析:
| tier | book state conveyed | analyses enabled | analyses NOT supported |
| L1 | best bid + best ask + sizes + last trade | spread, mid-price, tick-rule sign | queue-imbalance signals, queue position |
| L2 | top-N levels per side, aggregated size per level | queue-imbalance signals, depth-of-book pressure | queue position for a specific order |
| L3 | every resting order with order_id + size + arrival_seq | queue position, full microstructure research | (none — this is the complete view) |
L1 是五元组 (best_bid, best_bid_size, best_ask, best_ask_size, last_trade_price) 加 last_trade_size、last_trade_ts——足以计算 买卖价差(bid-ask spread)、中价、每笔成交的 tick rule 方向,以及基础策略信号。L2 在价位级聚合——(price, total_size, n_orders) 对前 N 档——足以算出 queue-imbalance 信号(L3 课的核心对象)与深度压力,但 不足以 算出某条具体挂单的队列位置,因为个体挂单不暴露。L3 是逐笔全簿;SSE / SZSE 逐笔委托 + 逐笔成交合起来构成 CN 的 L3 等价物,包含 order_id、size、arrival_seq,使策略能算出自己的被动挂单在队列里的精确位置。
场所特定复杂度:集合竞价、涨跌停、隐藏 / 冰山、中价钉
上一节的 {add, cancel, trade} 简单模型 几乎 是全部。四类场所特定复杂度以下面方式打破它,每条都要会:
1. auctions — 单一价格统一价集合竞价;调用期内簿可合法锁定 / 交叉;
一次撮合替代 N 次连续成交
2. price bands — CN 涨跌停;簿的某一侧锁在涨停 / 跌停价上,激进单只排队不成交
3. hidden + iceberg orders — display_qty < total_qty;消费者只看到显示切片;
储备每次刷新时 arrival_seq 重置(冰山委托每次刷新失去时间优先)
4. midpoint pegs — 钉在 BBO 动态中价上;BBO 每变化就重定价;不显示
集合竞价。 A 股开盘集合竞价 9:15-9:25(其中 9:20 之后不可撤单),收盘集合竞价 14:57-15:00(深交所 2018 年起,上交所之后跟进)。撮合算法选取使匹配成交量最大化的均衡价格,平局时按最小不平衡量、再次按距前收盘价最近的次序破除。所有撮合的买卖盘在统一均衡价成交。集合竞价调用期内委托簿可以合法锁定甚至交叉——best_bid < best_ask 不变量在此暂停。
涨跌停。 A 股主板 ±10% 涨跌停(涨停 / 跌停);科创板(STAR 科创板,SSE)与创业板(ChiNext,SZSE)±20%;ST / *ST 股票 ±5%。价格触及涨停 / 跌停时,簿的对应一侧锁定——任何激进的反向委托只能在涨跌停价排队,不能以违反价格带的方式成交;标的常伴随临时停牌。当涨跌停价排队耗尽时,重新回到连续竞价。中央对手方与中央托管所同样发挥撮合后清算职能;这部分由 CSRC 中国证监会监管。
冰山 / 隐藏委托。 SSE 与 SZSE 在普通散户 / 私募 层面 不官方支持 用户提交的冰山或隐藏委托;机构大单的「切片」由经纪商的执行算法在场外完成,到达交易所时表现为普通限价委托事件。规则口号:A 股 Level-2 数据里,逐笔委托 全量按真实大小显示;机构大单在到达交易所之前由经纪商的执行算法切片完成。中价钉委托同理对买方不标准化提供。
中价钉。 在能提供中价钉的离岸市场,委托钉在 BBO 动态中价上,每次 BBO 变化即重定价,且不显示——A 股市场没有标准化的对应产品。
收尾规则与通向 L3 的桥
四句话收尾。所有微观结构问题最终都归结为「这个事件让委托簿发生了什么」。如果你重建的委托簿与场所发布的 BBO 快照不一致,事件流消费器有 bug——基于它算出的每一个信号都是错的。L2 feed 够大部分信号研究;但队列位置问题必须靠 L3。集合竞价与涨跌停规则会打破 {add, cancel, trade} 简单模型,必须用场所专属代码路径处理。
本课触碰的术语—— 订单簿(order book)、限价订单簿(limit order book)、买卖价差(bid-ask spread)、做市商(market maker)、最小变动单位(tick size)、订单流(order flow)、队列位置(queue position)、涨停(limit-up)、跌停(limit-down)——是 L3 的底座;L3 把这些机械对象转换为 经验 可观测量。
练习
Exercise
给定本课的 LimitOrderBook 类骨架,从空簿开始,手工跟踪如下 8 事件序列(510300)。对每个事件汇报:(a) 处理的事件元组;(b) 处理后 的 买一价 / 卖一价 / 价差;(c) trade 事件具体识别按 order_id 被消耗的挂单。
事件 1:add(order_id=101, side='buy', price=4.130, size=10000, arrival_seq=1)。
事件 2:add(order_id=102, side='sell', price=4.135, size=20000, arrival_seq=2)。
事件 3:add(order_id=103, side='buy', price=4.130, size=5000, arrival_seq=3)。
事件 4:add(order_id=104, side='sell', price=4.135, size=10000, arrival_seq=4)。
事件 5:cancel(order_id=101)。
事件 6:add(order_id=105, side='buy', price=4.135, size=12000, arrival_seq=6)(可成交买单,越过价差)。
事件 7:trade(side='sell', price=4.135, size=12000)(从卖侧 deque 头部消耗)。
事件 8:add(order_id=106, side='buy', price=4.132, size=8000, arrival_seq=8)。
全部 8 个事件完成后,还要写出最终的 best_bid_size 与 best_ask_size,以及每侧仍驻留的挂单。
提示
[order_id=102 (size=20000), order_id=104 (size=10000)]。事件 5 把 101 从买侧 4.130 元的 deque 移除,仅留 order_id=103 (size=5000)。提示
Formula Explorer
spread = best\_ask - best\_bid参考卡
本课装配的组件,按次序:
- Inline-code listing — LOB 两条优先规则(
price priority、time priority)与best_bid < best_ask不变量。 - Fenced ```python 代码块 —
Order数据类与买侧 / 卖侧的Dict[float, Deque[Order]]类型签名。 - Inline-code listing — 三类标准簿事件(
add、cancel、trade)与时间优先口号。 - Inline-code listing — 四种委托类型(市价、可成交限价、被动限价、stop / stop-limit)。
- Inline-code listing — 三个时间在效标志(
IOC、FOK、GTC)与研究回测规则。 - Inline-code table — L1 / L2 / L3 feed 三档映射到 book state、analyses enabled、analyses NOT supported。
- Fenced ```python 代码块 —
LimitOrderBook类骨架与add/cancel/trade/best_bid/best_ask/mid/spread方法。 - Fenced ```text 跟踪 — 510300 上 10 事件可手工验证序列及每事件 BBO 摘要。
- Inline-code listing — 四类场所特定复杂度(集合竞价、涨跌停、隐藏 / 冰山、中价钉)。
- Exercise — 8 事件手工跟踪,含 Two progressive Hints 各保持简短。
- FormulaExplorer — 价差定义
spread = best_ask − best_bid。
四句话规则收尾:每个微观结构问题都是「这个事件让委托簿发生了什么」;重建必须匹配场所发布的 BBO 快照;L2 够大部分信号研究、L3 用于队列位置;集合竞价 与 涨跌停 规则要场所专属代码路径。
下一课
把 LOB 作为核心对象抓在手里之后,下一课研究委托簿 产生的现象——买卖价差(quoted、effective、realised)、订单流不平衡、市场冲击的平方根法则、队列位置在成交概率中的作用——并展示每一个现象如何分解为对你刚刚定义的委托簿事件流的某种变换。把 LimitOrderBook 类放在脑子里;L3 的每一个度量都挂在它身上。