Comparison with pyrb

The pyrb package uses different formulations for the risk parity problem with general linear constraints and with the addition of objective terms such as the mean return and the volatility. Nonetheless, we can fairly compare with their code for the case where only the risk parity term is included and the constraints are \(\texttt{sum}(\mathbf{w}) = 1\) and \(\mathbf{w} \geq 0\).

Following the example shown at https://github.com/jcrichard/pyrb/blob/master/notebooks/RiskBudgeting.ipynb, we have:

[1]:
from pyrb import EqualRiskContribution
import pandas as pd
import numpy as np
import riskparityportfolio as rpp
[2]:
covariance_matrix = pd.read_csv("https://raw.githubusercontent.com/jcrichard/pyrb/master/notebooks/data.csv",sep=";",index_col=0).pct_change().cov() * 260
[3]:
covariance_matrix
[3]:
US BONDS 10Y GERMAN BONDS 10Y S&P 500 EUROSTOXX 50 NIKKEI MSCI Emerging Commodities (CRB) Iboxx HY US Iboxx HY EUR Emerging Debt
US BONDS 10Y 0.004116 0.002149 -0.003859 -0.005064 -0.004085 -0.003381 -0.001574 -0.000879 -0.000245 0.000484
GERMAN BONDS 10Y 0.002149 0.003150 -0.002809 -0.005692 -0.003468 -0.003604 -0.001310 -0.000996 -0.000262 0.000280
S&P 500 -0.003859 -0.002809 0.042571 0.030959 0.029899 0.024753 0.008173 0.009952 0.000856 0.004332
EUROSTOXX 50 -0.005064 -0.005692 0.030959 0.064347 0.027036 0.031997 0.011019 0.009402 0.003169 0.006211
NIKKEI -0.004085 -0.003468 0.029899 0.027036 0.060668 0.031786 0.009928 0.010870 0.001777 0.003836
MSCI Emerging -0.003381 -0.003604 0.024753 0.031997 0.031786 0.058415 0.014405 0.011145 0.003468 0.007879
Commodities (CRB) -0.001574 -0.001310 0.008173 0.011019 0.009928 0.014405 0.031496 0.005023 0.001489 0.002312
Iboxx HY US -0.000879 -0.000996 0.009952 0.009402 0.010870 0.011145 0.005023 0.011670 0.001523 0.002549
Iboxx HY EUR -0.000245 -0.000262 0.000856 0.003169 0.001777 0.003468 0.001489 0.001523 0.004500 0.001282
Emerging Debt 0.000484 0.000280 0.004332 0.006211 0.003836 0.007879 0.002312 0.002549 0.001282 0.008640
[4]:
cov = np.asarray(covariance_matrix)
[5]:
ERC = EqualRiskContribution(cov)
[6]:
%timeit ERC.solve()
320 µs ± 55.9 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
[7]:
optimal_weights =  ERC.x
optimal_weights
[7]:
array([0.22447775, 0.27514463, 0.03922505, 0.03589295, 0.03428179,
       0.02991635, 0.05459806, 0.07111012, 0.15281389, 0.08253942])
[8]:
risk_contributions_scaled =  ERC.get_risk_contributions()
risk_contributions_scaled
[8]:
array([0.09999997, 0.09999999, 0.10000001, 0.10000001, 0.1       ,
       0.1       , 0.1       , 0.1       , 0.1       , 0.1       ])
[9]:
b = np.ones(len(cov)) / len(cov)
[10]:
%timeit rpp.vanilla.design(cov, b)
5.9 µs ± 59.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
[11]:
rpp.vanilla.design(cov, b)
[11]:
array([0.22447778, 0.27514462, 0.03922505, 0.03589295, 0.03428179,
       0.02991635, 0.05459806, 0.07111011, 0.15281388, 0.08253942])

It seems that, for this example, riskparityportfolio is around \(40\times\) faster than pyrb. However, more examples are to be done in order to draw more conclusive results, especially on more interesting scenarios involving general linear constraints and additional objective terms such as the mean return.

Constrained Risk Parity

For constrained risk parity, see the pyrb notebook: https://github.com/jcrichard/pyrb/blob/master/notebooks/ConstrainedRiskBudgeting.ipynb

[12]:
vol = [0.05,0.05,0.07,0.1,0.15,0.15,0.15,0.18]
cor = np.array([[100,  80,  60, -20, -10, -20, -20, -20],
               [ 80, 100,  40, -20, -20, -10, -20, -20],
               [ 60,  40, 100,  50,  30,  20,  20,  30],
               [-20, -20,  50, 100,  60,  60,  50,  60],
               [-10, -20,  30,  60, 100,  90,  70,  70],
               [-20, -10,  20,  60,  90, 100,  60,  70],
               [-20, -20,  20,  50,  70,  60, 100,  70],
               [-20, -20,  30,  60,  70,  70,  70, 100]])/100
cov = np.outer(vol,vol)*cor
[13]:
my_rpp = rpp.RiskParityPortfolio(covariance=cov)
[14]:
my_rpp.design()
/Users/mirca/opt/miniconda3/lib/python3.7/site-packages/jax/lib/xla_bridge.py:120: UserWarning: No GPU/TPU found, falling back to CPU.
  warnings.warn('No GPU/TPU found, falling back to CPU.')
  0%|          | 0/500 [00:00<?, ?it/s]
[16]:
my_rpp.weights
[16]:
array([0.26830622, 0.28676891, 0.11409499, 0.09798482, 0.05613501,
       0.05902912, 0.06655992, 0.05112101])
[18]:
my_rpp.risk_contributions
[18]:
array([0.12500001, 0.12500001, 0.125     , 0.12499999, 0.125     ,
       0.125     , 0.125     , 0.125     ])
[19]:
# inequality constraints matrix and vector
Dmat = np.array([[0,0,0,0,-1,-1,-1,-1],
                 [1,-1,0,0,1,-1,0,0]])
dvec = np.array([-0.3,-0.05])
[20]:
my_rpp.design(Dmat=Dmat, dvec=dvec)
  1%|▏         | 7/500 [00:00<00:44, 11.11it/s]
[22]:
my_rpp.weights
[22]:
array([0.23186426, 0.27676939, 0.12093305, 0.0704333 , 0.07264196,
       0.07773683, 0.08587855, 0.06374266])
[24]:
Dmat @ my_rpp.weights
[24]:
array([-0.3 , -0.05])
[25]:
my_rpp.risk_contributions
[25]:
array([0.07508326, 0.08382447, 0.10969463, 0.08355962, 0.1614587 ,
       0.1651071 , 0.16492485, 0.15634737])
[ ]: