Генерация графика котировок криптовалюты

Какую проблему я пытаюсь решить

Генерация графика котировок и объемов определенной криптовалюты за определенный период для отправки через Telegram Bot

Какое решение я ищу

Я бы хотел, используя какой-нибудь API или что-то в этом роде, получить готовый график цен и объемов для определенной криптовалюты, на одной из топовых бирж, добавить к нему свои метки, например, точку покупки и точку продажи, тейк, стоп. Затем отправить этот график как изображение или ссылку, которая откроет этот график с моими пометками. Генерация графика и нанесение пометок должно производиться автоматически, в соответствии с исходными данными, без ручного вмешательства.

Если у вас есть аналогичный опыт или вы знаете подходящие инструменты с небольшим порогом входа, пожалуйста, дайте мне подсказку в ответах.


Ответы (1 шт):

Автор решения: DiD

Можно использовать CanvasJS

var dps1 = [], dps2= [];
var stockChart = new CanvasJS.StockChart("chartContainer",{
  charts: [{
    axisY: {
      prefix: "$"
    },
    legend: {
      verticalAlign: "top",
      cursor: "pointer",
      itemclick: function (e) {
        if (typeof (e.dataSeries.visible) === "undefined" || e.dataSeries.visible) {
          e.dataSeries.visible = false;
        } else {
          e.dataSeries.visible = true;
        }
        e.chart.render();
      }
    },
    toolTip: {
      shared: true
    },
    data: [{
      type: "candlestick",
      showInLegend: true,
      name: "Stock Price",
      yValueFormatString: "$#,###.00",
      xValueType: "dateTime",
      dataPoints : dps1
    }],
  }],
  navigator: {
     data: [{
       dataPoints: dps2
     }],
    slider: {
      minimum: new Date(2018, 03, 01),
      maximum: new Date(2018, 05, 01)
    }
  }
});
$.getJSON("https://canvasjs.com/data/docs/ethusd2018.json", function(data) {
  for(var i = 0; i < data.length; i++){
    dps1.push({x: new Date(data[i].date), y: [Number(data[i].open), Number(data[i].high), Number(data[i].low), Number(data[i].close)]});
    dps2.push({x: new Date(data[i].date), y: Number(data[i].close)});
  }
  stockChart.render();
  /************* SMA indicator ***************/
  var sma = calculateSMA(dps1, 7);
  stockChart.charts[0].addTo("data", { type: "line", dataPoints: sma, showInLegend: true, yValueFormatString: "$#,###.00", name: "Simple Moving Average"})
  /************* EMA indicator ***************/  
    var ema = calculateEMA(dps1, 7);
    stockChart.charts[0].addTo("data", {type: "line", name: "EMA", showInLegend: true, yValueFormatString: "$#,###.##", dataPoints: ema});
  /************* MACD indicator ***************/
    var ema12 = calculateEMA(dps1, 12),
        ema26 = calculateEMA(dps1, 26),
        macd = [], ema9;
    for(var i = 0; i < ema12.length; i++) {
      macd.push({x: ema12[i].x, y: (ema12[i].y - ema26[i].y)});
    }
    var ema9 = calculateEMA(macd, 9);
    stockChart.addTo("charts", {height: 100, data: [{type: "line", name: "MACD", showInLegend: true, dataPoints: macd}], legend: {horizontalAlign: "left"}, toolTip: {shared: true}});
    stockChart.charts[1].addTo("data", {type: "line", name: "Signal", showInLegend: true, dataPoints: ema9});

});
function calculateSMA(dps, count){
  var avg = function(dps){
    var sum = 0, count = 0, val;
    for (var i = 0; i < dps.length; i++) {
      val = dps[i].y[3]; sum += val; count++;
    }
    return sum / count;
  };
  var result = [], val;
  count = count || 5;
  for (var i=0; i < count; i++)
    result.push({ x: dps[i].x , y: null});
  for (var i=count - 1, len=dps.length; i < len; i++){
    val = avg(dps.slice(i - count + 1, i));
    if (isNaN(val))
      result.push({ x: dps[i].x, y: null});
    else
      result.push({ x: dps[i].x, y: val});
  }
  return result;
}
  function calculateEMA(dps,count) {
    var k = 2/(count + 1);
    var emaDps = [{x: dps[0].x, y: dps[0].y.length ? dps[0].y[3] : dps[0].y}];
    for (var i = 1; i < dps.length; i++) {
      emaDps.push({x: dps[i].x, y: (dps[i].y.length ? dps[i].y[3] : dps[i].y) * k + emaDps[i - 1].y * (1 - k)});
    }
    return emaDps;
  }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script type="text/javascript" src="https://canvasjs.com/assets/script/canvasjs.stock.min.js"></script>
<div id="chartContainer" style="height: 450px; width: 100%;"></div>

Есть и полностью опенсурсные альтернативы: Chart.js

/*!
 * @license
 * chartjs-chart-financial
 * http://chartjs.org/
 * Version: 0.1.0
 *
 * Copyright 2021 Chart.js Contributors
 * Released under the MIT license
 * https://github.com/chartjs/chartjs-chart-financial/blob/master/LICENSE.md
 */
(function(global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('chart.js'), require('chart.js/helpers')) :
    typeof define === 'function' && define.amd ? define(['chart.js', 'chart.js/helpers'], factory) :
    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Chart, global.Chart.helpers));
}(this, (function(chart_js, helpers) {
  'use strict';

  /**
   * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap.
   * @private
   */
  function computeMinSampleSize(scale, pixels) {
    let min = scale._length;
    let prev, curr, i, ilen;

    for (i = 1, ilen = pixels.length; i < ilen; ++i) {
      min = Math.min(min, Math.abs(pixels[i] - pixels[i - 1]));
    }

    for (i = 0, ilen = scale.ticks.length; i < ilen; ++i) {
      curr = scale.getPixelForTick(i);
      min = i > 0 ? Math.min(min, Math.abs(curr - prev)) : min;
      prev = curr;
    }

    return min;
  }

  /**
   * This class is based off controller.bar.js from the upstream Chart.js library
   */
  class FinancialController extends chart_js.BarController {

    getLabelAndValue(index) {
      const me = this;
      const parsed = me.getParsed(index);
      const axis = me._cachedMeta.iScale.axis;

      const {
        o,
        h,
        l,
        c
      } = parsed;
      const value = `O: ${o}  H: ${h}  L: ${l}  C: ${c}`;

      return {
        label: `${me._cachedMeta.iScale.getLabelForValue(parsed[axis])}`,
        value
      };
    }

    getAllParsedValues() {
      const meta = this._cachedMeta;
      const axis = meta.iScale.axis;
      const parsed = meta._parsed;
      const values = [];
      for (let i = 0; i < parsed.length; ++i) {
        values.push(parsed[i][axis]);
      }
      return values;
    }

    /**
     * Implement this ourselves since it doesn't handle high and low values
     * https://github.com/chartjs/Chart.js/issues/7328
     * @protected
     */
    getMinMax(scale) {
      const meta = this._cachedMeta;
      const _parsed = meta._parsed;
      const axis = meta.iScale.axis;

      if (_parsed.length < 2) {
        return {
          min: 0,
          max: 1
        };
      }

      if (scale === meta.iScale) {
        return {
          min: _parsed[0][axis],
          max: _parsed[_parsed.length - 1][axis]
        };
      }

      let min = Number.POSITIVE_INFINITY;
      let max = Number.NEGATIVE_INFINITY;
      for (let i = 0; i < _parsed.length; i++) {
        const data = _parsed[i];
        min = Math.min(min, data.l);
        max = Math.max(max, data.h);
      }
      return {
        min,
        max
      };
    }

    _getRuler() {
      const me = this;
      const opts = me.options;
      const meta = me._cachedMeta;
      const iScale = meta.iScale;
      const axis = iScale.axis;
      const pixels = [];
      for (let i = 0; i < meta.data.length; ++i) {
        pixels.push(iScale.getPixelForValue(me.getParsed(i)[axis]));
      }
      const barThickness = opts.barThickness;
      const min = computeMinSampleSize(iScale, pixels);
      return {
        min,
        pixels,
        start: iScale._startPixel,
        end: iScale._endPixel,
        stackCount: me._getStackCount(),
        scale: iScale,
        ratio: barThickness ? 1 : opts.categoryPercentage * opts.barPercentage
      };
    }

    /**
     * @protected
     */
    calculateElementProperties(index, ruler, reset, options) {
      const me = this;
      const vscale = me._cachedMeta.vScale;
      const base = vscale.getBasePixel();
      const ipixels = me._calculateBarIndexPixels(index, ruler, options);
      const data = me.chart.data.datasets[me.index].data[index];
      const open = vscale.getPixelForValue(data.o);
      const high = vscale.getPixelForValue(data.h);
      const low = vscale.getPixelForValue(data.l);
      const close = vscale.getPixelForValue(data.c);

      return {
        base: reset ? base : low,
        x: ipixels.center,
        y: (low + high) / 2,
        width: ipixels.size,
        open,
        high,
        low,
        close
      };
    }

    draw() {
      const me = this;
      const chart = me.chart;
      const rects = me._cachedMeta.data;
      helpers.clipArea(chart.ctx, chart.chartArea);
      for (let i = 0; i < rects.length; ++i) {
        rects[i].draw(me._ctx);
      }
      helpers.unclipArea(chart.ctx);
    }

  }

  FinancialController.overrides = {
    label: '',

    parsing: false,

    hover: {
      mode: 'label'
    },

    datasets: {
      categoryPercentage: 0.8,
      barPercentage: 0.9,
      animation: {
        numbers: {
          type: 'number',
          properties: ['x', 'y', 'base', 'width', 'open', 'high', 'low', 'close']
        }
      }
    },

    scales: {
      x: {
        type: 'timeseries',
        offset: true,
        ticks: {
          major: {
            enabled: true,
          },
          fontStyle: context => context.tick.major ? 'bold' : undefined,
          source: 'data',
          maxRotation: 0,
          autoSkip: true,
          autoSkipPadding: 75,
          sampleSize: 100
        },
        afterBuildTicks: scale => {
          const DateTime = window && window.luxon && window.luxon.DateTime;
          if (!DateTime) {
            return;
          }
          const majorUnit = scale._majorUnit;
          const ticks = scale.ticks;
          const firstTick = ticks[0];
          if (!firstTick) {
            return;
          }

          let val = DateTime.fromMillis(firstTick.value);
          if ((majorUnit === 'minute' && val.second === 0) ||
            (majorUnit === 'hour' && val.minute === 0) ||
            (majorUnit === 'day' && val.hour === 9) ||
            (majorUnit === 'month' && val.day <= 3 && val.weekday === 1) ||
            (majorUnit === 'year' && val.month === 1)) {
            firstTick.major = true;
          } else {
            firstTick.major = false;
          }
          let lastMajor = val.get(majorUnit);

          for (let i = 1; i < ticks.length; i++) {
            const tick = ticks[i];
            val = DateTime.fromMillis(tick.value);
            const currMajor = val.get(majorUnit);
            tick.major = currMajor !== lastMajor;
            lastMajor = currMajor;
          }
          scale.ticks = ticks;
        }
      },
      y: {
        type: 'linear'
      }
    },

    plugins: {
      tooltip: {
        intersect: false,
        mode: 'index',
        callbacks: {
          label(ctx) {
            const point = ctx.parsed;

            if (!helpers.isNullOrUndef(point.y)) {
              return chart_js.defaults.plugins.tooltip.callbacks.label(ctx);
            }

            const {
              o,
              h,
              l,
              c
            } = point;

            return `O: ${o}  H: ${h}  L: ${l}  C: ${c}`;
          }
        }
      }
    }
  };

  const globalOpts$2 = chart_js.Chart.defaults;

  globalOpts$2.elements.financial = {
    color: {
      up: 'rgba(80, 160, 115, 1)',
      down: 'rgba(215, 85, 65, 1)',
      unchanged: 'rgba(90, 90, 90, 1)',
    }
  };

  /**
   * Helper function to get the bounds of the bar regardless of the orientation
   * @param {Rectangle} bar the bar
   * @param {boolean} [useFinalPosition]
   * @return {object} bounds of the bar
   * @private
   */
  function getBarBounds(bar, useFinalPosition) {
    const {
      x,
      y,
      base,
      width,
      height
    } = bar.getProps(['x', 'low', 'high', 'width', 'height'], useFinalPosition);

    let left, right, top, bottom, half;

    if (bar.horizontal) {
      half = height / 2;
      left = Math.min(x, base);
      right = Math.max(x, base);
      top = y - half;
      bottom = y + half;
    } else {
      half = width / 2;
      left = x - half;
      right = x + half;
      top = Math.min(y, base); // use min because 0 pixel at top of screen
      bottom = Math.max(y, base);
    }

    return {
      left,
      top,
      right,
      bottom
    };
  }

  function inRange(bar, x, y, useFinalPosition) {
    const skipX = x === null;
    const skipY = y === null;
    const bounds = !bar || (skipX && skipY) ? false : getBarBounds(bar, useFinalPosition);

    return bounds &&
      (skipX || x >= bounds.left && x <= bounds.right) &&
      (skipY || y >= bounds.top && y <= bounds.bottom);
  }

  class FinancialElement extends chart_js.Element {

    height() {
      return this.base - this.y;
    }

    inRange(mouseX, mouseY, useFinalPosition) {
      return inRange(this, mouseX, mouseY, useFinalPosition);
    }

    inXRange(mouseX, useFinalPosition) {
      return inRange(this, mouseX, null, useFinalPosition);
    }

    inYRange(mouseY, useFinalPosition) {
      return inRange(this, null, mouseY, useFinalPosition);
    }

    getRange(axis) {
      return axis === 'x' ? this.width / 2 : this.height / 2;
    }

    getCenterPoint(useFinalPosition) {
      const {
        x,
        low,
        high
      } = this.getProps(['x', 'low', 'high'], useFinalPosition);
      return {
        x,
        y: (high + low) / 2
      };
    }

    tooltipPosition(useFinalPosition) {
      const {
        x,
        open,
        close
      } = this.getProps(['x', 'open', 'close'], useFinalPosition);
      return {
        x,
        y: (open + close) / 2
      };
    }
  }

  const globalOpts$1 = chart_js.Chart.defaults;

  class CandlestickElement extends FinancialElement {
    draw(ctx) {
      const me = this;

      const {
        x,
        open,
        high,
        low,
        close
      } = me;

      let borderColors = me.borderColor;
      if (typeof borderColors === 'string') {
        borderColors = {
          up: borderColors,
          down: borderColors,
          unchanged: borderColors
        };
      }

      let borderColor;
      if (close < open) {
        borderColor = helpers.valueOrDefault(borderColors ? borderColors.up : undefined, globalOpts$1.elements.candlestick.borderColor);
        ctx.fillStyle = helpers.valueOrDefault(me.color ? me.color.up : undefined, globalOpts$1.elements.candlestick.color.up);
      } else if (close > open) {
        borderColor = helpers.valueOrDefault(borderColors ? borderColors.down : undefined, globalOpts$1.elements.candlestick.borderColor);
        ctx.fillStyle = helpers.valueOrDefault(me.color ? me.color.down : undefined, globalOpts$1.elements.candlestick.color.down);
      } else {
        borderColor = helpers.valueOrDefault(borderColors ? borderColors.unchanged : undefined, globalOpts$1.elements.candlestick.borderColor);
        ctx.fillStyle = helpers.valueOrDefault(me.color ? me.color.unchanged : undefined, globalOpts$1.elements.candlestick.color.unchanged);
      }

      ctx.lineWidth = helpers.valueOrDefault(me.borderWidth, globalOpts$1.elements.candlestick.borderWidth);
      ctx.strokeStyle = helpers.valueOrDefault(borderColor, globalOpts$1.elements.candlestick.borderColor);

      ctx.beginPath();
      ctx.moveTo(x, high);
      ctx.lineTo(x, Math.min(open, close));
      ctx.moveTo(x, low);
      ctx.lineTo(x, Math.max(open, close));
      ctx.stroke();
      ctx.fillRect(x - me.width / 2, close, me.width, open - close);
      ctx.strokeRect(x - me.width / 2, close, me.width, open - close);
      ctx.closePath();
    }
  }

  CandlestickElement.id = 'candlestick';
  CandlestickElement.defaults = helpers.merge({}, [globalOpts$1.elements.financial, {
    borderColor: globalOpts$1.elements.financial.color.unchanged,
    borderWidth: 1,
  }]);

  class CandlestickController extends FinancialController {

    updateElements(elements, start, count, mode) {
      const me = this;
      const dataset = me.getDataset();
      const ruler = me._ruler || me._getRuler();
      const firstOpts = me.resolveDataElementOptions(start, mode);
      const sharedOptions = me.getSharedOptions(firstOpts);
      const includeOptions = me.includeOptions(mode, sharedOptions);

      me.updateSharedOptions(sharedOptions, mode, firstOpts);

      for (let i = start; i < count; i++) {
        const options = sharedOptions || me.resolveDataElementOptions(i, mode);

        const baseProperties = me.calculateElementProperties(i, ruler, mode === 'reset', options);
        const properties = {
          ...baseProperties,
          datasetLabel: dataset.label || '',
          // label: '', // to get label value please use dataset.data[index].label

          // Appearance
          color: dataset.color,
          borderColor: dataset.borderColor,
          borderWidth: dataset.borderWidth,
        };

        if (includeOptions) {
          properties.options = options;
        }
        me.updateElement(elements[i], i, properties, mode);
      }
    }

  }

  CandlestickController.id = 'candlestick';
  CandlestickController.defaults = helpers.merge({
    dataElementType: CandlestickElement.id
  }, chart_js.Chart.defaults.financial);

  const globalOpts = chart_js.Chart.defaults;

  class OhlcElement extends FinancialElement {
    draw(ctx) {
      const me = this;

      const {
        x,
        open,
        high,
        low,
        close
      } = me;

      const armLengthRatio = helpers.valueOrDefault(me.armLengthRatio, globalOpts.elements.ohlc.armLengthRatio);
      let armLength = helpers.valueOrDefault(me.armLength, globalOpts.elements.ohlc.armLength);
      if (armLength === null) {
        // The width of an ohlc is affected by barPercentage and categoryPercentage
        // This behavior is caused by extending controller.financial, which extends controller.bar
        // barPercentage and categoryPercentage are now set to 1.0 (see controller.ohlc)
        // and armLengthRatio is multipled by 0.5,
        // so that when armLengthRatio=1.0, the arms from neighbour ohcl touch,
        // and when armLengthRatio=0.0, ohcl are just vertical lines.
        armLength = me.width * armLengthRatio * 0.5;
      }

      if (close < open) {
        ctx.strokeStyle = helpers.valueOrDefault(me.color ? me.color.up : undefined, globalOpts.elements.ohlc.color.up);
      } else if (close > open) {
        ctx.strokeStyle = helpers.valueOrDefault(me.color ? me.color.down : undefined, globalOpts.elements.ohlc.color.down);
      } else {
        ctx.strokeStyle = helpers.valueOrDefault(me.color ? me.color.unchanged : undefined, globalOpts.elements.ohlc.color.unchanged);
      }
      ctx.lineWidth = helpers.valueOrDefault(me.lineWidth, globalOpts.elements.ohlc.lineWidth);

      ctx.beginPath();
      ctx.moveTo(x, high);
      ctx.lineTo(x, low);
      ctx.moveTo(x - armLength, open);
      ctx.lineTo(x, open);
      ctx.moveTo(x + armLength, close);
      ctx.lineTo(x, close);
      ctx.stroke();
    }
  }

  OhlcElement.id = 'ohlc';
  OhlcElement.defaults = helpers.merge({}, [globalOpts.elements.financial, {
    lineWidth: 2,
    armLength: null,
    armLengthRatio: 0.8,
  }]);

  class OhlcController extends FinancialController {

    updateElements(elements, start, count, mode) {
      const me = this;
      const dataset = me.getDataset();
      const ruler = me._ruler || me._getRuler();
      const firstOpts = me.resolveDataElementOptions(start, mode);
      const sharedOptions = me.getSharedOptions(firstOpts);
      const includeOptions = me.includeOptions(mode, sharedOptions);

      for (let i = 0; i < count; i++) {
        const options = sharedOptions || me.resolveDataElementOptions(i, mode);

        const baseProperties = me.calculateElementProperties(i, ruler, mode === 'reset', options);
        const properties = {
          ...baseProperties,
          datasetLabel: dataset.label || '',
          lineWidth: dataset.lineWidth,
          armLength: dataset.armLength,
          armLengthRatio: dataset.armLengthRatio,
          color: dataset.color,
        };

        if (includeOptions) {
          properties.options = options;
        }
        me.updateElement(elements[i], i, properties, mode);
      }
    }

  }

  OhlcController.id = 'ohlc';
  OhlcController.defaults = helpers.merge({
    dataElementType: OhlcElement.id,
    datasets: {
      barPercentage: 1.0,
      categoryPercentage: 1.0
    }
  }, chart_js.Chart.defaults.financial);

  chart_js.Chart.register(CandlestickController, OhlcController, CandlestickElement, OhlcElement);

})));


/************************ РЕАЛИЗАЦИЯ ************************/

var barCount = 60;
var initialDateStr = '01 Apr 2017 00:00 Z';

var ctx = document.getElementById('chart').getContext('2d');
ctx.canvas.width = 1000;
ctx.canvas.height = 250;

var barData = getRandomData(initialDateStr, barCount);
function lineData() { return barData.map(d => { return { x: d.x, y: d.c} }) };

var chart = new Chart(ctx, {
    type: 'candlestick',
    data: {
        datasets: [{
            label: 'CHRT - Chart.js Corporation',
            data: barData
        }]
    }
});

var getRandomInt = function(max) {
    return Math.floor(Math.random() * Math.floor(max));
};

function randomNumber(min, max) {
    return Math.random() * (max - min) + min;
}

function randomBar(date, lastClose) {
    var open = +randomNumber(lastClose * 0.95, lastClose * 1.05).toFixed(2);
    var close = +randomNumber(open * 0.95, open * 1.05).toFixed(2);
    var high = +randomNumber(Math.max(open, close), Math.max(open, close) * 1.1).toFixed(2);
    var low = +randomNumber(Math.min(open, close) * 0.9, Math.min(open, close)).toFixed(2);
    return {
        x: date.valueOf(),
        o: open,
        h: high,
        l: low,
        c: close
    };

}

function getRandomData(dateStr, count) {
    var date = luxon.DateTime.fromRFC2822(dateStr);
    var data = [randomBar(date, 30)];
    while (data.length < count) {
        date = date.plus({days: 1});
        if (date.weekday <= 5) {
            data.push(randomBar(date, data[data.length - 1].c));
        }
    }
    return data;
}

var update = function() {
    var dataset = chart.config.data.datasets[0];

    // candlestick vs ohlc
    var type = document.getElementById('type').value;
    dataset.type = type;

    // linear vs log
    var scaleType = document.getElementById('scale-type').value;
    chart.config.options.scales.y.type = scaleType;

    // color
    var colorScheme = document.getElementById('color-scheme').value;
    if (colorScheme === 'neon') {
        dataset.color = {
            up: '#01ff01',
            down: '#fe0000',
            unchanged: '#999',
        };
    } else {
        delete dataset.color;
    }

    // border
    var border = document.getElementById('border').value;
    var defaultOpts = Chart.defaults.elements[type];
    if (border === 'true') {
        dataset.borderColor = defaultOpts.borderColor;
    } else {
        dataset.borderColor = {
            up: defaultOpts.color.up,
            down: defaultOpts.color.down,
            unchanged: defaultOpts.color.up
        };
    }

    // mixed charts
    var mixed = document.getElementById('mixed').value;
    if(mixed === 'true') {
        chart.config.data.datasets = [
            {
                label: 'CHRT - Chart.js Corporation',
                data: barData
            },
            {
                label: 'Close price',
                type: 'line',
                data: lineData()
            }   
        ]
    }
    else {
        chart.config.data.datasets = [
            {
                label: 'CHRT - Chart.js Corporation',
                data: barData
            }   
        ]
    }

    chart.update();
};

document.getElementById('update').addEventListener('click', update);

document.getElementById('randomizeData').addEventListener('click', function() {
    barData = getRandomData(initialDateStr, barCount);
    update();
});
<meta name="viewport" content="width=device-width, initial-scale=1">

<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>

<div style="width:1000px">
  <canvas id="chart"></canvas>
</div>

<div>
  Bar Type:
  <select id="type">
    <option value="candlestick" selected>Candlestick</option>
    <option value="ohlc">OHLC</option>
  </select>
  Scale Type:
  <select id="scale-type">
    <option value="linear" selected>Linear</option>
    <option value="logarithmic">Logarithmic</option>
  </select>
  Scheme:
  <select id="color-scheme">
    <option value="muted" selected>Muted</option>
    <option value="neon">Neon</option>
  </select>
  Border:
  <select id="border">
    <option value="true" selected>Yes</option>
    <option value="false">No</option>
  </select>
  Mixed:
  <select id="mixed">
    <option value="true">Yes</option>
    <option value="false" selected>No</option>
  </select>
  <button id="update">Update</button>
  <button id="randomizeData">Rand Data</button>
</div>

Исходный код примера

Финансовый график

Документация по Chart.js

→ Ссылка