sifting/io
Quant Research & Backtesting
7 min readSiftingIO Team

How accurate is real-time market data? A live spread snapshot across crypto, FX, and gold

Consolidated bid/ask spreads measured live on BTCUSD, EURUSD, XAUUSD and more: why one price is really a band, and how to reproduce the numbers yourself.

How accurate is real-time market data? A live spread snapshot across crypto, FX, and gold

How accurate is real-time market data Why do prices differ across sources Is one price really one number These questions come up whenever a dashboard shows one BTC price while an article published the same hour reports another. They deserve an answer grounded in measurements rather than adjectives, so this post presents a small timestamped dataset and a method you can rerun yourself this afternoon.

Start with the part most price displays hide. A live price is a two-sided quote: a best bid and a best ask, with a gap between them called the spread. Any single number you print (mid, last trade, close) is a point picked from inside that gap. The spread is the live uncertainty band around it, and you can measure it precisely at any millisecond.

All numbers below come from SiftingIO's consolidated feed at api.sifting.io, captured Wednesday 2026-08-26 between 10:48 and 10:50 UTC, a London morning. The capture is two matched rounds roughly 110 seconds apart. Treat it as a timestamped snapshot and a reproducible baseline rather than a multi-week statistic. The value is in the method and the orders of magnitude, and every figure carries the feed's own timestamp so you can check it.

Why prices differ across sources#

In fragmented markets, crypto and FX above all, there is no single official print. The same instrument trades on many independent venues at once. Books differ in depth, some feeds lag, and a thin pool can print a trade far from where real size clears. Each source reports its own local truth, so two apps reading two different sources will disagree, and both are technically correct about what they saw.

SiftingIO's response to that fragmentation is one consolidated price per instrument, formed as a volume-and-reputation-weighted median across multiple independent venues. A median has a useful property here: a minority of bad inputs, whether a lagging feed, a frozen feed, or a thin outlier print, can't drag the output, because more than half of the weighted inputs would have to err in the same direction before the blended number moves wrongly. That bound is also its honest limit. The output is a reference value rather than an official print from any single venue, and no median survives a majority of sources making the same coordinated error. The full pipeline (staleness checks, outlier scoring, per-venue reputation, weighted aggregation) is documented at /data-methodology.

One hard limit on this study follows from that design. The public API exposes only the final blended quote per symbol; per-source prices are never published. So this post can't report how far individual sources sat from each other in basis points, and doesn't. What it can measure honestly is the consolidated bid/ask spread: the two-sided disagreement embedded in the blended quote itself at a known millisecond.

The snapshot: spreads in basis points#

The measurement is one division: spread_bps = (ask - bid) / mid * 10000, where mid = (ask + bid) / 2. Basis points make asset classes comparable; 1 bp is 0.01%.

Round 1, at API timestamps t=1787741425269 through t=1787741426919:

SymbolBidAskSpread (bps)
BTCUSD78779.5760478782.323960.35
ETHUSD2473.707222473.802780.39
SOLUSD97.7888397.801171.26
EURUSD1.166321.166430.94
GBPUSD1.361981.362191.54
USDJPY159.083159.1021.19
XAUUSD4622.764622.76empty book (sizes 0), excluded

About 110 seconds later, a second matched round: BTCUSD had widened from 0.35 to 0.63 bps (bid 78795.38138, ask 78800.31862), EURUSD and USDJPY held at 0.94 and 1.19 bps, and XAUUSD now showed a real two-sided book at 2.90 bps with size 20 on each side.

Three readings follow. First, the band is small but real: 0.35 bps on a 78,780 BTC price is about $2.75 between bid and ask, and 0.94 bps on EURUSD is about 1.1 pips. An app quoting these instruments to more precision than that is displaying false certainty. Second, the band breathes. BTC's spread nearly doubled in under two minutes with no news attached, so a data-accuracy figure quoted without a timestamp says very little. Third, the band can vanish for the wrong reason: gold's round-1 book came back with bid equal to ask and zero size on both sides. Naive code would score that empty book as a perfect 0.0 bps spread.

A qualitative note on timeliness#

Spread measures one axis of accuracy: how tight the quote is. The other axis is whether the number is current. One illustration from the same week, offered as an anecdote and nothing more. When BTCUSD broke above $80,000 on Aug 24-25, the consolidated feed timestamped the break, the intraday tap near $81,300, and the rejection that followed. Written coverage published during that same hour still described prices below $79,000. That is a headline-versus-tick comparison, so it supports no seconds-of-lead claim. It does show why a timestamp belongs next to every price: a number without one may describe a market that no longer exists.

Measuring real-time market data accuracy yourself#

Every quote endpoint returns the fields needed: b and a for the book, B and A for sizes, and t, the feed's own millisecond epoch timestamp. The price and size fields come back as quoted strings, so convert them to numbers before any arithmetic; only t is already an integer.

curl -H "X-API-Key: $SIFTING_KEY" \
  "https://api.sifting.io/v1/last/quote/crypto/BTCUSD"

A minimal Python harness for the basket above:

import requests

BASKET = [("crypto", "BTCUSD"), ("crypto", "ETHUSD"), ("crypto", "SOLUSD"),
          ("forex", "EURUSD"), ("forex", "GBPUSD"), ("forex", "USDJPY"),
          ("commodities", "XAUUSD")]

def spread_bps(cls, sym, key):
    q = requests.get(
        f"https://api.sifting.io/v1/last/quote/{cls}/{sym}",
        headers={"X-API-Key": key},
    ).json()
    # Prices and sizes arrive as quoted strings; only t is a number.
    bid, ask = float(q["b"]), float(q["a"])
    bid_sz, ask_sz = float(q["B"]), float(q["A"])
    if bid_sz == 0 or ask_sz == 0:
        return {"s": sym, "t": q["t"], "bps": None}  # empty book
    mid = (ask + bid) / 2
    return {"s": sym, "t": q["t"], "bps": (ask - bid) / mid * 1e4}

Log each result against the returned t field. Run the loop on a fixed cadence across the UTC day (Asia, London, the London/New York overlap, the US afternoon) and time-of-day structure appears within a few sessions: spreads compress when two regions trade at once and widen when books thin out. That longer capture is the study this snapshot is a baseline for.

Common pitfalls#

An empty book scores as a perfect spread. The XAUUSD round-1 row above returned bid equal to ask with zero size on both sides. Code that computes (ask - bid) without checking B and A records 0.0 bps and drags every average toward false tightness. Skip any quote where either size is zero.

The quote fields are strings, not numbers. Bid, ask, and both sizes come back quoted, so subtracting them without a float conversion either concatenates text or raises a type error, and a size check against a plain zero silently never matches. Convert b, a, B, and A to numbers at the boundary; the t timestamp is already an integer.

Logging against your own clock corrupts time-of-day analysis. Local receive time includes network latency and any queueing in your harness, so always key measurements to the API's t. Related: a 503 with error code stale_snapshot means the live value is older than the freshness threshold. Treat it as a gap in the series; silently reusing the previous quote fabricates data.

The free tier allows 60 REST requests per minute. Polling this 7-symbol basket every 5 seconds is 84 requests per minute and returns 429 rate_limit_exceeded partway through each cycle, which leaves rounds half-matched. Watch X-RateLimit-Remaining, honor Retry-After, and either widen the cadence or subscribe over the WebSocket at wss://stream.sifting.io/ws/v1, where one connection pushes every update without counting against the REST quota.

The quote endpoints used here are available on the free tier, so the whole snapshot can be reproduced without a card on file. Read the docs

Keep reading

Related posts

How accurate is real-time market data? A live spread snapshot across crypto, FX, and gold · SiftingIO