2. 订单簿动态:订单到达、撤销与成交,订单簿事件驱动模型
订单簿这东西,说白了就是一个不停变化的“排队系统”。有人来排队,有人等不及走了,有人排到了就成交。我刚开始做高频策略的时候,总觉得订单簿就是个静态的快照,后来被市场狠狠教育了一顿——你看到的那个瞬间,下一秒可能就面目全非了。
今天我们就来聊聊订单簿到底是怎么“动”起来的。我会结合自己踩过的坑,把订单到达、撤销、成交这三个核心事件讲清楚,再给你一个能直接跑的事件驱动模型。
2.1 订单簿的三种基本事件
订单簿的变化,归根结底就三种动作:订单到达、订单撤销、订单成交。你想想看,市场里每秒几万笔的变动,拆开来看全是这三件事的组合。
2.1.1 订单到达(Order Arrival)
一个新订单进入系统,要么是买单,要么是卖单。它来了之后,系统会做两件事:
- 检查是否能立即成交——如果能,直接触发成交事件
- 如果不能成交——那就挂到订单簿上,成为新的限价单
我个人习惯把订单到达分成两类:吃单(Market Order)和挂单(Limit Order)。吃单是“我要马上成交,价格无所谓”;挂单是“我只接受这个价格,不着急”。
2.1.2 订单撤销(Order Cancellation)
挂上去的单子,随时可能被撤掉。高频交易里,撤单率可以高达90%以上。为什么?因为很多算法在“试探”市场——挂个单看看有没有对手盘,没有就秒撤。
撤单事件的处理逻辑其实很简单:
- 找到订单簿里对应的价格档位
- 从该档位的队列中移除这个订单
- 如果该档位空了,就删除这个价格档
嗯,这里要注意:撤单的顺序是先进先出(FIFO)还是按比例分配,取决于交易所的规则。国内A股是价格优先、时间优先,所以撤单时你得知道这个订单在队列里的位置。
2.1.3 订单成交(Order Execution)
成交是订单簿最核心的事件。当一个新订单到达时,如果它能和对手盘匹配,就会触发成交。成交的逻辑是:
- 从最优价格开始匹配
- 同一价格下,按时间顺序匹配
- 直到新订单全部成交,或者对手盘被吃光
我做过一个统计:在流动性好的股票上,一笔大单可能触发几十笔小单的连续成交。这时候如果你只盯着最后一笔成交价看,很容易被误导。
2.2 事件驱动模型:让订单簿“活”起来
静态的订单簿快照没什么用。真正有价值的是事件流——每一笔变动都触发一个事件,然后更新订单簿状态。这就是事件驱动模型的核心思想。
我画了一张图,帮你理解这个流程:
这个流程其实很简单:事件来了 → 处理器判断 → 更新订单簿 → 输出新事件。但真正实现的时候,细节多得让人头疼。
2.3 代码实现:一个最小的事件驱动订单簿
下面是我写的一个简化版订单簿。它只处理限价单,但核心逻辑和真实系统是一样的。
class OrderBook:
def __init__(self):
self.bids = {} # 买单:价格 -> 订单队列
self.asks = {} # 卖单:价格 -> 订单队列
self.order_map = {} # 订单ID -> 订单信息
def process_event(self, event):
"""处理单个事件"""
event_type = event['type']
if event_type == 'NEW':
self._add_order(event)
elif event_type == 'CANCEL':
self._cancel_order(event)
elif event_type == 'TRADE':
self._execute_order(event)
def _add_order(self, event):
"""添加新订单"""
order = {
'id': event['order_id'],
'side': event['side'],
'price': event['price'],
'quantity': event['quantity'],
'timestamp': event['timestamp']
}
self.order_map[order['id']] = order
if order['side'] == 'BUY':
# 检查是否能和卖单匹配
self._match_order(order, self.asks)
if order['quantity'] > 0:
self._add_to_book(order, self.bids)
else:
self._match_order(order, self.bids)
if order['quantity'] > 0:
self._add_to_book(order, self.asks)
def _match_order(self, order, opposite_book):
"""尝试匹配订单"""
# 按价格排序:买单从高到低,卖单从低到高
sorted_prices = sorted(opposite_book.keys(), reverse=(order['side'] == 'SELL'))
for price in sorted_prices:
if order['quantity'] <= 0:
break
# 检查价格是否匹配
if (order['side'] == 'BUY' and order['price'] < price) or \
(order['side'] == 'SELL' and order['price'] > price):
break
queue = opposite_book[price]
while queue and order['quantity'] > 0:
head_order = queue[0]
trade_qty = min(order['quantity'], head_order['quantity'])
# 执行成交
print(f"成交: {trade_qty}@{price}")
order['quantity'] -= trade_qty
head_order['quantity'] -= trade_qty
if head_order['quantity'] == 0:
queue.pop(0)
del self.order_map[head_order['id']]
if not queue:
del opposite_book[price]
def _add_to_book(self, order, book):
"""将未成交部分挂到订单簿"""
if order['price'] not in book:
book[order['price']] = []
book[order['price']].append(order)
def _cancel_order(self, event):
"""撤销订单"""
order_id = event['order_id']
if order_id not in self.order_map:
return
order = self.order_map[order_id]
book = self.bids if order['side'] == 'BUY' else self.asks
if order['price'] in book:
queue = book[order['price']]
# 找到并移除订单
for i, o in enumerate(queue):
if o['id'] == order_id:
queue.pop(i)
break
if not queue:
del book[order['price']]
del self.order_map[order_id]
def get_top_of_book(self):
"""获取最优买卖价"""
best_bid = max(self.bids.keys()) if self.bids else None
best_ask = min(self.asks.keys()) if self.asks else None
return best_bid, best_ask
2.4 事件驱动模型的关键设计点
写订单簿事件驱动模型,有几个地方特别容易翻车:
| 设计点 | 说明 | 我的建议 |
|---|---|---|
| 事件顺序 | 同一时间戳的事件,处理顺序可能影响结果 | 按交易所的序列号排序,不要依赖时间戳 |
| 部分成交 | 一个订单可能被拆成多次成交 | 用循环处理,直到订单完全成交或无法继续 |
| 价格档位管理 | 价格档位可能被清空,需要及时删除 | 每次操作后检查队列是否为空 |
| 内存占用 | 高频数据下,订单簿可能占用大量内存 | 使用数组或池化技术,减少对象创建 |
2.5 实战中的坑:我踩过的那些雷
讲几个真实案例吧。第一个是关于撤单延迟的。我曾经写过一个策略,假设撤单是瞬间完成的。结果实盘的时候,撤单指令发出去到真正从订单簿消失,中间有几十毫秒的延迟。这几十毫秒里,我的订单可能已经被成交了。后来我加了一个“撤单确认”的回调,才解决这个问题。
第二个坑是订单簿快照和增量更新的同步。交易所通常会每隔一段时间发一个全量快照,然后发增量更新。如果你在快照和增量之间断了一秒,那订单簿就全乱了。我的做法是:收到快照后,先暂停处理增量,等快照完全加载完再继续。
好了,关于订单簿动态和事件驱动模型,我就讲这么多。记住:理论再漂亮,不如动手写一遍。找个交易所的Level 2数据,自己实现一个订单簿,你会学到比看十篇文章都多的东西。