preface

Recently, I am doing the K chart of the exchange project, so I can share my experience with you. Code in the majority, traffic early warning !!!! Don’t get lost.

Technology selection

  • echarts
    • Based on Canvas drawing, a complete variety of visual chart library.
    • Official address: echarts.baidu.com/
  • highcharts
    • Based on SVG, you can customize a lot of content and manipulate the DOM more easily.
    • Official address: www.highcharts.com.cn/
  • tradingview
    • Canvas – based professional globalization trend chart.
    • Official address: cn.tradingview.com/
  • The advantages and disadvantages
    • Hightcharts: Looked at HightCharts in detail the other day www.fota.com/option/. Dom manipulation in SVG, along with customizing content, was found to be easier to implement, but almost always manual, succumbing to the pressure of short development cycles. The above project has been completed for three months, but it still has a sense of achievement.
    • Echarts: Echarts official cases are many, often do some background management system, show data will be used, convenient, easy to use, users are enough, the search engine chicken can solve any problem you have. But for some operations, such as drawing lines, it is slightly weaker. Not enough to meet demand.
    • Tradingview: As long as you enter the official website, you can see its professionalism, it is completely built for professional transactions, you just need to think about filling data can be, even in some common transaction content, TradingView can use its own data push.
  • reporter
    • So, professional trading chart, give professional library to do it
    • Manual dog head ~~~~(∩_∩)

The preparatory work

  • Apply for account number (Key)
    • After registering in the official website, there will be an email prompt, step by step to do it, here will not be repeated.
  • Environment set up
    • I used my React+ WebPack4 scaffolding, but you can use native JS, or whatever framework you like (the code posted below is for React).
    • Download the code base from the official
  • Understand websocket communication protocol
    • Send the request
    • Receive data
  • The outline
    • Tradingview Chinese development document b.aitrad. ga/books/tradi…
    • And the API sample tradingview. Making. IO/featuresets… (You need to prepare your own stairs here)
  • Demo of a great god github.com/tenggouwa/t…

Let’s get started


create

  • Contents page | - kLine / / k line folder | -- - | - API / / need to use methods | -- - | -- - | -- datafees. Js / / defines some common methods | -- - | -- - | -- dataUpdater. Js / / update the content of the call | -- - | -- - | -- socket. | -- - | js / / websocket method -- index. Js / / your code development | -- - | -- index. SCSS / / style developmentCopy the code
  • Add the following code to datafees

        /**
         * JS API
         */
        import React from 'react'
        import DataUpdater from './dataUpdater'Class datafeeds extends React.Component {/** * JS API * @param {*Object} react/constructor(self) { super(self) this.self = self this.barsUpdater = new DataUpdater(this) this.defaultConfiguration = This. DefaultConfiguration. Bind (this)} / * * * @ param Function} {* callback callback Function * ` onReady ` shouldreturn result asynchronously.
             */
            onReady(callback) {
                // console.log('=============onReady running')
                return new Promise((resolve) => {
                    let configuration = this.defaultConfiguration()
                    if(this.self.getConfig) { configuration = Object.assign(this.defaultConfiguration(), this.self.getConfig()) } resolve(configuration) }).then(data => callback(data)) } /** * @param {*Object} symbolInfo @param {*String} Resolution * @param {*Number} rangeStartDate timestamp, the leftmost requested K line time * @param {*Number} @param {*Function} onDataCallback callback * @param {*Function} onErrorCallback callback */ getBars(symbolInfo, resolution, rangeStartDate, rangeEndDate, onDataCallback) { const onLoadedCallback = (data) => { data && data.length ? onDataCallback(data, { noData:false }) : onDataCallback([], { noData: true}) } this.self.getBars(symbolInfo, resolution, rangeStartDate, rangeEndDate, OnLoadedCallback) /* eslint-enable */} /** * @param {*String} symbolName Ticker * @param {*Function} OnSymbolResolvedCallback succeeded * @param {*Function} onResolveErrorCallback failed * 'resolveSymbol' shouldreturn result asynchronously.
             */
            resolveSymbol(symbolName, onSymbolResolvedCallback, onResolveErrorCallback) {
                return new Promise((resolve) => {
                    // reject
                    let symbolInfoName
                    if (this.self.symbolName) {
                        symbolInfoName = this.self.symbolName
                    }
                    let symbolInfo = {
                        name: symbolInfoName,
                        ticker: symbolInfoName,
                        pricescale: 10000,
                    }
                    const { points } = this.props.props
                    const array = points.filter(item => item.name === symbolInfoName)
                    if(array) { symbolInfo.pricescale = 10 ** array[0].pricePrecision } symbolInfo = Object.assign(this.defaultConfiguration(), symbolInfo) resolve(symbolInfo) }).then(data => onSymbolResolvedCallback(data)).catch(err => OnResolveErrorCallback (err))} /** * Subscribe to K line data. The chart library will call the onRealtimeCallback method to update the live data * @param {*Object} symbolInfo commodity information * @param {*String} resolution * @param {*Function} onRealtimeCallback callback * @param {*String} unique identifier to listen to * @param {*Function} OnResetCacheNeededCallback (since 1.7) : */ subscribeBars(symbolInfo, Resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) { this.barsUpdater.subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, OnResetCacheNeededCallback)} / unsubscribe * * * * K line data @ param String} {* subscriberUID listening unique identifier * / unsubscribeBars (subscriberUID) {this. BarsUpdater. UnsubscribeBars (subscriberUID)} / * * * * the default configuration/defaultConfiguration = () = > {const object = { session:'24x7',
                    timezone: 'Asia/Shanghai',
                    minmov: 1,
                    minmov2: 0,
                    description: 'www.coinoak.com',
                    pointvalue: 1,
                    volume_precision: 4,
                    hide_side_toolbar: false,
                    fractional: false,
                    supports_search: false,
                    supports_group_request: false,
                    supported_resolutions: ['1'.'15'.'60'.'1D'],
                    supports_marks: false,
                    supports_timescale_marks: false,
                    supports_time: true,
                    has_intraday: true,
                    intraday_multipliers: ['1'.'15'.'60'.'1D'],}return object
            }
        }
        
        export default datafeeds
    
    Copy the code
  • The dataUpdater adds the following code

    class dataUpdater {
        constructor(datafeeds) {
            this.subscribers = {}
            this.requestsPending = 0
            this.historyProvider = datafeeds
        }
        subscribeBars(symbolInfonwq, resolutionInfo, newDataCallback, listenerGuid) {
            this.subscribers[listenerGuid] = {
                lastBarTime: null,
                listener: newDataCallback,
                resolution: resolutionInfo,
                symbolInfo: symbolInfonwq
            }
        }
        unsubscribeBars(listenerGuid) {
            delete this.subscribers[listenerGuid]
        }
        updateData() {
            if (this.requestsPending) return
            this.requestsPending = 0
            for (let listenerGuid in this.subscribers) {
                this.requestsPending++
                this.updateDataForSubscriber(listenerGuid).then(() => this.requestsPending--).catch(() => this.requestsPending--)
            }
        }
        updateDataForSubscriber(listenerGuid) {
            return new Promise(function (resolve, reject) {
              var subscriptionRecord = this.subscribers[listenerGuid];
              var rangeEndTime = parseInt((Date.now() / 1000).toString());
              var rangeStartTime = rangeEndTime - this.periodLengthSeconds(subscriptionRecord.resolution, 10);
              this.historyProvider.getBars(subscriptionRecord.symbolInfo, subscriptionRecord.resolution, rangeStartTime, rangeEndTime, function (bars) {
                this.onSubscriberDataReceived(listenerGuid, bars);
                resolve();
              }, function () {
                reject();
              });
            });
        }
        onSubscriberDataReceived(listenerGuid, bars) {
            if(! this.subscribers.hasOwnProperty(listenerGuid))return
            if(! bars.length)return
            const lastBar = bars[bars.length - 1]
            const subscriptionRecord = this.subscribers[listenerGuid]
            if(subscriptionRecord.lastBarTime ! == null && lastBar.time < subscriptionRecord.lastBarTime)returnconst isNewBar = subscriptionRecord.lastBarTime ! == null && lastBar.time > subscriptionRecord.lastBarTimeif (isNewBar) {
                if (bars.length < 2) {
                    throw new Error('Not enough bars in history for proper pulse update. Need at least 2.');
                }
                const previousBar = bars[bars.length - 2]
                subscriptionRecord.listener(previousBar)
            }
            subscriptionRecord.lastBarTime = lastBar.time
            console.log(lastBar)
            subscriptionRecord.listener(lastBar)
        }
        periodLengthSeconds =(resolution, requiredPeriodsCount) => {
            let daysCount = 0
            if (resolution === 'D' || resolution === '1D') {
                daysCount = requiredPeriodsCount
            } else if (resolution === 'M' || resolution === '1M') {
                daysCount = 31 * requiredPeriodsCount
            } else if (resolution === 'W' || resolution === '1W') {
                daysCount = 7 * requiredPeriodsCount
            } else {
                daysCount = requiredPeriodsCount * parseInt(resolution) / (24 * 60)
            }
            return daysCount * 24 * 60 * 60
        }
    }
    export default dataUpdater
    
    Copy the code
  • Socket.js adds the following code (you can also use your own websocket module)

        class socket {
            constructor(options) {
                this.heartBeatTimer = null
                this.options = options
                this.messageMap = {}
                this.connState = 0
                this.socket = null
            }
            doOpen() {
                if (this.connState) return
                this.connState = 1
                this.afterOpenEmit = []
                const BrowserWebSocket = window.WebSocket || window.MozWebSocket
                const socketArg = new BrowserWebSocket(this.url)
                socketArg.binaryType = 'arraybuffer'
                socketArg.onopen = evt => this.onOpen(evt)
                socketArg.onclose = evt => this.onClose(evt)
                socketArg.onmessage = evt => this.onMessage(evt.data)
                // socketArg.onerror = err => this.onError(err)
                this.socket = socketArg
            }
            onOpen() {
                this.connState = 2
                this.heartBeatTimer = setInterval(this.checkHeartbeat.bind(this), 20000)
                this.onReceiver({ Event: 'open'})}checkOpen() {
                return this.connState === 2
            }
            onClose() {
                this.connState = 0
                if (this.connState) {
                    this.onReceiver({ Event: 'close' })
                }
            }
            send(data) {
                this.socket.send(JSON.stringify(data))
            }
            emit(data) {
                return new Promise((resolve) => {
                    this.socket.send(JSON.stringify(data))
                    this.on('message'. (dataArray) => { resolve(dataArray) }) }) } onMessage(message) { try { const data = JSON.parse(message) this.onReceiver({ Event:'message', Data: data })
                } catch (err) {
                    // console.error(' >> Data parsing error:', err)
                }
            }
            checkHeartbeat() {
                const data = {
                    cmd: 'ping',
                    args: [Date.parse(new Date())]
                }
                this.send(data)
            }
            onReceiver(data) {
                const callback = this.messageMap[data.Event]
                if (callback) callback(data.Data)
            }
            on(name, handler) {
                this.messageMap[name] = handler
            }
            doClose() {
                this.socket.close()
            }
            destroy() {
                if(this.heartBeatTimer) { clearInterval(this.heartBeatTimer) this.heartBeatTimer = null } this.doClose() this.messageMap =  {} this.connState = 0 this.socket = null } }export default socket
    Copy the code

Initialization chart

  • Websocket data can be requested simultaneously.
  • Create a new init function and call onready/mounted/mounted etc.
  • init = () => { var resolution = this.interval; Var chartType = (localStorage.getItem('tradingview.chartType') | |'1') * 1; var locale = this.props.lang; Var skin = this.props. Theme; // Current skin (black/white)if(! This.widgets) {this.widgets = new tradingView.widget ({// create chart autosize:trueSymbol :this.symbolName, // Interval: resolution, container_id:'tv_chart_container'// container ID datafeed: this.datafeeds, // configuration, i.e. datafees. Js file in API folder library_path:'/static/TradingView/charting_library/'Enabled_features: [enabled_features: ['left_toolbar'],
                timezone: 'Asia/Shanghai'Timezone (UTC+8) // Timezone:'Etc/UTC', // Time zone is (UTC+0) custom_CSS_URL:'./css/tradingview_'+skin+'.css'// debug:false, disabled_features: [// Disable functions by default'edit_buttons_in_legend'.'timeframes_toolbar'.'go_to_date'.'volume_force_overlay'.'header_symbol_search'.'header_undo_redo'.'caption_button_text_if_possible'.'header_resolutions'.'header_interval_dialog_button'.'show_interval_dialog_on_key_press'.'header_compare'.'header_screenshot'.'header_saveload'], overrides: this.getoverrides (skin), // customized skin, default no cover default skin This.getstudiesoverrides (skin) // Custom skin}) var thats = this.widgets; // Trigger when diagram content is ready thats. OnChartReady (function() {
                createButton(buttons);
            })
            var buttons = [
                {title:'1m',resolution:'1',chartType:1},
                {title:'15m',resolution:'15',chartType:1},
                {title:'1h',resolution:'60',chartType:1},
                {title:'1D',resolution:'1D',chartType:1}, ]; // Create the button (in this case the time dimension) and style the selected buttonfunction createButton(buttons){
                for(var i = 0; i < buttons.length; i++){
                    (function(button){
                        let defaultClass =
                        thats.createButton()
                        .attr('title', button.title).addClass(`mydate ${button.resolution === '15' ? 'active' : ''}`)
                        .text(button.title)
                        .on('click'.function(e) {
                            if (this.className.indexOf('active')> -1){// Already selectedreturn false
                            }
                            let curent =e.currentTarget.parentNode.parentElement.childNodes
                            for(let index of curent) {
                                if (index.className.indexOf('my-group')> -1 && index.childNodes[0].className.indexOf('active')> -1) {
                                    index.childNodes[0].className = index.childNodes[0].className.replace('active'.' ')
                                }
                            }
                            this.className = `${this.className} active`
                            thats.chart().setResolution(button.resolution, function onReadyCallback() {})
                        }).parent().addClass('my-group'+(button.resolution == paramary.resolution ? ' active':' '))
                    })(buttons[i])
                }
            }
        }
    }
    Copy the code

The request data

  • Create a new initMessage function – call initMessage when you need to fetch data.
  •     initMessage = (symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback) => {
            letThat = this // Preserve the current callback that.cacheData['onLoadedCallback'] = onLoadedCallback; // Get the number of data to requestlet limitInitLimit (resolution, rangeStartDate, rangeEndDate) // If the current time node has changed, stop the subscription to the previous time node and change the time node valueif(that.interval ! == resolution){ that.interval = resolution paramary.endTime = parseInt((Date.now() / 1000), 10) }else{paramary. EndTime = rangeEndDate} // Get the data of the current time period and execute the onLoadedCallback paramary. Limit = in onMessagelimit
            paramary.resolution = resolution
            letParam // Load history in batchesif{param (isHistory. IsRequestHistory) = {/ / get the parameters of the historical records (with all the major difference is the timestamp)}}else} this.getklinelist(param) {getklinelist(param)}Copy the code
  • When the historical data is requested, the background interface is always requested because the conditions are not metFunction of the throttle
    • There are throttling methods in the Lodash library
    • First introduce the throttling function —-import throttle from 'lodash/throttle'
    • It’s as simple as putting —– in front of the functionthis.initMessage = throttle(this.initMessage, 1000);
      • In throttle(), the first parameter is the function to be throttled and the second parameter is the throttling time.

Data received. Render the chart

  • Can be called from where the data is receivedsocket.on('message', this.onMessage(res.data))
  • The onMessage function is used to render data into the chart content
  • // Render data onMessage = (data) => {// Pass data in with argumentslet thats = this
        if (data === []) {
            return} // The reason for introducing new data is that I want to add caching, so that switching the time dimension can greatly optimize the request time when there is a large amount of datalet newdata = []
        if(data && data.data) {
            newdata = data.data
        }
        const ticker = `${thats.symbolName}-${thats.interval}'// First full update (incremental data is pushed one by one, wait for full data to be requested)if(newdata && newdata.length >= 1 && ! thats.cacheData[ticker] && data.firstHisFlag ==='true'Var tickerState = '; // The value returned by websocket${ticker}State '// If no data is cached, populate directly and initiate a subscriptionif(! CacheData [ticker]){thats. CacheData [ticker] = newData thats. Subscribe () // go here to subscribe incremental data !!!!!!! } // onLoadedCallback is undefined if historical data is missingif(thats.cacheData['onLoadedCallback']){ // ToDo
                thats.cacheData['onLoadedCallback'](newData)} // Request completed, set the status tofalse
            thats.cacheData[tickerstate] = false// Record the current cache time, CacheData [ticker][thats.cacheData[ticker].leng-1].time}} I'll explain later)if(newdata && newdata.length > 1 && data.firstHisFlag === 'true' && paramary.klineId === data.klineId && paramary.resolution === data.resolution && thats.cacheData[ticker] && isHistory.isRequestHistory) {
            thats.cacheData[ticker] = newdata.concat(thats.cacheData[ticker])
            isHistory.isRequestHistory = false} // Single data ()if (newdata && newdata.length === 1 && data.hasOwnProperty('firstHisFlag') = = =falseKlineId === paramary. KlineId && paramary. Resolution === data.resolution) {// Construct incremental update dataletBarsData = newData [0] // If the incremental update time is longer than the cache time and there is data in the cache, the data length is greater than 0if(barsdata.time > thats.lastTime && thats.cacheData[ticker] && thats.cacheData[ticker].length) {// Add incremental updated data directly to the cache array Thats.cacheData[ticker].push(barsData) // Modify cache time thats.lastTime = barsdata.time}else if(barsdata.time == thats.lastTime && thats.cacheData[ticker] && thats.cacheData[ticker].length){// If the increment update time equals the cache time, CacheData [ticker][thats. CacheData [ticker].leng-1] = barsData} // Can start incremental updating apply colours to a drawing. Thats datafeeds. BarsUpdater. The updateData ()}}Copy the code

Logical center ===>getbars

  • Create a new getbars function (which is called automatically when the graph changes)
  • getBars = (symbolInfo, resolution, rangeStartDate, rangeEndDate, OnLoadedCallback) => {const timeInterval = resolution // This. Interval = resolutionlet ticker = `${this.symbolName}-${resolution}`
            let tickerload = `${ticker}load`
            var tickerstate = `${ticker}State 'this.cacheData[ticKerLoad] = rangeStartDate // If there is no data in the cache and no request is made, record the current node start time // Switch time or currencyif(! this.cacheData[ticker] && ! This.cachedata [ticKerState]){this.cacheData[ticKerLoad] = rangeStartDate; InitMessage (symbolInfo, Resolution, rangeStartDate, rangeEndDate, onLoadedCallback) // Set status totrue
                this.cacheData[tickerstate] = true
            }
            if(! Enclosing cacheData [tickerload] | | this. CacheData [tickerload] > rangeStartDate) {/ / if there is a data cache, but no data of the current period, This. cacheData[ticKerLoad] = rangeStartDate; This. initMessage(symbolInfo, Resolution, rangeStartDate, rangeEndDate, onLoadedCallback); // Set the status totruethis.cacheData[tickerstate] = ! 0; } // Getting data from websocket, disable all operationsif(this.cacheData[tickerstate]){
                return false} // Get the historical data and update the chartif (this.cacheData[ticker] && this.cacheData[ticker].length > 1) {
                this.isLoading = false
                onLoadedCallback(this.cacheData[ticker])
            } else {
                let self = this
                this.getBarTimer = setTimeout(function() {self.getBars(symbolInfo, Resolution, rangeStartDate, rangeEndDate, onLoadedCallback)}, 10)} Draw a circle ---- to slide forward and request historical data in stages to reduce the pressureif(this.cacheData[ticker] && this.cacheData[ticker].length > 1 && this.widgets && this.widgets._ready && ! isHistory.isRequestHistory && timeInterval ! = ='1D') {const rangeTime = this.widgets. Chart ().getVisiblerange () {from, const rangeTime = this.widgets. Chart ().getVisiblerange (); To} const dataTime = this.cacheData[ticker][0]. Time // Returns the first time for the dataif (rangeTime.from * 1000 <= dataTime + 28800000) { // trueDon't have to requestfalseNeed to request the following isHistory. EndTime = dataTime / 1000 isHistory. IsRequestHistory =trueThis. initMessage(symbolInfo, Resolution, rangeStartDate, rangeEndDate, onLoadedCallback)}}Copy the code

reporter

  • Tradingview is basically a combination of these functions.
  • useonLoadedCallback(this.cacheData[ticker])orthis.datafeeds.barsUpdater.updateData()To update the data.
  • Slide loading allows you to load 200 strips first and then 150 strips at a time, greatly reducing the amount of data and rendering time.
  • Throttling is often used for slide loading.

Advanced websocket

  • Binary transmission data

  • Websocket transmits data in plaintext, and generally has a large amount of data like historical data on K line. For security and faster loading of charts, we decided to transfer data in binary mode.

    • You can extract binary data by using pako.js
    • The introduction of pako. Jsyarn add pako -S
    • Method of use
      ifConst Blob = res.data const reader = new FileReader() Reader. ReadAsBinaryString (blob) reader. The onload () = = > {/ / to pako decompression of the results, first type is a string, Parse (pako.inflate(reader.result, {to:'string'}}}))Copy the code
    • After the conversion, the data size was reduced by about 20%.

About the same


Write in the last

  • Just a few simple things to share here, see the native JS version of Demo github.com/tenggouwa/t for details…
  • If you have questions about scrolling and binary content, leave a comment.
  • If this article is helpful to you, or let you know something about TradingView, please leave a comment or like, I will reply one by one.
  • The author’s greatest hope is that you can get something from my article, I will be very happy…
  • In the future, update an article at least once a month. You can’t get lost by liking it, buddy.