halo 的技术博客

返回

一个被忽略的事实:挂单 ≠ 成交#

你挂了一笔限价买单在买一档(best bid)。它会不会成交?什么时候成交?多数回测代码里,这个答案被悄悄写成”挂在买一就当成全部成交”—这恰恰是最隐蔽的一类前视偏差。

真相是:买一档后面排着一长队。你的单在第几位(排队位置 queue rank),决定它要等多久才轮到被市价卖单吃掉。排在队首,下一笔砸下来的卖单就是你的;排在队尾,前面几十笔单子先顶着。这篇文章用排队论把“等多久”算出来,并给出一个可被 OFI(订单流不平衡)调制的真实模型。

设想你要买入一万股,拆成挂在买一的几笔限价单。如果买一档后面已经排了八百位,你的单基本是“陪跑”——真正被市价卖单吃掉的,是前面那几百位。把这种等待量化,是执行成本建模的第一步,也是做市、抄底、套利所有“挂单等成交”策略的共同地基。一笔看似省了滑点的限价单,可能因为排在队尾而整整一天没成交、最终不得不在收盘前改市价追高——省下的 bps 全吐回去还倒贴。

一、排队位置与 Erlang 成交耗时#

把买一档简化成一个 FIFO 队列:新来的限价买单在队尾排队(位置越来越大),市价卖单从队首开始吃流动性。假设每笔订单都是单位尺寸、市价卖单的到达是强度为 λ 的泊松过程。

那么,排在第 r 位的你的单,要被吃掉必须等前面 r 笔市价卖单先后到达。成交耗时 T_r 就是 r 个独立指数间隔之和—服从 Erlang(r, λ) 分布:

P(Trt)=1k=0r1eλt(λt)kk!,E[Tr]=rλP(T_r \le t)=1-\sum_{k=0}^{r-1}\frac{e^{-\lambda t}(\lambda t)^k}{k!},\qquad E[T_r]=\frac{r}{\lambda}

这个公式的直觉很干净:期望等待时间 = 你的位置 ÷ 市价卖单到达率。位置越靠后、吃单越慢,等得越久。

下面用真实参数(λ=1.2 笔/单位时间)算几个位置:

排队位置 r期望成交时间 E[T_r]在 E[T_r] 内的成交概率
1(队首)0.830.632
32.500.577
54.170.560
108.330.542

注意一个反直觉的点:在”自身期望时间”内成交的概率只有约 0.54-0.63,而不是接近 1。因为 Erlang 是右偏的—平均要等 r/λ,但有一半以上的可能拖更久。排第 10 位,平均等 8.33 个单位时间,真到那时候还没成交的概率仍有四成多。

这个 Erlang 模型有几个明确的简化假设:订单都是单位尺寸、市价卖单到达是泊松的、队列里只有“被吃”没有“加队”、且不考虑价格档位跳动。它们让公式干净,但也意味着 E[T_r]=r/λ 是下界——真实里新限价单不断从队尾加入,会把你的相对位置往后顶,实际等待通常更长。下文的蒙特卡洛模拟才把“加队”这股反向力量加回来。

import numpy as np
from scipy.stats import poisson

def fill_prob(r, t, lam):
    """P(T_r <= t) = 1 - F_poisson(r-1; lam*t)"""
    return 1.0 - poisson.cdf(r - 1, lam * t)

LAM = 1.2
for r in [1, 3, 5, 10]:
    et = r / LAM
    print(f"r={r:>2}  E[T]={et:.2f}  P(fill by E[T])={fill_prob(r, et, LAM):.3f}")
python

不同排队位置的累计成交概率

图里四条曲线就是不同 r 下的累计成交概率。它们都单调上升、且越早越陡—越靠前的位置,早期命中概率爬升越快。这是限价单”时间价值”的来源:排队越靠后,你的单越像一张长期期权,大部分价值在等待中蒸发

二、OFI 如何调制你的成交速度#

上面的 λ 是”市价卖单吃买一”的到达率。但它不是常数,而是被订单流不平衡 OFI 驱动:

OFI=VbuyVsellVbuy+Vsell[1,1]OFI=\frac{V_{buy}-V_{sell}}{V_{buy}+V_{sell}}\in[-1,1]

你的单挂在买一,只会被市价单吃掉。买压越大(OFI 越正),砸向买一的卖单越少,你的单越慢成交。用一个简单的线性调制:

λeff=λ0max(0.05,  1αOFI)\lambda_{eff}=\lambda_0\cdot\max(0.05,\;1-\alpha\cdot OFI)

α 是灵敏度,这里取 1.0。代入 λ0=1.2:

OFIλ_effE[T_5]E[T_15]
-0.3(卖压)1.5603.219.62
0.01.2004.1712.50
+0.3(买压)0.8405.9517.86

结论非常直观:同一档位、同一排队位置,买压大时成交耗时几乎是卖压时的近 2 倍(E[T_15] 从 9.62 涨到 17.86)。这就是为什么在强势上涨段,你挂在买一的抄底单往往”挂了半天不成交”—不是没人卖,是卖压被买压盖过了。

def eff_lam(ofi, lam0=1.2, alpha=1.0):
    return lam0 * max(0.05, 1.0 - alpha * ofi)

for ofi in (-0.3, 0.0, 0.3):
    lam = eff_lam(ofi)
    print(f"OFI={ofi:+.1f}  λ_eff={lam:.3f}  E[T_5]={5/lam:.2f}  E[T_15]={15/lam:.2f}")
python

OFI 对期望成交时间的影响

这张图把”买压越大越难成交”画成三条曲线。实务含义是:如果你能估计当前 OFI,就能预测限价单的成交速度,从而决定该不该为确定性改挂市价单,或者把挂单价往前顶一档以减少排队

三、一条真实队列的蒙特卡洛模拟#

解析公式很美,但真实队列还有两股力量在拉扯:市价卖单从队首吃(消耗),新限价买单从队尾加(加队)。我们模拟一条买一档队列,前 300 分钟轻度卖压(OFI=-0.25,快成交),后 300 分钟转为买压(OFI=+0.30,明显变慢),初始队列 35 手、我们的单排第 28 位。

模拟结果(种子 100):我们的单在 t=108 分钟 成交,队列长度均值 20.3 手,整段市价卖单总吃量 114、限价买单总加量 113—消耗与加队基本平衡,队列没有失控膨胀。

模拟一条买一档队列

看图说话:上图队列长度在卖压段被持续吃掉、在买压段开始堆积;中图我们的排队位置从 28 缓慢前移,到 t=108 归零(命中,红线);下图吃单(红)与加队(蓝)在 t=300 的 OFI 翻转处明显易势。关键启示:如果你的单在卖压段没被吃掉,进入买压段后它会越埋越深—“等一等说不定就成交”在买压环境里往往是被埋的开始。

四、对策略与回测的实战含义#

1. 限价单的隐性成本 = 等待时间。 很多策略只比较”限价单省下的滑点”和”市价单的冲击成本”,却忘了限价单要付时间成本:等待期间你暴露在不利方向移动的风险里(逆向选择),也可能干脆错过整段行情。排队位置模型把这笔账量化了。

2. 用 OFI 做成交速度预测。 实时 OFI 可以估计 λ_eff,进而预测某档挂单的大致成交时间。做市与抄底策略常用它来动态决定:是往前顶一档(少排队但多付价差),还是守在原地(省价差但多等)。

3. 隐藏单 / 冰山单减少”被插队”。 完全暴露的限价单会让对手看到你的队列位置并针对性地插队(layering 等违规手法正是利用这一点)。冰山单只露一小部分,降低被针对的概率—但暴露越少,主动成交越慢,又回到权衡。

4. 回测里必须建模部分成交。 最诚实的写法是:只有排到队首且当步有市价吃单时才算成交;否则按“本步被吃穿的比例”做部分成交。绝不要假设“挂在买一 = 全量以该价成交”,那是前视+过度乐观。

5. 做市商的库存与排队联动。 做市商在买一挂单提供流动性,其队列位置直接决定库存累积速度;排队模型是做市库存风险模型(如 Avellaneda-Stoikov 风格)的输入之一——队列越深、被吃越慢,库存越难出清,报价就得随库存下移。

五、真实陷阱(别踩)#

  • 陷阱 1:挂单必成交假设。 回测里把限价单直接按挂单价全量撮合,等于默认你永远排第一—实战里你往往排第 N 位,前 N 笔卖单先把你前面的吃掉。
  • 陷阱 2:忽略队列优先级的时变。 交易所是”价格优先 + 时间优先”。同一价位的队列位置随时被新单插到后面,快照估计的队列有滞后,用快照反推位置会系统性偏差。
  • 陷阱 3:OFI 前视。 用”未来”的 OFI 来决定现在挂不挂单,等于偷看了答案。OFI 只能用到当前时刻及之前。
  • 陷阱 4:只算价差不算等待。 限价单省下的 bps 可能被等待期间的逆向选择完全吃掉,尤其在你挂单方向正好是”聪明钱”要砸的方向时。
  • 陷阱 5:抢位置的策略与规则冲突。 一些“抢排队头部”的微操作在部分交易所属于违规(spoofing/layering 边界),实盘落地前必须核对规则。

陷阱 6:跨所 / 暗池优先级不同。 不同交易所有不同优先级规则(有的按接收时间、有的显示单优先),同一套排队逻辑不能直接套到所有场所;暗池里常按成交量比例分配成交,队列位置的概念本身要重写。

六、进阶:queue-reactive 模型与真实队列估计#

上面的模型把队列长度当成外生给定。但真实订单簿里,队列长度本身是均值回复的,且由 OFI 驱动——这正是 Huang、Lehalle、Rosenbaum (2015) 的 queue-reactive 框架的核心:队列的到达/离开强度依赖于相邻档位的订单流不平衡。直觉是,当买一 OFI 很负(卖压重),买一队列会被快速吃掉、长度缩短;OFI 转正后队列又堆积。我们的 OFI 调制 λ_eff 正是这个思想的单变量近似。

实务上你怎么估计自己的排队位置?从 L2 快照(每个价位的挂单量)可以直接读到“你的单前面还有多少量”,这就是实时 queue rank。但快照有两大坑:一是延迟(你看到的队列是几十毫秒前的,期间已被吃/加过);二是隐藏单/冰山单不出现在快照里,会让你高估自己的真实位置。所以真实队列估计要叠加“这段时间内该档的成交量与新挂量”的增量校正,而不是只信某一帧快照。

小结#

限价单的成交不是”挂上就完”,而是一次排队等待:期望等待 ≈ 位置 ÷ 有效吃单率,而有效吃单率被 OFI 调制。买压大时成交慢近一倍;队列位置越靠后,时间价值蒸发越快;进入买压段后没成交的单会越埋越深。把 Erlang 模型 + OFI 调制 + 队列模拟接进你的执行层,能让你从”挂了不管”升级到”知道这笔单大概什么时候、以多大概率成交”,并在回测里诚实地建模部分成交。

代码与图表均由文中逻辑真实计算:Erlang 公式取 λ=1.2;OFI 调制 α=1.0;蒙特卡洛队列用种子 100、T=600 分钟、OFI 在 t=300 翻转。

订单簿排队位置模型:你的限价单何时成交、为何被插队
https://blog.halo26812.eu.org/blog/limit-order-book-queue
Author halo
Published at 2026年7月12日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨