A gold strategy can look different in EUR, JPY, IDR, INR, or TRY even when the underlying trade is the same. A USD gold series answers, "what did gold do in dollars?" A local-currency series answers, "what would this position have been worth to this investor?"
That distinction is simple arithmetic, but historical data can make it easy to get wrong. This guide combines settled daily XAU/USD bars from goldprice.dev with dated USD FX observations from exchangerate.dev. The important rule is that each FX value must be the value published for the simulated date, never a rate fetched today and placed in a historical row.
The local-currency price series
For a currency quoted as local currency per USD, calculate the gold close in that currency as:
local_gold_close = gold_usd_close × usd_to_local_fx
For example, XAU/USD × USD/IDR produces the rupiah value of one troy ounce of gold. The same approach applies to any supported local currency.
This is a conversion identity, not an explanation of why either market moved. Keep the two source series and their observation dates alongside the derived local price so that the result is reproducible.
Pull settled gold bars
Use the daily bars endpoint, not a current spot request. Bars are returned newest first, prices are decimal strings, and the currently forming bar can have is_closed: false. A backtest should use settled rows only.
curl -G "https://api.goldprice.dev/v1/bars" \
-H "Authorization: Bearer $GOLDPRICE_API_KEY" \
--data-urlencode "symbol=XAU-USD-SPOT" \
--data-urlencode "interval=1d" \
--data-urlencode "from=2025-07-10T00:00:00Z" \
--data-urlencode "to=2026-07-10T00:00:00Z" \
--data-urlencode "limit=400"
The response uses bar_start for the UTC start of each daily bar. Retain close and is_closed. Your plan's daily-history window still applies, and a longer range can require paging through next_cursor.
This exact one-year request uses Pro history. Free keys can request 30 days of daily history; use a shorter window or a plan with the required history depth before running it.
For a USD-only strategy, the existing OHLC backtesting guide is the simpler starting point. The remainder of this guide adds a second time series because the reporting currency itself moves.
A validated one-year check
The method is not just theoretical. On the shared, settled business dates 2025-07-10 and 2026-07-10, the XAU/USD daily close rose from 3323.81 to 4119.172: a 23.93% USD-gold return. The same position had different local-currency returns once the USD/local FX move was included:
| Reporting currency | Local-currency gold return |
|---|
| EUR | 26.95% |
| JPY | 37.10% |
| IDR | 37.97% |
| INR | 37.76% |
| TRY | 45.50% |
Each FX endpoint response was an exact-date, non-forward-filled ecb_daily observation. The calculation is a historical reconciliation, not a forecast, causal claim, or investment recommendation. For the exact gold, FX, and interaction decomposition behind these figures, see Was gold up, or was your currency down?.
Pull the FX series as it was known on each date
For multi-day work, exchangerate.dev's range endpoint returns the daily observations in one request. Here the base is USD and the requested currency is IDR, so each rate is rupiah per dollar.
curl -G "https://api.exchangerate.dev/v1/range" \
-H "Authorization: Bearer $EXCHANGERATE_API_KEY" \
--data-urlencode "base=USD" \
--data-urlencode "symbols=IDR" \
--data-urlencode "start_date=2025-07-10" \
--data-urlencode "end_date=2026-07-10"
The range response contains published business-day rows rather than manufacturing a calendar row for every weekend or holiday. Do not fill those missing calendar dates with a later observation when building the historical series.
Read historical FX time series in one call for the range response and how to backfill FX without look-ahead bias for the dated-observation rule.
Build an aligned local-currency series in Python
This example requests both series, keeps settled gold bars and non-forward-filled FX rows, then performs an inner join on the date. An inner join is intentional: a date missing from either source is not silently invented.
import os
from decimal import Decimal
import pandas as pd
import requests
GOLD_API = "https://api.goldprice.dev/v1"
FX_API = "https://api.exchangerate.dev/v1"
START = "2025-07-10"
END = "2026-07-10"
LOCAL_CURRENCY = "IDR"
# This one-year example uses Pro history. Free keys can request 30 days.
def fetch_gold_daily(start: str, end: str) -> pd.DataFrame:
response = requests.get(
f"{GOLD_API}/bars",
headers={"Authorization": f"Bearer {os.environ['GOLDPRICE_API_KEY']}"},
params={
"symbol": "XAU-USD-SPOT",
"interval": "1d",
"from": f"{start}T00:00:00Z",
"to": f"{end}T00:00:00Z",
"limit": 400,
},
timeout=15,
)
response.raise_for_status()
rows = response.json()["bars"]
gold = pd.DataFrame(rows)
gold = gold[gold["is_closed"] & gold["close"].notna()].copy()
gold["date"] = pd.to_datetime(gold["bar_start"], utc=True).dt.date
gold["gold_usd_close"] = gold["close"].map(Decimal)
return gold.set_index("date")[["gold_usd_close"]].sort_index()
def fetch_fx_daily(start: str, end: str, currency: str) -> pd.DataFrame:
response = requests.get(
f"{FX_API}/range",
headers={"Authorization": f"Bearer {os.environ['EXCHANGERATE_API_KEY']}"},
params={
"base": "USD",
"symbols": currency,
"start_date": start,
"end_date": end,
},
timeout=15,
)
response.raise_for_status()
# The range endpoint contains published business-day rows only. Keep its
# missing weekends and holidays missing rather than carrying a later rate.
fx = pd.DataFrame(response.json()["data"])
fx["date"] = pd.to_datetime(fx["date"]).dt.date
fx["usd_to_local"] = fx["rates"].map(lambda rates: Decimal(str(rates[currency])))
return fx.set_index("date")[["usd_to_local"]].sort_index()
gold = fetch_gold_daily(START, END)
fx = fetch_fx_daily(START, END, LOCAL_CURRENCY)
# Only use dates with an actual settled gold close and a fresh FX observation.
series = gold.join(fx, how="inner")
series["local_gold_close"] = series["gold_usd_close"] * series["usd_to_local"]
series["local_return"] = series["local_gold_close"].pct_change()
print(series.tail())
The use of Decimal preserves the decimal-string values returned by the gold bars endpoint. Convert only at the presentation edge if a charting library needs floats.
Avoid three common mistakes
1. Using today's rate in a historical row
/v1/latest is for a current value. It cannot reconstruct a rate that was knowable on a past day. Use the dated or range FX endpoints for historical work.
2. Treating a carried rate as a fresh observation
An FX value can be valid for display on a holiday while still not being newly published that day. The is_forward_filled field makes that distinction visible. Choose and document whether your model carries those values or restricts itself to jointly published dates. This example chooses the latter.
3. Acting on the same close that produced a signal
If a moving average uses a day's close, that close was not available before the day ended. Shift the trading signal by one row before applying it to returns.
series["fast_ma"] = series["local_gold_close"].rolling(50).mean()
series["slow_ma"] = series["local_gold_close"].rolling(200).mean()
series["signal"] = (series["fast_ma"] > series["slow_ma"]).astype(int)
# A signal formed from today's close may only affect the next row's return.
series["strategy_return"] = series["local_return"] * series["signal"].shift(1)
series["strategy_value"] = (1 + series["strategy_return"].fillna(0)).cumprod()
That shift removes a separate source of look-ahead bias. It does not make the strategy profitable, and it does not account for spreads, taxes, storage, execution, or any local-market premium.
For every run, save the date window, currency, API response timestamps, is_closed status, FX is_forward_filled status, and the exact code revision that generated the series. Those fields are what let a later reader distinguish a reproducible historical calculation from a current conversion pasted into an old spreadsheet.
For a live conversion or a consumer-facing local-currency price, use the gold conversion endpoint instead. Historical conversion is a two-series calculation by design: use settled historical bars and the dated FX observations that belong to them.