Order Book
🌆

Order Book

 
好问题!下面把**订单簿(Order Book)**从数据源→API事件→核心数据结构→维护与撮合→工程细节,系统讲清楚。读完你就能自己实现一个可落地的 Order Book(先做簿记/回放,再扩展到撮合引擎)。

一、你需要什么数据源 / API 事件?

订单簿维护依赖“可复原的事件流”。根据交易所/撮合系统提供的粒度,常见三类:
  1. L1(Top of Book)
  • 最优买一/卖一价与量、最近成交。
  • 只能展示价差与顶层,不足以重建整本书。
  1. L2(价位聚合簿 / Depth by Price)
  • 每个价位的聚合挂单量(不含逐笔订单)。
  • 增量消息通常是:UPDATE(level=price, side, size)DELETE(level)
  • 可以重建价位簿,但不能得到逐笔订单队列
  1. L3(逐笔订单簿 / Order-by-Order) ✅最完整
  • 事件流包含:
    • Add(order_id, side, price, qty) 新挂单
    • Modify(order_id, new_price?, new_qty?) 改价/改单(有的交易所用 Cancel+Add 表示改价)
    • Cancel(order_id, cancel_qty) 撤单(部分或全部)
    • Trade(aggressor_side, price, qty, resting_order_id) 成交(可含多 resting 订单)
    • Replace(old_id→new_id, price?, qty?)(某些撮合器风格)
  • 还有:Snapshot + Incrementalsequence_numbertrading_status 等。
结论:要“从零维护订单簿”,理想是拿 L3 逐笔事件;只有 L2 就维护价位聚合簿;只有 L1 就只能展示顶层。

二、自己实现时,推荐的数据结构

1) 目标与不变量

  • 价格优先、时间优先(price-time priority)
  • 支持 O(log N) 级别的插入/删改、快速获得最优价(best bid/ask)
  • 能回放/对账(replay & audit)

2) 结构分层(强烈推荐)

  • OrderMap(哈希表)order_id -> {side, price, qty, node_ptr}
    • 作用:O(1) 查找订单,便于撤单/改单。
  • PriceLevel 容器(有序):
    • Bids:按价格降序Asks:按价格升序
    • 每个 price -> Level(聚合层)。
  • Level 队列:同一价位的订单按到达顺序排队(FIFO)。
    • deque / linked list,节点里存 order_id, qty, ts
  • 聚合统计:每个价位维护 total_qty,全簿维护 best_bid/askdepth

可能的实现(语言无关)

  • 有序价格集合:std::map/TreeMap/sortedcontainers;或跳表/平衡树。
  • 队列:deque 或双向链表(撤单需要 O(1) 删除 → 需保存 node 指针)。
  • 哈希表:unordered_map / dict
不推荐仅用堆(heap)做价优,因为中间删除很慢(需要 lazy deletion + 额外哈希校验),实现复杂且容易出错。

三、核心 API(订单簿更新,而非撮合)

以下假设你获得 L3 事件来“复原”订单簿(不含你自己的撮合逻辑):
on_add(order_id, side, price, qty) on_cancel(order_id, cancel_qty) # 支持部分撤 on_modify(order_id, new_price?, new_qty?) # 价格变化=撤+增,数量变化=直接改 on_trade(trade_id, aggressor_side, price, qty, resting_order_ids[]) on_snapshot(book_snapshot) # 冷启动/断线恢复

处理要点

  • Add
      1. OrderMap 写入;
      1. 找到该 side 的 PriceLevel,不存在则创建并插入有序价格集合;
      1. 将订单节点入该价位 LevelQueue 末尾,更新 total_qty
  • Cancel
      1. 通过 OrderMap 找到节点,扣减 qty;
      1. qty→0 则从队列删除,OrderMap 删除;
      1. 若价位 total_qty==0,从有序集合移除该价位。
  • Modify
    • 仅改量:直接在节点与 total_qty 更新;
    • 改价:等价于 Cancel(order_id, old_qty) + Add(new_id or same_id, side, new_price, new_qty)(依交易所语义执行)。
  • Trade(逐笔成交)
    • 成交总是与 对手方 resting 队列队首开始撮对;
    • 从队首扣减,直至成交量满足;订单被吃完则弹出,继续下一个订单;
    • 同步更新 OrderMapLevel.total_qty、有序价格集合的存在性;
    • 若事件里带多个 resting_order_id,按顺序处理。
  • Snapshot + Incremental
    • 冷启动先整体快照(含 sequence),随后应用增量;
    • 增量必须按序号连续消费,丢包需 gap request / refresh
    • 定期校验簿的一致性(如顶层价量)。

四、撮合引擎(可选:自己撮合而不是被动复原)

若你不仅“构簿”,还要主动撮合(接你自家订单):

接口(最小闭环)

submit(order_id, side, price, qty, tif=GFD/IOC/FOK, type=Limit/Market) cancel(order_id) modify(order_id, new_price?, new_qty?)

撮合流程(极简限价单)

  1. Market/IOC/FOK 先与对手簿最佳价位起撮:
      • 检查可成交(买价 >= 卖价);
      • 逐个吃队首订单,生成 Trade
      • FOK 成交不全则全部撤销,IOC 成交剩余立即撤。
  1. Limit(GFD):若有剩余且未触发成交条件,入本方价位队尾排队。
  1. 边界处理
      • 负量、负价、超价、停市状态拒单;
      • 自成交保护(Self-Trade Prevention)规则(合并、拒绝或重定价);
      • 价格/数量步进(tick/lot)校验;
      • 交易时段与熔断状态。

五、时间复杂度

  • 有序价格集合(平衡树):insert/delete/find bestO(log P)P=活跃价位数。
  • 价位队列操作:入队/出队/按节点删除 → O(1)
  • 撤单/改单定位(哈希):O(1)
  • 整体:每条事件 O(log P),吞吐能达到百万级/秒(C++/Rust)。

六、关键工程细节(生产实务)

  • 序号一致性:每条增量有 seq; 必须严格按序应用,遇 GAP 立即重拉。
  • 幂等 & 去重:相同 seq 或重复消息要丢弃。
  • 时钟与顺序:以事件序号为准,不以本机到达时间为准。
  • 快照切换:快照应用时先清簿或做差分,并从快照序号继续增量。
  • 稀疏价位:使用有序映射比数组更节省内存。
  • 跨线程:网络线程→环形队列→簿记线程;撮合与持久化尽量解耦。
  • 校验:定期核对“聚合量 = 各订单量之和”;顶层价与行情对齐。
  • 回放/回测:事件日志按(channel, seq)落盘,可线性回放复现任何时刻订单簿状态。
  • 内存复用:对象池(slab)减少频繁分配;链表节点从池中借还。
  • 风控:最大价差、最大深度、最大单价位量、用户限额、STP 规则等。

七、一个简化的“仅簿记”伪代码

class Order: __slots__ = ("id","side","price","qty","node") class Level: def __init__(self): self.total = 0 self.q = DoublyLinkedList() # each node holds order_id, qty class SideBook: def __init__(self, ascending): # asks: True, bids: False self.levels = SortedMap() # price -> Level (ordered) def best_price(self): return self.levels.min_key() if self.ascending else self.levels.max_key() class OrderBook: def __init__(self): self.bids = SideBook(ascending=False) self.asks = SideBook(ascending=True) self.orders = {} # order_id -> Order def on_add(self, oid, side, price, qty): o = Order(); o.id=oid; o.side=side; o.price=price; o.qty=qty sidebk = self.bids if side=="B" else self.asks lvl = sidebk.levels.get(price) or sidebk.levels.put(price, Level()) node = lvl.q.push_back((oid, qty)); o.node = node lvl.total += qty; self.orders[oid] = o def _remove_node(self, sidebk, price, node, qty): lvl = sidebk.levels[price] node.value.qty -= qty lvl.total -= qty if node.value.qty == 0: lvl.q.erase(node) if lvl.total == 0: sidebk.levels.erase(price) def on_cancel(self, oid, cancel_qty): o = self.orders.get(oid); sidebk = self.bids if o.side=="B" else self.asks self._remove_node(sidebk, o.price, o.node, cancel_qty) o.qty -= cancel_qty if o.qty == 0: self.orders.pop(oid, None) def on_trade(self, aggressor_side, price, qty, resting_ids): # reduce resting orders at that price in order sidebk = self.bids if aggressor_side=="S" else self.asks # resting on opposite for rid, fill in resting_ids: ro = self.orders[rid] self._remove_node(sidebk, ro.price, ro.node, fill) ro.qty -= fill if ro.qty==0: self.orders.pop(rid, None)
真正撮合时,将 on_add 里的订单先与对手簿比较并吃单;只有有剩余才入本方队列。

八、如果只有 L2(价位聚合)怎么办?

数据结构更简单:
  • SortedMap<price, size> 分别维护 Bids/Asks;
  • 事件只有:Set(size) / Delete(level)
  • 没有 OrderMap、没有 Level 队列,无法追踪单个订单与时间优先;
  • 适合行情展示、风控、回放盘口,不适合逐笔策略。

九、测试与验证

  • 单元测试
    • 交叉价入单立即成交;
    • 部分成交/多 resting 单链式成交;
    • 改价跨档位、撤单边界;
    • 价位清空后正确移除。
  • 一致性测试
    • sum(level.q.qty) == level.total
    • 最优价与 best_price() 一致;
    • 回放与正式流对账(checksum、顶层价量)。

总结

  • API 事件:Add / Cancel / Modify / Trade(+Snapshot/Seq)
  • 核心结构有序价格集合 + 价位队列 + 订单哈希
  • 复杂度:每条事件 O(log 价位数)
  • 工程点:序号、幂等、快照、对象池、回放、有效性校验
  • 粒度:L3 可复原逐笔,L2 只能做聚合簿
如果你愿意,我可以给你 C++(std::map + 双向链表 + unordered_map)或 Python 的一个最小可跑实现(含随机回放与一致性校验脚本),方便你直接接入实盘/回测。
 
 

🧭 一、订单簿不平衡(Order Book Imbalance, OBI)


📘 1. 定义

订单簿不平衡描述了当前盘口中买卖挂单力量的相对大小
最常见的定义是:
其中:
  • :买一价上的挂单量;
  • :卖一价上的挂单量。

📊 2. 取值范围与解释

OBI 值
市场状态
直觉解释
+1
全部挂在买一
强买盘压力
0
买卖均衡
中性
–1
全部挂在卖一
强卖盘压力
例如:
Bid Qty
Ask Qty
OBI
解释
900
100
+0.8
盘口偏多,短期价格有上行动能
500
500
0
平衡
100
900
–0.8
卖压强,价格有下行动能

⚙️ 3. 扩展定义(多档深度)

可扩展到前 (n) 档:
 
也可加入价格权重:
 

💡 4. 经济含义

  • 盘口是市场的“短期供需平衡表”:
    • 买挂量多 ⇒ 潜在买方需求大 ⇒ 短期 upward pressure;
    • 卖挂量多 ⇒ 潜在卖压大 ⇒ downward pressure。
  • 高频实证研究发现:
    • 盘口不平衡能短期预测价格变化方向

🧩 5. 实际应用

应用场景
用法
高频方向预测
构建短期 price move predictor
做市风控
若 OBI 极端偏向某侧,拉宽价差或取消挂单
报价动态调整
在强买盘时提高报价中心;在强卖盘时下调
执行算法
VWAP/TWAP 可用 OBI 作为调速信号(slippage control)

🧮 6. Python 实现示例

def order_book_imbalance(bid_qty, ask_qty): return (bid_qty - ask_qty) / (bid_qty + ask_qty + 1e-9) obi = order_book_imbalance(V_bid_1, V_ask_1)
或多档扩展:
def multi_level_obi(bid_qtys, ask_qtys): return (sum(bid_qtys) - sum(ask_qtys)) / (sum(bid_qtys) + sum(ask_qtys) + 1e-9)

📈 7. 常见面试问题(附答案)

Q1:为什么订单簿不平衡可以预测短期价格方向?
A: 因为买卖盘的不对称代表潜在流动性压力(order flow imbalance)。
买方挂单多 → 对价格的支持强 → 价格可能上升。

Q2:订单簿不平衡在实盘做市中如何使用?
A:
  • 若 OBI 高 → 减少买单风险、拉宽 ask;
  • 若 OBI 低 → 减少卖单风险、拉宽 bid;
  • 极端 OBI 值时停止报价(避免被卷)。

Q3:它和 order flow imbalance (OFI) 有何区别?
A:
  • OBI:静态 snapshot 指标(挂单状态);
  • OFI:动态流指标(买卖订单变化流入流出);
    • OBI 反映瞬时供需,OFI 反映变化趋势。

🧭 二、Markout(成交后的价格走向分析)


📘 1. 定义

Markout 衡量某笔成交后,市场价格在一定时间后的变化方向与幅度。
通常用于判断是否被信息单打穿(adverse selection)

⚙️ 2. 公式定义

设在时间 (t) 成交一笔交易(买或卖),成交价为 (p_t),
在未来 时间后(如 1s, 5s, 1min)中间价为

(1)普通定义:

若为做市商视角(被动成交):
  • 正的 markout 表示有利(价格向有利方向移动);
  • 负的 markout 表示不利(被信息单卷走)。

📊 3. 示例解释

交易类型
成交价
Δt后中间价
Markout
解释
Passive Buy
100.00
100.10
+0.10
有利,价格上涨
Passive Buy
100.00
99.80
–0.20
不利,被信息单打穿
Passive Sell
100.10
99.90
+0.20
有利,价格下跌

🧮 4. 聚合指标

对所有被动成交求平均:
或统计分布(中位数、分位数)。
若长期平均 markout 为负 → 说明被“挑单”严重 → 需调整报价策略。

🧩 5. 经济含义

  • Adverse Selection:做市商报出太好的价格,被有信息单立即打。价格随后继续向单的方向移动,造成损失。
  • Good Fill Quality:如果成交后价格朝有利方向走,则说明成交质量高,报价合理。
换句话说:
Markout 衡量的是成交后,价格是否继续往你不利方向走(坏单)还是回调(好单)。

🔍 6. 常见时间窗口

窗口 Δt
含义
1 秒
高频做市质量
5 秒
低延迟策略稳定性
30 秒 – 1 分钟
市场冲击评估
5 – 10 分钟
中短期价格冲击消退

📈 7. 实盘用途

应用
用法
报价优化
评估每档报价的 markout 分布,调整 spread
流动性检测
判断哪些价位容易被信息单 hit
Adverse selection filter
自动拉宽对差的 markout 价位
模型评估
比较不同做市策略的平均 markout 或分布
VWAP/TWAP 执行
控制 post-trade slippage

🧮 8. Python 示例

def markout(trade_price, mid_after, side): if side == 'buy': return mid_after - trade_price else: return trade_price - mid_after # 举例 markout_buy = markout(100.0, 100.2, 'buy') # +0.2 markout_sell = markout(100.0, 99.8, 'sell') # +0.2
聚合评估:
import pandas as pd markout_mean = df['markout'].mean()

💬 9. 面试高频问答


Q1:Markout 为什么重要?
A: 它能直接衡量报价的好坏。
被动成交后若价格继续往同方向走,说明被有信息单打(bad fill);
若价格反转回中性,说明报价安全(good fill)。

Q2:你会如何利用 Markout 优化做市策略?
A:
  1. 统计不同价差、库存状态、OBI 状态下的平均 markout;
  1. 若在某些条件下 markout 长期为负 → 加宽价差;
  1. 若 markout 稳定为正 → 可适当缩价提高成交率。

Q3:Markout 与 realized spread 的区别?
指标
定义
用途
Markout
成交价与未来中间价的差
衡量 adverse selection
Realized Spread
(TradePrice – MidAfter) × 2(双边对称定义)
衡量报价盈利能力
Effective Spread
(TradePrice – MidAtTrade) × 2
衡量即时流动性成本

Q4:OBI 与 Markout 的关系?
  • OBI 反映交易前的盘口状态;
  • Markout 反映交易后的价格变化;
    • → 可以联合使用:
    • 若在 OBI>0 时的被动卖单 markout 为负 → 说明被错误方向打单;
    • 可作为 execution predictor 的验证指标。

📘 三、联合总结:OBI × Markout 的微观结构逻辑

阶段
指标
含义
决策作用
成交前
OBI
静态盘口供需不平衡
判断方向性压力、调整挂单
成交后
Markout
价格变化质量
衡量成交优劣、调整策略参数
💡 组合使用逻辑:
  1. 预测层(OBI):判断价格会不会动;
  1. 执行层(Markout):评估你报价的后果。
做市商理想状态:
  • 用 OBI、OFI 预测方向;
  • 用 Markout 评估风险与 adverse selection;
  • 通过动态调整 spread/skew 实现稳定正的 hedged PnL。