2020-03-28

Node.js x bitFlyer API #10 板情報の差分を取得・可視化

Realtime APIより板情報の差分を取得しvue-chart.jsにてどの程度まで可視化できるか考える。

Article Image

Node.jsで板情報の差分を取得する

板情報は数秒ごとに一覧が送られてくる「スナップショット」とは別に各プライスに変化があった時に随時送られてくる「差分」があることはご存知だと思う。

今までの実装ではスナップショットのみとなっていたが今回から差分を取得し、可視化を考えてみる。

また、差分と言えど1件づつ送られてくるわけではなくある程度まとまって送られてくる。

今回の完成品(動画)

調査内容

  1. vue-chart.jsがどれだけの描画頻度に耐えられるのか。実用的なのか。
  2. 送られてくるデータ量はどれぐらいなのか
  3. 可視化したデータを目視して分かることはあるか

概ねこのあたりに注目して進めた。

結論

わかったことを先に纏めておく。コードに興味ない場合はここだけ読めば良い。

板はおおよそ2000円幅に固まっている

4000円幅ぐらいまでは見ておいて損はないが2000円幅あたりのボリュームが最も多いように見える。板は遠くなるに従って急激に薄くなるところには注目したい。(正し統計的に数値を取ったわけではないので注意)

HFTは素人同然だが、高頻度BOTを作るのであればこのあたりのボリュームの統計は重要なのではないか。いずれテストしたい。

ありえないような位置に指値が入っている

恐らくCB-botのようなものだと思うが数十万離れた位置にポツんと指値が入っていることが多くしっかりこれも差分で送られてくる。

スナップショットだと表示されないので、離れている値は差分でしか取れないのではないかと考えている。

vue-chart.jsは負荷をかけるとブラウザが停止する

テストでは秒間おおよそ120-150件の差分データが入ってくることがわかった。

この長さのデータを常時更新して耐えるにはデータ項目400件程度に絞る必要があった。幅を大きく取りすぎるとデータが入っていない棒グラフ分も負荷がかかるようで画面がスカスカでもブラウザが停止する。

累積分布表の階級を10円幅にして中央値からプラスマイナス2000円以上はグラフをカットし(指値はほぼココに集中している)おおよそ400件でかろうじてデータ受信に合わせて同時更新が実現できた。

値幅を大きく表示したい場合は更新頻度を下げることによってある程度対応できることも分かっている。

これはマシンのスペックによって変わるかもしれないのであくまで参考程度に(i7-6700K グラボRX570にて検証)

vue-chart.jsでも使用に耐えるラインで可視化できた

現状の実装では負荷的にギリギリ合格点といったところだ。動画にて動作を確認してほしい。

スナップショットとマージせずとも板は描画できる

ものすごいスピードで板が更新されているのでスナップショットをわざわざマージせずとも数秒待っていれば自然と板の形が確認できる。ただ更新漏れが出てくる可能性があるため注意。

研究の過程

差分情報を生で表示してみる

差分を受信してみると分かるが結構な量が送られてくる。askとbit合わせて秒間およそ120-150件のデータ量だった。

どれぐらいの情報が入ってくるかを試すためNode.jsでconsole.logした画面を撮影した。

言うまでもないが人間の目視では価格をと厚みを追うことは到底不可能だろう。

視覚化の価値はある。

データ構造を変更する

前回までは送信されてきたデータをそのまま使用していたが頻繁にpriceを参照してsizeを設定するという動作のためデータ構造が非効率的なのではと考えデータ構造を変更することとした。

なぜ変更するのか

次はbitFlyerから送られてくるデータ構造

// 従来
{
  mid_price: 中央値,
  asks: [
    {price: 金額, size: サイズ },
    {price: 金額, size: サイズ },
    {price: 金額, size: サイズ }
    ...
  ]
  bids: [
    ...
  ]
}

よくある形ではあるが、asks と bids が配列になっている点に注目したい。

この構造ではとある価格のsizeを取りたい場合 data.asks[配列番号].size となる。

重要なのは配列番号と価格(price)は関係ないため、ここになんらかの要素番号計算か検索コードが必要となる。

前回までであれば入ってきたデータを元にそのままチャートに反映するだけでだったが、差分の場合は入ってきたデータと既存のデータにランダムアクセスしてマージする必要があるため特定のデータへアクセスしやすくする必要がある。

// 新
{
  mid_price: 中央値,
  asks {
    price: size,
    price: size,
    price: size,
    ...
  },
  bids {
    ...
  },
}

上のように構造を整形した。この場合は data.asks[価格] と入れてやるだけで参照できる。

またsizeが0となったデータについても delete 演算子で簡単に消去できるため効率的である。

このデータ構造の課題

いまのところ検証していないのだがデータ構造によってアルゴリズムの速度が変わる恐れがある。(配列をいちいち検索してデータを取得したほうが処理が軽い可能性)

これに関しては私の知見が少ない。今回は通信タイミングは多いもののデータ量としては少ないので無視とする。

整形処理のコード

次の実装にて整形した。

export const renewBoard = (message, board_data) => {
    board_data.mid_price = message.mid_price
    for (const ask of message.asks) {
        ask.size === 0 ? 
            delete board_data.asks[ask.price] : // sizeが0なら削除
            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
    }
    return board_data
}

message はbitFlyerから送られてくる生の差分データ。board_dataは単に整形データを格納しておく所。

messageで受け取ったデータを既存のboard_dataにマージしてreturnする構造だ。これで常にboard_dataが最新に更新されていく。

チャート用コードの修正

データ構造が変わるため既存のコード修正が必要だ。

今回コードがとても複雑になってしまったので解説はなし。askの板コードだけ提示しておく。

// 各板を昇順にソートする内部関数
const _sort = (message) => {
    const keys = Object.keys(message)
    keys.sort((a, b) => {
        // 昇順
        return a - b
    });
    let new_board = {}
    for (var i = 0; i < keys.length; i++) {
        new_board[keys[i]] = message[keys[i]]
    }
    return { new_board, min_price: parseInt(keys[0]) }
}

const askHistogram = (message, range, price_range) => {

    //asks(売り板)

    //price順で整列し直す(bitFlyerが順番を保証していないため)
    const sortedData = _sort(message.asks)
    const asks = sortedData.new_board

    //ヒストグラムを生成(階級幅:range)
    const min_price = sortedData.min_price //階級を確定するためには最小価格が必要
    let data = []
    for (const price in asks) {
        if (Math.abs(price - message.mid_price) <= price_range) {
            //階級=配列番号を計算
            const bin = Math.floor((price - min_price) / range)
            !data[bin] ? data[bin] = asks[price] : data[bin] = data[bin] + asks[price]
        }
    }

    //ラベルを設定
    let labels_sub = []
    for (let i = 0; i <= data.length - 1; i++) {
        const label = min_price + (range * i)
        labels_sub.push(label)

        //累積度数化
        if (!data[i]) { data[i] = 0 }
        i != 0 ? data[i] = data[i - 1] + data[i] : null
    }

    //配列を切り詰める
    const labels = labels_sub.filter((d) => {
        return Math.abs(d - message.mid_price) <= price_range
    })
    data.splice(0, data.length - labels.length)

    return { labels, data }
}

さいごに

処理の負荷でブラウザが停止するため調整が必要で、更に表示を絞るコード実装で思ったより時間を消費してしまった。

このようなパズルゲームをやっているかのようなコーディングはIQが試される所だと思う。圧倒的に私には不利に感じた。

また稼働し続けていると中央のグラフが描画されなくなっていく不具合が確認されている。今回はスナップショットとマージしていないので、そのあたりの更新漏れかと考えている。

余談:タイトルの体裁を変更

タイトルが長くなってしまいがちだったのでシンプルにしていくことにした。



この記事をシェア


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