Lección 2.6.2 · 65 min
Win rate, payoff, expectativa, alpha, beta, information ratio, turnover y capacidad
Las métricas de la lección 2.6.1 miran la curva de capital. Estas miran sus piezas: cada operación, la parte de la rentabilidad que se explica por el mercado y lo que cuesta mover la cartera. Con un control sin ventaja que acierta el 63 % de las veces y pierde dinero, y un placebo que pasa el listón de t = 2.
- Módulo
- 2.6 · Métricas de rendimiento
- Duración
- 65 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
De la curva a la lista de operaciones
La lección 2.6.1 resumió tres curvas de capital. Una curva es el resultado de muchas decisiones, y para saber de qué están hechas hay que mirarlas una a una. Esta lección trabaja en tres niveles: las operaciones (cuánto se gana y se pierde en cada una), la cartera frente a un índice (qué parte de la rentabilidad es mercado y qué parte no) y la ejecución (cuánto rota la cartera y cuánto capital admite).
Fuente Mermaid
flowchart TB P[Posiciones diarias] --> O[Operaciones] O --> WR[Win rate p] O --> WL[W y L medias] WR --> E[Expectativa] WL --> E WL --> PF[Profit factor] P --> R[Rentabilidad] R --> AB[Alfa y beta] P --> T[Turnover] T --> C[Capacidad]
Se empieza por reconstruir las dos estrategias de la lección 2.6.1 sobre los datos sintéticos del nivel 2: TSMOM en 24 futuros y el carry de diez divisas, del 1 de febrero de 2005 a 2025. Los efectos están plantados (un término de momentum en cada futuro y un carry que gana el 70 % del diferencial con cinco crashes): las cifras enseñan a medir, no dicen que esas estrategias funcionen fuera de estos datos.
import numpy as np
import pandas as pd
fut = pd.read_parquet("futuros2_continuos.parquet")
px = fut.pivot(index="fecha", columns="mercado",
values="cierre_ajustado")
r_fut = px.pct_change()
vol = r_fut.ewm(span=60, min_periods=60).std() * np.sqrt(252)
senal = np.sign(px / px.shift(252) - 1)
peso = (senal * 0.40 / px.shape[1] / vol).shift(1)
fx = pd.read_parquet("fx2_carry.parquet")
tabla = lambda c: fx.pivot(index="fecha", columns="divisa", values=c)
spot, dif = tabla("spot"), tabla("tipo_divisa") - tabla("tipo_usd")
dias = spot.index.to_series().diff().dt.days
interes = dif.shift(1) / 100 * dias.to_numpy()[:, None] / 360
r_fx = spot.pct_change() + interes
fin_mes = dif.index.to_series().groupby(dif.index.to_period("M")).max()
rango = dif.loc[fin_mes].rank(axis=1)
w_fx = ((rango >= 8).astype(float) - (rango <= 3).astype(float)) / 3
w_fx = w_fx.reindex(dif.index).ffill().shift(1)
inicio = "2005-02-01"
peso, r_fut = peso.loc[inicio:], r_fut.loc[inicio:]
w_fx, r_fx = w_fx.loc[inicio:], r_fx.loc[inicio:]
print(peso.shape, w_fx.shape)(5263, 24) (5457, 10)Win rate, payoff y expectativa
Una operación de TSMOM es una racha de posición con el mismo signo en un mercado: empieza cuando la señal cambia de signo y termina cuando vuelve a cambiar. Su resultado es la suma de lo que aporta a la cartera cada día, en fracción del capital. El tamaño de cada posición cambia con la volatilidad, pero la operación es la decisión de estar largo o corto.
def operaciones(peso, r):
"""Una operación: una racha del mismo signo en un mercado."""
aporte = peso * r # rendimiento que aporta a la cartera
filas = []
for c in peso.columns:
lado = np.sign(peso[c]).fillna(0)
racha = (lado != lado.shift()).cumsum()
d = pd.DataFrame({"lado": lado, "pnl": aporte[c],
"racha": racha})
d = d[d["lado"] != 0].reset_index()
t = d.groupby("racha").agg(
entrada=("fecha", "first"), salida=("fecha", "last"),
lado=("lado", "first"), pnl=("pnl", "sum"),
sesiones=("pnl", "size"))
filas.append(t.assign(mercado=c))
return pd.concat(filas, ignore_index=True)
op_tsmom = operaciones(peso, r_fut)
print(len(op_tsmom), "operaciones")
op_tsmom.sort_values("pnl").iloc[[0, -1]]2576 operaciones
entrada salida lado pnl sesiones mercado
1 2005-03-24 2005-08-24 -1.0 -0.021429 107 AGA
69 2019-04-03 2024-12-02 -1.0 0.123242 1427 AGASon 2.576 operaciones en 24 mercados, unas 107 por mercado en 21 años. Las dos extremas son del maíz ficticio AGA: la peor resta un 2,1 % del capital (corta de marzo a agosto de 2005) y la mejor suma un 12,3 % (corta de abril de 2019 a diciembre de 2024, 1.427 sesiones, casi seis años). Las operaciones que siguen abiertas el 31 de diciembre de 2025 y las que empezaron antes de febrero de 2005 cuentan solo por su parte dentro de la muestra.
Con la lista se calculan cuatro métricas. Si es la fracción de operaciones ganadoras (win rate), la ganancia media de las ganadoras y la pérdida media de las perdedoras (en positivo):
La expectativa es lo que se gana de media por operación. El profit factor compara lo ganado con lo perdido: mayor que 1 si y solo si . Igualando a cero sale el win rate de equilibrio, el mínimo que hace falta para no perder con un payoff dado:
Para el carry, que rebalancea cada fin de mes, la operación natural es el mes: cada mes se abre la cesta de seis divisas (en la práctica, con forwards a un mes) y se cierra al siguiente.
def metricas_op(pnl):
gana, pierde = pnl[pnl > 0], pnl[pnl <= 0]
p = len(gana) / len(pnl)
W, L = gana.mean(), -pierde.mean()
return {"operaciones": len(pnl), "win_rate": p,
"ganancia_media_pb": 1e4 * W,
"perdida_media_pb": 1e4 * L, "payoff": W / L,
"expectativa_pb": 1e4 * (p * W - (1 - p) * L),
"profit_factor": gana.sum() / -pierde.sum(),
"win_rate_equilibrio": 1 / (1 + W / L)}
aporte_fx = (w_fx * r_fx).sum(axis=1)
mes_carry = aporte_fx.groupby(aporte_fx.index.to_period("M")).sum()
pd.DataFrame({"tsmom": metricas_op(op_tsmom["pnl"]),
"carry_mes": metricas_op(mes_carry)}).round(3) tsmom carry_mes
operaciones 2576.000 251.000
win_rate 0.330 0.574
ganancia_media_pb 72.069 227.469
perdida_media_pb 20.389 223.261
payoff 3.535 1.019
expectativa_pb 10.083 35.325
profit_factor 1.738 1.371
win_rate_equilibrio 0.221 0.495TSMOM acierta solo el 33,0 % de sus operaciones, pero cada ganadora vale de media 3,54 veces lo que cuesta cada perdedora: su win rate de equilibrio es el 22,1 % y lo supera con holgura. El carry acierta el 57,4 % de los meses con un payoff de 1,02; su equilibrio está en el 49,5 %. Las dos tienen expectativa positiva (10,1 pb por operación y 35,3 pb por mes) y profit factor por encima de 1 (1,74 y 1,37). Son dos formas opuestas de ganar.
La media esconde cómo se reparte el beneficio. Ordenando las operaciones de mejor a peor:
def concentracion(pnl):
orden = pnl.sort_values(ascending=False)
n = len(pnl) // 10
return {"mejor_10%": orden.iloc[:n].sum() / pnl.sum(),
"peor_10%": orden.iloc[-n:].sum() / pnl.sum(),
"sin_extremos": orden.iloc[n:-n].sum() / pnl.sum()}
pd.DataFrame({"tsmom": concentracion(op_tsmom["pnl"]),
"carry_mes": concentracion(mes_carry)}).round(2) tsmom carry_mes
mejor_10% 2.12 1.45
peor_10% -0.63 -1.60
sin_extremos -0.49 1.15En TSMOM, el mejor 10 % de las operaciones aporta el 212 % del beneficio total: sin ellas, la estrategia pierde. El 80 % central resta un 49 % y el peor 10 %, un 63 %. Es la forma de un seguidor de tendencia: vive de unas pocas tendencias largas, y cualquier regla que recorte las ganadoras (un objetivo de beneficio, un filtro que se salte algunas señales) le quita justo lo que la sostiene. El carry es el espejo: el 80 % central de los meses gana (un 115 % del total) y el peor 10 % se lleva el 160 %, los meses de crash. Una estrategia con asimetría negativa parece funcionar todos los meses hasta el mes que no.
Por qué el win rate engaña
Un win rate alto es fácil de fabricar: basta con poner el objetivo de beneficio cerca y el stop lejos. Para verlo hace falta una estrategia sin ventaja, y los datos del nivel 2 la tienen: en las acciones del universo ficticio la reversión a corto plazo no tiene efecto plantado (es un control). La regla compra al cierre después de una caída de más de 2 desviaciones típicas, y sale al cierre en cuanto la ganancia acumulada pasa de +1σ (objetivo) o la pérdida de −3σ (stop), o a las 10 sesiones.
aj = pd.read_parquet("universo2_ajustados.parquet")
pa = aj.pivot(index="fecha", columns="ticker", values="cierre_ajustado")
lp = np.log(pa)
sig = lp.diff().ewm(span=60, min_periods=60).std().shift(1)
def reversion(objetivo=1.0, stop=3.0, max_ses=10, caida=2.0):
"""Compra al cierre tras caer `caida` sigmas; sale al cierre
al tocar +objetivo o -stop sigmas, o tras max_ses sesiones."""
filas = []
for tk in pa.columns:
x = lp[tk].dropna()
v, s = x.to_numpy(), sig[tk].reindex(x.index).to_numpy()
i = 61
while i < len(v) - max_ses:
if v[i] - v[i - 1] < -caida * s[i]:
for k in range(1, max_ses + 1):
c = v[i + k] - v[i]
if (c >= objetivo * s[i] or c <= -stop * s[i]
or k == max_ses):
break
filas.append((tk, x.index[i], k, np.expm1(c)))
i += k
i += 1
return pd.DataFrame(filas, columns=["ticker", "entrada",
"sesiones", "pnl"])
op_rev = reversion()
op_rev.head(3) ticker entrada sesiones pnl
0 QXAC 2006-05-01 4 0.008333
1 QXAC 2006-06-27 4 -0.035329
2 QXAC 2006-07-05 5 0.023794En un paseo aleatorio sin deriva, un objetivo a y un stop a se tocan antes con probabilidad : con 1σ y 3σ, el 75 % de las veces gana el objetivo. Pero la ganancia es tres veces menor que la pérdida, así que la expectativa es cero: la regla redistribuye el resultado, no lo crea. Con los datos y sus costes:
ch = pd.read_parquet("universo2_caracteristicas.parquet")
spr = ch.pivot(index="fecha", columns="ticker", values="horquilla_pb")
spr.index = spr.index.to_period("M")
mes_ant = op_rev["entrada"].dt.to_period("M") - 1
coste = [spr.at[m, t] if m in spr.index else np.nan
for m, t in zip(mes_ant, op_rev["ticker"])]
op_rev["coste"] = pd.Series(coste).fillna(spr.stack().median()) / 1e4
pd.DataFrame({"bruto": metricas_op(op_rev["pnl"]),
"neto": metricas_op(op_rev["pnl"] - op_rev["coste"])}
).round(3) bruto neto
operaciones 12633.000 12633.000
win_rate 0.634 0.633
ganancia_media_pb 321.795 315.899
perdida_media_pb 620.908 627.096
payoff 0.518 0.504
expectativa_pb -23.333 -29.709
profit_factor 0.897 0.871
win_rate_equilibrio 0.659 0.665La regla acierta el 63,4 % de 12.633 operaciones (menos del 75 % teórico, porque muchas salen por tiempo y los
cierres saltan por encima de los umbrales) con un payoff de 0,52. Su win rate de equilibrio es el 65,9 %: se queda
2,5 puntos por debajo y pierde 23,3 pb por operación antes de costes. La horquilla de cada acción (la del mes anterior
a la entrada, de universo2_caracteristicas.parquet) añade 6,4 pb, hasta −29,7 pb.
¿Por qué pierde incluso sin costes, si la reversión a corto plazo no tiene efecto plantado? Las caídas de más de 2σ no se reparten por igual en el tiempo: se concentran en los tramos de estrés del guion (las crisis con fechas fijas del README de los datos; ocho desde 2006), en los que el mercado tiene una deriva muy negativa durante semanas.
tramos = [("2008-09-02", "2009-03-09"), ("2010-04-26", "2010-07-05"),
("2011-07-25", "2011-10-03"), ("2015-08-17", "2016-02-11"),
("2018-10-01", "2018-12-25"), ("2020-02-20", "2020-03-23"),
("2022-01-04", "2022-10-12"), ("2025-02-20", "2025-04-08")]
en_estres = np.zeros(len(op_rev), dtype=bool)
for a, b in tramos:
en_estres |= op_rev["entrada"].between(a, b).to_numpy()
pd.DataFrame({"fuera": metricas_op(op_rev["pnl"][~en_estres]),
"estres": metricas_op(op_rev["pnl"][en_estres])}).round(3) fuera estres
operaciones 9542.000 3091.000
win_rate 0.667 0.532
ganancia_media_pb 304.080 390.319
perdida_media_pb 548.349 780.430
payoff 0.555 0.500
expectativa_pb 20.086 -157.369
profit_factor 1.110 0.569
win_rate_equilibrio 0.643 0.667Fuera de los tramos de estrés, la regla acierta el 66,7 % con un payoff de 0,555 y gana 20,1 pb por operación: está comprada unas pocas sesiones en un mercado con deriva positiva. Dentro, 3.091 operaciones (una de cada cuatro) aciertan solo el 53,2 % y pierden 157 pb cada una: compra caídas en mitad de una crisis y toca el stop. Toda la pérdida sale de ahí. Es la forma del carry de la sección anterior: ganancias pequeñas y frecuentes, pérdidas grandes agrupadas en las crisis. Un win rate alto, muchas veces, es eso: vender sin saberlo un seguro contra el crash.
La figura de la cabecera pone las tres estrategias sobre el plano win rate-payoff. La curva continua es la expectativa cero; por encima de ella se gana. TSMOM (y la nube de sus 24 mercados) está abajo a la derecha: acierta poco y gana mucho cuando acierta. El carry, en el centro. La reversión, arriba a la izquierda: es la que más acierta y la única que está por debajo de la curva.
Alfa y beta frente a un índice
La segunda pregunta es otra: de lo que gana una cartera, qué parte habría ganado cualquiera expuesto al mercado. La regresión de los excesos de rentabilidad sobre los del índice la separa:
es la exposición al índice y la rentabilidad media que no explica. Para probarlas hacen falta cuatro carteras sobre el universo de 120 acciones ficticias, con datos point-in-time (lección 1.4.4) y la rentabilidad de deslistado de las quiebras:
- Índice: los miembros del índice de cada mes, equiponderados. Es el benchmark.
- Factores: las 20 acciones con mayor puntuación compuesta de momentum 12-1, value y baja volatilidad, las tres primas plantadas en los datos. El efecto existe.
- ESG: las 20 de mayor puntuación ESG, un placebo puro (un AR(1) independiente de todo). La verdad es cero.
- Calidad (ROE) y tamaño: los dos controles sin prima. La calidad tiene una trampa documentada en los datos: el ROE es E/P por P/VC, está muy correlado con el value y en un ordenamiento univariante hereda su prima con el signo contrario.
from scipy import stats
ev = pd.read_csv("universo2_eventos.csv")
desl = ev.dropna(subset=["rentabilidad_deslistado"])
rd = pa.pct_change(fill_method=None)
for tk, r_desl in zip(desl["ticker"], desl["rentabilidad_deslistado"]):
i = pa.index.get_loc(pa[tk].last_valid_index())
if i + 1 < len(pa.index): # rentabilidad de deslistado
rd.loc[pa.index[i + 1], tk] = r_desl
mes = rd.index.to_period("M")
rm = ((1 + rd.fillna(0)).groupby(mes).prod() - 1).where(
rd.notna().groupby(mes).any())
def piv(col): # característica a fin de mes (point-in-time)
t = ch.pivot(index="fecha", columns="ticker", values=col)
return t.set_axis(rm.index, axis=0).reindex(columns=rm.columns)
miembro = piv("miembro").fillna(False).astype(bool)
fin = pa.index.to_series().groupby(mes).max()
vol252 = rd.rolling(252, min_periods=126).std().loc[fin]
vol252.index = rm.index
mom = (1 + rm).rolling(11).apply(np.prod, raw=True).shift(1) - 1
value = np.log(piv("patrimonio_neto") / piv("capitalizacion"))
def z(s): # puntuación normal por rangos entre miembros
s = s.where(miembro)
q = (s.rank(axis=1) - 0.5).div(s.notna().sum(axis=1), axis=0)
return pd.DataFrame(stats.norm.ppf(q), s.index, s.columns)
def top(puntos, n=20): # 20 mayores, equiponderadas
rk = puntos.where(miembro).rank(axis=1, ascending=False)
w = (rk <= n).astype(float)
return w.div(w.sum(axis=1), axis=0)
compuesto = (z(mom).fillna(0) + z(value).fillna(0)
- z(vol252).fillna(0))
pesos = {"indice": miembro.astype(float).div(miembro.sum(axis=1),
axis=0),
"factores": top(compuesto),
"esg": top(piv("puntuacion_esg")),
"calidad": top(piv("roe")),
"tamano": top(-np.log(piv("capitalizacion")))}
rent = pd.DataFrame({k: (w.shift(1) * rm).sum(axis=1, min_count=1)
for k, w in pesos.items()}).loc["2007-01":]
tl = pd.read_csv("tipo_libre2.csv", parse_dates=["fecha"],
index_col="fecha")["tipo_3m"]
rf = (tl / 1200).groupby(tl.index.to_period("M")).mean()
exceso = rent.sub(rf.reindex(rent.index), axis=0)
pd.DataFrame({"media": 12 * exceso.mean(),
"vol": np.sqrt(12) * exceso.std(),
"sharpe": np.sqrt(12) * exceso.mean() / exceso.std()}
).round(3) media vol sharpe
indice 0.068 0.220 0.308
factores 0.109 0.196 0.558
esg 0.092 0.218 0.421
calidad 0.023 0.223 0.105
tamano 0.072 0.241 0.299Las rentabilidades mensuales rm salen de los precios ajustados más la rentabilidad de deslistado de
universo2_eventos.csv. universo2_caracteristicas.parquet trae además una última fila por cada una de las diez
acciones que dejan de cotizar, sin precio, con miembro = False y con esa misma rentabilidad encadenada en
rentabilidad_mes (−100 % en las quiebras). Aquí no cambian nada: las señales y los pesos se forman solo con los
miembros, y el mes del deslistado ya está en rm. Quien use rentabilidad_mes en lugar de calcularla tiene que
conservar esas filas para el rendimiento y filtrarlas (por cierre no vacío) para las señales: sin ellas, una cartera
que tuviera una de las quiebras no vería su −100 %.
Para el error de se usan errores de Newey-West1Nota 1 · Whitney K. Newey y Kenneth D. West, «A Simple, Positive Semi-Definite, Heteroskedasticity and Autocorrelation
Consistent Covariance Matrix», Econometrica 55(3), 703-708 (1987). En statsmodels, fit(cov_type="HAC", cov_kwds={"maxlags": 5}). (lección 2.1.1), robustos a la heterocedasticidad y a la
autocorrelación de los residuos, con 5 retardos (una regla práctica habitual, , da 4,8 con 228
meses):
import statsmodels.api as sm
def alfa_beta(y, x, lags=5):
X = sm.add_constant(x.rename("beta"))
ols = sm.OLS(y, X).fit()
hac = sm.OLS(y, X).fit(cov_type="HAC", cov_kwds={"maxlags": lags})
return {"beta": hac.params["beta"],
"alfa_anual": 12 * hac.params["const"],
"t_ols": ols.tvalues["const"],
"t_hac": hac.tvalues["const"],
"ee_alfa_anual": 12 * hac.bse["const"]}
pd.DataFrame({k: alfa_beta(exceso[k], exceso["indice"])
for k in ["factores", "esg", "calidad", "tamano"]}
).T.round(3) beta alfa_anual t_ols t_hac ee_alfa_anual
factores 0.847 0.052 3.670 4.093 0.013
esg 0.960 0.027 2.152 2.006 0.013
calidad 0.986 -0.043 -3.529 -2.907 0.015
tamano 1.065 -0.000 -0.014 -0.013 0.014- Factores: beta 0,85 y alfa del 5,2 % anual con t = 4,1. El método recupera un efecto que existe.
- ESG: alfa del 2,7 % con t = 2,0. Un placebo pasa el listón clásico de dos errores típicos. No es un fallo del método: con un umbral de t = 2, más o menos una de cada veinte pruebas sin efecto lo pasa por azar. Por eso un t de 2 en la primera característica que se prueba no basta, y en la vigésima no significa nada (lección 2.1.8).
- Calidad: alfa del −4,3 % con t = −2,9. «Significativo» y falso: no hay prima de calidad en los datos; la cartera de ROE alto tiene P/VC alto (value bajo) y paga la prima del value con el signo contrario. Sin controlar por value, la regresión atribuye a la calidad lo que es del value.
- Tamaño: alfa del 0,0 % y t = 0,0. El control se comporta como debe.
Los errores de Newey-West cambian poco los t en estas carteras (de 3,67 a 4,09 en la de factores): sus residuos mensuales casi no tienen autocorrelación. En carteras con solapes (rendimientos de 12 meses calculados cada mes) o con activos poco líquidos, la diferencia es grande.
Fuente Mermaid
flowchart TB R[Rentabilidad] --> M[Beta x mercado] R --> A[Alfa] R --> E[Residuo] A --> AP[Appraisal ratio] E --> AP R --> ACT[Activa vs índice] ACT --> IR[Information ratio]
Information ratio y tracking error
Un gestor que compite con un índice se mide por su rentabilidad activa, la diferencia con el índice. Su desviación típica es el tracking error y el cociente, el information ratio:
El IR no ajusta por beta: una cartera con beta 0,85 pierde frente al índice en los meses alcistas por su menor exposición, y eso entra en su tracking error. El appraisal ratio divide el alfa de la regresión por la desviación típica del residuo: mide la calidad de la parte no explicada por el mercado.
def ratio_informacion(r, ref, n=12):
activa = r - ref
te = activa.std() * np.sqrt(n)
return {"activa_anual": activa.mean() * n, "tracking_error": te,
"ir": activa.mean() * n / te}
def appraisal(y, x, n=12):
fit = sm.OLS(y, sm.add_constant(x)).fit()
return n * fit.params.iloc[0] / (fit.resid.std() * np.sqrt(n))
pd.DataFrame({k: {**ratio_informacion(rent[k], rent["indice"]),
"appraisal": appraisal(exceso[k], exceso["indice"])}
for k in ["factores", "esg"]}).T.round(3) activa_anual tracking_error ir appraisal
factores 0.042 0.070 0.595 0.847
esg 0.024 0.054 0.440 0.497La cartera de factores bate al índice en un 4,2 % anual con un tracking error del 7,0 %: IR de 0,60. Su appraisal ratio es mayor, 0,85, porque al quitar la beta de 0,85 desaparece del denominador el ruido que pone el mercado. El placebo ESG tiene un IR de 0,44: con 19 años, también es compatible con cero. La regla para leerlos es la misma que para el Sharpe en la lección 2.6.1: un IR de 0,5 con 19 años tiene un error típico de unos 0,23.
Turnover y capacidad
Las métricas anteriores son brutas de costes. Para pasarlas a netas hay que saber cuánto se opera. El turnover de un rebalanceo es la mitad de la suma de los cambios absolutos de los pesos (cada compra tiene una venta enfrente) y el anual, su suma en un año:
def turnover_anual(w, n=12):
cambio = w.fillna(0).diff().abs().sum(axis=1) / 2
return cambio.loc["2007-01":].mean() * n
pd.Series({k: turnover_anual(w) for k, w in pesos.items()}).round(2)indice 0.01
factores 2.28
esg 2.65
calidad 0.40
tamano 0.64
dtype: float64El índice casi no rota (un 1 % al año: solo cambian los miembros). La cartera de factores rota el 228 %: cada año renueva su contenido más de dos veces, porque el momentum y la volatilidad cambian cada mes. El placebo ESG rota más aún (265 %), porque su puntuación es casi ruido. La calidad (40 %) y el tamaño (64 %) son características lentas. El cálculo usa los pesos objetivo de cada fin de mes y desprecia la deriva de los pesos entre rebalanceos: es una aproximación por abajo.
La capacidad es el capital a partir del cual los costes se comen la ventaja (lección 2.2.3). Depende de cuánto se opera, del tamaño de cada orden frente al volumen de la acción y de cómo crece el coste con ese tamaño. El modelo de la celda, deliberadamente simple, cobra por cada unidad operada media horquilla más un impacto de raíz cuadrada:
con la horquilla, la volatilidad diaria, el capital gestionado y el volumen medio diario
en dólares de la acción (volumen_medio_21d por el cierre). El coeficiente del impacto es 1, un supuesto: la lección
2.4.7 explica cómo se calibra y por qué el impacto real depende de cómo se reparte la orden en el tiempo.
adv = piv("volumen_medio_21d") * piv("cierre") # USD por sesión
spr_m = piv("horquilla_pb") / 1e4
dw = pesos["factores"].fillna(0).diff().abs().loc["2007-01":]
adv, spr_m, sd = (t.reindex_like(dw) for t in (adv, spr_m, vol252))
def capacidad(capital):
part = dw * capital / adv # fracción del ADV
coste = dw * (spr_m / 2 + sd * np.sqrt(part))
operadas = part.where(dw > 0).stack().dropna()
return {"coste_anual": 12 * coste.sum(axis=1).mean(),
"participacion_p95": operadas.quantile(0.95)}
pd.DataFrame({f"{c:.0e}": capacidad(c)
for c in [1e6, 1e7, 1e8, 3e8, 1e9, 3e9]}).T.round(4) coste_anual participacion_p95
1e+06 0.0030 0.0019
1e+07 0.0068 0.0190
1e+08 0.0191 0.1902
3e+08 0.0322 0.5707
1e+09 0.0577 1.9024
3e+09 0.0992 5.7072Con un millón de dólares, el coste es casi solo la horquilla: un 0,30 % al año frente a un alfa bruto del 5,2 %. Con 100 millones, el 1,91 %, y el percentil 95 de las órdenes es ya el 19 % del volumen diario de su acción. Con 1.000 millones, el 5,8 %, más que todo el alfa, y la orden del percentil 95 casi dobla el volumen diario: no se podría ejecutar en una sesión. Con este modelo, la cartera pierde la mitad del alfa hacia 190 millones y todo hacia 808 millones. Un límite de ejecución práctico (que el 95 % de las órdenes no pase del 10 % del ADV) la deja en unos 53 millones.
Fuente Mermaid
flowchart TB K[Capital] --> O[Orden por acción] TO[Turnover] --> O O --> PA[Participación ADV] PA --> IM[Impacto] S[Horquilla] --> CO[Coste] IM --> CO CO --> AN[Alfa neto]
Errores típicos al leer estas métricas
- Juzgar por el win rate. Solo dice algo junto al payoff: compáralo siempre con .
- Mezclar unidades. Un profit factor de días, uno de meses y uno de operaciones son tres cifras distintas.
- Olvidar las operaciones abiertas y los deslistados. Las que siguen abiertas al final de la muestra y las de acciones que dejan de cotizar cambian la expectativa.
- Llamar alfa a una beta. Una cartera con beta 1,2 bate al índice en un mercado alcista sin ningún alfa. Regresión con el benchmark adecuado y errores robustos.
- Creer el primer t de 2. El placebo ESG lo consigue. Cuenta cuántas características has probado.
- Ignorar la rotación. Un alfa bruto del 5 % con un turnover del 228 % es otra cosa con 500 millones que con uno.
Ejercicios
- Agrupa las operaciones de TSMOM por sector (columna
sectordefuturos2_especificaciones.csv) y calcula win rate, payoff y expectativa de cada uno. ¿Hay algún sector por debajo del equilibrio?
Solución
esp = pd.read_csv("futuros2_especificaciones.csv",
index_col="mercado")["sector"]
por_sector = op_tsmom.groupby(op_tsmom["mercado"].map(esp))["pnl"]
pd.DataFrame({s: metricas_op(g) for s, g in por_sector}).T[
["operaciones", "win_rate", "payoff", "expectativa_pb",
"win_rate_equilibrio"]].round(3) operaciones win_rate payoff expectativa_pb win_rate_equilibrio
agricolas 349.0 0.344 5.180 22.025 0.162
bonos 488.0 0.357 4.677 19.575 0.176
divisas 460.0 0.328 2.312 1.537 0.302
energia 421.0 0.280 2.532 -0.183 0.283
metales 414.0 0.343 2.635 5.262 0.275
renta_variable 444.0 0.324 3.635 13.350 0.216Cinco de los seis sectores están por encima de su win rate de equilibrio, con perfiles parecidos: aciertan entre el 28,0 % y el 35,7 %, y lo que cambia es el payoff. Los agrícolas tienen el payoff más alto (5,18) y la mayor expectativa (22,0 pb); los bonos, el win rate más alto (35,7 %) y un payoff de 4,68. La energía queda justo por debajo del equilibrio (28,0 % frente a 28,3 %, expectativa de −0,18 pb) y las divisas, justo por encima (1,5 pb). Sin embargo, los 24 mercados tienen plantado el mismo término de momentum (0,15 por unidad de volatilidad, README de los datos): las diferencias entre sectores son, sobre todo, ruido. Con 350 a 490 operaciones por sector no se distinguen; para decidir qué sectores operar haría falta su error típico, no solo la cifra, y aquí descartar la energía sería descartar un efecto que existe.
- Repite la reversión con objetivo y stop simétricos (2σ y 2σ). ¿Qué pasa con el win rate, el payoff y la expectativa?
Solución
op_sim = reversion(objetivo=2.0, stop=2.0)
pd.Series(metricas_op(op_sim["pnl"])).round(3)operaciones 12522.000
win_rate 0.495
ganancia_media_pb 459.519
perdida_media_pb 484.926
payoff 0.948
expectativa_pb -17.832
profit_factor 0.927
win_rate_equilibrio 0.513
dtype: float64Con objetivo y stop a la misma distancia, el win rate baja al 49,5 % y el payoff sube a 0,95: la regla ya no «acierta mucho». La expectativa sigue negativa (−17,8 pb por operación, equilibrio en el 51,3 %), por la misma razón que antes: las compras en plena crisis. Mover los umbrales cambia la forma de los resultados, no su media; solo una ventaja real (o evitar los tramos de estrés, si se pudiera saber de antemano) cambia la expectativa.
- Estima la beta de TSMOM frente al futuro del índice IDA con rendimientos mensuales y errores de Newey-West. ¿Qué papel tendría TSMOM en una cartera que ya tiene el índice?
Solución
tsmom_d = (peso * r_fut).sum(axis=1)
m = pd.DataFrame({"tsmom": tsmom_d, "ida": r_fut["IDA"]})
m = (1 + m).resample("ME").prod() - 1
ab = alfa_beta(m["tsmom"], m["ida"])
print(f"beta {ab['beta']:.3f} · alfa {ab['alfa_anual']:.2%} "
f"(t HAC {ab['t_hac']:.2f})")
print(f"correlación mensual: {m.corr().iloc[0, 1]:.3f}")beta 0.089 · alfa 11.38% (t HAC 4.36)
correlación mensual: 0.162La beta de TSMOM frente al índice es 0,09 y la correlación mensual, 0,16: casi no depende del mercado. Su alfa frente al índice (11,38 % anual, t de 4,36) es prácticamente toda su rentabilidad. En una cartera con el índice, TSMOM actuaría como diversificador: añade rentabilidad con poca correlación, y en estos datos gana en las crisis largas (2008, +26,9 %; 2011, +8,1 %; 2022, +8,4 %), pero no en la de 2020 (−1,7 %), una caída de dos meses demasiado rápida para una señal de 12 meses. Es un resultado de datos sintéticos con el efecto plantado; Moskowitz, Ooi y Pedersen (2012) estudian la relación real del momentum de series temporales con los mercados alcistas y bajistas.
Footnotes
-
Whitney K. Newey y Kenneth D. West, «A Simple, Positive Semi-Definite, Heteroskedasticity and Autocorrelation Consistent Covariance Matrix», Econometrica 55(3), 703-708 (1987). En statsmodels,
fit(cov_type="HAC", cov_kwds={"maxlags": 5}). ↩
Respuesta plegada en cada pregunta
Autoevaluación
Una estrategia acierta el 63 % de las operaciones con un payoff de 0,52. ¿Gana dinero?
- ASí: acierta casi dos de cada tres veces
- BNo: con un payoff de 0,518 el win rate de equilibrio es 1 / (1 + 0,518) = 65,9 %, y un 63,4 % se queda por debajo
- CDepende solo del número de operaciones
- DSí, si el profit factor es mayor que el win rate
Ver respuesta
Respuesta B. No: con un payoff de 0,518 el win rate de equilibrio es 1 / (1 + 0,518) = 65,9 %, y un 63,4 % se queda por debajo
Es la regla de reversión de la lección, aplicada a acciones sin efecto de reversión: objetivo a +1σ y stop a −3σ. Acierta mucho porque el objetivo está cerca, y pierde cuando toca el stop, que está tres veces más lejos. Su expectativa es de −23 pb por operación antes de costes, y toda la pérdida sale de las caídas compradas en plena crisis.
En TSMOM, el mejor 10 % de las operaciones aporta el 212 % del beneficio total. ¿Qué significa?
- AQue hay un error: un porcentaje no puede pasar de 100
- BQue el 90 % restante pierde en conjunto (−112 % del total): la estrategia vive de unas pocas tendencias largas y no se puede saltar ninguna
- CQue la estrategia tiene asimetría negativa
- DQue conviene quitar las operaciones grandes
Ver respuesta
Respuesta B. Que el 90 % restante pierde en conjunto (−112 % del total): la estrategia vive de unas pocas tendencias largas y no se puede saltar ninguna
Es la forma típica de un seguidor de tendencia: muchas pérdidas pequeñas y pocas ganancias grandes. Cualquier filtro que elimine algunas de las grandes (por ejemplo, un stop de beneficio) destruye la estrategia, y un periodo sin tendencias produce rachas largas de pérdidas.
La cartera de 20 acciones con más puntuación ESG (un placebo sin ninguna prima en los datos) tiene un alfa del 2,7 % anual con t = 2,0. ¿Qué concluyes?
- AQue el ESG tiene una prima del 2,7 %
- BQue es un falso positivo: con un umbral de t = 2, aproximadamente una de cada veinte carteras sin efecto lo pasa por azar, y aquí sabemos que la verdad es cero
- CQue los errores de Newey-West están mal calculados
- DQue el índice no es un buen benchmark
Ver respuesta
Respuesta B. Que es un falso positivo: con un umbral de t = 2, aproximadamente una de cada veinte carteras sin efecto lo pasa por azar, y aquí sabemos que la verdad es cero
Los datos sintéticos permiten saber la respuesta: la puntuación ESG es un AR(1) independiente de todo. Un t de 2 en una sola prueba ya es frágil; si además se han probado varias características, hay que corregir por pruebas múltiples (lección 2.1.8).
¿Qué diferencia hay entre el information ratio y el appraisal ratio?
- ANinguna
- BEl IR divide la rentabilidad activa (cartera menos índice) por su volatilidad, sin ajustar por beta; el appraisal ratio divide el alfa de la regresión por la volatilidad del residuo, después de quitar la parte explicada por el mercado
- CEl IR solo vale para fondos indexados
- DEl appraisal ratio usa rendimientos logarítmicos
Ver respuesta
Respuesta B. El IR divide la rentabilidad activa (cartera menos índice) por su volatilidad, sin ajustar por beta; el appraisal ratio divide el alfa de la regresión por la volatilidad del residuo, después de quitar la parte explicada por el mercado
Una cartera con beta 0,85 tiene una rentabilidad activa contaminada por el mercado: en los años alcistas pierde frente al índice por su menor beta. En la cartera de factores, IR 0,60 y appraisal 0,85.
La cartera de factores rota un 228 % al año. Con 1 millón de USD el coste anual estimado es del 0,30 %; con 1.000 millones, del 5,8 %. ¿Por qué crece tanto?
- APorque las comisiones son proporcionales
- BPorque la horquilla se paga igual con cualquier capital, pero el impacto crece con la participación en el volumen de cada acción: con más capital, cada orden es una fracción mayor del ADV y mueve más el precio
- CPorque con más capital hay que operar más veces
- DPorque el tipo libre sube
Ver respuesta
Respuesta B. Porque la horquilla se paga igual con cualquier capital, pero el impacto crece con la participación en el volumen de cada acción: con más capital, cada orden es una fracción mayor del ADV y mueve más el precio
La horquilla es un coste fijo por unidad operada; el impacto, con el modelo de raíz cuadrada, crece con la raíz de la participación. Con 1.000 millones, el percentil 95 de las órdenes es casi el doble del volumen medio diario de su acción: imposible de ejecutar en un día.
Ideas clave
- Expectativa = p·W − (1 − p)·L. El win rate de equilibrio es 1 / (1 + payoff): con un payoff de 3, basta acertar el 25 %; con 0,5, hace falta el 67 %.
- El win rate solo engaña: TSMOM acierta el 33 % y gana; una reversión sin ventaja acierta el 63 % y pierde 23 pb por operación, todo en las crisis: un win rate alto suele esconder un riesgo de crash.
- La forma importa: en TSMOM el mejor 10 % de las operaciones aporta el 212 % del beneficio; en el carry, el peor 10 % de los meses se lleva el 160 %.
- Alpha y beta se estiman con errores robustos (Newey-West). En los datos, la cartera de factores plantados da t = 4,1; el placebo ESG, t = 2,0 por azar; la calidad, −2,9 por confusión con el value.
- Information ratio = rentabilidad activa / tracking error; appraisal ratio = alfa / volatilidad residual. Informa de los dos y del benchmark.
- La capacidad sale de la rotación, la horquilla y la participación en el volumen. La cartera de factores pierde la mitad del alfa hacia 190 millones y todo hacia 800 millones, con los supuestos de la lección.
Recursos
Libros
- Whitney K. Newey y Kenneth D. West, «A Simple, Positive Semi-Definite, Heteroskedasticity and Autocorrelation Consistent Covariance Matrix», Econometrica 55(3), 703-708 (1987)
- Tobias J. Moskowitz, Yao Hua Ooi y Lasse Heje Pedersen, «Time series momentum», Journal of Financial Economics 104(2), 228-250 (2012)