Bitflyerの自動売買BOTに例外処理を入れて安定的に動くようにする

前回の記事の続きです。

前回までで、Bitflyerで自動的に売買シグナルを判定してエントリーし、さらに決済(利確・損切)まで自動で行うBOTを作ることができました。しかしこの自動売買BOTを実践で長時間、安定的に動かし続けるためには、「例外処理」が必要になります。

例外処理とは

簡単にいうと「サーバーエラー」の対策のことです。

自動売買BOTでは、pythonのプログラミングコード(構文や文法)自体に問題がなくても、外部のサーバーが原因で止まってしまうことがありえます。例えば、Bitflyerに買い注文を出したものの、Bitflyerのサーバーが混雑していてエラーになってしまったり、一時的にダウンしていて応答がなくなるような場合です。

このような場合に何も対策をしていないと、自動売買BOT自体もエラーを吐いて止まってしまいます。例えば、以下のような感じです。

↑ pythonで買い注文を出したところ、Bitflyerのサーバーから「500 Internal Server Error」(サーバーエラー)が返ってきたため、そこでプログラムの実行が停止されてしまった画面です。

キチンと例外処理をしていれば、外部サーバーから思わぬエラーが返ってきても、そこで自動売買BOTが停止することはなくなります。自動売買BOTがポジションを持ったままエラーを吐いて止まってしまうと、知らない間に損失が膨らみ続ける危険もあります。

特にBitflyerはサーバーから頻繁に500エラーが返ってくるため、きちんと例外処理を書くことが大切です。

例外処理の基本的な書き方

例外処理の基本的な構文は以下です。

try:
	#リクエスト通信
except 例外名 as e:
	#エラーが返ってきたときの処理

エラー処理が発生する可能性があるのは、リクエスト通信が必要な場面です。具体的にはAPIを使用する場面ですね。前回の記事で作成した自動売買BOTの例でいえば、以下のような場面です。

・Cryptowatchから価格データを取得する
・Bitflyerに買い・売り注文を出す
・Bitflyerから未約定の注文一覧を取得する
・Bitflyerから建玉の一覧を取得する

上記の場面では、外部サーバーとの通信が発生するため、すべて例外処理を書かなければなりません。逆にいえば、通信が発生する箇所にすべて例外処理を書けば、通信エラーで自動売買BOTが停止することはなくなります。

具体的なコード例

例えば、前回のCryptowatchから価格データを取得する関数に例外処理を入れてみましょう。前回までのコードは以下でした。

修正前

# Cryptowatchから価格を取得する関数
def get_price(min,i):
	response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc", params = { "periods" : 60 })
	data = response.json()
	return { "close_time" : data["result"][str(min)][i][0],
		"open_price" : data["result"][str(min)][i][1],
		"high_price" : data["result"][str(min)][i][2],
		"low_price" : data["result"][str(min)][i][3],
		"close_price": data["result"][str(min)][i][4] }

ここに例外処理を入れてみましょう。

例外処理

# Cryptowatchから価格を取得する関数
def get_price(min,i):
	while True:
		try:
			response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc", params = { "periods" : 60 }, timeout = 5)
			response.raise_for_status()
			data = response.json()
			return { "close_time" : data["result"][str(min)][i][0],
				"open_price" : data["result"][str(min)][i][1],
				"high_price" : data["result"][str(min)][i][2],
				"low_price" : data["result"][str(min)][i][3],
				"close_price": data["result"][str(min)][i][4] }
		except requests.exceptions.RequestException as e:
			print("Cryptowatchの価格取得でエラー発生 : ",e)
			print("10秒待機してやり直します")
			time.sleep(10)

上記のコードでは、正常にデータを取得できれば価格データを返し、もし通信エラーなどの異常が発生した場合は、10秒間待機した後で、データの取得をやり直すようにプログラムしています。そのため、全体をwhile文で囲んでいます。

「try:」の部分で、requests()というリクエスト通信用の関数を使い、「except requests.exceptions.RequestException as e:」の部分で、通信エラーがあった場合の処理を記述しています。エラーがあった場合は、print(“Cryptowatchの価格取得でエラー発生”, e)でエラーの内容も表示するようにしています。

実行結果の例

これを実行すると、Cryptowatchのサーバーから通信エラーが返ってきても、BOTは停止することなく自動的に10秒待機して通信をやり直してくれます。

Bitflyerへの注文の例外処理

同じように、Bitflyerのサーバーと通信する箇所にも例外処理を入れてみましょう。この記事の自動売買BOTは、すべてCCXTライブラリ経由でBitflyerと通信しています。そのため、例外処理はCCXTライブラリで用意されている「ccxt.BaseError」を使います。

ここでは、手仕舞いの成行注文を出す箇所のコードを使って説明します。

修正前

order = bitflyer.create_order(
	symbol = 'BTC/JPY',
	type='market',
	side='sell',	
	amount='0.01',
	params = { "product_code" : "FX_BTC_JPY" })
flag["position"]["exist"] = False
time.sleep(30)

ここに同じように例外処理を入れると、以下のようになります。

例外処理

while True:
	try:
		order = bitflyer.create_order(
			symbol = 'BTC/JPY',
			type='market',
			side='sell',
			amount='0.01',
			params = { "product_code" : "FX_BTC_JPY" })
		flag["position"]["exist"] = False
		time.sleep(30)
		break
	except ccxt.BaseError as e:
		print("BitflyerのAPIでエラー発生",e)
		print("注文の通信が失敗しました。30秒後に再トライします")
		time.sleep(30)

通信に失敗した場合に、何度も自動でやり直しをさせたい場合は、このように「While文」で囲みます。While文で書くときは、「break」を書くのを忘れないようにしてください。breakは、成功したらwhile文を抜ける、という意味です。

やり直しをさせる必要がない場合は、While文で囲む必要はありません。

実行結果の例

同じように、すべての通信箇所に例外処理を書いておくとBitflyerの注文が通りにくい時間帯でも、BOTがエラーで停止することはなくなります。

「例外名」を自分で調べる方法

さきほどの構文の「except 例外名 as e:」の部分を、なぜ「except requests.exceptions.RequestException」と書くのか?疑問に思った方もいるかもしれません。これは、requests()ライブラリの仕様でそう決まっているからです。

つまりrequests()ライブラリを開発してくれた人たちが、そういう名前のエラー(例外)をあらかじめ用意してくれている、ということです。

・requests()の公式ドキュメント

例外の名前の部分は使用するライブラリによって違います。例えば、CCXTライブラリで例外処理を書くときは、「except ccxt.BaseError as e:」と書きます。大抵の場合は、「ライブラリ名 error handling」「ライブラリ名 例外処理」などで検索すると、何らかのヒントが見つかります。

例)
requests 例外処理
CCXT error handling

・CCXTの例外処理の公式ドキュメント

例外処理を入れた完成版のpythonコード

さて、それでは前回の記事のコードに例外処理を入れたバージョンを作ってみましょう! 以下がpythonコードの全文です。


import requests
from datetime import datetime
import time
import ccxt


bitflyer = ccxt.bitflyer()
bitflyer.apiKey = '**********'
bitflyer.secret = '**********'


# Cryptowatchから価格を取得する関数
def get_price(min,i):
	while True:
		try:
			response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc", params = { "periods" : 60 }, timeout = 5)
			response.raise_for_status()
			data = response.json()
			return { "close_time" : data["result"][str(min)][i][0],
				"open_price" : data["result"][str(min)][i][1],
				"high_price" : data["result"][str(min)][i][2],
				"low_price" : data["result"][str(min)][i][3],
				"close_price": data["result"][str(min)][i][4] }
		except requests.exceptions.RequestException as e:
			print("Cryptowatchの価格取得でエラー発生 : ",e)
			print("10秒待機してやり直します")
			time.sleep(10)


# 時間と始値・終値を表示する関数
def print_price( data ):
	print( "時間: " + datetime.fromtimestamp(data["close_time"]).strftime('%Y/%m/%d %H:%M') + " 始値: " + str(data["open_price"]) + " 終値: " + str(data["close_price"]) )


# 各ローソク足が陽線・陰線の基準を満たしているか確認する関数
def check_candle( data,side ):
	realbody_rate = abs(data["close_price"] - data["open_price"]) / (data["high_price"]-data["low_price"]) 
	increase_rate = data["close_price"] / data["open_price"] - 1
	
	if side == "buy":
		if data["close_price"] < data["open_price"] : return False
		elif increase_rate < 0.0003 : return False
		elif realbody_rate < 0.5 : return False
		else : return True
		
	if side == "sell":
		if data["close_price"] > data["open_price"] : return False
		elif increase_rate > -0.0003 : return False
		elif realbody_rate < 0.5 : return False
		else : return True


# ローソク足が連続で上昇しているか確認する関数
def check_ascend( data,last_data ):
	if data["open_price"] > last_data["open_price"] and data["close_price"] > last_data["close_price"]:
		return True
	else:
		return False

# ローソク足が連続で下落しているか確認する関数
def check_descend( data,last_data ):
	if data["open_price"] < last_data["open_price"] and data["close_price"] < last_data["close_price"]:
		return True
	else:
		return False


# 買いシグナルが出たら指値で買い注文を出す関数
def buy_signal( data,last_data,flag ):
	if flag["buy_signal"] == 0 and check_candle( data,"buy" ):
		flag["buy_signal"] = 1

	elif flag["buy_signal"] == 1 and check_candle( data,"buy" )  and check_ascend( data,last_data ):
		flag["buy_signal"] = 2

	elif flag["buy_signal"] == 2 and check_candle( data,"buy" )  and check_ascend( data,last_data ):
		print("3本連続で陽線 なので" + str(data["close_price"]) + "で買い指値を入れます")
		flag["buy_signal"] = 3
		
		try:
			order = bitflyer.create_order(
				symbol = 'BTC/JPY',
				type='limit',
				side='buy',
				price= data["close_price"],
				amount='0.01',
				params = { "product_code" : "FX_BTC_JPY" })
			flag["order"]["exist"] = True
			flag["order"]["side"] = "BUY"
			time.sleep(30)
		except ccxt.BaseError as e:
			print("Bitflyerの注文APIでエラー発生",e)
			print("注文が失敗しました")
			
	else:
		flag["buy_signal"] = 0
	return flag


# 売りシグナルが出たら指値で売り注文を出す関数
def sell_signal( data,last_data,flag ):
	if flag["sell_signal"] == 0 and check_candle( data,"sell" ):
		flag["sell_signal"] = 1

	elif flag["sell_signal"] == 1 and check_candle( data,"sell" )  and check_descend( data,last_data ):
		flag["sell_signal"] = 2

	elif flag["sell_signal"] == 2 and check_candle( data,"sell" )  and check_descend( data,last_data ):
		print("3本連続で陰線 なので" + str(data["close_price"]) + "で売り指値を入れます")
		flag["sell_signal"] = 3
		
		try:
			order = bitflyer.create_order(
				symbol = 'BTC/JPY',
				type='limit',
				side='sell',
				price= data["close_price"],
				amount='0.01',
				params = { "product_code" : "FX_BTC_JPY" })
			flag["order"]["exist"] = True
			flag["order"]["side"] = "SELL"
			time.sleep(30)
		except ccxt.BaseError as e:
			print("BitflyerのAPIでエラー発生",e)
			print("注文が失敗しました")

	else:
		flag["sell_signal"] = 0
	return flag


# 手仕舞いのシグナルが出たら決済の成行注文を出す関数
def close_position( data,last_data,flag ):
	if flag["position"]["side"] == "BUY":
		if data["close_price"] < last_data["close_price"]:
			print("前回の終値を下回ったので" + str(data["close_price"]) + "あたりで成行で決済します")
			while True:
				try:
					order = bitflyer.create_order(
						symbol = 'BTC/JPY',
						type='market',
						side='sell',
						amount='0.01',
						params = { "product_code" : "FX_BTC_JPY" })
					flag["position"]["exist"] = False
					time.sleep(30)
					break
				except ccxt.BaseError as e:
					print("BitflyerのAPIでエラー発生",e)
					print("注文の通信が失敗しました。30秒後に再トライします")
					time.sleep(30)
			
	if flag["position"]["side"] == "SELL":
		if data["close_price"] > last_data["close_price"]:
			print("前回の終値を上回ったので" + str(data["close_price"]) + "あたりで成行で決済します")
			while True:
				try:
					order = bitflyer.create_order(
						symbol = 'BTC/JPY',
						type='market',
						side='buy',
						amount='0.01',
						params = { "product_code" : "FX_BTC_JPY" })
					flag["position"]["exist"] = False
					time.sleep(30)
					break
				except ccxt.BaseError as e:
					print("BitflyerのAPIでエラー発生",e)
					print("注文の通信が失敗しました。30秒後に再トライします")
					time.sleep(30)
	return flag


# サーバーに出した注文が約定したかどうかチェックする関数
def check_order( flag ):
	try:
		position = bitflyer.private_get_getpositions( params = { "product_code" : "FX_BTC_JPY" })
		orders = bitflyer.fetch_open_orders(
			symbol = "BTC/JPY",
			params = { "product_code" : "FX_BTC_JPY" })
	except ccxt.BaseError as e:
		print("BitflyerのAPIで問題発生 : ",e)
	else:
		if position:
			print("注文が約定しました!")
			flag["order"]["exist"] = False
			flag["order"]["count"] = 0
			flag["position"]["exist"] = True
			flag["position"]["side"] = flag["order"]["side"]
		else:
			if orders:
				print("まだ未約定の注文があります")
				for o in orders:
					print( o["id"] )
				flag["order"]["count"] += 1
				
				if flag["order"]["count"] > 6:
					flag = cancel_order( orders,flag )
			else:
				print("注文が遅延しているようです")
	return flag



# 注文をキャンセルする関数
def cancel_order( orders,flag ):
	try:
		for o in orders:
			bitflyer.cancel_order(
				symbol = "BTC/JPY",
				id = o["id"],
				params = { "product_code" : "FX_BTC_JPY" })
		print("約定していない注文をキャンセルしました")
		flag["order"]["count"] = 0
		flag["order"]["exist"] = False
		
		time.sleep(20)
		position = bitflyer.private_get_getpositions( params = { "product_code" : "FX_BTC_JPY" })
		if not position:
			print("現在、未決済の建玉はありません")
		else:
			print("現在、まだ未決済の建玉があります")
			flag["position"]["exist"] = True
			flag["position"]["side"] = position[0]["side"]
	except ccxt.BaseError as e:
		print("BitflyerのAPIで問題発生 : ", e)
	finally:
		return flag



# ここからメイン
last_data = get_price(60,-2)
print_price( last_data )
time.sleep(10)

flag = {
	"buy_signal":0,
	"sell_signal":0,
	"order":{
		"exist" : False,
		"side" : "",
		"count" : 0
	},
	"position":{
		"exist" : False,
		"side" : ""
	}
}

while True:
	if flag["order"]["exist"]:
		flag = check_order( flag )
	
	data = get_price(60,-2)
	if data["close_time"] != last_data["close_time"]:
		print_price( data )
		if flag["position"]["exist"]:
			flag = close_position( data,last_data,flag )			
		else:
			flag = buy_signal( data,last_data,flag )
			flag = sell_signal( data,last_data,flag )
		last_data["close_time"] = data["close_time"]
		last_data["open_price"] = data["open_price"]
		last_data["close_price"] = data["close_price"]
		
	time.sleep(10)

Bitflyerのサーバーと通信する部分(買い注文・売り注文・手仕舞いの成行注文・キャンセル・注文一覧の取得・建玉一覧の取得)には、すべて例外処理を書いています。面倒ですが、これがBOTを安定的に動かす上では重要になります。

完成!

これで「最初の自動売買BOTを作ってみる!」の章は完成です。
この章を読んだ方は、以下のことができるようになったはずです。

1.数字で定義できる売買ロジックを考える
2.ローソク足の実体やヒゲの長さをシグナルにする
3.ローソク足のパターン(三兵)をシグナルにする
4.過去の価格データを使ってシグナルをテストする
5.売買シグナルで自動的にエントリーし、手仕舞いするBOTを作る
6.サーバーエラーで停止しないように例外処理を書く

ここまで勉強したことを応用してカスタマイズすれば、「3本以上連続で陰線だが、徐々に実体が短くなって出来高も減っている」という局面で、「実体が短くて長い下ヒゲの付いた陽線(ハンマー)が現れたら買いでエントリーする」というような、より実践的な売買ロジックも作れるでしょう。

※ ちなみに、これは「出来高・価格分析の完全ガイド」という本で紹介されている売買ロジックです。興味がある方は練習で実装してみてください。

次回以降の記事

次以降の章では、もう少し高度な「移動平均線」「MACD」などのトレンドフォロー指標、「RSI」「ストキャスティクス」などのオシレーターをpythonで実装する方法を解説します。

自動売買BOTなどのシステムトレードは、もともとチャートパターン(例えば、Wボトムやエリオット波動などの視覚的なもの)を認識するよりも、MACDヒストグラムやRSIなどの数値指標で売買シグナルを判断する方が得意です。そのため、自動売買BOTの精度を上げるためには、テクニカル指標を理解して実装できるようになることが必須です。

次の記事では、自動売買BOTと相性のいいテクニカル指標とその実装方法を解説していきます。

「Bitflyerの自動売買BOTに例外処理を入れて安定的に動くようにする」への17件のフィードバック

  1. 初めてpython勉強しているのですが、何から始めればいいかわからない事が多かったのですごくありがたいです!
    続きも楽しみにしています

    1. ありがとうございます!
      先に勝率検証編を書くことにしたので、
      テクニカル指標はもう少しお待ちください^^

  2. はじめまして
    BTCFXのbot制作で調べていて、こちらにたどり着きました。
    とても丁寧に1から説明をされていて、とても読みやすくためになります。ありがとうございます。

    1点教えて頂きたい点があるのですが、
    last_data = get_price(60,-2)
    とある部分、
    “60”は1分足のデータ取得のため、と思いますが、
    その後ろの”-2″は、何を意味するのでしょうか?

    もしよければ教えてください。
    よろしくお願い致します。

    1. コメントありがとうございます!

      この記事のコードでは、get_price()の2番目の引数は、
      「Cryptowatchで取得したローソク足データのうち、
      〇番目のローソク足データを取り出す」
      という部分の〇番目を指定しています。

      pythonでは -1 を指定することで、配列の一番最後の値
      (=一番最新のローソク足)を指定することができます。
      ただし、一番最新のローソク足はリアルタイムで形成されているもので、
      まだ高値・安値・終値が確定していないデータです。

      そのため、終値まで固まった中で最も新しいデータとして、
      -2 を指定して、後ろから2番目のローソク足データを取り出しています。
      以下の記事でも解説しているので、良かったら読んでみてください!^^

      参考:Bitflyerのローソク足の情報をpythonで取得する

      1. リョータさん、返信ありがとうございます。
        “-2″をご説明いただいてありがとうございました。とってもよく解りました。

        def get_price(min,i):
        の関数で、最新からひとつ前の各データを取得するように定義されているのですね。
        教えていただいたように前の記事からひとつひとつ勉強していきます。
        近道しちゃだめですね。
        リョータさんの説明、とても解りやすいです。
        これからの記事も楽しみにしています。

        1. いえいえ、全然近道しちゃってください(笑)
          他にも同じ疑問を持ってる方もいるかもしれないので
          一応、前の記事も貼らせてもらいました。
          この記事から読み始めてくださってる方は多いみたいですね。

          ありがとうございます、
          また来てください^^

          1. 少しずつpythonの勉強をしています。
            本当にためになるブログで、ありがたいことです。

            自分でbot作れるようになるまで頑張ります。

  3. はじめまして。
    こちらのページを参考にbotを動かすことができました。
    有益な情報を無料で公開していただきありがとうございます。

    さて、突然で大変申し訳ないのですが、ちょっと一人では解決できないことがあり、もし原因がわかりましたら教えていただきたいのです。。
    bitflyer.create_orderで成行注文した際に、失敗した場合の例外処理を書いているのですが、注文が約定する前に処理を抜けていることが多々ございます。
    (注文が完了するまでは10秒間隔でリトライするようにしてます)
    上記事象の原因として考えられることがありましたらご教示いただきたく。

  4. すいません、上記の件勘違いでした。

    例外処理がうまく動かずポジションが残ってると思ってたんですが、おそらくコネクションタイムアウトのエラーで例外処理自体は正常に動作しておりました。
    しかし、裏側では注文自体は通っており、無駄に成行注文をしているように見受けられ、想定外のポジションが残っている状態でした。

    上記のような場合、無駄に発注しないための回避策は何かございますでしょうか。
    思いつくものがございましたら、ご教示いただきたく。
    お忙しところ恐れ入りますが、よろしくお願いいたします。

    1. コメントありがとうございます。
      接続のタイムアウトは厄介な通信エラーの1つで、レスポンスからはその注文リクエストが受理されたのか、拒否されたのか、遅延しているのか、等を確認する方法がありません。なので、少し時間を置いてポジションチェックの判定を入れてからリトライする、などの方法しかないと思います。対策としては、完璧ではありませんが、考えられるものを以下の記事で解説しています。

      Bitflyerの注文遅延・二重注文の対策コード

      通常のサーバーエラー(500エラー)と接続のタイムアウトとを別の例外処理にして、接続タイムアウトの場合は数十秒でリトライせずに、1分ほど待ってポジションチェックを入れる、等の方がいいのかもしれません。

      1. ご回答いただきありがとうございます。
        すでに二重注文について書かれた記事があったんですね!
        見逃してました。。失礼いたしました。

        一旦、アドバイスいただいたとおり、タイムアウトエラー(ccxt.NetworkError)とそれ以外(ccxt.ExchangeError)で処理を分けてみて様子を見たいと思います。

        アドバイスいただきありがとうございました。

  5. 素晴らしい記事をありがとうございます。1点初歩的な質問をさせてください。
    get_price()を定義している箇所で、While true→tryと続いておりますが、try内でbreakを記述しなくて良い理由を教えていただけますでしょうか。無限ループで価格情報を取得し続けることにはならないでしょうか。
    理解が及ばず恐縮ですが、ご教示いただけると幸いです。

    1. ご質問ありがとうございます。
      pythonでは、関数(def 〇〇)内に記述された return ×× は、「××を返して関数の実行を終了する(関数を呼び出した元の場所に戻る)」という意味です。return まで到達すると確実に関数から抜けることができので、関数内のwhile文についてbreakを書く必要はありません。関数を使わずにメイン処理の中に直接 while文を記述する場合は、breakが必要です。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です