4、相关系数与距离:皮尔逊相关系数、欧氏距离法、如何筛选初始配对候选池
好,咱们进入正题。
配对交易的第一步,也是最关键的一步——选对股票。
你想想看,如果选的两个标的本就不相关,那后面做回归、算残差、设阈值,全都是白费功夫。说白了,配对交易的核心逻辑就是「价差回归」,前提是这两个东西历史上确实有某种稳定的联动关系。
那怎么量化这种「联动关系」呢?
我个人习惯用两种方法:皮尔逊相关系数和欧氏距离。它们各有侧重,配合使用效果更好。
4.1 皮尔逊相关系数:衡量线性关系的亲密程度
皮尔逊相关系数,大家应该不陌生。它衡量的是两个变量之间的线性相关程度,取值范围在 [-1, 1] 之间。
- 1 表示完全正相关
- -1 表示完全负相关
- 0 表示没有线性关系
在配对交易里,我们通常找相关系数接近 1 的股票对。比如 0.8 以上,甚至 0.9 以上。
但这里有个坑——相关系数高,不代表价差一定可交易。
有一年我筛选出相关系数高达 0.98 的两只银行股,兴冲冲地开始跑策略。结果发现它们的价差根本不回归,反而越走越远。后来一查,原来是因为其中一只股票刚经历了除权除息,价格被硬生生拉低了。
教训:相关系数只看「形状」,不看「绝对水平」。一定要做价格调整(复权),否则就是白忙活。
计算皮尔逊相关系数的代码很简单:
import numpy as np
import pandas as pd
# 假设 df 是包含两只股票日收益率的数据框
# 注意:用收益率算相关系数,比用价格更稳定
corr = df['stock_a'].corr(df['stock_b'])
print(f'皮尔逊相关系数: {corr:.4f}')
我个人习惯用滚动窗口算相关系数,比如过去 60 天或 120 天。这样能动态观察相关性是否稳定。
# 滚动 60 天相关系数
rolling_corr = df['stock_a'].rolling(60).corr(df['stock_b'])
print(rolling_corr.tail())
如果滚动相关系数忽高忽低,甚至出现负值,那这对股票就不太适合做配对交易。稳定性比绝对值更重要。
4.2 欧氏距离法:另一种视角
相关系数看的是「走势是否同步」,而欧氏距离看的是「价格是否接近」。
你可能会问:价格接近有什么用?
嗯,这里要注意——欧氏距离法最早是由 Gatev、Goetzmann 和 Rouwenhorst 在 2006 年的经典论文中提出的。他们的思路很简单:
- 把两只股票的历史价格标准化(比如归一化到 [0,1] 区间)
- 计算它们标准化后价格的欧氏距离
- 距离越小,说明历史上价格走势越「贴合」
公式长这样:
距离 = sqrt( sum( (P_a_i - P_b_i)^2 ) )
其中 P_a_i 和 P_b_i 是两只股票在第 i 天的标准化价格。
代码实现:
from sklearn.preprocessing import MinMaxScaler
# 标准化价格到 [0,1]
scaler = MinMaxScaler()
price_norm = scaler.fit_transform(df[['stock_a', 'stock_b']])
# 计算欧氏距离
euclidean_dist = np.sqrt(np.sum((price_norm[:, 0] - price_norm[:, 1])**2))
print(f'欧氏距离: {euclidean_dist:.4f}')
欧氏距离法对「价格水平」敏感。如果两只股票价格长期在一个区间内波动,距离就会小。但要注意,如果它们只是偶然在某段时间价格接近,距离也会小——所以一定要用足够长的历史数据(至少 1-2 年)。
4.3 两种方法的对比
| 维度 | 皮尔逊相关系数 | 欧氏距离法 |
|---|---|---|
| 关注点 | 走势方向是否一致 | 价格水平是否接近 |
| 对价格水平敏感 | 不敏感 | 敏感 |
| 对异常值敏感 | 较敏感 | 非常敏感 |
| 适用场景 | 同行业、同板块 | 同行业、同市值区间 |
| 计算复杂度 | 低 | 低 |
你看,两种方法各有千秋。我一般会先用相关系数粗筛,再用欧氏距离精筛。
4.4 如何构建初始配对候选池
好了,理论讲完,咱们来点实战的。怎么从几千只股票里,快速找到潜在的配对候选?
我的流程是这样的:
- 行业过滤:先限定在同一行业或同一板块。跨行业的配对不是不能做,但逻辑上更难解释,风险也更大。
- 流动性过滤:剔除日均成交额低于 5000 万的股票。流动性差的股票,交易成本高,滑点大,策略很难执行。
- 相关系数粗筛:计算所有股票对之间的皮尔逊相关系数,保留 top 10% 或相关系数 > 0.8 的对。
- 欧氏距离精筛:对粗筛后的股票对,计算标准化价格的欧氏距离,保留距离最小的前 50-100 对。
- 人工复核:最后,我会手动看一下这些股票对的走势图,排除那些有明显结构变化(比如并购、重组、退市风险)的。
候选池不是越大越好。我见过有人搞出几千个候选对,结果大部分都是噪音。我个人习惯控制在 30-50 对,精挑细选,后面做协整检验时成功率会高很多。
代码实现一个简单的筛选流程:
import itertools
import numpy as np
import pandas as pd
def build_candidate_pool(price_df, industry_col, min_corr=0.8, top_n=100):
"""
构建初始配对候选池
price_df: 包含多只股票日收盘价的 DataFrame
industry_col: 行业标签列
"""
candidates = []
stocks = price_df.columns
# 只考虑同一行业的股票对
for (s1, s2) in itertools.combinations(stocks, 2):
if industry_col[s1] != industry_col[s2]:
continue
# 计算相关系数
corr = price_df[s1].corr(price_df[s2])
if corr < min_corr:
continue
# 计算欧氏距离
scaler = MinMaxScaler()
norm_prices = scaler.fit_transform(price_df[[s1, s2]])
dist = np.sqrt(np.sum((norm_prices[:, 0] - norm_prices[:, 1])**2))
candidates.append((s1, s2, corr, dist))
# 按距离排序,取前 top_n
candidates = sorted(candidates, key=lambda x: x[3])[:top_n]
return pd.DataFrame(candidates, columns=['stock_a', 'stock_b', 'corr', 'euclidean_dist'])
4.5 知识体系总览
下面这张图,帮你把本章的核心逻辑串起来:
这个流程不是死的。如果你做的是高频配对交易,可能还要加入波动率过滤、交易成本估算等步骤。但作为入门,这套流程已经够用了。
好了,关于相关系数和距离的筛选方法,就聊到这儿。记住一句话:候选池的质量,决定了你后面所有工作的上限。花时间把这一步做扎实,后面会省很多事。