Skip to main content

Chart Data Provider

ChartDataProvider - is the main data provider in dxchart5-react. It's responsible for historical candles, last candle updates, quotes, etc.

NOTE: it is the only entry point for your candles data.

Example

/** Copyright ©2024 Devexperts LLC.
All rights reserved. Any unauthorized use will constitute an infringement of copyright.
In case of any questions regarding types of use, please contact legal@devexperts.com.
This notice must remain intact.
**/
import { aggregationPeriodToString, periodToMinutes, } from '@dx-private/dxchart5-react/dist/chart/model/aggregation.model';
import { generateCandles } from '@dx-private/dxchart5-react/dist/utils/generator/candle-generator.utils';
import { createPriceDiffGenerator } from '@dx-private/dxchart5-react/dist/utils/last-candle-price-generator';
import { hashCode } from '@devexperts/dxcharts-lite/dist/chart/utils/string.utils';
const NUMBER_OF_CANDLES = 500;
/**
* Creates mock implementation of {@link ChartDataProvider}.
*/
export const createMockChartDataProvider = () => {
//#region candles
// First of all we should prepare the mock candles data.
// To speed up that step we can use `generateCandles` utility from `dxchart5-react`.
// `generateMockData` function will generate new candles data for a given symbol/aggregation pair.
const generateMockData = (symbol, aggregation, fromTime, toTime) => generateCandles({
period: periodToMinutes(aggregation) * 60 * 1000,
randomSeed: hashCode(symbol) + hashCode(aggregationPeriodToString(aggregation)),
startTimestamp: fromTime,
endTimestamp: toTime,
quantity: 500,
startPrice: 50,
candleSize: 1,
candleDiversity: 4,
highLowSize: 0.2,
highLowDiversity: 2,
volumeSize: 10000,
volumeDiversity: 50000,
}).map(candle => ({
time: candle.timestamp,
open: candle.open,
high: candle.hi,
low: candle.lo,
close: candle.close,
volume: candle.volume,
impVolatility: 0,
vwap: 0,
}));
// we store last history candles to use them as reference values for
// last candle real time updates generator
const lastHistoryCandles = new Map();
// mock global brokerage time to synchronize instruments last candle tick
let globalClockInSeconds = 0;
setInterval(() => {
globalClockInSeconds += 1;
}, 1000);
// logic to mimic last candle real time tick with random generator
const lastCandleSubscriptions = new Map();
const priceDiffGenerator = createPriceDiffGenerator();
const startLastCandleUpdateTicker = (symbol, aggregation, subscriptionId, callback) => {
const aggregationInSeconds = periodToMinutes(aggregation) * 60;
lastCandleSubscriptions.set(subscriptionId, setInterval(() => {
var _a;
const lastCandle = lastHistoryCandles.get(symbol);
if (!lastCandle) {
console.warn('No last candle for symbol ', symbol, ' found');
return;
}
const priceDiff = (_a = priceDiffGenerator.next().value) !== null && _a !== void 0 ? _a : 1;
const lastCandleUpdate = Object.assign(Object.assign({}, lastCandle), { close: lastCandle.close * priceDiff });
// # logic to add candle to data series when aggregation period exceeds
// the thing is, that `dxcharts-react` will automatically add new candle to the end of data series on chart
// if the timestamp of new last candle tick is bigger then it was on the previous last candle tick
// and difference in time is >= selected aggregation period
if (globalClockInSeconds % aggregationInSeconds === 0) {
lastCandleUpdate.time = lastCandleUpdate.time + aggregationInSeconds * 1000;
}
lastHistoryCandles.set(symbol, lastCandleUpdate);
callback([lastCandleUpdate]);
}, 1000));
};
/**
* `requestHistoryData` - `dxcharts-react` requests the historical candles data for a given
* symbol/aggregation/etc. combination + handles lazy load requests
*
* @see compare charts are handled here too, for example:
* Imagine AAPL as main instrument and GOOG as compare instrument are selected on chart.
* Because of this `requestHistoryData` will be called 2 times - first for AAPL, then for GOOG.
* Order of call is the same as the order of instruments added, where main instrument is *always* `first`.
*
* @see with multicharts it is still the same.
* `requestHistoryData` will be called in order of charts, for example:
* on 1st chart AAPL and GOOG are selected, on 2nd chart AMD and NVDA are selected.
* Because of that first `requestHistoryData` will be called first for 1st chart for AAPL and GOOG,
* secondly `requestHistoryData` will be called for 2nd chart for AMD and NVDA
*/
const requestHistoryData = (symbol, aggregation,
/**
* `fromTime` and `toTime` are candles timestamps, that simply reflect the current state of the chart.
*
* `extendedHours` is a flag that indicates whether candles from NOT REGULAR trading session required in addition to ones from REGULAR.
*
* `priceType` is a flag that tells which price ('bid' | 'ask' | 'last' | 'mark') should be used for candles.
*
* `priceIncrement` is a flag that describes which minimum price change should be reflected in candles data.
*/
options) => {
// when `toTime` option is provided - it is indicator for lazy loading functionality
// `toTime` means that you should provide candles data, where the rightmost candle will be <= `toTime`.
let toTime = options === null || options === void 0 ? void 0 : options.toTime;
// `fromTime` often is not provided, because you can give any amount of candles data you want,
// but if provided - it means that user requests data *at least* from `fromTime`, i.e leftmost candle should be <= `fromTime`.
let fromTime = options === null || options === void 0 ? void 0 : options.fromTime;
if (!toTime) {
toTime = Date.now();
}
if (!fromTime) {
fromTime = toTime - periodToMinutes(aggregation) * 60 * 1000 * NUMBER_OF_CANDLES;
}
const data = generateMockData(symbol, aggregation, fromTime, toTime);
if (data.length > 0) {
lastHistoryCandles.set(symbol, data[data.length - 1]);
}
return new Promise(resolve => {
setTimeout(() => resolve(data), 3000);
});
};
/**
* `subscribeCandles` - `dxcharts-react` requests the last candle updates data for a particular
* instrument/aggregation/.etc combination.
*
* updates should be passed to the `callback` each time the last candle data is updated.
*
* @see `subscriptionId` helps to identify the combination subscription.
* `subscriptionId` arg can be useful to control multiple instrument's
* last candle updates at a time
*
* @see compare charts are handled here too, similar to `requestHistoryData`.
*
* @see multicharts are handled here exactly the same way as for `requestHistoryData`.
*/
const subscribeCandles = (symbol, aggregation, subscriptionId, callback) => {
startLastCandleUpdateTicker(symbol, aggregation, subscriptionId, callback);
};
/**
* `unsubscribeCandles` - `dxcharts-react` fires that method when it no longer needs the last candle updates
* for a given `subscriptionId`
* @see `subscriptionId` is an `id` that was provided by {@link subscribeCandles} method
*/
const unsubscribeCandles = (subscriptionId) => {
const intervalId = lastCandleSubscriptions.get(subscriptionId);
if (!intervalId) {
console.warn('No last candle generator found for subscriptionId ', subscriptionId);
return;
}
clearInterval(intervalId);
lastCandleSubscriptions.delete(subscriptionId);
};
//#endregion candles
//#region service data
const serviceDataSubscriptions = new Map();
/**
* Service data is a data that provides some additional data to candles data,
* like bid/ask prices, prevDayClosePrice etc.
*/
const subscribeServiceData = (symbol, callback) => {
serviceDataSubscriptions.set(symbol, setInterval(() => {
const lastCandle = lastHistoryCandles.get(symbol);
if (!lastCandle) {
return;
}
callback({
prevDayClosePrice: lastCandle.open / 2,
prePostMarketClose: lastCandle.open / 2,
bid: lastCandle.close + lastCandle.close * 0.1,
ask: lastCandle.close - lastCandle.close * 0.1,
});
}, 1000));
};
const unsubscribeServiceData = (symbol) => {
const intervalId = serviceDataSubscriptions.get(symbol);
if (!intervalId) {
console.warn('No service data found for symbol ', symbol);
return;
}
clearInterval(intervalId);
serviceDataSubscriptions.delete(symbol);
};
//#endregion service data
return {
requestHistoryData,
subscribeCandles,
unsubscribeCandles,
subscribeServiceData,
unsubscribeServiceData,
};
};

More details regarding requestHistoryData

The information below might be useful to help understanding the data fetch process inside candles provider

dxchart5-react is a data-source agnostic library, any arguments dxchart5-react provides to data provider methods are not query parameters to the data source in most cases. fromTime and toTime arguments simply reflect the current state of the chart, not the chart request to load additional data.

Therefore, you can either use the current state of the chart to calculate the query parameters you need for your data source, or completely ignore them.

Both options: fromTime and toTime are optional and could be undefined.

fromTime

As was said, fromTime is an optional argument and not passed to options argument in most of the cases at all. Also, there is NO difference between the undefined and 0 value. When fromTime is undefined || 0 (in most of the cases) it shouldn't be interpreted by the provider code as that data should be fetched since the beginning of that instrument.

On the very first load, when user haven't performed any scrolling actions and there's no any user-related/layout-related data from previous chart usage, you can just load latest dataset. In this case, the rightmost candle will be the current moment - Date.now() and the leftmost candle will be any date in the past you want to be loaded.

On the second load, when chart was already loaded with some data (after very first load), you'll receive fromTime argument which is the leftmost candle timestamp of previously loaded data.

In addition, fromTime is usually used by timeframe presets functionality, so you will see fromTime parameter provided when user selects some timeframe preset.

toTime

For lazy loading only toTime is important because it identifies the oldest candle already loaded and passed to dxchart5-react to determine till which timestamp more candles need to be loaded. Number of lazy loaded candles can be defined by the provider itself.

If user will scroll the chart to the left, deep in the history data, your provider will receive toTime argument which will be a timestamp of leftmost candle in viewport. So, to load additional set of candles you should use toTime as a rightmost cut-off of data, and leftmost cut-off you can calculate based on the desired amount of data.

updateCallback

updateCallback allows to completely override initial history data if you need to update it after some period of time.

Since updateCallback function overrides all history data, make sure you provided all history data with updates, NOT just updates.

When chart asks you a data (for example on load or when you change aggregation/instrument) via requestHistoryData you should provide history data via return value, pretty simple. BUT when you want to change data after some time after initial requestHistoryData was called, then you should use updateCallback.

// ...your `ChartDataProvider` code

const initialHistoryDataForSelectedInstrument = [...]

// store `updateCallback` somewhere to access it out of requestHistoryData scope if you need
let updateHistoryCallback = undefined;

// imagine that after some period of time you need to update some part of history data for selected instrument
setTimeout(() => {
const historyDataUpdates: ChartCandleData[] = [
{ time: 1719218004, open: 10, high: 20, low: 10, close: 15, volume: 30000 },
];
// implement your function that will merge desired history data updates with initial history data
const mergedHistoryData = yourMergeFunction(initialHistoryDataForSelectedInstrument, historyDataUpdates)

// then pass merged data to `updateCallback`
updateHistoryCallback(mergedHistoryData)
}, 10000);

const requestHistoryData = (
symbol: string,
aggregation: AggregationPeriod,
options: RequestMoreDataOptions & ChartDataOptions,
updateCallback: (data: ChartCandleData[]) => void,
chartId: string,
) => {
updateHistoryCallback = updateCallback;

// average provider code,
// same as in the example above
let toTime = options?.toTime;
let fromTime = options?.fromTime;

if (!toTime) {
toTime = Date.now();
}

if (!fromTime) {
fromTime = toTime - periodToMinutes(aggregation) * 60 * 1000 * NUMBER_OF_CANDLES;
}

const data = initialHistoryDataForSelectedInstrument;

return new Promise<ChartCandleData[]>(resolve => {
setTimeout(() => resolve(data), 3000);
});
};

requestHistoryData provides ChartCandleData[], that have some options for indicators, for example vwap field, that provides calculated data for Volume Weighted Average Price indicator.

Please note that the VWAP indicator requires trading session data for its calculations, regardless of whether you are providing the data for VWAP in candle or not. Therefore, you need to create a TradingSessionsProvider using data generated from generateSessions.

const historyDataMap = (candle: Candle) => ({
time: candle.timestamp,
open: candle.open,
high: candle.hi,
low: candle.lo,
close: candle.close,
volume: candle.volume,
impVolatility: 0,
vwap: 0, // here you can provide your own vwap calculation or provide from other source, otherwise you can left this field undefined
});

API Reference

ChartDataProvider

ChartDataProvider.requestHistoryData
ChartDataProvider.requestHistoryData(symbol: string, aggregation: AggregationPeriod, options: { fromTime?: number; toTime?: number; } & ChartDataOptions, updateCallback: (data: ChartCandleData[]) => void, chartId: string): Promise<ChartCandleData[]>

There might be 3 possible cases: 1. Initial data request when left bound of viewport somewhere in the history data and right bound at current candle tick. In this situation `requestHistoryData` method will provide only `fromTime` parameter (that's a base case for initial viewport). In other words `fromTime` will point somewhere on the left bound of viewport. That means that in this case you should provide history data AT LEAST from `fromTime` to right now, the time when request were made. BUT you can give more history data if you want. 2. lnitial data request when left and right bound of viewport somewhere in the history data. This can happen when user for example reloads the page when their viewport somewhere in the history data. In this situation `requestHistoryData` will provide only `fromTime` parameter too. It means that even in this case you should provide all the history data from `fromTime` to right now, the time when request were made. That's pretty much the same case as №1. 3. Lazy load of candles, that are on the left to timeline, i.e older history data. On initial load you can give chart any amount of candles you like, it might be 100 or 1000, doesn't matter. Depends on you. This means that when we scroll to the left to older data, we will have as much candles as you provided. When we will reach the oldest history candle, chart will fire `requestHistoryData` to request even older data, i.e lazy load. In this situation `requestHistoryData` will only provide `toTime`. Why this way? Because again, you can give chart any amount of data you want, so there's no point in `fromTime` parameter. You may not give any data in the response - this would mean that there's no history data left.

Parameters
symbol: string
aggregation: AggregationPeriod
options: { fromTime?: number; toTime?: number; } & ChartDataOptions
`fromTime` and `toTime` are candles timestamps, that simply reflect the current state of the chart. Therefore, you can either use the current state of the chart to calculate the necessary query parameters, or completely ignore them. `fromTime` is an oldest candle's timestamp which should be data get from, `toTime` is the newest (usually `toTime` candle equals the oldest visible candle)
updateCallback: (data: ChartCandleData[]) => void
!!! WARNING !!! This function updates the whole instrument's history data
chartId: string
Returns
Promise<ChartCandleData[]>
ChartDataProvider.subscribeCandles
ChartDataProvider.subscribeCandles(symbol: string, aggregation: AggregationPeriod, subscriptionId: string, subscribeCallback: (data: ChartCandleData[]) => void, options: ChartDataOptions, chartId: string): void

Parameters
symbol: string
aggregation: AggregationPeriod
subscriptionId: string
subscribeCallback: (data: ChartCandleData[]) => void
options: ChartDataOptions
chartId: string
Returns
void
ChartDataProvider.unsubscribeCandles
ChartDataProvider.unsubscribeCandles(subscriptionId: string): void

Unsubscribe is called when symbol, aggregation or options are changed

Parameters
subscriptionId: string
Returns
void
ChartDataProvider.subscribeServiceData
ChartDataProvider.subscribeServiceData(symbol: string, subscribeCallback: (data: ServiceData) => void): void

Parameters
symbol: string
subscribeCallback: (data: ServiceData) => void
Returns
void
ChartDataProvider.unsubscribeServiceData
ChartDataProvider.unsubscribeServiceData(symbol: string): void

Parameters
symbol: string
Returns
void

Further reading