3、订单簿数据结构:使用Python字典/列表模拟订单簿、价格-数量映射关系。

做量化交易这几年,我每天打交道最多的数据结构就是订单簿。说白了,订单簿就是交易所里那个实时滚动的「挂单大屏」——左边是卖单,右边是买单,价格从高到低排得整整齐齐。

你想想看,一个订单簿里最核心的信息是什么?其实就是两样东西:价格数量。谁在什么价位上挂了多少钱,这就是全部秘密。

3.1 订单簿的核心结构

我个人习惯把订单簿拆成三个部分来理解:

  • 买盘(Bids):按价格从高到低排列,最高价在最前面
  • 卖盘(Asks):按价格从低到高排列,最低价在最前面
  • 时间戳:记录当前快照的时刻

为什么买盘要降序、卖盘要升序?因为买方当然希望用最低价买,但挂单时得按市场规则来——想立刻成交就得挂高价。所以买盘最高价就是「最佳买价」,卖盘最低价就是「最佳卖价」。这个价差,就是咱们常说的 spread。

3.2 用Python字典模拟订单簿

Python里最直观的模拟方式,就是用字典做价格-数量的映射。我在项目中遇到过好几次,新手喜欢用列表存价格和数量,结果查价的时候慢得不行。字典的哈希查找,才是正经路子。

来看一个最简单的例子:

# 模拟一个BTC/USDT的订单簿快照
order_book = {
    'bids': {  # 买盘:价格 -> 数量
        50000.0: 1.5,
        49990.0: 2.3,
        49980.0: 0.8,
        49970.0: 3.1,
        49960.0: 1.2
    },
    'asks': {  # 卖盘:价格 -> 数量
        50010.0: 0.9,
        50020.0: 1.7,
        50030.0: 2.5,
        50040.0: 1.1,
        50050.0: 3.0
    },
    'timestamp': 1700000000
}

嗯,这里要注意:字典的键是价格,值是挂单数量。为什么用价格做键?因为订单簿的核心操作就是「查某个价位的挂单量」,字典的O(1)查询速度刚好满足。

3.3 价格-数量映射关系的设计要点

我曾经踩过一个坑:直接用浮点数做字典键。浮点数在计算机里是不精确的,比如 50000.1 和 50000.10000000001 会被当成两个不同的键。后来我统一用整数表示价格,比如把价格乘以10000存成整数,或者直接用Decimal类型。

这里给出一个更健壮的实现:

from decimal import Decimal

class OrderBook:
    def __init__(self):
        self.bids = {}  # 买盘
        self.asks = {}  # 卖盘
        self.last_update = None
    
    def update(self, side: str, price: float, quantity: float):
        """更新订单簿"""
        # 用Decimal避免浮点精度问题
        p = Decimal(str(price))
        q = Decimal(str(quantity))
        
        if side == 'buy':
            if q == 0:
                self.bids.pop(p, None)  # 数量为0表示撤单
            else:
                self.bids[p] = q
        elif side == 'sell':
            if q == 0:
                self.asks.pop(p, None)
            else:
                self.asks[p] = q
        
        self.last_update = time.time()
    
    def get_best_bid(self) -> tuple:
        """获取最佳买价"""
        if not self.bids:
            return None
        best_price = max(self.bids.keys())
        return (best_price, self.bids[best_price])
    
    def get_best_ask(self) -> tuple:
        """获取最佳卖价"""
        if not self.asks:
            return None
        best_price = min(self.asks.keys())
        return (best_price, self.asks[best_price])
💡 实战技巧:我在做高频策略时,发现字典虽然快,但每次取最佳价都要遍历所有键。后来我改用「有序字典」或者「堆结构」来维护价格顺序,性能提升很明显。不过对于学习阶段,字典完全够用。

3.4 订单簿的深度聚合

光有价格-数量映射还不够,咱们经常需要看「累计深度」——比如在某个价格以下总共挂了多少买单。这个在计算市场冲击成本时特别有用。

举个例子:你想买入10个BTC,不能只看最佳卖价那0.9个,得把后面几个价位的单子都吃掉。这时候就需要算累计量:

def calculate_depth(order_book: dict, side: str, levels: int = 5):
    """计算指定深度的累计挂单量"""
    if side == 'bids':
        prices = sorted(order_book[side].keys(), reverse=True)
    else:
        prices = sorted(order_book[side].keys())
    
    cumulative = 0
    depth_data = []
    
    for i, price in enumerate(prices[:levels]):
        qty = order_book[side][price]
        cumulative += qty
        depth_data.append({
            'price': price,
            'quantity': qty,
            'cumulative': cumulative
        })
    
    return depth_data

# 使用示例
depth = calculate_depth(order_book, 'asks', levels=3)
for level in depth:
    print(f"价格: {level['price']:.2f}, "
          f"数量: {level['quantity']:.4f}, "
          f"累计: {level['cumulative']:.4f}")
核心要点:订单簿的本质就是「价格-数量」的二维映射。买盘降序、卖盘升序,这是全球交易所的统一规则。用Python字典模拟时,注意浮点数精度问题,建议用Decimal或整数化处理。

3.5 订单簿的可视化结构

为了让你更直观地理解,我画了一张订单簿的结构图。这张图展示了买盘和卖盘如何构成一个完整的市场深度:

订单簿数据结构 买盘 (Bids) 价格从高到低排列 价格: 50000.0 数量: 1.5 价格: 49990.0 数量: 2.3 价格: 49980.0 数量: 0.8 价格: 49970.0 数量: 3.1 价格: 49960.0 数量: 1.2 ↑ 价格递减 Spread 卖盘 (Asks) 价格从低到高排列 价格: 50010.0 数量: 0.9 价格: 50020.0 数量: 1.7 价格: 50030.0 数量: 2.5 价格: 50040.0 数量: 1.1 价格: 50050.0 数量: 3.0 ↑ 价格递增 最佳买价: 50000.0 | 最佳卖价: 50010.0 | 价差: 10.0
⚠️ 避坑指南:我曾经在实盘环境中直接用字典存储订单簿,结果遇到一个极端行情——同一价位上连续撤单和挂单,字典的键被频繁增删,导致内存碎片化严重。后来我改用「跳表」结构来维护价格层级,才解决了这个问题。不过对于学习和回测,字典模拟完全够用。

3.6 实战:从交易所API获取订单簿

光说不练假把式。咱们用币安的公开API拉一份真实的订单簿看看:

import requests
import json

def fetch_order_book(symbol: str, limit: int = 10):
    """从币安获取订单簿数据"""
    url = f"https://api.binance.com/api/v3/depth"
    params = {
        'symbol': symbol.upper(),
        'limit': limit
    }
    
    response = requests.get(url, params=params)
    data = response.json()
    
    # 转换成咱们的字典格式
    order_book = {
        'bids': {},
        'asks': {},
        'timestamp': data['lastUpdateId']
    }
    
    # 注意:交易所返回的是字符串,需要转成数值
    for bid in data['bids']:
        price = float(bid[0])
        qty = float(bid[1])
        if qty > 0:  # 忽略空单
            order_book['bids'][price] = qty
    
    for ask in data['asks']:
        price = float(ask[0])
        qty = float(ask[1])
        if qty > 0:
            order_book['asks'][price] = qty
    
    return order_book

# 拉取BTC/USDT的订单簿
book = fetch_order_book('btcusdt', limit=5)
print(f"最佳买价: {max(book['bids'].keys()):.2f}")
print(f"最佳卖价: {min(book['asks'].keys()):.2f}")
print(f"买盘数据: {book['bids']}")
print(f"卖盘数据: {book['asks']}")

运行这段代码,你就能看到真实的订单簿长什么样了。你会发现,真实市场的挂单量分布其实很不均匀——有些价位上堆着大单,有些价位上空空如也。这些信息,就是咱们做策略的「矿藏」。

📌 个人经验:我刚开始做订单簿分析时,总喜欢把数据存得特别细,每个价格变动都记录。后来发现,对于大多数策略来说,关注「关键价位」就够了——比如整数关口、前高前低、大单堆积处。数据不是越多越好,够用就行。

好了,这一章咱们把订单簿的数据结构讲透了。从字典模拟到API实战,从价格映射到深度聚合,这些都是后续分析的基础。记住一句话:订单簿就是市场的「心电图」,读懂它,你就能感知到资金的流向和博弈的节奏。

专注资料整理