├── .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 |
--------------------------------------------------------------------------------