Etapa 2 de 4 · Fundamentos en Python
Value at Risk (VaR) y Expected Shortfall: los 3 métodos en Python
Qué es el Value at Risk (VaR), cómo se calcula con los métodos histórico, paramétrico y de Monte Carlo en Python, por qué no basta y cómo complementarlo con el Expected Shortfall y un backtest.
En este artículo
- Qué es el Value at Risk
- Los datos: una cartera de acciones, bonos y oro
- Método 1: VaR histórico
- Método 2: VaR paramétrico (varianza-covarianza)
- Método 3: VaR de Monte Carlo
- Comparar los tres métodos
- El problema del VaR: no dice cuánto pierdes cuando falla
- Del VaR a 1 día al VaR a 10 días
- Backtesting: ¿funciona tu VaR?
- Caso real: LTCM, el fondo de los premios Nobel
- Resumen
«¿Cuánto podemos perder mañana?». Es la pregunta que se hace cada día cualquier banco, gestora o fondo, y la respuesta estándar de la industria tiene nombre propio: el Value at Risk, o VaR. Lo exige la regulación bancaria, aparece en los informes de los fondos y lo preguntan en casi todas las entrevistas quant.
En este artículo verás qué significa exactamente, cómo se calcula con los tres métodos clásicos en Python y, lo más importante, qué no te dice.
Qué es el Value at Risk
El VaR responde a una pregunta muy concreta:
VaR al 99% a 1 día = 10.000 € significa: «en 99 de cada 100 días, la cartera no debería perder más de 10.000 €». O dicho al revés: aproximadamente un día de cada cien se perderá más de esa cantidad.
Tiene siempre tres ingredientes: un nivel de confianza (95% o 99% son lo habitual), un horizonte (1 día, 10 días…) y el valor de la cartera. Matemáticamente es un cuantil de la distribución de pérdidas:
\[ \text{VaR}_\alpha = -\,q_{1-\alpha}(R)\cdot V \]
donde \( q_{1-\alpha}(R) \) es el percentil \( 1-\alpha \) de las rentabilidades (por ejemplo, el percentil 1 para un VaR al 99%) y \( V \) el valor de la cartera. El signo menos es para expresar la pérdida como un número positivo.
Los datos: una cartera de acciones, bonos y oro
Trabajaremos con una cartera clásica de 100.000 €: 60% en acciones de EE. UU. (SPY), 30% en bonos del Tesoro a largo plazo (TLT) y 10% en oro (GLD).
import numpy as np
import pandas as pd
import yfinance as yf
precios = yf.download(["SPY", "TLT", "GLD"], start="2012-01-01", end="2025-12-31",
auto_adjust=True)["Close"].dropna()from scipy.stats import norm
pesos = pd.Series({"SPY": 0.6, "TLT": 0.3, "GLD": 0.1})
V = 100_000
alpha = 0.99
rent = precios.pct_change().dropna()[pesos.index]
rent_cartera = rent @ pesos
print(f"{len(rent_cartera)} días de datos")Método 1: VaR histórico
El más intuitivo: se supone que el futuro se parecerá al pasado. Tomamos las rentabilidades diarias reales de la cartera, las ordenamos y buscamos el percentil 1.
var_hist = -np.percentile(rent_cartera, 100 * (1 - alpha)) * V
print(f"VaR histórico 99% 1 día: {var_hist:,.0f} €")- Ventajas: no supone ninguna distribución; captura las colas gruesas y las correlaciones reales tal como ocurrieron.
- Inconvenientes: depende totalmente de la ventana elegida. Si en tus datos no hay ninguna crisis, el VaR será optimista; y un día extremo entra y sale del cálculo de golpe.
Método 2: VaR paramétrico (varianza-covarianza)
Aquí se supone que las rentabilidades son normales. Entonces basta con la media y la volatilidad de la cartera, y el cuantil se obtiene de la normal:
\[ \text{VaR}_\alpha = -\left(\mu_p + z_{1-\alpha}\,\sigma_p\right) V, \qquad \sigma_p = \sqrt{w^\top \Sigma\, w} \]
donde \( w \) son los pesos, \( \Sigma \) la matriz de covarianzas y \( z_{0{,}01} \approx -2{,}326 \). Fíjate en el papel de las correlaciones: están dentro de \( \Sigma \), y son las que hacen que el riesgo de la cartera sea menor que la suma de los riesgos individuales.
mu_p = rent.mean() @ pesos
cov = rent.cov()
sigma_p = np.sqrt(pesos @ cov @ pesos)
var_param = -(mu_p + norm.ppf(1 - alpha) * sigma_p) * V
print(f"VaR paramétrico 99% 1 día: {var_param:,.0f} €")- Ventajas: rapidísimo, fácil de descomponer por activos y de explicar.
- Inconvenientes: la normal subestima las colas. Los días de −4 o −5 desviaciones típicas ocurren en los mercados mucho más de lo que la normal permite.
Método 3: VaR de Monte Carlo
El tercer método simula miles de escenarios para el día siguiente y mira el percentil de las pérdidas simuladas. Su gran ventaja es la flexibilidad: puedes usar cualquier modelo, incluidas carteras con opciones cuyo valor no es lineal en los precios.
Para simular varios activos correlacionados se usa la descomposición de Cholesky de la matriz de covarianzas: transforma números aleatorios independientes en otros con las correlaciones deseadas.
rng = np.random.default_rng(0)
n_sim = 200_000
L = np.linalg.cholesky(cov.values)
Z = rng.standard_normal((n_sim, len(pesos)))
sim = rent.mean().values + Z @ L.T # rentabilidades correlacionadas
sim_cartera = sim @ pesos.values
var_mc = -np.percentile(sim_cartera, 100 * (1 - alpha)) * V
print(f"VaR Monte Carlo 99% 1 día: {var_mc:,.0f} €")Con rentabilidades normales, el Monte Carlo reproduce el paramétrico (es una buena comprobación). La gracia está en cambiar el modelo: por ejemplo, simular con una t de Student, que tiene colas más gruesas.
nu = 4 # grados de libertad: colas gruesas
T = rng.standard_t(nu, size=(n_sim, len(pesos))) * np.sqrt((nu - 2) / nu)
sim_t = rent.mean().values + T @ L.T # misma covarianza, colas más gruesas
var_mc_t = -np.percentile(sim_t @ pesos.values, 1) * V
print(f"VaR Monte Carlo (t de Student) 99%: {var_mc_t:,.0f} €")El factor \( \sqrt{(\nu-2)/\nu} \) reescala la t para que tenga varianza 1 y así la volatilidad sea la misma; lo único que cambia es la forma de las colas.
Comparar los tres métodos
print(pd.Series({
"Histórico": var_hist,
"Paramétrico (normal)": var_param,
"Monte Carlo (normal)": var_mc,
"Monte Carlo (t de Student)": var_mc_t,
}).round(0))Con datos reales lo habitual es que el histórico salga por encima del paramétrico al 99%: es la huella de las colas gruesas. Al 95% las diferencias suelen ser pequeñas; cuanto más te adentras en la cola, más importa el supuesto de normalidad.
El problema del VaR: no dice cuánto pierdes cuando falla
El VaR dice dónde empieza el 1% peor, pero nada sobre lo grande que es ese 1%. Dos carteras con el mismo VaR pueden tener pérdidas extremas completamente distintas. Además, en algunos casos el VaR de una cartera diversificada puede salir mayor que la suma de los VaR de sus partes, lo que va contra la intuición de la diversificación.
Por eso la regulación bancaria actual (el marco FRTB de Basilea) ha pasado a usar el Expected Shortfall (ES, también llamado CVaR): la pérdida media en los días peores que el VaR.
\[ \text{ES}_\alpha = -\,\mathbb{E}\left[R \mid R \le q_{1-\alpha}(R)\right]\cdot V \]
umbral = np.percentile(rent_cartera, 1)
es_hist = -rent_cartera[rent_cartera <= umbral].mean() * V
print(f"VaR histórico 99%: {var_hist:,.0f} €")
print(f"ES histórico 99%: {es_hist:,.0f} €")
# Paramétrico (normal): fórmula cerrada
es_param = -(mu_p - sigma_p * norm.pdf(norm.ppf(0.01)) / 0.01) * V
print(f"ES paramétrico 99%: {es_param:,.0f} €")El ES siempre es mayor que el VaR con el mismo nivel de confianza, y la distancia entre ambos te dice lo gruesa que es la cola.
Del VaR a 1 día al VaR a 10 días
Si las rentabilidades diarias son independientes y con la misma distribución, la volatilidad crece con la raíz del tiempo, así que se suele escalar:
\[ \text{VaR}_{10\text{ días}} \approx \text{VaR}_{1\text{ día}} \cdot \sqrt{10} \]
print(f"VaR paramétrico 99% a 10 días ≈ {var_param * np.sqrt(10):,.0f} €")Es una aproximación: en las crisis la volatilidad se agrupa (días malos seguidos de días malos), y la regla de la raíz se queda corta.
Backtesting: ¿funciona tu VaR?
Un modelo de riesgo hay que ponerlo a prueba. La idea es sencilla: calcular el VaR de cada día solo con datos anteriores y contar cuántas veces la pérdida real lo supera (las excepciones). Con un VaR al 99%, deberían ser alrededor del 1% de los días.
ventana = 500
r = rent_cartera.values
excepciones = []
for t in range(ventana, len(r)):
var_t = -np.percentile(r[t - ventana:t], 1) # VaR histórico con datos pasados
excepciones.append(-r[t] > var_t)
excepciones = np.array(excepciones)
print(f"Días evaluados: {len(excepciones)}")
print(f"Excepciones: {excepciones.sum()} ({excepciones.mean():.2%}; esperado 1,00%)")Si salen muchas más del 1%, el modelo subestima el riesgo. Para decidir si la diferencia es significativa o mala suerte se usan tests estadísticos como el de Kupiec; y si las excepciones llegan en racha, el modelo no reacciona a los cambios de volatilidad. Esa es la motivación de los modelos GARCH, que dejan que la volatilidad cambie con el tiempo.
Caso real: LTCM, el fondo de los premios Nobel
Si hay una historia que explica por qué el riesgo de cola importa, es la de Long-Term Capital Management. En 1997, Robert Merton y Myron Scholes recibieron el Nobel por su trabajo sobre la valoración de opciones. Un año después, el hedge fund del que ambos eran socios perdió casi todo su capital en unos meses.
LTCM hacía arbitraje de convergencia: compraba un bono algo barato, vendía uno casi idéntico algo caro y esperaba a que la diferencia se cerrara. Esas diferencias eran minúsculas, así que para ganar un 40 % al año las multiplicaba con deuda. A principios de 1998 tenía unos 4.700 millones de dólares de capital y más de 125.000 millones en activos: un apalancamiento de 25 a 30 veces.
En agosto de 1998 Rusia dejó de pagar parte de su deuda, los inversores huyeron hacia los activos más seguros y líquidos, y todas las diferencias que LTCM esperaba que se cerraran se abrieron a la vez. Solo en agosto el fondo perdió en torno al 44 %. El 23 de septiembre, la Reserva Federal de Nueva York coordinó un rescate: catorce entidades aportaron 3.625 millones de dólares a cambio del 90 % del fondo.
Sus modelos de riesgo decían que una pérdida así era prácticamente imposible. La diferencia entre suponer rentabilidades normales y aceptar colas gruesas es de muchos órdenes de magnitud. Supón un fondo con una volatilidad diaria del 1,3 % (unos 20 % anuales): ¿cada cuánto perderá más de un 10 % en un solo día?
from scipy import stats
vol = 0.013 # volatilidad diaria del capital
z = -0.10 / vol # un día de -10 %
p_normal = stats.norm.cdf(z)
nu = 3 # t de Student: colas gruesas, misma volatilidad
p_t = stats.t.cdf(z * np.sqrt(nu / (nu - 2)), nu)
for nombre, p in [("Normal", p_normal), ("t de Student (ν=3)", p_t)]:
print(f"{nombre:<20} una vez cada {1 / p / 252:,.0f} años")Normal una vez cada 549,221,878,375 años t de Student (ν=3) una vez cada 9 años
Con la normal, un día así ocurriría una vez cada cientos de miles de millones de años; con colas gruesas y la misma volatilidad, aproximadamente una vez por década. A eso se sumaron las otras dos grandes lecciones de LTCM: en una crisis las correlaciones se van a 1 (todas sus apuestas dependían de lo mismo, de que la prima de liquidez bajara) y el riesgo de liquidez no aparece en el VaR: muchas de sus posiciones acabaron dando la razón al modelo… después del rescate.
Resumen
- El VaR es un percentil de las pérdidas: nivel de confianza, horizonte y valor de la cartera.
- Histórico (sin supuestos, depende de la ventana), paramétrico (rápido, subestima colas) y Monte Carlo (flexible, más costoso).
- El Expected Shortfall mide cuánto pierdes cuando el VaR falla y es hoy el estándar regulatorio.
- Un modelo de riesgo sin backtest es una opinión.
- El caso de LTCM resume lo que el VaR no ve: apalancamiento, colas gruesas, correlaciones que se disparan y liquidez.
Ya sabes medir el riesgo de una cartera. El siguiente paso es construirla: elegir los pesos que dan la mejor relación entre rentabilidad y riesgo con la optimización de Markowitz.