Lección 2.3.1 · 75 min

Seguimiento de tendencia y momentum

Dos familias con el mismo nombre: comprar lo que sube frente a su propia historia (tendencia) o frente a los demás (momentum transversal). Esta lección construye las dos con costes y ejecución a la sesión siguiente, sobre 24 futuros y 120 acciones sintéticas con la prima plantada, y las compara con controles que no tienen ninguna.

Fig. — Tres reglas de tendencia en 24 futuros sintéticos con la prima plantada, netas de costes, frente a un control con rendimientos permutados (2001-2025)
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

Dos ideas con el mismo nombre

«Momentum» se usa para dos estrategias distintas que comparten una intuición: lo que ha subido tiende a seguir subiendo durante un tiempo. La diferencia está en contra qué se compara.

  • Momentum de series temporales (time series momentum, TSMOM, o simplemente tendencia): cada activo se compara con su propio pasado. Si el futuro del crudo ha subido en los últimos doce meses, se compra; si ha bajado, se vende. Cada mercado se decide por separado y la cartera puede estar larga o corta en todo a la vez. Es lo que hacen los CTA y los fondos de gestión de futuros.
  • Momentum transversal (cross-sectional momentum): los activos se comparan entre sí. Cada mes se ordenan las acciones por su rentabilidad pasada, se compran las mejores y se venden las peores. La cartera es neutral por construcción: gana si los ganadores baten a los perdedores, aunque todo el mercado caiga.
Precios pasadosContra su pasadoContra los demásSigno por activoRango en el corteLargo o corto netoLargo-corto neutro
Fuente Mermaid
flowchart TB
  P[Precios pasados] --> TS[Contra su pasado]
  P --> XS[Contra los demás]
  TS --> S1[Signo por activo]
  XS --> S2[Rango en el corte]
  S1 --> R1[Largo o corto neto]
  S2 --> R2[Largo-corto neutro]

Las dos tienen evidencia académica amplia en datos reales. Jegadeesh y Titman documentaron en 1993 que comprar ganadores y vender perdedores de los 3 a 12 meses anteriores daba rentabilidades positivas significativas en acciones de EE. UU., y que parte se deshacía en los dos años siguientes1Nota 1 · N. Jegadeesh y S. Titman, «Returns to Buying Winners and Selling Losers: Implications for Stock Market Efficiency», Journal of Finance 48(1), 1993, 65-91. DOI 10.1111/j.1540-6261.1993.tb04702.x.. Moskowitz, Ooi y Pedersen encontraron en 2012 momentum de series temporales en cada uno de los 58 futuros líquidos que estudiaron (índices, divisas, materias primas y bonos): persistencia de 1 a 12 meses que se revierte en parte a horizontes más largos, y una cartera diversificada que rinde mejor en los mercados extremos2Nota 2 · T. J. Moskowitz, Y. H. Ooi y L. H. Pedersen, «Time series momentum», Journal of Financial Economics 104(2), 2012, 228-250. DOI 10.1016/j.jfineco.2011.11.003. Escalado al 40 % y estimador EWMA con centro de masa de 60 días: secciones 2.4 y 4.1; la sonrisa, figura 4.. Esta lección no repite esas cifras: construye las estrategias con el método de esos trabajos y las prueba en datos donde se conoce la respuesta.

Futuros: rentabilidades, rolls y costes

Los 24 mercados de futuros2_continuos.parquet cubren índices, bonos, energía, metales, agrícolas y divisas de 2000 a 2025. La columna cierre_ajustado es la serie continua con ajuste proporcional hacia atrás en cada roll: su variación porcentual es la rentabilidad de mantener el futuro (lección 1.4.6). La columna contrato dice qué vencimiento se tiene cada día, y con ella se detectan los rolls, que cuestan dinero.

IN [01]python
import numpy as np
import pandas as pd

fut = pd.read_parquet("futuros2_continuos.parquet")
spec = pd.read_csv("futuros2_especificaciones.csv")
spec = spec.set_index("mercado")
ancho = lambda col: fut.pivot(index="fecha", columns="mercado",
                              values=col)
px, cierre, contrato = (ancho("cierre_ajustado"), ancho("cierre"),
                        ancho("contrato"))
r = px.pct_change()
roll = (contrato != contrato.shift()).astype(float)
roll.iloc[0] = 0.0
print(px.shape[1], "mercados,", len(px), "sesiones,",
      px.index[0].date(), "a", px.index[-1].date())
rolls_ano = (roll.sum() / 26).round().astype(int)
print("rolls al año:", rolls_ano.value_counts().sort_index().to_dict())
OUT [01]Futuros continuos y rolls
24 mercados, 6539 sesiones, 2000-01-03 a 2025-12-31
rolls al año: {4: 14, 5: 5, 6: 1, 7: 1, 12: 3}

Cuatro rolls al año en los financieros (vencimientos trimestrales), doce en la energía (mensuales) y entre cuatro y siete en metales y agrícolas. Cada roll son dos operaciones: cerrar el contrato que vence y abrir el siguiente.

El coste de una operación sale de las especificaciones: un deslizamiento típico en ticks por lado más la comisión por contrato. Para expresarlo como fracción del nominal hay que dividir por el valor del contrato, cierre × multiplicador:

clado=nticks⋅tickF+comisionF⋅multiplicadorc_{\text{lado}} = \frac{n_{\text{ticks}} \cdot \text{tick}}{F} + \frac{\text{comision}}{F \cdot \text{multiplicador}}
IN [02]python
c_lado = (spec["coste_tipico_ticks"] * spec["tick"] / cierre
          + spec["comision_usd"] / (cierre * spec["multiplicador"]))
pb = (1e4 * c_lado.median()).sort_values()
print(pb.round(1).iloc[[0, 1, 2, 12, 21, 22, 23]].to_string())
print(f"mediana de los 24 mercados: {pb.median():.1f} pb")
OUT [02]Coste por lado en puntos básicos del nominal
mercado
IDB     0.7
DIA     0.8
BOA     0.9
BOE     3.1
AGA    10.9
AGB    11.3
MEB    13.2
mediana de los 24 mercados: 2.7 pb

La salida enseña los tres más baratos, el mercado del centro y los tres más caros. La mediana de los 24 es de 2,7 puntos básicos por lado, pero los extremos se separan casi veinte veces por una razón que conviene recordar con datos reales: el tick es fijo en precio. En el índice IDB, un tick de 0,25 puntos sobre un precio mediano de unos 5.000 pesa medio punto básico; en la plata (MEB), un tick de 0,005 USD sobre unos 7,9 USD pesa más de 6, y las especificaciones suponen dos ticks de deslizamiento por lado: 13,2 pb. El coste relevante no es «un tick», sino un tick dividido por el precio, y cambia con el tiempo: el trigo (AGB) se mueve en estos datos entre 1,86 y 11,58 USD, así que el mismo tick le cuesta seis veces más en porcentaje en su suelo que en su pico.

Momentum de series temporales con escalado de volatilidad

La regla de Moskowitz, Ooi y Pedersen tiene dos piezas. La señal es el signo de la rentabilidad de los últimos doce meses. El tamaño hace que cada mercado tenga el mismo riesgo ex ante: la posición es un objetivo de volatilidad (40 % anual en el artículo) dividido por la volatilidad estimada con una media exponencial de los rendimientos diarios al cuadrado, con centro de masa de 60 sesiones y con datos hasta la víspera2:

wi,t=signo⁡(Pi,tPi,t−252−1)⋅40 %σ^i,tw_{i,t} = \operatorname{signo}\left(\frac{P_{i,t}}{P_{i,t-252}} - 1\right) \cdot \frac{40\,\%}{\hat\sigma_{i,t}}

Sin ese escalado, el gas natural (50 % de volatilidad anual en estos datos) pesaría veinticinco veces más que el bono a 2 años (2 %). Con él, la media de los 24 mercados es una cartera en la que cada uno aporta un riesgo parecido.

La ejecución es la de todo el nivel: la señal se calcula con el cierre de la sesión t y se opera al cierre de t+1; la posición gana o pierde desde ahí. Los costes se cobran sobre cada cambio de posición y sobre la posición entera en cada roll (dos lados).

Cierre tSeñal y tamañoOrden cierre t+1Gana desde t+1
Fuente Mermaid
flowchart LR
  C[Cierre t] --> S[Señal y tamaño]
  S --> E[Orden cierre t+1]
  E --> G[Gana desde t+1]
IN [03]python
def backtest(senal, r, objetivo=0.40):
    """Señal en {-1, 0, 1} decidida al cierre de t; se opera en t+1."""
    vol = r.ewm(com=60, min_periods=60).std() * np.sqrt(252)
    w = (senal * objetivo / vol).clip(-4, 4)
    pos = w.shift(1).fillna(0.0)          # tenida desde el cierre t+1
    bruto = (pos.shift(1) * r).fillna(0.0)
    giro = (pos - pos.shift()).abs()
    coste = (giro * c_lado + pos.abs() * roll * 2 * c_lado).fillna(0.0)
    return bruto.mean(axis=1), coste.mean(axis=1), pos

def sharpe(x):
    return x.mean() / x.std() * np.sqrt(252)

def resumen(bruto, coste):
    neto = bruto - coste
    return pd.Series({
        "sharpe_bruto": sharpe(bruto), "sharpe_neto": sharpe(neto),
        "vol_anual": neto.std() * np.sqrt(252),
        "coste_anual": coste.mean() * 252,
        "max_dd": ((1 + neto).cumprod()
                   / (1 + neto).cumprod().cummax() - 1).min()})

tsmom = np.sign(px / px.shift(252) - 1)
b_ts, c_ts, pos_ts = backtest(tsmom, r)
resumen(b_ts, c_ts).round(3)
OUT [03]Un motor vectorizado para cualquier señal
sharpe_bruto    0.946
sharpe_neto     0.772
vol_anual       0.105
coste_anual     0.018
max_dd         -0.212
dtype: float64

La cartera de 24 mercados tiene un Sharpe bruto de 0,95 y neto de 0,77, con una volatilidad del 10,5 % anual (cerca del 12 % que da el artículo con el mismo objetivo del 40 % por mercado) y una caída máxima del 21 %. Los costes, 1,8 puntos al año, se llevan casi una quinta parte del Sharpe: el escalado de volatilidad cambia la posición todos los días un poco, y cada roll paga dos lados sobre toda la posición. El README de los datos da, con otra ponderación (10 % sobre la volatilidad de 63 sesiones) y sin costes, un Sharpe de 0,98 para la misma señal: el motor recupera lo que el generador puso. El clip(-4, 4) limita el apalancamiento de un mercado a cuatro veces su nominal cuando su volatilidad estimada se hunde. La figura de la cabecera enseña cómo crece 1 USD con esta regla y con las otras dos de la lección, frente al control de la sección siguiente.

IN [04]python
vol = r.ewm(com=60, min_periods=60).std() * np.sqrt(252)
pos_m = (tsmom * 0.40 / vol).clip(-4, 4).shift(1).fillna(0.0)
bruto_m = (pos_m.shift(1) * r).fillna(0.0)
coste_m = ((pos_m - pos_m.shift()).abs() * c_lado
           + pos_m.abs() * roll * 2 * c_lado).fillna(0.0)
por_mercado = pd.DataFrame({
    "bruto": bruto_m.apply(sharpe),
    "neto": (bruto_m - coste_m).apply(sharpe)})
print(por_mercado.describe().loc[["mean", "50%", "min", "max"]]
      .round(2).T.to_string())
print("mercados con Sharpe neto > 0:",
      (por_mercado["neto"] > 0).sum(), "de 24")
rho = (bruto_m.corr().to_numpy()[np.triu_indices(24, 1)]).mean()
print(f"correlación media entre mercados: {rho:.2f}")
OUT [04]Mercado a mercado
       mean   50%   min   max
bruto  0.29  0.26 -0.13  0.79
neto   0.24  0.24 -0.25  0.71
mercados con Sharpe neto > 0: 21 de 24
correlación media entre mercados: 0.05

Mercado a mercado, el Sharpe bruto medio es 0,29 y la mediana 0,26: por encima del 0,15 plantado, porque la regla también captura las tendencias del guion (las crisis largas en los índices, la caída de los bonos en 2022) y la deriva de los índices. El rango va de −0,13 a 0,79, y 21 de los 24 mercados ganan en neto: con 26 años de datos, el error típico de un Sharpe anual es ≈ 1/√26 ≈ 0,20, así que la dispersión entre mercados es casi toda ruido. Lo que convierte una ventaja pequeña y ruidosa en una cartera con Sharpe de 0,95 es la diversificación: con una correlación media de 0,05 entre los resultados de los mercados,

SRcartera≈SRmercadoN1+(N−1)ρ=0,29241+23⋅0,05≈0,97SR_{\text{cartera}} \approx SR_{\text{mercado}} \sqrt{\frac{N}{1 + (N-1)\rho}} = 0{,}29 \sqrt{\frac{24}{1 + 23 \cdot 0{,}05}} \approx 0{,}97

Es la razón de ser de los CTA: ninguna tendencia individual es fiable, pero muchas poco correladas sí suman.

Un control que hay que saber leer

¿Cuánto de ese Sharpe es tendencia de verdad? El control natural es repetir la estrategia sobre rendimientos permutados: los mismos rendimientos diarios de cada mercado en un orden aleatorio, sin ninguna memoria. Si la regla solo explotara la tendencia, daría cero. Hay dos maneras de permutar, y en general no dicen lo mismo:

IN [05]python
rng = np.random.default_rng(2028)

def permuta(r, quitar_media):
    out = r.copy()
    for m in r:
        v = r[m].to_numpy().copy()
        ok = ~np.isnan(v)
        x = v[ok] - (v[ok].mean() if quitar_media else 0.0)
        v[ok] = rng.permutation(x)
        out[m] = v
    return out

for quitar_media in (False, True):
    res = []
    for _ in range(10):
        rp = permuta(r, quitar_media)
        pp = (1 + rp.fillna(0)).cumprod()
        b, c, _ = backtest(np.sign(pp / pp.shift(252) - 1), rp)
        res.append([sharpe(b), sharpe(b - c)])
    m, s = np.mean(res, axis=0), np.std(res, axis=0)
    print(f"media restada={quitar_media!s:5}  bruto {m[0]:+.2f} "
          f"± {s[0]:.2f}   neto {m[1]:+.2f} ± {s[1]:.2f}")
OUT [05]Control: rendimientos permutados
media restada=False  bruto +0.06 ± 0.17   neto -0.21 ± 0.18
media restada=True   bruto -0.12 ± 0.22   neto -0.39 ± 0.22

Las dos permutaciones dan un Sharpe bruto compatible con cero: +0,06 ± 0,17 sin restar la media y −0,12 ± 0,22 restándola, en diez permutaciones cada una. En neto, −0,21 y −0,39: lo que cuesta operar sin ventaja. Frente a esos controles, el 0,95 de la regla real está a más de cuatro desviaciones típicas: es tendencia, no ruido.

La diferencia entre las dos versiones es la que hay que saber leer. Permutar conserva la media de cada mercado. En un mercado que sube en promedio, la rentabilidad de doce meses es positiva la mayor parte del tiempo y la regla está casi siempre larga: cobra la deriva, no una tendencia. Por eso el control sin restar la media sale por encima del otro (aquí, unos 0,2 de Sharpe, que con diez permutaciones tampoco se separa del ruido). En estos datos esa parte es pequeña porque casi no hay deriva que cobrar: los bonos no tienen prima de plazo, las materias primas y las divisas revierten a un ancla a través del carry y solo los cinco índices llevan la prima de la renta variable. En mercados reales, con décadas de bonos al alza y de prima de la renta variable, la deriva puede ser una parte grande de lo que gana una regla de tendencia larga en promedio. Las dos fuentes son legítimas, pero son dos apuestas distintas y conviene saber cuánto pesa cada una.

Cruces de medias y rupturas Donchian: la misma idea, otro horizonte

Las reglas clásicas de los CTA son variaciones de la misma idea con otros filtros:

  • Cruce de medias: largo si la media de 50 sesiones está por encima de la de 200; corto si está por debajo (lección 1.7.1 para las medias).
  • Ruptura Donchian: largo cuando el precio supera el máximo de las n sesiones anteriores; corto cuando perfora el mínimo. Las reglas originales de los Turtle, publicadas por uno de los traders del grupo, usaban dos sistemas basados en el canal de Richard Donchian: el Sistema 1, con ruptura de 20 sesiones y salida al perforar el extremo contrario de 10, y el Sistema 2, con 55 y 203Nota 3 · «The Original Turtle Trading Rules», publicadas en tradingblox.com/originalturtles: N y unidades en las páginas 13-14, sistemas de entrada en la 18 y salidas en la 26. Los Turtle operaban la ruptura en cuanto se producía durante la sesión; aquí se opera al cierre siguiente, que es más conservador.. El tamaño dependía de N, la media exponencial de 20 sesiones del rango verdadero: cada unidad se dimensionaba para que un movimiento de N costara el 1 % de la cuenta. Es otra forma de escalar por volatilidad.

La ruptura es una máquina de estados: plano, largo o corto, con transiciones que dependen del canal.

rompe máx. 55rompe mín. 55pierde mín. 20supera máx. 20rompe mín. 55rompe máx. 55PLANOLARGOCORTO
Fuente Mermaid
stateDiagram-v2
  [*] --> PLANO
  PLANO --> LARGO: rompe máx. 55
  PLANO --> CORTO: rompe mín. 55
  LARGO --> PLANO: pierde mín. 20
  CORTO --> PLANO: supera máx. 20
  LARGO --> CORTO: rompe mín. 55
  CORTO --> LARGO: rompe máx. 55

Con datos de cierre no hay máximos y mínimos de sesión, así que el canal se calcula con cierres y el tamaño se hace con la misma volatilidad EWMA que la regla de 12 meses: así las tres reglas solo se diferencian en la señal.

IN [06]python
cruce = np.sign(px.rolling(50).mean() - px.rolling(200).mean())

def donchian(p, entrada=55, salida=20):
    alto = p.rolling(entrada).max().shift(1).to_numpy()
    bajo = p.rolling(entrada).min().shift(1).to_numpy()
    s_alto = p.rolling(salida).max().shift(1).to_numpy()
    s_bajo = p.rolling(salida).min().shift(1).to_numpy()
    a = p.to_numpy()
    out = np.zeros(a.shape)
    for j in range(a.shape[1]):
        est = 0
        for i in range(a.shape[0]):
            if np.isnan(alto[i, j]):
                continue
            x = a[i, j]
            if x > alto[i, j]:
                est = 1
            elif x < bajo[i, j]:
                est = -1
            elif est == 1 and x < s_bajo[i, j]:
                est = 0
            elif est == -1 and x > s_alto[i, j]:
                est = 0
            out[i, j] = est
    return pd.DataFrame(out, index=p.index, columns=p.columns)

ruptura = donchian(px)
b_cr, c_cr, _ = backtest(cruce, r)
b_dn, c_dn, _ = backtest(ruptura, r)
tabla = pd.DataFrame({"tsmom_12m": resumen(b_ts, c_ts),
                      "cruce_50_200": resumen(b_cr, c_cr),
                      "donchian_55_20": resumen(b_dn, c_dn)})
print(tabla.round(3).to_string())
print("tiempo en mercado de la ruptura:",
      f"{(ruptura != 0).to_numpy().mean():.0%}")
OUT [06]Cruce 50/200 y ruptura Donchian 55/20
              tsmom_12m  cruce_50_200  donchian_55_20
sharpe_bruto      0.946         0.819           0.647
sharpe_neto       0.772         0.701           0.491
vol_anual         0.105         0.106           0.091
coste_anual       0.018         0.012           0.014
max_dd           -0.212        -0.214          -0.311
tiempo en mercado de la ruptura: 63%

Las tres reglas ganan con la misma prima plantada, y el orden sigue al horizonte. La regla de 12 meses, que mira exactamente lo que el generador premia (el signo de las 252 sesiones anteriores), tiene el mejor Sharpe neto (0,77); el cruce 50/200, que mira tendencias de varios meses, se queda cerca (0,70, con la misma caída máxima del 21 %); la ruptura Donchian es la peor (0,49 neto, caída del 31 %). La ruptura solo está en el mercado el 63 % del tiempo, porque la salida de 20 sesiones la deja plana tras cualquier retroceso, y reacciona a movimientos de dos o tres meses que aquí son sobre todo ruido. Con un único conjunto de datos y tres reglas, la diferencia entre 0,70 y 0,77 está dentro del error de un Sharpe de 26 años (≈ 0,2); la de 0,49 a 0,77 empieza a no estarlo, y tiene una razón: el efecto plantado es de un año.

IN [07]python
netos = pd.DataFrame({"tsmom": b_ts - c_ts, "cruce": b_cr - c_cr,
                      "donchian": b_dn - c_dn})
netos.loc["2001":].corr().round(2)
OUT [07]¿Cuánto se parecen?
          tsmom  cruce  donchian
tsmom      1.00   0.71      0.33
cruce      0.71   1.00      0.30
donchian   0.33   0.30      1.00

La regla de 12 meses y el cruce 50/200 tienen una correlación de 0,71: las dos miran tendencias de meses. La ruptura de 55 sesiones es más rápida y solo correla 0,33 y 0,30 con las otras dos. En la práctica, los CTA combinan varios horizontes por eso mismo: diversifican entre velocidades de tendencia además de entre mercados.

¿Hay sonrisa de la tendencia?

Una propiedad que hace valiosa la tendencia en una cartera es cómo se comporta cuando la renta variable se mueve mucho. Moskowitz, Ooi y Pedersen dibujan las rentabilidades trimestrales de su cartera diversificada frente a las del S&P 500 y encuentran una forma de sonrisa: le va mejor en los trimestres extremos, al alza y a la baja2. No es una ley de la regla, sino una consecuencia de cómo fueron las crisis de su muestra: largas, y precedidas de caídas que ya habían puesto la regla corta. Aquí se puede comprobar si sale en unos datos donde el efecto plantado no depende del estado de la renta variable.

IN [08]python
trim = lambda x: (1 + x).resample("QE").prod() - 1
q = pd.DataFrame({"indice": trim(r["IDA"].fillna(0)),
                  "tendencia": trim(b_ts - c_ts)}).loc["2001":]
a2, a1, a0 = np.polyfit(q["indice"], q["tendencia"], 2)
print(f"ajuste: {a0:+.3f} {a1:+.3f}·x {a2:+.3f}·x²")
extremos = q["indice"].abs() > q["indice"].abs().quantile(0.8)
print(q.groupby(extremos)["tendencia"].mean().rename(
    {False: "80 % central", True: "20 % extremo"}).round(4))
print(q.nsmallest(4, "indice").round(3))
OUT [08]Rentabilidades trimestrales frente al índice IDA
ajuste: +0.021 +0.078·x -0.094·x²
indice
80 % central    0.0236
20 % extremo    0.0131
Name: tendencia, dtype: float64
            indice  tendencia
fecha
2020-03-31  -0.510     -0.078
2001-09-30  -0.275      0.178
2008-12-31  -0.268      0.062
2011-09-30  -0.209     -0.086
Fig. — Sin sonrisa: rentabilidad trimestral de la cartera de tendencia en 24 futuros frente a la del índice IDA, 2001-2025

No sale. El término en x² del ajuste es −0,094, ligeramente negativo (una parábola casi plana y algo cóncava, no una sonrisa), y la pendiente, +0,078: la cartera de tendencia sube un poco con la renta variable, porque está larga en los índices la mayor parte del tiempo. En el 80 % central de los trimestres del índice la tendencia gana de media un 2,4 %; en el 20 % de trimestres más extremos, un 1,3 %. De los cuatro peores trimestres del índice, dos fueron buenos para la tendencia (el tercero de 2001, −27,5 % el índice y +17,8 % la tendencia, y el cuarto de 2008, −26,8 % y +6,2 %) y dos malos (el primero de 2020, −51,0 % y −7,8 %, y el tercero de 2011, −20,9 % y −8,6 %).

La diferencia entre unas crisis y otras se ve mejor con los tramos de estrés del guion de los datos (los del README), desde que la señal existe (enero de 2001), mirando la posición en el índice IDA al empezar cada tramo:

IN [09]python
tramos = [("2001-01-03", "2002-10-09"), ("2008-09-02", "2009-03-09"),
          ("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")]
neto = b_ts - c_ts
filas = []
for a, b in tramos:
    filas.append({"desde": a, "sesiones": len(r.loc[a:b]),
                  "IDA": (1 + r["IDA"].loc[a:b]).prod() - 1,
                  "tendencia": (1 + neto.loc[a:b]).prod() - 1,
                  "pos_IDA_ini": pos_ts["IDA"].loc[a:].iloc[0]})
print(pd.DataFrame(filas).set_index("desde").round(3).to_string())
OUT [09]La tendencia en los tramos de estrés
            sesiones    IDA  tendencia  pos_IDA_ini
desde
2001-01-03       442 -0.499      0.001        1.559
2008-09-02       130 -0.406      0.140       -2.693
2011-07-25        50 -0.213     -0.098        2.731
2015-08-17       124 -0.071     -0.138        3.689
2018-10-01        59 -0.115     -0.088        4.000
2020-02-20        23 -0.400     -0.082        2.939
2022-01-04       195 -0.038      0.072        2.307
2025-02-20        34 -0.144     -0.049        2.743

En seis de los ocho tramos la regla empezaba larga en el índice (pos_IDA_ini positiva), porque llegaban tras un periodo alcista. Las caídas rápidas de 23 a 59 sesiones (2011, 2018, 2020 y 2025) la pillan larga y no le dan tiempo a girar: pierde entre un 4,9 % y un 9,8 % en cada una. En 2015-2016 pierde un 13,8 % en un tramo de vaivenes en el que el índice solo cae un 7,1 %. El tramo de 2001-2002, el más largo (442 sesiones y −49,9 % del índice), lo termina plana (+0,1 %): gira a corto y gana el tercer trimestre de 2001, pero devuelve casi todo en los rebotes. Los dos tramos que gana son los que el guion hace largos o precedidos de caídas: 2008-2009 (+14,0 %), en el que ya estaba corta en el índice al empezar, y 2022 (+7,2 % con el índice en −3,8 %), donde la ganancia viene de los bonos, que caen durante meses.

Esa es la lectura útil de la sonrisa. Una regla de doce meses protege en las crisis que llegan despacio o que duran, y pierde en las caídas bruscas tras un mercado alcista; la prima plantada aquí (0,15·σ·signo) no depende del estado de la renta variable, así que la forma del gráfico depende solo de cómo son las crisis de la muestra. En los datos de Moskowitz, Ooi y Pedersen predominaron las del primer tipo. No es un seguro contra el próximo crash.

Momentum transversal 12-1 en acciones

El universo universo2 tiene 120 acciones ficticias de 2006 a 2025, con salidas a bolsa, cuatro quiebras y seis compras o fusiones. La señal clásica es la rentabilidad 12-1: del cierre de hace doce meses al de hace uno, saltándose el último mes (que en datos reales tiene un efecto de reversión de corto plazo propio; lección 2.3.2).

Tres decisiones evitan los sesgos de siempre:

  1. Universo point-in-time: cada mes solo entran las acciones que cotizaban ese mes (lección 1.4.4).
  2. Ejecución en la sesión siguiente: la señal se calcula con el cierre del último día del mes y la cartera se forma al cierre de la primera sesión del mes siguiente. La rentabilidad va de esa sesión a la primera del mes siguiente.
  3. Rentabilidad de deslistado: si una acción deja de cotizar durante el mes, su rentabilidad incluye la del deslistado (−100 % en una quiebra). Sin ella, las quiebras desaparecen justo antes de su peor día.
IN [10]python
aj = pd.read_parquet("universo2_ajustados.parquet")
P = aj.pivot(index="fecha", columns="ticker",
             values="cierre_ajustado")
ev = pd.read_csv("universo2_eventos.csv", parse_dates=["fecha"])
desl = ev.dropna(subset=["rentabilidad_deslistado"])
ses = P.index
mes = ses.to_period("M")
fin = ses[np.r_[mes[1:] != mes[:-1], True]][:-1]   # último día
ent = ses[ses.searchsorted(fin, side="right")]      # sesión siguiente

Pf = P.loc[fin]
mom = Pf.shift(1) / Pf.shift(12) - 1                # 12-1
Pe = P.loc[ent].set_axis(fin)
fwd = Pe.shift(-1) / Pe - 1                         # de ent a ent

for _, d in desl.iterrows():            # mes en que deja de cotizar
    k = ent.searchsorted(d["fecha"]) - 1
    p0 = Pe.iloc[k][d["ticker"]]
    p_ult = P[d["ticker"]].dropna().iloc[-1]
    fwd.iloc[k, fwd.columns.get_loc(d["ticker"])] = (
        p_ult / p0 * (1 + d["rentabilidad_deslistado"]) - 1)

mom, fwd = mom.loc["2007":], fwd.loc["2007":]
validas = mom.notna() & fwd.notna()
n_mes = validas.sum(axis=1).iloc[:-1]   # el último mes no tiene futuro
print(len(n_mes), "meses de cartera; acciones por mes:",
      n_mes.min(), "a", n_mes.max())
print(fwd.loc[:, ["QXQT", "QXKV"]].min().round(3).to_dict())
OUT [10]Señal 12-1 y rentabilidad del mes siguiente, con deslistados
226 meses de cartera; acciones por mes: 106 a 110
{'QXQT': -1.0, 'QXKV': -1.0}

Del orden de un centenar de acciones por mes (de 106 a 110: las salidas a bolsa entran cuando tienen doce meses de historia y los deslistados salen). Las dos primeras quiebras tienen en su último mes una rentabilidad de −100 %: el dato que faltaría si solo se usaran los precios.

El fichero universo2_caracteristicas.parquet trae esas rentabilidades ya calculadas: cada acción que deja de cotizar tiene una última fila sin precio, con una rentabilidad_mes que encadena lo que va de mes con la de deslistado. Aquí se construyen a mano desde los eventos para ver el mecanismo y porque la cartera va de la primera sesión de un mes a la del siguiente, no de fin de mes a fin de mes. Cuando más abajo se lee ese fichero para el placebo ESG y la horquilla, esas filas no cambian nada: caen en el mes en que la acción ya no tiene rentabilidad siguiente que predecir.

La medida estándar de una señal transversal es el coeficiente de información (IC): la correlación, cada mes, entre la señal y la rentabilidad siguiente en todo el corte transversal. La media de los IC mensuales y su estadístico t=IC‾/(sIC/T)t = \overline{IC} / (s_{IC} / \sqrt{T}) dicen si hay relación. Hay dos versiones:

IN [11]python
from scipy import stats

def ic_mensual(senal):
    filas = []
    for d in senal.index[:-1]:
        v = senal.loc[d].notna() & fwd.loc[d].notna()
        if v.sum() < 50:
            continue
        x, y = senal.loc[d][v], fwd.loc[d][v]
        filas.append((d, stats.spearmanr(x, y).statistic,
                      np.corrcoef(x, y)[0, 1]))
    return pd.DataFrame(filas, columns=["fecha", "rangos", "lineal"]
                        ).set_index("fecha")

ic = ic_mensual(mom)
t = ic.mean() / ic.std() * np.sqrt(len(ic))
pd.DataFrame({"ic_medio": ic.mean(), "t": t}).round(3)
OUT [11]IC de rangos frente a IC lineal
        ic_medio      t
rangos     0.027  2.808
lineal     0.041  4.295

El IC de rangos (Spearman) es 0,027 con t = 2,8: lo que predice el generador (≈ 0,03). El IC lineal (Pearson) es 0,041 con t = 4,3: parece una señal un 50 % más fuerte y mucho más segura. No lo es. La correlación lineal pondera cada observación por su distancia a la media, y cuatro quiebras, guiadas durante 320 sesiones hasta un último precio de entre 0,30 y 1,50 USD y que terminan en −100 %, son las observaciones más extremas del panel en las dos variables a la vez: momentum muy negativo y rentabilidad siguiente muy negativa. Unas pocas filas mueven la correlación. El IC de rangos solo usa el orden y no las ve más que como «la peor» de su mes.

Deciles, quiebras y un placebo

La imagen habitual del momentum es la de diez carteras: cada mes se ordenan las acciones por su señal, se reparten en deciles y se mide la rentabilidad media de cada uno. La cartera «ganadores menos perdedores» (WML, winners minus losers) compra el decil 10 y vende el 1.

IN [12]python
def deciles(senal, rent):
    out = {}
    for d in senal.index[:-1]:
        v = senal.loc[d].notna() & rent.loc[d].notna()
        if v.sum() < 50:
            continue
        g = pd.qcut(senal.loc[d][v].rank(method="first"), 10,
                    labels=range(1, 11))
        out[d] = rent.loc[d][v].groupby(g, observed=True).mean()
    return pd.DataFrame(out).T

D = deciles(mom, fwd)
quiebras = ["QXQT", "QXKV", "QXZT", "QXYX"]
D_sq = deciles(mom, fwd.drop(columns=quiebras))
car = pd.read_parquet("universo2_caracteristicas.parquet")
esg = car.pivot(index="fecha", columns="ticker",
                values="puntuacion_esg").reindex(mom.index)
D_esg = deciles(esg.where(mom.notna()), fwd)
anual = pd.DataFrame({"momentum": 12 * D.mean(),
                      "sin_quiebras": 12 * D_sq.mean(),
                      "placebo_esg": 12 * D_esg.mean()})
anual.loc["10-1"] = anual.loc[10] - anual.loc[1]
print(anual.round(3).T.to_string())
wml = D[10] - D[1]
w_esg = D_esg[10] - D_esg[1]
for nombre, x in (("WML momentum", wml), ("10-1 placebo", w_esg)):
    print(f"{nombre}: t = {x.mean() / x.std() * np.sqrt(len(x)):.2f}")
ic_esg = ic_mensual(esg.where(mom.notna()))["rangos"]
print(f"IC de rangos del placebo: {ic_esg.mean():.3f}, t = "
      f"{ic_esg.mean() / ic_esg.std() * np.sqrt(len(ic_esg)):.2f}")
OUT [12]Carteras por deciles
                  1      2      3      4      5      6      7      8      9     10   10-1
momentum     -0.046  0.049  0.097  0.101  0.096  0.089  0.110  0.094  0.075  0.114  0.160
sin_quiebras  0.013  0.045  0.103  0.101  0.098  0.096  0.106  0.094  0.076  0.112  0.098
placebo_esg   0.056  0.085  0.101  0.063  0.060  0.045  0.083  0.077  0.085  0.119  0.063
WML momentum: t = 4.25
10-1 placebo: t = 2.18
IC de rangos del placebo: 0.010, t = 1.46
Fig. — Rentabilidad anual media por deciles del momentum 12-1 y de un placebo (puntuación ESG) en 120 acciones sintéticas, 2007-2025

Tres lecturas de la tabla y de la figura:

  • Las quiebras hacen la mitad del diferencial. Con ellas, ganadores menos perdedores da un 16,0 % anual (t = 4,25); sin las cuatro acciones que quebraron, un 9,8 %. El decil 1 pasa de −4,6 % a +1,3 % al año. Lo plantado (0,0025·z al mes con z de ±1,75 en los deciles extremos) da ≈ 10 % anual: el diferencial sin quiebras es el que se acerca a la verdad.
  • El centro no está ordenado. Los deciles 2 a 9 no suben en escalera (el 9 rinde menos que el 3): con una docena de acciones por decil y volatilidades idiosincráticas del 20-30 %, la media de 19 años de cada decil tiene un error de varios puntos.
  • El placebo también tiene diferencial. La puntuación ESG, que no tiene ningún efecto, da 6,3 puntos al año entre su decil alto y el bajo, con un t de 2,18 que pasaría un contraste al 5 %. Su IC de rangos, con todo el corte transversal, es 0,010 con t = 1,46: no significativo. El diferencial de dos carteras extremas es una imagen, no una prueba (lección 2.1.8 para el problema de probar muchas variantes).

Costes y crashes

Una cartera de deciles se rehace cada mes. El coste de cada cambio es, como mínimo, media horquilla: la columna horquilla_pb de las características da la horquilla media cotizada de cada acción y mes.

IN [13]python
hs = car.pivot(index="fecha", columns="ticker",
               values="horquilla_pb").reindex(mom.index) / 2 / 1e4

def pesos(senal):
    W = {}
    for d in senal.index[:-1]:
        v = senal.loc[d].dropna()
        g = pd.qcut(v.rank(method="first"), 10, labels=False)
        w = pd.Series(0.0, index=senal.columns)
        w[g.index[g == 9]] = 1 / (g == 9).sum()
        w[g.index[g == 0]] = -1 / (g == 0).sum()
        W[d] = w
    return pd.DataFrame(W).T

W = pesos(mom)
bruto = (W * fwd.reindex(W.index)).sum(axis=1)
giro = (W - W.shift().fillna(0.0)).abs()
coste = (giro * hs.reindex(W.index)).sum(axis=1)
print(f"rotación mensual: {giro.sum(axis=1).mean():.0%} del capital")
print(f"bruto {12 * bruto.mean():.2%}  coste {12 * coste.mean():.2%}"
      f"  neto {12 * (bruto - coste).mean():.2%} al año")
OUT [13]Rotación y coste de la cartera WML
rotación mensual: 113% del capital
bruto 16.14%  coste 0.49%  neto 15.65% al año

La cartera cambia más de la mitad de sus posiciones cada mes (una rotación del 113 % sobre un capital bruto del 200 %, 100 % largo y 100 % corto), pero las acciones del universo son grandes y la media horquilla, de unos pocos puntos básicos, cuesta un 0,5 % al año. El momentum mensual en acciones grandes no lo matan los costes de horquilla; sí lo encarecen el impacto en carteras grandes y el préstamo en el lado corto, que en las acciones que van a quebrar es caro o imposible (lecciones 2.4.7 y 2.3.5).

El riesgo del momentum transversal no está en los costes, sino en los crashes. Daniel y Moskowitz muestran que el momentum sufre rachas de pérdidas infrecuentes y persistentes, en parte previsibles: llegan en estados de «pánico», tras caídas del mercado y con volatilidad alta, y suelen coincidir con rebotes4Nota 4 · K. Daniel y T. J. Moskowitz, «Momentum crashes», Journal of Financial Economics 122(2), 2016, 221-247. DOI 10.1016/j.jfineco.2015.12.002.. El mecanismo es sencillo: tras una caída fuerte, los perdedores son las acciones con más beta y más castigadas; cuando el mercado rebota, son las que más suben, y la cartera está corta en ellas. El generador de estos datos lo planta en los tres meses siguientes a los suelos de 2009 y 2020.

IN [14]python
meses_mes = wml.copy()
meses_mes.index = (wml.index.to_period("M") + 1).strftime("%Y-%m")
crash = meses_mes.loc[["2009-04", "2009-05", "2009-06",
                       "2020-04", "2020-05", "2020-06"]]
print(crash.round(3).to_string())
print("peores meses de toda la muestra:")
print(meses_mes.nsmallest(5).round(3).to_string())
OUT [14]WML en los meses tras los suelos
2009-04   -0.073
2009-05    0.035
2009-06   -0.161
2020-04   -0.046
2020-05   -0.032
2020-06   -0.032
peores meses de toda la muestra:
2009-06   -0.161
2022-02   -0.123
2014-09   -0.090
2008-06   -0.087
2023-03   -0.083

El trimestre tras el suelo de 2009 suma un −20 % (−7,3 %, +3,5 % y −16,1 %), y el de 2020, un −11 %. Lo plantado (coeficiente −0,012 al mes en esos seis meses, con z de ±1,75) da ≈ −4,2 % al mes, −12,6 % por trimestre; el ruido hace que 2009 salga peor y 2020 algo mejor. Junio de 2009 es el peor mes de toda la muestra. Los otros cuatro peores (febrero de 2022, septiembre de 2014, junio de 2008 y marzo de 2023) no están plantados: son ruido de una cartera de 24 acciones. Para un sistema real, la consecuencia es de tamaño: una estrategia con un 10 % anual de media que puede perder un 20 % en un trimestre necesita un objetivo de volatilidad y un capital que lo resistan. Daniel y Moskowitz proponen escalar el momentum por su volatilidad prevista, la misma idea que en los futuros4.

Rotación sectorial: diez activos son pocos

La rotación sectorial es el momentum transversal entre sectores: cada mes, comprar los sectores con mejor rentabilidad 12-1 y vender los peores. Moskowitz y Grinblatt encontraron en 1999 que el momentum de industrias explicaba buena parte del momentum de acciones individuales en EE. UU.5Nota 5 · T. J. Moskowitz y M. Grinblatt, «Do Industries Explain Momentum?», Journal of Finance 54(4), 1999, 1249-1290. DOI 10.1111/0022-1082.00146.. En este universo no hay una prima sectorial plantada: los sectores heredan la prima de sus acciones y tienen algunos tramos de deriva (finanzas en 2008, energía en 2014-2016 y 2020, tecnología en 2022).

IN [15]python
sector = pd.read_csv("universo2_sectores.csv").set_index("ticker")
R = P.pct_change(fill_method=None)
R_sec = R.T.groupby(sector["sector"]).mean().T     # equiponderado
S = (1 + R_sec.fillna(0.0)).cumprod()
Sf = S.loc[fin]
s_mom = (Sf.shift(1) / Sf.shift(12) - 1).loc["2007":]
Se = S.loc[ent].set_axis(fin)
s_fwd = (Se.shift(-1) / Se - 1).loc[s_mom.index]
rk = s_mom.rank(axis=1)
ls = (s_fwd[rk >= 8].mean(axis=1)
      - s_fwd[rk <= 3].mean(axis=1)).dropna()
ic_s = pd.Series([stats.spearmanr(s_mom.loc[d], s_fwd.loc[d]).statistic
                  for d in ls.index])
print(f"largo-corto: {12 * ls.mean():.2%} al año, t = "
      f"{ls.mean() / ls.std() * np.sqrt(len(ls)):.2f}")
print(f"IC de rangos: {ic_s.mean():.3f}, t = "
      f"{ic_s.mean() / ic_s.std() * np.sqrt(len(ic_s)):.2f}")
OUT [15]Tres mejores sectores menos tres peores
largo-corto: 3.98% al año, t = 1.34
IC de rangos: 0.024, t = 1.02

La rotación gana un 4,0 % al año con t = 1,34, y su IC de rangos es 0,024 con t = 1,02. Las dos cifras son compatibles con una prima pequeña y también con cero. El problema es de cantidad de información: un IC entre diez sectores tiene un error típico mensual enorme (con N = 10, una correlación de rangos por azar tiene una desviación típica de ≈ 1/√9 ≈ 0,33), y 226 meses no bastan para separar 0,02 de cero. Para una estrategia con tan pocos activos, la evidencia tiene que venir de otro sitio: de décadas de datos, de varios mercados o de una razón económica fuerte.

Lo que esto cambia en tus backtests

  • Pregunta por el control adecuado. Una regla larga-corta sobre activos con deriva gana algo solo por estar del lado de la media. El control con la media restada separa esa parte de la tendencia.
  • El coste es por tick, no por contrato. Un mercado cuyo precio ha caído mucho puede ser caro de operar aunque su tick no haya cambiado.
  • Rangos antes que niveles. En corte transversal, el IC de rangos y su estadístico son más robustos que el IC lineal o el diferencial de dos deciles, que mueven unas pocas observaciones extremas.
  • Deslistados dentro. Sin la rentabilidad de deslistado el decil perdedor parece mejor de lo que fue; con ella, hay que comprobar cuánto del resultado depende de un puñado de quiebras.
  • El riesgo del momentum es de cola. Una media positiva convive con meses de −10 % o peores justo cuando el mercado rebota.

Ejercicios

  1. Repite la regla de series temporales con horizontes de 1, 3 y 12 meses (21, 63 y 252 sesiones). ¿Cuál se acerca más al efecto plantado, que depende de las 252 sesiones anteriores, y qué pasa con los costes al acortar el horizonte?
Solución
IN [16]python
filas = {}
for n in (21, 63, 252):
    b, c, _ = backtest(np.sign(px / px.shift(n) - 1), r)
    filas[f"{n} sesiones"] = resumen(b, c)
pd.DataFrame(filas).loc[["sharpe_bruto", "sharpe_neto",
                         "coste_anual"]].round(3)
OUT [16]Horizonte de la señal
              21 sesiones  63 sesiones  252 sesiones
sharpe_bruto        0.414        0.607         0.946
sharpe_neto        -0.029        0.312         0.772
coste_anual         0.049        0.032         0.018

Los tres horizontes ganan en bruto porque una tendencia de un año también se ve, con más ruido, en tramos de uno y tres meses, pero el de 252 sesiones es el que recoge la prima plantada (0,95 bruto, frente a 0,61 y 0,41). Al acortar el horizonte la señal cambia mucho más a menudo: el coste anual pasa del 1,8 % al 4,9 % y la regla de un mes pierde todo en neto (−0,03). Un horizonte corto necesita una ventaja mucho mayor para pagar su rotación.

  1. Las reglas Turtle tenían también un «Sistema 1» más rápido: entrada a 20 sesiones y salida a 10. Pruébalo con la función donchian y compáralo con el de 55/20 en Sharpe neto, coste y correlación.
Solución
IN [17]python
b_d1, c_d1, _ = backtest(donchian(px, 20, 10), r)
print(pd.DataFrame({"20_10": resumen(b_d1, c_d1),
                    "55_20": resumen(b_dn, c_dn)}).round(3).to_string())
print("correlación de los netos:",
      round(np.corrcoef(b_d1 - c_d1, b_dn - c_dn)[0, 1], 2))
OUT [17]Donchian 20/10 frente a 55/20
              20_10  55_20
sharpe_bruto  0.484  0.647
sharpe_neto   0.212  0.491
vol_anual     0.096  0.091
coste_anual   0.026  0.014
max_dd       -0.483 -0.311
correlación de los netos: 0.76

El Sistema 1 es más rápido y opera más: cuesta casi el doble (2,6 % frente a 1,4 % anual), su Sharpe neto baja a 0,21 y su caída máxima sube al 48 %. Las dos versiones se parecen mucho (correlación de 0,76), así que combinarlas diversifica poco. En estos datos la prima de tendencia está en el horizonte de un año, y una ruptura de 20 sesiones reacciona a movimientos que son sobre todo ruido.

  1. Un compañero te enseña un backtest de momentum transversal en acciones reales con un diferencial de deciles del 15 % anual, sin rentabilidades de deslistado y con el universo de los miembros actuales del índice. Enumera qué le pedirías antes de creérselo.
Solución

Primero, el universo point-in-time: con los miembros actuales sobreviven las empresas que subieron, y el sesgo de supervivencia (lección 1.4.4) cambia los dos extremos. Después, las rentabilidades de deslistado: sin ellas el decil perdedor pierde sus peores meses y el diferencial cambia, en cualquier dirección según cómo se trate la última cotización. Tercero, el IC de rangos con su estadístico, no solo dos deciles: en esta lección, un placebo da 6 puntos de diferencial con un t de 2,2, aunque su IC de rangos no es significativo (t = 1,5). Cuarto, el resultado sin las cinco o diez observaciones más extremas. Quinto, la ejecución: señal al cierre de mes y cartera formada al cierre siguiente, con media horquilla y, en el lado corto, el coste de préstamo (lección 2.4.7). Y por último, los meses de crash tras los suelos de mercado: cuánto pierde y si el tamaño de la posición sobreviviría a ellos.

Footnotes

  1. N. Jegadeesh y S. Titman, «Returns to Buying Winners and Selling Losers: Implications for Stock Market Efficiency», Journal of Finance 48(1), 1993, 65-91. DOI 10.1111/j.1540-6261.1993.tb04702.x. ↩

  2. T. J. Moskowitz, Y. H. Ooi y L. H. Pedersen, «Time series momentum», Journal of Financial Economics 104(2), 2012, 228-250. DOI 10.1016/j.jfineco.2011.11.003. Escalado al 40 % y estimador EWMA con centro de masa de 60 días: secciones 2.4 y 4.1; la sonrisa, figura 4. ↩ ↩2 ↩3

  3. «The Original Turtle Trading Rules», publicadas en tradingblox.com/originalturtles: N y unidades en las páginas 13-14, sistemas de entrada en la 18 y salidas en la 26. Los Turtle operaban la ruptura en cuanto se producía durante la sesión; aquí se opera al cierre siguiente, que es más conservador. ↩

  4. K. Daniel y T. J. Moskowitz, «Momentum crashes», Journal of Financial Economics 122(2), 2016, 221-247. DOI 10.1016/j.jfineco.2015.12.002. ↩ ↩2

  5. T. J. Moskowitz y M. Grinblatt, «Do Industries Explain Momentum?», Journal of Finance 54(4), 1999, 1249-1290. DOI 10.1111/0022-1082.00146. ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. Para controlar una regla signo(rentabilidad de 12 meses), ¿por qué conviene restar la media de cada mercado antes de permutar sus rendimientos?

    • APorque sin restarla la permutación está mal hecha
    • BPorque la permutación conserva la deriva media de cada mercado: si un mercado sube en promedio, su rentabilidad de 12 meses suele ser positiva y la regla está casi siempre larga, cobrando esa deriva sin que haya tendencia
    • CPorque los costes son negativos
    • DPorque el escalado de volatilidad crea rentabilidad
    Ver respuesta

    Respuesta B. Porque la permutación conserva la deriva media de cada mercado: si un mercado sube en promedio, su rentabilidad de 12 meses suele ser positiva y la regla está casi siempre larga, cobrando esa deriva sin que haya tendencia

    Permutar destruye el orden temporal pero no la media. En estos datos la deriva es pequeña (bonos sin prima de plazo, materias primas y divisas que revierten al ancla), y las dos versiones dan un Sharpe bruto compatible con cero: +0,06 ± 0,17 sin restarla y −0,12 ± 0,22 restándola. En mercados con mucha deriva, lo que gana la regla por estar del lado de la media puede ser una parte grande, y eso no es «momentum».

  2. ¿Qué hace el escalado de volatilidad de Moskowitz, Ooi y Pedersen?

    • AOpera solo cuando la volatilidad es baja
    • BFija la posición en cada mercado como objetivo/σ estimada con datos hasta la víspera, para que cada mercado aporte un riesgo parecido
    • CEscala la señal según la fuerza de la tendencia
    • DReduce las comisiones
    Ver respuesta

    Respuesta B. Fija la posición en cada mercado como objetivo/σ estimada con datos hasta la víspera, para que cada mercado aporte un riesgo parecido

    Sin escalar, el gas natural (50 % de volatilidad) dominaría a un bono a 2 años (2 %). Con una posición de 40 %/σ cada mercado tiene un riesgo ex ante igual, y la cartera media de muchos mercados poco correlados queda en torno al 10,5 % anual.

  3. En el universo de acciones, el IC lineal del momentum 12-1 es 0,041 y el de rangos 0,027. ¿Cuál describe mejor la prima plantada y por qué?

    • AEl lineal, porque usa más información
    • BEl de rangos: el lineal lo inflan cuatro quiebras con rentabilidad de −100 % que estaban en el decil perdedor; unas pocas observaciones extremas mueven una correlación de Pearson
    • CNinguno: el IC no mide nada
    • DEl lineal, porque los rangos pierden el signo
    Ver respuesta

    Respuesta B. El de rangos: el lineal lo inflan cuatro quiebras con rentabilidad de −100 % que estaban en el decil perdedor; unas pocas observaciones extremas mueven una correlación de Pearson

    La prima del generador da un IC de rangos esperado de ≈ 0,03, que es lo que mide el de rangos. Las cuatro quiebras guiadas caen en el decil de perdedores y sus −100 % pesan muchísimo en una correlación lineal y en el diferencial de deciles (16,0 % anual con ellas, 9,8 % sin ellas).

  4. Una cartera por deciles de la puntuación ESG (un placebo sin ningún efecto) da un diferencial de 6,3 puntos al año entre el decil alto y el bajo. ¿Qué conclusión es correcta?

    • AQue el ESG tiene una prima del 6 % anual
    • BQue un diferencial entre dos carteras de 12 acciones es muy ruidoso: hay que mirar el IC de todo el corte transversal y su t (0,010 con t = 1,46 aquí); el diferencial de deciles llega a t = 2,18 por azar
    • CQue hay un error en los datos
    • DQue el placebo funciona como el momentum
    Ver respuesta

    Respuesta B. Que un diferencial entre dos carteras de 12 acciones es muy ruidoso: hay que mirar el IC de todo el corte transversal y su t (0,010 con t = 1,46 aquí); el diferencial de deciles llega a t = 2,18 por azar

    El decil extremo tiene una docena de acciones con volatilidades idiosincráticas del 20-30 %: su media de 19 años tiene un error de varios puntos. El diferencial de deciles es una buena imagen, pero la prueba es el IC y su estadístico.

  5. ¿Cuándo pierde más el momentum transversal, según la evidencia y según el generador de estos datos?

    • AEn las caídas fuertes del mercado
    • BEn los rebotes tras un suelo de mercado: los perdedores, con más beta, suben más que los ganadores
    • CEn los mercados laterales
    • DNunca pierde en un mes
    Ver respuesta

    Respuesta B. En los rebotes tras un suelo de mercado: los perdedores, con más beta, suben más que los ganadores

    Daniel y Moskowitz documentan crashes en estados de pánico, tras caídas y con volatilidad alta, que coinciden con rebotes del mercado. Aquí está plantado en los tres meses tras los suelos de 2009 y 2020: la cartera de ganadores menos perdedores pierde un 20 % en el trimestre de 2009.

Ideas clave

  • Tendencia (series temporales) compara un activo con su propio pasado y puede estar corta en todo; momentum transversal compara activos entre sí y es neutral por construcción.
  • La regla 12 meses con escalado de volatilidad recupera la prima plantada en los 24 futuros (Sharpe bruto 0,95, neto 0,77), a más de cuatro desviaciones típicas de un control por permutación; el control con la media restada separa además lo que sería cobrar la deriva de cada mercado.
  • Cruce de medias, Donchian y momentum de 12 meses son la misma idea con distintos horizontes: correlaciones de 0,3 a 0,7 entre ellas.
  • La «sonrisa» frente a la renta variable depende de las crisis de la muestra: aquí no aparece; la regla de 12 meses gana en las caídas largas o precedidas de caídas (2008-2009) y pierde en las bruscas tras un mercado alcista (2020).
  • En acciones, IC de rangos (0,027, t = 2,8) en vez de IC lineal o diferencial de deciles, que inflan unas pocas quiebras y el ruido de carteras pequeñas; un placebo da 6 puntos de diferencial (t = 2,2) por azar.
  • El momentum transversal sufre crashes en los rebotes tras los suelos, y una rotación entre diez sectores no tiene datos suficientes para separar una prima de cero.

Recursos

Datos