Risk Parity Portfolio
Build a risk parity portfolio construction methodology that equalizes the ex-ante risk contribution from each portfolio constituent rather than naively equal-weighting capital allocation, producing significantly more balanced and diversified portfolios less concentrated in the highest-volatility assets.
Risk Parity Portfolio — Portfolio Construction
Category: Portfolio | Subcategory: Construction
What This Notebook Does
Risk Parity allocates capital so that each asset contributes equally to total portfolio risk — not equal dollar amounts.
- Equal-weight portfolio: 50% to BTC, 50% to a low-vol bond → BTC dominates risk
- Risk parity: reduces BTC weight, increases bond weight until both contribute 50% of risk
Risk Contribution of asset i = wᵢ × (Σw)ᵢ / σ_portfolio
Target: all risk contributions equal → RC_i = 1/N for all i
This notebook:
- Fetches data (Yahoo Finance or synthetic)
- Implements risk parity via convex optimisation
- Computes marginal and total risk contributions
- Compares risk parity vs equal-weight vs MVO
- Backtests and plots equity curves
- 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.optimize import minimize
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 defines the configuration parameters for the notebook, including the data source, tickers, and date range. You can toggle between live data fetching from Yahoo Finance or using synthetic data for demonstration purposes.
# ── Data Source Toggle ─────────────────────────────
USE_LIVE_DATA = False # Set True to fetch from Yahoo Finance (no API key needed)
TICKERS = ['BTC-USD', 'ETH-USD', 'SOL-USD', 'BNB-USD', 'AVAX-USD']
START_DATE = '2022-01-01'
END_DATE = '2024-12-31'
# ──────────────────────────────────────────────────
ASSETS = ['BTC', 'ETH', 'SOL', 'BNB', 'AVAX']
print('Config ready.')Config ready.
Section 2 — Data
This section handles the data acquisition and preparation. Depending on the USE_LIVE_DATA flag, it either fetches historical stock prices from Yahoo Finance or generates synthetic return data. It then calculates the covariance matrix, which is crucial for risk parity optimization.
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 loaded: {len(returns)} trading days')
else:
rng = np.random.default_rng(42)
n = 730
corr = np.array([[1,.85,.70,.75,.65],
[.85,1,.75,.70,.68],
[.70,.75,1,.65,.72],
[.75,.70,.65,1,.60],
[.65,.68,.72,.60,1]])
vols = np.array([0.65,0.75,1.20,0.70,1.10]) / np.sqrt(252)
cov = np.outer(vols,vols)*corr
L = np.linalg.cholesky(cov)
raw = rng.standard_normal((n, len(ASSETS))) @ L.T
mu = np.array([0.50,0.45,0.80,0.35,0.70]) / 252
data = raw + 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')
cov_matrix = returns.cov().values
print('Annualised vol per asset:')
print((returns.std() * np.sqrt(252) * 100).round(1).to_string())Synthetic data: 730 days Annualised vol per asset: BTC 65.3 ETH 77.5 SOL 116.6 BNB 72.9 AVAX 111.3
Section 3 — Risk Parity Optimisation
This section performs the core risk parity optimization. It uses scipy.optimize.minimize with the SLSQP method to find asset weights that equalize risk contributions, subject to the constraints that weights sum to one and are within specified bounds. The resulting risk parity weights and their contributions are then displayed.
def risk_contributions(weights: np.ndarray, cov: np.ndarray) -> np.ndarray:
"""
Compute absolute risk contributions of each asset.
RC_i = w_i * (Σw)_i / σ_portfolio
"""
port_var = weights @ cov @ weights
marginal = cov @ weights
rc = weights * marginal / np.sqrt(port_var)
return rc
def risk_parity_objective(weights: np.ndarray, cov: np.ndarray) -> float:
"""
Minimise the sum of squared differences between risk contributions.
At optimum all RC_i are equal.
"""
rc = risk_contributions(weights, cov)
target = rc.sum() / len(weights)
return float(np.sum((rc - target) ** 2))
n_assets = len(ASSETS)
w0 = np.ones(n_assets) / n_assets
constraints = [{'type': 'eq', 'fun': lambda w: w.sum() - 1}]
bounds = [(0.01, 0.9)] * n_assets
result = minimize(risk_parity_objective, w0, args=(cov_matrix,),
method='SLSQP', bounds=bounds, constraints=constraints,
options={'maxiter': 1000, 'ftol': 1e-10})
w_rp = result.x
rc = risk_contributions(w_rp, cov_matrix)
rc_pct = rc / rc.sum() * 100
print('Risk Parity Weights and Risk Contributions:')
df_rp = pd.DataFrame({'Asset': ASSETS,
'Weight %': (w_rp * 100).round(2),
'Risk Contrib %': rc_pct.round(2)})
print(df_rp.to_string(index=False))Risk Parity Weights and Risk Contributions: Asset Weight % Risk Contrib % BTC 25.07 19.94 ETH 20.90 19.96 SOL 14.52 20.00 BNB 23.88 20.09 AVAX 15.63 20.01
risk_contributions(weights: np.ndarray, cov: np.ndarray) -> np.ndarray
This function calculates the absolute risk contribution of each asset to the total portfolio risk. The formula used is RC_i = w_i * (Σw)_i / σ_portfolio, where w_i is the weight of asset i, (Σw)_i is the marginal risk contribution of asset i, and σ_portfolio is the portfolio standard deviation.
risk_parity_objective(weights: np.ndarray, cov: np.ndarray) -> float
This function defines the objective to be minimized for risk parity. It calculates the risk contributions for a given set of weights and then computes the sum of squared differences between each asset's risk contribution and the average risk contribution. Minimizing this function aims to make all risk contributions equal.
Section 4 — Comparison vs Equal Weight
This section compares the performance and risk characteristics of the risk parity portfolio against an equal-weight portfolio. It calculates risk contributions for both strategies and generates equity curves to visually compare their growth over time. The visualizations highlight how risk parity aims to balance risk across assets.
w_ew = np.ones(n_assets) / n_assets
rc_ew = risk_contributions(w_ew, cov_matrix)
rc_ew_pct = rc_ew / rc_ew.sum() * 100
port_rp = (returns * w_rp).sum(axis=1)
port_ew = (returns * w_ew).sum(axis=1)
equity_rp = (1 + port_rp).cumprod()
equity_ew = (1 + port_ew).cumprod()
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
fig.suptitle('Risk Parity vs Equal Weight', fontsize=13, fontweight='bold')
ax1 = axes[0]
x = np.arange(n_assets)
ax1.bar(x - 0.2, rc_ew_pct, 0.4, label='Equal Weight', color='#9e9e9e', alpha=0.8)
ax1.bar(x + 0.2, rc_pct, 0.4, label='Risk Parity', color='#1976d2', alpha=0.8)
ax1.axhline(100/n_assets, color='red', ls='--', lw=1, label=f'Target {100/n_assets:.1f}%')
ax1.set_xticks(x); ax1.set_xticklabels(ASSETS)
ax1.set_ylabel('Risk Contribution (%)')
ax1.legend(fontsize=8); ax1.set_title('Risk Contributions')
ax2 = axes[1]
ax2.plot(equity_rp.index, equity_rp, label='Risk Parity', color='#1976d2', lw=1.5)
ax2.plot(equity_ew.index, equity_ew, label='Equal Weight', color='#e53935', lw=1.5, ls='--')
ax2.set_ylabel('Growth of $1'); ax2.legend(fontsize=8)
ax2.set_title('Equity Curve')
plt.tight_layout(); plt.show()Section 5 — Export
This final section is responsible for exporting the calculated risk parity weights. The weights are saved to a CSV file named risk_parity_portfolio.csv, allowing for easy integration with other tools or future analysis.
df_rp.to_csv('risk_parity_portfolio.csv', index=False)
print('Saved: risk_parity_portfolio.csv')Saved: risk_parity_portfolio.csv
Conclusion
This notebook successfully demonstrated the construction and comparison of a risk parity portfolio. By minimizing the differences in risk contributions, the risk parity strategy aims to create a more balanced portfolio from a risk perspective, potentially leading to more stable returns compared to an equal-weight approach, especially in portfolios with highly volatile assets.