Skip to main content

Implement datafeed

tip

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:

/chart‑project/src/datafeed.js
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.

  1. Add the following DatafeedConfiguration implementation for the datafeed sample:

    /chart‑project/src/datafeed.js
    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'}
    ]
    };
  2. Call the OnReadyCallback asynchronously and pass a DatafeedConfiguration object as a parameter:

    /chart‑project/src/datafeed.js
    onReady: (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.

  1. Create a helpers.js file for the CryptoCompare API functions and add the following code:

    /chart‑project/src/helpers.js
    // Your CryptoCompare API key
    export const apiKey = "<api-key>";

    // Makes requests to CryptoCompare API
    export async function makeApiRequest(path) {
    try {
    const url = new URL(`https://min-api.cryptocompare.com/${path}`);
    url.searchParams.append('api_key',apiKey)
    const response = await fetch(url.toString());
    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,
    };
    }

    At this step, you need a CryptoCompare API key (line 2). If you have not got this key yet, consider the CryptoCompare documentation. Also, note that the makeApiRequest and generateSymbol functions are specific to CryptoCompare API and will be used in resolveSymbol. You might not need them when implementing your own datafeed.

  2. Import the functions from helpers.js into datafeed.js:

    /chart‑project/src/datafeed.js
    import { makeApiRequest, generateSymbol } from './helpers.js';
  3. 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,
    ticker: symbol.short,
    description: symbol.short,
    exchange: exchange.value,
    type: 'crypto',
    };
    });
    allSymbols = [...allSymbols, ...symbols];
    }
    }
    return allSymbols;
    }
  4. Implement the resolveSymbol method and specify symbol information in a symbolInfo object according to LibrarySymbolInfo.

    /chart‑project/src/datafeed.js
    resolveSymbol: async (
    symbolName,
    onSymbolResolvedCallback,
    onResolveErrorCallback,
    extension
    ) => {
    console.log('[resolveSymbol]: Method call', symbolName);
    const symbols = await getAllSymbols();
    const symbolItem = symbols.find(({ ticker }) => ticker === symbolName);
    if (!symbolItem) {
    console.log('[resolveSymbol]: Cannot resolve symbol', symbolName);
    onResolveErrorCallback('Cannot resolve symbol');
    return;
    }
    // Symbol information object
    const symbolInfo = {
    ticker: symbolItem.ticker,
    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);
    },
    info

    In this tutorial, you specified supported_resolutions: ['1D', '1W', '1M'] in the onReady 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 setting has_weekly_and_monthly to false.

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.

  1. In helpers.js, implement a parseFullSymbol function that will parse a crypto pair symbol and return all parts of this symbol. Note that the full value is returned from generateSymbol.

    /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] };
    }
  2. Import parseFullSymbol into datafeed.js.

    /chart‑project/src/datafeed.js
    import { makeApiRequest, generateSymbol, parseFullSymbol } from './helpers.js';
  3. Implement getBars using parseFullSymbol and CryptoCompare's Daily Pair OHLCV to retrieve historic OHLCV data.

    caution

    The CryptoCompare API does not allow specifying the from date, so you have to filter bars on the client side.

    /chart‑project/src/datafeed.js
    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);
    }
    },

searchSymbols

The library uses the searchSymbols method to request symbols that match some user input.

/chart‑project/src/datafeed.js
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 fullName = `${symbol.exchange}:${symbol.ticker}`;
const isFullSymbolContainsInput = fullName
.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,
ticker: symbol.short,
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 fullName = `${symbol.exchange}:${symbol.ticker}`;
const isFullSymbolContainsInput = fullName
.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(({ ticker }) => ticker === symbolName);
if (!symbolItem) {
console.log('[resolveSymbol]: Cannot resolve symbol', symbolName);
onResolveErrorCallback('Cannot resolve symbol');
return;
}
// Symbol information object
const symbolInfo = {
ticker: symbolItem.ticker,
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
// Your CryptoCompare API key
export const apiKey = "<api-key>";

// Makes requests to CryptoCompare API
export async function makeApiRequest(path) {
try {
const url = new URL(`https://min-api.cryptocompare.com/${path}`);
url.searchParams.append('api_key',apiKey)
const response = await fetch(url.toString());
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,
};
}

// 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] };
}