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.

Fig. — Win rate frente a payoff, con curvas de igual expectativa (en unidades de la pérdida media L): la reversión acierta más que nadie y es la única que pierde
Lecciones del nivel 2
  1. 2.1 · Estadística y econometría para trading

    1. 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
    2. 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
    3. 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
    4. 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
    5. 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
    6. 2.1.6 Filtro de Kalman para hedge ratios dinámicos
    7. 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
    8. 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
  2. 2.2 · Anatomía de un sistema de trading

    1. 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
    2. 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
    3. 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
    4. 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
  3. 2.3 · Familias de estrategias

    1. 2.3.1 Seguimiento de tendencia y momentum
    2. 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
    3. 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
    4. 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
    5. 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
    6. 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
    7. 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
  4. 2.4 · Backtesting: motores y paradigmas

    1. 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
    2. 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
    3. 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
    4. 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
    5. 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
    6. 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
    7. 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
    8. 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
  5. 2.5 · Sesgos y validación

    1. 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
    2. 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
    3. 2.5.3 Validación cruzada combinatoria purgada para estrategias
    4. 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
    5. 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
    6. 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
    7. 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
    8. 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
  6. 2.6 · Métricas de rendimiento

    1. 2.6.1 CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index
    2. 2.6.2 Win rate, payoff, expectativa, alpha, beta, information ratio, turnover y capacidad
    3. 2.6.3 Informes con QuantStats o pyfolio-reloaded y tearsheets propios

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

Posiciones diariasOperacionesWin rate pW y L mediasExpectativaProfit factorRentabilidadAlfa y betaTurnoverCapacidad
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.

IN [01]python
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)
OUT [01]TSMOM y carry de la lección 2.6.1
(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.

IN [02]python
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]]
OUT [02]Lista de operaciones de TSMOM
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     AGA

Son 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 pp es la fracción de operaciones ganadoras (win rate), WW la ganancia media de las ganadoras y LL la pérdida media de las perdedoras (en positivo):

payoff=WL,E=p W−(1−p) L,PF=∑ganadoraspnl∣∑perdedoraspnl∣=p W(1−p) L\text{payoff} = \frac{W}{L}, \qquad E = p\,W - (1 - p)\,L, \qquad PF = \frac{\sum_{\text{ganadoras}} \text{pnl}}{\left|\sum_{\text{perdedoras}} \text{pnl}\right|} = \frac{p\,W}{(1 - p)\,L}

La expectativa EE es lo que se gana de media por operación. El profit factor PFPF compara lo ganado con lo perdido: mayor que 1 si y solo si E>0E > 0. Igualando EE a cero sale el win rate de equilibrio, el mínimo que hace falta para no perder con un payoff dado:

p∗=11+W/Lp^{*} = \frac{1}{1 + W/L}

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.

IN [03]python
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)
OUT [03]Win rate, payoff, expectativa y profit factor
                        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.495

TSMOM 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:

IN [04]python
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)
OUT [04]De dónde sale el dinero
              tsmom  carry_mes
mejor_10%      2.12       1.45
peor_10%      -0.63      -1.60
sin_extremos  -0.49       1.15
Fig. — De dónde sale el beneficio: aporte de las mejores, las centrales y las peores operaciones al beneficio total de TSMOM y del carry FX

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

IN [05]python
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)
OUT [05]Reversión sin ventaja: objetivo cercano, stop lejano
  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.023794

En un paseo aleatorio sin deriva, un objetivo a +a+a y un stop a −b-b se tocan antes con probabilidad b/(a+b)b/(a+b): 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:

IN [06]python
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)
OUT [06]Con la horquilla de cada acción
                         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.665

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

IN [07]python
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)
OUT [07]Dentro y fuera de los tramos de estrés
                        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.667

Fuera 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:

rt−rf,t=α+β (rm,t−rf,t)+εtr_t - r_{f,t} = \alpha + \beta\,(r_{m,t} - r_{f,t}) + \varepsilon_t

β\beta es la exposición al índice y α\alpha 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.
IN [08]python
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)
OUT [08]Cartera de factores, placebo ESG e índice equiponderado
          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.299

Las 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 α\alpha 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, 4 (T/100)2/94\,(T/100)^{2/9}, da 4,8 con 228 meses):

IN [09]python
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)
OUT [09]Alpha y beta con errores de Newey-West
           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
Fig. — Alfa anual de cuatro carteras de 20 acciones frente al índice equiponderado, 2007-2025, con errores de Newey-West: el placebo ESG pasa el listón de t = 2 por azar
  • 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.

RentabilidadBeta x mercadoAlfaResiduoAppraisal ratioActiva vs índiceInformation ratio
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:

IR=r−rm‾σ(r−rm) 12IR = \frac{\overline{r - r_m}}{\sigma(r - r_m)}\,\sqrt{12}

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.

IN [10]python
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)
OUT [10]Information ratio y tracking error
          activa_anual  tracking_error     ir  appraisal
factores         0.042           0.070  0.595      0.847
esg              0.024           0.054  0.440      0.497

La 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:

TO=12∑i∣wi,t−wi,t−1∣TO = \frac{1}{2}\sum_i \left| w_{i,t} - w_{i,t-1} \right|
IN [11]python
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)
OUT [11]Turnover
indice      0.01
factores    2.28
esg         2.65
calidad     0.40
tamano      0.64
dtype: float64

El í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:

ci=si2+σiΔwi⋅KADVic_i = \frac{s_i}{2} + \sigma_i \sqrt{\frac{\Delta w_i \cdot K}{ADV_i}}

con sis_i la horquilla, σi\sigma_i la volatilidad diaria, KK el capital gestionado y ADViADV_i 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.

IN [12]python
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)
OUT [12]Capacidad: coste anual según el capital gestionado
       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.7072
Fig. — Capacidad de la cartera de factores: alfa anual neto de horquilla e impacto según el capital gestionado (modelo de raíz cuadrada supuesto en la lección)

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

CapitalOrden por acciónTurnoverParticipación ADVImpactoHorquillaCosteAlfa neto
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 1/(1+payoff)1/(1 + \text{payoff}).
  • 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

  1. Agrupa las operaciones de TSMOM por sector (columna sector de futuros2_especificaciones.csv) y calcula win rate, payoff y expectativa de cada uno. ¿Hay algún sector por debajo del equilibrio?
Solución
IN [13]python
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)
OUT [13]TSMOM por sector
                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.216

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

  1. 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
IN [14]python
op_sim = reversion(objetivo=2.0, stop=2.0)
pd.Series(metricas_op(op_sim["pnl"])).round(3)
OUT [14]Reversión simétrica
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: float64

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

  1. 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
IN [15]python
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}")
OUT [15]Beta de TSMOM frente al índice
beta 0.089 · alfa 11.38% (t HAC 4.36)
correlación mensual: 0.162

La 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

  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}). ↩

Respuesta plegada en cada pregunta

Autoevaluación

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

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

  3. 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).

  4. ¿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.

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

Datos