Value at Risk (VaR)#

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = (10,5)
plt.rcParams['font.size'] = 13
from scipy.stats import norm

rets = pd.read_excel('../data/risk_etf_data.xlsx', sheet_name='total returns').set_index('Date')
QUANTILE = 0.05
from matplotlib.ticker import PercentFormatter

def tail_mean(values, q):
    """Mean of the lowest q probability mass of an empirical distribution."""
    x = np.sort(np.asarray(values, dtype=float))
    mass = q * len(x)
    whole = int(np.floor(mass))
    fraction = mass - whole
    total = x[:whole].sum()
    if fraction > 0:
        total += fraction * x[whole]
    return total / mass

VaR and CVaR#

VaR is a quantile of portfolio returns or PnL over a specified horizon. CVaR averages the outcomes in that tail. The figure uses SPY daily returns; the legend reports both measures as positive losses.

r = rets['SPY'].dropna()
q = np.quantile(r, QUANTILE, method='inverted_cdf')
CVaR = -tail_mean(r, QUANTILE)

fig, ax = plt.subplots()
ax.hist(r, bins=90, color='gray', alpha=0.7)
ax.axvline(q, color='black', lw=2, label=f'VaR (5%) = {-q:.1%}')
ax.axvline(tail_mean(r, QUANTILE), color='#b64c33', lw=2, ls='--', label=f'CVaR (5%) = {CVaR:.1%}')
ax.set_xlabel('SPY daily return')
ax.set_title('SPY Return Distribution')
ax.xaxis.set_major_formatter(PercentFormatter(1, decimals=0))
ax.set_ylabel('Observations')
ax.legend()
plt.show()
../_images/8aba22a72712ad777c211d2a334949642721b1bb69ee50900e90faf91e1d6227.png

Definition#

Let \(\pnl_{t,t+\tau}=\port_{t+\tau}-\port_t\) denote the dollar PnL over \(\tau\) days. For a continuous distribution, the lower-tail VaR at probability \(\quant\) satisfies

\[\Pr\left(\pnl_{t,t+\tau}\le\pnlVaRqtau_t\right)=\quant.\]

The loss convention uses \(\loss=-\pnl\) and reports \(L^{\VaRqtau}_t=-\pnlVaRqtau_t\):

\[\Pr\left(\loss_{t,t+\tau}>L^{\VaRqtau}_t\right)=\quant.\]

Specify the horizon, tail probability, and sign convention when quoting VaR.

Returns#

The return quantile is \(\rVaRqtau_t\). For a positive portfolio value,

\[\pnlVaRqtau_t=\rVaRqtau_t\,\port_t.\]

Profits versus Losses#

These notes use lower-tail returns and PnL in the formulas. Their quantiles are usually negative at the tail probabilities of interest.

Tables labeled as losses report the negative of the return or PnL quantile. The tail probability \(q\) corresponds to confidence level \(1-q\).

Distribution#

For a continuous, strictly increasing cumulative distribution function (CDF),

\[\pnlVaRqtau=\cdf_{\pnl}^{-1}(\quant), \qquad \rVaRqtau=\cdf_r^{-1}(\quant).\]

Discrete Distributions#

The lower quantile is defined more generally by

\[\pnlVaRqtau=\inf\left\{x:\cdf_{\pnl}(x)\ge\quant\right\}.\]

If there is probability mass at the cutoff, the probability strictly below it can be less than \(q\). The examples and sample-quantile calculations use this lower-quantile convention.

VaR Through Time#

Estimate the sample quantile over a rolling window and apply it to the next day’s return. A breach occurs when the return falls below that forecast.

A calibrated 5% VaR has a breach probability of 5%. Check both the frequency and clustering of breaches.

r = rets['SPY'].dropna()
WINDOW = 252
var_roll = r.rolling(WINDOW).apply(
    lambda x: np.quantile(x, QUANTILE, method='inverted_cdf'), raw=True).shift(1)
valid = var_roll.notna()
breach = valid & (r < var_roll)

fig, ax = plt.subplots()
ax.plot(r.index, r, color='gray', alpha=0.4, lw=0.5, label='SPY daily return')
ax.plot(var_roll.index, var_roll, color='black', lw=1.5,
        label='sample quantile (rolling)')
ax.plot(r[breach].index, r[breach], '.', color='#b64c33', label='breach')
ax.set_title('Rolling VaR and Daily Returns')
ax.set_ylabel('Daily return')
ax.yaxis.set_major_formatter(PercentFormatter(1, decimals=0))
ax.legend()
plt.show()

breach_summary = pd.DataFrame({'Forecast days': [valid.sum()],
                              'Breaches': [breach.sum()],
                              'Breach rate': [breach[valid].mean()]})
display(breach_summary.style.set_table_attributes('class="dataframe"').hide(axis='index').format(
    {'Forecast days': '{:,.0f}', 'Breaches': '{:,.0f}', 'Breach rate': '{:.1%}'},
    na_rep='').set_caption('Rolling sample-quantile VaR; target breach rate 5%'))
../_images/e2e7aa94ffa08254d788f3f8bb70d927db68360aac93f3d7cc346505ade8429c.png
Rolling sample-quantile VaR; target breach rate 5%
Forecast days Breaches Breach rate
2,132 115 5.4%

Each return enters the rolling window at the next update and remains until it falls outside the window. Equal weighting can make the estimate slow to adjust after a change in volatility. Filtered historical simulation adjusts past shocks to the current volatility forecast.

Normal Distribution#

Normal VaR provides a benchmark based on the mean and volatility. Its tail estimates depend on the normality assumption.

Suppose the \(\tau\)-period return has distribution

\[r_{t,t+\tau}\sim\mathcal{N}(\mu_\tau,\sigma_\tau^2).\]

Normal Formula#

Let \(\zscore_\quant\) be the standard normal quantile, so that \(\cdfz(\zscore_\quant)=\quant\). Then

\[\rVaRqtau=\mu_\tau+\zscore_\quant\sigma_\tau,\]

and the dollar PnL quantile is

\[\pnlVaRqtau_t=\rVaRqtau_t\,\port_t.\]
rows = {}
for a in ['SPY', 'HYG', 'USO', 'BTC']:
    x = rets[a].dropna()
    rows[a] = {'Sample 5%': -np.quantile(x, .05, method='inverted_cdf'),
               'Normal 5%': -(x.mean() + norm.ppf(.05)*x.std()),
               'Sample 1%': -np.quantile(x, .01, method='inverted_cdf'),
               'Normal 1%': -(x.mean() + norm.ppf(.01)*x.std())}
comparison = pd.DataFrame(rows).T
comparison.style.set_table_attributes('class="dataframe"').format({c: '{:.1%}' for c in comparison}, na_rep='').set_caption(
    'Daily VaR: sample quantiles and normal estimates, reported as losses')
Daily VaR: sample quantiles and normal estimates, reported as losses
  Sample 5% Normal 5% Sample 1% Normal 1%
SPY 1.7% 1.8% 3.3% 2.6%
HYG 0.7% 0.8% 1.4% 1.2%
USO 3.6% 4.0% 7.1% 5.7%
BTC 6.1% 6.7% 12.2% 9.6%
x = rets['BTC'].dropna()
sample_cut = np.quantile(x, .01, method='inverted_cdf')
normal_cut = x.mean() + norm.ppf(.01)*x.std()
fig, ax = plt.subplots()
ax.hist(x, bins=150, density=True, color='gray', alpha=.7)
grid = np.linspace(-.3, .3, 400)
ax.plot(grid, norm.pdf(grid, x.mean(), x.std()), color='black', lw=1.5, label='normal fit')
ax.axvline(sample_cut, color='#b64c33', lw=2, label=f'sample 1% VaR = {-sample_cut:.0%}')
ax.axvline(normal_cut, color='#b64c33', lw=2, ls='--', label=f'normal 1% VaR = {-normal_cut:.0%}')
ax.set_xlim(-.3, .3)
ax.set_xlabel('BTC daily return')
ax.set_ylabel('Density')
ax.set_title('BTC Returns and Normal Fit', loc='left')
ax.xaxis.set_major_formatter(PercentFormatter(1, decimals=0))
ax.legend()
plt.show()
../_images/06c8aa5cab62cb6b9d2d21813925c2587fc17e52097131a971619cd8d04eccb7.png

VaR in Practice#

  • Risk limits. VaR provides a common measure for monitoring positions.

  • Collateral. Some margin models use loss quantiles alongside other risk measures.

  • Regulatory capital. The Basel market-risk internal-models framework uses Expected Shortfall for capital and VaR for backtesting.

See the Basel market-risk framework.

Lognormal Prices#

Suppose one-period log returns are iid normal, with mean \(\mu\) and variance \(\sigma^2\). The \(\tau\)-period log return then satisfies

\[\rlog_{t,t+\tau}\sim\mathcal{N}(\tau\mu,\tau\sigma^2).\]

Its VaR is

\[\rlog^{\VaRqtau}=\tau\mu+\zscore_q\sigma\sqrt{\tau}.\]

If \(\mu=0\), this gives square-root-of-time scaling:

\[\rlog^{\VaRqtau}=\sqrt{\tau}\rlog^{\VaR_{q,1}}.\]

Horizon Scaling#

Square-root-of-time scaling is also used as an approximation for short-horizon simple returns and PnL. Serial dependence, changing volatility, and a nonzero mean affect the scaling. Simple returns also compound nonlinearly.

Dollar PnL#

Converting the log-return quantile to dollar PnL gives

\[\pnlVaRqtau_t=\port_t\left[\exp\left\{\tau\mu+ \zscore_q\sigma\sqrt{\tau}\right\}-1\right].\]

Conditional Value-at-Risk (CVaR)#

Definition#

Conditional VaR (CVaR), or Expected Shortfall (ES), averages the worst \(q\) probability mass. For continuous returns,

\[\rCVaRqtau=\E\left[r_{t,t+\tau}\mid r_{t,t+\tau}<\rVaRqtau\right],\]

with the same definition for PnL and \(\pnlCVaRqtau_t=\rCVaRqtau_t\port_t\).

For distributions with mass at the cutoff, use

\[\rCVaRqtau=\frac{1}{q}\int_0^q F_r^{-1}(u)\,du.\]

This includes only the fraction of probability at the cutoff needed to fill the \(q\) tail. The loss convention reverses the sign.

Normal Distribution#

For normally distributed returns,

\[\rCVaRqtau_t=\mu_{\tau,t}- \frac{\pdfz(\zscore_\quant)}{\quant}\sigma_{\tau,t}.\]

Truncated Normal#

For a standard normal variable \(Z\), with PDF \(\pdfz\) and CDF \(\cdfz\),

\[\E[Z\mid Z<a]=-\frac{\pdfz(a)}{\cdfz(a)}.\]

Substitute \(a=\zscore_q\) and \(r=\mu+\sigma Z\) to obtain the normal CVaR formula.

Normal Risk Measures#

Both normal VaR and normal CVaR depend only on the mean and volatility:

\[\rVaRqtau=\mu_{\tau,t}+\zscore_q\sigma_{\tau,t},\]
\[\rCVaRqtau=\mu_{\tau,t}- \frac{\pdfz(\zscore_q)}{q}\sigma_{\tau,t}.\]

With a zero mean and fixed tail probability, each is proportional to volatility.

Coherence#

Is \(\VaR\) a coherent risk measure?

No. In general, it is not subadditive.

Examples#

Payoffs#

Consider the payoff outcomes for \(\pnl_i\) and \(\pnl_j\) across five equally likely states of the world:

\[\begin{split}\pnl_i \in \begin{bmatrix}-2\\-1\\0\\1\\2\end{bmatrix}, \hspace{1cm} \pnl_j \in \begin{bmatrix}-1\\-2\\0\\1\\2\end{bmatrix}\end{split}\]

The states are ordered, such that in state \(s\), both securities deliver the payoff in row \(s\) of their respective arrays.

Consider a portfolio payoff that is the sum of the two: \(\pnl_p = \pnl_i + \pnl_j\).

Using the lower-quantile convention above, what is

\[\pnl_i^{\VaR_{.2}}, \hspace{1cm} \pnl_j^{\VaR_{.2}}, \hspace{1cm} \pnl_i^{\VaR_{.2}} + \pnl_j^{\VaR_{.2}}, \hspace{1cm} \pnl_p^{\VaR_{.2}}\]

Example: Returns Not Subadditive#

Suppose there are 100 equally likely outcomes (“states”), indexed by \(s=1,\ldots,100\). We have two assets, \(r_i\) and \(r_j\).

In almost every state, the return is simply

\[r_{s,i} = r_{s,j} = \frac{s-50}{100}\]

The only exception is the two states around the quantile cutoff, where the assets swap:

\[r_{i,(5)} = -.45, \quad r_{j,(5)} = -.46\]
\[r_{i,(4)} = -.46, \quad r_{j,(4)} = -.45\]

For each return individually,

\[r_i^{\VaR_{.05}} = -.45 = r_j^{\VaR_{.05}}\]

Yet for the equal-weighted portfolio, \(r_p = .5\, r_i + .5\, r_j\),

\[r_p^{\VaR_{.05}} = -.455\]

Data Example#

Compare the loss VaR of each equal-weighted ETF pair with the average of its individual loss VaRs. Use the same dates for all three quantities in each pair.

Subadditivity in this sample does not establish it for other distributions or portfolios. Failures can occur in continuous as well as discrete distributions.

from itertools import combinations

def loss_var(x):
    return -np.quantile(x, QUANTILE, method='inverted_cdf')

pairs = list(combinations(rets.columns.drop('SHV'), 2))
violations = 0
for a, b in pairs:
    pair = rets[[a, b]].dropna()
    violations += loss_var(pair.mean(axis=1)) > (
        loss_var(pair[a]) + loss_var(pair[b]))/2 + 1e-9
result = pd.DataFrame({'ETF pairs': [len(pairs)], 'Violations': [violations]})
result.style.set_table_attributes('class="dataframe"').hide(axis='index').format(
    {'ETF pairs': '{:,.0f}', 'Violations': '{:,.0f}'}, na_rep='').set_caption(
    'Sample VaR subadditivity at the 5% tail')
Sample VaR subadditivity at the 5% tail
ETF pairs Violations
45 1

Normal VaR#

Is Normal VaR coherent?

CVaR#

Is CVaR coherent?

  • For a normal distribution?

  • For the examples above?