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 到可见总深度以上做扫描,画出滑点—订单量曲线。曲线在某一档被吃穿处会出现明显折点,折点之后斜率陡增,这个折点对应的订单量就是本例要识别的临界规模。

滑点随订单量放大的结构

下图把示例快照的逐档吃单过程与滑点放大关系画在一起:左侧是卖侧五档的价格与数量,右侧是不同订单量对应的冲击滑点。可以看到,订单量在单档深度内时滑点接近 0,一旦跨档,滑点按档位价差跳升;接近总深度时曲线急剧上翘。

卖侧订单簿快照(价格 / 数量) 冲击滑点随订单量放大 卖五 10.05 3000 股 累计 8500 卖四 10.03 2000 股 累计 5500 卖三 10.02 1500 股 累计 3500 卖二 10.01 1200 股 累计 2000 卖一 10.00 800 股 累计 800 买入吃单方向 订单量(股) 滑点 bps 0 2000 3500 5500 8500 10000 单档内≈0 跨档跳升 接近总深度急升 临界规模:吃穿最后一档(示例 8500 股)后滑点不可由快照估算

识别临界订单规模

临界规模不是凭感觉定的,而是由快照的累计深度决定。对示例快照,累计深度序列为 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:快照深度已被吃穿,估算值只能作为下限,需要补充隐藏流动性或后续盘口数据才能得到更完整的冲击估计。

把上述扫描结果按交易信号的实际下单量分组,就能得到每个信号对应的冲击滑点估计,并标记出哪些信号的下单量已经落入“深度不足”区间。这一步的输出可以直接作为后续滑点归因和订单拆分决策的输入。