订单簿深度解析:限价单与市价单、买卖盘口、构建与更新机制

做市的核心,说白了就是跟订单簿打交道。你每天盯着屏幕上跳动的数字,其实就是在看订单簿的实时状态。我刚开始做量化的时候,觉得订单簿不就是一堆买单卖单嘛,有什么好研究的?后来在实盘中被狠狠教育了几次,才明白这东西的深度远超想象。

今天我们就来彻底拆解订单簿。从最基础的概念讲起,到背后的构建逻辑,再到更新机制。嗯,这部分内容虽然基础,但如果你理解透了,做市策略的很多坑就能提前避开。

一、限价单与市价单:两种最基本的武器

先问个问题:你下单的时候,到底是在跟谁做对手盘?

答案是:要么你挂单等别人来吃,要么你直接吃掉别人的挂单。这就是限价单和市价单的本质区别。

限价单(Limit Order):你指定一个价格,只有市场价到了这个位置,你的单子才会成交。说白了,你是「守株待兔」的那一方。

  • 优点:你能控制成交价格,不会滑点
  • 缺点:可能永远成交不了,或者成交很慢
  • 做市商最爱:我们就是靠挂限价单赚价差的

市价单(Market Order):你不管价格,直接按当前最优价成交。你是「主动出击」的那一方。

  • 优点:几乎立即成交,流动性好
  • 缺点:可能产生滑点,尤其在流动性差的时候
  • 做市商最怕:市价单就是来吃我们挂单的

核心认知:做市商本质上是在「提供限价单」,而交易者(包括其他做市商)用「市价单」来消耗这些流动性。你赚的钱,就是限价单和市价单之间的价差。

我在项目中遇到过一种情况:某个小币种流动性极差,我挂的限价单深度只有几百美金。结果一个市价单砸下来,直接把我三层挂单全吃了,滑点大得离谱。从那以后,我对流动性差的品种就格外小心。

三、买卖盘口:Bid 和 Ask 的博弈

订单簿的左右两侧,就是买卖双方的战场。

  • Bid(买盘):左侧,显示所有想买入的人挂的价格和数量。最高买价叫 Best Bid。
  • Ask(卖盘):右侧,显示所有想卖出的人挂的价格和数量。最低卖价叫 Best Ask。

这两个价格之间的差距,就是 Spread(价差)。做市商赚的就是这个。

概念 含义 做市商视角
Best Bid 当前最高买入价 我们挂卖单时参考的下限
Best Ask 当前最低卖出价 我们挂买单时参考的上限
Spread Ask - Bid 我们的利润空间
Depth 各价位的挂单总量 判断市场承接力

你想想看,如果某个币的 Best Bid 是 100,Best Ask 是 100.1,Spread 只有 0.1%。这意味着市场很活跃,流动性好。但如果 Spread 是 5%,那说明这个品种要么没人玩,要么波动极大。

个人经验:我一般会监控 Spread 的实时变化。如果某个品种的 Spread 突然从 0.1% 扩大到 0.5%,我会立刻暂停做市。这通常意味着有大户在撤单,或者市场情绪突变。我曾经因为没注意这个细节,在 ETH 上亏了一笔不小的钱。

三、订单簿的构建:从零到一

订单簿不是天然存在的,它是交易所根据所有挂单实时计算出来的。我们作为做市商,需要理解它的构建逻辑,才能更好地预测市场行为。

构建过程其实很简单:

  1. 交易所收到每一笔限价单指令(包括价格、数量、方向)
  2. 按价格优先、时间优先的原则排序
  3. 买盘从高到低排列,卖盘从低到高排列
  4. 相同价格的单子,按时间先后排队

举个例子,假设当前订单簿是这样的:

卖盘(Ask):
  价格 100.5  数量 200
  价格 100.4  数量 150
  价格 100.3  数量 100  ← Best Ask

买盘(Bid):
  价格 100.2  数量 120  ← Best Bid
  价格 100.1  数量 80
  价格 100.0  数量 300

这时候 Spread = 100.3 - 100.2 = 0.1。如果你想市价买入,你会先吃掉 100.3 的 100 个,然后继续吃 100.4 的 150 个,以此类推。

关键点:订单簿的深度决定了你的市价单会造成多大的滑点。深度越厚,滑点越小。做市商的任务之一,就是提供足够的深度,让市场更平滑。

四、更新机制:每一毫秒都在变化

订单簿不是静态的。每一秒都有新的限价单进来,也有旧的单子被成交或撤销。交易所会实时推送这些变化。

常见的更新事件有四种:

  • 新增订单:有人挂了新的限价单,插入到对应位置
  • 撤销订单:有人撤回了未成交的限价单
  • 成交:限价单被市价单吃掉,或者两个限价单相遇
  • 修改订单:有人改了价格或数量(通常先撤再挂)

我习惯在代码里维护一个本地的订单簿副本。每次收到交易所的增量更新(Incremental Update),就同步修改本地数据。这样做的好处是:

  • 不用每次都请求全量快照,节省带宽
  • 可以实时计算各种指标,比如订单簿不平衡度
  • 能更快地发现异常,比如价格突变

避坑指南:我曾经在维护本地订单簿时,因为没处理好时间戳的先后顺序,导致本地数据和交易所不一致。结果我的报价策略基于错误的数据运行了整整 3 秒,亏了大概 2000 美金。从那以后,我每次收到更新都会先校验序列号,确保没有丢包或乱序。

五、用 SVG 画一张订单簿结构图

光说可能不够直观,我画了一张图,帮你把整个结构串起来。

订单簿结构示意图 卖盘 (Ask Side) 价格 100.5 数量 200 价格 100.4 数量 150 价格 100.3 ← Best Ask 数量 100 Spread = 0.1 买盘 (Bid Side) 价格 100.2 ← Best Bid 数量 120 价格 100.1 数量 80 价格 100.0 数量 300 价格优先、时间优先原则:买盘从高到低,卖盘从低到高

这张图里,左侧是卖盘,价格从低到高排列。右侧是买盘,价格从高到低排列。中间的 Spread 就是我们的利润空间。你注意看 Best Ask 和 Best Bid 的位置,它们决定了当前市场的最优买卖价格。

六、实战中的订单簿监控

理论讲完了,说说实际怎么做。我一般会在策略里维护一个这样的数据结构:

class OrderBook:
    def __init__(self):
        self.bids = {}  # 价格 -> 数量
        self.asks = {}  # 价格 -> 数量
        self.best_bid = None
        self.best_ask = None
        self.sequence = 0  # 用于校验更新顺序
    
    def update(self, side, price, size, seq):
        if seq <= self.sequence:
            return  # 丢弃过期更新
        self.sequence = seq
        
        if side == 'bid':
            if size == 0:
                self.bids.pop(price, None)
            else:
                self.bids[price] = size
            self.best_bid = max(self.bids.keys()) if self.bids else None
        else:
            if size == 0:
                self.asks.pop(price, None)
            else:
                self.asks[price] = size
            self.best_ask = min(self.asks.keys()) if self.asks else None

这段代码虽然简单,但核心逻辑都在了。每次收到更新,先检查序列号,防止乱序。然后根据 side 更新对应的字典。size 为 0 表示撤销订单。

小技巧:我习惯用有序字典(比如 Python 的 SortedDict)来维护 bids 和 asks,这样取 best bid/ask 的时间复杂度是 O(1),而不是 O(n)。在高频场景下,这点性能差异可能就是盈亏的分水岭。

好了,订单簿的核心内容就这些。理解了这个基础,后面讲报价生成逻辑的时候,你就能明白为什么我们要盯着订单簿的每一个变化了。

专注资料整理