Подключение и настройка графиков TradingView
- понедельник, 21 сентября 2020 г. в 00:32:23
Если Вы — фрилансер или CTO финансового проекта, рано или позно Вы столкнетесь с вопросом подключения графиков, я сэкономлю Вам минимум сутки работы. Те, кто уже используют эту библиотеку, возможно, найдут что-то новое.
Статья будет в формате "книги рецептов" с open source решениями для криптовалютной биржи Binance и Forex.
Привет, Хабр!
У библиотеки TradingView (charting_library) высокий порог входа, при этом менее популярной она не стала из-за того, что используется на одноименном сервисе TradingView.com. Решил сделать "книгу рецептов" с ответами на основные вопросы.
Эта статья продолжение поста: "Финансовые графики для вашего приложения".
Контент буду дополнять по мере появляния новых сложностей. Если у Вас есть вопросы и Вы не нашли ответы в статье, пишите в комментариях, будем разбираться вместе :)
В статье буду указывать ссылки на документацию. Если, при переходе по ссылке, у Вас открывается 404 страница, это означает, что у Вас нет доступа.
Можно использовать бесплатно в коммерческих и некоммерческих целях. Самый главный критерий — сохранность логотипа компании на графиках.
При запросе доступа к графикам обязательно указывать конечный домен, где они будут использоваться. При реализации одного из проектов мы подключали Forex-дату к графику, все настроили и запустили. За 2-й месяц заказчик данные Forex не проплатил, из-за этого графики полностью не загружались и отсутствовал логотип. После сложившейся ситуации проверяющий связался с заказчиком последством эл.почты с вопросами для разъяснения ситуации.
У библиотеки закрытый доступ на GitHub, чтобы его получить необходимо:
Спустя примерно 4 недели после заполнения заявки мне прислали договор для подписания. Через 3 дня после подписания открыли доступ к библиотеке. Судя по отзывам, период получения доступа плавает и точных сроков нет.
Для решения нужно подключить виджет и глобально указать доступ к бибилиотеке.
// для Nodejs
import { widget } from '../public/charting_library/charting_library.min'
const widget = new widget({ <options> })
Указать глобальный путь к папке charting_library в опциях виджета library_path: '/charting_library/'
Глобальный путь будет отличаться от используемых модулей. В моем случае используется Vuejs с указанием в vue.config.js => publicPath: '/'
. Структура папок: /public/index.html
, /public/charting_library/
и настройки виджета, которые указаны выше.
В базовом варианте используются тестовые данные. Далее необходимо подключить свой провайдер данных, используя одно из двух решений: JS API или UDF. Напрямую "скормить" массив данных не получится. Мы расмотрим JSAPI, UDF подключается аналогично, с отличием в указании конечной точки на сервере, откуда будет получать данные.
Основное отличие JSAPI от UDF, в отсутствии возможности для UDF добавить WebSocket подключение. При указании конечной точки на сервере, вы выставляете интервал для каждого запроса: datafeed: new Datafeeds.UDFCompatibleDatafeed('http://localhost:3000/datafeed', 1000)
Чтобы настроить адаптер, нужно понимать, что каждый хук выполняется последовательно и для отладки лучше добавить вывод в консоль информации о запуске хука console.log('[<название хука>]: Method call')
.
Последовательность запуска: onReady => resolveSymbol => getBars => subscribeBars => unsubscribeBars.
Если вы меняете таймфрейм, символ, вызывается хук unsubscribeBars, который обращается к вашей функции, которая сбрасывает WebSocket подключение с провайдером данных. Если вы не используете subscribeBars, то и unsubscribeBars вам не нужен. getServerTime хук не обязательный, но если вам требуется использовать время сервера, подключайте его.
Если провайдер данных не отдает объемы, то можете указать в хуке resolveSymbol — has_no_volume: true.
export default {
// Инициализация настроек, должна отдаваться АСИНХРОННО
onReady: (callback) => {
console.log('[onReady]: Method call');
// setTimeout(() => callback(<объект с настройками>))
},
/*
// Не требуется, если не используете поиск
searchSymbols: (userInput, exchange, symbolType, onResultReadyCallback) => {
console.log('[searchSymbols]: Method call');
},
*/
// получение данных о конкретном символе
resolveSymbol: (symbolName, onSymbolResolvedCallback, onResolveErrorCallback) => {
console.log('[resolveSymbol]: Method call', symbolName);
// onSymbolResolvedCallback({ ..., has_no_volume: true})
},
// получение исторических данные для конкретного символа
getBars: (symbolInfo, interval, from, to, onHistoryCallback, onErrorCallback, firstDataRequest) => {
console.log('[getBars] Method call', symbolInfo, interval)
console.log('[getBars] First request', firstDataRequest)
},
// подписка на обновления WebSocket
subscribeBars: (symbolInfo, interval, onRealtimeCallback, subscribeUID, onResetCacheNeededCallback) => {
console.log('[subscribeBars]: Method call with subscribeUID:', subscribeUID);
},
// вызывается для отписки от стрима
unsubscribeBars: (subscriberUID) => {
console.log('[unsubscribeBars]: Method call with subscriberUID:', subscriberUID);
},
getServerTime: (callback) => {}
};
Иногда провайдер данных не позволяет запрашивать данные напрямую с клиента, например биржа Binance, поэтому запрос можно прокидывать через прокси.
Документация JS API | Рабочий пример
UDF адаптер актуален, когда данные запрашиваются со своего сервера. В конструкторе клиента нужно указать datafeed: new Datafeeds.UDFCompatibleDatafeed('http://localhost:3000/datafeed', 1000)
// пример оформления плагина для **Fastify**
// main.js
const app = Fastify()
app.register(import('./modules/tradingview'), {})
// tradingview.js
const plugin = async (app, options) => {
// проверяем работу конечной точки
app.get('/', (req, res) => {
res.code(200).header('Content-Type', 'text/plain')
.send('Welcome to UDF Adapter for TradingView. See ./config for more details.')
})
// время сервера
app.get('/time', (req, res) => {
console.log('[time]: Method call')
const time = Math.floor(Date.now() / 1000) // In seconds
res.code(200).header('Content-Type', 'text/plain').send(time.toString())
})
// аналог onReady
// https://github.com/tradingview/charting_library/wiki/UDF#data-feed-configuration-data
app.get('/config', (req, res) => {
console.log('[config]: Method call')
})
// вызывается если: supports_group_request: true & supports_search: false
app.get('/symbol_info', async (req, res) => {
console.log('[symbol_info]: Method call')
})
// вызывается если: supports_group_request: false & supports_search: true
app.get('/symbols', async (req, res) => {
console.log('[symbol_info]: Method call')
const symbol = await getSymbols(req.query.symbol)
return symbol
})
// аналог getBars, запрашивает исторических данные
app.get('/history', async (req, res) => {
console.log('[history]: Method call')
})
}
Так бывает, когда не хватает данных и библиотека самостоятельно пытается "догрузить" информацию. В хуке getBars есть параметр firstDataRequest, который возвращает булевское значение true\false
, используйте его. Возвращает true
только при загрузке маркета.
getBars: (symbolInfo, interval, from, to, onHistoryCallback, onErrorCallback, firstDataRequest) => {
console.log('[getBars] Method call', symbolInfo, interval)
console.log('[getBars] First request', firstDataRequest)
if (firstDataRequest) {
console.log('do something')
}
},
Не обязательно использовать UDF провайдер, если нет стрима. Интервал запросов задать не получится для JS API адаптера, но это не мешает нам добавить setInterval в subscribeBars и отдавать данные для обновления.
subscribeBars: (symbolInfo, resolution, onRealtimeCallback, subscribeUID, onResetCacheNeededCallback) => {
console.log('[subscribeBars]: Method call with subscribeUID:', subscribeUID)
window.interval = setInterval(function () {
getLastKline(symbolInfo.ticker, resolution).then(kline => onRealtimeCallback(kline))
}, 1000 * 60) // 60s update interval
},
unsubscribeBars: (subscriberUID) => {
console.log('[unsubscribeBars]: Method call with subscriberUID:', subscriberUID)
clearInterval(window.interval)
console.log('[unsubscribeBars]: cleared')
}
По умолчанию доступны две темы: theme: "Light" || "Dark"
. Также можно использовать собственные цветовые решение. Со временем столкнетесь с проблемой, когда цвета поменялись везде, кроме header_widget (верхний блок с кнопками поиска, сравнения и пр.), его нужно менять через .css.
В опциях виджета нужно указать: custom_css_url: '/tradingview.css'
, где /
— абсолютный путь от вашего index.html
. С контентом:
.chart-controls-bar {
border-top: none !important;
}
.chart-page, .group-wWM3zP_M- {
background: transparent !important;
}
.pane-separator {
display: none !important;
}
Возможно понадобится сохранять "рисовалки".
Самый простой вариант, который можно использовать, если не планируется рисовать много на графиках. Простой, потому что можете вызвать объект со всеми данными графика widget.save(cb => this.setOverlay(cb))
и сохранить там, где будет удобно.
Похож на UDF adapter. На сервере поднимаете конечные точки для сохранения\загрузки данных.
Реальный кейс, обратился фрилансер с проектом, проект был старый, он его переписывал. По итогу просто была старая версия библиотеки. Проверяйте версию.
Другая ситуация, когда пытаются вызвать методы у еще незагруженного графика, отслеживайте состояние через onChartReady. Если нет под капотом реактивности, чтобы отследить загрузку графика, используйте паттерн Observer.
widget.onChartReady(function() {
// It's now safe to call any other methods of the widget
});
Да, это нормально.
После добавления ордера на график, нет доступа массиву, поэтому необходимо самостоятельно отслеживать ордера. Поделюсь своим решением оформленное в формате миксина для Vuejs, суть будет понятна.
import orders from '../../../multiblock/orders/mixin'
import createOrder from './createOrder'
import openOrders from './openOrders'
import trades from './trades'
export default {
mixins: [orders, createOrder, openOrders, trades],
data: () => ({
lines: new Map()
}),
watch: {
onChartReady(val) {
if (val) {
//* Uncomment: Testing price line
// this.line({ id: 'test', price: 0.021, quantity: 100 })
}
},
},
methods: {
// Line: open orders
positionLine(data) {
this.line(data)
.onCancel(() => {
this.deleteLine(data.id)
this.$bus.$emit('market-orders-deleteOrder', data.id)
})
.onMove(() => this.$bus.$emit('market-orders-updateOrder', { id: data.id, price: this.lines.get(data.id).getPrice() }))
},
// Line: order mobule ('price', 'stopPrice')
orderLine({ id = 'price', ...data }) {
this.line({ id, ...data })
.onMove(() => {
// Set new value on draging
this.$store.commit('setMarketOrder', { [id]: this.lines.get(id).getPrice() })
})
.onCancel(() => {
// Delete price line & set price = 0
this.deleteLine(id)
this.$store.commit('setMarketOrder', { [id]: 0 }) // set 0 value in vuex storage
})
},
line({ id = 'price', text = 'Price', color = '#ff9f0a', price, quantity, fontColor = '#fff', lineStyle = 2, lineLength = 25 }) {
if (this.lines.has(id)) this.deleteLine(id)
// Creating line from scratch
const widget = this.widget.chart().createOrderLine()
.setText(text)
.setPrice(price)
.setQuantity(quantity)
.onModify(res => res) // Need for dragging
// Customize color
.setLineColor(color)
.setBodyTextColor(fontColor)
.setBodyBorderColor(color)
.setBodyBackgroundColor(color)
.setQuantityBorderColor(color)
.setQuantityTextColor(fontColor)
.setQuantityBackgroundColor(color)
.setCancelButtonBorderColor(color)
.setCancelButtonBackgroundColor(color)
.setCancelButtonIconColor(fontColor)
.setLineLength(lineLength) // Margin right 25%
.setLineStyle(lineStyle)
this.lines.set(id, widget)
return widget // return for orderLine func()
},
deleteLine(id) {
this.lines.get(id).remove()
this.lines.delete(id)
},
deleteLines() {
this.lines.forEach((value, key) => this.deleteLine(key))
}
}
}
Добавлять можно исключительно предложенные библитекой формы, которые используются в тулбаре. Это необходимо, когда нужно вывести информацию на график.
Документация | Список доступных форм
charting_library не поддерживает такой функционал. PineScript можно переписать на JavaScript и использовать алгоритм на клиентской или серверной части.
Посмотрите в сторону Custom Studies
В бесплатной версии charting_library такой функционал отсутствует. При необходимости можно своими силами это сделать HTML+CSS.
Статья будет дополняться. Если есть кейс с проблемой — решением, пишите, дополню статью с указанием авторства.
Также интересно услышать ваше мнение, опыт, вопросы и пожелания.
Спасибо за внимание!