Se você investe ou negocia criptomoedas certamente conhece o site Trading View, famoso pelos seus gráficos de velas atualizados em tempo real. Outro dia inclusive ensinei a criar um gráfico em tempo real com ReactJS + WebSockets, no melhor estilo TV, onde pegamos os dados da corretora Binance e plotamos eles em um candlestick chart. Em outra ocasião, eu ensinei aqui no blog uma proposta de algoritmo para detectar automaticamente linhas de suporte e resistência em gráfico de velas, o que você confere aqui, novamente usando o histórico de velas que a Binance fornece.
Mas e se juntássemos as duas coisas: gráfico de velas em tempo real que detecta automaticamente linhas de suporte e resistência? Seria legal, certo? Além disso, é uma oportunidade para recriarmos o projeto de clone do gráfico de velas do TradingView usando Next.js ao invés de React puro, o que nos traria uma série de vantagens como por exemplo não precisar de uma aplicação Node.js para consumir a API da Binance.
Para isso, vamos precisar que você já conheça ao menos o básico de ReactJS pois usaremos muito ele. Se nunca programou React antes, você pode pegar a base nesta série aqui do blog. Também precisamos que você tenha o Node.js instalado na sua máquina, coisa que ensino neste vídeo.
Se preferir, você pode acompanhar este tutorial no vídeo abaixo, que tem o mesmo conteúdo.
Vamos lá!
#1 – Setup do Projeto
O primeiro passo é fazer a configuração inicial do nosso projeto que será feito usando Next.js.
Para inicializar o nosso projeto, rode o seguinte comando no terminal (bash, MS-DOS, etc), que vai criar nossa aplicação Next.js.
1 2 3 |
npx create-next-app tradingview-ai |
O CLI do Next vai iniciar com algumas perguntas para você. Se tiver mais experiência com front React, decida as opções que melhor lhe agradarem, mas se for um iniciante, recomendo não usar TypeScript, nem ESLint e manter o resto padrão como sugerido.
Esse comando pode demorar um bocado para finalizar pois ele cria toda a estrutura de pastas e baixa uma cacetada de dependências. Quando ele terminar, você deve ainda instalar mais algumas dependências que vamos precisar, com o comando abaixo.
1 2 3 |
npm i apexcharts react-apexcharts axios react-use-websockets |
De maneira resumida, essas dependências (que vamos explorar melhor mais à frente) servem para:
- apexcharts e react-apexcharts: componente para os gráficos de velas (docs);
- axios: lib para comunicação com as APIs da Binance (docs);
- react-use-websockets: componente para atualização real-time do gráfico (docs);
Apenas para ver se deu tudo certo, você pode entrar na pasta do projeto e rodar o comando abaixo para inicializar ele.
1 2 3 |
npm run dev |
O resultado será o logo do Next.js apenas, mostrando que a aplicação está funcionando e pronta para ser customizada.
#2 – Modelando os Dados
Nosso gráfico será alimentado por duas fontes de dados diferentes: uma histórica, que servirá para dar sua aparência inicial, com as últimas x velas de um ativo. E a outra para recebermos a vela atual, em tempo real, que faremos mais à frente. Usarei como fontes de dados para ambos cenários a corretora Binance, maior do segmento e que fornece publicamente estes dados tanto via API REST quanto via WebSockets/Streams.
A API da Binance que vamos usar para pegar velas históricas é essa aqui, que recebe o symbol (par de moedas), o intervalo gráfico e a quantidade de velas que deseja, sendo que ele vai trazer as x mais recentes por padrão.
Independente dessa API da Binance, você pode substituir por dados de outra corretora se quiser, apenas adaptando as chamadas.
Por uma questão de organização, vou criar algumas classes assim como vimos no tutorial de Detecção de Suporte e Resistência, que você deve ler se quiser se aprofundar nesse algoritmo (apenas mudei levemente alguns nomes e atualizei a sintaxe). As 3 classes devem ser guardadas em uma nova pasta /src/lib:
- CandlePoint: um candle no nosso gráfico de velas (no formato esperado pela lib);
- LinePoint: um ponto no nosso gráfico de linha (no formato esperado pela lib);
- CandleChart: um gráfico de candles (array de CandlePoints), usado pelo algoritmo de detecção;
Vamos começar pelo arquivo src/lib/CandlePoint.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
export default class CandlePoint { constructor(openTime, open, high, low, close) { this.x = new Date(openTime); this.y = [parseFloat(open), parseFloat(high), parseFloat(low), parseFloat(close)]; } getTime() { return this.x; } getOpen() { return this.y[0]; } getHigh() { return this.y[1]; } getLow() { return this.y[2]; } getClose() { return this.y[3]; } } |
No construtor recebemos o timestamp de abertura da vela (seu id) e o OHLC. Com essa informação, inicializamos as coordenadas x/y conforme a lib ApexCharts de gráficos nos exige. Todas as demais funções são auxiliares para retornar informações específicas da vela, que serão usadas pelo algoritmo de detecção mais à frente.
Agora o src/lib/LinePoint.js:
1 2 3 4 5 6 7 8 |
export default class LinePoint { constructor(timestamp, value) { this.x = new Date(timestamp); this.y = parseFloat(value); } } |
Esse é ainda mais simples, seu construtor recebe o timestamp e o preço do ativo naquele instante (value), usando essas duas informações como coordenadas x/y, conforme a lib ApexCharts de gráficos nos exige.
Agora a última classe, a src/lib/CandleChart.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
export default class CandleChart { constructor(arr, tickSize) { this.candles = arr; this.TICK_SIZE = tickSize; } highestPrice() { const orderedKlines = this.candles.sort((a, b) => a.getHigh() - b.getHigh()); return orderedKlines[orderedKlines.length - 1].getHigh(); } lowestPrice() { const orderedKlines = this.candles.sort((a, b) => a.getLow() - b.getLow()); return orderedKlines[0].getLow(); } getMedium() { let atl = this.lowestPrice(); let ath = this.highestPrice(); return ((ath - atl) / 2) + atl; } getTrendTick(grouped, total) { let tickArr = Object.keys(grouped).map(k => { return { tick: k, count: grouped[k] } }); tickArr = tickArr.sort((a, b) => a.count - b.count); return { ...tickArr[tickArr.length - 1], total }; } getTicks(candle) { const priceOsc = candle.getHigh() - candle.getLow(); return priceOsc * (1 / this.TICK_SIZE); } getGroupedTicks(grouped, candle) { const ticks = this.getTicks(candle); for (let i = 0; i < ticks; i++) { const tick = candle.getLow() + (this.TICK_SIZE * i); if (grouped[tick]) grouped[tick]++; else grouped[tick] = 1; } return grouped; } findSupport() { const medium = this.getMedium(); const candles = this.candles.filter(candle => candle.getLow() < medium); let grouped = {}; candles.map(candle => grouped = this.getGroupedTicks(grouped, candle)); return this.getTrendTick(grouped, candles.length); } findResistance() { const medium = this.getMedium(); const candles = this.candles.filter(candle => candle.getHigh() > medium); let grouped = {}; candles.map(candle => grouped = this.getGroupedTicks(grouped, candle)); return this.getTrendTick(grouped, candles.length); } } |
Essa aqui é bem complexa e foi descrita em detalhes no tutorial de detecção de suporte e resistência, que super recomendo a leitura. Resumidamente, o constructor recebe um array de CandlePoints e o ticksize (menor fração da moeda da cotação, geralmente 0.01 se estiver cotando em moeda fiat). Mais tarde, as funções que mais importam e que vão ser chamadas externamente à classe são a findSupport, que encontra o valor de suporte e a findResistance, que encontra o valor de resistência.
Com essas três classes modeladas, podemos avançar para a utilização delas em um serviço de obtenção de dados que vamos criar.
#3 – Obtendo os Dados
Agora, vamos implementar a única chamada à API que vamos precisar, para pegar o histórico inicial de velas visando montar o gráfico. Aqui o objetivo não é apenas pegar os dados em si, como montar um objeto que possa ser usado mais tarde pela lib de gráficos (ApexChart) para renderizar o gráfico de velas sobreposto pelas linhas de suporte e resistência.
Crie um arquivo src/services/DataService.js e vamos começar importando as três classes que criamos, bem como a lib Axios que vamos usar para fazer as chamadas HTTP. Também defino logo no início o tick size, embora uma outra opção tanto pra ele quanto para o symbol e interval (que veremos a seguir) seria usar variáveis de ambiente.
1 2 3 4 5 6 7 8 |
import axios from 'axios'; import CandlePoint from '@/lib/CandlePoint'; import CandleChart from '@/lib/CandleChart'; import LinePoint from '@/lib/LinePoint'; const TICK_SIZE = 0.01; |
Agora vamos fazer a função getCandles, a única que precisaremos neste serviço:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
export async function getData(symbol = 'BTCUSDT', interval = '1m') { const response = await axios.get(`https://api.binance.com/api/v3/klines?symbol=${symbol.toUpperCase()}&interval=${interval}&limit=60`); const candles = response.data.map(k => { return new CandlePoint(k[0], k[1], k[2], k[3], k[4]) }) const candlechart = new CandleChart(candles, TICK_SIZE); const supportTick = candlechart.findSupport(); const support = [ new LinePoint(response.data[0][0], parseFloat(supportTick.tick)), new LinePoint(response.data[response.data.length - 1][0], parseFloat(supportTick.tick)) ]; const resistanceTick = candlechart.findResistance(); const resistance = [ new LinePoint(response.data[0][0], parseFloat(resistanceTick.tick)), new LinePoint(response.data[response.data.length - 1][0], parseFloat(resistanceTick.tick)) ]; return { candles, support, resistance }; } |
A função getCandles espera as informações de symbol e interval vindas do frontend e com elas faz a chamada com Axios para a API da Binance. Como o Next.js roda server-side por padrão, essa chamada de Axios será feito por um “backend”, logo não caindo em problemas de CORS por exemplo, que aconteceria se tentasse chamar via Axios direto do ReactJS.
Mas voltando à função, após recebermos os dados crus das velas da Binance, nós construímos um array de CandlePoints que usaremos para alimentar o CandleChart e depois o próprio gráfico em si. Com o CandleChart inicializado, podemos buscar os valores de suporte e resistência, usando eles para criar arrays de LinePoints com dois elementos cada. São necessários apenas dois elementos pois as linhas de suporte e resistência são retas, não havendo variação de preço, então preciso apenas do ponto inicial e final da linha e a lib de gráfico vai fazer o resto da magia acontecer.
Para testar esse código é bem simples, você pode ir no src/page.js, importar a função e chamá-la em um useEffect, imprimindo o resultado na tela ou no console de qualquer jeito só para ver se está funcionando. Repare que coloquei a diretiva “use client” no topo, pois nossa página vai ter o JS gerado no server-side, mas a renderização eu quero que seja client-side.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
"use client" import { useEffect } from 'react'; import { getData } from '@/services/DataService'; function App() { useEffect(() => { getData('BTCUSDT','1m') .then(data => console.log(data)) .catch(err => alert(err.response ? err.response.data : err.message)) }, []) ... |
Verá ao executar que nossa aplicação já traz agora não apenas o array de candles como também o array de pontos do suporte e da resistência.
Agora nossa próxima missão é usar dessas informações para renderizar o gráfico.
#4 – Gráfico de Velas
Para criação do gráfico de velas vamos usar uma biblioteca de componentes gráficos chamada ApexCharts que é gratuita e que possui uma versão para React chamada React ApexCharts, que nós já instalamos na etapa 1 deste tutorial.
Para encapsular a complexidade de montar o gráfico de velas, vamos criar um componente React chamado src/components/Chart.js com ele dentro.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
"use client" import ApexChart from 'react-apexcharts'; export default function Chart(props) { const options = { xaxis: { type: 'datetime' } } const series = [{ name: "candles", type: "candlestick", data: props.data.candles }, { name: "support", type: 'line', data: props.data.support }, { name: "resistance", type: 'line', data: props.data.resistance }] return ( <ApexChart options={options} series={series} width={800} height={600} /> ) } |
Aqui definimos que vamos receber via props um objeto data, contendo as propriedades candles, support e resistance, que serão usadas na renderização do gráfico. Essa renderização requer a configuração de um objeto options e um array series. Enquanto o options é bem direto, com as configurações gerais do gráfico que é apenas uma: xaxis (eixo x/horizontal) definido como do tipo datetime, o array de series é mais complexo.
O array de series é onde você configura os dados que serão usados no seu gráfico. Ele é um array pois a lib ApexChart permite sobreposição de gráficos, o que no nosso caso é imprescindível pois teremos três gráficos em um: as velas, o suporte e a resistência. Sendo assim, precisaremos montar três séries de dados nesse array.
A primeira série possui o nome de candles e é para um gráfico do tipo candlestick. Seus dados são obtidos a partir dos candles. A segunda e terceira séries são a de suporte e resistência, configuradas de acordo e com o tipo “line” para seus gráficos.
Esses dois objetos, options e series, usamos nas propriedades do componente ApexChart, junto das dimensões do gráfico. Agora para usar este gráfico é bem simples, você posiciona o componente no seu page.js e eu fiz questão de incluir dois selects também, para que o usuário possa trocar o symbol e interval exibidos no gráfico.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
"use client" import { useState, useEffect } from 'react'; import { getData } from '@/services/DataService'; import Chart from './components/Chart'; function App() { const [symbol, setSymbol] = useState("BTCUSDT"); const [interval, setInterval] = useState("1m"); const [data, setData] = useState({ candles: [], support: [], resistance: [] }); useEffect(() => { getData(symbol, interval) .then(data => setData(data)) .catch(err => alert(err.response ? err.response.data : err.message)) }, [symbol, interval]) function onSymbolChange(event) { setSymbol(event.target.value); } function onIntervalChange(event) { setInterval(event.target.value); } return ( <> <select onChange={onSymbolChange} value={symbol}> <option>BTCUSDT</option> <option>ETHUSDT</option> <option>ADAUSDT</option> </select> <select onChange={onIntervalChange} value={interval}> <option>1m</option> <option>15m</option> <option>1h</option> <option>1d</option> </select> <Chart data={data} /> </> ); } export default App; |
Os states que criei são autoexplicativos e servem para guardar os dados e forçar a renderização do gráfico atualizado quando necessário. Os selects também não têm nada de especial: apenas coloquei algumas informações úteis e no evento onChange estou alterando os respectivos states.
Repare também que atualizei o useEffect para que ele use os states, o que vai fazer com que qualquer alteração ele recarregue os dados. Com isso, o carregamento inicial dos dados está pronto e o resultado esperado pode ser visto abaixo, onde aparece o gráfico de velas e as linhas de suporte e resistência plotadas.
O que precisamos agora, é de um mecanismo para manter este gráfico sempre atualizado.
#5 – Atualizando o Gráfico
Nossa última etapa é garantir que nosso gráfico se mantenha atualizado conforme o preço do ativo subir ou descer e sem a necessidade de ficar fazendo refresh na tela. Isso é possível graças à tecnologia WebSocket que permite que seja criado um canal de comunicação entre o browser e a corretora, por onde ela envia atualizações sempre que houverem.
Na Binance são fornecidas publicamente diversas streams (fluxos) de dados que você pode se conectar para receber updates e a que vamos usar é a de velas, documentada aqui. Se estiver usando outra exchange, consulte a documentação da mesma para ver a existência desse recurso.
Uma coisa bacana do protocolo websocket é que você pode conectar seu frontend diretamente à stream, o que faremos no ReactJS usando um componente chamado React useWebSocket, que já instalamos na etapa 1 do tutorial. Esse componente faz a ligação conforme nossas configurações e define uma função que vai ser chamada a cada atualização recebida, que vamos usar para atualizar o gráfico, como abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
... import useWebSocket from 'react-use-websocket'; import CandlePoint from '@/lib/CandlePoint'; import LinePoint from '@/lib/LinePoint' function App() { ... const { lastJsonMessage } = useWebSocket(`wss://stream.binance.com:9443/ws/${symbol.toLowerCase()}@kline_${interval}`, { onOpen: () => console.log(`Connected to App WS`), onMessage: () => { if (lastJsonMessage) { const newCandle = new CandlePoint(lastJsonMessage.k.t, lastJsonMessage.k.o, lastJsonMessage.k.h, lastJsonMessage.k.l, lastJsonMessage.k.c); const newCandles = [...data.candles]; const newSupport = [...data.support]; const newResistance = [...data.resistance]; if (lastJsonMessage.k.x === false) { //candle incompleto newCandles[newCandles.length - 1] = newCandle;//substitui último candle pela versão atualizada } else { //remove candle primeiro candle e adiciona o novo último newCandles.splice(0, 1); newCandles.push(newCandle); //atualiza suporte newSupport.splice(1, 1); newSupport.push(new LinePoint(newCandle.x, newSupport[0].y)); //atualiza resistencia newResistance.splice(1, 1); newResistance.push(new LinePoint(newCandle.x, newResistance[0].y)); } setData({ candles: newCandles, support: newSupport, resistance: newResistance }); } }, onError: (event) => console.error(event), shouldReconnect: (closeEvent) => true, reconnectInterval: 3000 }); ... |
Eu omiti com reticências as partes que não nos interessam no momento e foquei aqui na configuração do hook useWebSocket. Primeiro, montamos a URL com as informações do symbol e interval armazenados no state. Depois, configuramos para que uma mensagem seja impressa no console logo que a conexão for estabelecida, para sabermos que funcionou.
O evento onMessage é o coração do componente e nele nós testamos se chegou um JSON como atualização, transformamos este JSON em um novo CandlePoint e fazemos uma cópia dos dados atuais do gráfico, para manipulá-los conforme a última vela que chegou: se a última vela estiver completa, removemos a primeira e colocamos ela na última posição, para que o gráfico sempre tenha 60 velas. Se a última vela estiver incompleta (os updates chegam a cada 2 segundos com valores parciais/incompletos), apenas substituímos a última exibida por ela, o que vai garantir que o gráfico esteja atualizado.
Ao término dos ajustes no array de velas, ajustamos o array de pontos de suporte e resistência, mas apenas o timestamp deles. Optei aqui por não ficar recalculando suporte e resistência, mas você pode se quiser, basta usar a classe CandleChart novamente. Após todas alterações, mandamos o state ser atualizado o que vai forçar a renderização do gráfico que inteligentemente não pisca, apenas ajusta a vela que atualizou.
Com isso conseguimos completar o funcionamento do principal recurso do TradingView que é o gráfico de velas atualizado em tempo real, adicionando esta incrível funcionalidade de detecção dos padrões que, embora não seja perfeita, pode ser a base para você evoluir ela ou até mesmo criar outros padrões gráficos e indicadores.
Estas e outras funcionalidades do Trading View quem sabe eu não mostro em outro tutorial futuro? Se gostou da ideia, deixe nos comentários!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.