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.
. - _ : / = 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.
| Field | What it decides | Get it wrong and… |
|---|---|---|
pricescale | Decimal 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. |
timezone | The IANA zone the session is expressed in. | The session shifts by hours twice a year at a DST boundary. |
minmov | The smallest price step, in pricescale units. Almost always 1. | Nothing visible, until an instrument whose tick is not one unit. |
has_intraday | Whether resolutions under a day may be requested at all. | The chart offers minute intervals your feed answers empty. |
supported_resolutions | Which intervals the picker offers. | A reader selects an interval that returns nothing. |
provider | Attribution 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.
| String | Means |
|---|---|
1 | One minute |
5 | Five minutes |
60 | One hour |
240 | Four hours |
1D | One day |
1W | One week |
1M | One 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.
return rows.map(function (r) {
return { time: r.openTimeSeconds, open: r.o, high: r.h, low: r.l, close: r.c };
});return rows.map(function (r) {
return { time: r.openTimeSeconds * 1000, open: r.o, high: r.h, low: r.l, close: r.c };
});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.
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.