MetalAPI
XAU3,997.89−1.54%XAG53.936−1.51%XPT1,554.47−0.83%XPD1,047.03−0.44%XCU6.5080+2.29%WTI103.27+3.82%BRENT111.70−1.30%EUR1.120400−0.01%GBP1.337500−0.01%JPY0.0063761+0.14%CHF1.213445−0.25%TRY0.020125−0.12%BTC80,637.11−0.17%ETH2,354.39−0.06%
全部文章
黄金Python教程

Python 黄金价格 API 教程:获取、缓存并绘制金价

在 Python 中使用黄金价格 API:用 requests 获取实时金价,处理错误与重试,缓存响应,并用 pandas 和 matplotlib 绘制金价时间序列图。

作者:MetalAPI Team··阅读约 7 分钟

Python 黄金价格 API:准备工作

本教程介绍如何在 Python 中使用黄金价格 API,从第一个请求开始,到带缓存的客户端,再到最近十个月的金价走势图。教程使用 MetalAPI(一个提供贵金属、工业金属、能源、货币和加密货币数据的 JSON REST API),并搭配三个常用库:用于 HTTP 请求的 requests、用于时间序列的 pandas 和用于绘图的 matplotlib。所有代码片段均可在 Python 3.9 或更高版本上运行。

创建免费账户,然后在控制台的 API keys 中创建一个 API 密钥。密钥以 mk_live_ 开头,并且只显示一次,因此请直接将其复制到环境变量中。Free 套餐无需信用卡,每月包含 100 次请求,价格每日更新,足以完成本文中的所有内容。

安装与配置
pip install requests pandas matplotlib

# macOS 和 Linux
export METALAPI_KEY="mk_live_your_key_here"

# Windows PowerShell
$env:METALAPI_KEY = "mk_live_your_key_here"

请使用 os.environ["METALAPI_KEY"] 读取密钥,而不要把它粘贴到脚本中;变量缺失时,这种写法会直接报错。如果您更喜欢使用 .env 文件,python-dotenv 包只需调用一次 load_dotenv() 即可加载它。请将 .env 添加到 .gitignore 中,确保密钥永远不会进入代码仓库。

如何在 Python 中获取金价

MetalAPI 的每个端点都是 https://metalapi.com/api/v1 下的 GET 请求。密钥可以放在 api_key 查询参数、Authorization: Bearer 请求头或 X-API-Key 请求头中。请使用请求头:完整的 URL 会出现在日志中,而且 requests 的某些异常会连同查询字符串一起打印 URL。/latest 端点返回当前价格,base 设置货币,currencies 将响应限定为您需要的品种:

gold.py
import os

import requests

resp = requests.get(
    "https://metalapi.com/api/v1/latest",
    params={"base": "USD", "currencies": "XAU"},
    headers={"X-API-Key": os.environ["METALAPI_KEY"]},
    timeout=10,
)
data = resp.json()
if not data["success"]:
    raise SystemExit(f"MetalAPI error {data['error']['code']}: {data['error']['info']}")

price = data["rates"][data["base"] + "XAU"]
print(f"Gold: {price:,.2f} {data['base']} per troy ounce")

响应如下所示。这些数字是 MetalAPI 文档中通用的示例数值,并非实时价格:

响应示例
{
  "success": true,
  "timestamp": 1792834892,
  "date": "2026-10-24",
  "base": "USD",
  "rates": { "XAU": 0.0002501319, "USDXAU": 3997.89 },
  "rate_limit": { "limit": 100, "remaining": 99, "used": 1, "plan": "Free", "...": "..." }
}

使用这些数值时,脚本会打印 Gold: 3,997.89 USD per troy ounce。rates["USDXAU"] 是 1 金衡盎司黄金以基础货币计的价格。rates["XAU"] 是它的倒数,即每美元可兑换的黄金盎司数,误读这个值是金价代码中最常见的错误。根据 data["base"] 拼出键名,可以让脚本在您切换到 base=EUR(键为 EURXAU)或 base=INR 时依然正确。添加 unit=gram 或 unit=kilogram 即可获取每克或每千克的价格。timestamp 是价格数据的时间(Unix 秒),而不是您发出请求的时间。

在 Python 中处理黄金价格 API 错误

请求失败时会返回 HTTP 错误状态码,以及包含 success: false 的 JSON 正文,其中的 error 对象包含数字 code、type 和 info 消息。请根据错误代码进行分支处理,切勿依赖消息文本。只有少数几种错误值得重试:

  • 104(too_many_requests,HTTP 429):每分钟请求限制。请等待 Retry-After 响应头中指定的秒数。
  • 500、503 和网络错误:使用指数退避重试。
  • 其他所有错误,例如 101 或 102(缺少密钥或密钥无效)、105(每月配额已用完)、106(套餐不包含该端点)以及 2xx 校验错误:请修正请求、密钥或套餐,而不是重试。

下面这个小模块封装了上述逻辑。它复用同一个 requests.Session,使连接在多次调用之间保持打开,并抛出一个可供检查的异常:

metalapi.py
import os
import time

import requests

API_URL = "https://metalapi.com/api/v1"
session = requests.Session()
session.headers["X-API-Key"] = os.environ["METALAPI_KEY"]


class MetalAPIError(Exception):
    def __init__(self, code, type_, info, retry_after=None):
        super().__init__(f"MetalAPI {code} ({type_}): {info}")
        self.code = code
        self.type = type_
        self.retry_after = retry_after


def _get(path, params):
    r = session.get(f"{API_URL}/{path}", params=params, timeout=10)
    try:
        data = r.json()
    except ValueError:  # 不是 JSON,例如代理返回的 HTML 错误页面
        raise MetalAPIError(r.status_code, "http_error", r.reason) from None
    if not data.get("success"):
        err = data["error"]
        retry_after = r.headers.get("Retry-After")
        raise MetalAPIError(err["code"], err["type"], err["info"], int(retry_after) if retry_after else None)
    return data


def call(path, params=None, retries=3):
    """GET 请求一个端点。仅对速率限制、服务器错误和网络故障进行重试。"""
    for attempt in range(retries + 1):
        try:
            return _get(path, params or {})
        except (MetalAPIError, requests.RequestException) as exc:
            code = getattr(exc, "code", None)  # 网络错误时为 None
            if attempt == retries or (code is not None and code != 104 and code < 500):
                raise
            time.sleep(getattr(exc, "retry_after", None) or 2**attempt)

退避会在放弃之前依次等待 1、2 和 4 秒。在这里重试不会产生任何费用,因为因 104 或服务器错误而被拒绝的请求不计费。校验错误则会计费,这也是绝不应在循环中重试它们的另一个原因。错误处理指南列出了每个错误代码及其 HTTP 状态码。

缓存金价以节省请求

每个套餐都按固定间隔更新价格:Free 每天一次,Lite 每 30 分钟,Standard 每 10 分钟,Advanced 和 Pro 每 60 秒。更新时间点是从 00:00 UTC 起算的该间隔的整数倍,因此在下一次更新之前再次请求只会得到相同的数字,白白消耗配额。下面的缓存会将每个响应保留到下一次更新,并在无法连接 API 时提供最后一次成功获取的价格:

prices.py
import math
import time

import requests

from metalapi import MetalAPIError, call

# 单位为秒。Free:86_400,Lite:3_600(每小时一次才不会超出其 1,000 次请求),Standard:600
UPDATE_INTERVAL = 600
_cache = {}  # (base, symbols, unit) -> (过期时间, data)


def latest(base="USD", symbols="XAU,XAG", unit="troy_oz"):
    key = (base, symbols, unit)
    now = time.time()
    hit = _cache.get(key)
    if hit and now < hit[0]:
        return hit[1]
    try:
        data = call("latest", {"base": base, "currencies": symbols, "unit": unit})
    except (MetalAPIError, requests.RequestException):
        if not hit:
            raise
        _cache[key] = (now + 60, hit[1])  # 提供最后一次成功获取的价格,一分钟后再试
        return hit[1]
    next_update = (math.floor(now / UPDATE_INTERVAL) + 1) * UPDATE_INTERVAL
    _cache[key] = (next_update + 5, data)  # 在下一次更新后几秒过期
    return data

latest("EUR", "XAU", "gram")["rates"]["EURXAU"] 返回每克黄金的欧元价格。请在一次调用中请求所需的所有品种:XAU,XAG,XPT,XPD 只消耗一次请求,而不是四次。请注意,这个缓存位于内存中。由 cron 启动的脚本每次都从空缓存开始,因此在这种情况下,调度计划本身就是缓存:每个更新间隔最多运行一次。如需查看配额使用情况,请调用 /usage,它从不计入配额。

将黄金价格时间序列加载到 pandas

获取历史数据时,/timeframe 端点(别名 /timeseries)返回 start_date 与 end_date 之间按日期索引的每日收盘价。在 Free 套餐中,一次请求最多覆盖 30 天,付费套餐为 365 天,更长的区间会返回错误 207。下面的函数将任意区间拆分为允许的分段,并返回一个 pandas Series:

history.py
from datetime import date, timedelta

import pandas as pd

from metalapi import call


def gold_history(start, end, base="USD", max_days=30):  # 付费套餐可设为 max_days=365
    rates = {}
    chunk_start = start
    while chunk_start <= end:
        chunk_end = min(chunk_start + timedelta(days=max_days - 1), end)
        data = call("timeframe", {
            "start_date": chunk_start.isoformat(),
            "end_date": chunk_end.isoformat(),
            "currencies": "XAU",
            "base": base,
        })
        rates.update(data["rates"])
        chunk_start = chunk_end + timedelta(days=1)

    df = pd.DataFrame.from_dict(rates, orient="index")
    df.index = pd.to_datetime(df.index)
    return df[f"{base}XAU"].sort_index().rename(f"gold_{base.lower()}")


gold = gold_history(date(2026, 1, 1), date(2026, 10, 23))
gold.to_csv("gold_usd.csv")
print(f"{len(gold)} trading days, high {gold.max():,.2f}, low {gold.min():,.2f}")

在 Free 套餐中,这十个月的数据需要 10 次请求。Series 中每个交易日占一行:周末和市场假日会缺失而不会被填充,因此行数会少于日历天数。如果您需要每天都有数值,gold.asfreq("D").ffill() 会把周五的收盘价延续到周末。之后就可以使用常见的 pandas 工具了,例如用 gold.pct_change() 计算日收益率,或用 gold.rolling(20).mean() 计算移动平均线。

过去的收盘价永远不会改变,因此 CSV 文件就是您的历史数据缓存:下次请直接加载它,而不要再次请求同一区间。如需将 2000 年以来的完整数据回填到数据库中,请参阅通过 API 获取黄金和白银历史价格。

用 matplotlib 绘制金价走势图

图表读取的是 CSV 文件,因此重新绘制不会消耗请求。它会绘制每日收盘价以及 20 日移动平均线:

plot.py
import matplotlib.pyplot as plt
import pandas as pd

gold = pd.read_csv("gold_usd.csv", index_col=0, parse_dates=True)["gold_usd"]
sma20 = gold.rolling(20).mean()

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(gold.index, gold, linewidth=1, label="Gold, USD per troy oz")
ax.plot(sma20.index, sma20, linewidth=2, label="20-day moving average")
ax.set_title(f"Gold price, {gold.index[0]:%d %b %Y} to {gold.index[-1]:%d %b %Y}")
ax.yaxis.set_major_formatter("{x:,.0f}")
ax.grid(alpha=0.3)
ax.legend()
fig.autofmt_xdate()
fig.tight_layout()
fig.savefig("gold.png", dpi=150)

由于索引中保存的是真实日期,matplotlib 会按时间间隔排列数据点,移动平均线则从第 20 个交易日开始。如需绘制以其他货币计的金价,请以 base="EUR" 调用 gold_history:汇率键遵循相同的模式,因此函数无需任何修改即可使用。

后续步骤

现在,您已经拥有大多数 Python 集成所需的一切:经过身份验证的请求、带有合理重试策略的错误处理、遵循套餐更新间隔的缓存,以及 pandas 中的历史数据。请先在 Free 套餐上构建和测试。当您需要更新鲜的价格时,请根据更新间隔和请求量选择套餐:每 10 分钟运行一次的任务每月约消耗 4,500 次请求,在 Standard 套餐的 10,000 次额度以内。请在定价页面上比较各套餐。

如果价格最终要显示在网站上,JavaScript 黄金价格 API 教程介绍了如何通过 Node.js 路由提供价格而不暴露密钥。

常见问题

有没有可在 Python 中使用的免费黄金价格 API?

有。MetalAPI 的 Free 套餐每月包含 100 次请求,价格每日更新,无需信用卡,本文中的所有代码片段都可以在该套餐上运行。它的最新价格是前一天的收盘价,一次 /timeframe 请求最多覆盖 30 天。免费黄金价格 API 对免费套餐做了更详细的介绍。

为什么 XAU 汇率是一个这么小的数字?

rates["XAU"] 是倒数汇率:1 单位基础货币可兑换的黄金金衡盎司数。价格位于带前缀的键中:基础货币为 USD 时是 rates["USDXAU"],基础货币为 EUR 时是 rates["EURXAU"]。以示例数值计算,1 ÷ 0.0002501319 得到的仍是 3997.89。

如何在 Python 中获取每克黄金价格?

在请求中添加 unit=gram,例如 call("latest", {"base": "EUR", "currencies": "XAU", "unit": "gram"})。此时 rates["EURXAU"] 就是每克黄金的欧元价格,响应中也会包含 "unit": "gram"。请避免使用 unit=ounce:它表示常衡盎司,而不是黄金计价所用的金衡盎司。

黄金历史价格可以追溯到多久以前?

每日金价数据从 2000 年开始,所有套餐都可以查询完整的历史数据。各套餐的区别仅在于一次 /timeframe 请求可以覆盖的天数:Free 为 30 天,付费套餐为 365 天。设置 max_days=365 时,获取 2000 年至今的全部数据需要 27 次请求。

Python 脚本应该多久调用一次 API?

调用频率不要超过套餐的价格更新频率:Free 每天一次,在 00:00 UTC 之后调用;Lite 每小时一次,因为每 30 分钟一次每月约需 1,490 次请求,超出 1,000 次的配额;Standard 每 10 分钟一次。周末价格会保持不变:现货黄金从纽约时间周五 17:00 到周日 17:00 停止交易。

用您自己的数据试一试

创建免费账户,两分钟内即可发起第一次请求。每月 100 次请求,无需信用卡。

继续阅读