PQN #023: Use CVaR to capture tail risk

PQN #023 Use CVaR to capture tail risk

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:

  1. Get stock data
  2. Compute VaR and CVaR
  3. Visualize the differences between the two

And do it all in Python.

Let’s go!

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',

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))
 PQN #023: Use CVaR to capture tail risk
Step 2: Compute VaR and CVaR
Start by building a function to compute VaR so you can compare it to CVaR.

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(
    returns, weights, 
    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(
    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.ylabel('Observation Frequency')
 PQN #023: Use CVaR to capture tail risk
The dashed line is CVaR and the solid line is VaR. VaR pinpoints the negative return at the 5th percentile. CVaR averages all the losses left of VaR (the orange bars), including the large negative return.
Returns are not normally distributed. CVaR captures it.

Well, that’s it for today. I hope you enjoyed it.
See you again next week.