初めてバックテストの領域に足を踏み入れることにする。
当面の間は強いストラテジを作ることよりも気軽に戦略を入力できるような手軽さを目指したい。
初回は次の実装を考える。
また最後にバックテストの正当性を考える。
{
date: 3177646225510,
O_date: '2020-05-07T02:15:26.310Z',
O: 987236,
H: 987261,
L: 987155,
C: 987261,
C_date: '2020-05-07T02:15:29.479Z',
V: { BUY: 0.12000000000000001, SELL: 2.79155104 }
}
ファイルはJSON形式。CSVのほうが若干データ量で有利かと思うが面倒なので扱いやすいJSONのままにした。
dateは5秒足の時間の区切りの開始時刻。00時00分00秒
~00時00分4.999...秒
の足となるが、この00時00分00秒
の方が記録されているだけだ。dayjs
に放り込むだけで使える形式(UNIX秒)
VはVolume。BUYとSELLに分けた。TradingView
ではわからない情報だ。
損益の配列 []
クローズが発生するごとにプロット。ドテン時は内部でクローズしてから反転という構造で動作している。
// 設定 ----
const slippage = 0
// ---------
let position = 0 // 1:long, -1:short, 0:ノーポジ
let entryPoint = null //エントリーした価格
let totalProfit = 0 //トータルの損益
let profitHistory = [] //損益をプロットする配列
const long = (d) => {
switch (position) {
case 1:
//何もしない
break
case -1: //ドテンロング
close(d)
long(d)
break
case 0: //新規ロング
entryPoint = d.C + (d.C * slippage)
position = 1
break
}
}
const short = (d) => {
switch (position) {
case -1:
//何もしない
break
case 1: //ドテンショート
close(d)
short(d)
break
case 0: //新規ショート
entryPoint = d.C - (d.C * slippage)
position = -1
break
}
}
const close = (d) => {
let profit = 0
let closePoint
switch (position) {
case 1: //ロング決済
closePoint = d.C - (d.C * slippage)
profit = closePoint - entryPoint
break
case -1: //ショート決済
closePoint = d.C + (d.C * slippage)
profit = entryPoint - closePoint
break
}
position = 0 //ノーポジ
entryPoint = null //エントリー無し
totalProfit += profit //トータル計算
profitHistory.push(totalProfit) //損益履歴
}
メインのコードは本当にこれだけ。
とりあえず急ぎで実装した。関数で処理するよりクラス化したほうが長い目で見て良いかも?少しずつ改修して汎用化していきたいところだ。
遅延・滑り・サーバーエラー・・・などなど課題は山積みだがTradingViewも基本はクローズ価格でエントリーするのでまずはこれでよいだろう。
実際に戦略を書く場合はこれに続いて次のように書いていく。
for (const d of data) {
if(d){ //足が抜けてる場合があるので考慮する
if(BUY条件){long(d)}
if(SELL条件){short(d)}
if(クローズ条件){close(d)}
}
}
関数内でプロットが行われるのであとはフロントエンドにグラフを出すだけだ。
Socket.ioでフロントエンド(Vue.js)にprofitHistory
の配列ごと渡している。
また損益グラフは私がこれまで書いてきた通りvue-chart.js
にて描画を行っている。
正直なところPython
のmatplotlib
と思うと何倍もの実装時間コストが必要かもしれないがグラフィカルに再描画が可能であり何度もテストを行う意味でWEB-UI上からパラメータを自在に変化させることも出来るので時間コスト分の価値はあるはずだ。
基本的なライングラフであれば20分もあれば実装できる。
特別新しいコードではないため記述しないが、グラフ描画用のdatacollection
作成コードは次のようになった。
ここでのdata
は損益履歴がplotされた単純な一次元配列。
export const renderProfit = (data) => {
let labels = []
data.map(( d, index) =>{
labels.push(index)
})
return {
labels,
datasets: [
{
label: "Profit(Yen)",
data: data,
fill: false,
borderColor: "green",
pointRadius: 0, //ポイントを描画しない
borderWidth: 1, //ラインの細さ
}
]
}
}
3時間の間に記録された5秒足で戦略をテストした。この程度ならサクっと処理が終わるため。
実際には様々な時間で試していく必要がある。
今回使用したストラテジはは公開しないことにした。
5分ぐらいで考えた手法のため大したものではないと思っていたがどうやらHFTでは割とよくある考え方のようで、書いてしまうとエッジ消失に留まらず他で戦略を解説している方への中傷となりかねないためご了承いただきたい。
この記事ではそういった意図はなく正しくバックテストを行うためのアドバイスとして見てもらいたい。
理論上は1BTCのトレードを繰り返したProfit。
グラフX軸はトレード総数。3時間で1125回のトレードなので0.16秒に1回トレードしている計算。HFTと言ってもよいだろう。
2020/05/18訂正:計算式を間違えていたため訂正。約12秒に1回のトレードであるため単なるスキャルピングの誤り。グラフ上はドテン時2回のトレードとカウントされるのでそちらも考慮したらもう少しトレード間隔は短くなる。
3時間で3万円に近い収益を出している。24時間稼働したら日時20万超え。1年半あれば億り人だ。
と、思うだろう?
そう、我々素人はこうやって騙されるのである。確かにグラフは稼いでいる。理論上は稼いでいる。
だが、騙されてはいけない。
本当にそのグラフの利益が得られるのだろうか?
そう、上のコードではクローズ時LTPの価格で約定したことになっている。更に約定率100%。
これはかなり有利な値段で約定しているのでは?と疑問が起こる。
これを成り行きで取引した場合どうなるだろうか。
私はHFTトレーダーではないのでざっくりしかわからないが現在稼働しているBOTの滑りを目視すると0.1BTCの成り行きでも200-500円ぐらいの値幅を滑っている。1000円以上滑ることもしばしば。
別のBotterから貰った情報だとbitFlyerでは0.05%平均ほど滑るらしい。
だいたい私の目視と一致している。まして1BTCなら更に滑るだろう。
さて、既に気づいておられる方もいるだろうが私のコードに次の一行がある。
const slippage = 0
ここに滑る係数を入力する。0.05%なら 0.05 / 100 。
私のコード上はロングもショートもクローズ時価格から「不利な方へ」slippage分動いて約定する想定だ。
実際には本当に約定履歴や板から逆算し約定するかどうかのコードを追記してバックテストする必要がありそうだが今回は初回のためざっくり上記の方法で0.05%滑らせてみる。
これ、ぜんぜんあかんやつやん・・・
6万円負けている。1年待たずして逆億り人である。
永遠に負け続けるストラテジがあるとすればそれを逆にトレードすれば勝てるはずである。
よし、エントリー条件を逆にしてみよう(slippageは0.05%のまま)
ちょっと考えたら分かるが・・・そりゃそうだ。
滑りを手数料と考えテストに織り込むことがいかに重要かが分かったと思う。
つまり強BOTTERというのはこの条件も織り込んでトータルプラスとなっているのである。
非常に難しい領域であることは間違いない。
さて、コードはサクっと完成したがHFTがいかに難しいかが実感できた。
しかし落ち込むのはまだ早い。指値を使って思い通りの価格で約定できたなら時給1万円とはいかないまでもプラス側になる可能性は残っているはずである(その指値コントロールがとてつもなく難しいのだが)
さて、まだバックテストの挑戦は始まったばかり。
当面の目標は勝つストラテジを作ることではなくより手軽にバックテストを行える環境を作ることである。
※グラフが綺麗すぎる気がするのでバグ等あれば教えて頂けたら幸いである。