Skip to content

Implementation

How-To Guides

Point-in-Time Macro Backtesting: Keeping Look-Ahead Bias Out of FX Event Studies

Most macro backtests fail in the data, not the strategy: revised consensus columns, reference-period timestamps, and silently updated actuals all leak the future into the past. This guide walks through the point-in-time acceptance rule, how FXMacroData enforces it structurally, honest pre-release forecast coverage by event family, and a full forecast-to-actual join in Python.

Share article X LinkedIn Email
Pip, the FXMacroData robot mascot, flips through a tall stack of dated glowing snapshot cards in a time-archive room with clocks on the wall.

The quiet way macro backtests lie

Most macro event studies fail before a single trade is simulated. The failure is not in the strategy logic — it is in the data: a "consensus" column that was actually revised after the release, a forecast timestamped by reference period instead of publication time, or an actual value that silently reflects a later revision rather than the number the market saw at 8:30am. Each of these injects information from the future into the past, and the backtest quietly reports an edge that never existed.

The discipline that prevents this has one rule: a value may enter the study only if it was knowable before the event. For a forecast, that means its publication timestamp must precede the release timestamp. For an actual, it means using the release-timestamped first print, not today's revised series. This article walks through how to apply that rule to FX macro event research end to end — and, just as importantly, how to check what pre-release history actually exists before you design the study.


The acceptance rule, drawn out

Every forecast row served by the FXMacroData predictions API carries a generated_at timestamp, and every announcement carries an announcement_datetime. The acceptance test sits between them:

Point-in-time acceptance timeline

Before the release: usable

Forecasts with generated_at < announcement_datetime. Nowcasts, surveys, and projections published ahead of the print.

announcement_datetime

The release moment. The first print enters the information set here.

After the release: reject

Any forecast generated at or after the release is look-ahead and must not enter the study.

Takeaway: the test is a single timestamp comparison per forecast row.

With most data vendors you have to run that rejection yourself and hope the vendor's timestamps are honest. FXMacroData enforces it structurally: the prediction store refuses to persist any forecast generated at or after its announcement, so the rejected region of that timeline is empty by construction. Query with pre_release_only=true (the default), keep your own generated_at < announcement_datetime assertion as a belt-and-braces check, and it will simply never fire.


Step 1: check coverage before designing the study

Pre-release forecast history exists only where an official publisher has been producing forecasts for years. That makes coverage a property of the upstream source, not of any subscription tier — and it means an honest event study starts by checking depth per series, not by assuming a uniform consensus archive back to 2014. For the United States, the verified picture looks like this:

Event family Pre-release source Verified history Study suitability
CPI and Core CPI (y/y and m/m)Cleveland Fed nowcastEvery monthly release since the August 2013 reference periodFull event study, about 155 released months through August 2026
Unemployment rateFOMC SEP medians + NY Fed Survey of Market ExpectationsEvery year-end (December reference month) release since 2015, each with the SEP vintages published before it; NY Fed SME added from the 2023 year-endYear-end event studies with vintage paths; monthly releases uncovered
FOMC rate decisionAtlanta Fed Market Probability TrackerTen past meetings from September 2023 to September 2026, roughly three per yearPartial meeting coverage
Non-Farm Payrolls, earnings, PPI, retail sales—No historical event-specific market consensusNot backtestable on consensus

The same honesty applies across currencies: the forecast coverage matrix lists every currency and indicator pair that carries an external, source-labelled forecast feed, from the ECB's Survey of Professional Forecasters to the RBA's market economists' table. A pair that is absent from the matrix has no external forecast history, on any plan — designing a study around it wastes a week discovering what the matrix states in one row.

Rule of thumb: if a study needs N pre-release forecasts per year and the matrix shows a sparse cadence, change the study design — widen to more currencies, switch to the covered variant, or use the release itself (surprise vs. prior print) instead of surprise vs. consensus.

Step 2: pull forecasts and actuals, join on announcement_id

Both endpoints share a stable join key, announcement_id, in the form {currency}_{indicator}_{date}. The predictions side contributes the forecast, its source, and generated_at; the announcements side contributes the release-timestamped actual, the previous value, and the revision history. Always pass explicit dates and page through the results. Without dates, predictions default to a window of roughly the last six months and announcements return 20 rows per page, and even with dates a page holds at most limit rows, newest first. The CPI m/m join below spans about 155 releases, so a single page of 100 would silently drop 2013 to early 2018:

import requests

BASE = "https://api.fxmacrodata.com/v1"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}
PARAMS = {"frequency": "mom",
          "start_date": "2013-08-01", "end_date": "2026-08-31", "limit": 100}


def fetch_all(path, params):
    """Follow offset pagination until the endpoint reports no more rows."""
    rows, offset = [], 0
    while True:
        body = requests.get(f"{BASE}/{path}", params={**params, "offset": offset},
                            headers=HEADERS, timeout=30).json()
        rows.extend(body["data"])
        has_more = body.get("has_more") or body.get("pagination", {}).get("has_more")
        if not has_more or not body["data"]:
            return rows
        offset += len(body["data"])


preds = fetch_all("predictions/usd/inflation", {**PARAMS, "pre_release_only": "true"})
actuals = fetch_all("announcements/usd/inflation", {**PARAMS, "revisions": "all"})

actual_by_id = {row["announcement_id"]: row for row in actuals}
events = []
for group in preds:
    actual = actual_by_id.get(group["announcement_id"])
    if actual is None:
        continue
    first = actual["revisions"][0]  # earliest captured vintage
    known_at = first.get("epoch") or actual["announcement_datetime"]
    for p in group["predictions"]:
        # Belt-and-braces: the store already guarantees this holds.
        assert p["generated_at"] < group["announcement_datetime"]
        events.append({
            "release_utc": group["announcement_datetime"],
            "known_at": known_at,
            "forecast": p["predicted_value"],
            "source": p["prediction_source"],
            "actual_first_print": first["val"],
            "surprise": first["val"] - p["predicted_value"],
        })

Two details in that snippet carry the point-in-time weight. First, the actual comes from revisions[0], the earliest captured vintage, not from the latest value of the series. Each vintage carries its own epoch where one was recorded; when it is null, as on some recently captured rows, fall back to the row's announcement_datetime. Second, the m/m variant is selected with frequency=mom on both endpoints, so the forecast and the actual describe the same series rather than a y/y forecast being compared against an m/m print.


Step 3: decide what each row is allowed to know

Once events are joined, the remaining look-ahead risks are in your own feature engineering. A useful habit is to write the knowability budget down as a table before coding:

Input Knowable at event time? Correct field
Pre-release forecastYes, when generated_at precedes the releasepredictions[].predicted_value
Actual, as the market saw itYes, from the release timestamp onwardrevisions[0].val at announcement_datetime
Previous valueYes — it was released earlierprevious_value, previous_announcement_datetime
Revised actualNot until the revision's own epochLater revisions[] entries, gated by their epoch
A forecast generated after the eventNeverDoes not exist in the store

The revision entries deserve emphasis. Each carries its own epoch, which means revisions are not a contaminant to be avoided — they are additional point-in-time events. A study of how Federal Reserve communication responds to data can legitimately use a revised CPI print, as long as the row enters the information set at the revision's epoch rather than the original release date.


Where to go deeper

The compact reference version of this workflow — field tables for both endpoints, the pagination pattern for full-archive pulls, and access details — lives in the point-in-time backtesting guide. Series-level history start dates for every published pair are on the data coverage catalogue, and upcoming events to test forward are on the release calendar. USD announcements are open without a key for recent-window evaluation, and forecasts need a key on every currency; the full archive across every supported currency comes with the Individual plan — the same data on every paid tier, with no deeper archive hiding behind a higher price.

FXMacroData API data

Data endpoints used in this article

No FXMacroData API data endpoint is attributed to this article. Its evidence base is identified in the article and source links.

Explore the FXMacroData API reference

Frequently asked

Questions about this topic

When is a forecast valid for a point-in-time backtest?

Only when its generated_at timestamp is strictly before the announcement_datetime of the release it predicts. FXMacroData enforces this at the storage layer, so the predictions API cannot serve a look-ahead forecast row.

Which USD releases have historical pre-release forecast coverage?

CPI and Core CPI carry Cleveland Fed nowcasts for every monthly release from the August 2013 reference period onward, on both y/y and m/m series, about 155 events through August 2026. The unemployment rate has FOMC Summary of Economic Projections medians for every year-end release since 2015, with the New York Fed Survey of Market Expectations added from 2023. FOMC rate decisions have Atlanta Fed Market Probability Tracker readings for roughly three meetings a year since September 2023. Historical event-specific market consensus is not available for Non-Farm Payrolls, average hourly earnings, PPI, or retail sales.

How should the actual value be chosen for an event study?

Use the first print from the revisions array at its release timestamp, not the latest revised series value. Later revisions can be used too, but only from each revision's own epoch onward.

Keep reading

Blogroll

AI Answer-Ready

Key Facts

Page
Point-in-Time Macro Backtesting: Keeping Look-Ahead Bias Out of FX Event Studies
Section
Articles
Canonical URL
https://fxmacrodata.com/articles/point-in-time-macro-backtesting
Source
FXMacroData editorial and official publisher references
Last Updated
2026-10-05 23:23 UTC

Provenance And Trust

Cite the canonical URL and source field above. Where available, this page maps to official publisher releases and timestamped updates.

Quick Q&A

When is a forecast valid for a point-in-time backtest? Only when its generated_at timestamp is strictly before the announcement_datetime of the release it predicts. FXMacroData enforces this at the storage layer, so the predictions API cannot serve a look-ahead forecast row.

Which USD releases have historical pre-release forecast coverage? CPI and Core CPI carry Cleveland Fed nowcasts for every monthly release from the August 2013 reference period onward, on both y/y and m/m series, about 155 events through August 2026. The unemployment rate has FOMC Summary of Economic Projections medians for every year-end release since 2015, with the New York Fed Survey of Market Expectations added from 2023. FOMC rate decisions have Atlanta Fed Market Probability Tracker readings for roughly three meetings a year since September 2023. Historical event-specific market consensus is not available for Non-Farm Payrolls, average hourly earnings, PPI, or retail sales.

How should the actual value be chosen for an event study? Use the first print from the revisions array at its release timestamp, not the latest revised series value. Later revisions can be used too, but only from each revision's own epoch onward.

Prompt Packs

Use these in ChatGPT, Claude, Gemini, Mistral, Perplexity, or Grok for consistent source-aware outputs.

Share page X LinkedIn Email