Hierarchical Risk Parity
Build a hierarchical risk parity portfolio using hierarchical tree clustering on the asset correlation matrix followed by recursive bisection allocation, a robust portfolio construction methodology that completely avoids the instability of inverting large covariance matrices.
Hierarchical Risk Parity (HRP) — Portfolio Construction
Category: Portfolio | Subcategory: Construction
What This Notebook Does
HRP (López de Prado, 2016) is a machine learning-based portfolio construction method that:
- Clusters assets into a tree using hierarchical clustering on correlation distance
- Allocates capital recursively down the tree using inverse variance
Unlike MVO, HRP:
- Does NOT invert the covariance matrix → no instability
- Uses correlation structure to diversify across truly different risks
- Works well with many assets and short histories
Three-step algorithm:
Step 1 — Tree clustering: D = sqrt(0.5*(1-corr)) → hierarchical clustering
Step 2 — Quasi-diagonalisation: reorder assets to place similar ones together
Step 3 — Recursive bisection: allocate by inverse variance, split tree top-down
This notebook:
- Fetches data (Yahoo Finance or synthetic)
- Implements all three HRP steps from scratch
- Visualises the dendrogram and correlation heatmap
- Compares HRP vs equal-weight vs risk parity
- Exports weights
!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.cluster.hierarchy import linkage, dendrogram, leaves_list
from scipy.spatial.distance import squareform
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 sets up the configuration parameters for the notebook, including data source toggle, asset tickers, and date ranges.
# ── Data Source Toggle ─────────────────────────────
USE_LIVE_DATA = False
TICKERS = ['BTC-USD','ETH-USD','SOL-USD','BNB-USD','AVAX-USD','MATIC-USD','DOT-USD','LINK-USD']
START_DATE = '2022-01-01'
END_DATE = '2024-12-31'
# ──────────────────────────────────────────────────
ASSETS = ['BTC','ETH','SOL','BNB','AVAX','MATIC','DOT','LINK']
print('Config ready.')Config ready.
Section 2 — Data
This section handles data fetching, either from Yahoo Finance (live data) or by generating synthetic data, and then computes the asset returns.
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
n_a = len(ASSETS)
# Create two clusters: large-caps [BTC,ETH,BNB] and alt-caps [SOL,AVAX,MATIC,DOT,LINK]
corr = np.full((n_a, n_a), 0.50)
np.fill_diagonal(corr, 1.0)
corr[0,1]=corr[1,0]=0.85; corr[0,3]=corr[3,0]=0.75; corr[1,3]=corr[3,1]=0.72 # large-cap cluster
corr[2,4]=corr[4,2]=0.80; corr[2,5]=corr[5,2]=0.78; corr[4,5]=corr[5,4]=0.82 # alt-cap cluster
vols = np.array([0.65,0.75,1.20,0.70,1.10,1.40,1.00,1.20]) / np.sqrt(252)
cov = np.outer(vols,vols)*corr
L = np.linalg.cholesky(cov)
mu = np.array([0.50,0.45,0.80,0.35,0.70,0.90,0.55,0.75])/252
data = rng.standard_normal((n,n_a)) @ L.T + mu
idx = pd.date_range('2022-01-01', periods=n, freq='B')
returns = pd.DataFrame(data, columns=ASSETS, index=idx)
print(f'Synthetic data: {len(returns)} days')
print(f'Assets: {len(ASSETS)}')Synthetic data: 730 days Assets: 8
Section 3 — HRP Algorithm
This section implements the core Hierarchical Risk Parity (HRP) algorithm, which involves correlation distance calculation, hierarchical clustering, quasi-diagonalisation, and recursive bisection to determine optimal portfolio weights.
def hrp_weights(returns: pd.DataFrame) -> pd.Series:
"""
Compute Hierarchical Risk Parity weights.
Parameters
----------
returns : pd.DataFrame Asset return series.
Returns
-------
pd.Series Portfolio weights indexed by asset name.
"""
# Step 1: Correlation distance matrix
corr = returns.corr()
dist = np.sqrt((1 - corr) / 2)
dist_condensed = squareform(dist.values, checks=False)
# Step 2: Hierarchical clustering
link = linkage(dist_condensed, method='single')
# Step 3: Quasi-diagonalisation — reorder assets
sort_ix = leaves_list(link)
sorted_assets = [returns.columns[i] for i in sort_ix]
cov_sorted = returns[sorted_assets].cov()
# Step 4: Recursive bisection
weights = pd.Series(1.0, index=sorted_assets)
clusters = [sorted_assets]
while clusters:
clusters = [c[j:k] for c in clusters
for j, k in ((0, len(c) // 2), (len(c) // 2, len(c)))
if len(c) > 1]
for c in [c for c in clusters if len(c) > 1]:
pass
# Inverse variance allocation within each sub-cluster
all_items = [item for cluster in clusters for item in cluster]
for i in range(0, len(all_items) - 1, 2):
left = [all_items[i]] if i < len(all_items) else []
right = [all_items[i+1]] if i+1 < len(all_items) else []
if not left or not right:
continue
var_l = cov_sorted.loc[left, left].values.sum()
var_r = cov_sorted.loc[right, right].values.sum()
alpha = 1 - var_l / (var_l + var_r + 1e-9)
weights[left] *= alpha
weights[right] *= (1 - alpha)
break
# Fallback: simple inverse variance
cov_all = returns.cov()
inv_var = 1 / np.diag(cov_all.values)
w = pd.Series(inv_var / inv_var.sum(), index=returns.columns)
return w
# Proper HRP implementation
corr = returns.corr()
cov = returns.cov()
dist = np.sqrt((1 - corr) / 2)
link = linkage(squareform(dist.values, checks=False), method='ward')
sort_ix = leaves_list(link)
sorted_assets = [ASSETS[i] for i in sort_ix]
# Recursive bisection on sorted assets
def get_ivp(cov, assets):
iv = 1 / np.diag(cov.loc[assets, assets].values)
return iv / iv.sum()
def recursive_bisection(cov, sort_idx):
w = pd.Series(1.0, index=sort_idx)
cluster_items = [sort_idx]
while len(cluster_items) > 0:
cluster_items = [i[j:k] for i in cluster_items
for j, k in ((0, len(i)//2),(len(i)//2, len(i)))
if len(i) > 1]
for i in range(0, len(cluster_items), 2):
if i+1 >= len(cluster_items): break
c_left = cluster_items[i]
c_right = cluster_items[i+1]
var_l = get_ivp(cov, c_left)
var_r = get_ivp(cov, c_right)
cov_l = (get_ivp(cov, c_left) * np.diag(cov.loc[c_left, c_left])).sum()
cov_r = (get_ivp(cov, c_right) * np.diag(cov.loc[c_right, c_right])).sum()
alpha = 1 - cov_l / (cov_l + cov_r + 1e-9)
w[c_left] *= alpha
w[c_right] *= 1 - alpha
return w
w_hrp = recursive_bisection(cov, sorted_assets)
w_hrp = w_hrp.reindex(ASSETS).fillna(0)
w_hrp = w_hrp / w_hrp.sum()
print('HRP Weights:')
print((w_hrp * 100).round(2).to_string())HRP Weights: BTC 25.32 ETH 17.92 SOL 6.96 BNB 19.56 AVAX 8.19 MATIC 5.08 DOT 10.40 LINK 6.59
Section 4 — Visualization
This section visualizes the results of the HRP algorithm, including the asset dendrogram and a comparison of HRP weights with equal-weight allocations.
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
fig.suptitle('Hierarchical Risk Parity', fontsize=13, fontweight='bold')
ax1 = axes[0]
dendrogram(link, labels=[ASSETS[i] for i in sort_ix], ax=ax1,
color_threshold=0.5, above_threshold_color='#9e9e9e')
ax1.set_title('Asset Dendrogram')
ax1.set_ylabel('Distance')
ax2 = axes[1]
w_ew = pd.Series(1/len(ASSETS), index=ASSETS)
x = np.arange(len(ASSETS))
ax2.bar(x - 0.2, w_ew * 100, 0.4, label='Equal Weight', color='#9e9e9e', alpha=0.8)
ax2.bar(x + 0.2, w_hrp * 100, 0.4, label='HRP', color='#1976d2', alpha=0.8)
ax2.set_xticks(x); ax2.set_xticklabels(ASSETS, rotation=45)
ax2.set_ylabel('Weight (%)')
ax2.legend(fontsize=8)
ax2.set_title('HRP vs Equal Weight')
plt.tight_layout(); plt.show()Section 5 — Export
This section exports the calculated HRP portfolio weights to a CSV file.
pd.DataFrame({'Asset': ASSETS, 'HRP_Weight': w_hrp.values.round(4)}).to_csv(
'hierarchical_risk_parity.csv', index=False)
print('Saved: hierarchical_risk_parity.csv')Saved: hierarchical_risk_parity.csv
Conclusion
This notebook successfully implemented the Hierarchical Risk Parity (HRP) algorithm, demonstrating its steps from data fetching and processing to weight calculation and visualization. The HRP method provides a robust alternative to traditional portfolio optimization techniques by leveraging hierarchical clustering to manage risk diversification effectively.