9、流动性螺旋的量化指标(三):Pastor-Stambaugh流动性因子
聊到流动性因子,很多人第一反应就是Amihud非流动性指标。没错,那个指标确实经典,用起来也顺手。但说实话,它有个硬伤——它只捕捉了价格冲击的大小,却忽略了价格冲击的 reversibility,也就是可逆性。
什么意思呢?
你想想看,如果一个订单把价格打下去5毛钱,但下一秒价格就弹回来了,这说明流动性其实还不错,市场有深度。但如果价格被打下去之后,半天都回不来,那才是真正的流动性枯竭。Amihud指标把这两种情况混为一谈,而Pastor-Stambaugh因子,恰恰就是冲着这个痛点来的。
核心思想:Pastor-Stambaugh流动性因子衡量的是——订单流对价格的持续性影响。影响越大,流动性越差。
9.1 因子构建逻辑
我记得第一次读Pastor和Stambaugh那篇2003年的论文时,第一反应是:这方法真够巧妙的。他们用了一个非常直观的回归模型:
r_{i,t+1} = α + β₁ · r_{i,t} + β₂ · sign(r_{i,t}) · v_{i,t} + ε_{i,t+1}
其中:
r_{i,t+1}:股票i在t+1期的超额收益r_{i,t}:股票i在t期的收益v_{i,t}:股票i在t期的成交额(以美元计)sign(r_{i,t}):收益的符号(+1或-1)
关键就在那个交互项 sign(r_{i,t}) · v_{i,t} 上。它的系数β₂,就是我们要的流动性度量。
为什么会这样?
逻辑其实很简单:如果市场流动性好,那么今天的成交额(订单流)不应该对明天的收益有预测能力。但如果流动性差,大额订单会留下"痕迹"——比如今天的大额买入(正收益+高成交额)会推高价格,明天价格就会回调。这个回调的幅度,就是β₂。
嗯,这里要注意:β₂通常是负值。绝对值越大,说明流动性越差。
个人经验:我在做A股因子研究时发现,Pastor-Stambaugh因子在小市值股票上的信号特别强。大市值股票流动性本身就比较好,这个因子的区分度反而没那么明显。所以如果你做的是沪深300成分股,这个因子可能不如Amihud好用。
9.2 因子构建步骤
好,理论说完了,咱们直接上代码。我习惯用Python来实现,因为pandas处理面板数据确实方便。
import pandas as pd
import numpy as np
from statsmodels.regression.linear_model import OLS
def build_ps_factor(returns, volume, window=60):
"""
构建Pastor-Stambaugh流动性因子
参数:
returns: DataFrame, 个股日收益率 (index=日期, columns=股票代码)
volume: DataFrame, 个股日成交额 (index=日期, columns=股票代码)
window: int, 滚动回归窗口
返回:
ps_factor: DataFrame, 流动性因子值
"""
stocks = returns.columns
ps_factor = pd.DataFrame(index=returns.index, columns=stocks)
for stock in stocks:
r = returns[stock].dropna()
v = volume[stock].dropna()
# 对齐数据
data = pd.concat([r, v], axis=1, keys=['ret', 'vol'])
data = data.dropna()
if len(data) < window:
continue
# 构建回归变量
data['sign_ret'] = np.sign(data['ret'])
data['interaction'] = data['sign_ret'] * data['vol']
data['ret_lag'] = data['ret'].shift(1)
# 滚动回归
for i in range(window, len(data)):
y = data['ret'].iloc[i-window+1:i+1]
X = data[['ret_lag', 'interaction']].iloc[i-window:i]
X = sm.add_constant(X)
try:
model = OLS(y, X).fit()
# 取交互项系数,取负值(流动性越好,值越大)
ps_factor.iloc[i, stocks.get_loc(stock)] = -model.params['interaction']
except:
ps_factor.iloc[i, stocks.get_loc(stock)] = np.nan
return ps_factor
避坑指南:我曾经在构建这个因子时踩过一个坑——成交额的单位问题。如果不同股票之间成交额量级差异太大(比如茅台和ST股),回归系数会严重失真。我建议先对成交额做横截面标准化,或者用成交额的对数形式。另外,窗口期我一般选60个交易日,太短了回归不稳定,太长了又不够灵敏。
9.3 因子在多因子模型中的应用
Pastor-Stambaugh因子不是孤立使用的。在实际的多因子模型中,它通常作为流动性维度的代表因子出现。我一般把它放在这样一个框架里:
在实际应用中,我通常把Pastor-Stambaugh因子和Amihud指标、换手率放在一起,构成一个流动性因子组合。这三个指标各有侧重:
| 流动性指标 | 衡量维度 | 优势 | 劣势 |
|---|---|---|---|
| Amihud非流动性 | 价格冲击大小 | 计算简单,数据易得 | 忽略价格可逆性 |
| 换手率 | 交易活跃度 | 直观易懂 | 受市值影响大 |
| Pastor-Stambaugh | 价格冲击持续性 | 捕捉流动性螺旋 | 计算复杂,需高频数据 |
9.4 实战中的注意事项
说几个我在实战中总结出来的要点:
- 频率问题:Pastor-Stambaugh因子对数据频率很敏感。我试过用日频数据构建,效果还行。但如果用5分钟高频数据,因子质量会提升一个档次。当然,数据成本也上去了。
- 行业中性化:这个因子有明显的行业特征。金融股和科技股的流动性模式完全不同。我习惯在做完因子值之后,对每个行业做横截面标准化,消除行业偏差。
- 与市场状态的关系:在市场恐慌时期,Pastor-Stambaugh因子的预测能力会显著增强。说白了,流动性螺旋往往发生在最需要流动性的时候。我曾在2015年股灾期间回测过,这个因子对后续1-2周的收益预测能力是平时的3倍以上。
- 因子衰减速度:这个因子的半衰期大约在3-6个月。也就是说,如果你用半年前的数据算出来的因子值,基本没什么用了。我建议每个月更新一次因子值。
我的一个小技巧:在做多因子模型时,我通常把Pastor-Stambaugh因子作为风险因子而非alpha因子来使用。什么意思呢?就是用它来对冲流动性风险,而不是用它来选股。这样做的原因是,流动性因子的收益不稳定,但作为风险暴露的度量,它非常可靠。
9.5 一个简单的回测框架
最后,给你一个简单的回测思路。假设我们构建了一个包含Pastor-Stambaugh因子的多因子模型:
# 伪代码示例
def backtest_ps_factor(returns, volume, factors):
# 1. 构建PS因子
ps = build_ps_factor(returns, volume)
# 2. 与其他因子合并
all_factors = pd.concat([factors, ps], axis=1)
# 3. 横截面回归(Fama-MacBeth风格)
# 每月做一次回归,得到因子暴露
exposures = []
for month in unique_months:
y = returns.loc[month] # 下月收益
X = all_factors.loc[month-1] # 上月因子值
model = OLS(y, X).fit()
exposures.append(model.params)
# 4. 分析PS因子的显著性
ps_coefs = [e['ps'] for e in exposures]
t_stat = np.mean(ps_coefs) / (np.std(ps_coefs) / np.sqrt(len(ps_coefs)))
return t_stat
嗯,这个框架虽然简单,但足够你验证Pastor-Stambaugh因子在你关注的市场上是否有效。我建议你先在沪深300上跑一遍,看看结果。如果t统计量绝对值大于2,那恭喜你,这个因子在你的市场上是有价值的。
好了,关于Pastor-Stambaugh流动性因子,今天就聊到这里。记住,流动性因子的核心价值不在于它本身能赚多少钱,而在于它能帮你识别什么时候该跑。毕竟,在流动性枯竭的时候,你手里有再好的alpha策略也白搭。