sifting/io
Commodities
7 min readSiftingIO Team

Gold-silver ratio in Python: calculate it from API prices

Calculate the gold-silver ratio with Python and commodity price data. Match timestamps, preserve missing bars and avoid misleading ratio candles.

Gold-silver ratio in Python: calculate it from API prices

The gold-silver ratio is the gold price divided by the silver price, with both quoted in the same currency and weight unit. If gold is $3,000 per troy ounce and silver is $30, the ratio is 100. That is a price comparison, not a prediction or a promise that physical metal can be exchanged at that rate.

For a metals dashboard, the useful part is calculating the ratio consistently. This guide uses the SiftingIO Commodities API to obtain gold and silver history, then joins matching hourly bars in Python. You can create a free account and get an API key to follow along. The worked examples below use invented prices so the calculation is easy to check.

What does the gold-silver ratio measure?#

Use this formula:

gold-silver ratio = gold price per troy ounce / silver price per troy ounce

A value of 100 means one troy ounce of gold has the same reference-price value as 100 troy ounces of silver, before transaction costs and physical premiums. It does not mean gold is certain to fall or silver is certain to rise.

Gold, USD per troy ounceSilver, USD per troy ounceRatio
3,00030100.00
3,0003293.75
3,1203297.50

These are arithmetic examples, not current market prices. The second row shows the ratio falling because silver rises while gold stays unchanged. In the third, gold rises too. A ratio chart alone cannot tell you which individual price changed, so keep both input prices available beside it.

Match the units before dividing#

SiftingIO uses XAUUSD for gold and XAGUSD for silver, quoted in US dollars per troy ounce. The gold symbol page and silver symbol page provide the instrument context.

Do not combine gold in dollars per gram with silver in dollars per ounce. The numbers may look plausible, but the result is not the usual gold-silver ratio. Likewise, convert prices to the same currency before dividing if they arrive in different currencies.

The LBMA's guide to precious-metal pricing explains the troy-ounce convention and why a wholesale reference price differs from the price of a small physical bar or jewellery. That is background on units, not a claim that this example uses an LBMA benchmark feed.

Keep the price basis consistent as well. A spot reference price divided by a futures contract price mixes two different instruments. For this tutorial, use the two reference-price series from the same API product rather than substituting an ETF share price or a retail dealer's quote.

Download the same hourly window for gold and silver#

The historical commodities endpoint returns an OHLCV array in data and request metadata in meta. Each bar has an opening timestamp t, in Unix milliseconds, and a closing price c.

Set SIFTINGIO_API_KEY in your local shell or server environment. Then run these commands in a new working folder. The files contain price data, not your key. --fail stops on HTTP errors, while --compressed requests and decodes gzip.

curl --fail --show-error --silent --compressed \
  -H "X-API-Key: $SIFTINGIO_API_KEY" \
  "https://api.sifting.io/v1/hist/commodities/XAUUSD/bars?start=2026-10-01T00:00:00Z&end=2026-10-01T23:59:59Z&interval=1h&limit=100" \
  -o gold.json

curl --fail --show-error --silent --compressed \
  -H "X-API-Key: $SIFTINGIO_API_KEY" \
  "https://api.sifting.io/v1/hist/commodities/XAGUSD/bars?start=2026-10-01T00:00:00Z&end=2026-10-01T23:59:59Z&interval=1h&limit=100" \
  -o silver.json

Only continue when both commands succeed. These requests use a completed historical day rather than today's still-forming bar. They ask for the same interval and boundaries; the number of returned bars still depends on available observations. A market closure or missing observation is not a zero price.

The window is intentionally small. If you expand it, follow meta.next_cursor until all pages are collected. The script below refuses a response containing a next cursor so it cannot quietly calculate from only the first page. For a larger export, use the cursor pagination guide.

Calculate the ratio in Python without hiding gaps#

Save the following as gold_silver_ratio.py beside the two JSON files. It uses Python's standard library, so no package installation is needed.

import csv
import json
import sys
from datetime import datetime, timezone
from decimal import Decimal


def load_closes(path, symbol):
    with open(path, encoding="utf-8") as source:
        payload = json.load(source, parse_float=Decimal)
    meta = payload["meta"]
    if meta.get("symbol") != symbol or meta.get("interval") != "1h":
        raise ValueError(f"{path}: wrong symbol or interval")
    if meta.get("next_cursor"):
        raise ValueError(f"{path}: collect every page before calculating")
    if not isinstance(payload["data"], list):
        raise ValueError(f"{path}: expected a data array")
    closes = {}
    for bar in payload["data"]:
        timestamp, raw_close = bar["t"], bar["c"]
        if type(timestamp) is not int or timestamp <= 0:
            raise ValueError(f"{path}: invalid millisecond timestamp")
        if timestamp in closes:
            raise ValueError(f"{path}: duplicate bar timestamp")
        if isinstance(raw_close, bool) or raw_close is None:
            raise ValueError(f"{path}: invalid close")
        close = Decimal(str(raw_close))
        if not close.is_finite() or close <= 0:
            raise ValueError(f"{path}: close must be finite and positive")
        closes[timestamp] = close
    return closes


def write_ratios(gold, silver, output):
    timestamps = sorted(gold.keys() | silver.keys())
    if not timestamps:
        raise ValueError("No bars returned for either metal")
    writer = csv.writer(output)
    writer.writerow(["bar_open_utc", "gold_close", "silver_close", "ratio", "status"])
    for timestamp in timestamps:
        g, s = gold.get(timestamp), silver.get(timestamp)
        label = datetime.fromtimestamp(timestamp / 1000, timezone.utc).isoformat()
        ratio = f"{g / s:.2f}" if g is not None and s is not None else ""
        status = "matched" if ratio else "missing_gold" if g is None else "missing_silver"
        writer.writerow([label, g, s, ratio, status])


if __name__ == "__main__":
    write_ratios(load_closes("gold.json", "XAUUSD"),
                 load_closes("silver.json", "XAGUSD"), sys.stdout)

Run python3 gold_silver_ratio.py > ratio.csv. The result can be opened in a spreadsheet or used as the input to a line chart. The ratio is rounded to two decimal places for display, after division. Python's Decimal documentation explains the arithmetic and rounding model.

The code joins on timestamps, not array positions. An absent silver bar therefore cannot shift every later silver price against the wrong gold price. It also rejects duplicate timestamps rather than choosing one silently.

Here is a small invented example of the output:

Bar opening time, UTCGold closeSilver closeRatioStatus
2026-10-01 09:003,00030100.00matched
2026-10-01 10:003,030unavailableunavailablemissing_silver
2026-10-01 11:003,0403295.00matched

The CSV leaves unavailable values blank. In a chart, render those blanks as gaps rather than zero or an automatically connected line. The script retains timestamps found in either file; an interval missing from both files is not detectable from that union alone. Detecting those intervals requires a separate expected-session calendar.

Matching hourly timestamps means the bars share a labeled interval, not that their final observations occurred at exactly the same instant. Call the result an hourly close-price ratio, not a synchronized executable quote. This distinction matters most when one instrument has sparse updates or a gap near the interval boundary.

Why gold-high divided by silver-high is not a ratio high#

Separate OHLCV bars discard the order of prices within each interval. Their highs and lows need not occur together. Consider two observations in one invented interval:

ObservationGoldSilverGold / silver
First3,00030100
Second3,1004077.5

Dividing gold's high by silver's high gives 3,100 / 40 = 77.5, yet the observed ratio high was 100. Dividing gold's high by silver's low gives about 103.33, a value that never occurred in this example.

For ratio candles, first calculate a ratio series from time-aligned observations, with an explicit rule for acceptable time differences and missing values. Then aggregate that series into open, high, low and close. Finer input data gives a finer sampled result; it does not recover observations your feed never supplied. The hourly close-ratio CSV above is a line-series input, not reconstructed ratio OHLCV.

Put the ratio beside the prices, not in place of them#

A useful dashboard shows the ratio, both component prices, the interval and the date range. That lets a reader distinguish a silver move from a gold move and see when data is unavailable. Do not attach a fixed "buy" or "sell" label to a ratio threshold without a separate, tested investment methodology.

SiftingIO's Commodities product supplies the gold and silver price inputs; your application owns the calculation and presentation. Start with the two downloads, verify the missing-bar case, and then extend the date range. The Commodities API overview covers the product, and the free account registration is the next step when you are ready to try the requests.

Keep reading

Related posts