ゼロから始めるBitcoinの自動取引④

この記事は全4回中の第4回です。他の回についてはこちら(#1 / #2 / #3 )。

bitFlyerを利用したBitcoinの自動取引について、Pythonと裁定取引についてはある程度わかるけど他はわからないという人のために概要を簡潔に書いていきます。

第4回の今回はバックテスト編です。最後に簡単に発展的手法にも言及します。

バックテスト

概観

botの利点として過去のデータで取引ルールを検証できることが挙げられます。

基本的に自分の用いているbotのコードを改変してバックテストをすることになります。 今回は#3で作った取引コードを元にバックテストコードを書いていきます。

コードを変更するポイントとして、

  • OHLCデータを毎回Cryptowatch APIから取得していたが、はじめにまとめて取得してindexをずらしていく形式にする
  • 一定時間botを停止する処理や実際に注文を行う処理を消す
  • 損益に関してグラフを描画したり、損益率やProfit Factorなどの値を計算したりして分析を加える

があります。

分析については有名なものをいくつか紹介します。

  • 勝率:勝ちトレード数/トレード総数 の値。当然高いほど良い。
  • 損益率:平均利益/平均損失 の値。1より大きい場合が利大損小で理想的。1より小さい場合はいわゆるコツコツドカン型を意味する。
  • Profit Factor総利益/総損失 の値。2.0を越えると相当優秀。1.5を越えるなら悪くはないイメージ。
  • 最大ドローダウン:最大資産の最大下落率。例えば元資本10万円で20万円まで増えたあと15万円まで減った場合、下落率は(20-15)/20=0.25なのでドローダウンは25%である。

以上の4つを調べれば概ね十分だと思います。これらを総合的に踏まえてロジックを評価します。例えば、勝率が良くても損益率が悪ければいいロジックとは言えません。

バックテストコード

上に述べた変更点を踏まえたバックテストのコードは次のようになります。

なお、今回は簡単のため、グラフの横軸はトレード回数としましたが、datetimeを利用することで時刻を横軸に設定にすることも可能です。

# coding: utf-8

import csv,json
import numpy as np
import pandas as pd
import datetime
from pprint import pprint
import matplotlib.pyplot as plt
import time
import requests

# ログ設定
import logging
from logging import getLogger,FileHandler,Formatter
logger = getLogger("backtest")
logger.setLevel(logging.INFO)
fh = FileHandler('backtest.log')
fh.setLevel(logging.INFO)
format = Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(format)
logger.addHandler(fh)

import utils

# ドローダウン計算  
def drawdown(result):
    top = 0
    down = 0
    for i in range(len(result)):
        if top < result[i]:
            top = result[i]
        else:
            if down < top - result[i]:
                down = top - result[i]
        
    return down/top*100




if __name__ == "__main__":
    logger.info("【バックテストを行います】")
    print("【バックテストを行います】")


    # ロジックの読み込み
    # ------------------------------
    logic = utils.Logic_test()
    period = 60
    size = 0.01
    # ------------------------------


    # bot
    flag = {'check':True, 'sell_position':False, 'buy_position':False}
    result = [0]

    # OHLCデータをはじめにまとめて取得しておく
    response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = {"periods": period})
    rawohlc = np.array(response.json()['result'][str(period)])
    index = 0
    
    try:
        while True:
            # 建玉未保有時
            while flag['check']:
                # ohlcの設定
                ohlc = rawohlc[index:index+100]

                # IndexErrorの発生
                err = rawohlc[index+100]



                # entryの判断
                entry,_ = logic.try_entry(ohlc)

                if entry['buy']:
                    logger.info("買い注文をします 現在価格:"+str(ohlc[-1][4]))
                    print("買い注文をします 現在価格:"+str(ohlc[-1][4]))
                    

                    # flagの更新
                    flag['check'] = False
                    flag['buy_position'] = True

                    index += 1
                    
                    break

                if entry['sell']:
                    logger.info("売り注文をします 現在価格:"+str(ohlc[-1][4]))
                    print("売り注文をします 現在価格:"+str(ohlc[-1][4]))

                    # flagの更新
                    flag['check'] = False
                    flag['sell_position'] = True

                    index += 1

                    break
                
                index += 1


            # 買いポジション保有時
            while flag['buy_position']:
                # ohlcの設定
                ohlc = rawohlc[index:index+100]

                # IndexErrorの発生
                err = rawohlc[index+100]
                
                # exitの判断
                exit = logic.buy_exit(ohlc)
                if exit['settle']:
                    logger.info("売り決済をします 損益:"+str(exit['result']))
                    print("売り決済をします 損益:"+str(exit['result']))
                    result.append(exit['result'])
                    

                    # flagの更新
                    flag['check'] = True
                    flag['buy_position'] = False

                    index += 1
                    
                    break
                
                index += 1

            
            # 売りポジション保有時
            while flag['sell_position']:
                # ohlcの設定
                ohlc = rawohlc[index:index+100]

                # IndexErrorの発生
                err = rawohlc[index+100]

                # exitの判断
                exit = logic.sell_exit(ohlc)
                if exit['settle']:
                    logger.info("買い決済をします 損益:"+str(exit['result']))
                    print("買い決済をします 損益:"+str(exit['result']))
                    result.append(exit['result'])


                    # flagの更新
                    flag['check'] = True
                    flag['sell_position'] = False

                    index += 1

                    break
                
                index += 1




    # index+100がohlcの長さを超えたところがバックテストの終了条件
    except IndexError:
        logger.info("【バックテストを終了します】\n")
        print("【バックテストを終了します】\n")

        # 分析
        result = np.array(result)
        profit = sum(result)
        winrate = len(result[result > 0])/len(result) # 勝率
        pandlratio = -(sum(result[result > 0])/len(result[result > 0]))/(sum(result[result < 0])/len(result[result < 0])) # 損益率
        pf = -sum(result[result > 0])/sum(result[result < 0]) # profit factor
        dd = drawdown(result) # ドローダウン

        print(f"---バックテスト結果---\nトレード回数:{len(result)}\n総利益:{profit:.0f}\n勝率:{winrate:.2f} \
            \n損益率:{pandlratio:.2f}\nprofit factor:{pf:.2f}\n最大ドローダウン:{dd:.2f} %")
        logger.info(f"---バックテスト結果---\nトレード回数:{len(result)}\n総利益:{profit:.0f}\n勝率:{winrate:.2f} \
            \n損益率:{pandlratio:.2f}\nprofit factor:{pf:.2f}\n最大ドローダウン:{dd:.2f} %")

        # 描画
        plt.plot(range(len(result)),result.cumsum())
        plt.title("Result")
        plt.xlabel("Number of entries made")
        plt.ylabel(f"Profit (×{size} JPY)")
        plt.savefig("result.png")

これをutils.pyと同じ階層で実行すればバックテスト結果がコンソールとbacktest.logに書き出され、資産の推移がresult.pngにプロットされます。

今回のロジックでバックテストを行うと例えば以下のような結果になります。

---バックテスト結果---  
トレード回数:36  
総利益:3000  
勝率:0.53         
損益率:1.00  
profit factor:1.19  
最大ドローダウン:200.00 %

backtest

このロジックでは値幅を利確損切りともに一定値の1000に設定しているので損益率は必ず1.0になります。 勝率も0.53なのでほぼ半々で勝ったり負けたりしているロジックということになり、あまり利益が期待できないことがわかります。

また、今回のようにトレード回数が36回と少ないというのも問題です。エントリーしづらいロジックなのかバックテスト期間が短いのかの二択ですが、今回は後者でしょう。 これはCryptowatchでは過去6000件のデータしか取得できないことに起因していて、もしもっと長い期間のOHLCデータがあればその分長い期間のバックテストが行えます。

例えば取引所の約定履歴データを自分で蓄積しておき、そこからOHLCを作るといった方法によって改善することができます。

バックテストを行う際の留意点

バックテストには見落としがちな注意点がいくつかあります。

まず一つ目は実際の取引はバックテスト通りに行えるとは限らないということです。 要因は様々ですが、主に成行注文によるスリッピング、サーバーアクセス集中による注文拒否、手数料の影響などがあります。 Cryptowatchの配信情報とbitFlyerのリアルタイム価格は厳密にはズレがあるというのも、短い時間足になればなるほど影響してきます。

あくまでバックテストは概算値であり、実際にはそれよりも悪くなることがほとんどだということは頭に入れておく必要があります。

二つ目は過学習についてです。 十分な長さのOHLCを用いてバックテストを行なった結果、あるロジックのあるパラメータ値のとき良い結果が得られたとしましょう。 しかし、過去のデータに合わせて過度に調整したロジックは、未来でも同様に通用するとは限りません。(過学習で調べてみてください) 特に、ロジックが複雑であればあるほど過去のデータに対して過学習している可能性が高まるので、いざ使い始めたら結果が散々ということもよくあります。

全てのトレードに勝てるような戦略は存在しません。ほどほどにシンプルでかつ、じわじわと利益を挙げられているようなロジックを目指すと良いです。 PFが3.0を超えるようなロジックはほぼ確実に過学習しているので気をつけてください。

発展的な手法

今回作ったのはテクニカル指標を用いたシンプルなスイングトレードbotです。 しかしこれは人間でもできるトレード手法を自動化しているにすぎず、安定した利益を得ることは難しいのが実情です。

一方でbotであることを生かした取引手法もいくつか存在します。今回はそれらのうちmmbotと機械学習botについて簡単に説明します。

mmbot

MarketMakerbotの略です。 非常に短い時間足(数秒〜数分)の世界で、同時に一定の値幅だけ離れた売りと買いの指値注文を同数量発注し、一定時間内に両方とも約定させることでその値幅分の利益を得ます。 発想はシンプルですが、実際の値動きの中でどこまで指値注文の値幅を広げられるかという問題を解くことは簡単ではありません。 もし片方の注文が約定しなかった場合に、反対方向に動いた在庫が残ってしまうことになり、この在庫をうまく処理できなければ損失が膨らんでしまいます。

板情報やエッジなどを頼りにこの指値幅を推定しつつ上手に在庫管理することが、mmbot制作において大事です。

機械学習bot

機械学習を用いて値動きの上下を予測することも考えられます。 ただし、市場の値動きはランダムウォークだという仮説もあるくらいで、値動きの予測は非常に難易度の高い問題のため、この手法は成功している人がほぼいないイメージです(もちろん成功していれば表には出てこないのでどちらにせよわからないのですが)。
 

市場参加者は大多数の何の情報も持っていない人たちと、ごく少数の値動きに関する情報を持っている人たち(あるいは仕手グループのように値動きを自分たちで操れる人たち)で構成されているとします。

情報を持っていない人たちはテクニカル指標だのファンダメンタルズだのと好き勝手に値動きを予想し取引を行うのでランダム性を生み出します。一方で情報を持っている人たちは着々と値動きの方向の建玉を積んでいきます。
この情報を持っている人たちの動きを感知できれば、値動きを予測することができるというわけです。

実際、草コインのpumpの仕手の予測に成功したという報告もTwitter上であったので、不可能なことではないのかもしれませんが、Bitcoinのように大きな市場だとそれを行うのは非常に困難です。

ちなみに自分も、趣味程度のレベルですが機械学習や強化学習を用いて取引botを学習させるということを不定期に試みているので、もし一緒に研究してくれる方がいるならぜひTwitterまで連絡してくれると喜びます。笑
 

他にも取引所間の価格差を利用して利益をだすアービトラージという手法も存在しますが、今回は割愛します。

総括

以上、全4回で簡単なスイングトレードbotの作成を行ってきました。   簡単に作れる分、このようなbotで安定して利益を出すというのは難しいのかもしれませんが、全ての基本となるところなのでbot作りのエッセンスが詰まっていると思います。   また、良い指標が見つかれば1,2ヶ月くらいなら安定して利益を得ることは不可能でないことを私自身実際に経験しているので、皆さんもぜひ色々なサイトを参考にして挑戦してみてください。
 

bot作りはうまくいけば利益に直接繋がることもそうですし、あれこれ仮説を立てながらロジックを試していくのも楽しいのでおすすめです。 この記事が少しでも皆さんの参考になれば幸いです。
 

第3回の最後でも書きましたが、トラブルを避けるため注意書きを再掲します。

一つ目。 本連載のコードをコピペして改変せずにそのまま使うことは絶対にしないでください。また、もしそのような利用をしたことにより使用者が不利益を被っても執筆者である私は一切の責任を負いません。

二つ目。 万が一、本連載のコードに何か誤りがあって、それにより使用者が不利益を被っても、執筆者である私は一切の責任を負いません。

三つ目。 本連載のコードを無断で転載、再配布することは禁止します。

以上、よろしくお願いします。

今回のまとめ

  • botのコードを少し改変することでバックテストが行える
  • バックテストには過学習等の落とし穴も存在する
  • bot作りは楽しいのでおすすめ

BACK