Value at Risk and Expected Shortfall

At its core, financial risk management is concerned with quantifying potential losses to assess the downside of financial endeavors and prepare for worst-case scenarios. Perhaps the two most salient risk measures in the field of risk management are value at risk (VaR) and expected shortfall, also known as conditional value at risk (CVaR). Using statistical underpinnings, these measures estimate the probabilistic maximal loss of a portfolio over a given time period.

VaR

For a given confidence level $\alpha\in(0,1)$ and timeframe, VaR measures the maximum loss such that $(1-\alpha)\%$ of the time, the loss will be greater than the VaR. Mathematically, VaR is the $(1-\alpha)$-quantile loss, or: $$ \text{VaR}_\alpha=F_X^{-1}(1-\alpha), $$ where $F_X^{-1}(\cdot)$ is the inverse cumulative distribution function of the random variable $X$ of returns. Under the assumption that returns (equivalently, negative losses) are normally distributed under $X\sim\mathcal{N}(\mu,\sigma^2)$, the VaR is: $$ \text{VaR}_\alpha = \mu + \sigma\cdot F^{-1}(1-\alpha), $$ where $F^{-1}(\cdot)$ is the inverse cumulative distribution function of the standard normal.

Calculating historical VaR is therefore as simple as using the empirical distribution to find the value at which $(1-\alpha)\%$ of the returns lie below:

In [6]:
returns = raw.Close["^GSPC"].diff().dropna()

conf = .95
fig, ax = plt.subplots(figsize=(5,15/4))

var = np.quantile(returns, 1 - conf)
ax.axvline(var, color="#B03845", label=r"$\mathrm{VaR}_{\alpha=95\%}$")

_, bins, patches = ax.hist(returns, bins=200, density=True, color="#0A1E3D")
for patch, l_edge in zip(patches, bins[:-1]):
    if l_edge < var:
        patch.set_facecolor("#B03845")

plt.xlabel("Daily returns (USD)")
plt.ylabel("Frequency (normalised)")
plt.title("Daily S&P 500 returns from 1980 to 2025")

ax.spines[['right', 'top']].set_visible(False)
plt.xlim(-100, 100)
plt.legend(frameon=False)

print("VaR: {:.2f}; VaR under normality: {:.2f}".format(var,
                                                        np.mean(returns) + np.std(returns) * scipy.stats.norm.ppf(.05)))

plt.show()
VaR: -25.21; VaR under normality: -32.21
2025-02-18T21:20:54.322346 image/svg+xml Matplotlib v3.6.2, https://matplotlib.org/

Expressed in English, from 1980 to 2025, the S&P 500 index had a 1-day 95% VaR of \$25.21; or there was a 95% probability that losses would not exceed \$25.21 in one day. This is quite different to the VaR under the normality assumption, which has a 1-day 95% VaR of \$32.21.

Although relatively easy to calculate, VaR is sensitive to the assumed underlying probability model: A light-tail distribution (e.g., the normal) used in place of a true fat-tailed distribution can lead to underestimation in VaR as $\alpha\to1$:

In [7]:
Expand Code
2025-02-18T21:20:54.442099 image/svg+xml Matplotlib v3.6.2, https://matplotlib.org/

CVaR

The greatest drawback of VaR is the information lost by summarizing the tail of the distribution with just its upper value—VaR does not quantify the shape of the left tail. The conditional VaR (CVaR) or expected shortfall addresses this by calculating the average loss given the worst $(1-\alpha)\%$ of returns. In other words, it is the conditional expectation: $$ \begin{align*} \text{CVaR}_\alpha&=\mathbb{E}\left[X\mid X\le \text{VaR}_\alpha\right]\\ &= \frac{1}{1-\alpha}\int_{-\infty}^{\text{VaR}_\alpha} x\cdot f(x) dx, \end{align*} $$ where $f(\cdot)$ is the probability density function of $X$. Under the normality assumption, this simplifies to: $$ \text{CVaR}_\alpha=\mu - \sigma\frac{\phi(F^{-1}(\alpha))}{1-\alpha}, $$

where $\phi(\cdot)$ is the probability density function of the standard normal.

CVaR is often considered an enhancement of VaR as it quantifies tail risk on top of answering the question of how often portfolio losses will exceed the worst $(1-\alpha)\%$ of cases. Reusing the same example above:

In [8]:
returns = raw.Close["^GSPC"].diff().dropna()

conf = .95
fig, ax = plt.subplots(figsize=(5,15/4))

var = np.quantile(returns, 1 - conf)
ax.axvline(var, color="#B03845", label=r"$\mathrm{VaR}_{\alpha=95\%}$")

_, bins, patches = ax.hist(returns, bins=200, density=True, color="#0A1E3D")
for patch, l_edge in zip(patches, bins[:-1]):
    if l_edge < var:
        patch.set_facecolor("#B03845")

cvar = np.mean(returns[returns <= var])
ax.axvline(cvar, color="#94D2F6", label=r"$\mathrm{CVaR}_{\alpha=95\%}$")

plt.xlabel("Daily returns (USD)")
plt.ylabel("Frequency (normalised)")
plt.title("Daily S&P 500 returns from 1980 to 2025")

ax.spines[['right', 'top']].set_visible(False)
plt.xlim(-100, 100)
plt.legend(frameon=False)

print(f"VaR: {var:.2f}; VaR under normality: {normalVaR(np.mean(returns), np.std(returns), .95)}")
print(f"CVaR: {cvar:.2f}; CVaR under normality: {normalCVaR(np.mean(returns), np.std(returns), .95)}")

plt.show()
VaR: -25.21; VaR under normality: -32.21043951619077
CVaR: -50.47; CVaR under normality: -40.52311515074122
2025-02-18T21:20:54.600386 image/svg+xml Matplotlib v3.6.2, https://matplotlib.org/

From 1980 to 2025, the S&P 500 index had a 1-day 95% CVaR of \$50.47, meaning that in the worst 5% of 1-day S&P 500 returns, the average loss would be \$50.47. This last example presents the common case where the CVaR under the normality assumption underestimates the true CVaR due to the presence of fat-tailed returns.

References

Comments