Skip to main content

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

ConcernOwner
When to request, which range, which directionChart — it calls loadBars(symbol, interval, request) with a time window, requested bar count, and load direction.
Fetching, ascending order, bar correctnessYour feed — return real vendor bars; never invent data.
Sorting, timestamp dedup, synthetic aggregationcreateCachingDatafeed, 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

datafeed.ts
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:

datafeed.ts
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 / hasNewer when you know whether more history exists beyond the returned range.
  • Range is empty but the symbol is valid — return { bars: [] }. Set hasOlder: false at the start of history so the chart stops requesting, or dataUnavailable: true when 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.