Guide/Backfill guide

How to backfill FX rates without look-ahead bias

When you reconstruct a historical EUR/USD series for analytics or backtesting, the rate you query must be the one that was knowable on each simulated date, not a value published later. Here is the pattern.

ERexchangerate.dev·Jun 19, 2026·6 min read

Use GET /v1/{date}/{base} to retrieve the rate as published on a specific past date, going back to 1999. The /v1/latest endpoint always returns today's value, so it has no place in a historical series. Check `is_forward_filled` on every row: when it is true, no rate was published that day and the prior value was carried forward.

Key points
Query /v1/{date}/{base} for historical work. Never use /v1/latest, which returns today's value regardless of date.
is_forward_filled: true means no rate was published on that date (weekend or public holiday); the prior value is carried.
Daily history runs back to 1999, covering ECB and Federal Reserve series.
Look-ahead bias is a data-correctness problem: it means using a value that was not knowable on the date being simulated.
Rates are indicative, published for reference and analytics, not for settlement or dealing.

What look-ahead bias means for FX data

Look-ahead bias occurs when your historical series contains a value that was not actually published on the date you are simulating. With FX rates this is easy to introduce accidentally: if you query /v1/latest/USD today and store the result against a past date, you are using today's rate in a historical slot. The data will appear self-consistent but it describes a world that did not exist on that date.

The fix is to query each date individually via the point-in-time path. The response reflects what was published that day, not a later revision.

The dated endpoint

Put the date in the path: GET /v1/{date}/{base}. The date must be ISO 8601 (YYYY-MM-DD) and the base is a three-letter currency code.

shell · request a specific datecopy
$ curl -s https://api.exchangerate.dev/v1/2026-03-14/USD \
    -H "Authorization: Bearer exr_live_..."

The response shape is the same as /v1/latest with one addition: is_forward_filled. When that field is true, no rate was published on the requested date and the API returned the most recent prior value.

Reading is_forward_filled

For reference-rate sources (ECB and Federal Reserve), rates are only published on business days. Requesting a Saturday, Sunday, or public holiday returns the last published value with is_forward_filled: true. The flag lets you treat those rows deliberately rather than silently treating a carried value as a freshly published one.

ApproachNaive /v1/latest pullDated /v1/{date}/{base} pull
Historical dateReturns today's rate stored against a past slotReturns the rate published on that exact date
Weekend or holidayCarries prior value with no signalCarries prior value and sets is_forward_filled: true
DetectabilityBias is silent; no field indicates itForward-fill is explicit; you can filter or flag

Python: looping dates to build a series

The example below requests USD rates for a week, printing each date's EUR rate and whether it was forward-filled. Throttle to stay within the 12-request-per-minute free-tier limit.

python · build a dated FX seriescopy
import time
from datetime import date, timedelta
import requests

API = "https://api.exchangerate.dev/v1"
KEY = "exr_live_..."   # free key: https://exchangerate.dev/signup

start = date(2026, 6, 9)
end   = date(2026, 6, 15)

current = start
while current <= end:
    r = requests.get(
        f"{API}/{current}/USD",
        headers={"Authorization": f"Bearer {KEY}"},
        timeout=10,
    )
    r.raise_for_status()
    data = r.json()

    eur  = data["rates"]["EUR"]
    ffwd = data.get("is_forward_filled", False)
    print(f"{current}  EUR={eur}  forward_filled={ffwd}")

    current += timedelta(days=1)
    time.sleep(5)  # stay under 12 req/min (free tier)

On June 14 (Saturday) and June 15 (Sunday) you will see forward_filled=True. Both are non-publication days for reference-rate sources.

Handling forward-filled rows
A common pattern is to carry the flag alongside each rate in your datastore, then filter or annotate in your analysis layer. Dropping forward-filled rows silently creates gaps; keeping them without the flag conflates carried values with published ones.

Cross-checking with data_updated_at

The response also carries data_updated_at, the timestamp when the underlying rate was last written. On a forward-filled day this will point to the previous business day's publication time, giving you an additional signal about the age of the value you received.

python · log data freshnesscopy
data = r.json()
print(data["data_updated_at"])   # e.g. 2026-06-13T16:15:00Z on a weekend date
print(data["is_forward_filled"]) # True

A note on rate type

Indicative, not for settlement
All rates are indicative, published for reference, analytics, and display. They are not a dealing quote and should not be used to settle a transaction. The notice field on every response states this explicitly.
ER
exchangerate.dev
Data guides for quantitative developers.

Keep reading

GuideHistorical FX rates and time series in one callRead ReferenceReading source and market_sessionRead BlogWhat ECB-based FX APIs miss between fixesRead
More ComparisonsFixer vs exchangerate.devOpen Exchange Rates vs exchangerate.devCurrencylayer vs exchangerate.devCurrencylayer vs ExchangeRate-API
LearnReading source and market_session in your pipelineIndicative vs executable FX rates: what a rates API actually gives youECB reference rates, explained
Live RatesEUR/USDGBP/USDUSD/JPY