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.329,75−0,55 %ETH2329,36−1,13 %
Todos los artículos
OroPlataDatos históricosTutorial

Precios históricos del oro y la plata por API: 25 años de datos diarios

Obtén precios diarios del oro y la plata desde 2000 con /historical, /timeframe, /change y /ohlc, divide los rangos largos por años, guarda los datos y crea gráficos.

Por MetalAPI Team··10 min de lectura

Qué historial está disponible

MetalAPI guarda una barra diaria por símbolo y día, con apertura, máximo, mínimo y cierre. Para el oro y la plata son más de 25 años de datos diarios: suficientes para representar tendencias a largo plazo, probar una regla de precios con datos del pasado o revalorizar facturas antiguas al precio del día.

CategoríaSímbolosHistorial diario desde
Metales preciososXAU, XAG, XPT, XPD2000
Metales industriales y energíaXCU, ALU, ZNC, HRC, WTI, BRENT y otros2000
Divisas57, entre ellas USD, EUR, GBP y JPY1999-01-04
Criptomonedas13, entre ellas BTC y ETH2014

Todos los planes pueden consultar el historial completo. Los planes solo se diferencian en cuántos días puede abarcar una única solicitud de rango. Las fechas usan el formato YYYY-MM-DD en UTC, y los valores de cada día son los precios de cierre de ese día. Una fecha anterior al inicio del historial de un símbolo devuelve el error 300 (no_results).

Los precios de los metales anteriores a octubre de 2026 se derivan de los futuros del primer vencimiento, por lo que pueden diferir ligeramente del precio al contado del mismo día. Tenlo en cuenta cuando compares valores antiguos con precios al contado de otra fuente.

Cuatro endpoints cubren el trabajo con datos históricos: /historical para un día, /timeframe para un rango, /change para la variación entre dos días y /ohlc para la vela de un día. Las secciones siguientes los recorren uno a uno y después construyen un pequeño archivo local con un gráfico encima.

Un día: /historical

Para obtener los precios de cierre de un solo día, pon la fecha en la ruta. /historical?date=YYYY-MM-DD hace lo mismo, y /yesterday es un atajo para el día UTC anterior:

bash
curl "https://metalapi.com/api/v1/2024-01-15?currencies=XAU,XAG" \
  -H "X-API-Key: $METALAPI_KEY"

La respuesta tiene la misma forma que la de /latest, más historical: true. Estos son los valores de ejemplo de la referencia de la API:

Respuesta de ejemplo
{
  "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 es el precio de una onza troy de oro en la divisa base, y XAU es su inverso. base y unit funcionan como siempre, así que base=EUR&unit=gram devuelve euros por gramo para ese día.

Los fines de semana y los festivos no hacen fallar la solicitud: para un día sin negociación se devuelve el cierre del día de negociación más reciente. El campo date de la respuesta sigue siendo el día que pediste, así que si necesitas saber si los mercados estaban abiertos, mantén un calendario de negociación por tu cuenta. Una fecha futura devuelve el error 205 (invalid_date).

Rangos de fechas: /timeframe

Para una serie, /timeframe devuelve una entrada por día entre start_date y end_date, ambos incluidos, con la fecha como clave. /timeseries es un alias que marca la respuesta con timeseries: true en lugar de timeframe: true.

bash
curl "https://metalapi.com/api/v1/timeframe?start_date=2026-10-19&end_date=2026-10-23&currencies=XAU" \
  -H "X-API-Key: $METALAPI_KEY"
Respuesta de ejemplo
{
  "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 }
  }
}

A diferencia del endpoint de un solo día, /timeframe no rellena los huecos. Los metales al contado dejan de negociarse desde el viernes a las 17:00 hasta el domingo a las 17:00, hora de Nueva York, así que los fines de semana y los festivos de mercado pueden faltar en el resultado. No supongas que hay una entrada por cada día del calendario y, si mezclas símbolos con calendarios de negociación distintos, como el oro y el bitcoin, comprueba que una clave existe antes de leerla.

El rango más largo que puede abarcar una sola solicitud depende del plan:

PlanRango máximo por solicitudUnidad de paginación que siempre cabe
Free30 díasUn mes natural
Lite y superiores365 díasUn año natural

El límite es la distancia entre las dos fechas, así que de 2025-01-01 a 2025-01-31 cabe en el límite de Free y de 2024-01-01 a 2024-12-31 cabe en el límite de los planes de pago, incluso en un año bisiesto. Un rango más largo devuelve el error 207 (timeframe_too_long) con una upgrade_url. Cada llamada a /timeframe cuenta como una solicitud, sin importar cuántos días devuelva.

Comparar dos fechas: /change

Cuando solo necesitas la variación entre dos días, como la rentabilidad en lo que va de año en un panel, /change devuelve la cotización inicial, la cotización final, la variación absoluta y la variación porcentual sin descargar los días intermedios:

bash
curl "https://metalapi.com/api/v1/change?start_date=2026-01-02&end_date=2026-10-23&currencies=XAU,XAG" \
  -H "X-API-Key: $METALAPI_KEY"
Respuesta de ejemplo (solo oro)
{
  "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 }
  }
}

Lee la clave con prefijo, USDXAU. La clave inversa XAU se mueve en sentido contrario, así que su change_pct tiene el signo opuesto y otra magnitud: en este ejemplo el precio del oro bajó un 7,43 % mientras que el inverso subió un 8,03 %. Mostrar el porcentaje inverso como variación del precio es un error fácil de llevar a producción.

/change tiene los mismos límites de rango que /timeframe. Igual que /historical, usa el cierre más reciente cuando una fecha cae en fin de semana o festivo.

Velas diarias: /ohlc

/ohlc devuelve la apertura, el máximo, el mínimo y el cierre de un día, en ambas direcciones: USDXAU en dólares por onza y XAU como inverso. Sin date, o con date=latest, devuelve la barra de hoy hasta el momento.

bash
curl "https://metalapi.com/api/v1/ohlc?date=2026-10-23&currencies=XAU" \
  -H "X-API-Key: $METALAPI_KEY"
Respuesta de ejemplo
{
  "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 }
  }
}

Fíjate en que, en el bloque inverso, el máximo procede del precio más bajo y el mínimo del precio más alto, porque la inversión invierte el orden. Representa en el gráfico el bloque USDXAU.

Cada llamada cubre un día, así que un gráfico de velas de un año completo requiere una solicitud por día de negociación. Para gráficos largos, los precios de cierre de /timeframe son mucho más baratos. Usa /ohlc para las velas recientes o para mostrar el rango de negociación del día junto al precio actual.

Para velas semanales o mensuales de muchos años, también puedes construir barras a partir de los cierres diarios almacenados: la apertura es el primer cierre del periodo, el cierre es el último, y el máximo y el mínimo son los extremos de los cierres. No recogen los máximos y mínimos intradía, así que indica que se basan en cierres, pero sirven para gráficos de largo plazo.

Descargar 25 años, un año por solicitud

Con un límite de 365 días por solicitud, el historial completo del oro y la plata desde 2000 requiere una llamada a /timeframe por año natural: 27 solicitudes de 2000 a 2026. Cabe sin problemas en la cuota mensual de cualquier plan de pago. En el plan Free, las ventanas de 30 días necesitarían más de 300 solicitudes, unas tres veces su cuota mensual de 100, así que una descarga completa no es práctica; unos pocos meses de historial sí lo son.

Este script de Python pagina por años, respeta el límite por minuto y escribe las claves de precio de ambos metales en SQLite. Guárdalo como backfill.py:

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"):
    """Cierres diarios entre start y end (ambos incluidos), con la fecha como clave."""
    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:  # límite por minuto: espera y reintenta (no se factura)
            time.sleep(int(r.headers.get("Retry-After", "60")))
            continue
        if err["code"] == 300:  # no hay datos en esta ventana
            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)

Puedes volver a ejecutar el script sin riesgo: insert or replace sobrescribe las filas que ya existen, así que una descarga interrumpida se puede reiniciar sin más. El año en curso termina en la fecha UTC de hoy, porque una end_date futura devuelve 205. Los límites por minuto son más bajos en los planes más pequeños (10 solicitudes por minuto en Free y 30 en Lite), por eso el script respeta Retry-After ante el error 104 en lugar de fallar.

Guarda USDXAU y USDXAG, los precios, no las claves inversas. Si necesitas el historial en otra divisa, pasa base o guarda también los precios en dólares de las divisas y convierte localmente con las filas del mismo día.

Almacenar y actualizar los datos

Los precios de cierre de los días pasados no cambian, así que una copia local es la forma más barata y rápida de servir gráficos e informes. Unas pocas reglas la mantienen correcta:

  • Identifica las filas por día, símbolo y base. La tabla anterior usa (day, symbol, base) como clave primaria, así que descargar un rango dos veces sobrescribe las filas en lugar de duplicarlas.
  • Registra lo que pediste. Guarda base, y unit si lo usas, junto con los datos. Mezclar filas por gramo y por onza en una misma columna es una causa clásica de picos en un gráfico.
  • Actualiza una vez al día. Después de la descarga inicial, una solicitud diaria mantiene la tabla al día. Descarga los últimos siete días, para que una ejecución fallida o un cierre tardío se completen automáticamente.
  • Considera provisional el día de hoy. Solo la barra del día en curso sigue moviéndose. Déjala fuera del archivo o sobrescríbela en la siguiente ejecución, como hace el script de abajo.
daily.py (ejecútalo una vez al día, después de las 00:00 UTC)
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")

A una solicitud por día, la actualización cuesta unas 30 solicitudes al mes, lo que deja margen incluso en el plan Free. Para archivos más grandes, el mismo patrón funciona con PostgreSQL, un archivo Parquet o cualquier base de datos de series temporales; solo cambia la función store.

Antes de construir nada sobre una descarga histórica, revísala. Tres comprobaciones detectan la mayoría de los problemas:

  • Filas por año. Cada año completo debería tener aproximadamente una fila por día de negociación para cada metal. Un año con muchas menos filas indica que una solicitud falló o no devolvió datos.
  • Huecos. Un fin de semana normal deja tres días entre dos cierres y un puente largo, cuatro. Cualquier hueco mayor merece una revisión, y volver a descargar esa ventana cuesta una sola solicitud.
  • Saltos. Un movimiento de un día a otro por un factor de aproximadamente 31 o 1000 apunta a unidades mezcladas, y un valor cercano a cero, a una clave inversa que se ha colado.

Las dos primeras son consultas breves en SQLite:

checks.sql
-- Filas por año: un año completo tiene aproximadamente una fila por día de negociación
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;

-- Huecos de más de cuatro días entre cierres almacenados
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;

Representar los datos en gráficos

Con los datos almacenados localmente, los gráficos no consumen solicitudes. Este script de matplotlib representa el precio del oro en dólares junto con la relación oro/plata, es decir, el número de onzas de plata que se compran con una onza de oro:

chart.py
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)

En un sitio web, sirve la serie almacenada desde tu propio backend y dibújala en el navegador. Tu clave API se queda en el servidor y las visitas a la página no gastan tu cuota. Una página mínima con Chart.js que lee un array JSON de objetos { day, price } desde tu propio endpoint:

gold-chart.html
<canvas id="gold" height="320"></canvas>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.js"></script>
<script>
  // Tu propio endpoint, que devuelve [{ "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>

Veinticinco años de días de negociación son más de 6000 puntos por serie. Desactivar los marcadores de puntos y la animación, como arriba, mantiene el gráfico fluido; en pantallas pequeñas, plantéate reducir la serie a cierres semanales.

La guía de datos históricos incluye una función auxiliar en JavaScript para rangos y un ejemplo de media móvil, y la documentación de la API enumera todos los parámetros. Si trabajas con asistentes de IA, el servidor MCP de MetalAPI puede obtener las mismas series temporales desde un chat. Para claves, errores y caché en general, empieza por la guía de la API de precios del oro, y compara los límites de rango y las cuotas en la página de precios.

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