[First published on my personal blog]

0. Being pushed

A few days ago, the company leader mentioned my blog, saying that I was lazy and did not update it.

Take advantage of the holiday to update round… Wait, when did this become a job?

1. What is TradingView

Today let’s talk about a special – TradingView, which is a professional chart library, specializing in K chart, and K chart is a stock, fund and other exchanges must have one thing. The project itself is free, but not open source. There is a private library hosted on Github that developers can access by submitting the necessary information. The main repository contains compressed library files and simple data access cases; Development documentation is available in the Wiki, as well as hands-on examples in other repositories.

Several chart libraries commonly used in the front end, like ECharts, DataV actually support the drawing of the basic K chart (some called candle chart, called different), with bar chart and line chart, but also to draw volume, MA and other indicators. TradingView as a professional industry products, in addition to the previously mentioned these charts, also offers a wide range of professional measurement tools, for the use of the professional investors and analysts, all these if by developers to realize by oneself, will need to spend a lot of effort, this kind of package of packaging solutions, is it the most attractive places.

Company ongoing a project recently, is a digital assets exchange, competing goods research found that when colleagues almost without exception are chosen this chart libraries, even fire COINS, FCoin vane all levels of the company chose this graph library, visible in the authority of industry, as well as a near-monopoly. And because of that, we started looking at it.

2. Expertise === trouble

Major belongs to major, but it is developed for specific needs of specific industries after all, there are a lot of professional concepts, terms, practices we do not understand, we have to learn. The quality of the documentation, which is available as a Wiki on Github, is mediocre and seems to cover everything, but the lines are full of obscure concepts, the annotations on parameters are incomplete, and many operational details are missing, making the reading experience terrible. While the project website offers a Chinese option and the chart library itself supports multiple languages, the documentation is in English only (although, personally, the language itself is not a pressure; But if you need it, here is a Chinese version that someone else put together, including a video tutorial on UDF solutions by a veteran developer at TradingView. Some of them will be used here for ease of explanation, thanks to the author).

Compared with ECharts and DataV, which are all ready and only need to fill in data and set parameters, TradingView is much more difficult to get started. It requires developers to implement a set of data source API according to its rules. Although the functions and parameters of each API are explained, some key points are not clearly explained, and many developers (including myself and others I have spoken with) still don’t have a good understanding of “how the hell this works” after reading the documentation. Write this blog, is the hope can solve this problem to do a little contribution, let later comers can relax some.

3. To save time

To be clear, this blog is not going to be a step-by-step guide to building the whole thing. I’m assuming that you’ve at least read the official documentation, made some tentative attempts, ran into problems, turned to search engines, and then got here.

This blog is more like a FAQ, based on my own experience of stepping on pits, some of the more difficult things, according to my personal understanding to share with you.

So if you were hoping that this blog post would allow you to fully master TradingView and draw k-lines without having to look at the official documentation, you’re going to be disappointed.

4. Concept first

There are some technical concepts in TradingView that are not easy to understand, but are very important. Here are some simple explanations.

4.1. The Symbol

The Symbol is a commodity. The K line shows the trend of price changes. As for the price of something, it can be a stock, currency or any commodity. TradingView provides such an abstract concept for generality. A Symbol is a JS object that describes some attributes of a commodity (name, price decimal, supported time resolution, trading opening time, etc., refer to official documentation for details). The chart library determines which data to obtain according to the Symbol definition.

The fixed format of the trade name is “EXCHANGE:SYMBOL”. SYMBOL represents a commodity, such as a stock, a trading pair. EXCHANGE is the name of an EXCHANGE. The same commodity may have different prices on different exchanges, so it needs to be distinguished.

4.2. Resolution

Resolution is the time interval between two adjacent columns in a K-chart. I haven’t studied the technical term for it, but I feel that it is an expression that you can use in other words, but TradingView chose it.

4.3. Study

Indicators such as volume, moving averages, and various other analytical indicators. Developers can add their own through the API provided by TradingView.

4.4. Chart

The graph body, especially the K-graph and related indicators, does not include the toolbar. A chart instance can contain multiple metrics

4.5. The Widget

Widgets, similar to widgets on Android. Widgets can be thought of as containers, mainly toolbars, and an area for drawing real diagrams, without the diagram body. A Widget can contain multiple chart instances

4.6. FeatureSet

A feature set that is part of the Widget configuration options for customizing some of the functions of the chart library (including display and style).

4.7. Overrides

Overwrite, part of the Widget configuration option that customizes the style of the chart library (mainly the color of the parts of the chart). The entire chart library is made up of an outer DOM structure and an internal canvas, so the stylesheet Settings are also divided into two parts. Here is the Settings for the Canvas, and there is a custom_cSS_URL property that specifies a CSS file. Where you can override the style of the DOM part. Specific can be combined with the official documentation, as well as Chrome DevTool to locate.

4.8. Title

Data sources, that’s what we’re going to talk about. It is a collection of methods for TradingView to obtain and process data, as well as the core of TradingView data access, which needs to be realized by users themselves. It can be an instance of a Class, or it can be a simple object.

5. How do I access my own data

It is not difficult to create a chart library instance, which should be understood by those who have seen the documentation and hands-on cases. The difficulty is how to fill in the data. I believe that most of the TradingView headache friends are stuck here, as long as the data is connected, the rest are small problems.

TradingView’s versatility lies in its separation of data and presentation. The chart library itself only provides the part of presentation. No matter what kind of data you have, as long as it can be organized into a specified format and filled in, it is ok. In plain English, you need to implement an adapter yourself.

TradingView provides two ways to fetch Data, an HTTP-based solution (UDF, Universal Data Feed, which is used in the demo case in the main repository) and a Websocket-based solution (JS API).

In either case, the data can be divided into two parts: historical data up to date, and newly generated data.

5.1. UDF programme

The solution is very simple, the front end is already defined, you just need to access the interface provided in the demo code provided in the example (the demo code is written in TypeScript, which has a little extra cognitive cost, but is not a problem), and the main work is on the back end, which needs to provide the corresponding query interface as required. One of the most core is to obtain specified products, specified resolution, specified time range of data, the specific format can refer to the official document. We’re not going to expand it here.

Polling – we know to be an effective but highly deprecated practice (unless the environment doesn’t support WebSocket, which is the only way to use it), because a lot of times you don’t get new data and it’s a waste of performance. We would prefer to be notified whenever new data comes in, which leads to the following scheme.

5.2. JS API

Each API is described in the official document, of which onReady(), resolveSymbol(), getBars(), subscribeBars() and unsubscribeBars() are essential, and the rest are implemented as required. Here we are talking about the most basic use. The first two are easy, but let’s focus on the latter. (This is done as an instance method of the DataFeed class, or you can simply create a JS object containing these functions.)

5.2.1. GetBars ()

This interface is specifically used to retrieve historical data, that is, data prior to the current moment. TradingView will delimit a time range from the current moment based on Resolution, and attempt to obtain the data within this time range, specifying the Resolution of Symbol. For performance reasons, TradingView only retrieves data within the visible range, and data beyond the visible range is lazily loaded in segments as the chart is dragged and scaled.

This part of the implementation code is more, we step by step, first to implement an internal function to send data:

getBars (symbolInfo, resolution, from, to, onHistoryCallback, onErrorCallback, firstDataRequest) {
  function _send (data) {
    // Filter by time
    const dataInRange = data.length
      ? data.filter(n= > n.time >= from && n.time <= to)
      : []

    // return noData if there is noData
    const meta = {
      noData: !dataInRange.length
    }

    // There is data, then collate into the chart library required format
    const bar = [...dataInRange]

    // Trigger the callback
    onHistoryCallback(bar, meta)
  }
}
Copy the code

We use this function as an internal function of getBars(), where from, to, and onHistoryCallback are the parameters provided by the API, data is the data we get, and (bar, meta) is the fixed format required by TradingView.

This function is responsible for calling the callback function to pass the data we retrieved to the diagram. Next, let’s get the data (demo code, some of the sensitive and compatible code has been omitted, leaving only the most basic, publicizable logic) :

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

  function _send (data) {
    // ...
  }
  
  // A simple tool function to achieve reverse lookup
  / / can be as simple as Array. Prototype. FindIndex version in reverse chronological order
  // will be used later
  function _findLastIndex (arr, fn) {
    for (var i = arr.length - 1; i >= 0; i--) {
      if (fn(arr[i])) return i
    }
    return - 1
  }

  // For data sharing purposes
  // We put the retrieved data into Redux
  // First try to get existing data from Redux
  const existingData = store.getState().kChartData || []

  // If there is already data in Redux, read it directly
  if (existingData.length) {
    _send(existingData)
    return
  }

  // If there is no data in Redux, load it through WebSocket
  // Our design is to use WebSocket for historical data and real-time updates
  // Push historical data for the first time, and push updates later
  // Only one WebSocket request will be initiated for the same transaction pair and resolution

  // Determine functionality support first
  // We use WebWorker to separate the WebSocket logic from the main thread
  // for performance optimization purposes, more on this later.
  if (!window.Worker) return

  // Restrict Worker singletons
  const hasWSInstance = !!window.kChartWorker
  window.kChartWorker = window.kChartWorker || new window.Worker('./worker-kchart.js')

  // WebWorker data push callback
  window.kChartWorker.onmessage = e= > {
    const { data = {} } = e

    // When there is data push
    if (data.kChartData) {
      // Get the existing data
      const kChartData = store.getState().kChartData
      
      // Incremental update
      for (const item of data.kChartData) {
        // Because the data of line K is arranged in chronological order,
        // Data updates are at the end, so reverse search is faster
        const idx = _findLastIndex(kChartData, n => n.time === item.time)
        idx < 0? kChartData.push(item) : kChartData[idx] = { ... kChartData[idx], ... item } }// Record the new data to Redux
      const promise = new Promise((resolve, reject) = > {
        store.dispatch(setKChartData(kChartData))
        resolve({
          full: kChartData, // The latest complete data
          updates: data.kChartData // The content of this update
        })
      })

      promise.then(res= > {
        // dataInited is our custom variable
        // It is used to distinguish initial push from subsequent push
        // The initial value is false and the first push value is true
        if (this.dataInited) {
          // If not the first push
          // Subscribe to each subscriber in the global K-line list (more on this later)
          window.kChartSubscriberList = window.kChartSubscriberList || []
          for (const sub of window.kChartSubscriberList) {
            // Filter by transaction pair and resolution
            if(sub.symbol ! = =this.symbol) return
            if(sub.resolution ! == resolution)return

            // Push data through the callback function
            if (typeofsub.callback ! = ='function') return
            // Only one data can be added to the chart library at a time, or the latest data can be updated
            // Our push data is an array, which may contain more than one piece of data
            // So push it one by one
            for (const update of res.updates) {
              sub.callback(update)
            }
          }
        } else {
          // First push
          _send(res.full)
          this.dataInited = true}}}})// Prepare WebWorker messages
  // Only if there is no data available
  // Therefore, only when initializing and switching the trading pair/resolution
  // a WebSocket request is initiated
  const msg = {
    // action indicates the purpose of the action
    // init for initialization
    // restart to switch transaction pairs/resolution
    // Corresponding to different WebSocket operations
    action: hasWSInstance ? 'restart' : 'init'.symbol: symbolInfo,
    resolution: resolution,
    url: WEBSOCKET_URL
  }

  // Send WebWorker messages
  window.kChartWorker.postMessage(msg)
}
Copy the code

At this point, we have successfully retrieved the historical data and sent a push of real-time updates to each subscriber (although theoretically there could always be only one subscriber, from a system design perspective, we still design for multiple subscribers).

The specific operation of WebSocket has nothing to do with TradingView, you can choose any method you are familiar with, we will not repeat here, just tell the launch time and callback processing.

GetBars () is fine, but once you figure out how it works, there’s nothing particularly hard about it. It’s more about data structure design and performance optimization. What I’m sure is confusing to a lot of people is the following function.

5.2.2. SubscribeBars ()

The onHistoryCallback callback of getBars() is called only once. The onHistoryCallback of getBars() is called only once, and the onHistoryCallback is called only once. Real-time push fetching needs to be implemented in subscribeBars(). In fact, it’s just adding a subscriber and storing the callback function that adds the updated data in the outer layer, which is actually called in the previous getBars(). This function just queues up and gets and distributes all the data in getBars().

subscribeBars (symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) {
  // Restrict singletons
  window.kChartSubscriberList = window.kChartSubscriberList || []

  // Avoid double subscriptions
  const found = window.kChartSubscriberList.some(n= > n.uid === subscriberUID)
  if (found) return

  // Add a subscription
  window.kChartSubscriberList.push({
    symbol: symbolInfo,
    resolution: resolution,
    uid: subscriberUID,
    callback: onRealtimeCallback
  })
}
Copy the code

This function is called once for each Symbol + Resolution combination, passing the corresponding identification information and callback function to the subscription list. When the push data arrives, it traverses the subscription list, finds the eligible subscriber, and calls its callback function to pass the data. It’s basically the observer model.

5.2.3 requires. UnsubscribeBars ()

After you understand subscribeBars(), you will understand unsubscribeBars() very well.

unsubscribeBars (subscriberUID) {
  window.kChartSubscriberList = window.kChartSubscriberList || []

  const idx = window.kChartSubscriberList.findIndex(n= > n.uid === subscriberUID)
  if (idx < 0) return

  window.kChartSubscriberList.splice(idx, 1)}Copy the code

6. How to switch trading pair/resolution

Once the widget instance is created, you can retrieve the Chart instance using a specific method, and then update Symbol and Resolution using a specific method that refires the previously mentioned functions with new parameters. From this point of view, these functions are a bit like lifecycle functions, describing when a column of operations such as retrieving data, subscribing to updates, etc., takes place, with the developer deciding what to do and when.

this.widget = new window.TradingView.widget(widgetOptions)
this.widget.onChartReady((a)= > {
  this.chart = this.widget.chart()

  // Set the chart type (for example, the time-sharing chart has a different type than the regular candle chart)
  this.chart.setChartType(chartType)

  / / switch Symbol
  this.chart.setSymbol(symbol, callback)

  Switch / / Resolution
  this.chart.setResolution(resolution, callback)
})
Copy the code

7. Other pits in TradingView

  • Functions in the JS API are automatically called when appropriate, passing in arguments, without having to take the function outside and call it manually.
  • The JS APIonReady()resolveSymbol()These two functions, their callbacks must be called asynchronously, don’t ask why, they are required.
  • The function that toggles Symbol and Resolution has a callback that is not triggered if the new value set is the same as the existing value.

8. Performance optimization of K line

In the process of using WebSocket, we used WebWorker for performance optimization.

When the transaction frequency reaches a certain level, websockets will frequently push data to the client. If you put this logic directly into the React component and go to setState() as soon as new data comes in, the page will immediately become jammed (the hard way). SetState () with very short intervals is cached and consolidated into a single update to reduce unnecessary computation and rendering. If data continues to be poured in frequently, a large number of updates will not be committed, and components will never enter the next render round. In addition, every time new data comes in, it needs to be incrementally merged with old data, and the high frequency and high load of calculation will occupy the main thread resources, resulting in not enough computing resources for page rendering, and the page will be stuck.

With this in mind, the solution is to take these computation-intensive tasks off the main thread and delegate them to a concurrent thread, the WebWorker.

But it’s not enough just to hand over computations. Even though the main thread is loaded, updates are still frequent.

Scientific data show that the human eye visual retention time of about 0.1 seconds, that is to say, even if really make the Numbers on the page for a second change a dozen times or more, the eye to see clearly, also in terms of use, 1 seconds to change a 4-5 times already is the limit, even if a does not affect 0.5 update, Therefore, it is not necessary to update the page according to the frequency of WebSocket data push. We can create a buffer band, cache the data pushed by WebSocket into an array, check the array at regular intervals to see if there is any content, notify the main thread to update, and do nothing. This strikes a balance between performance and effectiveness.

Some people will be concerned about WebWorker compatibility issues, after all, ordinary H5 pages rarely use this, not familiar. WebWorker’s browser compatibility is roughly the same with WebSocket, at least in the scope we care about, it is the same, IE 10 and above, ivy browser need not say much has long supported, so unless you have to be compatible with the needs of the old, rest assured to use well.

9. Summary

The exchange project, should be a relatively large project in recent years to take over, involving a lot of things, many of which have not contacted before, are to learn to sell now. Encountered a lot of pits in the process, but also a lot of growth. I’ll share some of the other pitfalls I’ve encountered in the future.