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

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

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

第3回の今回は実際に動かすコードを書いていきます。

なお、この連載記事は昔自分が行なっていた自動取引をまとめ直すという記事のため、今回示すコードと当時自分が使っていたコードとで実装が異なる部分がかなりあります。

(簡単な動作確認はしたものの)今回示したコードを使って長期間運用した実績はないのであくまでサンプルとして見ていただけると幸いです。

構想

実際に書いていく前にコード全体の構成を考えます。

今回はmmbotやアービトラージのようなbotであることを生かした特殊な手法では無く、単に裁量取引の厳密なルールに基づく自動化というコンセプトでコードを書いていきます。 実際に取引をする部分は終了条件を満たすまで無限ループさせて、そのループの中でentry,exitを繰り返すような構成にし、簡単のため建玉中は新たなentryはしないようにします。また、注文は全て成行で行います。

entry,exitの判断は、今回は現在時刻までのOHLCデータを引数とする関数として実装します。

また、bitFlyerAPIを利用して取引をする関数、テクニカル指標を計算する関数はクラスにまとめることで整理できます。

今回はロジックとテクニカル指標計算の関数をまとめたファイルを utils.py とし、実際に動かすコードを bot.py として作っていきます。

bitFlyerAPI関連

#1で作った関数をクラスとしてまとめていくことで取引所関連の操作の関数を整理することができます。

ただし、それをすでにやってくれたものがpybitflyerなので今回はありがたく使わせてもらうことにします。

utils.py
import pybitflyer
# ***には自分のAPI keyとAPI secretを入れる
api = pybitflyer.API(api_key="***",api_secret="***")

準備はこれだけです。bitFlyerに関するAPIは基本的にこの api を用います。

テクニカル指標関連

テクニカル指標をクラス Indicators に定義していきます。

基本的には#2で実装したものをクラス用に書き直したものですが、RSIに関してはtalibと比べて自分で書いたコードが遅かったのでtalibがインストールされてる場合にはそちらを使うようにしています。

注意点として、これらのテクニカル指標は入力したOHLCデータの全ての時刻に対して計算するので、 len(ohlc) が大きいとその分計算も遅くなります。 よって ind = Indicators(ohlc) とする際に渡す ohlc は長すぎないようにします。

utils.py
import numpy as np
import pandas as pd
# ta-libの読み込み
try:
    import talib
    talibflag = True
except ModuleNotFoundError:
    print("talibがinstallされていないため一部指標の計算が遅くなります")
    talibflag = False
 
# 指標
class Indicators:
    def __init__(self,ohlc):
        """
        ohlc : np.array([[timestamp,o,h,l,c],...])
        """
        self.ohlc = ohlc # OHLC配列(5,*)
        self.closeprice = ohlc[:,4] # 終値配列(*)
 
    def ATR(self,n):
        """
        return : ATRのndarray(length=len(ohlc))
        """
        if talibflag:
            return talib.ATR(self.ohlc[:,2],self.ohlc[:,3],self.closeprice,timeperiod=n)
        else:
            tr = []
            atr = []
            for i in range(1,len(self.ohlc)):
                tr.append(max(self.ohlc[-i][2],self.ohlc[-i-1][4]) - min(self.ohlc[-i][3],self.ohlc[-i-1][4]))
            tr.reverse()
            atr = pd.Series(tr).ewm(span=n).mean().values
            return atr
 
 
    # EMA
    def EMA(self,n):
        """
        return : EMAのndarray(length=len(ohlc))
        """
        alpha = 2/(n+1)
        x0 = self.closeprice[:n].mean()
        ema = [x0]
        for i in range(len(self.closeprice)-n):
            ema.append(ema[-1]*(1-alpha)+self.closeprice[n+i]*alpha)
 
        nanlist = np.zeros(n-1)
        nanlist[:] = np.nan
        return np.concatenate([nanlist,np.array(ema)])
    
    # SMA
    def SMA(self,n):
        """
        return : SMAのndarray(length=len(ohlc))
        """
        return pd.Series(self.closeprice).rolling(n).mean().values
    
    # MACD(n1:shortEMA,n2:longEMA,n3:signal)
    def MACD(self,n1,n2,n3):
        """
        return : MACDのndarray,signalのndarray
        """
        macd = self.EMA(n1) - self.EMA(n2)
        macd[:n2] = np.nan
        signal = pd.Series(macd).rolling(n3).mean().values
 
        return macd,signal
    
    
    def RSI(self,n):
        """
        return : RSIのndarray
        """
        if talibflag:
            rsi = talib.RSI(self.closeprice,timeperiod=n)
            return rsi
        else:
            RSI_period = n
            diff = pd.Series(self.closeprice).diff(1)
            positive = diff.clip(lower=0).ewm(alpha=1.0/RSI_period).mean()
            negative = diff.clip(upper=0).ewm(alpha=1.0/RSI_period).mean()
            rsi = 100 - 100/(1-positive/negative)
            return rsi.values
 
 
    def RCI(self,n):
        """
        return: RCIのndarray
        """
        rci = []

        for j in range(len(self.closeprice) - (n-1)):
            table = np.zeros([2,n])
            # closeprice[-n:0]になるのを回避
            if j == len(self.closeprice)-n:
                table[0] = self.closeprice[-n:]
            else:
                table[0] = self.closeprice[-len(self.closeprice)+j:-len(self.closeprice)+n+j]
            table[1] = np.arange(n,0,-1)
    
            sortedtabel = table[:,np.argsort(table[0])]
    
            index = np.arange(n,0,-1)
            d = 0
            for i in range(n):
                d += (index[i]-sortedtabel[1][i])**2
            rci.append((1-6*d/(n*(n*n-1)))*100)
        nanlist = np.zeros(n-1)
        nanlist[:] = np.nan
        return np.concatenate([nanlist,np.array(rci)])
 
    def BB(self,n,sigma=2):
        base = pd.Series(self.closeprice).rolling(n).mean().values
        sig = pd.Series(self.closeprice).rolling(n).std(ddof=1).values
    
        upper_band = base + sigma*sig
        lower_band = base - sigma*sig
    
        return upper_band, lower_band

ロジック関連

ロジックも同様にクラスで書いていきます。

try_entry(ohlc) :ohlcを渡すと、売り買いどちらかにエントリーするか何もしないかを判断する関数
sell_exit(ohlc) :(売りポジションを持っている状態で)ohlcを渡すと、損益を確定させるかどうかを判断する関数
buy_exit(ohlc) :(買いポジションを持っている状態で)ohlcを渡すと、損益を確定させるかどうかを判断する関数

の3つを Logic_test というクラス内に定義します。

ただし、テストロジックとして「1分足のEMA25とEMA10を使って、ゴールデンクロスで買いエントリー、デッドクロスで売りエントリーして、5000の値幅が動いたら利確/損切り」という非常に単純なものを設定しています。

utils.py
 
class Logic_test:
    """
    EMA25とEMA10のゴールデンクロスで買い、デッドクロスで売り
    """
    
    def __init__(self):
        self.state = {'buy':False, 'sell':False}
        self.exit = {'settle':False, 'result':None}
        self.width = {'base':-1, 'p_width':-1, 'l_width':-1}
 
    def try_entry(self,ohlc):
        """
        signalがTrueになる条件(売買のエントリー条件)と
        利確損切りのwidthを定義する。
        """
        ind = Indicators(ohlc)
 
        self.state['buy'] = self.state['sell'] = False
        self.width['p_width'] = self.width['l_width'] = -1
 
        """      
        ロジック部分
        """
        EMA25 = ind.EMA(25)
        EMA10 = ind.EMA(10)
        if EMA25[-2] > EMA10[-2] and EMA25[-1] < EMA10[-1]:
            self.state['buy'] = True
        
        if EMA25[-2] < EMA10[-2] and EMA25[-1] > EMA10[-1]:
            self.state['sell'] = True
        
        # 一定値の値幅で利確損切りをする場合以下を設定する
        self.width['p_width'] = self.width['l_width'] = 1000
 
        self.width['base'] = ohlc[-1][4]
        
        return self.state,self.width
 
    def sell_exit(self,ohlc):
        """
        ショートポジションの際のexit条件を記載する。
        条件を満たすならself.exitのsettleにTrueを、resultにその際の(予想される)損益を入れる。
        """
        ind = Indicators(ohlc)
        self.exit = {'settle':False, 'result':None}
 
        # 値幅exitの場合
        if self.width['p_width'] != -1:
            # 利確
            if self.width['base'] - ohlc[-1][4] > self.width['p_width']:
                self.exit['settle'] = True
                self.exit['result'] = self.width['p_width']
                
            # 損切り
            if ohlc[-1][4] - self.width['base'] > self.width['l_width']:
                self.exit['settle'] = True
                self.exit['result'] = -self.width['l_width']
 
        """
        # ドテンの場合
        else:
            signal,_ = self.try_entry(ohlc)
            if signal['buy']:
                self.exit['settle'] = True
                self.exit['result'] = self.width['base'] - ohlc[-1][4]
        
        # その他のexitの場合
        if hogehoge:
            self.exit['settle'] = True
            self.exit['result'] = self.width['base'] - ohlc[-1][4]
        """
 
        return self.exit
        
    def buy_exit(self,ohlc):
        """
        ロングポジションの際のexit条件を記載する。
        条件を満たすならself.exitのsettleにTrueを、resultにその際の(予想される)損益を入れる。
        """
        ind = Indicators(ohlc)
        self.exit = {'settle':False, 'result':None}
 
        # 値幅exitの場合
        if self.width['p_width'] != -1:
            # 利確
            if ohlc[-1][4] - self.width['base'] > self.width['p_width']:
                self.exit['settle'] = True
                self.exit['result'] = self.width['p_width']
            # 損切り
            if self.width['base'] - ohlc[-1][4] > self.width['l_width']:
                self.exit['settle'] = True
                self.exit['result'] = -self.width['l_width']
            
        """
        # ドテンの場合
        signal,_ = self.try_entry(ohlc)
        if signal['sell']:
            self.exit['settle'] = True
            self.exit['result'] = ohlc[-1][4] - self.width['base']
        
        # その他のexitの場合
        if hogehoge:
            self.exit['settle'] = True
            self.exit['result'] = ohlc[-1][4] - self.width['base']
        """
               
        return self.exit

辞書型のクラス変数3つはそれぞれ以下のような役割をしています。

self.state = {'buy':False, 'sell':False} :ポジションを持っているかの状態を格納
self.exit = {'settle':False, 'result':None} :決済注文をするかどうかの判断と、そのときの期待損益を格納
self.width = {'base':-1, 'p_width':-1, 'l_width':-1} :ポジションを持ったときの値段と、利確損切りの値幅を格納

entryやexitの条件を書き換えることで様々なロジックを実現できます。

実際に取引する部分

さて、実際に取引する部分を書いていきます。 はじめに説明した通り、無限ループの中で「ポジションなし」「買いポジション」「売りポジション」の3状態を推移します。

bot.py
# 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("bot")
logger.setLevel(logging.INFO)
fh = FileHandler('bot.log')
fh.setLevel(logging.INFO)
format = Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(format)
logger.addHandler(fh)

import pybitflyer
api = pybitflyer.API(api_key="***",api_secret="***")

import utils

# 注文拒否対策をした注文
def sendchildorder(side,size):
    cnt = 0
    while cnt < 20:
        response = api.sendchildorder(product_code="FX_BTC_JPY",child_order_type="MARKET",side=side,size=size)
        try:
            if bool(response['child_order_acceptance_id']):
                break
        except:
            pass
        cnt += 1
        time.sleep(2)

# CryptowatchからOHLCデータ取得
def getohlc(periods):
    response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = {"periods": periods})
    ohlc = np.array(response.json()['result'][str(periods)])[-100:,:5] # 長すぎると計算時間が増えるので最新100件に整形
    return ohlc

if __name__ == "__main__":
    logger.info("【botを起動します】")
    print("【botを起動します】")


    # ロジックの設定
    # ------------------------------
    logic = utils.Logic_test()
    periods = 60
    size = 0.01
    # ------------------------------


    # bot
    flag = {'check':True, 'sell_position':False, 'buy_position':False}
    result = [0]
    
    try:
        while True:
            # 建玉未保有時
            while flag['check']:
                # ohlc取得
                ohlc = getohlc(periods)


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

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

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

                    time.sleep(periods)
                    
                    break

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

                    # 注文
                    sendchildorder("SELL",size)

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

                    time.sleep(periods)

                    break
                
                time.sleep(periods//5)


            # 買いポジション保有時
            while flag['buy_position']:
                # ohlc取得
                ohlc = getohlc(periods)
                
                # exitの判断
                exit = logic.buy_exit(ohlc)
                if exit['settle']:
                    logger.info("売り決済をします 損益:"+str(exit['result']))
                    print("売り決済をします 損益:"+str(exit['result']))
                    result.append(exit['result'])
                    
                    # 決済注文
                    sendchildorder("SELL",size)

                    # flagの更新
                    flag['check'] = True
                    flag['buy_position'] = False
                    
                    break
                
                time.sleep(periods//5)

            
            # 売りポジション保有時
            while flag['sell_position']:
                # ohlc取得
                ohlc = getohlc(periods)

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

                    #決済注文
                    sendchildorder("BUY",size)

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

                    break
                
                time.sleep(periods//5)

                """
                # 終了条件(あれば)
                if hogehoge:
                    break
                """


    except KeyboardInterrupt:
        logger.info("【botを終了します】\n")
        print("【botを終了します】\n")

        # 描画
        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")
</pre>
</div>
<p>
loggerを利用しているので少し複雑に見えるかもしれませんが、構造自体は単純です。
もしわかりづらければlogger関連の部分を全て消しても本質的には問題ありません。

また、実際に注文をする部分については、bitFlyerの注文拒否対策として新たに関数を定義し、
</p>

<div class="programming">
<p class="lang">Python</p>
<pre class="sourcecode prettyprint linenums lang-py" style="display:block">
def sendchildorder(side,size):
    cnt = 0
    while cnt < 20:
        response = api.sendchildorder(product_code="FX_BTC_JPY",child_order_type="MARKET",side=side,size=size)
        try:
            if bool(response['child_order_acceptance_id']):
                break
        except:
            pass
        cnt += 1
        time.sleep(2)

という書き方をしています。 これは response['child_order_acceptance_id'] にIDがちゃんと入っていることが確認できるまで注文をトライし、20回連続で失敗したら諦めてループを抜けるという処理です。

bitFlyerはサーバーが重くなると注文の遅延や拒否が頻繁に起こるのでbotがその度エラーで止まってしまわないようにいろいろと工夫が必要です。 例外処理やメール通知などを実装して、対応できるようにしておかないと、痛い目を見ることがあるので注意してください。

実際自分が取引しているときにはそのほかにも二重注文や注文消失など様々な問題が起こったので、これだけの対策では不十分かもしれませんし、注文拒否対策についてももっと良い書き方があるかもしれません。

実行

ローカルの環境で動かすとパソコンの電源が落ちたり通信が途切れるたびに止まってしまうので、AWSなどのクラウドサーバー上で動かすことになると思います。

今回示したコードだと、utils.pyとbot.pyを同じ階層に作って、自分のAPIをbot.py内の *** に入れ、bot.pyを実行することになります。

うまく実装できていればbot.logという名前のログファイルとコンソールに「売り注文をします 現在価格:992048.0」のようなログが記録されていきます。

aws console

さて、今回はここまでにします。

今回示したコードはシンプルなbotの構成アイデアが伝わるように書いたつもりです。 そのため、実際に動かす際に直面しうる様々な例外的な状況への対応は足りていないかもしれません。 また、今回仮設定しているロジックも非常に単純なものなので到底利益を生み出せるものではないと考えられます。

では、利益を生み出せるような戦略はどのように探していけば良いのか?ということで、最終回の次回はbotのバックテストと応用的な自動取引の概観について書いていきたいと思います。

 

トラブルを防ぐために最後にいくつか注意書きです。

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

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

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

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

要はコードを参考にするのも、自動取引を行うのも、全て自己責任でお願いしますということです。

今回のまとめ

  • ライブラリの利用やクラスの実装によりシンプルな自動取引コードが実装できる

BACK