API цен на золото на Python: запросы, кэш и графики
API цен на золото на Python: актуальные цены через requests, обработка ошибок и повторов, кэширование ответов и график временного ряда в pandas и matplotlib.
API цен на золото на Python: что понадобится
В этом туториале показано, как работать с API цен на золото на Python — от первого запроса до клиента с кэшем и графика за последние десять месяцев. Мы используем MetalAPI — JSON REST API для драгоценных и промышленных металлов, энергоносителей, валют и криптовалют — вместе с тремя известными библиотеками: requests для HTTP, pandas для временных рядов и matplotlib для графиков. Все фрагменты кода работают на Python 3.9 и новее.
Создайте бесплатный аккаунт и создайте ключ API в разделе API keys личного кабинета. Ключи начинаются с 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 — это запрос GET к адресу внутри https://metalapi.com/api/v1. Ключ можно передать в параметре запроса api_key, в заголовке Authorization: Bearer или в заголовке X-API-Key. Используйте заголовок: полные URL попадают в журналы, а некоторые исключения requests выводят URL вместе со строкой запроса. Эндпоинт /latest возвращает текущие цены, base задаёт валюту, а currencies ограничивает ответ нужными вам символами:
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"] — цена одной тройской унции золота в базовой валюте. rates["XAU"] — обратная величина, количество унций золота за один доллар, и ошибочное чтение этого ключа — самая частая ошибка в коде для цен на золото. Если формировать ключ из data["base"], скрипт останется корректным при переходе на base=EUR (ключ EURXAU) или base=INR. Добавьте unit=gram или unit=kilogram, чтобы получить цены за грамм или килограмм. timestamp — время ценовых данных в секундах Unix, а не время вашего запроса.
Обработка ошибок API цен на золото в Python
Неудачный вызов возвращает HTTP-статус ошибки и JSON-тело с success: false и объектом error, в котором есть числовой code, type и сообщение info. Ветвите логику по коду, но никогда — по тексту сообщения. Повторять имеет смысл лишь несколько кодов:
104(too_many_requests, HTTP 429): поминутный лимит. Подождите столько секунд, сколько указано в заголовкеRetry-After.500,503и сетевые ошибки: повторяйте запрос с экспоненциальной задержкой.- Всё остальное, например
101или102(ключ отсутствует или недействителен),105(месячная квота исчерпана),106(эндпоинт не входит в ваш тариф) и ошибки валидации2xx: вместо повторов исправьте запрос, ключ или тариф.
Этот небольшой модуль инкапсулирует такую логику. Он переиспользует один requests.Session, поэтому соединения остаются открытыми между вызовами, и выбрасывает исключение, которое можно проанализировать:
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, каждые 30 минут на Lite, каждые 10 минут на Standard и каждые 60 секунд на Advanced и Pro. Обновления приходятся на моменты, кратные этому интервалу, начиная с 00:00 UTC, поэтому повторный запрос до следующего обновления вернёт те же числа и впустую потратит квоту. Этот кэш хранит каждый ответ до следующего обновления и отдаёт последние удачные цены, если API недоступен:
import math
import time
import requests
from metalapi import MetalAPIError, call
# Секунды. Free: 86_400, Lite: 3_600 (раз в час укладывается в его 1000 запросов), 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]) # отдаём последние удачные цены, повторим через минуту
return hit[1]
next_update = (math.floor(now / UPDATE_INTERVAL) + 1) * UPDATE_INTERVAL
_cache[key] = (next_update + 5, data) # истекает через несколько секунд после следующего обновления
return datalatest("EUR", "XAU", "gram")["rates"]["EURXAU"] возвращает цену грамма в евро. Запрашивайте все нужные символы одним вызовом: XAU,XAG,XPT,XPD стоит одного запроса, а не четырёх. Учтите, что этот кэш живёт в памяти. Скрипт, запускаемый по cron, каждый раз начинает с пустым кэшем, поэтому там роль кэша играет расписание: запускайте скрипт не чаще одного раза за интервал обновления. Чтобы следить за квотой, вызывайте /usage — этот эндпоинт её никогда не расходует.
Загрузка временного ряда цен на золото в pandas
Для истории эндпоинт /timeframe (псевдоним /timeseries) возвращает дневные цены закрытия между start_date и end_date с датами в качестве ключей. Один запрос охватывает до 30 дней на тарифе Free и до 365 дней на платных тарифах, а более длинный диапазон завершается ошибкой 207. Эта функция разбивает любой диапазон на допустимые части и возвращает pandas Series:
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-дневное скользящее среднее:
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-го торгового дня. Чтобы построить график цены золота в другой валюте, вызовите gold_history с base="EUR": ключи курсов строятся по той же схеме, поэтому функция работает без изменений.
Что дальше
Теперь у вас есть всё, что нужно большинству интеграций на Python: аутентифицированный запрос, обработка ошибок с разумными повторами, кэш, следующий интервалу обновления вашего тарифа, и история в pandas. Разрабатывайте и тестируйте всё это на тарифе Free. Когда понадобятся более свежие цены, выбирайте тариф по интервалу обновления и объёму запросов: задача, которая запускается каждые 10 минут, расходует около 4 500 запросов в месяц, что укладывается в 10 000 запросов тарифа Standard. Сравните тарифы на странице тарифов.
Если цены нужно показывать на сайте, статья «API цен на золото на JavaScript» показывает, как отдавать их через маршрут Node.js, не раскрывая ключ.
Частые вопросы
Есть ли бесплатный API цен на золото для Python?
Да. Тариф Free в MetalAPI включает 100 запросов в месяц с ежедневным обновлением цен и не требует банковской карты, и все фрагменты кода из этой статьи на нём работают. Последние цены на нём — это цены закрытия предыдущего дня, а один запрос /timeframe охватывает до 30 дней. Подробнее о бесплатном тарифе — в статье «Бесплатный API цен на золото».
Почему курс XAU — такое маленькое число?
rates["XAU"] — обратный курс: количество тройских унций золота на одну единицу базовой валюты. Цена находится под ключом с префиксом: rates["USDXAU"] для базы USD или rates["EURXAU"] для базы EUR. С примерными значениями 1 ÷ 0,0002501319 снова даёт 3 997,89.
Как получить цену грамма золота на Python?
Добавьте к запросу unit=gram, например call("latest", {"base": "EUR", "currencies": "XAU", "unit": "gram"}). Тогда rates["EURXAU"] — цена в евро за грамм, а ответ содержит "unit": "gram". Не используйте unit=ounce: это торговая унция (avoirdupois), а не тройская унция, в которой котируется золото.
Насколько глубока история цен на золото?
Дневные цены на золото доступны с 2000 года, и на любом тарифе можно запрашивать всю историю. Тарифы различаются только тем, сколько дней может охватывать один запрос /timeframe: 30 на Free и 365 на платных тарифах. С max_days=365 вся история с 2000 года по сегодняшний день занимает 27 запросов.
Как часто скрипт на Python должен вызывать API?
Не чаще, чем ваш тариф обновляет цены: раз в день на Free, после 00:00 UTC; раз в час на Lite, потому что опрос каждые 30 минут потребовал бы около 1 490 запросов в месяц при квоте 1 000; каждые 10 минут на Standard. На выходных цены не меняются: спотовое золото не торгуется с 17:00 пятницы до 17:00 воскресенья по нью-йоркскому времени.
Попробуйте на своих данных
Создайте бесплатный аккаунт и отправьте первый запрос меньше чем за две минуты. 100 запросов в месяц, без банковской карты.
Читайте также
Что означает XAU/USD: коды XAU, XAG, XPT и XPD
Что означает XAU/USD: почему у золота есть валютный код ISO 4217, что обозначают XAG, XPT и XPD и как читать направление котировки в ценах и данных API.
Бесплатный API цен на золото: что дают 100 запросов в месяц
Бесплатный API цен на золото без прикрас: что даёт тариф Free MetalAPI, как растянуть 100 запросов в месяц с помощью кеша и объединения запросов и когда менять тариф.
API цен на золото на JavaScript: Node.js, TypeScript и Next.js
Цена золота в JavaScript через API: fetch с async/await, типы ответа в TypeScript, кэширующий маршрут на Express или Next.js и безопасная работа с ключом API.