第三章:因子与商品的相关性分析
做量化对冲,最核心的问题是什么?
说白了就一句话:你选的因子,跟商品价格到底有没有关系?
我见过太多人,回测曲线漂亮得不行,一上实盘就崩。为什么?因为相关性是假的,是偶然的。今天我们就来彻底解决这个问题。
3.1 相关性分析:量化交易的基石
相关性分析,听起来高大上,其实就干三件事:
- 找关系:因子涨,商品是不是也跟着涨?
- 测强度:这个关系有多紧密?是铁哥们还是点头之交?
- 看方向:是同向还是反向?
我个人习惯,拿到任何因子数据,第一件事就是算相关性。不是跑模型,不是调参数,就是算相关系数。这一步能帮你过滤掉80%的垃圾因子。
核心观点:相关性 ≠ 因果性,但没关系肯定没因果。这是因子筛选的第一道门槛。
3.2 皮尔逊相关系数:最常用的线性关系度量
皮尔逊相关系数,你肯定见过。它的公式长这样:
r = Σ((xᵢ - x̄)(yᵢ - ȳ)) / √(Σ(xᵢ - x̄)² · Σ(yᵢ - ȳ)²)
取值范围在 -1 到 1 之间。1 表示完全正相关,-1 表示完全负相关,0 表示没关系。
但我要提醒你:皮尔逊有个大坑——它只捕捉线性关系。
举个例子。我几年前分析一个库存因子和螺纹钢价格的关系。算出来皮尔逊系数只有 0.12,我以为这因子没用。后来一个老前辈提醒我:你画个散点图看看。结果发现,库存和价格是明显的U型关系——库存极低或极高时,价格都会涨。皮尔逊完全没捕捉到。
避坑指南:我曾经就因为只看皮尔逊系数,错过了一个好因子。现在我的习惯是:先画散点图,再算相关系数。永远不要跳过可视化。
Python里算皮尔逊很简单:
import pandas as pd
import numpy as np
from scipy import stats
# 假设 factor 和 price 是两个 Series
r, p_value = stats.pearsonr(factor, price)
print(f"皮尔逊相关系数: {r:.4f}, p值: {p_value:.4f}")
3.3 斯皮尔曼秩相关系数:非线性关系的救星
斯皮尔曼系数,说白了就是皮尔逊的升级版。它不关心具体数值,只关心排名。
公式也很简单:
ρ = 1 - (6 * Σdᵢ²) / (n * (n² - 1))
其中 dᵢ 是每个样本的排名差。
为什么需要它?因为大宗商品市场充满了非线性关系。
你想想看,原油价格和航空股的关系。油价从30涨到60,航空股跌得很惨。但从60涨到90,航空股可能已经通过套保消化了成本,跌幅就没那么大了。这就是非线性。
斯皮尔曼能捕捉这种关系。我建议你:任何时候都同时算皮尔逊和斯皮尔曼。如果两者差异很大,说明关系是非线性的,需要进一步分析。
# 斯皮尔曼相关系数
rho, p_value = stats.spearmanr(factor, price)
print(f"斯皮尔曼相关系数: {rho:.4f}, p值: {p_value:.4f}")
# 对比两者
print(f"差异: {abs(r - rho):.4f}")
3.4 滚动窗口相关性:捕捉动态变化
静态相关性有个致命问题:它假设关系是稳定的。
但市场不是这样的。因子和商品的关系会变,而且变得很快。
我记得2020年疫情刚爆发时,很多因子的相关性在一周内就发生了翻转。如果你用的是静态相关性,那段时间的策略会亏得很惨。
滚动窗口相关性就是解决这个问题的。它的思路很简单:
- 选一个窗口大小(比如60个交易日)
- 在每个时间点,只取窗口内的数据算相关性
- 窗口不断向前滑动
这样你就能看到相关性随时间的变化轨迹。
def rolling_correlation(factor, price, window=60):
"""计算滚动窗口相关性"""
df = pd.DataFrame({
'factor': factor,
'price': price
})
# 滚动计算
rolling_corr = df['factor'].rolling(window=window).corr(df['price'])
return rolling_corr
# 使用示例
rolling_corr = rolling_correlation(factor, price, window=60)
print(rolling_corr.tail())
实战技巧:窗口大小的选择很关键。太短(比如20天)噪声大,太长(比如250天)反应慢。我个人习惯用60天和120天两个窗口同时看。60天看短期变化,120天看趋势。
3.5 实战案例:原油库存因子与价格分析
光说不练假把式。我们拿真实数据走一遍流程。
假设我们有一个原油库存因子(EIA周度数据)和原油期货价格。目标是分析两者的相关性。
import pandas as pd
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
# 模拟数据(实际使用时替换为真实数据)
np.random.seed(42)
dates = pd.date_range('2020-01-01', periods=500, freq='D')
inventory = np.random.randn(500) * 0.5 + 0.1 * np.sin(np.linspace(0, 10, 500))
price = -0.3 * inventory + np.random.randn(500) * 0.2
# 1. 静态相关性
r_pearson, p_pearson = stats.pearsonr(inventory, price)
r_spearman, p_spearman = stats.spearmanr(inventory, price)
print(f"皮尔逊: {r_pearson:.4f} (p={p_pearson:.4f})")
print(f"斯皮尔曼: {r_spearman:.4f} (p={p_spearman:.4f})")
# 2. 滚动相关性
window = 60
rolling_corr = pd.Series(inventory).rolling(window).corr(pd.Series(price))
# 3. 可视化
plt.figure(figsize=(12, 8))
# 子图1:散点图
plt.subplot(2, 2, 1)
plt.scatter(inventory, price, alpha=0.5)
plt.xlabel('库存因子')
plt.ylabel('价格')
plt.title('散点图')
# 子图2:滚动相关性
plt.subplot(2, 2, 2)
plt.plot(dates, rolling_corr)
plt.axhline(y=0, color='r', linestyle='--')
plt.xlabel('日期')
plt.ylabel('滚动相关性')
plt.title(f'滚动窗口相关性 (窗口={window})')
plt.tight_layout()
plt.show()
运行这段代码,你会看到:
- 散点图显示负相关关系(库存涨,价格跌)
- 滚动相关性曲线在 -0.4 到 -0.2 之间波动
- 某些时间段相关性会变弱甚至转正
这就是为什么静态相关性不够用。如果你在相关性转正的那段时间还按原来的逻辑交易,会出大问题。
3.6 相关性分析的完整框架
说了这么多,我总结一下自己的分析流程:
- 数据清洗:处理缺失值、异常值、对齐时间戳
- 可视化初探:画散点图、时间序列叠加图
- 静态相关性:同时算皮尔逊和斯皮尔曼,对比差异
- 滚动相关性:用60天和120天窗口,看稳定性
- 统计检验:看p值是否显著(一般要求p < 0.05)
- 经济逻辑验证:问自己,这个关系有道理吗?
记住:统计显著 ≠ 经济显著。我见过p值0.001的相关性,但实际交易中根本赚不到钱。因为那个关系太微弱了,交易成本就能吃掉所有收益。
3.7 常见陷阱与应对
| 陷阱 | 表现 | 应对方法 |
|---|---|---|
| 伪相关 | 两个无关序列偶然相关 | 做差分或去趋势后再算 |
| 滞后相关 | 因子领先或滞后于价格 | 计算不同滞后的相关性 |
| 结构突变 | 相关性在某时间点突变 | 用滚动窗口检测突变点 |
| 异常值干扰 | 少数极端值拉高相关性 | 用斯皮尔曼或剔除异常值 |
嗯,这里要注意。伪相关是大宗商品分析里最常见的坑。比如,你发现某个因子和铜价相关性高达0.8,但仔细一看,两者都在2020年因为疫情暴跌过。去掉那段时间,相关性直接掉到0.1。这就是伪相关。
我的做法是:分段验证。把数据分成牛市、熊市、震荡市三段,分别算相关性。如果三段都稳定,那才是真关系。
个人经验:我习惯在算完相关性后,做一个简单的回测。用因子信号交易,看实际收益。如果收益曲线和相关性分析对不上,那一定是哪里出了问题。
3.8 本章小结
相关性分析不是终点,是起点。它帮你快速筛选因子,但别指望它告诉你全部真相。
记住三个要点:
- 皮尔逊看线性,斯皮尔曼看排名,两者互补
- 滚动窗口看动态变化,别用静态数据骗自己
- 统计显著不等于能赚钱,最终要看实盘表现
下一章,我们会把这些相关性分析应用到多因子组合中,看看如何构建一个真正稳健的量化对冲策略。
公众号:蓝海资料掘金营,微信deep3321