[Rails]D3.js, Techan.js, APIを使って仮想通貨のローソク足チャートを描画する

仮想通貨取引所で公開されているAPIを利用することで、様々な情報を取得することが可能です。
[以前の記事](https://qiita.com/akkie/items/fdfebd29833f8c1a4c30)では現在の価格を取得する方法を紹介しましたが、今回はローソク足チャートを描画する方法を紹介します。

## 使用するAPI
bitbankのAPIを使用します。
[bitbank.cc API ドキュメント](https://docs.bitbank.cc/)

ローソク足の情報を取得する場合は上記のページのCandlestickを利用します。
例えば、2019年のビットコインの日足チャートを取得する場合は以下のurlにリクエストを送ります。
https://public.bitbank.cc/btc_jpy/candlestick/1day/2019
すると、以下のようなレスポンスが返ってきます。

```json
{
  "success":1,
  "data":{ 
    "candlestick":[{
      "type":"1day",
      "ohlcv":[
        ["406990","420900","400160","419653","1461.5678",1546300800000],
        ["419600","425005","412002","417738","2749.5887",1546387200000],
        ・・・
      ]
    }],
    "timestamp":1562930808433
  }
}
```

ohlcvに入っている配列がローソク足チャートに必要な情報です。
`[始値, 高値, 安値, 終値, 出来高, UnixTime]`

## 環境

### バージョン
```terminal
$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin18]
$ rails -v
Rails 5.2.3
```

### 導入gem

```ruby
gem 'bootstrap', '~> 4.3.1'
gem 'jquery-rails'
gem 'haml-rails'
```
今回はjQueryを使用してAjax通信をします。また、レイアウトを整えるためにBootstrapを入れています。ビューにはhamlを採用しています。

### ライブラリの読み込み

ローソク足チャートを描画するためにjavascriptのライブラリであるD3.jsと、D3.jsのファイナンス用ライブラリであるTechan.jsをCDNで読み込みます。

headタグの中に以下を追記します。

```ruby:application.html.haml
  %script{src: "https://d3js.org/d3.v4.min.js", type: "text/javascript"}
  %script{src: "http://techanjs.org/techan.min.js", type: "text/javascript"}
```

## ルーティング
[以前の記事](https://qiita.com/akkie/items/fdfebd29833f8c1a4c30#ルーティング)と同様にクロスドメイン制約を回避するためにコントローラー経由でAPIサーバーにリクエストを送ります。
なので、今回は名前空間を使って以下のようなルーティングにします。

```ruby:routes.rb
Rails.application.routes.draw do
  root 'money#show'
  resources :money, only: :show
  namespace :api do
    resources :money, only: :show, defaults: { format: 'json' }
  end
end
```

## ビュー

moneyコントローラーのshowアクションにチャートを表示します。
日足・５分足・１分足チャートを表示するため以下のようなビューにし、`card-body`にチャートを挿入します。

```haml:views/money/show.html.haml
#candlestick-1day-container.container.mt-3
  .row
    .col-12.px-0
      %h5.text-right ＜単位：日足＞
      .card
        .card-body.candlestick-1day

#candlestick-5min-container.container.mt-3
  .row
    .col-12.px-0
      %h5.text-right ＜単位：5分足＞
      .card
        .card-body.candlestick-5min

#candlestick-1min-container.container.mt-3
  .row
    .col-12.px-0
      %h5.text-right ＜単位：1分足＞
      .card
        .card-body.candlestick-1min

```

scssでチャートのフォントサイズと色を調整します。

```scss:candlestick.scss
svg text {
  font-size: 14px;
}
path.candle {
  stroke: #000000;
}
path.candle.body {
  stroke-width: 0;
}
path.candle.up {
  fill: #00AA00;
  stroke: #00AA00;
}
path.candle.down {
  fill: #FF0000;
  stroke: #FF0000;
}
path.volume {
  fill: #696969;
  stroke-width: 1;
}
.sma path.line {
  fill: none;
  stroke-width: 1;
}
.ma25 path.line {
  stroke: #0000cd;
}
```

## コントローラー
controllersフォルダ直下のコントローラーとcontrollers/apiフォルダのコントローラーは以下のようになります。

```ruby:controllers/money_controller.rb
class MoneyController < ApplicationController
  def show
  end
end
```
今回はcontrollersフォルダ直下のコントローラーでは何もしません。適宜処理を追加してください。

```ruby:controllers/api/money_controller.rb
class Api::MoneyController < ApplicationController
  # 今回の場合 params[:id] => "btc"
  # jsファイルからリクエストする時にparams[:candleType]に1day、1min、5minを入れる
  def show
    # 日足と分足ではリクエストするurlが変わるので条件分岐してurlをつくる
    if params[:candleType] == "1day"
      # 例） ymd => "2019"
      ymd = Date.today.strftime("%Y")
    elsif params[:candleType] == "1min" || params[:candleType] == "5min"
      # 例) ymd => "20190712"
      ymd = Date.today.strftime("%Y%m%d")
    end
    url = URI.parse("https://public.bitbank.cc/#{params[:id]}_jpy/candlestick/#{params[:candleType]}/#{ymd}")
    response_text = Net::HTTP.get(url)
    response_hash = JSON.parse(response_text)
    render json: response_hash
  end
end

```

## jsファイル

```javascript:application.js
//= require rails-ujs
//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require_tree .
```

bootstrapがレスポンシブ対応しているので、それに合わせてリサイズした時にもチャートを描画するようにします。

```javascript:candlestick.js
$(function() {

  //unixTimeをymd形式に変換する関数
  function unixTime2ymd(intTime){
    let d = new Date( intTime );
    let year  = d.getFullYear();
    let month = d.getMonth() + 1;
    let day  = d.getDate();
    let hour = ( '0' + d.getHours() ).slice(-2);
    let min  = ( '0' + d.getMinutes() ).slice(-2);
    return( year + '-' + month + '-' + day + " " + hour + ":" + min);
  };

  // ローソクチャートを表示する関数
  // candletypeには 1min 5min 1dayが選べる
  function displayCandlestick(data, candleType) {

    // グラフを挿入するエリア（card）のwidthを取得,（今回、heightはwidthの8/5に設定）
    let cardWidth = $(`.candlestick-${candleType}`).width();
    let cardHeight = cardWidth / 8 * 5;

    // グラフの領域とマージンを設定
    let margin = {top: 5, right: 5, bottom: 30, left: 80};
    let width = cardWidth - margin.left - margin.right;
    let height = cardHeight - margin.top - margin.bottom;

    // %Y-%m-%d %H:%M の形式のデータを受け取りパースするための変数
    let parseDate = d3.timeParse("%Y-%m-%d %H:%M");

    // グラフ領域のうちx軸が占めるwidthを設定(今回は0~width)
    let x = techan.scale.financetime()
            .range([0, width]);

    // グラフ領域のうちy軸が占めるheightを設定
    // レートと出来高を7対3の比率に設定
    let y = d3.scaleLinear()
            .range([height * 7 / 10, 0]);
    let yVolume = d3.scaleLinear()
            .range([height, height * 7 / 10]);

    // ローソク足チャートをcandlestickとして定義
    let candlestick = techan.plot.candlestick()
            .xScale(x)
            .yScale(y);

    // 日足チャートの場合x軸の定義
    if (candleType == "1day") {
      var xAxis = d3.axisBottom()
            .scale(x)
            .tickFormat(d3.timeFormat("%b")) // 日足なので、月(略称)表示にする
            .ticks(width/90); // 何データずつメモリ表示するか(レスポンシブ対応するためwidthによって変わるようにする)
    // 分足チャートの場合x軸の定義
    } else {
      var xAxis = d3.axisBottom()
            .scale(x)
            .tickFormat(d3.timeFormat("%H:%M")) // 分足なので、時：分表示にする
            .ticks(width/90); // 何データずつメモリ表示するか
    }

    // y軸(レート)の定義
    let yAxis = d3.axisLeft()
            .scale(y)
            .ticks(height/70); // 何データずつメモリ表示するか

    // 移動平均線をsmaとして定義(単純移動平均線 SMA: Simple Moving Average)
    let sma = techan.plot.sma()
            .xScale(x)
            .yScale(y);

    // y軸(出来高)の定義
    let volume = techan.plot.volume()
            .xScale(x)
            .yScale(yVolume);

    // svgの挿入（既存のチャートを削除してから挿入）
    $(`.candlestick-${candleType}`).children("svg").remove();
    let svg = d3.select(`.candlestick-${candleType}`)
            .append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .append("g")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // 日時で並べ替えを行う
    let cAccessor = candlestick.accessor();
    let vAccessor = volume.accessor();
    // 最大180個のデータ数を表示する
    if (data.length > 180) {
      var first_data = data.length - 180;
    } else {
      var first_data = 0;
    }
    data = data.slice(first_data, data.length).map(function(d) {
      return {
          date: parseDate(d.Date),
          open: +d.Open,
          high: +d.High,
          low: +d.Low,
          close: +d.Close,
          volume: +d.Volume
      };
    }).sort(function(a, b) { return d3.ascending(cAccessor.d(a), cAccessor.d(b)); });
    // 描画関数
    x.domain(data.map(cAccessor.d));
    y.domain(techan.scale.plot.ohlc(data, cAccessor).domain());
    yVolume.domain(techan.scale.plot.volume(data, vAccessor.v).domain());

    // 出来高を挿入する
    svg.append("g")
            .attr("class", "volume")
            .data([data])
            .call(volume);
    // ローソク足を挿入する
    svg.append("g")
            .attr("class", "candlestick")
            .data([data])
            .call(candlestick);
    // 移動平均線（データ数25）を追加する
    svg.append("g")
            .attr("class", "sma ma25")
            .datum(techan.indicator.sma().period(25)(data))
            .call(sma);
    // x軸を追加する
    svg.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + height + ")")
            .call(xAxis);
    // y軸（出来高）を追加する
    svg.append("g")
            .call(d3.axisLeft(yVolume)
            .ticks(height/150) // 何データずつメモリ表示するか
            .tickFormat(d3.format(",.3s")));
    // y軸（レート）を追加する
    svg.append("g")
            .attr("class", "y axis")
            .call(yAxis);
    // y軸のラベルを追加する
    svg.append("g")
            .append("text")
            .attr("transform", "rotate(-90)") // Y軸ラベルを縦書きに
            .attr("y", 15) // 位置調整
            .style("text-anchor", "end") // テキスト開始位置
            .text("価格 (円)");
  }

  // APIから情報を取得し、内部でdisplayCandlestickを呼び出す関数
  // candleTypeには 1min 5min 1dayが選べる
  function getAPIAndDisplayCandlestick(candleType) {
    // コントローラーにリクエストを送る
    let requestUrl = "/api/money/btc";
    $.ajax({
      url: requestUrl,
      type: 'get',
      dataType: 'json',
      data: { candleType: candleType }
    })
    .done(function(json) {
      // ローソク足チャートに必要な情報を取り出す
      let row_data = json.data.candlestick[0].ohlcv
      // 配列dataに連想配列としてデータを入れる
      let data = [];
      row_data.forEach(function(datum) {
        let open = datum[0];
        let high = datum[1];
        let low = datum[2];
        let close = datum[3];
        let volume = datum[4];
        let date = unixTime2ymd(datum[5])
        let modifiedDatum = { Date: date, Open: open, High: high, Low: low, Close: close, Volume: volume };
        data.push(modifiedDatum);
      });
      // 画面を読み込んだ時に発火する
      displayCandlestick(data, candleType);
      // 画面をリサイズした時に発火する
      $(window).on("resize", function() {
        displayCandlestick(data, candleType);
      });
    })
    .fail(function() {
      alert('error');
    });
  }

  // 日足チャートを描画する
  getAPIAndDisplayCandlestick("1day");
  // ５分足チャートを描画する
  getAPIAndDisplayCandlestick("5min");
  // 1分足チャートを描画する
  getAPIAndDisplayCandlestick("1min");
});

```

これで、以下のように三種類のローソク足チャートが表示されるはずです。
<img width="994" alt="スクリーンショット 2019-07-12 21.33.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/400652/32f111f1-1b6f-93e6-3da5-fc416d521f73.png">
<img width="996" alt="スクリーンショット 2019-07-12 21.34.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/400652/cd8c9858-673d-dfe3-1790-ee566c0b33cb.png">
<img width="997" alt="スクリーンショット 2019-07-12 21.34.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/400652/480b846f-36a8-335b-d5dd-12a1173b7e55.png">

## 最後に
今回は仮想通貨のローソク足チャートを描画してみました。
techan.jsのサンプルを見ると、ドラッグでチャートを動かしたりズームしたりする機能も追加できるようです。D3.jsとtechan.jsはいろいろできて便利ですね。
ただし、自分自身がD3.jsとtechan.jsについて理解していない部分があり、参考サイトからそのままコピーしたところも多々あります。
もしかしたらおかしな部分があるかもしれません。間違っているところがあればご指摘お願いします。

## 参考
[bitbank.cc API](https://docs.bitbank.cc/)
[d3.js](https://d3js.org/)
[TechanJS Candlestick](http://bl.ocks.org/andredumas/27c4a333b0e0813e093d)
[【Python】D3.js でWEBページにローソク足チャートを描画する](https://www.doraxdora.com/blog/2018/05/20/post-4694/)