これまでのあらすじ
こんにちは、ミナピピン(@python_mllover)です。今回も引き続きPythonでビットコインの自動取引プログラムを実装について解説していきます。
これまでは日本の仮想通貨取引所「GMOコイン」の提供しているAPI機能を使って価格データの取得と口座残高の確認・注文処理を行ってきました。今回はいよいよ満を持してこの2つ記事でやったことを組み合わせて簡単な自動売買Botを作ってみます。
この記事で作るのはあくまで簡単なテクニカル指標に従って自動で売買するプログラムなので儲かるとかそういうことは考えずあくまで動かすことを目的としています。
コードが動くと楽しいですし達成感がありますのでプログラミングのモチベ向上になると思います。
自動売買のトレードシグナルを作る
これまでの記事では「GMOコイン」の提供しているAPI機能を使って価格データの取得と口座残高の確認・注文処理を行ってきました.
最初の記事の「Pythonで自動売買Botを作る①~システムトレードの流れを把握する」でも説明したように、自動売買botを作る上においてはプログラムから売買注文を出す処理の他に自動で売買を行う判断に使う「トレードシグナル」を作る必要があります。
このトレードシグナルを何にするかは人によって変わってきます。基本的には過去の価格データをもとに売買シュミレーション(バックテスト)を行ってその中でリターンが高かったトレードシグナルを採用するというのが一般的です。
関連記事:【Python】トレード戦略をバックテストして有効性を検証する
本来ならばバックテストをしてリターンが出せているかを確認したりしますが、今回はとりあえず簡単なものを作るだけなので、有名なテクニカル指標である移動平均線を使います。
移動平均線とは何か?
まず移動平均についてざっと説明すると、移動平均線とは、ある一定期間の価格から平均値を計算し、その数値を線で表したものです。その日を含めた過去何日間(または何週間)かの価格を毎日計算するため、平均値が移動していくことから、移動平均と呼ばれます。
例えば[100・110・120・130・140・150・160]という値動きをしている金融商品があったとするならば、その5日移動平均はまず4日目まではデータが足りないので算出不可能です。
そして5日目からの移動平均は(100+110+120+130+140)÷5=120、6日目の移動平均は(110+120+130+140+150)÷5=130、7日目の移動平均は(120+130+140+150+160)÷5=140、という風な計算になります。
このように計算期間がローリングして移動していくので、移動平均(rolling_means)と言われています。そして、この移動平均は代表的なテクニカル指標のひとつで、価格の傾向(トレンド)など、相場の方向性を図る意味合いでよく使われます。
YahooFinanceとかにもチャート画面にも移動平均線はデフォルトで乗っていたり、移動平均はテクニカルの王道中の王道なので投資・トレードに興味のある人は頭の隅にとどめておきましょう。
他にも移動平均の考え方はMACDなど、他のテクニカルチャートにも応用して利用されています。株式投資やFXを少しでもやったことのある人ならわかると思いますが、ようはチャートに画面に引いてあるこんな線のことですね。
引用元:https://www.jibunbank.co.jp/products/foreign_deposit/chart/help/sma/
移動平均線の使い方
移動平均線の使い方としては、任意の2つの期間で計算した長期移動平均線と短期移動平均線が交差する「ゴールデンクロス」「デッドクロス」がトレードシグナルです。
まず「ゴールデンクロス」とは短期移動平均線が長期移動平均線を上に突き上げる形で交差することを指し、直近の価格が上向きだということで上がる可能性が高い=買い、反対に「デッドクロス」とは短期移動平均線が長期移動平均線を下に突き抜ける形で交差することを指し、直近の価格が下向きだということで下がる可能性が高い=売りという感じになります。
引用元:https://www.jibunbank.co.jp/products/foreign_deposit/chart/help/sma/
まあ「ゴールデンクロス=買い、デッドクロス=売り」という感じで覚えていれば問題ありません。このゴールデンクロスとデッドクロスを構成する長期と短期の2つの移動平均線の計算期間は人によってそれぞれですが、一般的に「5日・25日」とか「25日・75日」とかいう組み合わせがよく見かけますね。
Pythonで移動平均線を計算する
というわけで移動平均の計算方法と使い方についてざっと説明したので次は実際にPythonで移動平均線計算していきます。移動平均の計算方法は簡単なので、自分でfor文で計算するのも全然アリですが、Pythonだと「Pandas」というライブラリを使えば、簡単に計算できます。
このPandasは基本的にエクセルのような表計算をpythonに実装するためのライブラリですが、拡張機能が豊富で、今回みたく移動平均線が計算出来たり日経平均株価やダウ工業平均の価格データも関数1つで入手できるので非常に便利なライブラリです。pythonを使いこなすあたっては絶対に使えるようにしておきたいライブラリです。
参照記事:【Python】pandasで日経平均の株価データをスクレイピングする
Pandasはpythonの拡張ツールである「Anaconda」をインストールしていればデフォルトでインストールされているので、pipでわざわざインストールうる必要はなく、importするだけで使用できます。
参照記事:【Python】Anacondaのインストールと初期設定から便利な使い方までを徹底解説!
加えてPythonの拡張ソフトウェアである「Anaconda」もPythonでプログラミングするにあたって非常に便利なソフトなので絶対にインストールしておいたほうが良いです。
そして、今回移動平均を計算のもとになるビットコインの価格データですが、tickerをちまちま集めていると時間が掛かるので、今日はとりあえず仮想通貨の価格サイトからビットコインの価格データを引っ張ってきます。
ちなみにトレードシグナルの計算のもとになるビットコインを始めとする仮想通貨の価格データの集め方は以下の2つの方法がありますが、基本的には②でいいと思います。
①「取引所が配信しているtickerを一定時間ごとに取得する」
②「仮想通貨の価格データを集めているサイトから引っ張ってくる」
そして、今回使うビットコインの価格データは「Coingecko」という仮想通貨の価格サイトから取得します。他にもビットコインの価格データは「CryptoWatch」という仮想通貨価格サイトからも取得できます。
参照記事:【Python】CoinGeckoのAPIでビットコイン・アルトコインの価格データを取得する
という訳で上の記事で紹介した自作の関数を使ってビットコインの価格データの取得&前処理を行い、そのデータから移動平均線を計算していきます。
# ビットコインの価格データを取得するプログラム def get_bitcoinprice(ticker): """[CoinGeckoのAPIからビットコインの価格を取得する関数] Arguments: ticker {[str]} -- [各仮想通貨のticker(例:bitcoin・ethereum・ripple…)] Returns: [DataFrame] -- [日付・価格・変化率のデータフレーム] """ # APIから価格データを取得する url = 'https://api.coingecko.com/api/v3/coins/' + \ str(ticker) + '/market_chart?vs_currency=jpy&days=1' r = requests.get(url) r = json.loads(r.text) # jsonをデータフレームに変換 bitcoin = pd.DataFrame(r['prices'], columns=['date', 'price']) # データフレームに変化率の列を加える bitcoin['change'] = bitcoin['price'].pct_change() return bitcoin # 関数を使ってビットコインの価格データを取得する bitcoinprice = pd.Series(get_bitcoinprice('bitcoin')['price']) # データの確認 print(bitcoinprice.head())
これは直近1日分のビットコインの5分足価格データを取得する関数なので、数値は実行した日によって返ってくる数値は変わってしまいますが、こういう感じの結果が返ってくると思います。
次はこのビットコインの5分足の価格データから移動平均線を計算します。Pythonでの移動平均の計算は「変数名.rolling(window=任意の数).mean()」で計算できます。
引数のwindowで計算する移動平均を計算する期間を指定できます。今回は5分足のデータから25分と75分の移動平均を計算したいので、window=5とwindow=15として、25分移動平均線と75分移動平均線を計算します。
# Pandasで移動平均線を計算する SMA25 = price.rolling(window=5).mean() SMA75 = price.rolling(window=15).mean()
これで移動平均が計算できました。一応確認のためビットコインの価格と移動平均線をプロットしてみます。
# ビットコインの価格と移動平均線(5日・25日)をプロットする from pylab import rcParams import matplotlib.pyplot as plt rcParams['figure.figsize'] = 15, 5 plt.plot(bitcoinprice, label='bitcoin') plt.plot(SMA25, label='EMA5') plt.plot(SMA75, label='EMA25') plt.legend() plt.grid(which='both')
こういう感じのグラフがプロットできます。pythonは計算だけでなく、「Matplotlib」というライブラリを使うと様々なグラフをプロットすることもできるので、便利です。
関連記事:【Python】Matplotlibでのグラフ作成の基本をわかりやすく解説する
こんな感じで価格データの取得とトレードシグナルとする長期と短期の2つの移動平均線の計算ができました。
これとシリーズ3つ目の記事「Pythonで自動売買Botを作る③~プライベートAPIを使って仮想通貨を売買する」で説明したプライベートAPIを使った売買注文のプログラムでPythonでビットコインを自動売買するために必要な要素はすべて集まったので、次はこれらを組み合わせたPythonによるビットコインの自動売買botを作っていきます。
ビットコインの自動売買botの作る
というわけで、いよいよ本題の日本の仮想通貨取引所「GMOコイン」で移動平均線を使ったビットコインの自動売買プログラムの作り方とコードの中身を解説していきます。
import numpy as np import pandas as pd import time from datetime import datetime import talib import requests import json import hmac import hashlib #GMOコインのAPI鍵 api_key = '' secret = '' def build_position(ticker,side, amount, exe_type, price='', loss_rate=''): timestamp = '{0}000'.format(int(time.mktime(datetime.now().timetuple()))) method = 'POST' endPoint = 'https://api.coin.z.com/private' path = '/v1/order' reqBody = { "symbol": ticker, "side": side, "executionType": exe_type, "timeInForce": "FAK", "price": price, "losscutPrice": loss_rate, "size": amount } text = timestamp + method + path + json.dumps(reqBody) sign = hmac.new(bytes(secret.encode('ascii')), bytes(text.encode('ascii')), hashlib.sha256).hexdigest() headers = { "API-KEY": api_key, "API-TIMESTAMP": timestamp, "API-SIGN": sign } res = requests.post(endPoint + path, headers=headers, data=json.dumps(reqBody)) return res.json() def get_position(): timestamp = '{0}000'.format(int(time.mktime(datetime.now().timetuple()))) method = 'GET' endPoint = 'https://api.coin.z.com/private' path = '/v1/openPositions' text = timestamp + method + path sign = hmac.new(bytes(secret.encode('ascii')), bytes(text.encode('ascii')), hashlib.sha256).hexdigest() parameters = { "symbol": "BTC_JPY", "page": 1, "count": 100 } headers = { "API-KEY": api_key, "API-TIMESTAMP": timestamp, "API-SIGN": sign } res = requests.get(endPoint + path, headers=headers, params=parameters) return res.json() def close_position(ticker, side, amount, exe_type, position_id): timestamp = '{0}000'.format(int(time.mktime(datetime.now().timetuple()))) method = 'POST' endPoint = 'https://api.coin.z.com/private' path = '/v1/closeOrder' reqBody = { "symbol": ticker, "side": side, "executionType": exe_type, "timeInForce": "", "price": "", "settlePosition": [ { "positionId": position_id, "size": amount } ] } text = timestamp + method + path + json.dumps(reqBody) sign = hmac.new(bytes(secret.encode('ascii')), bytes(text.encode('ascii')), hashlib.sha256).hexdigest() headers = { "API-KEY": api_key, "API-TIMESTAMP": timestamp, "API-SIGN": sign } res = requests.post(endPoint + path, headers=headers, data=json.dumps(reqBody)) print('a',endPoint + path) print('b',headers) print('a',reqBody) return res.json() def exe_position(side,position): if side == 'BUY': close_side = 'SELL' elif side == 'SELL': close_side = 'BUY' for i in position: if i['side']=='BUY': close_res = close_position(i['symbol'], close_side, i['size'], 'MARKET',i['positionId']) if close_res['status'] ==0: print('買い注文を決済しました') else: print(close_res) elif i['side']=='SELL': close_res = close_position(i['symbol'], close_side, i['size'], 'MARKET',i['positionId']) if close_res['status'] ==0: print('売り注文を決済しました') else: print(close_res) def exe_all_position(): position = get_position() if position['data'] == {}: print('ポジションはありません') else: for i in position['data']['list']: if i['side']=='BUY': close_res = close_position(i['symbol'], 'SELL', i['size'], 'MARKET',i['positionId']) if close_res['status'] ==0: print(datetime.now(), '注文を決済(sell)しました') else: print(close_res) elif i['side']=='SELL': close_res = close_position(i['symbol'], 'BUY', i['size'], 'MARKET',i['positionId']) if close_res['status'] ==0: print(datetime.now(), '注文を決済(buy)しました') else: print(close_res) def get_btcprice(): url = 'https://api.cryptowat.ch/markets/bitflyer/btcjpy/ohlc?periods=300&after=' + str(int(time.time()-10800)) r = requests.get(url) r2 = json.loads(r.text) pricedata = [] for data in r2['result']['300']: dt = datetime.fromtimestamp(data[0]) price = data[4] vol = data[5] pricedata.append([dt, price, vol]) return pd.DataFrame(pricedata, columns=['date','price','vol']) def order_process(side): if side == 'BUY': result = build_position(ticker,side, amount, exe_type, price='', loss_rate='') print('ビットコインを' + str(get_position()['data']['list'][0]['price']) + '円でロングしました') elif side == 'SELL': result = build_position(ticker,side, amount, exe_type, price='', loss_rate='') print('ビットコインを' + str(get_position()['data']['list'][0]['price']) + '円でショートしました') else: pass def algorithmic_trade(): """[トレードのロジック部分を記述した関数] """ if momentam[-1] > 0 and macd[2][-1] > 0: order_process('BUY') elif momentam[-1] < 0 and macd[2][-1] < 0 and trend[2][-1] < 0 and vol_trend.iloc[-1] < 0: order_process('SELL') else: order_process('no') print('シグナル無反応',momentam[-1],macd[2][-1]) def get_balance(): timestamp = '{0}000'.format(int(time.mktime(datetime.now().timetuple()))) method = 'GET' endPoint = 'https://api.coin.z.com/private' path = '/v1/account/margin' text = timestamp + method + path sign = hmac.new(bytes(secret.encode('ascii')), bytes(text.encode('ascii')), hashlib.sha256).hexdigest() headers = { "API-KEY": api_key, "API-TIMESTAMP": timestamp, "API-SIGN": sign } res = requests.get(endPoint + path, headers=headers) return res.json() # ーーーーーーーーーーーーーーーbot本体の処理ーーーーーーーーーーーーーーーーーーーーー# while True: # 銘柄と売買区分・最初に入金した金額の設定 ticker = "BTC_JPY" exe_type = 'MARKET' default_balance = 50000 # ポジションを決済する exe_all_position() time.sleep(3) after_balances = int(get_balance()['data']['availableAmount']) profit_loss = after_balances - default_balance if profit_loss > 0: print('現在の残高は' + str(after_balances) + '円で、' + str(profit_loss) + '円の利益です') elif profit_loss <= 0: print('現在の残高は' + str(after_balances) + '円で、' + str(profit_loss) + '円の損失です') time.sleep(3) # 価格取得&シグナル算出 botを走らせる前準備(テクニカル指標はグローバル変数) bitcoin_price = get_btcprice()['price'] today_price = bitcoin_price[-1] print('現在のビットコイン価格は' + str(today_price) + '円です') # 残高とBTC価格から注文量を計算する amount = round(after_balances / today_price * 2, 2) numpyprice = np.array([float(x) for x in bitcoin_price]) # macdの短期トレンドを計算す macd = talib.MACD(numpyprice, fastperiod=5, slowperiod=25, signalperiod=3) momentam = talib.MOM(numpyprice, timeperiod=6) # macdの長期トレンドを計算する trend = talib.MACD(numpyprice, fastperiod=9, slowperiod=10, signalperiod=10) # 出来高のトレンドを計算する df = get_btcprice()['vol'] vol_trend = df.rolling(window=5).mean() - df.rolling(window=25).mean() # 指定したシグナルに従って取引する algorithmic_trade() print('-------------------------------------------')
このコードだとビットコインの価格を取得する→シグナルに使っているテクニカル指標を計算する→条件を満たしている場合買い注文を入れる→一定時間がたったら売り注文を入れるという処理を5分おきに繰り返しています。
短期移動平均線が長期移動平均線を突き抜けるゴールデンクロスという現象をプログラミングの条件式にすると、「n-5分における 長期移動平均線の数値>短期移動平均線の数値」and「n分における 長期移動平均線の数値<短期移動平均線の数値」という風になります。
そして、ゴールデンクロスは価格が上がる可能性が高いサインなので、ゴールデンクロスが発生したらビットコインを購入して、一定時間(5分後)にビットコインを売却するという処理を行っています。
ちなみにデッドクロスが発生した場合は売るという処理を行いたい場合は、売りから入ることができる「信用取引」ができる取引所である必要があります。残念ながらビットバンクは現物しかなく、信用取引で売りから入ることはできないので、売買機会が少なくなってしまいます。
終わり
以上がpythonでビットコインを自動売買するための知見です。今回はデモなので単純移動平均(SMA)と現物取引でしたが、株式やビットコインを自動売買するプログラムを組んで、システムトレードで本気で儲けようとする場合は、移動平均線ではなく、もっと有効性の高いトレード戦略を自分でバックテストなりで見つけたり、売りから入れる信用取引にするなどして、売買の条件処理をもう少し複雑にする必要があります。まあそれでも儲かるとは限りませんが・・・
また自動化した売買プログラムを常時実行させたい場合はレンタルサーバーと契約して、そこにプログラムのソースをおいてCronやWhile文で定期実行するのが一般的です。詳細は次の記事で解説しています
関連記事:Pythonで自動売買Botを作る⑤~レンタルサーバーでプログラムを定期実行する
ちなみにpythonで移動平均線以外のテクニカル指標を計算するにあたっては「ta-lib」というライブラリを使うと、MACD・RSI・ボリンジャーバンドなどメジャーなテクニカル指標は全部関数1つで計算することができるようになります。
参照記事:【Python】テクニカル指標が簡単に計算できるTa-libの使い方
とりあえずシステムトレード(自動売買)をプログラムで作る上で抑えておきたい流れとしては、①APIとかを使って板情報と価格を取得→②トレードシグナルを計算→③買いシグナルが出たら買い注文を出す→④売りシグナルがでたら売り注文を出す という感じですね。
これがBotを作るうえでの基本でここから色々アレンジをしていく形になります。そして、やはり本気でリターンを出そうと思うのであれば様々なトレード戦略やアービトラージやサーバーの高速化など色んな知識が必要になってきます。しばらく忙しいので更新できるか分かりませんが、時間が出来ればもっと凝ったコードも作っていきたいと思います。
続き→Pythonでビットコインの自動売買Botを作る⑤~レンタルサーバーでプログラムを定期実行する
【お知らせ】
Pythonの定期実行環境構築でお悩みの方はMENTAにて環境構築及びスクリプトの定期実行に関するご相談を受け付けておりますので、お気軽にご相談ください。(招待リンクから登録すると利用料が500円割引になるのでお得です)
招待リンク:https://menta.work/invite/wYZXKkSpS2fg9Z3f
自分のプロフィール:https://menta.work/user/26168
コメント
>条件を満たしている場合買い注文を入れる→一定時間がたったら売り注文を入れるという処理を5分おきに繰り返しています。
買い注文後すぐに売り注文を出すようなコードになっています。