PQN #014: Build your own risk parity portfolio

Build your own risk parity portfolio

Build your own risk parity portfolio

In today’s issue I’m going to show you how to build your own risk parity portfolio.

Risk parity is a strategy that uses risk to find the allocations of an investment portfolio. It allocates money to stocks based on a target risk level – usually volatility.

In other words, instead of equal dollar weights, risk parity portfolios have equal risk weights.

Dollar to risk

The first asset manager to use risk parity at scale was Bridgewater Associates in the 1990s. Their risk parity fund now has $25,000,000,000 under management. Risk parity is a complex strategy used by some of the most sophisticated investors in the world.

The goal of risk parity is to earn the optimal level of return at a targeted risk level

The problem with dollar-weighted portfolios is not every stock has the same risk. That means if you equally weigh two stocks, but one has higher risk, portfolio returns will be dominated by the higher-risk stock. Investors use risk parity to avoid this problem.

By the end of this issue, you’ll know how to:

  1. Get stock data
  2. Create a portfolio with equal risk weights
  3. Create a portfolio with a minimum return constraint

Risk parity took a decade for teams of PhDs to figure out.

I’m going to show you how to do it in 5 minutes in a few lines of Python.

Step 1: Get stock price data

Start by importing packages and getting data. I like to use yfinance for getting stock data. I’ll use the excellent Riskfolio-Lib package to create the risk parity portfolios.

import yfinance as yf
import riskfolio as rp
import warnings
warnings.filterwarnings("ignore")

Riskfolio-Lib prints out some warnings that are safe to ignore. So ignore them.

After the imports, create a list of tickers you want to use. Use any tickers you want. Since you can download data for more than one stock at a time, it’s one line of code to get all the data.

# portfolio tickers
assets = ["JCI", "TGT", "CMCSA", "CPB", "MO", "APA", "MMC", "JPM",
          "ZION", "PSA", "BAX", "BMY", "LUV", "PCAR", "TXT", "TMO",
          "DE", "MSFT", "HPQ", "SEE", "VZ", "CNP", "NI", "T", "BA"]

# sort tickers
assets.sort()

#download data
data = yf.download(assets, start="2016-01-01", end="2019-12-30")

# compute non-compounding, daily returns
returns = data['Adj Close'].pct_change().dropna()

Step 2: Create a portfolio with equal risk weights

First, setup a portfolio with equal risk weights. This means Riskfolio-Lib will find the weights that cause the risk contribution of each stock to be equal.

port = rp.Portfolio(returns=returns)

port.assets_stats(method_mu='hist', method_cov='hist', d=0.94)

w_rp = port.rp_optimization(
    model="Classic",  # use historical
    rm="MV",  # use mean-variance optimization
    hist=True,  # use historical scenarios
    rf=0,  # set risk free rate to 0
    b=None  # don't use constraints
)

First, build the portfolio object with the stock returns. Then estimate the expected returns and covariance based on historic data. Finally, use the classical mean-variance optimization to find the risk parity weights.

Riskfolio-Lib makes it easy to visualize the weights.

ax = rp.plot_pie(w=w_rp)

Equal risk weight

You can see the weight of each stock is not equal. That’s because the portfolio is risk-weighted. Higher-risk stocks have lower weights to maintain the overall portfolio risk target.

What about the risk contributions.

# show the risk contribution for each asset is equal
ax = rp.plot_risk_con(
    w_rp,
    cov=port.cov,
    returns=port.returns,
    rm="MV",
    rf=0,
)

Equal risk contribution

They’re equal! That’s exactly what to expect.

Step 3: Create a portfolio with a minimum return constraint

A criticism of risk parity is that without leverage, returns lag. So add a constraint to weight the stocks in a way to reach a minimum portfolio return. This adds weight to higher-risk stocks to push the portfolio returns higher.

port.lowerret = 0.0008

# estimate the optimal portfolio with risk parity with the constraint
w_rp_c = port.rp_optimization(
    model="Classic",  # use historical
    rm="MV",  # use mean-variance optimization
    hist=True,  # use historical scenarios
    rf=0,  # set risk free rate to 0
    b=None  # don't use constraints
)

Add a constraint for the minimum level of expected returns for the entire portfolio.

Again, plotting is easy.

ax = rp.plot_pie(w=w_rp_c)

Return constraint

MSFT dominates with a 15.3% weight. That’s because to reach the minimum return threshold, risk parity overweights stocks with higher risk. And with higher risk comes higher return.

Plot the risk contributions.

ax = rp.plot_risk_con(
    w_rp_c,
    cov=port.cov,
    returns=port.returns,
    rm="MV",
    rf=0,
)

Return constraint

They’re no longer equal. That’s because risk parity figured out the optimal risk weights to hit the minimum portfolio return constraint.

Risk parity is a great way to manage a predictable portfolio that performs consistently in most markets.