동일한 거래라도 금 전략은 EUR, JPY, IDR, INR, TRY로 표시했을 때 다르게 보일 수 있다. USD 금 시계열은 "달러로 금이 어떻게 움직였는가"에 답한다. 반면 현지 통화 시계열은 "이 포지션이 이 투자자에게 실제로 얼마의 가치였는가"에 답한다.
이 구분은 단순한 산술이지만, 과거 데이터를 다루다 보면 쉽게 틀리기 마련이다. 이 가이드는 goldprice.dev의 확정된 XAU/USD 일별 바와 exchangerate.dev의 날짜가 명시된 USD 환율 관측치를 결합한다. 중요한 규칙은 각 환율 값이 반드시 시뮬레이션 대상 날짜에 발표된 값이어야 하며, 오늘 가져온 환율을 과거 행에 끼워 넣어서는 안 된다는 것이다.
현지 통화 가격 시계열
1달러당 현지 통화로 고시되는 통화의 경우, 해당 통화 기준 금 종가는 다음과 같이 계산한다:
local_gold_close = gold_usd_close × usd_to_local_fx
예를 들어 XAU/USD × USD/IDR은 금 1트로이온스의 루피아 가치를 산출한다. 동일한 방식이 지원되는 모든 현지 통화에 적용된다.
이는 환산 항등식일 뿐, 두 시장 중 어느 쪽이 왜 움직였는지에 대한 설명이 아니다. 결과를 재현할 수 있도록 두 원천 시계열과 그 관측 날짜를 산출된 현지 가격과 함께 보관하라.
확정된 금 바 가져오기
현재 스팟 요청이 아니라 일별 바 엔드포인트를 사용하라. 바는 최신 순으로 반환되고, 가격은 소수 문자열이며, 현재 형성 중인 바는 is_closed: false일 수 있다. 백테스트에는 확정된 행만 사용해야 한다.
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"
응답에서는 각 일별 바의 UTC 시작 시각을 bar_start로 표시한다. close와 is_closed를 보존하라. 요금제의 일별 히스토리 기간 제한은 여전히 적용되며, 더 긴 범위를 조회하려면 next_cursor를 통한 페이징이 필요할 수 있다.
위와 같은 1년치 요청에는 Pro 히스토리가 필요하다. 무료 키는 30일치 일별 히스토리만 요청할 수 있으므로, 실행하기 전에 더 짧은 기간을 사용하거나 필요한 히스토리 깊이를 갖춘 요금제로 전환하라.
USD 단일 통화 전략이라면 기존 OHLC 백테스트 가이드가 더 간단한 출발점이다. 이 가이드의 나머지 부분은 보고 통화 자체가 움직이기 때문에 두 번째 시계열을 추가한다.
검증된 1년치 확인
이 방법은 이론에 그치지 않는다. 공통으로 확정된 영업일인 2025-07-10과 2026-07-10 사이, XAU/USD 일별 종가는 3323.81에서 4119.172로 상승해 USD 기준 금 수익률 **23.93%**를 기록했다. 동일한 포지션이라도 USD/현지 통화 환율 변동을 반영하면 현지 통화 기준 수익률은 달라진다:
| 표시 통화 | 현지 통화 기준 금 수익률 |
|---|
| EUR | 26.95% |
| JPY | 37.10% |
| IDR | 37.97% |
| INR | 37.76% |
| TRY | 45.50% |
각 환율 엔드포인트 응답은 정확한 날짜의, 이월 보정(forward-fill)되지 않은 ecb_daily 관측치였다. 이 계산은 과거 데이터를 정합시킨 결과일 뿐, 예측이나 인과 주장, 투자 권유가 아니다. 이 수치를 이루는 금, 환율, 상호작용 분해에 대한 정확한 내역은 금이 오른 것인가, 당신의 통화가 내린 것인가?를 참고하라.
각 날짜 시점에 알려진 환율 시계열 가져오기
여러 날에 걸친 작업이라면 exchangerate.dev의 range 엔드포인트가 한 번의 요청으로 일별 관측치를 반환한다. 여기서는 기준 통화가 USD이고 요청 통화가 IDR이므로, 각 환율은 1달러당 루피아 값이다.
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"
range 응답에는 모든 주말과 공휴일에 대해 행을 만들어내는 대신, 실제로 발표된 영업일 행만 담긴다. 과거 시계열을 구성할 때 이렇게 비어 있는 달력 날짜를 이후 관측치로 채워 넣지 마라.
range 응답에 대해서는 한 번의 호출로 보는 과거 환율 시계열을, 날짜 지정 관측 규칙에 대해서는 선견 편향 없이 환율을 백필하는 방법을 참고하라.
Python으로 정렬된 현지 통화 시계열 구성하기
이 예제는 두 시계열을 모두 요청하고, 확정된 금 바와 이월 보정되지 않은 환율 행만 유지한 다음, 날짜 기준으로 내부 조인(inner join)을 수행한다. 내부 조인은 의도적인 선택이다. 어느 한쪽 원천에 없는 날짜를 조용히 만들어내지 않기 위해서다.
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())
Decimal을 사용하는 이유는 금 바 엔드포인트가 반환하는 소수 문자열 값을 그대로 보존하기 위해서다. 차트 라이브러리가 float를 요구하는 경우에만 표시 단계에서 변환하라.
흔한 실수 세 가지 피하기
1. 과거 행에 오늘의 환율을 사용하는 것
/v1/latest는 현재 값을 위한 것이다. 과거 특정일에 알 수 있었던 환율을 재구성할 수는 없다. 과거 데이터 작업에는 날짜 지정 또는 range 환율 엔드포인트를 사용하라.
2. 이월된 환율을 새로운 관측치로 취급하는 것
환율 값은 공휴일에 표시하기에는 유효하더라도 그날 새로 발표된 것은 아닐 수 있다. is_forward_filled 필드가 이 구분을 드러내 준다. 모델이 이런 값을 이월해서 사용할지, 아니면 두 시계열이 공통으로 발표된 날짜로 한정할지를 선택하고 문서화하라. 이 예제는 후자를 선택한다.
3. 신호를 만든 것과 같은 종가로 매매하는 것
이동평균이 어느 날의 종가를 사용한다면, 그 종가는 그날이 끝나기 전에는 알 수 없었다. 매매 신호를 수익률에 적용하기 전에 한 행만큼 시프트하라.
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()
이 시프트는 또 다른 선견 편향의 원인을 제거할 뿐이다. 전략을 수익성 있게 만들어주지는 않으며, 스프레드, 세금, 보관 비용, 체결, 현지 시장 프리미엄 등은 전혀 반영하지 않는다.
입력값을 결과와 함께 기록하기
실행할 때마다 날짜 범위, 통화, API 응답 타임스탬프, is_closed 상태, 환율의 is_forward_filled 상태, 그리고 해당 시계열을 생성한 정확한 코드 리비전을 저장하라. 이런 필드들이 있어야 나중에 읽는 사람이, 재현 가능한 과거 계산과 오래된 스프레드시트에 붙여 넣은 현재 시점 환산값을 구분할 수 있다.
실시간 환산이나 소비자에게 보여줄 현지 통화 가격이 필요하다면 대신 금 환산 엔드포인트를 사용하라. 과거 데이터 환산은 설계상 두 시계열 계산이다. 확정된 과거 바와 그에 대응하는 날짜 지정 환율 관측치를 사용하라.