---
title: "4.2.3 VaR, Expected Shortfall y backtesting del VaR"
description: "VaR histórico, normal, EWMA, FHS, GARCH-t y Monte Carlo frente al VaR verdadero; Kupiec, Christoffersen, semáforo de Basilea y Acerbi-Székely."
url: "https://pythonparatrading.com/curso-python-trading/nivel-4/var-expected-shortfall-y-backtesting-del-var/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-4/var-expected-shortfall-y-backtesting-del-var.png"
---

# 4.2.3 · VaR, Expected Shortfall y backtesting del VaR

> El VaR resume en una cifra la pérdida que no debería superarse casi nunca, y el Expected Shortfall dice cuánto se pierde cuando se supera. Esta lección calcula los dos con seis métodos sobre un libro cuyo VaR verdadero se conoce cada día, los somete a las pruebas de Kupiec, Christoffersen y Acerbi-Székely y al semáforo del Comité de Basilea, y mide cuánto se equivoca cada uno y cuándo.

- Curso: [Nivel 4: Carteras, riesgo, ejecución y trading en real](https://pythonparatrading.com/curso-python-trading/nivel-4/)
- Módulo: 4.2 · Gestión del riesgo y tamaño de posición
- Duración: 75 min
- Requisitos: [1.6.2 · Hechos estilizados: colas gruesas, agrupamiento, autocorrelación y apalancamiento](https://pythonparatrading.com/curso-python-trading/nivel-1/hechos-estilizados-de-los-mercados/), [2.1.4 · Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch](https://pythonparatrading.com/curso-python-trading/nivel-2/volatilidad-arch-garch-egarch-gjr/), [2.5.5 · Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales](https://pythonparatrading.com/curso-python-trading/nivel-2/monte-carlo-bootstrap-y-permutaciones/), [4.2.2 · Criterio de Kelly y volatility targeting: cuánto arriesgar](https://pythonparatrading.com/curso-python-trading/nivel-4/kelly-y-volatility-targeting/)

## Al terminar esta lección

- Definir el VaR y el Expected Shortfall de un horizonte y un nivel de confianza, y explicar por qué el ES dice lo que el VaR calla.
- Calcular el VaR al 99 % histórico, normal, EWMA, por simulación histórica filtrada (FHS) y con un GJR-GARCH-t estimado con `arch`, sin anticipación, y compararlos con el verdadero.
- Estimar un VaR a 10 días por Monte Carlo con un GARCH y ver cuándo falla la regla de la raíz de 10.
- Aplicar las pruebas de Kupiec y de Christoffersen y el semáforo de Basilea (MAR32) a una serie de excesos, conociendo su potencia.
- Contrastar el ES al 97,5 % con el estadístico Z2 de Acerbi y Székely y calcular su valor crítico por simulación.

## Qué mide el VaR y qué no

El **VaR** (*value at risk*) de un libro a un horizonte $h$ y un nivel de confianza $1-\alpha$ es la pérdida que solo
se supera con probabilidad $\alpha$. Con $L$ la pérdida (positiva cuando se pierde) en ese horizonte:

$$\mathrm{VaR}_\alpha = \inf\{\ell : P(L > \ell) \le \alpha\}, \qquad
\mathrm{ES}_\alpha = \mathrm{E}\left[L \mid L \ge \mathrm{VaR}_\alpha\right].$$

El VaR es un cuantil: dice dónde empieza el 1 % peor, no cuánto se pierde allí. El **Expected Shortfall** (ES, también
CVaR o pérdida esperada en la cola) es la media de esas pérdidas. Dos libros con el mismo VaR pueden tener colas muy
distintas: uno que vende opciones lejos del dinero tiene un VaR pequeño y un ES enorme (lección 3.6.1). Además, el VaR
no es subaditivo (el VaR de una cartera puede superar la suma de los VaR de sus partes) y el ES sí lo es: es una
medida coherente en el sentido de Artzner, Delbaen, Eber y Heath (1999)[^1].

La regulación usa las dos. En el marco de Basilea para riesgo de mercado, el capital del modelo interno se calcula
con un ES al 97,5 % (MAR33.3) y el **backtesting**, la comprobación diaria del modelo contra la P&L, con un VaR al
99 % a un día (MAR32.5). Un sistema de trading propio no necesita capital regulatorio, pero sí las dos preguntas:
cuánto puedo perder en un mal día y si mi modelo de riesgo lo ha sabido decir.

```mermaid
flowchart LR
  P[P&L hasta ayer] --> M[Modelo]
  M --> V[VaR y ES de hoy]
  V --> C{Pérdida de hoy}
  C --> E[Exceso: sí o no]
  E --> T[Pruebas]
```

El orden importa: el VaR de hoy se calcula al cierre de ayer y se compara con la pérdida de hoy. Un VaR que use la
pérdida de hoy para calcular el de hoy pasa cualquier prueba y no sirve para nada.

## Un libro con el VaR verdadero conocido

`riesgo4_pyg.csv` es la P&L diaria de un libro de 10 millones de USD de nocional constante, 2006-2025. Es sintética y
tiene una distribución condicional conocida: un GJR-GARCH(1,1) con innovaciones t de Student tipificadas de 5 grados
de libertad, $a = 0{,}05$, $\gamma = 0{,}10$, $b = 0{,}88$, y una varianza de largo plazo que cambia con el régimen del
universo del nivel (calma, estrés, inflación). `riesgo4_pyg_verdad.csv` da la volatilidad verdadera de cada día,
conocida al cierre de la víspera, y el VaR y el ES verdaderos. La primera celda comprueba de dónde salen:

```python
import numpy as np
import pandas as pd
from scipy import stats

N = 10_000_000                       # nocional constante (USD)
pyg = pd.read_csv("riesgo4_pyg.csv", index_col="fecha",
                  parse_dates=True)
verdad = pd.read_csv("riesgo4_pyg_verdad.csv", index_col="fecha",
                     parse_dates=True)
r, perdida = pyg["rendimiento"], -pyg["pyg"]   # pérdida > 0

# cuantil y ES de la t de Student tipificada (varianza 1)
nu, mu = 5, 0.0003
esc = np.sqrt((nu - 2) / nu)
q01 = stats.t.ppf(0.01, nu) * esc
q025 = stats.t.ppf(0.025, nu) * esc
t025 = stats.t.ppf(0.025, nu)
es_t = (nu + t025**2) / (nu - 1) * stats.t.pdf(t025, nu) / 0.025
print(f"q 1 %: {q01:.4f}   ES 2,5 %: {es_t * esc:.4f}")
var99 = -(mu + verdad["sigma"] * q01) * N
es975 = (-mu + verdad["sigma"] * es_t * esc) * N
print("Diferencia máxima con el fichero (USD):",
      round(max((var99 - verdad["var_99"]).abs().max(),
                (es975 - verdad["es_975"]).abs().max()), 2))
verdad.loc["2020-03-16":"2020-03-18"].round(
    {"sigma": 5, "var_95": 0, "var_975": 0, "var_99": 0, "es_975": 0})
```
```output
q 1 %: -2.6065   ES 2,5 %: 2.7278
Diferencia máxima con el fichero (USD): 1.37
              sigma    var_95   var_975    var_99    es_975
fecha
2020-03-16  0.01155  177307.0  227016.0  298095.0  312112.0
2020-03-17  0.01205  185123.0  236988.0  311147.0  325772.0
2020-03-18  0.01174  180195.0  230700.0  302917.0  317159.0
```

El cuantil al 1 % de una t de 5 grados tipificada a varianza 1 es −2,6065, un 12 % más lejos de cero que el de la
normal (−2,3263), y su ES al 2,5 %, 2,7278. Con $\mathrm{VaR}_{99} = -(\mu + \sigma_t q_{0{,}01})\cdot N$ las cifras del fichero se
reproducen salvo redondeos de 1,37 USD. En la semana del 16 de marzo de 2020 la volatilidad diaria verdadera es del
1,2 % y el VaR al 99 %, de unos 300.000 USD. Tener la verdad permite algo que con datos reales es imposible: medir el
error de cada método día a día, no solo contar excesos.

## Seis VaR con lo que se sabía cada día

Los métodos habituales, todos calculados con datos hasta el cierre de la víspera (`shift(1)`):

- **Histórico**: el cuantil al 1 % de los 250 últimos rendimientos. No supone ninguna distribución, pero da el mismo
  peso a la sesión de ayer que a la de hace once meses.
- **Normal**: $-\hat\sigma_{250}\, z_{0{,}01}$ con la desviación típica de 250 sesiones. Suponer normalidad infravalora
  las colas.
- **EWMA** (RiskMetrics): $\hat\sigma^2_t = \lambda\hat\sigma^2_{t-1} + (1-\lambda)r^2_{t-1}$ con $\lambda = 0{,}94$,
  el valor del documento técnico de RiskMetrics para datos diarios, y cuantil normal.
- **FHS** (simulación histórica filtrada, Barone-Adesi et al.): se dividen los rendimientos por la volatilidad EWMA de
  su víspera, se toma el cuantil de esos residuos en 250 sesiones y se reescala con la volatilidad de hoy. Combina la
  reacción del EWMA con la forma empírica de las colas.
- **GJR-GARCH-t** con `arch` (lección 2.1.4): se reestima cada enero con todo el pasado y se usa con los parámetros
  fijos durante el año. Es el modelo verdadero, pero con los parámetros estimados.
- **Verdadero**: el del fichero, la referencia.

```python
import warnings
from arch import arch_model

V = 250                                  # ventana de un año
z01 = stats.norm.ppf(0.01)
lam = 0.94                               # RiskMetrics, diario

sig_ewma = np.sqrt((r**2).ewm(alpha=1 - lam, adjust=False).mean())
res_ewma = r / sig_ewma.shift(1)         # residuos filtrados
var = pd.DataFrame({
    "verdadero": verdad["var_99"],
    "historico": -r.rolling(V).quantile(0.01).shift(1) * N,
    "normal": -(r.rolling(V).std() * z01).shift(1) * N,
    "ewma": -(sig_ewma * z01).shift(1) * N,
    "fhs": -(sig_ewma.shift(1)
             * res_ewma.rolling(V).quantile(0.01).shift(1)) * N})


def garch_t(r, inicio=V):
    """GJR-GARCH(1,1)-t reestimado cada enero con todo el pasado."""
    y = 100 * r                           # arch prefiere porcentajes
    out, params = [], {}
    for año in sorted(set(r.index.year)):
        n = int((r.index.year < año).sum())
        if n < inicio:
            continue
        am = arch_model(y, mean="Constant", p=1, o=1, q=1, dist="t")
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", RuntimeWarning)
            fit = am.fit(last_obs=n, disp="off")
        prev = fit.forecast(start=n - 1, reindex=False).variance
        dias = r.index[r.index.year == año]
        s = np.sqrt(prev["h.1"]).shift(1).reindex(dias) / 100
        out.append(pd.DataFrame({"sigma": s, "nu": fit.params["nu"],
                                 "mu": fit.params["mu"] / 100}))
        params[año] = fit.params
    return pd.concat(out), params


gt, params = garch_t(r)
q_gt = stats.t.ppf(0.01, gt["nu"]) * np.sqrt((gt["nu"] - 2) / gt["nu"])
var["garch_t"] = -(gt["mu"] + gt["sigma"] * q_gt) * N
print("Ajuste de 2020:", params[2020].round(3).to_dict())

prueba = gt.index                          # de 2007 a 2025
var = var.loc[prueba]
L = perdida.loc[prueba]
excesos = L.values[:, None] > var.values
pd.DataFrame({"excesos": excesos.sum(axis=0),
              "pct": 100 * excesos.mean(axis=0),
              "var_medio": var.mean(),
              "error_medio_abs": (var.sub(var["verdadero"], axis=0)
                                  ).abs().mean(),
              "sesgo": var.sub(var["verdadero"], axis=0).mean()},
             index=var.columns).round(2)
```
```output
Ajuste de 2020: {'mu': 0.036, 'omega': 0.009, 'alpha[1]': 0.061, 'gamma[1]': 0.097, 'beta[1]': 0.887, 'nu': 4.71}
           excesos   pct  var_medio  error_medio_abs     sesgo
verdadero       49  1.03  218634.57             0.00      0.00
historico       94  1.97  242182.61        105831.93  23548.03
normal         112  2.34  215419.30         94591.87  -3215.27
ewma            78  1.63  196732.87         36019.11 -21901.70
fhs             69  1.44  226313.08         45680.86   7678.51
garch_t         55  1.15  217830.13         16162.03   -804.45
```

*Fig. — Frecuencia de excesos del VaR al 99 % a un día de seis métodos sobre el mismo libro, 2007-2025 (4 780 sesiones); la línea discontinua es el 1 % que se espera de un VaR correcto.* Barras horizontales: Verdadero 1,03 % (49); Histórico 1,97 % (94); Normal 2,34 % (112); EWMA 0,94 1,63 % (78); FHS 1,44 % (69); GARCH-t 1,15 % (55). Solo el verdadero y el GARCH-t estimado (destacado) quedan cerca del 1 %; el normal y el histórico tienen el doble de excesos.

Con 4.780 sesiones de prueba (2007-2025) se esperan 48 excesos de un VaR al 99 % correcto. El verdadero tiene 49. El
GARCH-t estimado, 55 (1,15 %), y se queda de media a **16.200 USD** del verdadero, con un sesgo casi nulo: sus
parámetros de 2020 ($a = 0{,}061$, $\gamma = 0{,}097$, $b = 0{,}887$, $\nu = 4{,}7$) están cerca de los verdaderos. El
EWMA reacciona igual de deprisa (error medio de 36.000 USD) pero con cuantil normal: 78 excesos y un sesgo de −21.900
USD. El FHS corrige la cola (69 excesos). Y los dos métodos de ventana fija fallan por mucho: el normal tiene 112
excesos y el histórico 94, con un error medio de 105.800 USD frente al verdadero **aunque de media esté 23.500 USD por
encima**. Estar por encima de media no basta si se está alto cuando no hace falta y bajo cuando sí.

La figura de la cabecera lo enseña en el episodio de estrés de 2010. El VaR verdadero pasa de unos 150.000 a 1,34
millones de USD entre mayo y septiembre. El histórico sigue en 145.000-160.000 durante mayo y junio, sube a escalones
a medida que las pérdidas entran en su ventana y acumula 15 excesos en ese tramo. Después se queda en 664.000 USD
todo el invierno, cuando el riesgo verdadero ya ha vuelto a la mitad o menos: un año de capital inmovilizado de más
detrás de cada crisis.

## Monte Carlo: el VaR a varios días

Para un día, el VaR de un GARCH tiene fórmula. Para varios días no: la volatilidad de pasado mañana depende del
choque de mañana, y la suma de diez rendimientos GARCH-t no tiene una distribución conocida. La salida es simular
muchos caminos con el modelo y tomar el cuantil de la suma. La alternativa habitual, la **regla de la raíz de 10**
($\mathrm{VaR}_{10} \approx \sqrt{10}\,\mathrm{VaR}_1$), supone rendimientos independientes con la misma volatilidad
los diez días. MAR33.4 parte de un ES a 10 días para escalar a horizontes de liquidez más largos, así que la pregunta
no es académica.

La celda simula 50.000 caminos de 10 días desde tres fechas, con el modelo verdadero (con la varianza de largo plazo
del régimen de ese día) y con el GARCH-t estimado ese año:

```python
def mc_suma(mu, omega, a, gam, b, nu, sig1, h=10, n=50_000,
            semilla=3):
    """Simula h días de un GJR-GARCH-t desde sigma de mañana."""
    rng = np.random.default_rng(semilla)
    s2 = np.full(n, sig1**2)
    total = np.zeros(n)
    for _ in range(h):
        z = rng.standard_t(nu, n) * np.sqrt((nu - 2) / nu)
        eps = np.sqrt(s2) * z
        total += mu + eps
        s2 = omega + (a + gam * (eps < 0)) * eps**2 + b * s2
    return total


fac = pd.read_csv("universo4_regimenes.csv",
                  parse_dates=["desde", "hasta"])
v_reg = {"calma": 0.008, "estres": 0.020, "inflacion": 0.012}
filas = {}
for dia in ["2017-06-01", "2019-11-01", "2008-10-15"]:
    t0 = pd.Timestamp(dia)
    reg = fac[(fac.desde <= t0) & (fac.hasta >= t0)].regimen.item()
    om_v = (1 - 0.05 - 0.88 - 0.1 / 2) * v_reg[reg] ** 2
    sig_v = verdad.at[t0, "sigma"]        # sigma de t0, conocida antes
    sig_g = gt.at[t0, "sigma"]
    p = params[t0.year]                   # el ajuste de ese año (en %)
    tot_v = mc_suma(mu, om_v, 0.05, 0.1, 0.88, 5, sig_v)
    tot_g = mc_suma(p["mu"] / 100, p["omega"] / 1e4, p["alpha[1]"],
                    p["gamma[1]"], p["beta[1]"], p["nu"], sig_g)
    real = -r.loc[t0:].iloc[:10].sum() * N
    filas[dia] = {"regimen": reg,
                  "raiz10_verdad": np.sqrt(10)
                  * verdad.at[t0, "var_99"],
                  "mc_verdad": -np.quantile(tot_v, 0.01) * N,
                  "mc_garch": -np.quantile(tot_g, 0.01) * N,
                  "perdida_real": real}
tabla = pd.DataFrame(filas).T
tabla.iloc[:, 1:] = tabla.iloc[:, 1:].astype(float).round(-3)
tabla
```
```output
           regimen raiz10_verdad mc_verdad   mc_garch perdida_real
2017-06-01   calma      966000.0  949000.0  1066000.0    -545000.0
2019-11-01   calma      441000.0  452000.0   482000.0    -326000.0
2008-10-15  estres      858000.0  945000.0   597000.0    -358000.0
```

- **En estrés** (15 de octubre de 2008), el Monte Carlo verdadero da 945.000 USD y la raíz de 10, 858.000: con
  $\gamma > 0$ un choque negativo sube la volatilidad del día siguiente más que uno positivo, y las pérdidas se
  encadenan. La raíz de 10 se queda corta justo cuando más importa.
- **En calma** las dos se parecen (966.000 frente a 949.000 en junio de 2017; 441.000 frente a 452.000 en noviembre de
  2019).
- **El GARCH estimado** con un solo régimen se equivoca en los dos sentidos: en la calma su volatilidad vuelve hacia
  una media de largo plazo que mezcla crisis y calma y sobrestima (1.066.000 y 482.000); en octubre de 2008, con
  parámetros estimados solo con 2006-2007, no conoce el nivel del estrés y da 597.000, un 37 % menos que la verdad.

En las tres fechas el libro acabó ganando dinero en los diez días siguientes (la columna `perdida_real` es negativa):
un VaR es una afirmación sobre una distribución y una fecha no lo confirma ni lo desmiente. Por eso los modelos se
juzgan con muchas fechas.

## Kupiec, Christoffersen y la agrupación de los excesos

Cada día se apunta $I_t = 1$ si la pérdida supera al VaR. Con un VaR correcto, las $I_t$ son independientes y valen 1
con probabilidad $p = 1\,\%$. Dos pruebas de razón de verosimilitudes, las dos $\chi^2$:

$$LR_{uc} = -2\ln\frac{(1-p)^{n-x}p^{x}}{(1-\hat p)^{n-x}\hat p^{x}} \sim \chi^2_1,
\qquad LR_{cc} = LR_{uc} + LR_{ind} \sim \chi^2_2.$$

La de **Kupiec** (cobertura incondicional, $LR_{uc}$) compara la frecuencia observada $\hat p = x/n$ con $p$. La de
**Christoffersen** (independencia, $LR_{ind}$) ajusta una cadena de Markov a las $I_t$ y compara la probabilidad de un
exceso tras un día sin exceso ($\pi_{01}$) con la de tras un día con exceso ($\pi_{11}$). Juntas dan la cobertura
condicional:

```python
from scipy.stats import chi2


def kupiec(I, p=0.01):
    """Cobertura incondicional: ¿la frecuencia de excesos es p?"""
    n, x = len(I), int(I.sum())
    ph = x / n
    lr = -2 * ((n - x) * np.log(1 - p) + x * np.log(p)
               - (n - x) * np.log(1 - ph) - x * np.log(ph))
    return lr, chi2.sf(lr, 1)


def christoffersen(I):
    """Independencia: ¿un exceso hace más probable el siguiente?"""
    a, b = I[:-1], I[1:]
    n00 = ((a == 0) & (b == 0)).sum()
    n01 = ((a == 0) & (b == 1)).sum()
    n10 = ((a == 1) & (b == 0)).sum()
    n11 = ((a == 1) & (b == 1)).sum()
    p01, p11 = n01 / (n00 + n01), n11 / (n10 + n11)
    p1 = (n01 + n11) / (n00 + n01 + n10 + n11)
    ll0 = (n00 + n10) * np.log(1 - p1) + (n01 + n11) * np.log(p1)
    ll1 = (n00 * np.log(1 - p01) + n01 * np.log(p01)
           + n10 * np.log(1 - p11)
           + (n11 * np.log(p11) if n11 else 0))
    lr = -2 * (ll0 - ll1)
    return lr, chi2.sf(lr, 1), p11


filas = {}
for j, m in enumerate(var.columns):
    I = excesos[:, j].astype(int)
    lr_uc, p_uc = kupiec(I)
    lr_ind, p_ind, p11 = christoffersen(I)
    filas[m] = {"excesos": I.sum(), "p_kupiec": p_uc,
                "p_indep": p_ind, "p_exceso_tras_exceso": p11,
                "p_condicional": chi2.sf(lr_uc + lr_ind, 2)}
pd.DataFrame(filas).T.round(3)
```
```output
           excesos  p_kupiec  p_indep  p_exceso_tras_exceso  p_condicional
verdadero     49.0     0.862    0.314                 0.000          0.593
historico     94.0     0.000    0.000                 0.096          0.000
normal       112.0     0.000    0.000                 0.089          0.000
ewma          78.0     0.000    0.798                 0.013          0.000
fhs           69.0     0.004    0.997                 0.014          0.015
garch_t       55.0     0.307    0.164                 0.036          0.225
```

El verdadero pasa las tres (p de 0,862, 0,314 y 0,593) y nunca tiene dos excesos seguidos. El GARCH-t estimado también
pasa (0,307, 0,164, 0,225). El histórico y el normal fallan las dos: tienen demasiados excesos y, tras un exceso, la
probabilidad de otro al día siguiente es del 9,6 % y del 8,9 %, nueve veces la que debería. El EWMA y el FHS fallan
solo la de Kupiec: reaccionan bien (independencia 0,798 y 0,997) pero con la cola equivocada.

## El semáforo de Basilea

El supervisor bancario no usa p-valores, sino un **semáforo** sobre el número de excesos del VaR al 99 % en los
últimos 250 días hábiles (MAR32.9, tabla 1). Cada zona lleva un multiplicador del capital por backtesting:

| Zona | Excesos en 250 días | Multiplicador | Probabilidad con un modelo correcto |
|---|---|---|---|
| Verde | 0 a 4 | 1,50 | 89,2 % |
| Ámbar | 5, 6, 7, 8, 9 | 1,70, 1,76, 1,83, 1,88, 1,92 | 10,8 % |
| Rojo | 10 o más | 2,00 | 0,03 % |

En rojo, el supervisor aumenta el multiplicador automáticamente o puede retirar el modelo (MAR32.15). La última
columna sale de una binomial de 250 días con $p = 1\,\%$, en la celda:

```python
conteo = pd.DataFrame(excesos, index=prueba,
                      columns=var.columns).rolling(250).sum()
conteo = conteo.dropna()
zona = pd.cut(conteo.stack(), [-1, 4, 9, 250],
              labels=["verde", "ámbar", "rojo"])
reparto = (zona.groupby(level=1, observed=False)
           .value_counts(normalize=True).unstack()
           .reindex(var.columns) * 100)
print(reparto.round(1))

# con un modelo correcto: binomial(250, 1 %)
b = stats.binom(250, 0.01)
print(f"\nModelo correcto: verde {b.cdf(4):.1%}, "
      f"ámbar {b.cdf(9) - b.cdf(4):.1%}, rojo {b.sf(9):.2%}")


def multiplicador(n):                     # MAR32, tabla 1
    return {5: 1.70, 6: 1.76, 7: 1.83, 8: 1.88,
            9: 1.92}.get(n, 1.50 if n < 5 else 2.00)


peor = conteo["historico"].idxmax()
print(f"\nPeor ventana del histórico: 250 días hasta {peor.date()}")
print(pd.DataFrame({"excesos": conteo.loc[peor].astype(int),
                    "multiplicador": conteo.loc[peor].astype(int)
                    .map(multiplicador)}).to_string())
```
```output
           verde  ámbar  rojo
verdadero   88.0   12.0   0.0
historico   54.2   36.3   9.5
normal      44.8   30.2  25.0
ewma        58.8   41.2   0.0
fhs         74.8   25.2   0.0
garch_t     88.2   11.8   0.0

Modelo correcto: verde 89.2%, ámbar 10.8%, rojo 0.03%

Peor ventana del histórico: 250 días hasta 2010-09-29
           excesos  multiplicador
verdadero        4           1.50
historico       15           2.00
normal          16           2.00
ewma             6           1.76
fhs              5           1.70
garch_t          4           1.50
```

*Fig. — Semáforo de Basilea (MAR32): excesos del VaR al 99 % en los 250 días anteriores, para el VaR histórico y el verdadero del libro, 2007-2025.* Dos escalones. El VaR verdadero se mueve entre 0 y 7 excesos y pasa el 88 % del tiempo en verde (0 a 4), como se espera de un modelo correcto. El histórico (destacado) sube tras cada crisis: llega a 15 excesos en 2010 y pasa el 9,5 % del tiempo en rojo (10 o más) y el 36,3 % en ámbar.

El verdadero pasa el 88,0 % de los días en verde y el 12,0 % en ámbar, lo que se espera de un modelo correcto. El
GARCH-t estimado, lo mismo (88,2 % y 11,8 %). El histórico pasa el 9,5 % del tiempo en rojo y el normal, el 25,0 %.
El EWMA nunca llega a rojo pero pasa el 41,2 % del tiempo en ámbar. En la peor ventana del histórico, los 250 días
hasta el 29 de septiembre de 2010, el histórico tiene 15 excesos y el normal 16, los dos con multiplicador 2,00,
mientras que el verdadero y el GARCH-t tienen 4 (verde).

<aside class="callout" data-kind="aviso">

Un año de datos da poca información. Con un modelo correcto se espera un 10,8 % de años en ámbar, y un modelo que
tenga el doble de excesos de los debidos (2 % en lugar de 1 %) queda en verde con frecuencia (ejercicio 2). Por eso
el semáforo es una regla de supervisión, no una prueba estadística fuerte: para un sistema propio conviene mirar la
serie completa, la agrupación de los excesos y el ES.

</aside>

## Expected Shortfall y su backtest

Durante años se repitió que el ES no se podía contrastar porque no es «elicitable». Acerbi y Székely (2014) mostraron
que sí, con estadísticos que solo necesitan guardar cada día el VaR, el ES y la P&L. El segundo de ellos, con $X_t$ la
P&L (negativa cuando se pierde) e $I_t = 1[X_t + \mathrm{VaR}_t < 0]$ al nivel $\alpha = 2{,}5\,\%$:

$$
Z_2 = \sum_{t=1}^{T} \frac{X_t\, I_t}{T\,\alpha\,\mathrm{ES}_t} + 1.
$$

Con un ES correcto, $\mathrm{E}[Z_2] = 0$; si el modelo infravalora la cola, $Z_2 < 0$. El README de los datos da el
mismo estadístico con las pérdidas en positivo, con el signo cambiado. Para cada método, el ES de su propio modelo:
la media de la cola de 250 datos (histórico), la fórmula de la normal $\sigma\,\varphi(z_\alpha)/\alpha$, la de la t
tipificada para el GARCH-t y la media de la cola de los residuos filtrados (FHS):

```python
a = 0.025
za = stats.norm.ppf(a)
es_norm = stats.norm.pdf(za) / a          # ES de la normal tipificada


def es_historico(x):
    cola = np.sort(x)[: int(np.ceil(a * len(x)))]
    return -cola.mean()


tg = stats.t.ppf(a, gt["nu"])
es_tg = ((gt["nu"] + tg**2) / (gt["nu"] - 1) * stats.t.pdf(tg, gt["nu"])
         / a * np.sqrt((gt["nu"] - 2) / gt["nu"]))
sig_n = r.rolling(V).std().shift(1)
cola_fhs = res_ewma.rolling(V).apply(es_historico, raw=True).shift(1)
modelos = {
    "verdadero": (verdad["var_975"], verdad["es_975"]),
    "historico": (-r.rolling(V).quantile(a).shift(1) * N,
                  r.rolling(V).apply(es_historico, raw=True)
                  .shift(1) * N),
    "normal": (-sig_n * za * N, sig_n * es_norm * N),
    "ewma": (-sig_ewma.shift(1) * za * N,
             sig_ewma.shift(1) * es_norm * N),
    "fhs": (-sig_ewma.shift(1) * res_ewma.rolling(V).quantile(a)
            .shift(1) * N, sig_ewma.shift(1) * cola_fhs * N),
    "garch_t": ((-(gt["mu"] + gt["sigma"] * tg
                   * np.sqrt((gt["nu"] - 2) / gt["nu"]))) * N,
                (-gt["mu"] + gt["sigma"] * es_tg) * N)}
X = pyg["pyg"].loc[prueba]                 # P&L (negativo = pérdida)


def z2(X, var_a, es_a):
    """Acerbi-Székely (2014): ≈ 0 si el ES es correcto; < 0 si no."""
    I = (X + var_a < 0)
    return (X * I / (len(X) * a * es_a)).sum() + 1


filas = {}
for m, (v, e) in modelos.items():
    v, e = v.loc[prueba], e.loc[prueba]
    filas[m] = {"es_medio": e.mean(),
                "excesos_975_pct": 100 * (X + v < 0).mean(),
                "Z2": z2(X, v, e)}
tabla_es = pd.DataFrame(filas).T
tabla_es.round(3)
```
```output
             es_medio  excesos_975_pct     Z2
verdadero  228952.309            2.490  0.004
historico  245963.360            3.703 -0.691
normal     216480.024            3.619 -0.835
ewma       197701.580            3.075 -0.401
fhs        238706.681            2.908 -0.229
garch_t    228438.812            2.573 -0.067
```

Acerbi y Székely dan para $Z_2$ valores críticos casi constantes entre distribuciones, alrededor de −0,7 al 5 % para
un año de datos. Con el modelo verdadero de los datos se puede calcular el valor crítico exacto, también para toda la
muestra:

```python
rng = np.random.default_rng(5)
sig = verdad["sigma"].loc[prueba].values
v_v = verdad["var_975"].loc[prueba].values
e_v = verdad["es_975"].loc[prueba].values
sim_total, sim_anual = [], []
for _ in range(1000):
    z = rng.standard_t(nu, len(sig)) * esc
    Xs = (mu + sig * z) * N
    I = Xs + v_v < 0
    sim_total.append((Xs * I / (len(Xs) * a * e_v)).sum() + 1)
    k = 250                                  # una ventana de un año
    sim_anual.append((Xs[:k] * I[:k] / (k * a * e_v[:k])).sum() + 1)
print(f"Z2 crítico al 5 % con {len(sig)} días: "
      f"{np.quantile(sim_total, 0.05):.3f}")
print(f"Z2 crítico al 5 % con 250 días:  "
      f"{np.quantile(sim_anual, 0.05):.3f}")
```
```output
Z2 crítico al 5 % con 4780 días: -0.160
Z2 crítico al 5 % con 250 días:  -0.691
```

Con 250 días el valor crítico es −0,691, el del artículo. Con los 4.780 días de la prueba es −0,160: con más datos se
detectan desviaciones más pequeñas. El verdadero da 0,004 y el GARCH-t −0,067: pasan. El FHS (−0,229), el EWMA
(−0,401), el histórico (−0,691) y el normal (−0,835) infravaloran la cola y se rechazan. El normal tiene la peor cola:
con un ES medio de 216.500 USD, un 5 % por debajo del verdadero, tiene un 3,62 % de excesos de su VaR al 97,5 % en lugar
del 2,5 %, y cuando falla falla por más. El ES histórico medio (246.000 USD) está por encima del verdadero y aun así se
rechaza, por la misma razón que su VaR: está alto cuando no hace falta.

## Lo que el backtest no ve

- **Posiciones que cambian.** Este libro tiene nocional constante. En uno real, la P&L de hoy viene de posiciones que
  el VaR de ayer no conocía si se operó durante el día: por eso MAR32 compara con la P&L real y con la hipotética (las
  posiciones del cierre de ayer revaloradas), y cuenta el mayor número de excesos de las dos.
- **Escenarios que no están en la muestra.** Un VaR histórico de 250 sesiones solo conoce el último año; un GARCH
  estimado en la calma no conoce el estrés (el Monte Carlo de octubre de 2008). Las pérdidas que nunca han ocurrido
  las cubren las pruebas de estrés de la lección 4.2.4.
- **La agregación.** El VaR de una cartera depende de las correlaciones, que cambian en las crisis (4.2.4). Un modelo
  que acierte el VaR de cada estrategia por separado puede fallar el de la suma.
- **El uso del VaR como límite.** Si un sistema reduce posiciones cuando sube su VaR, el VaR afecta a la P&L que lo
  juzga. Eso es deseable, pero hay que tenerlo en cuenta al leer el backtest de un sistema que lo usa.

[^1]: Artzner, P., Delbaen, F., Eber, J.-M. y Heath, D. (1999): Coherent Measures of Risk. Mathematical Finance 9(3),
    203-228, <https://doi.org/10.1111/1467-9965.00068>. Barone-Adesi, G., Giannopoulos, K. y Vosper, L. (1999): VaR
    without correlations for portfolios of derivative securities. Journal of Futures Markets 19(5), 583-602,
    <https://doi.org/10.1002/(SICI)1096-9934(199908)19:5%3C583::AID-FUT5%3E3.0.CO;2-S>.

## Ejercicios

1. **RiskMetrics para inversores.** El documento técnico de RiskMetrics proponía $\lambda = 0{,}94$ para datos diarios
   de trading y $\lambda = 0{,}97$ para horizontes de inversión. Calcula el VaR EWMA al 99 % con 0,97 y con un cuantil t
   de 5 grados tipificado en lugar del normal, y compara excesos, p de Kupiec y p de independencia con el EWMA normal de
   0,94.

**Solución.**

```python
sig97 = np.sqrt((r**2).ewm(alpha=0.03, adjust=False).mean())
variantes = {"0,94 normal": -(sig_ewma * z01).shift(1) * N,
             "0,97 normal": -(sig97 * z01).shift(1) * N,
             "0,94 t5": -(sig_ewma * q01).shift(1) * N,
             "0,97 t5": -(sig97 * q01).shift(1) * N}
filas = {}
for k, v in variantes.items():
    I = (L > v.loc[prueba]).astype(int).values
    filas[k] = {"excesos": I.sum(), "p_kupiec": kupiec(I)[1],
                "p_indep": christoffersen(I)[1]}
pd.DataFrame(filas).T.round(3)
```
```output
             excesos  p_kupiec  p_indep
0,94 normal     78.0     0.000    0.798
0,97 normal     77.0     0.000    0.524
0,94 t5         54.0     0.377    0.267
0,97 t5         48.0     0.977    0.324
```

Cambiar la λ casi no cambia nada (78 a 77 excesos): el problema del EWMA normal no es la velocidad, es la cola.
Con el cuantil de la t de 5 grados, el EWMA de 0,94 baja a 54 excesos (p de Kupiec 0,377) y el de 0,97 a 48,
exactamente los esperados (p 0,977), y los dos siguen pasando la prueba de independencia. Un EWMA con colas gruesas
se acerca al GARCH-t sin estimar nada; aquí ayuda que el cuantil elegido sea el verdadero de las innovaciones, cosa
que con datos reales habría que estimar.

2. **La potencia del semáforo.** Calcula, con una binomial de 250 días, la probabilidad de que un modelo cuyos excesos
   ocurren de verdad con un 2 % y con un 3 % de probabilidad quede en verde, en ámbar y en rojo. ¿Qué dice esto de un
   año en verde?

**Solución.**

```python
zonas = {}
for p_real in (0.01, 0.02, 0.03):
    bb = stats.binom(250, p_real)
    zonas[p_real] = {"verde": bb.cdf(4),
                     "ambar": bb.cdf(9) - bb.cdf(4),
                     "rojo": bb.sf(9)}
pd.DataFrame(zonas).T.round(3)
```
```output
      verde  ambar   rojo
0.01  0.892  0.108  0.000
0.02  0.439  0.531  0.030
0.03  0.128  0.651  0.221
```

Un modelo correcto queda en verde el 89,2 % de los años. Uno cuyos excesos ocurren el doble de veces (2 %) aún queda
en verde el 43,9 % de los años y solo llega a rojo un 3,0 %; con el triple (3 %), en verde el 12,8 %. Un año en verde
es compatible con un modelo que infravalora el riesgo a la mitad: la prueba tiene poca potencia con 250 datos. Un
año en ámbar tampoco condena a un modelo correcto (10,8 %). Por eso los resultados se acumulan durante varios años y
se miran junto con la agrupación de los excesos y el ES.

3. **Un VaR para un sistema propio.** Tienes un sistema con tres estrategias y quieres un VaR al 99 % a un día para
   fijar un límite de pérdidas diario. ¿Qué método elegirías entre los de la lección, con qué datos lo estimarías y qué
   comprobarías cada mes?

**Solución.**

Un modelo que reaccione a la volatilidad reciente con colas gruesas: un GARCH-t o un FHS sobre la P&L agregada del
sistema (no sobre cada estrategia por separado, para que las correlaciones entren solas), reestimado con todo el
pasado cada pocos meses y calculado al cierre con datos hasta ese cierre. La P&L debe ser la del sistema con sus
posiciones de cada día; si las estrategias son nuevas, la de su backtest, sabiendo que el backtest no tiene los costes
ni los errores del real (lección 4.5.3). Cada mes: número de excesos y su agrupación (Kupiec y Christoffersen con todo
el historial, no solo el mes), el semáforo de 250 días, el Z2 del ES y una lista de cada exceso con su explicación,
como pide MAR32.12. Y, aparte, las pruebas de estrés de la lección 4.2.4 para lo que el VaR no ve.

## Autoevaluación

1. Un VaR al 99 % a un día de 200.000 USD significa que...

   - A. La pérdida máxima posible mañana es 200.000 USD
   - B. Con un modelo correcto, la pérdida de mañana supera 200.000 USD con una probabilidad del 1 %; el VaR no dice cuánto se pierde ese día
   - C. Se pierden 200.000 USD uno de cada cien días
   - D. La pérdida media de los peores días es 200.000 USD

   *Respuesta B: Con un modelo correcto, la pérdida de mañana supera 200.000 USD con una probabilidad del 1 %; el VaR no dice cuánto se pierde ese día.* El VaR es un cuantil: marca la frontera del 1 % peor, no su tamaño. Lo que hay más allá lo mide el Expected Shortfall, la pérdida media condicionada a superar el VaR.

2. El VaR histórico de 250 sesiones tiene un 1,97 % de excesos en los datos y su error medio frente al verdadero es de 105.800 USD, aunque en media está por encima del verdadero. ¿Por qué falla?

   - A. Porque usa la distribución normal
   - B. Porque la ventana es demasiado larga para tener suficientes datos
   - C. Porque reacciona tarde: no sube hasta que las pérdidas de la crisis entran en su ventana y no baja hasta que salen, un año después; los excesos se agrupan al principio de cada crisis
   - D. Porque el libro tiene media positiva

   *Respuesta C: Porque reacciona tarde: no sube hasta que las pérdidas de la crisis entran en su ventana y no baja hasta que salen, un año después; los excesos se agrupan al principio de cada crisis.* Un cuantil de 250 datos iguales en peso no distingue la volatilidad de ayer de la de hace once meses. Por eso el VaR histórico falla la prueba de independencia de Christoffersen (p 0,000): un exceso hace más probable el siguiente.

3. Un banco tiene 7 excesos de su VaR al 99 % en los últimos 250 días. Según la tabla 1 de MAR32, ¿qué ocurre?

   - A. Nada: está en verde
   - B. Está en rojo y pierde el modelo interno
   - C. El supervisor le obliga a cambiar a un VaR histórico
   - D. Está en zona ámbar y su multiplicador por backtesting pasa de 1,50 a 1,83

   *Respuesta D: Está en zona ámbar y su multiplicador por backtesting pasa de 1,50 a 1,83.* Verde de 0 a 4 excesos (1,50), ámbar de 5 a 9 (1,70, 1,76, 1,83, 1,88 y 1,92) y rojo con 10 o más (2,00). Con un modelo correcto la zona ámbar sale un 10,8 % de las veces: no prueba que el modelo esté mal, pero lo encarece.

4. ¿Qué comprueba la prueba de Christoffersen que no comprueba la de Kupiec?

   - A. Que los excesos no se agrupen: la probabilidad de un exceso no debe depender de si ayer hubo otro
   - B. Que el número de excesos sea el esperado
   - C. Que la pérdida media de los excesos sea la correcta
   - D. Que el VaR sea mayor que el ES

   *Respuesta A: Que los excesos no se agrupen: la probabilidad de un exceso no debe depender de si ayer hubo otro.* Kupiec mira solo la frecuencia. Un VaR puede acertar el 1 % de media y fallar en racha en cada crisis. Christoffersen compara la probabilidad de exceso tras un día sin exceso con la de tras un día con exceso.

5. Con 250 días, el valor crítico al 5 % del Z2 de Acerbi y Székely sale −0,69 en la simulación; con 4.780 días, −0,16. ¿Qué implica para el FHS, que da Z2 = −0,229 en la muestra completa?

   - A. Que pasa: está por encima de −0,69
   - B. Que con toda la muestra se rechaza su ES al 5 % (−0,229 < −0,16), aunque en una ventana de un año ese valor no llamaría la atención
   - C. Que el ES del FHS es demasiado alto
   - D. Que Z2 no sirve para el FHS

   *Respuesta B: Que con toda la muestra se rechaza su ES al 5 % (−0,229 < −0,16), aunque en una ventana de un año ese valor no llamaría la atención.* El valor crítico depende del número de días: con más datos, desviaciones menores son significativas. El FHS infravalora la cola en conjunto y, con 19 años, se detecta; con uno, no.

## Ideas clave

- El VaR es un cuantil de la pérdida y el ES la media de lo que queda más allá. Basilea usa el VaR al 99 % para el backtesting (MAR32) y el ES al 97,5 % para el capital (MAR33).
- Con el mismo libro, el VaR histórico y el normal de 250 sesiones tienen el doble de excesos de los que deberían, y el error del histórico frente al verdadero es de 105.800 USD de media: reaccionan tarde y se quedan altos un año.
- Un GJR-GARCH-t estimado cada enero con el pasado se queda a 16.200 USD del VaR verdadero de media, pasa Kupiec y Christoffersen y pasa el 88 % del tiempo en verde, como el modelo verdadero.
- La regla de la raíz de 10 falla cuando la volatilidad vuelve a la media: el Monte Carlo del modelo verdadero da 945.000 USD a 10 días en octubre de 2008 frente a 858.000 con la raíz de 10.
- Las pruebas tienen poca potencia con un año de datos: un modelo correcto cae en ámbar el 10,8 % de las veces y uno con el doble de excesos queda en verde a menudo. Hay que mirar series largas, la agrupación y el ES, no solo el conteo.
- El Z2 de Acerbi-Székely contrasta el ES con lo que se guarda cada día (VaR, ES y P&L): rechaza el histórico, el normal, el EWMA y el FHS, y no el GARCH-t ni el verdadero.

## Recursos

### Documentación

- [BCBS, MAR32: backtesting y prueba de atribución de P&L del modelo interno (versión en vigor desde el 1 de enero de 2023)](https://www.bis.org/committees/bcbs/basel-framework/81326/chapter.pdf) · bis.org
- [BCBS, MAR33: cálculo del capital con modelos internos (ES al 97,5 %, MAR33.3)](https://www.bis.org/committees/bcbs/basel-framework/81327/chapter.pdf) · bis.org
- [J.P. Morgan/Reuters (1996): RiskMetrics Technical Document, 4.ª ed.](https://www.msci.com/documents/10199/5915b101-4206-4ba0-aee2-3449d5c7e95a) · msci.com

### Libros

- [Acerbi, C. y Szekely, B. (2014): Backtesting Expected Shortfall. MSCI Research Insight](https://www.msci.com/documents/10199/22aa9922-f874-4060-b77a-0f0e267a489b) · msci.com
- [Kupiec, P. H. (1995): Techniques for Verifying the Accuracy of Risk Measurement Models. The Journal of Derivatives 3(2), 73-84](https://doi.org/10.3905/jod.1995.407942) · doi.org
- [Christoffersen, P. F. (1998): Evaluating Interval Forecasts. International Economic Review 39(4), 841-862](https://doi.org/10.2307/2527341) · doi.org

### Datos

- [Datos del nivel 4: P&L del libro con su VaR y ES verdaderos (CSV)](https://pythonparatrading.com/curso/datos/nivel-4/riesgo4_pyg_verdad.csv) · Descarga · riesgo4_pyg_verdad.csv

Lección anterior: [4.2.2 · Criterio de Kelly y volatility targeting: cuánto arriesgar](https://pythonparatrading.com/curso-python-trading/nivel-4/kelly-y-volatility-targeting/)

Lección siguiente: [4.2.4 · Stress testing y riesgos de correlación, liquidez, gap y contraparte](https://pythonparatrading.com/curso-python-trading/nivel-4/stress-testing-y-riesgos-de-correlacion-liquidez-y-contraparte/)
