Implement datafeed
This article is part of a tutorial about implementing Datafeed API. We recommend that you follow the guide from the start.
At this stage, you will know how the Datafeed API works and implement your own datafeed and methods.
How the datafeed works
Datafeed API is a set of methods that you should implement and assign to the datafeed object in Widget Constructor. The library calls these methods to access and process data and fill the current chart with it. The datafeed returns results using callback functions. Refer to the Datafeed API topic for more information.
Step 1. Create a datafeed mock
Create a datafeed.js
file in src
and add the following code:
export default {
onReady: (callback) => {
console.log('[onReady]: Method call');
},
searchSymbols: (userInput, exchange, symbolType, onResultReadyCallback) => {
console.log('[searchSymbols]: Method call');
},
resolveSymbol: (symbolName, onSymbolResolvedCallback, onResolveErrorCallback, extension) => {
console.log('[resolveSymbol]: Method call', symbolName);
},
getBars: (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => {
console.log('[getBars]: Method call', symbolInfo);
},
subscribeBars: (symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) => {
console.log('[subscribeBars]: Method call with subscriberUID:', subscriberUID);
},
unsubscribeBars: (subscriberUID) => {
console.log('[unsubscribeBars]: Method call with subscriberUID:', subscriberUID);
},
};
This code sample represents a datafeed that writes a message to the console when any method is called. Now this is only a mock implementation, but you will implement all the methods in the next steps.
Step 2. Implement methods
onReady
The onReady
method is the first datafeed method that is called when the chart is initialized.
The library uses it to get datafeed configuration such as supported resolutions and exchanges.
Add the following
DatafeedConfiguration
implementation for the datafeed sample:/chart‑project/src/datafeed.jsconst configurationData = {
// Represents the resolutions for bars supported by your datafeed
supported_resolutions: ['1D', '1W', '1M'],
// The `exchanges` arguments are used for the `searchSymbols` method if a user selects the exchange
exchanges: [
{ value: 'Bitfinex', name: 'Bitfinex', desc: 'Bitfinex'},
{ value: 'Kraken', name: 'Kraken', desc: 'Kraken bitcoin exchange'},
],
// The `symbols_types` arguments are used for the `searchSymbols` method if a user selects this symbol type
symbols_types: [
{ name: 'crypto', value: 'crypto'}
]
};Call the
OnReadyCallback
asynchronously and pass aDatafeedConfiguration
object as a parameter:/chart‑project/src/datafeed.jsonReady: (callback) => {
console.log('[onReady]: Method call');
setTimeout(() => callback(configurationData));
},
resolveSymbol
The resolveSymbol
method is called once the datafeed is configured.
The library uses it to retrieve symbol information such as exchange, time zone, price scale, and etc.
As mentioned before, the library does not contain or provide any data. For this reason, this tutorial uses the CryptoCompare API.
Create a
helpers.js
file for the CryptoCompare API functions and add the following code:/chart‑project/src/helpers.js// Makes requests to CryptoCompare API
export async function makeApiRequest(path) {
try {
const response = await fetch(`https://min-api.cryptocompare.com/${path}`);
return response.json();
} catch(error) {
throw new Error(`CryptoCompare request error: ${error.status}`);
}
}
// Generates a symbol ID from a pair of the coins
export function generateSymbol(exchange, fromSymbol, toSymbol) {
const short = `${fromSymbol}/${toSymbol}`;
return {
short,
full: `${exchange}:${short}`,
};
}The
makeApiRequest
andgenerateSymbol
functions are specific to CryptoCompare API and will be used inresolveSymbol
. You might not need them when implementing your own datafeed.Import the functions from
helpers.js
intodatafeed.js
:/chart‑project/src/datafeed.jsimport { makeApiRequest, generateSymbol } from './helpers.js';
Implement the
getAllSymbols
function to obtain all symbols for all supported exchanges./chart‑project/src/datafeed.js// DatafeedConfiguration implementation
// ...
// Obtains all symbols for all exchanges supported by CryptoCompare API
async function getAllSymbols() {
const data = await makeApiRequest('data/v3/all/exchanges');
let allSymbols = [];
for (const exchange of configurationData.exchanges) {
const pairs = data.Data[exchange.value].pairs;
for (const leftPairPart of Object.keys(pairs)) {
const symbols = pairs[leftPairPart].map(rightPairPart => {
const symbol = generateSymbol(exchange.value, leftPairPart, rightPairPart);
return {
symbol: symbol.short,
full_name: symbol.full,
description: symbol.short,
exchange: exchange.value,
type: 'crypto',
};
});
allSymbols = [...allSymbols, ...symbols];
}
}
return allSymbols;
}Implement the
resolveSymbol
method and specify symbol information in asymbolInfo
object according toLibrarySymbolInfo
./chart‑project/src/datafeed.jsresolveSymbol: async (
symbolName,
onSymbolResolvedCallback,
onResolveErrorCallback,
extension
) => {
console.log('[resolveSymbol]: Method call', symbolName);
const symbols = await getAllSymbols();
const symbolItem = symbols.find(({ full_name }) => full_name === symbolName);
if (!symbolItem) {
console.log('[resolveSymbol]: Cannot resolve symbol', symbolName);
onResolveErrorCallback('Cannot resolve symbol');
return;
}
// Symbol information object
const symbolInfo = {
ticker: symbolItem.full_name,
name: symbolItem.symbol,
description: symbolItem.description,
type: symbolItem.type,
session: '24x7',
timezone: 'Etc/UTC',
exchange: symbolItem.exchange,
minmov: 1,
pricescale: 100,
has_intraday: false,
visible_plots_set: 'ohlc',
has_weekly_and_monthly: false,
supported_resolutions: configurationData.supported_resolutions,
volume_precision: 2,
data_status: 'streaming',
};
console.log('[resolveSymbol]: Symbol resolved', symbolName);
onSymbolResolvedCallback(symbolInfo);
},infoIn this tutorial, you specified
supported_resolutions: ['1D', '1W', '1M']
in theonReady
method. The library can build weekly and monthly resolutions from the daily ones (1D
). However, you need to directly specify that the datafeed does not have these resolutions by settinghas_weekly_and_monthly
tofalse
.
getBars
The library uses getBars
to get historical data for a symbol within a certain range.
Historical data will be retrieved from the CryptoCompare API.
In
helpers.js
, implement aparseFullSymbol
function that will parse a crypto pair symbol and return all parts of this symbol. Note that thefull
value is returned fromgenerateSymbol
./chart‑project/src/helpers.js// makeApiRequest and generateSymbol implementation
// ...
// Returns all parts of the symbol
export function parseFullSymbol(fullSymbol) {
const match = fullSymbol.match(/^(\w+):(\w+)\/(\w+)$/);
if (!match) {
return null;
}
return { exchange: match[1], fromSymbol: match[2], toSymbol: match[3] };
}Import
parseFullSymbol
intodatafeed.js
./chart‑project/src/datafeed.jsimport { makeApiRequest, generateSymbol, parseFullSymbol } from './helpers.js';
Implement
getBars
usingparseFullSymbol
and CryptoCompare's Daily Pair OHLCV to retrieve historic OHLCV data.cautionThe CryptoCompare API does not allow specifying the
from
date, so you have to filter bars on the client side./chart‑project/src/datafeed.jsgetBars: async (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => {
const { from, to, firstDataRequest } = periodParams;
console.log('[getBars]: Method call', symbolInfo, resolution, from, to);
const parsedSymbol = parseFullSymbol(symbolInfo.full_name);
const urlParameters = {
e: parsedSymbol.exchange,
fsym: parsedSymbol.fromSymbol,
tsym: parsedSymbol.toSymbol,
toTs: to,
limit: 2000,
};
const query = Object.keys(urlParameters)
.map(name => `${name}=${encodeURIComponent(urlParameters[name])}`)
.join('&');
try {
const data = await makeApiRequest(`data/histoday?${query}`);
if (data.Response && data.Response === 'Error' || data.Data.length === 0) {
// "noData" should be set if there is no data in the requested period
onHistoryCallback([], { noData: true });
return;
}
let bars = [];
data.Data.forEach(bar => {
if (bar.time >= from && bar.time < to) {
bars = [...bars, {
time: bar.time * 1000,
low: bar.low,
high: bar.high,
open: bar.open,
close: bar.close,
}];
}
});
console.log(`[getBars]: returned ${bars.length} bar(s)`);
onHistoryCallback(bars, { noData: false });
} catch (error) {
console.log('[getBars]: Get error', error);
onErrorCallback(error);
}
},
searchSymbols
The library uses the searchSymbols
method to request symbols that match some user input.
searchSymbols: async (
userInput,
exchange,
symbolType,
onResultReadyCallback
) => {
console.log('[searchSymbols]: Method call');
const symbols = await getAllSymbols();
const newSymbols = symbols.filter(symbol => {
const isExchangeValid = exchange === '' || symbol.exchange === exchange;
const isFullSymbolContainsInput = symbol.full_name
.toLowerCase()
.indexOf(userInput.toLowerCase()) !== -1;
return isExchangeValid && isFullSymbolContainsInput;
});
onResultReadyCallback(newSymbols);
},
In this case, you will request all available symbols from the API and filter them in datafeed.js
.
If a user has not selected an exchange, the exchange
argument will be equal to an empty string.
Result
At this point, you have implemented datafeed.
Save all your changes and run the following command from your project directory (chart‑project
in this tutorial):
npx serve
Open the library locally in your web browser to see the results. You should see a chart plotted and be able to search symbols and display historical data.
Next steps
In the next stage, you will implement real-time data streaming via WebSocket.
Complete code
Click the following sections to reveal the complete code for the examples at this stage of the tutorial.
datafeed.js
import { makeApiRequest, generateSymbol, parseFullSymbol } from './helpers.js';
// DatafeedConfiguration implementation
const configurationData = {
// Represents the resolutions for bars supported by your datafeed
supported_resolutions: ['1D', '1W', '1M'],
// The `exchanges` arguments are used for the `searchSymbols` method if a user selects the exchange
exchanges: [
{ value: 'Bitfinex', name: 'Bitfinex', desc: 'Bitfinex'},
{ value: 'Kraken', name: 'Kraken', desc: 'Kraken bitcoin exchange'},
],
// The `symbols_types` arguments are used for the `searchSymbols` method if a user selects this symbol type
symbols_types: [
{ name: 'crypto', value: 'crypto'}
]
};
// Obtains all symbols for all exchanges supported by CryptoCompare API
async function getAllSymbols() {
const data = await makeApiRequest('data/v3/all/exchanges');
let allSymbols = [];
for (const exchange of configurationData.exchanges) {
const pairs = data.Data[exchange.value].pairs;
for (const leftPairPart of Object.keys(pairs)) {
const symbols = pairs[leftPairPart].map(rightPairPart => {
const symbol = generateSymbol(exchange.value, leftPairPart, rightPairPart);
return {
symbol: symbol.short,
full_name: symbol.full,
description: symbol.short,
exchange: exchange.value,
type: 'crypto',
};
});
allSymbols = [...allSymbols, ...symbols];
}
}
return allSymbols;
}
export default {
onReady: (callback) => {
console.log('[onReady]: Method call');
setTimeout(() => callback(configurationData));
},
searchSymbols: async (
userInput,
exchange,
symbolType,
onResultReadyCallback
) => {
console.log('[searchSymbols]: Method call');
const symbols = await getAllSymbols();
const newSymbols = symbols.filter(symbol => {
const isExchangeValid = exchange === '' || symbol.exchange === exchange;
const isFullSymbolContainsInput = symbol.full_name
.toLowerCase()
.indexOf(userInput.toLowerCase()) !== -1;
return isExchangeValid && isFullSymbolContainsInput;
});
onResultReadyCallback(newSymbols);
},
resolveSymbol: async (
symbolName,
onSymbolResolvedCallback,
onResolveErrorCallback,
extension
) => {
console.log('[resolveSymbol]: Method call', symbolName);
const symbols = await getAllSymbols();
const symbolItem = symbols.find(({ full_name }) => full_name === symbolName);
if (!symbolItem) {
console.log('[resolveSymbol]: Cannot resolve symbol', symbolName);
onResolveErrorCallback('Cannot resolve symbol');
return;
}
// Symbol information object
const symbolInfo = {
ticker: symbolItem.full_name,
name: symbolItem.symbol,
description: symbolItem.description,
type: symbolItem.type,
session: '24x7',
timezone: 'Etc/UTC',
exchange: symbolItem.exchange,
minmov: 1,
pricescale: 100,
has_intraday: false,
visible_plots_set: 'ohlc',
has_weekly_and_monthly: false,
supported_resolutions: configurationData.supported_resolutions,
volume_precision: 2,
data_status: 'streaming',
};
console.log('[resolveSymbol]: Symbol resolved', symbolName);
onSymbolResolvedCallback(symbolInfo);
},
getBars: async (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => {
const { from, to, firstDataRequest } = periodParams;
console.log('[getBars]: Method call', symbolInfo, resolution, from, to);
const parsedSymbol = parseFullSymbol(symbolInfo.full_name);
const urlParameters = {
e: parsedSymbol.exchange,
fsym: parsedSymbol.fromSymbol,
tsym: parsedSymbol.toSymbol,
toTs: to,
limit: 2000,
};
const query = Object.keys(urlParameters)
.map(name => `${name}=${encodeURIComponent(urlParameters[name])}`)
.join('&');
try {
const data = await makeApiRequest(`data/histoday?${query}`);
if (data.Response && data.Response === 'Error' || data.Data.length === 0) {
// "noData" should be set if there is no data in the requested period
onHistoryCallback([], { noData: true });
return;
}
let bars = [];
data.Data.forEach(bar => {
if (bar.time >= from && bar.time < to) {
bars = [...bars, {
time: bar.time * 1000,
low: bar.low,
high: bar.high,
open: bar.open,
close: bar.close,
}];
}
});
console.log(`[getBars]: returned ${bars.length} bar(s)`);
onHistoryCallback(bars, { noData: false });
} catch (error) {
console.log('[getBars]: Get error', error);
onErrorCallback(error);
}
},
subscribeBars: (symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) => {
console.log('[subscribeBars]: Method call with subscriberUID:', subscriberUID);
},
unsubscribeBars: (subscriberUID) => {
console.log('[unsubscribeBars]: Method call with subscriberUID:', subscriberUID);
},
};
helpers.js
// Makes requests to CryptoCompare API
export async function makeApiRequest(path) {
try {
const response = await fetch(`https://min-api.cryptocompare.com/${path}`);
return response.json();
} catch(error) {
throw new Error(`CryptoCompare request error: ${error.status}`);
}
}
// Generates a symbol ID from a pair of the coins
export function generateSymbol(exchange, fromSymbol, toSymbol) {
const short = `${fromSymbol}/${toSymbol}`;
return {
short,
full: `${exchange}:${short}`,
};
}
// Returns all parts of the symbol
export function parseFullSymbol(fullSymbol) {
const match = fullSymbol.match(/^(\w+):(\w+)\/(\w+)$/);
if (!match) {
return null;
}
return { exchange: match[1], fromSymbol: match[2], toSymbol: match[3] };
}