4. 成交量分布(VPIN)模型:基于成交量的指令流毒性指标构建

各位同学,今天我们来聊一个在微观结构里非常核心的概念——VPIN。

VPIN,全称是 Volume-synchronized Probability of Informed Trading。翻译过来就是“成交量同步的知情交易概率”。

说白了,它衡量的是:当前这笔成交量里,到底有多少是“聪明钱”在动手?

我最早接触VPIN是在2015年,当时国内股指期货市场波动剧烈。我尝试用传统PIN模型去抓异常,结果发现计算太慢,根本跟不上行情。后来看到Easley等人提出的VPIN,才觉得“嗯,这才是实战该用的东西”。

4.1 为什么需要VPIN?

传统的PIN模型(Probability of Informed Trading)需要估计订单流的买卖不平衡参数。它依赖最大似然估计,计算量很大,而且对数据频率敏感。

VPIN不一样。它直接基于成交量来划分时间区间,而不是固定时间窗口。你想想看,市场在活跃时段每秒成交几千笔,在冷清时段可能几秒才一笔。用固定时间窗口去算,信息密度完全不一样。

VPIN的核心思想是:把成交量分成等量的小篮子(buckets),每个篮子里的成交量相等,然后看这个篮子里买卖方向的不平衡程度。

关键区别:

  • PIN:基于时间窗口,计算复杂
  • VPIN:基于成交量窗口,计算简单,实时性强

4.2 VPIN的计算步骤

VPIN的计算其实不复杂,但有几个细节容易踩坑。我一步步说清楚。

第一步:确定成交量篮子大小 V

V 是每个篮子包含的成交量。通常取过去一段时间日均成交量的 1/50 或 1/100。比如某股票日均成交 1000 万股,那 V 可以取 20 万股。

我个人习惯用过去20天的平均成交量来动态调整 V,这样能适应市场活跃度的变化。

第二步:将逐笔成交数据按时间顺序填充到篮子里

每个篮子填满 V 股后,就关闭这个篮子,开始下一个。注意,最后一笔成交可能跨篮子,需要拆分。

第三步:计算每个篮子的买卖方向

这里需要用到“成交量标记法”(Volume-based Tick Rule)。

简单说:

  • 如果当前价格高于上一笔价格,标记为买方主动(Buy)
  • 如果低于上一笔,标记为卖方主动(Sell)
  • 如果价格不变,沿用上一笔的标记

然后,每个篮子的买卖不平衡量就是:

OI = |V_buy - V_sell| / V

其中 V_buy 和 V_sell 分别是该篮子中买方和卖方成交量。

第四步:计算 VPIN

取最近 n 个篮子的 OI 平均值,就是 VPIN 值。

VPIN = (1/n) * Σ OI_i

n 通常取 50 或 100。我一般取 50,因为对短期信号更敏感。

4.3 代码实现

下面是一个完整的 VPIN 计算函数。我尽量写得简洁,但保留了关键细节。

import pandas as pd
import numpy as np

def compute_vpin(trade_data, bucket_volume=200000, n_buckets=50):
    """
    计算 VPIN 指标
    
    参数:
        trade_data: DataFrame,必须包含 'price' 和 'volume' 列
        bucket_volume: 每个篮子的目标成交量
        n_buckets: 用于计算 VPIN 的篮子数量
    
    返回:
        vpin_series: VPIN 时间序列
    """
    # 1. 标记买卖方向(Tick Rule)
    trade_data['side'] = 0
    trade_data.loc[trade_data['price'] > trade_data['price'].shift(1), 'side'] = 1  # Buy
    trade_data.loc[trade_data['price'] < trade_data['price'].shift(1), 'side'] = -1 # Sell
    trade_data['side'] = trade_data['side'].fillna(method='ffill')
    
    # 2. 按成交量分组到篮子
    trade_data['cum_vol'] = trade_data['volume'].cumsum()
    trade_data['bucket_id'] = (trade_data['cum_vol'] // bucket_volume).astype(int)
    
    # 3. 计算每个篮子的买卖不平衡
    bucket_agg = trade_data.groupby('bucket_id').agg(
        buy_vol=('volume', lambda x: x[trade_data.loc[x.index, 'side'] == 1].sum()),
        sell_vol=('volume', lambda x: x[trade_data.loc[x.index, 'side'] == -1].sum()),
        total_vol=('volume', 'sum')
    )
    bucket_agg['oi'] = np.abs(bucket_agg['buy_vol'] - bucket_agg['sell_vol']) / bucket_agg['total_vol']
    
    # 4. 计算 VPIN(滚动平均)
    bucket_agg['vpin'] = bucket_agg['oi'].rolling(window=n_buckets, min_periods=1).mean()
    
    return bucket_agg['vpin']

避坑指南:

我曾经在实盘回测时发现 VPIN 值突然跳变,排查了半天才发现是 Tick Rule 标记在开盘第一笔数据时出了问题。第一笔没有上一笔价格,side 会变成 NaN。记得用 fillna 处理,或者直接标记为 0(中性)。

4.4 VPIN 的解读与应用

VPIN 的值域在 0 到 1 之间。数值越高,说明当前成交量中买卖方向越不平衡,指令流毒性越强。

VPIN 区间 含义 操作建议
0 - 0.3 指令流毒性低,市场均衡 正常交易,无需特殊处理
0.3 - 0.6 中等毒性,存在信息不对称 降低订单规模,增加监控
0.6 - 0.8 高毒性,可能有重大信息 暂停大单,改用冰山订单
0.8 - 1.0 极端毒性,市场可能剧烈波动 建议暂停交易,等待信息释放

我在做高频做市商策略时,会把 VPIN 作为风控指标。当 VPIN 超过 0.7 时,自动降低报价深度,减少被“聪明钱”吃掉的概率。

4.5 VPIN 的局限性

VPIN 不是万能的。我遇到过几个问题:

  • 篮子大小选择敏感:V 太小,噪声大;V 太大,信号滞后。需要根据品种流动性调整。
  • Tick Rule 在零股交易时容易误判:价格不变但成交方向可能不同,这时候沿用上一笔标记会引入误差。
  • 无法区分信息驱动和流动性驱动:有时候大额买卖只是因为机构调仓,并非信息交易。

注意:VPIN 更适合用于期货、ETF 等流动性好的品种。对于成交稀疏的股票,篮子可能很久都填不满,导致指标失效。

4.6 知识体系图

下面这张图总结了 VPIN 的核心逻辑和计算流程:

VPIN 计算流程与知识体系 逐笔成交数据 步骤1:确定篮子大小 V(日均成交量/50) 步骤2:按成交量填充篮子(Tick Rule标记方向) 步骤3:计算每个篮子的 OI = |V_buy - V_sell| / V VPIN = 滚动平均 OI 关键参数: • 篮子大小 V:50-100笔/篮子 • 滚动窗口 n:50-100个篮子 • Tick Rule:价格比较法 应用场景: • 指令流毒性监控 • 大单拆分时机选择 • 做市商风控

4.7 实战中的一点体会

VPIN 这个指标,说实话,单独用效果一般。我一般会结合订单簿的深度变化一起看。

举个例子:如果 VPIN 突然升高,同时买一卖一价差也在扩大,那基本可以确定有大资金在动手。这时候我的策略会立刻切换到“防御模式”——要么撤单,要么缩小报价规模。

反过来,如果 VPIN 升高但价差稳定,可能只是短期流动性冲击,不用太紧张。

核心要点回顾:

  • VPIN 用成交量篮子代替时间窗口,计算效率高
  • 核心是买卖方向标记和滚动平均
  • 高 VPIN 意味着指令流毒性强,需要谨慎
  • 结合其他微观结构指标使用效果更好

好了,VPIN 的内容就到这里。这个指标虽然简单,但在实战中非常实用。建议你拿自己的数据跑一遍,感受一下不同市场状态下的 VPIN 变化。

专注资料整理