4、距离方法与配对选择:欧氏距离、相关系数、最小距离法、配对筛选的实战流程

做配对交易这么多年,我最大的体会是:选对配对,策略就成功了一半。很多人一上来就写代码、跑回测,结果亏得莫名其妙。其实问题往往出在最开始——配对选错了。

这一章,我们就来聊聊怎么科学地选配对。说白了,就是怎么用数学工具,找到那些「天生就该一起走」的股票对。

4.1 为什么需要距离方法?

你想想看,A股市场几千只股票,两两组合有多少种可能?几百万对。我们不可能每一对都去测试。所以需要一种方法,快速筛选出「看起来有戏」的候选对。

距离方法,就是干这个的。它衡量两只股票价格走势的「相似度」。走势越像,越有可能存在稳定的价差关系。

核心思想:如果两只股票长期走势高度一致,那么它们之间的价差大概率会回归均值。这就是配对交易的基础。

4.2 欧氏距离:最直观的相似度度量

欧氏距离,说白了就是两只股票价格序列的「直线距离」。数学上很简单:

# 计算两只股票的欧氏距离
import numpy as np
import pandas as pd

def euclidean_distance(series1, series2):
    """
    计算两只股票价格序列的欧氏距离
    注意:需要先归一化,否则价格高的股票会主导距离
    """
    # 归一化到[0,1]区间
    s1_norm = (series1 - series1.min()) / (series1.max() - series1.min())
    s2_norm = (series2 - series2.min()) / (series2.max() - series2.min())
    
    # 计算欧氏距离
    dist = np.sqrt(np.sum((s1_norm - s2_norm) ** 2))
    return dist

# 示例:计算贵州茅台和五粮液的欧氏距离
maotai = pd.Series([1800, 1820, 1790, 1850, 1830])
wuliangye = pd.Series([220, 225, 218, 230, 228])

dist = euclidean_distance(maotai, wuliangye)
print(f"欧氏距离: {dist:.4f}")

我的经验:欧氏距离对价格水平很敏感。茅台1800块,五粮液200多块,直接算距离会被茅台主导。所以归一化是必须的。我在项目中吃过这个亏,一开始没归一化,选出来的配对全是高价股配高价股,毫无意义。

4.3 相关系数:衡量线性关系的强弱

相关系数大家应该很熟悉了。它衡量的是两只股票价格变动的同步性。值越接近1,说明它们涨跌越同步。

# 计算皮尔逊相关系数
def pearson_correlation(series1, series2):
    """
    计算两只股票价格序列的皮尔逊相关系数
    返回:相关系数(-1到1之间)
    """
    return series1.corr(series2)

# 示例
corr = pearson_correlation(maotai, wuliangye)
print(f"相关系数: {corr:.4f}")

# 实际应用中,我们通常用滚动窗口计算动态相关系数
def rolling_correlation(series1, series2, window=60):
    """
    计算滚动相关系数
    window: 滚动窗口大小(交易日)
    """
    return series1.rolling(window).corr(series2)

嗯,这里要注意:相关系数只捕捉线性关系。如果两只股票存在非线性关系(比如A涨1%,B涨2%),相关系数可能不高,但它们仍然可能适合配对。

我曾经踩过的坑:只看相关系数选配对。有一次选出了相关系数0.98的配对,结果回测时亏得一塌糊涂。后来发现,这两只股票虽然走势高度相关,但价差没有均值回归特性。相关系数高 ≠ 适合配对交易。

4.4 最小距离法:综合排序的利器

最小距离法,就是把欧氏距离和相关系数结合起来,给每对股票打分。我个人的习惯是:

  1. 计算欧氏距离:距离越小越好
  2. 计算相关系数:相关系数越大越好
  3. 综合打分:把两个指标标准化后加权求和
def min_distance_scoring(pairs_df):
    """
    最小距离法打分
    pairs_df: 包含 'euclidean_dist' 和 'correlation' 两列
    返回:综合得分(越小越好)
    """
    # 标准化欧氏距离(越小越好,所以取负)
    dist_score = -pairs_df['euclidean_dist']
    dist_score = (dist_score - dist_score.mean()) / dist_score.std()
    
    # 标准化相关系数(越大越好)
    corr_score = pairs_df['correlation']
    corr_score = (corr_score - corr_score.mean()) / corr_score.std()
    
    # 综合得分(等权重)
    pairs_df['total_score'] = dist_score + corr_score
    
    # 按综合得分排序
    return pairs_df.sort_values('total_score', ascending=False)

# 示例数据
pairs_data = pd.DataFrame({
    'pair': ['茅台-五粮液', '茅台-泸州老窖', '五粮液-泸州老窖'],
    'euclidean_dist': [0.15, 0.22, 0.18],
    'correlation': [0.95, 0.88, 0.92]
})

result = min_distance_scoring(pairs_data)
print(result)

实战建议:我一般会给相关系数更高的权重(比如0.6),欧氏距离0.4。因为相关系数更能反映同步性,而欧氏距离容易受短期波动影响。当然,这个权重不是固定的,你可以根据市场环境调整。

4.5 配对筛选的实战流程

好了,理论讲完了。我们来看看实际怎么做。下面是我在项目中常用的完整流程:

import yfinance as yf
import pandas as pd
import numpy as np
from itertools import combinations

def pair_screening_pipeline(tickers, start_date, end_date, top_n=20):
    """
    配对筛选完整流程
    tickers: 候选股票列表
    start_date, end_date: 回测时间范围
    top_n: 返回前N对候选
    """
    # 1. 下载数据
    data = yf.download(tickers, start=start_date, end=end_date)['Adj Close']
    
    # 2. 计算日收益率(用收益率代替价格,避免价格水平影响)
    returns = data.pct_change().dropna()
    
    # 3. 生成所有可能的配对
    pairs = list(combinations(tickers, 2))
    
    results = []
    for pair in pairs:
        s1, s2 = pair
        
        # 4. 计算欧氏距离(用收益率序列)
        dist = np.sqrt(np.sum((returns[s1] - returns[s2]) ** 2))
        
        # 5. 计算相关系数
        corr = returns[s1].corr(returns[s2])
        
        # 6. 计算协整检验p值(可选,但强烈推荐)
        from statsmodels.tsa.stattools import coint
        _, p_value, _ = coint(data[s1], data[s2])
        
        results.append({
            'pair': f'{s1}-{s2}',
            'euclidean_dist': dist,
            'correlation': corr,
            'coint_pvalue': p_value
        })
    
    # 7. 转换为DataFrame并打分
    results_df = pd.DataFrame(results)
    
    # 过滤:相关系数 > 0.8,协整p值 < 0.05
    filtered = results_df[
        (results_df['correlation'] > 0.8) & 
        (results_df['coint_pvalue'] < 0.05)
    ]
    
    # 8. 最小距离法排序
    final = min_distance_scoring(filtered)
    
    return final.head(top_n)

# 使用示例
tickers = ['600519.SS', '000858.SZ', '000568.SZ', '600809.SS', '000596.SZ']
top_pairs = pair_screening_pipeline(tickers, '2023-01-01', '2024-01-01')
print(top_pairs[['pair', 'correlation', 'coint_pvalue', 'total_score']])

4.6 知识体系总览

下面这张图,是我梳理的配对筛选核心逻辑。你看一遍,应该就能把握住整体脉络了。

配对筛选核心流程 第一步:数据准备 价格数据 → 收益率序列 第二步:计算距离 欧氏距离 + 相关系数 第三步:协整检验 ADF检验 p值 第四步:条件过滤 相关系数 > 0.8, p值 < 0.05 第五步:综合排序 最小距离法打分 Top N 候选配对 关键指标说明 • 欧氏距离:越小越好 • 相关系数:越大越好 • 协整p值:< 0.05 • 综合得分:越高越好

4.7 实战中的避坑指南

最后,分享几个我这些年总结的经验:

  • 不要只看历史:过去走势相似,不代表未来也会同步。我建议每3个月重新筛选一次配对。
  • 行业要一致:茅台和五粮液是同行业,配对有意义。茅台和中国石油,就算相关系数再高,也别碰。
  • 流动性要匹配:一只日成交额100亿,一只只有1000万,配对交易时滑点会让你亏到怀疑人生。
  • 协整检验不是万能的:它只能告诉你过去有没有协整关系,不能保证未来。我一般会结合滚动协整检验,看关系是否稳定。

我的小技巧:筛选出候选配对后,我会先做一轮「目视检查」。把两只股票的价格走势画在一张图上,用肉眼看看价差是否真的在均值附近来回摆动。有时候,数学指标完美,但图形一看就不对劲。这种直觉,做久了自然就有了。

好了,这一章的内容就到这里。距离方法和配对筛选,是配对交易的基石。把这些搞扎实了,后面的策略构建才能站得住脚。


专注资料整理