Lección 1.7.4 · 60 min
Multi-timeframe sin anticipación
Mezclar marcos temporales es la forma más fácil de meter el futuro en un backtest sin darse cuenta: una barra de las 10:00 que «conoce» el cierre del día convierte una regla mediocre en una excelente. Esta lección reproduce los tres errores clásicos con NOVA, mide cuánto inflan el resultado y fija la regla que los evita: cada dato se une por el instante en que termina su barra.
- Módulo
- 1.7 · Análisis técnico programado
- Duración
- 60 min
Lecciones del nivel 1
1.1 · Cómo funcionan los mercados
1.2 · Clases de activos
- 1.2.1 Acciones: splits, dividendos y eventos corporativos
- 1.2.2 ETFs: réplica, tracking error, apalancados y UCITS
- 1.2.3 Futuros: especificaciones, margen, roll y contango
- 1.2.4 Forex: pares, pips, lotes, rollover y CFD
- 1.2.5 Opciones: call, put, valor intrínseco y valor temporal
- 1.2.6 Cripto: spot, perpetuos, funding y custodia
- 1.2.7 Renta fija y tipos de interés; CFDs y productos apalancados
1.3 · Fuentes de datos de mercado
- 1.3.1 Tipos de datos de mercado: EOD, barras, ticks, libro y referencia
- 1.3.2 Fuentes de datos gratuitas
- 1.3.3 APIs de datos retail y prosumer
- 1.3.4 Proveedores de datos profesionales
- 1.3.5 Datos de cripto y on-chain
- 1.3.6 Brókeres como fuente de datos
- 1.3.7 Licencias y términos de uso de datos
- 1.3.8 Cómo elegir proveedor de datos
1.4 · Ingeniería de datos financieros
- 1.4.1 Descarga por REST y websockets: paginación, límites y reintentos
- 1.4.2 Limpieza de datos: faltantes, ticks erróneos, huecos y zonas horarias
- 1.4.3 Ajustes por splits y dividendos: precios ajustados y sin ajustar
- 1.4.4 Universos sin sesgo de supervivencia: deslistados, fusiones y cambios de símbolo
- 1.4.5 Construcción de barras: tiempo, ticks, volumen, dólar y desequilibrio
- 1.4.6 Series continuas de futuros: reglas de roll, ajuste por diferencia y ajuste por ratio
- 1.4.7 Almacenamiento: CSV, Parquet, particionado, DuckDB, TimescaleDB y ArcticDB
- 1.4.8 Pipeline reproducible: descarga, validación, actualización incremental y tests de calidad
1.5 · Datos fundamentales y macro
1.6 · Análisis exploratorio financiero
1.7 · Análisis técnico programado
El problema: dos relojes en la misma tabla
Un sistema multi-timeframe toma decisiones en un marco rápido (barras de 5 minutos) con información de uno lento (el diario, el semanal): «compra en 5 minutos solo si la tendencia diaria es alcista». La tentación es unir las dos tablas por la fecha. El problema es que la fila diaria del 4 de junio lleva el cierre del 4 de junio, que no se conoce hasta las 16:00, y al unir por fecha aparece en todas las barras del 4 de junio, también en la de las 9:30.
La figura de la cabecera lo dibuja con tres sesiones de NOVA. La línea gris es el precio de 5 minutos. El escalón correcto es el último cierre diario conocido en cada barra: el de ayer hasta las 16:00, el de hoy después. El erróneo es el cierre del día unido por fecha: a las 9:30 del 4 de junio ya «sabe» que NOVA cerrará a 55,14, un 2,7 % por encima de la apertura.
La regla que evita el problema es una sola:
Fuente Mermaid
flowchart LR L[Barra lenta] --> F[Hora de cierre] F --> A[merge_asof atrás] R[Barra rápida] --> C[Hora de cierre] C --> A A --> D[Decisión]
Cada dato se une por el instante en que termina su barra, y una decisión tomada en el instante solo ve los datos que terminaron antes de . La fecha, la etiqueta o el nombre de la barra no cuentan.
Barras de 5 minutos y barras diarias
Las barras de 5 minutos de NOVA (nova_barras_5min.parquet) cubren las 502 sesiones de la NYSE de 2024 y 2025. Su
marca ts es el inicio de la barra, en UTC. Para trabajar hacen falta tres columnas más: la hora de Nueva York,
la sesión a la que pertenece la barra y la hora a la que termina:
import numpy as np
import pandas as pd
b = pd.read_parquet("nova_barras_5min.parquet")
b["ny"] = b["ts"].dt.tz_convert("America/New_York")
b["sesion"] = b["ny"].dt.normalize().dt.tz_localize(None)
b["t_cierre"] = b["ts"] + pd.Timedelta("5min")
print(len(b), "barras,", b["sesion"].nunique(), "sesiones")
b[["ts", "ny", "t_cierre", "cierre"]].head(3)38940 barras, 502 sesiones
ts ny t_cierre cierre
0 2024-01-02 14:30:00+00:00 2024-01-02 09:30:00-05:00 2024-01-02 14:35:00+00:00 43.46
1 2024-01-02 14:35:00+00:00 2024-01-02 09:35:00-05:00 2024-01-02 14:40:00+00:00 43.28
2 2024-01-02 14:40:00+00:00 2024-01-02 09:40:00-05:00 2024-01-02 14:45:00+00:00 43.24Las barras diarias se construyen agregando por sesión. La columna fin guarda la hora de cierre de la última barra
de cada sesión: el instante en que el dato diario existe.
d = b.groupby("sesion").agg(o=("apertura", "first"),
h=("maximo", "max"), l=("minimo", "min"),
c=("cierre", "last"), v=("volumen", "sum"),
fin=("t_cierre", "last"))
csv = pd.read_csv("nova_diario_sin_ajustar.csv", parse_dates=["fecha"],
index_col="fecha").loc["2024":"2025"]
igual = (np.allclose(d["c"], csv["cierre"])
and (d["v"] == csv["volumen"]).all())
print("Igual que el CSV diario:", igual)
print(d["fin"].dt.tz_convert("America/New_York").dt.strftime("%H:%M")
.value_counts())Igual que el CSV diario: True
fin
16:00 496
13:00 6
Name: count, dtype: int64Las barras diarias reconstruidas coinciden con el CSV diario. Y fin ya enseña por qué la fecha no basta: 496
sesiones terminan a las 16:00 de Nueva York, pero seis medias sesiones (3 de julio, viernes después de Acción de
Gracias, 24 de diciembre) terminan a las 13:00. En UTC, además, las 16:00 son las 21:00 en invierno y las 20:00 en
verano.
Error 1: el indicador diario unido por fecha
La regla de ejemplo es un filtro de tendencia: a las 10:00 (cierre de la barra de las 9:55), si el cierre diario está
por encima de su SMA de 20 sesiones, comprar y vender al cierre de la sesión. La versión errónea une por fecha; la
correcta, con merge_asof por hora de disponibilidad, sin coincidencias exactas:
d["sma20"] = d["c"].rolling(20).mean()
diario = d[["c", "sma20"]].assign(disponible=d["fin"])
# MAL: cada barra ve la fila diaria de su propia fecha
por_fecha = b.merge(diario, left_on="sesion", right_index=True)
# BIEN: cada barra ve la última fila diaria que terminó antes
por_hora = pd.merge_asof(b.sort_values("t_cierre"),
diario.sort_values("disponible"),
left_on="t_cierre", right_on="disponible",
allow_exact_matches=False)
def filtro_diario(barras):
a_las_10 = barras[barras["ny"].dt.strftime("%H:%M") == "09:55"]
cierre_dia = a_las_10["sesion"].map(d["c"])
r = cierre_dia / a_las_10["cierre"] - 1
activo = a_las_10["c"] > a_las_10["sma20"]
return {"dias_activo_pct": 100 * activo.mean(),
"anual_pct": 100 * r.where(activo, 0.0).mean() * 252,
"aciertos_pct": 100 * (r[activo] > 0).mean()}
pd.DataFrame({"por fecha (mal)": filtro_diario(por_fecha),
"merge_asof (bien)": filtro_diario(por_hora)}).T.round(1) dias_activo_pct anual_pct aciertos_pct
por fecha (mal) 60.2 40.5 59.9
merge_asof (bien) 60.0 4.4 52.8Con el error, el filtro está activo casi los mismos días (60,2 % frente a 60,0 %), pero los elige mucho mejor: un 40,5 % anual y un 59,9 % de aciertos, frente a un 4,4 % y un 52,8 % con la alineación correcta. La diferencia es información del futuro: en los días en que NOVA cierra fuerte, el cierre de hoy supera la media, el filtro se activa y la operación de 10:00 a 16:00 gana. Nada de eso se podía saber a las 10:00.
Lo que ve cada versión en la primera barra de una sesión:
primera = lambda x: x[x["ny"] == pd.Timestamp("2025-06-04 09:30",
tz="America/New_York")]
pd.concat([primera(por_fecha).assign(union="por fecha"),
primera(por_hora).assign(union="merge_asof")]
)[["union", "ny", "cierre", "c", "disponible"]] union ny cierre c disponible
27660 por fecha 2025-06-04 09:30:00-04:00 54.24 55.14 2025-06-04 20:00:00+00:00
27660 merge_asof 2025-06-04 09:30:00-04:00 54.24 53.00 2025-06-03 20:00:00+00:00La unión por fecha le da a la barra de las 9:30 un cierre diario de 55,14 que no estará disponible hasta las 20:00
UTC de ese día. merge_asof le da el del 3 de junio, 53,00, disponible desde la víspera.
La alternativa clásica es desplazar la tabla diaria una sesión antes de unir por fecha: d.shift(1). Da lo
mismo… si las sesiones son contiguas y no falta ninguna:
desplazado = b.merge(diario[["c", "sma20"]].shift(1),
left_on="sesion", right_index=True)
iguales = np.allclose(desplazado.sort_values("t_cierre")["c"],
por_hora["c"], equal_nan=True)
print("Mismos valores que merge_asof:", iguales)Mismos valores que merge_asof: TrueCoinciden. Pero shift(1) desplaza filas, no tiempo: si falta una sesión en los datos diarios (un día que el
proveedor no envió, lección 1.4.2), la barra del día siguiente recibe el dato de hace dos sesiones sin avisar; y si
el marco lento no es una partición limpia del rápido (semanas con festivo, barras de 4 horas en un mercado con
pausas), no hay «fila anterior» que tenga sentido. merge_asof sobre la hora de disponibilidad funciona en todos los
casos, y deja escrita la suposición.
Error 2: la etiqueta de resample
Para pasar de barras de 5 minutos a horarias, resample necesita dos decisiones: qué borde del intervalo es cerrado
(closed) y con qué borde se etiqueta (label). Por defecto, en frecuencias de minutos y horas, los dos son
'left': el intervalo contiene las barras con marca de 9:30 a 10:25 y se etiqueta 9:30. Con
marcas de inicio (las de NOVA), es lo correcto. offset="30min" alinea los intervalos con la apertura:
s = b.set_index("ts")
agg = {"apertura": "first", "maximo": "max", "minimo": "min",
"cierre": "last", "volumen": "sum"}
izq = s.resample("1h", offset="30min").agg(agg)
der = s.resample("1h", offset="30min", closed="right").agg(agg)
izq, der = izq.dropna(subset=["cierre"]), der.dropna(subset=["cierre"])
def barras_por_hora(**kw):
n = s["cierre"].resample("1h", offset="30min", **kw).count()
return n[n > 0].value_counts().to_dict()
print("closed='left': ", barras_por_hora())
print("closed='right':", barras_por_hora(closed="right"))
der.head(3)closed='left': {12: 2994, 6: 502}
closed='right': {12: 2994, 1: 502, 5: 502}
apertura maximo minimo cierre volumen
ts
2024-01-02 13:30:00+00:00 43.36 43.53 43.36 43.46 298380
2024-01-02 14:30:00+00:00 43.46 43.58 43.19 43.50 1215459
2024-01-02 15:30:00+00:00 43.50 43.55 43.36 43.45 749578Con closed='left', cada sesión tiene seis horas de 12 barras y una media hora final de 6 (15:30 a 16:00). Con
closed='right', la barra de las 9:30 queda sola en el intervalo : una «hora» de una barra, con
la apertura de la sesión. Todas las demás se desplazan cinco minutos y la última hora queda en 5 barras. Para barras
marcadas al final (algunos proveedores lo hacen: la barra de 9:30 a 9:35 se marca 9:35) la combinación correcta
es justo la contraria, closed='right' y label='right'.
Fuente Mermaid
flowchart TB
Q{¿Marca de inicio?}
Q -- sí --> L[closed left]
Q -- no --> R[closed right]
L --> D[Disponible: fin]
R --> Dlabel no cambia qué barras entran en cada intervalo, pero sí dónde se une la barra horaria si se une por su
etiqueta. Con label='left', la hora etiquetada 10:30 contiene datos hasta las 11:30. Unida por su etiqueta a las
barras de 5 minutos (un join y un ffill, el atajo habitual), la barra de las 10:30 ya conoce el cierre de las
11:30:
por_hora_s = s["cierre"].resample("1h", offset="30min")
hora_actual = por_hora_s.transform("last")
def regla_horaria(c_hora, nombre):
"""Compra si el cierre horario visto es mayor que el precio; vende
al cierre de la hora en curso."""
compra = c_hora > s["cierre"]
r = (hora_actual / s["cierre"] - 1)[compra]
return {"regla": nombre, "operaciones": int(compra.sum()),
"aciertos_pct": 100 * (r > 0).mean(),
"media_pb": 1e4 * r.mean()}
# MAL: la barra horaria, en su etiqueta de inicio, y ffill
mal = izq["cierre"].reindex(s.index).ffill()
# BIEN: disponible al terminar su última barra de 5 minutos
horas = izq[["cierre"]].assign(
disponible=s["t_cierre"].resample("1h", offset="30min").last())
bien = pd.merge_asof(b.sort_values("t_cierre"),
horas.sort_values("disponible"),
left_on="t_cierre", right_on="disponible",
allow_exact_matches=False, suffixes=("", "_hora"))
bien = bien.set_index("ts")["cierre_hora"]
pd.DataFrame([regla_horaria(mal, "etiqueta + ffill (mal)"),
regla_horaria(bien, "merge_asof (bien)")]).round(2) regla operaciones aciertos_pct media_pb
0 etiqueta + ffill (mal) 17359 100.00 32.22
1 merge_asof (bien) 18803 45.22 -0.33Con el atajo, la regla gana en el 100 % de sus 17.359 operaciones, 32 puntos básicos de media cada una: compra cuando «sabe» que la hora cerrará más arriba. Con la hora disponible de verdad (la última terminada), la misma regla gana en el 45,2 % y pierde 0,33 puntos básicos de media, antes de costes. La regla no ha cambiado; solo cuándo se conoce el dato.
Error 3: semanas con W-FRI
Las barras semanales desde las diarias se construyen con resample("W-FRI"): semanas que terminan en viernes,
etiquetadas con el viernes y, por defecto en frecuencias semanales, closed='right' y label='right'. Dos trampas
aparecen enseguida: el viernes de la etiqueta puede no ser una sesión, y la última semana puede no haber terminado.
semanal = d.resample("W-FRI").agg({"o": "first", "h": "max",
"l": "min", "c": "last"})
sin_sesion = [t.date() for t in semanal.index if t not in d.index]
print(len(semanal), "semanas; etiquetas que no son sesión:", sin_sesion)
perdidas = semanal["c"].reindex(d.index).notna().sum()
print("Semanas que sobreviven a reindex sobre sesiones:", perdidas)105 semanas; etiquetas que no son sesión: [datetime.date(2024, 3, 29), datetime.date(2025, 4, 18), datetime.date(2025, 7, 4), datetime.date(2026, 1, 2)]
Semanas que sobreviven a reindex sobre sesiones: 101De las 105 semanas, cuatro tienen la etiqueta en un día sin sesión: el Viernes Santo de 2024 (29 de marzo) y de 2025
(18 de abril), el 4 de julio de 2025 (viernes festivo) y el 2 de enero de 2026, la etiqueta de la última semana, que
en los datos solo tiene tres sesiones (29 a 31 de diciembre). reindex sobre las sesiones las borra en silencio, y un
ffill posterior arrastra la semana anterior: un retraso de una semana que nadie ve.
El error grave es el contrario: unir cada día con su semana, por ejemplo con to_period("W-FRI"). El lunes ve el
cierre del viernes siguiente:
dias = d.assign(semana=d.index.to_period("W-FRI"))
cierre_sem = semanal["c"].copy()
cierre_sem.index = cierre_sem.index.to_period("W-FRI")
dias["c_sem_mal"] = dias["semana"].map(cierre_sem)
fin_semana = d["fin"].resample("W-FRI").last()
semanas = semanal[["c"]].assign(disponible=fin_semana)
alineado = pd.merge_asof(d.reset_index().sort_values("fin"),
semanas.rename(columns={"c": "c_sem"})
.sort_values("disponible"),
left_on="fin", right_on="disponible",
allow_exact_matches=False)
dias["c_sem_bien"] = alineado.set_index("sesion")["c_sem"]
def regla_semanal(c_sem):
compra = c_sem > dias["c"]
# salida: el cierre de la semana en curso
r = (dias["c_sem_mal"] / dias["c"] - 1)[compra]
return {"operaciones": int(compra.sum()),
"aciertos_pct": 100 * (r > 0).mean(),
"media_pb": 1e4 * r.mean()}
pd.DataFrame({"por periodo (mal)": regla_semanal(dias["c_sem_mal"]),
"merge_asof (bien)": regla_semanal(dias["c_sem_bien"])}
).T.round(1) operaciones aciertos_pct media_pb
por periodo (mal) 209.0 100.0 229.3
merge_asof (bien) 216.0 40.7 14.5La regla «compra al cierre si el cierre semanal es mayor; vende al cierre de la semana» acierta en las 209
operaciones con el cierre de la propia semana y gana 229 puntos básicos de media por operación. Con la última semana
terminada (merge_asof por la hora real de cierre de su última sesión, que en Viernes Santo es el jueves a las
16:00), acierta en el 40,7 % de 216 operaciones, con una media de 14,5 puntos básicos que, con operaciones solapadas
y una acción que sube en el periodo, no distingue nada del ruido. El detalle de las semanas con festivo lo resuelve solo: la columna disponible es la
del jueves, y merge_asof no necesita que exista un viernes.
alineado.set_index("sesion").loc["2024-03-27":"2024-04-02",
["c", "c_sem", "disponible"]] c c_sem disponible
sesion
2024-03-27 51.44 50.50 2024-03-22 20:00:00+00:00
2024-03-28 51.34 50.50 2024-03-22 20:00:00+00:00
2024-04-01 51.77 51.34 2024-03-28 20:00:00+00:00
2024-04-02 51.56 51.34 2024-03-28 20:00:00+00:00El jueves 28 de marzo todavía ve la semana anterior (cierre 50,50, disponible el viernes 22): con
allow_exact_matches=False, una semana no está disponible en el mismo instante en que termina. El lunes 1 de abril
ya ve la semana del Viernes Santo, cerrada el jueves a 51,34.
Comprobar la alineación
Como en la lección 1.7.1, la mejor defensa es una comprobación que falle sola. Con la columna de disponibilidad a la vista, la prueba es una línea: ningún dato puede estar disponible después de la decisión que lo usa. Y la prueba de truncado vale también aquí: los valores alineados hasta un instante no pueden cambiar si se cortan los datos en ese instante.
def test_disponible_antes_de_decidir():
assert (por_hora["disponible"].dropna()
< por_hora.loc[por_hora["disponible"].notna(), "t_cierre"]
).all()
def test_truncado():
corte = pd.Timestamp("2025-06-04 15:00", tz="UTC")
parcial = b[b["t_cierre"] <= corte]
d_parcial = parcial.groupby("sesion").agg(
c=("cierre", "last"), fin=("t_cierre", "last"))
# solo las sesiones que ya habían cerrado en el corte
d_parcial = d_parcial[d_parcial["fin"] <= corte]
d_parcial = d_parcial.rename(columns={"fin": "disponible"})
unido = pd.merge_asof(parcial.sort_values("t_cierre"), d_parcial,
left_on="t_cierre", right_on="disponible",
allow_exact_matches=False)
completo = por_hora[por_hora["t_cierre"] <= corte]
np.testing.assert_allclose(unido["c"], completo["c"],
equal_nan=True)
def test_union_por_fecha_falla():
try:
assert (por_fecha["disponible"] < por_fecha["t_cierre"]).all()
except AssertionError:
return
raise AssertionError("la unión por fecha debería fallar")
for t in [test_disponible_antes_de_decidir, test_truncado,
test_union_por_fecha_falla]:
t()
print("PASSED", t.__name__)PASSED test_disponible_antes_de_decidir
PASSED test_truncado
PASSED test_union_por_fecha_fallaEl tercer test es un test del propio test: comprueba que la condición detecta la unión por fecha. Un test que no puede fallar no protege de nada.
Resumen de las convenciones
| Situación | Error típico | Forma correcta |
|---|---|---|
| Indicador diario en barras intradía | merge por fecha | merge_asof por hora de cierre de la sesión, o shift(1) si las sesiones son contiguas |
| Barras con marca de inicio a horas | closed='right' | closed='left' (por defecto) y offset para alinear con la apertura |
| Barras con marca de fin a horas | closed='left' | closed='right', label='right' |
| Barra lenta unida por su etiqueta | join + ffill por etiqueta | Columna de disponibilidad = cierre de su última barra |
| Semanas | to_period('W-FRI') o reindex sobre sesiones | merge_asof por el cierre de la última sesión de la semana |
| Última barra del marco lento | Usarla incompleta | Descartarla hasta que termine |
| Medias sesiones y horario de verano | Horas de cierre fijas en UTC | Hora de cierre real de cada sesión |
La figura junta los tres errores: con el dato del futuro, la proporción de operaciones ganadoras salta del entorno del 50 % a valores imposibles. Un backtest que acierta casi siempre no es una buena estrategia; casi siempre es una alineación mal hecha.
Ejercicios
- Añade a las barras de 5 minutos el ATR diario de 14 sesiones (lección 1.7.1) correctamente alineado. ¿Qué ATR ve la primera barra del 4 de junio de 2025 y de qué sesión es?
Solución
c_ant = d["c"].shift(1)
tr = pd.concat([d["h"] - d["l"], (d["h"] - c_ant).abs(),
(d["l"] - c_ant).abs()], axis=1).max(axis=1)
d["atr14"] = tr.ewm(alpha=1 / 14, adjust=False, min_periods=14).mean()
atr_diario = d[["atr14"]].assign(disponible=d["fin"],
sesion_atr=d.index)
con_atr = pd.merge_asof(b.sort_values("t_cierre"),
atr_diario.sort_values("disponible"),
left_on="t_cierre", right_on="disponible",
allow_exact_matches=False)
primera(con_atr)[["ny", "atr14", "sesion_atr"]].round({"atr14": 3}) ny atr14 sesion_atr
27660 2025-06-04 09:30:00-04:00 2.209 2025-06-03La barra de las 9:30 del 4 de junio ve el ATR calculado con la sesión del 3 de junio, la última que había terminado.
El mismo patrón sirve para cualquier columna diaria: se añade disponible y se une con merge_asof.
- Comprueba que la columna
finrecoge las medias sesiones: lista las sesiones de 2024 y 2025 que terminan a las 13:00 de Nueva York y, para una de ellas, qué dato diario ve la primera barra de la sesión siguiente.
Solución
hora_fin = (d["fin"].dt.tz_convert("America/New_York")
.dt.strftime("%H:%M"))
medias = d.index[hora_fin == "13:00"]
print([f.date().isoformat() for f in medias])
siguiente = por_hora[por_hora["sesion"] == "2024-07-05"].iloc[0]
print(siguiente["ny"], "ve el cierre", siguiente["c"],
"disponible", siguiente["disponible"])['2024-07-03', '2024-11-29', '2024-12-24', '2025-07-03', '2025-11-28', '2025-12-24']
2024-07-05 09:30:00-04:00 ve el cierre 68.25 disponible 2024-07-03 17:00:00+00:00Las seis medias sesiones de la NYSE en el periodo están ahí. La sesión del 5 de julio de 2024 (tras el festivo del
4) ve el cierre del 3 de julio, disponible a las 17:00 UTC (13:00 de Nueva York). Una regla que supusiera «el dato
diario está a las 20:00 o 21:00 UTC» se equivocaría en esos días; la columna disponible no.
- Una regla intradía usa la apertura del día (la de la subasta de las 9:30) desde la barra de las 9:30. ¿Es anticipación?
Solución
No, si se usa a partir del instante en que existe. La apertura oficial es el precio de la subasta de apertura, que
se fija a las 9:30:00 (lección 1.1.2); cualquier decisión posterior puede usarla. Lo que no puede usar la barra de
las 9:30 es su propio cierre (9:35), ni su máximo o su mínimo. La forma de no equivocarse es la misma de toda la
lección: darle a la apertura su columna de disponibilidad (las 9:30:00 de cada sesión, 13:30 o 14:30 UTC según el
horario de verano) y unirla con merge_asof como cualquier otro dato.
Respuesta plegada en cada pregunta
Autoevaluación
Unes la SMA de 20 sesiones diaria a las barras de 5 minutos con
mergepor la fecha de la sesión. ¿Qué valor ve la barra de las 10:00?- ALa SMA calculada con el cierre de ayer
- BLa SMA calculada con el cierre de hoy, que no se conoce hasta las 16:00
- CUn valor vacío
- DLa SMA de la semana
Ver respuesta
Respuesta B. La SMA calculada con el cierre de hoy, que no se conoce hasta las 16:00
La fila diaria de la fecha t lleva el cierre de t. Unida por fecha, aparece en todas las barras de t, también en las de la mañana. En NOVA, un filtro de tendencia así pasa de un 4,4 % a un 40,5 % anual.
Las barras de 5 minutos de tu proveedor llevan la marca de tiempo del **inicio** de la barra. ¿Qué
resampleda barras horarias que empiezan a las 9:30 y contienen las doce de 5 minutos de esa hora?- A
resample('1h', closed='right') - B
resample('1h', offset='30min'), con closed y label a la izquierda (los valores por defecto) - C
resample('1h', label='right')sinoffset - DCualquiera: dan lo mismo
Ver respuesta
Respuesta B.
resample('1h', offset='30min'), con closed y label a la izquierda (los valores por defecto)Con marcas de inicio, la barra de las 9:30 cubre de 9:30 a 9:35 y pertenece al intervalo [9:30, 10:30). Es
closed='left'. Conclosed='right', la barra de las 9:30 queda sola en el intervalo anterior y las demás se desplazan cinco minutos.- A
Una barra horaria etiquetada a las 10:30 con
label='left'contiene datos hasta las 11:30. ¿Cuándo puede usarse su cierre?- AA las 10:30
- BA las 11:30, cuando termina la última barra que contiene
- CAl cierre de la sesión
- DAl día siguiente
Ver respuesta
Respuesta B. A las 11:30, cuando termina la última barra que contiene
La etiqueta solo es un nombre. El dato está disponible cuando termina su última barra. Unirla por su etiqueta a barras de 5 minutos da a las de 10:30 a 11:25 el cierre de las 11:30: en NOVA, una regla de «compra si el cierre de la hora es mayor» gana en el 100 % de las operaciones.
Reindexas la serie semanal (
resample('W-FRI')) sobre las sesiones diarias de 2024-2025 y rellenas conffill. ¿Qué falla?- ANada
- BLas semanas cuyo viernes es festivo (Viernes Santo, 4 de julio de 2025) tienen la etiqueta en un día sin sesión y desaparecen al reindexar, y la última semana está incompleta
- CEl volumen se duplica
- DLas semanas empiezan en domingo
Ver respuesta
Respuesta B. Las semanas cuyo viernes es festivo (Viernes Santo, 4 de julio de 2025) tienen la etiqueta en un día sin sesión y desaparecen al reindexar, y la última semana está incompleta
W-FRIetiqueta cada semana con su viernes, haya sesión o no. Al reindexar sobre sesiones, las tres semanas con viernes festivo y la última (etiquetada el 2 de enero de 2026) se pierden.merge_asofpor la hora real de cierre de la semana no tiene ese problema.¿Cuál es la regla general para combinar dos marcos temporales sin anticipación?
- AUsar siempre
ffill - BUnir cada dato del marco lento por el instante en que termina su barra, y que solo lo vean las decisiones tomadas después de ese instante
- CRedondear las marcas de tiempo a la hora
- DCalcular todo en la misma frecuencia
Ver respuesta
Respuesta B. Unir cada dato del marco lento por el instante en que termina su barra, y que solo lo vean las decisiones tomadas después de ese instante
La fecha o la etiqueta de una barra no dicen cuándo se conoce. El instante de cierre sí.
merge_asof(…, direction='backward', allow_exact_matches=False)sobre esa columna es la regla, en cualquier combinación de frecuencias.- AUsar siempre
Ideas clave
- Un dato de un marco temporal lento se conoce cuando termina su barra, no en su fecha ni en su etiqueta. Esa hora de disponibilidad es la clave de la unión.
merge_asofsobre la hora de cierre, hacia atrás y sin coincidencias exactas, alinea cualquier par de frecuencias, también con medias sesiones y huecos;shift(1)y la fecha bastan solo si las sesiones son contiguas y completas.- En
resample,closedylabeldependen de cómo marca el proveedor sus barras: con marcas de inicio,closed='left'; la etiqueta, a la izquierda o a la derecha, no cambia los datos, pero sí dónde se une la barra. - Los tres errores clásicos con NOVA: filtro diario por fecha (40,5 % anual frente a 4,4 %), cierre horario por su etiqueta (ganan el 100 % de las operaciones) y cierre semanal por periodo (todas ganan).
W-FRIetiqueta cada semana con su viernes aunque sea festivo, y la última semana puede estar incompleta: no se reindexa sobre sesiones, se une por la hora real de cierre.