Integrating Tradingview’s Technical Analysis Charts with Bitquery GraphQL API using VueJS

This article focuses on integrating Tradingview’s Technical Analysis charts with Bitquery’s GraphQL API in a VueJS web application and using the Moving Average Exponential indicator within the integrated chart to get vital insights of the data. In this article, we will be integrating the Technical Analysis Charts with VueJS and adding an Moving Average Exponential indicator later.

Technical Analysis Charts is a powerful, scalable and informative charting library provided by Tradingview only to regulated brokers and public project websites.

The above is an example of Tradingview’s Technical Analysis chart. Technical Analysis charts can be a great help to investors and traders for all purposes. Now, on that note Bitquery’s GraphQL API gets easily integrated even with Technical Analysis charts. One can obtain data through running a GraphQL query through Bitquery GraphQL API and can visualize the data on the Technical Analysis Charts.

Integrating Bitquery GraphQL API with Tradingview’s Technical Analysis Chart

Before we can move on to the complex stuff, you need to make sure that you have
the access to the charting library code in github. If you are new to Tradingview’s Technical Analysis charts, please go through this link Getting Access to Technical Analysis Charts from Tradingview - Google Docs which guides you through the process of obtaining access to the charting library from Tradingview.

Installing Dependencies

Installing Axios for making calls to Bitquery GraphQL API

npm install axios

Installing Vue CLI to start off our web application

npm install -g @vue/cli

Alas! Initiating our VueJS application

vue create Tradingview-three-VueJS

Copying necessary files in the project folder

After getting the access to the charting library, you need to copy two folders from Tradingview Charting Library Github, charting_library and datafeed into the ./src folder of your VueJS project and in the ./public folder as well.

Adding a DOM container

You need to have some DOM container that will be used to display the chart. In the HTML file index.html in your project’s ./public folder and add the following code

<body style="background-color: #171717;">
    <header style="background-color: #171717;;">
        <h1 style="color: white; text-align: center; font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', sans-serif;">Bitquery & Tradingview Integration</h1>
    </header>
    <div id="app"></div>
</body>

In the above code the <div id='app'> tag acts as a container to display the chart.

Setting components

In the ./src folder, make a separate folder named ./components to keep all the components used in this project separately so it’s easy for us to identify them later on. We will then make a ./TVChartContainer/Bitquery.js file inside the ./components folder. In this particular VueJS web application, we have made a separate folder named ./api to house the index.js.

Bitquery.js

In this file, we’ll be initializing the Bitquery GraphQL API’s endpoint as shown below.

export const endpoint = 'https://graphql.bitquery.io';

.api/index.js

This file will create a Charting Library Widget. The Charting Library is used to display financial data, but it doesn’t contain any data itself. Whatever you have, a web API, a database or a CSV file, you can display your data in the Charting Library. Datafeed is an Object you supply to the TradingView Widget. It has a set of methods like “getBars” or “resolveSymbol” that are called by the Charting Library in certain cases. The datafeed returns results using callback functions.

In the first line of the datafeed.js, we need to import Axios and in the second line we will reference the endpoint created in ./TVChartContainer/Bitquery.

We will then set the resolution of the charts. In this example we are allowing the options of 1m, 5m, 15m, 30m, 60m, 1D, 1W, and 1M. The code below explains the above.

import axios from 'axios';
import * as Bitquery from './../TVChartContainer/Bitquery';

const configurationData = {
    supported_resolutions: ['1','5','15','30', '60','1D', '1W', '1M']
};

onReady
We’ll then start with initializing the onReady method. onReady is used by the charting library to get a configuration of your datafeed (eg: supported resolutions, exchanges and so on). This is the first method in the export default section.

export default(baseCurrency) => ({
    onReady: (callback) => {
        setTimeout(() => callback(configurationData));
    },

resolveSymbol
This method is used by the library to retrieve information about a specific symbol (exchange, price scale, full symbol etc.). It is an asynchronous function which takes 3 arguments/parameters; symbolName, onSymbolResolvedCallback & onResolveErrorCallback.

Then, we would use Axios to make a POST request to Bitquery GraphQL API inside the resolveSymbol method. We will reference the endpoint from Bitquery.js file made earlier and put up our GraphQL query in the query field of the API call method.

Remember to setup mode: 'cors', in order to avoid ‘CORS’ error while compilation of the code.

 resolveSymbol: async (symbolName, onSymbolResolvedCallback, onResolveErrorCallback) =>{

        const response = await axios.post(
            Bitquery.endpoint, {
                query: `
                        {
                          ethereum(network: bsc) {
                            dexTrades(
                              options: {desc: ["block.height", "transaction.index"], limit: 1}
                              exchangeAddress: {is: "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73"}
                              baseCurrency: {is: "${baseCurrency}"}
                              quoteCurrency: {is: "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c"}
                            ) 
                            {
                              block {
                                height
                                timestamp {
                                  time(format: "%Y-%m-%d %H:%M:%S") 
                                }
                              }
                              transaction {
                                index
                              }
                              baseCurrency {
                                name
                                symbol
                                decimals
                              }
                              quotePrice
                            }
                          }
                        }
                        `,
                variables: {
                    "tokenAddress": symbolName
                },
                mode: 'cors',
            }, {
                headers: {
                    "Content-Type": "application/json",
                    "X-API-KEY": "YOUR UNIQUE API KEY"
                }
            }
        );

        const coin = response.data.data.ethereum.dexTrades[0].baseCurrency;

        if(!coin){
            onResolveErrorCallback();
        }else{
            const symbol = {
                ticker: symbolName,
                name: `${coin.symbol}/BNB`,
                session: '24x7',
                timezone: 'Etc/UTC',
                minmov: 1,
                pricescale: 10000000,
                has_intraday: true,
                intraday_multipliers: ['1', '5', '15', '30', '60'],
                has_empty_bars: true,
                has_weekly_and_monthly: false,
                supported_resolutions: configurationData.supported_resolutions,
                volume_precision: 1,
                data_status: 'streaming',
            }
            onSymbolResolvedCallback(symbol)
        }
    },

If you are not familiar with how to generate your unique API key through Bitquery GraphQL API, Getting your Bitquery API key is a must read. After the POST request is successful, we’ll store the value from the API in a variable ‘coin’ as shown above. If the API call is succesful and the value of ‘coin’ is not null then you need to set data_status attribute’s value to ‘streaming’ as shown in the above.

Additional configuration of the resolveSymbol could be found in the documentation of the charting_library

getBars
Our next step would be to implement the getBars method. This method is used by the Charting Library to get historical data for our symbol. It basically calls the OHLC data and formats the return data for the charts to be able to process it.

Just like onResolve, getBars is also an asynchronous function which takes symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback & first as it’s parameters/arguments. Inside the getBars method, we’ll be making a try and catch block and making a POST request call using Axios to Bitquery GraphQL API to process the following query

{
  ethereum(network: bsc) {
    dexTrades(
      options: {asc: "timeInterval.minute"}
      date: {since: "2021-06-20T07:23:21.000Z", till: "${new Date().toISOString()}"}
      exchangeAddress: {is: "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73"}
      baseCurrency: {is: "${baseCurrency}"},
      quoteCurrency: {is: "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c"},
      tradeAmountUsd: {gt: 10}
    ) 
    {
      timeInterval {
        minute(count: 15, format: "%Y-%m-%dT%H:%M:%SZ")  
      }
      volume: quoteAmount
      high: quotePrice(calculate: maximum)
      low: quotePrice(calculate: minimum)
      open: minimum(of: block, get: quote_price)
      close: maximum(of: block, get: quote_price) 
    }
  }
}

in order to obtain OHLC data which can be set into a candlesticks format to show in the final product.

We’ll save the response which we got from the API call in a variable called ‘bars’ and we’ll implement the map( ) function to set the data for the opening, closing, maximum and minimum prices of a candlestick. The below code is the complete getBars method.

getBars: async(symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) =>{
        try{
            if (resolution==='1D') {
                resolution = 1440;
            }
            const response2 = await axios.post(Bitquery.endpoint, {
                query: `
                        {
                          ethereum(network: bsc) {
                            dexTrades(
                              options: {asc: "timeInterval.minute"}
                              date: {since: "2021-06-20T07:23:21.000Z", till: "${new Date().toISOString()}"}
                              exchangeAddress: {is: "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73"}
                              baseCurrency: {is: "${baseCurrency}"},
                              quoteCurrency: {is: "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c"},
                              tradeAmountUsd: {gt: 10}
                            ) 
                            {
                              timeInterval {
                                minute(count: 15, format: "%Y-%m-%dT%H:%M:%SZ")  
                              }
                              volume: quoteAmount
                              high: quotePrice(calculate: maximum)
                              low: quotePrice(calculate: minimum)
                              open: minimum(of: block, get: quote_price)
                              close: maximum(of: block, get: quote_price) 
                            }
                          }
                        }
                        `,
                variables: {
                    "from": new Date("2021-06-20T07:23:21.000Z").toISOString(),
                    "to": new Date("2021-06-23T15:23:21.000Z").toISOString(),
                    "interval": Number(resolution),
                    "tokenAddress": symbolInfo.ticker
                },
                mode: 'cors',

            }, {
                headers: {
                    "Content-Type": "application/json",
                    "X-API-KEY": "YOUR UNIQUE API KEY"
                }
            })

            const bars = response2.data.data.ethereum.dexTrades.map(el => ({
                time: new Date(el.timeInterval.minute).getTime(), // date string in api response
                low: el.low,
                high: el.high,
                open: Number(el.open),
                close: Number(el.close),
                volume: el.volume
            }))

            if (bars.length){
                onHistoryCallback(bars, {noData: false});
            }else{
                onHistoryCallback(bars, {noData: true});
            }

        } catch(err){
            console.log({err})
        }
    },

Inserting the contents of the ./api/index.js file in TVChartContainer.vue

mounted() {
        const widgetOptions = {
            symbol: this.symbol,
            datafeed: api(this.baseCurrency),
            interval: this.interval,
            container_id: this.containerId,
            library_path: this.libraryPath,
            theme: 'Dark',
            locale: 'en',
            disabled_features: ['use_localstorage_for_settings'],
            charts_storage_url: this.chartsStorageUrl,
            charts_storage_api_version: this.chartsStorageApiVersion,
            client_id: this.clientId,
            user_id: this.userId,
            fullscreen: this.fullscreen,
            autosize: this.autosize,
            studies_overrides: this.studiesOverrides,
            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
        }
},

For the above snippet, in line-4 the datafeed attribute gets a value of api(this.baseCurrency). The complete code for the TVChartContainer.vue file is as follows.

<template>
    <div>
        <div class="TVChartContainer" id="tv_chart_container"/>
    </div>
</template>

<script>
import api from './api/index'
export default {
    name: 'TVChartContainer',
    props: {
        symbol: {
            default: 'BITQUERY',
            type: String,
        },
        interval: {
            default: 'D',
            type: String,
        },
        containerId: {
            default: 'tv_chart_container',
            type: String,
        },
        datafeedUrl: {
            default: 'https://demo-feed-data.tradingview.com',
            type: String,
        },
        libraryPath: {
            default: '/charting_library/charting_library/',
            type: String,
        },
        chartsStorageUrl: {
            default: 'https://saveload.tradingview.com',
            type: String,
        },
        chartsStorageApiVersion: {
            default: '1.2',
            type: String,
        },
        clientId: {
            default: 'tradingview.com',
            type: String,
        },
        fullscreen: {
            default: false,
            type: Boolean,
        },
        autosize: {
            default: true,
            type: Boolean,
        },
        studiesOverrides: {
            type: Object,
        }
    },
    tvWidget: null,
    data() {
        return {
            baseCurrency: '0x2170ed0880ac9a755fd29b2688956bd959f933f8',
            addressInfo: {},
            searchValue: "",
            getSearchAddress: [],
            getAllAddressValue: [],
            showSearchChild: false,
        }
    },
    mounted() {
        const widgetOptions = {
            symbol: this.symbol,
            datafeed: api(this.baseCurrency),
            interval: this.interval,
            container_id: this.containerId,
            library_path: this.libraryPath,
            theme: 'Dark',
            locale: 'en',
            disabled_features: ['use_localstorage_for_settings'],
            charts_storage_url: this.chartsStorageUrl,
            charts_storage_api_version: this.chartsStorageApiVersion,
            client_id: this.clientId,
            user_id: this.userId,
            fullscreen: this.fullscreen,
            autosize: this.autosize,
            studies_overrides: this.studiesOverrides,
            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
        }
        this.tvWidget = new TradingView.widget(widgetOptions)
    },
    methods: {
        getCoinInfo() {
            const query = `
                        {
                          ethereum(network: bsc) {
                            dexTrades(
                              options: {desc: ["block.height", "transaction.index"], limit: 1}
                              exchangeAddress: {is: "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73"}
                              baseCurrency: {is: "${this.baseCurrency}"}
                              quoteCurrency: {is: "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c"}
                            )
                            {
                              block {
                                height
                                timestamp {
                                  time(format: "%Y-%m-%d %H:%M:%S")
                                }
                              }
                              transaction {
                                index
                              }
                              baseCurrency {
                                name
                                symbol
                                decimals
                              }
                              quotePrice
                            }
                          }
                        }
                        `;
            const url = "https://graphql.bitquery.io/";
            const opts = {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    "X-API-KEY": "YOUR UNIQUE API KEY"
                },
                body: JSON.stringify({
                    query
                })
            };
            fetch(url, opts)
                .then(res => res.json())
                .then(response => {
                    this.addressInfo = response.data.ethereum.dexTrades[0].baseCurrency;
                })
                .catch(console.error);
        },
    },
}
</script>

<style>
.TVChartContainer {
    position: absolute;
    width: 100%;
    height: 80%;
}
</style>

Injecting Component to Home.vue and Tradingview.vue

Now, after creating the TVChartContainer.vue, in order to display the same in our VueJS application, we need to make a Home.vue file inside ./views folder and call the TVChartContainer.vue file’s contents inside it.
The following code snippet would make this more clear.

<template>
    <TVChartContainer :symbol="'BITQUERY'" :interval="'60'"></TVChartContainer>
</template>

<script>
import TVChartContainer from "../components/TVChartContainer";
export default {
    name: "Home",
    components: {TVChartContainer}
}
</script>

<style scoped>
</style>

NOTE!
In the attached project, we have used VueJS Router. What it does is that when the user starts the VueJS application in his/her local browser, if a request like https://localhost:8080/ is made, then Home.vue file’s contents will be displayed and if https://localhost:8080/trading-view is made, then Tradingview.vue file’s contents will be displayed!

Furthermore, in the App.vue the <router-view /> component would render the Home.vue or Tradingview.vue component as per the request made by the user as shown below.

<template>
    <div id="app">
        <router-view />
    </div>
</template>

<script>
export default {
    name: 'App'
};
</script>

<style scoped></style>

Running the VueJS project

In the root folder of your project, run the following command in the terminal to run the VueJS application.

npm run serve

Open your browser and move to https://localhost:8080/ and you’ll see a similar screen as shown below.

Using the indicator markdown in the Technical Chart, select Moving Average Exponential indicator and you’ll notice a blue line passing through the candlesticks.

This is the Exponential Moving Average criteria which is a type of weighted moving average that gives more weighting or importance to recent price data.

You can use some other Indicators and some other form of data representation (other than candlesticks) for example

Area Graph

Line Graph

GitHub Repository

Do you have any information on how to properly use the subscribeBars() method in the datafeed to properly update the chart with live data? This seems to be the biggest hurdle while using bitQuery as an endpoint for the chart data. Thanks

At the present moment Realtime updates are not implemented but it will be implemented ASAP. Meanwhile you can checkout this documentation provided by Tradingview themselves regarding Realtime updates using Web Sockets