Correlation Risk Monitor
Monitor portfolio correlation risk in real time by continuously tracking the average pairwise correlation level, the correlation matrix stability, and the effective portfolio diversification ratio, providing early warning detection when assumed diversification benefits begin deteriorating during market stress.
Correlation Risk Monitor — Portfolio Risk
Correlation risk is the risk that diversification benefits disappear exactly when you need them most — during a crash. This is the "all correlations go to 1" phenomenon: in normal markets, crypto assets have different correlations; in a crisis, they all crash together.
Monitoring correlation dynamics helps:
- Detect regime shifts — rising correlations signal growing systemic risk
- Measure true diversification — high average correlation = less diversification than weights imply
- Trigger de-risking — when average pairwise correlation exceeds a threshold, reduce exposure
Key metrics this notebook tracks:
- Rolling pairwise correlations: how each pair evolves over time
- Average correlation: single number summarising portfolio diversification quality
- Correlation regime: clustering correlation levels into low/medium/high regimes
- Diversification ratio:
weighted avg vol / portfolio vol— drops when correlations spike
This notebook:
- Fetches data (Yahoo Finance or synthetic)
- Tracks rolling pairwise and average correlations
- Classifies correlation regimes
- Computes the diversification ratio over time
- Builds a correlation risk alert system
- Exports correlation history
!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, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')Imports ready.
Configuration
CORR_WINDOW is the rolling window for correlation estimation — 30 days reacts quickly but is noisy; 90 days is smoother. CORR_ALERT_THRESHOLD triggers a risk alert when average pairwise correlation exceeds this level, indicating that diversification is being lost.
# ── Data Source Toggle ─────────────────────────────
USE_LIVE_DATA = False
TICKERS = ['BTC-USD','ETH-USD','SOL-USD','BNB-USD','AVAX-USD','MATIC-USD']
START_DATE = '2022-01-01'
END_DATE = '2024-12-31'
# ──────────────────────────────────────────────────
ASSETS = ['BTC','ETH','SOL','BNB','AVAX','MATIC']
WEIGHTS = np.array([0.30, 0.25, 0.18, 0.12, 0.08, 0.07])
CORR_WINDOW = 30
CORR_ALERT_THRESHOLD = 0.85 # alert when avg correlation exceeds this
print('Config ready.')Config ready.
Data Acquisition
The synthetic path deliberately cycles between low-correlation (normal) and high-correlation (crisis) regimes. Days 200–350 simulate a crash where all assets become highly correlated, making the correlation monitor fire its alert. This makes the detection capability clearly visible.
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:
rng = np.random.default_rng(42)
n = 730
vols = np.array([0.65,0.75,1.20,0.70,1.10,1.40]) / np.sqrt(252)
mu = np.array([0.40,0.35,0.60,0.25,0.55,0.70]) / 252
# Normal regime: moderate correlation
corr_normal = np.full((6,6), 0.55); np.fill_diagonal(corr_normal, 1.0)
# Crisis regime: high correlation
corr_crisis = np.full((6,6), 0.92); np.fill_diagonal(corr_crisis, 1.0)
data = np.zeros((n, 6))
for i in range(n):
corr_use = corr_crisis if 200 <= i <= 350 else corr_normal
cov = np.outer(vols, vols) * corr_use
L = np.linalg.cholesky(cov)
daily_mu = mu if i < 200 or i > 350 else -mu * 2
data[i] = rng.standard_normal(6) @ L.T + daily_mu
idx = pd.date_range('2022-01-01', periods=n, freq='B')
returns = pd.DataFrame(data, columns=ASSETS, index=idx)
print(f'Synthetic data with correlation regime shift: {n} days')Synthetic data with correlation regime shift: 730 days
Rolling Correlation & Diversification Ratio
The average pairwise correlation is the mean of all off-diagonal elements of the rolling correlation matrix. The diversification ratio (DR) is the ratio of the weighted average asset volatility to the portfolio volatility. DR = 1 means no diversification benefit; DR > 1 means volatilities partially cancel due to less-than-perfect correlation. When correlations spike to ~1, DR collapses toward 1.
avg_corr = []
div_ratio = []
alerts = []
for i in range(CORR_WINDOW, len(returns)):
window = returns.iloc[i - CORR_WINDOW:i]
corr = window.corr().values
# Average off-diagonal correlation
mask = np.ones_like(corr, dtype=bool)
np.fill_diagonal(mask, False)
avg_c = corr[mask].mean()
avg_corr.append(avg_c)
# Diversification ratio
vols_w = window.std().values * np.sqrt(252)
cov_w = window.cov().values * 252
weighted_avg_vol = WEIGHTS @ vols_w
port_vol = np.sqrt(WEIGHTS @ cov_w @ WEIGHTS)
dr = weighted_avg_vol / (port_vol + 1e-9)
div_ratio.append(dr)
alerts.append(1 if avg_c > CORR_ALERT_THRESHOLD else 0)
idx_range = returns.index[CORR_WINDOW:]
avg_corr_s = pd.Series(avg_corr, index=idx_range)
div_ratio_s = pd.Series(div_ratio, index=idx_range)
alerts_s = pd.Series(alerts, index=idx_range)
print(f'Correlation alerts fired: {alerts_s.sum()} days')
print(f'Average correlation range: [{avg_corr_s.min():.2f}, {avg_corr_s.max():.2f}]')
print(f'Diversification ratio range: [{div_ratio_s.min():.2f}, {div_ratio_s.max():.2f}]')Correlation alerts fired: 126 days Average correlation range: [0.34, 0.94] Diversification ratio range: [1.02, 1.46]
Visualisation
The top panel shows the rolling average correlation over time — the red dashed alert threshold line and red shading clearly mark the crisis period. The bottom left shows the diversification ratio falling during the same period, confirming that diversification disappeared when it mattered most. The bottom right shows the full pairwise correlation matrix as a heatmap for the most recent 30-day window.
fig, axes = plt.subplots(2, 2, figsize=(14, 9))
fig.suptitle('Correlation Risk Monitor', fontsize=13, fontweight='bold')
ax1 = axes[0, 0]
ax1.plot(avg_corr_s.index, avg_corr_s, color='#1976d2', lw=1.5, label='Avg Pairwise Corr')
ax1.axhline(CORR_ALERT_THRESHOLD, color='red', ls='--', lw=1,
label=f'Alert threshold = {CORR_ALERT_THRESHOLD}')
ax1.fill_between(avg_corr_s.index, avg_corr_s, CORR_ALERT_THRESHOLD,
where=avg_corr_s > CORR_ALERT_THRESHOLD, alpha=0.2, color='red')
ax1.set_ylabel('Average Correlation'); ax1.legend(fontsize=8)
ax1.set_title(f'{CORR_WINDOW}d Rolling Average Pairwise Correlation')
ax2 = axes[0, 1]
ax2.plot(div_ratio_s.index, div_ratio_s, color='#43a047', lw=1.5)
ax2.axhline(1, color='black', ls='--', lw=0.8, label='DR=1 (no diversification)')
ax2.set_ylabel('Diversification Ratio'); ax2.legend(fontsize=8)
ax2.set_title('Diversification Ratio Over Time')
ax3 = axes[1, 0]
recent_corr = returns.tail(CORR_WINDOW).corr()
sns.heatmap(recent_corr, ax=ax3, cmap='RdYlGn', center=0, vmin=-1, vmax=1,
annot=True, fmt='.2f', linewidths=0.5, cbar_kws={'label': 'Correlation'})
ax3.set_title(f'Most Recent {CORR_WINDOW}d Correlation Matrix')
ax4 = axes[1, 1]
# Plot pairwise correlation time series for BTC-ETH and BTC-SOL
for a1, a2, color in [('BTC','ETH','#1976d2'),('BTC','SOL','#e53935'),('ETH','SOL','#43a047')]:
pair_corr = returns[[a1,a2]].rolling(CORR_WINDOW).corr().unstack()[a2][a1]
ax4.plot(pair_corr.index, pair_corr, lw=1.3, color=color, label=f'{a1}-{a2}')
ax4.axhline(CORR_ALERT_THRESHOLD, color='red', ls='--', lw=0.8)
ax4.set_ylabel('Pairwise Correlation'); ax4.legend(fontsize=8)
ax4.set_title('Key Pair Correlation Over Time')
plt.tight_layout(); plt.show()Conclusion
This notebook provides a framework for monitoring correlation risk in a portfolio. By tracking rolling pairwise correlations, average correlation, and the diversification ratio, it helps to identify periods of increased systemic risk and potential loss of diversification benefits. The alert system based on average correlation can signal when de-risking actions might be necessary. The exported data can be used for further analysis or integration into risk dashboards.