Value at Risk (VaR) en Python: histórico, paramétrico y Monte Carlo | Quantum Club

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
  1. Qué es el Value at Risk
  2. Los datos: una cartera de acciones, bonos y oro
  3. Método 1: VaR histórico
  4. Método 2: VaR paramétrico (varianza-covarianza)
  5. Método 3: VaR de Monte Carlo
  6. Comparar los tres métodos
  7. El problema del VaR: no dice cuánto pierdes cuando falla
  8. Del VaR a 1 día al VaR a 10 días
  9. Backtesting: ¿funciona tu VaR?
  10. Caso real: LTCM, el fondo de los premios Nobel
  11. 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).

Python
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()
Python
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.

Python
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.

Python
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.

Python
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.

Python
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

Python
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))
-5 % -2 % 0 % 3 % 5 % Distribución diaria -6 % -4 % -2 % Zoom en la cola izquierda Rentabilidades (colas gruesas) Normal con la misma volatilidad VaR 99 % histórico VaR 99 % normal
Rentabilidades con colas gruesas (simuladas con una t de Student) frente a una normal con la misma volatilidad. En la cola izquierda la normal se queda corta y su VaR al 99 % subestima la pérdida.

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 \]

Python
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} \]

Python
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.

Python
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.

-10 % -8 % -5 % -2 % 0 % 2 % 5 % 8 % 10 % Movimiento del valor de los activos -100 % -50 % 0 % 50 % 100 % Cambio en el capital propio Con 25:1, una caída del 4 % en los activos borra todo el capital Apalancamiento 1:1 Apalancamiento 5:1 Apalancamiento 25:1
Con apalancamiento L, un movimiento x en los activos se convierte en L·x en el capital. Con 25 a 1, basta con que los activos pierdan un 4 % para que el capital desaparezca.

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?

Python
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.

Quantum Pablo

Sobre Quantum Pablo

Físico de partículas y quant. Enseño finanzas cuantitativas en español desde la intuición y con código: más de 400 alumnos en más de 25 países se han formado en Quantum Club. También en YouTube e Instagram.

Etapa 2 de 4 · Fundamentos en Python

  1. 06Python para finanzas: rentabilidad, volatilidad, Sharpe y drawdown
  2. 07Simulación de Monte Carlo en finanzas con Python
  3. 08El modelo de Black-Scholes explicado desde la intuición (con Python)
  4. 09Value at Risk (VaR) y Expected Shortfall: los 3 métodos en Python
Ver el recorrido completo →