4. 距离方法与配对选择:最小化距离法、标准化欧氏距离、时间序列的相似性度量、配对筛选的实战技巧
配对交易的核心,说白了就是找两只走势高度相似的股票。但怎么定义「相似」?靠眼睛看K线图?不靠谱。我见过太多新手,看着两段曲线差不多就冲进去,结果价差越走越远,最后亏得莫名其妙。
这一章,我们就来聊聊如何用数学工具量化「相似性」。我会把最小化距离法、标准化欧氏距离、时间序列相似性度量这些概念,结合我实际踩过的坑,掰开揉碎了讲清楚。
核心观点:配对筛选不是找「过去长得像」的股票,而是找「未来价差能回归」的股票。距离度量只是第一步,后续的协整检验和半衰期计算才是真正的试金石。
4.1 最小化距离法:最直观的配对方式
最小化距离法的思路很简单:把两只股票的历史价格序列对齐,计算每个时间点的差值平方和,然后求和。和越小,说明两只股票走得越近。
公式长这样:
D(P, Q) = Σ (Pᵢ - Qᵢ)²
其中 Pᵢ 和 Qᵢ 是两只股票在第 i 天的价格。注意,这里用的是标准化后的价格,不是原始价格。我刚开始做的时候,直接用原始价格算,结果茅台和工商银行的距离永远很大——因为茅台单价高啊。后来才意识到,必须先做标准化处理。
我的习惯:标准化时用 z-score,即 (价格 - 均值) / 标准差。这样所有股票都变成均值为0、标准差为1的序列,可比性大大提升。
具体实现代码:
import numpy as np
import pandas as pd
def min_distance(prices_a, prices_b):
"""
计算最小化距离
prices_a, prices_b: 标准化后的价格序列
"""
# 确保长度一致
min_len = min(len(prices_a), len(prices_b))
a = prices_a[-min_len:]
b = prices_b[-min_len:]
# 计算欧氏距离平方和
distance = np.sum((a - b) ** 2)
return distance
# 示例:计算两只股票的标准化距离
def zscore_series(series):
return (series - series.mean()) / series.std()
# 假设 df 包含多只股票的历史价格
# 对每只股票做 z-score 标准化
df_z = df.apply(zscore_series)
# 计算所有配对的距离
pairs = []
for i in range(len(df_z.columns)):
for j in range(i+1, len(df_z.columns)):
dist = min_distance(df_z.iloc[:, i], df_z.iloc[:, j])
pairs.append((df_z.columns[i], df_z.columns[j], dist))
# 按距离升序排列
pairs_sorted = sorted(pairs, key=lambda x: x[2])
print("距离最小的前5个配对:")
for pair in pairs_sorted[:5]:
print(f"{pair[0]} - {pair[1]}: {pair[2]:.4f}")
避坑指南:我曾经用60天滚动窗口算距离,结果发现距离最小的配对经常是同一行业的两只股票。但实盘跑下来,很多配对在半衰期还没到就崩了。原因在于:最小化距离法只关注「形状相似」,不关注「价差是否具有均值回复性」。所以它只能作为初筛工具,不能作为最终决策依据。
4.2 标准化欧氏距离:解决量纲问题
标准化欧氏距离,说白了就是给每个维度加上权重。在配对交易里,不同时间点的价格波动率不一样——比如最近一个月的波动可能比一年前大很多。标准化欧氏距离通过除以标准差,让每个时间点的贡献「平等」。
公式:
D_std(P, Q) = Σ ((Pᵢ - Qᵢ) / σᵢ)²
其中 σᵢ 是第 i 个时间点的标准差(通常用滚动窗口估计)。
嗯,这里要注意:标准化欧氏距离和 z-score 标准化是两回事。z-score 是对整个序列做标准化,而标准化欧氏距离是对每个时间点单独做标准化。我个人更推荐先用 z-score 标准化整个序列,再用普通欧氏距离——这样计算简单,效果也不差。
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 最小化距离法 | 快速初筛 | 计算简单,直观 | 忽略波动率差异 |
| 标准化欧氏距离 | 波动率变化大的市场 | 考虑时变波动 | 需要估计σᵢ,参数敏感 |
| z-score + 欧氏距离 | 通用场景 | 平衡了简单性和有效性 | 假设序列平稳 |
4.3 时间序列的相似性度量:不止是欧氏距离
欧氏距离有个硬伤:它要求两个序列在时间轴上严格对齐。但实际中,两只股票的走势可能「形状相似但相位不同」——比如A股比B股晚两天反应同一个消息。这时候欧氏距离会很大,但DTW(动态时间规整)就能捕捉到这种滞后相关性。
DTW 的核心思想:允许时间轴「拉伸」或「压缩」,找到最优的对齐路径。我曾在做跨市场配对时用过DTW,比如追踪A股和港股的同股同权标的。效果确实比欧氏距离好,但计算量也大得多——O(n²) 的复杂度,对于几千只股票的全市场扫描,基本跑不动。
我的建议:DTW 适合小规模精细分析(比如50只以内的候选池),不适合全市场扫描。全市场初筛还是用欧氏距离,把候选对压缩到100对以内,再用DTW做二次筛选。
除了DTW,还有几种常用的相似性度量:
- 相关系数:衡量线性相关性,但无法捕捉非线性关系。我见过有人用相关系数>0.9作为筛选条件,结果选出来的全是同行业龙头股——相关性高,但价差根本不回复。
- 互信息:能捕捉非线性关系,但计算复杂,且对数据量要求高。
- 余弦相似度:关注方向而非幅度,适合收益率序列的相似性比较。
4.4 配对筛选的实战技巧
理论说完了,来点干货。我总结了一套实战筛选流程,这些年一直在用:
- 行业初筛:先按行业分类,只在同行业内找配对。跨行业配对不是不行,但逻辑上更难解释,也更容易出现「伪回归」。
- 流动性过滤:剔除日均成交额低于1亿的股票。流动性差的股票,交易成本高,滑点大,策略很难赚钱。
- 距离计算:用 z-score 标准化后,计算欧氏距离。保留距离最小的前10%的配对。
- 协整检验:对初筛后的配对做 Engle-Granger 两步法检验。p值小于0.05才算通过。
- 半衰期计算:计算价差序列的半衰期。我一般要求半衰期在5-20个交易日之间——太短说明噪音大,太长说明回复太慢,资金效率低。
- 回测验证:用过去2年的数据做回测,看夏普比率和最大回撤。夏普比率低于1的直接淘汰。
实战经验:我做过一个统计,从全市场4000只股票出发,经过上述6步筛选,最终能进入实盘的配对通常不超过20对。而且这20对里,真正能稳定盈利的,可能只有5-8对。配对交易不是「找到一对就能赚钱」,而是「找到一堆候选,然后动态管理」。
最后说一个我踩过的坑:幸存者偏差。刚开始做回测时,我用的是当前还在交易的股票。结果回测曲线漂亮得不行,实盘却一塌糊涂。后来才发现,那些退市的、被ST的股票,在回测时已经被剔除了——但它们在历史上可能正是最好的配对标的。所以我现在做回测,一定用「历史快照」数据,即回测时间点实际存在的股票池。