摘要:進階二進制傳輸數據在傳輸數據的時候是明文傳輸,而且像線上的歷史數據,一般數據量比較大。為了安全性以及更快的加載出圖表,我們決定使用二進制的方式傳輸數據。
前言
最近在做交易所項目里的K線圖,得些經驗,與大家分享。
代碼居多,流量預警!!!!
點贊 收藏 不迷路。
echrats
基于canvas繪制,種類齊全的可視化圖表庫。
官方地址: https://echarts.baidu.com/
highcharts
基于svg繪制,可定制化內容很多,操縱dom更方便。
官方地址: https://www.highcharts.com.cn/
tradingview
基于canvas的專業全球化走勢圖表。
官方地址: https://cn.tradingview.com/
優缺點
hightcharts: 前些日子有仔細研究過 hightcharts https://www.fota.com/option。 發現svg中的dom操作,以及定制化內容更好實現,但幾乎都需要手動實現的這個特性在開發周期短的壓迫下屈服了。上面的這個項目在慢慢摸索下也做了小三個月的樣子,但還是很有成就感的。
echrats: echarts的官方案例很多,經常在做一些后臺管理系統,展現數據時候會用到,方便,易用,使用者也足夠多,搜索引擎雞本能夠解決你的任何問題。但對一些在圖上劃線,等操作,就顯得略微疲軟。不夠能滿足需求。
tradingview: 只要進入官網,就可見其專業性,他完全就是為了專業交易兒打造的,您只需要想里面填充數據就可以了,甚至在一些常用的交易內容上,可以使用tradingview自己的數據推送。
小記
所以,專業的交易圖表,就交給專業的庫來做吧
手動狗頭~~~~(∩_∩)
準備工作
申請賬號(key)
在官網注冊后會有郵件提示的,一步一步跟著做就可以了,這里就不做贅述了。
環境搭建
我使用的是自己搭建的React+webpack4腳手架,你也可以使用原生JS,或者你喜歡的任何框架(后面貼出來的代碼都是在React環境下的)。
從官方下載代碼庫
了解websocket通訊協議
發送請求
接收數據
大綱
這里附上tradingview中文開發文檔 https://b.aitrade.ga/books/tr...
以及api示例 http://tradingview.github.io/... (此處需要自備梯子)
一位大神的Demo https://github.com/tenggouwa/...
準備開始吧
page |--kLine // k線內容文件夾 |--|--api // 需要使用的方法 |--|--|--datafees.js // 定義了一些公用方法 |--|--|--dataUpdater.js // 更新時調用的內容 |--|--|--socket.js // websocket方法 |--|--index.js // 自己代碼開發 |--|--index.scss // 樣式開發
datafees.js加入如下代碼
import React from "react" import DataUpdater from "./dataUpdater" class datafeeds extends React.Component { constructor(self) { super(self) this.self = self this.barsUpdater = new DataUpdater(this) this.defaultConfiguration = this.defaultConfiguration.bind(this) } 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)) } 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) } 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)) } subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) { this.barsUpdater.subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) } unsubscribeBars(subscriberUID) { this.barsUpdater.unsubscribeBars(subscriberUID) } 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
dataUpdater加入如下代碼
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) return const isNewBar = subscriptionRecord.lastBarTime !== null && lastBar.time > subscriptionRecord.lastBarTime if (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
socket.js加入如下代碼(也可以使用自己的websocket模塊)
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初始化圖表
可以同時請求websocket數據。
新建init函數,并在onready/mounted/mounted等時候去調用(代碼的含義在注釋里,我盡量寫的詳細一點)
+
init = () => { var resolution = this.interval; // interval/resolution 當前時間維度 var chartType = (localStorage.getItem("tradingview.chartType") || "1")*1; var locale = this.props.lang; // 當前語言 var skin = this.props.theme; // 當前皮膚(黑/白) if (!this.widgets) { this.widgets = new TradingView.widget({ // 創建圖表 autosize: true, // 自動大小(適配,寬高百分百) symbol:this.symbolName, // 商品名稱 interval: resolution, container_id: "tv_chart_container", // 容器ID datafeed: this.datafeeds, // 配置,即api文件夾下的datafees.js文件 library_path: "/static/TradingView/charting_library/", // 圖表庫的位置,我這邊放在了static,因為已經壓縮過 enabled_features: ["left_toolbar"], timezone: "Asia/Shanghai", // 圖表的內置時區(常用UTC+8) // timezone: "Etc/UTC", // 時區為(UTC+0) custom_css_url: "./css/tradingview_"+skin+".css", //樣式位置 locale, // 語言 debug: false, disabled_features: [ // 在默認情況下禁用的功能 "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), // 定制皮膚,默認無蓋默認皮膚 studies_overrides: this.getStudiesOverrides(skin) // 定制皮膚,默認無蓋默認皮膚 }) var thats = this.widgets; // 當圖表內容準備就緒時觸發 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}, ]; // 創建按鈕(這里是時間維度),并對選中的按鈕加上樣式 function 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){// 已經選中 return 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]) } } } }請求數據
新建initMessage函數---在需要去獲取數據的時候,調取initMessage。
initMessage = (symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback) => { let that = this //保留當前回調 that.cacheData["onLoadedCallback"] = onLoadedCallback; //獲取需要請求的數據數目 let limit = that.initLimit(resolution, rangeStartDate, rangeEndDate) //如果當前時間節點已經改變,停止上一個時間節點的訂閱,修改時間節點值 if(that.interval !== resolution){ that.interval = resolution paramary.endTime = parseInt((Date.now() / 1000), 10) } else { paramary.endTime = rangeEndDate } //獲取當前時間段的數據,在onMessage中執行回調onLoadedCallback paramary.limit = limit paramary.resolution = resolution let param // 分批次獲取歷史(這邊區分了歷史記錄分批加載的請求) if (isHistory.isRequestHistory) { param = { // 獲取歷史記錄時的參數(與全部主要區別是時間戳) } } else { param = { // 獲取全部記錄時的參數 } } this.getklinelist(param) }
在請求歷史數據時,由于條件不滿足,會一直請求后臺接口,所以需要加上 函數節流
在lodash這個庫里面是有節流的方法的
首先引入節流函數----import throttle from "lodash/throttle"
使用非常簡單,只要在函數前面套一層-----this.initMessage = throttle(this.initMessage, 1000);
throttle()函數里面,第一個參數是需要截留的函數,第二個為節流時間。
收到數據,渲染圖表可以在接收數據的地方調用socket.on("message", this.onMessage(res.data))
onMessage函數,是為渲染數據進入圖表內容
// 渲染數據 onMessage = (data) => { // 通過參數將數據傳遞進來 let thats = this if (data === []) { return } // 引入新數據的原因,是我想要加入緩存,這樣在大數據量的時候,切換時間維度可以大大的優化請求時間 let newdata = [] if(data && data.data) { newdata = data.data } const ticker = `${thats.symbolName}-${thats.interval}` // 第一次全部更新(增量數據是一條一條推送,等待全部數據拿到后再請求) if (newdata && newdata.length >= 1 && !thats.cacheData[ticker] && data.firstHisFlag === "true") { // websocket返回的值,數組代表時間段歷史數據,不是增量 var tickerstate = `${ticker}state` // 如果沒有緩存數據,則直接填充,發起訂閱 if(!thats.cacheData[ticker]){ thats.cacheData[ticker] = newdata thats.subscribe() // 這里去訂閱增量數據!!!!!!! } // 新數據即當前時間段需要的數據,直接喂給圖表插件 // 如果出現歷史數據不見的時候,就說明 onLoadedCallback 是undefined if(thats.cacheData["onLoadedCallback"]){ // ToDo thats.cacheData["onLoadedCallback"](newdata) } //請求完成,設置狀態為false thats.cacheData[tickerstate] = false //記錄當前緩存時間,即數組最后一位的時間 thats.lastTime = thats.cacheData[ticker][thats.cacheData[ticker].length - 1].time } // 更新歷史數據 (這邊是添加了滑動按需加載,后面我會說明) 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 } // 單條數據() if (newdata && newdata.length === 1 && data.hasOwnProperty("firstHisFlag") === false && data.klineId === paramary.klineId && paramary.resolution === data.resolution) { //構造增量更新數據 let barsData = newdata[0] //如果增量更新數據的時間大于緩存時間,而且緩存有數據,數據長度大于0 if (barsData.time > thats.lastTime && thats.cacheData[ticker] && thats.cacheData[ticker].length) { //增量更新的數據直接加入緩存數組 thats.cacheData[ticker].push(barsData) //修改緩存時間 thats.lastTime = barsData.time } else if(barsData.time == thats.lastTime && thats.cacheData[ticker] && thats.cacheData[ticker].length){ //如果增量更新的時間等于緩存時間,即在當前時間顆粒內產生了新數據,更新當前數據 thats.cacheData[ticker][thats.cacheData[ticker].length - 1] = barsData } // 通知圖表插件,可以開始增量更新的渲染了 thats.datafeeds.barsUpdater.updateData() } }邏輯中心===>getbars
新建getbars函數(該函數會在圖表有變化時自動調用)
getBars = (symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback) => { const timeInterval = resolution // 當前時間維度 this.interval = resolution let ticker = `${this.symbolName}-${resolution}` let tickerload = `${ticker}load` var tickerstate = `${ticker}state` this.cacheData[tickerload] = rangeStartDate //如果緩存沒有數據,而且未發出請求,記錄當前節點開始時間 // 切換時間或幣種 if(!this.cacheData[ticker] && !this.cacheData[tickerstate]){ this.cacheData[tickerload] = rangeStartDate //發起請求,從websocket獲取當前時間段的數據 this.initMessage(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback) //設置狀態為true this.cacheData[tickerstate] = true } if(!this.cacheData[tickerload] || this.cacheData[tickerload] > rangeStartDate){ //如果緩存有數據,但是沒有當前時間段的數據,更新當前節點時間 this.cacheData[tickerload] = rangeStartDate; //發起請求,從websocket獲取當前時間段的數據 this.initMessage(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback); //設置狀態為true this.cacheData[tickerstate] = !0; } //正在從websocket獲取數據,禁止一切操作 if(this.cacheData[tickerstate]){ return false } // 拿到歷史數據,更新圖表 if (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) } // 這里很重要,畫圈圈----實現了往前滑動,分次請求歷史數據,減小壓力 // 根據可視窗口區域最左側的時間節點與歷史數據第一個點的時間比較判斷,是否需要請求歷史數據 if (this.cacheData[ticker] && this.cacheData[ticker].length > 1 && this.widgets && this.widgets._ready && !isHistory.isRequestHistory && timeInterval !== "1D") { const rangeTime = this.widgets.chart().getVisibleRange() // 可視區域時間值(秒) {from, to} const dataTime = this.cacheData[ticker][0].time // 返回數據第一條時間 if (rangeTime.from * 1000 <= dataTime + 28800000) { // true 不用請求 false 需要請求后續 isHistory.endTime = dataTime / 1000 isHistory.isRequestHistory = true // 發起歷史數據的請求 this.initMessage(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback) } } }小記
tradingview主要就是這幾個函數之間的搭配。
使用onLoadedCallback(this.cacheData[ticker])或者this.datafeeds.barsUpdater.updateData()去更新數據。
滑動加載時,可以先加載200條,后面每次150條,這樣大大縮小了數據量,增加了渲染時間。
滑動加載時的節流會經常用到。
進階websocket二進制傳輸數據
websocket在傳輸數據的時候是明文傳輸,而且像K線上的歷史數據,一般數據量比較大。為了安全性以及更快的加載出圖表,我們決定使用二進制的方式傳輸數據。
可以通過使用pako.js解壓二進制數據
引入pako.jsyarn add pako -S
使用方法
if (res.data instanceof Blob) { // 看下收到的數據是不是Blob對象 const blob = res.data // 讀取二進制文件 const reader = new FileReader() reader.readAsBinaryString(blob) reader.onload = () => { // 首先對結果進行pako解壓縮,類型是string,再轉換成對象 data = JSON.parse(pako.inflate(reader.result, { to: "string" })) } }
轉換后,數據大小大概減少了20%。
差不多了
這里只分享些簡單的內容,細節可以參照原生js版本的Demo https://github.com/tenggouwa/...
關于滾動加載,以及二進制的內容有問題的可以評論留言。
如果這篇文章對你有幫助,或者是讓您對tradingview有些了解,歡迎留言或點贊,我會一一回復。
筆者最大的希望就是您能從我的文章里獲得點什么,我就很開心啦。。。
后面,至少每個月更新一篇文章。點贊關注不迷路啊,老鐵。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/54168.html
摘要:進階二進制傳輸數據在傳輸數據的時候是明文傳輸,而且像線上的歷史數據,一般數據量比較大。為了安全性以及更快的加載出圖表,我們決定使用二進制的方式傳輸數據。 前言 最近在做交易所項目里的K線圖,得些經驗,與大家分享。 代碼居多,流量預警!!!! 點贊 收藏 不迷路。 技術選型 echrats showImg(https://segmentfault.com/img/remote/14...
摘要:圖表庫希望通過僅一次調用,接收所有的請求歷史。更新后臺返回線最新的數據網上比較少關于引入的文章小弟不才粗略的分享一下我的實現方法 **前言: 本文使用的是1.10版本 , 可通過TradingView.version()查看當前版本. 附上開發文檔地址:https://zlq4863947.gitbooks.i...** 一、修改datafeed.js為export導出,并在vue文件...
摘要:今年七月入坑,中間斷斷續續做了別的項目,因為沒有完全掌握這個插件,所以一直沒有嵌入項目。引入圖表上有可以下載,支持多種語言,但是缺少關鍵的核心庫,這個需要到官網申請獲得。官方使用的數據獲取方式為獲取,數據接口是官方提供的。 今年七月入坑,中間斷斷續續做了別的項目,因為沒有完全掌握這個插件,所以一直沒有嵌入項目。現在已經四個月過去了,迭代工作沒那么忙,是時候整合tradingview到項...
摘要:用戶量量大,數據量大,而且要求實時更新數據的時候,需要使用。該方法接收的有兩種,一種是數組。是歷史數據,時間段的數據,根據時間顆粒來劃分。 1、websocket 用戶量量大,數據量大,而且要求實時更新數據的時候,需要使用websocket。tradingview正好就是這樣的應用場景。 2、tradingview和websocket結合 getBars方法。tradingview圖...
摘要:無奈,還是需要對這份代碼進行加工。功能缺少,主要指業務邏輯實現上的功能缺少。缺少的功能主要是歷史記錄獲取展示的功能。查詢緩存是否為空,如果為空,表示數據還沒有下發,后再查詢一次。如果有數據,取到當前數據,執行回調。 前幾天寫了一篇關于tradingView和webSocket的文章傳送門,因為代碼本身還在整合中,所以比較混亂,而且也沒有demo可以運行。這兩天在GitHub上面看到了一...
閱讀 1203·2021-11-17 09:33
閱讀 3599·2021-09-28 09:42
閱讀 3326·2021-09-13 10:35
閱讀 2478·2021-09-06 15:00
閱讀 2438·2021-08-27 13:12
閱讀 3609·2021-07-26 23:38
閱讀 1826·2019-08-30 15:55
閱讀 539·2019-08-30 15:53