Lección 2.5.5 · 70 min

Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales

Un backtest produce una sola curva de capital, y esa curva es una muestra de tamaño uno. Esta lección genera miles de alternativas plausibles de tres formas distintas, cada una con su pregunta: qué drawdown cabe esperar, cuánto se fía uno del Sharpe y si la señal acierta el momento o solo recoge la exposición.

Fig. — Mil caminos de cinco años de la cartera de tendencia, remuestreados con bootstrap estacionario (bloques de 75 sesiones de media): bandas del 5-95 % y del 25-75 % y mediana
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

Una curva de capital es una muestra de tamaño uno

Un backtest devuelve una curva de capital, un Sharpe y un drawdown máximo. Son cifras de una historia: la que ocurrió. Si el mercado hubiera repartido los mismos días buenos y malos en otro orden, o la estrategia hubiera cerrado sus operaciones en otra secuencia, el drawdown sería otro. La lección 2.1.1 presentó el bootstrap para estimar la incertidumbre de un estadístico; aquí se aplica a la pregunta práctica de un sistema de trading: qué parte del resultado es estructura y qué parte es azar del orden.

«Monte Carlo» en este contexto agrupa tres familias de remuestreo. Cada una parte de un objeto distinto del backtest y responde a una pregunta distinta:

BacktestOperacionesRendimientosSeñal y mercadoRemuestreo tradesBootstrap bloquesPermutaciónDrawdown y ruinaIC y abanicop-valor timing
Fuente Mermaid
flowchart TB
  B[Backtest] --> O[Operaciones]
  B --> R[Rendimientos]
  B --> S[Señal y mercado]
  O --> T[Remuestreo trades]
  R --> K[Bootstrap bloques]
  S --> P[Permutación]
  T --> T2[Drawdown y ruina]
  K --> K2[IC y abanico]
  P --> P2[p-valor timing]
MétodoQué remuestreaQué conservaQué rompePregunta
Desordenar tradesEl orden de las operacionesCada operación y el resultado finalEl agrupamiento temporal de las pérdidas¿Qué drawdown cabe con estas operaciones?
Remuestrear tradesOperaciones con reemplazoLa distribución de una operaciónEl orden y el solapamiento¿Qué resultado y qué ruina cabe esperar?
Bootstrap de bloquesTramos de rendimientos diariosDependencia dentro de cada bloqueLa dependencia entre bloques¿Cuánto me fío del Sharpe y del drawdown?
Permutar la señalLa dirección respecto a los rendimientosRendimientos, escala de riesgo, exposición mediaLa relación señal-rendimiento¿Acierta el momento o solo la exposición?

Ninguno de los tres sabe cuántas estrategias se probaron antes de quedarse con esta. Esa pregunta, la del sesgo de selección, es la de la lección 2.5.4 y vuelve al final de esta.

La estrategia y su control

La estrategia es un momentum de series temporales clásico sobre los 24 futuros sintéticos del nivel 2: en cada mercado, largo si la rentabilidad de las últimas 252 sesiones es positiva y corto si es negativa, con una posición escalada por la inversa de la volatilidad (EWMA de 60 sesiones) para que cada mercado aporte un riesgo parecido. La señal se calcula al cierre de cada sesión y se opera en la siguiente. La lección 2.3.1 la construye paso a paso; la referencia empírica en mercados reales es Moskowitz, Ooi y Pedersen (2012)1Nota 1 · Moskowitz, Ooi y Pedersen (2012), «Time series momentum», Journal of Financial Economics 104(2), 228-250, doi:10.1016/j.jfineco.2011.11.003..

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

def sharpe(x):
    return np.mean(x) / np.std(x, ddof=1) * np.sqrt(252)

def tendencia(px, L=252, riesgo=0.30):
    """Signo de la rentabilidad de L sesiones, escalado por vol."""
    r = px.pct_change()
    vol = r.ewm(span=60, min_periods=60).std() * np.sqrt(252)
    signo = np.sign(px.pct_change(L))       # se conoce al cierre de t
    escala = riesgo / vol / px.shape[1]     # riesgo por mercado
    pnl = (signo * escala).shift(1) * r     # se opera en t + 1
    return signo, escala, r, pnl.loc["2001-01-02":]

fut = pd.read_parquet("futuros2_continuos.parquet")
px = fut.pivot(index="fecha", columns="mercado",
               values="cierre_ajustado")
signo, escala, r, pnl = tendencia(px)
cartera = pnl.sum(axis=1)
print(f"Sesiones: {len(cartera)}")
print(f"Volatilidad anual: {cartera.std() * np.sqrt(252):.1%}")
print(f"Sharpe: {sharpe(cartera):.2f}")
OUT [01]Tendencia en 24 futuros
Sesiones: 6287
Volatilidad anual: 9.0%
Sharpe: 1.00

Veinticinco años, una volatilidad del 9,0 % y un Sharpe de 1,00, sin costes (la lección 2.4.7 los añade; aquí no cambian el método). El 1,00 es mayor que el 0,15 por mercado del README por dos razones: la diversificación entre 24 mercados poco correlados y lo que la regla captura de paso, sobre todo el carry (el README mide una correlación de 0,50 entre el carry y el signo de 252 sesiones) y la prima de los índices. El README da 0,98 para la misma regla con otra ponderación (10 % entre la volatilidad de 63 sesiones): la cifra no depende de ese detalle.

Para cada método hace falta un control: algo con la misma forma que la estrategia y sin efecto. El más directo es un gemelo de los propios futuros: los mismos rendimientos diarios, las mismas filas (cada sesión conserva los 24 mercados juntos, y con ellos sus correlaciones), pero en un orden de sesiones barajado. Así se conservan la distribución de cada día y la estructura entre mercados y se destruye cualquier memoria temporal: en el gemelo, una regla de tendencia no puede tener ventaja.

IN [02]python
rng = np.random.default_rng(2028)
orden = rng.permutation(len(r) - 1)
r_g = pd.DataFrame(r.to_numpy()[1:][orden], index=r.index[1:],
                   columns=r.columns)
px_g = (1 + r_g).cumprod()                  # mismos días, otro orden
signo_g, escala_g, r_g, pnl_g = tendencia(px_g)
gemelo = pnl_g.sum(axis=1)
iu = np.triu_indices(24, 1)
print(f"Correlación media entre mercados: real "
      f"{r.corr().to_numpy()[iu].mean():.3f}, "
      f"gemelo {r_g.corr().to_numpy()[iu].mean():.3f}")
print(f"Sharpe de la tendencia en el gemelo: {sharpe(gemelo):.2f}")
OUT [02]El control: un gemelo sin memoria
Correlación media entre mercados: real 0.081, gemelo 0.081
Sharpe de la tendencia en el gemelo: 0.03

La misma correlación media entre mercados (0,081) y un Sharpe de 0,03: lo esperable en una serie sin memoria. Más abajo se usa también el control que trae el propio README, la serie ruptura de regimenes2.parquet.

Remuestreo de operaciones: drawdown y riesgo de ruina

Es la forma de Monte Carlo más extendida entre los traders: tomar la lista de operaciones cerradas y generar secuencias alternativas. Hay dos variantes. Desordenar (permutar sin reemplazo) conserva exactamente las mismas operaciones, y por tanto el mismo resultado final, y solo cambia el orden: sirve para ver qué drawdown podría haber salido con las mismas operaciones. Remuestrear (con reemplazo) cambia también qué operaciones salen y cuántas veces: sirve para la distribución del resultado y del riesgo de ruina.

En una estrategia de tendencia con señal diaria, una operación es una racha de sesiones con el mismo signo en un mercado:

IN [03]python
def operaciones(signo, pnl):
    """Una operación: racha de sesiones con el mismo signo."""
    filas = []
    for m in pnl.columns:
        s = signo[m].shift(1).loc[pnl.index]
        racha = (s != s.shift()).cumsum()
        g = pnl[m].groupby(racha)
        fechas = pnl.index.to_series().groupby(racha)
        filas.append(pd.DataFrame({
            "mercado": m, "salida": fechas.last(),
            "sesiones": g.size(), "pnl": g.sum()}))
    return (pd.concat(filas).sort_values("salida", kind="stable")
            .reset_index(drop=True))

ops = operaciones(signo, pnl)
print(f"{len(ops)} operaciones; ganadoras: "
      f"{(ops['pnl'] > 0).mean():.0%}")
print("Sesiones por operación:",
      ops["sesiones"].quantile([0.25, 0.5, 0.75, 0.99])
      .round().astype(int).to_dict())
(100 * ops["pnl"]).quantile([0.01, 0.1, 0.5, 0.9, 0.99]).round(3)
OUT [03]Las operaciones de la cartera
3229 operaciones; ganadoras: 32%
Sesiones por operación: {0.25: 1, 0.5: 4, 0.75: 16, 0.99: 631}
0.01   -0.827
0.10   -0.254
0.50   -0.050
0.90    0.263
0.99    3.477
Name: pnl, dtype: float64

El perfil es el típico de una tendencia: 3.229 operaciones, solo un 32 % ganadoras, la mitad dura 4 sesiones o menos (la señal cambia de signo varias veces cuando la rentabilidad de 12 meses ronda el cero) y unas pocas duran años. La operación mediana pierde un 0,050 % del capital; la del percentil 99 gana un 3,48 %. El resultado lo hacen las pocas operaciones largas, y por eso el orden en que llegan las pérdidas pequeñas importa tanto para el drawdown.

IN [04]python
rng = np.random.default_rng(7)
x = ops["pnl"].to_numpy()
N = 2000

def dd_max(m):
    """Drawdown máximo de cada fila de una matriz de resultados."""
    eq = np.cumprod(1 + m, axis=-1)
    pico = np.maximum.accumulate(eq, axis=-1)
    return (eq / pico - 1).min(axis=-1)

dd_hist = dd_max(x)
dd_perm = dd_max(rng.permuted(np.tile(x, (N, 1)), axis=1))
dd_boot = dd_max(rng.choice(x, size=(N, len(x))))

q = [0.05, 0.5, 0.95]
print(f"Drawdown de la secuencia real: {dd_hist:.1%}")
print(f"Desordenados con un drawdown peor: "
      f"{(dd_perm <= dd_hist).mean():.1%}")
pd.DataFrame({
    "desordenar": np.quantile(dd_perm, q),
    "remuestrear": np.quantile(dd_boot, q)},
    index=["5 %", "mediana", "95 %"]).mul(100).round(1)
OUT [04]Desordenar y remuestrear las operaciones
Drawdown de la secuencia real: -21.9%
Desordenados con un drawdown peor: 0.0%
         desordenar  remuestrear
5 %           -15.0        -15.3
mediana       -10.0        -10.1
95 %           -7.4         -7.2

rng.permuted(..., axis=1) desordena cada fila por separado: 2.000 órdenes distintos de las mismas operaciones en una sola llamada. El resultado es incómodo. Con las operaciones en orden aleatorio, el drawdown mediano es del −10,0 % y el del percentil 5, del −15,0 %. La secuencia real tuvo un −21,9 %: peor que todas las secuencias desordenadas.

Fig. — Drawdown máximo de 2.000 órdenes al azar de las 3.229 operaciones de la cartera de tendencia, frente al de la secuencia real

No es mala suerte. Desordenar supone que las operaciones son intercambiables, y en una cartera de 24 mercados no lo son: se solapan en el tiempo, y cuando el mercado da la vuelta (un final de tendencia, una crisis) varios mercados pierden a la vez. La celda siguiente lo mide:

IN [05]python
eq = np.cumprod(1 + x)
fondo = int(np.argmin(eq / np.maximum.accumulate(eq)))
pico = int(np.argmax(eq[:fondo + 1]))
print(f"Peor tramo: {ops['salida'][pico]:%Y-%m-%d} a "
      f"{ops['salida'][fondo]:%Y-%m-%d}, {fondo - pico} operaciones")

mes = ops["salida"].dt.to_period("M").to_numpy()
real = pd.Series(x).groupby(mes).sum().std()
barajado = np.mean([pd.Series(rng.permutation(x)).groupby(mes)
                    .sum().std() for _ in range(200)])
print(f"Desviación típica del resultado mensual: real {real:.2%}, "
      f"barajado {barajado:.2%}")
OUT [05]Las pérdidas llegan juntas
Peor tramo: 2001-01-02 a 2003-01-16, 378 operaciones
Desviación típica del resultado mensual: real 3.30%, barajado 2.52%

Con las operaciones en su mes real, el resultado mensual tiene una desviación típica del 3,30 %; con las mismas operaciones repartidas al azar entre los mismos meses, del 2,52 %. Las ganancias y las pérdidas se agrupan en el tiempo, y el peor tramo de la secuencia real, de enero de 2001 a enero de 2003, encadena 378 operaciones.

Con esa reserva, el remuestreo sirve para una pregunta que el backtest no contesta: qué apalancamiento soporta la estrategia. Si se define la ruina como un drawdown del 20 % o del 40 % (el punto en que un gestor cerraría la estrategia o un cliente retiraría el dinero), basta con multiplicar las operaciones por el apalancamiento:

IN [06]python
filas = {}
for apal in (1, 2, 3, 4):
    dd = dd_max(apal * rng.choice(x, size=(N, len(x))))
    filas[f"x{apal}"] = {
        "dd_mediano": np.median(dd),
        "P(dd < -20 %)": (dd < -0.20).mean(),
        "P(dd < -40 %)": (dd < -0.40).mean()}
pd.DataFrame(filas).T.round(3)
OUT [06]Riesgo de ruina según el apalancamiento
    dd_mediano  P(dd < -20 %)  P(dd < -40 %)
x1      -0.101          0.005          0.000
x2      -0.192          0.428          0.000
x3      -0.278          0.952          0.046
x4      -0.359          1.000          0.289

Sin apalancar, la probabilidad de pasar por un drawdown del 20 % en 25 años es del 0,5 %; con el doble de tamaño, del 43 %; con el triple, del 95 %. La de llegar al 40 % es nula con el doble (ninguno de 2.000 caminos), del 4,6 % con el triple y del 29 % con el cuádruple. El drawdown mediano crece algo menos que proporcionalmente (−10,1 %, −19,2 %, −27,8 % y −35,9 %), porque en un capital compuesto cada pérdida se aplica sobre un capital ya menor; la probabilidad de cruzar un umbral fijo, en cambio, se dispara. Y todas estas cifras heredan el sesgo optimista de la celda anterior: la secuencia real ya tuvo un −21,9 % sin apalancar.

Bootstrap de bloques sobre los rendimientos diarios

El bootstrap de los rendimientos diarios evita el problema del solapamiento: cada día ya contiene todas las posiciones abiertas, con su correlación. Pero el bootstrap clásico de Efron (sacar días sueltos con reemplazo) supone que los días son independientes, y los rendimientos de una cartera no lo son: su volatilidad viene en rachas (lección 1.6.2). La solución es remuestrear bloques de días consecutivos. Künsch (1989) propuso bloques de longitud fija; el bootstrap circular une el final de la serie con el principio para que todos los días tengan la misma probabilidad de aparecer, y el bootstrap estacionario de Politis y Romano (1994) usa bloques de longitud aleatoria, geométrica de media bb:

P(L=k)=1b(1−1b)k−1,E[L]=bP(L = k) = \frac{1}{b}\left(1 - \frac{1}{b}\right)^{k-1}, \qquad E[L] = b

Con bloques de longitud aleatoria, la serie remuestreada es estacionaria, cosa que no garantizan los bloques fijos. El mecanismo cabe en un bucle:

IN [07]python
def indices_estacionario(n, largo, b, rng):
    """Índices de un remuestreo estacionario (Politis y Romano).

    Cada paso sigue al día siguiente con probabilidad 1 - 1/b o
    salta a un día al azar con probabilidad 1/b: bloques de
    longitud geométrica de media b, con vuelta circular."""
    idx = np.empty(largo, dtype=np.int64)
    idx[0] = rng.integers(n)
    salto = rng.random(largo) < 1 / b
    nuevo = rng.integers(n, size=largo)
    for t in range(1, largo):
        idx[t] = nuevo[t] if salto[t] else (idx[t - 1] + 1) % n
    return idx

rng = np.random.default_rng(11)
print(indices_estacionario(len(cartera), 16, 5, rng))
OUT [07]Bootstrap estacionario desde cero
[ 841  842 3221 5917 5918 5275 3452 3453 3454 3455 3456 3457 3458 5179
 5180 5181]

Se ven los bloques: 841-842, un día suelto, 5917-5918, otro suelto, siete días seguidos desde el 3452 y otros tres al final, con media 5 porque b=5. La librería arch tiene la versión eficiente (StationaryBootstrap, CircularBlockBootstrap) y el selector automático de longitud de bloque de Politis y White (2004), con la corrección de Patton, Politis y White (2009), que estima la bb óptima a partir de la autocorrelación de la serie:

IN [08]python
from arch.bootstrap import optimal_block_length

pd.concat({"r": optimal_block_length(cartera),
           "r²": optimal_block_length(cartera ** 2)}).round(1)
OUT [08]Longitud de bloque de Politis y White
         stationary  circular
r  None         1.2       1.4
r² None        47.6      54.5

Los rendimientos casi no tienen autocorrelación, y el selector pide bloques de 1 sesión. Sus cuadrados, que miden la volatilidad, piden 48 (estacionario) o 55 (circular). La elección depende de qué estadístico se quiere: la media depende de los rendimientos; el drawdown, de las rachas de volatilidad. Lo prudente es mirar varias longitudes:

IN [09]python
from arch.bootstrap import StationaryBootstrap

def drawdown(x):
    eq = np.cumprod(1 + np.asarray(x))
    return (eq / np.maximum.accumulate(eq) - 1).min()

def stats(x):
    return np.array([sharpe(x), drawdown(x)])

print(f"Histórico: Sharpe {sharpe(cartera):.2f}, "
      f"drawdown {drawdown(cartera):.1%}")
filas = {}
for b in (1, 5, 20, 75, 250):
    bs = StationaryBootstrap(b, cartera.to_numpy(), seed=3)
    v = bs.apply(stats, 1000)
    filas[b] = {"sharpe_2.5": np.quantile(v[:, 0], 0.025),
                "sharpe_97.5": np.quantile(v[:, 0], 0.975),
                "dd_mediano": np.median(v[:, 1]),
                "dd_5 %": np.quantile(v[:, 1], 0.05)}
pd.DataFrame(filas).T.rename_axis("bloque").round(3)
OUT [09]Intervalos del Sharpe y del drawdown según el bloque
Histórico: Sharpe 1.00, drawdown -14.7%
        sharpe_2.5  sharpe_97.5  dd_mediano  dd_5 %
bloque
1            0.591        1.391      -0.179  -0.264
5            0.613        1.374      -0.178  -0.270
20           0.630        1.380      -0.178  -0.265
75           0.600        1.399      -0.177  -0.262
250          0.661        1.326      -0.169  -0.235

Tres lecturas. La primera: el intervalo del 95 % del Sharpe va de 0,6 a 1,4 con cualquier bloque. Se parece a la aproximación de Lo (2002)2Nota 2 · Lo, A. W. (2002), «The Statistics of Sharpe Ratios», Financial Analysts Journal 58(4), 36-52, doi:10.2469/faj.v58.n4.2453. para rendimientos i.i.d., σ(SR^)≈(1+SR2/2)/T\sigma(\widehat{SR}) \approx \sqrt{(1 + SR^2/2)/T} con TT en años: con 1,00 y 25 años, 0,24, y ±1,96 veces eso da de 0,52 a 1,48; el bootstrap sale algo más estrecho. Veinticinco años de datos no bastan para distinguir un Sharpe de 0,6 de uno de 1,4.

La segunda: en esta cartera el drawdown apenas depende del bloque. El mediano va del −17,9 % con días sueltos al −17,7 % con bloques de 75 sesiones, y el del percentil 5, del −26,4 % al −26,2 %. La razón está en la construcción: cada posición se escala por la inversa de su volatilidad, y eso quita a la cartera casi toda la agrupación de volatilidad que un bloque largo conservaría. En una serie sin escalar es distinto. La celda siguiente repite la prueba con el futuro de índice IDA comprado y mantenido:

IN [10]python
ida = r["IDA"].loc["2001-01-02":].to_numpy()
b_ida = optimal_block_length(ida ** 2)["stationary"].iloc[0]
print(f"IDA: bloque de Politis y White para r²: {b_ida:.0f}")
print(f"Drawdown histórico: {drawdown(ida):.1%}")
for b in (1, 75):
    v = StationaryBootstrap(b, ida, seed=3).apply(
        lambda x: np.array([drawdown(x)]), 500)[:, 0]
    print(f"Bloque {b:>2}: drawdown mediano {np.median(v):.1%}, "
          f"percentil 5 {np.quantile(v, 0.05):.1%}")
OUT [10]El bloque sí importa en un índice sin escalar
IDA: bloque de Politis y White para r²: 156
Drawdown histórico: -62.2%
Bloque  1: drawdown mediano -53.0%, percentil 5 -74.9%
Bloque 75: drawdown mediano -65.3%, percentil 5 -86.8%

En IDA el selector pide bloques de 156 sesiones para los cuadrados (48 en la cartera), y el drawdown mediano pasa del −53,0 % con días sueltos al −65,3 % con bloques de 75 sesiones; el percentil 5, del −74,9 % al −86,8 %. Los bloques largos conservan las rachas de volatilidad, y con ellas las caídas encadenadas; los días sueltos las reparten y el drawdown sale optimista: con ellos, el histórico de IDA (−62,2 %) parecería peor de lo normal. La regla práctica: el bloque importa para el drawdown en la medida en que la serie tenga rachas de volatilidad, y lo prudente es mirar varias longitudes.

La tercera: el drawdown histórico de la cartera (−14,7 %) es más suave que la mediana del bootstrap con cualquier bloque (entre −16,9 % y −17,9 %). La historia que ocurrió fue algo afortunada en el orden de sus días: el riesgo que conviene esperar es el del bootstrap, no el del backtest. (El −21,9 % de la secuencia de operaciones no es comparable: allí cada operación se apunta entera el día de su salida.)

El abanico de capital

Con el bootstrap estacionario se pueden generar caminos de cualquier longitud. Mil caminos de cinco años con bloques de 75 sesiones de media dan el abanico de la figura de la cabecera:

IN [11]python
rng = np.random.default_rng(5)
n, largo, b = len(cartera), 5 * 252, 75
caminos = np.array([
    cartera.to_numpy()[indices_estacionario(n, largo, b, rng)]
    for _ in range(1000)])
capital = np.cumprod(1 + caminos, axis=1)
bandas = pd.DataFrame(
    np.quantile(capital, [0.05, 0.25, 0.5, 0.75, 0.95], axis=0).T,
    columns=["p5", "p25", "p50", "p75", "p95"])
print(bandas.iloc[[251, 503, 755, 1007, 1259]]
      .set_axis([1, 2, 3, 4, 5]).rename_axis("año").round(2))
print(f"P(perder a 5 años): {(capital[:, -1] < 1).mean():.1%}")
OUT [11]Abanico de cinco años
       p5   p25   p50   p75   p95
año
1    0.94  1.02  1.09  1.16  1.25
2    0.96  1.10  1.20  1.30  1.45
3    1.01  1.17  1.29  1.44  1.67
4    1.07  1.26  1.41  1.59  1.88
5    1.14  1.36  1.54  1.77  2.10
P(perder a 5 años): 1.2%

La mediana llega a 1,54 en cinco años; la banda del 25 al 75 %, de 1,36 a 1,77. El percentil 5 es la línea que importa para decidir el tamaño: pasa el primer año por 0,94 (un 6 % de pérdida), el segundo por 0,96, no vuelve a 1 hasta el tercero (1,01) y a los cinco años está en 1,14. Aun con un Sharpe de 1,00, un 1,2 % de los caminos pierde dinero a cinco años y uno de cada veinte pasa dos años en pérdidas. Es lo que conviene enseñar a quien vaya a invertir antes de que lo viva.

Permutar la señal: ¿acierta el momento?

El bootstrap dice cuánto varía el Sharpe; no dice si es distinto de lo que daría una estrategia sin acierto. Un test de permutación construye esa referencia directamente: se rompe la relación entre la señal y los rendimientos y se recalcula el estadístico muchas veces. El p-valor es la fracción de permutaciones que igualan o superan lo observado, con la corrección habitual para que nunca sea cero:

p=1+#{i:S(i)≥Sobs}1+Np = \frac{1 + \#\{i : S^{(i)} \ge S_{\text{obs}}\}}{1 + N}

Lo delicado es decidir qué se permuta. La pregunta es si la dirección elegida en cada momento aporta algo, así que se permuta solo el signo; la escala por volatilidad y los rendimientos se quedan en su sitio. Y hay dos formas de permutar el signo: barajar las sesiones (rompe también la persistencia de la señal) o desplazarlo circularmente un número aleatorio de sesiones (conserva sus rachas, su rotación y su autocorrelación, y solo cambia su alineación con el mercado). White (2000) y Hansen (2005)3Nota 3 · Hansen, P. R. (2005), «A Test for Superior Predictive Ability», Journal of Business & Economic Statistics 23(4), 365-380, doi:10.1198/073500105000000063. extienden la idea al caso de muchas reglas a la vez.

IN [12]python
def p_valor_perm(signo, escala, r, n_perm=1000, seed=0,
                 modo="circular"):
    """Sharpe observado, p-valor y distribución nula.

    Solo se permuta la DIRECCIÓN (el signo); la escala por
    volatilidad y los rendimientos quedan en su sitio."""
    rng = np.random.default_rng(seed)
    s = signo.shift(1).to_numpy()
    e = escala.shift(1).to_numpy()
    rr = r.to_numpy()
    ok = ~(np.isnan(s).any(axis=1) | np.isnan(e).any(axis=1)
           | np.isnan(rr).any(axis=1))
    s, e, rr = s[ok], e[ok], rr[ok]
    obs = sharpe((s * e * rr).sum(axis=1))
    nula = np.empty(n_perm)
    for i in range(n_perm):
        if modo == "barajar":
            sp = s[rng.permutation(len(s))]
        else:  # desplazamiento circular de al menos un año
            sp = np.roll(s, rng.integers(252, len(s) - 252), axis=0)
        nula[i] = sharpe((sp * e * rr).sum(axis=1))
    p = (1 + (nula >= obs).sum()) / (1 + n_perm)
    return obs, p, nula

res, nulas = {}, {}
for nombre, args in {"tendencia": (signo, escala, r),
                     "gemelo": (signo_g, escala_g, r_g)}.items():
    for modo in ("barajar", "circular"):
        obs, p, nula = p_valor_perm(*args, modo=modo)
        nulas[(nombre, modo)] = nula
        res[(nombre, modo)] = {"sharpe": obs, "p": p,
                               "nula_media": nula.mean(),
                               "nula_95": np.quantile(nula, 0.95)}
pd.DataFrame(res).T.round(3)
OUT [12]Test de permutación de la señal
                    sharpe      p  nula_media  nula_95
tendencia barajar    0.999  0.001       0.202    0.519
          circular   0.999  0.001       0.048    0.552
gemelo    barajar    0.026  0.536       0.039    0.376
          circular   0.026  0.248      -0.066    0.212
Fig. — Test de permutación de la señal (desplazamiento circular, 1.000 veces): distribución nula del Sharpe y Sharpe observado, en los futuros y en su gemelo sin memoria

El método distingue los dos casos. En los futuros con efecto plantado, el Sharpe observado (1,00) supera a todas las permutaciones, barajando y desplazando: p = 0,001, el mínimo posible con 1.000. En el gemelo sin memoria, el 0,03 cae dentro de su nula: p = 0,25 con el desplazamiento circular (0,54 barajando).

Fíjate en la media de la nula: 0,20 al barajar y 0,05 con el desplazamiento circular, no 0. Cualquier permutación conserva la proporción de sesiones largas y cortas de cada mercado, y con ella la parte del resultado que viene de la exposición media:

IN [13]python
largo = (signo.shift(1).loc[pnl.index] > 0).mean()
largo.sort_values().round(2).iloc[[0, 1, -2, -1]].to_dict()
OUT [13]La exposición que la permutación conserva
{'MEA': 0.42, 'DIC': 0.43, 'IDB': 0.7, 'IDA': 0.71}

La cartera está larga el 71 % de las sesiones en IDA y el 70 % en IDB, y solo el 42 % en el oro (MEA). Los índices llevan la prima de la renta variable del guion, así que una permutación que los deja largos el 70 % del tiempo gana algo con cualquier orden; lo mismo pasa con la parte del carry que la regla recoge. El test no pregunta si la estrategia gana dinero, sino si el momento elegido añade algo sobre esa exposición. Una estrategia siempre larga en un mercado alcista ganaría dinero y no pasaría el test.

riesgo¿acierta?¿elegí bien?síno¿Qué preguntas?Bootstrap bloquesPermutaciónDSR y PBO¿Pérdidas juntas?Bloques largosTrades o i.i.d.
Fuente Mermaid
flowchart TB
  Q{¿Qué preguntas?} -- riesgo --> D[Bootstrap bloques]
  Q -- ¿acierta? --> P[Permutación]
  Q -- ¿elegí bien? --> S[DSR y PBO]
  D --> C{¿Pérdidas juntas?}
  C -- sí --> B[Bloques largos]
  C -- no --> T[Trades o i.i.d.]

Potencia: no rechazar no es no tener efecto

Los 24 mercados tienen el mismo efecto plantado. Mercado a mercado:

IN [14]python
p_merc = pd.Series({
    m: p_valor_perm(signo[[m]], escala[[m]], r[[m]],
                    n_perm=500, seed=1)[1]
    for m in r.columns})
print(f"Mercados con p < 0,05: {(p_merc < 0.05).sum()} de 24")
print(p_merc.sort_values().round(3).head(6).to_dict())
OUT [14]Mercado a mercado: la potencia del test
Mercados con p < 0,05: 7 de 24
{'AGB': 0.002, 'BOD': 0.002, 'BOC': 0.01, 'AGC': 0.012, 'BOA': 0.034, 'AGA': 0.042}

Solo 7 de los 24 pasan al 5 %, con los agrícolas y los bonos en cabeza. Con un Sharpe verdadero de momentum en torno a 0,15 por mercado y 25 años de datos, el error típico del Sharpe (≈ 0,2) es mayor que el efecto: el test casi nunca lo detecta en un mercado solo. La cartera lo detecta porque suma 24 efectos débiles poco correlados. Descartar 17 mercados por «no significativos» sería descartar un efecto real; la conclusión correcta es que un mercado solo no tiene datos suficientes.

Y en el ruido, ¿cuántos falsos positivos?

El control del README es la serie ruptura de regimenes2.parquet después de su ruptura (el 2013-05-22): rendimientos normales i.i.d. de media cero, donde ninguna regla de tendencia tiene ventaja. Una sola serie de ruido no basta para saber si el test está bien calibrado, así que se añaden 200 series simuladas como ella:

IN [15]python
reg = pd.read_parquet("regimenes2.parquet")
ru = (reg[reg["serie"] == "ruptura"].set_index("fecha")[["precio"]]
      .loc["2013-05-22":])          # tras la ruptura: i.i.d., media 0
s_ru, e_ru, r_ru, _ = tendencia(ru, riesgo=0.10)
obs, p, _ = p_valor_perm(s_ru, e_ru, r_ru)
print(f"Serie «ruptura» desde 2013: Sharpe {obs:.2f}, p = {p:.3f}")

rng = np.random.default_rng(99)
ps = []
for i in range(200):
    ruido = pd.DataFrame(
        np.cumprod(1 + 0.013 * rng.standard_normal(len(ru))),
        index=ru.index)
    ps.append(p_valor_perm(*tendencia(ruido, riesgo=0.10)[:3],
                           n_perm=200, seed=i)[1])
ps = np.array(ps)
print(f"Series de ruido con p < 0,05: {(ps < 0.05).mean():.1%}")
print(np.histogram(ps, bins=5, range=(0, 1))[0])
OUT [15]El control del README y 200 series de ruido
Serie «ruptura» desde 2013: Sharpe 0.55, p = 0.032
Series de ruido con p < 0,05: 4.5%
[36 47 33 27 57]

La serie del README da un Sharpe de 0,55 y p = 0,032: un falso positivo. No hay error en el test: en 200 series de ruido como ella, un 4,5 % sale con p < 0,05, y los p-valores se reparten de forma aproximadamente uniforme entre 0 y 1 (36, 47, 33, 27 y 57 por quintil). Eso es lo que significa un nivel del 5 %: uno de cada veinte controles sale «significativo». Por eso un único p-valor por debajo de 0,05 no demuestra nada, y por eso un control que pasa el test no invalida el método: invalida la costumbre de probar una sola cosa y creer el resultado.

Lo que ningún remuestreo corrige

Los tres métodos trabajan sobre una estrategia ya elegida. Si esa estrategia es la mejor de muchas, todos heredan el sesgo de la elección. El conjunto ruido2_estrategias.parquet tiene 1.000 estrategias, 995 de ellas sin ninguna ventaja (la verdad está en ruido2_verdad.csv):

IN [16]python
rs = pd.read_parquet("ruido2_estrategias.parquet").pivot(
    index="fecha", columns="estrategia", values="rendimiento")
verdad = pd.read_csv("ruido2_verdad.csv", index_col="estrategia")
sin_ventaja = verdad.index[verdad["sharpe_real"] == 0]
sr = rs[sin_ventaja].apply(sharpe)
mejor = sr.idxmax()
print(f"{len(sin_ventaja)} estrategias sin ventaja; la mejor: "
      f"{mejor}, Sharpe {sr.max():.2f}")
bs = StationaryBootstrap(5, rs[mejor].to_numpy(), seed=2)
v = bs.apply(sharpe, 2000)[:, 0]
print(f"Intervalo del 95 %: {np.quantile(v, 0.025):.2f} a "
      f"{np.quantile(v, 0.975):.2f}")
print(f"Remuestreos con Sharpe <= 0: {(v <= 0).mean():.2%}")
OUT [16]Un falso positivo que el bootstrap no detecta
995 estrategias sin ventaja; la mejor: e0598, Sharpe 1.50
Intervalo del 95 %: 0.46 a 2.54
Remuestreos con Sharpe <= 0: 0.15%

La mejor de las 995 tiene un Sharpe de 1,50 en cuatro años, un intervalo bootstrap de 0,46 a 2,54 y solo un 0,15 % de remuestreos con Sharpe negativo. Todo indica una estrategia excelente, y su Sharpe verdadero es cero. El bootstrap responde bien a su pregunta (cuánto varía el Sharpe de esta serie) y no puede responder a otra que no se le hizo: cuántas series se miraron antes de elegirla. Con 995 intentos y un error típico del Sharpe de 0,5, un máximo de 1,5 es lo esperable. La corrección es el Deflated Sharpe Ratio o el PBO de la lección 2.5.4, que necesitan el número de pruebas, y el diario de experimentos de la lección 2.2.4, que es donde ese número queda escrito.

En resumen, el orden de trabajo con una estrategia candidata:

  1. Bootstrap de bloques de los rendimientos diarios, con varias longitudes de bloque: intervalo del Sharpe, del drawdown y abanico para decidir el tamaño.
  2. Remuestreo de operaciones solo como suelo del riesgo, comprobando dónde cae la secuencia real.
  3. Test de permutación de la señal (circular si la señal es persistente) para separar acierto de exposición, también en un control sin efecto.
  4. Deflated Sharpe o PBO con el número real de pruebas (lección 2.5.4); la lección 2.5.8 junta todo en una lista.

Ejercicios

  1. Repite el riesgo de ruina con el bootstrap estacionario de los rendimientos diarios (bloques de 75) en lugar de las operaciones: probabilidad de un drawdown del 20 % en 25 años con apalancamiento 1 y 2. Compáralo con la tabla de las operaciones.
Solución
IN [17]python
rng = np.random.default_rng(21)
n = len(cartera)
caminos = np.array([cartera.to_numpy()[
    indices_estacionario(n, n, 75, rng)] for _ in range(500)])
for apal in (1, 2):
    dd = dd_max(apal * caminos)
    print(f"x{apal}: drawdown mediano {np.median(dd):.1%}, "
          f"P(dd < -20 %) {(dd < -0.20).mean():.1%}")
OUT [17]Ruina con bloques de rendimientos diarios
x1: drawdown mediano -17.6%, P(dd < -20 %) 29.8%
x2: drawdown mediano -33.0%, P(dd < -20 %) 99.6%

Con los rendimientos diarios en bloques, el drawdown mediano en 25 años sin apalancar es del −17,6 % y la probabilidad de pasar por un −20 %, del 29,8 %, frente al 0,5 % que daban las operaciones tratadas como independientes. Con apalancamiento 2, el drawdown mediano es del −33,0 % y el −20 % llega en el 99,6 % de los caminos. El bootstrap de bloques conserva el solapamiento de las posiciones y las rachas de volatilidad; el remuestreo de operaciones, no. Para decidir el tamaño, esta es la cifra de referencia, y la diferencia entre las dos es el tamaño del error que comete el remuestreo de trades en una cartera.

  1. Aplica el test de permutación circular a la misma regla con una ventana de 63 sesiones en lugar de 252, en los futuros y en el gemelo. ¿Detecta el test un efecto que el README planta a 252 sesiones?
Solución
IN [18]python
for nombre, precios in (("tendencia", px), ("gemelo", px_g)):
    s63, e63, r63, _ = tendencia(precios, L=63)
    obs, p, nula = p_valor_perm(s63, e63, r63, n_perm=500)
    print(f"{nombre}: Sharpe {obs:.2f}, p = {p:.3f}, "
          f"media de la nula {nula.mean():.2f}")
OUT [18]Ventana de 63 sesiones
tendencia: Sharpe 0.63, p = 0.020, media de la nula 0.07
gemelo: Sharpe 0.00, p = 0.403, media de la nula -0.04

Con 63 sesiones la regla sigue capturando parte del efecto en los futuros: Sharpe 0,63 y p = 0,020. El signo de 3 meses está correlado con el de 12 meses, y el carry, que es persistente, se refleja en la rentabilidad de cualquier ventana. En el gemelo no hay nada que capturar: Sharpe 0,00 y p = 0,403. Que una ventana distinta de la plantada también funcione es la idea de la lección 2.5.6: un efecto real suele dar una meseta de parámetros, no un pico.

  1. Una estrategia está larga todos los días en un índice que sube un 8 % anual. ¿Qué p-valor da el test de permutación de la señal y por qué? ¿Qué te dice eso sobre lo que mide el test?
Solución

Si la posición es siempre +1, cualquier permutación (barajar o desplazar) devuelve la misma serie de signos: todas las permutaciones tienen exactamente el Sharpe observado y el p-valor es (1 + N)/(1 + N) = 1. La estrategia gana dinero, pero no tiene ningún acierto de momento: todo su resultado es exposición. El test de permutación mide solo el valor del timing respecto a una cartera con la misma exposición media; la rentabilidad de la exposición (la prima del mercado) hay que juzgarla con otras herramientas, como el alfa frente a un índice (lección 2.6.2).

Footnotes

  1. Moskowitz, Ooi y Pedersen (2012), «Time series momentum», Journal of Financial Economics 104(2), 228-250, doi:10.1016/j.jfineco.2011.11.003. ↩

  2. Lo, A. W. (2002), «The Statistics of Sharpe Ratios», Financial Analysts Journal 58(4), 36-52, doi:10.2469/faj.v58.n4.2453. ↩

  3. Hansen, P. R. (2005), «A Test for Superior Predictive Ability», Journal of Business & Economic Statistics 23(4), 365-380, doi:10.1198/073500105000000063. ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. Al desordenar 2.000 veces las 3.229 operaciones de la cartera de tendencia, ninguna secuencia tiene un drawdown peor que el de la secuencia real (−21,9 %). ¿Qué indica?

    • AQue la estrategia tuvo mala suerte y el drawdown real no se repetirá
    • BQue las pérdidas reales llegaron agrupadas en el tiempo, y desordenar las operaciones (que supone independencia) infravalora el drawdown
    • CQue hay un error en el cálculo del drawdown
    • DQue hacen falta más simulaciones
    Ver respuesta

    Respuesta B. Que las pérdidas reales llegaron agrupadas en el tiempo, y desordenar las operaciones (que supone independencia) infravalora el drawdown

    Desordenar supone que el orden de las operaciones es intercambiable. En una cartera con 24 mercados las operaciones se solapan y las pérdidas llegan juntas: la desviación típica del resultado mensual es del 3,30 % con el orden real y del 2,52 % con el orden barajado. El remuestreo de trades es un suelo del riesgo, no una estimación.

  2. Con el bootstrap estacionario, el drawdown mediano de la cartera de tendencia apenas cambia entre bloques de 1 y de 75 sesiones (−17,9 % y −17,7 %), y el de IDA comprado y mantenido pasa del −53,0 % al −65,3 %. ¿Por qué?

    • APorque la cartera tiene más sesiones que IDA
    • BPorque la cartera escala cada posición por la inversa de su volatilidad y le quedan pocas rachas de volatilidad; en IDA siguen ahí, y los bloques largos las conservan
    • CPorque el bootstrap estacionario no sirve para índices
    • DPorque con bloques largos el drawdown siempre sale peor
    Ver respuesta

    Respuesta B. Porque la cartera escala cada posición por la inversa de su volatilidad y le quedan pocas rachas de volatilidad; en IDA siguen ahí, y los bloques largos las conservan

    El drawdown depende de que las caídas encadenadas se conserven, y eso solo pasa si la serie tiene rachas de volatilidad que un bloque largo pueda conservar. El selector de Politis y White lo anticipa: pide bloques de 48 sesiones para los cuadrados de la cartera y de 156 para los de IDA. El intervalo del Sharpe apenas cambia con el bloque en ningún caso.

  3. En el test de permutación, ¿por qué se permuta solo el signo de la posición y no la escala por volatilidad?

    • APor velocidad de cálculo
    • BPorque la pregunta es si acertar la dirección en cada momento aporta algo; desordenar también la escala mezclaría una segunda cosa (el ajuste de riesgo) y la nula dejaría de ser comparable
    • CPorque la escala es siempre constante
    • DPorque así el p-valor sale más bajo
    Ver respuesta

    Respuesta B. Porque la pregunta es si acertar la dirección en cada momento aporta algo; desordenar también la escala mezclaría una segunda cosa (el ajuste de riesgo) y la nula dejaría de ser comparable

    Un test de permutación responde a una pregunta concreta: se rompe la relación que se quiere probar (dirección frente a rendimiento) y se conserva todo lo demás. Si la escala por volatilidad se desordena, la nula toma posiciones grandes en días agitados y su Sharpe baja por otra razón.

  4. La nula del test con la señal barajada tiene media 0,20, no 0. ¿Por qué?

    • APorque el generador de números aleatorios está sesgado
    • BPorque barajar conserva la proporción de días largos y cortos de cada mercado: la nula recoge la exposición media (la prima de los índices, el carry). El test mide si el momento elegido añade algo sobre esa exposición
    • CPorque los futuros siempre suben
    • DPorque hay pocas permutaciones
    Ver respuesta

    Respuesta B. Porque barajar conserva la proporción de días largos y cortos de cada mercado: la nula recoge la exposición media (la prima de los índices, el carry). El test mide si el momento elegido añade algo sobre esa exposición

    La cartera está larga el 71 % de las sesiones en IDA y el 70 % en IDB, índices que llevan la prima de la renta variable. Cualquier permutación conserva esos porcentajes y, con ellos, la parte del resultado que viene de la exposición media. Por eso un test de permutación no confunde exposición con acierto.

  5. La mejor de 995 estrategias sin ventaja tiene un Sharpe de 1,50 y su intervalo bootstrap del 95 % va de 0,46 a 2,54. ¿Qué falla?

    • AEl bootstrap está mal programado
    • BNada: la estrategia tiene ventaja
    • CEl bootstrap remuestrea la serie ya elegida: no sabe que se eligió la mejor de 995. La selección se corrige con el Deflated Sharpe o el PBO (lección 2.5.4)
    • DHacen falta bloques más largos
    Ver respuesta

    Respuesta C. El bootstrap remuestrea la serie ya elegida: no sabe que se eligió la mejor de 995. La selección se corrige con el Deflated Sharpe o el PBO (lección 2.5.4)

    Remuestrear una serie dice cuánto varía su estadístico por azar dentro de esa serie, no cuántas series se miraron antes de elegirla. Entre 995 estrategias sin ventaja, la mejor tiene un Sharpe alto por construcción, y su intervalo se centra en ese valor inflado.

Ideas clave

  • Cada método de Monte Carlo conserva una estructura y rompe otra: desordenar trades rompe el agrupamiento de pérdidas; el bootstrap de bloques conserva las rachas de volatilidad dentro del bloque; permutar la señal rompe solo la relación entre dirección y rendimiento.
  • En la cartera de tendencia, el drawdown de la secuencia real de operaciones (−21,9 %) es peor que el de las 2.000 secuencias desordenadas: con operaciones solapadas, el remuestreo de trades infravalora el riesgo.
  • La longitud de bloque importa para el drawdown cuando la serie tiene rachas de volatilidad (IDA: mediana de −53,0 % con bloques de 1 y −65,3 % con bloques de 75) y casi nada en una cartera escalada por volatilidad o para el intervalo del Sharpe (de 0,6 a 1,4 con cualquier bloque).
  • El drawdown histórico de la cartera (−14,7 %) es más suave que la mediana del bootstrap (≈ −17,8 %): el backtest enseña una historia algo afortunada, no el riesgo que cabe esperar.
  • El test de permutación separa el acierto del momento de la exposición media: p = 0,001 en la tendencia con efecto plantado, p = 0,25 en su gemelo sin memoria y un 4,5 % de falsos positivos al 5 % en 200 series de ruido.
  • Con 25 años de datos y un efecto débil por mercado, solo 7 de los 24 mercados pasan el test por separado: no rechazar no es lo mismo que no tener efecto.
  • Ningún remuestreo corrige la selección: la mejor de 995 estrategias sin ventaja tiene un intervalo bootstrap que excluye el cero.

Recursos