4. 订单簿深度不足导致的冲击滑点估算
订单簿快照与逐档吃单模型
冲击滑点的本质是:市价单(或激进限价单)在消耗盘口挂单时,成交价会沿着订单簿逐档变差。要估算它,必须拿到限价订单簿(LOB)快照,而不是只看最优买卖价。快照至少包含:买一/卖一价格与数量、后续各档价格与数量、档位数量。对一笔买入订单,从卖一档开始逐档吃单;对卖出订单,从买一档开始逐档吃单。
设买入订单数量为 Q,卖侧各档价格为 p_i、数量为 q_i(i=1,2,…,p 递增)。逐档消耗后,成交均价为:
VWAP = Σ(成交价 × 该档成交量) / Q
冲击滑点(买入,bps) = (VWAP − p_1) / p_1 × 10000
其中 p_1 是卖一价,作为基准。卖出方向对称:VWAP 从买一档向下吃,滑点 = (p_1 − VWAP) / p_1 × 10000。注意这里只算冲击部分,不含买卖价差基准(价差基准是另一层,本例不重复)。
用快照做逐档吃单估算
下面是一段可直接运行的 Python 实现。它接收一个卖侧档位列表(价格、数量),对给定订单量逐档吃单,返回成交均价、冲击滑点和是否穿透最后一档。
def impact_slippage_bps(order_qty, levels, side="buy"):
"""
levels: [(price, qty), ...] 按价格从优到劣排列
buy -> 卖侧,价格递增
sell -> 买侧,价格递减
"""
if order_qty <= 0 or not levels:
return None
best = levels[0][0]
remaining = order_qty
notional = 0.0
filled = 0.0
for price, qty in levels:
take = min(remaining, qty)
notional += take * price
filled += take
remaining -= take
if remaining <= 1e-12:
break
if filled <= 0:
return None
vwap = notional / filled
if side == "buy":
slip_bps = (vwap - best) / best * 1e4
else:
slip_bps = (best - vwap) / best * 1e4
return {
"filled": filled,
"vwap": round(vwap, 4),
"slip_bps": round(slip_bps, 2),
"exhausted": remaining > 1e-12, # True 表示订单量超过可见深度
}
# 示例快照:卖侧五档
asks = [(10.00, 800), (10.01, 1200), (10.02, 1500),
(10.03, 2000), (10.05, 3000)]
for q in [500, 800, 2000, 3500, 5500, 8500]:
r = impact_slippage_bps(q, asks, side="buy")
print(q, r)
示例快照的卖侧累计深度为 800 / 2000 / 3500 / 5500 / 8500 股。订单量 500 股只吃卖一档,滑点为 0;800 股刚好吃完卖一档,滑点仍为 0;2000 股吃到卖二档,均价被抬高,滑点开始出现;3500 股吃到卖三档;5500 股吃到卖四档;8500 股刚好吃完五档。超过 8500 股时 exhausted=True,说明可见深度不足,估算结果只能作为下限,真实滑点会更大。
滑点随订单量放大的结构
下图把示例快照的逐档吃单过程与滑点放大关系画在一起:左侧是卖侧五档的价格与数量,右侧是不同订单量对应的冲击滑点。可以看到,订单量在单档深度内时滑点接近 0,一旦跨档,滑点按档位价差跳升;接近总深度时曲线急剧上翘。
识别临界订单规模
临界规模不是凭感觉定的,而是由快照的累计深度决定。对示例快照,累计深度序列为 800、2000、3500、5500、8500。把订单量扫描一遍,记录每次跨档时的滑点跳变,就能定位“滑点急剧放大”的起点。下面这段扫描代码直接输出每个档位边界处的滑点,便于找出临界点。
def scan_critical(levels, side="buy"):
cum = 0
rows = []
for price, qty in levels:
cum += qty
r = impact_slippage_bps(cum, levels, side=side)
rows.append((cum, price, r["vwap"], r["slip_bps"]))
return rows
for cum, price, vwap, bps in scan_critical(asks):
print(f"累计 {cum:5d} 股 | 该档价 {price:.2f} | VWAP {vwap:.4f} | 滑点 {bps:6.2f} bps")
输出会显示:累计 800 股时滑点 0 bps;累计 2000 股时滑点约 3 bps;累计 3500 股时约 6.7 bps;累计 5500 股时约 11.8 bps;累计 8500 股时约 18.8 bps(具体数值由上面的公式直接算出,属于示例快照下的计算结果)。可以看到,从 5500 到 8500 股这一段,滑点增量明显大于前面各段,这就是深度不足开始显现的区间。
exhausted=True 时,必须明确标注“估算不完整”,不能把结果当作完整冲击滑点使用。结果判读与使用边界
- 滑点为 0:订单量未超过卖一档数量,冲击部分为 0,但真实成交仍要承担价差成本,本例不重复计算价差。
- 滑点小幅跳升:订单量跨入第二、第三档,滑点由档位价差和该档占比共同决定,属于正常冲击。
- 滑点斜率陡增:订单量接近或超过累计可见深度,说明深度不足,此时应缩小订单量、拆分下单或改用限价单,而不是继续按市价单估算。
- exhausted=True:快照深度已被吃穿,估算值只能作为下限,需要补充隐藏流动性或后续盘口数据才能得到更完整的冲击估计。
把上述扫描结果按交易信号的实际下单量分组,就能得到每个信号对应的冲击滑点估计,并标记出哪些信号的下单量已经落入“深度不足”区间。这一步的输出可以直接作为后续滑点归因和订单拆分决策的输入。