← Volver al blog

Mi pipeline decía OK. Los datos llevaban mal desde enero.

Este es el mensaje que me llegaba por Telegram cada noche entre semana, al terminar el pipeline de STAIR:

✅ STAIR pipeline OK — 2026-08-14. Precio: 776.340027, VIX: ok, News: ok, Macro: pendiente (FRED).

Verde. Código de salida 0. El monitor externo recibía su ping. Todo en orden.

Mientras tanto, una de las variables con las que entreno los modelos tenía 147 valores equivocados en la base de datos, del 2 de enero al 7 de agosto de 2026. No lo detectó ningún test, ninguna validación del pipeline, ningún monitor. Lo encontré por accidente, construyendo una herramienta para otra cosa.

Si vienes del vídeo, esta es la versión larga: qué falló exactamente, por qué no saltó nada y qué tendría que haber existido para verlo antes.

Una precisión antes de seguir, porque importa. Son más de siete meses de datos corrompidos, no siete meses de un bug en ejecución. STAIR no lleva tanto tiempo en producción. El pipeline ingiere histórico, así que un error de configuración puede escribir meses de filas malas sin haber estado vivo durante esos meses.

El dato

La variable es yield_spread_10y3m: la diferencia entre el rendimiento del bono del Tesoro estadounidense a 10 años y el de la letra a 3 meses. Es una de las variables macro clásicas en cualquier modelo de mercado, porque resume en un número lo que el mercado de deuda espera de la economía. En mi sistema es una variable del núcleo: si falta en una fila, esa fila no entra en el entrenamiento.

Las dos patas salen de FRED, la base de datos de la Reserva Federal de St. Louis. El bono a 10 años es la serie DGS10, diaria. Para la letra a 3 meses, FRED ofrece dos series que se parecen mucho:

  • DGS3MO: rendimiento a vencimiento constante a 3 meses, diaria.
  • TB3MS: tipo de la letra a 3 meses en el mercado secundario, mensual.

Mismo plazo, nombre casi igual, frecuencia distinta. Mi pipeline pedía la segunda.

El fallo: una línea de configuración

La lista de series que el pipeline descarga de FRED vive en settings.yaml. Ahí ponía TB3MS. Según git log -S, esa línea entró con el commit que montó el pipeline completo, el 22 de abril de 2026.

Lo irónico es que el código sí conocía la serie correcta. En daily_pipeline.py había un bloque con DGS3MO, con el comentario "daily 3M CMT (matches historical base)". Pero era el valor por defecto de un .get() que solo se usa si el YAML no define las series. Y el YAML sí las definía. La versión correcta estaba escrita, documentada y era inalcanzable: dos fuentes de verdad, y ganaba la equivocada.

Una serie mensual metida en una tabla diaria no da ningún error. Da un valor que se repite día tras día hasta el mes siguiente. Así se veían las dos primeras semanas de enero en mi base de datos frente a FRED (en puntos porcentuales):

Fecha En mi base FRED Diferencia
2026-01-02 0.64 0.54 +0.10
2026-01-05 0.64 0.53 +0.11
2026-01-06 0.64 0.55 +0.09
2026-01-07 0.64 0.53 +0.11
2026-01-08 0.64 0.57 +0.07
2026-01-09 0.64 0.56 +0.08
2026-01-12 0.64 0.52 +0.12
2026-01-13 0.64 0.51 +0.13
2026-01-14 0.64 0.48 +0.16
2026-01-15 0.64 0.49 +0.15

El valor real se mueve entre 0.48 y 0.57. El mío es 0.64 todos los días. Ninguno de esos números es absurdo: un spread de 0.64 es perfectamente plausible. Ese es el problema.

Por qué no saltó nada

Repasé cada capa que debería haberlo detectado, y todas tenían una buena razón para no hacerlo.

Los tests. Los tests de las variables macro trabajan con series sintéticas: comprueban que las columnas existen, que los valores tienen sentido, que los rangos son razonables. Ninguno depende del identificador de la serie que se pide a FRED. Un test que fabrica sus propios datos no puede enterarse de que la fuente real es otra.

La validación. Cualquier comprobación que mire un valor aislado —si existe, si tiene el tipo correcto, si cae en un rango razonable— da por bueno un 0.64. Porque lo es, como número. Lo que está mal no es el valor, sino que no corresponde a ese día.

El monitor. Esta es la parte que más me escuece. El mensaje de Telegram sí decía algo: "Macro: pendiente (FRED)". Pero el script lo trataba como información y no como aviso, porque FRED publica con uno o dos días de retraso y yo había decidido que eso era normal. El aviso aparecía todas las noches. Y un aviso que aparece todas las noches deja de leerse.

El patrón común es que todas las comprobaciones comparaban el sistema consigo mismo. ¿Ha terminado el flujo? ¿Existe la fila de hoy? ¿El valor tiene buena pinta? Ninguna preguntaba lo único que importaba: ¿lo que tengo guardado coincide con lo que dice la fuente?

Cómo apareció

No lo encontré buscándolo. En agosto el mismo fallo empezó a manifestarse de forma más visible: desde el 10 de agosto el spread se escribía como NULL. El log del pipeline lo decía sin rodeos:

FRED [TB3MS] → 'rate_3m': 0 non-null obs, 100.0% NaN

El cron diario pide a FRED solo los últimos días, y en una ventana tan corta una serie mensual no tiene ninguna observación.

Un NULL en una variable del núcleo tiene un efecto silencioso: el cargador de datos descarta cualquier fila incompleta, así que la ventana de entrenamiento se acortaba sin que nada avisara. Persiguiendo esos huecos llegué a settings.yaml y cambié la serie a DGS3MO. Para rellenar los NULL escribí una herramienta que compara market_features con FRED fila a fila, con una tolerancia de 0.005.

El 24 de agosto la lancé sobre todo el histórico, desde el año 2000. Además de los huecos, devolvió esto:

VALORES NO-NULL QUE NO COINCIDEN CON FRED (solo informe — NO se tocan)
  [yield_spread_10y3m] 147 filas | 2026-01-02 -> 2026-08-07

Los NULL eran solo la punta visible. Antes de dejar huecos, la configuración llevaba meses escribiendo valores plausibles y equivocados. La misma herramienta sobre 2024 y 2025 no encontró ni una discrepancia: todo lo anterior a 2026 coincide con FRED dentro de la tolerancia.

No he reconstruido con certeza por qué la frontera cae justo en el cambio de año, y prefiero decirlo a inventarme una explicación.

El arreglo, y el arreglo que no hice

Corregir los valores nuevos fue cambiar una línea. Corregir los 147 históricos tenía trampa.

Ya tenía un script de reparación que recalcula el histórico macro desde FRED. Su ejecución en seco (dry-run) mostró que no recalculaba solo el spread: arrastraba doce columnas en cascada, incluidas variables derivadas del VIX que no tenían nada que ver con este fallo, y dejaba treinta huecos nuevos en otra variable del núcleo que en ese momento estaba completa. Arreglar 147 celdas a cambio de romper otras era un mal intercambio, así que no lo ejecuté.

En su lugar añadí al script de relleno una opción que aplica exactamente lo que la reconciliación solo reportaba, restringida a esa columna. Primero en seco: 147 correcciones previstas, ninguna otra columna afectada. Después, de verdad. El 29 de agosto la auditoría de todo 2026 devolvió "Sin discrepancias", y enero volvió a moverse día a día: 0.54, 0.53, 0.55…

Lo que no medí

No medí cuánto cambiaron los modelos por este error. Los datos están corregidos y cualquier reentreno posterior parte de la base corregida, pero no hice una comparación antes y después, así que no voy a sugerir un impacto que no tengo.

Y conviene decir lo que este error no es. No es la historia de un bug que destrozó unos resultados brillantes: el resultado central de STAIR ya es que ningún modelo supera de forma robusta al baseline. Es la historia de un bug que nadie habría visto nunca, porque nada en el sistema estaba diseñado para verlo.

Lo que me llevo

Validar contra la fuente, no contra uno mismo. Tests, validaciones y monitor comprobaban coherencia interna. Solo una comparación con la fuente original podía ver un valor plausible y falso. Hoy esa comparación existe como herramienta en el repositorio.

Un aviso permanente no es un aviso. Si una alerta aparece todas las noches y has decidido que es normal, has construido un filtro, no un monitor. "Pendiente (FRED)" debería haber tenido fecha de caducidad: pendiente un día, bien; pendiente varias sesiones seguidas, alarma.

Cero filas no es éxito. Una descarga que devuelve cero observaciones terminaba igual que una que devuelve cien. Una respuesta vacía de una fuente que debería tener datos es un fallo, aunque no lance ninguna excepción.

Una sola fuente de verdad para la configuración. El valor correcto estaba en el código, en una rama que nunca se ejecutaba. Un valor por defecto que contradice la configuración real no es una red de seguridad: es documentación falsa.

Un valor plausible y equivocado es peor que un hueco. El NULL de agosto rompió algo, y por eso lo vi. Los 147 valores de antes no rompían nada. Los errores que hacen ruido se arreglan; los silenciosos se quedan.


STAIR es una plataforma de investigación construida como trabajo de fin de grado en la Universidade da Coruña. No constituye asesoramiento financiero. Las cifras de este artículo proceden de las salidas reales del pipeline y de la herramienta de reconciliación contra FRED.