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