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.
- 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, 2.1.7 · Detección de regímenes: modelos de Markov ocultos y cambios estructurales, 2.5.4 · Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO), 4.2.1 · Riesgo por operación, stops y riesgo de ruina, 4.2.2 · Criterio de Kelly y volatility targeting: cuánto arriesgar
Lecciones del nivel 4
4.1 · Teoría y optimización de carteras
- 4.1.1 Markowitz: frontera eficiente y restricciones reales
- 4.1.2 Estimación de covarianzas: shrinkage, modelos factoriales y denoising
- 4.1.3 Black-Litterman: del equilibrio del mercado a las visiones
- 4.1.4 Risk parity, mínima varianza, HRP y HERC
- 4.1.5 Optimización con CVaR, drawdown, rotación y costes
- 4.1.6 Modelos factoriales: CAPM, Fama-French, PCA y atribución
- 4.1.7 Rebalanceo y carteras de estrategias
- 4.1.8 Herramientas de carteras: Riskfolio-Lib, PyPortfolioOpt, skfolio y bt
4.2 · Gestión del riesgo y tamaño de posición
- 4.2.1 Riesgo por operación, stops y riesgo de ruina
- 4.2.2 Criterio de Kelly y volatility targeting: cuánto arriesgar
- 4.2.3 VaR, Expected Shortfall y backtesting del VaR
- 4.2.4 Stress testing y riesgos de correlación, liquidez, gap y contraparte
- 4.2.5 Control del drawdown y kill switch
- 4.2.6 Riesgo de derivados: griegas de cartera, margen y liquidaciones
4.3 · Microestructura y ejecución
4.4 · Brokers y APIs
- 4.4.1 Elegir bróker desde España o la UE: regulación, activos, costes y API
- 4.4.2 Interactive Brokers: TWS e IB Gateway, ib_async, contratos, datos y órdenes
- 4.4.3 Alpaca, Tradier, MetaTrader 5 con Python y OANDA v20
- 4.4.4 APIs de exchanges cripto: ccxt, Binance, Bybit, Kraken y Coinbase
- 4.4.5 Credenciales seguras: variables de entorno, gestores de secretos y permisos mínimos
4.5 · Sistema de trading en vivo
- 4.5.1 Arquitectura de un sistema en vivo: de los datos a la reconciliación
- 4.5.2 Gestión de órdenes: estados, fills parciales, cancelaciones y reconexiones
- 4.5.3 Paper trading, shadow trading y seguimiento del slippage
- 4.5.4 Programación de tareas y alertas: cron, APScheduler, Prefect, Telegram, Slack y email
- 4.5.5 Tolerancia a fallos: idempotencia, estado persistente y reconciliación al arrancar
- 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:
| 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:
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 --> NLa 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:
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) 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.55Las 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:
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)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.200En 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 , en el espíritu de Grossman y Zhou (1993), que demostraron que invertir en el activo con riesgo una cantidad proporcional a , con el máximo del capital, garantiza en tiempo continuo que la caída nunca pase de :
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 %:
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) 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.723La 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 , −13,5 % con 15 % y −16,1 % con 20 %, por debajo de 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:
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) 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.077La 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.
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:
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) 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.557Un 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 , 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.
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) 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.410El 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:
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%}")['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, , una media esperada (la del backtest) y una media alternativa (la estrategia muerta):
Mientras la estrategia está viva, tiene media y vuelve a cero una y otra vez; cuando muere, el incremento medio pasa a y crece hasta cruzar . Todo depende de lo pequeño que es : con Sharpe 1, desviaciones típicas por día. El umbral 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).
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) 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.89La tabla de la simulación es el centro de la lección y está en la figura de abajo. Con la falsa alarma en 10 años es segura y la detección llega en 0,3 años de mediana; con , la falsa alarma baja al 18 % y la detección tarda 3,9 años de mediana (8,5 en el percentil 90); con , 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.
¿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):
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) 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.00El 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:
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})")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 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:
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 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-11Las 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:
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}") 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.694El 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:
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.
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.estadorevisar 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.
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)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:
ks.enviar(t + 300, "O31")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
- Calibra el CUSUM () 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
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) 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-03Las 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 casi cero, el incremento medio del CUSUM de una estrategia
viva es , 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.
- Modifica
regla_caidapara 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 endegradada.
Solución
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) 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.792El 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 , 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.
- 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
-
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. ↩
-
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
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 %.
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.
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.
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.
¿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.