Skip to content

Open In Colab

Open In Colab

Precious Metals: Daily Spot Price Snapshot

A quick daily view of gold, silver, platinum, and palladium with performance, volatility, and a simple 12-month trend projection.

Data source & cadence: Alpha Vantage (daily), using TIME_SERIES_DAILY with symbolUSD and an FX_DAILY fallback, refreshed each morning with the AV_API_KEY secret. Timestamps below show the latest run (UTC).

Precious Metal Spot Price Comparison (5 Years)

This notebook compares precious metal spot prices over the last five years and builds a simple forecast using interactive charts.

Where to get spot price data

Common sources for spot price data include:

  • LBMA (London Bullion Market Association): Official daily gold and silver price benchmarks. Useful for authoritative spot pricing.
  • Metals-API: Paid/free tiers with JSON API access for multiple metals (gold, silver, platinum, palladium).
  • Alpha Vantage: Free tier provides precious metals data with API keys and rate limits.
  • Quandl/Nasdaq Data Link: Offers LBMA and other datasets (free and paid).
  • Yahoo Finance: Convenient access for analysis (e.g., XAUUSD=X, XAGUSD=X, XPTUSD=X, XPDUSD=X). While not an official benchmark, it's easy to use for exploratory analysis.

This notebook uses Yahoo Finance spot proxies for convenience.

# If needed, install dependencies
!pip install -q yfinance pandas plotly statsmodels
import os
import time
import pandas as pd
import requests
import plotly.express as px
import plotly.graph_objects as go
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from datetime import datetime, timezone
from IPython.display import Markdown, display
import yfinance as yf

Data download

Download the last five years of daily closes for each metal.

tickers = {
    "Gold (XAUUSD)": "GC=F",
    "Silver (XAGUSD)": "SI=F",
    # "Platinum (XPTUSD)": "PL=F",
    # "Palladium (XPDUSD)": "PA=F",
}

price_frames = []
skipped = []

for label, yahoo_symbol in tickers.items():
    try:
        ticker = yf.Ticker(yahoo_symbol)
        # Download data for the last 5 years using Ticker.history
        # auto_adjust=True is the default for history(), so no need to specify.
        # progress=False is not a valid argument for history()
        data = ticker.history(period="5y", interval="1d")
        if not data.empty:
            # Use 'Close' as auto_adjust handles splits/dividends
            series = data['Close'].rename(label)
            price_frames.append(series)
        else:
            skipped.append(f"{label} (No data returned from Yahoo Finance for symbol {yahoo_symbol})")
    except Exception as e:
        skipped.append(f"{label} (Error fetching data from Yahoo Finance: {type(e).__name__} - {e})")
    time.sleep(1) # Be nice to Yahoo Finance

if not price_frames:
    raise ValueError("No price data returned from Yahoo Finance for any metal. Skipped: " + ", ".join(skipped))

prices = pd.concat(price_frames, axis=1).sort_index().ffill().dropna(how="all")
if prices.empty:
    raise ValueError("Price data empty after cleaning; check Yahoo Finance availability.")
prices.index.name = "Date"
prices.tail()
Gold (XAUUSD) Silver (XAGUSD)
Date
2026-01-09 00:00:00-05:00 4490.299805 78.884003
2026-01-12 00:00:00-05:00 4604.299805 84.610001
2026-01-13 00:00:00-05:00 4589.200195 85.876999
2026-01-14 00:00:00-05:00 4626.299805 90.869003
2026-01-15 00:00:00-05:00 4620.500000 92.209999
normalized = prices / prices.iloc[0] * 100
normalized.head()
Gold (XAUUSD) Silver (XAGUSD)
Date
2021-01-15 00:00:00-05:00 100.000000 100.000000
2021-01-19 00:00:00-05:00 100.557588 101.828796
2021-01-20 00:00:00-05:00 102.000764 103.661625
2021-01-21 00:00:00-05:00 101.967966 104.016112
2021-01-22 00:00:00-05:00 101.443170 102.815707
pct_change = prices.pct_change().dropna() * 100
pct_change.head()
Gold (XAUUSD) Silver (XAGUSD)
Date
2021-01-19 00:00:00-05:00 0.557588 1.828796
2021-01-20 00:00:00-05:00 1.435174 1.799913
2021-01-21 00:00:00-05:00 -0.032155 0.341965
2021-01-22 00:00:00-05:00 -0.514668 -1.154056
2021-01-25 00:00:00-05:00 -0.043106 -0.270335

Spot price history

See how absolute prices have moved over time.

fig = px.line(
    prices.reset_index(),
    x="Date",
    y=prices.columns,
    title="Spot Price Comparison (Last 5 Years)",
    labels={"value": "USD per troy ounce", "Date": "Date"},
)
fig.update_layout(legend_title_text="Metal")
fig.show()

Performance since start

Each series is rebased to 100 on day one to compare relative performance.

fig = px.line(
    normalized.reset_index(),
    x="Date",
    y=normalized.columns,
    title="Normalized Performance (100 = Start of Period)",
    labels={"value": "Index (100 = start)", "Date": "Date"},
)
fig.update_layout(legend_title_text="Metal")
fig.show()

Day-to-day volatility

Daily percent change highlights short-term swings and risk.

fig = px.line(
    pct_change.tail(30).reset_index(),
    x="Date",
    y=pct_change.columns,
    title="Daily Percent Change (Last 30 Days)",
    labels={"value": "Percent change (%)", "Date": "Date"},
)
fig.update_layout(legend_title_text="Metal")
fig.show()

Simple Forecast (12 Months)

We use a basic Holt-Winters exponential smoothing model on monthly averages to produce a simple, transparent forecast.

12-month outlook

Monthly averages feed a simple Holt-Winters model to sketch a near-term trend. Treat this as directional only.

monthly = prices.resample("M").mean().dropna()
forecast_horizon = 12

forecast_frames = []
for metal in monthly.columns:
    model = ExponentialSmoothing(
        monthly[metal],
        trend="add",
        seasonal=None,
        initialization_method="estimated",
    ).fit()
    forecast = model.forecast(forecast_horizon)
    forecast.name = metal
    forecast_frames.append(forecast)

forecast_df = pd.concat(forecast_frames, axis=1)
forecast_df.tail()
/tmp/ipython-input-120228698.py:1: FutureWarning:

'M' is deprecated and will be removed in a future version, please use 'ME' instead.


Gold (XAUUSD) Silver (XAGUSD)
2026-09-30 00:00:00-04:00 5756.569793 197.002944
2026-10-31 00:00:00-04:00 5912.852272 211.483924
2026-11-30 00:00:00-05:00 6069.134752 225.964904
2026-12-31 00:00:00-05:00 6225.417231 240.445885
2027-01-31 00:00:00-05:00 6381.699710 254.926865
fig = go.Figure()
for metal in monthly.columns:
    fig.add_trace(
        go.Scatter(
            x=monthly.index,
            y=monthly[metal],
            name=f"{metal} (history)",
        )
    )
    fig.add_trace(
        go.Scatter(
            x=forecast_df.index,
            y=forecast_df[metal],
            name=f"{metal} (forecast)",
            line=dict(dash="dash"),
        )
    )

fig.update_layout(
    title="12-Month Forecast (Holt-Winters, Monthly Avg)",
    xaxis_title="Date",
    yaxis_title="USD per troy ounce",
    legend_title_text="Series",
)
fig.show()


timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')
display(Markdown(f'**Last updated:** {timestamp}'))

Last updated: 2026-01-15 22:10 UTC