Ce que la VaR ne dit pas
Une VaR à 99 % de 3 % dit que, 99 jours sur 100, vous perdez moins de 3 %. Elle ne dit rien du centième jour : ce peut être 3,1 % ou 30 %. L'expected shortfall (ES, CVaR) comble ce manque. C'est la perte moyenne les jours où la VaR est dépassée, donc elle décrit la forme de la queue. Comme elle fait une moyenne, elle varie aussi de façon continue et, contrairement à la VaR, elle récompense toujours la diversification, ce qui explique le passage de Bâle de la VaR à l'ES pour le risque de marché.
import numpy as np
rng = np.random.default_rng(5)
returns = rng.standard_t(4, size=1000) * 0.01
cutoff = np.quantile(returns, 0.05) # the 95% VaR is minus this
tail = np.sort(returns[returns <= cutoff]) # the worst 5% of the days, lowest first
print(len(tail), round(float(-cutoff), 5)) # 50 days; the VaR
print(np.round(tail[:4], 4)) # the four worst daysMoyenner la queue
Historiquement, l'ES est simple : on sélectionne les rendements au niveau du seuil de la VaR ou en dessous, on les moyenne et on change le signe. Le résultat n'est jamais inférieur à la VaR, car chaque rendement de la queue est au moins aussi mauvais que le seuil. Le rapport ES / VaR indique l'épaisseur de la queue. Pour une loi normale, il vaut environ 1,2 à 95 %, et les données à queues épaisses montrent en général un rapport plus grand.
import numpy as np
def tail_returns(returns, confidence):
cutoff = np.quantile(returns, 1 - confidence)
return np.sort(returns[returns <= cutoff])
def historical_es(returns, confidence):
return float(-tail_returns(returns, confidence).mean())
rng = np.random.default_rng(5)
returns = rng.standard_t(4, size=1000) * 0.01
for confidence in (0.95, 0.99):
var = -np.quantile(returns, 1 - confidence)
es = historical_es(returns, confidence)
print(confidence, round(float(var), 5), round(es, 5), round(es / float(var), 3))Une formule pour le cas normal
Pour un rendement normal, la moyenne de la queue a une forme explicite : ES = sigma * pdf(z) / (1 - confidence) - mu, où z est le quantile utilisé pour la VaR et pdf la densité normale centrée réduite en z. Le calcul tient en deux lignes et n'est exact que si les données sont normales ; le comparer à l'ES historique est donc un test rapide de l'écart à la normalité.
from statistics import NormalDist
def normal_es(mu, sigma, confidence):
z = NormalDist().inv_cdf(confidence)
return sigma * NormalDist().pdf(z) / (1 - confidence) - mu
for confidence in (0.95, 0.975, 0.99):
var = NormalDist().inv_cdf(confidence) # in standard deviations, mu = 0
es = normal_es(0.0, 1.0, confidence)
print(confidence, round(var, 3), round(es, 3), round(es / var, 3)) # ES/VaR: 1.254, 1.193, 1.146# The formula against a simulated tail
import numpy as np
from statistics import NormalDist
rng = np.random.default_rng(0)
sample = rng.normal(0.0, 0.01, size=1_000_000)
tail = sample[sample <= np.quantile(sample, 0.01)]
simulated = -tail.mean()
z = NormalDist().inv_cdf(0.99)
formula = 0.01 * NormalDist().pdf(z) / 0.01
print(round(float(simulated), 5), round(formula, 5)) # both close to 0.02665- L'ES est la perte moyenne dans la queue au-delà de la VaR : l'opposé de la moyenne des rendements au niveau du quantile
(1 - confidence)ou en dessous. - Elle n'est jamais inférieure à la VaR ;
ES / VaRmesure l'épaisseur de la queue, et la formule normale estsigma * pdf(z) / (1 - confidence) - mu. - Rapportez la VaR comme seuil et l'ES comme gravité ; l'ES est plus bruitée car elle repose sur peu d'observations de queue.
À suivre : les tests de résistance, demander ce qu'un portefeuille perdrait dans un scénario précis et extrême.
Lab : ce qui est noté
- Isoler les pires jours avec
tail_returns25 pts - Moyenner la queue avec
historical_es25 pts - L'obtenir par une formule avec
normal_es25 pts - Comparer queue et seuil avec
es_var_ratio25 pts