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.
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
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_URLwins 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
- Performance — what all this costs on the client.
- Integration — which screen passes which market type.
- Troubleshooting — reading an empty chart back to its cause.