跳转至


课程  因子投资  机器学习  Python  Poetry  ppw  tools  programming  Numpy  Pandas  pandas  算法  hdbscan  聚类  选股  Algo  minimum  numpy  algo  FFT  模式识别  配对交易  GBDT  LightGBM  XGBoost  statistics  CDF  KS-Test  monte-carlo  VaR  回测  过拟合  algorithms  machine learning  strategy  python  sklearn  pdf  概率  数学  面试题  量化交易  策略分类  风险管理  Info  interview  career  xgboost  PCA  wavelet  时序事件归因  SHAP  Figures  Behavioral Economics  graduate  arma  garch  人物  职场  Quantopian  figure  Banz  金融行业  买方  卖方  story  量化传奇  rsi  zigzag  穹顶压力  因子  ESG  因子策略  投资  策略  pe  ORB  Xgboost  Alligator  Indicator  factor  alpha101  alpha  技术指标  wave  quant  algorithm  pearson  spearman  tushare  因子分析  Alphalens  涨停板  herd-behaviour  momentum  因子评估  review  SMC  聪明钱  trade  history  indicators  zscore  波动率  强化学习  顶背离  freshman  resources  others  AI  DeepSeek  network  量子计算  金融交易  IBM  weekly  LLT  backtest  backtrader  研报  papers  UBL  quantlib  jupyter-notebook  scikit-learn  pypinyin  qmt  xtquant  blog  static-site  duckdb  工具  colors  free resources  barra  world quant  Alpha  openbb  数据  risk-management  llm  prompt  CANSLIM  Augment  arsenal  copilot  vscode  code  量化数据存储  hdf5  h5py  cursor  augment  trae  Jupyter  jupysql  pyarrow  parquet  数据源  quantstats  实盘  clickhouse  notebook  redis  remote-agent  AI-tools  Moonshot  回测,研报,tushare 

algo »

hdbscan 聚类算法扫描配对交易 速度提升99倍


配对交易是一种交易策略,由摩根士丹利的量化分析师农齐奥.塔尔塔里亚在 20 世纪 80 年代首创。他现在是意大利雅典娜资产管理公司的创始人。该策略涉及监控两只历史上相关性较强的证券,并监控两者之间的价格差。一旦价格差超出一个阈值(比如 n 倍的标准差),价格回归就是大概率事件,于是交易者就做空价格高的一支,做多价格低的一支,从而获利。

农齐奥.塔尔塔里亚

这种策略允许交易者在几乎任何市场条件下获利,因此一直是大型资产管理机构的主流策略。

例如,通用汽车和福特生产类似的产品(汽车),因此基于整体汽车市场,它们的股价走势相似。如果通用汽车的股价显著上涨而福特的股价保持不变,采用对冲交易策略的人就会卖出通用汽车的股票并买入福特的股票。假设股价会回到历史平衡点:如果通用汽车的股价下跌,投资者会获利;如果福特的股价上涨,投资者也会获利。

这个策略的正确性和有效性无可质疑,它几乎像数学定理一样完美。但是,如何才能找到这样的配对股资产呢?

显然,同行业的几家龙头公司,它们之间常常满足这样的关系。但是,正因为几乎所有人都知道其中存在着套利机会,市场的有效性就会在一定程度上对这种套利空间进行压缩。比如,如果你对过去几年的建设银行和工商银行执行配对交易策略的话,年化利润将不超过 1%。

而且,公司的业务形态总在发生变化,过去同质化竞争的企业,可能突然就因为收购、开拓新业务而不再是同一赛道的战友。老的赛道,也随时可能有新的掠食者加入进来。历史既在重复自己,也在开拓新的可能。

作为量化人,我们能够纯粹依赖数据分析,更加敏锐、快速找到这些新的机会吗?

答案是肯定的:我们可以仅凭资产的价格走势,通过协整检验,来发现可能的配对资产。

协整关系和平稳性

我们应该如何用数学的语言来描述像通用与福特之间的关系呢?这种关系在数学(更精确地说,在统计学上)上被称为协整关系

要如何检验协整关系呢? Engle(恩格尔)和 Granger(格兰杰)在 1987 年提出了协整检验的两步法:先用 OLS 对两个变量 X 和 Y 进行回归,再用 ADF 检验 X 和 Y 是否平稳。他们俩人也因为在协整理论方面的开创性工作,获得了 2003 年的诺贝尔经济学奖。

这里又涉及到另一个概念,即平稳时间序列和平稳性检验。

平稳时间序列指随机变量的统计特性(如均值、方差、自协方差等)在时间上是不变的。比如,白噪声是平稳时间序列,它在均值和方差都是恒定的。下图显示了时间平稳序列(左上)和几种非时间平稳序列的对比:

各种时间序列

显然,对量化人来说,平稳时间序列有着非常有趣的特性:既然一个平稳时间序列的均值、方差恒定并且有自协相关特性,那么,一旦它偏离了均值,迟早都会回归到均值上,否则,它就不是平稳时间序列。

于是,基于一个假设条件(该时间序列过去和现在都是平稳时间序列,未来仍然也是),我们获得了难得的预言未来的能力

检验一个时间序列是否平稳,一般使用 Dickey-Fuller 检验。

对量化人来说,资产价格序列和收益率是常见的两种时间序列。我们可以通过 Dickey-Fuller 检验来判断它们是否是平稳的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.stattools import adfuller

start = datetime.date(2023, 1, 1)
end = datetime.date(2023, 12, 30)
barss = load_bars(start, end, ('000001.XSHE',))

close = barss.xs("000001.XSHE", level=1).close
returns = close.diff().dropna()

# 绘制平稳时间序列
plt.figure(figsize=(12, 6))
ax1 = plt.gca()
ax1.plot(close, label='Price')
ax1.grid(False)

ax2 = ax1.twinx()
ax2.plot(returns, label="Returns", color='orange')
ax2.grid(False)

lines, labels = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines + lines2, labels + labels2, loc='best')

plt.show()

# 进行 ADF 检验
result1 = adfuller(close)
result2 = adfuller(returns)

df = pd.DataFrame({
    "ADF Stat": (result1[0], result2[0]),
    "P-Value": (result1[1], result2[1]),
    "Critical Values 1%": (result1[4]["1%"], result2[4]["1%"]),
    "Critical Values 5%": (result1[4]["5%"], result2[4]["5%"]),
    "Critical Values 10%": (result1[4]["10%"], result2[4]["10%"]),
     }, index=["close", "returns"])

df.T

这段代码显示结果如下:

平稳性检验是协整检验的第二步。第一步则是对相关联的两个时间序列进行回归,构建残差序列。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller

start = datetime.date(2021, 1, 1)
end = datetime.date(2023, 12, 30)

gsyh = "601398.XSHG"
jsyh = "601939.XSHG"

barss = load_bars(start, end, (gsyh, jsyh))

pair1 = barss.xs(gsyh, level=1).close
pair2 = barss.xs(jsyh, level=1).close

def hedge_ratio(price1: NDArray, price2: NDArray) -> float:
    X = sm.add_constant(price1)
    model = sm.OLS(price2, X).fit()
    return model.params[1]

hr = hedge_ratio(pair1, pair2)
print(f"hedge_ratio 为:{hr:.2f}")
spreads = pair2 - pair1 * hr
result = adfuller(spreads)
if result[1] < 0.05:
    print(f"p-value: {result[1]:.2f} < 0.05, 协整关系成立")
else:
    print(f"p-value: {result[1]:.2f} > 0.05 协整关系不成立")

这段代码就完整地完成了一个协整检验。不仅如此,它还计算出了两个价格序列之间的对冲比(hedge ratio),正是通过 hedge ratio,我们才能构建出具有平稳性的价差序列。

不过,了解原理之后,一般情况下,我们并不会使用上面的方法来进行协整检验,因为 statsmodels 已经帮我们完成了所有这些工作:

1
2
3
4
from statsmodels.tsa.stattools import coint

result = coint(pair1, pair2)
t, p_value, *_ = result

只要 p 值小于 0.05,我们就可以认为这两个时间序列是协整的。

性能问题

既然协整检验这么容易,那么,寻找配对交易的过程岂不是易如反掌?然而并不!

在一组资产池里,寻找协整对的计算量是一个组合数。如果资产池里有 100 支资产,计算量会是 4950 次。如果我们把资产池限定为国内资产,考虑到协整关系可能发生在 A 股、基金、期货和指数间,那么,资产总数在 1 万支左右,这样计算量就变成了接近 5000 万次。这个运算量实在是太大了。

在我们的课程环境里,运算 3 万次协整运算的时间约 20 分钟,也就是一次协整运算大约耗时 40 毫秒。所以,如果要完成近 5000 万次的协整检验,大概需要运行 24 天左右:来不及等你算完,长在你卫生间水管里的小虫子 -- 白斑蛾蚋已经走过了它的一生。

白斑蛾蚋

此时,聚类算法就可以发挥它的巨大威力了:如果我们能把这些资产分成若干簇,只在簇内进行协整检验,这样就可以大大减少协整检验的次数。

假设我们可以将样本聚类成\(K\)个簇,且每簇内样本数量分别为\(( N_1, N_2, \ldots, N_K )\)\(( \sum_{i=1}^{K} N_i = N )\)

那么,总的协整检验次数为:

\[\sum_{i=1}^{K} \binom{N_i}{2} = \sum_{i=1}^{K} \frac{N_i (N_i - 1)}{2}\]

如果每个簇的样本数量大致相同,此时总的协整检验次数就会少不少。比如,如果有 100 支资产,可以聚类成 10 个 size 一样的簇,那么总的协整检验次数就只有\(10 * C_{10}^2\)次,即 450 次,比之前的 4950 次,下降了 90%还多。

hdbscan 聚类

聚类算法有很多种,sklearn 中提到的就有 10 多种。不过,对通用目的而言,最好的聚类算法可能是 hdbscan。它不令速度快(仅次于 kmeans),而且能容忍噪声,并且超参数也很容易理解和设置。

下图是在同一个数据集上,hdbscan, dbscan 和 k-means 聚类的效果对比:

DBSCAN 不能找出比指定的核密度更稀疏的簇;k-means 会把噪声也归划分到距离最近的簇中,并且你得告诉它这个数据集中有多少个簇;hdbscan 的表现几近完美。

接下来,我们就运用 hdbscan 来对资产进行聚类。在聚类完成之后,我们还将对它进行协整检验,并将最终结果可视化展示出来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import hdbscan
import pandas as pd
from sklearn.manifold import TSNE
import plotly.express as px

start = datetime.date(2022, 1, 1)
end = datetime.date(2023,12,31)

barss = load_bars(start, end, 2000)

closes = (barss["close"].unstack().
                        ffill().
                        dropna(axis=1, how='any'))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 使用 HDBSCAN 进行聚类
clusterer = hdbscan.HDBSCAN(min_cluster_size=3, min_samples=2)
cluster_labels = clusterer.fit_predict(closes.T)

# 将聚类结果添加到 DataFrame 中
clustered = closes.T.copy()
clustered['cluster'] = cluster_labels

# 剔除类别为-1 的点,这些是噪声,而不是一个类别
clustered = clustered[clustered['cluster'] != -1]
clustered_close = clustered.drop("cluster", axis=1)

# 使用 t-SNE 进行降维
tsne = TSNE(n_components=3, random_state=42)
tsne_results = tsne.fit_transform(clustered_close)

# 将 t-SNE 结果添加到 DataFrame 中
reduced_tsne = pd.DataFrame(data=tsne_results, 
                            columns=['tsne_1', 'tsne_2', 'tsne_3'],
                            index=clustered_close.index)

reduced_tsne['cluster'] = clustered['cluster']

fig_tsne = px.scatter_3d(
    reduced_tsne, 
    x='tsne_1', y='tsne_2', z='tsne_3',
    color='cluster', 
    title='t-SNE Clustering of Stock Returns',
    labels={'tsne_1': 't-SNE Component 1', 
            'tsne_2': 't-SNE Component 2'}
)

fig_tsne.layout.width = 1200
fig_tsne.layout.height = 1100

fig_tsne.show()

我们将得到一个三维的 t-SNE 图,其中颜色表示每个点的聚类。在我们的研究平台上,你可以运行代码并生成这个三维图,然后拖拽它以变换观察视角。

hdbscan 的聚类效果如何?我们能否在它划分的类别中,寻找到协整对呢?我们通过下面的代码来可视化簇类的样本走势,并执行协整检验。这是随机抽取的第 12 簇的情况:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from statsmodels.tsa.stattools import coint
import hdbscan
import pandas as pd
from sklearn.manifold import TSNE
import plotly.express as px

start = datetime.date(2022, 1, 1)
end = datetime.date(2023,12,31)

barss = load_bars(start, end, 2000)

closes = barss["close"].unstack().ffill().dropna(axis=1, how='any')

# 使用 HDBSCAN 进行聚类
clusterer = hdbscan.HDBSCAN(min_cluster_size=3, min_samples=2)
cluster_labels = clusterer.fit_predict(closes.T)
clustered = closes.T.copy()
clustered['cluster'] = cluster_labels

# 剔除类别为-1 的点,这些是噪声,而不是一个类别
clustered = clustered[clustered['cluster'] != -1]
clustered_close = clustered.drop("cluster", axis=1)

plt.figure(figsize=(12,10))
cluster_12 = clustered.query("cluster == 12").index.tolist()
for code in cluster_12:
    bars = barss.xs(code, level=1)["close"]
    plt.plot(bars)

pairs = []
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
for i in range(len(cluster_12)):
    for j in range(i + 1, len(cluster_12)):
        pair1 = cluster_12[i]
        pair2 = cluster_12[j]
        price1 = barss.xs(pair1, level=1)["close"].ffill().dropna()
        price2 = barss.xs(pair2, level=1)["close"].ffill().dropna()
        minlen = min(len(price1), len(price2))
        t, p, *_ = coint(price1[-minlen:], price2[-minlen:])
        if p < 0.05:
            pairs.append((pair1, pair2))

row = max(1, len(pairs) // 3)
col = len(pairs) // row

if row * col < len(pairs):
    row += 1

cells = row * col

fig, axes = plt.subplots(row, col, figsize=(col * 3,row * 3))
axes = np.array(axes).flatten()
fig.suptitle('Cointegrated Pairs')

plot_index = 0
for pair1, pair2 in pairs:
    ax = axes[plot_index]

    price1 = barss.xs(pair1, level=1)["close"]
    price2 = barss.xs(pair2, level=1)["close"]

    ax.plot(price1, label=pair1)
    ax.plot(price2, label=pair2)
    ax.set_title(f'{pair1[:-5]} & {pair2[:-5]}')
    ax.set_xticks([])
    plot_index += 1

plt.tight_layout()
plt.show()
print(len(pairs)/(len(cluster_12) * (len(cluster_12) - 1)) * 2)
![](https://images.jieyu.ai/images/2025/01/cluster-12-all.jpg) 从结果可以看出,簇类的个股走势几乎一样,但不是簇内所有的组合都是协整对。这也很容易理解,毕竟,**聚类有聚类的逻辑,协整又是协整的逻辑**。两者有重叠的空间,但决不是一码事儿。 重要的是,我们大大压缩了计算时间。叹隙中驹,石中火,梦中身,但现在,只在白驹隙的这一瞬,我们就发现了新的财富密码。抢先在量化中是极其重要的,一旦机会被其他人也找到,有限的资金容量就会被他人抢走。 ## 摇人!赶快摇人!
匡醍科技 (Quantide) 新年开始,要招人啦!我们在各个招聘平台发了英雄贴,不过,最契合的候选人很可能应该来自于公众号的粉丝。如果你喜欢我的文章,也可能喜欢跟我一起工作,一起探索量化交易。 在匡醍,我们一边研究(量化框架研发、交易策略),一边产出(通过公众号或者视频号),永远在成长,费曼学习法。毕竟,量化研究就象西西弗斯,你每天滚石上山,但没有一劳永逸的成功,只有日复一日的失败、学习和再次出发! 具体地说,这个职位有以下非(脱敏)典型症状,精通量化所需要的概率、统计知识,熟练掌握 Python 和 numpy, pandas,statsmodels, scipy, 机器学习算法,文字爱好者,会修图,懂 AI,新概念、新技术的 super fans。妥妥的六边形战士。不过,不管你是三角形还是正方形,只要真正的热爱这样的工作,都可以来撩。我现在最想要的,是一个学习博主,真真正正学习的那种,不是只摆个 pose 的那种。 _具体岗位请致电 [email protected] 查询,或者直接空投简历,在校生可投实习岗位_ ![](https://images.jieyu.ai/images/2025/01/optical-valley.jpg) 热爱是最好的老师。在宇宙中心等你。