博客/Developer

在不引入外汇前视偏差的情况下回测本币计价的黄金

将结算后的XAU/USD日线K线与历史外汇观测值相结合,以本币回测黄金策略,避免无意中使用了在当时尚不可得的汇率。

Developer

即便标的交易完全相同,一项黄金策略在EUR、JPY、IDR、INR或TRY计价下也可能呈现不同面貌。以美元计价的黄金序列回答的是"黄金在美元下表现如何?"而本币计价的序列回答的是"这笔仓位对这位投资者而言实际值多少钱?"

这只是简单的算术,但历史数据很容易让人在这一点上出错。本指南将goldprice.dev提供的结算后XAU/USD日线K线,与exchangerate.dev提供的带日期的美元外汇观测值相结合。这里的关键规则是:每一个外汇数值都必须是模拟日期当天所发布的数值,绝不能是今天获取后再填入历史行的汇率。

本币计价的价格序列

对于以"本币兑每美元"报价的货币,按以下方式计算该货币下的黄金收盘价:

local_gold_close = gold_usd_close × usd_to_local_fx

例如,XAU/USD × USD/IDR 得到的是一盎司黄金的印尼盾价值。同样的方法适用于任何受支持的本币。

这是一个换算恒等式,而非对两个市场为何变动的解释。请将两条来源序列及其观测日期与推导出的本币价格一并保留,以确保结果可复现。

拉取结算后的黄金K线

请使用日线K线端点,而不是当前现货请求。K线按最新在前的顺序返回,价格为十进制字符串,当前正在形成的K线可能带有 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"

响应中使用 bar_start 表示每根日K线的UTC起始时间。请保留 closeis_closed 字段。你的套餐所对应的日线历史时间窗口依然适用,更长的时间范围可能需要通过 next_cursor 分页获取。

这个整整一年的请求示例使用了Pro版历史数据权限。免费密钥可请求30天的日线历史数据;在运行此请求前,请缩短时间窗口,或升级到具备所需历史深度的套餐。

对于纯美元策略,已有的OHLC回测指南是更简单的入门起点。本指南的其余部分之所以引入第二条时间序列,是因为报告货币本身也在变动。

一项经过验证的一年期核对

该方法并非纯理论。在共同结算的营业日 2025-07-102026-07-10,XAU/USD日线收盘价从 3323.81 升至 4119.172,即美元计价黄金收益率为 23.93%。在纳入USD/本币外汇变动后,同一仓位在本币计价下的收益率各不相同:

报告货币本币计价黄金收益率
EUR26.95%
JPY37.10%
IDR37.97%
INR37.76%
TRY45.50%

每一次外汇端点响应,都是确切日期、未经前向填充的 ecb_daily 观测值。这一计算是一次历史性的核对,而非预测、因果关系断言或投资建议。关于这些数字背后确切的黄金、外汇及交互分解,参见黄金上涨了,还是你的货币下跌了?

按每个日期当时已知的情况拉取外汇序列

对于多日的工作,exchangerate.dev的区间端点可在一次请求中返回每日观测值。这里的基准货币是USD,请求的货币是IDR,因此每个汇率都是每美元对应的印尼盾数。

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"

区间响应包含的是已发布的营业日行,而不是为每个周末或假日人为编造一行。在构建历史序列时,不要用后续的观测值填补这些缺失的日历日期。

阅读一次调用获取历史外汇时间序列了解区间响应,以及如何在不引入前视偏差的情况下回填外汇数据了解带日期观测值的规则。

用Python构建对齐的本币计价序列

下面的示例请求两条序列,保留结算后的黄金K线和未经前向填充的外汇行,然后按日期执行内连接。使用内连接是刻意为之:任一来源缺失的日期都不会被凭空捏造。

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 可保留黄金K线端点返回的十进制字符串数值的精度。仅在图表库需要浮点数时,才在展示层进行转换。

避免三个常见错误

1. 在历史行中使用今天的汇率

/v1/latest 用于获取当前值。它无法还原某个过去日期当时可知的汇率。历史工作请使用带日期或区间的外汇端点。

2. 把延续沿用的汇率当作新鲜观测值

某个外汇数值在假日可能仍可用于展示,但这并不意味着它是当天新发布的。is_forward_filled 字段使这一区别变得可见。请选择并明确记录你的模型是延续沿用这些数值,还是只使用双方同时发布的日期。本示例选择了后者。

3. 用产生信号的同一根K线收盘价来行动

如果一条移动平均线使用了某一天的收盘价,那么该收盘价在这一天结束前是不可得的。在将交易信号应用于收益之前,请将其向后移动一行。

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 状态,以及生成该序列的确切代码版本。正是这些字段,让日后的读者能够区分一次可复现的历史计算,与一次贴到旧电子表格里的当前换算。

若用于实时换算或面向消费者的本币计价黄金价格展示,请改用黄金换算端点。历史换算按设计是一次双序列计算:使用结算后的历史K线和与之对应的带日期外汇观测值。

相关指南

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 接口提供。