O que o VaR não diz
Um VaR de 99% de 3% diz que em 99 dias de 100 você perde menos de 3%. Não diz nada sobre o centésimo dia: pode ser 3,1% ou 30%. O expected shortfall (ES, CVaR) fecha a lacuna. É a perda média nos dias em que o VaR é excedido, então descreve a forma da cauda. Por ser uma média, também muda suavemente e, diferentemente do VaR, premia sempre a diversificação, e por isso Basileia passou do VaR para o ES no risco de mercado.
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 daysFazer a média da cauda
Historicamente o ES é direto: selecionam-se os retornos iguais ou abaixo do corte do VaR, tira-se a média e inverte-se o sinal. O resultado nunca é menor que o VaR, pois cada retorno da cauda é pelo menos tão ruim quanto o corte. A razão ES / VaR indica o peso da cauda. Para uma normal é cerca de 1,2 a 95%, e dados de caudas pesadas costumam mostrar razão maior.
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))Uma fórmula para o caso normal
Para um retorno normal a média da cauda tem forma fechada: ES = sigma * pdf(z) / (1 - confidence) - mu, em que z é o quantil usado no VaR e pdf a densidade normal padrão em z. Reduz o cálculo a uma função de duas linhas e só é exata se os dados forem normais, então compará-la com o ES histórico é um teste rápido de quanto os dados se afastam da normalidade.
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- O ES é a perda média na cauda além do VaR: menos a média dos retornos iguais ou abaixo do quantil
(1 - confidence). - Nunca é menor que o VaR;
ES / VaRmede o peso da cauda, e a fórmula normal ésigma * pdf(z) / (1 - confidence) - mu. - Reporte o VaR como limiar e o ES como gravidade; o ES é mais ruidoso porque se apoia em poucas observações de cauda.
A seguir: os testes de estresse, perguntar quanto uma carteira perderia em um cenário específico e extremo.
Lab: o que é avaliado
- Isolar os piores dias com
tail_returns25 pts - Fazer a média da cauda com
historical_es25 pts - Obtê-lo por uma fórmula com
normal_es25 pts - Comparar cauda e limiar com
es_var_ratio25 pts