Point-in-time backtest for nowcasting US CPI (m/m), using only what was publicly known at each simulated date — never a future revision.
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.
$ 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.00196actual 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).
- 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-data—CPIAUCSL(target),T10YIE,DCOILWTICO,MICH(regressors), through the publicseries_vintageinterface and the sameDADOS_MERCADO_PUBLICO_POSTGRES_DSNvariablevol-implicita-spxalready uses. - Out of scope: a multivariate state-space/Kalman model, forecasting more than 1 month ahead, a dashboard, more than 3 auxiliary regressors.
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.
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 MICHuv sync
cp .env.example .env
uv run python -m inflation_nowcasting backtest --start 2015-01 --end 2020-12Without 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.
uv run python scripts/check.pyThe check runs format, lint, types and tests with coverage. It's the same command CI runs, on Windows, Linux and macOS.
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
- 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 onpublic-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.
MIT. Third-party data keeps its own license and terms of use.