2020-04-28

【謎のチャート】bFのスプレッドを可視化【完成版】

前回のスナップショットからスプレッドを生成するコードに差分を追加した。スプレッドの変化が最速で目視できる。

Article Image

実際に動くデモ

https://www.ultra-noob.com/vuedemos/2020-04-28_spread_demo/

※実際にbitFlyerに接続してリアルタイムにデータを受信する

映像デモ

アルゴリズム

  1. 差分とスナップショットを受信しPC内部で板を構築
  2. Best Ask - Best Bid によりスプレッドを計算
  3. プロット(スプレッドに変化がなくてもデータが1件くるごとに1プロット)

スプレッドを算出する上での考察

差分の更新情報は非常に多くBest Bit, Best Ask付近のみであればスナップショットをマージせず構築できてしまうのではと考えていたがこの実装ではスプレッドが異常値(マイナス)を示すことが散見された。

そのためスナップショット受信時にPC内部に蓄積しているデータを一旦消去し、スナップショットの情報だけで新規板とすると殆どエラーは出なくなった。

これはスナップショットが更新されるタイミングで、スナップショット情報に本来差分で送信される分が含まれておりこのタイミングの差分情報は送信されていないのでは、というのが私の考えである。

つまり、差分のみ受信(スナップショットを無視)していると板のデータは崩れると考えているが、100%正しいとは言い切れないので知見があれば教えていただければと思う。

データの送受信が早いので通信の関係で差分情報が欠損している可能性もある。

データ構造

今回もこれまで板情報をデプスチャート化したときのデータと同じ物を利用した。

bitFlyerからは {price: 価格, size: サイズ }の配列が送られてくるが { 価格 : サイズ } に変換しており、高値・安値の調査とその数量をコード上処理しやすくしている。

この構造で処理速度の問題はあまり感じていない。

余談

この実装に合わせて謎のチャートに実装されているデプスチャートの内部ロジックもスナップショットで逐次リセットされるように修正した。

所感

板情報を目視していると確かに数百円のスプレッドは頻繁に起こる。が、最速であるはずの差分更新タイミングで見ても一瞬で埋まるようにも見える。

この程度では1ミリもマーケットメイク出来るようには見えない。更に上を目指すにはどのような処理をすればよいのだろうか。

凄腕botterと比べるとまだ入り口にも立っていない印象だ。

以下コード

vue.jsおよびvue-chart.jsのコードのため非常に理解しにくいが何かの参考になるかもしれないので公開しておく。

コンポーネント

Spread.vue

<template>
  <div>
    <SpreadChart :chart-data="datacollection" :options="options" />
  </div>
</template>

<script>
import SpreadChart, { renewSpread } from "./SpreadChart.js";

export default {
  components: {
    SpreadChart
  },
  props: ["boardMessage"],
  data: () => ({
    datacollection: {
      labels: [],
      datasets: [
        {
          label: [],
          data: []
        }
      ]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      scales: {
        yAxes: [
          {
            position: "right"
          }
        ]
      }
    },
    maxCnt: 30
  }),
  methods: {},
  watch: {
    boardMessage() {
      this.datacollection = renewSpread(
        this.boardMessage,
        this.datacollection,
        this.maxCnt
      );
    }
  }
};
</script>

vue-chart.js用

SpreadChart.js

import { Line, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins

export default {
    mixins: [Line, reactiveProp],
    props: ['options'],
    mounted() {
        this.renderChart(this.data, this.options)
    }
}

// bidsとasksが{ 価格: 数量 }の構造版
export const renewSpread = (message, datacollection, maxCnt) => {
    let best_bid_price = null
    let best_ask_price = null

    //bid max
    for (const d in message.bids) {
        if (best_bid_price === null) {
            best_bid_price = d
        } else {
            best_bid_price = d > best_bid_price ? d : best_bid_price
        }
    }
    //asks min
    for (const d in message.asks) {
        if (best_ask_price === null) {
            best_ask_price = d
        } else {
            best_ask_price = d < best_ask_price ? d : best_ask_price
        }
    }

    const spread = best_ask_price - best_bid_price
    if (spread < 0) {
        console.log(`Warning: Minus Spread ${spread}`);
    }

    //ラベルは「時:分:秒」で表示
    const date = new Date();
    const time =
        date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds()


    datacollection.labels.push(time);
    datacollection.datasets[0].data.push(spread)

    //画面に表示する件数を超えたらshiftで削る
    while (datacollection.labels.length > maxCnt) {
        datacollection.labels.shift()
        datacollection.datasets.forEach((array) => {
            array.data.shift()
        })
    }

    const return_datacollection = {
        labels: datacollection.labels,
        datasets: [
            {
                label: "Spread",
                data: datacollection.datasets[0].data,
                fill: false,
                borderColor: "blue"
            },
        ]
    }

    return return_datacollection

}

親コンポーネント

※一部コードのみ

  //スナップショット
  this.socket.on("lightning_board_snapshot_FX_BTC_JPY", message => {
    this.board_data = renewBoard(message, this.board_data, true)
  });

  //差分
  this.socket.on("lightning_board_FX_BTC_JPY", message => {
    this.board_data = renewBoard(message, this.board_data) //差分を挿入or削除(ソートされていない)
  });

APIから受け取ったmessageをrenewBoard関数を通してPC内部に板を構築している

第三引数のtrueはスナップショットの合図(内部板をリセットするフラグ)

板作成用関数 renewBoard

//データの挿入と削除
export const renewBoard = (message, board_data, isSnapshot) => {
    board_data.mid_price = message.mid_price
    if (isSnapshot) { //スナップショットで板をリセット
        board_data.asks = {}
        board_data.bids = {}
        console.log("info: Snapshot Received")
    }
    
    for (const ask of message.asks) {
        ask.size === 0 ?
            delete board_data.asks[ask.price] :
            board_data.asks[ask.price] = ask.size
    }
    for (const bid of message.bids) {
        bid.size === 0 ?
            delete board_data.bids[bid.price] :
            board_data.bids[bid.price] = bid.size
    }

    //リアクティブ用に新しいオブジェクトを生成
    const return_board_data = {
        mid_price: board_data.mid_price,
        asks: board_data.asks,
        bids: board_data.bids
    }

    return return_board_data
}


この記事をシェア


謎の技術研究部 (謎技研)