LearnGuide

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.

19 Jun 2026 · 6 min read

Use GET /v1/{date}/{base} for a past date’s published rate; Plus and Pro reach 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.

  • 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.
  • ECB daily history covers about 30 currencies since 1999.
  • 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}. Use a Basic or higher key for this example. Dates use ISO 8601 (YYYY-MM-DD); the base is a three-letter currency code.

shell · request a specific date
$ 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

ECB reference 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. Use a Basic or higher key for this older window.

python · build a dated FX series
import timefrom datetime import date, timedeltaimport requestsAPI = "https://api.exchangerate.dev/v1"KEY = "exr_live_..."   # Basic or higher: https://exchangerate.dev/pricingstart = date(2026, 6, 9)end   = date(2026, 6, 15)current = startwhile 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(6)  # 10 req/min, within Basic’s 60 req/min

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 freshness
data = r.json()print(data["data_updated_at"])   # e.g. 2026-06-13T16:15:00Z on a weekend dateprint(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.

Questions

What is look-ahead bias in FX data?

Look-ahead bias occurs when a historical series contains a rate that was not published on the date being simulated. Storing today's rate in a past slot is a common example. The data appears consistent but describes values that were not knowable on those dates. It is a data-correctness problem, not a strategy problem.

How do I request the rate as it was known on a specific past date?

Call GET /v1/{date}/{base} with an ISO date, such as /v1/2026-03-14/USD. Use Basic or higher for that date; Plus and Pro history reaches 1999.

What does is_forward_filled mean?

When is_forward_filled is true, no rate was published on the requested date. This happens on weekends and public holidays for ECB reference rates. The API returns the most recent prior value and sets the flag so you can detect and handle those rows deliberately.

Can I retrieve the whole series in one call instead of looping dates?

Yes. The /v1/range endpoint accepts start_date and end_date query parameters and returns the published daily rows in that window in a single response, each with its own is_forward_filled. For large date ranges that is more efficient than looping individual date requests.

Keep reading

Start with a free API key

5,000 requests a month on the free plan. No credit card.

Get a free key