---
title: "2.5.7 Datos sintéticos para pruebas de estrés"
description: "GBM, GJR-GARCH-t y bootstrap de bloques: qué reproduce cada generador, pruebas de estrés con crisis insertadas y límites de los datos sintéticos."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/datos-sinteticos-para-pruebas-de-estres/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/datos-sinteticos-para-pruebas-de-estres.png"
---

# 2.5.7 · Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping

> La historia de un mercado es una sola y no contiene las crisis que todavía no han ocurrido. Un generador de datos sintéticos produce cientos de historias alternativas: sirve para medir cómo se comporta una estrategia en escenarios que la muestra no tiene y para comprobar que un método encuentra lo que se sabe que hay, pero no para demostrar que una ventaja existe.

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.5 · Sesgos y validación
- Duración: 65 min
- Requisitos: [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/), [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/)

## Al terminar esta lección

- Generar caminos con un movimiento browniano geométrico, con un GJR-GARCH-t ajustado con `arch` y con un bootstrap estacionario de la historia.
- Comparar los hechos estilizados de cada generador con los de la serie real: colas, asimetría, memoria de la volatilidad, efecto apalancamiento y drawdown.
- Diseñar una prueba de estrés insertando crisis de distinta forma en caminos sintéticos y comparar estrategias en cada una.
- Explicar cómo están construidos los datos sintéticos de este curso, por qué llevan efectos plantados y qué límites tiene cualquier generador.

## Por qué fabricar historias

Un backtest sobre la historia responde a una pregunta: qué habría pasado con **estos** datos. La lección 2.5.5
remuestreó esa historia para ver cuánto podía variar el resultado. Pero el remuestreo solo recombina lo que ya ocurrió:
si la muestra no tiene una caída del 35 % en un mes, ningún bootstrap la va a inventar. Un **generador** de datos
sintéticos es un modelo que produce series nuevas con las propiedades estadísticas que se le pidan: se le puede
pedir una crisis que no está en la muestra, mil años de datos o un mercado sin ninguna ventaja.

Tres usos legítimos y uno que no lo es:

```mermaid
flowchart LR
  G[Generador] --> C[Probar el código]
  G --> E[Pruebas de estrés]
  G --> R[Riesgo y tamaño]
  G --> V[Validar una ventaja]
  C --> C2[Respuesta conocida]
  E --> E2[Crisis nuevas]
  R --> R2[Colas y drawdown]
  V --> V2[No: circular]
```

- **Probar el código**: si el generador planta un efecto con un tamaño conocido, un método correcto debe recuperarlo, y
  en una serie sin efecto no debe encontrar nada. Es lo que hacen todos los datos de este curso.
- **Pruebas de estrés**: insertar escenarios que la historia no tiene, o tiene una sola vez, y ver cómo responde la
  estrategia.
- **Riesgo y tamaño**: estimar la distribución de drawdowns o de pérdidas con más caminos de los que da la historia.
- **Validar una ventaja**: no. En datos sintéticos, una estrategia gana si el generador contiene lo que la estrategia
  explota. El razonamiento es circular: se vuelve a eso al final.

## Tres generadores

La serie de referencia es el futuro sobre el índice ficticio IDA del nivel 2, 26 años de sesiones. Es, a su vez,
sintética, y eso permite algo que con datos reales no se puede: comparar lo que cada generador estima con el modelo
que de verdad produjo los datos (más abajo).

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

fut = pd.read_parquet("futuros2_continuos.parquet")
ida = (fut[fut["mercado"] == "IDA"].set_index("fecha")
       ["cierre_ajustado"])
r = np.log(ida).diff().dropna()
print(f"{len(r)} sesiones, {r.index[0]:%Y-%m-%d} "
      f"a {r.index[-1]:%Y-%m-%d}")
print(f"Media anual {252 * r.mean():.1%}, "
      f"volatilidad {np.sqrt(252) * r.std():.1%}")
```
```output
6538 sesiones, 2000-01-04 a 2025-12-31
Media anual 6.3%, volatilidad 20.1%
```

### Movimiento browniano geométrico

El modelo más simple, y el de Black-Scholes: el logaritmo del precio es un paseo aleatorio con rendimientos normales
independientes de media $\mu$ y volatilidad $\sigma$ constantes:

$$
\ln S_{t+1} - \ln S_t = \mu + \sigma\,\varepsilon_t, \qquad \varepsilon_t \sim N(0, 1)\ \text{i.i.d.}
$$

```python
N, T = 200, len(r)
rng = np.random.default_rng(2025)

mu, sigma = r.mean(), r.std()
gbm = mu + sigma * rng.standard_normal((N, T))
print(gbm.shape)
```
```output
(200, 6538)
```

### GJR-GARCH con innovaciones t

El GARCH de Bollerslev (1986) hace que la varianza de hoy dependa del shock y de la varianza de ayer; la extensión de
Glosten, Jagannathan y Runkle (1993) añade un término $\gamma$ que solo actúa tras los días negativos (el efecto
apalancamiento), y las innovaciones $t$ de Student dan colas más gruesas que la normal (lección 2.1.4):

$$
r_t = \mu + \varepsilon_t,\quad \varepsilon_t = \sigma_t z_t,\quad
\sigma_t^2 = \omega + (\alpha + \gamma\,\mathbb{1}_{\varepsilon_{t-1}<0})\,\varepsilon_{t-1}^2 + \beta\,\sigma_{t-1}^2
$$

`arch` ajusta el modelo por máxima verosimilitud. Para simular cientos de caminos es más rápido escribir la recursión
con NumPy, un paso de tiempo para todos los caminos a la vez, que llamar 200 veces al `simulate` del modelo:

```python
from arch import arch_model

ajuste = arch_model(100 * r, mean="Constant", vol="GARCH",
                    p=1, o=1, q=1, dist="t").fit(disp="off")
print(ajuste.params.round(3).to_dict())

def simular_gjr(p, T, N, rng, burn=500):
    """N caminos de T sesiones de un GJR-GARCH-t (en tanto por 1)."""
    m, w, a, g, b, nu = (p["mu"], p["omega"], p["alpha[1]"],
                         p["gamma[1]"], p["beta[1]"], p["nu"])
    z = rng.standard_t(nu, (T + burn, N)) * np.sqrt((nu - 2) / nu)
    s2 = np.full(N, w / (1 - a - b - g / 2))
    e = np.zeros(N)
    out = np.empty((T + burn, N))
    for t in range(T + burn):
        s2 = w + (a + g * (e < 0)) * e ** 2 + b * s2
        e = np.sqrt(s2) * z[t]
        out[t] = m + e
    return out[burn:].T / 100

p_sim = ajuste.params.copy()
p_sim["mu"] = 100 * r.mean()     # la media muestral (ver el texto)
garch = simular_gjr(p_sim, T, N, rng)
persistencia = (ajuste.params["alpha[1]"] + ajuste.params["beta[1]"]
                + ajuste.params["gamma[1]"] / 2)
print(f"Persistencia: {persistencia:.3f}; "
      f"media del ajuste: {252 * ajuste.params['mu'] / 100:.1%} anual")
```
```output
{'mu': 0.054, 'omega': 0.032, 'alpha[1]': 0.044, 'gamma[1]': 0.104, 'beta[1]': 0.882, 'nu': 5.422}
Persistencia: 0.978; media del ajuste: 13.5% anual
```

Dos detalles. La persistencia, $\alpha + \beta + \gamma/2$, es 0,978: un shock de volatilidad tarda unas 31 sesiones en
reducirse a la mitad ($\ln 0{,}5 / \ln 0{,}978$). Y la media estimada, un 0,054 % diario, son un 13,5 % anual, más del
doble de la media muestral (6,3 %). No es un error: con innovaciones $t$, la verosimilitud da poco peso a los días
extremos, que en IDA son sobre todo caídas, y la media estimada sube. Simular con ella dibujaría un mercado bastante más
alcista que el real, así que la simulación usa la media muestral.

### Bootstrap estacionario de la historia

El tercer generador no supone ningún modelo: recompone la historia en bloques de longitud aleatoria, como en la lección
2.5.5, con 75 sesiones de media para conservar las rachas de volatilidad.

```python
def indices_estacionario(n, largo, b, rng):
    idx = np.empty(largo, dtype=np.int64)
    idx[0] = rng.integers(n)
    salto = rng.random(largo) < 1 / b
    nuevo = rng.integers(n, size=largo)
    for t in range(1, largo):
        idx[t] = nuevo[t] if salto[t] else (idx[t - 1] + 1) % n
    return idx

x = r.to_numpy()
boot = np.array([x[indices_estacionario(T, T, 75, rng)]
                 for _ in range(N)])
print(gbm.shape, garch.shape, boot.shape)
```
```output
(200, 6538) (200, 6538) (200, 6538)
```

## ¿Qué reproduce cada uno?

Los hechos estilizados de la lección 1.6.2, medidos en IDA y en la mediana de los 200 caminos de cada generador:

```python
def acf(x, k):
    x = x - x.mean()
    return (x[k:] * x[:-k]).sum() / (x * x).sum()

def hechos(x):
    z = (x - x.mean()) / x.std()
    eq = np.exp(np.cumsum(x))
    return {"vol": np.sqrt(252) * x.std(),
            "curtosis": stats.kurtosis(x),
            "asimetría": stats.skew(x),
            "días >4σ": (np.abs(z) > 4).sum(),
            "acf r 1": acf(x, 1),
            "acf |r| 1": acf(np.abs(x), 1),
            "acf |r| 20": acf(np.abs(x), 20),
            "apalanc.": np.corrcoef(x[:-1], np.abs(x[1:]))[0, 1],
            "dd máx": (eq / np.maximum.accumulate(eq) - 1).min()}

tabla = {"IDA": hechos(x)}
for nombre, sims in (("GBM", gbm), ("GJR-t", garch),
                     ("bootstrap", boot)):
    tabla[nombre] = pd.DataFrame([hechos(s) for s in sims]).median()
pd.DataFrame(tabla).round(3)
```
```output
               IDA    GBM   GJR-t  bootstrap
vol          0.201  0.201   0.182      0.201
curtosis    12.584 -0.004   9.009     12.141
asimetría   -0.373  0.001   0.105     -0.369
días >4σ    51.000  0.000  39.000     49.000
acf r 1      0.020  0.000  -0.003      0.019
acf |r| 1    0.247 -0.001   0.215      0.240
acf |r| 20   0.180  0.002   0.122      0.134
apalanc.    -0.056 -0.000  -0.038     -0.054
dd máx      -0.706 -0.540  -0.505     -0.661
```

| Hecho | IDA | GBM | GJR-GARCH-t | Bootstrap (b = 75) |
|---|---|---|---|---|
| Colas (curtosis en exceso) | 12,6 | 0: ninguna | 9,0: buena parte | 12,1: casi todas |
| Días a más de 4σ | 51 | 0 | 39 | 49 |
| Asimetría | −0,37 | 0 | 0,105 | −0,37 |
| Agrupamiento (ACF de \|r\| a 1 sesión) | 0,25 | 0 | 0,21 | 0,24 |
| Efecto apalancamiento (corr. de r con \|r\| siguiente) | −0,056 | 0 | −0,038 | −0,054 |
| Drawdown máximo (mediana) | −70,6 % | −54,0 % | −50,5 % | −66,1 % |

El GBM no reproduce nada salvo la media y la volatilidad: ni colas, ni rachas, ni asimetría. El GJR-GARCH-t reproduce
el agrupamiento a corto plazo (algo menos: 0,21 frente a 0,25) y buena parte de las colas y del efecto apalancamiento
(lo pone el término $\gamma$), pero no la asimetría de los rendimientos diarios, porque sus innovaciones $t$ son
simétricas. El bootstrap reproduce casi todo, como era de esperar: son los mismos días. Su drawdown mediano (−66,1 %)
se acerca al histórico (−70,6 %); los otros dos se quedan en −54 % y −51 % porque reparten las caídas sin agruparlas.

La figura de la cabecera muestra el hecho que ninguno reproduce bien: la **memoria larga** de la volatilidad. La
autocorrelación de |r| de IDA sigue en 0,130 a 50 sesiones y en 0,077 a 100, y se apaga hacia las 150. La del GJR-t cae
a 0,052 y 0,010, porque su memoria decae de forma exponencial; la del bootstrap, a 0,062 y 0,015, porque los bloques de
75 sesiones cortan cualquier dependencia más larga.

```python
RETARDOS = [1, 2, 5, 10, 20, 50, 100, 150, 200, 250]
memoria = pd.DataFrame(
    {"IDA": [acf(np.abs(x), k) for k in RETARDOS]}, index=RETARDOS)
for nombre, sims in (("GBM", gbm), ("GJR-t", garch),
                     ("bootstrap", boot)):
    memoria[nombre] = [np.median([acf(np.abs(s), k) for s in sims])
                       for k in RETARDOS]
memoria.rename_axis("retardo").round(3)
```
```output
           IDA    GBM  GJR-t  bootstrap
retardo
1        0.247 -0.001  0.215      0.240
2        0.277  0.000  0.210      0.272
5        0.253  0.000  0.192      0.234
10       0.211  0.001  0.166      0.184
20       0.180  0.002  0.122      0.134
50       0.130  0.001  0.052      0.062
100      0.077 -0.001  0.010      0.015
150      0.007  0.002  0.002     -0.003
200      0.012 -0.001 -0.000     -0.002
250     -0.011  0.001 -0.005     -0.003
```

*Fig. — Drawdown máximo de 200 caminos de 26 años de cada generador (barra fina: percentiles 5-95; gruesa: 25-75; punto: mediana) frente al de la serie IDA.* Tres filas. GBM: mediana −54,0 %, percentil 25 −60,6 %, del −74,5 % al −37,9 %. GJR-GARCH-t: mediana −50,5 %, percentil 25 −60,2 %, del −82,8 % al −33,6 %. Bootstrap: mediana −66,1 %, percentil 25 −76,7 %, del −87,9 % al −40,9 %. La línea destacada marca el drawdown real de IDA, −70,6 %, dentro de la caja central del bootstrap y en la cola de los otros dos.

Para una prueba de riesgo, la consecuencia es directa: el GBM y el GJR-t sitúan el drawdown real de IDA en la cola de su
distribución (por debajo de su percentil 25: −60,6 % en el GBM y −60,2 % en el GJR-t), es decir, lo tratan como mala
suerte. El bootstrap lo deja dentro de su caja central (percentil 25: −76,7 %): lo trata como lo normal. Elegir el
generador es elegir cuánto riesgo se va a ver.

## Lo que el ajuste no devuelve

Como IDA es sintética, se puede comparar con el modelo que la generó. El README del nivel 2 lo documenta: el factor de
mercado de la renta variable es un GJR-GARCH(1,1) con innovaciones $t$ ($\nu = 6$, $\alpha = 0{,}03$,
$\gamma = 0{,}11$, $\beta = 0{,}86$ en el código del generador) cuya volatilidad de largo plazo y cuya deriva cambian
según un **guion** de tramos con fechas fijas. Desde 2006, el futuro IDA sigue exactamente ese factor (antes, uno
propio con el mismo guion y los mismos parámetros), más el carry y el momentum plantados. El ajuste de arriba devolvió
$\alpha = 0{,}044$, $\gamma = 0{,}104$, $\beta = 0{,}882$ y $\nu = 5{,}4$: cerca en $\gamma$ y en $\nu$, pero con una
persistencia de 0,978 frente al 0,945 del generador ($0{,}03 + 0{,}86 + 0{,}11/2$). El guion se lee en los datos, pero
no tal cual:

```python
guion = {  # tramos del README del nivel 2: (desde, hasta, vol, deriva)
    "calma 2004-07": ("2004-01-02", "2007-07-13", 0.12, 0.07),
    "crisis 2008-09": ("2008-09-02", "2009-03-09", 0.52, -0.95),
    "calma 2012-15": ("2012-06-01", "2015-08-14", 0.12, 0.13),
    "covid 2020": ("2020-02-20", "2020-03-23", 0.80, -4.00),
    "2022": ("2022-01-04", "2022-10-12", 0.25, -0.32),
}
filas = {}
for k, (a, b, v, d) in guion.items():
    t = r.loc[a:b]
    filas[k] = {"sesiones": len(t), "vol_lp_guion": v,
                "vol_IDA": np.sqrt(252) * t.std(),
                "deriva_guion": d, "deriva_IDA": 252 * t.mean()}
pd.DataFrame(filas).T.round(2)
```
```output
                sesiones  vol_lp_guion  vol_IDA  deriva_guion  deriva_IDA
calma 2004-07      888.0          0.12     0.13          0.07        0.09
crisis 2008-09     130.0          0.52     0.50         -0.95       -1.01
calma 2012-15      806.0          0.12     0.13          0.13        0.13
covid 2020          23.0          0.80     0.53         -4.00       -5.60
2022               195.0          0.25     0.19         -0.32       -0.05
```

En los tramos largos y tranquilos, la volatilidad realizada (0,13) se acerca a la del guion (0,12), y en las 130
sesiones de 2008-2009 también (0,50 frente a 0,52). En el tramo corto de 2020, no llega: el guion fija la volatilidad
**de largo plazo**, y un GARCH tarda decenas de sesiones en acercarse a ella. En sus 23 sesiones, el guion pedía 0,80 y
la volatilidad realizada fue 0,53. Las derivas se apartan más, porque el ruido GARCH-t va encima de la del guion: en
2020, −5,60 frente a −4,00; en 2022, −0,05 frente a −0,32 (el README del nivel 2 documenta, año a año, cuánto se aparta
el factor realizado del guion). Un GARCH ajustado a toda la serie no ve tramos: ve una volatilidad que sube y baja con
mucha inercia, y la resume en una persistencia más alta que la del generador (0,978 frente a 0,945).

Es una lección general. Los parámetros estimados son los del modelo que se ajusta, no los del proceso que produjo los
datos, y cuando el modelo no es el verdadero (con datos reales, nunca lo es) el generador que sale del ajuste hereda
sus simplificaciones. Para comprobar que la estimación sí funciona cuando el modelo es el correcto, basta con ajustar
el GJR-t a sus propios caminos:

```python
import warnings
from arch.utility.exceptions import ConvergenceWarning

warnings.filterwarnings("ignore", category=ConvergenceWarning)
rng_ajuste = np.random.default_rng(7)
propios = simular_gjr(ajuste.params, T, 10, rng_ajuste)
reajustes = pd.DataFrame([
    arch_model(100 * s, mean="Constant", vol="GARCH", p=1, o=1, q=1,
               dist="t").fit(disp="off").params for s in propios])
pd.DataFrame({"verdad": ajuste.params,
              "media_10_ajustes": reajustes.mean(),
              "desv_10_ajustes": reajustes.std()}).round(3)
```
```output
          verdad  media_10_ajustes  desv_10_ajustes
mu         0.054             0.059            0.007
omega      0.032             0.031            0.003
alpha[1]   0.044             0.047            0.009
gamma[1]   0.104             0.102            0.010
beta[1]    0.882             0.881            0.007
nu         5.422             5.351            0.351
```

Sobre caminos generados por el propio GJR-t, el ajuste recupera sus parámetros con un error pequeño. Sobre IDA se
acercaba en $\alpha$, $\gamma$ y $\nu$, pero no en la persistencia, porque IDA no es un GJR-t con parámetros fijos: su
volatilidad de largo plazo cambia por tramos. El aviso `ConvergenceWarning` de `arch` (que el optimizador puede emitir
en alguna serie) se silencia por su categoría para que la salida sea estable; en un trabajo real conviene revisarlo.

## Pruebas de estrés con crisis insertadas

La utilidad principal de un generador con modelo es poder **insertar** escenarios. Sobre los 200 caminos GJR-t se
insertan, a partir de la sesión 3.000, tres crisis de forma distinta: una caída rápida del 35 % en 23 sesiones (como la
de 2020) con la volatilidad triplicada, una caída lenta del 45 % en 500 sesiones (como la de 2000-2002) con la
volatilidad ×1,5, y una caída rápida seguida de un rebote del 45 % en 60 sesiones (una V). En cada tramo se sustituye
la media de los rendimientos por la deriva del escenario y se amplifican sus desviaciones:

```python
T0 = 3000  # sesión en que empieza el escenario

def escenario(sims, caida, sesiones, mult_vol, rebote=0.0,
              sesiones_rebote=0):
    """Inserta una caída (y un rebote) en cada camino simulado."""
    out = sims.copy()
    tramo = slice(T0, T0 + sesiones)
    base = out[:, tramo] - out[:, tramo].mean(axis=1, keepdims=True)
    out[:, tramo] = np.log(1 + caida) / sesiones + mult_vol * base
    if sesiones_rebote:
        tramo2 = slice(T0 + sesiones, T0 + sesiones + sesiones_rebote)
        base2 = out[:, tramo2] - out[:, tramo2].mean(axis=1,
                                                     keepdims=True)
        out[:, tramo2] = (np.log(1 + rebote) / sesiones_rebote
                          + mult_vol * base2)
    return out

ESCENARIOS = {
    "base": garch,
    "caída rápida": escenario(garch, -0.35, 23, 3.0),
    "caída lenta": escenario(garch, -0.45, 500, 1.5),
    "en V": escenario(garch, -0.35, 23, 3.0, rebote=0.45,
                      sesiones_rebote=60),
}
for nombre, sims in ESCENARIOS.items():
    caida = np.exp(sims[:, T0:T0 + 560].cumsum(axis=1)).min(axis=1)
    print(f"{nombre:>13}: peor punto mediano "
          f"{np.median(caida) - 1:.1%}")
```
```output
         base: peor punto mediano -10.1%
 caída rápida: peor punto mediano -42.5%
  caída lenta: peor punto mediano -50.0%
         en V: peor punto mediano -37.2%
```

En el escenario base, el peor punto de las 560 sesiones siguientes a la sesión 3.000 está, en mediana, un 10,1 % por
debajo del inicio. Con las crisis insertadas, entre el −37,2 % y el −50,0 %. Tres estrategias sencillas se someten a
cada escenario: mantener el índice, mantenerlo con un **objetivo de volatilidad** del 15 % (posición = 15 % / volatilidad
EWMA de 20 sesiones, con un máximo de 2) y la **tendencia** de 12 meses de la lección 2.5.5 con el mismo escalado.
Todas usan solo datos hasta la sesión anterior.

```python
def ewm_vol(sims, span=20):
    a = 2 / (span + 1)
    v = np.empty_like(sims)
    v[:, 0] = sims[:, :250].var(axis=1)
    for t in range(1, sims.shape[1]):
        v[:, t] = (1 - a) * v[:, t - 1] + a * sims[:, t - 1] ** 2
    return np.sqrt(252 * v)          # con datos hasta t - 1

def estrategias(sims):
    simple = np.expm1(sims)
    vol = ewm_vol(sims)
    tend = np.sign(pd.DataFrame(sims.T).rolling(252).sum()
                   .shift(1).fillna(0).to_numpy().T)
    peso_vol = np.minimum(0.15 / vol, 2.0)
    return {"comprar y mantener": simple,
            "objetivo de vol.": peso_vol * simple,
            "tendencia 12 m": tend * peso_vol * simple}

def peor_caida(rend):
    eq = np.cumprod(1 + rend[:, T0:T0 + 560], axis=1)
    return eq.min(axis=1) - 1

estres = {}
for esc, sims in ESCENARIOS.items():
    for est, rend in estrategias(sims).items():
        c = peor_caida(rend)
        estres[(esc, est)] = {"mediana": np.median(c),
                              "p5": np.quantile(c, 0.05)}
estres = pd.DataFrame(estres).T
for col in ("mediana", "p5"):
    print(f"Peor caída en 560 sesiones ({col}):")
    print(estres[col].unstack().reindex(list(ESCENARIOS))
          .round(3).to_string(), end="\n\n")
```
```output
Peor caída en 560 sesiones (mediana):
              comprar y mantener  objetivo de vol.  tendencia 12 m
base                      -0.101            -0.085          -0.149
caída rápida              -0.425            -0.253          -0.185
caída lenta               -0.500            -0.401          -0.137
en V                      -0.372            -0.204          -0.228

Peor caída en 560 sesiones (p5):
              comprar y mantener  objetivo de vol.  tendencia 12 m
base                      -0.442            -0.299          -0.398
caída rápida              -0.607            -0.433          -0.430
caída lenta               -0.651            -0.494          -0.265
en V                      -0.513            -0.336          -0.436
```

*Fig. — Prueba de estrés sobre 200 caminos GJR-GARCH-t con tres escenarios insertados: peor caída mediana de cada estrategia.* Cuatro filas de escenarios con tres marcas cada una. Base: mantener −10,1 %, objetivo de volatilidad −8,5 %, tendencia −14,9 %. Caída rápida del 35 % en 23 sesiones: −42,5 %, −25,3 % y −18,5 %. Caída lenta del 45 % en 500 sesiones: −50,0 %, −40,1 % y −13,7 %. En V, con rebote del 45 %: −37,2 %, −20,4 % y, destacada, la tendencia con −22,8 %, peor que en la caída rápida porque se pone corta en el fondo y pierde el rebote.

Cada estrategia tiene su escenario malo:

- **Mantener** absorbe cada crisis entera: −42,5 % en la caída rápida y −50,0 % en la lenta (medianas).
- El **objetivo de volatilidad** reduce la caída rápida del −42,5 % al −25,3 %, porque la volatilidad se dispara y la
  posición baja en pocos días; en la lenta ayuda menos (−40,1 %), porque la volatilidad sube poco y la posición apenas
  cambia.
- La **tendencia** es la mejor en la caída lenta (−13,7 %): tiene tiempo para cambiar de signo y acaba corta. En la V
  pierde más que en la caída rápida sin rebote (−22,8 % frente a −18,5 %), porque se pone corta cerca del fondo y el
  rebote la pilla al revés. Y en el escenario base, un GJR-t sin ninguna tendencia plantada, es la peor de las tres
  (−14,9 %): sin efecto que capturar, solo paga los cambios de signo.

Los percentiles 5 cuentan lo mismo con más crudeza: en la caída rápida, uno de cada veinte caminos deja una pérdida de
más del 60 % al que mantiene. Esta es la información que una prueba de estrés debe dar antes de operar: no si la
estrategia gana, sino en qué tipo de mercado pierde y cuánto.

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

Los escenarios son hipótesis. Una caída del 35 % en 23 sesiones con la volatilidad triplicada es una elección del
investigador, igual que su forma lineal y su posición en el camino. Una prueba de estrés honesta documenta los
escenarios, prueba varios tamaños y formas, y no los ajusta después de ver qué estrategia sale mejor: eso sería otra
forma de sobreajuste.

</aside>

## Los datos de este curso son sintéticos

Todo lo anterior describe también cómo se han hecho los datos que usa este curso. El README de los datos del nivel 2
(enlazado en los recursos) documenta su generador, y es un buen ejemplo de lo que se puede y no se puede pedir a un
generador:

```mermaid
flowchart LR
  G[Guion de tramos] --> M[Mercado GJR-t]
  M --> A[Rendimientos]
  S[Sectores GARCH-t] --> A
  I[Idiosincrático] --> A
  P[Efectos plantados] --> A
  E[Eventos] --> A
  A --> D[Ficheros y README]
```

- Un **guion** de tramos con fechas fijas (calma, crisis de 2000-2002, 2008, 2020, 2022…) fija la deriva y la
  volatilidad de largo plazo del mercado; en los tramos de estrés las betas se comprimen hacia 1 y las correlaciones
  suben.
- Un **factor de mercado** GJR-GARCH con innovaciones $t$, **factores de sector** GARCH-t y un componente
  **idiosincrático** por activo dan las colas, el agrupamiento y la estructura de correlaciones.
- Los **efectos plantados** tienen fórmulas y tamaños publicados: el momentum de series temporales de los futuros, las
  primas de momentum, value y baja volatilidad de las acciones, la cointegración de los pares, el carry de las
  divisas… Al lado, **controles** sin efecto: la «calidad» y el placebo ESG de las acciones, los pares correlados no
  cointegrados, la serie `ruptura` tras su ruptura, 995 de las 1.000 estrategias de `ruido2`.
- Los **eventos** (quiebras guiadas, splits, dividendos, rebalanceos de índices, resultados) y los fundamentales se
  generan coherentes con los precios.
- Una **semilla** fija por conjunto hace que todo se pueda regenerar byte a byte con el comando que da el README.

Esa construcción es lo que permite usar los datos como banco de pruebas: si un método aplicado al momentum de los
futuros no lo encuentra, el método falla; si encuentra un efecto en el placebo ESG con más frecuencia que el nivel de
significación, también. Pero tiene límites que hay que tener presentes cada vez que se lee una cifra de este curso:

| El generador del curso tiene | El generador del curso no tiene |
|---|---|
| Colas gruesas y agrupamiento de volatilidad | Saltos de liquidez, huecos de cotización por suspensiones, fallos técnicos |
| Crisis en fechas fijas, comunes a todas las series | Crisis nuevas o con otra forma que las del guion |
| Efectos constantes con el tamaño publicado | Efectos que se debilitan al hacerse conocidos o se arbitran |
| Costes y horquillas por fórmula | Impacto de mercado de las propias órdenes |
| Una «verdad» conocida | Ninguna garantía de que esa verdad se parezca a la de un mercado real |

El límite más importante es el tercero de la derecha. En los mercados reales, las anomalías publicadas pierden parte
de su rentabilidad después de publicarse[^1], y las estrategias rentables atraen capital que las erosiona. En los datos
del curso, el momentum plantado vale lo mismo en 2001 que en 2025 porque así está escrito. Ninguna rentabilidad de
estos datos es evidencia de que una estrategia funcione: lo es de que el código encuentra lo que el generador contiene.

## Ejercicios

1. Añade un cuarto escenario: una caída del 50 % en 10 sesiones con la volatilidad multiplicada por 4 (un crash
   extremo) y calcula la peor caída mediana y el percentil 5 de las tres estrategias.

**Solución.**

```python
crash = escenario(garch, -0.50, 10, 4.0)
for est, rend in estrategias(crash).items():
    c = peor_caida(rend)
    print(f"{est:>18}: mediana {np.median(c):.1%}, "
          f"p5 {np.quantile(c, 0.05):.1%}")
```
```output
comprar y mantener: mediana -55.5%, p5 -70.0%
  objetivo de vol.: mediana -27.0%, p5 -44.6%
    tendencia 12 m: mediana -18.2%, p5 -42.5%
```

Con un crash del 50 % en 10 sesiones, mantener deja una peor caída mediana del −55,5 % (−70,0 % en el percentil 5). El
objetivo de volatilidad la reduce a la mitad (−27,0 %), pero en el percentil 5 todavía pierde un 44,6 %: en 10
sesiones, la EWMA de 20 no tiene tiempo de recortar toda la posición antes de que pase lo peor. La tendencia queda en
−18,2 % de mediana, con un percentil 5 del −42,5 %: cuando el camino venía bajista ya estaba corta, y cuando venía
alcista, el escalado por volatilidad es lo único que la protege. Ninguna de las tres evita una pérdida grande en los
peores caminos.

2. Repite el bootstrap con bloques de 250 sesiones de media. ¿Cambian la memoria de la volatilidad a 100 sesiones y
   el drawdown mediano? ¿Qué se gana y qué se pierde con bloques tan largos?

**Solución.**

```python
rng250 = np.random.default_rng(250)
boot250 = np.array([x[indices_estacionario(T, T, 250, rng250)]
                    for _ in range(N)])
h = pd.DataFrame([hechos(s) for s in boot250]).median()
print(f"ACF |r| a 100: "
      f"{np.median([acf(np.abs(s), 100) for s in boot250]):.3f}")
print(f"Drawdown mediano: {h['dd máx']:.1%}")
```
```output
ACF |r| a 100: 0.041
Drawdown mediano: -63.7%
```

Con bloques de 250 sesiones, la autocorrelación de |r| a 100 sesiones sube de 0,015 a 0,041 (IDA: 0,077) y el drawdown
mediano cambia poco (del −66,1 % al −63,7 %). Se gana memoria larga, porque cada bloque conserva casi un año de historia
seguida; se pierde variedad, porque con 6.538 sesiones solo caben unos 26 bloques distintos por camino y los caminos se
parecen mucho entre sí y a la historia. En el límite, un bloque de la longitud de la serie devuelve la propia historia.

3. Un compañero propone validar una estrategia de reversión intradía generando 10.000 días con un GJR-GARCH-t
   ajustado al índice y comprobando que gana en el 90 % de ellos. ¿Qué le dirías?

**Solución.**

Que el resultado no puede validar la estrategia. Un GJR-GARCH-t con media constante genera rendimientos sin
autocorrelación: una estrategia de reversión no puede tener ventaja en esos datos salvo por un error (de alineación,
de costes, de anticipación) o por un efecto que el modelo produzca sin querer. Si gana en el 90 % de los caminos, lo
primero que hay que buscar es ese error, y los datos sintéticos son excelentes para eso, porque se sabe que la
respuesta correcta es «no gana». Lo que sí puede hacer con el generador es una prueba de estrés: cuánto pierde la
estrategia cuando la volatilidad se triplica o hay huecos grandes. La evidencia de la ventaja tiene que venir de datos
reales, con las validaciones de este módulo (lecciones 2.5.2 a 2.5.6) y de una hipótesis sobre de dónde sale
(lección 2.2.1).

[^1]: McLean, R. D. y Pontiff, J. (2016), «Does Academic Research Destroy Stock Return Predictability?», The Journal of
    Finance 71(1), 5-32, [doi:10.1111/jofi.12365](https://doi.org/10.1111/jofi.12365): en 97 variables que predecían
    rentabilidades de acciones, las carteras rinden un 26 % menos fuera de la muestra original y un 58 % menos tras la
    publicación.

## Autoevaluación

1. Los caminos GBM tienen la misma media y volatilidad que IDA. ¿Qué hecho estilizado reproducen?

   - A. Las colas gruesas
   - B. Ninguno de los de la tabla salvo la volatilidad media: curtosis 0, ningún día a más de 4σ, ninguna memoria de la volatilidad y ningún efecto apalancamiento
   - C. El agrupamiento de volatilidad
   - D. El drawdown de IDA

   *Respuesta B: Ninguno de los de la tabla salvo la volatilidad media: curtosis 0, ningún día a más de 4σ, ninguna memoria de la volatilidad y ningún efecto apalancamiento.* Un GBM tiene rendimientos logarítmicos normales e independientes. Reproduce la media y la volatilidad, y nada más: IDA tiene una curtosis en exceso de 12,6 y 51 días a más de 4σ; los caminos GBM, 0 y 0. Una prueba de estrés con GBM infravalora los días extremos y las rachas de pérdidas.

2. El GJR-GARCH-t ajustado a IDA tiene una media diaria de 0,054 % (13,5 % anual) y la media muestral de IDA es del 6,3 % anual. ¿Por qué la lección simula con la media muestral?

   - A. Porque la media del ajuste está mal calculada
   - B. Porque la verosimilitud t da poco peso a los días extremos, que en IDA son sobre todo caídas: su media estimada es más alta que la muestral, y simular con ella dibujaría un mercado más alcista que el real
   - C. Porque así el drawdown sale más pequeño
   - D. Porque `arch` no permite simular con la media ajustada

   *Respuesta B: Porque la verosimilitud t da poco peso a los días extremos, que en IDA son sobre todo caídas: su media estimada es más alta que la muestral, y simular con ella dibujaría un mercado más alcista que el real.* Con innovaciones t, el estimador de máxima verosimilitud de la media se parece a una media robusta: rebaja la influencia de los días extremos. En una serie con caídas grandes, eso sube la media: aquí, a más del doble. Simular con ella daría caminos con una deriva que la serie no tuvo y drawdowns más suaves.

3. La autocorrelación de |r| de IDA sigue en 0,077 a 100 sesiones; la del GJR-GARCH-t ajustado es 0,010 y la del bootstrap con bloques de 75, 0,015. ¿De dónde viene esa memoria larga en IDA?

   - A. De un error en los datos
   - B. Del guion de tramos del generador del curso: la volatilidad de largo plazo cambia por épocas, y un GARCH con una sola volatilidad de largo plazo la convierte en persistencia, pero su memoria decae de forma exponencial
   - C. Del carry de los futuros
   - D. De los rolls de los contratos

   *Respuesta B: Del guion de tramos del generador del curso: la volatilidad de largo plazo cambia por épocas, y un GARCH con una sola volatilidad de largo plazo la convierte en persistencia, pero su memoria decae de forma exponencial.* Los datos del nivel 2 combinan un GJR-GARCH con una volatilidad de largo plazo distinta en cada tramo del guion (0,12 en calma, 0,52 en 2008…). Ningún generador de la lección tiene esos cambios de régimen: el GARCH los absorbe en una persistencia más alta que la del generador (0,978 frente a 0,945) y el bootstrap los corta en bloques de 75 sesiones.

4. En el escenario en V (caída del 35 % en 23 sesiones y rebote del 45 % en 60), la regla de tendencia pierde más (−22,8 %) que en la caída rápida sin rebote (−18,5 %). ¿Por qué?

   - A. Porque la volatilidad es más alta
   - B. Porque tras la caída la señal de 12 meses se vuelve corta cerca del fondo y el rebote le cuesta dinero: la tendencia sufre en los giros rápidos
   - C. Porque la tendencia no usa objetivo de volatilidad
   - D. Por azar: son pocos caminos

   *Respuesta B: Porque tras la caída la señal de 12 meses se vuelve corta cerca del fondo y el rebote le cuesta dinero: la tendencia sufre en los giros rápidos.* Una regla de tendencia necesita tiempo para cambiar de signo. En una caída lenta tiene ese tiempo y acaba corta (−13,7 %, la mejor de las tres); en una V, cambia tarde, justo antes del rebote. Es el tipo de debilidad que una prueba de estrés debe hacer visible antes de operar.

5. Una estrategia gana mucho sobre los datos sintéticos de este curso. ¿Qué se puede concluir?

   - A. Que funcionará en mercados reales
   - B. Que el código encuentra el efecto que el generador plantó (o uno que el generador produce sin querer); sobre los mercados reales, nada
   - C. Que el generador es realista
   - D. Que no hay sesgos en el backtest

   *Respuesta B: Que el código encuentra el efecto que el generador plantó (o uno que el generador produce sin querer); sobre los mercados reales, nada.* En datos sintéticos, la «verdad» es el modelo que los generó. Un resultado sobre ellos dice si el método recupera lo que el modelo contiene. La evidencia de una ventaja real tiene que venir de datos reales, con las validaciones de este módulo, y de una hipótesis económica.

## Ideas clave

- Cada generador reproduce lo que lleva dentro: el GBM, solo media y volatilidad; el GJR-GARCH-t, colas y agrupamiento con memoria corta; el bootstrap de bloques, casi todo lo de la historia, pero solo lo que la historia contiene y troceado en bloques.
- Frente a IDA (curtosis 12,6, drawdown −70,6 %), el GBM da curtosis 0 y un drawdown mediano del −54 %; el GJR-t, curtosis 9,0 y −51 %; el bootstrap, 12,1 y −66 %.
- Los parámetros de un ajuste no son los del proceso que generó los datos: con la volatilidad de largo plazo cambiando por tramos, el GARCH estima una persistencia de 0,978 (la del generador es 0,945) y una media del 13,5 % anual frente al 6,3 % muestral.
- Una prueba de estrés útil inserta crisis con formas distintas: en una caída rápida, el objetivo de volatilidad reduce la peor caída mediana del −42,5 % al −25,3 %; en una V, la tendencia pierde más que en la caída sin rebote.
- Los datos de este curso son sintéticos con efectos plantados y documentados en su README: sirven para comprobar que un método recupera un efecto conocido y no se inventa otros, nunca para demostrar que una estrategia funciona en mercados reales.

## Recursos

### Research

- [Procesos Estocásticos - Paseos Aleatorios](https://pythonparatrading.com/procesos-estocasticos/) · Research-0005 · 12 min
- [Estimadores de Volatilidad para Trading](https://pythonparatrading.com/estimadores-de-volatilidad-para-trading/) · Research-0002 · 4 min

### Documentación

- [arch: introducción a los modelos ARCH (ajuste, previsión y simulación)](https://arch.readthedocs.io/en/latest/univariate/introduction.html) · arch.readthedocs.io

### Libros

- [Bollerslev, T. (1986): Generalized autoregressive conditional heteroskedasticity. Journal of Econometrics 31(3), 307-327](https://doi.org/10.1016/0304-4076(86)90063-1) · doi.org
- [Glosten, L., Jagannathan, R. y Runkle, D. (1993): On the Relation between the Expected Value and the Volatility of the Nominal Excess Return on Stocks. The Journal of Finance 48(5), 1779-1801](https://doi.org/10.1111/j.1540-6261.1993.tb05128.x) · doi.org
- [Cont, R. (2001): Empirical properties of asset returns: stylized facts and statistical issues. Quantitative Finance 1(2), 223-236](https://doi.org/10.1080/713665670) · doi.org

### Datos

- [Datos del nivel 2: futuros sintéticos (índice IDA incluido)](https://pythonparatrading.com/curso/datos/nivel-2/futuros2_continuos.parquet) · Descarga · futuros2_continuos.parquet

Lección anterior: [2.5.6 · Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos](https://pythonparatrading.com/curso-python-trading/nivel-2/sensibilidad-de-parametros-y-robustez/)

Lección siguiente: [2.5.8 · Checklist de «backtest honesto» antes de pasar a paper trading](https://pythonparatrading.com/curso-python-trading/nivel-2/checklist-de-backtest-honesto/)
