How exchange data feeds differ

Two venues quoting the same pair do not produce the same candles, and the reasons are structural rather than accidental.

11 minute read · no account needed · nothing on this page is advice about what to buy or sell

What you will leave with

  • Every venue is its own market: its own book, its own participants, its own last trade. Identical hourly bars from two venues differ by a small but visible amount.
  • Sparse against dense: some venues emit zero-volume filler for intervals with no trades and some omit the bucket entirely, which changes what a gap in your chart means.
  • Bucket boundaries and timezones, and why every serious store keeps UTC and labels late.
  • Revisions and late trades: a bar can change after it closed, and a store has to decide whether it believes the first answer or the last.
  • Why stitching two venues into one series produces a seam that quietly corrupts every indicator across the join.

01Every venue is its own market, not a view of one market

The most useful correction anybody can make early is this: there is no such thing as “the price of BTC/USD”. There is the price on one exchange, which is the price of its own last trade, in its own order book, between its own participants, under its own fee schedule and its own set of people who can reach it. Two venues quoting the same pair are two markets that happen to be about the same asset, and arbitrage keeps them close without ever making them equal.

two venues, one pair, hourly closesvenue Avenue B023B − A, in basis points: it changes sign, it changes size, and it is never zero.
At chart scale the two lines are one line, and that is the observation rather than a drafting problem — so the lower pane plots the difference instead. Arbitrage enforces the shape; it does not make two order books clear at the same instant.

A few basis points sounds negligible until you remember what a chart is used for. A stop placed at a level that only one venue reached; a high that one book printed and another did not; an alert that fired on one feed and not the other. None of those is a data error. They are the honest consequence of asking a question about one market and getting the answer from another.

02Sparse against dense: what a missing bar means

Ask two venues for the same window on an illiquid symbol and you can get two payloads of very different size, both correct. The difference is what they do about an interval in which nothing traded.

Dense
Every bucket in the requested range comes back. Quiet intervals are padded with a filler bar carrying the previous close as open, high, low and close, and a volume of zero.
Sparse
Only buckets that saw trades come back. A quiet interval is simply absent from the response, and the timestamps jump.
DENSEevery bucketcarry-forward filler,volume 0, o=h=l=cSPARSEtraded buckets onlythe bucket is simply absent
The same twelve hours from a dense feed and a sparse one. Both are accurate; they disagree about whether “nothing happened” deserves a row.

The proportions are larger than intuition suggests. One hourly request against an illiquid pair on a dense venue returned 721 rows of which 501 were filler — seventy per cent of the payload describing intervals in which nobody traded. On a sparse venue the same window would have returned 220 rows. That is the argument for normalising to sparse at ingest and reconstructing density at display time if you want it: storing filler means storing, and then aggregating, and then transmitting, a great deal of carefully preserved nothing.

03Bucket boundaries, and the day that does not start at midnight

A bucket has two candidate labels — the instant it opened and the instant it closed — and both conventions are in use. Most exchange candle endpoints label by the open. At least one widely used market-data API labels by the close, so its value at 14:00 is the price at 14:00, which is the close of the bucket that opened at 13:00. Read it as an open-labelled series and every bar on the chart is shifted by exactly one interval.

read correctly — each bucket sits at the hour it OPENEDread wrongly — a close-labelled feed taken for an open-labelled oneone whole bucket late09:0010:0011:0012:0013:0014:0015:00
A one-bucket shift is the least detectable data error there is: the series is still ascending, highs still cover opens and closes, every indicator still computes, and the volume pane still lines up with the price pane — because both are wrong the same way.

Daily bars carry a second version of the same problem. A “day” is a timezone decision, and a venue headquartered outside UTC may anchor its daily candle to its own local midnight. One major venue’s plain daily bar opens at 16:00 UTC; the same venue offers an explicitly UTC-anchored variant under a different parameter, and the two series are eight hours apart on every single bar. Stored side by side with a UTC daily series from anywhere else, that is a permanent, unexplained offset that no invariant check catches.

04The shape of a response is not the shape you assume

Beyond the semantic differences, the mechanical ones are where an integration actually breaks — and they break quietly, because a mis-parsed candle is still a well-formed candle. Four that are worth knowing about in general, each of which is a real, measured property of a real public API:

Column order
One major venue returns rows as [time, low, high, open, close, volume]. Read as OHLC — which is what almost every other venue returns — it swaps the body of every candle: rising bars render as falling ones. The series still satisfies high ≥ low, still ascends, still renders, and no invariant check can catch it.
Page limits are not row limits
One venue caps a request at 300 aggregation intervals between the start and end you name, whatever that window happens to contain — so a 300-minute window returning ten rows is a quiet market, not a short page. Another caps every response at exactly 721 rows regardless of what you ask for.
The key is not the symbol you sent
One venue accepts a request for XBTUSD and returns the data under XXBTZUSD, alongside a continuation cursor stored in the same object as the data. Indexing the response by the symbol you requested returns nothing for dozens of pairs — including most of the major ones.
Numbers arrive as strings
Including timestamps. Sorting unparsed string timestamps appears to work, because JavaScript coerces them for a greater-than comparison and not for subtraction. The bug surfaces later, somewhere else, as arithmetic.

The general lesson is stronger than any of the four: verify a feed against evidence outside its own documentation. Two independent venues quoting the same instant should agree to a few basis points; aligned wrongly, they disagree by an order of magnitude more. That comparison settles column order and time anchor at once, and it does not require anybody to have documented either correctly.

05An empty response does not mean the end of history

If you walk backwards through a sparse venue’s history, eventually you get an empty page. The obvious reading is “we have reached the beginning”. It is wrong, and it is wrong in the most expensive possible way, because “this pair did not exist yet” and “nobody traded during these five hours” arrive as byte-identical responses.

A collector that stops on the first empty page stops on the second kind too, records the history as complete, and leaves a hole that no later pass will ever look at again. The fix is cheap and has to be done first: establish the oldest bar that exists at a coarse grain — a daily walk is a dozen requests for a decade of history — and then treat an empty page above that floor as a gap to repair rather than a stopping condition.

06Bars change after they close

A closed bar looks immutable and is not. Trades can be reported late, a venue can correct a bad print, and a busy matching engine can publish a bucket before the last trades in it have finished settling into the tape. So the same bucket, requested twice, can come back twice with different numbers — usually a volume adjustment, occasionally a high or a low.

Every store has to take a position on this, and there are only two honest ones: believe the first answer and never revisit it, or re-request recent history on a schedule and overwrite. The second is more accurate and more work, and it has a consequence worth stating out loud — a chart can legitimately change under you for bars that have already closed. A store that reconciles should say when it does it, so that a user comparing a screenshot from yesterday against the chart today has an explanation available.

07Never stitch two venues into one series

The temptation is obvious. Venue A has five years of history and no live feed; venue B has a live feed and eighteen months of history. Joining them gives you five years of live data and it is the single worst thing you can do to a chart.

venue changes herethe largest “move” in the seriesone series, stitched from two books — and one bar that never traded
At the join, the small persistent difference between two books becomes one enormous single-bar return. Nothing traded there. Every indicator with memory will treat it as the largest move in the series.

A two per cent step that happened over no time at all does not stay local. Any smoothed average carries it forward for hundreds of bars; average true range sees the largest range in the series and stays inflated; anything measuring volatility reports a regime change on a date when nothing happened. And the seam is invisible on the chart, because a line does not tell you which rows came from where.

The alternative is not to blend but to name: keep each venue’s series separate, identify an instrument by the venue and the symbol, and let a reader compare two charts rather than one fiction. It costs a longer instrument identifier and it buys a series in which every bar was produced by the same market.

08What to ask about any feed before you trust it

Open or close?
Which instant is the bucket labelled by? Verify against a second source; do not read it off the documentation.
What timezone is a day?
And is there a UTC-anchored variant.
Sparse or dense?
And if dense, how do you identify a filler bar — a zero volume, a trade count, or nothing at all?
What is the page limit counting?
Rows, or intervals of elapsed time?
What does an empty page mean?
Almost never what it appears to mean.
Do bars get revised?
And if so, how far back and how often.

Most of these are twenty minutes of work each and every one of them, unanswered, becomes a class of bug that shows up months later as a user screenshot with two charts side by side. The first guide in this series covers what the four numbers in a bar mean in the first place, which is the other half of reading a feed correctly.

Where Chartwright comes into it

It is why an instrument here is always a venue and a symbol together — coinbase:BTC-USD and bitstamp:BTC-USD are two instruments, never one blended series.

Open a chartWhat it does

Keep reading