Lección 1.5.2 · 60 min
Datos macro, calendarios económicos, estimaciones y transcripciones
Un dato macro se publica, se revisa y se vuelve a revisar: el PIB que ves hoy no es el que movió el mercado el día de su publicación. Esta lección trabaja con las primeras publicaciones y sus revisiones, el calendario de cuándo se conoce cada cifra (en UTC) y la sorpresa frente al consenso, que es lo que de verdad mueve los precios.
- Módulo
- 1.5 · Datos fundamentales y macro
- 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
Un dato, varias versiones
Las estadísticas macro se publican antes de estar completas. Un instituto de estadística da una primera estimación del PIB con una parte de la información del trimestre y la corrige cuando le llegan encuestas, datos fiscales o balanzas que faltaban. El IPC suele tener un avance y un dato definitivo; el paro se revisa al mes siguiente y en la revisión anual de las encuestas. Cada versión es un vintage: la serie tal como se conocía en una fecha.
Fuente Mermaid
flowchart LR A[Avance] --> S[Segunda estim.] S --> T[Tercera estim.] T --> R[Revisión anual]
Para un sistema de trading esto tiene dos consecuencias. La primera es la de la lección 1.5.1 con otro nombre: si un backtest usa la serie de hoy, usa cifras que no existían cuando simula la decisión. La segunda es más sutil: el mercado no reacciona al dato, sino a la diferencia entre el dato y lo que se esperaba. Las dos se resuelven con lo mismo, una fecha de conocimiento por cifra.
Los datos de esta lección son series ficticias de Valdoria, un país inventado con un calendario de publicación
parecido al de EE. UU., y de Brenia, uno con calendario europeo. macro_vintages.csv guarda cada publicación de
cuatro series con su fecha:
import pandas as pd
mv = pd.read_csv("macro_vintages.csv",
parse_dates=["fecha_publicacion"])
print(mv.groupby(["serie", "vintage"]).size().unstack(fill_value=0))
mv[(mv["serie"] == "pib_trimestral")
& mv["periodo"].isin(["2019T4", "2020T1"])]vintage 1 2 3 4
serie
ipc_interanual 83 83 0 0
paro 83 82 19 0
pib_trimestral 27 27 27 14
tipo_oficial 84 0 0 0
serie periodo fecha_publicacion valor vintage
362 pib_trimestral 2019T4 2020-01-30 -0.3 1
363 pib_trimestral 2019T4 2020-02-27 2.3 2
364 pib_trimestral 2019T4 2020-03-26 1.7 3
365 pib_trimestral 2020T1 2020-04-30 -3.8 1
366 pib_trimestral 2020T1 2020-05-28 -5.0 2
367 pib_trimestral 2020T1 2020-06-25 -4.6 3
368 pib_trimestral 2020T1 2021-07-29 -4.5 4El IPC tiene dos versiones por mes (avance y definitivo); el paro, dos o tres; el PIB, tres estimaciones y, en la mitad de los trimestres, una revisión anual; el tipo oficial no se revisa. El cuarto trimestre de 2019 es el ejemplo extremo: la primera estimación, el 30 de enero de 2020, dijo −0,3 % (tasa trimestral anualizada); un mes después, +2,3 %; la tercera, +1,7 %.
Primera publicación frente a última versión
¿Cuánto se revisa cada serie? Comparando la primera versión de cada periodo con la última:
def primera_y_ultima(mv, serie):
s = mv[mv["serie"] == serie].sort_values("vintage")
g = s.groupby("periodo")["valor"]
return pd.DataFrame({"primera": g.first(), "ultima": g.last()})
filas = {}
for serie in ["ipc_interanual", "paro", "pib_trimestral"]:
pu = primera_y_ultima(mv, serie)
rev = pu["ultima"] - pu["primera"]
filas[serie] = {"periodos": len(pu),
"revisados_%": 100 * (rev.abs() > 1e-9).mean(),
"rev_media_abs": rev.abs().mean(),
"rev_max_abs": rev.abs().max()}
pd.DataFrame(filas).T.round(2) periodos revisados_% rev_media_abs rev_max_abs
ipc_interanual 83.0 61.45 0.07 0.3
paro 83.0 54.22 0.06 0.2
pib_trimestral 27.0 81.48 0.49 2.0El IPC y el paro se mueven poco: décimas, en algo más de la mitad de los meses (61 % y 54 %). El PIB se revisa en el 81,5 % de los trimestres, con una revisión media de casi medio punto y una máxima de 2 puntos, la del cuarto trimestre de 2019. La figura de la cabecera pone las dos versiones de cada trimestre una al lado de la otra.
Medio punto de PIB anualizado es la diferencia entre «la economía se frena» y «la economía va bien». Y cambia el signo en algunos trimestres:
pib = primera_y_ultima(mv, "pib_trimestral")
signo = (pib["primera"] < 0) != (pib["ultima"] < 0)
pib[signo] primera ultima
periodo
2019T4 -0.3 1.7Con la primera estimación, el cuarto trimestre de 2019 fue de contracción. Una regla del tipo «reducir exposición si el PIB del último trimestre es negativo» habría actuado en febrero de 2020 con esa cifra. Con la serie de hoy, esa señal no existe. Ninguna de las dos es «la buena»: para estudiar la economía, la última; para simular una decisión, la que se conocía.
La serie tal como se conocía: vintages y ALFRED
Reconstruir la serie vigente en una fecha es filtrar por fecha de publicación y quedarse con la última versión de cada
periodo. Es la idea de ALFRED (Archival FRED), el archivo de vintages de la Reserva Federal de San Luis: FRED
devuelve por defecto la serie tal como está hoy; ALFRED permite pedirla tal como estaba en una fecha pasada1Nota 1 · Documentación de la API de FRED: fred/series/observations (parámetros realtime_start, realtime_end,
vintage_dates, output_type; clave api_key de 32 caracteres) y «Real-Time Periods»: por defecto el periodo de
tiempo real es hoy; ALFRED permite recuperar lo que se sabía en una fecha pasada..
def conocida_en(mv, serie, fecha):
"""La serie tal como se conocía al cierre de `fecha`."""
s = mv[(mv["serie"] == serie)
& (mv["fecha_publicacion"] <= fecha)]
return (s.sort_values("vintage")
.groupby("periodo")["valor"].last())
fechas = ["2020-02-15", "2020-06-30", "2025-12-31"]
cortes = {f: conocida_en(mv, "pib_trimestral", f) for f in fechas}
pd.DataFrame(cortes).loc["2019T3":"2020T2"] 2020-02-15 2020-06-30 2025-12-31
periodo
2019T3 2.8 2.8 3.0
2019T4 -0.3 1.7 1.7
2020T1 NaN -4.6 -4.5
2020T2 NaN NaN -29.4Cada columna es un vintage. El 15 de febrero de 2020 nadie sabía nada del primer trimestre de 2020 y el cuarto de 2019 era −0,3; a finales de junio el cuarto de 2019 ya era +1,7 y el primero de 2020 se había revisado dos veces. La tabla completa, periodos por filas y fechas de vintage por columnas, es el «triángulo de revisiones» que los economistas usan para estudiar la calidad de las primeras estimaciones.
Para usar una serie macro como variable en un backtest diario, el join es el mismo que con fundamentales: un
merge_asof por fecha de publicación, con la versión de cada día. Comparándolo con el join ingenuo (la última versión
de cada trimestre, desde el día en que termina):
sesiones = pd.read_csv("fix_indice_diario.csv",
parse_dates=["fecha"])[["fecha"]]
p = mv[mv["serie"] == "pib_trimestral"].copy()
# fin de cada trimestre: 2019T4 -> 2019-12-31
p["periodo_fin"] = (pd.PeriodIndex(p["periodo"].str.replace(
"T", "Q"), freq="Q").end_time.normalize())
pit = pd.merge_asof(
sesiones, p.sort_values("fecha_publicacion")
[["fecha_publicacion", "periodo", "valor"]],
left_on="fecha", right_on="fecha_publicacion",
allow_exact_matches=False)
ult = p.sort_values("vintage").groupby("periodo").last()
ingenuo = pd.merge_asof(
sesiones, ult.reset_index().sort_values("periodo_fin")
[["periodo_fin", "periodo", "valor"]],
left_on="fecha", right_on="periodo_fin")
c = pit.merge(ingenuo, on="fecha", suffixes=("_pit", "_ing"))
c = c.dropna(subset=["valor_pit", "valor_ing"])
print(f"Sesiones: {len(c)}")
print("Otro trimestre:",
f"{(c['periodo_pit'] != c['periodo_ing']).mean():.1%}")
print("Otro valor:",
f"{(c['valor_pit'] != c['valor_ing']).mean():.1%}")
negativo_distinto = (c["valor_pit"] < 0) != (c["valor_ing"] < 0)
print(f"Signo distinto: {negativo_distinto.mean():.1%}")Sesiones: 1681
Otro trimestre: 36.6%
Otro valor: 89.5%
Signo distinto: 9.8%En el 89,5 % de las sesiones el join ingenuo usa un valor distinto del que se conocía y en el 36,6 %, incluso otro trimestre: el que aún no se había publicado. En una de cada diez (9,8 %) ni siquiera coincide el signo del crecimiento. Un modelo macro entrenado así aprende una relación entre la economía y el mercado que ningún inversor pudo explotar.
El calendario económico: cuándo se conoce cada dato
Un calendario económico lista, para cada publicación, la fecha y hora, el consenso (la media o la mediana de las
previsiones de los analistas que encuesta el proveedor del calendario), el valor anterior y el publicado. calendario_economico.csv
es el de 2025 de Valdoria y Brenia:
ce = pd.read_csv("calendario_economico.csv",
parse_dates=["fecha_hora"])
print(ce["fecha_hora"].dtype, len(ce), "publicaciones")
ce[["fecha_hora", "evento", "pais", "consenso",
"publicado"]].head(6)datetime64[us, UTC] 123 publicaciones
fecha_hora evento pais consenso publicado
0 2025-01-02 09:00:00+00:00 PMI manufacturero Brenia 47.70 47.70
1 2025-01-02 15:00:00+00:00 PMI manufacturero Valdoria 51.30 51.00
2 2025-01-03 13:30:00+00:00 Tasa de paro Valdoria 4.30 4.20
3 2025-01-13 13:30:00+00:00 IPC interanual (definitivo) Valdoria 2.70 2.40
4 2025-01-16 13:30:00+00:00 Ventas minoristas, mensual Valdoria 0.70 0.60
5 2025-01-29 19:00:00+00:00 Decisión de tipos Valdoria 4.35 4.35Las horas vienen en UTC, como deben guardarse. Antes de usarlo conviene comprobar que casa con las series: el publicado de cada IPC avance de Valdoria debe ser el vintage 1 de ese mes, y el «anterior», la última versión conocida del mes previo.
av = ce[(ce["pais"] == "Valdoria")
& (ce["evento"] == "IPC interanual (avance)")].copy()
av["fecha"] = av["fecha_hora"].dt.tz_convert(
"America/New_York").dt.tz_localize(None).dt.normalize()
ipc1 = mv[(mv["serie"] == "ipc_interanual")
& (mv["vintage"] == 1)]
cmp = av.merge(ipc1, left_on="fecha",
right_on="fecha_publicacion")
print(len(av), "avances;", len(cmp), "con su vintage;",
"iguales:", (cmp["publicado"] == cmp["valor"]).all())11 avances; 11 con su vintage; iguales: TrueLas once publicaciones coinciden. Es una comprobación barata que conviene automatizar en un pipeline (lección 1.4.8): un calendario de un proveedor y una serie de otro pueden discrepar en el vintage, en la unidad o en el día.
La sorpresa: dato menos consenso
El precio de un activo ya descuenta lo que el mercado espera. Lo que llega con la publicación es la parte inesperada:
Para comparar eventos con unidades y volatilidades distintas se normaliza dividiendo por la desviación típica de las sorpresas de ese evento (sorpresa estandarizada). En un estudio real esa desviación se estima solo con el pasado, con una ventana que no incluya la publicación que se evalúa; aquí, con un año de datos, se usa la del año para describir la distribución.
ce["sorpresa"] = ce["publicado"] - ce["consenso"]
corto = ce["evento"].str.replace(
"PIB trimestral, tasa anualizada", "PIB")
ce["evento_pais"] = corto + " · " + ce["pais"]
res = ce.groupby("evento_pais")["sorpresa"].agg(
n="size", media="mean", desv="std",
exactas=lambda s: (s.abs() < 1e-9).mean())
res.sort_values("desv", ascending=False).round(2) n media desv exactas
evento_pais
Confianza del consumidor · Valdoria 12 -0.67 1.27 0.00
PIB (segunda estimación) · Valdoria 4 -0.03 0.51 0.00
PMI manufacturero · Valdoria 12 -0.07 0.46 0.00
PIB (avance) · Valdoria 4 -0.47 0.43 0.00
PMI manufacturero · Brenia 12 -0.09 0.43 0.17
Ventas minoristas, mensual · Valdoria 12 -0.07 0.31 0.33
PIB (tercera estimación) · Valdoria 4 -0.35 0.31 0.00
IPC interanual (avance) · Valdoria 11 0.08 0.22 0.09
IPC interanual (definitivo) · Valdoria 12 -0.02 0.16 0.50
Tasa de paro · Valdoria 12 -0.07 0.15 0.50
IPC interanual (avance) · Brenia 12 -0.01 0.08 0.42
Decisión de tipos · Brenia 8 0.00 0.00 1.00
Decisión de tipos · Valdoria 8 0.00 0.00 1.00Las decisiones de tipos no sorprenden nunca en este año: el consenso acierta las dieciséis, lo esperable en bancos centrales que preparan al mercado antes de decidir. El paro y el IPC definitivo aciertan la mitad de las veces: se mueven por décimas y, en el definitivo, el avance ya está publicado. Las sorpresas más dispersas son las de la confianza del consumidor (1,27 puntos de índice), el PIB y los PMI. Tres de las cuatro sorpresas del avance del PIB fueron negativas (media de −0,47 puntos), pero con cuatro datos eso no es un sesgo del consenso, es una anécdota. Las desviaciones están en unidades distintas (puntos de índice, porcentajes): por eso hay que normalizar. Un evento sin sorpresas apenas mueve el precio; uno con sorpresas grandes y frecuentes es el que merece un estudio de eventos (nivel 2).
desv = ce.groupby("evento_pais")["sorpresa"].transform("std")
ce["z"] = ce["sorpresa"] / desv
# en el paro, más es peor: se invierte el signo
ce.loc[ce["evento"] == "Tasa de paro", "z"] *= -1
top = ce.dropna(subset=["z"]).reindex(
ce["z"].abs().sort_values(ascending=False).index)
top[["fecha_hora", "evento_pais", "consenso",
"publicado", "z"]].head(5).round({"z": 2}) fecha_hora evento_pais consenso publicado z
54 2025-06-06 12:30:00+00:00 Tasa de paro · Valdoria 4.7 4.3 2.67
50 2025-05-30 14:00:00+00:00 Confianza del consumidor · Valdoria 95.3 92.3 -2.36
46 2025-05-16 12:30:00+00:00 Ventas minoristas, mensual · Valdoria 0.7 -0.0 -2.25
37 2025-04-24 12:30:00+00:00 PIB (avance) · Valdoria -0.3 -1.2 -2.07
27 2025-03-27 12:30:00+00:00 PIB (tercera estimación) · Valdoria 3.0 2.4 -1.93Con la sorpresa estandarizada y con el signo económico (para el paro, menos es mejor), las publicaciones de distintos indicadores se pueden poner en una misma escala y ordenar por lo inesperadas que fueron.
Horas en UTC, mercados en hora local
Valdoria publica sus datos a las 08:30 de Nueva York y decide tipos a las 14:00; Brenia publica a las 10:00 u 11:00 de Madrid y decide a las 14:15. En UTC, esas horas cambian con el horario de verano, y no a la vez: EE. UU. cambia el segundo domingo de marzo y el primero de noviembre; la UE, el último domingo de marzo y de octubre (lección 0.4.4).
ny = ce["fecha_hora"].dt.tz_convert("America/New_York")
mad = ce["fecha_hora"].dt.tz_convert("Europe/Madrid")
ce["hora_local"] = ny.dt.strftime("%H:%M").where(
ce["pais"] == "Valdoria", mad.dt.strftime("%H:%M"))
ce["hora_utc"] = ce["fecha_hora"].dt.strftime("%H:%M")
print(pd.crosstab([ce["pais"], ce["hora_local"]], ce["hora_utc"]))
# cambios de hora de 2025 según la base de datos de zonas (tzdata)
dias = pd.date_range("2025-01-01", "2025-12-31", freq="D",
tz="UTC") + pd.Timedelta(hours=12)
for zona in ["America/New_York", "Europe/Madrid"]:
off = dias.tz_convert(zona).map(lambda t: t.utcoffset())
cambio = dias[1:][off[1:] != off[:-1]]
print(zona, [str(d.date()) for d in cambio])hora_utc 08:00 09:00 10:00 12:15 12:30 13:15 13:30 14:00 15:00 18:00 19:00
pais hora_local
Brenia 10:00 7 5 0 0 0 0 0 0 0 0 0
11:00 0 7 5 0 0 0 0 0 0 0 0
14:15 0 0 0 4 0 4 0 0 0 0 0
Valdoria 08:30 0 0 0 0 39 0 20 0 0 0 0
10:00 0 0 0 0 0 0 0 15 9 0 0
14:00 0 0 0 0 0 0 0 0 0 6 2
America/New_York ['2025-03-09', '2025-11-02']
Europe/Madrid ['2025-03-30', '2025-10-26']Las 08:30 de Nueva York son las 12:30 UTC con horario de verano y las 13:30 sin él. Un filtro escrito como «no operar entre las 13:25 y las 13:45 UTC» acierta en invierno y llega una hora tarde de marzo a noviembre. Según la base de datos de zonas horarias, en 2025 Nueva York cambió el 9 de marzo y el 2 de noviembre, y Madrid el 30 de marzo y el 26 de octubre. Entre esas fechas, Nueva York y Madrid estuvieron a cinco horas en lugar de seis: dos ventanas al año en las que una regla «hora de Madrid menos seis» falla.
Fuente Mermaid
flowchart TB
C[Calendario UTC] --> L[Hora local]
L --> S{¿Mercado abierto?}
S -- sí --> B[Barra del evento]
S -- no --> N[Siguiente sesión]
B --> V[Ventana reacción]
N --> VPara un estudio de reacción hay que unir cada publicación con la primera barra en la que el mercado puede
reaccionar. Con las barras de 5 minutos de NOVA (sesión de 09:30 a 16:00 de Nueva York) y un merge_asof hacia
delante:
barras = pd.read_parquet("nova_barras_5min.parquet")
ev = ce[ce["pais"] == "Valdoria"].sort_values("fecha_hora")
m = pd.merge_asof(ev, barras[["ts"]], left_on="fecha_hora",
right_on="ts", direction="forward")
m["espera_min"] = (m["ts"] - m["fecha_hora"]).dt.total_seconds() / 60
print(m.groupby("hora_local")["espera_min"].median())
largas = m[m["espera_min"] > 120]
largas[["fecha_hora", "ts", "espera_min"]]hora_local
08:30 60.0
10:00 0.0
14:00 0.0
Name: espera_min, dtype: float64
fecha_hora ts espera_min
11 2025-02-17 13:30:00+00:00 2025-02-18 14:30:00+00:00 1500.0
47 2025-07-04 12:30:00+00:00 2025-07-07 13:30:00+00:00 4380.0
61 2025-09-01 14:00:00+00:00 2025-09-02 13:30:00+00:00 1410.0
81 2025-11-27 13:30:00+00:00 2025-11-28 14:30:00+00:00 1500.0
89 2025-12-25 13:30:00+00:00 2025-12-26 14:30:00+00:00 1500.0Los datos de las 08:30 llegan con el mercado de acciones cerrado: la primera barra que puede reaccionar es la de la apertura, una hora después, y lo que pasó entre medias está en el hueco de apertura (y en los futuros y las divisas, que sí cotizan). Los de las 10:00 y la decisión de las 14:00 caen dentro de la sesión. Y hay cinco publicaciones en días en que la NYSE cierra (en este calendario ficticio, el 17 de febrero, el 4 de julio, el 1 de septiembre, el 27 de noviembre y el 25 de diciembre): su primera barra es la de la sesión siguiente, de uno a tres días después. Un estudio que mida la reacción «en la barra de las 09:30 del día del dato» mezclaría esos cinco con sesiones sin noticia.
Las API: FRED, BCE e INE
FRED, el BCE y el INE, tres fuentes públicas de datos macro de EE. UU., de la zona del euro y de España, tienen API abiertas. Todas necesitan red, así que el código va sin ejecutar.
FRED (Reserva Federal de San Luis) pide una clave (api_key) de 32 caracteres. El endpoint de observaciones
admite el periodo de tiempo real (realtime_start, realtime_end), una lista de fechas de vintage (vintage_dates) y el
tipo de salida (output_type: 1, por periodo de tiempo real; 2, todas las observaciones por vintage; 3, solo las
nuevas y revisadas; 4, solo la primera publicación)1:
import os
import requests
URL = "https://api.stlouisfed.org/fred/series/observations"
base = {"series_id": "GDP", "api_key": os.environ["FRED_API_KEY"],
"file_type": "json"}
hoy = requests.get(URL, params=base, timeout=30).json()
primeras = requests.get(URL, params=base | {"output_type": 4},
timeout=30).json()
en_2020 = requests.get(URL, params=base | {
"realtime_start": "2020-02-15", "realtime_end": "2020-02-15"},
timeout=30).json()
# cada observación: realtime_start, realtime_end, date, valueLa respuesta trae una lista observations en la que cada fila lleva date (el periodo), value (como texto; "."
cuando falta) y el intervalo de tiempo real en que esa cifra estuvo vigente. Es exactamente la estructura de
macro_vintages.csv con dos columnas de fecha en lugar de una.
BCE (Data Portal): las URL siguen la sintaxis SDMX data-api.ecb.europa.eu/service/data/<flujo>/<clave>, sin
clave de API. format=csvdata o jsondata eligen el formato; startPeriod y endPeriod, el rango;
updatedAfter, solo lo cambiado desde una fecha (útil para actualizar una base local), e includeHistory=true
devuelve también las versiones anteriores de los datos2Nota 2 · ECB Data Portal, «Data»: entrada data-api.ecb.europa.eu/service/, formatos csvdata y jsondata, parámetros
startPeriod, endPeriod, updatedAfter e includeHistory (versiones anteriores de los datos).:
import io
url = ("https://data-api.ecb.europa.eu/service/data/"
"EXR/D.USD.EUR.SP00.A")
r = requests.get(url, params={"format": "csvdata",
"startPeriod": "2025-01-01"},
timeout=30)
eurusd = pd.read_csv(io.StringIO(r.text))
# columnas SDMX: KEY, FREQ, CURRENCY, ..., TIME_PERIOD, OBS_VALUEINE: la API JSON tiene la forma servicios.ine.es/wstempus/js/{idioma}/{función}/{input}; la función
DATOS_SERIE devuelve los datos de una serie por su código y el parámetro nult limita la respuesta a los últimos
periodos3Nota 3 · INE, «Referencia de la API»: estructura https://servicios.ine.es/wstempus/js/{idioma}/{función}/{input},
función DATOS_SERIE y parámetro nult.. Las fechas de cada dato (Fecha) llegan como milisegundos desde 1970, y aquí hay una trampa de zona
horaria. La respuesta de ejemplo siguiente tiene la estructura de la API con un código y unos valores inventados:
import json
ej = json.loads("""{"COD": "SERIE_EJEMPLO",
"Nombre": "Serie de ejemplo. Variación anual.",
"Data": [
{"Fecha": 1761951600000, "FK_Periodo": 11, "Anyo": 2025,
"Valor": 3.1},
{"Fecha": 1764543600000, "FK_Periodo": 12, "Anyo": 2025,
"Valor": 2.8}]}""")
d = pd.DataFrame(ej["Data"])
d["utc"] = pd.to_datetime(d["Fecha"], unit="ms", utc=True)
d["madrid"] = d["utc"].dt.tz_convert("Europe/Madrid")
d["mes_mal"] = d["utc"].dt.strftime("%Y-%m")
d["mes_bien"] = d["madrid"].dt.strftime("%Y-%m")
d[["FK_Periodo", "utc", "mes_mal", "mes_bien", "Valor"]] FK_Periodo utc mes_mal mes_bien Valor
0 11 2025-10-31 23:00:00+00:00 2025-10 2025-11 3.1
1 12 2025-11-30 23:00:00+00:00 2025-11 2025-12 2.8Leídos en UTC, los dos datos caen el último día del mes anterior: el de noviembre parece de octubre. La marca de tiempo
es la medianoche en Madrid del primer día del periodo, que en UTC es las 23:00 (o las 22:00 en verano) del día antes.
Convertir a Europe/Madrid antes de sacar el mes, o usar Anyo y FK_Periodo, lo resuelve. La petición real:
url = ("https://servicios.ine.es/wstempus/js/ES/DATOS_SERIE/"
"<código de la serie>")
r = requests.get(url, params={"nult": 12}, timeout=30)
datos = r.json()["Data"]Ninguna de estas API dice cuándo se publicó cada cifra en la respuesta por defecto: FRED necesita el periodo de tiempo
real, el BCE includeHistory, y el INE, consultar su calendario de publicaciones. Si guardas datos macro para
backtests, guarda también el día en que los descargaste: tu propia base de datos se convierte así en un archivo de
vintages.
Estimaciones de analistas y transcripciones
Las estimaciones de analistas (beneficio por acción, ingresos, recomendaciones) son la versión empresarial del consenso macro: cada analista publica su previsión, el proveedor calcula la media o la mediana y la sorpresa de resultados es el beneficio publicado menos ese consenso. Tienen los mismos problemas de fecha: cada estimación se revisa, el consenso de un trimestre cambia día a día hasta la publicación y un dataset sin la fecha de cada revisión no sirve para simular. Antes de pagar por una base de estimaciones (lección 1.3.4), comprueba si guarda cada previsión con su fecha o solo el consenso del momento de la descarga.
Las transcripciones de las presentaciones de resultados (earnings calls) son texto: lo que la dirección dice sobre el trimestre y las previsiones, y las preguntas de los analistas. Son la entrada de los modelos de lenguaje del nivel 6: tono, cambios en las previsiones (guidance), palabras que anticipan problemas.
Fuente Mermaid
flowchart LR T[Transcripción] --> M[Marca de tiempo] M --> X[Texto limpio] X --> F[Señal NLP]
El principio es el mismo de toda esta sección: una transcripción vale desde que termina la llamada, no desde el cierre del trimestre ni desde la publicación del 10-Q. Las lecciones 6.1.2 y 6.2.3 construyen señales sobre filings y transcripciones con esa marca de tiempo.
Lo que esto cambia en un backtest
- Usa vintages. Si la fuente da solo la serie actual, guarda tus descargas con fecha o usa ALFRED. Para una serie sin archivo de vintages, retrasa cada dato hasta su fecha de publicación real, aunque sigas usando la versión final, y dilo.
- Opera la sorpresa, no el nivel. El nivel del IPC está en los precios mucho antes de publicarse; lo nuevo es la diferencia con el consenso.
- UTC para guardar, hora local para operar. Convierte con
zoneinfocada vez; nunca con desfases fijos. - Festivos y mercados cerrados. Una publicación con el mercado cerrado se refleja en la apertura siguiente o en los mercados que sí cotizan (futuros, divisas). Decide cuál mides.
Ejercicios
- Calcula, para el paro, la revisión entre el vintage 1 y el 2 y entre el 2 y el 3 (cuando existe). ¿Cuál de las dos revisiones es mayor en media absoluta?
Solución
paro = mv[mv["serie"] == "paro"].pivot(
index="periodo", columns="vintage", values="valor")
r12 = (paro[2] - paro[1]).abs()
r23 = (paro[3] - paro[2]).abs()
print(f"v1 -> v2: media {r12.mean():.3f}, n = {r12.count()}")
print(f"v2 -> v3: media {r23.mean():.3f}, n = {r23.count()}")v1 -> v2: media 0.062, n = 82
v2 -> v3: media 0.100, n = 19La revisión anual (vintage 3) solo existe en los meses que cambian, así que su media se calcula sobre pocos meses y es mayor: cuando la revisión anual toca un mes, lo mueve más que la revisión mensual. Para un modelo con datos del paro, el vintage 3 llega más de un año después del periodo: si se usa desde el principio, mete información de un año después.
- Para cada publicación de Valdoria, calcula la hora local de Nueva York y comprueba que todas las de las 08:30 caen fuera de la sesión de la NYSE y que la decisión de tipos cae dentro.
Solución
val = ce[ce["pais"] == "Valdoria"].copy()
local = val["fecha_hora"].dt.tz_convert("America/New_York")
minuto = local.dt.hour * 60 + local.dt.minute
val["en_sesion"] = minuto.between(9 * 60 + 30, 16 * 60 - 1)
val.groupby("hora_local")["en_sesion"].mean()hora_local
08:30 0.0
10:00 1.0
14:00 1.0
Name: en_sesion, dtype: float64Todas las de las 08:30 están fuera de la sesión de contado y las de las 10:00 y las 14:00, dentro. La comprobación no
mira festivos: para eso hace falta el calendario de la bolsa (exchange_calendars, lección 1.1.2), que es lo que hizo
el merge_asof con las barras reales de NOVA.
- Un proveedor te ofrece un histórico de consensos con una sola fila por publicación (el consenso final). ¿Qué estudio puedes hacer con él y cuál no?
Solución
Con el consenso final y el dato publicado se puede medir la sorpresa y estudiar la reacción del precio en los minutos o días siguientes a la publicación: es un estudio de eventos, y el consenso final es lo que el mercado esperaba justo antes. No se puede estudiar cómo se formó la expectativa (revisiones del consenso en las semanas previas) ni construir una señal que opere antes de la publicación con el consenso de ese momento, porque esa versión no está: usar el consenso final para una decisión de días antes sería anticipación.
Footnotes
-
Documentación de la API de FRED:
fred/series/observations(parámetrosrealtime_start,realtime_end,vintage_dates,output_type; claveapi_keyde 32 caracteres) y «Real-Time Periods»: por defecto el periodo de tiempo real es hoy; ALFRED permite recuperar lo que se sabía en una fecha pasada. ↩ ↩2 -
ECB Data Portal, «Data»: entrada
data-api.ecb.europa.eu/service/, formatoscsvdatayjsondata, parámetrosstartPeriod,endPeriod,updatedAftereincludeHistory(versiones anteriores de los datos). ↩ -
INE, «Referencia de la API»: estructura
https://servicios.ine.es/wstempus/js/{idioma}/{función}/{input}, funciónDATOS_SERIEy parámetronult. ↩
Respuesta plegada en cada pregunta
Autoevaluación
La primera estimación del PIB de un trimestre fue −0,3 % y la última versión es +1,7 %. ¿Qué cifra debe usar un backtest para decidir el día de la primera publicación?
- A+1,7 %, porque es la correcta
- B−0,3 %, la que se conocía ese día
- CLa media de las dos
- DNinguna: el PIB no sirve para operar
Ver respuesta
Respuesta B. −0,3 %, la que se conocía ese día
El mercado reaccionó a −0,3 %. La última versión no existía ese día: usarla es anticipación. Por eso existe ALFRED, que guarda cada versión con su fecha.
En la API de FRED, ¿qué parámetro permite pedir los datos tal como estaban en una fecha pasada?
- A
file_type - B
realtime_startyrealtime_end(ovintage_dates) - C
series_id - D
frequency
Ver respuesta
Respuesta B.
realtime_startyrealtime_end(ovintage_dates)El periodo de tiempo real dice qué se sabía y cuándo. Por defecto es hoy: FRED devuelve la versión actual. Con un periodo pasado o con
vintage_dates, devuelve lo que se conocía entonces;output_type=4da solo la primera publicación.- A
Un dato de Valdoria se publica a las 08:30 de Nueva York. ¿A qué hora UTC aparece en el calendario?
- ASiempre a las 13:30
- BA las 12:30 con horario de verano de EE. UU. y a las 13:30 sin él
- CA las 08:30
- DA las 14:30
Ver respuesta
Respuesta B. A las 12:30 con horario de verano de EE. UU. y a las 13:30 sin él
Nueva York está a UTC−4 en verano y a UTC−5 en invierno. Guardar el calendario en UTC es correcto, pero una regla fija del tipo «13:30 UTC» se equivoca de hora durante unos siete meses al año.
El consenso del paro era 4,7 % y se publica 4,3 %. ¿Cuál es la sorpresa y qué signo tiene para la economía?
- A+0,4 puntos: peor de lo esperado
- B−0,4 puntos: menos paro del esperado, una sorpresa positiva para la economía
- C4,3 %: la sorpresa es el dato
- D0: el consenso siempre acierta
Ver respuesta
Respuesta B. −0,4 puntos: menos paro del esperado, una sorpresa positiva para la economía
Sorpresa = publicado − consenso = 4,3 − 4,7 = −0,4. En el paro, un signo negativo es una buena noticia; para comparar eventos conviene darle a cada uno su signo económico y normalizarlo por la dispersión de sus sorpresas.
¿Por qué una transcripción de una presentación de resultados necesita una marca de tiempo propia en un dataset de NLP?
- APorque el texto cambia con el tiempo
- BPorque la información llega al mercado durante la llamada, no al cierre del trimestre ni al publicarse el 10-Q: es la fecha a partir de la cual una señal puede usarla
- CPara ordenarlas alfabéticamente
- DNo la necesita
Ver respuesta
Respuesta B. Porque la información llega al mercado durante la llamada, no al cierre del trimestre ni al publicarse el 10-Q: es la fecha a partir de la cual una señal puede usarla
Es el mismo principio que con fundamentales y macro: cada dato vale desde el momento en que se conoce. Una señal de sentimiento construida sobre la transcripción solo puede operar después de la llamada (nivel 6).
Ideas clave
- Los datos macro se revisan: en estos datos, el 81 % de los trimestres de PIB cambia entre la primera y la última versión, con una revisión media absoluta de 0,49 puntos.
- Un backtest debe usar el vintage conocido en cada fecha. FRED da por defecto la versión actual; ALFRED y los parámetros de tiempo real dan las anteriores.
- Lo que mueve el precio es la sorpresa frente al consenso, no el dato. Hay que normalizarla por evento y darle su signo económico.
- Guarda el calendario en UTC y conviértelo a la hora local del mercado con zoneinfo: la hora UTC de un mismo dato cambia con el horario de verano, y los festivos desplazan la reacción a la sesión siguiente.
- Estimaciones de analistas y transcripciones son datos con fecha, como los estados financieros: valen desde que se publican.