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)
}