ブログ/Developer

為替のルックアヘッド・バイアスを避けて現地通貨建てゴールドをバックテストする

確定済みのXAU/USD日次バーと過去のFX観測値を組み合わせ、当時は入手できなかったレートを誤って使うことなく、現地通貨建てのゴールド戦略をテストする方法。

Developer

同じ取引であっても、金の戦略はEUR、JPY、IDR、INR、TRYの各通貨で見え方が異なることがある。USD建てのゴールド・シリーズが答えるのは「ドルで見て金はどう動いたか」であり、現地通貨建てのシリーズが答えるのは「このポジションはこの投資家にとってどれだけの価値があったか」である。

この違い自体は単純な算術だが、過去データを扱うと間違えやすい。本ガイドでは、goldprice.devの確定済みXAU/USD日次バーと、exchangerate.devの日付付きUSD為替観測値を組み合わせる。重要なルールは、各FX値がシミュレーション対象日に公表された値でなければならず、今日取得したレートを過去の行に置いてはならないという点だ。

現地通貨建て価格系列

現地通貨 per USD として建値される通貨については、その通貨での金の終値を次のように計算する:

local_gold_close = gold_usd_close × usd_to_local_fx

例えば、XAU/USD × USD/IDR は金1トロイオンスのルピア建て価値を算出する。同じ手法は、対応している他の現地通貨にも適用できる。

これは換算上の恒等式であり、どちらの市場がなぜ動いたかを説明するものではない。結果を再現可能にするため、導出した現地通貨建て価格とあわせて、二つの元系列とその観測日を保持しておくこと。

確定済みのゴールド・バーを取得する

現在のスポット取得ではなく、日次バーのエンドポイントを使う。バーは新しい順に返され、価格は10進数の文字列で、現在形成中のバーは 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 で表す。closeis_closed を保持しておくこと。プランごとの日次履歴の範囲制限は引き続き適用され、より長い期間を取得する場合は next_cursor によるページングが必要になることがある。

この1年分のリクエストはProプランの履歴を利用する。無料キーでは30日分の日次履歴しか取得できないため、実行前に期間を短くするか、必要な履歴深度を持つプランを用意すること。

USD建てのみの戦略であれば、既存のOHLCバックテストガイドのほうがよりシンプルな出発点になる。本ガイドの残りの部分では、報告通貨そのものが変動するため、第二の時系列を追加している。

検証済みの1年間チェック

この手法は理論上のものにとどまらない。共通の確定済み営業日である 2025-07-102026-07-10 の間で、XAU/USDの日次終値は 3323.81 から 4119.172 へと上昇し、USD建てゴールドのリターンは**23.93%**だった。同じポジションでも、USD/現地通貨のFX変動を含めると、現地通貨建てのリターンは異なるものになる:

報告通貨現地通貨建てゴールド・リターン
EUR26.95%
JPY37.10%
IDR37.97%
INR37.76%
TRY45.50%

各FXエンドポイントのレスポンスは、正確な日付に対応した、繰越補完(フォワードフィル)されていない ecb_daily の観測値だった。この計算は過去データの照合であり、予測でも因果の主張でも投資推奨でもない。これらの数値の背後にある、金・FX・相互作用の厳密な分解については、金が上がったのか、それとも自国通貨が下がったのか?を参照のこと。

各日付の時点で判明していたFX系列を取得する

複数日にまたがる作業では、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レスポンスについては一回の呼び出しで取得する過去のFX時系列を、日付付き観測値のルールについてはルックアヘッド・バイアスなしでFXをバックフィルする方法を参照のこと。

Pythonで整合の取れた現地通貨建て系列を構築する

この例では両方の系列を取得し、確定済みの金バーと、繰越補完されていないFXの行のみを残したうえで、日付を基準にインナー結合を行う。インナー結合であることは意図的なもので、どちらかのソースに欠けている日付を黙って作り出すことはしない。

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 を使うことで、金バーのエンドポイントが返す10進数文字列の値をそのまま保持できる。チャートライブラリがfloatを必要とする場合のみ、表示の直前で変換すること。

よくある3つの間違いを避ける

1. 過去の行に今日のレートを使ってしまう

/v1/latest は現在値のためのものだ。過去のある日に知り得たレートを再構築することはできない。過去データを扱う作業には、日付指定またはrangeのFXエンドポイントを使うこと。

2. 繰り越されたレートを新しい観測値として扱ってしまう

FX値は、祝日の表示用としては有効であっても、その日に新たに公表されたものではない場合がある。is_forward_filled フィールドはその違いを可視化する。自分のモデルがそうした値を繰り越すのか、それとも両方が公表されている日付のみに限定するのかを選び、明記すること。この例では後者を選んでいる。

3. シグナルを生んだのと同じ終値でそのシグナルに基づいて行動してしまう

移動平均がある日の終値を使う場合、その終値はその日が終わるまでは入手できなかったものである。リターンに適用する前に、トレーディング・シグナルを1行分シフトさせること。

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 の状態、FXの is_forward_filled の状態、そしてその系列を生成した正確なコードのリビジョンを保存すること。これらの項目があってはじめて、後から読む人が、再現可能な過去の計算なのか、それとも古いスプレッドシートに貼り付けられた現在時点の換算値なのかを見分けられる。

リアルタイムの換算や、消費者向けの現地通貨建て価格が必要な場合は、代わりにゴールド換算エンドポイントを使うこと。過去データの換算は設計上、二つの系列による計算であり、確定済みの過去バーと、それに対応する日付付きのFX観測値を使う。

関連ガイド

Developer

Gold price WebSocket on Reddit: the streaming questions, answered

The real-time gold streaming questions that come up on Reddit, answered: whether a gold WebSocket exists, how the goldprice.dev XAU and XAG tick stream works end to end, and which other APIs actually offer one (most metals APIs just poll).

読む →
Developer

Gold price API on Stack Overflow: developer troubleshooting, answered

The gold-price-API errors developers hit on Stack Overflow, fixed: 401 auth, 400 invalid_symbol, 429 rate limits, CORS from the browser, reading the response, and pulling historical data.

読む →
Developer

Gold price API on Reddit: the developer questions, answered

The gold-price-API questions that come up on Reddit, answered straight: free tier, update frequency, rate limits, auth, where the number comes from, commercial use, and whether it's good enough to build on.

読む →

goldprice.dev

リアルタイムの金価格、ヒストリカルOHLC、マルチソース集計 — REST・SSE経由で提供。