MetalAPI
XAU3997,89−1,54 %XAG53,936−1,51 %XPT1554,47−0,83 %XPD1047,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.522,74−0,31 %ETH2342,79−0,56 %
Todos los artículos
OroPythonTutorial

API de precios del oro en Python: consulta, caché y gráficos

Usa una API de precios del oro en Python: obtén precios en tiempo real con requests, gestiona errores y reintentos, usa caché y grafica una serie con pandas.

Por MetalAPI Team··7 min de lectura

API de precios del oro en Python: qué necesitas

Este tutorial muestra cómo usar una API de precios del oro en Python, desde la primera solicitud hasta un cliente con caché y un gráfico de los últimos diez meses. Usa MetalAPI, una API REST en JSON para metales preciosos, metales industriales, energía, divisas y criptomonedas, junto con tres bibliotecas conocidas: requests para HTTP, pandas para la serie temporal y matplotlib para el gráfico. Todos los fragmentos funcionan con Python 3.9 o posterior.

Crea una cuenta gratuita y crea una clave API en API keys dentro del panel. Las claves empiezan por mk_live_ y solo se muestran una vez, así que copia la tuya directamente en una variable de entorno. El plan Free no requiere tarjeta de crédito e incluye 100 solicitudes al mes con actualización diaria de precios, suficiente para todo lo que hay en este artículo.

Instalación
pip install requests pandas matplotlib

# macOS y Linux
export METALAPI_KEY="mk_live_your_key_here"

# Windows PowerShell
$env:METALAPI_KEY = "mk_live_your_key_here"

Lee la clave con os.environ["METALAPI_KEY"], que falla de forma visible cuando falta la variable, en lugar de pegarla en un script. Si prefieres un archivo .env, el paquete python-dotenv lo carga con una sola llamada a load_dotenv(). Añade .env a .gitignore para que la clave nunca llegue a tu repositorio.

Cómo obtener el precio del oro en Python

Cada endpoint de MetalAPI es una solicitud GET bajo https://metalapi.com/api/v1. La clave puede ir en un parámetro de consulta api_key, en una cabecera Authorization: Bearer o en una cabecera X-API-Key. Usa una cabecera: las URL completas acaban en los registros, y algunas excepciones de requests imprimen la URL con su cadena de consulta. El endpoint /latest devuelve los precios actuales, base establece la divisa y currencies limita la respuesta a los símbolos que necesitas:

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")

La respuesta tiene este aspecto. Las cifras son los valores de ejemplo que se usan en toda la documentación de MetalAPI, no precios en tiempo real:

Respuesta de ejemplo
{
  "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", "...": "..." }
}

Con estos valores, el script imprime Gold: 3,997.89 USD per troy ounce. rates["USDXAU"] es el precio de una onza troy de oro en la divisa base. rates["XAU"] es su inverso, onzas de oro por dólar, y leerlo por error es el fallo más común en el código de precios del oro. Construir la clave a partir de data["base"] mantiene el script correcto cuando cambias a base=EUR (clave EURXAU) o a base=INR. Añade unit=gram o unit=kilogram para obtener precios por gramo o por kilogramo. timestamp es la hora de los datos de precios en segundos Unix, no la hora de tu solicitud.

Gestión de errores de la API de precios del oro en Python

Una llamada fallida devuelve un código de estado HTTP de error y un cuerpo JSON con success: false y un objeto error que contiene un code numérico, un type y un mensaje info. Decide según el código, nunca según el texto del mensaje. Solo merece la pena reintentar unos pocos códigos:

  • 104 (too_many_requests, HTTP 429): el límite por minuto. Espera el número de segundos que indica la cabecera Retry-After.
  • 500, 503 y errores de red: reintenta con espera exponencial.
  • Todo lo demás, como 101 o 102 (clave ausente o no válida), 105 (cuota mensual agotada), 106 (endpoint no incluido en tu plan) y los errores de validación 2xx: corrige la solicitud, la clave o el plan.

Este pequeño módulo encapsula esa lógica. Reutiliza una única requests.Session, de modo que las conexiones permanecen abiertas entre llamadas, y lanza una excepción que puedes inspeccionar:

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:  # no es JSON, p. ej. una página de error HTML de un proxy
        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 a un endpoint. Solo reintenta límites de solicitudes, errores del servidor y fallos de red."""
    for attempt in range(retries + 1):
        try:
            return _get(path, params or {})
        except (MetalAPIError, requests.RequestException) as exc:
            code = getattr(exc, "code", None)  # None en errores de red
            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)

La espera exponencial aguarda 1, 2 y 4 segundos antes de rendirse. Reintentar no cuesta nada aquí, porque las solicitudes rechazadas con 104 o con un error del servidor no se facturan. Los errores de validación sí se facturan, otra razón más para no reintentarlos nunca en bucle. La guía de manejo de errores enumera todos los códigos con su estado HTTP.

Guardar los precios del oro en caché para ahorrar solicitudes

Cada plan actualiza los precios a un intervalo fijo: una vez al día en Free, cada 30 minutos en Lite, cada 10 minutos en Standard y cada 60 segundos en Advanced y Pro. Las actualizaciones caen en múltiplos de ese intervalo contados desde las 00:00 UTC, así que volver a preguntar antes de la siguiente devuelve las mismas cifras y gasta cuota para nada. Esta caché conserva cada respuesta hasta la siguiente actualización y sirve los últimos precios válidos si no se puede contactar con la API:

prices.py
import math
import time

import requests

from metalapi import MetalAPIError, call

# Segundos. Free: 86_400, Lite: 3_600 (una vez por hora cabe en sus 1000 solicitudes), 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])  # sirve los últimos precios válidos y reintenta en un minuto
        return hit[1]
    next_update = (math.floor(now / UPDATE_INTERVAL) + 1) * UPDATE_INTERVAL
    _cache[key] = (next_update + 5, data)  # caduca unos segundos después de la próxima actualización
    return data

latest("EUR", "XAU", "gram")["rates"]["EURXAU"] devuelve el precio en euros por gramo. Pide todos los símbolos que necesites en una sola llamada: XAU,XAG,XPT,XPD cuesta una solicitud, no cuatro. Ten en cuenta que esta caché vive en memoria. Un script lanzado por cron empieza cada vez con la caché vacía, así que ahí la programación es la caché: ejecútalo como máximo una vez por intervalo de actualización. Para vigilar tu cuota, llama a /usage, que nunca cuenta para ella.

Cargar una serie temporal del precio del oro en pandas

Para el historial, el endpoint /timeframe (alias /timeseries) devuelve los precios de cierre diarios entre start_date y end_date, indexados por fecha. Una solicitud cubre hasta 30 días en el plan Free y 365 días en los planes de pago, y un rango más largo falla con el error 207. Esta función divide cualquier rango en tramos permitidos y devuelve una Series de pandas:

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 en los planes de pago
    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}")

En el plan Free, estos diez meses requieren 10 solicitudes. La Series tiene una fila por día de negociación: los fines de semana y los festivos del mercado faltan en lugar de rellenarse, así que espera menos filas que días naturales. Si necesitas un valor para cada día, gold.asfreq("D").ffill() arrastra el cierre del viernes durante el fin de semana. A partir de aquí se aplican las herramientas habituales de pandas, como gold.pct_change() para las rentabilidades diarias o gold.rolling(20).mean() para una media móvil.

Los precios de cierre pasados nunca cambian, así que el archivo CSV es tu caché del historial: cárgalo la próxima vez en lugar de volver a pedir el mismo rango. Para una carga completa desde 2000 en una base de datos, consulta Precios históricos del oro y la plata por API.

Graficar el precio del oro con matplotlib

El gráfico lee el archivo CSV, así que volver a dibujarlo no cuesta solicitudes. Representa el cierre diario junto con una media móvil de 20 días:

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)

Como el índice contiene fechas reales, matplotlib espacia los puntos según el tiempo, y la media móvil empieza en el vigésimo día de negociación. Para graficar el oro en otra divisa, llama a gold_history con base="EUR": las claves de las cotizaciones siguen el mismo patrón, así que la función funciona sin cambios.

Próximos pasos

Ya tienes lo que necesitan la mayoría de las integraciones en Python: una solicitud autenticada, gestión de errores con reintentos razonables, una caché que sigue el intervalo de actualización de tu plan y el historial en pandas. Créala y pruébala en el plan Free. Cuando necesites precios más recientes, elige un plan según el intervalo de actualización y el volumen: una tarea que se ejecuta cada 10 minutos consume unas 4500 solicitudes al mes, que caben en las 10.000 de Standard. Compara los planes en la página de precios.

Si los precios acaban en un sitio web, API de precios del oro en JavaScript muestra cómo servirlos desde una ruta de Node.js sin exponer tu clave.

Preguntas frecuentes

¿Hay una API de precios del oro gratuita para Python?

Sí. El plan Free de MetalAPI incluye 100 solicitudes al mes con actualización diaria de precios y no requiere tarjeta de crédito, y todos los fragmentos de este artículo funcionan con él. Sus últimos precios son el cierre del día anterior, y una solicitud a /timeframe cubre hasta 30 días. API de precios del oro gratuita explica el plan gratuito con más detalle.

¿Por qué la cotización XAU es un número tan pequeño?

rates["XAU"] es la cotización inversa: onzas troy de oro por cada unidad de la divisa base. El precio está en la clave con prefijo, rates["USDXAU"] para la base USD o rates["EURXAU"] para la base EUR. Con los valores de ejemplo, 1 ÷ 0,0002501319 vuelve a dar 3997,89.

¿Cómo obtengo el precio del oro por gramo en Python?

Añade unit=gram a la solicitud, por ejemplo call("latest", {"base": "EUR", "currencies": "XAU", "unit": "gram"}). rates["EURXAU"] pasa a ser el precio en euros por gramo, y la respuesta incluye "unit": "gram". Evita unit=ounce: es la onza avoirdupois, no la onza troy en la que se cotiza el oro.

¿Desde cuándo hay historial del precio del oro?

Los precios diarios del oro empiezan en 2000, y todos los planes pueden consultar el historial completo. Los planes solo se diferencian en cuántos días puede cubrir una solicitud a /timeframe: 30 en Free y 365 en los planes de pago. Con max_days=365, todo el periodo desde 2000 hasta hoy requiere 27 solicitudes.

¿Con qué frecuencia debe llamar a la API un script de Python?

No con más frecuencia de la que tu plan actualiza los precios: una vez al día en Free, después de las 00:00 UTC; cada hora en Lite, porque cada 30 minutos supondría unas 1490 solicitudes al mes frente a una cuota de 1000; cada 10 minutos en Standard. Espera precios planos durante el fin de semana, cuando el oro al contado no cotiza desde el viernes a las 17:00 hasta el domingo a las 17:00, hora de Nueva York.

Pruébalo con tus propios datos

Crea una cuenta gratuita y haz tu primera solicitud en menos de dos minutos. 100 solicitudes al mes, sin tarjeta de crédito.

Sigue leyendo