2020-05-02

【node.js x bitFlyer】約定データから秒足をつくる

どうやって秒足を生成するのだろうか。Pythonと違って良さげなライブラリは無いので自前で色々やってみた。コードは私の個人的な実験のため無料で公開する。

Article Image

新バージョンあり

2020/05/03更新

一部バグと機能追加の新v0.1.0こちらのページ

無料

そろそろバックテストに使えそうなコードを少しづつ研究していきたいと思うが、このあたりからnoteでも有料コードが多くなってくる印象だ。

とはいえそれも2019までの話だろう。

2020からは私が無料で公開していく。

(と言いつつ、実験用に作ったコードなので売り物にしにくいだけ)

目標

約定データから5秒足を生成する。

パラメータを変化させれば2秒足や10秒足のように自由に設定できる

(2.3秒足等60を割り切れない足の挙動はテストしていない)

今回は可視化ではないのでチャートは描画しない。

前提

過去の約定データを自前のInfluxDBから取得している。データソースは各自で実装が違うと思われるので無視でよい。

便利なチャート系ライブラリは一切使用しない。

日付計算用で dayjs を使用している。

完成品の実行結果

1分間の5秒足を作ってみた。結果が次。

スタート時間: Sat, 02 May 2020 12:32:20 GMT
[O: 967039][H: 967155][L: 966395][C: 966411] index: 0
[O: 966411][H: 966611][L: 966117][C: 966126] index: 1
[O: 966248][H: 966755][L: 966248][C: 966726] index: 2
[O: 966705][H: 966705][L: 966106][C: 966148] index: 3
[O: 966131][H: 966299][L: 965796][C: 965817] index: 4
[O: 965886][H: 966304][L: 965817][C: 966050] index: 5
[O: 966051][H: 966420][L: 966006][C: 966194] index: 6
[O: 966217][H: 966659][L: 966217][C: 966421] index: 7
[O: 966420][H: 966699][L: 966200][C: 966429] index: 8
[O: 966200][H: 967062][L: 966182][C: 966892] index: 9
[O: 966806][H: 966806][L: 966763][C: 966801] index: 10
--- Report ---
Execution time: 0s 92.076201ms

まだテスト段階のため余計なindexなども表示している。

エラー時は次のようにレポート部に出力される

--- Report ---
info: index211 dosen't exists.

余談:タイムスタンプ

DBに記録したタイムスタンプと約定に記録されている「exec_date」どちらを使うか。

これは「exec_date」を利用すべきだろう。

DBに記録したタイムスタンプは私だけの時間(遅延を含んでいる)がexec_dateに記されているデータは誰が持っているデータも共通だからだ。

約定データの構造

約定データの構造を確認しておく。

次は公式サイトから引用した例である。

[
  {
    "id": 39361,
    "side": "SELL",
    "price": 35100,
    "size": 0.01,
    "exec_date": "2015-07-07T10:44:33.547Z",
    "buy_child_order_acceptance_id": "JRF20150707-014356-184990",
    "sell_child_order_acceptance_id": "JRF20150707-104433-186048"
  }
]

初心者が覚えておくべきこと

side: "SELL" とはどういう意味だろうか。

「売り板が約定した」or「成り行きで売りが入った」どちらなのだろうか。

同サイトにて解説がされている。

side: この約定を発生させた注文 (テイカー) の売買種別です。板寄せ時に約定した場合は、空文字列となります。

つまり「成行でSELL」されたということだ。板から見れば「買い板(bid)が消費された」と考えれば良い。

秒足のデータ構造

内部で処理するデータ構造はどうすべきか。

最初に思いついたのは連想配列でキー名をYYYYMMDDHHMM として内側に 60秒/n秒 の配列。

これはだめだ。なぜなら連想配列はキー名でソートされる保証がないためイテレータによる足の出力がバラバラになる可能性がある。

キー名用配列を別で用意しても良いが冗長で処理コストも大きくなりそうなので却下したが、コード上の取り扱いは楽になるかもしれない。

というわけで親を配列構造 [] にし、要素に連想配列{OHLC+Oの時刻+Cの時刻}とする。

Pythonによるチャートデータの取り扱いはPandasがあるから便利そうだ。

JavaScriptでも調べたがデファクトスタンダード的な物は見つからなかった。

配列の要素番号計算が必要

配列の構造なので要素番号の計算が必要だ。

更に悪いことに日付ベースの計算になるので非常に面倒だ。

今回は少しでも時間計算を分かりやすくするためdayjsを利用した。

処理時間

24時間分のデータを処理してみた。

Execution time: 35s 491.3097ms

約35秒かかった。

これは殆どがデータサーバーから約定データをSELECTしてくるのに必要な時間だ。

サーバーはローカルネットワーク上の古いPC。

このスピードが早いかどうかは不明。

このデータ利用時の注意

短い秒足だとデータが存在しないレコードが出るので、この処理で生成した足データを再利用する場合は考慮が必要。

メンテや記録エラーでも発生すると思われる。

所感

本日頭痛のため非常に作業効率が悪かった。

こんな状況で要素番号の計算は最悪だ。

とはいえバックテスト最初のワンステップでもあるので大切にしたい回だ。

秒足は正直まだ実験段階でコードの完成度も低いがこれから少しづつ洗練させていきたい。

以下コード

無料だからという免罪符を使うわけではないが、詳しくは説明しない。

また私のInfluxDB関係のコードは無視するか、過去記事を読んで頂く必要がある(一応全部書いてあるはずだ)

import Config from '../utils/config.mjs'
import DB from './classes.mjs'
import dayjs from 'dayjs'

//------------------------------------------
// 5秒足作成
//------------------------------------------

const config = new Config('../config/config.yaml') //DB用設定の読み込み
const use_validation = true //生成された足に不整合がないか検証する

// DB - 出力される側
const remoteDB = new DB({
    host: config.db_host_remote,
    username: config.db_username,
    password: config.db_password,
}).influx

const sec_time_span = 5 //秒足の秒数

const process_time_start = process.hrtime()
remoteDB.query(`SELECT * FROM "bitFlyer_db"."autogen"."lightning_executions_FX_BTC_JPY" WHERE time > now() - 1m`)
    .then((res) => {
        chandleMain(res)
    }).catch((err) => {
        console.log(err)
    })

const chandleMain = (data) => {
    const start_time = caliculateStartTime(data)
    let candle = [] //足が格納される箱
    let errors = [] //エラー報告用
    for (const d of data) {
        const arrayNum = caliculateArrayNum(start_time, d.exec_date)
        //要素を新規
        if (!candle[arrayNum]) {
            candle[arrayNum] = {}
        }

        //Oを生成
        if (candle[arrayNum].O_date) {
            //より早いレコードが入ってきた場合oを更新
            if (candle[arrayNum].O_date > dayjs(d.exec_date)) {
                candle[arrayNum].O_date = dayjs(d.exec_date)
                candle[arrayNum].O = d.price
            }
        } else { //新規
            candle[arrayNum].O_date = dayjs(d.exec_date)
            candle[arrayNum].O = d.price
        }

        //Hを生成
        if (candle[arrayNum].H) {
            if (candle[arrayNum].H < d.price) {
                candle[arrayNum].H = d.price
            }
        } else { //新規
            candle[arrayNum].H = d.price
        }

        //Lを生成
        if (candle[arrayNum].L) {
            if (candle[arrayNum].L > d.price) {
                candle[arrayNum].L = d.price
            }
        } else { //新規
            candle[arrayNum].L = d.price
        }

        //Cを生成
        if (candle[arrayNum].C_date) {
            //より遅いレコードが入ってきた場合Cを更新
            if (candle[arrayNum].C_date < dayjs(d.exec_date)) {
                candle[arrayNum].C_date = dayjs(d.exec_date)
                candle[arrayNum].C = d.price
            }
        } else { //新規
            candle[arrayNum].C_date = dayjs(d.exec_date)
            candle[arrayNum].C = d.price
        }
    }

    //データを表示
    candle.map((c, index, array) => {
        //不整合がないか検証
        if (use_validation) { validation({ c, index, array }) }

        //出力
        console.log(`[O: ${c.O}][H: ${c.H}][L: ${c.L}][C: ${c.C}] index: ${index}`)
    })

    //レポート
    console.log("--- Report ---")
    for (const e of errors) {
        console.log(e)
    }

    //処理時間計測
    const performance_time_end = process.hrtime(process_time_start)
    console.log(`Execution time: ${performance_time_end[0]}s ${performance_time_end[1] / 1000000}ms`)
}

// --------------------------------------------------------------------------------------------------------------------------------
// funcs 
// --------------------------------------------------------------------------------------------------------------------------------

const validation = ({ c, index, array }) => {
    try {
        if (c.L > c.H) errors.push(`Error(index${index}): L > H`) //LがHより大きい
        if (c.O_date > c.C_date) errors.push(`Error(${index}): O_date > C_date`) //Oの日付よりCの日付のほうが早い
        if (index > 0) {
            if (array[index - 1]) {
                if (array[index - 1].C_date > c.O_date) { errors.push("Error(${index}): C_date[-1] > O_date") } //前レコードのCの日付より今のレコードのOの日付のほうが早い
            } else {
                errors.push(`info: index${index - 1} dosen't exists.`)
            }
        }
    } catch {
        //不明なエラー
        errors.push(`Unknown Error - index: ${index}`)
    }
}

//開始時間の計算
const caliculateStartTime = (data) => {
    const start1 = dayjs(data[0].exec_date).second() //最初の秒数
    //console.log(`秒数 ${start1}`)
    const start2 = Math.floor(start1 / sec_time_span) //要素番号
    //console.log(`要素番号: ${start2}`)
    const start3 = start2 * sec_time_span
    //console.log(`スタート秒数: ${start3}`)
    const start4 = dayjs(data[0].exec_date).startOf('minute').second(start3)
    console.log(`スタート時間: ${start4}`)
    return start4
}

//要素番号の計算
const caliculateArrayNum = (start_time, exec_date) => {
    const time = dayjs(exec_date).unix() - start_time.unix()
    return Math.floor(time / 5)
}


この記事をシェア


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