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.

Fig. — PIB trimestral de Valdoria, 2019–2025: primera estimación y última versión de cada trimestre; la barra es la revisión
Lecciones del nivel 1
  1. 1.1 · Cómo funcionan los mercados

    1. 1.1.1 Participantes y centros de negociación
    2. 1.1.2 Sesiones, subastas, festivos y calendarios
    3. 1.1.3 Libro de órdenes: bid, ask, spread y profundidad
    4. 1.1.4 Tipos de órdenes
    5. 1.1.5 Costes, liquidación, apalancamiento y margen
  2. 1.2 · Clases de activos

    1. 1.2.1 Acciones: splits, dividendos y eventos corporativos
    2. 1.2.2 ETFs: réplica, tracking error, apalancados y UCITS
    3. 1.2.3 Futuros: especificaciones, margen, roll y contango
    4. 1.2.4 Forex: pares, pips, lotes, rollover y CFD
    5. 1.2.5 Opciones: call, put, valor intrínseco y valor temporal
    6. 1.2.6 Cripto: spot, perpetuos, funding y custodia
    7. 1.2.7 Renta fija y tipos de interés; CFDs y productos apalancados
  3. 1.3 · Fuentes de datos de mercado

    1. 1.3.1 Tipos de datos de mercado: EOD, barras, ticks, libro y referencia
    2. 1.3.2 Fuentes de datos gratuitas
    3. 1.3.3 APIs de datos retail y prosumer
    4. 1.3.4 Proveedores de datos profesionales
    5. 1.3.5 Datos de cripto y on-chain
    6. 1.3.6 Brókeres como fuente de datos
    7. 1.3.7 Licencias y términos de uso de datos
    8. 1.3.8 Cómo elegir proveedor de datos
  4. 1.4 · Ingeniería de datos financieros

    1. 1.4.1 Descarga por REST y websockets: paginación, límites y reintentos
    2. 1.4.2 Limpieza de datos: faltantes, ticks erróneos, huecos y zonas horarias
    3. 1.4.3 Ajustes por splits y dividendos: precios ajustados y sin ajustar
    4. 1.4.4 Universos sin sesgo de supervivencia: deslistados, fusiones y cambios de símbolo
    5. 1.4.5 Construcción de barras: tiempo, ticks, volumen, dólar y desequilibrio
    6. 1.4.6 Series continuas de futuros: reglas de roll, ajuste por diferencia y ajuste por ratio
    7. 1.4.7 Almacenamiento: CSV, Parquet, particionado, DuckDB, TimescaleDB y ArcticDB
    8. 1.4.8 Pipeline reproducible: descarga, validación, actualización incremental y tests de calidad
  5. 1.5 · Datos fundamentales y macro

    1. 1.5.1 Estados financieros, ratios, XBRL y SEC EDGAR
    2. 1.5.2 Datos macro, calendarios económicos, estimaciones y transcripciones
  6. 1.6 · Análisis exploratorio financiero

    1. 1.6.1 Rentabilidades, volatilidad, correlaciones móviles y drawdown
    2. 1.6.2 Hechos estilizados: colas gruesas, agrupamiento, autocorrelación y apalancamiento
    3. 1.6.3 Estacionalidad: día de la semana, mes, cambio de mes y sesiones intradía
  7. 1.7 · Análisis técnico programado

    1. 1.7.1 Indicadores técnicos en Python
    2. 1.7.2 Implementación propia frente a TA-Lib: validar resultados
    3. 1.7.3 Patrones de velas y soportes y resistencias
    4. 1.7.4 Multi-timeframe sin anticipación

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.

AvanceSegunda estim.Tercera estim.Revisión anual
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:

IN [01]python
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"])]
OUT [01]Series macro con sus vintages
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        4

El 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:

IN [02]python
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)
OUT [02]Tamaño de las revisiones
                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.0

El 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:

IN [03]python
pib = primera_y_ultima(mv, "pib_trimestral")
signo = (pib["primera"] < 0) != (pib["ultima"] < 0)
pib[signo]
OUT [03]Trimestres que cambian de signo
         primera  ultima
periodo
2019T4      -0.3     1.7

Con 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..

IN [04]python
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"]
OUT [04]La serie vigente en una fecha
         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.4

Cada 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):

IN [05]python
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%}")
OUT [05]PIB point-in-time frente a ingenuo, sesión a sesión
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:

IN [06]python
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)
OUT [06]Calendario económico de 2025
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.35

Las 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.

IN [07]python
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())
OUT [07]El calendario frente a los vintages
11 avances; 11 con su vintage; iguales: True

Las 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:

sorpresa=publicado−consenso\text{sorpresa} = \text{publicado} - \text{consenso}

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.

IN [08]python
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)
OUT [08]Sorpresas por evento
                                         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.00
Fig. — Sorpresas de 2025 por evento (publicado − consenso), divididas por la desviación típica de las sorpresas de ese evento; en el paro, con el signo cambiado (menos paro = sorpresa positiva)

Las 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).

IN [09]python
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})
OUT [09]Sorpresa estandarizada y signo económico
                  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.93

Con 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).

IN [10]python
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])
OUT [10]La misma hora local, dos horas UTC
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']
Fig. — Hora UTC de las publicaciones de 2025 de las 08:30 de Nueva York (Valdoria) y de las 10:00 de Madrid (Brenia); en las franjas, los dos horarios de verano no coinciden

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.

sínoCalendario UTCHora local¿Mercado abierto?Barra del eventoSiguiente sesiónVentana reacción
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 --> V

Para 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:

IN [11]python
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"]]
OUT [11]Primera barra tras cada publicación de Valdoria
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.0

Los 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:

IN [12]python
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, value

La 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).:

IN [13]python
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_VALUE

INE: 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:

IN [14]python
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"]]
OUT [14]INE: milisegundos que son medianoche en Madrid
   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.8

Leí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:

IN [15]python
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.

TranscripciónMarca de tiempoTexto limpioSeñal NLP
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 zoneinfo cada 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

  1. 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
IN [16]python
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()}")
OUT [16]Revisiones del paro
v1 -> v2: media 0.062, n = 82
v2 -> v3: media 0.100, n = 19

La 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.

  1. 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
IN [17]python
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()
OUT [17]Dentro y fuera de la sesión
hora_local
08:30    0.0
10:00    1.0
14:00    1.0
Name: en_sesion, dtype: float64

Todas 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.

  1. 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

  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. ↩ ↩2

  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). ↩

  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. ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. 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.

  2. En la API de FRED, ¿qué parámetro permite pedir los datos tal como estaban en una fecha pasada?

    • Afile_type
    • Brealtime_start y realtime_end (o vintage_dates)
    • Cseries_id
    • Dfrequency
    Ver respuesta

    Respuesta B. realtime_start y realtime_end (o vintage_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=4 da solo la primera publicación.

  3. 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.

  4. 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.

  5. ¿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.

Recursos