2020-04-20

【謎のチャート】Tickerの遅延を計測【Vue.js】

高頻度Botterなら必ず処理する遅延計測。今回はその入口を探る。

Article Image

実装サンプル

今回は次のような表示で遅延を可視化をした。

※めんどいのでTickerしか実装していない。

遅延とは

サーバー上の取引データが確定してからAPIを通してローカルPCが処理できるまでに必ず遅延が発生する。

bitFlyerではこの遅延が30秒を超えることもあるらしく高頻度の取引のみならず安定した取引のために多くのBotterがチェックしている数値だと思われる。

では、どうやって遅延を取得すればよいのだろうか。

ローカルの時計との差

遅延とははローカルPCの時間とbitFlyerから送られてくるメッセージに入っているtimestampの差である。

遅延はその仕組み上必ず発生するのでメッセージに刻印されている時刻はローカルの時刻より僅かに早い時刻を示しているはずである。

つまり ローカル時刻 - メッセージのタイムスタンプ = 遅延 であると言える。

遅延の種類

メッセージにサーバー側で押されるタイムスタンプが入っていれば遅延は計測できる。

よってbitFlyerでは次の遅延を計測できる。

  • Ticker
  • 約定
  • 注文イベント(Private Channel)

また板情報に関してはTimestampが入っていないので遅延が取れないということも覚えておきたい。

補足:Realtime APIではなくHTTP APIでは「板の状態」としてbusy等の混雑状況が取得できるので、単純に注文が通りやすいかどうかを判断したい場合はそちらのほうが便利かもしれない。

遅延の誤差

遅延計測に問題点がある。それはローカルPCの時刻が正確でない場合だ。

意外に知られていないがWindowsの時刻同期はなんと2秒(2000ミリセカンド)の誤差を許容している。そして何度同期しても2秒以内の誤差であれば補正は実行されない。

つまり最大2000ミリ秒のズレが発生する。実際の遅延は軽い時間帯であれば数十ミリ秒に収まるので、これに2000ミリ秒の誤差が加わってしまうと正確な遅延が把握できない。

AWS等内部時計にある程度信頼が置けるサーバーを利用している場合を除いて、ローカルから遅延を計測する場合は正確な内部時計の補正が必要だ。

余談:Linuxの時間って正確なの?

サーバー関連のプロではないのでわからない。同じPCでVMWareを使ってLinuxからTime.isにアクセスしてみた。

2020 04 20 23h37 28

差が完全に0秒になっている。もしかしたら凄いのかもしれない。

同じマシン上のWindowsは次のようになっている。

2020 04 20 23h41 17

赤枠の箇所を見ると僅かにズレている。Linuxはもしかしたら凄いのかもしれない(二度目)

時計設定アプリ

今回は「iネッ時計 ~インターネット時刻補正~」というソフトウェアを利用させていただいた。

このアプリは正常に動作するが2008年から更新されておらず、また当時からスパイウェア扱いをされていたJWord(JWord自体のサービスも終了している)のプラグインのインストールを進めてくるためWindows Defender他で一部がブロックされる。この理由でリンクはコチラからは貼らないことにした。

私の環境ではウィルス対策ソフトにより自動的にインストール用の実行ファイルが排除されるためインストール中にエラーが出るが問題なくインストールが可能である。

外部アプリに頼らない対応

上記の理由もあり外部アプリに頼らず遅延計測するためTime.isから得られる情報をユーザーが手入力して補正できるようおまけ機能を付けた。Time.isは内部時計がどれだけズレているかを正確に確認できるサイトだ。残念ながら自動で内部時計を調整する機能などはないようだ(npmパッケージなども検索したが便利なツールはなかった)

所感

bitFlyerの遅延に対する不平不満は至るところで聞くためかなり多くのBotterが実装していると考えている。一方で遅延計測に関して言及しているユーザーは少ない。

今回はめんどいのでTickerの線グラフのみとなったが誤差補正を正確に行いInfluxDBに記録したデータを元に時間帯別遅延ヒストグラム等作ってみたら面白いかもしれない。

現状公開中の「謎のチャート」には実装していない。

コード

役に立つかわからないがコードを張っておく。何かの参考になれば幸いだ。

delayChart.js

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

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

export const renewDelay = (message, datacollection, maxCnt, offset) => {

    offset = offset ? offset : 0

    //ラベルは「時:分:秒」で表示
    const date = new Date(message.timestamp)
    const now = new Date(Date.now() + offset * 1000) //offsetを加算してやると正しい時刻になる
    const delay_ticker = now - date
    const time = date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds()
    
    //data
    datacollection.labels ? datacollection.labels.push(time) : console.log("error: labels dosen't exists")
    if (datacollection.datasets) {
        datacollection.datasets[0].data.push(delay_ticker)
    } else {
        console.log("error: datasets dosen't exists")
    }

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

    return {
        labels: datacollection.labels,
        datasets: [
            {
                label: "Ticker Delay",
                data: datacollection.datasets[0].data,
                fill: false,
                borderColor: "green"
            },
        ]
    }
}

Delay.vue

※謎のチャート上で整理するため子コンポーネント化し親側から呼び出してチャートを貼り付ける仕様とした。

<template>
  <div>
    <p>Delay: {{delay_time}}ms</p>
    <DelayChart :chart-data="datacollection" :options="options" />

    <!-- options -->
    <v-expansion-panels>
      <v-expansion-panel>
        <v-expansion-panel-header>Options(Delay))</v-expansion-panel-header>
        <v-expansion-panel-content>
          <v-switch v-model="delay_on" :label="delay_on ? 'ON' : 'OFF'"></v-switch>
          <v-slider
            v-model="plot_max"
            label="X軸の表示件数"
            step="1"
            min="1"
            max="1000"
            thumb-label="always"
            ticks
          ></v-slider>
          <v-row>
            <v-col cols="6">
              <v-text-field v-model="offset" label="誤差補正" suffix="秒" :rules="[numberRule]"></v-text-field>
            </v-col>
            <v-col cols="6">
              <v-radio-group v-model="isLocalDelay" :mandatory="true" row>
                <v-radio label="遅" :value="true"></v-radio>
                <v-radio label="進" :value="false"></v-radio>
              </v-radio-group>
            </v-col><a href="https://time.is/">Time.is</a>を参考にするのがおすすめ
          </v-row>
        </v-expansion-panel-content>
      </v-expansion-panel>
    </v-expansion-panels>
  </div>
</template>

<script>
import DelayChart, { renewDelay } from "./DelayChart.js";

export default {
  components: {
    DelayChart
  },
  props: ["message"],
  data: () => ({
    numberRule: v => {
      if (!v.trim()) return true;
      if (!isNaN(parseFloat(v)) && v >= 0) return true;
      return "0以上の数値を入力して下さい。";
    },
    offset: 0.0,
    isLocalDelay: true,
    delay_on: true,
    plot_max: 20,
    datacollection: null,
    options: null,
    subcollection: {
      labels: [0],
      datasets: [
        {
          label: "Ticker Delay",
          data: [0],
          fill: false,
          borderColor: "green"
        }
      ]
    }
  }),
  methods: {
    fillData() {
      this.options = {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          yAxes: [
            {
              position: "right"
            }
          ]
        }
      };
    }
  },
  computed: {
    delay_time() {
      const offset =
        this.isLocalDelay === true ? this.offset : this.offset * -1;
      const date = new Date(Date.now() + offset * 1000);
      const stamp = new Date(this.message.timestamp);
      const delay = date - stamp;
      return delay;
    }
  },
  watch: {
    message() {
      const offset =
        this.isLocalDelay === true ? this.offset : this.offset * -1;
      if (this.delay_on) {
        this.datacollection = renewDelay(
          this.message,
          this.subcollection,
          this.plot_max,
          offset
        );
      }
    }
  },
  mounted() {
    this.fillData();
  }
};
</script>

覚書

vue-chart.jsで良く間違える箇所

  • datacollection = 複製のdatacollection のコードをを入れないとリアクティブにならない

  • optionsはdatacollectionが初回描画されて初めて適応される

  • javascriptでのミリ秒計算はDate.now()

  • 次のエラーは解決に至っていない

    2020/04/22: 【vue-chart.js】Invalid propエラーを解決! にて解決した。

    webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:620 [Vue warn]: Invalid prop: type check failed for prop "chartData". Expected Object, got Null



この記事をシェア


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