2. 订单簿动态:订单到达、撤销与成交,订单簿事件驱动模型

订单簿这东西,说白了就是一个不停变化的“排队系统”。有人来排队,有人等不及走了,有人排到了就成交。我刚开始做高频策略的时候,总觉得订单簿就是个静态的快照,后来被市场狠狠教育了一顿——你看到的那个瞬间,下一秒可能就面目全非了。

今天我们就来聊聊订单簿到底是怎么“动”起来的。我会结合自己踩过的坑,把订单到达、撤销、成交这三个核心事件讲清楚,再给你一个能直接跑的事件驱动模型。

2.1 订单簿的三种基本事件

订单簿的变化,归根结底就三种动作:订单到达订单撤销订单成交。你想想看,市场里每秒几万笔的变动,拆开来看全是这三件事的组合。

2.1.1 订单到达(Order Arrival)

一个新订单进入系统,要么是买单,要么是卖单。它来了之后,系统会做两件事:

  • 检查是否能立即成交——如果能,直接触发成交事件
  • 如果不能成交——那就挂到订单簿上,成为新的限价单

我个人习惯把订单到达分成两类:吃单(Market Order)挂单(Limit Order)。吃单是“我要马上成交,价格无所谓”;挂单是“我只接受这个价格,不着急”。

避坑指南: 我曾经在回测里把市价单和限价单混在一起处理,结果回测收益高得离谱。后来才发现,市价单的冲击成本被我忽略了。记住:市价单一定会吃掉对手盘的最优价格,别想得太美好。

2.1.2 订单撤销(Order Cancellation)

挂上去的单子,随时可能被撤掉。高频交易里,撤单率可以高达90%以上。为什么?因为很多算法在“试探”市场——挂个单看看有没有对手盘,没有就秒撤。

撤单事件的处理逻辑其实很简单:

  1. 找到订单簿里对应的价格档位
  2. 从该档位的队列中移除这个订单
  3. 如果该档位空了,就删除这个价格档

嗯,这里要注意:撤单的顺序是先进先出(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数据,自己实现一个订单簿,你会学到比看十篇文章都多的东西。


专注资料整理