Skip to content

Reference

Symbols & resolutions

A symbol is a string you choose and your datafeed answers for. This page says what the chart does with it, what an interval means on the wire, and the two units that make an otherwise-correct feed draw an empty chart.

The symbol is yours

The chart does not know what BTCUSDT means, and deliberately does not try. It passes the string from data-symbol to your datafeed’s resolveSymbol, and whatever comes back is what the chart draws and captions. Your feed is the authority on your own universe.

That is why there is no symbol registry to register with, no prefix convention to learn, and no mapping table to keep in sync. If your backend calls an instrument NASDAQ:AAPL, or aapl.us, or 7, use that.

The one constraint is the URL. A symbol travels as a query parameter, so it must be printable, under 40 characters, and free of characters an allowlist would refuse, letters, digits, and . - _ : / = are accepted. A symbol that fails that check shows an explanation rather than a chart, because substituting a default would draw the wrong instrument under the right caption.

What resolveSymbol has to answer

Most of the fields are display decisions, and the two that are not are pricescale and session. Everything else has a sensible default.

FieldWhat it decidesGet it wrong and…
pricescaleDecimal places on the axis and crosshair. 100 = 2dp, 100000000 = 8dp.A price quoted to 8 places renders as a flat line of identical numbers.
session"24x7" for anything continuous; "0930-1600" for a cash equity.The chart draws gaps where the market was open, or none where it was shut.
timezoneThe IANA zone the session is expressed in.The session shifts by hours twice a year at a DST boundary.
minmovThe smallest price step, in pricescale units. Almost always 1.Nothing visible, until an instrument whose tick is not one unit.
has_intradayWhether resolutions under a day may be requested at all.The chart offers minute intervals your feed answers empty.
supported_resolutionsWhich intervals the picker offers.A reader selects an interval that returns nothing.
providerAttribution shown beside the symbol, when the venue is not the source.Nothing, it is optional, and absent means the exchange served it.
data_status"streaming", "endofday" or "delayed_streaming".A delayed feed presents as live, which is the one lie worth avoiding.

A note on the last two. If your data is delayed or comes through a redistributor, saying so costs a small glyph in the legend; not saying so means a reader treats a fifteen-minute print as the current price. The chart draws nothing at all when a feed is real-time and venue-direct, so honesty is free in the common case.

Resolution strings

The chart asks for an interval as a string, in the vocabulary the common datafeed contract uses, so if you have implemented one before, this is already familiar.

StringMeans
1One minute
5Five minutes
60One hour
240Four hours
1DOne day
1WOne week
1MOne month

A bare number is minutes. Anything else carries a letter. Your getBars maps them to whatever your upstream calls the same thing, a dictionary is usually the whole implementation:

var STEP = {
  '1': '1m', '5': '5m', '15': '15m', '30': '30m',
  '60': '1h', '240': '4h', '1D': '1d', '1W': '1w', '1M': '1M'
};

Offer only what you can serve. supported_resolutions is what the picker shows, and an interval in that list that returns an empty page reads to a reader as a broken chart rather than as an unsupported timeframe.

The two unit traps

These are not exotic. They are the reason a feed that looks entirely correct draws an empty chart, and between them they account for most of the time anyone loses here.

1. The window is in SECONDS, a bar’s time is in MILLISECONDS

getBars receives period.from and period.to as UNIX seconds, and every bar you return must carry time in UNIX milliseconds. It is inherited from the contract this mirrors, it is the commonest mistake, and it fails silently: return seconds and every bar lands in 1970, so the chart draws nothing in the window it asked about and reports no error.

Wrong: a thousand times too small
return rows.map(function (r) {
  return { time: r.openTimeSeconds, open: r.o, high: r.h, low: r.l, close: r.c };
});
Right
return rows.map(function (r) {
  return { time: r.openTimeSeconds * 1000, open: r.o, high: r.h, low: r.l, close: r.c };
});
Many venues already stamp klines in milliseconds, Binance does, in which case you pass the value through and only period.from/period.to need multiplying on the way out. Check your upstream rather than assuming either way.

2. Serve countBack bars ENDING at `to`, not everything after `from`

The request carries a countBack alongside the window, and it is the number the chart actually needs. Paging from from returns too few bars whenever the window holds less history than the chart asked for, which is every request near the start of an instrument’s life, and the reason a chart can look fine on BTC and empty on a recently listed pair.

Right: anchored on the END of the window
url.searchParams.set('endTime', String(period.to * 1000));
url.searchParams.set('limit', String(Math.min(1000, period.countBack)));

Return the bars ascending by time, with no duplicates. The chart validates both and refuses a page that is out of order rather than drawing it, an unordered series produces a chart that is wrong in a way nobody can see.

Where history ends

Return an empty array. That is how the chart learns to stop asking for older windows, and it is a valid answer rather than an error, refusing it would make “no more data” indistinguishable from a broken feed, and the chart would page backwards forever.

The same applies within a series: a period your venue was closed for legitimately returns no bars. Do not fill it. A synthesised bar is a price nobody traded at, and it is indistinguishable on screen from one somebody did.

Real-time bars

subscribeBars hands you a push function. Call it with the CURRENT bar, the same object shape, with the same millisecond time, as often as it changes. Re-pushing a bar with an existing timestamp updates it in place; pushing a new timestamp opens a new bar. There is no separate “tick” shape to learn.

subscribeBars: function (symbol, resolution, uid, push) {
  var ws = new WebSocket(streamUrlFor(symbol, resolution));
  ws.onmessage = function (ev) {
    var k = JSON.parse(ev.data);
    push({ time: k.t, open: +k.o, high: +k.h, low: +k.l, close: +k.c, volume: +k.v });
  };
  sockets[uid] = ws;
},

unsubscribeBars: function (uid) {
  if (sockets[uid]) { sockets[uid].close(); delete sockets[uid]; }
}

Keep the uid. It is the only handle you get, and a subscription that is never closed is a socket per chart per symbol change, the reason unsubscribeBars exists at all.