---
title: "2.1.1 Contrastes, bootstrap y errores Newey-West"
description: "¿La media es distinta de cero? Contraste t, bootstrap i.i.d. y por bloques, potencia y regresión con errores HAC, probados con efectos plantados."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/contrastes-bootstrap-y-errores-robustos/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/contrastes-bootstrap-y-errores-robustos.png"
---

# 2.1.1 · Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos

> Un backtest es una muestra y su rentabilidad media, una estimación con error. Esta lección mide ese error con tres herramientas (el contraste t, el bootstrap y los errores de Newey-West) sobre datos sintéticos donde se sabe qué estrategias tienen ventaja y cuáles no, y enseña por qué las rentabilidades solapadas fabrican un t de 10,5 donde el honesto es 1,8.

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.1 · Estadística y econometría para trading
- Duración: 70 min
- Requisitos: [0.7.2 · Probabilidad y distribuciones, correlación frente a causalidad y regresión simple](https://pythonparatrading.com/curso-python-trading/nivel-0/probabilidad-distribuciones-y-regresion/), [1.6.2 · Hechos estilizados: colas gruesas, agrupamiento, autocorrelación y apalancamiento](https://pythonparatrading.com/curso-python-trading/nivel-1/hechos-estilizados-de-los-mercados/), [1.6.3 · Estacionalidad: día de la semana, mes, cambio de mes y sesiones intradía](https://pythonparatrading.com/curso-python-trading/nivel-1/estacionalidad-y-efectos-calendario/)

## Al terminar esta lección

- Plantear un contraste sobre la rentabilidad media de una estrategia y leer su p-valor, sus errores de tipo I y II y su potencia.
- Calcular intervalos de confianza por la fórmula t, por bootstrap i.i.d. y por bootstrap de bloques, y saber cuándo difieren.
- Medir cómo las rentabilidades solapadas inflan el estadístico t y corregirlo con errores de Newey-West (`cov_type="HAC"`) o con muestras sin solape.
- Estimar una regresión múltiple en un panel de futuros con errores robustos a la autocorrelación y a la correlación entre mercados (Driscoll-Kraay).

## Un backtest es una muestra

Un backtest de cuatro años con una rentabilidad media del 15 % anual no dice que la estrategia gane un 15 % al año.
Dice que, en esos cuatro años concretos, ganó eso. Si el mercado hubiera tirado otros dados, la media sería otra. La
pregunta de esta lección es cuánto podría haber salido distinta y, en particular, si un resultado así es compatible
con que la ventaja real sea **cero**.

La lección 1.6.3 ya usó el estadístico t y el bootstrap para los efectos calendario. Aquí se formaliza el método, se
mide su potencia y se tratan los dos problemas que más t falsos producen en la práctica: las observaciones que se
solapan y los activos que se mueven a la vez.

```mermaid
flowchart TB
  H[Hipótesis nula] --> E[Estadístico]
  E --> D[Distrib. bajo H0]
  D --> P[p-valor]
  P --> R{¿p < α?}
  R -- sí --> RE[Rechazar H0]
  R -- no --> NR[No rechazar]
```

- **Hipótesis nula** (H0): la rentabilidad media esperada es cero. Es lo que se intenta refutar.
- **Estadístico**: un número calculado con la muestra que sería pequeño si H0 fuera cierta. Para la media, el t de
  Student:

$$
t = \frac{\overline{r}}{s / \sqrt{n}}
$$

  con $\overline{r}$ la media de las $n$ rentabilidades y $s$ su desviación típica.
- **Distribución bajo H0**: cómo se repartiría el estadístico si H0 fuera cierta. Con observaciones independientes y
  $n$ grande, una normal estándar; con el bootstrap, la que sale de remuestrear los datos.
- **p-valor**: la probabilidad, si H0 fuera cierta, de obtener un estadístico al menos tan extremo como el observado.
  No es la probabilidad de que H0 sea cierta.

Al decidir con un umbral α (el 5 % por costumbre) se pueden cometer dos errores:

| | H0 cierta (sin ventaja) | H0 falsa (hay ventaja) |
|---|---|---|
| Se rechaza H0 | Error de tipo I (falso positivo), probabilidad α | Acierto: la **potencia** |
| No se rechaza H0 | Acierto | Error de tipo II (falso negativo) |

En trading, el error de tipo I es el caro: se pone dinero en una estrategia sin ventaja y se pagan costes por nada. El
de tipo II es más silencioso: se descarta una estrategia buena porque la muestra era corta.

Una identidad útil conecta el t con el Sharpe. Si $SR$ es el Sharpe anual y la muestra dura $T$ años,

$$
t = \frac{\overline{r}}{s}\sqrt{n} = SR_{\text{diario}}\sqrt{n} = SR \sqrt{T}
$$

Un Sharpe de 1 durante 4 años da un t de 2; un Sharpe de 0,5 necesita 16 años para llegar a 2.

## Cuatro estrategias con respuesta conocida

`ruido2_estrategias.parquet` tiene los rendimientos diarios de 1.000 estrategias ficticias en las 1.003 sesiones de
2022 a 2025. Son datos sintéticos: 995 no tienen ninguna ventaja (Sharpe verdadero 0) y cinco tienen una plantada,
documentada en `ruido2_verdad.csv`. Que una de ellas gane aquí no dice nada de ningún mercado real; sirve para saber
si un método acierta. Se eligen cuatro:

- `e0518`, Sharpe verdadero 1,5: la ventaja más clara del conjunto.
- `e0258`, Sharpe verdadero 0,7: ventaja real pero modesta.
- `e0001`, sin ventaja: una cualquiera.
- la mejor de las 995 sin ventaja, por su Sharpe medido.

```python
import numpy as np
import pandas as pd
from scipy import stats

r = pd.read_parquet("ruido2_estrategias.parquet").pivot(
    index="fecha", columns="estrategia", values="rendimiento")
verdad = pd.read_csv("ruido2_verdad.csv").set_index("estrategia")
sr = r.mean() / r.std() * np.sqrt(252)
nulas = verdad.index[verdad["sharpe_real"] == 0]
mejor_nula = sr[nulas].idxmax()
elegidas = ["e0518", "e0258", "e0001", mejor_nula]

filas = {}
for e in elegidas:
    t, p = stats.ttest_1samp(r[e], 0)
    filas[e] = {"sharpe_real": verdad.loc[e, "sharpe_real"],
                "sharpe_medido": sr[e],
                "media_anual_%": 100 * 252 * r[e].mean(),
                "t": t, "p": p,
                "sr_x_raiz_anos": sr[e] * np.sqrt(len(r) / 252)}
print(len(r), "sesiones;", len(nulas), "estrategias sin ventaja")
pd.DataFrame(filas).T.round(3)
```
```output
1003 sesiones; 995 estrategias sin ventaja
       sharpe_real  sharpe_medido  media_anual_%      t      p  sr_x_raiz_anos
e0518          1.5          1.579         33.165  3.150  0.002           3.150
e0258          0.7          0.622          8.492  1.240  0.215           1.240
e0001          0.0          0.003          0.063  0.007  0.995           0.007
e0598          0.0          1.503         26.607  2.999  0.003           2.999
```

La última columna comprueba la identidad: el t de `scipy.stats.ttest_1samp` coincide con el Sharpe medido por la raíz
de los años (3,98 años). La tabla ya enseña los dos errores:

- `e0518` (Sharpe verdadero 1,5) sale con t = 3,15 y p = 0,002: el contraste acierta.
- `e0258` tiene ventaja real, pero su Sharpe medido es 0,62 y su p, 0,215: con cuatro años no se distingue del ruido.
  Es un error de tipo II, y no es mala suerte excepcional: con un Sharpe verdadero de 0,7 y cuatro años, el t
  esperado es 1,4.
- La mejor de las nulas, `e0598`, tiene un Sharpe medido de 1,50 y p = 0,003: más «significativa» que una de las
  buenas. Es el máximo de 995 contrastes; el p-valor de un contraste solo vale para una hipótesis elegida antes de
  mirar. Esa selección la trata la lección 2.1.8; aquí basta con recordar que ningún método de esta lección la
  corrige.

## El bootstrap: la distribución sin fórmulas

El contraste t supone que la media se reparte como una normal. Con rentabilidades de colas gruesas (lección 1.6.2) y
un millar de observaciones, la aproximación suele ser buena, pero no siempre se tiene una fórmula: para el Sharpe, el
drawdown o la mediana hay que trabajar más. El **bootstrap** evita la fórmula: remuestrea los datos con reemplazo miles
de veces, recalcula el estadístico en cada remuestra y usa la dispersión de esos valores como su error.

```mermaid
flowchart LR
  M[Muestra n días] --> R[Remuestra B veces]
  R --> E[Estadístico ×B]
  E --> I[Percentiles IC]
```

El bootstrap **i.i.d.** sortea días sueltos, así que destruye cualquier dependencia entre días consecutivos. El
**bootstrap de bloques** sortea tramos de días seguidos y conserva la dependencia dentro de cada tramo. La librería
`arch` tiene los dos: `IIDBootstrap` y `StationaryBootstrap` (Politis y Romano, 1994), cuyos bloques tienen longitud
aleatoria con media dada.

```python
from arch.bootstrap import IIDBootstrap, StationaryBootstrap

def media_anual(x):
    return 252 * x.mean()

def intervalos(x, bloque=20, reps=2000, semilla=7):
    x = np.asarray(x)
    m, se = media_anual(x), 252 * x.std(ddof=1) / np.sqrt(len(x))
    q = stats.t.ppf(0.975, len(x) - 1)
    out = {"t": (m - q * se, m + q * se)}
    for nombre, bs in [("iid", IIDBootstrap(x, seed=semilla)),
                       ("bloques", StationaryBootstrap(
                           bloque, x, seed=semilla))]:
        ic = bs.conf_int(media_anual, reps=reps, method="percentile")
        out[nombre] = tuple(ic.ravel())
    return out

ic = {(e, k): 100 * np.array(v) for e in elegidas
      for k, v in intervalos(r[e]).items()}
pd.DataFrame(ic, index=["inf_%", "sup_%"]).T.round(1)
```
```output
               inf_%  sup_%
e0518 t         12.5   53.8
      iid       13.5   55.0
      bloques    9.0   55.5
e0258 t         -4.9   21.9
      iid       -4.7   22.1
      bloques   -5.0   22.2
e0001 t        -18.8   19.0
      iid      -18.7   20.2
      bloques  -19.1   19.3
e0598 t          9.2   44.0
      iid        9.0   43.7
      bloques    6.8   46.0
```

*Fig. — Intervalo del 95 % de la rentabilidad media anual de cuatro estrategias sintéticas, por la fórmula t, por bootstrap i.i.d. y por bootstrap de bloques.* Doce segmentos horizontales, tres por estrategia, con un punto en la media. Los tres métodos casi coinciden en cada estrategia. e0518, con Sharpe real 1,5: de 9 a 55 %. e0258, con Sharpe real 0,7: de −5 a 22 %, cruza el cero. e0001, sin ventaja: de −19 a 20 %. La mejor de las 995 estrategias sin ventaja, destacada: de 7 a 46 %, sin tocar el cero aunque su ventaja real es nula.

Los tres métodos dan casi el mismo intervalo en las cuatro estrategias; el de bloques sale algo más ancho por abajo en
`e0518` y `e0598` (9,0 frente a 12,5 en la primera), por el azar del remuestreo y por el agrupamiento de volatilidad,
que los bloques conservan. No es casualidad que coincidan: estos rendimientos diarios no tienen autocorrelación (solo agrupamiento de volatilidad), así que remuestrear días sueltos o bloques de
20 da lo mismo, y con 1.003 días la media ya se reparte casi como una normal. Es la situación habitual con
rentabilidades **diarias** de una estrategia que no mantiene posiciones muchos días: el contraste t es suficiente.

La lección práctica está en la anchura. El intervalo de `e0518` va de alrededor del 12 % al 55 % anual: la ventaja es real, pero
cuatro años no dicen si es grande o pequeña. El de `e0258`, de −6 % a +22 %, incluye el cero aunque la ventaja exista.

El bootstrap también da un p-valor. Para contrastar H0, se centra la muestra en cero (se le resta su media, así la
nula es cierta en las remuestras) y se cuenta qué fracción de las medias remuestreadas es tan extrema como la
observada:

```python
x = r["e0258"].to_numpy()
bs = StationaryBootstrap(20, x - x.mean(), seed=7)
medias = np.array([media_anual(d[0][0])
                   for d in bs.bootstrap(4000)])
p_bs = np.mean(np.abs(medias) >= abs(media_anual(x)))
print(f"media anual observada: {100 * media_anual(x):.2f} %")
print(f"p bootstrap: {p_bs:.3f}  (p del t: "
      f"{stats.ttest_1samp(x, 0).pvalue:.3f})")
```
```output
media anual observada: 8.49 %
p bootstrap: 0.232  (p del t: 0.215)
```

Los dos p-valores son parecidos (0,232 frente a 0,215) y llevan a la misma conclusión. En rentabilidades diarias sin
autocorrelación, el bootstrap confirma el contraste t; su valor aparece cuando la fórmula no existe o la dependencia sí, como en las secciones que
vienen.

## La potencia: cuánta historia hace falta

La figura de la cabecera responde a la pregunta inversa: si la ventaja existe, ¿con qué probabilidad la detecta un
contraste? Se simulan 4.000 estrategias de 1.003 días con innovaciones t de Student (colas gruesas) y cuatro Sharpe
verdaderos. Es una simulación con semilla, no los datos del curso: así se conoce la respuesta en cada una.

```python
rng = np.random.default_rng(211)
n, sims = 1003, 4000
filas = {}
for sr_real in [0, 0.5, 1.0, 1.5]:
    z = rng.standard_t(5, (sims, n)) * np.sqrt(3 / 5)  # varianza 1
    x = sr_real / np.sqrt(252) + z
    t = x.mean(1) / (x.std(1, ddof=1) / np.sqrt(n))
    p = 2 * stats.t.sf(np.abs(t), n - 1)
    filas[sr_real] = {
        "rechaza_5%": np.mean(p < 0.05),
        "teorica": stats.norm.cdf(sr_real * np.sqrt(n / 252) - 1.96),
        "p_mediano": np.median(p)}
pd.DataFrame(filas).T.round(3)
```
```output
     rechaza_5%  teorica  p_mediano
0.0       0.056    0.025      0.498
0.5       0.166    0.168      0.294
1.0       0.528    0.514      0.042
1.5       0.850    0.849      0.003
```

Con Sharpe 0 (la nula), un 5,6 % de las simulaciones rechaza al 5 %: el tamaño nominal, con el ruido de 4.000
réplicas. Los p-valores son **uniformes**: cada tramo de 0,05 recibe un 5 % de ellos, que es lo que se ve en el panel
izquierdo de la figura. Con Sharpe 1, la potencia es del 52,8 %: la mitad de las veces una estrategia de Sharpe 1
probada durante cuatro años no pasa el umbral. Con Sharpe 0,5, solo un 16,6 %. La columna `teorica` (la normal con
media $SR\sqrt{T}$, sin contar el rechazo por el lado negativo) acierta casi exactamente.

Despejando $T$ de la condición «t esperado = 1,96 + 0,84» (el 0,84 da un 80 % de potencia) sale la regla de bolsillo:

```python
for s in [0.25, 0.5, 1.0, 1.5, 2.0]:
    anos = ((stats.norm.ppf(0.975) + stats.norm.ppf(0.8)) / s) ** 2
    print(f"Sharpe {s:4.2f}: {anos:6.1f} años")
```
```output
Sharpe 0.25:  125.6 años
Sharpe 0.50:   31.4 años
Sharpe 1.00:    7.8 años
Sharpe 1.50:    3.5 años
Sharpe 2.00:    2.0 años
```

Con un Sharpe verdadero de 0,5, una cifra ya buena para una estrategia real después de costes, harían falta 31 años
de historia para detectarla con un 80 % de probabilidad. Por eso los backtests cortos con Sharpe alto deben leerse
con desconfianza (el Sharpe medido de las afortunadas está inflado, lección 2.5.4) y por eso las estrategias de
Sharpe bajo se validan con muchos activos a la vez más que con mucha historia de uno solo.

## Rentabilidades solapadas: el t que se multiplica

Muchas señales predicen un horizonte largo: «la rentabilidad de las próximas 21 sesiones». Si se calcula ese objetivo
**cada día**, dos observaciones consecutivas comparten 20 de sus 21 sesiones. La media sigue siendo correcta; el error
típico de la fórmula clásica, no. Con solape $h$, las autocovarianzas son positivas hasta el retardo $h-1$ y la
varianza real de la media es unas $h$ veces la que supone $s^2/n$: el t ingenuo sale inflado por cerca de $\sqrt{h}$.

El estimador de **Newey-West** (1987) corrige la varianza sumando las autocovarianzas $\hat\gamma_j$ hasta un retardo
$L$, con pesos decrecientes que garantizan que el resultado sea positivo:

$$
\widehat{\operatorname{Var}}(\overline{y}) = \frac{1}{n}\left[\hat\gamma_0 + 2\sum_{j=1}^{L}\left(1-\frac{j}{L+1}\right)\hat\gamma_j\right]
$$

En statsmodels es una regresión sobre una constante con `cov_type="HAC"`. Primero, a mano y con la librería, sobre
una estrategia sin ventaja:

```python
import statsmodels.api as sm

def nw_var_media(y, L):
    y = np.asarray(y) - np.mean(y)
    n = len(y)
    g = [y[j:] @ y[:n - j] / n for j in range(L + 1)]
    lrv = g[0] + 2 * sum((1 - j / (L + 1)) * g[j]
                         for j in range(1, L + 1))
    return lrv / n

h = 21
lr = np.log1p(r["e0001"]).cumsum()
y = (lr.shift(-h) - lr).dropna()       # 21 sesiones, cada día
ols = sm.OLS(y.to_numpy(), np.ones(len(y))).fit()
hac = sm.OLS(y.to_numpy(), np.ones(len(y))).fit(
    cov_type="HAC", cov_kwds={"maxlags": h})
print(f"autocorrelación en el retardo 1: {y.autocorr(1):.3f}")
print(f"t ingenuo: {ols.tvalues[0]:.2f}; "
      f"t Newey-West: {hac.tvalues[0]:.2f}")
print(f"error típico a mano: {np.sqrt(nw_var_media(y, h)):.6f}; "
      f"statsmodels: {hac.bse[0]:.6f}")
```
```output
autocorrelación en el retardo 1: 0.959
t ingenuo: -0.97; t Newey-West: -0.25
error típico a mano: 0.007131; statsmodels: 0.007131
```

La autocorrelación en el retardo 1 es de 0,96, cerca del 20/21 teórico. El t ingenuo es unas cuatro veces el de
Newey-West, y el error típico calculado a mano coincide con el de statsmodels (que usa por defecto los mismos pesos de
Bartlett). En esta estrategia los dos t están lejos de 2, pero la pregunta importante es qué pasa en todas.

Las 995 estrategias sin ventaja permiten medir la **tasa de falsos positivos** de cada método sobre datos cuya verdad
se conoce:

```python
lr = np.log1p(r[nulas]).cumsum()
Y = (lr.shift(-h) - lr).dropna()

def p_media(y, L=None):
    m = sm.OLS(y, np.ones(len(y)))
    f = m.fit() if L is None else m.fit(
        cov_type="HAC", cov_kwds={"maxlags": L})
    return f.pvalues[0]

tasas = {
    "diario, t": np.mean([stats.ttest_1samp(r[e], 0).pvalue < 0.05
                          for e in nulas]),
    "solapado, OLS": np.mean([p_media(Y[e].to_numpy()) < 0.05
                              for e in nulas])}
for L in [10, 21, 42]:
    tasas[f"solapado, NW L={L}"] = np.mean(
        [p_media(Y[e].to_numpy(), L) < 0.05 for e in nulas])
tasas["sin solape, t"] = np.mean(
    [stats.ttest_1samp(Y[e].iloc[::h], 0).pvalue < 0.05
     for e in nulas])
pd.Series(tasas, name="falsos_positivos").round(3)
```
```output
diario, t            0.044
solapado, OLS        0.653
solapado, NW L=10    0.183
solapado, NW L=21    0.094
solapado, NW L=42    0.068
sin solape, t        0.039
Name: falsos_positivos, dtype: float64
```

*Fig. — Falsos positivos al 5 % en 995 estrategias sin ventaja según el contraste: rentabilidades diarias, rentabilidades a 21 sesiones calculadas cada día (solapadas) y la misma serie sin solape.* Barras horizontales con la proporción de estrategias sin ventaja declaradas significativas al 5 %. Diario con t: 4,4 %. Solapado con errores clásicos, destacada: 65,3 %. Solapado con Newey-West de 10, 21 y 42 retardos: 18,3, 9,4 y 6,8 %. Sin solape: 3,9 %. Una línea vertical marca el 5 % nominal.

Con rentabilidades diarias, el contraste t da un 4,4 % de falsos positivos: lo nominal. Con las mismas estrategias y
un objetivo solapado a 21 sesiones, la regresión con errores clásicos declara significativas al 65,3 %: dos de cada
tres estrategias sin ventaja «predicen» su propia rentabilidad. Newey-West lo corrige casi del todo, y la elección de
$L$ importa: con 10 retardos (menos que el solape) queda un 18,3 %; con 21, un 9,4 %; con 42, un 6,8 %. Muestrear una
observación cada 21 sesiones (sin solape) devuelve el 3,9 %, a costa de quedarse con 47 observaciones.

<aside class="callout" data-kind="riesgo">

El solape aparece sin que nadie lo decida. Una regresión de «la rentabilidad a 12 meses» sobre una señal con datos
mensuales tiene solape 12; un backtest que abre una posición cada día y la mantiene 10 días produce rentabilidades
diarias de una cartera de 10 posiciones solapadas; un objetivo de *machine learning* a 5 días (nivel 3) tiene solape
5. En todos los casos, un t calculado como si las observaciones fueran independientes está inflado.

</aside>

El bootstrap también falla con el solape si se usa el i.i.d., y funciona con bloques más largos que el solape:

```python
y = Y["e0001"].to_numpy()
ee = {"fórmula clásica": y.std(ddof=1) / np.sqrt(len(y)),
      "Newey-West L=42": np.sqrt(nw_var_media(y, 42))}
for nombre, bs in [
        ("bootstrap iid", IIDBootstrap(y, seed=3)),
        ("bloques media 63", StationaryBootstrap(63, y, seed=3))]:
    m = [d[0][0].mean() for d in bs.bootstrap(2000)]
    ee[nombre] = np.std(m)
pd.Series(ee, name="error_tipico_media").round(5)
```
```output
fórmula clásica     0.00186
Newey-West L=42     0.00810
bootstrap iid       0.00191
bloques media 63    0.00792
Name: error_tipico_media, dtype: float64
```

El bootstrap i.i.d. reproduce el error de la fórmula clásica (0,0019) porque, al sortear días sueltos, rompe el solape;
el de bloques, con bloques de 63 sesiones de media, da un error de la misma escala que Newey-West, unas cuatro veces
mayor. La regla es la misma para los dos métodos: la ventana (retardos o bloque) tiene que cubrir la dependencia.

## Regresión múltiple con errores robustos: carry y momentum en futuros

El caso más frecuente es una regresión predictiva: la rentabilidad futura de un activo sobre una o varias señales de
hoy. `futuros2_continuos.parquet` y `futuros2_carry.parquet` tienen 24 mercados de futuros sintéticos de 2000 a 2025
con dos efectos plantados (README del nivel 2): el rendimiento esperado anual es
$0{,}5\,c_{\text{exogeno}} + 0{,}1\,c_{\text{reversion}} + 0{,}15\,\sigma\,\operatorname{signo}(r_{252})$, con
$\sigma$ la volatilidad de largo plazo de cada mercado y el último término un momentum de series temporales. El carry
observado, `carry_anual`, es la suma de las dos partes: una exógena, con prima, y otra que solo descuenta la vuelta del
precio a su ancla, casi sin prima. La regresión natural es

$$
y_{i,t} = a + b_1\, c_{i,t} + b_2\, \sigma_{i,t}\operatorname{signo}(r_{i,t-252:t}) + \varepsilon_{i,t}
$$

con $y$ la rentabilidad anualizada de las 21 sesiones siguientes y $c$ el carry observado. La respuesta conocida es
$b_2 \approx 0{,}15$; $b_1$ es una mezcla de 0,5 y 0,1 según qué parte del carry domine (el README mide 0,15 con datos
de fin de mes). $\sigma$ se estima con todos los rendimientos hasta cada fecha (ventana creciente): es la volatilidad
de largo plazo sin mirar adelante. Las observaciones son diarias, así que hay solape de 21; además, los 24 mercados
comparten días y factores. El estimador de **Driscoll-Kraay** (1998) trata las dos cosas: suma, para cada fecha, los productos regresor
× residuo de todos los mercados y aplica Newey-West a esa serie. En statsmodels es `cov_type="hac-groupsum"` con el
índice de fecha.

```python
cont = pd.read_parquet("futuros2_continuos.parquet")
carry = pd.read_parquet("futuros2_carry.parquet")
lp = np.log(cont.pivot(index="fecha", columns="mercado",
                       values="cierre_ajustado"))
c = carry.pivot(index="fecha", columns="mercado",
                values="carry_anual")
fwd = (lp.shift(-h) - lp) * 252 / h            # anualizada
# σ de largo plazo de cada mercado, sin mirar adelante
vol = lp.diff().expanding(252).std() * np.sqrt(252)
mom = np.sign(lp - lp.shift(252)) * vol
panel = pd.concat({"y": fwd, "carry": c, "mom": mom}, axis=1)
panel = panel.stack(future_stack=True).dropna()
X = sm.add_constant(panel[["carry", "mom"]])
fecha = panel.index.get_level_values("fecha").factorize()[0]

ols = sm.OLS(panel["y"], X).fit()
dk = sm.OLS(panel["y"], X).fit(
    cov_type="hac-groupsum", cov_kwds={"time": fecha, "maxlags": h})
print(len(panel), "filas,", panel.index.get_level_values(
    "mercado").nunique(), "mercados")
pd.DataFrame({"coef": ols.params, "t_OLS": ols.tvalues,
              "t_DK": dk.tvalues}).round(3)
```
```output
150384 filas, 24 mercados
        coef   t_OLS   t_DK
const  0.013   5.844  0.808
carry  0.171   8.683  1.596
mom    0.107  10.511  1.834
```

Los coeficientes recuperan la respuesta con error: 0,107 para el momentum (verdad 0,15) y 0,171 para el carry, dentro
de la mezcla esperada y cerca del 0,15 del README. Lo que cambia por completo es la confianza. Con errores clásicos,
los t son 8,68 y 10,51: certeza aparente. Con Driscoll-Kraay, 1,60 y 1,83: ninguno pasa el umbral, aunque los dos
efectos existen. Son 150.384 filas, pero la información es mucho menor: unas 300 rentabilidades mensuales
independientes por mercado, y mercados que se mueven juntos en las crisis. Que el contraste honesto no rechace no
dice que no haya efecto (aquí se sabe que lo hay): dice que 26 años de 24 mercados correlados no bastan para
demostrarlo con un t de 2. Es la potencia de la sección anterior, ahora en un panel.

La comprobación sin fórmulas sofisticadas es quitar el solape: una fecha de cada 21 y errores agrupados por fecha
(`cov_type="cluster"`), que corrigen la correlación entre mercados del mismo día.

```python
fechas = np.sort(panel.index.get_level_values("fecha").unique())
mensual = panel[panel.index.get_level_values("fecha").isin(
    fechas[::h])]
Xm = sm.add_constant(mensual[["carry", "mom"]])
grupos = mensual.index.get_level_values("fecha").factorize()[0]
om = sm.OLS(mensual["y"], Xm).fit()
cl = sm.OLS(mensual["y"], Xm).fit(
    cov_type="cluster", cov_kwds={"groups": grupos})
print(len(mensual), "filas en", len(fechas[::h]), "fechas")
pd.DataFrame({"coef": om.params, "t_OLS": om.tvalues,
              "t_cluster": cl.tvalues}).round(3)
```
```output
7176 filas en 299 fechas
        coef  t_OLS  t_cluster
const  0.011  1.100      0.588
carry  0.209  2.288      1.536
mom    0.053  1.126      0.668
```

Sin solape, el t clásico del carry baja de 8,68 a 2,29 y el del momentum, de 10,51 a 1,13; agrupando por fecha, a
1,54 y 0,67. Los coeficientes se mueven más que con Driscoll-Kraay (0,209 y 0,053: quedarse con una fecha de cada 21
tira información y deja la estimación más ruidosa), pero la conclusión es la misma: la incertidumbre honesta es varias
veces la ingenua.

¿Y si la señal no sirviera para nada? Un regresor **placebo** persistente, sin ninguna relación con los precios, es
la prueba de fuego. Se simulan 20 placebos AR(1) con la misma persistencia que el carry del generador (0,997 diario),
cada uno independiente por mercado, y se cuenta cuántas veces sale «significativo»:

```python
rng = np.random.default_rng(2111)
rech = {"OLS": 0, "Driscoll-Kraay": 0, "DK L=63": 0}
for _ in range(20):
    e = rng.standard_normal(lp.shape) * np.sqrt(1 - 0.997**2)
    pl = np.empty(lp.shape)
    pl[0] = rng.standard_normal(lp.shape[1])
    for i in range(1, len(pl)):
        pl[i] = 0.997 * pl[i - 1] + e[i]
    pl = pd.DataFrame(pl, index=lp.index, columns=lp.columns)
    d = pd.concat({"y": fwd, "carry": c, "mom": mom, "pl": pl},
                  axis=1).stack(future_stack=True).dropna()
    Xp = sm.add_constant(d[["carry", "mom", "pl"]])
    tt = d.index.get_level_values("fecha").factorize()[0]
    m = sm.OLS(d["y"], Xp)
    rech["OLS"] += m.fit().pvalues["pl"] < 0.05
    for k, L in [("Driscoll-Kraay", h), ("DK L=63", 63)]:
        f = m.fit(cov_type="hac-groupsum",
                  cov_kwds={"time": tt, "maxlags": L})
        rech[k] += f.pvalues["pl"] < 0.05
print({k: f"{v} de 20" for k, v in rech.items()})
```
```output
{'OLS': '14 de 20', 'Driscoll-Kraay': '1 de 20', 'DK L=63': '1 de 20'}
```

Con errores clásicos, el placebo es «significativo» en 14 de 20 simulaciones. Con Driscoll-Kraay, en 1 con 21
retardos y en 1 con 63. Veinte réplicas son pocas para medir una tasa con precisión, pero el orden de magnitud es
claro: con señales persistentes y objetivos solapados, los errores clásicos no son una aproximación optimista, son
un generador de descubrimientos.

## Qué error típico usar

```mermaid
flowchart TB
  Q1{¿Dependencia?} -- no --> Q2{¿Varios activos?}
  Q1 -- sí --> Q3{¿Varios activos?}
  Q2 -- no --> A[t clásico o iid]
  Q2 -- sí --> B[Cluster por fecha]
  Q3 -- no --> C[Newey-West L=2h]
  Q3 -- sí --> D[Driscoll-Kraay]
```

| Situación | Error típico | En statsmodels | Comprobación |
|---|---|---|---|
| Rentabilidades diarias de una estrategia | Clásico; bootstrap i.i.d. si no hay fórmula | `fit()` | Bootstrap de bloques: debe coincidir |
| Objetivo solapado de $h$ sesiones, un activo | Newey-West con $L \ge h$ | `cov_type="HAC"` | Submuestra sin solape |
| Varios activos, sin solape | Agrupado por fecha | `cov_type="cluster"` | Fama-MacBeth (lección 2.3.5) |
| Varios activos y solape | Driscoll-Kraay | `cov_type="hac-groupsum"` | Sin solape + cluster por fecha |
| Cualquier estadístico sin fórmula (Sharpe, drawdown) | Bootstrap de bloques | `arch.bootstrap` | Varias longitudes de bloque |

Tres advertencias que no resuelve ninguna fila de la tabla:

- **Selección.** Si se probaron cien variantes y se informa de la mejor, todos estos p-valores son demasiado bajos
  (lecciones 2.1.8 y 2.5.4).
- **Costes.** Un coeficiente significativo en rentabilidades brutas puede no sobrevivir a la horquilla y las
  comisiones (lección 2.4.7). La significación estadística no es rentabilidad.
- **Estabilidad.** Un t de 3 sobre 20 años puede venir de una sola crisis. Mira el coeficiente por subperiodos antes
  de fiarte (lección 2.5.6).

## Ejercicios

1. Repite la tabla de falsos positivos de las 995 estrategias nulas con un objetivo solapado a 5 sesiones y a 63
   sesiones, con errores clásicos y con Newey-West de $L = h$. ¿Cómo crece el problema con el solape?

**Solución.**

```python
for hh in [5, 63]:
    lr = np.log1p(r[nulas]).cumsum()
    Yh = (lr.shift(-hh) - lr).dropna()
    ols_fp = np.mean([p_media(Yh[e].to_numpy()) < 0.05
                      for e in nulas])
    nw_fp = np.mean([p_media(Yh[e].to_numpy(), hh) < 0.05
                     for e in nulas])
    print(f"h = {hh:2d}: OLS {ols_fp:.3f}; NW L=h {nw_fp:.3f}")
```
```output
h =  5: OLS 0.364; NW L=h 0.076
h = 63: OLS 0.805; NW L=h 0.138
```

Con solape 5, los errores clásicos ya dan un 36,4 % de falsos positivos; con 63, un 80,5 %: el t ingenuo crece con
$\sqrt{h}$ y cada vez más estrategias nulas superan el umbral. Newey-West baja la tasa en los dos casos (7,6 % y 13,8 %), pero con
63 retardos sobre unas 940 observaciones queda muy por encima de lo nominal: con un solape tan largo hay pocas observaciones
independientes (unas 15) y cualquier estimador de la varianza es impreciso. Con objetivos muy largos, lo prudente es
muestrear sin solape o usar más historia.

2. Calcula el intervalo del 95 % del **Sharpe anual** de `e0518` y de `e0258` con un bootstrap estacionario de
   bloques de media 20. ¿Incluye el Sharpe verdadero? ¿Incluye el cero?

**Solución.**

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

for e in ["e0518", "e0258"]:
    bs = StationaryBootstrap(20, r[e].to_numpy(), seed=11)
    inf, sup = bs.conf_int(sharpe, reps=2000,
                           method="percentile").ravel()
    print(f"{e}: Sharpe medido {sharpe(r[e].to_numpy()):.2f}, "
          f"IC 95 % [{inf:.2f}, {sup:.2f}], "
          f"verdadero {verdad.loc[e, 'sharpe_real']}")
```
```output
e0518: Sharpe medido 1.58, IC 95 % [0.43, 2.77], verdadero 1.5
e0258: Sharpe medido 0.62, IC 95 % [-0.43, 1.69], verdadero 0.7
```

Los dos intervalos incluyen el Sharpe verdadero y tienen una anchura de unos dos puntos de Sharpe: es el error típico
de ≈ 0,5 que da la regla $1/\sqrt{T}$ con cuatro años. El de `e0518` excluye el cero; el de `e0258` lo incluye. Un
Sharpe medido de 1,58 es compatible con un verdadero de 0,5 o de 2,7: cuatro años no distinguen una estrategia
excelente de una mediocre.

3. Un compañero presenta una regresión de la rentabilidad a 12 meses de 50 acciones sobre una señal de valor, con
   datos mensuales de 15 años (9.000 filas), y un t de 6 con errores clásicos. ¿Qué dos problemas tiene el t y cómo
   lo recalcularías?

**Solución.**

Primero, solape: con rentabilidades a 12 meses observadas cada mes, cada observación comparte 11 meses con la
siguiente, y el t está inflado por cerca de $\sqrt{12} \approx 3{,}5$. Segundo, correlación entre acciones del mismo
mes: las 50 rentabilidades de una fecha se mueven juntas, así que no son 50 datos independientes. Lo recalcularía con
Driscoll-Kraay (`cov_type="hac-groupsum"` con la fecha y al menos 12 retardos) y lo comprobaría con una submuestra
anual sin solape y errores agrupados por fecha. Un t de 6 ingenuo puede quedarse en 1 o 2. Y después preguntaría
cuántas señales se probaron antes de esta (lección 2.1.8).

## Autoevaluación

1. Una estrategia tiene un Sharpe anual verdadero de 1,0 y la pruebas con cuatro años de datos diarios. ¿Qué probabilidad hay de que el contraste t al 5 % la declare significativa?

   - A. Casi el 100 %: un Sharpe de 1 es muy alto
   - B. Alrededor del 50 %: con cuatro años, el estadístico t esperado es 1 × √4 = 2, justo en el umbral
   - C. El 5 %
   - D. El 95 %

   *Respuesta B: Alrededor del 50 %: con cuatro años, el estadístico t esperado es 1 × √4 = 2, justo en el umbral.* El estadístico t de la media es aproximadamente el Sharpe anual por la raíz de los años. Con t esperado de 2 y umbral de 1,96, la mitad de las muestras quedan por debajo. En la simulación de la lección, el 52,8 % de 4.000 muestras rechaza la nula.

2. Con rentabilidades a 21 sesiones calculadas cada día (solapadas), ¿por qué el t de la media sale inflado?

   - A. Porque las rentabilidades a 21 días son más grandes
   - B. Porque dos observaciones consecutivas comparten 20 de sus 21 días: no son independientes, y la fórmula clásica cuenta 21 veces más información de la que hay
   - C. Porque la media de las rentabilidades solapadas está sesgada
   - D. Porque la varianza de las rentabilidades a 21 días es menor

   *Respuesta B: Porque dos observaciones consecutivas comparten 20 de sus 21 días: no son independientes, y la fórmula clásica cuenta 21 veces más información de la que hay.* La media no está sesgada; su error típico sí. La fórmula s/√n supone observaciones independientes; con solape h, la varianza real de la media es unas h veces la que da la fórmula. En las 995 estrategias sin ventaja, el contraste ingenuo da un 65,3 % de falsos positivos.

3. Newey-West con 21 retardos sobre rentabilidades solapadas a 21 sesiones da un 9,4 % de falsos positivos en vez del 5 %. ¿Qué lo explica mejor?

   - A. Que Newey-West está mal programado en statsmodels
   - B. Que los pesos de Bartlett (1 − j/(L+1)) infraponderan las autocovarianzas cercanas al retardo L; con L igual al solape, el error típico se queda algo corto. Con L = 2h baja al 6,8 %
   - C. Que hay que usar 0 retardos
   - D. Que las estrategias sí tenían ventaja

   *Respuesta B: Que los pesos de Bartlett (1 − j/(L+1)) infraponderan las autocovarianzas cercanas al retardo L; con L igual al solape, el error típico se queda algo corto. Con L = 2h baja al 6,8 %.* Con solape h, las autocovarianzas son positivas hasta el retardo h − 1 y Bartlett las pondera cada vez menos. Es un sesgo conocido en muestras finitas; más retardos lo reducen a costa de más varianza del estimador. Muestrear sin solape es la comprobación sencilla.

4. En el panel de 24 futuros, el coeficiente del momentum tiene t = 10,51 con OLS y t = 1,83 con Driscoll-Kraay. ¿Qué cambia entre uno y otro?

   - A. El coeficiente estimado
   - B. Solo el error típico: Driscoll-Kraay suma los productos regresor × residuo de todos los mercados en cada fecha y aplica Newey-West a esa serie, así que tiene en cuenta el solape y que los 24 mercados se mueven a la vez
   - C. El número de observaciones
   - D. La variable dependiente

   *Respuesta B: Solo el error típico: Driscoll-Kraay suma los productos regresor × residuo de todos los mercados en cada fecha y aplica Newey-West a esa serie, así que tiene en cuenta el solape y que los 24 mercados se mueven a la vez.* El estimador es el mismo (0,107). OLS trata las 150.384 filas como independientes; en realidad hay unas 300 rentabilidades mensuales distintas por mercado y los mercados están correlados el mismo día. Con el error honesto, el momentum plantado no llega a t = 2.

5. Entre 995 estrategias sin ventaja, la mejor tiene t = 3,0 en su contraste diario y su intervalo bootstrap de bloques no incluye el cero. ¿Qué conclusión es correcta?

   - A. Tiene ventaja: el bootstrap de bloques es robusto
   - B. Nada: el contraste y el bootstrap son válidos para una estrategia elegida de antemano, no para la mejor de cientos. La selección hay que corregirla aparte (lección 2.1.8)
   - C. Que hay que usar un nivel del 1 %
   - D. Que el bootstrap tiene un error

   *Respuesta B: Nada: el contraste y el bootstrap son válidos para una estrategia elegida de antemano, no para la mejor de cientos. La selección hay que corregirla aparte (lección 2.1.8).* Ningún método de error típico corrige la selección. Con 995 nulas se esperan unas 50 con p < 0,05 y alguna con p < 0,005. La respuesta conocida del conjunto lo confirma: su Sharpe real es 0.

## Ideas clave

- El estadístico t de la media es aproximadamente Sharpe anual × √años: con un Sharpe verdadero de 0,5 harían falta 31 años para tener un 80 % de potencia.
- Bajo la nula los p-valores son uniformes; bajo la alternativa se amontonan cerca de cero, pero con poca potencia muchas estrategias buenas quedan por encima de 0,05.
- El bootstrap i.i.d. y el de bloques coinciden si no hay dependencia serial; con rentabilidades solapadas, solo el de bloques (con bloques más largos que el solape) da el error correcto.
- Las rentabilidades solapadas multiplican el t por cerca de √h: 65,3 % de falsos positivos en 995 estrategias nulas. Newey-West con L ≥ h o muestrear sin solape lo corrige.
- En un panel, la correlación entre activos del mismo día también infla el t: Driscoll-Kraay baja el t del momentum de 10,51 a 1,83 sin tocar el coeficiente, y un efecto que existe deja de ser significativo.
- Ningún error típico corrige la selección entre muchas pruebas: eso es la lección 2.1.8.

## Recursos

### Research

- [Estacionalidad en Python III - Estadisticas sobre la operativa](https://pythonparatrading.com/estacionalidad-intradia-estadisticas/) · Research-0026 · 13 min

### Documentación

- [statsmodels: OLS.fit y sus tipos de covarianza (HAC, cluster, hac-groupsum)](https://www.statsmodels.org/stable/generated/statsmodels.regression.linear_model.OLS.fit.html) · statsmodels.org
- [arch: bootstrap i.i.d., estacionario y por bloques (documentación)](https://arch.readthedocs.io/en/latest/bootstrap/timeseries-bootstraps.html) · arch.readthedocs.io

### Libros

- [Newey, W. K. y West, K. D. (1987): A Simple, Positive Semi-Definite, Heteroskedasticity and Autocorrelation Consistent Covariance Matrix. Econometrica 55(3), 703-708](https://doi.org/10.2307/1913610) · doi.org
- [Politis, D. N. y Romano, J. P. (1994): The Stationary Bootstrap. Journal of the American Statistical Association 89(428), 1303-1313](https://doi.org/10.1080/01621459.1994.10476870) · doi.org
- [Driscoll, J. C. y Kraay, A. C. (1998): Consistent Covariance Matrix Estimation with Spatially Dependent Panel Data. Review of Economics and Statistics 80(4), 549-560](https://doi.org/10.1162/003465398557825) · doi.org

### Datos

- [Datos del nivel 2: 1.000 estrategias sintéticas con su verdad (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/ruido2_estrategias.parquet) · Descarga · ruido2_estrategias.parquet

Lección siguiente: [2.1.2 · Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional](https://pythonparatrading.com/curso-python-trading/nivel-2/estacionariedad-adf-kpss-y-diferenciacion-fraccional/)
