3、流动性指标计算实战:用Python实现买卖价差、有效价差、实现价差、深度指标、Amihud指标、Roll指标的计算,并构建流动性因子面板数据

好,咱们直接进入正题。

上一节我们把流动性的理论框架搭好了,这一节就是真刀真枪地干。我会带着你,用Python把几个最核心的流动性指标一个个算出来。说实话,这些指标我在实盘策略里反复用过,踩过的坑也不少,今天一并分享给你。

3.1 准备工作:数据与工具

在动手之前,先把工具备齐。我个人习惯用 pandas 做数据处理,numpy 做数值计算,matplotlib 偶尔画个图看看分布。

数据方面,我们需要的是高频的逐笔成交和报价数据。最低要求是分钟级,但如果你有Tick级数据,那算出来的指标会更敏感。我曾在某个项目中用日频数据算Amihud,结果发现信号滞后得厉害——嗯,这里要注意,频率越低,流动性指标的时效性越差。

import pandas as pd
import numpy as np

# 模拟一份高频数据
np.random.seed(42)
n = 10000
data = pd.DataFrame({
    'time': pd.date_range('2024-01-01', periods=n, freq='1min'),
    'bid_price': np.random.uniform(99, 101, n),
    'ask_price': np.random.uniform(100, 102, n),
    'trade_price': np.random.uniform(99.5, 101.5, n),
    'volume': np.random.randint(100, 10000, n),
    'mid_price': (np.random.uniform(99, 101, n) + np.random.uniform(100, 102, n)) / 2
})
data.head()

3.2 买卖价差(Bid-Ask Spread)

这是最直观的流动性指标。说白了,就是你想买的时候,卖一价和买一价之间差多少。差得越小,流动性越好。

计算公式很简单:

Spread = Ask_Price - Bid_Price

或者用相对值:Relative_Spread = (Ask - Bid) / Mid_Price

data['spread'] = data['ask_price'] - data['bid_price']
data['relative_spread'] = data['spread'] / data['mid_price']

我曾经在回测一个高频做市策略时,发现直接用绝对价差会受价格水平影响。比如茅台和工商银行,价差绝对值差很多,但相对价差反而能统一比较。所以,我建议你在做截面分析时,优先用相对价差。

3.3 有效价差(Effective Spread)

买卖价差有个问题——它只反映了报价层面的流动性,但实际成交可能发生在报价之外。有效价差就是为了解决这个问题的。

它衡量的是:实际成交价格相对于买卖中间价的偏离程度。

Effective_Spread = 2 * |Trade_Price - Mid_Price|

data['effective_spread'] = 2 * abs(data['trade_price'] - data['mid_price'])

你想想看,如果一笔大单子把价格推得很远,有效价差就会很大。这说明市场深度不够,流动性出了问题。我在监控一个日内策略时,就靠这个指标提前发现了某只小盘股的流动性危机——嗯,那次跑得及时。

3.4 实现价差(Realized Spread)

有效价差虽然好,但它包含了逆向选择成本。什么意思?就是做市商担心跟知情交易者做对手盘,所以会把价差拉大。实现价差就是剔除这部分成本后的净收益。

计算方法是:用成交价减去一段时间后的中间价。

Realized_Spread = 2 * (Trade_Price - Mid_Price_After_5min)

data['mid_price_shifted'] = data['mid_price'].shift(-5)  # 5分钟后的中间价
data['realized_spread'] = 2 * (data['trade_price'] - data['mid_price_shifted'])

这里有个坑:shift的窗口长度怎么选?我试过1分钟、5分钟、10分钟,发现不同股票的最佳窗口不一样。大盘股流动性好,窗口可以短一些;小盘股则需要更长的时间来消化冲击。建议你做个滚动窗口的敏感性分析。

3.5 深度指标(Depth)

价差只告诉你交易成本,但没告诉你能交易多少。深度指标就是回答这个问题的:在最优报价上,到底有多少量等着你?

最简单的深度指标就是买卖一档的挂单量之和。

# 假设我们有买卖一档的挂单量数据
data['bid_vol'] = np.random.randint(1000, 50000, n)
data['ask_vol'] = np.random.randint(1000, 50000, n)
data['depth'] = data['bid_vol'] + data['ask_vol']

更精细的做法是计算「价差加权深度」:把不同档位的挂单量按距离加权求和。离中间价越近的订单,权重越大。我在做订单簿重建时,发现这个指标比单纯加总更能反映真实的流动性供给。

3.6 Amihud指标

前面几个指标都需要高频数据,但很多时候我们只有日频数据。Amihud指标就是为这种情况设计的——它用日内的价格变化除以成交额,来衡量每一块钱交易对价格的冲击。

Amihud = |Return| / Volume * Price

# 先聚合到日频
daily_data = data.resample('D', on='time').agg({
    'trade_price': 'last',
    'volume': 'sum'
})
daily_data['return'] = daily_data['trade_price'].pct_change()
daily_data['amihud'] = abs(daily_data['return']) / (daily_data['volume'] * daily_data['trade_price'])

这个指标有个特点:值越大,流动性越差。我曾在A股市场做过测试,Amihud指标对ST股票的识别效果特别好——那些流动性枯竭的股票,Amihud值会突然飙升几个数量级。

3.7 Roll指标

Roll指标是个很有意思的玩意儿。它假设市场是有效的,价差完全由买卖报价的反弹引起。通过观察相邻价格变化的自协方差,就能反推出隐含价差。

Roll = 2 * sqrt(-Cov(ΔP_t, ΔP_{t-1}))

price_diff = data['trade_price'].diff()
roll_cov = price_diff.rolling(window=20).cov(price_diff.shift(1))
data['roll'] = 2 * np.sqrt(np.maximum(-roll_cov, 0))

注意看,我用了 np.maximum 来处理协方差为正的情况。为什么会有正数?因为Roll模型的假设在现实中不一定成立——比如趋势行情下,价格变化会正相关。这时候Roll指标会失效。我曾经在牛市中用Roll指标算价差,结果全是NaN,后来才意识到是趋势破坏了模型假设。

3.8 构建流动性因子面板数据

单个指标算完,下一步就是组装成面板数据。面板数据就是「时间×股票」的二维结构,这是量化策略的输入基础。

# 假设我们有多个股票
stocks = ['000001', '000002', '000003']
panel_data = []

for stock in stocks:
    # 模拟每个股票的数据
    stock_data = data.copy()
    stock_data['stock'] = stock
    stock_data['spread'] = stock_data['ask_price'] - stock_data['bid_price']
    stock_data['effective_spread'] = 2 * abs(stock_data['trade_price'] - stock_data['mid_price'])
    stock_data['depth'] = stock_data['bid_vol'] + stock_data['ask_vol']
    stock_data['amihud'] = abs(stock_data['trade_price'].pct_change()) / (stock_data['volume'] * stock_data['trade_price'])
    
    # 按日聚合
    daily = stock_data.resample('D', on='time').agg({
        'spread': 'mean',
        'effective_spread': 'mean',
        'depth': 'mean',
        'amihud': 'mean',
        'volume': 'sum'
    })
    daily['stock'] = stock
    panel_data.append(daily)

# 合并成面板
panel_df = pd.concat(panel_data)
panel_df = panel_df.set_index(['stock', panel_df.index])
panel_df.head()

核心要点:面板数据的索引是「股票+时间」的复合索引。这样你在做因子分析时,可以方便地做截面排序(横截面)和时间序列回归(纵截面)。

3.9 可视化:流动性指标的关系

最后,我们画一张图,看看这些指标之间是什么关系。我习惯用散点图矩阵,一眼就能看出哪些指标是冗余的。

import matplotlib.pyplot as plt
import seaborn as sns

# 选取代表性样本
sample = panel_df.dropna().sample(1000)
sns.pairplot(sample[['spread', 'effective_spread', 'depth', 'amihud']])
plt.show()

从经验来看,买卖价差和有效价差通常高度相关(相关系数0.8以上),而深度指标和Amihud指标往往呈负相关——深度越大,价格冲击越小。这个关系在做多因子组合时很有用,可以避免引入重复信息。

避坑指南:我曾经在构建因子库时,同时用了买卖价差和有效价差,结果两个因子相关性太高,导致多因子模型共线性严重。后来我只保留了有效价差,因为它包含了更多成交信息。

3.10 本章小结

这一节我们实战了六个流动性指标的计算:

  • 买卖价差:最基础,适合快速筛查
  • 有效价差:考虑实际成交,更真实
  • 实现价差:剔除逆向选择,适合做市商
  • 深度指标:衡量容量,适合大资金
  • Amihud指标:日频可用,覆盖面广
  • Roll指标:模型驱动,但假设严格

这些指标各有优劣,没有哪个是万能的。我的建议是:根据你的策略频率和数据条件,选择2-3个互补的指标。比如高频策略用有效价差+深度,低频策略用Amihud+Roll。

下一节,我们会把这些指标组合成复合流动性因子,并测试它们在选股策略中的表现。嗯,到时候你会发现,流动性因子和动量因子搭配起来,效果出奇的好。


专注资料整理