Skip to content
All library documents

Fixing an Equal Risk Contribution Optimizer That Reuses Initial Weights

Article Quant Q&A · Author: GbAni

Summary

The note diagnoses a failure in an equal risk contribution portfolio optimizer that returns its starting weights after one iteration. The central bug is in the objective function: although the optimizer passes candidate weights into it, the calculations instead use globally defined initial weights. Consequently, the objective does not change as the optimizer explores alternatives, so the starting point appears optimal. The correction is to compute portfolio volatility and each asset’s risk contribution from the candidate weights and covariance matrix supplied to the objective.

The discussion also shows how to express risk contributions with NumPy arrays and covariance operations, avoiding deprecated matrix usage. An example computes contributions for a three-asset covariance matrix and equal starting allocations. The example illustrates the calculation, but the key lesson is the objective-function dependency: verify that every optimizer evaluation actually uses its input variables. It does not provide a general treatment of constraints, convergence diagnostics, or portfolio behavior across data sets.

Key ideas

  • An optimization objective must calculate its value from the candidate weights passed by the optimizer.
  • Using fixed initial weights makes the objective effectively constant during optimization.
  • Risk contributions can be computed from weights and the covariance matrix using array operations.
  • Equal risk contribution portfolios seek allocations whose asset risk contributions match target budgets.
  • A successful optimizer exit alone does not establish that the intended objective was optimized.

Tags

Full text
# Equal Risk Contribution portfolio scipy optimization not working


# Equal Risk Contribution portfolio scipy optimization not working












I'm trying to create a tool for an equal risk contribution portfolio,essentially following this article (https://quantdare.com/risk-parity-in-python/) but it is failing at the last step (def risk_parity_weights) as the scipy optimizer isn't working. It keeps giving me the initial weights as the optimized weights, and I know they aren't the optimized weights because even Excel Solver has been able to optimize this. All the others functions have been checked and are correct. Not sure what I'm doing wrong - please help!

```
import pandas as pd
pd.core.common.is_list_like = pd.api.types.is_list_like
import pandas_datareader.data as web
import numpy as np
import datetime
from scipy.optimize import minimize
Tolerance = 1e-10

def calculate_risk_contribution(weights,covariances):

    #Convert weights array to numpy matrix
    weights = np.matrix(weights)

    #Calculate portfolio st.dev
    portfolio_stdev = np.sqrt(weights*covariances*weights.T)[0,0]

    #Calculate Marginal Risk Contribution of each asset
    MRC = covariances*weights.T/portfolio_stdev

    #Calculate Risk Contribution of each asset
    RC = np.multiply(MRC,weights.T)
    return RC

def risk_budget_objective_error(weights,*args):

    #Covariance table occupies the first position in args variable
    covariances = args[0]

    #State risk budgets
    assets_risk_budget = args[1]

    #Convert weights array to numpy matrix
    weights = np.matrix(weights)

    #Calculate portfolio st_dev
    portfolio_stdev = calculate_portfolio_stdev(ca_begweights,ca_cov)

    #Calculate risk contributions
    assets_risk_contribution = calculate_risk_contribution(ca_begweights,ca_cov)

    #Calculate desired risk contribution of each asset
    assets_risk_target = np.asmatrix(np.multiply(portfolio_stdev,assets_risk_budget))

    #Calculate error between desired contribution and calculated distribution of each asset
    error = sum(np.square(assets_risk_contribution - assets_risk_target.T))[0,0]

    return error

def risk_parity_weights(covariances,assets_risk_budget, initial_weights):

    #Constraints to optimization
    #sum equals 100%
    cons = ({'type':'eq','fun':lambda x: np.sum(x) - 1.0},
              {'type':'ineq','fun':lambda x: x})

    #Optimization in scipy
    optimize_result = minimize(risk_budget_objective_error,
                           x0 = initial_weights,
                           args = (covariances, assets_risk_budget),
                           method = 'SLSQP',
                           constraints = cons,
                           tol = Tolerance,
                           options = {'disp':True})

    #Get optimized weights
    weights = optimize_result.x

    return weights
```

`risk_parity_weights(ca_cov,risk_budget_all, ca_begweights)` gives me

```
Optimization terminated successfully.    (Exit mode 0)
            Current function value: 9.54000328523598e-07
            Iterations: 1
            Function evaluations: 5
            Gradient evaluations: 1
```

see data below

```
    ca_cov = array([[ 5.28024463e-06, 3.29734889e-07, -7.04781216e-08], [ 3.29734889e-07, 1.32373854e-05, 3.71807979e-08], [-7.04781216e-08, 3.71807979e-08, 3.50845569e-05]]) 

risk_budget_all = Unnamed: 1 0.333333 Unnamed: 2 0.333333 Unnamed: 3 0.333333 Name: Risk Budget, dtype: object 

ca_begweights = array([0.33333333, 0.33333333, 0.33333333])
```

## Answer by Attack68 (score 1, accepted)

https://quant.stackexchange.com/a/41830

Further to my comment is it because your functions are returning matrix's which are deprecated?

Why not re-write your functions using ndarrays;

```
import numpy as np
ca_cov = np.array([[ 5.28024463e-06, 3.29734889e-07, -7.04781216e-08], [ 3.29734889e-07, 1.32373854e-05, 3.71807979e-08], [-7.04781216e-08, 3.71807979e-08, 3.50845569e-05]])
ca_ini_weights = np.array([0.33333333, 0.33333333, 0.33333333])

def risk_contribution(weights,covariances):
    # weights: ndarray of shape (n); covariances: ndarray of shape (n,n)
    s_dev = np.sqrt(np.einsum('i,ij,j->', weights, covariances, weights))
    risk_contrib = np.einsum('i,ij,j->i',weights, covariances, weights) / s_dev
    return risk_contrib

risk_contribution(ca_ini_weights, ca_cov):
>>> array([0.00025082, 0.00061599, 0.00158709])
```

As I looked to improve your objective function I noticed it had one major flaw:

> It accepts `weights` as an arguments but then uses `ca_begweights` in all its calculations so as an objective function all it ever does is return the initial value. In which case it returns the 'optimal' value given what it knows,.

## Answer by Hog (score 3)

https://quant.stackexchange.com/a/71348

For me, `weights@covariances*weights` is much easier to read than `np.einsum('i,ij,j->i',weights, covariances, weights)`.

So a one-liner is `risk_contrib = (weights@covariances*weights) / np.sqrt(weights@covariances@weights)`

## Answer by GbAni (score 1)

https://quant.stackexchange.com/a/41996

I figured out the issue, in the `risk_budget_objective_error(weights,*args)` function, I used pre-defined variables to figure calculate portfolio_stdev and assets_risk_contribution, which is the optimizer kept spitting back the initial weights after only one iteration.

Instead, I should have used `portfolio_stdev = calculate_portfolio_stdev(weights,covariances)` and `assets_risk_contribution = calculate_risk_contribution(weights,covariances)`. This worked for me.

EDIT: I now realize that this is what @Attack68 said much earlier, but I couldn't understand at the time.

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.