---
# System prepended metadata

title: Hierarchical Risk Parity

---

# Hierarchical Risk Parity

## Introduction
Investment managers face the daily challenge of constructing portfolios that reflect their views and predictions on risks and returns. Hierarchical Risk Parity (HRP) is an innovative portfolio allocation method designed to address this challenge.

**Key Idea:** HRP combines machine learning techniques with financial theory to build diversified portfolios that aim to perform well under various market conditions.

### Strengths of HRP
* **Robustness:** Doesn't require the covariance matrix to be invertible, unlike traditional mean-variance optimization.
* **Handles Ill-Conditioned Data:** Can work with singular or nearly singular covariance matrices.
* **Reduced Risk:** Empirical studies have shown HRP often produces portfolios with lower out-of-sample variance compared to other methods like the Critical Line Algorithm (CLA).
* **Strong Performance:** HRP portfolios have historically outperformed traditional approaches in various market scenarios.



## HRP Algorithm: A Three-Stage Process
1. **Tree Clustering:** Groups assets based on their correlation, creating a hierarchical structure resembling a tree.
2. **Quasi-Diagonalization:** Reorganizes the covariance matrix to place highly correlated assets together, aiding in risk management.
3. **Recursive Bisection:** Allocates weights to assets within each cluster, balancing risk and return based on an inverse-variance approach.
### Tree Clustering
This stage groups assets with similar risk profiles together. It uses a distance metric based on pairwise correlations:

#### Algorithm
:::success
<font color=blue>Algorithm: Tree Clustering
</font>
Input: $T \times N$ matrix $X$ of observations (e.g., returns series of $N$ variables over $T$ periods)
Output: Hierarchical structure of clusters

1. Compute $N \times N$ correlation matrix $\rho$:
   $\rho = (\text{corr}(X_i, X_j))_{i,j = 1,\dots,N}$
2. Compute $N \times N$ distance matrix $D$:
   $D = (d_{ij})_{i,j = 1,...,N}$ where $d_{ij} = \sqrt{\frac{1 - \rho_{ij}}{2}}$
3. Compute pairwise Euclidean distances $\tilde{d}$:
   $\tilde{d}_{ij} = \sqrt{\sum_{k=1}^N (d_{ki} - d_{kj})^2}$
4. Initialize clusters: $C = \{\{1\}, \{2\}, \dots, \{N\}\}$
5. While $|C| > 1$:
   a. Find $(i^*, j^*) = \arg\min_{i,j, i \neq j} {\tilde{d}_{ij}}$
   b. Create new cluster $u = C[i^*] ∪ C[j^*]$
   c. Update distances: For each cluster $v$ in $C$, $v \neq C[i^*]$, $v \neq C[j^*]$: $\tilde{d}[u,v] = \min(\tilde{d}[C[i^*],v], \tilde{d}[C[j^*],v])$
   d. Update $C$: $C = C \backslash \{C[i^*], C[j^*]\} \cup \{u\}$
   e. Update $\tilde{d}$ matrix:
      - Remove rows and columns for $C[i^*]$ and $C[j^*]$
      - Add row and column for u
6. Return final cluster hierarchy
:::

#### Code Implementation
```python=
def correlDist(corr):
	# A distance matrix based on correlation, where 0<=d[i,j]<=1
	# This is a proper distance metric
	dist=((1-corr)/2.)**.5 # distance matrix
	return dist

dist=correlDist(corr)
condensed_dist = squareform(dist)
link=sch.linkage(condensed_dist,'single')
```

**Linkage Matrix:** This matrix describes the hierarchical clustering process, capturing which assets are merged at each step and their corresponding distance.
**Dendrogram Interpretation:** The dendrogram is a tree-like structure where the leaves represent the individual data points, and the branches represent the merges. The height of the merge indicates the dissimilarity between the merged clusters. ![005-visualizing-dendrograms-cutree-1](https://hackmd.io/_uploads/rkMGe7eZC.png)

#### Linkage Methods
* **Complete Linkage:** Maximum distance between points in two clusters. $$\max_{x \in C_i,\,y \in C_j}d(x, y)$$
* **Single Linkage:** Maximum distance between points in two clusters. $$\min_{x \in C_i,\,y \in C_j}d(x, y)$$
* **Ward's Method:** Minimizes the increase in within-cluster variance when merging clusters. $$\sum_{x \in C_i \bigcup C_j}d(x, \mu_{C_i \bigcup C_j})$$



### Quasi-Diagonalization
This step reorders the rows and columns of the covariance matrix. The goal is to place the largest elements along the diagonal, which helps to visualize clusters and relationships between assets.

**Key Benefit:** Quasi-diagonalization places similar investments together and dissimilar investments further apart. This can be helpful in understanding risk concentrations within a portfolio.

#### Algorithm
:::success
<font color=blue>Algorithm: Quasi-Diagonalization</font>
Input: 
- Linkage matrix `Y` from hierarchical clustering
- Original covariance matrix `Sigma`

Output: 
- Reordered covariance matrix `Sigma'`

1. Initialize order list `L = []`
2. Function `QuasiDiagonalize(cluster)`:
    * If `cluster` is a leaf node (original asset):
        * Append cluster to `L`
    * Else:
        * `QuasiDiagonalize(cluster.left_child)`
        * `QuasiDiagonalize(cluster.right_child)`
3. Start with the root cluster (last row of $Y$):
    * `QuasiDiagonalize((Y[N-1, 1], Y[N-1, 2]))`
4. Reorder `Sigma` based on `L`:
   `Sigma' = Sigma[L, :][:, L]`
5. Return `Sigma'`
:::

#### Code Implementation
```python=
def getQuasiDiag(link):
    # Sort clustered items by distance
    link=link.astype(int)
    sortIx=pd.Series([link[-1,0],link[-1,1]])
    numItems=link[-1,3] # number of original items
    while sortIx.max()>=numItems:
        sortIx.index=range(0,sortIx.shape[0]*2,2) # make space
        df0=sortIx[sortIx>=numItems] # find clusters
        i=df0.index;j=df0.values-numItems
        sortIx[i]=link[j,0] # item 1
        df0=pd.Series(link[j,1],index=i+1)
        sortIx=sortIx.append(df0) # item 2
        sortIx=sortIx.sort_index() # re-sort
        sortIx.index=range(sortIx.shape[0]) # re-index
    return sortIx.tolist()
```


## Recursive Bisection
This is where the actual asset weights are determined. The algorithm recursively divides the asset clusters into two equal subsets and rebalances the weights using an inverse-variance approach.

**Constraints:** The recursive bisection algorithm is flexible and can easily incorporate constraints on individual asset weights or groups of assets.

:::success
<font color=blue>Algorithm: Recursive Bisection</font>
Input: 
- Covariance matrix $V$
- Quasi-diagonalized order of assets $L$

Output:
- Portfolio weights $w$

1. Initialize:
    * $L = \{L_0\}$, where $L_0 = \{1, \dots, N\}$
    * $w_n = 1$ for $n = 1, \dots, N$
2. While any $|L_i| > 1$ for $L_i \in L$:
    * For each $L_i \in L$ with $|L_i| > 1$:
        * Bisect $L_i$ into $L^{(1)}_i$ and $L^{(2)}_i$:
            * $L^{(1)}_i$ is the first $\text{int}(\frac{|L_i|}{2})$ elements of $|L_i|$
            * $L^{(2)}_i$ is the remaining elements of $L_i$
        * Compute inverse-variance weights for each subset $j = 1, 2$:
            * $V^{(j)}_i$ is the covariance matrix between the constituents of the $L^{(j)}_i$ bisection
            * $\tilde{W}^{(j)}_i = \frac{\text{diag}[V^{(j)}_i ]^{−1}}{\text{tr}[\text{diag}[V^{(j)}_i]^{−1}]}$
            * $\tilde{v}^{(j)}_i ≡ (\widetilde{W}^{(j)}_i)^T V^{(j)}_i \widetilde{W}^{(j)}_i$
         * Compute split factor: $\alpha = 1 - \frac{\tilde{v}^{(1)}_i}{\tilde{v}^{(1)}_i + \tilde{v}^{(2)}_i}$ $\in [0,1]$
         * Update weights:
             * For $n \in L^{(1)}_i$: $w_n \leftarrow \alpha w_n$
             * For $n \in L^{(2)}_i$: $w_n \leftarrow (1 - \alpha) w_n$
         * Update $L$:
         $L = L \backslash \{L_i\} \cup \{L^{(1)}_i, L^{(2)}_i\}$
3. Return $W$
:::

### Code Implementation
```python=
def getIVP(cov,**kargs):
	# Compute the inverse-variance portfolio
	ivp=1./np.diag(cov)
	ivp/=ivp.sum()
	return ivp

def getClusterVar(cov,cItems):
	# Compute variance per cluster
	cov_=cov.loc[cItems,cItems] # matrix slice
	w_=getIVP(cov_).reshape(-1,1)
	cVar=np.dot(np.dot(w_.T,cov_),w_)[0,0]
	return cVar

def getRecBipart(cov,sortIx):
    # Compute HRP alloc
    w=pd.Series(1,index=sortIx)
    cItems=[sortIx] # initialize all items in one cluster
    while len(cItems)>0:
        cItems=[i[j:k] for i in cItems for j,k in ((0,len(i)/2),\
            (len(i)/2,len(i))) if len(i)>1] # bi-section
        for i in xrange(0,len(cItems),2): # parse in pairs
            cItems0=cItems[i] # cluster 1
            cItems1=cItems[i+1] # cluster 2
            cVar0=getClusterVar(cov,cItems0)
            cVar1=getClusterVar(cov,cItems1)
            alpha=1-cVar0/(cVar0+cVar1)
            w[cItems0]*=alpha # weight 1
            w[cItems1]*=1-alpha # weight 2
    return w
```

### Covariance Predictors
We can see that only the method of predicting the covariance matrix directly affects the calculation of each asset weight.
Here we will introduce two difference covariance predictors: Rolling Window and Exponentially Weighted Moving Average (EWMA)
#### Rolling Window
```python=
def rolling_window(returns, memory, min_periods=20):
	rollw=pd.DataFrame()
	min_periods = max(min_periods, 1)
	times = returns.index
	assets = returns.columns
	returns = returns.values

	Sigmas = np.zeros((returns.shape[0], returns.shape[1], returns.shape[1]))
	Sigmas[0] = np.outer(returns[0], returns[0])

	for t in range(1, returns.shape[0]):
		alpha_old = 1 / min(t + 1, memory)
		alpha_new = 1 / min(t + 2, memory)

		if t >= memory:
			Sigmas[t] = alpha_new / alpha_old * Sigmas[t - 1] + alpha_new * (
             np.outer(returns[t], returns[t])
             - np.outer(returns[t - memory], returns[t - memory])
          )
		else:
			Sigmas[t] = alpha_new / alpha_old * Sigmas[t - 1] + alpha_new * (
                np.outer(returns[t], returns[t])
            )

	Sigmas = Sigmas[min_periods - 1 :]
	times = times[min_periods - 1 :]
	rollw=pd.concat([pd.DataFrame(Sigmas[t], index=assets, columns=assets) for t in range(len(times))], keys=[times[t].date() for t in range(len(times))])
	return rollw
```
#### Exponentially Weighted Moving Average (EWMA)
```

```

## Example: US Finance Stocks and Futures Portfolio
```python=
import matplotlib.pyplot as mpl
import scipy.cluster.hierarchy as sch
from scipy.spatial.distance import squareform
import numpy as np
import pandas as pd
import yfinance as yf  # Yahoo Finance API
from datetime import timedelta ,date
from dateutil.relativedelta import relativedelta
```
To showcase the effectiveness of HRP, we constructed a portfolio consisting of 35 diversified assets:

* **US Stocks:** `AAPL`, `ABT`, `ADBE`, `AMAT`, `AMZN`, `AVY`, `BALL`, `BAX`, `BDX`, `CMI`, `CPB`, `CSX`, `GILD`, `HAS`, `INSM`, `KO`, `MCD`, `MMM`, `MSFT`, `NVDA`, `PFE`, `TGT`, `TJX`, `TSM`, `WFC`, `XOM`, `YUM`
* **Futures Contracts:** `GC=F` (Gold), `HG=F` (Copper), `SI=F` (Silver), `^FVX` (CBOE Volatility Index), `^GSPC` (S&P 500), `^NDX` (Nasdaq 100), `^TNX` (10-Year Treasury Yield), `^TYX` (30-Year Treasury Yield)

The portfolio was rebalanced monthly using an 8-month lookback period, spanning from September 2000 to May 2024.

```python=
class Stocks():
  def __init__(self, arg_stocks_list=[], arg_begin='2015-01-01', arg_end='2023-10-01'):
    """
    Initialize the Stocks class.

    Parameters:
    - arg_stocks_list: List of stock symbols.
      For example:
        ['2330.TW','2337.TW','2357.TW','2454.TW','3231.TW','3443.TW'] or
        ['TSM','AAPL','GOOG','MSFT','AMZN','GOOGL']
    - arg_begin: Start date for the stock data.
    - arg_end: End date for the stock data.

    Returns:
    - None: This function doesn't return anything but initializes the class instance.
    """
    self.stocks_list = arg_stocks_list  # List of stock symbols
    self.stocks_list.sort()
    self.start_date = arg_begin  # Start date
    self.end_date = arg_end  # End date
    self.N = len(self.stocks_list)  # Number of stocks
    self.data = None
    self.set_data()
    self.M = len(self.data)  # The number of rows in the data
    self.rtns = pd.DataFrame()
    self.log_rtns = pd.DataFrame()
    self.set_rtns()
  def set_data(self):
    self.data = pd.DataFrame()  # Initialize an empty DataFrame to store data
    # Iterate over each stock in the list of stocks
    for stock in self.stocks_list:
      # Download the stock data using the yfinance library
      df = yf.download(stock, start=self.start_date, end=self.end_date)
      # Select only the adjusted closing price from the downloaded data
      df = df[['Adj Close']]
      # Rename the column to the stock symbol for clarity
      df = df.rename(columns = {'Adj Close': stock})
      # Check if the main DataFrame is empty
      if self.data.empty:
        # If it is, assign the downloaded data to the main DataFrame
        self.data = df
      else:
        # If it's not, concatenate the downloaded data to the main DataFrame
        self.data = pd.concat([self.data, df], axis=1)
    return
  def set_rtns(self):
    self.rtns = self.data.pct_change() + 1
    self.log_rtns = np.log(self.data.pct_change() + 1)
    self.rtns.fillna(1, inplace=True)
    self.log_rtns.fillna(1, inplace=True)
    return
  def plotComparePrice(self,use_log_rtns=True):
    mpl.figure(figsize=(20,12))
    mpl.title('Normalized Prices', fontsize=20)
    mpl.xticks(fontsize=16)
    mpl.yticks(fontsize=16)
    if use_log_rtns:
      mpl.ylabel('Logarithmic Price', fontsize=16)
      mpl.plot(1+ np.log(self.data/self.data.loc[self.data.index[0]]),label=self.data.columns)
    else:
      mpl.ylabel('Price', fontsize=16)
      mpl.plot((self.data/self.data.loc[self.data.index[0]]),label=self.data.columns)
    mpl.legend(loc='upper left', bbox_to_anchor=(1, 1), fontsize=12)
    mpl.savefig('ComparePrice.png', format='png', bbox_inches='tight')
    mpl.clf();mpl.close()
    return

stocks = Stocks(stocks_list,start_date ,end_date)
stocks.plotComparePrice()
```

![ComparePrice](https://i.imgur.com/mRLDHTN.png)



---
```python=
def testPerformance():
	# data
	start_date = date(2000,1,1)
	end_date = date(2024,6,1)
	td = (end_date - start_date)/timedelta(days=1)
	stocks_list=['AAPL', 'ABT', 'ADBE', 'AMAT', 'AMZN', 'AVY', 'BALL', 'BAX', 'BDX', 'CMI', 'CPB', 'CSX','GC=F', 'GILD', 'HAS','HG=F', 'INSM', 'KO', 'MCD', 'MMM', 'MSFT', 'NVDA', 'PFE','SI=F', 'TGT', 'TJX', 'TSM', 'WFC', 'XOM', 'YUM', '^FVX', '^GSPC', '^NDX', '^TNX', '^TYX']
	stocks = Stocks(stocks_list,start_date ,end_date)
	weight_history=pd.DataFrame()
	tm=8
	assetHRP=1
	assetEW=1
	assetRandom_1=1
	assetRandom_2=1
	assetRandom_3=1
	xl=[start_date + relativedelta(months=+(tm))]
	y_HRP=[1]
	y_EW=[1]
	y_Random_1=[1]
	y_Random_2=[1]
	y_Random_3=[1]
	np.random.seed(0)
	rollw=rolling_window(stocks.rtns, memory=168, min_periods=168)
	for i in range(int((td/30)-tm-4)):
		x=stocks.rtns[start_date + relativedelta(months=+i) : start_date + relativedelta(months=+(tm+i))]
		
		dt=start_date + relativedelta(months=+(tm+i))
		temp=rollw[dt:dt]
		while temp.empty:
			dt=dt + relativedelta(days=+1)
			temp=rollw[dt:dt]
		cov=pd.DataFrame(temp.values, index=temp.columns, columns=temp.columns)

		corr=x.corr()
		# cluster
		dist=correlDist(corr)
		condensed_dist = squareform(dist)
		link=sch.linkage(condensed_dist,'single')
		sortIx=getQuasiDiag(link)
		sortIx=corr.index[sortIx].tolist() # recover labels
		# Capital allocation
		hrp=getRecBipart(cov,sortIx)
		weight_history=pd.concat([weight_history,pd.DataFrame([hrp],index=[start_date + relativedelta(months=+(tm+i))])])
		tempHRP=hrp.copy()
		tempEW=0
		tempRandom_1=hrp.copy()
		rd_w_1=random_weight(len(stocks.rtns.columns))
		tempRandom_2=hrp.copy()
		rd_w_2=random_weight(len(stocks.rtns.columns))
		tempRandom_3=hrp.copy()
		rd_w_3=random_weight(len(stocks.rtns.columns))

		k=len(stocks.rtns.columns)-1
		for key in stocks_list:
			rtn=float(stocks.rtns[start_date + relativedelta(months=+(tm+i)): start_date + relativedelta(months=+(tm+1+i))].cumprod(axis = 0).iloc[-1][key])
			tempHRP[key]*=rtn
			tempRandom_1[key]=rd_w_1[k]*rtn
			tempRandom_2[key]=rd_w_2[k]*rtn
			tempRandom_3[key]=rd_w_3[k]*rtn
			k-=1
			tempEW+=rtn
		tempEW/=len(stocks_list)
		assetHRP*=tempHRP.sum()
		assetEW*=tempEW
		assetRandom_1*=tempRandom_1.sum()
		assetRandom_2*=tempRandom_2.sum()
		assetRandom_3*=tempRandom_3.sum()
		xl.append(start_date + relativedelta(months=+(tm+i)))
		y_HRP.append(assetHRP)
		y_EW.append(assetEW)
		y_Random_1.append(assetRandom_1)
		y_Random_2.append(assetRandom_2)
		y_Random_3.append(assetRandom_3)
	plotPerformance(xl,y_HRP.copy(),y_EW.copy(),y_Random_1.copy(),y_Random_2.copy(),y_Random_3.copy())
	plotHistoryWeight(weight_history)
	summary_stat(y_HRP.copy())
	summary_stat(y_EW.copy())
	summary_stat(y_Random_1.copy())
	summary_stat(y_Random_2.copy())
	summary_stat(y_Random_3.copy())
	plotDrawdown(xl,y_HRP.copy(),y_EW.copy(),y_Random_1.copy(),y_Random_2.copy(),y_Random_3.copy())
	return

def random_weight(arg_len):
	rd = np.random.random(size=arg_len-1)
	rd.sort()
	rd_w=rd.copy()
	rd_w=np.append(rd_w,1)
	for k in range(len(rd)):
		rd_w[k+1]=rd_w[k+1]-rd[k]
	return rd_w
```
### Tree Clustering
The dendrogram below illustrates the hierarchical clustering of assets, revealing natural groupings based on their correlation structure.

```python=
def plotDendrogram(path,linkage,labels=None):
	if labels is None:labels=[]
	p = len(labels)
	mpl.figure(figsize=(12,6))
	mpl.title('Hierarchical Clustering', fontsize=20)
	mpl.ylabel('stock symbol', fontsize=16)
	mpl.xlabel('distance', fontsize=16)

	# call dendrogram to get the returned dictionary 
	# (plotting parameters can be ignored at this point)
	R = sch.dendrogram(
		linkage,
		truncate_mode='lastp',  # show only the last p merged clusters
		p=p,  # show only the last p merged clusters
		no_plot=True, orientation='right'
	)
	# create a label dictionary
	temp = {R["leaves"][ii]: labels[ii] for ii in range(len(R["leaves"]))}
	def llf(xx):
		return "{}".format(temp[xx])

	sch.dendrogram(
		linkage,
		truncate_mode='lastp',  # show only the last p merged clusters
		p=p,  # show only the last p merged clusters
		color_threshold=0.35, # If color_threshold is None or ‘default’,the threshold is set to 0.7*max(Z[:,2])
		leaf_label_func=llf,
		# leaf_rotation=60.,
		leaf_font_size=12.,
		show_contracted=True,  # to get a distribution impression in truncated branches
		orientation='right', 
	)
	mpl.savefig(path, format='png', bbox_inches='tight')
	mpl.clf();mpl.close() # reset pylab
	return
```
Use `color_threshold=0.35` let the height less than 0.35 be same color, which also meaning they are highly correlated assets

![HRP_Dendrogram_single](https://i.imgur.com/z8zPHJ8.png)





### Quasi-Diagonalization
By reordering the correlation matrix based on the clustering, we obtain a quasi-diagonalized matrix. This visually highlights blocks of highly correlated assets, aiding in risk assessment.

```python=
def plotCorrMatrix(path,corr,labels=None):
	# Heatmap of the correlation matrix
	if labels is None:labels=[]
	mpl.figure(figsize=(8,6))
	mpl.pcolor(corr)
	mpl.colorbar()
	mpl.yticks(np.arange(.5,corr.shape[0]+.5),labels)
	mpl.xticks(np.arange(.5,corr.shape[0]+.5),labels,rotation = 60.)
	mpl.savefig(path)
	mpl.clf();mpl.close() # reset pylab
	return
plotCorrMatrix('HRP_corr.png',corr,labels=corr.columns)
```
**Correlation matrix**
![HRP3_corr0](https://hackmd.io/_uploads/HkHRzruL0.png)
```python=
sortIx=getQuasiDiag(link)
sortIx=corr.index[sortIx].tolist() # recover labels
df0=corr.loc[sortIx,sortIx] # reorder
plotCorrMatrix('HRP_corr_Q_diag.png',df0,labels=df0.columns)
```
**Quasi-diagonalisation of the correlation matrix**
![HRP3_corr1](https://hackmd.io/_uploads/Sy5RMB_UR.png)

### Recursive Bisection and Performance:
The final HRP portfolio weights were determined through recursive bisection. 
Here we use Covariance Predictors-Rolling Window to Predict the covariance matrix of rebalance month.




**History Weight**
```python=
def plotHistoryWeight(w_history):
	tot = np.zeros(len(w_history))
	mpl.figure(figsize=(30,13))
	mpl.title('History of weight values', fontsize=30)
	mpl.xticks(fontsize=16)
	mpl.yticks(fontsize=16)
	mpl.ylabel('Weights', fontsize=24)
	for col_idx in w_history:
		mpl.fill_between(x=w_history.index.tolist(), y1=w_history[col_idx].values+tot,y2=tot,step='post',label=col_idx)
		tot += w_history[col_idx].values
	mpl.legend(loc='upper left', bbox_to_anchor=(1, 1), fontsize=12)
	mpl.savefig('HistoryWeight.png', format='png', bbox_inches='tight')
	mpl.clf();mpl.close()
	return
```
**With Rolling Window**
![History of weight values](https://i.imgur.com/MBA6b8a.png)

**With EWMA**
![]()

**Compare Performance**
The resulting portfolio's performance is compared against equal-weighted (EW), Buy and hold portfolio (B&H) and randomly weighted portfolios in the figures below.

```python=
def plotPerformance(d,target1,target2,target3,target4,target5):
	mpl.figure(figsize=(12,6))
	mpl.plot(d, target1, 'r',label='HRP')
	mpl.plot(d, target2, 'b',label='EW')
	mpl.plot(d, target3, 'g',label='Random_1')
	mpl.plot(d, target4, 'm',label='Random_2')
	mpl.plot(d, target5, 'c',label='Random_3')
	mpl.title('Performance', fontsize=20)
	mpl.xticks(rotation = 60.)
	mpl.ylabel('cumprod', fontsize=16)
	mpl.legend()
	mpl.savefig('Performance.png', format='png', bbox_inches='tight')
	mpl.clf();mpl.close()
	return
```
![performance](https://i.imgur.com/KCytkSS_d.webp?maxwidth=760&fidelity=grand)

**History Drawdown**
```python=
def historyDrawdown(target):
	history_drawdown=[]
	max_price=target[0]
	for price in target:
		if price > max_price:
			max_price=price
		history_drawdown.append((price - max_price) *100 / max_price)
	return history_drawdown

def plotDrawdown(d,target1,target2,target3,target4,target5):
	drawdown_HRP=historyDrawdown(target1)
	drawdown_EW=historyDrawdown(target2)
	drawdown_Random_1=historyDrawdown(target3)
	drawdown_Random_2=historyDrawdown(target4)
	drawdown_Random_3=historyDrawdown(target5)

	mpl.figure(figsize=(12,6))
	mpl.title('History Drawdown(%)', fontsize=16)
	mpl.plot(d, drawdown_HRP, 'r',label='HRP Drawdown(%)')
	mpl.plot(d, drawdown_EW, 'b',label='EW Drawdown(%)')
	mpl.plot(d, drawdown_Random_1, 'g',label='Random_1 Drawdown(%)')
	mpl.plot(d, drawdown_Random_2, 'm',label='Random_2 Drawdown(%)')
	mpl.plot(d, drawdown_Random_3, 'c',label='Random_3 Drawdown(%)')
	mpl.ylabel('Drawdown(%)', fontsize=16)
	mpl.legend()
	mpl.savefig('HistoryDrawdown.png', format='png', bbox_inches='tight')
	mpl.clf();mpl.close()
	return 
```
![Drawdown](https://i.imgur.com/Zm9CeQ0.png)

### Key Observations
* **Risk Management:** HRP demonstrates lower volatility and smaller maximum drawdowns, particularly during the 2008 financial crisis and the 2020 pandemic, suggesting superior risk-adjusted performance.
* **Competitive Returns:** 
	- HRP achieved a CAGR of 16.87%, slightly outperforming the equally weighted (EW) portfolio with a CAGR of 16.26%
	- HRP’s volatility was 15.78%, slightly lower than the EW portfolio’s 15.88% , lower than all random portfolios.
	- HRP’s Sharpe ratio was higher than EW and all random portfolios.
	- HRP experienced a maximum drawdown of 36.14%, which was slightly better than the EW portfolio’s 37.18%, better than Random Portfolio 2 and 3.
* **Summary Statistics:** The table below summarizes key performance metrics:
```python=
def summary_stat(target):
	max_drawdown = 0
	max_price=target[0]
	for price in target:
		if price > max_price:
			max_price=price
		drawdown = (price - max_price) / max_price
		if drawdown < max_drawdown:
			max_drawdown = drawdown
	pct_rtn=(np.array(target[1:])/np.array(target[:-1]))-1
	print('Compound annual growth rate',(target[-1]/target[0])**(12/len(target))-1)
	print('Vol.(annual)', (pct_rtn.std())*(12**(1/2)))
	print('Sharpe ratio(annual)',(pct_rtn.mean())*(12**(1/2))/(pct_rtn.std()))
	print('max_drawdown',max_drawdown)
	print('Sortino ratio(annual)',(pct_rtn.mean())*(12**(1/2))/(pct_rtn[pct_rtn<0].std()))
	print('Calmar ratio(annual)',(pct_rtn.mean())*(12**(1/2))/max_drawdown)
	return
```
|                        | HRP with Rolling window |   EW    | Random 1 | Random 2 | Random 3 | 1/n B&H | HRP with EWMA |
|:----------------------:|:-----------------------:|:-------:|:--------:|:--------:|:--------:|:-------:|:-------------:|
|          CAGR          |         16.87%          | 16.26%  |  18.62%  |  15.36%  |  16.35%  |         |               |
| Volatility(annualized) |         15.78%          | 15.88%  |  16.46%  |  17.10%  |  17.18%  |         |               |
|      Sharpe ratio      |          1.077          |  1.038  |  1.131   |  0.929   |  0.976   |         |               |
|    Maximum drawdown    |         -36.14%         | -37.18% | -31.14%  | -37.82%  | -37.11%  |         |               |



---


## **Reference**
* [Marcos López de Prado](https://www.quantresearch.org/index.html)
* [Building Diversified Portfolios that Outperform Out-of-Sample](https://doi.org/10.3905/jpm.2016.42.4.059)
* [HRP.py.txt](https://www.quantresearch.org/HRP.py.txt)
* [HRP_MC.py.txt](https://www.quantresearch.org/HRP_MC.py.txt)
* [Hierarchical Construction of Investment Portfolios Using Clustered Machine Learning](https://patents.google.com/patent/US20180089762A1)
* [Asset Allocation - Hierarchical Risk Parity](https://www.mathworks.com/videos/asset-allocation-hierarchical-risk-parity-1577957794663.html)
* [Covariance/Correlation Matrix HRP-Clustering](https://youtu.be/wtd3K-Ubr1g?si=rQomxWT2Xinw8UXo)
* [Towards Robust Portfolios](https://www.quantresearch.org/FIVE_VROBUST_Index__Research-EN.pdf)
* [Hierarchical Risk Parity on RAPIDS: An ML Approach to Portfolio Allocation](https://developer.nvidia.com/blog/hierarchical-risk-parity-on-rapids-an-ml-approach-to-portfolio-allocation/)
* [Asset Allocation - Hierarchical Risk Parity](https://youtu.be/e21MfMe5vtU?si=hS23MDNGDQlSmD-A)
* [Covariance Predictor](https://hackmd.io/@Howard531/B1DphEqXR)