通过 API 获取黄金和白银历史价格:25 年每日数据
使用 /historical、/timeframe、/change 和 /ohlc 获取 2000 年以来的黄金和白银每日历史价格,按年分页拉取长区间数据,并在本地存储和绘制图表。
可用的历史数据
MetalAPI 为每个品种每天保存一条日线数据,包含开盘价、最高价、最低价和收盘价。对黄金和白银而言,这意味着超过 25 年的每日数据:足以绘制长期走势图、用历史数据检验定价规则,或按当日价格重新估值旧发票。
| 类别 | 品种 | 每日历史数据起始时间 |
|---|---|---|
| 贵金属 | XAU、XAG、XPT、XPD | 2000 年 |
| 工业金属和能源 | XCU、ALU、ZNC、HRC、WTI、BRENT 等 | 2000 年 |
| 货币 | 57 种,包括 USD、EUR、GBP 和 JPY | 1999-01-04 |
| 加密货币 | 13 种,包括 BTC 和 ETH | 2014 年 |
所有套餐都可以查询完整的历史数据,各套餐的区别仅在于单次区间请求可覆盖的天数。日期采用 YYYY-MM-DD 格式(UTC),每天的数值为当天的收盘价。如果日期早于某个品种历史数据的起始时间,将返回错误 300(no_results)。
2026 年 10 月之前的金属价格来源于近月期货,因此可能与同一天的现货价格略有差异。将旧数据与其他来源的现货价格进行比较时,请注意这一点。
四个端点涵盖了历史数据的各类用途:/historical 查询单日,/timeframe 查询区间,/change 查询两个日期之间的变化,/ohlc 查询某一天的 K 线。下面将依次介绍,然后搭建一个小型本地数据存档,并在此基础上绘制图表。
单日数据:/historical
要获取某一天的收盘价,将日期放在路径中即可。/historical?date=YYYY-MM-DD 的效果相同,/yesterday 则是查询前一个 UTC 日的快捷方式:
curl "https://metalapi.com/api/v1/2024-01-15?currencies=XAU,XAG" \
-H "X-API-Key: $METALAPI_KEY"响应结构与 /latest 相同,另外附带 historical: true。以下是 API 参考文档中的示例数值:
{
"success": true,
"timestamp": 1705276800,
"historical": true,
"date": "2024-01-15",
"base": "USD",
"rates": {
"XAU": 0.0004874244,
"USDXAU": 2051.6,
"XAG": 0.0431741646,
"USDXAG": 23.162
}
}USDXAU 是 1 金衡盎司黄金以基础货币计的价格,XAU 是它的倒数。base 和 unit 的用法与平常一样,因此 base=EUR&unit=gram 会返回当天每克黄金的欧元价格。
周末和节假日不会导致请求失败:对于没有交易的日子,API 会返回最近一个交易日的收盘价。响应中的 date 仍是您请求的日期,因此如果需要知道当天市场是否开放,请在您这边维护一份交易日历。未来的日期会返回错误 205(invalid_date)。
日期区间:/timeframe
如需获取一个序列,/timeframe 会为 start_date 与 end_date 之间(含首尾两天)的每一天返回一条数据,并以日期为键。/timeseries 是一个别名,它在响应中标记 timeseries: true 而不是 timeframe: true。
curl "https://metalapi.com/api/v1/timeframe?start_date=2026-10-19&end_date=2026-10-23¤cies=XAU" \
-H "X-API-Key: $METALAPI_KEY"{
"success": true,
"timeframe": true,
"start_date": "2026-10-19",
"end_date": "2026-10-23",
"base": "USD",
"rates": {
"2026-10-19": { "XAU": 0.0002403788, "USDXAU": 4160.1 },
"2026-10-20": { "XAU": 0.0002413943, "USDXAU": 4142.6 },
"2026-10-21": { "XAU": 0.0002409639, "USDXAU": 4150 },
"2026-10-22": { "XAU": 0.0002462812, "USDXAU": 4060.4 },
"2026-10-23": { "XAU": 0.0002495072, "USDXAU": 4007.9 }
}
}与单日端点不同,/timeframe 不会填补缺口。现货金属在纽约时间周五 17:00 至周日 17:00 之间停止交易,因此结果中可能缺少周末和市场假日。不要假设每个自然日都有一条数据;如果混合查询交易日历不同的品种(例如黄金和比特币),请在读取前先检查键是否存在。
单次请求可覆盖的最长区间取决于套餐:
| 套餐 | 单次请求最长区间 | 始终符合限制的分页单位 |
|---|---|---|
| Free | 30 天 | 一个自然月 |
| Lite 及以上 | 365 天 | 一个自然年 |
该限制指的是两个日期之间的间隔,因此 2025-01-01 至 2025-01-31 符合 Free 套餐的限制,2024-01-01 至 2024-12-31 即使在闰年也符合付费套餐的限制。区间更长时将返回错误 207(timeframe_too_long),并附带 upgrade_url。每次 /timeframe 调用都只计为一次请求,无论返回多少天的数据。
比较两个日期:/change
如果您只需要两天之间的变化(例如数据看板上的年初至今表现),/change 会返回起始价、结束价、绝对变化和百分比变化,无需下载中间每一天的数据:
curl "https://metalapi.com/api/v1/change?start_date=2026-01-02&end_date=2026-10-23¤cies=XAU,XAG" \
-H "X-API-Key: $METALAPI_KEY"{
"success": true,
"change": true,
"start_date": "2026-01-02",
"end_date": "2026-10-23",
"base": "USD",
"rates": {
"XAU": { "start_rate": 0.0002309682, "end_rate": 0.0002495072, "change": 0.000018539, "change_pct": 8.02665 },
"USDXAU": { "start_rate": 4329.6, "end_rate": 4007.9, "change": -321.7, "change_pct": -7.43025 }
}
}请读取带前缀的键 USDXAU。倒数键 XAU 的变化方向相反,因此其 change_pct 符号相反,幅度也不同:在本例中,金价下跌了 7.43%,而倒数上涨了 8.03%。把倒数的百分比当作价格变化展示出来,是一个很容易被带到线上的错误。
/change 的区间限制与 /timeframe 相同。与 /historical 一样,当日期落在周末或节假日时,它会使用最近一个收盘价。
每日 K 线:/ohlc
/ohlc 返回某一天的开盘价、最高价、最低价和收盘价,并同时提供两个方向:USDXAU 为每盎司的美元价格,XAU 为其倒数。省略 date 或传入 date=latest,则返回今天截至目前的数据。
curl "https://metalapi.com/api/v1/ohlc?date=2026-10-23¤cies=XAU" \
-H "X-API-Key: $METALAPI_KEY"{
"success": true,
"ohlc": true,
"date": "2026-10-23",
"base": "USD",
"rates": {
"USDXAU": { "open": 4042.2, "high": 4050.2, "low": 3989.1, "close": 4007.9 },
"XAU": { "open": 0.000247390, "high": 0.000250683, "low": 0.000246901, "close": 0.000249507 }
}
}请注意,在倒数区块中,最高值来自最低价,最低值来自最高价,因为取倒数会颠倒大小顺序。绘图时请使用 USDXAU 区块。
每次调用只覆盖一天,因此绘制一整年的 K 线图需要为每个交易日发送一次请求。对于长周期图表,使用 /timeframe 的收盘价要划算得多。/ohlc 适合用于近期的 K 线,或在当前价格旁边显示当天的交易区间。
如需多年的周 K 或月 K,也可以用已存储的每日收盘价自行构建:开盘价取该周期的第一个收盘价,收盘价取最后一个收盘价,最高价和最低价取期间收盘价的极值。这样会遗漏日内的最高价和最低价,因此请将其标注为基于收盘价,但用于长周期图表完全没有问题。
回填 25 年数据,每次请求一年
在单次请求最多 365 天的限制下,获取 2000 年以来完整的黄金和白银历史数据,只需为每个自然年调用一次 /timeframe:2000 年至 2026 年共 27 次请求,任何付费套餐的每月配额都绰绰有余。在 Free 套餐中,按 30 天的窗口需要 300 多次请求,约为其每月 100 次配额的三倍,因此在 Free 套餐中完整回填并不现实,但获取几个月的历史数据是可行的。
下面的 Python 脚本按年分页,遇到每分钟限制时等待,并将两种金属的价格键写入 SQLite。将其保存为 backfill.py:
import os
import sqlite3
import time
from datetime import datetime, timezone
import requests
API = "https://metalapi.com/api/v1"
session = requests.Session()
session.headers["X-API-Key"] = os.environ["METALAPI_KEY"]
db = sqlite3.connect("metals.db")
db.execute(
"""create table if not exists daily_close (
day text not null,
symbol text not null,
base text not null,
price real not null,
primary key (day, symbol, base)
)"""
)
def fetch_timeframe(start, end, symbols, base="USD"):
"""start 与 end 之间(含首尾)的每日收盘价,以日期为键。"""
while True:
r = session.get(
f"{API}/timeframe",
params={"start_date": start, "end_date": end, "currencies": ",".join(symbols), "base": base},
timeout=30,
)
data = r.json()
if data.get("success"):
return data["rates"]
err = data["error"]
if err["code"] == 104: # 每分钟限制:等待后重试(不计费)
time.sleep(int(r.headers.get("Retry-After", "60")))
continue
if err["code"] == 300: # 该窗口内没有数据
return {}
raise RuntimeError(f"{err['code']} {err['type']}: {err['info']}")
def store(rates, symbols, base="USD"):
rows = [
(day, s, base, values[f"{base}{s}"])
for day, values in rates.items()
for s in symbols
if f"{base}{s}" in values
]
db.executemany("insert or replace into daily_close values (?, ?, ?, ?)", rows)
db.commit()
return len(rows)
def backfill(first_year, symbols=("XAU", "XAG"), base="USD"):
today = datetime.now(timezone.utc).date()
for year in range(first_year, today.year + 1):
start = f"{year}-01-01"
end = min(f"{year}-12-31", today.isoformat())
n = store(fetch_timeframe(start, end, symbols, base), symbols, base)
print(f"{year}: {n} rows")
if __name__ == "__main__":
backfill(2000)该脚本可以放心地重复运行:insert or replace 会覆盖已存在的行,因此中断的回填只需重新启动即可。当年的结束日期取今天的 UTC 日期,因为未来的 end_date 会返回 205。最小的几个套餐每分钟限制也最低(Free 为每分钟 10 次,Lite 为每分钟 30 次),因此脚本在遇到错误 104 时会遵循 Retry-After 等待,而不是直接失败。
脚本存储的是价格 USDXAU 和 USDXAG,而不是倒数键。如果需要以其他货币计的历史数据,请传入 base,或者同时存储各货币的美元价格,再用同一天的数据在本地换算。
存储和更新数据
过去日期的收盘价不会改变,因此本地副本是提供图表和报表最便宜、最快捷的方式。遵循以下几条规则可以保证数据正确:
- 以日期、品种和基础货币作为行的键。上面的表以
(day, symbol, base)为主键,因此重复获取同一区间会覆盖已有的行,而不会产生重复数据。 - 记录您请求时使用的参数。将
base(如果使用了unit,也包括unit)与数据一同保存。在同一列中混合每克和每盎司的数据,是图表中出现尖刺的典型原因。 - 每天补充一次。回填完成后,每天一次请求即可让表保持最新。请获取最近七天的数据,这样漏跑的任务或延迟的收盘价都会被自动补齐。
- 将今天的数据视为临时数据。只有当天的数据仍在变化。不要将其放入存档,或者像下面的脚本那样在下次运行时覆盖它。
from datetime import datetime, timedelta, timezone
from backfill import fetch_timeframe, store
SYMBOLS = ("XAU", "XAG")
today = datetime.now(timezone.utc).date()
start = (today - timedelta(days=7)).isoformat()
end = (today - timedelta(days=1)).isoformat()
print(store(fetch_timeframe(start, end, SYMBOLS), SYMBOLS), "rows updated")每天一次请求,补充数据每月只需约 30 次请求,即使在 Free 套餐中也还有余量。对于更大的存档,同样的模式也适用于 PostgreSQL、Parquet 文件或任何时序数据库,只需修改 store 函数即可。
在基于回填数据构建任何功能之前,请先检查数据。以下三项检查可以发现大多数问题:
- 每年的行数。每个完整年份中,每种金属的行数应大致等于交易日数。如果某一年的行数少得多,说明有请求失败或未返回数据。
- 缺口。普通周末会使两个收盘价之间相隔三天,长假周末则相隔四天。间隔更长的情况都值得检查一下,而重新获取该窗口只需一次请求。
- 跳变。如果相邻两天的价格相差约 31 倍或 1,000 倍,说明混用了单位;如果数值接近于零,说明混入了倒数键。
前两项检查在 SQLite 中只需简短的查询:
-- 每年的行数:完整年份大致每个交易日一行
select substr(day, 1, 4) as year, symbol, count(*) as days from daily_close
where base = 'USD' group by year, symbol order by year, symbol;
-- 已存储的收盘价之间超过四天的缺口
select symbol, prev_day, day from (
select symbol, day, lag(day) over (partition by symbol order by day) as prev_day
from daily_close where base = 'USD'
) where julianday(day) - julianday(prev_day) > 4;绘制图表
数据存储在本地之后,绘制图表不再消耗任何请求。下面的 matplotlib 脚本绘制以美元计的金价,以及金银比(1 盎司黄金可以买到的白银盎司数):
import sqlite3
from datetime import date
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
db = sqlite3.connect("metals.db")
rows = db.execute(
"""select g.day, g.price, s.price
from daily_close g
join daily_close s on s.day = g.day and s.base = g.base and s.symbol = 'XAG'
where g.symbol = 'XAU' and g.base = 'USD'
order by g.day"""
).fetchall()
days = [date.fromisoformat(day) for day, _, _ in rows]
gold = [g for _, g, _ in rows]
ratio = [g / s for _, g, s in rows]
fig, (top, bottom) = plt.subplots(2, 1, sharex=True, figsize=(10, 6))
top.plot(days, gold, linewidth=1)
top.set_ylabel("Gold, USD per troy oz")
bottom.plot(days, ratio, linewidth=1)
bottom.set_ylabel("Gold/silver ratio")
bottom.xaxis.set_major_locator(mdates.YearLocator(base=5))
fig.tight_layout()
fig.savefig("gold-silver.png", dpi=150)在网站上,请从您自己的后端提供已存储的数据序列,并在浏览器中绘制。这样 API 密钥始终保存在服务器上,页面浏览也不会消耗您的配额。下面是一个极简的 Chart.js 页面,它从您自己的端点读取由 { day, price } 对象组成的 JSON 数组:
<canvas id="gold" height="320"></canvas>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.js"></script>
<script>
// 您自己的端点,返回 [{ "day": "2026-10-23", "price": 4007.9 }, ...]
fetch("/api/gold-history")
.then((res) => res.json())
.then((points) => {
new Chart(document.getElementById("gold"), {
type: "line",
data: {
labels: points.map((p) => p.day),
datasets: [{ label: "Gold, USD per troy oz", data: points.map((p) => p.price), pointRadius: 0, borderWidth: 1 }],
},
options: {
animation: false,
scales: { x: { ticks: { maxTicksLimit: 10 } } },
},
});
});
</script>25 年的交易日意味着每个序列有 6,000 多个数据点。像上面那样关闭数据点标记和动画,可以让图表保持流畅;在小屏幕上,可以考虑降采样为每周收盘价。
历史数据指南提供了一个 JavaScript 区间辅助函数和移动平均线示例,API 文档列出了所有参数。如果您使用 AI 助手,MetalAPI MCP 服务器可以在对话中获取同样的时间序列。关于密钥、错误和缓存的通用知识,请从黄金价格 API 指南开始阅读;各套餐的区间限制和配额可在定价页面中比较。
继续阅读
黄金价格 API:开发者完全指南
黄金价格 API 使用指南:获取密钥,用 curl、JavaScript 和 Python 发起调用,正确解读汇率格式,并妥善处理历史数据、错误、配额与缓存。
如何在网站上添加实时金价小组件
在 WordPress、Shopify 或纯 HTML 页面中添加实时金价小组件:在 MetalAPI 控制台中创建小组件,选择品种、布局和主题,然后粘贴两行代码即可完成。
K 金价格详解:24K、22K、18K 及其计算方法
24K、22K 和 18K 分别代表什么,916、750 等成色印记如何与 K 数对应,怎样计算每克 K 金价格,以及如何通过金价 API 直接获取各 K 数的金价。