好问题!下面把**订单簿(Order Book)**从数据源→API事件→核心数据结构→维护与撮合→工程细节,系统讲清楚。读完你就能自己实现一个可落地的 Order Book(先做簿记/回放,再扩展到撮合引擎)。
一、你需要什么数据源 / API 事件?
订单簿维护依赖“可复原的事件流”。根据交易所/撮合系统提供的粒度,常见三类:
- L1(Top of Book)
- 最优买一/卖一价与量、最近成交。
- 只能展示价差与顶层,不足以重建整本书。
- L2(价位聚合簿 / Depth by Price)
- 每个价位的聚合挂单量(不含逐笔订单)。
- 增量消息通常是:
UPDATE(level=price, side, size)或DELETE(level)。
- 可以重建价位簿,但不能得到逐笔订单队列。
- 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 + Incremental、sequence_number、trading_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/ask、depth。
可能的实现(语言无关)
- 有序价格集合:
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:
- 在
OrderMap写入; - 找到该 side 的
PriceLevel,不存在则创建并插入有序价格集合; - 将订单节点入该价位
LevelQueue末尾,更新total_qty。
- Cancel:
- 通过
OrderMap找到节点,扣减 qty; - qty→0 则从队列删除,
OrderMap删除; - 若价位
total_qty==0,从有序集合移除该价位。
- Modify:
- 仅改量:直接在节点与
total_qty更新; - 改价:等价于
Cancel(order_id, old_qty)+Add(new_id or same_id, side, new_price, new_qty)(依交易所语义执行)。
- Trade(逐笔成交):
- 成交总是与 对手方 resting 队列 的队首开始撮对;
- 从队首扣减,直至成交量满足;订单被吃完则弹出,继续下一个订单;
- 同步更新
OrderMap、Level.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?)
撮合流程(极简限价单)
- Market/IOC/FOK 先与对手簿最佳价位起撮:
- 检查可成交(买价 >= 卖价);
- 逐个吃队首订单,生成
Trade; - FOK 成交不全则全部撤销,IOC 成交剩余立即撤。
- Limit(GFD):若有剩余且未触发成交条件,入本方价位队尾排队。
- 边界处理:
- 负量、负价、超价、停市状态拒单;
- 自成交保护(Self-Trade Prevention)规则(合并、拒绝或重定价);
- 价格/数量步进(tick/lot)校验;
- 交易时段与熔断状态。
五、时间复杂度
- 有序价格集合(平衡树):
insert/delete/find best→ O(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:
- 统计不同价差、库存状态、OBI 状态下的平均 markout;
- 若在某些条件下 markout 长期为负 → 加宽价差;
- 若 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 | 价格变化质量 | 衡量成交优劣、调整策略参数 |
💡 组合使用逻辑:
- 预测层(OBI):判断价格会不会动;
- 执行层(Markout):评估你报价的后果。
做市商理想状态:
- 用 OBI、OFI 预测方向;
- 用 Markout 评估风险与 adverse selection;
- 通过动态调整 spread/skew 实现稳定正的 hedged PnL。
