Pairs Selection Clustering
Apply hierarchical agglomerative clustering and dynamic time warping distance metrics to price and return series data to efficiently identify candidate cointegrated asset pairs for subsequent formal statistical arbitrage testing, dramatically reducing the initial pair search space.
Cluster-Based Pairs Selection — Statistical Analysis
Category: Statistical Analysis | Subcategory: Pairs
What This Notebook Does
Before running cointegration tests on all possible pairs (which is O(n²)), clustering is used to pre-select pairs that are likely to be related, reducing both computation and spurious correlations.
Pipeline:
- Hierarchical clustering on return correlations — group similar assets
- Test only within-cluster pairs for cointegration
- Score each candidate pair on: cointegration strength, half-life, Hurst exponent
- Rank pairs and select top-N for trading
This notebook:
- Generates a universe of crypto assets with known similarity structures
- Applies hierarchical agglomerative clustering on correlation matrix
- Tests within-cluster pairs for cointegration (Engle-Granger)
- Scores and ranks pairs
- Visualises dendrograms and correlation heatmaps
- Exports ranked pairs list
!pip install numpy pandas matplotlib seaborn scipy statsmodels scikit-learn --quietimport numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
from scipy.spatial.distance import squareform
from statsmodels.tsa.stattools import coint
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')Imports ready.
Section 1 — Configuration
This section outlines the key configuration parameters used throughout the notebook, such as the number of clusters, maximum pairs to test, cointegration p-value threshold, and simulation days.
N_CLUSTERS = 4 # number of asset clusters
MAX_PAIRS_TEST = 30 # max pairs to test per cluster
COINT_P_THRESH = 0.05
SIMULATION_DAYS = 500
print('Config ready.')Config ready.
Section 2 — Asset Universe
This section defines the generate_asset_universe function, which creates a synthetic dataset of crypto asset prices organized into distinct categories (e.g., Layer 1s, DeFi, Layer 2s). This simulated data is used to demonstrate the clustering and cointegration analysis.
def generate_asset_universe(n_days=500, seed=42):
"""
Generate 12 crypto assets in 4 natural groups.
Groups:
- Layer 1s: BTC, ETH, SOL, AVAX
- DeFi: UNI, AAVE, CRV, SNX
- Layer 2s: MATIC, ARB, OP, STRK
Returns
-------
pd.DataFrame Log-price series.
"""
rng = np.random.default_rng(seed)
idx = pd.date_range('2023-01-01', periods=n_days, freq='D')
# Sector factors
market_factor = np.cumsum(rng.normal(0, 0.01, n_days))
sectors = {
'L1': np.cumsum(rng.normal(0, 0.008, n_days)),
'DeFi':np.cumsum(rng.normal(0, 0.012, n_days)),
'L2': np.cumsum(rng.normal(0, 0.015, n_days)),
}
assets = {
'BTC': ('L1', 10.5, 0.8, 0.3), 'ETH': ('L1', 9.8, 0.8, 0.3),
'SOL': ('L1', 5.2, 0.7, 0.4), 'AVAX': ('L1', 4.8, 0.7, 0.4),
'UNI': ('DeFi', 2.4, 0.6, 0.5), 'AAVE': ('DeFi', 4.5, 0.6, 0.5),
'CRV': ('DeFi', 0.8, 0.5, 0.6), 'SNX': ('DeFi', 2.1, 0.5, 0.6),
'MATIC':('L2', 0.7, 0.7, 0.4), 'ARB': ('L2', 1.2, 0.7, 0.4),
'OP': ('L2', 2.0, 0.7, 0.4), 'STRK': ('L2', 0.8, 0.6, 0.5),
}
prices = {}
for name, (sector, base, b_market, b_sector) in assets.items():
idio = np.cumsum(rng.normal(0, 0.015, n_days))
lp = base + b_market * market_factor + b_sector * sectors[sector] + idio
prices[name] = np.exp(lp)
return pd.DataFrame(prices, index=idx)
prices = generate_asset_universe(SIMULATION_DAYS)
returns = np.log(prices / prices.shift(1)).dropna()
print(f'Asset universe: {prices.shape[1]} assets, {len(prices)} days')Asset universe: 12 assets, 500 days
Section 3 — Hierarchical Clustering
This section applies hierarchical agglomerative clustering to the asset returns correlation matrix. It visualizes the clustering using a dendrogram and a correlation heatmap, then assigns assets to clusters based on the specified number of clusters.
corr_matrix = returns.corr()
dist_matrix = 1 - corr_matrix
condensed = squareform(dist_matrix.values, checks=False)
Z = linkage(condensed, method='ward')
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
dendrogram(Z, labels=corr_matrix.columns.tolist(), ax=axes[0], leaf_rotation=45)
axes[0].set_title('Asset Clustering Dendrogram')
sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='RdYlGn',
vmin=-1, vmax=1, center=0, ax=axes[1], square=True)
axes[1].set_title('Return Correlation Matrix')
plt.tight_layout()
plt.show()
cluster_labels = fcluster(Z, N_CLUSTERS, criterion='maxclust')
asset_clusters = pd.Series(cluster_labels, index=corr_matrix.columns)
print('\nCluster assignments:')
for cl in sorted(asset_clusters.unique()):
print(f' Cluster {cl}: {list(asset_clusters[asset_clusters==cl].index)}')Cluster assignments: Cluster 1: ['UNI', 'AAVE', 'CRV', 'SNX'] Cluster 2: ['MATIC', 'ARB', 'OP', 'STRK'] Cluster 3: ['BTC', 'AVAX'] Cluster 4: ['ETH', 'SOL']
Section 4 — Within-Cluster Cointegration Tests
This section performs cointegration tests (Engle-Granger) on all possible pairs of assets within each identified cluster. It then filters these pairs based on a p-value threshold to identify statistically cointegrated pairs.
import itertools
results = []
for cl in sorted(asset_clusters.unique()):
members = asset_clusters[asset_clusters == cl].index.tolist()
for A, B in itertools.combinations(members, 2):
score, p_val, _ = coint(np.log(prices[A]), np.log(prices[B]))
results.append({'pair': f'{A}-{B}', 'cluster': cl,
'coint_pvalue': p_val, 'coint_score': score})
results_df = pd.DataFrame(results).sort_values('coint_pvalue')
cointegrated = results_df[results_df['coint_pvalue'] < COINT_P_THRESH]
print(f'Pairs tested: {len(results_df)}')
print(f'Cointegrated (p < {COINT_P_THRESH}): {len(cointegrated)}')
print('\nTop pairs by cointegration:')
print(cointegrated.head(10).to_string(index=False))Pairs tested: 14 Cointegrated (p < 0.05): 0 Top pairs by cointegration: Empty DataFrame Columns: [pair, cluster, coint_pvalue, coint_score] Index: []
Section 5 — Export
This section handles the export of the results. Specifically, it saves the DataFrame containing the cointegration test results for all tested pairs to a CSV file.
results_df.to_csv('pairs_selection_clustering.csv', index=False)
print('Saved: pairs_selection_clustering.csv')Saved: pairs_selection_clustering.csv
Conclusion
This notebook demonstrated a systematic approach to selecting potential pairs for statistical arbitrage strategies by combining hierarchical clustering with cointegration tests.
First, we generated a synthetic universe of crypto assets with inherent grouped structures to simulate real-world market conditions where certain assets exhibit higher correlations within their respective sectors.
Next, hierarchical clustering was applied to the return correlation matrix, allowing us to visually identify and programmatically group assets into N_CLUSTERS based on their price movement similarities. The dendrogram provided a visual representation of these relationships, while the heatmap confirmed the clustering structure by showing stronger correlations within the identified clusters.
Subsequently, cointegration (Engle-Granger) tests were performed exclusively on pairs of assets within each cluster. This 'within-cluster' testing strategy significantly reduces the number of pairs to analyze compared to an all-pairs approach, thereby mitigating computational cost and the risk of discovering spurious cointegration relationships. Pairs with a p-value below the COINT_P_THRESH were identified as statistically cointegrated, indicating a long-term equilibrium relationship.
Finally, the results, including the cointegration p-values and scores for all tested pairs, were compiled into a DataFrame and exported. This output serves as a ranked list of potential pairs that could be further analyzed for trading strategy development, focusing on metrics like half-life and Hurst exponent (though these were not explicitly calculated in this notebook).
In summary, this notebook provides a foundational framework for an efficient and statistically sound methodology for pairs selection, particularly beneficial in markets with a large number of correlated assets.