├── .gitmodules ├── .gitignore ├── index.html ├── src ├── main.js ├── helpers.js ├── streaming.js └── datafeed.js ├── LICENSE └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "charting_library_cloned_data"] 2 | path = charting_library_cloned_data 3 | url = git@github.com:tradingview/charting_library.git 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/* 2 | /.vscode/* 3 | .vscode/* 4 | *.sublime-project 5 | *.sublime-workspace 6 | node_modules/ 7 | npm-debug.log* 8 | .DS_Store 9 | /.vs/* 10 | package-lock.json 11 | /bin/* 12 | *.sln 13 | *.csproj 14 | *.pyproj 15 | *.user 16 | 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TradingView Advanced Charts example 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // Datafeed implementation 2 | import Datafeed from './datafeed.js'; 3 | 4 | window.tvWidget = new TradingView.widget({ 5 | symbol: 'Bitfinex:BTC/USD', // Default symbol 6 | interval: '1D', // Default interval 7 | fullscreen: true, // Displays the chart in the fullscreen mode 8 | container: 'tv_chart_container', // Reference to an attribute of the DOM element 9 | datafeed: Datafeed, 10 | library_path: '../charting_library_cloned_data/charting_library/', 11 | }); 12 | 13 | // Wait for the chart to be ready 14 | tvWidget.onChartReady(() => { 15 | console.log('Chart is ready'); 16 | const chart = tvWidget.activeChart(); 17 | 18 | // Subscribe to interval changes and then clear cache 19 | chart.onIntervalChanged().subscribe(null, () => { 20 | tvWidget.resetCache(); 21 | chart.resetData(); 22 | }); 23 | }); 24 | window.frames[0].focus(); 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TradingView, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | // Get a CryptoCompare API key CryptoCompare https://www.cryptocompare.com/coins/guides/how-to-use-our-api/ 2 | export const apiKey = 3 | ""; 4 | // Makes requests to CryptoCompare API 5 | export async function makeApiRequest(path) { 6 | try { 7 | const url = new URL(`https://min-api.cryptocompare.com/${path}`); 8 | url.searchParams.append('api_key',apiKey) 9 | const response = await fetch(url.toString()); 10 | return response.json(); 11 | } catch (error) { 12 | throw new Error(`CryptoCompare request error: ${error.status}`); 13 | } 14 | } 15 | 16 | // Generates a symbol ID from a pair of the coins 17 | export function generateSymbol(exchange, fromSymbol, toSymbol) { 18 | const short = `${fromSymbol}/${toSymbol}`; 19 | return { 20 | short, 21 | full: `${exchange}:${short}`, 22 | }; 23 | } 24 | 25 | // Returns all parts of the symbol 26 | export function parseFullSymbol(fullSymbol) { 27 | const match = fullSymbol.match(/^(\w+):(\w+)\/(\w+)$/); 28 | if (!match) { 29 | return null; 30 | } 31 | 32 | return { 33 | exchange: match[1], 34 | fromSymbol: match[2], 35 | toSymbol: match[3], 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advanced Charts: Connecting data via the Datafeed API 2 | 3 | ## Overview 4 | 5 | This repository contains sample code for the [Datafeed API tutorial], which demonstrates how to implement real-time data streaming in [Advanced Charts]. 6 | As an example, the tutorial describes a connection via the free [CryptoCompare] API that provides data from different crypto exchanges. 7 | 8 | > [!NOTE] 9 | > Advanced Charts is a standalone client-side library that is used to display financial charts, prices, and technical analysis tools. 10 | > Learn more about Advanced Charts on the [TradingView website]. 11 | 12 | ## Prerequisites 13 | 14 | - The [Advanced Charts repository] is private. 15 | Refer to [Getting Access] for more information on how to get the library. 16 | - To use the [CryptoCompare] API, you should create an account and generate a free API key. For more information, refer to the [CryptoCompare documentation](https://www.cryptocompare.com/coins/guides/how-to-use-our-api/). 17 | 18 | ## How to run 19 | 20 | Take the following steps to run this project: 21 | 22 | 1. Clone the repository. 23 | Note that for the real project, it is better to use this repository as a submodule in yours. 24 | 25 | ```bash 26 | git clone https://github.com/tradingview/charting-library-tutorial.git 27 | ``` 28 | 29 | 2. Go to the repository folder and initialize the Git submodule with the library: 30 | 31 | ```bash 32 | git submodule update --init --recursive 33 | ``` 34 | 35 | Alternatively, you can download the [library repository] from a ZIP file or clone it using Git. 36 | 37 | 3. Run the following command to serve static files: 38 | 39 | ```bash 40 | npx serve 41 | ``` 42 | 43 | ## Release notes 44 | 45 | ### September, 2025 46 | 47 | The latest version introduces several key improvements: 48 | 49 | - **Intraday resolutions**: Added support for minute and hour resolutions. 50 | - **SymbolInfo update**: Removed `full_name` from the `SymbolInfo` object. Now, `ticker` is used instead. 51 | - **Improved search**: `searchSymbols` now properly filters results by user input, selected exchange, and symbol type. 52 | - **Improved `getBars`**: `getBars` now selects the correct API endpoint based on the requested `resolution` (minute, hour, or day), ensuring the most appropriate data is used. 53 | - **Enhanced streaming**: Reworked streaming logic to support [multiple subscriptions] to data updates. 54 | 55 | [Advanced Charts]: https://www.tradingview.com/charting-library-docs/ 56 | [Datafeed API tutorial]: https://www.tradingview.com/charting-library-docs/latest/tutorials/implement_datafeed_tutorial/ 57 | [CryptoCompare]: https://www.cryptocompare.com/ 58 | [TradingView website]: https://www.tradingview.com/HTML5-stock-forex-bitcoin-charting-library/?feature=technical-analysis-charts 59 | [Advanced Charts repository]: https://github.com/tradingview/charting_library 60 | [Getting Access]: https://www.tradingview.com/charting-library-docs/latest/getting_started/quick-start#getting-access 61 | [multiple subscriptions]: https://www.tradingview.com/charting-library-docs/latest/connecting_data/datafeed-api/required-methods#multiple-subscriptions 62 | [library repository]: https://github.com/tradingview/charting_library 63 | -------------------------------------------------------------------------------- /src/streaming.js: -------------------------------------------------------------------------------- 1 | import { parseFullSymbol, apiKey } from './helpers.js'; 2 | 3 | const socket = new WebSocket( 4 | 'wss://streamer.cryptocompare.com/v2?api_key=' + apiKey 5 | ); 6 | // Example ▼ {"TYPE":"20","MESSAGE":"STREAMERWELCOME","SERVER_UPTIME_SECONDS":1262462,"SERVER_NAME":"08","SERVER_TIME_MS":1753184197855,"CLIENT_ID":2561280,"DATA_FORMAT":"JSON","SOCKET_ID":"7zUlXfWU+zH7uX7ViDS2","SOCKETS_ACTIVE":1,"SOCKETS_REMAINING":0,"RATELIMIT_MAX_SECOND":30,"RATELIMIT_MAX_MINUTE":60,"RATELIMIT_MAX_HOUR":1200,"RATELIMIT_MAX_DAY":10000,"RATELIMIT_MAX_MONTH":20000,"RATELIMIT_REMAINING_SECOND":29,"RATELIMIT_REMAINING_MINUTE":59,"RATELIMIT_REMAINING_HOUR":1199,"RATELIMIT_REMAINING_DAY":9999,"RATELIMIT_REMAINING_MONTH":19867} 7 | 8 | const channelToSubscription = new Map(); 9 | 10 | socket.addEventListener('open', () => { 11 | console.log('[socket] Connected'); 12 | }); 13 | 14 | socket.addEventListener('close', (reason) => { 15 | console.log('[socket] Disconnected:', reason); 16 | }); 17 | 18 | socket.addEventListener('error', (error) => { 19 | console.log('[socket] Error:', error); 20 | }); 21 | 22 | // Calculates the start time of the bar based on the resolution 23 | function getNextBarTime(barTime, resolution) { 24 | const date = new Date(barTime); 25 | const interval = parseInt(resolution); 26 | 27 | if (resolution === '1D') { 28 | date.setUTCDate(date.getUTCDate() + 1); 29 | date.setUTCHours(0, 0, 0, 0); 30 | } else if (!isNaN(interval)) { // Handles '1' and '60' (minutes) 31 | // Add the interval to the current bar's time 32 | date.setUTCMinutes(date.getUTCMinutes() + interval); 33 | } 34 | return date.getTime(); 35 | } 36 | 37 | socket.addEventListener('message', (event) => { 38 | const data = JSON.parse(event.data); 39 | 40 | const { 41 | TYPE: eventType, 42 | M: exchange, 43 | FSYM: fromSymbol, 44 | TSYM: toSymbol, 45 | TS: tradeTime, // This is a UNIX timestamp in seconds 46 | P: tradePrice, 47 | Q: tradeVolume, 48 | } = data; 49 | 50 | // Handle Trade event updates only 51 | if (parseInt(eventType) !== 0) { 52 | return; 53 | } 54 | // example TYPE:"0" 55 | // M:"Coinbase" 56 | // FSYM:"BTC" 57 | // TSYM:"USD" 58 | // F:"1" 59 | // ID:"852793745" 60 | // TS:1753190418 61 | // Q:0.34637342 62 | // P:119283.1 63 | // TOTAL:41316.495295202 64 | // RTS:1753190418 65 | // CCSEQ:852777369 66 | // TSNS:654000000 67 | // RTSNS:708000000 68 | 69 | // Description of Q parameters: 70 | // The from asset (base symbol / coin) volume of the trade 71 | // (for a BTC-USD trade, how much BTC was traded at the trade price) 72 | 73 | const channelString = `0~${exchange}~${fromSymbol}~${toSymbol}`; 74 | const subscriptionItem = channelToSubscription.get(channelString); 75 | 76 | if (subscriptionItem === undefined) { 77 | return; 78 | } 79 | 80 | const lastBar = subscriptionItem.lastBar; 81 | 82 | // The resolution will be '1', '60', or '1D' 83 | const nextBarTime = getNextBarTime(lastBar.time, subscriptionItem.resolution); 84 | 85 | let bar; 86 | // If the trade time is greater than or equal to the next bar's start time, create a new bar 87 | if (tradeTime * 1000 >= nextBarTime) { 88 | bar = { 89 | time: nextBarTime, 90 | open: tradePrice, 91 | high: tradePrice, 92 | low: tradePrice, 93 | close: tradePrice, 94 | volume: tradeVolume, 95 | }; 96 | } else { 97 | // Otherwise, update the last bar 98 | bar = { 99 | ...lastBar, 100 | high: Math.max(lastBar.high, tradePrice), 101 | low: Math.min(lastBar.low, tradePrice), 102 | close: tradePrice, 103 | volume: (lastBar.volume || 0) + tradeVolume, 104 | }; 105 | } 106 | subscriptionItem.lastBar = bar; 107 | 108 | // Send data to every subscriber of that symbol 109 | subscriptionItem.handlers.forEach((handler) => handler.callback(bar)); 110 | }) 111 | 112 | export function subscribeOnStream( 113 | symbolInfo, 114 | resolution, 115 | onRealtimeCallback, 116 | subscriberUID, 117 | onResetCacheNeededCallback, 118 | lastBar 119 | ) { 120 | // Valid SymbolInfo 121 | if (!symbolInfo || !symbolInfo.ticker) { 122 | console.error('[subscribeBars]: Invalid symbolInfo:', symbolInfo); 123 | return; 124 | } 125 | const parsedSymbol = parseFullSymbol(symbolInfo.ticker); 126 | 127 | // Subscribe to the trade channel to build bars ourselves 128 | const channelString = `0~${parsedSymbol.exchange}~${parsedSymbol.fromSymbol}~${parsedSymbol.toSymbol}`; 129 | 130 | const handler = { 131 | id: subscriberUID, 132 | callback: onRealtimeCallback, 133 | }; 134 | 135 | let subscriptionItem = channelToSubscription.get(channelString); 136 | if (subscriptionItem) { 137 | console.log('Updating existing subscription with new resolution:', resolution); 138 | subscriptionItem.resolution = resolution; 139 | subscriptionItem.lastBar = lastBar; 140 | subscriptionItem.handlers.push(handler); 141 | return; 142 | } 143 | 144 | subscriptionItem = { 145 | subscriberUID, 146 | resolution, 147 | lastBar, 148 | handlers: [handler], 149 | }; 150 | 151 | channelToSubscription.set(channelString, subscriptionItem); 152 | console.log('[subscribeBars]: Subscribe to streaming. Channel:', channelString); 153 | 154 | const subRequest = { 155 | action: 'SubAdd', 156 | subs: [channelString], 157 | }; 158 | console.log('[subscribeBars]: Sending subscription request:', subRequest); 159 | // Only send SubAdd if the socket is open 160 | if (socket.readyState === WebSocket.OPEN) { 161 | socket.send(JSON.stringify(subRequest)); 162 | } 163 | } 164 | 165 | 166 | export function unsubscribeFromStream(subscriberUID) { 167 | for (const channelString of channelToSubscription.keys()) { 168 | const subscriptionItem = channelToSubscription.get(channelString); 169 | const handlerIndex = subscriptionItem.handlers.findIndex( 170 | (handler) => handler.id === subscriberUID 171 | ); 172 | 173 | if (handlerIndex !== -1) { 174 | subscriptionItem.handlers.splice(handlerIndex, 1); 175 | 176 | if (subscriptionItem.handlers.length === 0) { 177 | console.log('[unsubscribeBars]: Unsubscribe from streaming. Channel:', channelString); 178 | const subRequest = { 179 | action: 'SubRemove', 180 | subs: [channelString], 181 | }; 182 | socket.send(JSON.stringify(subRequest)); 183 | channelToSubscription.delete(channelString); 184 | break; 185 | } 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/datafeed.js: -------------------------------------------------------------------------------- 1 | import { 2 | makeApiRequest, 3 | generateSymbol, 4 | parseFullSymbol, 5 | } from './helpers.js'; 6 | import { 7 | subscribeOnStream, 8 | unsubscribeFromStream, 9 | } from './streaming.js'; 10 | 11 | // Use a Map to store the last bar for each symbol subscription. 12 | // This is essential for the streaming logic to update the chart correctly. 13 | const lastBarsCache = new Map(); 14 | 15 | // DatafeedConfiguration implementation 16 | const configurationData = { 17 | // Represents the resolutions for bars supported by your datafeed 18 | supported_resolutions: ['1', '5', '15', '60', '180', '1D', '1W', '1M'], 19 | 20 | // The `exchanges` arguments are used for the `searchSymbols` method if a user selects the exchange 21 | exchanges: [{ 22 | value: 'Bitfinex', 23 | name: 'Bitfinex', 24 | desc: 'Bitfinex', 25 | }, 26 | { 27 | value: 'Kraken', 28 | // Filter name 29 | name: 'Kraken', 30 | // Full exchange name displayed in the filter popup 31 | desc: 'Kraken bitcoin exchange', 32 | }, 33 | ], 34 | // The `symbols_types` arguments are used for the `searchSymbols` method if a user selects this symbol type 35 | symbols_types: [{ 36 | name: 'crypto', 37 | value: 'crypto', 38 | }, 39 | ], 40 | }; 41 | 42 | // Obtains all symbols for all exchanges supported by CryptoCompare API 43 | async function getAllSymbols() { 44 | const data = await makeApiRequest('data/v3/all/exchanges'); 45 | let allSymbols = []; 46 | 47 | for (const exchange of configurationData.exchanges) { 48 | if (data.Data[exchange.value]) { 49 | const pairs = data.Data[exchange.value].pairs; 50 | 51 | for (const leftPairPart of Object.keys(pairs)) { 52 | const symbols = pairs[leftPairPart].map(rightPairPart => { 53 | const symbol = generateSymbol(exchange.value, leftPairPart, rightPairPart); 54 | return { 55 | symbol: symbol.short, 56 | ticker: symbol.full, 57 | description: symbol.short, 58 | exchange: exchange.value, 59 | type: 'crypto' 60 | }; 61 | }); 62 | allSymbols = [...allSymbols, ...symbols]; 63 | } 64 | } 65 | } 66 | return allSymbols; 67 | } 68 | 69 | export default { 70 | onReady: (callback) => { 71 | console.log('[onReady]: Method call'); 72 | setTimeout(() => callback(configurationData)); 73 | }, 74 | 75 | searchSymbols: async ( 76 | userInput, 77 | exchange, 78 | symbolType, 79 | onResultReadyCallback, 80 | ) => { 81 | console.log('[searchSymbols]: Method call'); 82 | const symbols = await getAllSymbols(); 83 | const newSymbols = symbols.filter(symbol => { 84 | const isExchangeValid = exchange === '' || symbol.exchange === exchange; 85 | const isFullSymbolContainsInput = symbol.ticker 86 | .toLowerCase() 87 | .indexOf(userInput.toLowerCase()) !== -1; 88 | return isExchangeValid && isFullSymbolContainsInput; 89 | }); 90 | onResultReadyCallback(newSymbols); 91 | }, 92 | 93 | resolveSymbol: async ( 94 | symbolName, 95 | onSymbolResolvedCallback, 96 | onResolveErrorCallback, 97 | extension 98 | ) => { 99 | console.log('[resolveSymbol]: Method call', symbolName); 100 | const symbols = await getAllSymbols(); 101 | const symbolItem = symbols.find(({ 102 | ticker, 103 | }) => ticker === symbolName); 104 | if (!symbolItem) { 105 | console.log('[resolveSymbol]: Cannot resolve symbol', symbolName); 106 | onResolveErrorCallback("unknown_symbol"); // for ghost icon 107 | return; 108 | } 109 | // Symbol information object 110 | const symbolInfo = { 111 | ticker: symbolItem.ticker, 112 | name: symbolItem.symbol, 113 | description: symbolItem.description, 114 | type: symbolItem.type, 115 | exchange: symbolItem.exchange, 116 | listed_exchange: symbolItem.exchange, 117 | session: '24x7', 118 | timezone: 'Etc/UTC', 119 | minmov: 1, 120 | pricescale: 10000, 121 | has_intraday: true, 122 | intraday_multipliers: ["1", "60"], 123 | has_daily: true, 124 | daily_multipliers: ["1"], 125 | visible_plots_set: "ohlcv", 126 | supported_resolutions: configurationData.supported_resolutions, 127 | volume_precision: 2, 128 | data_status: 'streaming', 129 | }; 130 | 131 | console.log('[resolveSymbol]: Symbol resolved', symbolName); 132 | onSymbolResolvedCallback(symbolInfo); 133 | }, 134 | 135 | getBars: async (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => { 136 | const { from, to, firstDataRequest } = periodParams; 137 | console.log('[getBars]: Method call', symbolInfo, resolution, from, to); 138 | const parsedSymbol = parseFullSymbol(symbolInfo.ticker); 139 | 140 | let endpoint; 141 | // Determine the correct endpoint based on the resolution requested by the library 142 | if (resolution === '1D') { 143 | endpoint = 'histoday'; 144 | } else if (resolution === '60') { 145 | endpoint = 'histohour'; 146 | } else if (resolution === '1') { 147 | endpoint = 'histominute'; 148 | } else { 149 | onErrorCallback(`Invalid resolution: ${resolution}`); 150 | return; 151 | } 152 | 153 | const urlParameters = { 154 | e: parsedSymbol.exchange, 155 | fsym: parsedSymbol.fromSymbol, 156 | tsym: parsedSymbol.toSymbol, 157 | toTs: to, 158 | limit: 2000, 159 | }; 160 | 161 | // Example of historical OHLC 5 minute data request: 162 | // https://min-api.cryptocompare.com/data/v2/histominute?fsym=ETH&tsym=USDT&limit=10&e=Binance&api_key="API_KEY" 163 | const query = Object.keys(urlParameters) 164 | .map(name => `${name}=${encodeURIComponent(urlParameters[name])}`) 165 | .join('&'); 166 | 167 | try { 168 | const data = await makeApiRequest(`data/v2/${endpoint}?${query}`); 169 | if ((data.Response && data.Response === 'Error') || !data.Data || !data.Data.Data || data.Data.Data.length === 0) { 170 | // "noData" should be set if there is no data in the requested period 171 | onHistoryCallback([], { noData: true }); 172 | return; 173 | } 174 | 175 | let bars = []; 176 | data.Data.Data.forEach(bar => { 177 | if (bar.time >= from && bar.time < to) { 178 | bars.push({ 179 | time: bar.time * 1000, 180 | low: bar.low, 181 | high: bar.high, 182 | open: bar.open, 183 | close: bar.close, 184 | volume: bar.volumefrom, 185 | }); 186 | } 187 | }); 188 | 189 | if (firstDataRequest) { 190 | lastBarsCache.set(symbolInfo.ticker, { ...bars[bars.length - 1] }); 191 | } 192 | console.log(`[getBars]: returned ${bars.length} bar(s)`); 193 | onHistoryCallback(bars, { noData: false }); 194 | } catch (error) { 195 | console.log('[getBars]: Get error', error); 196 | onErrorCallback(error); 197 | } 198 | }, 199 | 200 | subscribeBars: ( 201 | symbolInfo, 202 | resolution, 203 | onRealtimeCallback, 204 | subscriberUID, 205 | onResetCacheNeededCallback, 206 | ) => { 207 | console.log('[subscribeBars]: Method call with subscriberUID:', subscriberUID); 208 | subscribeOnStream( 209 | symbolInfo, 210 | resolution, 211 | onRealtimeCallback, 212 | subscriberUID, 213 | onResetCacheNeededCallback, 214 | // Pass the last bar from cache if available 215 | lastBarsCache.get(symbolInfo.ticker) 216 | ); 217 | }, 218 | 219 | unsubscribeBars: (subscriberUID) => { 220 | console.log('[unsubscribeBars]: Method call with subscriberUID:', subscriberUID); 221 | unsubscribeFromStream(subscriberUID); 222 | }, 223 | }; 224 | --------------------------------------------------------------------------------