предисловие
Недавно я работал над графиком К-линии в биржевом проекте и приобрел некоторый опыт, которым могу поделиться с вами.
Большая часть кода, предупреждение о трафике! ! ! !
Мол, любимый, не теряйся.
Технический отбор
-
echarts
- На основе рисунка на холсте полный спектр библиотеки визуальных диаграмм.
- Официальный адрес:echarts.baidu.com/
-
highcharts
- На основе рисования svg есть много настраиваемого контента, и dom удобнее манипулировать.
- Официальный адрес:www.highcharts.com.cn/
-
tradingview
- Профессиональная диаграмма тенденций глобализации на основе холста.
- Официальный адрес:cn.tradingview.com/
-
Преимущества и недостатки
- hightcharts: несколько дней назад внимательно изучил хайчартыstupa.com/option/.Выяснилось, что операции dom в svg и настроенном контенте реализованы лучше, но эта функция, которую почти необходимо реализовывать вручную, уступила под давлением коротких циклов разработки. Вышеупомянутый проект также выполнялся в течение трех месяцев после медленного изучения, но он все еще очень полезен.
- echarts: Официальных кейсов эчартов много, и они часто используются в некоторых системах фонового управления для отображения данных. Он удобен, прост в использовании и имеет достаточное количество пользователей. Курица поисковой системы может решить любые ваши проблемы. Но для некоторых линий на карте и других операций он кажется слабоватым. Недостаточно для удовлетворения спроса.
- tradingview: Пока вы заходите на официальный сайт, вы можете убедиться в его профессионализме. Он полностью создан для профессиональной торговли. Вам нужно только заполнить данные. Вы даже можете использовать собственный push-уведомление о данных tradeview для некоторых часто используемых торговых материалов.
-
Примечания
- Поэтому профессиональные торговые графики передаются в профессиональные библиотеки.
- Ручная голова собаки~~~~(∩_∩)
Готов к работе
- Подать заявку на учетную запись (ключ)
- После регистрации на официальном сайте придет напоминание по электронной почте, просто следуйте ему шаг за шагом, поэтому я не буду здесь вдаваться в подробности.
- Строительство окружающей среды
- Я использую скаффолдинг React+webpack4, созданный мной, вы также можете использовать нативный JS или любой фреймворк, который вам нравится (код, опубликованный позже, находится в среде React).
- Загрузите кодовую базу с официального
- Понимание протокола связи через веб-сокет
- послать запрос
- Получать данные
- контур
- Здесь прилагается документ по развитию TradingView для Китая.Нет. Торговля любовью. Карма/книги/ Если Ади…
- и пример апиторговый вид.GitHub.IO/features ETS…(Вам нужно принести свою лестницу сюда)
- Демонстрация великого богаGitHub.com/Pain Dog Baby/Он…
Готов начать
Создайте
-
page |--kLine // k线内容文件夹 |--|--api // 需要使用的方法 |--|--|--datafees.js // 定义了一些公用方法 |--|--|--dataUpdater.js // 更新时调用的内容 |--|--|--socket.js // websocket方法 |--|--index.js // 自己代码开发 |--|--index.scss // 样式开发 -
datafees.js добавьте следующий код
/** * JS API */ import React from 'react' import DataUpdater from './dataUpdater' class datafeeds extends React.Component { /** * JS API * @param {*Object} react react实例 */ constructor(self) { super(self) this.self = self this.barsUpdater = new DataUpdater(this) this.defaultConfiguration = this.defaultConfiguration.bind(this) } /** * @param {*Function} callback 回调函数 * `onReady` should return 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 时间戳、最左边请求的K线时间 * @param {*Number} rangeEndDate 时间戳、最右边请求的K线时间 * @param {*Function} onDataCallback 回调函数 * @param {*Function} onErrorCallback 回调函数 */ 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 成功回调 * @param {*Function} onResolveErrorCallback 失败回调 * `resolveSymbol` should return 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)) } /** * 订阅K线数据。图表库将调用onRealtimeCallback方法以更新实时数据 * @param {*Object} symbolInfo 商品信息 * @param {*String} resolution 分辨率 * @param {*Function} onRealtimeCallback 回调函数 * @param {*String} subscriberUID 监听的唯一标识符 * @param {*Function} onResetCacheNeededCallback (从1.7开始): 将在bars数据发生变化时执行 */ subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) { this.barsUpdater.subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) } /** * 取消订阅K线数据 * @param {*String} subscriberUID 监听的唯一标识符 */ 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
Инициализировать диаграмму
- Данные веб-сокета могут быть запрошены одновременно.
- Создадим новую функцию 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, что значительно уменьшает объем данных и сокращает время рендеринга.
- Часто используется дросселирование во время загрузки слайдов.
Расширенный веб-сокет
-
двоичные данные передачи
-
Когда веб-сокет передает данные, они передаются в виде простого текста, и, как и исторические данные на линии K, общий объем данных относительно велик. Для безопасности и более быстрой загрузки графиков мы решили использовать бинарный режим для передачи данных.
- Двоичные данные можно распаковать с помощью pako.js.
- импорт pako.js
yarn 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.GitHub.com/Pain Dog Baby/Он…
- Если у вас есть какие-либо вопросы о загрузке прокрутки и двоичном содержимом, вы можете оставить комментарий.
- Если эта статья полезна для вас или дает вам некоторое представление о TradingView, пожалуйста, оставьте сообщение или лайк, и я отвечу один за другим.
- Самая большая надежда автора состоит в том, что вы сможете что-то почерпнуть из моей статьи, и я очень этому рад. . .
- Позже как минимум одна статья обновляется каждый месяц. Ставь лайк и подписывайся, не теряйся, старое железо.