Use CVaR to capture tail risk
In today’s issue, I’m going to show you how to compute the conditional value at risk (CVaR) of a portfolio of stocks.
In a previous newsletter issue, you calculated the value at risk (VaR) of a portfolio. VaR estimates how much your portfolio might lose over a set time. One of the caveats with VaR is it assumes the distribution of returns is normal, which it’s not. Another caveat is VaR assumes losses will not exceed the cutoff point, which they likely will.
CVaR captures more information than VaR
CVaR is an improvement over VaR and is considered superior by practitioners. It takes into consideration the actual shape of the distribution and quantifies the tail risk. CVaR is also known as the expected shortfall since it measures the expectation of all the different possible losses greater than VaR.
Non-professional traders and investors should consider using CVaR over VaR for their own risk management. Unfortunately, most don’t.
By the end of this issue, you’ll know how to:
- Get stock data
- Compute VaR and CVaR
- Visualize the differences between the two
And do it all in Python.
Step 1: Get stock data
Start by importing the libraries and getting the data. I like to use yfinance for getting stock data and NumPy for math functions. Soon, I’ll use the OpenBB SDK for data.
import numpy as np import pandas as pd import yfinance as yf import matplotlib.pyplot as plt
I’m going to use 90 stocks in the S&P 100 index. Grab the data and compute the returns.
oex = ['MMM','T','ABBV','ABT','ACN','ALL','GOOGL','GOOG','MO','AMZN','AXP','AIG','AMGN','AAPL','BAC', 'BRK-B','BIIB','BLK','BA','BMY','CVS','COF','CAT','CVX','CSCO','C','KO','CL','CMCSA', 'COP','DHR','DUK','DD','EMC','EMR','EXC','XOM','META','FDX','F','GD','GE','GM','GILD', 'GS','HAL','HD','HON','INTC','IBM','JPM','JNJ','KMI','LLY','LMT','LOW','MA','MCD','MDT','MRK', 'MET','MSFT','MS','NKE','NEE','OXY','ORCL','PYPL','PEP','PFE','PM','PG','QCOM', 'SLB','SPG','SO','SBUX','TGT','TXN','BK','USB','UNP','UPS','UNH','VZ','V','WMT', 'WBA','DIS','WFC'] num_stocks = len(oex) data = yf.download(oex, start='2014-01-01', end='2016-04-04') returns = data['Adj Close'].pct_change() returns = returns - returns.mean(skipna=True) # de-mean the returns
Create a mock portfolio by generating random weights and multiplying them by the returns. You can use your actual weights to represent your own portfolio.
def scale(x): return x / np.sum(np.abs(x)) weights = scale(np.random.random(num_stocks)) plt.bar(np.arange(num_stocks),weights)
Step 2: Compute VaR and CVaR
Start by building a function to compute VaR so you can compare it to CVaR.
def value_at_risk( value_invested, returns, weights, alpha=0.95, lookback_days=500 ): returns = returns.fillna(0.0) portfolio_returns = returns.iloc[-lookback_days:].dot(weights) return np.percentile(portfolio_returns, 100 * (1-alpha)) * value_invested
First, replace any NaNs with 0.0s in the DataFrame of returns. Then take the last few days of returns and multiply them by the portfolio weights to create portfolio returns. Finally, compute VaR by taking the 5th percentile of returns and multiplying it by the value invested. This gets the amount you can expect to lose in one day with 95% confidence.
This is the main difference between VaR and CVaR. VaR represents a worst-case loss associated with a probability and a time horizon. CVaR is the expected loss if that worst-case threshold is crossed. In other words, CVaR quantifies the expected losses that occur beyond the VaR cutoff. VaR only measures the cutoff.
def cvar( value_invested, returns, weights, alpha=0.95, lookback_days=500 ): var = value_at_risk(value_invested, returns, weights, alpha, lookback_days=lookback_days) returns = returns.fillna(0.0) portfolio_returns = returns.iloc[-lookback_days:].dot(weights) var_pct_loss = var / value_invested return np.nanmean(portfolio_returns[portfolio_returns < var_pct_loss]) * value_invested
Compute VaR to get the cutoff point on the distribution that equals the 5th percentile. Then compute the portfolio returns and convert VaR back to a percentage instead of a dollar amount. To compute CVaR, take the average of all returns less than VaR and multiply by the invested value.
Taking the average of all returns less than VaR is the same as taking the area of the distribution left of that cutoff point.
Step 3: Vizualize the difference between the two
Compute the CVaR and VaR of the portfolio and note the difference.
cvar(value_invested, returns, weights) value_at_risk(value_invested, returns, weights)
CVaR is a bigger negative number than VaR. The difference between the two is the extra information CVaR captures by taking the average of all returns less than VaR. If you are dependent on VaR for risk management, you miss that information.
If this still isn’t clear, a chart will help.
lookback_days = 500 portfolio_returns = returns.fillna(0.0).iloc[-lookback_days:].dot(weights) portfolio_VaR = value_at_risk(value_invested, returns, weights) portfolio_VaR_return = portfolio_VaR / value_invested portfolio_CVaR = cvar(value_invested, returns, weights) portfolio_CVaR_return = portfolio_CVaR / value_invested plt.hist(portfolio_returns[portfolio_returns > portfolio_VaR_return], bins=20) plt.hist(portfolio_returns[portfolio_returns < portfolio_VaR_return], bins=10) plt.axvline(portfolio_VaR_return, color='red', linestyle='solid') plt.axvline(portfolio_CVaR_return, color='red', linestyle='dashed') plt.legend(['VaR', 'CVaR', 'Returns', 'Returns < VaR']) plt.title('Historical VaR and CVaR') plt.xlabel('Return') plt.ylabel('Observation Frequency')