quantamentry

How we built a 169-country macro platform on free public data

Quantamentry — engineering notes on the cheap-stack approach to country risk

Snapshot Sun May 03 2026 00:00:00 GMT+0000 (Coordinated Universal Time) · 9 min read

How we built a 169-country macro platform on free public data

Quantamentry — engineering notes on the cheap-stack approach to country risk.


Why this exists

If you want a structured, daily-updated, country-risk dataset with consistent coverage of 100+ countries, you have three options today:

  1. Bloomberg / Refworld / Refinitiv / S&P CapIQ. Excellent. Comprehensive. Around $24,000–$60,000 per seat per year.
  2. Mid-market vendors — Trading Economics, CEIC, Macrobond, Oxford Economics. $5,000–$30,000 per year for partial-country, partial-frequency coverage.
  3. Build it yourself on free APIs. This post is about path 3.

The price-per-seat math is the headline. The licensing math is the harder one — every commercial vendor has a redistribution clause that prevents you from baking their data into a product you sell to anyone except the largest institutional clients. So even if you can afford the seat, you usually cannot serve the data through your own API or product without a separate, much larger licensing deal.

Public sources have no such restriction. World Bank, IMF, BIS, FRED, OECD, ILO, GDELT, Open Exchange Rates — all of these publish under terms that allow re-use, re-distribution, and inclusion in a commercial product. That's the entire economic foundation of what we do.

The catch: the public-data ecosystem is fragmented, inconsistent, and was never designed with daily ingestion in mind. So the engineering question is whether you can stitch it into something that behaves like a single coherent dataset. After ~9 months of iteration, the answer is yes — but the path through is non-obvious. This post is the map.

The free-data thesis, by source

Eleven active sources today. 22.3 million observations in the warehouse, 169 countries scored end-to-end, ~37 distinct indicators.

SourceRowsCountriesIndicatorsCadenceWhat we get
BIS Policy Rates19.2M561dailycentral bank policy rate history back to 1945
IMF WEO1.08M1695semi-annualGDP, CPI, current account, debt, unemployment forecasts
World Bank WDI1.08M17110annualGDP growth, debt, trade, FDI, reserves, structural
BIS Credit257K503quarterlycredit-to-GDP, debt service ratio, REER
ILO ILOSTAT148K1672annualunemployment, labor force participation
FRED CPI122K227monthlymonthly CPI for major economies
Open Exchange Rates99K1051dailyFX rates against USD
FRED Global69K126variesVIX, WTI, Brent, DXY, gold, copper
OECD CPI67K261monthlyOECD-tracked CPI
OECD CLI63K131monthlycomposite leading indicator
IMF IFS43K461monthlyfallback monthly CPI for non-OECD economies

Coverage isn't uniform. Major economies have monthly CPI, daily policy rates, quarterly credit metrics. Frontier markets have annual WB WDI and the IMF semi-annual WEO. We tier countries by data thickness — gold (27 countries with full monthly stack), silver (53 with monthly CPI + semi-annual WEO), bronze (89 with annual-only) — and apply different scoring formulas at each tier.

That tiering is honesty, not artifice. The model works for Niger, but the inputs are thinner, and the output should be read with that in mind. Every score we publish carries a tier flag.

The five hard parts

1. Point-in-time correctness

Macro data revises. Aggressively. The Q3 GDP print you see today is not the print that existed on the day it was first released — every public source republishes history with each new vintage. If you back-test a strategy against the current historical record, you're back-testing against information that wasn't available at the time. That's the cardinal sin of macro back-testing.

We solve it by storing every observation with both an observation_date (the period the value refers to — e.g. 2025-09-30) and a release_date (when we first ingested it). All scoring queries pull "the latest observation as of date X where release_date <= X" — never the current best estimate. The PostgreSQL index that makes this fast:

CREATE INDEX idx_obs_pit ON observations
  (country_iso, indicator, observation_date, release_date);

It's a four-column composite, used by every scoring asset. Cost: a few extra GB of disk. Benefit: every back-test is honest.

When a value gets revised, we don't update the row — we insert a new row and mark the old one with superseded_by = <new_id>. Lossless audit. Slightly more disk; a lot more correct.

2. Frontier markets without inflation targets

The Taylor rule and the LINEX credibility-gap loss both need a target. Most of the 169 countries we cover don't have a formally-stated inflation target. Of the 92 bronze-tier countries, ~70 either run a peg, target a basket, or have no explicit nominal anchor at all.

We could exclude them. That would cap the dataset at the same ~50 countries every commercial vendor covers, which defeats the point. Instead we use a rolling 3-year CPI mean as a proxy target for any country without a stated one. The target slowly drifts with realized inflation, so a country that's been at 6% for three years will score "on target" at 6%. That's intentional: we're scoring credibility against the implicit anchor the central bank has revealed through behavior, not against a textbook target it never claimed.

Honest about the limitation: this means a country can have a "good" credibility score by drifting together with its (untracked) inflation, rather than by anchoring it. We surface the derivation in the methodology and tag every bronze-tier score with a data-quality flag.

3. Sources die. Plan for it.

In May 2025 the FRED OECD-MEI mirror series stopped updating mid-stream. The series didn't return an error — the endpoint kept returning data, but the most recent observation was frozen at 2025-04-01. If you weren't watching the metadata, you'd think the series was healthy and your CPI for OECD countries would silently go stale.

Now we check observation_end on every series before merging. Any series whose latest observation is more than 90 days old gets skipped with a logged warning. We also wired our /api/health endpoint to fail loudly when the most recent score is more than 2 days stale, so a frozen upstream surfaces inside an hour, not weeks later.

The same defensive posture applies to IMF IFS, which has a known habit of returning HTTP 200 with an empty body during their backend outages. We treat empty responses as failures and raise a Dagster Failure so the run goes red on the dashboard rather than silently succeeding with zero rows.

4. Schema drift

The BIS renamed its credit-aggregates dataflow from WS_TC_PRIV to WS_TC mid-2024. The IMF moved its WEO data from a Datamapper API to a different host endpoint. The OECD migrated its SDMX endpoint from stats.oecd.org to sdmx.oecd.org/public/rest and changed the request key format from 6 dimensions to 7.

Each of these broke ingestion silently or noisily over the last year. The fix is rarely complex — it's usually a one-line client change — but the detection is the hard part. If a daily job runs successfully but ingests zero rows because the dataflow was renamed, you don't notice until your scoring goes stale.

Two patterns help:

  1. Explicit row-count assertions per source. Each ingestion asset asserts a minimum expected row count for the run. A 2024-format request that returns zero rows in 2025 fails loud, doesn't fall through silently.
  2. A weekly synthetic test that hits each upstream and validates the response schema against a saved fixture. When the schema drifts, the test fails before the daily job does.

5. Coverage of the pegged country case

Some countries — Saudi Arabia, Hong Kong, Denmark, the CFA franc bloc — don't run their own monetary policy in any operationally meaningful way. The Taylor rule is the wrong instrument for them. Our framework still scores them, but the methodology page now includes a paragraph for each pegged country explaining what the score actually represents (which is "what would the rule say if you were free to set policy independently").

This is a known limitation. We surface it rather than hide it.

A real bug — the one we still tell new contributors about

Right at the end of last year, the daily pipeline started crash-looping with this error:

asyncpg.exceptions.ForeignKeyViolationError:
  insert or update on table "observations" violates
  foreign key constraint "observations_country_iso_fkey"
  DETAIL: Key (country_iso)=(EAR) is not present in table "countries".

EAR is the World Bank ISO code for "Early-demographic dividend" — an aggregate region, not a country. Our _WB_AGGREGATE_CODES filter in worldbank.py was supposed to skip aggregate-region rows. But the filter list had 48 of the 79 aggregate codes the WB actually uses. Thirty-one were missing, and EAR appeared in their data for the first time on the day this broke.

The fix wasn't to add EAR. The fix was to query the WB's /country?format=json endpoint, filter to region.id == 'NA' (the WB's marker for "this is an aggregate, not a real country"), and use that as the canonical aggregate-codes list, refreshed automatically on every ingestion. PR [#15], one Python loop, ~20 minutes. Pipeline back up that evening.

The lesson — and this is now in our project memory for future contributors: never hard-code a list of values that the upstream owns. If the upstream's enumeration grows, you'll find out the hard way at 2am.

A handful of similar bugs over the months: an ECB rate that needed to be mirrored to all 21 euro-area countries (PR [#40]), a CPI deduplication issue that double-counted revisions (PR [#43]), a derived-recompute audit that skipped CPI source preference (PR [#44]). Same pattern in each case — a missing assumption surfaced as a foreign-key violation or a silently-drifting score. The PIT design and the row-count assertions catch most of these now.

Stack

Nothing exotic. The boring choices are doing the work:

ingestion         Python 3.11 / httpx / asyncio
orchestration     Dagster (31 assets, partitioned daily back to 2025-01-01)
warehouse         PostgreSQL 16 (managed, single instance, 11 tables, ~22M rows)
DB access         asyncpg
scoring           pure Python — math + numpy only, no DB inside scoring fns
NLP               CentralBankRoBERTa (FinBERT family) + Claude Haiku reconciliation
api               FastAPI + Pydantic
dashboard         Streamlit
deploy            Docker Swarm (homelab cluster, 5 nodes, daily image rolls via CI)
secrets           Docker Swarm secrets (DB password, GCP key, LiteLLM key)

Three deliberate constraints we've stuck with:

  1. Scoring functions are pure. No DB, no IO. The pipeline asset is the glue that fetches inputs, calls the function, stores the output. This makes scoring trivially testable and reproducible — python -c "from quantamentry.scoring import linex; print(linex.gap(cpi=3.2, target=2.0))" is a valid usage.
  2. Every numeric column is numeric, never float8. We learned the hard way that Decimal("3.625") - Decimal("3.26") and 0.365 = 3.625 - 3.26 (the float version) drift in ways that surface only after months of accumulated rounding. The cost of numeric is a few percent on storage and arithmetic. The benefit is no surprise drift.
  3. No vendor lock-in. Every source is fronted by a thin client we own. Swapping FRED for ALFRED, OXR for ECB-published rates, or IMF Datamapper for the IFS direct endpoint is a one-file change. We have not had to do any of these — but the option matters.

What's open

  • Source code for the scoring modules is intended to be open-sourced as a small library, separate from the pipeline. Composite credibility, LINEX gap, Taylor-rule deviation, communication stance — these are short, well-tested, and useful in isolation. The pipeline glue stays private.
  • The methodology page at quantamentry.com/methodology documents every dimension end-to-end. Every formula, every weight, every threshold.
  • The full data dictionary lists all 37 indicators with sources, frequencies, coverage, and known caveats.

What's not — and probably won't be

  • We won't add CDS spreads or sovereign yield premia from premium vendors. The economics don't work for our pricing — and FRED + ECB give us enough yield-curve data for the major countries.
  • We won't add intra-day data. The slowest moving piece of any of our composites updates monthly; intraday adds noise without signal.
  • We won't push to add 50 more countries from Trading Economics or CEIC. The 169-country boundary is set by what publicly available data covers honestly.

Coming soon

The Quantamentry API. REST, three pricing tiers between $49 and $499 per month, all the data above plus the credibility composites. Subscribe for the launch announcement and a 14-day trial when it opens.

Quantamentry, [publish date]