← Volver ao blog

O meu pipeline dicía OK. Os datos levaban mal dende xaneiro.

Esta é a mensaxe que me chegaba por Telegram cada noite entre semana, ao rematar o pipeline de STAIR:

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

Verde. Código de saída 0. O monitor externo recibía o seu ping. Todo en orde.

Mentres tanto, unha das variables coas que adestro os modelos tiña 147 valores equivocados na base de datos, do 2 de xaneiro ao 7 de agosto de 2026. Non o detectou ningún test, ningunha validación do pipeline, ningún monitor. Atopeino por accidente, construíndo unha ferramenta para outra cousa.

Se vés do vídeo, esta é a versión longa: que fallou exactamente, por que non saltou nada e que tería que existir para velo antes.

Unha precisión antes de seguir, porque importa. Son máis de sete meses de datos corrompidos, non sete meses dun bug en execución. STAIR non leva tanto tempo en produción. O pipeline inxire histórico, así que un erro de configuración pode escribir meses de filas malas sen estar vivo durante eses meses.

O dato

A variable é yield_spread_10y3m: a diferenza entre o rendemento do bono do Tesouro estadounidense a 10 anos e o da letra a 3 meses. É unha das variables macro clásicas en calquera modelo de mercado, porque resume nun número o que o mercado de débeda espera da economía. No meu sistema é unha variable do núcleo: se falta nunha fila, esa fila non entra no adestramento.

As dúas patas saen de FRED, a base de datos da Reserva Federal de St. Louis. O bono a 10 anos é a serie DGS10, diaria. Para a letra a 3 meses, FRED ofrece dúas series que se parecen moito:

  • DGS3MO: rendemento a vencemento constante a 3 meses, diaria.
  • TB3MS: tipo da letra a 3 meses no mercado secundario, mensual.

Mesmo prazo, nome case igual, frecuencia distinta. O meu pipeline pedía a segunda.

O fallo: unha liña de configuración

A lista de series que o pipeline descarga de FRED está en settings.yaml. Alí poñía TB3MS. Segundo git log -S, esa liña entrou co commit que montou o pipeline completo, o 22 de abril de 2026.

O irónico é que o código si coñecía a serie correcta. En daily_pipeline.py había un bloque con DGS3MO, co comentario "daily 3M CMT (matches historical base)". Pero era o valor por defecto dun .get() que só se usa se o YAML non define as series. E o YAML si as definía. A versión correcta estaba escrita, documentada e era inalcanzable: dúas fontes de verdade, e gañaba a equivocada.

Unha serie mensual metida nunha táboa diaria non dá ningún erro. Dá un valor que se repite día tras día ata o mes seguinte. Así se vían as dúas primeiras semanas de xaneiro na miña base de datos fronte a FRED (en puntos porcentuais):

Data Na miña base FRED Diferenza
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

O valor real móvese entre 0.48 e 0.57. O meu é 0.64 todos os días. Ningún deses números é absurdo: un spread de 0.64 é perfectamente plausible. Ese é o problema.

Por que non saltou nada

Repasei cada capa que debería detectalo, e todas tiñan unha boa razón para non facelo.

Os tests. Os tests das variables macro traballan con series sintéticas: comproban que as columnas existen, que os valores teñen sentido, que os rangos son razoables. Ningún depende do identificador da serie que se lle pide a FRED. Un test que fabrica os seus propios datos non pode decatarse de que a fonte real é outra.

A validación. Calquera comprobación que mire un valor illado —se existe, se ten o tipo correcto, se cae nun rango razoable— dá por bo un 0.64. Porque o é, como número. O que está mal non é o valor, senón que non corresponde a ese día.

O monitor. Esta é a parte que máis me doe. A mensaxe de Telegram si dicía algo: "Macro: pendiente (FRED)". Pero o script tratábao como información e non como aviso, porque FRED publica con un ou dous días de atraso e eu decidira que iso era normal. O aviso aparecía todas as noites. E un aviso que aparece todas as noites deixa de lerse.

O padrón común é que todas as comprobacións comparaban o sistema consigo mesmo. Rematou o fluxo? Existe a fila de hoxe? O valor ten boa pinta? Ningunha preguntaba o único que importaba: o que teño gardado coincide co que di a fonte?

Como apareceu

Non o atopei buscándoo. En agosto o mesmo fallo comezou a manifestarse dun xeito máis visible: dende o 10 de agosto o spread escribíase como NULL. O log do pipeline dicíao sen rodeos:

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

O cron diario pídelle a FRED só os últimos días, e nunha xanela tan curta unha serie mensual non ten ningunha observación.

Un NULL nunha variable do núcleo ten un efecto silencioso: o cargador de datos descarta calquera fila incompleta, así que a xanela de adestramento acurtábase sen que nada avisase. Perseguindo eses ocos cheguei a settings.yaml e cambiei a serie a DGS3MO. Para encher os NULL escribín unha ferramenta que compara market_features con FRED fila a fila, cunha tolerancia de 0.005.

O 24 de agosto lanceina sobre todo o histórico, dende o ano 2000. Ademais dos ocos, devolveu isto:

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

Os NULL eran só a punta visible. Antes de deixar ocos, a configuración levaba meses escribindo valores plausibles e equivocados. A mesma ferramenta sobre 2024 e 2025 non atopou nin unha discrepancia: todo o anterior a 2026 coincide con FRED dentro da tolerancia.

Non reconstruín con certeza por que a fronteira cae xusto no cambio de ano, e prefiro dicilo a inventarme unha explicación.

O arranxo, e o arranxo que non fixen

Corrixir os valores novos foi cambiar unha liña. Corrixir os 147 históricos tiña trampa.

Xa tiña un script de reparación que recalcula o histórico macro dende FRED. A súa execución en seco (dry-run) amosou que non recalculaba só o spread: arrastraba doce columnas en fervenza, incluídas variables derivadas do VIX que non tiñan nada que ver con este fallo, e deixaba trinta ocos novos noutra variable do núcleo que nese momento estaba completa. Arranxar 147 celas a cambio de romper outras era un mal intercambio, así que non o executei.

No seu lugar engadín ao script de recheo unha opción que aplica exactamente o que a reconciliación só reportaba, restrinxida a esa columna. Primeiro en seco: 147 correccións previstas, ningunha outra columna afectada. Despois, de verdade. O 29 de agosto a auditoría de todo 2026 devolveu "Sin discrepancias", e xaneiro volveu moverse día a día: 0.54, 0.53, 0.55…

O que non medín

Non medín canto cambiaron os modelos por este erro. Os datos están corrixidos e calquera readestramento posterior parte da base corrixida, pero non fixen unha comparación antes e despois, así que non vou suxerir un impacto que non teño.

E convén dicir o que este erro non é. Non é a historia dun bug que esnaquizou uns resultados brillantes: o resultado central de STAIR xa é que ningún modelo supera de forma robusta o baseline. É a historia dun bug que ninguén vería nunca, porque nada no sistema estaba deseñado para velo.

O que me levo

Validar contra a fonte, non contra un mesmo. Tests, validacións e monitor comprobaban coherencia interna. Só unha comparación coa fonte orixinal podía ver un valor plausible e falso. Hoxe esa comparación existe como ferramenta no repositorio.

Un aviso permanente non é un aviso. Se unha alerta aparece todas as noites e decidiches que é normal, construíches un filtro, non un monitor. "Pendiente (FRED)" debería ter data de caducidade: pendente un día, ben; pendente varias sesións seguidas, alarma.

Cero filas non é éxito. Unha descarga que devolve cero observacións remataba igual ca unha que devolve cen. Unha resposta baleira dunha fonte que debería ter datos é un fallo, aínda que non lance ningunha excepción.

Unha soa fonte de verdade para a configuración. O valor correcto estaba no código, nunha rama que nunca se executaba. Un valor por defecto que contradí a configuración real non é unha rede de seguridade: é documentación falsa.

Un valor plausible e equivocado é peor ca un oco. O NULL de agosto rompeu algo, e por iso o vin. Os 147 valores de antes non rompían nada. Os erros que fan ruído arránxanse; os silenciosos quedan.


STAIR é unha plataforma de investigación construída como traballo de fin de grao na Universidade da Coruña. Non constitúe asesoramento financeiro. As cifras deste artigo proceden das saídas reais do pipeline e da ferramenta de reconciliación contra FRED.