Skip to content

Repository files navigation

inflation-nowcasting

Point-in-time backtest for nowcasting US CPI (m/m), using only what was publicly known at each simulated date — never a future revision.

The problem

Every nowcasting backtest has the same way of lying: unknowingly using data that was only published after the date the backtest pretends to be simulating. An "updated" regressor or a CPI revision that leaks into a past month inflates the model's apparent accuracy — and that's exactly the mistake that separates a portfolio result from one that would survive in production. This repository treats point-in-time discipline as the central piece, not a footnote.

Result

$ uv run python -m inflation_nowcasting backtest --start 2018-01 --end 2018-12
month,nowcast,actual,baseline
2018-01,0.00154,0.00239,0.00234
2018-02,0.00151,-0.00036,0.00239
2018-03,0.00135,-0.00058,-0.00036
2018-04,0.00137,-0.00003,-0.00058
2018-05,0.00148,0.00447,-0.00003
2018-06,0.00119,0.00481,0.00447
2018-07,0.00142,0.00120,0.00481
2018-08,0.00136,0.00269,0.00120
2018-09,0.00160,0.00398,0.00269
2018-10,0.00134,0.00081,0.00398
2018-11,0.00150,-0.00083,0.00081
2018-12,0.00144,0.00307,-0.00083
# MAE nowcast=0.00175 baseline=0.00196

actual is the value the market saw on release day (not a later revision); baseline repeats the previous month's m/m change. This transcript is real, but from synthetic data (CPI and regressors generated by a random walk, 108 months) run against an ephemeral Postgres — not against the real FRED, because live ingestion requires an API key this environment doesn't have (see Limitations).

Problem and scope

  • User: me, and whoever reviews the portfolio evaluating for quantitative research.
  • Decision: it's possible to measure whether a simple bridge model beats a naive baseline for m/m CPI, without the result being inflated by a leak.
  • Input: FRED via Postgres from public-market-dataCPIAUCSL (target), T10YIE, DCOILWTICO, MICH (regressors), through the public series_vintage interface and the same DADOS_MERCADO_PUBLICO_POSTGRES_DSN variable vol-implicita-spx already uses.
  • Out of scope: a multivariate state-space/Kalman model, forecasting more than 1 month ahead, a dashboard, more than 3 auxiliary regressors.

Architecture

Postgres (public-market-data, series_vintage)
    │
    ▼
point_in_time.py   every read filters by data_publicacao <= cutoff
    │
    ├─► regressors.py    T10YIE, DCOILWTICO, MICH → feature vector
    │
    └─► backtest.py       walk-forward: trains OLS on prior months only,
                           predicts the current month, compares to baseline
                           │
                           ▼
                        cli.py   CSV + MAE

Three decisions worth explaining:

Each month's cutoff comes from the data itself, not an assumed calendar. _release_cutoff uses the real data_publicacao of that CPI vintage, minus one day — not a hardcoded rule like "the 12th of the following month," which would silently break in any month the release ran late.

Every regressor and CPI query goes through point_in_time.py. It's that module — three functions, data_publicacao <= cutoff in the WHERE clause — that prevents leakage, not a code review at every consumption site. The project's most important test (test_data_published_after_the_cutoff_does_not_change_a_past_nowcast) proves this by inserting a "future" value after the nowcast has already been computed, and checking that the result doesn't change.

OLS via numpy.linalg.lstsq, not statsmodels. This cut doesn't need p-values or residual diagnostics — just the coefficient, to predict. Bringing in statsmodels as a dependency just for that wasn't worth the cost.

Run locally

Requirements: Python 3.12, uv, Docker (for the ephemeral test Postgres), and public-market-data running with all four series ingested:

# in public-market-data:
docker compose up -d
uv run python -m public_market_data ingest --source fred --series CPIAUCSL
uv run python -m public_market_data ingest --source fred --series T10YIE
uv run python -m public_market_data ingest --source fred --series DCOILWTICO
uv run python -m public_market_data ingest --source fred --series MICH
uv sync
cp .env.example .env
uv run python -m inflation_nowcasting backtest --start 2015-01 --end 2020-12

Without all four series ingested (or without enough months of history before --start for --min-training, default 36), the command explains why and exits with code 2 instead of making up a result.

Quality

uv run python scripts/check.py

The check runs format, lint, types and tests with coverage. It's the same command CI runs, on Windows, Linux and macOS.

Structure

src/inflation_nowcasting/
    point_in_time.py   point-in-time reads — the piece that prevents leakage
    regressors.py       feature vector built from the regressors
    backtest.py          walk-forward: training, prediction, baseline, MAE
    cli.py                coordination

Limitations

  • Not validated against real FRED data. Live ingestion requires a free FRED API key this development environment doesn't have. All the point-in-time logic is validated with controlled synthetic data (tests/) — real Postgres behavior depends only on public-market-data, already tested separately, but the end-to-end path with real CPI has never run here.
  • Simple bridge equation (OLS, 3 regressors, no variable selection or regularization) — not the state of the art for nowcasting (dynamic factor model, MIDAS), it's the right floor before adding complexity.
  • One-month-ahead forecast only; no revision of the nowcast itself as more of the current month's data arrives.

License

MIT. Third-party data keeps its own license and terms of use.

About

Point-in-time backtest for nowcasting US CPI, using only what was known at each date

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages