---
title: "4.2.4 Stress testing: correlación, liquidez y contraparte"
description: "Escenarios históricos e hipotéticos, escenario inverso, correlaciones que cambian en las crisis, liquidez, gaps y contrapartes de una cartera."
url: "https://pythonparatrading.com/curso-python-trading/nivel-4/stress-testing-y-riesgos-de-correlacion-liquidez-y-contraparte/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-4/stress-testing-y-riesgos-de-correlacion-liquidez-y-contraparte.png"
---

# 4.2.4 · Stress testing y riesgos de correlación, liquidez, gap y contraparte

> El VaR describe los días normales y malos; las pruebas de estrés preguntan qué pasa en los peores. Esta lección somete un fondo multiactivo a ocho escenarios (2008, 2020, 2022, un lunes negro como el de 1987 y cuatro hipotéticos), busca el choque más probable que le costaría un 20 %, mide cómo se pierde la diversificación cuando cambian las correlaciones y calcula cuánto cuesta salir, qué hace un stop ante un hueco y cuánto dinero está en manos de cada contraparte.

- 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: [2.5.7 · Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping](https://pythonparatrading.com/curso-python-trading/nivel-2/datos-sinteticos-para-pruebas-de-estres/), [4.2.2 · Criterio de Kelly y volatility targeting: cuánto arriesgar](https://pythonparatrading.com/curso-python-trading/nivel-4/kelly-y-volatility-targeting/), [4.2.3 · VaR, Expected Shortfall y backtesting del VaR](https://pythonparatrading.com/curso-python-trading/nivel-4/var-expected-shortfall-y-backtesting-del-var/)

## Al terminar esta lección

- Construir escenarios de estrés históricos (con precios) e hipotéticos (con cargas factoriales) y aplicarlos a una cartera por activo y por clase.
- Resolver un escenario inverso: el choque de factores más probable que produce una pérdida dada.
- Medir cuánto cambian las correlaciones por régimen y separar el efecto de las volatilidades del de las correlaciones en el riesgo de una cartera.
- Calcular los días y el coste de liquidar una cartera en condiciones normales y de crisis con la ley de la raíz cuadrada.
- Cuantificar el riesgo de gap de un stop y la exposición a contrapartes con margen segregado, colateral y límites por entidad.

## Lo que el VaR no pregunta

El VaR de la lección 4.2.3 se estima con la historia reciente y dice cuánto se pierde un día malo de una distribución
que se supone conocida. Una **prueba de estrés** hace otra pregunta: qué le pasa a la cartera si ocurre algo concreto
y grave, haya o no ocurrido en la ventana de datos. Los principios del Comité de Basilea para pruebas de estrés (2018)
lo resumen en una condición: los escenarios deben ser «suficientemente graves pero plausibles» y cubrir los riesgos
materiales de la entidad, y deben revisarse para que sigan siendo relevantes (principio 4). Hay cuatro familias:

```mermaid
flowchart TB
  E[Pruebas de estrés] --> H[Históricas]
  E --> P[Hipotéticas]
  E --> I[Inversas]
  E --> S[Sensibilidad]
  H --> R[Revalorar cartera]
  P --> R
  I --> C[Choque que causa L]
  S --> R
```

- **Históricas**: se repiten los movimientos de una crisis real (2008, 2020, 2022). Son plausibles por definición, pero
  la siguiente crisis no será igual.
- **Hipotéticas**: se inventa un choque coherente (los tipos suben 200 pb, el dólar un 10 %) y se traslada a cada
  activo con un modelo, aquí las cargas factoriales.
- **Inversas**: se parte de la pérdida que no se puede tolerar y se busca qué escenario la produce.
- **Sensibilidad**: se mueve un solo factor (un punto de volatilidad, 1 pb de tipos) para conocer la exposición. Las
  griegas de la lección 4.2.6 son eso para los derivados.

## Los escenarios del nivel

`riesgo4_escenarios.csv` trae ocho escenarios como vectores de choques (rendimiento simple total) sobre los 38 activos
del universo sintético del nivel. Cuatro son históricos y salen de los propios precios: la crisis de 2008 desde la
quiebra de Lehman hasta el mínimo, el covid de febrero y marzo de 2020, la inflación de 2022 y el peor día de
`RV_EEUU` en la muestra. Cuatro son hipotéticos: un lunes negro como el del 19 de octubre de 1987 (el universo empieza
en 2006), una subida paralela de tipos de 200 pb, una apreciación del dólar del 10 % y una crisis de crédito.

```python
import numpy as np
import pandas as pd

esc = pd.read_csv("riesgo4_escenarios.csv")
choques = esc.pivot(index="activo", columns="escenario",
                    values="choque")
orden = ["crisis_2008", "covid_2020", "inflacion_2022", "peor_dia",
         "lunes_negro_1987", "tipos_mas_200pb", "dolar_mas_10",
         "crisis_credito"]
choques = choques[orden]
print(esc.groupby("escenario")["tipo"].first().loc[orden].to_string())
ver = ["RV_EEUU", "RV_EMG", "ALT_PRI", "RF_T710", "RF_T20", "RF_CHY",
       "MP_ORO"]
(100 * choques.loc[ver]).T.round(1)
```
```output
escenario
crisis_2008          historico
covid_2020           historico
inflacion_2022       historico
peor_dia             historico
lunes_negro_1987    hipotetico
tipos_mas_200pb     hipotetico
dolar_mas_10        hipotetico
crisis_credito      hipotetico
activo            RV_EEUU  RV_EMG  ALT_PRI  RF_T710  RF_T20  RF_CHY  MP_ORO
escenario
crisis_2008         -47.3   -59.8    -63.5     12.5    29.5   -24.6     8.5
covid_2020          -34.3   -41.0    -46.8      5.3    14.0   -20.9    -1.7
inflacion_2022      -25.9   -41.1    -29.9    -14.5   -33.5   -13.6    -1.5
peor_dia            -11.4   -14.0    -15.2     -0.3    -0.6    -2.0    -1.7
lunes_negro_1987    -19.8   -22.8    -28.3      2.0     4.6   -10.3     1.5
tipos_mas_200pb      -5.1    -4.5     -6.5    -14.0   -32.2    -4.2    -5.6
dolar_mas_10          0.0    -8.6      0.0      0.0     0.0     0.0    -6.4
crisis_credito      -15.0   -13.5    -25.5      3.0     6.9   -32.7     1.2
```

Las filas dicen lo que importa de cada escenario. En 2008 los bonos del Tesoro suben (12,5 % el de 7-10 años, 29,5 % el
de más de 20 años) mientras las acciones caen un 47 %; en 2022 caen las dos cosas (−14,5 % y −33,5 %). El capital
riesgo cotizado (`ALT_PRI`) cae más que el índice en todos los escenarios de acciones. El oro sube en 2008 y casi no se
mueve en los demás. El lunes negro hipotético es un día del −19,8 % para `RV_EEUU`; el real de 1987, según la Reserva
Federal, fue una caída del 22,6 % del Dow Jones en una sola sesión.

La segunda celda comprueba de dónde salen los dos tipos. Los históricos son el cociente de precios entre las dos fechas
del escenario; los hipotéticos, las cargas verdaderas del modelo factorial (`universo4_verdad.csv`) multiplicadas por
el choque de los factores, sin componente idiosincrático:

```python
precios = pd.read_parquet("universo4_precios.parquet")
precios = precios.set_index("fecha")
verdad = pd.read_csv("universo4_verdad.csv", index_col="activo")
B = verdad.filter(like="b_").rename(columns=lambda c: c[2:])

# histórico: precio al cierre de `fin` / al cierre de `inicio` − 1
ini, fin = "2008-09-12", "2009-03-09"
hist = precios.loc[fin] / precios.loc[ini] - 1
print("Histórico 2008, diferencia máxima:",
      f"{(hist - choques['crisis_2008']).abs().max():.1e}")

# hipotético: cargas verdaderas × choque de factores
lunes = pd.Series({"mercado": -0.20, "tamano": -0.02, "tipos": 0.02,
                   "credito": -0.03, "materias": -0.05,
                   "dolar": -0.02, "emergentes": -0.05,
                   "europa": -0.03, "japon": -0.03}
                  ).reindex(B.columns, fill_value=0.0)
print("Lunes negro, diferencia máxima:",
      f"{(B @ lunes - choques['lunes_negro_1987']).abs().max():.1e}")
```
```output
Histórico 2008, diferencia máxima: 5.2e-06
Lunes negro, diferencia máxima: 5.6e-17
```

Un escenario hipotético es tan bueno como su modelo: si las cargas de un activo cambian en una crisis, o si el activo
tiene un riesgo propio que el modelo no recoge, la revaloración se equivoca. En los datos, el modelo es el verdadero;
en una cartera real, las cargas se estiman (lección 4.1.6).

## Un fondo multiactivo bajo estrés

La cartera de la lección es un fondo de 500 millones de USD repartido entre catorce activos: un 45 % de acciones, un
38 % de renta fija, un 10 % de alternativos (capital riesgo cotizado, infraestructuras e inmobiliario) y un 7 % de
materias primas. Su pérdida en cada escenario, por clase de activo, en puntos porcentuales del fondo:

```python
pesos = pd.Series({
    "RV_EEUU": .25, "RV_EUR": .08, "RV_EMG": .07, "RV_EEUU_P": .05,
    "ALT_PRI": .03, "INFRA": .03, "INM_EEUU": .04,
    "RF_T710": .15, "RF_T20": .05, "RF_CIG": .08, "RF_CHY": .07,
    "RF_EMG": .03, "MP_ORO": .04, "MP_AMP": .03})
capital = 500e6
activos = pd.read_csv("universo4_activos.csv", index_col="activo")
clase = activos["clase"].replace({"inmobiliario": "alternativo"})

aporte = choques.loc[pesos.index].mul(pesos, axis=0)   # en tanto por 1
por_clase = 100 * aporte.groupby(clase.loc[pesos.index]).sum()
por_clase.loc["total"] = por_clase.sum()
print("Pesos por clase:",
      pesos.groupby(clase.loc[pesos.index]).sum().round(2).to_dict())
por_clase.T.round(2)
```
```output
Pesos por clase: {'alternativo': 0.1, 'materias_primas': 0.07, 'renta_fija': 0.38, 'renta_variable': 0.45}
clase             alternativo  materias_primas  renta_fija  renta_variable  total
escenario
crisis_2008             -5.52            -0.78        1.62          -23.09 -27.78
covid_2020              -3.81            -0.68       -0.41          -16.42 -21.31
inflacion_2022          -2.88             0.32       -6.56          -12.22 -21.35
peor_dia                -1.22            -0.12       -0.28           -5.75  -7.37
lunes_negro_1987        -2.06            -0.06       -0.41           -9.26 -11.79
tipos_mas_200pb         -1.06            -0.22       -5.30           -2.18  -8.76
dolar_mas_10            -0.13            -0.45       -0.02           -1.40  -2.01
crisis_credito          -1.90             0.05       -2.68           -6.60 -11.14
```

La figura de la cabecera ordena el resultado. La peor pérdida es la de 2008 (−27,8 %, 139 millones), con la renta fija
sumando 1,6 puntos. En el covid (−21,3 %) la renta fija ya resta 0,4: el crédito de alto rendimiento cae más de lo que
suben los bonos del Tesoro. Y en la inflación de 2022 (−21,4 %) la renta fija resta **6,6 puntos**: el fondo pierde
casi lo mismo que en el covid con una caída de las acciones mucho menor, porque la pata que debía protegerlo cae con
ellas. La subida de tipos hipotética (−8,8 %) lo confirma: la renta fija es aquí la mayor fuente de pérdida.

¿Qué habría dicho el VaR? Con los 20 años de datos, el VaR histórico de la cartera:

```python
rend = precios / precios.shift(1).fillna(100) - 1
r_cartera = rend[pesos.index] @ pesos         # rendimientos simples
mes = (1 + r_cartera).rolling(21).apply(np.prod, raw=True) - 1
print(f"VaR 99 % a 1 día (histórico 20 años): "
      f"{-r_cartera.quantile(0.01):.2%}")
print(f"VaR 99 % a 21 días (histórico 20 años): "
      f"{-mes.quantile(0.01):.2%}")
print(f"Peor día: {r_cartera.min():.2%} ({r_cartera.idxmin().date()})")
print(f"Peor mes móvil: {mes.min():.2%} ({mes.idxmin().date()})")
```
```output
VaR 99 % a 1 día (histórico 20 años): 2.27%
VaR 99 % a 21 días (histórico 20 años): 15.62%
Peor día: -7.44% (2008-09-04)
Peor mes móvil: -21.62% (2011-10-24)
```

El VaR al 99 % a un día es del 2,27 % y el peor día de la muestra, del −7,44 %; el lunes negro hipotético costaría un
11,8 %. A un mes, el VaR es del 15,6 % y el peor mes móvil de la historia, del −21,6 % (en 2011). Los escenarios de 2008
y 2022 duran meses y pierden más que cualquier VaR mensual. Esto no hace inútil al VaR: describe la distribución de
los días normales y malos; los escenarios describen los peores. Un límite de riesgo serio necesita las dos cosas, como
se vio con la venta de prima en la lección 3.6.1.

## El escenario inverso

Un escenario inverso parte de la pérdida inaceptable, por ejemplo un 20 % del fondo en un mes, y busca **qué** la
produce. Hay infinitas combinaciones; la más probable, con una covarianza $\Sigma$ de los factores, es la de menor
distancia de Mahalanobis. Si $b = B^\top w$ es la exposición de la cartera a cada factor (las cargas ponderadas por los
pesos), el choque que pierde $L$ con la menor distancia es

$$
f^* = -L\,\frac{\Sigma b}{b^\top \Sigma b}, \qquad d = \frac{L}{\sqrt{b^\top \Sigma b}}.
$$

La celda usa la covarianza verdadera de los factores en el régimen de estrés, escalada a un mes (21 sesiones), y
compara el resultado con el movimiento acumulado de los factores en el covid (20 de febrero a 23 de marzo de 2020):

```python
fv = pd.read_csv("universo4_factores_verdad.csv")


def cov_factores(regimen):
    t = fv[fv.regimen == regimen].set_index("factor")
    corr = t.filter(like="corr_").rename(columns=lambda c: c[5:])
    vol = t["vol_anual"]
    return corr.loc[B.columns, B.columns] * np.outer(vol, vol)


S = cov_factores("estres") * 21 / 252        # un mes de estrés
b = B.loc[pesos.index].T @ pesos             # exposición a factores
perdida = 0.20
f_inv = -perdida * (S @ b) / (b @ S @ b)     # mínima distancia
d = perdida / np.sqrt(b @ S @ b)             # en desviaciones típicas

fd = pd.read_parquet("universo4_factores_diarios.parquet")
fd = fd.set_index("fecha")
f20 = fd.loc["2020-02-20":"2020-03-23"].filter(like="f_").sum()
f20.index = [c[2:] for c in f20.index]
print(f"Exposición a mercado {b['mercado']:.2f}, "
      f"tipos {b['tipos']:.2f}, crédito {b['credito']:.2f}; "
      f"distancia {d:.2f} desv. típicas")
pd.DataFrame({"inverso_%": 100 * f_inv,
              "covid_2020_%": 100 * f20}).round(1)
```
```output
Exposición a mercado 0.55, tipos 0.43, crédito 0.35; distancia 2.54 desv. típicas
            inverso_%  covid_2020_%
mercado         -32.6         -42.0
tamano           -2.3          -7.3
valor            -1.2          -9.1
tipos             2.2           5.5
credito          -3.9          -6.8
materias        -12.4         -22.8
dolar             3.5           4.6
emergentes       -5.6          -9.1
europa           -0.2           1.7
japon            -0.0          -4.5
```

El fondo tiene una exposición de 0,55 al mercado, 0,43 a los tipos (por los bonos) y 0,35 al crédito. El choque más
probable que le cuesta un 20 % en un mes de estrés es una caída del 32,6 % del factor de mercado acompañada de las
materias primas (−12,4 %), los emergentes (−5,6 %) y el crédito (−3,9 %), con el dólar subiendo un 3,5 % y los bonos un
2,2 %: está a 2,54 desviaciones típicas de un mes de estrés. Es una crisis como la del covid, que en el factor de
mercado fue peor (−42,0 %). El escenario inverso no dice que eso vaya a pasar; dice que la pérdida inaceptable está a
un covid de distancia y que la cobertura de los bonos apenas cambia la cifra.

## Correlaciones que cambian en las crisis

Las correlaciones estimadas con toda la muestra son, sobre todo, las de la calma: 4.367 de las 5.031 sesiones. Por
régimen, en la muestra y en la verdad del modelo:

```python
exceso = rend.sub(fd["rf"], axis=0)
cov = pd.read_csv("universo4_cov_verdad.csv")


def cov_verdad(regimen, nombres):
    c = cov[cov.regimen == regimen].set_index("activo")
    return c.loc[nombres, nombres]


pares = [("RV_EEUU", "RF_T710"), ("RV_EEUU", "RF_CHY"),
         ("RV_EEUU", "MP_ORO"), ("RV_EEUU", "RV_EMG")]
filas = {}
for reg in ("calma", "estres", "inflacion"):
    dias = fd.index[fd["regimen"] == reg]
    m = exceso.loc[dias].corr()
    for a, b_ in pares:
        c = cov_verdad(reg, [a, b_])
        filas[(f"{a}-{b_}", reg)] = {
            "muestra": m.at[a, b_],
            "verdad": c.iat[0, 1] / np.sqrt(c.iat[0, 0] * c.iat[1, 1])}
print("Sesiones por régimen:", fd["regimen"].value_counts().to_dict())
pd.DataFrame(filas).T.unstack().round(2)
```
```output
Sesiones por régimen: {'calma': 4367, 'estres': 469, 'inflacion': 195}
                muestra                  verdad
                  calma estres inflacion  calma estres inflacion
RV_EEUU-MP_ORO     0.08   0.14      0.26   0.06   0.15      0.19
RV_EEUU-RF_CHY     0.62   0.71      0.75   0.62   0.73      0.75
RV_EEUU-RF_T710   -0.23  -0.41      0.53  -0.24  -0.46      0.51
RV_EEUU-RV_EMG     0.84   0.96      0.85   0.84   0.96      0.88
```

*Fig. — Correlación de 63 sesiones entre la renta variable de EE. UU. (RV_EEUU) y el bono de 7-10 años (RF_T710) del universo, a fin de mes, 2006-2025; sombreados, los tramos de estrés y de inflación.* Una línea casi siempre negativa, entre −0,63 y 0, más negativa en los tramos de estrés (sombreados en gris), cuando los bonos cubren a las acciones. En 2022 (tramo de inflación, destacado) sube hasta 0,65: acciones y bonos caen a la vez y la cobertura desaparece.

La correlación entre `RV_EEUU` y el bono de 7-10 años es de −0,23 en calma, −0,41 en estrés (huida a la calidad) y
**+0,53 en el régimen de inflación**. La de las acciones con el alto rendimiento sube de 0,62 a 0,75; la de EE. UU. con
emergentes, de 0,84 a 0,96 en estrés: en una crisis, la diversificación geográfica de las acciones casi desaparece. La
figura enseña la correlación móvil acciones-bonos de 2006 a 2025: casi siempre negativa, más en las crisis, y positiva
solo en 2022. Las cifras de la muestra se parecen a las verdaderas porque el régimen se conoce; en la realidad, el
régimen se sabe después.

¿Cuánto riesgo añade el cambio de correlaciones? La celda calcula la volatilidad verdadera del fondo en cada régimen y
la que tendría con las volatilidades de ese régimen y las correlaciones de la calma:

```python
nombres = list(pesos.index)
w = pesos.values
filas = {}
Cc = cov_verdad("calma", nombres).values
corr_calma = Cc / np.outer(np.sqrt(np.diag(Cc)), np.sqrt(np.diag(Cc)))
for reg in ("calma", "estres", "inflacion"):
    C = cov_verdad(reg, nombres).values
    vol = np.sqrt(np.diag(C))
    mixta = np.outer(vol, vol) * corr_calma   # vols del régimen,
    filas[reg] = {                            # correlación de calma
        "vol_cartera": np.sqrt(w @ C @ w),
        "vol_con_corr_calma": np.sqrt(w @ mixta @ w),
        "suma_vols": w @ vol,
        "ratio_diversif": (w @ vol) / np.sqrt(w @ C @ w)}
pd.DataFrame(filas).T.round(3)
```
```output
           vol_cartera  vol_con_corr_calma  suma_vols  ratio_diversif
calma            0.093               0.093      0.131           1.398
estres           0.273               0.259      0.327           1.198
inflacion        0.158               0.136      0.192           1.218
```

En calma, el fondo tiene una volatilidad del 9,3 % y la suma ponderada de las volatilidades de sus activos es del
13,1 %: un ratio de diversificación de 1,40. En estrés, 27,3 % (ratio 1,20): con las correlaciones de la calma serían
25,9 %, así que casi todo el aumento viene de las volatilidades. En inflación, 15,8 % frente a 13,6 % con las
correlaciones de la calma: aquí las correlaciones explican un tercio del aumento (2,2 de los 6,5 puntos), porque cambia
de signo la correlación entre las dos patas más grandes. Un modelo de riesgo estimado en calma subestima los dos
efectos; la prueba de estrés debe mover volatilidades y correlaciones a la vez.

## Liquidez: cuánto cuesta salir

Las pérdidas de un escenario suponen que la cartera se puede valorar y vender a esos precios. Con una cartera grande o
activos poco líquidos no es así: venderla lleva días y cada venta mueve el precio. `riesgo4_liquidez.csv` da, para cada
activo (como ETF ficticio), el volumen medio diario (ADV), la horquilla normal y en estrés, cuánto cae el ADV en una
crisis y el coeficiente de la ley de la raíz cuadrada, que la lección 4.3.1 estudia con las órdenes de una mesa:

$$\text{coste} \approx \frac{\text{horquilla}}{2} + Y\,\sigma_{\text{diaria}}\sqrt{\frac{q}{\text{ADV}}},
\qquad Y = 0{,}7.$$

La celda supone que se vende un 20 % del ADV cada día (o todo en un día si la posición es menor) y compara las
condiciones normales con las de estrés: horquilla de estrés, volatilidad diaria del régimen de estrés y ADV reducido:

```python
liq = pd.read_csv("riesgo4_liquidez.csv", index_col="activo")
liq = liq.loc[pesos.index]
vol_estres = verdad.loc[pesos.index, "vol_estres"] / np.sqrt(252)
Q = pesos * capital
part = 0.20                        # se vende un 20 % del ADV al día


def coste_pb(horq, sigma, q_adv):
    """Media horquilla + ley de la raíz cuadrada (Y = 0,7)."""
    return 1e4 * (horq / 2e4 + liq["impacto_y"] * sigma
                  * np.sqrt(q_adv))


adv_e = liq["adv_usd"] * liq["adv_estres_factor"]
q_n = np.minimum(part, Q / liq["adv_usd"])  # menos de un día: todo
q_e = np.minimum(part, Q / adv_e)
t = pd.DataFrame({
    "posicion_m": Q / 1e6,
    "dias": Q / (part * liq["adv_usd"]),
    "dias_estres": Q / (part * adv_e),
    "coste_pb": coste_pb(liq["horquilla_pb"], liq["vol_diaria"], q_n),
    "coste_pb_estres": coste_pb(liq["horquilla_pb_estres"],
                                vol_estres, q_e),
    "un_dia_estres_pb": coste_pb(liq["horquilla_pb_estres"],
                                 vol_estres, Q / adv_e)})
total = {"normal": (t.coste_pb * Q).sum() / 1e6 / 1e4,
         "estres": (t.coste_pb_estres * Q).sum() / 1e6 / 1e4,
         "estres_un_dia": (t.un_dia_estres_pb * Q).sum() / 1e6 / 1e4}
print("Coste total (millones de USD):",
      {k: round(float(v), 2) for k, v in total.items()})
t.sort_values("dias_estres", ascending=False).head(7).round(1)
```
```output
Coste total (millones de USD): {'normal': 0.7, 'estres': 3.1, 'estres_un_dia': 3.54}
          posicion_m  dias  dias_estres  coste_pb  coste_pb_estres  un_dia_estres_pb
INFRA           15.0   3.2          6.4      38.3            349.8             454.2
ALT_PRI         15.0   3.0          5.9      57.7            468.4             647.0
RF_T710         75.0   0.8          1.1      11.1             26.0              26.9
INM_EEUU        20.0   0.5          1.0      28.8            125.8             128.1
RF_CHY          35.0   0.5          0.7      12.6             55.6              55.6
MP_AMP          15.0   0.5          0.7      25.8            104.0             104.0
RF_T20          25.0   0.4          0.5      17.1             37.4              37.4
```

*Fig. — Días que se tarda en vender cada posición de un fondo de 500 millones de USD vendiendo el 20 % del volumen medio diario, en condiciones normales y con el volumen de una crisis.* Barras por activo, de más a menos días en estrés (normal / estrés): INFRA 3,2 / 6,4; ALT_PRI 3,0 / 5,9; RF_T710 0,8 / 1,1; INM_EEUU 0,5 / 1,0; RF_CHY 0,5 / 0,7; MP_AMP 0,5 / 0,7; RF_T20 0,4 / 0,5. Las dos posiciones pequeñas en ETF poco líquidos, infraestructuras y capital riesgo cotizado (destacado), tardan seis días en estrés; la de renta variable de EE. UU., cinco veces mayor, menos de un día.

Los activos grandes no son el problema: 125 millones en `RV_EEUU` son el 1,7 % de su ADV. Lo son las dos posiciones
pequeñas en ETF de poco volumen. Los 15 millones en infraestructuras (`INFRA`) tardan 3,2 días en venderse en
condiciones normales y 6,4 en estrés; los de capital riesgo cotizado (`ALT_PRI`), 3,0 y 5,9. Su coste pasa de 58 a 468
pb en `ALT_PRI` porque la horquilla de estrés es de 688 pb. Para todo el fondo, liquidar cuesta 0,70 millones de USD en
condiciones normales y 3,10 en estrés; venderlo todo en un día de estrés, 3,54. Y mientras se vende durante seis días
de estrés, el precio sigue moviéndose: con una volatilidad diaria del 4 % en `ALT_PRI`, seis días son un 10 % de
desviación típica sobre lo que queda por vender. Una prueba de estrés de liquidez suma las dos cosas.

## Gaps: lo que hace un stop cuando el precio salta

Un stop de venta es una orden que se convierte en orden a mercado cuando el precio toca un nivel. Si el precio salta por
encima del nivel sin negociar en él (un hueco de apertura, un anuncio fuera de hora, un día como el de 1987), el stop
se ejecuta al primer precio disponible. Con las barras de 5 minutos del ETF sintético `KAPA` del nivel 2 (2022-2025,
con el hueco nocturno en la primera barra de cada sesión), tres stops que esperan en el mercado por debajo del cierre
previo:

```python
kapa = pd.read_parquet("indice2_5min.parquet")
kapa["dia"] = kapa["ts"].dt.tz_convert("America/New_York").dt.date
dia = kapa.groupby("dia").agg(apertura=("apertura", "first"),
                              minimo=("minimo", "min"),
                              cierre=("cierre", "last"))
dia["previo"] = dia["cierre"].shift(1)
dia = dia.dropna()

filas = {}
for x in (0.005, 0.01, 0.015):   # stop de venta bajo el cierre previo
    stop = dia["previo"] * (1 - x)
    salta = dia["minimo"] <= stop
    en_gap = dia["apertura"] <= stop
    precio = np.where(en_gap, dia["apertura"], stop)
    extra = (stop - precio) / dia["previo"] * 1e4
    filas[f"stop {x:.1%}"] = {"dias_que_salta": int(salta.sum()),
                              "en_la_apertura": int(en_gap.sum()),
                              "extra_medio_pb": extra[salta].mean(),
                              "extra_max_pb": extra.max()}
print(f"Peor hueco de apertura: "
      f"{(dia.apertura / dia.previo - 1).min():.2%}")
pd.DataFrame(filas).T.round(1)
```
```output
Peor hueco de apertura: -1.88%
           dias_que_salta  en_la_apertura  extra_medio_pb  extra_max_pb
stop 0.5%           561.0           121.0             6.0         137.6
stop 1.0%           276.0            18.0             1.8          87.6
stop 1.5%           127.0             4.0             0.6          37.6
```

El stop del 1 % salta 276 veces; 18 de ellas en la apertura, por un hueco que lo atraviesa, con 1,8 pb de coste extra
de media y 87,6 pb en el peor caso (un stop del 1 % que pierde un 1,88 %). Cuanto más cerca está el stop, más veces lo
salta un hueco: el del 0,5 % lo hace 121 veces de 561. En `KAPA` el peor hueco es del −1,88 % porque el índice es
tranquilo; en una acción el día después de presentar resultados o en el lunes negro hipotético (−19,8 % en un día), el stop del
1 % perdería todo el hueco. Un stop limita la pérdida típica, no la máxima: el tamaño de la posición debe aguantar el
hueco plausible, que es lo que mide el escenario de un día (lección 4.2.1 para el riesgo por operación).

## Contraparte: quién tiene el dinero

El riesgo de contraparte es perder lo que otro debe si quiebra: el efectivo depositado en un bróker o en un exchange, el
margen entregado si no está en una cuenta segregada del cliente y el valor a favor de los derivados no cubierto por el
colateral recibido.

```mermaid
flowchart LR
  E[Efectivo] --> X[Exposición]
  M[Margen sin segregar] --> X
  D[MtM menos colateral] --> X
  X --> L{¿Supera el límite?}
```

`riesgo4_contrapartes.csv` tiene ocho relaciones con seis entidades ficticias a 31 de diciembre de 2025. La exposición
se calcula por relación y se agrega **por entidad**, porque la quiebra es de la entidad, no de la cuenta:

```python
cp = pd.read_csv("riesgo4_contrapartes.csv")
cp["margen_en_riesgo"] = cp["margen_entregado"].where(~cp["segregado"],
                                                      0)
cp["neto_derivados"] = (cp["mtm_derivados"]
                        - cp["colateral_recibido"]).clip(lower=0)
cp["exposicion"] = (cp["efectivo"] + cp["margen_en_riesgo"]
                    + cp["neto_derivados"])
print("Igual que la verdad:",
      bool((cp["exposicion"] == cp["exposicion_verdad"]).all()))

ent = cp.groupby("contraparte").agg(
    exposicion=("exposicion", "sum"), limite=("limite", "sum"),
    cds_pb=("cds_pb", "first"), tipo=("tipo", "first"))
ent["uso_limite"] = ent["exposicion"] / ent["limite"]
ent["pd_1a"] = ent["cds_pb"] / 1e4 / (1 - 0.40)  # recuperación 40 %
ent["perdida_esperada"] = ent["pd_1a"] * 0.60 * ent["exposicion"]
cols = ["exposicion", "limite", "uso_limite", "pd_1a",
        "perdida_esperada"]
ent.sort_values("exposicion", ascending=False)[cols].round(3)
```
```output
Igual que la verdad: True
                        exposicion    limite  uso_limite  pd_1a  perdida_esperada
contraparte
Corredora Atlas            5120000  16000000       0.320  0.012           35840.0
Nordbank Capital           3400000   3000000       1.133  0.018           37400.0
Kestrel Exchange           3200000   1500000       2.133    NaN               NaN
Banco Meridiano            2200000  25000000       0.088  0.008            9900.0
Bolt Prime                 1800000   4000000       0.450  0.035           37800.0
Cámara Central Europea      250000  50000000       0.005  0.003             500.0
```

El cálculo reproduce la columna de verdad. Dos entidades superan su límite: Nordbank Capital, una contraparte OTC que
tiene 0,5 millones de margen no segregado y 2,9 millones de valor a favor sin colateral (3,4 millones frente a un
límite de 3), y Kestrel Exchange, un exchange de cripto sin rating ni CDS con 2,3 millones de efectivo y 0,9 de margen
no segregado (3,2 millones frente a 1,5: más del doble). Corredora Atlas aparece dos veces (bróker principal y cuenta de
CFD): por separado ninguna cuenta llama la atención; juntas son la mayor exposición, 5,1 millones, aunque su margen
principal (2,6 millones) está segregado y no cuenta. Con el CDS se puede aproximar una probabilidad de impago a un año,
$PD \approx s/(1-R)$ con una recuperación $R$ del 40 %: Bolt Prime (210 pb) tiene un 3,5 % y la mayor pérdida esperada
(37.800 USD) con solo 1,8 millones de exposición. Para Kestrel no hay CDS: no tener precio de mercado del riesgo no lo
hace pequeño. La lección 3.7.2 midió con datos sintéticos lo que queda de una posición cuando quiebra el exchange que
la custodia.

## Ejercicios

1. **Una cartera 60/40 y una con oro.** Calcula la pérdida en los ocho escenarios de una 60/40 (`RV_EEUU` / `RF_T710`)
   y de una variante con un 10 % de oro (`MP_ORO`) que sale de los bonos (60/30/10). ¿En qué escenarios ayuda el oro y
   en cuáles no?

**Solución.**

```python
c6040 = pd.Series({"RV_EEUU": 0.6, "RF_T710": 0.4})
c_oro = pd.Series({"RV_EEUU": 0.6, "RF_T710": 0.3, "MP_ORO": 0.1})
pd.DataFrame({
    "60/40": 100 * choques.loc[c6040.index].T @ c6040,
    "60/30/10": 100 * choques.loc[c_oro.index].T @ c_oro}).round(2)
```
```output
                  60/40  60/30/10
escenario
crisis_2008      -23.37    -23.76
covid_2020       -18.42    -19.13
inflacion_2022   -21.33    -20.03
peor_dia          -6.97     -7.11
lunes_negro_1987 -11.08    -11.13
tipos_mas_200pb   -8.66     -7.82
dolar_mas_10       0.00     -0.64
crisis_credito    -7.80     -7.98
```

El oro ayuda poco y no en todas partes. Mejora la inflación de 2022 (−21,33 % a −20,03 %) y la subida de tipos (−8,66 %
a −7,82 %), los escenarios en los que fallan los bonos, y empeora un poco 2008, el covid y el peor día, en los que los
bonos del Tesoro suben más que el oro. En la subida del dólar resta: el oro en USD cae un 6,4 %. Ninguna variante
evita la pérdida grande: en 2008 la 60/40 pierde un 23,4 %. La diversificación entre activos de riesgo y bonos reduce
la pérdida de los escenarios en los que la correlación es negativa; en los demás, solo la reduce el tamaño.

2. **Escenario inverso en un mes de inflación.** Repite el escenario inverso del 20 % con la covarianza de los factores
   del régimen de inflación. ¿Qué factores cambian de papel y cuál es la distancia?

**Solución.**

```python
S_inf = cov_factores("inflacion") * 21 / 252
f_inf = -perdida * (S_inf @ b) / (b @ S_inf @ b)
print(f"Distancia: {perdida / np.sqrt(b @ S_inf @ b):.2f}"
      " desv. típicas")
pd.DataFrame({"estres_%": 100 * f_inv,
              "inflacion_%": 100 * f_inf}).round(1)
```
```output
Distancia: 4.40 desv. típicas
            estres_%  inflacion_%
mercado        -32.6        -27.5
tamano          -2.3         -2.2
valor           -1.2          3.4
tipos            2.2         -6.5
credito         -3.9         -2.8
materias       -12.4         -1.6
dolar            3.5          3.4
emergentes      -5.6         -5.5
europa          -0.2         -0.4
japon           -0.0         -0.0
```

En inflación los **tipos** cambian de papel: el escenario inverso pide que los bonos caigan un 6,5 % en lugar de subir
un 2,2 %, porque en ese régimen bajan con las acciones, y el mercado cae menos (−27,5 %). Las materias primas, que en
2022 subieron, casi no intervienen. La distancia es de 4,40 desviaciones típicas porque la volatilidad del régimen de
inflación es menor que la del estrés: perder un 20 % en un mes de inflación es mucho menos probable que en uno de
estrés, pero cuando ocurre, los bonos no protegen.

3. **Una política de contrapartes.** Con la tabla de contrapartes, propón tres reglas para el sistema de trading de la
   lección y di qué harías hoy con Kestrel Exchange y con Nordbank Capital.

**Solución.**

Tres reglas razonables: (1) límites **por entidad**, no por cuenta, calculados con la exposición que se perdería en
una quiebra (efectivo, margen no segregado y valor neto de los derivados) y revisados cada día; (2) límites más bajos
para entidades sin rating, sin CDS o fuera de un régimen de segregación de activos que se haya comprobado en sus
condiciones y en la regulación aplicable (lecciones 3.7.5 y 4.4.1), y el efectivo que no se necesita para margen,
fuera de la entidad; (3) colateral obligatorio en los derivados OTC por encima de un umbral pequeño, con valoración
diaria. Hoy: en Kestrel, retirar efectivo hasta quedar por debajo del límite (1,7 millones como mínimo) y no dejar en
el exchange más que el margen de las posiciones abiertas; en Nordbank, pedir colateral por el valor a favor (2,9
millones) o reducir posiciones, y preguntar por qué el margen entregado no está segregado.

## Autoevaluación

1. En el escenario de inflación de 2022 el fondo pierde un 21,4 %, casi lo mismo que en el covid (−21,3 %), aunque las acciones caen menos (−26 % frente a −34 % en `RV_EEUU`). ¿Por qué?

   - A. Porque en 2022 el fondo tenía más acciones
   - B. Porque la renta fija, que en 2008 y 2020 amortiguaba, cae a la vez que las acciones: aporta −6,6 puntos en lugar de sumar
   - C. Por las materias primas
   - D. Porque el escenario de 2022 es más largo y acumula costes

   *Respuesta B: Porque la renta fija, que en 2008 y 2020 amortiguaba, cae a la vez que las acciones: aporta −6,6 puntos en lugar de sumar.* La correlación acciones-bonos pasa de negativa a +0,5 en el régimen de inflación. La cobertura en la que se apoyaba la cartera desaparece justo en ese escenario.

2. ¿Qué es un escenario inverso (reverse stress test)?

   - A. Un escenario en el que el mercado sube
   - B. Repetir un escenario histórico al revés
   - C. Partir de una pérdida inaceptable y buscar qué combinación de movimientos la produce; con una covarianza, la más probable es la de menor distancia de Mahalanobis
   - D. Un escenario sin correlaciones

   *Respuesta C: Partir de una pérdida inaceptable y buscar qué combinación de movimientos la produce; con una covarianza, la más probable es la de menor distancia de Mahalanobis.* En lugar de preguntar cuánto se pierde en un escenario dado, se pregunta qué escenario hace perder lo que no se puede perder. Con la covarianza de los factores en estrés, el choque más probable es f = −L·Σb/(b′Σb).

3. En el régimen de estrés la volatilidad verdadera del fondo es del 27,3 %; con las volatilidades del estrés pero las correlaciones de la calma sería del 25,9 %. ¿Qué dice esa diferencia?

   - A. Que las correlaciones no importan nunca
   - B. Que el modelo está mal
   - C. Que la cartera está mejor diversificada en estrés
   - D. Que en este fondo la mayor parte del aumento de riesgo en estrés viene de las volatilidades; las correlaciones añaden 1,4 puntos. En el régimen de inflación su parte es mayor (13,6 % frente a 15,8 %), por la correlación acciones-bonos

   *Respuesta D: Que en este fondo la mayor parte del aumento de riesgo en estrés viene de las volatilidades; las correlaciones añaden 1,4 puntos. En el régimen de inflación su parte es mayor (13,6 % frente a 15,8 %), por la correlación acciones-bonos.* Separar volatilidades y correlaciones dice qué cobertura falla. En estrés suben las dos; en inflación cambia el signo de la que más pesa en una cartera mixta.

4. Un stop de venta un 1 % por debajo del cierre previo salta 276 veces en los datos de KAPA; 18 de ellas en la apertura. ¿Qué precio obtiene en esas 18?

   - A. El de la apertura, peor que el stop: el hueco lo atraviesa sin negociar en su precio; el peor caso cuesta 87,6 pb más de lo previsto
   - B. El del stop
   - C. El cierre del día
   - D. No se ejecuta

   *Respuesta A: El de la apertura, peor que el stop: el hueco lo atraviesa sin negociar en su precio; el peor caso cuesta 87,6 pb más de lo previsto.* Un stop es una orden que se vuelve de mercado al tocarse el precio. Si el precio abre por debajo, se ejecuta al primer precio disponible. Un stop limita la pérdida esperada, no la máxima.

5. Kestrel Exchange tiene 2,3 millones en efectivo y 0,9 de margen no segregado: una exposición de 3,2 millones frente a un límite de 1,5. ¿Por qué cuenta todo el margen?

   - A. Porque no está segregado: si la entidad quiebra, ese dinero está en su balance y el cliente es un acreedor más; el margen segregado de Corredora Atlas no cuenta
   - B. Porque el margen siempre cuenta
   - C. Porque es un exchange de cripto
   - D. Porque no tiene rating

   *Respuesta A: Porque no está segregado: si la entidad quiebra, ese dinero está en su balance y el cliente es un acreedor más; el margen segregado de Corredora Atlas no cuenta.* La exposición a una contraparte es lo que se pierde si quiebra mañana: efectivo, margen en cuentas no segregadas y valor neto a favor de los derivados no cubierto por colateral.

## Ideas clave

- Una prueba de estrés revalora la cartera en escenarios graves pero plausibles: históricos, que salen de los precios, e hipotéticos, que salen de un choque de factores y de las cargas.
- El fondo multiactivo de la lección pierde un 27,8 % en 2008 y un 21,4 % en la inflación de 2022, frente a un VaR al 99 % a un mes del 15,6 %: el VaR no es un límite de pérdidas.
- El escenario inverso busca el choque más probable que causa una pérdida inaceptable: para un 20 % en un mes de estrés, una caída del mercado del 33 % con las materias primas, a 2,5 desviaciones típicas.
- Las correlaciones cambian con el régimen: la de acciones y bonos pasa de −0,24 en calma a +0,51 en inflación. En una cartera mixta, la cobertura que falla es justo la que más pesa.
- La liquidez se mide en días y en coste: 15 millones en un ETF poco líquido tardan 6 días en venderse en estrés y cuestan 468 pb; en un año normal, 58.
- Un stop no limita la pérdida máxima (un hueco lo atraviesa) y la exposición a contrapartes se agrega por entidad: dos de las seis superan su límite.

## Recursos

### Research

- [Liquidez Order Book - Trading Liquidez Agregada](https://pythonparatrading.com/limit-order-book-y-liquidez-de-mercado/) · Research-0008 · 11 min

### Documentación

- [BCBS (2018): Stress testing principles](https://www.bis.org/bcbs/publ/d450.htm) · bis.org
- [Federal Reserve History: Stock Market Crash of 1987](https://www.federalreservehistory.org/essays/stock-market-crash-of-1987) · federalreservehistory.org

### Datos

- [Datos del nivel 4: escenarios de estrés sobre 38 activos (CSV)](https://pythonparatrading.com/curso/datos/nivel-4/riesgo4_escenarios.csv) · Descarga · riesgo4_escenarios.csv
- [Datos del nivel 4: liquidez de los 38 activos (CSV)](https://pythonparatrading.com/curso/datos/nivel-4/riesgo4_liquidez.csv) · Descarga · riesgo4_liquidez.csv
- [Datos del nivel 4: exposición a contrapartes (CSV)](https://pythonparatrading.com/curso/datos/nivel-4/riesgo4_contrapartes.csv) · Descarga · riesgo4_contrapartes.csv

Lección anterior: [4.2.3 · VaR, Expected Shortfall y backtesting del VaR](https://pythonparatrading.com/curso-python-trading/nivel-4/var-expected-shortfall-y-backtesting-del-var/)

Lección siguiente: [4.2.5 · Control del drawdown y kill switch](https://pythonparatrading.com/curso-python-trading/nivel-4/control-del-drawdown-y-kill-switch/)
