Portfolio

Risk Parity/Risk Budgeting Portfolio in Python

Risk Parity Portfolio is an investment allocation strategy which focuses on the allocation of risk, rather than the allocation of capital. For example, a typical 40% bond 60% equity portfolio has a significant risk in equity. A risk parity (equal risk) portfolio is a portfolio, which individual assets, in this case equity and bond, have equal risk contribution to the portfolio risk. The allocation strategy has gained popularity in the last decades, the idea of asset allocation base on risk has been used in many strategies such as managed futures strategy, and the famous Bridgewater all weather fund. Some people argue that this allocation strategy provides better risk adjusted return than capital based allocation strategies. (I will do some simple backtest in the future)

I will go over a very basic example of what risk parity is and how to construct a simple risk parity (equal risk) portfolio  and extend it to a risk budgeting portfolio (target risk allocation)

First define the marginal risk contribution as:fixed-1ex

Then, the risk contribution of asset j to the total portfolio is:
fixed-2

Risk Parity portfolio is a portfolio which RC are equal across all individual assets

For example, going back to the previous example and assume we have an equal weight portfolio of four assets with weight = 25% each, it’s RC (in total portfolio risk %) is:

risk1

To compute the weight of a risk parity portfolio, we could use the optimise function from python

Let the sum of squared error of a portfolio assets RC  be:3Then4In python

from __future__ import division
import numpy as np
from matplotlib import pyplot as plt
from numpy.linalg import inv,pinv
from scipy.optimize import minimize

 # risk budgeting optimization
def calculate_portfolio_var(w,V):
    # function that calculates portfolio risk
    w = np.matrix(w)
    return (w*V*w.T)[0,0]

def calculate_risk_contribution(w,V):
    # function that calculates asset contribution to total risk
    w = np.matrix(w)
    sigma = np.sqrt(calculate_portfolio_var(w,V))
    # Marginal Risk Contribution
    MRC = V*w.T
    # Risk Contribution
    RC = np.multiply(MRC,w.T)/sigma
    return RC

def risk_budget_objective(x,pars):
    # calculate portfolio risk
    V = pars[0]# covariance table
    x_t = pars[1] # risk target in percent of portfolio risk
    sig_p =  np.sqrt(calculate_portfolio_var(x,V)) # portfolio sigma
    risk_target = np.asmatrix(np.multiply(sig_p,x_t))
    asset_RC = calculate_risk_contribution(x,V)
    J = sum(np.square(asset_RC-risk_target.T))[0,0] # sum of squared error
    return J

def total_weight_constraint(x):
    return np.sum(x)-1.0

def long_only_constraint(x):
    return x

x_t = [0.25, 0.25, 0.25, 0.25] # your risk budget percent of total portfolio risk (equal risk)
cons = ({'type': 'eq', 'fun': total_weight_constraint},
{'type': 'ineq', 'fun': long_only_constraint})
res= minimize(risk_budget_objective, w0, args=[V,x_t], method='SLSQP',constraints=cons, options={'disp': True})
w_rb = np.asmatrix(res.x)

Then the weights in the risk parity portfolio are

[ 0.19543974,  0.21521557,  0.16260951,  0.42673519]

and the risk contributions to the total portfolio are

figure_1

The problem is actually set up to  calculate any risk budget allocation since it actually usescapture you can set up your target risk allocation for each asset then minimise the objective function of squared error

e.g.

x_t = [0.3, 0.3, 0.1, 0.3] # your risk budget percent of total portfolio risk

The weights are
[ 0.22837243, 0.25116466, 0.08875776, 0.43170515]

individual asset RC percentages are:

figure_3

7 thoughts on “Risk Parity/Risk Budgeting Portfolio in Python

  1. Great article – it helps me a lot solving my problem in python. However, when i run your code, I always end up with an equal weighted portfolio. Any hint you can give me? I operate with
    w0 = [1/3] * 3
    x_t = [1 /3] * 3
    and
    V=
    [[ 0.00021555 0.00041903 0.0003287 ]
    [ 0.00041903 0.00216102 0.00106898]
    [ 0.0003287 0.00106898 0.00140555]]

    any hints?

    1. Hi Hansueli,
      Thanks for verifying, I just checked with my code, it seems the minimize function only runs 1 iteration in this case which is not enough to find an optimal solution. It’s caused by the precision goal settings for the minimize function, which is defaulted at 1e-6, and your problem start at 1e-8.

      There are two ways to modify this:

      Change the risk_budget_objective function by penalising the cost and it should work. (e.g. I multiplied J*1000)
      def risk_budget_objective(x,pars):
      # calculate portfolio risk
      V = pars[0]# covariance table
      x_t = pars[1] # risk target in percent of portfolio risk
      sig_p = np.sqrt(calculate_portfolio_var(x,V)) # portfolio sigma
      risk_target = np.asmatrix(np.multiply(sig_p,x_t))
      asset_RC = calculate_risk_contribution(x,V)
      J = sum(np.square(asset_RC-risk_target.T))[0,0] *1000 # sum of squared error
      return J

      or you can change the ftol : float (Precision goal for the value of f in the stopping criterion) in the minimise function to something smaller (in this case I use 1e-12)
      res= minimize(risk_budget_objective, w0, args=[V,x_t], method=’SLSQP’,constraints=cons, options={‘disp’: True, ‘ftol’: 1e-12})

      By doing so, you would potentially run more iteration to get more precision. (a trade off)

  2. Hi DZ,
    This helped, thanks for the hint. It is very useful for me as I am not yet very experienced in the world of python optimizers.
    Best Regards

    Hansueli

  3. I read this article several weeks ago. It is very inspiring and helpful to my research. Thanks for sharing.
    I had once been puzzled by the same problem with Hansueli, i.e. the minimize function only runs 1 iteration with certain covariance matrix input. I also tried two ways, i.e. (1)penalizing the cost (2)changing the stopping criterion in the minimize function to 1e-15. It solved the problem in some situation. But there is still problems with certain covariance matrix input.
    For example, I have this 9*9 covariance matrix:
    V=np.matrix([[3.23822414e-02,2.09586336e-02,1.19953181e-03,-3.42945597e-04,1.44226523e-03,3.18839922e-03,1.28094942e-04,1.09620895e-04,-8.14963119e-05]
    ,[2.09586336e-02,1.39930713e-01,6.50801720e-03,3.02103328e-03,1.08176923e-02,4.80103744e-02,7.69201371e-04,-1.25520964e-02,-1.88265113e-04]
    ,[1.19953181e-03,6.50801720e-03,3.69433053e-03,-5.13665176e-04,2.20203633e-03,2.31740623e-03,8.55164862e-05,-7.56440183e-04,-7.81788792e-06]
    ,[-3.42945597e-04,3.02103328e-03,-5.13665176e-04,1.95207568e-03,-3.27383676e-04,4.38795650e-04,2.68556355e-05,-2.18586766e-04,-2.54360233e-06]
    ,[1.44226523e-03,1.08176923e-02,2.20203633e-03,-3.27383676e-04,2.96759371e-03,4.80443514e-03,1.93560587e-04,-1.06510489e-03,-2.46200168e-05]
    ,[3.18839922e-03,4.80103744e-02,2.31740623e-03,4.38795650e-04,4.80443514e-03,2.36495619e-02,9.62021325e-04,-1.86584784e-03,-9.06395586e-05]
    ,[1.28094942e-04,7.69201371e-04,8.55164862e-05,2.68556355e-05,1.93560587e-04,9.62021325e-04,2.89909625e-04,5.31840917e-04,2.55116425e-06]
    ,[1.09620895e-04,-1.25520964e-02,-7.56440183e-04,-2.18586766e-04,-1.06510489e-03,-1.86584784e-03,5.31840917e-04,9.29821052e-03,2.14245629e-05]
    ,[-8.14963119e-05,-1.88265113e-04,-7.81788792e-06,-2.54360233e-06,-2.46200168e-05,-9.06395586e-05,2.55116425e-06,2.14245629e-05,5.78390121e-06]])
    With equal risk budgets, the resulting weights in this risk parity portfolio are:
    [ 3.96638553e-02 1.31838486e-02 1.15744406e-01 2.23200288e-01
    1.00733213e-01 2.98562419e-02 3.71432543e-01 1.06185604e-01
    5.36184804e-19]
    The weight of the last asset is 0! And the optimization appears not to reach the 1e-15 stopping criterion:
    Optimization terminated successfully. (Exit mode 0)
    Current function value: 0.000738113620943
    Iterations: 21
    Function evaluations: 232
    Gradient evaluations: 21
    The possible reason is that the variance of the last asset, which represents the money market mutual fund, is far more smaller than other assets. This is the usual case in practical market. I am trying to find ways to solve this problem, and to make this algorithm more robust. Do you have any suggestions?
    Thank you very much.

  4. And when I enter different initial values, it gives totally different results. So strange that the algorithm is sensitive to initial values.
    case 1.
    w0=np.ones(9)*1.0/9
    Optimization terminated successfully. (Exit mode 0)
    Current function value: 0.000738113620943
    Iterations: 21
    Function evaluations: 232
    Gradient evaluations: 21
    The resulting weights:
    [[ 3.96638553e-02 1.31838486e-02 1.15744406e-01 2.23200288e-01
    1.00733213e-01 2.98562419e-02 3.71432543e-01 1.06185604e-01
    5.36184804e-19]]

    case 2.
    w0=[0.05217992,0.04176691,0.08060158,0.03545008,0.00784548,0.00609901,0.01657915,0.07723674,0.68224114]
    Optimization terminated successfully. (Exit mode 0)
    Current function value: 1.00601342965e-15
    Iterations: 33
    Function evaluations: 363
    Gradient evaluations: 33
    The resulting weights:
    [[ 0.00787686 0.00260464 0.020088 0.03888814 0.01903772 0.0061617
    0.05590877 0.01845764 0.83097653]]

  5. I read this article several weeks ago. It is very inspiring and helpful to my research. Thanks for sharing.
    I had once been puzzled by the same problem with Hansueli, i.e. the minimize function only runs 1 iteration with certain covariance matrix input. I also tried two ways, i.e. (1)penalizing the cost (2)changing the stopping criterion in the minimize function to 1e-15. It solved the problem in some situation. But there is still problems with certain covariance matrix input.
    For example, I have this 9*9 covariance matrix:
    V=np.matrix([[3.23822414e-02,2.09586336e-02,1.19953181e-03,-3.42945597e-04,1.44226523e-03,3.18839922e-03,1.28094942e-04,1.09620895e-04,-8.14963119e-05]
    ,[2.09586336e-02,1.39930713e-01,6.50801720e-03,3.02103328e-03,1.08176923e-02,4.80103744e-02,7.69201371e-04,-1.25520964e-02,-1.88265113e-04]
    ,[1.19953181e-03,6.50801720e-03,3.69433053e-03,-5.13665176e-04,2.20203633e-03,2.31740623e-03,8.55164862e-05,-7.56440183e-04,-7.81788792e-06]
    ,[-3.42945597e-04,3.02103328e-03,-5.13665176e-04,1.95207568e-03,-3.27383676e-04,4.38795650e-04,2.68556355e-05,-2.18586766e-04,-2.54360233e-06]
    ,[1.44226523e-03,1.08176923e-02,2.20203633e-03,-3.27383676e-04,2.96759371e-03,4.80443514e-03,1.93560587e-04,-1.06510489e-03,-2.46200168e-05]
    ,[3.18839922e-03,4.80103744e-02,2.31740623e-03,4.38795650e-04,4.80443514e-03,2.36495619e-02,9.62021325e-04,-1.86584784e-03,-9.06395586e-05]
    ,[1.28094942e-04,7.69201371e-04,8.55164862e-05,2.68556355e-05,1.93560587e-04,9.62021325e-04,2.89909625e-04,5.31840917e-04,2.55116425e-06]
    ,[1.09620895e-04,-1.25520964e-02,-7.56440183e-04,-2.18586766e-04,-1.06510489e-03,-1.86584784e-03,5.31840917e-04,9.29821052e-03,2.14245629e-05]
    ,[-8.14963119e-05,-1.88265113e-04,-7.81788792e-06,-2.54360233e-06,-2.46200168e-05,-9.06395586e-05,2.55116425e-06,2.14245629e-05,5.78390121e-06]])
    With equal risk budgets, the resulting weights in this risk parity portfolio are:
    [ 3.96638553e-02 1.31838486e-02 1.15744406e-01 2.23200288e-01
    1.00733213e-01 2.98562419e-02 3.71432543e-01 1.06185604e-01
    5.36184804e-19]
    The weight of the last asset is 0! And the optimization appears not to reach the 1e-15 stopping criterion:
    Optimization terminated successfully. (Exit mode 0)
    Current function value: 0.000738113620943
    Iterations: 21
    Function evaluations: 232
    Gradient evaluations: 21
    The possible reason is that the variance of the last asset, which represents the money market mutual fund, is far more smaller than other assets. This is the usual case in practical market. I am trying to find ways to solve this problem, and to make this algorithm more robust. Do you have any suggestions?
    Thank you very much.

    And when I enter different initial values, it gives totally different results. So strange that the algorithm is sensitive to initial values.
    case 1.
    w0=np.ones(9)*1.0/9
    Optimization terminated successfully. (Exit mode 0)
    Current function value: 0.000738113620943
    Iterations: 21
    Function evaluations: 232
    Gradient evaluations: 21
    The resulting weights:
    [[ 3.96638553e-02 1.31838486e-02 1.15744406e-01 2.23200288e-01
    1.00733213e-01 2.98562419e-02 3.71432543e-01 1.06185604e-01
    5.36184804e-19]]

    case 2.
    w0=[0.05217992,0.04176691,0.08060158,0.03545008,0.00784548,0.00609901,0.01657915,0.07723674,0.68224114]
    Optimization terminated successfully. (Exit mode 0)
    Current function value: 1.00601342965e-15
    Iterations: 33
    Function evaluations: 363
    Gradient evaluations: 33
    The resulting weights:
    [[ 0.00787686 0.00260464 0.020088 0.03888814 0.01903772 0.0061617
    0.05590877 0.01845764 0.83097653]]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s