---
title: "4.2.5 Control del drawdown y kill switch"
description: "Reducir exposición con el drawdown, circuit breakers y kill switch probados sobre una estrategia que deja de funcionar: retraso, falsas alarmas y coste."
url: "https://pythonparatrading.com/curso-python-trading/nivel-4/control-del-drawdown-y-kill-switch/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-4/control-del-drawdown-y-kill-switch.png"
---

# 4.2.5 · Control del drawdown y kill switch

> Cuatro defensas contra las pérdidas, de los segundos a los años: un kill switch operativo, un circuit breaker, la reducción de exposición con el drawdown y un kill switch estadístico. Se prueban sobre diez estrategias sintéticas, una de ellas muerta desde julio de 2019, y cada una se presenta con lo que tarda, con las veces que se equivoca y con lo que cuesta cuando la estrategia estaba sana.

- 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: 65 min
- Requisitos: [2.6.1 · CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index](https://pythonparatrading.com/curso-python-trading/nivel-2/metricas-de-rentabilidad-y-riesgo/), [2.1.7 · Detección de regímenes: modelos de Markov ocultos y cambios estructurales](https://pythonparatrading.com/curso-python-trading/nivel-2/regimenes-hmm-y-cambios-estructurales/), [2.5.4 · Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)](https://pythonparatrading.com/curso-python-trading/nivel-2/deflated-sharpe-ratio-y-pbo/), [4.2.1 · Riesgo por operación, stops y riesgo de ruina](https://pythonparatrading.com/curso-python-trading/nivel-4/riesgo-por-operacion-stops-y-riesgo-de-ruina/), [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

- Distinguir los cuatro niveles de control (operativo, circuit breaker, exposición por drawdown y kill switch estadístico) por su horizonte y por lo que detectan.
- Calcular la distribución de la máxima caída de una estrategia sana y no confundir la caída del backtest con el peor caso.
- Programar una regla de exposición proporcional al colchón de drawdown y medir su coste, su trampa (la exposición que no se recupera) y el efecto de un piso.
- Construir un CUSUM sobre los rendimientos, calibrar su umbral con el compromiso entre falsas alarmas y retraso, y medir los dos sobre datos con la respuesta conocida.
- Escribir un kill switch operativo con reloj inyectado, rearme manual y pruebas guionizadas.

## Cuatro defensas con horizontes distintos

Un sistema que pierde dinero puede estar en cuatro situaciones muy distintas: un fallo de software que manda órdenes
sin control, un día malo de una estrategia sana, una racha mala de una estrategia sana y una estrategia que ha dejado
de funcionar. Cada una pide una defensa distinta, que actúa en un horizonte distinto y se equivoca de una forma
distinta:

| Defensa | Horizonte | Qué detecta | Cómo se equivoca |
|---|---|---|---|
| Kill switch operativo | Segundos | Órdenes que no deberían existir, datos viejos, pérdidas fuera de escala | Para el sistema por un dato malo del bróker |
| Circuit breaker | Días | Un día de pérdida fuera de lo normal | Se pierde el rebote del día siguiente |
| Exposición por drawdown | Semanas a meses | Una racha de pérdidas | Opera poco justo cuando la estrategia se recupera |
| Kill switch estadístico | Meses a años | Una ventaja que ha desaparecido | Apaga estrategias sanas en sus malas rachas |

En el día a día, una señal pasa por las tres últimas antes de llegar al bróker, y el kill switch operativo vigila
todo el camino:

```mermaid
flowchart TB
  S[Señal] --> E[Escala por caída]
  E --> C{Breaker en pausa?}
  C -- no --> K{Kill switch}
  C -- sí --> N[Sin operar]
  K -- activo --> O[Orden al broker]
  K -- disparado --> N
```

La lección usa `estrategias4_diarias.parquet`: rendimientos diarios simples, en exceso del tipo libre, de diez
estrategias sintéticas parecidas a las de los niveles 2 y 3, de 2006 a 2025. Nueve tienen una ventaja constante
(sus parámetros verdaderos están en `estrategias4_verdad.csv`: Sharpe objetivo entre 0,25 y 0,50) y sufren las crisis
del universo del nivel. La décima, `degradada`, tiene un Sharpe verdadero de 1 hasta el 30 de junio de 2019 y una media
de cero desde el 1 de julio. Con la respuesta conocida se puede medir lo que en real nunca se sabe: cuándo una alarma
es falsa y cuánto tarda una verdadera. Son datos sintéticos: nada de lo que ganan estas estrategias es evidencia de
que funcionen en mercados reales.

## La caída de una estrategia sana

Antes de decidir qué caída es demasiado, conviene ver las que sufren estrategias que no tienen nada roto:

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

r = pd.read_parquet("estrategias4_diarias.parquet").set_index("fecha")
verdad = pd.read_csv("estrategias4_verdad.csv",
                     index_col="estrategia")


def caidas(x):
    """Serie de drawdown de unos rendimientos simples diarios."""
    eq = (1 + x).cumprod()
    return eq / eq.cummax() - 1


def tabla_caidas(r):
    filas = {}
    for c in r.columns:
        dd = caidas(r[c])
        bajo = (dd < 0).astype(int)
        # racha más larga por debajo del máximo, en sesiones
        racha = bajo.groupby((bajo == 0).cumsum()).sum().max()
        filas[c] = {"sharpe": r[c].mean() / r[c].std() * 252**0.5,
                    "max_caida": dd.min(), "fecha": dd.idxmin().date(),
                    "anos_bajo_max": racha / 252}
    return pd.DataFrame.from_dict(filas, orient="index")


tabla_caidas(r).round(2)
```
```output
                   sharpe  max_caida       fecha  anos_bajo_max
tendencia_futuros    0.53      -0.36  2024-04-29           9.04
carry_futuros        0.38      -0.27  2011-01-13           8.12
carry_divisas        0.53      -0.35  2011-11-03           9.50
venta_puts           0.31      -0.49  2009-02-24           8.41
pares                0.40      -0.22  2007-08-09           7.04
momentum_acciones    0.39      -0.28  2009-06-10           6.08
valor_acciones       0.27      -0.24  2013-04-03          10.67
venta_volatilidad    0.25      -0.49  2023-02-22           9.31
reversion_corto      0.45      -0.24  2008-12-04           9.69
degradada            0.70      -0.43  2024-02-06           4.55
```

Las nueve estrategias sanas tienen caídas máximas del 22 % al 49 % y pasan entre 6 y casi 11 años por debajo de un
máximo anterior. La venta de puts y la de volatilidad caen casi a la mitad (−49,0 % y −48,9 %). Ninguna de ellas está
rota: es lo que hace una estrategia de Sharpe 0,3-0,5 en 20 años con dos crisis grandes. `degradada` tiene la mayor
caída tras las dos ventas de prima (−43,4 %), toda ella después de morir.

El error más común es tomar la caída máxima del backtest como el peor caso. Es una sola muestra de una distribución.
Una simulación de estrategias sanas con los parámetros de `degradada` antes de morir (Sharpe 1, volatilidad del 8 %,
innovaciones t de Student) da esa distribución para 3,5 y 10 años:

```python
rng = np.random.default_rng(7)


def max_caidas(sharpe, vol, anos, n=4000):
    """Máxima caída de n trayectorias sanas: media diaria constante,
    innovaciones t de Student (nu = 5) de varianza 1."""
    T = int(252 * anos)
    z = rng.standard_t(5, (T, n)) / np.sqrt(5 / 3)
    x = sharpe * vol / 252 + vol / np.sqrt(252) * z
    eq = np.cumprod(1 + x, axis=0)
    return (eq / np.maximum.accumulate(eq, axis=0) - 1).min(axis=0)


bt = r.loc[:"2015-12-31", "degradada"]
sharpe_bt = bt.mean() / bt.std() * 252**0.5
print(f"Backtest 2006-2015: Sharpe {sharpe_bt:.2f},"
      f" máxima caída {caidas(bt).min():.1%}")
filas = {}
for anos in (3.5, 10):
    m = max_caidas(1.0, 0.08, anos)
    filas[f"{anos} años"] = {
        "mediana": np.median(m), "p90": np.quantile(m, 0.1),
        "p99": np.quantile(m, 0.01),
        "P(peor que -16,4 %)": (m < -0.164).mean()}
pd.DataFrame(filas).T.round(3)
```
```output
Backtest 2006-2015: Sharpe 1.14, máxima caída -16.4%
          mediana    p90    p99  P(peor que -16,4 %)
3.5 años   -0.094 -0.147 -0.214                0.059
10 años    -0.127 -0.188 -0.262                0.200
```

En el backtest de 2006-2015, `degradada` tiene un Sharpe de 1,14 y una caída máxima del 16,4 %. Para una estrategia
sana con esos parámetros, la mediana de la peor caída en 3,5 años es del 9,4 % y en 10 años del 12,7 %, pero en 10
años la supera el 20 % de las trayectorias, y el percentil 1 llega al −26,2 %. Fijar el kill switch en «la caída
del backtest» apaga una de cada cinco estrategias sanas en 10 años. Y eso con rendimientos independientes: con
agrupamiento de volatilidad, como el de los datos, la cola es más gruesa.

## Reducir la exposición con el drawdown

La regla más extendida no apaga nada: reduce el tamaño a medida que la cuenta cae desde su máximo. La versión de la
lección hace la exposición proporcional al **colchón** que queda hasta una caída máxima tolerada $D$, en el espíritu
de Grossman y Zhou (1993), que demostraron que invertir en el activo con riesgo una cantidad proporcional a
$W_t - (1 - D)\,M_t$, con $M_t$ el máximo del capital, garantiza en tiempo continuo que la caída nunca pase de $D$:

$$
e_{t+1} = \min\left(1,\ \max\left(\text{piso},\ 1 - \frac{DD_t}{D}\right)\right), \qquad DD_t = 1 - \frac{W_t}{M_t}
$$

La exposición de mañana se decide con el cierre de hoy (sin anticipación) y el drawdown es el de la propia cuenta
gestionada con la regla, no el de la estrategia sin regla. Primero sobre la cartera equiponderada de las nueve
estrategias sanas, con el doble de nominal para que tenga una volatilidad del 9 %:

```python
def regla_caida(x, D=0.20, piso=0.0):
    """Exposición de mañana = 1 - drawdown de hoy / D, entre piso y 1.
    El drawdown es el de la propia cuenta gestionada con la regla."""
    w = np.empty(len(x)); res = np.empty(len(x))
    eq, maximo, expo = 1.0, 1.0, 1.0
    for i, ri in enumerate(x.to_numpy()):
        w[i] = expo
        res[i] = expo * ri
        eq *= 1 + res[i]
        maximo = max(maximo, eq)
        expo = float(np.clip(1 - (1 - eq / maximo) / D, piso, 1.0))
    return pd.Series(res, x.index), pd.Series(w, x.index)


def metricas(x):
    anos = len(x) / 252
    return {"sharpe": x.mean() / x.std() * 252**0.5,
            "cagr": (1 + x).prod() ** (1 / anos) - 1,
            "max_caida": caidas(x).min(),
            "peor_mes": (1 + x).resample("ME").prod().min() - 1}


nueve = list(r.columns[:9])
cartera = 2 * r[nueve].mean(axis=1)   # equiponderada, al doble
filas = {"sin regla": metricas(cartera)}
for D in (0.10, 0.15, 0.20):
    res, w = regla_caida(cartera, D)
    filas[f"D = {D:.0%}"] = metricas(res) | {"expo_media": w.mean()}
pd.DataFrame(filas).T.round(3)
```
```output
           sharpe   cagr  max_caida  peor_mes  expo_media
sin regla   0.841  0.074     -0.274    -0.115         NaN
D = 10%     0.777  0.037     -0.098    -0.064       0.455
D = 15%     0.843  0.050     -0.135    -0.070       0.630
D = 20%     0.854  0.056     -0.161    -0.074       0.723
```

La cartera al doble tiene un Sharpe de 0,84, un CAGR del 7,4 % y una caída máxima del 27,4 %. La regla hace lo que
promete con la caída: −9,8 % con $D = 10\ \%$, −13,5 % con 15 % y −16,1 % con 20 %, por debajo de $D$ en los tres
casos (en tiempo discreto no está garantizado: un salto de un día puede pasar del colchón). El precio es la rentabilidad: la
exposición media baja al 46 %, 63 % y 72 % y el CAGR al 3,7 %, 5,0 % y 5,6 %. El Sharpe apenas cambia (0,78, 0,84 y
0,85): la regla no mejora la estrategia, la hace más pequeña cuando va mal. Sobre una cartera diversificada eso es
casi lo mismo que tener menos nominal. Estrategia a estrategia, el resultado es menos benigno:

```python
filas = {}
for c in r.columns:
    base = metricas(r[c])
    res, w = regla_caida(r[c], 0.20)
    con = metricas(res)
    filas[c] = {"cagr": base["cagr"], "cagr_regla": con["cagr"],
                "caida": base["max_caida"],
                "caida_regla": con["max_caida"],
                "expo_media": w.mean(),
                "expo_2025": w["2025"].mean()}
pd.DataFrame(filas).T.round(3)
```
```output
                    cagr  cagr_regla  caida  caida_regla  expo_media  expo_2025
tendencia_futuros  0.053       0.037 -0.356       -0.177       0.491      0.212
carry_futuros      0.026       0.014 -0.275       -0.162       0.613      0.900
carry_divisas      0.045       0.028 -0.352       -0.182       0.564      0.898
venta_puts         0.030       0.006 -0.490       -0.192       0.363      0.275
pares              0.022       0.015 -0.224       -0.144       0.652      0.853
momentum_acciones  0.041       0.011 -0.277       -0.175       0.416      0.462
valor_acciones     0.021       0.005 -0.244       -0.165       0.530      0.711
venta_volatilidad  0.028      -0.010 -0.489       -0.198       0.132      0.041
reversion_corto    0.030       0.020 -0.243       -0.149       0.637      0.758
degradada          0.054       0.059 -0.434       -0.187       0.736      0.077
```

La caída queda entre el 14 % y el 20 % en todas, pero el CAGR baja en nueve de diez. En la venta de puts pasa del
3,0 % al 0,6 % y en la de momentum del 4,1 % al 1,1 %. El peor caso es la venta de volatilidad: con una exposición
media del 13 % (del 4 % en 2025), termina perdiendo un 1,0 % al año. Es la **trampa del drawdown**: tras la caída de
2008 la exposición queda cerca de cero, la cuenta gestionada apenas se mueve y nunca recupera su máximo, así que la
exposición nunca vuelve. La figura lo enseña: la estrategia sin regla termina con 1,74 veces su capital; con la
regla, con 0,82.

*Fig. — Venta de volatilidad con y sin una regla que reduce la exposición con el drawdown (D = 20 %): la regla corta la caída, pero la exposición no se recupera.* Dos paneles, 2006-2025. Arriba, el capital en escala logarítmica: sin regla termina en 1.74 veces el inicial con caídas de casi la mitad; con la regla (destacada) la caída máxima se queda en un 20 % pero termina en 0.82. Abajo, la exposición de la regla: cae por debajo del 5 % en la crisis de 2008, vuelve a subir despacio hasta un 43 % en enero de 2018, el desplome de febrero de 2018 la deja en un 6 % y desde mediados de 2020 no pasa del 12 %.

La única estrategia que mejora es `degradada` (CAGR del 5,4 % al 5,9 %, caída del 43,4 % al 18,7 %): la regla la
reduce al 8 % de exposición en 2025 sin saber que está muerta. Es la ventaja de una regla de drawdown frente a un
kill switch: actúa sin necesidad de diagnosticar. Contra la trampa hay dos remedios: un **piso** de exposición o
reiniciar el máximo tras un tiempo (ejercicio 2). Con un piso:

```python
x = r["venta_volatilidad"]
filas = {"sin regla": metricas(x)}
for piso in (0.0, 0.25, 0.5):
    res, w = regla_caida(x, 0.20, piso)
    filas[f"piso {piso:.0%}"] = metricas(res) | {
        "expo_media": w.mean()}
pd.DataFrame(filas).T.round(3)
```
```output
           sharpe   cagr  max_caida  peor_mes  expo_media
sin regla   0.253  0.028     -0.489    -0.244         NaN
piso 0%    -0.266 -0.010     -0.198    -0.088       0.132
piso 25%    0.004 -0.002     -0.240    -0.196       0.329
piso 50%    0.194  0.014     -0.310    -0.203       0.557
```

Un piso del 25 % devuelve un Sharpe de 0,00 con una caída del 24,0 %; uno del 50 %, un Sharpe de 0,19 y una caída del
31,0 %, aún por debajo del 0,25 de la estrategia sin regla. Cada punto de piso compra rentabilidad con caída: no hay
un valor que dé las dos cosas, y elegirlo con estos mismos datos sería ajustar la regla a la muestra (lección 2.5.1).
La regla además rota posición cada día; con costes (lección 2.4.7) el resultado empeora algo más.

## Circuit breakers: pausar tras un día malo

Un circuit breaker no mira la racha sino el día: si la pérdida supera un umbral, la estrategia se para unas sesiones.
Las bolsas los usan para el mercado entero; un sistema los usa para sí mismo. La versión de la lección mide el umbral
en unidades de la volatilidad reciente (EWMA con $\lambda = 0{,}94$, calculada con los datos de la víspera): una
pérdida de más de 3 sigmas para la estrategia 5 sesiones. Con datos diarios el breaker solo puede actuar **después**
del día malo: lo que evita son los días siguientes.

```python
def breaker(x, k=3.0, pausa=5, lam=0.94):
    """Si la pérdida del día supera k veces la volatilidad EWMA de la
    víspera, la estrategia no opera las `pausa` sesiones siguientes."""
    sig = np.sqrt(x.pow(2).ewm(alpha=1 - lam).mean().shift(1))
    sig = sig.bfill().to_numpy()
    xv = x.to_numpy(); res = np.zeros(len(x)); w = np.zeros(len(x))
    parada, disparos = 0, []
    for i in range(len(x)):
        if parada == 0:
            w[i] = 1.0
            res[i] = xv[i]
            if xv[i] < -k * sig[i]:
                parada = pausa
                disparos.append(x.index[i])
        else:
            parada -= 1
    return pd.Series(res, x.index), pd.Series(w, x.index), disparos


filas = {}
for c in r.columns:
    res, w, disp = breaker(r[c])
    a, b = metricas(r[c]), metricas(res)
    filas[c] = {"disparos": len(disp), "sharpe": a["sharpe"],
                "sharpe_breaker": b["sharpe"],
                "caida": a["max_caida"],
                "caida_breaker": b["max_caida"]}
pd.DataFrame.from_dict(filas, orient="index").round(3)
```
```output
                   disparos  sharpe  sharpe_breaker  caida  caida_breaker
tendencia_futuros        32   0.533           0.519 -0.356         -0.359
carry_futuros            34   0.381           0.365 -0.275         -0.255
carry_divisas            36   0.529           0.576 -0.352         -0.314
venta_puts               31   0.305           0.402 -0.490         -0.497
pares                    37   0.401           0.537 -0.224         -0.166
momentum_acciones        36   0.388           0.389 -0.277         -0.293
valor_acciones           41   0.272           0.226 -0.244         -0.251
venta_volatilidad        38   0.253           0.351 -0.489         -0.477
reversion_corto          36   0.453           0.407 -0.243         -0.246
degradada                35   0.697           0.679 -0.434         -0.410
```

El breaker dispara entre 31 y 41 veces en 20 años por estrategia, unas dos veces al año: un día de 3 sigmas no es
raro con colas gruesas. El efecto es pequeño y de signo variable. Mejora el Sharpe en las estrategias con agrupamiento
de pérdidas y eventos encadenados (venta de puts de 0,31 a 0,40, pares de 0,40 a 0,54, venta de volatilidad de 0,25 a
0,35) y lo empeora en las que rebotan tras un mal día (reversión de 0,45 a 0,41, valor de 0,27 a 0,23). La caída
máxima apenas cambia salvo en pares (del 22,4 % al 16,6 %), porque las grandes caídas son lentas y un breaker de 5
sesiones no las ve. El episodio que mejor lo explica es marzo de 2020 en la venta de volatilidad:

```python
x = r["venta_volatilidad"]
res, w, disp = breaker(x)
tramo = slice("2020-02-20", "2020-04-30")
print([d.date().isoformat() for d in disp if d.year == 2020])
print(f"Sin breaker: {(1 + x[tramo]).prod() - 1:.1%}")
print(f"Con breaker: {(1 + res[tramo]).prod() - 1:.1%}")
dias_fuera = x[tramo][w[tramo] == 0]
print(f"Días fuera: {len(dias_fuera)}, lo que hicieron:"
      f" {(1 + dias_fuera).prod() - 1:.1%}")
```
```output
['2020-02-24', '2020-03-09', '2020-07-29']
Sin breaker: -35.7%
Con breaker: -17.5%
Días fuera: 10, lo que hicieron: -22.0%
```

En marzo de 2020 el breaker de la venta de volatilidad dispara el 24 de febrero y el 9 de marzo. Pasa 10 sesiones
fuera, que habrían perdido un 22,0 %, y deja la pérdida del tramo en un 17,5 % frente al 35,7 %. Es su mejor caso:
dos días malos que anuncian otros peores. En los mercados que rebotan tras el pánico, el mismo breaker se pierde la
subida, y por eso en reversión y valor empeora. Un breaker diario no sustituye a un tamaño correcto; reduce el daño
de los episodios que se encadenan.

## Kill switch estadístico: ¿ha muerto la estrategia?

Las reglas anteriores protegen el capital; no dicen si la estrategia sigue teniendo ventaja. Para eso hace falta un
contraste secuencial: cada día llega un rendimiento nuevo y la pregunta es si la media ha bajado. El **CUSUM** de Page
(1954) acumula la evidencia de una bajada y la olvida cuando los rendimientos vuelven a ser buenos. Con los
rendimientos en unidades de su volatilidad, $z_t = r_t / \sigma$, una media esperada $\mu_0$ (la del backtest) y una
media alternativa $\mu_1 = 0$ (la estrategia muerta):

$$
C_t = \max\left(0,\ C_{t-1} + k - z_t\right), \qquad k = \frac{\mu_0 + \mu_1}{2} = \frac{\mu_0}{2}, \qquad \text{alarma si } C_t > h
$$

Mientras la estrategia está viva, $z_t$ tiene media $\mu_0 > k$ y $C_t$ vuelve a cero una y otra vez; cuando muere, el
incremento medio pasa a $+k$ y $C_t$ crece hasta cruzar $h$. Todo depende de lo pequeño que es $\mu_0$: con Sharpe 1,
$\mu_0 = 1/\sqrt{252} = 0{,}063$ desviaciones típicas por día. El umbral $h$ fija el compromiso entre falsas alarmas
y retraso, y ese compromiso se mide simulando.

La simulación usa 2.000 trayectorias de 30 años de una estrategia de Sharpe 1 con innovaciones t de Student: en unas
la media sigue igual (para contar falsas alarmas) y en otras cae a cero el primer día (para medir el retraso).

```python
def cusum(z, k):
    """CUSUM de Page para detectar una bajada de la media de z.
    z: matriz (sesiones, trayectorias) en unidades de sigma."""
    c = np.zeros(z.shape[1]); out = np.empty_like(z)
    for t in range(z.shape[0]):
        c = np.maximum(0.0, c + k - z[t])
        out[t] = c
    return out


def primera_alarma(c, h):
    hit = c > h
    return np.where(hit.any(axis=0), hit.argmax(axis=0), c.shape[0])


rng = np.random.default_rng(11)
S0, T, n = 1.0, 252 * 30, 2000
mu0 = S0 / np.sqrt(252)
e = rng.standard_t(5, (T, n)) / np.sqrt(5 / 3)
viva = cusum(mu0 + e, mu0 / 2)      # la media sigue en mu0
muerta = cusum(e, mu0 / 2)          # la media cae a cero en t = 0
filas = {}
for h in (10, 20, 30, 40, 50, 60):
    fa, det = primera_alarma(viva, h), primera_alarma(muerta, h)
    filas[h] = {"P(falsa alarma en 10 años)": (fa < 2520).mean(),
                "retraso_mediano_anos": np.median(det) / 252,
                "retraso_p90_anos": np.quantile(det, 0.9) / 252}
compromiso = pd.DataFrame(filas).T
compromiso.round(2)
```
```output
    P(falsa alarma en 10 años)  retraso_mediano_anos  retraso_p90_anos
10                        1.00                  0.31              0.84
20                        0.98                  0.95              2.47
30                        0.69                  1.80              4.40
40                        0.37                  2.81              6.47
50                        0.18                  3.88              8.46
60                        0.09                  5.02             10.89
```

La tabla de la simulación es el centro de la lección y está en la figura de abajo. Con $h = 10$ la falsa alarma en
10 años es segura y la detección llega en 0,3 años de mediana; con $h = 50$, la falsa alarma baja al 18 % y la
detección tarda 3,9 años de mediana (8,5 en el percentil 90); con $h = 60$, 9 % y 5 años. Un umbral con pocas falsas
alarmas necesita años para ver una estrategia muerta, y la culpa no es del CUSUM, que tiene el menor retraso en el peor caso para un tiempo medio entre falsas alarmas
dado cuando el tamaño del cambio es conocido[^2]: es que la señal es pequeña.

[^2]: G. Lorden, «Procedures for reacting to a change in distribution», Annals of Mathematical Statistics 42 (1971),
1897-1908, DOI 10.1214/aoms/1177693055, demostró la optimalidad asintótica; G. V. Moustakides, «Optimal stopping times
for detecting changes in distributions», Annals of Statistics 14 (1986), 1379-1387, DOI 10.1214/aos/1176350164, la
exacta con el mismo criterio. La lección no lo demuestra. Lo que sí mide es que con colas gruesas y agrupamiento de
volatilidad, como en los datos, las falsas alarmas son más frecuentes que en la simulación.

*Fig. — CUSUM para una estrategia de Sharpe 1 que pasa a Sharpe 0: subir el umbral h reduce las falsas alarmas y alarga la detección. Ningún umbral da las dos cosas.* Curva con seis umbrales del CUSUM. h = 10: falsa alarma casi segura en 10 años y detección en 0,3 años. h = 20: 98 % y 1 año. h = 30: 69 % y 1,8 años. h = 40 (destacado): 37 % y 2,8 años. h = 50: 18 % y 3,9 años. h = 60: 9 % y 5 años.

¿Y con otro Sharpe? Para Sharpe 0,5, 1 y 2 se busca el umbral más bajo que deja las falsas alarmas en 10 años en un
20 % como mucho, y se mide el retraso con ese umbral (con 1.000 de las trayectorias, para no alargar la celda):

```python
filas = {}
for S0 in (0.5, 1.0, 2.0):
    mu0 = S0 / np.sqrt(252)
    hs = np.arange(5, 400, 5)
    viva = cusum(mu0 + e[:, :1000], mu0 / 2)
    muerta = cusum(e[:, :1000], mu0 / 2)
    # el umbral más bajo con P(falsa alarma en 10 años) <= 20 %
    h = next(h for h in hs
             if (primera_alarma(viva, h) < 2520).mean() <= 0.20)
    det = primera_alarma(muerta, h)
    filas[S0] = {"h": h, "retraso_mediano_anos": np.median(det) / 252,
                 "sin_detectar_en_30": (det == T).mean()}
pd.DataFrame(filas).T.round(2)
```
```output
        h  retraso_mediano_anos  sin_detectar_en_30
0.5  65.0                  7.96                0.02
1.0  50.0                  3.89                0.00
2.0  35.0                  1.56                0.00
```

El Sharpe manda. Con la misma tolerancia (como mucho un 20 % de falsas alarmas en 10 años), una estrategia de Sharpe 2
se detecta en 1,6 años de mediana, una de Sharpe 1 en 3,9 y una de Sharpe 0,5 en 8, y un 2 % de las de Sharpe 0,5 no
se detectan en 30 años. Para las estrategias típicas de este nivel, con Sharpe entre 0,3 y 0,5, un kill switch basado
en el rendimiento no es una herramienta: no hay muestra suficiente en la vida útil de la estrategia.

Ahora, los datos. Supón que `degradada` se investigó con 2006-2015 y empezó a operar el 4 de enero de 2016. Muere el
1 de julio de 2019 sin que nadie lo sepa:

```python
eq = (1 + r["degradada"]).cumprod()
CORTE = pd.Timestamp("2019-07-01")
cima = eq[CORTE:].idxmax()
print(f"Del corte a su máximo ({cima.date()}): "
      f"{eq[cima] / eq[:CORTE].iloc[-2] - 1:+.1%}")
print(f"Del máximo al final de 2025: {eq.iloc[-1] / eq[cima] - 1:+.1%}")
media_verdad = verdad.loc["degradada", "media_diaria_desde_corte"]
print(f"Media anual tras el corte: "
      f"{r.loc[CORTE:, 'degradada'].mean() * 252:+.1%}"
      f" (verdad: {media_verdad})")
```
```output
Del corte a su máximo (2021-06-08): +26.4%
Del máximo al final de 2025: -38.6%
Media anual tras el corte: -3.5% (verdad: 0.0)
```

Del día de su muerte a junio de 2021 gana un 26,4 %: dos años de suerte con media cero. Después pierde un 38,6 %. La
media de todo el tramo muerto es del −3,5 % anual, por debajo de la verdad (cero). Ninguna regla puede distinguir los
dos primeros años de una estrategia viva, porque no hay nada que distinguir. Se comparan cuatro reglas desde 2016: el
CUSUM con $h = 40$ calibrado con el Sharpe y la volatilidad verdaderos de cada estrategia (en real se usarían los del
backtest: ejercicio 1), una caída de más de 1,5 veces la del backtest, el Sharpe de 12 meses negativo y el de 36
meses negativo:

```python
VIVO = "2016-01-04"


def reglas(c, h=40):
    x = r[c]
    vivo = x[VIVO:]
    S0 = verdad.loc[c, "sharpe_objetivo"]
    sd = verdad.loc[c, "vol_objetivo"] / np.sqrt(252)
    k = S0 / np.sqrt(252) / 2
    cs = pd.Series(cusum((vivo / sd).to_numpy()[:, None], k)[:, 0],
                   vivo.index)
    dd_bt = caidas(x[:"2015-12-31"]).min()
    s12 = (x.rolling(252).mean() / x.rolling(252).std())[VIVO:]
    s36 = (x.rolling(756).mean() / x.rolling(756).std())[VIVO:]
    alarmas = {f"cusum_h{h}": cs > h,
               "caida>1,5xbt": caidas(vivo) < 1.5 * dd_bt,
               "sharpe12m<0": s12 < 0, "sharpe36m<0": s36 < 0}
    return {k: (a.idxmax().date() if a.any() else None)
            for k, a in alarmas.items()}


alarmas = pd.DataFrame({c: reglas(c) for c in r.columns}).T
alarmas
```
```output
                    cusum_h40 caida>1,5xbt sharpe12m<0 sharpe36m<0
tendencia_futuros  2018-09-28         None  2017-04-27  2016-02-24
carry_futuros            None         None  2016-06-03  2018-06-05
carry_divisas            None         None  2016-01-04        None
venta_puts         2020-03-16         None  2016-01-04  2021-09-01
pares                    None         None  2017-01-25  2016-01-25
momentum_acciones  2022-05-27         None  2016-08-10  2019-02-01
valor_acciones           None         None  2016-01-04  2017-07-18
venta_volatilidad  2020-03-13         None  2016-04-04  2020-03-23
reversion_corto          None         None  2017-05-04        None
degradada          2022-10-03   2022-10-24  2022-01-11  2022-10-11
```

Las cuatro reglas detectan `degradada` en 2022: el Sharpe de 12 meses el 11 de enero, el CUSUM el 3 de octubre, el
Sharpe de 36 meses el 11 de octubre y la caída de 1,5 veces la del backtest el 24 de octubre. Entre dos años y medio y
tres años y cuatro meses después de su muerte, como predecía la simulación. Sus falsas alarmas son muy distintas:

- **Sharpe de 12 meses negativo**: dispara en las nueve sanas, la mayoría en 2016. Una estrategia de Sharpe 0,4 tiene un
  año negativo con una probabilidad de un tercio: la regla es ruido.
- **Sharpe de 36 meses negativo**: dispara en siete de nueve.
- **CUSUM con h = 40**: dispara en cuatro de nueve: la tendencia (septiembre de 2018), la venta de volatilidad (13 de
  marzo de 2020), la venta de puts (16 de marzo de 2020) y el momentum (mayo de 2022).
- **Caída de 1,5 veces la del backtest**: no dispara en ninguna sana, porque sus backtests incluyen 2008 y sus caídas
  ya eran enormes. Por la misma razón, solo vigila de verdad a una estrategia con un backtest tranquilo.

Si el kill switch es definitivo, lo que cuesta se mide con la rentabilidad acumulada de 2016-2025 con y sin él:

```python
def apagada(c, fecha):
    x = r[c][VIVO:]
    return x if fecha is None else x.where(x.index <= str(fecha), 0.0)


filas = {}
for c in r.columns:
    f = alarmas.loc[c, "cusum_h40"]
    base, con = r[c][VIVO:], apagada(c, f)
    filas[c] = {"alarma": f, "total_2016_25": (1 + base).prod() - 1,
                "con_kill_switch": (1 + con).prod() - 1}
coste = pd.DataFrame.from_dict(filas, orient="index")
coste["diferencia"] = coste["con_kill_switch"] - coste["total_2016_25"]
print(coste.round(3))
print(f"Suma de diferencias, nueve sanas: "
      f"{coste['diferencia'].iloc[:9].sum():+.3f}")
```
```output
                       alarma  total_2016_25  con_kill_switch  diferencia
tendencia_futuros  2018-09-28          0.293           -0.082      -0.375
carry_futuros            None          0.403            0.403       0.000
carry_divisas            None          1.348            1.348       0.000
venta_puts         2020-03-16          0.816            0.460      -0.356
pares                    None          0.564            0.564       0.000
momentum_acciones  2022-05-27          0.352           -0.148      -0.501
valor_acciones           None          0.285            0.285       0.000
venta_volatilidad  2020-03-13          0.901            0.440      -0.462
reversion_corto          None          0.770            0.770       0.000
degradada          2022-10-03          0.199            0.673       0.474
Suma de diferencias, nueve sanas: -1.694
```

El kill switch salva 47 puntos de rentabilidad acumulada en `degradada` (del 19,9 % al 67,3 %) y cuesta 169 puntos en
las sanas: la tendencia pasa del +29,3 % al −8,2 %, el momentum del +35,2 % al −14,8 %, la venta de puts del +81,6 % al
+46,0 % y la de volatilidad del +90,1 % al +44,0 %. Las dos ventas de prima se apagan en el fondo de marzo de 2020: el
kill switch realiza la pérdida y renuncia al rebote. Con una estrategia muerta entre diez, el kill switch por
rendimiento pierde dinero; con más estrategias muertas en la cartera la cuenta cambiaría, y en la realidad la
proporción no se conoce.

Lo que sale de aquí no es «no uses kill switch», sino tres reglas de uso. Un kill switch de rendimiento solo tiene
sentido para estrategias de Sharpe alto, donde la detección cabe en su vida útil. Para el resto, la defensa es el
tamaño (lección 4.2.2), la exposición por drawdown y vigilar el **mecanismo** de la estrategia (el IC de la señal, el
deslizamiento, el funding que cobra un carry), que cambia antes que el P&L. Y una alarma no debería apagar, sino
pasar la estrategia a revisión con menos tamaño: la decisión de retirarla es humana.

## El kill switch operativo

El 1 de agosto de 2012, el sistema de enrutado de Knight Capital procesó 212 órdenes pequeñas de clientes con un
código antiguo que había quedado activo en uno de sus ocho servidores. En unos 45 minutos mandó millones de órdenes
hijas, obtuvo más de 4 millones de ejecuciones en 154 valores por más de 397 millones de acciones y terminó con una
posición larga de unos 3.500 millones de dólares y una corta de unos 3.150 millones. Perdió más de 460 millones[^1]. El
servidor seguía mandando hijas sin tener en cuenta las ejecuciones que ya había recibido. Ninguna regla estadística
habría servido: hacía falta un control que comparase lo ejecutado con lo pedido y lo parase en segundos.

[^1]: SEC, Release No. 70694, de 16 de octubre de 2013, puntos 1 y 15-17. La SEC sancionó a Knight por no tener
controles y procedimientos razonables para limitar los riesgos de su acceso al mercado.

En la UE, el Reglamento Delegado (UE) 2017/589 (las normas técnicas de la negociación algorítmica de MiFID II) obliga
a las empresas de servicios de inversión que hacen negociación algorítmica a poder «anular de inmediato, como medida
de emergencia, cualquiera o la totalidad de las órdenes no ejecutadas» (artículo 12, «función cortacircuito»); a
tener controles prenegociación de bandas de precio, valor y volumen máximos por orden y límites de mensajes
(artículo 15.1); y a que, tras un número predeterminado de ejecuciones repetidas, el sistema se desactive «hasta que
vuelva a ser reactivado por un miembro designado del personal» (artículo 15.3). Un particular no está sujeto a ese
reglamento, pero el diseño es el que conviene copiar. Como máquina de estados:

```mermaid
stateDiagram-v2
  [*] --> ACTIVO
  ACTIVO --> DISPARADO: límite roto
  DISPARADO --> REVISION: órdenes anuladas
  REVISION --> ACTIVO: rearme manual
  REVISION --> [*]: retirada
```

Tres decisiones de diseño. La hora se **inyecta**: el kill switch no lee el reloj del sistema, recibe la hora con cada
evento, así que sus pruebas son deterministas y se pueden reproducir con los mensajes de un día real. Los límites son
**independientes de la estrategia**: no preguntan si la señal es buena, solo si lo que pasa es posible. Y el rearme
es **manual**: lo que dispara el control suele ser algo que nadie entiende todavía.

```python
from dataclasses import dataclass, field


@dataclass
class Limites:
    perdida_diaria: float = 0.03      # fracción del patrimonio
    ordenes_por_minuto: int = 30
    datos_viejos_s: float = 10.0
    exposicion_bruta: float = 1.5     # veces el patrimonio


@dataclass
class KillSwitch:
    patrimonio_inicio_dia: float
    lim: Limites = field(default_factory=Limites)
    estado: str = "ACTIVO"
    motivo: str = ""
    envios: list = field(default_factory=list)

    def _disparar(self, motivo):
        if self.estado == "ACTIVO":
            self.estado, self.motivo = "DISPARADO", motivo

    def revisar(self, ahora, patrimonio, exposicion, ultimo_dato,
                pedido, ejecutado):
        """Comprueba todos los límites con la hora que le pasan."""
        perdida = 1 - patrimonio / self.patrimonio_inicio_dia
        if perdida > self.lim.perdida_diaria:
            self._disparar(f"pérdida diaria {perdida:.1%}")
        if exposicion / patrimonio > self.lim.exposicion_bruta:
            self._disparar("exposición bruta")
        if ahora - ultimo_dato > self.lim.datos_viejos_s:
            self._disparar("datos sin actualizar")
        for orden, q in ejecutado.items():
            if q > pedido.get(orden, 0):
                self._disparar(f"{orden}: ejecutado {q} > pedido")
        return self.estado

    def enviar(self, ahora, orden):
        if self.estado != "ACTIVO":
            raise RuntimeError(f"bloqueado ({self.motivo})")
        self.envios = [t for t in self.envios if ahora - t < 60]
        if len(self.envios) >= self.lim.ordenes_por_minuto:
            self._disparar("demasiadas órdenes por minuto")
            raise RuntimeError(f"bloqueado ({self.motivo})")
        self.envios.append(ahora)
        return orden

    def rearmar(self, quien):
        if self.estado == "DISPARADO" and quien:
            self.estado, self.motivo = "ACTIVO", ""
        return self.estado
```

`revisar` se llama con cada evento (cotización, ejecución, cambio de patrimonio) y `enviar` antes de cada orden.
Las pruebas no usan el reloj del sistema: la hora es un número de segundos de un reloj lógico que avanza el guion.

```python
ks = KillSwitch(patrimonio_inicio_dia=100_000)
t = 1_000.0          # segundos de un reloj lógico, no del sistema
print(ks.revisar(t, 99_000, 120_000, t - 1, {"A1": 100},
                 {"A1": 100}))
print(ks.revisar(t + 5, 99_000, 120_000, t + 4, {"A1": 100},
                 {"A1": 100, "A2": 300}), "-", ks.motivo)
print(ks.rearmar("operador"))
for i in range(30):
    ks.enviar(t + 10 + i, f"O{i}")
try:
    ks.enviar(t + 45, "O30")
except RuntimeError as err:
    print(err)
print(ks.rearmar("operador"))
print(ks.revisar(t + 200, 96_500, 50_000, t + 199, {}, {}),
      "-", ks.motivo)
```
```output
ACTIVO
DISPARADO - A2: ejecutado 300 > pedido
ACTIVO
bloqueado (demasiadas órdenes por minuto)
ACTIVO
DISPARADO - pérdida diaria 3.5%
```

Primero, todo en orden. Después llega una ejecución de una orden `A2` que el sistema nunca pidió: es el caso Knight,
y el control dispara. Rearmado a mano, el sistema envía 30 órdenes en 30 segundos y la 31.ª, seis segundos después,
supera el límite de órdenes por minuto. Rearmado otra vez, el patrimonio baja un 3,5 % en el día y vuelve a disparar.
Mientras está disparado, cualquier envío lanza una excepción:

```python raises
ks.enviar(t + 300, "O31")
```
```output
RuntimeError: bloqueado (pérdida diaria 3.5%)
```

En un sistema real (lecciones 4.5.1 y 4.5.5) este objeto vive en un proceso aparte del de la estrategia, recibe los
mensajes del bróker directamente y, al disparar, manda la cancelación de todas las órdenes abiertas. Si vive en el
mismo proceso, un fallo de la estrategia puede llevárselo por delante.

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

Un kill switch no protege de lo que no vigila. Las órdenes que ya están en el mercado siguen vivas hasta que se
anulan; una posición abierta sigue perdiendo aunque no se envíen órdenes nuevas, y cerrarla en un mercado roto puede
costar más que esperar. Decide de antemano qué hace el sistema al disparar: anular, cerrar o solo bloquear.

</aside>

## Errores típicos

- **La caída del backtest como límite.** Es una muestra; una estrategia sana la supera a menudo.
- **El Sharpe del backtest como hipótesis nula.** El backtest suele estar inflado por la selección (lección 2.5.4): un
  CUSUM calibrado con él dispara en estrategias que funcionan como de verdad funcionaban.
- **Reducir sin plan de vuelta.** Una regla de drawdown sin piso ni reinicio deja la cuenta en la trampa.
- **Anticipación en el control.** El breaker que se aplica al mismo día que lo dispara, o el drawdown calculado con el
  cierre del día que se opera, dan un backtest que el sistema real no puede repetir.
- **Kill switch de rendimiento en estrategias de cola.** Las ventas de prima pierden en las crisis por diseño: un
  control de rendimiento las apaga en el fondo.
- **Rearme automático y control en el mismo proceso.** El control tiene que sobrevivir al fallo que vigila y no
  deshacerse solo.
- **Probar con el reloj del sistema.** Las pruebas de un control de tiempos tienen que dar lo mismo con la máquina
  cargada; con `time.time()` dentro, no lo hacen.

## Ejercicios

1. Calibra el CUSUM ($h = 40$) con el Sharpe y la volatilidad del backtest de 2006-2015 de cada estrategia, en lugar
   de los verdaderos, y compara las alarmas desde 2016 con las de la lección. ¿Qué pasa con las estrategias cuyo
   backtest salió peor que su verdad?

**Solución.**


```python
def alarma_bt(c, h=40):
    bt, vivo = r[c][:"2015-12-31"], r[c][VIVO:]
    k = bt.mean() / bt.std() / 2
    cs = cusum((vivo / bt.std()).to_numpy()[:, None], k)[:, 0]
    fila = {"sharpe_bt": bt.mean() / bt.std() * 252**0.5,
            "sharpe_verdad": verdad.loc[c, "sharpe_objetivo"]}
    hit = np.flatnonzero(cs > h)
    fila["alarma_bt"] = vivo.index[hit[0]].date() if len(hit) else None
    fila["alarma_verdad"] = alarmas.loc[c, "cusum_h40"]
    return fila


pd.DataFrame.from_dict({c: alarma_bt(c) for c in r.columns},
                       orient="index").round(2)
```
```output
                   sharpe_bt  sharpe_verdad   alarma_bt alarma_verdad
tendencia_futuros       0.76           0.45  2018-09-06    2018-09-28
carry_futuros           0.29           0.40        None          None
carry_divisas           0.07           0.35        None          None
venta_puts              0.05           0.40  2020-03-18    2020-03-16
pares                   0.02           0.50        None          None
momentum_acciones       0.50           0.35  2022-03-30    2022-05-27
valor_acciones          0.22           0.25        None          None
venta_volatilidad       0.02           0.50  2020-03-16    2020-03-13
reversion_corto         0.06           0.40        None          None
degradada               1.14           1.00  2022-09-23    2022-10-03
```

Las alarmas apenas cambian de fecha, pero por razones que conviene ver. Las crisis de 2007-2009 están dentro del
backtest, así que seis de las nueve sanas tienen un Sharpe de backtest muy por debajo del verdadero (la venta de puts,
0,05 frente a 0,40; pares, 0,02 frente a 0,50). Con $k$ casi cero, el incremento medio del CUSUM de una estrategia
viva es $-\mu$, más negativo que con la verdad: el control se vuelve más tolerante, y con una estrategia muerta
(media cero) el incremento medio sería cero, así que solo dispararía por azar. Un backtest pesimista deja el kill
switch casi ciego. Con un backtest optimista pasa lo contrario: la tendencia (0,76 frente a 0,45) y el momentum (0,50
frente a 0,35) disparan antes (6 de septiembre de 2018 y 30 de marzo de 2022), y `degradada` (1,14 frente a 1), diez
días antes. El parámetro que más importa del kill switch es el que peor se estima.

2. Modifica `regla_caida` para que, si la exposición lleva 126 sesiones por debajo del 25 %, el máximo se reinicie al
   capital de ese día (se acepta la pérdida y se empieza de nuevo). Compárala con la regla sin reinicio en la venta de
   volatilidad y en `degradada`.

**Solución.**


```python
def regla_reinicio(x, D=0.20, umbral=0.25, sesiones=126):
    w = np.empty(len(x)); res = np.empty(len(x))
    eq, maximo, expo, bajas = 1.0, 1.0, 1.0, 0
    for i, ri in enumerate(x.to_numpy()):
        w[i] = expo
        res[i] = expo * ri
        eq *= 1 + res[i]
        maximo = max(maximo, eq)
        bajas = bajas + 1 if expo < umbral else 0
        if bajas >= sesiones:          # se acepta la pérdida
            maximo, bajas = eq, 0
        expo = float(np.clip(1 - (1 - eq / maximo) / D, 0.0, 1.0))
    return pd.Series(res, x.index), pd.Series(w, x.index)


filas = {}
for c in ("venta_volatilidad", "degradada"):
    for nombre, f in (("sin reinicio", regla_caida),
                      ("con reinicio", regla_reinicio)):
        res, w = f(r[c])
        filas[(c, nombre)] = metricas(res) | {"expo_media": w.mean()}
pd.DataFrame(filas).T.round(3)
```
```output
                                sharpe   cagr  max_caida  peor_mes  expo_media
venta_volatilidad sin reinicio  -0.266 -0.010     -0.198    -0.088       0.132
                  con reinicio   0.278  0.025     -0.270    -0.190       0.597
degradada         sin reinicio   0.974  0.059     -0.187    -0.082       0.736
                  con reinicio   0.887  0.055     -0.264    -0.082       0.792
```

El reinicio saca a la venta de volatilidad de la trampa: la exposición media sube del 13 % al 60 % y el Sharpe de
−0,27 a 0,28 (algo mejor que el 0,25 sin regla), a cambio de una caída del 27,0 %, mayor que $D$, porque cada reinicio
empieza a contar desde un capital más bajo. En `degradada`, que ya estaba muerta, el reinicio le devuelve exposición en
el peor momento: la caída pasa del 18,7 % al 26,4 % y el Sharpe de 0,97 a 0,89. Ninguna versión es mejor en las dos:
la regla de drawdown protege de las rachas, no distingue una racha de una muerte.

3. Diseña los límites del kill switch operativo para una cuenta de 50.000 EUR que opera tres estrategias diarias con
   órdenes a mercado al cierre (unas 10 órdenes al día). ¿Qué límites pondrías y qué haría el sistema al dispararse?

**Solución.**

Una propuesta razonable, a ajustar con los datos de cada sistema: pérdida diaria del 3 % del patrimonio (unas 3 veces
la volatilidad diaria de una cartera de un 15 % anual, un día que debería darse muy pocas veces al año); exposición
bruta de 1,5 veces el patrimonio si las estrategias no usan más; un máximo de 30 órdenes por minuto y de 50 por día
(el triple de lo normal, que para un sistema así ya es una anomalía); datos de más de 60 segundos de antigüedad en la
ventana de envío; ejecutado mayor que pedido en cualquier orden; y diferencias entre la posición propia y la del
bróker (lección 4.5.5). Al dispararse: anular todas las órdenes abiertas, no enviar más, avisar por un canal que no
dependa del propio sistema (lección 4.5.4) y esperar al rearme manual. Cerrar posiciones solo si el disparo es por
exposición o por ejecuciones que no se pidieron; si es por datos viejos, cerrar a ciegas puede ser peor que esperar.

## Autoevaluación

1. El backtest de una estrategia (2006-2015) tiene una máxima caída del 16,4 %. En real, a los ocho años, cae un 18 %. ¿Qué indica eso por sí solo?

   - A. Que la estrategia ha dejado de funcionar: ha superado su peor caída
   - B. Poco: con su Sharpe y su volatilidad, una estrategia sana supera esa caída en 10 años una de cada cinco veces
   - C. Que el backtest tenía un error de código
   - D. Que hay que doblar la exposición para recuperar

   *Respuesta B: Poco: con su Sharpe y su volatilidad, una estrategia sana supera esa caída en 10 años una de cada cinco veces.* La caída del backtest es una muestra de una distribución. En la simulación de la lección, una estrategia sana de Sharpe 1 y 8 % de volatilidad cae más de un 16,4 % en 10 años en el 20 % de las trayectorias, y la mediana de su peor caída en 10 años es del 12,7 %.

2. Una regla reduce la exposición a 1 − drawdown / 20 %. ¿Cuál es su riesgo principal?

   - A. Que tras una caída profunda la exposición queda cerca de cero y la cuenta ya no puede recuperarse: en la venta de volatilidad, una media del 13 % de exposición y un Sharpe negativo
   - B. Que no limita la caída
   - C. Que aumenta la volatilidad
   - D. Que necesita datos intradía

   *Respuesta A: Que tras una caída profunda la exposición queda cerca de cero y la cuenta ya no puede recuperarse: en la venta de volatilidad, una media del 13 % de exposición y un Sharpe negativo.* La exposición es proporcional al colchón que queda hasta la caída tolerada. Si la cuenta se queda cerca del suelo, opera tan poco que solo una racha muy larga la saca de ahí. Un piso de exposición o un reinicio del máximo lo evita, a cambio de dejar pasar caídas mayores.

3. Un CUSUM con h = 40 vigila una estrategia de Sharpe 1. Según la simulación, ¿qué se puede esperar?

   - A. Detectar la muerte de la estrategia en unas semanas sin falsas alarmas
   - B. Ninguna falsa alarma nunca
   - C. Un 37 % de probabilidad de falsa alarma en 10 años y una detección que tarda unos 2,8 años de mediana
   - D. Detección inmediata si la estrategia pierde un día

   *Respuesta C: Un 37 % de probabilidad de falsa alarma en 10 años y una detección que tarda unos 2,8 años de mediana.* Es el compromiso de la figura: el umbral decide cuántas falsas alarmas se aceptan y cuánto tarda la detección. Con Sharpe 1 la diferencia entre una estrategia viva y una muerta es de 0,06 desviaciones típicas por día: hacen falta años para verla.

4. En los datos, el CUSUM de h = 40 apagó cuatro de las nueve estrategias sanas entre 2016 y 2025. ¿Por qué dos de ellas en marzo de 2020?

   - A. Porque dejaron de funcionar en 2020
   - B. Por un error en los datos
   - C. Porque el CUSUM solo funciona con datos semanales
   - D. Porque las ventas de prima pierden mucho en las crisis: el CUSUM acumula esas pérdidas y dispara justo en el fondo, antes de la recuperación

   *Respuesta D: Porque las ventas de prima pierden mucho en las crisis: el CUSUM acumula esas pérdidas y dispara justo en el fondo, antes de la recuperación.* La venta de puts y la de volatilidad tienen una cola izquierda gruesa por construcción. Un kill switch de rendimiento no distingue una crisis prevista de una estrategia rota, y apagar en el fondo realiza la pérdida y renuncia al rebote: en los datos, 36 y 46 puntos de rentabilidad acumulada.

5. ¿Qué debe hacer un kill switch operativo cuando se dispara, según la lección y el Reglamento Delegado (UE) 2017/589?

   - A. Esperar un minuto y reactivarse solo
   - B. Doblar la posición para compensar
   - C. Anular las órdenes no ejecutadas y bloquear el envío hasta que una persona designada lo reactive
   - D. Enviar un correo y seguir operando

   *Respuesta C: Anular las órdenes no ejecutadas y bloquear el envío hasta que una persona designada lo reactive.* El artículo 12 pide poder anular de inmediato las órdenes no ejecutadas («función cortacircuito») y el 15.3, que el sistema se desactive tras un número de ejecuciones repetidas hasta que lo reactive un miembro designado del personal. El rearme automático deshace el control.

## Ideas clave

- El control de pérdidas tiene cuatro niveles con horizontes distintos: el kill switch operativo (segundos, errores del sistema), el circuit breaker (días), la exposición por drawdown (semanas) y el kill switch estadístico (meses o años, la ventaja que desaparece).
- La máxima caída del backtest es una muestra, no el peor caso: una estrategia sana de Sharpe 1 la supera en 10 años en una de cada cinco trayectorias.
- Reducir la exposición con el drawdown limita la caída a costa de rentabilidad, y tiene una trampa: tras una caída profunda la exposición no se recupera. En la venta de volatilidad, la caída pasa del 48,9 % al 19,8 % y el Sharpe de 0,25 a −0,27.
- Detectar que una estrategia ha muerto es lento: con Sharpe 1 y un 37 % de falsas alarmas en 10 años, el CUSUM tarda 2,8 años de mediana; con Sharpe 0,5, unos 8. La estrategia muerta de los datos ganó un 26 % después de morir y las cuatro reglas la detectaron en 2022.
- Un kill switch de rendimiento cuesta en las estrategias sanas: en los datos apagó cuatro de nueve, dos de ellas en el fondo de marzo de 2020, y la suma de lo perdido (−169 puntos) supera lo que salvó en la muerta (+47).
- El kill switch operativo es software independiente, con reloj inyectado, límites de pérdida, de exposición, de órdenes por minuto, de datos viejos y de cantidad ejecutada, y rearme manual.

## Recursos

### Research

- [Volatility Targeting en Python](https://pythonparatrading.com/volatility-targeting/) · Research-0004 · 8 min

### Documentación

- [Reglamento Delegado (UE) 2017/589 (RTS 6): requisitos de la negociación algorítmica, función cortacircuito (art. 12) y controles prenegociación (art. 15)](https://eur-lex.europa.eu/legal-content/ES/TXT/?uri=CELEX:32017R0589) · eur-lex.europa.eu
- [SEC, Release No. 70694 (2013): el caso Knight Capital del 1 de agosto de 2012](https://www.sec.gov/litigation/admin/2013/34-70694.pdf) · sec.gov

### Libros

- [E. S. Page, «Continuous inspection schemes», Biometrika 41 (1954), 100-115 (DOI)](https://doi.org/10.1093/biomet/41.1-2.100) · doi.org
- [S. J. Grossman y Z. Zhou, «Optimal investment strategies for controlling drawdowns», Mathematical Finance 3 (1993), 241-276 (DOI)](https://doi.org/10.1111/j.1467-9965.1993.tb00044.x) · doi.org

### Datos

- [Datos del nivel 4: rendimientos diarios de diez estrategias (Parquet)](https://pythonparatrading.com/curso/datos/nivel-4/estrategias4_diarias.parquet) · Descarga · estrategias4_diarias.parquet
- [Datos del nivel 4: parámetros verdaderos de las estrategias (CSV)](https://pythonparatrading.com/curso/datos/nivel-4/estrategias4_verdad.csv) · Descarga · estrategias4_verdad.csv

Lección anterior: [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/)

Lección siguiente: [4.2.6 · Riesgo de derivados: griegas de cartera, margen y liquidaciones](https://pythonparatrading.com/curso-python-trading/nivel-4/riesgo-de-derivados-griegas-margen-y-liquidaciones/)
