一个被忽略的事实:挂单 ≠ 成交#
你挂了一笔限价买单在买一档(best bid)。它会不会成交?什么时候成交?多数回测代码里,这个答案被悄悄写成”挂在买一就当成全部成交”—这恰恰是最隐蔽的一类前视偏差。
真相是:买一档后面排着一长队。你的单在第几位(排队位置 queue rank),决定它要等多久才轮到被市价卖单吃掉。排在队首,下一笔砸下来的卖单就是你的;排在队尾,前面几十笔单子先顶着。这篇文章用排队论把“等多久”算出来,并给出一个可被 OFI(订单流不平衡)调制的真实模型。
设想你要买入一万股,拆成挂在买一的几笔限价单。如果买一档后面已经排了八百位,你的单基本是“陪跑”——真正被市价卖单吃掉的,是前面那几百位。把这种等待量化,是执行成本建模的第一步,也是做市、抄底、套利所有“挂单等成交”策略的共同地基。一笔看似省了滑点的限价单,可能因为排在队尾而整整一天没成交、最终不得不在收盘前改市价追高——省下的 bps 全吐回去还倒贴。
一、排队位置与 Erlang 成交耗时#
把买一档简化成一个 FIFO 队列:新来的限价买单在队尾排队(位置越来越大),市价卖单从队首开始吃流动性。假设每笔订单都是单位尺寸、市价卖单的到达是强度为 λ 的泊松过程。
那么,排在第 r 位的你的单,要被吃掉必须等前面 r 笔市价卖单先后到达。成交耗时 T_r 就是 r 个独立指数间隔之和—服从 Erlang(r, λ) 分布:
这个公式的直觉很干净:期望等待时间 = 你的位置 ÷ 市价卖单到达率。位置越靠后、吃单越慢,等得越久。
下面用真实参数(λ=1.2 笔/单位时间)算几个位置:
| 排队位置 r | 期望成交时间 E[T_r] | 在 E[T_r] 内的成交概率 |
|---|---|---|
| 1(队首) | 0.83 | 0.632 |
| 3 | 2.50 | 0.577 |
| 5 | 4.17 | 0.560 |
| 10 | 8.33 | 0.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 越正),砸向买一的卖单越少,你的单越慢成交。用一个简单的线性调制:
α 是灵敏度,这里取 1.0。代入 λ0=1.2:
| OFI | λ_eff | E[T_5] | E[T_15] |
|---|---|---|---|
| -0.3(卖压) | 1.560 | 3.21 | 9.62 |
| 0.0 | 1.200 | 4.17 | 12.50 |
| +0.3(买压) | 0.840 | 5.95 | 17.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,就能预测限价单的成交速度,从而决定该不该为确定性改挂市价单,或者把挂单价往前顶一档以减少排队。
三、一条真实队列的蒙特卡洛模拟#
解析公式很美,但真实队列还有两股力量在拉扯:市价卖单从队首吃(消耗),新限价买单从队尾加(加队)。我们模拟一条买一档队列,前 300 分钟轻度卖压(OFI=-0.25,快成交),后 300 分钟转为买压(OFI=+0.30,明显变慢),初始队列 35 手、我们的单排第 28 位。
import numpy as np
LAM_SIM, ALPHA, T = 0.20, 1.0, 600
rng = np.random.default_rng(100)
queue_len, our_rank = 35, 28
qlen_hist, ourpos_hist, eat_hist, add_hist = [], [], [], []
for t in range(T):
ofi = -0.25 if t < 300 else 0.30
lam_eat = LAM_SIM * max(0.05, 1.0 - ALPHA * ofi) # 吃单(市价卖)
lam_add = LAM_SIM * max(0.05, 1.0 + ALPHA * ofi) # 加队(限价买)
eat, add = rng.poisson(lam_eat), rng.poisson(lam_add)
if eat >= our_rank: # 被吃穿 -> 成交
our_rank = 0
else:
our_rank = max(1, our_rank - eat) # 队首被吃,我们前移
queue_len = max(1, queue_len - eat + add)
qlen_hist.append(queue_len); ourpos_hist.append(our_rank)
eat_hist.append(eat); add_hist.append(add)python模拟结果(种子 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 翻转。