3. 订单簿数据结构:使用Python字典与列表实现Level 2订单簿,买卖盘口的维护

做量化交易的朋友都知道,订单簿是市场的核心。说白了,它就是交易所里所有未成交订单的实时快照。Level 2数据,也就是我们常说的深度行情,能看到买卖各档位的挂单量和价格。今天我就带你手撸一个精简版的Level 2订单簿。

3.1 订单簿的核心结构

我个人习惯用Python字典来组织订单簿。为什么?因为字典的键值对结构天然适合存储价格-数量映射。买盘(Bids)和卖盘(Asks)各维护一个字典,键是价格,量是数量。

举个例子,一个简单的订单簿长这样:

# 买盘:价格从高到低排序
bids = {
    100.50: 200,   # 有人愿意以100.50买200股
    100.30: 150,
    100.10: 300
}

# 卖盘:价格从低到高排序
asks = {
    100.60: 180,   # 有人愿意以100.60卖180股
    100.80: 250,
    101.00: 100
}

你想想看,这个结构是不是很直观?价格作为键,一眼就能找到对应的挂单量。

3.2 为什么需要列表辅助?

字典虽然查找快,但它不保证顺序。而订单簿的核心需求之一就是按价格排序——买盘从高到低,卖盘从低到高。这时候就需要列表来帮忙了。

我在项目中遇到过一个问题:直接用字典存储,每次取最优价都要遍历所有键,效率太低了。后来我加了一个列表来维护价格顺序,每次更新时同步排序,查询就变成了O(1)操作。

核心思路:字典负责快速查找和更新,列表负责维护价格顺序。两者配合,既快又稳。

3.3 买卖盘口的维护操作

订单簿的维护说白了就三个动作:新增、修改、删除。我们来看看具体怎么做。

3.3.1 新增订单

当新订单进来时,先判断价格是否已存在。如果存在,累加数量;如果不存在,插入新键值对,并更新价格列表。

def add_order(book, price, quantity, is_bid=True):
    """
    新增订单到订单簿
    book: 订单簿字典
    price: 价格
    quantity: 数量
    is_bid: True表示买盘,False表示卖盘
    """
    if price in book:
        book[price] += quantity
    else:
        book[price] = quantity
        # 更新价格列表(这里假设有全局的价格列表)
        if is_bid:
            bid_prices.append(price)
            bid_prices.sort(reverse=True)  # 买盘降序
        else:
            ask_prices.append(price)
            ask_prices.sort()  # 卖盘升序

3.3.2 修改订单

修改其实就是先删后加。不过要注意,修改可能改变价格,这时候要处理两个价格键。

def modify_order(book, old_price, new_price, new_quantity, is_bid=True):
    """
    修改订单:先删除旧价格,再添加新价格
    """
    # 删除旧订单
    if old_price in book:
        del book[old_price]
        # 从价格列表中移除
        if is_bid:
            bid_prices.remove(old_price)
        else:
            ask_prices.remove(old_price)
    
    # 添加新订单
    add_order(book, new_price, new_quantity, is_bid)

注意:我曾经踩过一个坑——修改订单时忘了处理数量为0的情况。如果新数量为0,应该直接删除该价格档位,而不是保留一个空档。否则后续计算深度时会出问题。

3.3.3 删除订单

删除相对简单,但要注意完全成交的情况。当某个价格档位的数量被吃光时,要彻底清除。

def remove_order(book, price, quantity, is_bid=True):
    """
    从订单簿中移除指定数量的订单
    返回剩余数量(如果完全移除则返回0)
    """
    if price not in book:
        return 0
    
    current_qty = book[price]
    if quantity >= current_qty:
        # 完全移除该档位
        del book[price]
        if is_bid:
            bid_prices.remove(price)
        else:
            ask_prices.remove(price)
        return 0
    else:
        # 部分移除
        book[price] -= quantity
        return book[price]

3.4 完整的订单簿类

把上面的逻辑封装成一个类,用起来就顺手多了。我一般这样设计:

class OrderBook:
    def __init__(self):
        self.bids = {}      # 买盘字典
        self.asks = {}      # 卖盘字典
        self.bid_prices = []  # 买盘价格列表(降序)
        self.ask_prices = []  # 卖盘价格列表(升序)
    
    def update(self, side, price, quantity):
        """
        更新订单簿
        side: 'bid' 或 'ask'
        price: 价格
        quantity: 数量(0表示删除该档位)
        """
        book = self.bids if side == 'bid' else self.asks
        prices = self.bid_prices if side == 'bid' else self.ask_prices
        
        if quantity == 0:
            # 删除档位
            if price in book:
                del book[price]
                prices.remove(price)
        else:
            # 新增或更新
            is_new = price not in book
            book[price] = quantity
            if is_new:
                prices.append(price)
                if side == 'bid':
                    prices.sort(reverse=True)
                else:
                    prices.sort()
    
    def get_top(self, n=5):
        """
        获取前n档买卖盘口
        """
        top_bids = [(p, self.bids[p]) for p in self.bid_prices[:n]]
        top_asks = [(p, self.asks[p]) for p in self.ask_prices[:n]]
        return top_bids, top_asks
    
    def get_spread(self):
        """
        获取买卖价差
        """
        if not self.bid_prices or not self.ask_prices:
            return None
        best_bid = self.bid_prices[0]
        best_ask = self.ask_prices[0]
        return best_ask - best_bid

3.5 知识体系结构图

下面这张图帮你理清订单簿数据结构的核心逻辑:

订单簿数据结构核心逻辑 订单簿 (OrderBook) 买盘 (Bids) 卖盘 (Asks) 字典: {价格: 数量} 列表: [价格] 降序 字典: {价格: 数量} 列表: [价格] 升序 核心操作:新增 / 修改 / 删除 输出:最优价 / 深度 / 价差

3.6 实际应用中的注意事项

嗯,这里要注意几个实战中容易翻车的地方:

问题 表现 解决方案
浮点数精度 价格比较时出现意外 使用Decimal或乘以10000转整数
并发更新 数据不一致 加锁或使用原子操作
内存泄漏 废弃价格未清理 定期清理数量为0的档位
排序效率 频繁排序拖慢性能 使用bisect.insort插入排序

我的经验:刚开始做订单簿时,我直接用sorted()每次全量排序,结果回测速度慢得离谱。后来改用bisect模块的insort方法,只在插入时做一次二分查找,性能提升了10倍以上。代码就一行:

import bisect
bisect.insort(bid_prices, new_price)  # 自动插入到正确位置

说白了,订单簿的数据结构并不复杂,难的是在毫秒级行情下保持数据的一致性和高性能。字典加列表的组合,是我用过的最平衡的方案——既保留了字典的O(1)查找,又通过列表保证了顺序。

你想想看,如果让你自己设计一个订单簿,你会怎么选?我个人觉得,先把基础结构搭好,后续优化才有方向。毕竟,跑得稳比跑得快更重要。


公众号:蓝海资料掘金营,微信deep3321