1. 订单簿基础:限价单与市价单、买卖盘口、订单簿数据结构

大家好,欢迎来到《订单簿微观结构与算法交易》的第一章。

说实话,订单簿这东西,看着简单,但坑特别多。我刚开始做量化的时候,以为订单簿不就是一堆买单卖单嘛,结果第一次实盘就被盘口数据给坑了——嗯,这事后面再细说。

这一章,咱们把订单簿最核心的三个东西讲清楚:订单类型盘口结构数据怎么存。你把这些搞明白了,后面讲做市、讲套利、讲高频策略,你才能听得懂。

1.1 限价单 vs 市价单:两种最基本的玩法

先问个问题:你想买100股茅台,你怎么办?

两种选择:

  • 市价单(Market Order):不管价格,立马成交。你下单的时候,系统直接按当前最优卖价给你吃掉。
  • 限价单(Limit Order):你指定一个价格,比如1500块。只有有人愿意1500卖给你,才成交。否则就挂在那等。

我个人习惯,做策略的时候,市价单用来抢流动性限价单用来提供流动性。你想想看,如果你做高频,市价单就是你的子弹,打出去就要见血;限价单呢,更像是钓鱼,挂在那等鱼上钩。

核心区别一句话:

  • 市价单:成交优先,价格不确定(可能滑点)
  • 限价单:价格确定,成交不确定(可能不成交)

我在项目中遇到过一件事:有个同事用市价单做回测,回测里每次都按最优价成交,结果实盘一跑,滑点直接吃掉2个tick,策略直接亏钱。后来我帮他改成限价单+被动挂单,虽然成交率低了点,但每笔交易的成本稳定多了。

避坑指南: 我曾经在回测里忽略了一个细节——市价单在流动性不足的时候,会吃掉多个档位的订单。比如你买1000手,卖一只有200手,剩下的800手会继续吃卖二、卖三……价格越吃越高。这个叫「冰山效应」,后面我们会专门讲。

1.2 买卖盘口:订单簿的「脸面」

订单簿长什么样?说白了,就是一张表,左边是买单,右边是卖单。

举个例子,某时刻的盘口数据:

买价买量卖价卖量
1500.102001500.20150
1500.005001500.30300
1499.908001500.40600
1499.8012001500.50900

这里面有几个关键概念:

  • 最优买价(Bid):当前愿意出的最高买入价,这里是1500.10
  • 最优卖价(Ask):当前愿意卖的最低卖出价,这里是1500.20
  • 买卖价差(Spread):Ask - Bid = 0.10,这就是你来回交易一次的成本
  • 盘口深度(Depth):每个价位上的挂单量,比如买一挂了200手

你想想看,如果价差只有0.01,说明流动性好;如果价差有1块钱,说明这个品种没人玩,你一进去就是被收割的命。

个人经验: 我一般看盘口,第一眼先看价差,第二眼看买一卖一的挂单量。如果买一只有10手,卖一有1000手,那说明卖方压力大,价格可能要跌。这个叫「订单簿不平衡」,后面章节会专门讲怎么量化。

1.3 订单簿数据结构:怎么存才高效?

好了,现在你知道订单簿长什么样了。但问题是——计算机里怎么存?

你想想看,每秒可能有几千笔订单进来,有的撤单,有的改价,有的成交。如果存得不好,你的策略还没算完,行情已经变了。

我常用的数据结构有两种:

1.3.1 数组/列表(适合小规模)

最简单的方式,用两个列表分别存买单和卖单,按价格排序。

# 伪代码示例
bids = [(1500.10, 200), (1500.00, 500), (1499.90, 800)]
asks = [(1500.20, 150), (1500.30, 300), (1500.40, 600)]

# 获取最优买卖价
best_bid = bids[0][0]   # 1500.10
best_ask = asks[0][0]   # 1500.20
spread = best_ask - best_bid

这种方式的优点是简单,缺点是插入和删除慢。如果你每秒处理几百笔订单,还行;但如果是高频场景,每秒几万笔,那就扛不住了。

1.3.2 红黑树/有序字典(适合高频)

真正的高频系统,一般用红黑树(Red-Black Tree)或者跳表(Skip List)。Python里可以用 sortedcontainers 库,或者自己实现一个。

from sortedcontainers import SortedDict

# 买单:价格从高到低排序
bids = SortedDict({1500.10: 200, 1500.00: 500, 1499.90: 800})
# 卖单:价格从低到高排序
asks = SortedDict({1500.20: 150, 1500.30: 300, 1500.40: 600})

# 获取最优买卖价
best_bid = bids.peekitem(-1)[0]  # 最后一个元素,最高价
best_ask = asks.peekitem(0)[0]   # 第一个元素,最低价

红黑树的插入、删除、查找都是 O(log n),比列表的 O(n) 快得多。我在做期货高频策略的时候,用的就是这种结构。

注意: 别用Python自带的dict来存订单簿,因为dict是无序的。你每次都要排序,性能直接爆炸。我曾经见过一个新手,用dict存盘口,然后每次更新都sort一遍,回测跑了一晚上没跑完……

1.4 订单簿的微观结构:一张图看懂

下面这张图,是我自己画的,把订单簿的核心结构展示出来。你看一眼,基本就全明白了。

订单簿微观结构示意图 买单 (Bids) 卖单 (Asks) 买一: 1500.10 × 200 买二: 1500.00 × 500 买三: 1499.90 × 800 买四: 1499.80 × 1200 卖一: 1500.20 × 150 卖二: 1500.30 × 300 卖三: 1500.40 × 600 卖四: 1500.50 × 900 价差 = 0.10 ← 价格从高到低排序 → ← 价格从低到高排序 →

这张图里,左边是买单,价格从高到低排列;右边是卖单,价格从低到高排列。中间那个空隙,就是价差。你想想看,如果价差为0,那就是成交了。

1.5 实战中的订单簿更新逻辑

最后,咱们聊聊订单簿怎么更新。真实行情里,你会收到三种消息:

  1. 新增订单:有人挂了一个新单子,插入到对应位置
  2. 撤单:有人把挂着的单子撤了,删除对应记录
  3. 成交:买卖双方匹配成功,从订单簿里移除

我一般用事件驱动的方式来处理:

class OrderBook:
    def __init__(self):
        self.bids = SortedDict()  # 买单
        self.asks = SortedDict()  # 卖单

    def update(self, side, price, volume):
        """side: 'buy' 或 'sell', volume=0 表示撤单"""
        if side == 'buy':
            if volume == 0:
                del self.bids[price]
            else:
                self.bids[price] = volume
        elif side == 'sell':
            if volume == 0:
                del self.asks[price]
            else:
                self.asks[price] = volume

    def best_bid(self):
        return self.bids.peekitem(-1) if self.bids else None

    def best_ask(self):
        return self.asks.peekitem(0) if self.asks else None

小技巧: 我习惯在更新订单簿的时候,顺便记录一下时间戳。这样后面做回测的时候,可以精确到微秒级别,分析订单簿的微观变化。这个在讲「订单簿重建」的时候会详细展开。

好了,这一章的内容就到这里。订单簿的基础,说白了就是:知道怎么挂单、怎么看盘口、怎么存数据。你把这些搞熟了,后面讲做市策略、讲套利逻辑,你才能跟得上。

记住一句话:订单簿是市场的「心电图」。读懂了它,你就读懂了市场。


专注资料整理