Una estrategia de oro puede verse distinta en EUR, JPY, IDR, INR o TRY incluso cuando la operación subyacente es la misma. Una serie de oro en USD responde: «¿qué hizo el oro en dólares?». Una serie en moneda local responde: «¿cuánto habría valido esta posición para este inversor?».
Esa distinción es aritmética simple, pero los datos históricos pueden hacer que sea fácil equivocarse. Esta guía combina barras diarias liquidadas de XAU/USD de goldprice.dev con observaciones de FX de USD fechadas de exchangerate.dev. La regla importante es que cada valor de FX debe ser el valor publicado para la fecha simulada, nunca una tasa obtenida hoy y colocada en una fila histórica.
La serie de precios en moneda local
Para una moneda cotizada como moneda local por USD, calcula el cierre del oro en esa moneda como:
local_gold_close = gold_usd_close × usd_to_local_fx
Por ejemplo, XAU/USD × USD/IDR produce el valor en rupias de una onza troy de oro. El mismo enfoque aplica a cualquier moneda local soportada.
Esta es una identidad de conversión, no una explicación de por qué se movió alguno de los dos mercados. Conserva las dos series fuente y sus fechas de observación junto con el precio local derivado para que el resultado sea reproducible.
Obtén las barras de oro liquidadas
Usa el endpoint de barras diarias, no una solicitud de spot actual. Las barras se devuelven de más reciente a más antigua, los precios son cadenas decimales, y la barra que se está formando actualmente puede tener is_closed: false. Un backtest debe usar solo filas liquidadas.
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"
La respuesta usa bar_start para el inicio UTC de cada barra diaria. Conserva close e is_closed. La ventana de historial diario de tu plan sigue aplicando, y un rango más largo puede requerir paginar mediante next_cursor.
Esta solicitud exacta de un año usa historial Pro. Las claves gratuitas pueden solicitar 30 días de historial diario; usa una ventana más corta o un plan con la profundidad de historial requerida antes de ejecutarla.
Para una estrategia solo en USD, la guía existente de backtesting con OHLC es el punto de partida más simple. El resto de esta guía añade una segunda serie temporal porque la propia moneda de referencia se mueve.
Una verificación validada de un año
El método no es solo teórico. En las fechas hábiles liquidadas compartidas 2025-07-10 y 2026-07-10, el cierre diario de XAU/USD subió de 3323.81 a 4119.172: un retorno del oro en USD del 23.93%. La misma posición tuvo retornos distintos en moneda local una vez incluido el movimiento de FX USD/local:
| Moneda de referencia | Retorno del oro en moneda local |
|---|
| EUR | 26.95% |
| JPY | 37.10% |
| IDR | 37.97% |
| INR | 37.76% |
| TRY | 45.50% |
Cada respuesta del endpoint de FX fue una observación ecb_daily de fecha exacta, no rellenada hacia adelante. El cálculo es una reconciliación histórica, no un pronóstico, una afirmación causal ni una recomendación de inversión. Para ver la descomposición exacta de oro, FX e interacción detrás de estas cifras, consulta ¿Subió el oro, o bajó tu moneda?.
Obtén la serie de FX tal como se conocía en cada fecha
Para trabajo de varios días, el endpoint de rango de exchangerate.dev devuelve las observaciones diarias en una sola solicitud. Aquí la base es USD y la moneda solicitada es IDR, así que cada tasa es rupias por dólar.
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"
La respuesta de rango contiene filas de días hábiles publicados en lugar de fabricar una fila de calendario para cada fin de semana o feriado. No rellenes esas fechas de calendario faltantes con una observación posterior al construir la serie histórica.
Lee serie temporal histórica de FX en una sola llamada para la respuesta de rango y cómo rellenar FX históricamente sin sesgo de look-ahead para la regla de observación fechada.
Construye una serie alineada en moneda local con Python
Este ejemplo solicita ambas series, conserva las barras de oro liquidadas y las filas de FX no rellenadas hacia adelante, y luego realiza un inner join por fecha. El inner join es intencional: una fecha faltante en cualquiera de las dos fuentes no se inventa silenciosamente.
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())
El uso de Decimal preserva los valores de cadena decimal que devuelve el endpoint de barras de oro. Convierte solo en el borde de presentación si una librería de gráficos necesita floats.
Evita tres errores comunes
1. Usar la tasa de hoy en una fila histórica
/v1/latest es para un valor actual. No puede reconstruir una tasa que era conocible en un día pasado. Usa los endpoints de FX fechados o de rango para trabajo histórico.
2. Tratar una tasa arrastrada como una observación nueva
Un valor de FX puede ser válido para mostrarse en un feriado sin haber sido publicado de nuevo ese día. El campo is_forward_filled hace visible esa distinción. Elige y documenta si tu modelo arrastra esos valores o se restringe a las fechas publicadas conjuntamente. Este ejemplo elige lo segundo.
3. Actuar sobre el mismo cierre que produjo una señal
Si una media móvil usa el cierre de un día, ese cierre no estaba disponible antes de que terminara el día. Desplaza la señal de trading una fila antes de aplicarla a los retornos.
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()
Ese desplazamiento elimina una fuente separada de sesgo de look-ahead. No hace que la estrategia sea rentable, y no tiene en cuenta spreads, impuestos, almacenamiento, ejecución, ni ninguna prima del mercado local.
Registra las entradas junto con el resultado
En cada ejecución, guarda la ventana de fechas, la moneda, las marcas de tiempo de respuesta de la API, el estado is_closed, el estado is_forward_filled de FX, y la revisión exacta de código que generó la serie. Esos campos son lo que permite a un lector posterior distinguir un cálculo histórico reproducible de una conversión actual pegada en una hoja de cálculo antigua.
Para una conversión en vivo o un precio en moneda local orientado al consumidor, usa en su lugar el endpoint de conversión de oro. La conversión histórica es, por diseño, un cálculo de dos series: usa barras históricas liquidadas y las observaciones de FX fechadas que les corresponden.