Lección 4.2.5 · 65 min

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.

Fig. — La estrategia «degradada» desde 2016: deja de funcionar el 1 de julio de 2019, sigue ganando dos años por azar y las reglas de kill switch solo la detectan en 2022
Lecciones del nivel 4
  1. 4.1 · Teoría y optimización de carteras

    1. 4.1.1 Markowitz: frontera eficiente y restricciones reales
    2. 4.1.2 Estimación de covarianzas: shrinkage, modelos factoriales y denoising
    3. 4.1.3 Black-Litterman: del equilibrio del mercado a las visiones
    4. 4.1.4 Risk parity, mínima varianza, HRP y HERC
    5. 4.1.5 Optimización con CVaR, drawdown, rotación y costes
    6. 4.1.6 Modelos factoriales: CAPM, Fama-French, PCA y atribución
    7. 4.1.7 Rebalanceo y carteras de estrategias
    8. 4.1.8 Herramientas de carteras: Riskfolio-Lib, PyPortfolioOpt, skfolio y bt
  2. 4.2 · Gestión del riesgo y tamaño de posición

    1. 4.2.1 Riesgo por operación, stops y riesgo de ruina
    2. 4.2.2 Criterio de Kelly y volatility targeting: cuánto arriesgar
    3. 4.2.3 VaR, Expected Shortfall y backtesting del VaR
    4. 4.2.4 Stress testing y riesgos de correlación, liquidez, gap y contraparte
    5. 4.2.5 Control del drawdown y kill switch
    6. 4.2.6 Riesgo de derivados: griegas de cartera, margen y liquidaciones
  3. 4.3 · Microestructura y ejecución

    1. 4.3.1 Implementation shortfall, VWAP, TWAP y Almgren-Chriss
    2. 4.3.2 Order flow imbalance, señales de microestructura y latencia
  4. 4.4 · Brokers y APIs

    1. 4.4.1 Elegir bróker desde España o la UE: regulación, activos, costes y API
    2. 4.4.2 Interactive Brokers: TWS e IB Gateway, ib_async, contratos, datos y órdenes
    3. 4.4.3 Alpaca, Tradier, MetaTrader 5 con Python y OANDA v20
    4. 4.4.4 APIs de exchanges cripto: ccxt, Binance, Bybit, Kraken y Coinbase
    5. 4.4.5 Credenciales seguras: variables de entorno, gestores de secretos y permisos mínimos
  5. 4.5 · Sistema de trading en vivo

    1. 4.5.1 Arquitectura de un sistema en vivo: de los datos a la reconciliación
    2. 4.5.2 Gestión de órdenes: estados, fills parciales, cancelaciones y reconexiones
    3. 4.5.3 Paper trading, shadow trading y seguimiento del slippage
    4. 4.5.4 Programación de tareas y alertas: cron, APScheduler, Prefect, Telegram, Slack y email
    5. 4.5.5 Tolerancia a fallos: idempotencia, estado persistente y reconciliación al arrancar
    6. 4.5.6 NautilusTrader o LEAN en real: del backtest a producción con el mismo código

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:

DefensaHorizonteQué detectaCómo se equivoca
Kill switch operativoSegundosÓrdenes que no deberían existir, datos viejos, pérdidas fuera de escalaPara el sistema por un dato malo del bróker
Circuit breakerDíasUn día de pérdida fuera de lo normalSe pierde el rebote del día siguiente
Exposición por drawdownSemanas a mesesUna racha de pérdidasOpera poco justo cuando la estrategia se recupera
Kill switch estadísticoMeses a añosUna ventaja que ha desaparecidoApaga 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:

nosíactivodisparadoSeñalEscala por caídaBreaker en pausa?Kill switchSin operarOrden al broker
Fuente 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:

IN [01]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)
OUT [01]Diez estrategias y sus caídas
                   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:

IN [02]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)
OUT [02]El drawdown de una estrategia sana: simulación
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 DD, en el espíritu de Grossman y Zhou (1993), que demostraron que invertir en el activo con riesgo una cantidad proporcional a Wt−(1−D) MtW_t - (1 - D)\,M_t, con MtM_t el máximo del capital, garantiza en tiempo continuo que la caída nunca pase de DD:

et+1=min⁡(1, max⁡(piso, 1−DDtD)),DDt=1−WtMte_{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 %:

IN [03]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)
OUT [03]Exposición según el drawdown
           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 %D = 10\ \%, −13,5 % con 15 % y −16,1 % con 20 %, por debajo de DD 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:

IN [04]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)
OUT [04]La misma regla, estrategia a estrategia
                    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

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:

IN [05]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)
OUT [05]Un piso de exposición
           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 λ=0,94\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.

IN [06]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)
OUT [06]Circuit breaker: pausa tras una pérdida de 3 sigmas
                   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:

IN [07]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%}")
OUT [07]Marzo de 2020 con y sin breaker
['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, zt=rt/σz_t = r_t / \sigma, una media esperada μ0\mu_0 (la del backtest) y una media alternativa μ1=0\mu_1 = 0 (la estrategia muerta):

Ct=max⁡(0, Ct−1+k−zt),k=μ0+μ12=μ02,alarma si Ct>hC_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, ztz_t tiene media μ0>k\mu_0 > k y CtC_t vuelve a cero una y otra vez; cuando muere, el incremento medio pasa a +k+k y CtC_t crece hasta cruzar hh. Todo depende de lo pequeño que es μ0\mu_0: con Sharpe 1, μ0=1/252=0,063\mu_0 = 1/\sqrt{252} = 0{,}063 desviaciones típicas por día. El umbral hh 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).

IN [08]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)
OUT [08]CUSUM: retraso frente a falsas alarmas
    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=10h = 10 la falsa alarma en 10 años es segura y la detección llega en 0,3 años de mediana; con h=50h = 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=60h = 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 conocido1Nota 1 · 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.: es que la señal es pequeña.

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

¿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):

IN [09]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)
OUT [09]Con menos Sharpe, más retraso
        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:

IN [10]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})")
OUT [10]Lo que hizo degradada después de morir
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=40h = 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:

IN [11]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
OUT [11]Cuatro reglas sobre los datos, desde 2016
                    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:

IN [12]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}")
OUT [12]Lo que cuesta el kill switch
                       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 millones2Nota 2 · 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.. 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.

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:

límite rotoórdenes anuladasrearme manualretiradaACTIVODISPARADOREVISION
Fuente 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.

IN [13]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.

IN [14]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)
OUT [14]Pruebas con eventos guionizados
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:

IN [15]python
ks.enviar(t + 300, "O31")
OUT [15]Bloqueado hasta que alguien lo rearme
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.

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=40h = 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
IN [16]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)
OUT [16]CUSUM con los parámetros del backtest
                   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 kk 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.

  1. 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
IN [17]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)
OUT [17]Regla de drawdown con reinicio del máximo
                                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 DD, 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.

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

Footnotes

  1. 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. ↩

  2. 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. ↩

Respuesta plegada en cada pregunta

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?

    • AQue la estrategia ha dejado de funcionar: ha superado su peor caída
    • BPoco: con su Sharpe y su volatilidad, una estrategia sana supera esa caída en 10 años una de cada cinco veces
    • CQue el backtest tenía un error de código
    • DQue hay que doblar la exposición para recuperar
    Ver respuesta

    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?

    • AQue 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
    • BQue no limita la caída
    • CQue aumenta la volatilidad
    • DQue necesita datos intradía
    Ver respuesta

    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?

    • ADetectar la muerte de la estrategia en unas semanas sin falsas alarmas
    • BNinguna falsa alarma nunca
    • CUn 37 % de probabilidad de falsa alarma en 10 años y una detección que tarda unos 2,8 años de mediana
    • DDetección inmediata si la estrategia pierde un día
    Ver respuesta

    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?

    • APorque dejaron de funcionar en 2020
    • BPor un error en los datos
    • CPorque el CUSUM solo funciona con datos semanales
    • DPorque 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
    Ver respuesta

    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?

    • AEsperar un minuto y reactivarse solo
    • BDoblar la posición para compensar
    • CAnular las órdenes no ejecutadas y bloquear el envío hasta que una persona designada lo reactive
    • DEnviar un correo y seguir operando
    Ver respuesta

    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