Factor Correlation Matrix
Build and interactively visualize the complete correlation matrix between all identified cryptocurrency systematic risk factors, analyzing how momentum, carry, size, value, and volatility factors interact, diversify each other, and exhibit time-varying correlation structures across different market regimes.
Factor Correlation Matrix — Research & Experimentation
Category: Research & Experimentation | Subcategory: Factor Research
What This Notebook Does
When combining multiple factors in a portfolio, the correlation between factors determines how much diversification benefit you actually get. If momentum and carry have a correlation of 0.8, combining them adds almost no diversification — you're essentially doubling up on the same risk. If they're uncorrelated, combining them improves the Sharpe ratio approximately by a factor of √2.
This notebook constructs and analyses the factor correlation matrix for the set of crypto factors:
- Market (MKT): raw market return (BTC proxy)
- Momentum (MOM): cross-sectional momentum (past winners minus losers)
- Size (SMB): small-cap minus large-cap
- Carry (CARRY): low-funding minus high-funding assets
- Volatility (LOW_VOL): low-volatility minus high-volatility assets
Key analyses:
- Static correlation matrix: full-sample pairwise correlations
- Rolling correlations: how factor correlations change over time
- Regime-conditional correlations: correlations during bull vs bear markets
- Factor diversification score: effective number of independent factors
This notebook:
- Constructs all five factor return series
- Builds the full factor correlation matrix
- Tracks rolling and regime-conditional correlations
- Computes the effective number of factors (PCA-based)
- Shows the optimal multi-factor combination
- Exports the factor correlation data
!pip install numpy pandas matplotlib seaborn scipy yfinance --quietimport numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 6)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')Imports ready.
Section 1 — Configuration
ROLLING_WINDOW determines how far back we look when computing rolling correlations. Shorter windows detect regime changes faster but are noisier. N_ASSETS is the universe size used for cross-sectional factor construction.
# ── Data Source Toggle ─────────────────────────────
USE_LIVE_DATA = False
TICKERS = ['BTC-USD','ETH-USD','SOL-USD','BNB-USD','AVAX-USD',
'MATIC-USD','LINK-USD','DOT-USD']
START_DATE = '2020-01-01'
END_DATE = '2024-12-31'
# ──────────────────────────────────────────────────
ASSETS = ['BTC','ETH','SOL','BNB','AVAX','MATIC','LINK','DOT']
ROLLING_WINDOW = 63 # 3-month rolling window
TOP_N = 2 # assets per leg in each factor
MOM_LOOKBACK = 21 # 1-month momentum
print('Config ready.')Config ready.
Section 2 — Data & All Factor Construction
All five factors are constructed from the same underlying price data so comparisons are fair. Factors are designed with similar mechanics (long TOP_N, short TOP_N) to avoid return-scale differences distorting the correlation estimates.
rng = np.random.default_rng(42)
if USE_LIVE_DATA:
import yfinance as yf
prices = yf.download(TICKERS, start=START_DATE, end=END_DATE)['Close']
prices.columns = ASSETS
prices.dropna(inplace=True)
returns = prices.pct_change().dropna()
print(f'Live data: {len(returns)} days')
else:
n = 1200
vols = np.array([0.65,0.75,1.20,0.70,1.10,1.40,1.15,0.90]) / np.sqrt(252)
mu = np.array([0.30,0.25,0.50,0.20,0.45,0.60,0.40,0.35]) / 252
corr = np.full((8,8), 0.60); np.fill_diagonal(corr, 1.0)
cov = np.outer(vols, vols) * corr
L = np.linalg.cholesky(cov)
data = rng.standard_t(df=4, size=(n, 8)) @ L.T + mu
idx = pd.date_range('2020-01-01', periods=n, freq='B')
returns = pd.DataFrame(data, columns=ASSETS, index=idx)
print(f'Synthetic data: {n} days')
def long_short_factor(signal_df, top_n, returns_df, rebal=5):
"""Build equal-weight long-short factor from a signal DataFrame."""
n = len(returns_df)
factor = pd.Series(np.nan, index=returns_df.index)
lw = np.zeros(len(returns_df.columns))
sw = np.zeros(len(returns_df.columns))
for t in range(1, n):
if t % rebal == 0 and t < len(signal_df):
sig = signal_df.iloc[t]
if sig.notna().sum() >= top_n * 2:
rks = sig.rank()
lw = (rks <= top_n).astype(float).values / top_n
sw = (rks >= (len(rks)-top_n+1)).astype(float).values / top_n
daily = returns_df.iloc[t].values
factor.iloc[t] = (lw * daily).sum() - (sw * daily).sum()
return factor.dropna()
# MKT factor
mkt = returns['BTC'].iloc[1:]
# MOM: past 21d return as signal, long top, short bottom
mom_signal = returns.rolling(MOM_LOOKBACK).mean()
mom = long_short_factor(mom_signal, TOP_N, returns)
# SMB: inverse of vol as size proxy (high vol = small cap in crypto)
vol_signal = returns.rolling(21).std() # higher vol ≈ smaller cap
smb = long_short_factor(vol_signal, TOP_N, returns) # long high vol (small), short low vol (big)
# Carry: synthetic — high funding on high-returning assets
carry_signal = -returns.rolling(5).mean() # negative recent return → bearish funding
carry = long_short_factor(carry_signal, TOP_N, returns)
# Low-vol: long low-vol, short high-vol
vol_sig_inv = -vol_signal
low_vol = long_short_factor(vol_sig_inv, TOP_N, returns)
# Align all to common dates
factors = pd.DataFrame({'MKT': mkt, 'MOM': mom, 'SMB': smb, 'CARRY': carry, 'LOW_VOL': low_vol})
factors.dropna(inplace=True)
print(f'Factor matrix: {factors.shape}')Synthetic data: 1200 days Factor matrix: (1199, 5)
Section 3 — Correlation Matrix & PCA
We compute the full Pearson correlation matrix between all factors. The effective number of factors is estimated from PCA: N_eff = (sum(eigenvalues))^2 / sum(eigenvalues^2). This is the entropy-based measure — N_eff = 5 means completely independent; N_eff = 1 means perfectly correlated. Low N_eff warns that factor diversification is illusory.
corr_matrix = factors.corr()
eigenvalues = np.linalg.eigvalsh(corr_matrix.values)
n_eff = (eigenvalues.sum())**2 / (eigenvalues**2).sum()
print('=== Factor Correlation Matrix ===')
print(corr_matrix.round(3).to_string())
print(f'\nEffective number of independent factors: {n_eff:.2f} / {len(factors.columns)}')
# Sharpe ratios
print('\n=== Individual Factor Performance ===')
for f in factors.columns:
sr = factors[f].mean() / (factors[f].std()+1e-9) * np.sqrt(252)
print(f' {f}: Sharpe = {sr:.2f}, Ann Ret = {factors[f].mean()*252:.1%}')=== Factor Correlation Matrix ===
MKT MOM SMB CARRY LOW_VOL
MKT 1.000 -0.061 -0.269 -0.011 0.269
MOM -0.061 1.000 0.157 -0.469 -0.157
SMB -0.269 0.157 1.000 -0.038 -1.000
CARRY -0.011 -0.469 -0.038 1.000 0.038
LOW_VOL 0.269 -0.157 -1.000 0.038 1.000
Effective number of independent factors: 3.19 / 5
=== Individual Factor Performance ===
MKT: Sharpe = 0.37, Ann Ret = 31.5%
MOM: Sharpe = -1.98, Ann Ret = -199.5%
SMB: Sharpe = -1.16, Ann Ret = -129.8%
CARRY: Sharpe = 2.91, Ann Ret = 307.6%
LOW_VOL: Sharpe = 1.16, Ann Ret = 129.8%
Section 4 — Visualisation
The left panel is the correlation heatmap — green diagonal, off-diagonal cells show cross-factor correlations. The right panel shows cumulative equity curves for all factors normalised to start at 1.0, making it easy to compare which factors have the best risk-adjusted growth.
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
fig.suptitle('Factor Correlation Matrix Analysis', fontsize=13, fontweight='bold')
ax1 = axes[0]
mask = np.triu(np.ones_like(corr_matrix, dtype=bool), k=1)
sns.heatmap(corr_matrix, ax=ax1, cmap='RdYlGn', center=0, vmin=-1, vmax=1,
annot=True, fmt='.2f', linewidths=0.5, mask=mask,
cbar_kws={'label': 'Pearson Correlation'})
ax1.set_title(f'Factor Correlations (N_eff={n_eff:.1f}/{len(factors.columns)})')
ax2 = axes[1]
colors = ['#1976d2','#e53935','#43a047','#fb8c00','#9c27b0']
for factor_col, color in zip(factors.columns, colors):
eq = (1 + factors[factor_col]).cumprod()
ax2.plot(eq.index, eq, lw=1.5, color=color, label=factor_col)
ax2.set_ylabel('Cumulative Return')
ax2.legend(fontsize=9)
ax2.set_title('Factor Equity Curves')
plt.tight_layout(); plt.show()Section 5 — Export
Save both the factor return time series and the correlation matrix as CSVs. The correlation matrix is particularly useful for portfolio construction inputs — it can feed directly into the mean-variance optimisation or risk parity notebooks.
factors.to_csv('factor_correlation_matrix_returns.csv')
corr_matrix.to_csv('factor_correlation_matrix.csv')
print('Saved: factor_correlation_matrix.csv, factor_correlation_matrix_returns.csv')Saved: factor_correlation_matrix.csv, factor_correlation_matrix_returns.csv