---
title: "1.5.2 Datos macro, calendarios y estimaciones"
description: "Datos macro con sus vintages (FRED, ALFRED, BCE, INE), calendario económico en UTC, sorpresas frente al consenso y transcripciones como puente al NLP."
url: "https://pythonparatrading.com/curso-python-trading/nivel-1/datos-macro-calendarios-y-estimaciones/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-1/datos-macro-calendarios-y-estimaciones.png"
---

# 1.5.2 · 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.

- Curso: [Nivel 1: Datos financieros, mercados y análisis](https://pythonparatrading.com/curso-python-trading/nivel-1/)
- Módulo: 1.5 · Datos fundamentales y macro
- Duración: 60 min
- Requisitos: [0.4.4 · Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias](https://pythonparatrading.com/curso-python-trading/nivel-0/ficheros-pathlib-y-zonas-horarias/), [1.5.1 · Estados financieros, ratios, XBRL y SEC EDGAR](https://pythonparatrading.com/curso-python-trading/nivel-1/estados-financieros-xbrl-y-edgar/)

## Al terminar esta lección

- Distinguir primera publicación, revisiones y última versión (vintage) de una serie macro y reconstruir la serie tal como se conocía en una fecha.
- Medir cuánto se revisan el IPC, el paro y el PIB y por qué un backtest debe usar el vintage conocido en cada momento (ALFRED).
- Calcular la sorpresa de una publicación frente al consenso y normalizarla por evento.
- Alinear un calendario económico en UTC con barras de mercado, con horario de verano y festivos.
- Leer las API de FRED, del BCE y del INE, y situar estimaciones de analistas y transcripciones como fuentes con fecha.

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

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

```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"])]
```
```output
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:

```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)
```
```output
                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:

```python
pib = primera_y_ultima(mv, "pib_trimestral")
signo = (pib["primera"] < 0) != (pib["ultima"] < 0)
pib[signo]
```
```output
         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 pasada[^1].

```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"]
```
```output
         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):

```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%}")
```
```output
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:

```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)
```
```output
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.

```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())
```
```output
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:

$$
\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.

```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)
```
```output
                                         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).* Una fila por evento de Valdoria (V) y Brenia (B), con una marca por publicación entre −3 y +3 desviaciones típicas. Casi todas caen entre −2 y +2. La mayor, destacada, es el paro de junio de 2025 en Valdoria: 4,3 % publicado frente a 4,7 % de consenso, +2,67 desviaciones con el signo económico. Las decisiones de tipos no aparecen: no sorprendieron nunca.

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

```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})
```
```output
                  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).

```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])
```
```output
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.* Dos líneas escalonadas a lo largo de 2025. La de Valdoria, a las 13:30 UTC en invierno y a las 12:30 del 9 de marzo al 2 de noviembre; la de Brenia, a las 9:00 UTC en invierno y a las 8:00 del 30 de marzo al 26 de octubre. Las marcas verticales son las publicaciones reales del calendario. Dos franjas destacadas, del 9 al 30 de marzo y del 26 de octubre al 2 de noviembre, marcan cuando Nueva York y Madrid están a cinco horas y no a seis.

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.

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

```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"]]
```
```output
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]:

```python skip
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 datos[^2]:

```python skip
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
periodos[^3]. 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:

```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"]]
```
```output
   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:

```python skip
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.

```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.**

```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()}")
```
```output
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.

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

```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()
```
```output
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.

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

[^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]: 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`.

## 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
   - C. La media de las dos
   - D. Ninguna: el PIB no sirve para operar

   *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?

   - A. `file_type`
   - B. `realtime_start` y `realtime_end` (o `vintage_dates`)
   - C. `series_id`
   - D. `frequency`

   *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?

   - A. Siempre a las 13:30
   - B. A las 12:30 con horario de verano de EE. UU. y a las 13:30 sin él
   - C. A las 08:30
   - D. A las 14:30

   *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
   - C. 4,3 %: la sorpresa es el dato
   - D. 0: el consenso siempre acierta

   *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?

   - A. Porque el texto cambia con el tiempo
   - 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
   - C. Para ordenarlas alfabéticamente
   - D. No la necesita

   *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

### Research

- [Bancos Centrales y Política Monetaria en Python](https://pythonparatrading.com/politica-monetaria-en-python/) · Research-0010 · 15 min

### Documentación

- [FRED API: fred/series/observations (realtime_start, realtime_end, vintage_dates, output_type)](https://fred.stlouisfed.org/docs/api/fred/series_observations.html) · fred.stlouisfed.org
- [FRED API: periodos de tiempo real (ALFRED)](https://fred.stlouisfed.org/docs/api/fred/realtime_period.html) · fred.stlouisfed.org
- [ALFRED: Archival FRED](https://alfred.stlouisfed.org/) · alfred.stlouisfed.org
- [ECB Data Portal: API de datos (includeHistory, updatedAfter, csvdata)](https://data.ecb.europa.eu/help/api/data) · data.ecb.europa.eu
- [INE: referencia de la API JSON (wstempus, DATOS_SERIE, nult)](https://www.ine.es/dyngs/DAB/index.htm?cid=1100) · ine.es

### Datos

- [Datos del nivel 1: series macro de Valdoria con sus vintages (CSV)](https://pythonparatrading.com/curso/datos/nivel-1/macro_vintages.csv) · Descarga · macro_vintages.csv

Lección anterior: [1.5.1 · Estados financieros, ratios, XBRL y SEC EDGAR](https://pythonparatrading.com/curso-python-trading/nivel-1/estados-financieros-xbrl-y-edgar/)

Lección siguiente: [1.6.1 · Rentabilidades, volatilidad, correlaciones móviles y drawdown](https://pythonparatrading.com/curso-python-trading/nivel-1/volatilidad-correlaciones-y-drawdown/)
