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.
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:
- Get stock data
- Create a portfolio with equal risk weights
- 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)
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, )
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)
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, )
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.