Historical Bars
Historical bars populate the initial visible chart and every older range the
user scrolls back into. loadBars is the only datafeed method the chart
requires, and its request, response, timestamp, and cache-ownership rules are
the contract behind every rendered candle.
Who owns what
| Concern | Owner |
|---|---|
| When to request, which range, which direction | Chart — it calls loadBars(symbol, interval, request) with a time window, requested bar count, and load direction. |
| Fetching, ascending order, bar correctness | Your feed — return real vendor bars; never invent data. |
| Sorting, timestamp dedup, synthetic aggregation | createCachingDatafeed, when you wrap the feed with it. |
| Signaling "no data" vs. "failure" | Your feed — empty results are data (bars: []), failures are thrown errors. The SDK never converts one into the other. |
Minimal working path
import type { MarketDataFeed } from '@tradescript/pro/sdk/core';
export const datafeed: MarketDataFeed = {
onReady() {
return {
historyRequestBarCount: 1000,
};
},
async loadBars(symbol, interval, request) {
const query = new URLSearchParams({
symbol: symbol.ticker,
interval,
from: String(request.startTime),
to: String(request.endTime),
count: String(request.barCount),
});
const response = await fetch(`/api/bars?${query}`);
if (!response.ok) {
throw new Error(`Bars request failed: ${response.status}`);
}
return response.json(); // { bars: Bar[] } ascending by time
},
};
Checkpoint: mount the chart with this feed. Candles render for the initial
window, and scrolling left issues another loadBars call with
loadDirection: 'older' that extends the series without gaps.
Configure bars per history request
Declare historyRequestBarCount from the feed's onReady() configuration to
control how many bars the chart asks for in each automatic history loadBars
call:
const datafeed: MarketDataFeed = {
onReady() {
return {
historyRequestBarCount: 1000,
supportedIntervals: ['1m', '5m', '15m', '1H', '1D'],
providedIntervals: ['1m', '5m', '15m', '1H', '1D'],
};
},
async loadBars(symbol, interval, request) {
// request.barCount is 1000 for automatic initial, older, and newer history calls.
return vendor.loadBars(symbol, interval, request);
},
};
historyRequestBarCount sets request.barCount for automatic initial,
older-history, and newer-history calls. The default is 500 bars per request.
Valid declarations are integers from 1 through 20,000; invalid values fail
validation before loadBars is called. Return no more than
request.barCount bars. A larger value reduces network round trips but makes
each response larger, so declare the largest count your real upstream contract
can serve reliably.
createCachingDatafeed preserves this declaration. When the cache can satisfy
the requested range and bar count, it returns bars without calling the inner
feed. Otherwise it forwards the same request.barCount to the inner feed and
merges the result. Bars already loaded by the mounted chart also remain local,
so moving within that loaded range makes no history request.
Request timeline
A symbol/interval load can require multiple history requests, not a single
loadBars call:
Three outcomes, three signals:
- Bars exist — return them ascending; set
hasOlder/hasNewerwhen you know whether more history exists beyond the returned range. - Range is empty but the symbol is valid — return
{ bars: [] }. SethasOlder: falseat the start of history so the chart stops requesting, ordataUnavailable: truewhen the feed has no data at all for this symbol/interval and the chart should show its no-data state. - The request failed — throw (or reject). Never resolve a failed fetch with an empty or fabricated result: that silently truncates history instead of surfacing the failure.
Request and response contracts
loadBars(symbol, interval, request) receives the selected ChartInterval.
The interval grammar is a positive integer plus unit, for example 1T, 10T,
100T, 30s, 6m, 2D, 3W, 6M, 12M, or 1Y. The SDK is exact by
default: your feed returns bars at the selected interval. If the feed or cache
wrapper resolves resolutionRebuildPolicy to aggregate, the SDK may request
a smaller provided interval and build the selected interval before bars reach
the chart.
Full BarHistoryRequest and BarHistoryResult interfaces
export interface BarHistoryRequest {
startTime: number;
endTime: number;
barCount: number;
initialDataLoad?: boolean;
loadDirection?: 'initial' | 'older' | 'newer';
metadata?: Record<string, unknown>;
}
export interface BarHistoryResult {
bars: Bar[];
hasOlder?: boolean;
hasNewer?: boolean;
dataUnavailable?: boolean;
}
startTime and endTime are Unix milliseconds (UTC). barCount is the
maximum number of bars requested in that call. Automatic history calls use
MarketDataFeedConfig.historyRequestBarCount, or 500 when it is omitted.
For automatic paging, the current edge candle is an exclusive anchor:
loadDirection: 'older' must return bars strictly before endTime, and
loadDirection: 'newer' must return bars strictly after startTime.
hasOlder / hasNewer state whether more history exists before or after the
returned range. dataUnavailable tells the chart to stop requesting and show
its no-data state.
When emptyBars is enabled through MarketDataFeedConfig.emptyBars,
SymbolInfo.emptyBars, or createCachingDatafeed, SDK-generated flat bars
include isEmpty: true, volume: 0, and OHLC values set to the previous
close. The chart exposes inactivityGapsVisible /
getTimeScale().inactivityGaps(), but visible gaps still depend on
feed/session-calendar truth instead of invented timestamps.
Bar ordering
Return no more than request.barCount bars, all inside the requested range,
in strictly ascending time order with one bar per timestamp. The SDK rejects
unordered, duplicate, out-of-range, oversized, or automatic pages that repeat
the current chart edge; rejected pages never mutate the visible series. If
createCachingDatafeed sees duplicate timestamps it deduplicates by time and
keeps the last value it received, but feeds should not rely on duplicate bars
as an update channel. Corrections should arrive through realtime
same-timestamp updates or through resetCache.
Bar ownership
Treat bars as immutable after handing them to the SDK. The cache layer sorts
and deduplicates arrays without promising to clone each vendor Bar object,
and the chart may retain references while building derived series. If your
feed keeps its own mutable vendor cache, return fresh bar objects from
loadBars and emit fresh objects from subscribeRealTimeBars.
Hosts should apply the same rule to arrays returned from SDK/cache helpers: read them as snapshots, do not mutate bar objects in place.
Time units
The public contract uses Unix milliseconds for Bar.time.
Calendar bar timestamps
For daily, weekly, monthly, and yearly vendor bars, timestamp the bar at
00:00:00.000Z for the trading day or calendar bucket that owns the bar. For
a daily bar, that means the trading date at UTC midnight, for example
2026-07-09T00:00:00.000Z.
The SDK does not shift vendor bars to repair a session/timezone mismatch.
Exact vendor bars are displayed at the timestamp the feed supplies. When the
SDK builds calendar bars from smaller source bars, it emits timestamps at the
first trading day present in the bucket at 00:00:00Z, using
windows[].tradingDay when a session calendar is available.
Resolution contract
Keep the interval selector and loadBars implementation aligned:
onReady() {
return {
supportedIntervals: ['1m', '10m', '1H', '1D'],
providedIntervals: ['1m', '5m', '1D'],
};
}
In this example, native loadBars should handle 1m, 5m, and 1D. If you
pass the feed through createCachingDatafeed(rawFeed, { resolutionRebuildPolicy: 'aggregate', syntheticAggregation: { sourceIntervals: { '10m': '5m', '1H': '1m' } } }),
the SDK can build 10m from 5m and 1H from 1m.
For tick bars, keep the same split: advertise the tick intervals users can select, and list the tick intervals your feed returns directly.
onReady() {
return {
supportedIntervals: ['10T', '100T', '1m', '5m'],
providedIntervals: ['10T', '1m', '5m'],
};
}
With createCachingDatafeed(rawFeed, { syntheticAggregation: true }), the SDK
uses the default tick source map, including 100T -> 50T. With
createCachingDatafeed(rawFeed, { syntheticAggregation: { sourceIntervals: { '100T': '10T' } } }),
the SDK groups ten 10T source bars into each 100T bar instead.
For custom intraday intervals, syntheticAggregation: true can derive from
the SDK base tier, for example 6m -> 5m.
Calendar intervals use stricter rules. Multi-day intervals build from smaller
day bars. Weekly intervals can build from 1D or a smaller weekly interval.
Monthly intervals can build from 1D or a smaller monthly interval. Yearly
intervals can build from 1D, 1M, or a smaller yearly interval. The SDK
does not build daily, weekly, monthly, or yearly bars from
intraday/hourly/tick data.
See Resolutions for the full decision matrix.
Session calendars for synthetic bars
If the SDK builds bars or fills empty intraday bars for an exchange-traded
symbol, provide resolveSessionCalendar so aggregation and gap filling follow
real sessions instead of fixed UTC buckets.
async resolveSessionCalendar(request) {
return {
symbol: request.symbol,
timezone: 'America/New_York',
windows: [
{
opensAt: Date.parse('2024-01-02T14:30:00Z'),
closesAt: Date.parse('2024-01-02T21:00:00Z'),
tradingDay: '2024-01-02',
state: 'regular',
},
],
};
}
opensAt and closesAt are UTC epoch milliseconds; closesAt is exclusive.
Include only windows that should contribute to the selected session mode. Omit
holidays by omitting their windows, and encode half-days or corrections as the
corrected window for that tradingDay.
When a session calendar is present, intraday/hourly synthetic bars use
session-open anchored timestamps. Daily source bars match
windows[].tradingDay; for week/month/year buckets the emitted timestamp is
the first trading day present in that bucket at 00:00:00Z.
Empty-bar generation uses the same calendar but only for seconds/minutes/hours intervals. It fills expected timestamps inside active windows after the first real bar in that same session and leaves tick and day/week/month/year intervals unchanged. Exact rebuild policy skips empty-bar generation.
Next steps
- Realtime Bars — keep the last bar live once history loads.
- Resolutions — which intervals the feed serves natively vs. derived.