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,703.96−0.09%ETH2,352.61−0.14%
すべての記事
金Pythonチュートリアル

Pythonで金価格APIを使う:価格の取得、キャッシュ、グラフ化

Pythonで金価格APIを使う方法を解説します。requestsでの最新価格の取得、エラー処理とリトライ、レスポンスのキャッシュ、pandasの時系列とmatplotlibによるグラフ化まで紹介します。

執筆:MetalAPI Team··7分で読めます

Pythonで金価格APIを使うための準備

このチュートリアルでは、Pythonで金価格APIを使う方法を、最初のリクエストからキャッシュ付きのクライアント、直近10か月のグラフまで順を追って説明します。使用するのは、貴金属、産業用金属、エネルギー、通貨、暗号資産を扱うJSON REST APIのMetalAPIと、よく知られた3つのライブラリです。HTTPにはrequests、時系列にはpandas、グラフにはmatplotlibを使います。すべてのコードはPython 3.9以降で動作します。

無料アカウントを作成し、ダッシュボードのAPI keysでAPIキーを作成します。キーはmk_live_で始まり、一度しか表示されないため、そのまま環境変数にコピーしてください。Freeプランはクレジットカード不要で、月100リクエストと毎日の価格更新が含まれています。この記事の内容はすべてFreeプランで試せます。

セットアップ
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()を1回呼び出すだけで読み込めます。キーがリポジトリに含まれないように、.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"]はその逆数で、1ドルあたりの金のオンス数です。これを誤って読み取ってしまうのが、金価格のコードで最も多いバグです。キーをdata["base"]から組み立てておけば、base=EUR(キーはEURXAU)やbase=INRに切り替えてもスクリプトは正しく動作します。1グラムあたりや1キログラムあたりの価格が必要なら、unit=gramまたはunit=kilogramを追加します。timestampは価格データの時刻をUnix秒で表したもので、リクエストの時刻ではありません。

Pythonで金価格APIのエラーを処理する

失敗した呼び出しは、HTTPエラーステータスと、success: falseを含むJSON本文を返します。本文のerrorオブジェクトには、数値のcode、type、infoメッセージが含まれます。分岐にはコードを使い、メッセージのテキストは決して使わないでください。再試行する価値があるのは、ごく一部のコードだけです:

  • 104(too_many_requests、HTTP 429):1分あたりの上限です。Retry-Afterヘッダーの秒数だけ待ってください。
  • 500、503、ネットワークエラー:指数バックオフで再試行します。
  • それ以外のコード:101や102(キーがない、または無効)、105(月間クォータを使い切った)、106(エンドポイントがプランに含まれていない)、2xxのバリデーションエラーなどは、再試行せずにリクエスト、キー、プランを修正してください。

次の小さなモジュールは、このロジックをまとめたものです。1つの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やサーバーエラーで拒否されたリクエストは課金されないため、ここでの再試行にコストはかかりません。一方、バリデーションエラーは課金されます。これも、バリデーションエラーをループで再試行してはいけない理由の1つです。エラーハンドリングガイドには、すべてのコードとそのHTTPステータスが掲載されています。

金価格をキャッシュしてリクエストを節約する

価格は、プランごとに決まった間隔で更新されます。Freeでは1日1回、Liteでは30分ごと、Standardでは10分ごと、AdvancedとProでは60秒ごとです。更新はUTCの00:00から数えて、その間隔の倍数の時刻に行われます。そのため、次の更新より前に再度リクエストしても同じ数値が返され、クォータを無駄に消費するだけです。次のキャッシュは、各レスポンスを次の更新まで保持し、APIに到達できない場合は最後に取得できた価格を返します:

prices.py
import math
import time

import requests

from metalapi import MetalAPIError, call

# 秒。Free:86_400、Lite:3_600(1時間ごとなら1,000リクエストに収まる)、Standard:600
UPDATE_INTERVAL = 600
_cache = {}  # (base, symbols, unit) -> (expires_at, 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])  # 最後に取得できた価格を返し、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"]は、1グラムあたりのユーロ建て価格を返します。必要なシンボルはすべて1回の呼び出しで取得してください。XAU,XAG,XPT,XPDなら、4リクエストではなく1リクエストで済みます。なお、このキャッシュはメモリ上にあります。cronで起動されるスクリプトは毎回空のキャッシュから始まるため、その場合はスケジュールそのものがキャッシュの役割を果たします。実行は更新間隔ごとに最大1回にしてください。クォータを確認するには/usageを呼び出します。このエンドポイントはクォータにカウントされません。

金価格の時系列をpandasに読み込む

過去データには、/timeframeエンドポイント(エイリアス/timeseries)を使います。start_dateからend_dateまでの日次終値を、日付をキーとして返します。1リクエストでカバーできるのは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か月分の取得に10リクエストかかります。Seriesは取引日ごとに1行で、週末や市場の休日は補完されずに欠落するため、行数は暦日数より少なくなります。毎日の値が必要な場合は、gold.asfreq("D").ffill()で金曜日の終値を週末に引き継げます。ここから先は、日次リターンならgold.pct_change()、移動平均ならgold.rolling(20).mean()のように、通常のpandasのツールを使えます。

過去の終値は変わらないため、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リクエストと毎日の価格更新が含まれ、クレジットカードは不要です。この記事のコードはすべてFreeプランで動作します。最新価格は前日の終値で、1回の/timeframeリクエストで最大30日分を取得できます。無料プランの詳細は無料の金価格APIで説明しています。

XAUのレートが非常に小さな数値なのはなぜですか?

rates["XAU"]は逆数のレートで、基準通貨1単位あたりの金のトロイオンス数です。価格は接頭辞付きのキーにあり、基準通貨がUSDならrates["USDXAU"]、EURならrates["EURXAU"]です。例の値では、1÷0.0002501319で3997.89に戻ります。

Pythonで1グラムあたりの金価格を取得するには?

リクエストにunit=gramを追加します。たとえばcall("latest", {"base": "EUR", "currencies": "XAU", "unit": "gram"})のようにします。このときrates["EURXAU"]は1グラムあたりのユーロ建て価格になり、レスポンスには"unit": "gram"が含まれます。unit=ounceは避けてください。これは常衡オンスで、金の価格に使われるトロイオンスではありません。

金価格の過去データはどこまでさかのぼれますか?

金の日次価格は2000年から提供されており、どのプランでも全期間を照会できます。プランによって異なるのは、1回の/timeframeリクエストでカバーできる日数だけで、Freeでは30日、有料プランでは365日です。max_days=365なら、2000年から現在までのデータを27リクエストで取得できます。

PythonスクリプトからAPIをどのくらいの頻度で呼び出すべきですか?

プランの価格更新より頻繁に呼び出す必要はありません。Freeでは1日1回(UTCの00:00以降)、Liteでは1時間ごと(30分ごとだと月に約1,490リクエストとなり、1,000のクォータを超えるため)、Standardでは10分ごとです。週末は価格が動かないものと考えてください。スポット金は、ニューヨーク時間の金曜17:00から日曜17:00まで取引されないためです。

ご自身のデータで試してみましょう

無料アカウントを作成すれば、2分以内に最初のリクエストを送信できます。月100リクエスト、クレジットカード不要。

関連記事