Data sources

Every endpoint and WebSocket the chart touches, the query parameters and candle shape it expects, how the market type selects between them, and the caching on both sides of the wire.

5 min readUpdated 3 August 2026api, websocket, candles, cache

Chart Engine has no backend of its own. It reads the candle endpoints core and the trading addons already expose, and it keeps itself current on the market data sockets those same modules already run. Which pair it uses is decided by one value — the market type the host screen passes — and getting that wrong produces a chart that loads without any error and shows nothing.

The four market types

Market type History endpoint Live socket Used by
spot (default) /api/exchange/chart /api/exchange/market CEX markets on the trade and binary pages
eco /api/ecosystem/chart /api/ecosystem/market Ecosystem native markets, and every bot terminal
futures /api/futures/chart /api/futures/market Perpetual futures markets
forex /api/forex-trading/chart /api/forex-trading/market FX instruments with trading calendars

Bots trade Ecosystem markets only, so the bot terminal pins its market type to eco. Left at the default spot, it would ask the CEX endpoint for a market that does not exist there, get an empty array back, and render an empty chart with no error anywhere. The same trap applies to any custom screen that mounts the chart.

Historical candles

CEX candles. Reads a gzipped disk cache first, then the exchange
Ecosystem candles, from the ScyllaDB candles table
Futures candles
FX candles, filtered by the instrument's trading calendar

Query parameters

Parameter Required by Meaning
symbol all BASE/QUOTE, e.g. BTC/USDT
interval all 1m, 3m, 5m, 15m, 30m, 1h, 4h, 1d, 1w
from all Start timestamp, milliseconds
to all End timestamp, milliseconds
duration /api/exchange/chart The interval's length in milliseconds

/api/exchange/chart rejects a request missing any of the five with a 400. It also validates the symbol against ^[A-Z0-9]+/[A-Z0-9]+$ — not fussiness, but because the symbol becomes part of a filesystem cache path and a separator that is not a slash could escape the cache directory.

Response shape

Every one of the four returns the same thing: an array of arrays, oldest first, each row being

[timestamp, open, high, low, close, volume]

The chart maps that into its own candle objects and sorts by time. A missing sixth element is read as zero volume. There is no envelope, no pagination cursor and no metadata — an empty array is a valid, successful answer meaning "no candles in that window".

Authentication

None of the four candle endpoints declares requiresAuth, so they are public. They still pass through the licence gate and a per-IP rate limit, which is what stops a public chart endpoint being an unthrottled scraping target. A chart on a signed-out page will load; a client hammering the endpoint will be throttled.

Interval support differs by market

The chart offers nine timeframes. Ecosystem's candle store accepts more than that — 2h, 6h, 12h and 3d as well — and validates the interval against its own list, returning a 400 that names the supported set rather than an empty array. That distinction matters: on Ecosystem, an interval is a partition key, so an unrecognised one is a full-partition miss the client would otherwise read as "no data".

Timeframe Interval sent Candle length
1m 1m 60,000 ms
3m 3m 180,000 ms
5m 5m 300,000 ms
15m 15m 900,000 ms
30m 30m 1,800,000 ms
1h 1h 3,600,000 ms
4h 4h 14,400,000 ms
1d 1d 86,400,000 ms
1w 1w 604,800,000 ms

Server-side caching (CEX only)

/api/exchange/chart is the only one of the four with a cache of its own, and it is worth knowing about because it changes what an empty response means.

  • Candles are stored gzipped on disk at data/chart/<symbol>/<interval>.json.gz, relative to the backend's working directory, with a Redis layer in front.
  • Identical in-flight requests are de-duplicated — the same symbol, interval and normalised window shares one upstream call.
  • A request that takes longer than 20 seconds is abandoned.
  • If the cache already holds at least 90% of the expected bars and its newest candle is within two intervals of now, the cached set is returned without touching the exchange at all.
  • If the exchange provider has rate-limited or banned the install, the endpoint returns whatever is cached and nothing more. No error, no warning. A chart that stops extending on a market you have not looked at for a while is usually this.

Ecosystem and futures candles come from ScyllaDB directly; FX candles are filtered by the instrument's trading calendar so closed sessions do not produce phantom bars.

Live updates

The chart subscribes to an OHLCV stream on the market socket for its market type, at its current interval.

{ "action": "SUBSCRIBE", "payload": { "type": "ohlcv", "interval": "1m", "symbol": "BTC/USDT" } }
{ "action": "UNSUBSCRIBE", "payload": { "type": "ohlcv", "interval": "1m", "symbol": "BTC/USDT" } }

Frames arrive as { "stream": "…", "data": [[timestamp, open, high, low, close, volume]] } — the same row shape as the HTTP response, one row at a time. Anything that is not six numbers is discarded rather than drawn.

Socket URLs

The client derives the socket origin at runtime:

  • NEXT_PUBLIC_WEBSOCKET_URL wins if it is set.
  • In development it connects directly to the backend port (NEXT_PUBLIC_BACKEND_PORT, default 4000), because Next.js rewrites do not proxy WebSocket upgrades.
  • In production it uses the page's own host, so your reverse proxy must forward the upgrade on /api/…/market.

Historical candles arrive over ordinary HTTP, so the chart draws correctly and then never moves. That reads as a chart bug and is a proxy configuration problem. See Troubleshooting.

How a frame becomes a candle

The chart compares the frame's timestamp with its newest candle:

Condition Action
Inside the current candle's period Update the last candle — high takes the max, low the min, close the new close, volume accumulates
Within one further period Append it as a new candle
Further ahead than that Update the displayed price only

The last row is what stops a gap appearing when a trader is looking at history: a live candle from now must not be stapled onto candles from last Tuesday. The missing range is filled by a forward fetch instead.

How much the chart asks for

Everything is sized from the viewport, not from a fixed constant, so a phone does not download a desktop's worth of candles.

  • The chart targets 8 pixels per candle when fully zoomed out, 12 at rest and 25 fully zoomed in, against the measured chart width.
  • The initial fetch is 1.5 times the zoomed-out candle count, giving a buffer to pan into.
  • Panning left triggers a history fetch of half the zoomed-out count, with a floor of 30, debounced to at most one request every 300 ms. When a batch comes back less than half full, the chart concludes it has reached the beginning and stops asking.
  • Panning back toward now triggers a forward fetch, but only when the newest loaded candle is more than five candle-lengths behind the clock — inside that window the socket fills the gap on its own.
  • At most 2,000 candles are held in memory; older ones are dropped as new ones arrive.

Client-side caching and de-duplication

The browser keeps its own cache so switching timeframe and panning back over ground you have already covered are instant:

Behaviour Value
Cache lifetime 5 minutes
Cache entries 10, evicted oldest first
Keying Symbol, timeframe and the hour the window centres on
Merge Overlapping fetches expand an existing entry rather than replacing it

Three separate guards stop React re-renders turning into request storms: an identical request already in flight is skipped, the same symbol and timeframe requested twice within two seconds is skipped, and a successful fetch suppresses another for the same pair for 30 seconds. The socket is what keeps the chart current in that window — which is also why a broken socket looks like a frozen chart rather than a slow one.

Responses are discarded rather than drawn if the symbol, the timeframe or the zoom anchor changed while the request was in flight. Rapidly clicking through timeframes therefore cannot leave you looking at the wrong market's candles.

Symbol formatting

The chart normalises whatever the host passes into BASE/QUOTE before it calls anything:

Input Sent as
BTC/USDT BTC/USDT
BTC-USDT BTC/USDT
BTC_USDT BTC/USDT
BTCUSDT BTC/USDT

The concatenated form is split by matching a known quote asset off the end — USDT, USDC, BTC, ETH, BNB, BUSD. A concatenated symbol whose quote is not in that list is passed through unchanged, and the endpoint's symbol validation then rejects it. Pass the slashed form and the question never arises.

Next