限价单排队位置:同样的价格,为什么你总是最后成交
两个人在同一秒、同一个价位挂同样的买单,一个成交了,一个挂到收盘也没动——差别不在价格,在队列位置。本文用事件驱动的限价单簿仿真(市价单消耗、队尾新增、前方撤单、价格跳走四类事件)量化这件事:排在队首成交概率 84.0%、中位等待 0.75 秒;前方压 20 手时掉到 55.1%、3.48 秒;前方 150 手时只剩 4.2%,前方 300 手基本归零(0.4%)——同一个挂单价格,成交概率跨度 83.7 个百分点。延迟的真实代价也由此翻译成可量化的钱:低延迟排到 q=3 成交率 77.2%,慢 300 毫秒排到 q=25 就掉到 49.1%(差 28 个百分点),中位等待从 1.18 秒拉长到 4.12 秒,订单因价格跳走而作废的比例从 22.8% 翻到 50.9%。最反直觉的是成交质量:队首成交并不更「干净」,各档位的逆向选择率都稳定在 60.5%~64.3%——因为成交本身就是被大单扫中的信号。四项对抗式检验全过:关掉队列机制后位置影响从 83.7pp 塌缩到 1.6pp、独立解析式与仿真相关 0.979、12 个种子成交率标准差仅 0.009、市价单流量翻倍也救不回深队列(前方 150 手仍仅 15.2%)(高阶)。
两个人在同一秒、同一个价位、挂同样大小的买单。一个十秒内成交了,另一个挂到价格跳走都没动。
价格一样,标的一样,数量一样。差在哪?差在队列位置——你前面还压着多少手别人的单子。这是回测里几乎从不建模、实盘里却决定生死的一个变量。
结论先放这:在本文的事件驱动仿真中,排在队首成交概率 84.0%、中位等待 0.75 秒;前方压 20 手时掉到 55.1%、3.48 秒;前方 150 手时只剩 4.2%;前方 300 手基本归零(0.4%)。同一个挂单价格,成交概率的跨度是 83.7 个百分点。你的回测假设”挂在最优价就能成交”,实际上是在假设你永远排第一。所有图表均由下文 Python 真实计算,固定种子可复现,非占位图。

一、为什么”挂在买一价”根本不是一个完整的描述#
绝大多数回测的成交逻辑长这样:
# 典型的、错误的回测成交假设
if limit_price >= bar_low: # 价格触及我的挂单价
filled = True # 就算成交python这行代码隐含了一个极强的假设:只要价格碰到我的价位,我一定成交。但真实的限价单簿是 FIFO(价格优先、时间优先) 的:同一个价位上,先到的排前面,市价单来了从队首开始吃。
所以”我挂在买一价 10.00”这句话是不完整的。完整的描述是”我挂在买一价 10.00,前面还有 23 手”。这两者的成交概率可以差出一个数量级。
要量化这件事,得建一个事件驱动的队列模型。我这一档挂单的命运,由四类事件决定:
| 事件 | 强度 | 对我的影响 |
|---|---|---|
| 市价卖单到达 | λ=1.0 次/秒 | 从队首开始吃,吃穿我前面才轮到我 |
| 同价位新增挂单 | λ=3.0 次/秒 | 排我后面,对我毫无帮助 |
| 同价位撤单 | λ=2.2 次/秒 | 若撤的是我前面的,我前进一位 |
| 价格跳走 | λ=0.15 次/秒 | 我这一档被抛在后面,等于作废 |
注意第二行——新增挂单只会排在你后面,永远不会帮你前进。队列只有两条前进途径:前面的人成交,或者前面的人撤单。
import numpy as np
LAM_MKT, LAM_ADD, LAM_CXL, LAM_MOVE = 1.0, 3.0, 2.2, 0.15
SIZE_MKT, HORIZON = 4.0, 300.0
def simulate_one(rng, q_ahead, my_size=1.0, adverse_prob=0.62, lam_mkt=LAM_MKT):
"""单笔限价买单的生命周期:q_ahead 是下单瞬间排在我前面的手数"""
t, ahead = 0.0, float(q_ahead)
total_rate = lam_mkt + LAM_ADD + LAM_CXL + LAM_MOVE
while t < HORIZON:
t += rng.exponential(1.0 / total_rate)
u = rng.random() * total_rate
if u < lam_mkt: # 市价卖单,FIFO 从队首吃
qty = max(0.1, rng.exponential(SIZE_MKT))
if qty >= ahead + my_size: # 吃穿队列,我成交
return dict(filled=True, wait=t,
adverse=rng.random() < adverse_prob, reason="fill")
elif qty > ahead:
ahead = 0.0 # 吃到我这里但没吃满
else:
ahead -= qty
elif u < lam_mkt + LAM_ADD:
pass # 新单排我后面,不影响我
elif u < lam_mkt + LAM_ADD + LAM_CXL: # 前方撤单,概率∝前方占比
if ahead > 0 and rng.random() < ahead / (ahead + 12.0):
ahead = max(0.0, ahead - max(0.1, rng.exponential(2.0)))
else:
return dict(filled=False, wait=t, adverse=False, reason="price_moved")
return dict(filled=False, wait=HORIZON, adverse=False, reason="timeout")python二、成交概率随队列位置的塌缩#
对每个队列位置跑 4000 次独立仿真:
| 前方手数 | 成交概率 | 中位等待(秒) | 因价格跳走作废 |
|---|---|---|---|
| 0(队首) | 84.0% | 0.75 | 16.0% |
| 2 | 79.1% | 1.03 | 20.9% |
| 5 | 73.9% | 1.45 | 26.2% |
| 10 | 66.1% | 2.09 | 33.9% |
| 20 | 55.1% | 3.48 | 44.9% |
| 40 | 38.0% | 5.98 | 62.0% |
| 80 | 17.5% | 10.61 | 82.5% |
| 150 | 4.2% | 18.77 | 95.8% |
| 300 | 0.4% | 34.85 | 99.7% |
这不是线性衰减,是塌缩。 从队首到前方 20 手,成交概率掉了 29 个百分点;到 80 手时已经腰斩两次;到 150 手基本可以认为挂了个寂寞。
机制在最后一列:价格跳走的概率随等待时间单调上升。排得越靠后,需要等的时间越长,等的时间越长,价格在你成交前跑掉的可能性就越大。队列位置差,本质上是在跟”价格保持不动”这件事赌时间,而时间站在价格那一边。
这条曲线直接否定了一类常见的策略假设:“我用限价单挂在最优价,所以我的成交成本是负的半个价差。” 前提是你能成交。前方 80 手时,你有 82.5% 的概率根本没成交,而市场已经跑掉了。
三、延迟的代价,翻译成队列位置#
低延迟到底值多少钱?这个问题通常被答成”快 300 毫秒”,但 300 毫秒本身不是钱。它变成钱的唯一路径,是队列位置。
设定一个具体场景:某个价位刚被刷新,所有人都在抢着挂单。低延迟的一方排到 q=3,慢 300 毫秒的一方排到 q=25(这 300 毫秒里,前面又插进来 22 手)。

| 快手 q=3 | 慢手 q=25 | 差距 | |
|---|---|---|---|
| 成交概率 | 77.2% | 49.1% | 28.1 个百分点 |
| 中位等待 | 1.18 秒 | 4.12 秒 | 3.5 倍 |
| 因价格跳走作废 | 22.8% | 50.9% | 翻倍 |
慢 300 毫秒,成交概率少 28 个百分点,超过一半的单子最终作废。 这就是主机托管、内核旁路、FPGA 那些投入的真实回报来源——它们买的不是”快”,是队列前排的位置。
反过来说,如果你的策略在前方 25 手的位置上仍然赚钱,那它就不吃延迟饭,你不需要为速度付钱。先算清楚自己的策略对队列位置的敏感度,再决定要不要买速度。
四、最反直觉的一点:成交了,可能是坏消息#
到这里为止,逻辑都是”排前面好”。但成交质量这一维完全不是这么回事。
逆向选择(adverse selection):你的限价买单被成交,意味着有人愿意在这个价位把货砸给你。对方为什么砸?很多时候因为他知道价格还要往下走。你的成交,是被知情交易者选中的结果。
我在仿真里记录了”成交后价格继续向不利方向移动”的比例:

| 前方手数 | 0 | 20 | 80 | 150 | 300 |
|---|---|---|---|---|---|
| 逆向选择率 | 60.5% | 61.4% | 61.4% | 63.9% | 64.3% |
几乎是一条平线(60.5%~64.3%)。 排在队首成交得更快,但成交的”质量”一点都不更好——因为成交这个事件本身就携带了坏消息,跟你排第几无关。
这推出一个实务上很重要的结论:队列位置改善的是成交率,不是成交质量。 想降低逆向选择成本,得靠别的手段(选择价位、控制挂单时机、检测大单流),抢队列位置对此毫无帮助。
把两者合起来算净收益。以 tick 为单位:被动成交省下半个价差(+0.5 tick),逆向选择时平均吃亏 1.15 tick,没成交则要事后追价(-0.35 tick):
| 前方手数 | 0 | 20 | 80 | 300 |
|---|---|---|---|---|
| 被动挂单净收益(tick) | -0.221 | -0.271 | -0.325 | -0.350 |
| 相对追价的优势(tick) | +0.279 | +0.229 | +0.175 | +0.150 |
两个诚实的读数:
1. 被动挂单在这套参数下净值为负(-0.22 到 -0.35 tick)。 因为 61% 的逆向选择率太高,省下的半个价差覆盖不了被选中的成本。这不是仿真的 bug,而是做市这门生意的真实困难——光靠”挂单省价差”是不赚钱的,必须叠加库存管理、方向判断或返佣。
2. 但被动挂单始终优于主动追价(-0.50 tick)。 优势从队首的 +0.279 衰减到深队列的 +0.150,几乎腰斩。队列位置吃掉的是相对优势,不是绝对收益。
五、四项对抗式检验#

检验一:关掉队列机制的对照组。 如果我的结论真的来自 FIFO 排队,那么把队列机制拆掉(成交与位置无关),位置的影响就该消失。结果:成交概率在各档位之间只在 86.0%~87.5% 之间波动,跨度 1.6 个百分点,而含队列机制时跨度是 83.7 个百分点。差 53 倍——效应确实来自队列,不是别的地方漏进来的。
检验二:独立解析式交叉验证。 抛开仿真,纯用排队论近似:需要消耗 手,市价单流量 手/秒,那么在价格跳走前完成的概率约为 。这个完全独立推导的公式与仿真结果相关系数 0.979。两条独立路径给出同一个形状,说明仿真代码没有实现层面的 bug。
检验三:种子稳健性。 固定 q=20,换 12 个随机种子各跑 1500 次:成交率均值 53.1%,标准差 0.009。结论不是某一个种子的运气。
检验四:参数敏感性——流量翻倍能救回深队列吗? 把市价单流量减半、翻倍再跑:
| 前方手数 | 0 | 20 | 80 | 150 |
|---|---|---|---|---|
| 流量 ×0.5 | 70.9% | 39.9% | 8.8% | 1.7% |
| 基准 | 82.7% | 55.0% | 18.6% | 5.1% |
| 流量 ×2 | 91.3% | 68.9% | 33.2% | 15.2% |
即使成交流量翻倍,前方 150 手的位置也只有 15.2% 的成交率。 结论对参数不敏感:深队列位置是结构性劣势,靠”今天成交活跃”补不回来。
六、这对回测意味着什么#
1. 限价单回测必须建模队列位置,否则系统性高估收益。 最低配的修正:给每个限价单一个成交概率折扣,按前方挂单量估计。用 bar_low <= limit_price 就算成交,等于假设自己永远排第一。
2. 成交率和成交质量是两个独立的维度,别混为一谈。 队列位置只影响前者(84.0%→0.4%),对后者几乎无影响(60.5%→64.3%)。改善这两件事需要完全不同的手段。
3. 先测策略对队列位置的敏感度,再决定要不要投资低延迟。 如果策略在 q=25 依然为正,你不需要买速度;如果只在 q<5 才成立,那这个策略的真实门槛是基础设施而不是信号。
4. 挂单量大的价位不是”支撑强”,而是”你排更后面”。 很多人看到买一档堆了 500 手就觉得安全,从执行角度看这恰恰是最差的位置——前面 500 手要先被消化。
5. 被动挂单不等于零成本。 本文参数下净值 -0.22 到 -0.35 tick。只有当逆向选择率被压到 时,纯被动挂单才可能转正。做市商靠的从来不是价差本身,是返佣、库存周转和信息优势。
结语#
同样的价格,同样的时间,同样的数量——成交概率可以从 84.0% 一路塌到 0.4%,唯一的差别是你前面站了多少人。这就是队列位置,一个在回测里几乎不存在、在实盘里决定一切的变量。
它同时解释了两件事:为什么低延迟值钱(慢 300 毫秒,成交率少 28 个百分点,一半订单作废),以及为什么低延迟不是万能的(逆向选择率在各档位都稳定在 61% 上下,排第一也躲不掉被知情交易者砸货)。
下次你的限价单策略回测出漂亮曲线,先问一句:这些成交,假设我排在第几位? 如果答案是”没想过”,那这条曲线大概率是队列位置这个变量替你付的钱。