TradingView Charting Library JS API Setup for Crypto: Part 1

Check out the Introduction to this tutorial series, if you haven’t already. Setting up TradingView charts can be a convoluted process, so please excuse the disclaimers and notes.

Disclaimer: The TradingView charting library is a free, but private project on Github that you must apply for access to. The license agreement I believe bars me from distributing it to you, so to complete this guide fully you will need to apply for access to download the charting library.

To run this part of the tutorial locally (assuming you have access to the charting library) clone the repo available below, and then copy the charting library folder into the /public/ directory in the part1 folder. Run npm install and then npm start to fire up the development server.

Tutorial Repo:

Deployed Preview:

TradingView allows you to use their Charting Library on your own site, with your own source of data.

There are two ways to get your Data into TradingView, the UDF API and the JS API. The JS API gives you the most control over your data, and in my opinion is much more flexible. Plus it’s Javascript!

You can implement the data connection almost however you want, but actual implementation details are pretty fuzzy. The purpose of this tutorial is to show you a working example of using your own data source with TradingView’s charts to create a basic static chart.

NOTE: TradingView DOES NOT provide you with this source of data, and assumes you have implemented your own source. This tutorial relies on CryptoCompare’s historic price API as a data source for convenience.

This guide builds off the React Javascript TradingView Example available here


When the chart widget is first loaded, it will call the JS API method resolveSymbol with the symbolName for the default pair. In our example Coinbase:BTC/USD is the default pair. You are expected to pass a symbolInfo object to the onSymbolResolved callback passed into the resolveSymbol function by the charting library.

The whole integration is composed of several parts:

  • TV Widget Constructor — takes widget options object, pass in the datafeed, the default symbol pair to display, user options, chart load/save options
  • Datafeed — Interface between the JS API and your backend
  • JS API — Function signatures required by TV to display your data
  • History Provider — OHLCV bar API
  • Realtime Provider — Update latest candle in realtime, start new candles
  • Symbol Store — List of available symbols

Part 1 covers TV Widget Constructor, Datafeed, JS API, and History Provider for creating a single static chart, with a hardcoded symbol pair.

Widget Constructor

Widget constructor options configures the TradingView Chart widget, and affects which features are enabled when the chart first loads, as well as what options can be set by the user.

Here we set options like the User ID, style settings, language, the symbol pair to load, public path to the charting library, and pass in our JS API Datafeed implementation.

The docs for the widget constructor options are available here, this will 404 if you haven’t applied for access to the Charting Library

Here is the constructor options we are starting with:

const widgetOptions = {
debug: false,
symbol: 'Coinbase:BTC/USD',
datafeed: Datafeed, // our datafeed object
interval: '15',
container_id: 'tv_chart_container',
library_path: '/charting_library/',
locale: getLanguageFromURL() || 'en',
disabled_features: ['use_localstorage_for_settings'],
enabled_features: [],
client_id: 'test',
user_id: 'public_user_id',
fullscreen: false,
autosize: true,
overrides: {
"paneProperties.background": "#131722",
"paneProperties.vertGridProperties.color": "#363c4e",
"paneProperties.horzGridProperties.color": "#363c4e",
"symbolWatermarkProperties.transparency": 90,
"scalesProperties.textColor" : "#AAA",
"mainSeriesProperties.candleStyle.wickUpColor": '#336854',
"mainSeriesProperties.candleStyle.wickDownColor": '#7f323f',

This configures the widget to show us Coinbase:BTC/USD with candle intervals of 15 minutes, and sets some other customizations (disabled features, default settings to override, what language to use, etc).

Once loaded, you shouldn’t need to change any of these options, but the widget exposes methods that can be used to change some settings dynamically. (changing symbols can be done through the symbol search which we will implement in part 3 of the tutorial)

I’ve set my charts up to default to dark mode by setting the overrides."painProperties.background": “#131722” .

JS API Datafeed Integration

Now that we have the widget configured and styled how we like, let’s look at how we are connecting our chart data to the TradingView Charting Library’s JS API.

The JS API is really an Object you supply to the TradingView Widget, which exposes the functions that TradingView will call, and in most cases you are expected to pass data to the Callbacks within those functions to get your data working with tradingview.

For example’s sake, we are using CryptoCompare’s historic chart data*, and in part 2, their websocket API to get realtime price updates.

Tradingview will call the methods you provide as needed to fill the current chart with data, as well as other lifecycle methods which you must implement.

Below is the entire JS API object signature that Tradingview expects you to pass to the widget. Some methods are optional, see the docs for more info

/* mandatory methods for realtime chart */
onReady: cb => {},
// only need searchSymbols when search is enabled
searchSymbols: (userInput, exchange, symbolType, onResultReadyCallback) => {},
resolveSymbol: (symbolName, onSymbolResolvedCallback, onResolveErrorCallback) => {},getBars: (symbolInfo, resolution, from, to, onHistoryCallback, onErrorCallback, firstDataRequest) => {},subscribeBars: (symbolInfo, resolution, onRealtimeCallback, subscribeUID, onResetCacheNeededCallback) => {},unsubscribeBars: subscriberUID => {},
/* optional methods */
getServerTime: cb => {},calculateHistoryDepth: (resolution, resolutionBack, intervalBack) => {},getMarks: (symbolInfo, startDate, endDate, onDataCallback, resolution) => {},getTimeScaleMarks: (symbolInfo, startDate, endDate, onDataCallback, resolution) => {}

When the chart first loads, the JS API flow goes like this:

1. onReady gets called, pass datafeed configuration options to cb

2. resolveSymbol gets called, pass symbolInfo object to onSymbolResolvedCallback

3. getBars is called, pass array of ohlcv objects with UTC time in milliseconds to onHistoryCallback

Let’s look at our implementation for each one of these


const config = {
supported_resolutions: ["1", "3", "5", "15", "30", "60", "120", "240", "D"]
onReady: cb => {
console.log('=====onReady running')
setTimeout(() => cb(config), 0)

onReady is called immediately after the chart widget initializes, and we must pass the datafeed config options into the onReady cb function. The charting library wants this to be executed asynchronously, and suggests wrapping in setTimeout with delay of 0 to force this behavior.

Right now we are only specifying one of the options possible, supported_resolutions which tells the charting library which interval choices our datafeed supports for bars. These will be shown to the user, and can be overridden per symbol pair later in resolveSymbol . The list we supplied translates to:

1 minute, 3 minute, 15 minute, 30 minute, 1 hour, 2 hour, 4 hour, 1 day

Later in the tutorial we will add options to our Datafeed config, as we implement search and realtime charts.


Once the datafeed is configured, the charting library will call resolveSymbol with the symbol property from the widget config object. We are given only a string value, and must return a symbolInfo object that represents the corresponding symbol.

resolveSymbol: (symbolName, onSymbolResolvedCallback, onResolveErrorCallback) => {
var split_data = symbolName.split(/[:/]/)

var symbol_stub = {
name: symbolName,
description: '',
type: 'crypto',
session: '24x7',
timezone: 'America/New_York',
ticker: symbolName,
minmov: 1,
pricescale: 100000000,
has_intraday: true,
intraday_multipliers: ['1', '60'],
supported_resolution: ["1", "3", "5", "15", "30", "60", "120", "240", "D"],
volume_precision: 8,
data_status: 'streaming',
if (split_data[2].match(/USD|EUR|JPY|AUD|GBP|KRW|CNY/)) {
symbol_stub.pricescale = 100

setTimeout(function() {
}, 0)

This is where you configure the individual pair, set the number of decimal places to display, how much it moves per tick (for crypto it’s almost always going to be 1), and very important and easy to screw up, the intraday_multipliers ! Because crypto is traded nonstop, we set the session to ‘24x7' . Timezone is supposed to be the timezone of the exchange this symbol is traded on, not very important with 24 hour sessions.

All the documentation for symbolInfo is here, make sure to familiarize yourself with it.

intraday_multipliers and has_intraday control showing bar intervals below 1 day. Now here is where I made lots of mistakes: Tradingview can build some bars for you. For example, let’s pretend our historic data API can only give us data for 1 minute intervals, meaning if we request the past 24 hours of data, we will get 1440 data points, the number of minutes in 24 hours. But what if we want to display 15 minute bars? You can tell tradingview that the our intraday_multiplier is ‘1’ and only pass it 1 minute bars. The charting library will build the 15 minute bars for you, and display them on the chart.

We are doing the same for hour bars, telling tradingview we can supply 60 minute bars, and that it should build out our 2 hour and 4 hour bars itself from our 60 minute bars.

Ticker is also very important. If set, then the charting library will use ticker internally to refer to this unique pair (ticker value will be sent to resolveSymbol in place of the name field). The name field is what will be displayed to users. I set both name and ticker to the same value to make my life easier, and because the names I am using include all the information I need to identify the symbol: exchange, to symbol, and from symbol (e.g. Coinbase:BTC/USD)

Pricescale is a little interesting, because different pairs can use different decimal precision. For example, BTC/USD is measured to two decimal places, so pricescale = 100 but for say TRX/BTC (0.00000771 BTC at time of writing), we measure it to satoshi’s, 8 decimal places. So for TRX/BTC pricescale = 100000000 but for TRX/USD ($0.059432 at time of writing), we are going to 6 decimal places and pricescale = 1000000.

It is important to understand how symbolInfo affects your chart, so do check out the docs.


Now on to the fun part! Getting chart data from our API source and handing it off to TradingView.

getBars: function(symbolInfo, resolution, from, to, onHistoryCallback, onErrorCallback, firstDataRequest) {

historyProvider.getBars(symbolInfo, resolution, from, to, firstDataRequest)
.then(bars => {
if (bars.length) {
onHistoryCallback(bars, {noData: false})
} else {
onHistoryCallback(bars, {noData: true})
}).catch(err => {
.../* historyProvider.js */
var rp = require('request-promise').defaults({json: true})
getBars: function(symbolInfo, resolution, from, to, first, limit) {
var split_symbol =[:/]/)

const url = resolution === 'D' ? '/data/histoday' : resolution >= 60 ? '/data/histohour' : '/data/histominute'
const qs = {
e: split_symbol[0], // Coinbase
fsym: split_symbol[1], // BTC
tsym: split_symbol[2], // USD
toTs: to ? to : '',
limit: 2000,
return rp({
url: `${api_root}${url}`,
.then(data => {
if (data.Response && data.Response === 'Error') {
console.log('CryptoCompare API error:',data.Message)
return []
if (data.Data.length) {
var bars = => {
return {
time: el.time * 1000, //TradingView requires bar time in ms
low: el.low,
high: el.high,
close: el.close,
volume: el.volumefrom
return bars
} else {
return []

Okay, let’s break all that code down!

Tradingview calls getBars and passes in the symbolInfo object we passed to the resolveSymbol resolve callback, resolution (do we need 1 minute bars? 60 minute bars? 1 day?), to and from timestamps, and a boolean marking if this is the first data request for this symbol.

From there, we are calling historyProvider.getBars which is code we have written to retrieve historic ohlcv data from Cryptocompare’s historic price API. We must pass an array of bar data to getBar’s onHistoryCallback , that array could look like this for 1 minute bar data:

time: 1528322340000, //bar time must be in milliseconds
open: 7678.85,
high: 7702.55,
low: 7656.98,
close: 7658.25,
volume: 0.9834

So our historyProvider file is responsible for actually making the request to CryptoCompare to get the appropriate data. To make the request with CrytoCompare we need to know the to symbol, from symbol, and the specific exchange we want data from.

Because we have chosen to put all the relevant information into the symbol name (Coinbase:BTC/USD), we are able to extract those parameters from the string.

TradingView also passes the resolution to getBars, which will inform what API endpoint we request from CryptoCompare, the minute, hour, or day historic data endpoint.

Because of the limits of CryptoCompare’s API (we are only able to get 2000 records at a time) we may be passing an incomplete set of the data TradingView has requested. No worries! getBars will be called again, with new to and from timestamps, until all the data it needs to fill the visible part of the chart is obtained.

Hooray for Static Charts!

I hope this has helped you. This process overwhelmed me at first, which is why I’m trying to share my learnings with you, this is a confusing process.

You are likely thinking “Okay great Jon, but static charts don’t help me much”. In part 2 of this tutorial series we implement realtime updates to the chart. It’s important to grasp the concepts outlined here first, and get familiar with TradingView’s documentation.

Here is a deployed preview of part 1:

I’m sure many of you are still confused, or found errors I may have made. Feel free to comment here, or reach out via the email below 👇👇


Checkout the code for all parts of this tutorial series here

*Relying on a 3rd party service to provide your chart data in production is less than optimal. For example, Cryptocompare limits minute data to only 7 days in the past. Fine for our tutorial, but this prevents us charting minute data beyond 7 days into the past. Also, you are limited by CryptoCompare’s API rate limits and all downtime they may experience.

Javascript Developer