---
title: "1.6.3 Estacionalidad y efectos calendario"
description: "Día de la semana, mes, cambio de mes y perfil intradía con estadísticos t, bootstrap y pruebas múltiples: por qué casi ningún efecto calendario resiste."
url: "https://pythonparatrading.com/curso-python-trading/nivel-1/estacionalidad-y-efectos-calendario/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-1/estacionalidad-y-efectos-calendario.png"
---

# 1.6.3 · Estacionalidad: día de la semana, mes, cambio de mes y sesiones intradía

> Los efectos calendario son la forma más fácil de encontrar un patrón en los datos y una de las más fáciles de engañarse. Esta lección mide los clásicos con estadísticos t e intervalos bootstrap, enseña cuántos «significativos» salen por azar cuando se prueban muchos a la vez y separa lo que sí es estable: el perfil intradía de la volatilidad y el volumen.

- Curso: [Nivel 1: Datos financieros, mercados y análisis](https://pythonparatrading.com/curso-python-trading/nivel-1/)
- Módulo: 1.6 · Análisis exploratorio financiero
- Duración: 60 min
- Requisitos: [0.5.3 · Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles](https://pythonparatrading.com/curso-python-trading/nivel-0/pandas-groupby-merge-resample-y-rolling/), [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/)

## Al terminar esta lección

- Calcular la rentabilidad media por día de la semana, por mes y en el cambio de mes con su estadístico t y su intervalo bootstrap.
- Diseñar bien el contraste: comparar un día con el resto, no con cero.
- Contar cuántos efectos significativos se esperan por azar y aplicar correcciones de Bonferroni, Holm y FDR.
- Medir el perfil intradía de rentabilidad, volatilidad y volumen con barras de 5 minutos en hora de Nueva York.

## Patrones en el calendario

Un **efecto calendario** es una diferencia sistemática de rentabilidad o de volatilidad ligada a una fecha: el día de
la semana, el mes, los días alrededor del cambio de mes, la hora del día, los días antes de un festivo. Son atractivos
porque son fáciles de programar y de explicar. También son la forma más sencilla de encontrar en los datos un patrón
que no existe: hay muchos calendarios posibles, cada uno con pocos datos, y basta con probar suficientes para que
alguno salga «significativo».

Esta lección sigue siempre el mismo flujo:

```mermaid
flowchart TB
  H[Hipótesis previa] --> D[Datos y calendario]
  D --> T[Contraste t o bootstrap]
  T --> M[Corrección múltiple]
  M --> F[Fuera de muestra]
  F --> C[Costes y decisión]
```

Los datos son ISX y FIX, los dos índices diarios del nivel 1 (2019-2025), los 57 valores del universo de la lección
1.4.4 y las barras de 5 minutos de NOVA de 2024 y 2025. Una advertencia previa, como en la lección 1.6.2: son datos
sintéticos y su generador **no** incluye efectos de día de la semana, mes ni cambio de mes. Lo que salga en esas
pruebas es azar o un efecto lateral del modelo, y por eso son un buen banco de pruebas: sabemos la respuesta correcta.
El perfil intradía de la volatilidad y del volumen, en cambio, sí está en el generador.

## Día de la semana

El estadístico t de una media es la media dividida por su error típico:

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

Con unos cientos de datos, |t| > 1,96 corresponde a p < 0,05 (dos colas).

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

isx = pd.read_csv("etf_indice_diario.csv", parse_dates=["fecha"],
                  index_col="fecha")["indice"]
fix = pd.read_csv("fix_indice_diario.csv", parse_dates=["fecha"],
                  index_col="fecha")["indice"]
px = pd.DataFrame({"ISX": isx, "FIX": fix})
r = np.log(px).diff().dropna()
DIAS = ["lun", "mar", "mié", "jue", "vie"]

def por_grupo(x, grupos):
    g = x.groupby(grupos)
    out = pd.DataFrame({"n": g.size(), "media_%": 100 * g.mean()})
    out["t"] = g.mean() / (g.std() / np.sqrt(g.size()))
    return out

tablas = {c: por_grupo(r[c], r.index.dayofweek) for c in r}
dia = pd.concat(tablas, axis=1)
dia.index = DIAS
dia.round(3)
```
```output
     ISX                 FIX
       n media_%      t    n media_%      t
lun  329   0.040  0.527  329   0.082  1.212
mar  364   0.032  0.414  364  -0.017 -0.231
mié  360   0.101  1.505  360   0.173  2.669
jue  353   0.044  0.645  353   0.005  0.080
vie  353   0.037  0.595  353   0.047  0.725
```

Todas las medias son pequeñas y casi todos los t, menores que 2 en valor absoluto. La excepción es el miércoles de FIX:
0,17 % de media y t = 2,67. Antes de celebrarlo, dos cuentas. Son diez contrastes (dos índices por cinco días): con
diez contrastes independientes al 5 %, la probabilidad de que al menos uno salga significativo por azar es
1 − 0,95¹⁰ ≈ 40 %. Y el contraste está mal planteado.

### Contra el resto, no contra cero

El t de la tabla contrasta «la media del miércoles es cero». Pero un índice que sube un 14 % al año tiene una media
diaria positiva todos los días: la pregunta de un efecto calendario es si el miércoles **se diferencia de los demás
días**. Con un contraste de Welch (medias de dos grupos con varianzas distintas):

```python
def frente_al_resto(x, grupos):
    filas = {}
    for g in np.unique(grupos):
        dentro, fuera = x[grupos == g], x[grupos != g]
        t, p = stats.ttest_ind(dentro, fuera, equal_var=False)
        filas[g] = {"dif_%": 100 * (dentro.mean() - fuera.mean()),
                    "t": t, "p": p}
    return pd.DataFrame(filas).T

res = frente_al_resto(r["FIX"], r.index.dayofweek)
res.index = DIAS
res.round(3)
```
```output
     dif_%      t      p
lun  0.030  0.393  0.695
mar -0.094 -1.173  0.241
mié  0.145  1.981  0.048
jue -0.066 -0.890  0.374
vie -0.013 -0.181  0.856
```

Frente al resto de días, el miércoles de FIX se queda en t = 1,98 y p = 0,048: justo por debajo de 0,05 y el único de
los cinco. Con la hipótesis bien planteada el resultado se debilita pero no desaparece, así que la pregunta pasa a ser
la otra: ¿es esperable un
resultado así entre todos los que hemos mirado? Se responde más abajo, con muchos más contrastes.

## Mes del año: un dato por año

Para el efecto mes, cada observación es la rentabilidad de un mes completo: con siete años hay siete eneros, siete
febreros… Un intervalo bootstrap (remuestrear con reemplazo los siete valores y recalcular la media miles de veces)
enseña lo poco que dicen:

```python
mens = np.log(px.resample("ME").last()).diff().dropna()["ISX"]
rng = np.random.default_rng(42)
filas = {}
for mes, x in mens.groupby(mens.index.month):
    ic = stats.bootstrap((x.to_numpy(),), np.mean, n_resamples=4000,
                         method="percentile", rng=rng)
    filas[mes] = {"n": len(x), "media_%": 100 * x.mean(),
                  "ic_bajo_%": 100 * ic.confidence_interval.low,
                  "ic_alto_%": 100 * ic.confidence_interval.high,
                  "t": x.mean() / (x.std() / np.sqrt(len(x)))}
meses = pd.DataFrame(filas).T
meses.round(2)
```
```output
      n  media_%  ic_bajo_%  ic_alto_%     t
1   6.0     2.84      -0.21       5.60  1.73
2   7.0     0.34      -0.87       1.71  0.46
3   7.0    -5.53     -18.92       6.68 -0.74
4   7.0     0.45      -4.68       5.86  0.15
5   7.0     5.19      -0.04      10.90  1.65
6   7.0     5.97       1.82      10.61  2.45
7   7.0     0.16      -4.76       4.91  0.06
8   7.0    -0.88      -3.68       2.03 -0.56
9   7.0    -0.44      -3.83       2.52 -0.26
10  7.0     2.90       0.69       5.40  2.21
11  7.0     1.01      -0.73       2.97  0.99
12  7.0     1.10      -0.84       3.06  1.02
```

*Fig. — ISX, 2019–2025: rentabilidad media de cada mes del año con su intervalo bootstrap del 95 % (siete años, seis eneros).* Doce cuadrados con la rentabilidad media de cada mes y una barra vertical con su intervalo bootstrap. Las medias van del −5,5 % de marzo al +6,0 % de junio. El intervalo de marzo, destacado, va del −18,9 % al +6,7 % por el desplome de 2020; casi todos los demás incluyen el cero, salvo los de junio y octubre, por poco.

Las medias van del −5,5 % de marzo al +6,0 % de junio y los intervalos son enormes: el de marzo va del −18,9 % al
+6,7 %. Marzo es negativo por el desplome de 2020, un solo año. Junio y octubre salen con t mayor que 2 y con
intervalos que no incluyen el cero, pero con siete datos cada uno y doce meses probados. En estos datos, además, se sabe por qué: el generador fija las grandes
subidas y caídas en fechas concretas (marzo de 2020, la primavera de 2025), y eso deja huella en los meses en que
cayeron. Es un efecto de la historia, no del calendario. Con datos reales pasa lo mismo: la mayoría de los «efectos
mes» de un activo con diez o veinte años de historia son la huella de unos pocos acontecimientos.

## El cambio de mes

El efecto cambio de mes (*turn of the month*) dice que las rentabilidades se concentran en el último día de
negociación de un mes y los primeros días del siguiente. Una definición habitual: el último día y los tres primeros.

```python
mes = r.index.to_period("M")
orden = pd.Series(1, index=r.index).groupby(mes).cumsum()
total = pd.Series(1, index=r.index).groupby(mes).transform("sum")
cambio = (orden <= 3) | (orden == total)
print(f"Sesiones de cambio de mes: {cambio.sum()} de {len(r)}")
for c in r:
    a, b = r.loc[cambio, c], r.loc[~cambio, c]
    t, p = stats.ttest_ind(a, b, equal_var=False)
    print(f"{c}: {100 * a.mean():.3f} % frente a "
          f"{100 * b.mean():.3f} %  (t = {t:.2f}, p = {p:.3f})")
```
```output
Sesiones de cambio de mes: 336 de 1759
ISX: 0.096 % frente a 0.040 %  (t = 0.78, p = 0.434)
FIX: 0.054 % frente a 0.059 %  (t = -0.07, p = 0.947)
```

En ISX las sesiones de cambio de mes rinden más de media que el resto, pero con t = 0,78; en FIX, prácticamente lo
mismo. Nada. Fíjate en la definición: «último día y tres primeros» es una de muchas posibles (dos y dos, uno y
cuatro…). Probar varias y quedarse con la mejor es otra forma de multiplicar los contrastes, aunque no se vea en el
código final.

## Pruebas múltiples: cuántos «significativos» da el azar

Si no hay ningún efecto, el p-valor de un contraste bien hecho se distribuye de forma uniforme entre 0 y 1: un 5 % de
los contrastes sale por debajo de 0,05 por puro azar. Con los 57 valores del universo y cinco días de la semana hay
285 contrastes:

```python
from statsmodels.stats.multitest import multipletests

uni = pd.read_parquet("universo_precios.parquet").pivot(
    index="fecha", columns="ticker", values="cierre")
ru = np.log(uni).diff()
filas = []
for tk in ru:
    s = ru[tk].dropna()
    dias = s.index.dayofweek
    for d in range(5):
        t, p = stats.ttest_ind(s[dias == d], s[dias != d],
                               equal_var=False)
        filas.append({"ticker": tk, "dia": DIAS[d], "t": t, "p": p})
pr = pd.DataFrame(filas)
print(f"Contrastes: {len(pr)}; esperados con p < 0,05: "
      f"{0.05 * len(pr):.1f}; observados: {(pr['p'] < 0.05).sum()}")
for metodo in ["bonferroni", "holm", "fdr_bh"]:
    rechaza = multipletests(pr["p"], alpha=0.05, method=metodo)[0]
    print(f"{metodo:>10}: {rechaza.sum()} significativos")
pr.sort_values("p").head(3).round(4)
```
```output
Contrastes: 285; esperados con p < 0,05: 14.2; observados: 10
bonferroni: 0 significativos
      holm: 0 significativos
    fdr_bh: 0 significativos
    ticker  dia       t       p
127   ZXLC  mié  2.9605  0.0032
20    ZXBI  lun  2.7957  0.0055
93    ZXHX  jue -2.7190  0.0067
```

*Fig. — Histograma de los p-valores de 285 contrastes de día de la semana (57 valores × 5 días, cada día frente al resto).* Veinte barras de anchura 0,05 entre 0 y 1, de alturas entre 10 y 21 contrastes, alrededor de una línea de 14,25 que marca lo esperado si no hay ningún efecto. La primera barra, destacada, con p < 0,05, tiene 10 contrastes: menos que lo esperado. El histograma es plano, la firma de que no hay efecto.

Diez contrastes por debajo de 0,05, menos de los catorce que daría el azar, y ninguno sobrevive a una corrección. El
histograma de los 285 p-valores es plano: la firma de que no hay nada. Las tres correcciones responden a preguntas
distintas:

| Método | Qué controla | Umbral efectivo aquí |
|---|---|---|
| Bonferroni | Probabilidad de **algún** falso positivo | p < 0,05 / 285 ≈ 0,00018 |
| Holm | Lo mismo, de forma escalonada y algo menos estricta | De 0,05 / 285 a 0,05 |
| Benjamini-Hochberg (FDR) | Proporción esperada de falsos positivos entre los rechazos | Depende de cuántos p-valores pequeños haya |

Si los miércoles de FIX formaran parte de una búsqueda así, su p = 0,048 no pasaría ninguna. La lección 2.1.8 vuelve
sobre esto con el «zoo de factores»: la literatura de efectos calendario y de anomalías tiene el mismo problema a
escala de miles de artículos.

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

El contraste mal planteado del principio engaña en el universo de forma espectacular. Si se compara la media de cada
día con cero en lugar de con el resto, un valor que quiebra (ZXIR, que pierde un 1 % de media cada sesión hasta su
deslistado) da un «efecto lunes» con t = −4,3 que sobrevive incluso a Bonferroni. No es un efecto lunes: es una acción
que cae todos los días. Lo comprueba el ejercicio 1.

</aside>

## Sesiones intradía

El calendario más fino es el del reloj. Con las barras de 5 minutos de NOVA (502 sesiones de 2024 y 2025), en hora de
Nueva York, se puede calcular por franja la rentabilidad media, su t, la volatilidad y el volumen:

```python
b = pd.read_parquet("nova_barras_5min.parquet")
ny = b["ts"].dt.tz_convert("America/New_York")
b["franja"] = ny.dt.strftime("%H:%M")
b["r"] = np.log(b["cierre"] / b["apertura"])
g = b.groupby("franja")
perfil = pd.DataFrame({
    "n": g.size(), "media_pb": 1e4 * g["r"].mean(),
    "vol_pb": 1e4 * g["r"].std(),
    "volumen_miles": g["volumen"].mean() / 1e3})
perfil["t"] = perfil["media_pb"] / (perfil["vol_pb"]
                                    / np.sqrt(perfil["n"]))
print(f"Franjas: {len(perfil)}; |t| > 1,96: "
      f"{(perfil['t'].abs() > 1.96).sum()}")
perfil.iloc[[0, 1, 2, 30, 31, 76, 77]].round(1)
```
```output
Franjas: 78; |t| > 1,96: 2
          n  media_pb  vol_pb  volumen_miles    t
franja
09:30   502       2.1    47.8          351.6  1.0
09:35   502       2.1    42.2          282.4  1.1
09:40   502      -0.1    36.3          236.8 -0.0
12:00   502      -0.4    14.9          115.5 -0.6
12:05   502      -0.1    15.8          118.3 -0.2
15:50   496       0.5    29.2          205.2  0.4
15:55   496       2.2    33.2         1277.8  1.5
```

La figura de la cabecera dibuja las dos columnas que importan. La volatilidad de la primera barra (09:30) es unas tres
veces la de mediodía y la de la última barra vuelve a subir; el volumen hace la misma U y en la barra de las 15:55 se
dispara, porque incluye la subasta de cierre (lección 1.1.2). Las franjas desde las 13:00 tienen 496 sesiones en lugar
de 502: en las seis medias sesiones del periodo el mercado cierra a esa hora.

Las rentabilidades medias, en cambio, son ruido: 2 franjas de 78 con |t| > 1,96, cuando el azar daría unas 4.

```python
perfil.loc[perfil["t"].abs() > 1.96, ["n", "media_pb", "t"]].round(2)
```
```output
          n  media_pb     t
franja
13:40   496      1.76  2.43
15:35   496     -2.56 -2.07
```

*Fig. — NOVA, 2024–2025: estadístico t de la rentabilidad media de cada barra de 5 minutos; la banda marca |t| < 1,96.* 78 barras de estadísticos t entre −2,1 y +2,4 alrededor de cero, casi todas dentro de la banda sombreada de ±1,96. Solo dos salen, destacadas: 13:40, con t = 2,43, y 15:35, con t = −2,07. Por azar se esperarían unas cuatro.

Una regla del tipo «comprar a las 13:40 y vender a las 13:45» sale de mirar 78 franjas y quedarse con la mejor. Con 1,8
puntos básicos de media en 5 minutos y una horquilla de un céntimo en una acción de unos 50 USD (2 puntos básicos de ida y
vuelta solo en la horquilla), ni siquiera sería rentable si fuera real.

Lo que sí sirve del perfil intradía es su forma. ¿Qué parte de la varianza de un día se concentra en cada tramo?

```python
def tramo(h):
    if h < "10:00":
        return "1 apertura 9:30-10:00"
    if h >= "15:30":
        return "3 cierre 15:30-16:00"
    return "2 resto 10:00-15:30"

completas = b.groupby(ny.dt.date)["franja"].transform("size") == 78
bc = b[completas]
var = bc.groupby(bc["franja"].map(tramo))["r"].apply(
    lambda x: (x ** 2).sum())
reparto = pd.DataFrame({"barras": bc.groupby(
    bc["franja"].map(tramo)).size() / completas.sum() * 78,
    "varianza_%": 100 * var / var.sum()})
reparto.round(1)
```
```output
                       barras  varianza_%
franja
1 apertura 9:30-10:00     6.0        23.9
2 resto 10:00-15:30      66.0        61.1
3 cierre 15:30-16:00      6.0        15.0
```

Las seis barras de la primera media hora y las seis de la última (un 15 % del tiempo de la sesión) concentran el
38,9 % de la varianza del día: el 23,9 % la apertura y el 15,0 % el cierre. Para un sistema tiene tres usos directos: repartir
una orden grande siguiendo el perfil de volumen (como hace un algoritmo VWAP, lección 1.7.1), poner stops más amplios
en la apertura que a mediodía y normalizar cualquier señal intradía por la volatilidad típica de su franja antes de
compararla con otras horas.

## Cómo no engañarse con el calendario

- **Hipótesis antes que datos.** Escribe qué efecto vas a probar, con qué definición y en qué periodo, antes de mirar.
  La lección 2.2.4 propone un diario de investigación para eso.
- **El contraste correcto.** Un día frente al resto, un mes frente al resto; no frente a cero.
- **Cuenta los contrastes.** Los que hiciste y los que descartaste mentalmente (otras definiciones, otros activos,
  otros periodos). Corrige por todos.
- **Fuera de la muestra y con costes.** Un efecto que sobrevive en un periodo que no usaste para encontrarlo y que paga
  la horquilla y las comisiones es otra cosa. Los research del sitio sobre estacionalidad intradía en futuros siguen
  ese camino hasta el backtest y sus estadísticas.

## Ejercicios

1. Repite los 285 contrastes del universo comparando la media de cada día con **cero** (`stats.ttest_1samp`) en lugar
   de con el resto de días. ¿Cuántos salen con p < 0,05 y cuántos sobreviven a Bonferroni? ¿Qué valores son?

**Solución.**

```python
filas = []
for tk in ru:
    s = ru[tk].dropna()
    for d in range(5):
        x = s[s.index.dayofweek == d]
        t, p = stats.ttest_1samp(x, 0.0)
        filas.append({"ticker": tk, "dia": DIAS[d], "t": t, "p": p})
p0 = pd.DataFrame(filas)
bonf = multipletests(p0["p"], alpha=0.05, method="bonferroni")[0]
print(f"p < 0,05: {(p0['p'] < 0.05).sum()};"
      f" tras Bonferroni: {bonf.sum()}")
p0[bonf].round(5)
```
```output
p < 0,05: 23; tras Bonferroni: 1
    ticker  dia        t        p
100   ZXIR  lun -4.27665  0.00006
```

Salen bastantes más que con el contraste correcto, porque los valores con una tendencia fuerte (al alza o a la baja)
dan medias distintas de cero en todos los días. El que sobrevive a Bonferroni es ZXIR, el valor que quiebra en 2016,
con un lunes muy negativo: refleja su caída continua, no un efecto del lunes.

2. Calcula la volatilidad por franja de NOVA solo con 2024 y solo con 2025 y la correlación entre los dos perfiles.
   ¿Es estable la forma?

**Solución.**

```python
ano = ny.dt.year
v24 = b[ano == 2024].groupby("franja")["r"].std()
v25 = b[ano == 2025].groupby("franja")["r"].std()
print(f"Correlación de los perfiles: {v24.corr(v25):.3f}")
print(f"Apertura/mediodía 2024: {v24['09:30'] / v24['12:00']:.2f}")
print(f"Apertura/mediodía 2025: {v25['09:30'] / v25['12:00']:.2f}")
```
```output
Correlación de los perfiles: 0.974
Apertura/mediodía 2024: 3.12
Apertura/mediodía 2025: 3.26
```

La forma se repite casi idéntica de un año a otro: la correlación entre los dos perfiles es muy alta y la relación
entre la apertura y el mediodía se mantiene. Es la diferencia con las medias de rentabilidad por franja, que cambian de
signo de un año a otro: la forma de la volatilidad es una propiedad del mercado; la media de una franja, una muestra.

3. Un artículo afirma que «el mercado sube en diciembre» y lo apoya con 30 años de datos: 21 diciembres positivos de
   30. ¿Qué más necesitas para tomarlo en serio?

**Solución.**

Comparar con el resto de meses (si el mercado sube en el 65 % de los meses, 21 de 30 no es nada especial), el
tamaño y la variabilidad de la diferencia (un t o un intervalo bootstrap), cuántos meses y definiciones se probaron
antes de elegir diciembre, si el efecto aparece en un periodo distinto del que se usó para descubrirlo y en otros
mercados, y si paga los costes de entrar y salir. Y una explicación económica plausible (flujos de fin de año,
fiscalidad) que se pueda contrastar por separado.

## Autoevaluación

1. La rentabilidad media de FIX los miércoles tiene un estadístico t de 2,67. ¿Qué concluyes?

   - A. Que hay un efecto miércoles en FIX y conviene comprar los martes al cierre
   - B. Nada todavía: es uno de diez contrastes (dos índices por cinco días), con uno o dos significativos esperables por azar, y en estos datos el generador no tiene efecto día de la semana
   - C. Que el miércoles es el día más volátil
   - D. Que los datos tienen un error

   *Respuesta B: Nada todavía: es uno de diez contrastes (dos índices por cinco días), con uno o dos significativos esperables por azar, y en estos datos el generador no tiene efecto día de la semana.* Con diez contrastes al 5 %, la probabilidad de que al menos uno salga significativo por azar es de un 40 %. Un efecto así hay que contrastarlo con una hipótesis previa, con corrección por pruebas múltiples y fuera de la muestra.

2. Pruebas el efecto día de la semana en 57 valores: 285 contrastes. ¿Cuántos esperas con p < 0,05 si no hay ningún efecto?

   - A. Ninguno
   - B. Unos 14
   - C. Unos 57
   - D. Unos 285

   *Respuesta B: Unos 14.* El 5 % de 285 son 14,25. En la lección salen 10: menos que los esperados por azar, y ninguno sobrevive a Bonferroni, Holm o FDR.

3. Un valor que quiebra cae un 1 % de media cada día. Si contrastas «la rentabilidad media del lunes es cero», ¿qué pasa?

   - A. El lunes sale significativamente negativo, aunque no haya efecto lunes: el contraste detecta la tendencia del valor, no el día
   - B. No sale nada
   - C. Sale significativo el viernes
   - D. El contraste es correcto

   *Respuesta A: El lunes sale significativamente negativo, aunque no haya efecto lunes: el contraste detecta la tendencia del valor, no el día.* La hipótesis nula correcta para un efecto calendario es «el lunes no se diferencia del resto de días». Comparar con cero confunde la deriva del activo con el día de la semana.

4. ¿Qué patrón intradía de NOVA es estable y útil para un sistema?

   - A. Que la barra de las 13:40 sube
   - B. Que la volatilidad y el volumen siguen una U: altos en la apertura y el cierre, bajos a mediodía
   - C. Que el viernes por la tarde baja
   - D. Ninguno

   *Respuesta B: Que la volatilidad y el volumen siguen una U: altos en la apertura y el cierre, bajos a mediodía.* Las medias de rentabilidad por franja son ruido (2 de 78 franjas significativas, menos de las que daría el azar). La forma de la volatilidad y del volumen, en cambio, es enorme y persistente: sirve para repartir la ejecución, fijar stops y normalizar señales.

5. ¿Por qué la rentabilidad media de marzo de ISX (−5,5 %) no es un efecto estacional?

   - A. Porque marzo tiene menos sesiones
   - B. Porque con siete marzos, uno solo (el desplome de 2020) domina la media y el intervalo bootstrap incluye el cero
   - C. Porque marzo siempre baja
   - D. Porque es una media logarítmica

   *Respuesta B: Porque con siete marzos, uno solo (el desplome de 2020) domina la media y el intervalo bootstrap incluye el cero.* Un efecto mensual se mide con un dato por año. Con siete años, la media de un mes es la de siete números y un acontecimiento la mueve entera.

## Ideas clave

- Un efecto calendario se contrasta frente a los demás días o meses, no frente a cero, y con su estadístico t o un intervalo bootstrap.
- Con muchos contrastes, los significativos por azar son la norma: 285 contrastes al 5 % dan unos 14. Corrige con Bonferroni, Holm o FDR y valida fuera de la muestra (lección 2.1.8).
- Un efecto mensual tiene un dato por año: con siete años, un solo acontecimiento decide la media.
- En estos datos ningún efecto de día de la semana, mes o cambio de mes resiste; el histograma de p-valores es plano, la firma de que no hay nada.
- Lo que sí es estable es el perfil intradía de la volatilidad y del volumen: una U con la apertura y el cierre muy por encima del mediodía.

## Recursos

### Research

- [Estacionalidad en Python. Buscando Patrones Intradia](https://pythonparatrading.com/estacionalidad-intradia-preprocesado/) · Research-0024 · 9 min
- [Estacionalidad en Python II - Backtesting](https://pythonparatrading.com/estacionalidad-intradia-backtesting/) · Research-0025 · 8 min
- [Estacionalidad Intradiaria IV - Busqueda de campo](https://pythonparatrading.com/estacionalidad-intradia-optimizacion/) · Research-0027 · 7 min

### Documentación

- [SciPy: scipy.stats.bootstrap](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.bootstrap.html) · docs.scipy.org
- [statsmodels: multipletests (Bonferroni, Holm, FDR)](https://www.statsmodels.org/stable/generated/statsmodels.stats.multitest.multipletests.html) · statsmodels.org

### Datos

- [Datos del nivel 1: barras de 5 minutos de NOVA, 2024-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-1/nova_barras_5min.parquet) · Descarga · nova_barras_5min.parquet

Lección anterior: [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/)

Lección siguiente: [1.7.1 · Indicadores técnicos en Python](https://pythonparatrading.com/curso-python-trading/nivel-1/indicadores-tecnicos-en-python/)
