3、订单簿动态变化:订单到达、撤销与成交、订单簿事件驱动模型
订单簿不是一张静态的快照。它每时每刻都在变化。
我个人习惯把订单簿比作一个「活着的生物」——有人下单,它动一下;有人撤单,它又动一下;一旦成交,它整个结构都可能重组。你想想看,如果只看某一时刻的盘口深度,那跟看一张照片有什么区别?我们要的是电影,不是照片。
3.1 订单到达:流动性注入的瞬间
订单到达,说白了就是有人往订单簿里塞了一笔单子。这个动作会改变订单簿的形态。
我在项目中遇到过一种情况:某个小币种,平时深度很薄,突然来了一笔大单挂在买一。这时候整个订单簿的形态瞬间变了——买一价格被撑住,卖一那边反而显得很空。这种「单边加厚」其实是个信号,说明有人在这个价位有强烈的意愿。
订单到达的影响可以分为几种:
- 限价单到达:直接插入到对应的价格档位。如果价格与现有订单重合,就合并到同一档位;如果不重合,就新建一档。
- 市价单到达:不进入订单簿,直接与对手方最优价格成交。它会吃掉流动性,而不是增加流动性。
- 冰山订单到达:只显示一部分,剩余部分隐藏。这种订单到达时,你看到的只是冰山一角。
关键点:订单到达的速度和规模,直接决定了订单簿形态变化的剧烈程度。高频交易中,每秒可能有上千笔订单到达,订单簿的形态在毫秒级别内不断重构。
3.2 订单撤销:流动性抽走的陷阱
订单撤销比订单到达更值得警惕。
为什么?因为订单到达是「明牌」——你看到了,就知道有人在买或卖。但订单撤销是「暗牌」——你看到的是撤单,但你不知道撤单的人为什么撤。我曾经踩过一个坑:某个品种的买一挂单很大,看起来支撑很强,我正准备入场做多。结果那笔大单突然撤了,价格瞬间跳水。嗯,后来我才明白,那是做市商在测试市场深度。
订单撤销的几种常见模式:
- 全部撤销:整个订单从订单簿消失。对应的价格档位深度减少。
- 部分撤销:只减少订单数量,不改变价格档位。档位还在,但深度变薄。
- 批量撤销:多个订单同时撤销。这种情况通常发生在算法交易中,策略切换或风控触发。
避坑指南:我曾经在实盘中看到买一和卖一同时出现大单撤销,紧接着价格剧烈波动。后来我总结了一个规律——如果买卖双方同时撤单,说明市场分歧加大,价格即将选择方向。这时候不要急着入场,等方向明确了再说。
3.3 订单成交:形态重构的触发器
订单成交是订单簿变化中最核心的事件。它不仅仅是「一笔交易完成」这么简单。
成交会改变订单簿的拓扑结构。举个例子:如果卖一被吃掉,原来的卖二就变成了新的卖一。这个「档位晋升」的过程,会改变整个订单簿的形态特征——价差、深度分布、斜率都会变。
成交的几种类型:
- 吃单成交:市价单吃掉限价单。这是最常见的成交方式,会直接减少对手方的深度。
- 对敲成交:买卖双方同时到达,价格匹配。这种成交对订单簿形态影响较小,因为双方同时消失。
- 大额成交:一笔成交吃掉多个档位。这种成交会彻底改变订单簿的形态,通常伴随着价格跳空。
个人经验:我习惯把大额成交后的订单簿形态变化叫做「伤口愈合过程」——成交后,新的订单会逐渐填补被吃掉的档位。这个愈合速度,其实反映了市场的恢复能力。愈合快的市场,流动性好;愈合慢的市场,说明参与者谨慎。
3.4 订单簿事件驱动模型
好了,前面讲了三种事件。现在的问题是:怎么把这些事件组织起来,形成一个可用的模型?
我个人推荐用事件驱动模型。说白了,就是「来了什么事件,就做什么处理」。这种模型在量化交易中非常实用,因为它天然适合处理异步、高并发的订单簿数据。
事件驱动模型的核心结构:
- 事件源:交易所推送的订单簿增量数据(snapshot + update)。
- 事件队列:按时间顺序排列的事件列表。每个事件包含类型(到达/撤销/成交)、价格、数量、时间戳。
- 事件处理器:根据事件类型更新本地订单簿副本。
- 状态机:维护订单簿的当前状态,供策略层查询。
下面是一个简化的事件驱动模型代码示例:
class OrderBookEventDriver:
def __init__(self):
self.order_book = {} # 价格 -> 数量
self.event_queue = []
def on_order_arrive(self, price, quantity, side):
# 订单到达事件处理
if price not in self.order_book:
self.order_book[price] = {'bid': 0, 'ask': 0}
self.order_book[price][side] += quantity
self._check_imbalance()
def on_order_cancel(self, price, quantity, side):
# 订单撤销事件处理
if price in self.order_book:
self.order_book[price][side] -= quantity
if self.order_book[price][side] <= 0:
del self.order_book[price]
def on_trade(self, price, quantity, side):
# 成交事件处理
# side 是主动方
passive_side = 'ask' if side == 'bid' else 'bid'
if price in self.order_book:
self.order_book[price][passive_side] -= quantity
if self.order_book[price][passive_side] <= 0:
del self.order_book[price]
def _check_imbalance(self):
# 检查订单簿失衡程度
best_bid = max([p for p in self.order_book if self.order_book[p]['bid'] > 0], default=0)
best_ask = min([p for p in self.order_book if self.order_book[p]['ask'] > 0], default=float('inf'))
if best_bid and best_ask < float('inf'):
imbalance = (self.order_book[best_bid]['bid'] -
self.order_book[best_ask]['ask']) / \
(self.order_book[best_bid]['bid'] +
self.order_book[best_ask]['ask'])
if abs(imbalance) > 0.7:
print(f"警告:订单簿严重失衡,失衡度 {imbalance:.2f}")
核心思想:事件驱动模型的关键在于「增量更新」。你不需要每次都重建整个订单簿,只需要根据事件类型做局部修改。这样效率高,延迟低,适合高频场景。
3.5 事件驱动模型的实战要点
在实际项目中,有几个坑需要注意:
- 事件顺序:交易所推送的事件可能乱序。我建议用时间戳+序列号双重校验,确保事件按正确顺序处理。
- 快照与增量:每隔一段时间需要拉一次全量快照,用来校正本地订单簿。因为增量更新久了,误差会累积。
- 事件丢失:网络波动可能导致事件丢失。我习惯在本地维护一个「事件序列号计数器」,如果发现跳号,立即请求重新同步。
曾经踩过的坑:有一次我忘了处理「订单簿清空」事件——交易所收盘后,订单簿会被清空。我的模型还在用旧数据,结果第二天开盘时,策略直接报错。后来我加了一个「reset」方法,每天收盘后自动重置订单簿状态。
3.6 订单簿形态变化的知识体系
下面这张图展示了订单簿动态变化的核心逻辑:
这张图展示了事件驱动模型的核心流程。三个事件源(到达、撤销、成交)产生事件,进入事件队列,由事件处理器按顺序处理,最终更新订单簿状态机。这个模型的好处是:每个事件独立处理,互不干扰,而且可以并行化。
我的建议:在实际开发中,不要把事件处理器写得过于复杂。每个事件只做一件事——更新对应的价格档位。至于策略逻辑,放在状态机外面。这样代码清晰,也容易调试。
好了,订单簿的动态变化就讲到这里。记住一句话:订单簿不是静态的,它是市场参与者用真金白银投票的结果。每一笔订单的到达、撤销、成交,都是市场情绪的体现。学会读懂这些变化,你就能在价格反转之前嗅到信号。
公众号:蓝海资料掘金营,微信deep3321