Lección 2.2.1 · 60 min
Hipótesis de ventaja: riesgo, comportamiento, estructura e información
Un sistema de trading gana dinero solo si alguien, al otro lado, acepta perderlo o pagarlo. Esta lección ordena las cuatro razones por las que eso ocurre y mide cuatro ventajas plantadas en los datos del nivel, con su rentabilidad, su asimetría, sus crashes y su tamaño.
- Módulo
- 2.2 · Anatomía de un sistema de trading
- Duración
- 60 min
Lecciones del nivel 2
2.1 · Estadística y econometría para trading
- 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
- 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
- 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
- 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
- 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
- 2.1.6 Filtro de Kalman para hedge ratios dinámicos
- 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
- 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
2.2 · Anatomía de un sistema de trading
- 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
- 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
- 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
- 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
2.3 · Familias de estrategias
- 2.3.1 Seguimiento de tendencia y momentum
- 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
- 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
- 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
- 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
- 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
- 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
2.4 · Backtesting: motores y paradigmas
- 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
- 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
- 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
- 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
- 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
- 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
- 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
- 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
2.5 · Sesgos y validación
- 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
- 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
- 2.5.3 Validación cruzada combinatoria purgada para estrategias
- 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
- 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
- 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
- 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
- 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
2.6 · Métricas de rendimiento
Un edge es una hipótesis, no un patrón
Una ventaja (edge) es una razón para esperar que una regla de trading gane dinero en el futuro, después de costes. La palabra importante es «razón». Un patrón encontrado en un backtest («las acciones que suben el martes siguen subiendo el miércoles») no es una ventaja: es una observación, y la mayoría de las observaciones de un backtest son azar (lecciones 2.1.8 y 2.2.4). Para convertirla en hipótesis hay que contestar tres preguntas:
- ¿Quién está al otro lado? Toda operación tiene contraparte. Si el sistema gana, alguien gana menos o pierde.
- ¿Por qué lo acepta? Porque cobra algo a cambio (se quita un riesgo de encima), porque se equivoca, porque está obligado o porque todavía no sabe lo que el sistema ya ha descontado.
- ¿Por qué no lo ha corregido nadie? Arbitrarlo tiene un riesgo, un coste o un límite de capital que los demás no quieren o no pueden asumir.
Las respuestas a la segunda pregunta dan una taxonomía de cuatro fuentes:
Fuente Mermaid
flowchart TB E[Ventaja] --> R[Riesgo] E --> C[Comportamiento] E --> S[Estructura] E --> I[Información] R --> R1[Carry] C --> C1[Momentum] S --> S1[Rebalanceos] I --> I1[Deriva PEAD]
| Fuente | Por qué paga la contraparte | Ejemplo en los datos del nivel | Qué predice la hipótesis |
|---|---|---|---|
| Prima de riesgo | Se quita un riesgo que no quiere soportar y paga por ello | Carry de divisas: largo en tipos altos, corto en tipos bajos | Persiste, pero con pérdidas grandes en las crisis (asimetría negativa) |
| Comportamiento | Reacciona tarde o de más a las noticias, o vende lo que pierde demasiado pronto | Momentum 12-1 de acciones | Mayor donde arbitrar cuesta; puede menguar; crashes en los rebotes |
| Estructura | Una norma o un mandato le obliga a operar en una fecha o a un precio | Altas y bajas de un índice | Concentrada en fechas conocidas; poca capacidad; compite con quien conoce el calendario |
| Información | Aún no ha incorporado una noticia pública al precio | Deriva tras resultados (PEAD) | Decae con el tiempo tras la noticia y con la velocidad del mercado |
La clasificación no es rígida. El momentum tiene explicaciones de comportamiento (infrarreacción a las noticias) y de riesgo (sus crashes llegan cuando el mercado rebota tras una crisis, el peor momento); el carry, además de prima, puede leerse como una restricción de los inversores que no pueden endeudarse en divisas baratas. Lo que importa es que cada explicación predice algo distinto y comprobable: dónde debería ser mayor la ventaja, cuándo debería fallar y cómo debería ser la distribución de sus rentabilidades.
Los datos: efectos plantados y un placebo
Los datos del nivel 2 son sintéticos y llevan efectos con parámetros conocidos (/curso/datos/nivel-2/README.md). Esta
lección mide cuatro de ellos y un control:
- Carry de divisas (
fx2_carry.parquet): la divisa de tipo alto solo devuelve el 30 % del diferencial en el tipo de cambio, y en cinco tramos de estrés las divisas de tipo alto caen de golpe. - Momentum (
universo2_caracteristicas.parquet): prima mensual de0,0025 × zsobre la rentabilidad 12-1, con un coeficiente negativo en los tres meses tras los suelos de 2009 y de 2020. - Rebalanceos de índice (
eventos2_rebalanceos.csv): las altas suben un 3 % entre el anuncio y la fecha efectiva y las bajas caen otro tanto; en las 20 sesiones siguientes revierten la mitad. - Deriva tras resultados (
eventos2_resultados.csv): durante 60 sesiones tras la reacción, el precio deriva en la dirección de la sorpresa. - Placebo: la
puntuacion_esges un AR(1) independiente de todo. Su prima verdadera es cero.
Todas las ventajas se comparan con la misma vara: una serie de rentabilidades mensuales de una cartera, y un mismo resumen de su perfil.
import numpy as np
import pandas as pd
def perfil(m):
"""m: rentabilidades mensuales (en tanto por uno)."""
sr = m.mean() / m.std() * np.sqrt(12)
return pd.Series({
"meses": len(m),
"rent_anual": 12 * m.mean(),
"vol_anual": np.sqrt(12) * m.std(),
"sharpe": sr,
"t": sr * np.sqrt(len(m) / 12),
"asimetria": m.skew(),
"curtosis": m.kurt(),
"peor_mes": m.min(),
"meses_pos": (m > 0).mean(),
})
def a_mensual(r_diaria):
return (1 + r_diaria).resample("ME").prod() - 1La t es el Sharpe anual por la raíz del número de años: la t de la media de las rentabilidades. La curtosis es la
curtosis en exceso (0 en una normal) y la asimetria, la de los meses.
Riesgo: el carry de divisas
La hipótesis del carry es una prima de riesgo. Quien presta en la divisa de tipo alto y se endeuda en la de tipo bajo cobra el diferencial de tipos. La paridad descubierta de intereses diría que la divisa de tipo alto se deprecia exactamente lo necesario para anularlo; si solo lo hace en parte, queda una ganancia media. La explicación de por qué dura es que el carry pierde mucho justo cuando todo pierde: en las crisis, los inversores deshacen sus posiciones a la vez y las divisas de tipo alto se desploman1Nota 1 · Brunnermeier, Nagel y Pedersen (2008), en los recursos, documentan la asimetría negativa del carry y la atribuyen a que las posiciones se deshacen de golpe cuando caen el apetito por el riesgo y la liquidez de financiación..
La cartera es la clásica: cada fin de mes, largo en las tres divisas de tipo más alto y corto en las tres de tipo más bajo, con pesos iguales. La rentabilidad diaria de estar largo en una divisa es la variación del tipo de cambio más el diferencial de tipos devengado (README, sección d).
fx = pd.read_parquet("fx2_carry.parquet")
spot = fx.pivot(index="fecha", columns="divisa", values="spot")
tipo = fx.pivot(index="fecha", columns="divisa", values="tipo_divisa")
usd = fx.groupby("fecha")["tipo_usd"].first()
dif = tipo.sub(usd, axis=0) # % anual
dias = spot.index.to_series().diff().dt.days
interes = dif.shift(1).div(100).mul(dias / 360, axis=0)
r_fx = spot.pct_change() + interes # largo en la divisa
# última sesión de cada mes: largo 3 de tipo alto, corto 3 de bajo
fin_mes = dif.groupby(dif.index.to_period("M")).tail(1)
rango = fin_mes.rank(axis=1)
peso = (rango >= 8).astype(float) / 3 - (rango <= 3) / 3
peso_d = peso.reindex(r_fx.index).ffill().shift(1)
carry_d = (peso_d * r_fx).sum(axis=1).loc["2005-02":]
carry = a_mensual(carry_d)
perfil(carry).round(3)meses 251.000
rent_anual 0.044
vol_anual 0.108
sharpe 0.409
t 1.871
asimetria -1.384
curtosis 7.389
peor_mes -0.199
meses_pos 0.574
dtype: float64El shift(1) final hace que el peso decidido con los tipos del cierre de un mes se aplique desde la sesión
siguiente: sin él, la cartera cobraría el día de la decisión una rentabilidad que aún no podía tener.
¿Dónde están las pérdidas? El README fija cinco tramos de crash. La suma de lo que gana o pierde la cartera dentro de cada tramo, y fuera de todos ellos:
tramos = [("2008-09-15", "2008-12-31"), ("2010-05-03", "2010-05-31"),
("2011-08-01", "2011-09-30"), ("2015-08-17", "2015-09-30"),
("2020-02-24", "2020-03-31")]
en_crash = pd.Series(False, index=carry_d.index)
for ini, fin in tramos:
en_crash.loc[ini:fin] = True
print(ini[:7], f"{carry_d.loc[ini:fin].sum():+.1%}")
print(f"sesiones de crash: {en_crash.mean():.1%} del total")
print(f"suma en crash: {carry_d[en_crash].sum():+.1%}")
print(f"suma fuera: {carry_d[~en_crash].sum():+.1%}")
carry.nsmallest(4).round(3)2008-09 -23.4%
2010-05 -7.5%
2011-08 -7.8%
2015-08 -4.3%
2020-02 -15.5%
sesiones de crash: 3.7% del total
suma en crash: -58.6%
suma fuera: +147.2%
fecha
2009-02-28 -0.199
2020-03-31 -0.124
2008-11-30 -0.083
2022-08-31 -0.077
dtype: float64La cartera gana un 4,4 % anual con un 10,8 % de volatilidad: Sharpe de 0,41 y t de 1,87, en el límite de lo que distingue una prima del azar con 21 años. La asimetría mensual es de −1,38 y la curtosis en exceso, de 7,4. Las sesiones de los cinco tramos de crash son el 3,7 % del total y en ellas la cartera suma −58,6 puntos; fuera, +147,2. Solo el tramo de 2008 se lleva 23,4 puntos y el de 2020, 15,5.
El perfil es el de quien vende un seguro: gana poco y casi siempre, y devuelve una parte grande de golpe. La figura lo enseña sobre la curva de la cartera. Dos detalles del resultado importan para un sistema real:
- El peor mes, febrero de 2009 (−19,9 %), no cae dentro de un tramo plantado, sino después: la volatilidad GARCH de las divisas de tipo alto sigue disparada tras el crash. Las crisis no terminan cuando termina la caída; el riesgo de cola dura más.
- Un backtest de cinco años sin ninguna crisis dentro enseñaría una curva casi recta y un Sharpe alto. La prima solo se ve completa con muestras que incluyen los episodios por los que se paga.
Comportamiento (o riesgo): el momentum
El momentum transversal compra las acciones que más han subido en los últimos doce meses, sin el último, y vende las que más han caído2Nota 2 · Jegadeesh y Titman (1993), en los recursos, es el estudio clásico de las carteras de ganadoras y perdedoras.. Las explicaciones de comportamiento dicen que los inversores infrarreaccionan a las noticias y que el precio tarda meses en reflejarlas; las de riesgo, que la cartera larga-corta pierde mucho en los rebotes tras una crisis, cuando los perdedores (muy castigados, con beta alta) suben más que nadie3Nota 3 · Daniel y Moskowitz (2016), en los recursos, estudian los crashes del momentum en los rebotes tras las caídas..
En los datos del nivel la señal es la de la primera hipótesis y el crash, el de la segunda. Con las rentabilidades
mensuales point-in-time de universo2_caracteristicas.parquet:
car = pd.read_parquet("universo2_caracteristicas.parquet")
rm = car.pivot(index="fecha", columns="ticker",
values="rentabilidad_mes")
hay = car.pivot(index="fecha", columns="ticker",
values="cierre").notna() # False: deslistado
# rentabilidad de m-11 a m-1 (se salta el último mes), solo
# donde hay precio al cierre de m
mom = np.expm1(np.log1p(rm.where(hay)).shift(1)
.rolling(11).sum()).where(hay)
sig_mes = rm.shift(-1) # m+1, con el deslistado incluido
ic = mom.rank(axis=1).corrwith(sig_mes.rank(axis=1), axis=1).dropna()
print(f"IC de rangos medio {ic.mean():.3f}, "
f"t = {ic.mean() / ic.std() * np.sqrt(len(ic)):.2f}")
def quintiles(senal, fut, excluir=()):
s, f = senal.drop(columns=list(excluir)), fut.drop(
columns=list(excluir))
rk = s.rank(axis=1, pct=True)
m = (f[rk > 0.8].mean(axis=1) - f[rk <= 0.2].mean(axis=1))
m = m.dropna()
# etiqueta: fin del mes del rendimiento (el siguiente)
sig = m.index.to_period("M") + 1
m.index = sig.to_timestamp() + pd.offsets.MonthEnd(0)
return m
momentum = quintiles(mom, sig_mes)
perfil(momentum).round(3)IC de rangos medio 0.028, t = 2.96
meses 227.000
rent_anual 0.101
vol_anual 0.120
sharpe 0.838
t 3.644
asimetria 0.136
curtosis 0.082
peor_mes -0.090
meses_pos 0.573
dtype: float64La señal y la rentabilidad futura no usan las mismas filas. El fichero tiene, para cada acción que deja de cotizar,
una última fila sin precio en el mes de su salida, con una rentabilidad_mes que encadena lo que va de mes con la
rentabilidad de deslistado (−100 % en las cuatro quiebras). Esa fila no sirve para formar una señal (ese día la acción
ya no se puede comprar), y por eso mom se calcula solo donde hay precio. Pero sí tiene que entrar en sig_mes: es
lo que gana o pierde quien tenía la acción en cartera el mes anterior. Un backtest que la descartara no vería ninguna
quiebra.
El IC de rangos (la correlación de Spearman entre la señal de un mes y la rentabilidad del siguiente, lección 2.3.5) mide la relación sin dejarse arrastrar por unos pocos valores extremos. La cartera de quintiles sí se deja: el README avisa de que cuatro quiebras guiadas caen durante meses hasta casi cero y acaban siempre en el quintil perdedor, del lado corto, donde el −100 % del deslistado es una ganancia del 100 %. Su aportación se mide quitándolas:
quiebras = ["QXQT", "QXKV", "QXZT", "QXYX"]
sin_q = quintiles(mom, sig_mes, excluir=quiebras)
print(f"con quiebras {12 * momentum.mean():.1%} anual, "
f"sin ellas {12 * sin_q.mean():.1%}")
crash = ["2009-04", "2009-05", "2009-06",
"2020-04", "2020-05", "2020-06"]
momentum.loc[momentum.index.strftime("%Y-%m").isin(crash)].round(3)con quiebras 10.1% anual, sin ellas 6.9%
fecha
2009-04-30 -0.060
2009-05-31 -0.031
2009-06-30 -0.058
2020-04-30 -0.044
2020-05-31 -0.006
2020-06-30 -0.059
dtype: float64El IC medio es de 0,028 con t de 2,96, cerca del +0,03 esperado. La cartera de quintiles gana un 10,1 % anual con Sharpe de 0,84; sin las quiebras, un 6,9 %: casi un tercio de la rentabilidad (3,2 puntos) viene de cuatro empresas, que en su último mes con precio están en el percentil 1 de la señal. Con datos reales pasa lo mismo con un signo peligroso: sin las rentabilidades de deslistado, o con un universo de supervivientes (lección 1.4.4), el lado corto parece peor de lo que fue, o desaparece.
Los seis meses plantados son negativos, pero ninguno es el peor de la serie (−9,0 %, en febrero de 2021). Junio de 2009 se queda en −5,8 % por una razón que no tiene que ver con la señal: ese mes deja de cotizar QXKV, una de las quiebras, y el lado corto cobra su −100 % mientras los perdedores rebotan. El crash de estos datos es moderado: la asimetría mensual de la cartera es de 0,14, casi nula, y la curtosis en exceso, de 0,08. En el momentum real los crashes han sido más violentos que aquí3; un modelo de datos lleva el riesgo de cola que su autor decidió ponerle.
Estructura: los rebalanceos del índice
Una ventaja estructural no depende de que nadie se equivoque: depende de que alguien está obligado a operar. Un fondo que replica un índice tiene que comprar las acciones que entran y vender las que salen al cierre de la fecha efectiva, y esa fecha se anuncia días antes. Quien compra entre el anuncio y la fecha efectiva vende después al indexador; si la demanda de acciones no es perfectamente elástica, el precio sube con la compra obligada y revierte en parte después4Nota 4 · Shleifer (1986), en los recursos, usa las inclusiones en el S&P 500 para contrastar si la demanda de acciones tiene pendiente negativa..
Cada evento se mide como la rentabilidad del valor desde el cierre del día del anuncio hasta el de la fecha efectiva, con signo (largo en altas, corto en bajas) y en exceso sobre la media del universo. Las bajas por deslistado no tienen efecto plantado y se quitan:
aj = pd.read_parquet("universo2_ajustados.parquet")
px = aj.pivot(index="fecha", columns="ticker",
values="cierre_ajustado")
r = px.pct_change(fill_method=None)
mercado = r.mean(axis=1)
sesiones = px.index
rb = pd.read_csv("eventos2_rebalanceos.csv",
parse_dates=["fecha_anuncio", "fecha_efectiva"])
rb = rb[rb["motivo"] != "deslistado"]
def evento(e):
ia = sesiones.searchsorted(e.fecha_anuncio)
ie = sesiones.searchsorted(e.fecha_efectiva)
s = 1 if e.tipo == "alta" else -1
p, m = px[e.ticker], (1 + mercado.iloc[ia + 1:ie + 1]).prod()
return pd.Series({
"sesiones": ie - ia,
"exceso": s * (p.iloc[ie] / p.iloc[ia] - m),
"despues_20": s * (p.iloc[ie + 20] / p.iloc[ie] - 1),
})
ev = pd.concat([rb, rb.apply(evento, axis=1)], axis=1)
ev.groupby("tipo")[["sesiones", "exceso", "despues_20"]].agg(
["mean", "count"]).round(3) sesiones exceso despues_20
mean count mean count mean count
tipo
alta 4.741 27 0.022 27 -0.012 27
baja 5.000 20 0.040 20 -0.003 20Las altas ganan en exceso un 2,2 % de media en 4,7 sesiones y las bajas, cortas, un 4,0 % en 5, frente al 3 % plantado: con 27 y 20 eventos y una desviación típica de 3-4 puntos por evento, el error de cada media es de 0,7 y 0,9 puntos. La reversión de las 20 sesiones siguientes sale con el signo esperado (−1,2 % y −0,3 % para la posición del evento), pero con un error de 2 puntos: con tan pocos eventos no se distingue de cero. Medir bien una ventaja de eventos exige muchos eventos.
Para compararla con las demás, la ventaja se convierte en una cartera diaria: largo en las altas y corto en las bajas mientras dura cada ventana, y en liquidez el resto del tiempo.
pos = pd.DataFrame(0.0, index=sesiones, columns=px.columns)
for e in ev.itertuples():
ia = sesiones.searchsorted(e.fecha_anuncio)
ie = sesiones.searchsorted(e.fecha_efectiva)
pos.iloc[ia + 1:ie + 1, px.columns.get_loc(e.ticker)] = (
1 if e.tipo == "alta" else -1)
n_pos = pos.abs().sum(axis=1)
r_exc = r.sub(mercado, axis=0).fillna(0)
rebal_d = (pos * r_exc).sum(axis=1) / n_pos.where(n_pos > 0, 1)
rebal = a_mensual(rebal_d)
print(f"sesiones con posición: {(n_pos > 0).mean():.1%}")
print(f"eventos al año: {len(ev) / 20:.1f}")
perfil(rebal).round(3)sesiones con posición: 2.4%
eventos al año: 2.4
meses 240.000
rent_anual 0.035
vol_anual 0.046
sharpe 0.755
t 3.375
asimetria 3.223
curtosis 14.768
peor_mes -0.046
meses_pos 0.092
dtype: float64El perfil es el contrario del carry: solo el 9,2 % de los meses son positivos (casi todos los demás valen cero), unos pocos ganan mucho y la asimetría es de +3,2. El Sharpe, de 0,75, engaña por otro lado: la cartera solo tiene posición en el 2,4 % de las sesiones, una de cada cuarenta. Esa es la capacidad de una ventaja estructural: pocas oportunidades, cortas y conocidas por todos los que leen el mismo calendario. Hay más capital persiguiéndolas cuanto más se publica el efecto, y su precio de entrada sube.
Información: la deriva tras resultados
La deriva tras resultados (PEAD, post-earnings-announcement drift) es la tendencia del precio a seguir moviéndose, semanas después de la presentación, en la dirección de la sorpresa5Nota 5 · Bernard y Thomas (1989), en los recursos, contrastan si la deriva es una respuesta retardada o una prima de riesgo.. La hipótesis es de información: el mercado incorpora despacio una noticia pública. Predice que la deriva es mayor cuanto mayor la sorpresa y que decae con el tiempo.
La sorpresa se tipifica con la de los trimestres anteriores (SUE: sorpresa dividida por la desviación típica de las 8 anteriores, con al menos 4). La reacción es la misma sesión si se publica antes de la apertura y la siguiente si es tras el cierre; la cartera entra al cierre de la reacción, para no contar el salto del día, y mide las 60 sesiones siguientes en exceso sobre el universo:
res = pd.read_csv("eventos2_resultados.csv",
parse_dates=["periodo_fin", "fecha_anuncio"])
res = res.sort_values(["ticker", "fecha_anuncio"])
previa = res.groupby("ticker")["sorpresa"].transform(
lambda s: s.shift(1).rolling(8, min_periods=4).std())
res["sue"] = res["sorpresa"] / previa
tras = (res["momento"] == "tras_cierre").to_numpy()
res["i"] = sesiones.searchsorted(res["fecha_anuncio"]) + tras
res = res.dropna(subset=["sue"])
res = res[res["i"] + 61 < len(sesiones)]
res["decil"] = res.groupby("periodo_fin")["sue"].transform(
lambda s: pd.qcut(s.rank(method="first"), 10, labels=False))
log_exc = np.log1p(r_exc).cumsum().to_numpy()
j = px.columns.get_indexer(res["ticker"])
i = res["i"].to_numpy()
res["deriva_60"] = np.expm1(log_exc[i + 60, j] - log_exc[i, j])
(100 * res.groupby("decil")["deriva_60"].mean()).round(2)decil
0 -1.05
1 -0.20
2 -0.31
3 0.54
4 -0.53
5 0.02
6 0.29
7 -0.11
8 0.97
9 1.41
Name: deriva_60, dtype: float64Los deciles extremos se separan: el de sorpresas más positivas deriva un 1,41 % al alza y el de las más negativas, un 1,05 % a la baja; la diferencia, 2,5 puntos, está dentro de los 2-3 del README. En medio la relación es ruidosa: la deriva plantada es pequeña por unidad de sorpresa, y la volatilidad de 60 sesiones de una acción, grande. Como cartera, largo en el decil alto y corto en el bajo durante sus 60 sesiones:
L = np.zeros(px.shape)
S = np.zeros(px.shape)
for ii, jj, d in zip(i, j, res["decil"]):
if d == 9:
L[ii + 1:ii + 61, jj] = 1
elif d == 0:
S[ii + 1:ii + 61, jj] = 1
R = r.fillna(0).to_numpy()
largo = (L * R).sum(1) / np.maximum(L.sum(1), 1)
corto = (S * R).sum(1) / np.maximum(S.sum(1), 1)
pead = a_mensual(pd.Series(largo - corto, index=sesiones))
print(f"eventos al año: {(res['decil'].isin([0, 9])).sum() / 20:.0f}")
perfil(pead.loc["2006-04":]).round(3)eventos al año: 82
meses 237.000
rent_anual 0.089
vol_anual 0.117
sharpe 0.768
t 3.412
asimetria -0.149
curtosis 0.296
peor_mes -0.100
meses_pos 0.595
dtype: float64La cartera gana un 8,9 % anual con Sharpe de 0,77, por detrás solo del momentum (0,84, que debe una parte a las quiebras), y es de las más diversificadas: 82 eventos al año, repartidos en fechas distintas, con una asimetría casi nula. Su riesgo no es un crash, sino que desaparezca: si la deriva es información que se incorpora despacio, cuanto más rápido y más vigilado está el mercado, menos deriva queda.
El perfil de riesgo de cada ventaja
Falta el control. La cartera de quintiles de la puntuacion_esg, una variable sin ningún efecto:
esg = car.pivot(index="fecha", columns="ticker",
values="puntuacion_esg").where(hay)
placebo = quintiles(esg, sig_mes)
ventajas = {"carry_fx": carry, "momentum": momentum,
"rebalanceos": rebal, "pead": pead.loc["2006-04":],
"placebo_esg": placebo}
tabla = pd.DataFrame({k: perfil(v) for k, v in ventajas.items()}).T
tabla[["rent_anual", "vol_anual", "sharpe", "t", "asimetria",
"curtosis", "peor_mes"]].round(2) rent_anual vol_anual sharpe t asimetria curtosis peor_mes
carry_fx 0.04 0.11 0.41 1.87 -1.38 7.39 -0.20
momentum 0.10 0.12 0.84 3.64 0.14 0.08 -0.09
rebalanceos 0.03 0.05 0.75 3.37 3.22 14.77 -0.05
pead 0.09 0.12 0.77 3.41 -0.15 0.30 -0.10
placebo_esg 0.03 0.10 0.33 1.46 -0.42 1.31 -0.12La figura de la cabecera pone las cinco en un plano: Sharpe en horizontal, asimetría de los meses en vertical. Tres lecturas:
- El Sharpe no ordena el riesgo. El carry (0,41) y el placebo (0,33) tienen un Sharpe parecido; el primero esconde un mes de −20 % y el segundo, uno de −12 %. El del placebo no es una cola de la señal: es julio de 2020, cuando QXYX quiebra estando, por azar, en su quintil alto. El del carry es la prima misma. Un Sharpe de 0,4 con asimetría −1,4 no es el mismo producto que uno de 0,3 con asimetría −0,4.
- El placebo pasa por una ventaja pequeña. Gana un 3,1 % anual con t de 1,46, sin ninguna prima detrás. Sin una hipótesis previa, una t así no distingue nada: con veinte variables sin efecto e independientes, la probabilidad de que alguna dé |t| > 2 ronda el 60 % (lección 2.1.8).
- La curtosis delata a la de eventos. Los rebalanceos tienen la curtosis más alta (14,8): muchos meses en cero y unos pocos grandes. Es un perfil distinto del carry aunque las dos tengan colas: una cola a favor y otra en contra.
La comparación de las distribuciones completas, con las rentabilidades mensuales tipificadas (divididas por su desviación típica) para que se vean en la misma escala:
q = [0.01, 0.05, 0.5, 0.95, 0.99]
z = {k: (v - v.mean()) / v.std() for k, v in ventajas.items()}
cuant = pd.DataFrame({k: v.quantile(q) for k, v in z.items()}).T
cuant["min"] = [v.min() for v in z.values()]
cuant["max"] = [v.max() for v in z.values()]
cuant.round(2) 0.01 0.05 0.5 0.95 0.99 min max
carry_fx -2.69 -1.51 -0.01 1.46 1.89 -6.49 2.48
momentum -2.09 -1.61 -0.06 1.72 2.52 -2.85 2.75
rebalanceos -1.18 -0.22 -0.22 1.84 4.39 -3.67 6.01
pead -2.54 -1.62 0.01 1.67 2.23 -3.20 2.77
placebo_esg -2.43 -1.69 0.05 1.55 2.29 -4.49 2.58El peor mes del carry está a 6,5 desviaciones típicas por debajo de su media; el de la deriva tras resultados, a 3,2, y el del momentum, a 2,85. El placebo tiene uno a 4,5: el de la quiebra de QXYX. Una cartera sin ventaja también cobra o paga los deslistados que le tocan, y quitarlos de los datos la haría parecer más limpia de lo que es. En una normal, un mes a −6,5σ tiene una probabilidad de : menos de una vez en mil millones de años. En el carry de estos datos ha ocurrido una vez en 21. Los rebalanceos son la imagen especular: su cola larga está a la derecha (+6,0σ). Una regla de sizing que use solo la volatilidad (lección 2.2.2) asigna el mismo riesgo a las dos y se equivoca con la primera.
La última dimensión es el tamaño. Un Sharpe alto sobre poco volumen negociable no admite capital. Con el volumen medio de 21 sesiones en dólares de lo que negocia cada cartera de acciones:
car["adv_musd"] = car["volumen_medio_21d"] * car["cierre"] / 1e6
adv = car.pivot(index="fecha", columns="ticker", values="adv_musd")
def adv_eventos(fechas, tickers):
"""ADV del último fin de mes anterior a cada evento."""
previo = adv.reindex(fechas, method="ffill")
return np.nanmedian([previo.iloc[k][t]
for k, t in enumerate(tickers)])
rk = mom.rank(axis=1, pct=True)
ext = res[res["decil"].isin([0, 9])]
liq = pd.DataFrame({
"apuestas_año": [12 * ((rk > 0.8) | (rk <= 0.2)).sum(axis=1)
.mean(), len(ev) / 20, len(ext) / 20],
"sesiones": [21, ev["sesiones"].mean(), 60],
"adv_musd": [adv[(rk > 0.8) | (rk <= 0.2)].stack().median(),
adv_eventos(ev["fecha_anuncio"], ev["ticker"]),
adv_eventos(ext["fecha_anuncio"], ext["ticker"])],
}, index=["momentum", "rebalanceos", "pead"])
liq.round(1) apuestas_año sesiones adv_musd
momentum 491.8 21.0 111.0
rebalanceos 2.4 4.9 20.0
pead 82.5 60.0 118.7El momentum reparte su riesgo entre unas 41 acciones cada mes (492 posiciones al año, aunque solapadas y no independientes) con un volumen mediano de 111 millones de USD al día. La deriva tras resultados abre 82 posiciones al año en acciones parecidas (119 millones). Los rebalanceos, 2,4 al año en acciones cinco veces menos líquidas (20 millones): las que entran en el índice vienen de fuera, de entre las pequeñas. Cuánto capital admite cada ventaja con estas cifras es el tema de la lección 2.2.3.
Cómo escribir una hipótesis de ventaja
Antes de abrir un backtest, la hipótesis se escribe. Una plantilla que obliga a contestar lo importante:
Fuente Mermaid
flowchart TB M[Mecanismo] --> C[Contraparte] C --> P[Predicciones] P --> T[Prueba y control] T --> F[Cuándo falla] F --> M
| Campo | Ejemplo: carry de divisas |
|---|---|
| Mecanismo | Prima por soportar el riesgo de crash de las divisas de tipo alto |
| Contraparte | Quien se cubre o no quiere ese riesgo; quien se endeuda en la divisa de tipo alto |
| Por qué persiste | Arbitrarlo exige soportar el mismo crash; el capital se retira justo cuando más rinde |
| Predicciones | Ganancia media positiva; asimetría negativa; pérdidas concentradas en las crisis; mayor con diferenciales mayores |
| Prueba | Cartera larga-corta por diferencial, rentabilidad dentro y fuera de los tramos de estrés |
| Control | Misma cartera con una variable sin relación (placebo) o con los diferenciales barajados |
| Cuándo falla | Crisis de liquidez; convergencia de tipos (sin diferencial no hay prima) |
| Capacidad | Mercados de divisas muy líquidos; el límite lo pone el riesgo de cola, no el volumen |
La plantilla tiene dos usos. El primero es filtrar antes de probar: una idea que no tiene contraparte plausible ni razón para persistir no merece las horas de backtest, y cada prueba que se ahorra es una prueba menos en la cuenta de las pruebas múltiples (lección 2.2.4). El segundo es leer el resultado: si la hipótesis predice asimetría negativa y el backtest sale con asimetría positiva, algo no cuadra, aunque el Sharpe sea bonito.
Ejercicios
- Repite la cartera de carry con dos divisas por lado en lugar de tres. ¿Sube o baja la rentabilidad? ¿Y la asimetría y el peor mes? Explica el resultado con la hipótesis de prima de riesgo.
Solución
peso2 = (rango >= 9).astype(float) / 2 - (rango <= 2) / 2
carry2_d = (peso2.reindex(r_fx.index).ffill().shift(1)
* r_fx).sum(axis=1).loc["2005-02":]
pd.DataFrame({"3 por lado": perfil(carry),
"2 por lado": perfil(a_mensual(carry2_d))}).round(3) 3 por lado 2 por lado
meses 251.000 251.000
rent_anual 0.044 0.055
vol_anual 0.108 0.139
sharpe 0.409 0.393
t 1.871 1.798
asimetria -1.384 -1.011
curtosis 7.389 5.388
peor_mes -0.199 -0.238
meses_pos 0.574 0.542Con dos por lado la cartera se concentra en los extremos del diferencial. La rentabilidad sube del 4,4 % al 5,5 % y el peor mes empeora del −19,9 % al −23,8 %: más diferencial, más prima y más crash, como predice la hipótesis de prima de riesgo. La volatilidad sube más que la rentabilidad (Sharpe de 0,39 frente a 0,41) y la asimetría es menos negativa (−1,01): con dos divisas por lado, la volatilidad de cada una pesa más en los meses normales y diluye la cola en términos relativos. La predicción de la hipótesis se cumple en la rentabilidad y en la pérdida máxima; la asimetría, como cociente, depende también del ruido de la cartera.
- Mide la deriva tras resultados por separado en la primera mitad de la muestra (hasta 2015) y en la segunda. Si la ventaja fuera información que el mercado aprende a descontar, ¿qué esperarías? ¿Qué encuentras en estos datos y por qué?
Solución
mitades = {"2006-2015": pead.loc["2006-04":"2015-12"],
"2016-2025": pead.loc["2016-01":]}
pd.DataFrame({k: perfil(v) for k, v in mitades.items()}).T[
["rent_anual", "sharpe", "t"]].round(2) rent_anual sharpe t
2006-2015 0.07 0.56 1.75
2016-2025 0.11 0.99 3.13Si fuera una ineficiencia que se arbitra, la segunda mitad debería ser más débil. Aquí sale al revés: Sharpe de 0,56 hasta 2015 y de 0,99 después. En estos datos la deriva está plantada con el mismo coeficiente en toda la muestra, así que la diferencia es ruido de muestreo: con diez años por mitad, el error típico de cada Sharpe es de unos 0,3 y el de la diferencia, de unos 0,45. Con datos reales, una caída entre mitades sería compatible con la hipótesis de información, pero tampoco la probaría por sí sola.
- Escribe la hipótesis de ventaja del momentum con la plantilla de la lección. ¿Qué predicción la distinguiría de una explicación de riesgo puro?
Solución
Mecanismo: infrarreacción a las noticias (comportamiento) o prima por el riesgo de crash en los rebotes (riesgo). Contraparte: quien vende pronto lo que gana y mantiene lo que pierde, o quien no quiere el riesgo de rebote. Predicciones comunes: rentabilidad media positiva de la cartera 12-1 y crashes tras los suelos de mercado. Una predicción que las separa: si es infrarreacción, la ventaja debe ser mayor en acciones poco seguidas (pequeñas, con pocos analistas) y en noticias graduales, y menguar con el tiempo; si es prima de riesgo, debe estar ligada a la exposición a esos rebotes y no a la cobertura de analistas. Control: la misma cartera con una señal placebo y con la rentabilidad del último mes (que el 12-1 se salta a propósito).
Footnotes
-
Brunnermeier, Nagel y Pedersen (2008), en los recursos, documentan la asimetría negativa del carry y la atribuyen a que las posiciones se deshacen de golpe cuando caen el apetito por el riesgo y la liquidez de financiación. ↩
-
Jegadeesh y Titman (1993), en los recursos, es el estudio clásico de las carteras de ganadoras y perdedoras. ↩
-
Daniel y Moskowitz (2016), en los recursos, estudian los crashes del momentum en los rebotes tras las caídas. ↩ ↩2
-
Shleifer (1986), en los recursos, usa las inclusiones en el S&P 500 para contrastar si la demanda de acciones tiene pendiente negativa. ↩
-
Bernard y Thomas (1989), en los recursos, contrastan si la deriva es una respuesta retardada o una prima de riesgo. ↩
Respuesta plegada en cada pregunta
Autoevaluación
Un backtest encuentra que comprar las acciones que suben el martes da beneficio. ¿Qué le falta para ser una hipótesis de ventaja?
- AMás años de datos
- BUn mecanismo: quién está al otro lado, por qué acepta perder y por qué no lo ha corregido nadie; sin eso es un patrón, no una hipótesis
- CUn Sharpe mayor que 1
- DProbarlo en otro mercado
Ver respuesta
Respuesta B. Un mecanismo: quién está al otro lado, por qué acepta perder y por qué no lo ha corregido nadie; sin eso es un patrón, no una hipótesis
Más datos y otros mercados ayudan a contrastarla, pero primero hay que tener algo que contrastar. Una hipótesis explica de dónde sale el dinero y predice cosas que se pueden comprobar (en qué activos, en qué épocas y con qué riesgo debe aparecer).
El carry de divisas de la lección tiene Sharpe positivo y una asimetría mensual muy negativa. ¿Qué encaja mejor con ese perfil?
- AUn sesgo de comportamiento que el mercado tardará en corregir
- BUna prima de riesgo: cobra un poco casi todos los meses a cambio de pérdidas grandes en las crisis, cuando el dinero vale más
- CUn error de los datos
- DUna ventaja de información
Ver respuesta
Respuesta B. Una prima de riesgo: cobra un poco casi todos los meses a cambio de pérdidas grandes en las crisis, cuando el dinero vale más
Ganar poco y a menudo y perder mucho de golpe, justo en las crisis, es el perfil de quien vende un seguro. Esa es la explicación clásica del carry (Brunnermeier, Nagel y Pedersen, 2008), y en los datos del nivel está plantada así: los crashes de las divisas de tipo alto caen en los tramos de estrés.
¿Por qué la ventaja de los rebalanceos de índice tiene poca capacidad aunque su rentabilidad por evento sea alta?
- APorque es un efecto falso
- BPorque hay pocos eventos al año, cada uno dura unas pocas sesiones y en esas sesiones compiten por las mismas acciones todos los que conocen el calendario
- CPorque las acciones del índice son ilíquidas
- DPorque la rentabilidad es negativa
Ver respuesta
Respuesta B. Porque hay pocos eventos al año, cada uno dura unas pocas sesiones y en esas sesiones compiten por las mismas acciones todos los que conocen el calendario
En los datos hay 47 eventos con efecto en 20 años, unos 2,4 al año, de unas 5 sesiones cada uno. Aunque cada evento rinda entre un 2 % y un 4 %, el capital que se puede desplegar está limitado por el volumen de esas pocas sesiones. Es una ventaja estructural: existe porque los fondos indexados tienen que operar en esas fechas.
La puntuación ESG de los datos es un placebo sin efecto y su cartera larga-corta gana un 3 % anual con t ≈ 1,5. ¿Qué enseña?
- AQue el ESG funciona
- BQue una cartera sin ninguna ventaja puede dar cifras de apariencia razonable por azar; sin un mecanismo y sin corregir por el número de pruebas, no se distingue de una ventaja pequeña
- CQue los datos están mal
- DQue hay que quitar el ESG de los datos
Ver respuesta
Respuesta B. Que una cartera sin ninguna ventaja puede dar cifras de apariencia razonable por azar; sin un mecanismo y sin corregir por el número de pruebas, no se distingue de una ventaja pequeña
Con 20 años de datos mensuales, el error típico de la media anual de una cartera con casi un 10 % de volatilidad es de unos 2 puntos. Una t de 1,5 está dentro de lo que da el azar; con muchas variables probadas, aparecerán varias así (lección 2.1.8).
¿Qué distingue una ventaja de información, como la deriva tras resultados, de una prima de riesgo?
- AQue la de información no tiene riesgo
- BQue existe porque el precio incorpora la noticia despacio, así que debería menguar cuando hay más gente que la explota o la información llega más rápido; una prima de riesgo es la compensación por soportar algo que nadie quiere y puede durar
- CQue la de información solo funciona en cripto
- DNinguna: son lo mismo
Ver respuesta
Respuesta B. Que existe porque el precio incorpora la noticia despacio, así que debería menguar cuando hay más gente que la explota o la información llega más rápido; una prima de riesgo es la compensación por soportar algo que nadie quiere y puede durar
La hipótesis predice cosas distintas. Si la ventaja es una prima, persiste pero con crashes; si es una ineficiencia (comportamiento o información lenta), debería ser mayor donde arbitrar cuesta más (acciones pequeñas, poco seguidas) y menguar con el tiempo.
Ideas clave
- Una ventaja es una hipótesis con mecanismo: quién está al otro lado, por qué acepta perder y por qué no lo ha arbitrado nadie. Un patrón sin mecanismo es candidato a falso positivo.
- Cuatro fuentes: prima por soportar un riesgo, sesgo de comportamiento de otros participantes, restricción estructural que obliga a alguien a operar, y difusión lenta de la información.
- Cada fuente deja un perfil: el carry de los datos gana un 4,4 % anual con asimetría mensual de −1,4 y su peor mes cae un 19,9 %; la deriva tras resultados tiene un Sharpe de 0,77 y una asimetría casi nula.
- El rendimiento no basta: la capacidad (apuestas al año, liquidez y duración) decide cuánto capital admite. Los rebalanceos rinden entre un 2 % y un 4 % por evento, pero hay 2,4 al año.
- El placebo ESG gana un 3,1 % anual (t = 1,46) sin efecto alguno. Los datos del nivel son sintéticos y sus efectos están plantados: enseñan a medir, no prueban que nada funcione en un mercado real.
Recursos
Libros
- Brunnermeier, Nagel y Pedersen (2008): Carry Trades and Currency Crashes. NBER Macroeconomics Annual 23, 313-348
- Jegadeesh y Titman (1993): Returns to Buying Winners and Selling Losers. Journal of Finance 48(1), 65-91
- Daniel y Moskowitz (2016): Momentum crashes. Journal of Financial Economics 122(2), 221-247
- Bernard y Thomas (1989): Post-Earnings-Announcement Drift: Delayed Price Response or Risk Premium? Journal of Accounting Research 27
- Shleifer (1986): Do Demand Curves for Stocks Slope Down? Journal of Finance 41(3), 579-590