こんにちは!スナフキンです.
ずっとやりたかったことの1つに,ビットコインの自動売買botのバックテスト実装があります.バックテストというのは,過去の価格データを使って自分の売買ロジックの検証を行うことです.
相場は価格以外にも,板の厚さやイナゴの状況によって,注文の執行コストなどが異なるため,バックテストで確認できたパフォーマンス通りの結果が出ることはないのですが,最低でも理想的な環境(注文が必ず一定の価格で約定する,など)でのバックテストでプラスの損益を出すBotでなければ,実際の相場で利益を出すのは難しいですし,利益が出たとしてもそれは単に「運がよかった」だけの可能性があります.
僕自身は,pythonを始めたばかりでこれまでバックテストの実装ができる技術がなかったこともあり,仕方なくbotをBitflyerの最小取引枚数である,0.001枚で実戦投入してその期待値を測っていました.(バックテストができるようになっても,少ない枚数で実践投入することは意味があります.上述の通り,バックテストと実際の相場での損益は異なるからです.)
ということで今日は,大学が春休みに入ったこともあり比較的時間もできたのでバックテストの実装を行ってみました.今回はとりあえず,非常によく知られている「移動平均線のゴールデンクロスとデッドクロス」によるトレードを,バックテストで検証してみようと思います.

図の緑の線(短期移動平均)が赤の線(中期移動平均)を追い越しているところがいわゆる「ゴールデンクロス」.買いシグナルと言われている.
僕自身バックテストについては知っていましたが,過去の価格データを使ってテストするというのは,一体どうやってやればよいのかまったく見当がつかず放置していましたので,同じように困っている方がいれば参考にしてみてください.
バックテストでやることを明確に理解する
まず大前提として,何らかの動作をプログラムに組み込むためには,その動作が何をしているのかを明確に理解する必要があります.何が言いたいかというと,「バックテスト」はどういう工程から成り立っているかを知る必要があるということです.ぼくたち人間は,「移動平均線が交差したらトレード」と言われればそれだけで何をやるか理解できますが,コンピュータの場合はもっと明確に何をするか指示する必要があります.
それを踏まえてバックテストで行う工程は,以下のように分けられます.
- 時系列データの一点一点における売り,買い,あるいは待機の判断.
- それらの判断を踏まえて売買を行った場合の損益計算.
まだやや抽象的なので,具体例を挙げてみます.以下は2/2の1分足データの一部で,実際にバックテストに使ったデータの一部です.OHLCデータはCryptowatchから毎時間自動収集するプログラムを作り取得しています.移動平均は終値から計算しました.(実際のプログラム内では,テクニカル指標計算ライブラリに計算させればよいです.)
この表内のデータでは,売買シグナルが出ていないため,バックテスト内で注文は行いません.しかし,表内の売買判断のところが「買い」あるいは「売り」になったときにはバックテスト内で注文を行います.では,バックテスト内で「注文を行う」とはどういうことかと言えば単純に,i番目のデータで買いの判断をしたときのビットコイン価格,買い枚数を$$Pb_i, Sb_i$$と売りの判断をした時のビットコイン価格,売り枚数を$$Ps_i, Ss_i$$として,すべてのデータにおける$${Pb_i}{Sb_i}-{Ps_i}{Ss_i}$$の合計を求れば良いです.
つまり,バックテストを回した全期間における売買損益は,次の式で表されます.
$$\sum ({Pb_i}{Sb_i}-{Ps_i}{Ss_i})$$
日時 | 始値 | 高値 | 安値 | 終値 | 出来高 | 短期移動平均 | 中期移動平均 | 売買判断 |
2018/2/2 23:27 | 927337 | 933006 | 925830 | 930000 | 343.5786 | 938819.7 | 976762.9 | 待機 |
2018/2/2 23:28 | 930625 | 931500 | 921800 | 923122 | 471.2546 | 935634.1 | 976810.8 | 待機 |
2018/2/2 23:29 | 923871 | 933108 | 923122 | 930000 | 397.0662 | 934863.7 | 977652.2 | 待機 |
2018/2/2 23:30 | 930000 | 933108 | 926000 | 930000 | 300.6519 | 932623.6 | 977930.8 | 待機 |
2018/2/2 23:31 | 930000 | 945965 | 928000 | 945965 | 311.734 | 934277.4 | 979755 | 待機 |
2018/2/2 23:32 | 937759 | 945965 | 935000 | 941002 | 362.9392 | 933467.2 | 980208.5 | 待機 |
2018/2/2 23:33 | 940369 | 941002 | 930000 | 934000 | 288.1619 | 932809 | 979439.2 | 待機 |
2018/2/2 23:34 | 934000 | 936500 | 926000 | 929500 | 194.7687 | 932108.9 | 978670 | 待機 |
2018/2/2 23:35 | 927888 | 933978 | 926500 | 932131 | 246.4504 | 932785.1 | 977699.3 | 待機 |
2018/2/2 23:36 | 932131 | 932632 | 922160 | 925009 | 269.5918 | 931573.8 | 976090.4 | 待機 |
2018/2/2 23:37 | 928997 | 930000 | 924978 | 929886 | 194.9879 | 932737.9 | 975102.5 | 待機 |
2018/2/2 23:38 | 928000 | 932470 | 925110 | 926498 | 202.2256 | 932048.9 | 974110.2 | 待機 |
2018/2/2 23:39 | 926498 | 926498 | 917000 | 923499 | 494.4462 | 931098.9 | 972706.3 | 待機 |
2018/2/2 23:40 | 927876 | 928099 | 918100 | 925000 | 300.0257 | 929152.5 | 971965.4 | 待機 |
2018/2/2 23:41 | 922872 | 925972 | 912300 | 916160 | 372.6278 | 925784.3 | 970286.1 | 待機 |
2018/2/2 23:42 | 916883 | 918026 | 912126 | 913002 | 416.8435 | 923368.7 | 968818.2 | 待機 |
2018/2/2 23:43 | 913776 | 913964 | 903288 | 906670 | 386.3896 | 920452.5 | 967242.7 | 待機 |
2018/2/2 23:44 | 906709 | 916800 | 905252 | 912450 | 251.4067 | 919062.4 | 965943.9 | 待機 |
2018/2/2 23:45 | 914443 | 917020 | 910000 | 917020 | 309.2952 | 918720.5 | 964924.3 | 待機 |
価格データの準備,ロジックの実装
さて,バックテストの概要はこのぐらいにして,実際にコードを書いていきます.テクニカル指標計算用のライブラリとして,ta-libを使います.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import talib as ta import numpy as np myfile = '20180203.csv' data = np.loadtxt(myfile, delimiter=",", usecols=(1,2,3,4)) short_ema = ta.EMA(data[:,3], timeperiod=9) middle_ema = ta.EMA(data[:,3], timeperiod=50) #買いなら1売りなら-1,待機なら0を返す.入れるデータはiterableなもの. def judge_from_ema(ema_shorter, ema_longer): judgements = [0] * len(data) #デッドクロス,ゴールデンクロスの判断は最後はできないので,len(ema_shorter)-1までしかループは回さない. for i in range(len(ema_shorter)-1): if ema_shorter[i] < ema_longer[i] and ema_shorter[i+1] > ema_longer[i+1]: #ゴールデンクロス judgements[i] = 1 elif ema_shorter[i] > ema_longer[i] and ema_shorter[i+1] < ema_longer[i+1]: #デッドクロス judgements[i] = -1 else: judgements[i] = 0 return judgements |
まずはcsvから1分足データをロードします.使うのは終値だけですが,一応OHLC全てロードしています.次に指数平滑移動平均(EMA)をta.EMAで計算し,それによって売り(-1),買い(1),待機(0)を判断する関数を実装します.ここではゴールデンクロスなら買い,デッドクロスなら売りとしています.
バックテストを実装する
バックテストを実装します.日本円初期保有量は,資産曲線を描くために設定しましたが,今回は使用しなくてもokです.今回は,0.1ビットコインを先ほど実装した関数の判断結果に従って売買したときの利益を,最終的なtotal – capitalによって計算しました.シグナルの出現回数が奇数回だった場合,ポジションが決済されずに残ってしまうので,最後の足の終値で決済することにしました.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#日本円初期保有量 capital = 10000 total = capital #取引単位(BTC) units = 0.1 #買いポジを持っているときは1,売りポジを持っているときは-1,ポジを持っていないときは0 position = 0 number_of_trade = 0 judgements = judge_from_ema(short_ema, middle_ema) #バックテスト.ローソク足の終値で取引すると仮定.最後のローソク足で全てのポジションを決済する. for i in range(len(data)-1): if judgements[i] == 1: total = total - units * data[i+1, 3] position += 1 number_of_trade += 1 elif judgements[i] == -1: total = total + units * data[i+1, 3] position += -1 number_of_trade += 1 else: pass #最後の足でポジションをまとめて決済. if position == 1:#買いポジを最後に持っていたら売り決済 total = total + units * data[-1,3] position += -1 number_of_trade += 1 elif position == -1:#売りポジを最後に持っていたら買い決済 total = total - units * data[-1,3] position += 1 number_of_trade += 1 number_of_trade = number_of_trade / 2 print("Trade unit: {}".format(units)) print("The position code is {} now.".format(position)) print("Total P/L per day: {}".format(total-capital)) print("The number of trade: {}".format(number_of_trade)) |
バックテストの結果
2/3の1分足で,このシグナルに従ってトレードした場合は損益がマイナスになってしまいました.つまり,単純な移動平均線の交差を用いたトレードは必ずしもワークしないということです.もちろん,もっと長い期間で検証しなければ断定はできませんが.今後は,バックテストの過程を可視化して,ロジックの問題点を探れるようにしていきたいと思います.