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のBTCFXで自動売買ができる試作版のBOTを作る!

前回の記事の続きです。

エントリーの条件・手仕舞いの条件などの売買ロジックが完成したので、ここに実際にBitflyerにオーダーを出すコードを入れてみましょう。なお、ついでに今回の記事から「売りシグナルの判定」と「売りからのエントリー」にも対応できるようコードを改良します。(こちらの記事の答え合わせです)

暫定的な完成版の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):
	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] }


# 時間と始値・終値を表示する関数
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
		
		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"
			
	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
		
		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"

	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"]) + "あたりで成行で決済します")
			order = bitflyer.create_order(
				symbol = 'BTC/JPY',
				type='market',
				side='sell',
				amount='0.01',
				params = { "product_code" : "FX_BTC_JPY" })
			flag["position"]["exist"] = False
			
	if flag["position"]["side"] == "SELL":
		if data["close_price"] > last_data["close_price"]:
			print("前回の終値を上回ったので" + str(data["close_price"]) + "あたりで成行で決済します")
			order = bitflyer.create_order(
				symbol = 'BTC/JPY',
				type='market',
				side='buy',
				amount='0.01',
				params = { "product_code" : "FX_BTC_JPY" })
			flag["position"]["exist"] = False
	return flag


# サーバーに出した注文が約定したかどうかチェックする関数
def check_order( flag ):
	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" })
		
	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 ):
	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"]
	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)

こちらのコードをそのまま書き写せば、とりあえず動きます。
ただし安定的に(エラーで止まったりせずに)何日も連続で動かし続けるためには、あともう一工夫だけ必要です。これについては、この次の記事で解説します。

コードの解説

前回の記事からの変更点で、重要な部分について解説しておきます。

注文状況を管理する変数 flag

前回の記事でも、「買いシグナルの状況」「サーバーに出した注文状況」「ポジションの保有有無」などの情報を管理するために、flagという変数を作りました。今回は、売りシグナル・売りエントリーも考えないといけないので、以下のように変数flagを改造しましょう。

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

buy_signalとsell_signalは、それぞれ「何本連続で陽線が続いているか?」「何本連続で陰線が続いているか?」を管理するフラッグです。この値が「3」になったらその方向(BUY/SELL)でエントリーします。

orderは「現在、サーバーに未約定の注文があるか?」を管理するためのフラッグです。今回は、ロング・ショートの両側を考えるため、orderというフラッグの中に、さらにexistとsideという2つの要素を作ります。existが注文の有無、sideが買いか売りか、を管理するための要素です。

countは「一定時間で注文が約定しなければキャンセルする」というロジックのために使います。メイン処理では全体のループを10秒間隔で回すため、例えば、「1分間(60秒)約定しなければキャンセルする」というロジックを組むために、このcountをループごとに1ずつ増やし、6を超えたらオーダーキャンセルを出すようにします。

positionは、「現在、ポジション(建玉)があるかどうか?」を管理するためのフラッグです。こちらも「買いポジション」「売りポジション」の両方があることを考慮して、「side」という要素を持つ入れ子構造に修正しておきます。

ローソク足の条件を判定する関数

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

買いシグナルと売りシグナルの両方に対応できるように、「side」という引数を受けとり、それによってシグナルを出し分けるように改良しました。

買い注文を出す関数

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"
		

前回の記事で「#ここに買い注文のコードを入れる」と書いておいた部分に上記のコードを入れます。

bitflyer.create_order()と書けば、CCXT経由で簡単にbitflyerに注文を出すことができます。このコードの書き方は、「CCXTでBitflyerに注文を出す方法」の記事ですでに解説しましたので、そちらを参考にしてください。

同じように売りシグナルを判断して売り注文を出す関数も作っています。こちらは買い注文を単に反対にしただけなので、解説は省略します。

未約定の注文を管理する関数

def check_order( flag ):
	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" })
		
	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

前回の記事で、「# 注文状況を確認して通っていたら以下を実行」「# 一定時間で注文が通っていなければキャンセルする」と書いておいた部分に、上記のコードを入れていきます。

まず get_getpositions()で建玉の一覧を取得し、さらにfetch_open_orders()で未約定の注文一覧を取得します。

もし既に建玉があれば、指値注文が通ったと判断できますので「注文が約定しました!」と表示します。また、flag["order"]["exist"](未約定の注文の有無を管理するflag)をFalseに更新し、flag["orders"]["exist"](ポジションの有無を管理するflag)をTrueに更新します。

flag["order"]["exist"] = False
flag["position"]["exist"] = True
flag["position"]["side"] = flag["order"]["side"]

もしまだ建玉がなければ、orders(未約定の注文一覧)を確認します。

未約定の注文があれば、「まだ未約定の注文があります」と表示します。そして先ほど説明したように、flag["order"]["count"]というフラッグにカウント1を追加します。全体の処理はWhile文で10秒おきに回っているので、このフラッグのカウントが「6」以上になれば、「1分間約定しなかった」と判断して注文をキャンセルします。

cancel_order() というのは、今回の記事で新たに作成した注文キャンセルのための関数です。ここにキャンセル処理をそのまま書いてもいいのですが、少しだけ長い処理になるので、読みやすいように別の関数として外に出すことにしました。内容は後ほど説明します。

Bitflyerの遅延対策

最後のelse:の部分は、エントリー注文を出したものの、「建玉の一覧」にも「未約定注文の一覧」にも、どちらにも情報が存在しないパターンを捕まえています。

else:
	print("注文が遅延しているようです")

これは注文のリクエスト(通信)は正常に受理されたものの、サーバー側への注文の反映が「遅延」しているときにおこる現象です。Bitflyerでは1分以上注文の反映が遅延することもあるので、今回のように「建玉一覧」と「未約定注文一覧」の両方を取得しなければ、現在がどういう状態にあるのか判別することができません。

▽ 「遅延」と「未約定」の違いの例

※ 「遅延」はまだサーバー側に注文が認識(反映)されていない状態。「未約定」は注文が反映されてまだ約定していない状態。

注文をキャンセルする関数

# 注文をキャンセルする関数
def cancel_order( orders,flag ):
	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

さきほどの続きで、「未約定の注文一覧」をキャンセルするための関数です。CCXTライブラリを使えば、bitflyer.cancel_order()で未約定の注文をキャンセルすることができます。

・CCXTでBitflyerの注文をキャンセルする方法

注文のキャンセルを出した時点で、「約定しない注文をキャンセルしました」と表示し、flag["order"]["exist"](未約定の注文を管理するフラッグ)をFalseに戻します。またカウント用のフラッグも0に戻しておきます。

ただしBitflyerの注文キャンセルのAPIを使った場合でも、通信のすれ違いで注文が約定してしまうことがあります。そのため、未約定の注文をキャンセルした後に、20秒間待機し、続けて「ポジションが残ってないかどうか?」を念のために確認します。それが以下の部分です。

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"]
return flag

bitflyer.private_get_getpositions()で、現在のポジション(建玉)の状況を確認します。もし空のデータが返ってきたら、未約定の注文のキャンセルに成功したということです。この場合は、「現在、未決済の建玉はありません」と表示します。

一方、1つでもデータが返ってきた場合は、「未約定の注文をキャンセルするつもり」だったものの、「スレ違いで注文が約定してしまいポジションを持ってしまった」という状況になります。そのため、flag["position"]["exist"](ポジションの有無を管理するフラッグ)をTrueに更新します。

▽ 注文キャンセルしたもののスレ違いで約定して建玉が残ってる例

ここでポジション用のフラッグをTrueに更新しないと、BOTを動かしているうちに、どんどん意図しない建玉が溜まっていくので注意!

手仕舞いのための関数

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"]) + "あたりで成行で決済します")
			order = bitflyer.create_order(
				symbol = 'BTC/JPY',
				type='market',
				side='sell',
				amount='0.01',
				params = { "product_code" : "FX_BTC_JPY" })
			flag["position"]["exist"] = False
			
	if flag["position"]["side"] == "SELL":
		if data["close_price"] > last_data["close_price"]:
			print("前回の終値を上回ったので" + str(data["close_price"]) + "あたりで成行で決済します")
			order = bitflyer.create_order(
				symbol = 'BTC/JPY',
				type='market',
				side='buy',
				amount='0.01',
				params = { "product_code" : "FX_BTC_JPY" })
			flag["position"]["exist"] = False
	return flag

flag["position"](ポジションの保有の有無を管理するフラッグ)を使って、買いポジションであれば売り決済、売りポジションであれば買い決済の成行注文を出します。

成行注文を出した後は、flag["position"]["exist"]をFlaseに更新しておきます。これでまたエントリーができるようになります。

メインのループ処理

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)

ここがメインのループ処理の部分です。

長くなりそうなコードはすべて関数にして外に出しておいたので、メイン処理自体は、かなりすっきりと書くことができました。このように全体のロジック部分をシンプルにしておくと、後で(何をやってるのか?)が見やすくなり、管理もしやすくなります。

一応、1つずつ確認しておきます。

if flag["order"]["exist"]:
	flag = check_order( flag )

まず最初に最優先で「未約定の注文がないか?」をフラッグで確認します。もしあれば、先ほどの「未約定の注文を管理する関数」を呼びます。

メイン処理のWhile文は10秒間隔で回しているので、10秒おきに「注文が実行されたかどうか?」を確認することになります。

data = get_price(60,-2)
if data["close_time"] != last_data["close_time"]:
	print_price( data )

次にCryptowatchから1分足(60秒足)のデータを取得し、最新のローソク足のOHLC(日時・始値・高値・安値・終値)を取り出します。

データを取得するのは10秒おきですが、ローソク足に更新があるのは(1分足ベースであれば)1分おきです。なので、日時データに更新があった場合のみ、その始値・終値などのデータを表示し、次の処理に進むようにします。

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 )

ポジション(建玉)の有無を確認し、もしポジションがあれば、さきほどのclose_position()関数を呼んで、手仕舞いの条件を満たしているかどうかを確認します。

ポジションを持っていない場合は、買いシグナル(buy_signal)、売りシグナル(sell_signal)を順番に確認します。今回の自動売買ロジックでは、売りシグナルと買いシグナルが両方点灯することはありえないので、単に順番に実行するだけで構いません。

実行結果

こちらを実行すると以下のような感じになります。

20時14分頃に3本連続で陰線のシグナルが出たのでショートでエントリーし、次の足ですぐに終値が上がってしまったので損切の決済をしたようです。実際にBitflyerの管理画面にも売買の履歴が入っています。

チャート上では以下のタイミングでエントリーしています。

今回は、挙動を確認するためのテストなので、実体率などの追加条件(increase_rate や realbody_rate の条件)は緩和し、単純に陰線/陽線が3本続いたらそれを売り/買いのシグナルとして動かしました。

左の矢印で3本連続の陰線による売りシグナルが点灯し「売り注文」、右の矢印で前回の終値を上回って引けたので「決済」しています。とりあえず、意図した通りの挙動はしているようです。

例外処理について

今回の記事で、ようやく自動で売買判断をしながら動き続けるBOTを作ることができました!しかし実はあと1つだけ学ばなければならないことがあります。それが例外処理です。

上記の自動売買BOTは、そのままでも動くことは動きます。pythonのプログラムコードや構文には問題はありません。しかし、このままだと、1度でもBitflyerやCryptowatchのサーバーからエラーが返ってきたら、そこで止まってしまいます。

特にBitflyerのサーバーは、混雑時などに頻繁に「500 Internal Server Error」を返します。これに対する処理を入れておかないと、そこでBOTがエラーを吐いて中断してしまいます。このような対策のことを「例外処理」といいます。

次の記事では、相手のサーバーが応答しなくてもエラーを吐いて止まることなく、何日でも安定して動き続けるための「例外処理」の書き方について学びます!

次回:サーバーエラーで停止しないよう例外処理を記述する

BTCFXのエントリーと手仕舞い(決済)をpythonで自動化しよう

さて、前回の記事で作った「赤三兵のローソク足パターンで買いシグナルを出すpythonコード」を改良して、エントリーと手仕舞いまでを自動化するコードを書いてみましょう。

手仕舞いの条件を決める

今回は以下のようなシンプルなルールにしましょう。
1.現在の終値が、前回の終値を下回ったら手仕舞う

手仕舞いのロジック

今回は、赤三兵のローソク足パターンを、上昇トレンドの序盤を捉える買いシグナルとして使います。そのため、買いエントリーした後、そのまま素直に上がり続ければポジションを持ち続け、1度でも終値が下げて引けたら、そこで成行で手仕舞うようにします。

手仕舞いのロジックとしては、「利確」と「損切」にわけて、それぞれの値幅で決める方法もあります。ですが、今回ははじめてのBOT作成の練習なので、損切りと利確とで条件を分けることはせず、共通の条件で一律に手仕舞うことにします。

Pythonコードの解説

今回の記事の構成としては、まずポイントとなるpythonコードを部分的に解説していき、最後に全体のコードを示す、という流れにします。

全体の流れを管理する方法

まず flag という変数を作り、そこに「買いシグナルの状況」「サーバーに出した注文の状況」「ポジション保有の有無」などの情報を保有させます。

flag = {
	"buy_signal":0,
	"order":False,
	"position":False
}

buy_signal は「何本連続で陽線が続いているか?」を示すフラッグで、これが「3」になれば買い注文を出すことにします。今回の記事では省略しますが、もしショートでもエントリーしたい場合は、sell_signal という要素を新しくフラッグに追加します。

order は「現在サーバーに未約定の注文があるかどうか?」を管理するためのフラッグです。position は、「現在ポジション(建玉)を保有しているかどうか?」を管理するためのフラッグです。

買い注文の関数

まずは「価格データからシグナルをチェックして、買いシグナルが点灯していれば買い注文を出す」という部分の自作の関数を作ります。以下のようになります。

def buy_signal( data,last_data,flag ):
	if flag["buy_signal"] == 0 and check_candle( data ):
		flag["buy_signal"] = 1
	elif flag["buy_signal"] == 1 and check_candle( data )  and check_ascend( data,last_data ):
		flag["buy_signal"] = 2
	elif flag["buy_signal"] == 2 and check_candle( data )  and check_ascend( data,last_data ):
		print("3本連続で陽線 なので" + str(data["close_price"]) + "で買い指値")
		# ここに買い注文のコードを入れる
		
		flag["buy_signal"] = 3
		flag["order"] = True
	else:
		flag["buy_signal"] = 0
	return flag

「赤三兵」というローソク足パターン(3本連続の陽線)を判定するために、buy_signal()という関数を使います。これは今まで解説していた「While文」の中身のロジックを外に出して、関数にまとめただけです。

・買いシグナルを出すpythonコード

check_candle() の条件を満たす陽線が1本現れたら、flag[“buy_signal”] に 1 を代入します。これが2本続けば、flag[“buy_signal”]=2、3本続けば、flag[“buy_signal”]=3 にして、Bitflyerのサーバーに指値で買い注文を出します。

今回は、先に過去データで検証をしたいので、まだ実際に注文を出すコードは書きません。が、ここに注文を出すコードを入れれば、注文を出せます。注文を出すためのコードは以下の記事で説明しています。

・BitflyerにCCXTで注文を出す方法

注文状況を管理する関数

def check_order( flag ):
	
	# 注文状況を確認して通っていたら以下を実行
	# 一定時間で注文が通っていなければキャンセルする

	flag["order"] = False
	flag["position"] = True
	return flag

今回の自動売買ロジックでは、買いシグナルが点灯したら、その時点での終値付近でBitflyerのサーバーに指値注文を出します。

指値注文は刺さるかどうかがわかりませんので、10秒おきに注文状況を確認し、このcheck_order() で注文が通ったかどうかを確認します。注文が通っていた場合は、flag[“position”] を True に更新します。これで初めて手仕舞いのための関数が動作する仕組みです。

また、もし一定時間で注文が通っていなければ、注文をキャンセルするようにします。この部分もあとで check_order() 関数に書き足しますが、テスト検証の時点では、買い指値はすべて「刺さったもの」として進めたいので、上記のように書いておきます。

手仕舞いのための関数

def close_position( data,last_data,flag ):
	if data["close_price"] < last_data["close_price"]:
		print("前回の終値を下回ったので" + str(data["close_price"]) + "で決済")
		flag["position"] = False
	return flag

ポジションを保有している場合には、以下の close_position() 関数が呼ばれるようにしておきます。

このclose_position() 関数の中では、以下のif文を使い、前回の終値と今回の終値を比較します。

if data["close_price"] < last_data["close_price"]:

もし終値が下がっていれば、決済のために売り注文をサーバーに出します。そして、flag["position"] の値を False に更新します。これによって、また先頭の「買いシグナルを探して買い注文を出す関数」が動くようになります。

全体のループ文

さて、このように「買い注文(エントリー)の関数」「注文の約定をチェックする関数」「決済をする関数」の3つが準備できたところで、全体の流れを管理するためのループ処理を作ります。

while True:		
	if flag["order"]:
		flag = check_order( flag )

	data = get_price(60,i)	
	if data["close_time"] != last_data["close_time"]:
		print_price( data )
		
		if flag["position"]:
			flag = close_position( data,last_data,flag )			
		else:
			flag = buy_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)

上記のWhile文の中では、まず最優先で「未約定の注文がサーバーにあるかどうか?」を、flag["order"] を使って確認します。もし未約定の注文があれば、先ほど作った check_order() 関数を使って、Bitflyerのサーバーに注文状況を問い合わせます。必要があれば、ここで注文のキャンセルもします。

以下の部分です。

if flag["order"]:
	flag = check_order( flag )

次に10秒おきにCryptowatchのAPIを使用して最新の価格データを取得します。

そして1分足の価格データ(例)に更新があったかどうかを確認し、もし更新があった場合は、日時や価格をprint()で表示します。また「現在ポジションを保有しているかどうか?」を flag["position"] で確認し、その結果に応じて条件分岐します。

if flag["position"]:
	flag = close_position( data,last_data,flag )			
else:
	flag = buy_signal( data,last_data,flag )	

ポジションを保有している場合

if flag["position"] でポジションを保有中かどうかを確認します。もし保有中であれば、先ほど作ったclose_position() 関数を呼んで、手仕舞いのための条件を満たしているかどうかを確認します。

手仕舞いの条件を満たしていれば、売りの決済注文を出します。満たしていなければ、また10秒待機して次のループ処理に戻ります。

ポジションを保有していない場合

if flag["position"] でポジションを保有中かどうかを確認して、もしポジションを保有していなければ、最初に作った buy_signal() 関数を実行し、買いシグナルが点灯するかどうかを確認します。買いシグナル(3本連続の陽線)が点灯すれば、サーバーに買い注文を出します。

以上が、全体のロジックです。

テスト検証のためのコード

以下は、過去データのテスト検証のための、pythonコードの全文です。

ここでいう過去データの検証とは、リアルタイムの足ではなく、直近の500件の確定足を使って「売買シグナルが点灯するかどうか?」などの挙動を確認するための検証のことです。勝率等の検証(いわゆるバックテスト)のことではありません。詳しくは以下を読んでください。

・過去の価格データを使った検証

※ サンプルコードでは1分足のデータを使っていますが、60の部分を300に変更すれば5分足、1800に変更すれば30分足、3600に変更すれば1時間足に修正できます。実際に売買BOTを動かすときは、最新価格の取得の遅延、約定までの時間などを考慮するので、(この売買ロジックの場合)1分足は適していないと思います。

import requests
from datetime import datetime
import time

response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = { "periods" : 60 })

def get_price(min,i):
	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] }

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 ):
	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 data["close_price"] < data["open_price"] : return False
	elif increase_rate < 0.0005 : 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 buy_signal( data,last_data,flag ):
	if flag["buy_signal"] == 0 and check_candle( data ):
		flag["buy_signal"] = 1
	elif flag["buy_signal"] == 1 and check_candle( data )  and check_ascend( data,last_data ):
		flag["buy_signal"] = 2
	elif flag["buy_signal"] == 2 and check_candle( data )  and check_ascend( data,last_data ):
		print("3本連続で陽線 なので" + str(data["close_price"]) + "で買い指値")
		# ここに買い注文のコードを入れる
		
		flag["buy_signal"] = 3
		flag["order"] = True
		
	else:
		flag["buy_signal"] = 0
	return flag


# 手仕舞いのシグナルが出たら決済の成行注文を出す関数
def close_position( data,last_data,flag ):

	if data["close_price"] < last_data["close_price"]:
		print("前回の終値を下回ったので" + str(data["close_price"]) + "で決済")
		flag["position"] = False
	return flag


# サーバーに出した注文が約定したか確認する関数
def check_order( flag ):
	
	# 注文状況を確認して通っていたら以下を実行
	# 一定時間で注文が通っていなければキャンセルする
	
	flag["order"] = False
	flag["position"] = True
	return flag


# ここからメイン処理
last_data = get_price(60,0)
print_price( last_data )

flag = {
	"buy_signal":0,
	"sell_signal":0,
	"order":False,
	"position":False
}
i = 1

while i<500:		
	if flag["order"]:
		flag = check_order( flag )

	data = get_price(60,i)
	if data["close_time"] != last_data["close_time"]:
		print_price( data )
		
		if flag["position"]:
			flag = close_position( data,last_data,flag )			
		else:
			flag = buy_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"]
		i += 1	

	time.sleep(0)

実行結果

このpythonコードを実行すると以下のようになります。

今回の例では、直近の1分足のデータを500件分チェックしたところ、以下の2カ所でシグナルが点灯していました。

1箇所目

・2018/3/22 1時45分
・買い 992000円
・決済 993631円

2箇所目

・2018/3/22 3時08分
・買い 980925円
・決済 982266円

実際のチャートを見てみると、以下の部分でシグナルが点灯していたことがわかります。

1つ目の矢印が「赤三兵」での買いシグナルが出たローソク足、2つ目の矢印が「前回の終値を下回った」ことで決済シグナルの出たローソク足です。なおこのテストでは、前述のように「買い指値はすぐに刺さったもの」として検証しています。

注意

繰り返しますが、今回の自動売買BOTは「はじめて作る方」のための練習用であり、統計的に勝率を検証したものではありません。今回の例ではたまたま利益が出ていますが、もっと広範にテストすれば、このままのロジックでは全く勝てないことがわかると思います。

勝率を検証する方法などは今後、解説していく予定ですが、今回は単に「自分の設計したとおりの場面でエントリー・決済をしているか?」を確認するためにテストをしている、と考えてください。

次回記事:実際に自動売買ができる試作版のBOTを作る!

過去のCryptowatchの価格データで売買シグナルを検証をする

売買シグナルをpythonで実装し、実際にどのようなチャートでシグナルが点灯するのかを確認したい場合、リアルタイムの価格データで検証するのは時間の無駄です。

特にちょっとしたパラメーター(%など)の調整のために、どのようなローソク足がヒットするかを確認したいだけであれば、リアルタイムのデータを使う必要はありません。

今回の記事では、Cryptowatchで取得できるBitflyerの価格の過去データからシグナルを検証する方法を紹介します。

今回、検証する売買シグナル

前回までの記事で紹介した「(酒田五法の)赤三兵というチャートパターンを買いシグナルにするpythonコード」を使って説明していきます。そのため、こちらで紹介しているコードを改良するかたちで説明します。

・前回の記事

検証用のコード


import requests
from datetime import datetime
import time

response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = { "periods" : 60 })

def get_price(min,i):
	data = response.json()
	last_data = data["result"][str(min)][i]

	return { "close_time" : last_data[0],
		"open_price" : last_data[1],
		"high_price" : last_data[2],
		"low_price" : last_data[3],
		"close_price":last_data[4] }


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 ):
	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 data["close_price"] < data["open_price"] : return False
	elif increase_rate < 0.0005 : 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


last_data = get_price(60,0)
print_price( last_data )
time.sleep(10)

flag = 0
i = 1

while i < 500:	
	data = get_price(60,i)
	
	if data["close_time"] != last_data["close_time"]:
		print_price( data )
		
		if flag == 0 and check_candle( data ):
			flag = 1
		elif flag == 1 and check_candle( data )  and check_ascend( data,last_data ):
			print("2本連続で陽線")
			flag = 2
		elif flag == 2 and check_candle( data )  and check_ascend( data,last_data ):
			print("3本連続で陽線 なので 買い!")
			flag = 3
		else:
			flag = 0
		
		last_data["close_time"] = data["close_time"]
		last_data["open_price"] = data["open_price"]
		last_data["close_price"] = data["close_price"]
	
	i += 1
	time.sleep(0)

前回の実践用のコードからの主な変更点は3か所だけです。

上記のコードには無駄な部分もあり、もっとコード量を減らすことはできます。ですが、あまり大幅に変更してしまうと、リアルタイム用と過去データ用のソースコードが全然違うものになってしまい、2種類のコードを管理しなければならなくなります。

そのため、今回は実践用のコードの内容をできるだけ変更せずに、最小限の修正だけで過去データの検証をすることで、いつでも簡単に実践用(リアルタイム用)のコードに戻せるようにしておきます。

コードの変更点

APIを呼ぶのは最初の1回だけにする

response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = { "periods" : 60 })

先頭で、requests.get()を使って1度だけAPIを使用しています。

実践用のコードでは10秒間に1回APIを使用するため、get_price()の中にrequests.get()を入れていましたが、過去データの検証では1回だけ取得したデータを使うので、requests.get()をget_price()から外して先頭に記述しています。

get_price()の中身を修正する

def get_price(min,i):
	data = response.json()
	last_data = data["result"][str(min)][i]

	return { "close_time" : last_data[0],
		"open_price" : last_data[1],
		"high_price" : last_data[2],
		"low_price" : last_data[3],
		"close_price":last_data[4] }

実践用のコードでは、get_price()は最新のローソク足の価格データを1つだけ返す関数として作成しました。

しかし過去データの検証では、1度だけ取得した価格データを古い順番(時系列順)に試していきます。そのため、i という引数を追加し、指定された i番目のデータを返す関数に修正します。変更点は「i」のところを追加しただけです。

While文の中でiという変数を1ずつ増やす

i = 1
while i < 500:	
	data = get_price(60,i)
	#if文の処理

	i += 1
	time.sleep(0)

実践用のコードでは、While文の中で、10秒に1回APIを使用して最新の価格を取得しにいきます。

しかし過去データの検証では、最初に取得した価格データを先頭から順番(時系列順)に試していくだけです。そのため、While文の中の get_price()に i番目 という意味の変数 i を渡し、ループ処理のたびに i を1ずつ増やす処理に変更します。また10秒待機する必要はないので、sleep()の中を0秒にします。

変更点は以下の部分だけです。

・i = 1 の宣言
・data = get_price(60,i)の部分
・i += 1 を追加
・time.sleep()を0秒に修正

While文のループを500回に限定する

実践用のコードは、リアルタイムの価格を取得し続けるプログラムなので、While True:という無限ループで記述しています。

しかし過去データの検証では、最初に取得した価格データを先頭から順番(時系列順)に試していくだけです。Cryptowatchの価格データの取得件数は、デフォルトでは500件なので、500回ループしたら止まるように、While True:の部分を While i<500: に変更します。

実行結果

これを実行すると以下のようになります。

実際にシグナルが点灯した日のチャートを目で確認しながら、実体の割合、上ヒゲや下ヒゲの割合、上昇率、などのパラメーターを調整していけば、自分の理想のローソク足のパターンでシグナルが点灯するように、コードをカスタマイズすることができます。

今回は1分足で解説していますが、もちろん1時間足や日足でもやり方は同じです。その場合は、最初のAPIの{ periods : } を3600(1時間足)や86400(日足)に変えて使ってください。

なお、勝率の検証などのいわゆる「バックテスト」については、もっと高度な内容になりますが、そちらも今後、解説していく予定です。

pythonコードの解説記事(酒田五法の赤三兵・黒三兵)

こちらの記事で説明した酒田五法の「赤三兵」「黒三兵」の売買シグナルのpythonコードの解説です。

pythonコード


import requests
from datetime import datetime
import time

def get_price(min):
	response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = { "periods" : min })
	data = response.json()
	last_data = data["result"][str(min)][-2]

	return { "close_time" : last_data[0],
		"open_price" : last_data[1],
		"high_price" : last_data[2],
		"low_price" : last_data[3],
		"close_price":last_data[4] }


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 ):
	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 data["close_price"] < data["open_price"] : return False
	elif increase_rate < 0.0005 : 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


last_data = get_price(60)
print_price( last_data )
flag = 0

while True:	
	data = get_price(60)
	
	if data["close_time"] != last_data["close_time"]:
		print_price( data )
		
		if flag == 0 and check_candle( data ):
			flag = 1
		elif flag == 1 and check_candle( data )  and check_ascend( data,last_data ):
			print("2本連続で陽線")
			flag = 2
		elif flag == 2 and check_candle( data )  and check_ascend( data,last_data ):
			print("3本連続で陽線 なので 買い!")
			flag = 3
		else:
			flag = 0
		
		last_data["close_time"] = data["close_time"]
		last_data["open_price"] = data["open_price"]
		last_data["close_price"] = data["close_price"]
		
	time.sleep(10)

かなり長いので部分的に順番に説明してきます。

解説パート1

import requests
from datetime import datetime
import time

CryptowatchのAPIを使うために、「requests」ライブラリをインポートしています。

またAPIで取得したタイムスタンプ(UNIX時間)を人間の読める時間に変換するために、「datetime」ライブラリをインポートします。「time」ライブラリは、sleep()という関数を使って、ループ処理の間の待機をするために使います。

解説パート2

def get_price(min):
	response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = { "periods" : min })
	data = response.json()
	last_data = data["result"][str(min)][-2]

	return { "close_time" : last_data[0],
		"open_price" : last_data[1],
		"high_price" : last_data[2],
		"low_price" : last_data[3],
		"close_price":last_data[4] }

CryptowatchのAPIで最新のBitflyerFXの最新のローソク足の価格データを取得して、それを配列(辞書リスト)で返すための、自作の関数(def get_price())を作ります。

これで外部から、get_price(60)などと指定するだけで、最新の(終値の固まった)1分足のローソク足の価格データを取得できます。5分足を取得したい場合は、get_price(300)とするだけでOKです。

この関数を使って取得したデータは、data[“close_time”]で日時、data[“open_price”]で始値、data[“close_price”]で終値を取り出せるようにしておきます。

解説パート3

def print_price( data ):
	print( "時間: " + datetime.fromtimestamp(data["close_time"]).strftime('%Y/%m/%d %H:%M') + " 始値: " + str(data["open_price"]) + " 終値: " + str(data["close_price"]) )

APIで取得したデータを黒い画面(Anacondaプロンプト)に表示するためのコードも、何度も使うので関数にまとめています。ここでは、日時、始値、終値の3つだけを表示するようにしておきます。

解説パート4

def check_candle( data ):
	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 data["close_price"] < data["open_price"] : return False
	elif increase_rate < 0.0005 : return False
	elif realbody_rate < 0.5 : return False
	else : return True

取得した各ローソク足(単体)が条件を満たしているかどうかをチェックするために、check_candle() という関数を自作します。

この関数では、実体の割合(ローソク足の値幅のうちの実体部分の割合)と、上昇率の2つを計算しています。前のロジック定義の記事で説明したように、今回はテストとして、実体の大きさが価格の0.05%以上、ヒゲを含めた全値幅のうちの実体の割合が50%以上、というのを条件にしています。ここは自分の好みで調整してください。

条件を1つでも満たしていなければFalseを返し、すべてを満たしていればTrueを返します。

解説パート5

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

次に2本目、3本目のローソク足が上昇しているかどうか(終値・始値の両方が切りあがっているかどうか)を検証するために、check_ascend() という関数を自作します。これはロジック定義の記事のルール2に該当する部分です。

今回の始値と前回の始値、今回の終値と前回の終値を比較し、両方とも上昇していればTrueを返します。片方しか上昇していない、またはどちらも上昇していない場合は、Falseを返します。

解説パート6

last_data = get_price(60)
print_price( last_data )
flag = 0

ここはWhile文(無限ループ処理)に入る前の準備です。

今回のロジックでは、「前回の終値」と「今回の終値」を比較する必要があるため、ループ処理に入る前に、1度、「前回の終値」を取得しておきます。この価格データは、last_data という配列の変数に格納されます。

また「何本連続で陽線が続いているか?」を判定するために、flagという変数を用意しておきます。条件を満たす陽線が1本現れたらflag=1、2本連続で現れたらflag=2、3本連続で現れたらflag=3 として、買いシグナルを点灯させます。

解説パート6

while True:	
	data = get_price(60)
	if data["close_time"] != last_data["close_time"]:
	#if文の中身
	time.sleep(10)

ここからはWhile文の中身の解説です。
今回のサンプルコードで使っているのは1分足ですが、なるべく価格の取得に遅れが出ないようにAPIは10秒おきに利用しています。そのため、「更新があった場合だけ次の処理をする」ために、3行目のif文を書いています。

全体の構成としては、「APIで価格を取得 → 更新があったかチェック → 更新があればif文の中身を実行 → 10秒待機する」を無限ループしています。このWhileの部分は、前の記事でも説明済です。

解説パート7

if data["close_time"] != last_data["close_time"]:
	print_price( data )
	
	if flag == 0 and check_candle( data ):
		flag = 1
	elif flag == 1 and check_candle( data )  and check_ascend( data,last_data ):
		print("2本連続で陽線")
		flag = 2
	elif flag == 2 and check_candle( data )  and check_ascend( data,last_data ):
		print("3本連続で陽線 なので 買い!")
		flag = 3
	else:
		flag = 0

この部分はWhile文の中のif文の中身の解説です。

ローソク足の1分足データが更新された場合に、まず先ほど作っておいた flag という変数をチェックします。もしflag=0 の場合(まだ1本も陽線のシグナルが出ていない場合)は、check_candle()関数で、今回のローソク足が条件を満たしているか確認します。

条件を満たしていれば、flagの値を1にします。条件を満たしていなければ、flag=0 のまま次のループに入ります。

もしflag=1の場合(前回の足が陽線の場合)は、今回の足が2つの条件(前回のロジック定義の記事で定義したルール1とルール2)を満たしているかどうかを、check_candle()とcheck_ascend()の両方でチェックします。両方とも満たしていれば、flag = 2 にします。どちらか一方でも満たしていなければ、flag = 0 に戻します。

もしflag=2 の場合(前の足が2本連続で陽線の場合)は、今回の足も2つの条件(ルール1とルール2)と満たしているかどうか、check_candle()とcheck_ascend()で確認します。両方とも満たしていれば、買いシグナルを出します。どちらか一方でも満たしていなければ、flag=0に戻します。

解説パート8

last_data["close_time"] = data["close_time"] 
last_data["open_price"] = data["open_price"]
last_data["close_price"] = data["close_price"]

if文の最後の部分です。

ローソク足の更新があったので、「前回の価格データ」を「今回の価格データ」で上書きします。次のループからは、「今回の価格データ(の日時)」と次回の価格データの日時を比較して、更新があったかどうかを判断します。

BTCFXで酒田五法の赤三兵・黒三兵の自動売買ロジックを作る

今回は「自動売買BOTの実装の練習」として、酒田五法の赤三兵と黒三兵(三羽鳥)を、自動売買ロジックとして実装するための手順を説明していきます。

三兵の売買ロジックを定義する

酒田五法の三兵とは、以下のように陽線・または陰線が3本連続で続くローソク足パターンのことをいいます。陽線が3本連続で続いた場合は、上昇トレンドなので買いのサイン、陰線が3本連続で続いた場合は、下降トレンドなので売りのサイン、と判断します。

ここでは単純に以下のように定義してpythonでプログラムしてみます。

赤三兵

1.3足連続で(終値>始値)の足が続いている
2.3回連続で始値と終値が前の足より上昇している

黒三兵

1.3足連続で(終値<始値)の足が続いている
2.3回連続で始値と終値が前の足より下落している

1と2のルールは両方必要です。
「3足連続で 終値>始値 」「3足連続で 終値<始値」のルールだけだと、必ずしも赤三兵や黒三兵の形にならないので注意してください。

ルール1で「陽線」であることは保証されますが、それが右肩上がり(または右肩下がり)かどうかは、ルール2を加えなければ判定できません。陽線が連続で続いていても、その始値や終値が前の足より低い、ということは十分あり得るからです。

追加ルール

このままでも練習としては問題ないのですが、上記のロジックだけだと以下のようなローソク足パターンも検出してしまいます。

なので、ローソク足の実体がある程度の大きさである、ヒゲの割合よりも実体の割合の方が大きい、という2つの条件を加えてみましょう。今回は以下のような条件にします。

1.実体の大きさ(上昇率)が始値の0.05%以上
2.ヒゲを含む値幅のうち実体の割合が50%以上

どちらも明確に、OHLCの価格データから数字で定義できます。

1)の条件は、「終値/始値 ― 1」の計算結果のことです。今回はテストで1分足を見てるので、「0.05%」に設定していますが、いくつが最適なのかは見ている時間軸によって違います。実際に動かす予定の5分足では「0.2%」くらいはあった方がいいかもしれません。自分で色々調整してみてください。

2)の条件は、「(始値 ― 終値)の絶対値 / (高値 ― 安値)」で計算できます。これも50%が最適かどうかはわかりませんが、今回は50%にします。

注意

ローソク足パターンの傾向分析」などの本を読めばわかりますが、酒田五法の「赤三兵」「黒三兵」は、それだけを単独のシグナルとして用いても、統計上の勝率がそれほど良くないことがわかっています。

赤三兵を買いのシグナルとして使うなら、本来は、底値圏で捉えなければ意味がないので、各テクニカル指標やオシレーターと組み合わせて使うべきです。ですが、今回ははじめてのBOT作成の練習なので、シンプルに上のロジックだけで定義します。

また人によっては、三兵は陽線・陰線が「窓を開けずに」3本連続で続くことを条件として定義するようですが、今回は窓があるかどうかは考慮しません。

「赤三兵」のPythonコード

まずは赤三兵(3本連続で陽線が続く買いシグナル)を実装してみましょう。1)3足連続で(終値>始値)の足が続いている、2)3回連続で始値と終値が前の足より上昇している、の条件を満たす場合に、買いシグナルを出すコードを作ります。

なお、前回までの記事(「10秒おきにBitflyerのローソク足を取得するプログラム」)までの内容は、書けることを前提として進めます。

以下がpythonコードになります。


import requests
from datetime import datetime
import time

def get_price(min):
	response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = { "periods" : min })
	data = response.json()
	last_data = data["result"][str(min)][-2]

	return { "close_time" : last_data[0],
		"open_price" : last_data[1],
		"high_price" : last_data[2],
		"low_price" : last_data[3],
		"close_price":last_data[4] }


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 ):
	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 data["close_price"] < data["open_price"] : return False
	elif increase_rate < 0.0005 : 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


last_data = get_price(60)
print_price( last_data )
flag = 0

while True:	
	data = get_price(60)
	
	if data["close_time"] != last_data["close_time"]:
		print_price( data )
		
		if flag == 0 and check_candle( data ):
			flag = 1
		elif flag == 1 and check_candle( data )  and check_ascend( data,last_data ):
			print("2本連続で陽線")
			flag = 2
		elif flag == 2 and check_candle( data )  and check_ascend( data,last_data ):
			print("3本連続で陽線 なので 買い!")
			flag = 3
		else:
			flag = 0
		
		last_data["close_time"] = data["close_time"]
		last_data["open_price"] = data["open_price"]
		last_data["close_price"] = data["close_price"]
		
	time.sleep(10)


今回は少し長いので、コードの解説記事は、別の記事にしておきます。
先に実行結果の方を見ておきましょう。

・コードの解説記事はこちら

実行結果

このpythonプログラムを実行すると、以下のようになります。


これを見ると、例えば、3月21日の夜中2時45分にシグナルが点灯してるのがわかります。実際に、BitflyerFXのチャートを見てみると、以下のようになっていました。

….これは、明らかに「買い」で入ってはいけなそうな局面ですね。

もし改良したい場合

このように、赤三兵は、上昇トレンドの終わりで現れた場合は天井(バイイングクライマックス)のシグナルとも言われています。繰り返しますが、赤三兵だけをシグナルに買いエントリーするのはお勧めできません。

実際に動かすBOTでは、RSIなどのオシレーターなどと組み合わせて、「売られ過ぎ」のときにエントリーするよう改良すべきでしょう。pythonでテクニカル指標を実装する方法は、今後、解説していきます。

またよく言われるように、赤三兵の3本目の足に上ヒゲが付いている場合は、弱気のシグナルです。上記の、check_candle()関数を自分で調整して、実体の割合ではなく、上ヒゲの割合でフィルターをかけてもいいかもしれません。

今回はとりあえず「テストBOTを作れるようになる」ことが目標なので、このまま話を進めます。

もっと効率的にテストする方法

上記のプログラムを実際に動かしてみるとわかりますが、実際のリアルタイムのチャートで検証しようとすると、シグナルが点灯するまで何十分・何時間もパソコンの前で待機しなければなりません。

例えば、上記のシグナルの条件に『3本目の足に上ヒゲがついてない場合』という条件を付け加えたとしましょう。その売買シグナルが、理想のタイミングで点灯するかどうかをテストするために、また何時間も待つのはあまりに非効率です。

なので、シグナルの条件のテストに、直近の過去データを使う方法を紹介しておきます。過去データでテストできれば、以下のようにどのくらいの頻度でシグナルが点灯するか、すぐに確認できます。

以下の記事では、その方法を解説しておきます!

・過去の価格データからシグナルを検証する

練習問題

今回のpythonコードを改良して、黒三兵による売りシグナルと赤三兵による買いシグナルの両方が点灯するようにしてみましょう!

ポイントとして、赤三兵のシグナル(買い)と黒三兵のシグナル(売り)をそれぞれ関数にしてしまい、While文の外に出すと見やすくなります。またflagという変数を、買いエントリー用と売りエントリー用の2つ用意するといいです。

・練習問題の解答例

自動売買BOTの中で何度も使いそうな処理を関数にまとめてみよう!

前回の記事では、CryptowatchのAPIを使って、Bitflyerの1分足の価格データ(始値と終値)を取得して表示するプログラムを書きました。

今回の記事では「関数」を勉強します。前回作った以下の処理を「自作の関数」にして後から1行で呼び出せるようにしてみましょう!

(1)CryptowatchのAPIで最新の価格を取得する処理
(2)ローソク足データの日時・始値・終値の3つを表示する処理

関数とは

関数というのは、プログラムの中で何度も登場するようなコード(処理)を1つの部品としてまとめておいて、後から1行で呼び出せるようにする記述方法のことです。

以下のように「def 関数名()」と記述することで、何度も使う処理を1つの関数にまとめることができます。

def 関数名():
	# 実行したい処理
	return x

関数名の後ろについている()は引数といいます。関数に何かデータを渡したいときは、この引数にデータを指定します。また return は、関数側からデータを外部に返したいときに使います。

また外部から関数を呼び出すときは以下のように記述します。

関数名()

具体的なコード

教科書的な説明だけ聞いても退屈でさっぱりわからないと思います。習うより慣れろです。どんどん実践用のコードを見て行きましょう!

まずは前回までのコード、つまりBitflyerから最新の1分足の価格データを取得して、日時に更新があった場合のみ表示するコードをもう1度、見ておきましょう。以下でしたね。


import requests
from datetime import datetime
import time

while True:
	
	response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc?periods=60")
	response = response.json()

	data = response["result"]["60"][-2]
	
	close_time = datetime.fromtimestamp(data[0]).strftime('%Y/%m/%d %H:%M')
	open_price = data[1]
	close_price = data[4]

	if close_time != last_time:
		print( "時間: " + close_time
			+ " 始値: " + str(open_price)
			+ " 終値: " + str(close_price) )
		last_time = close_time

	time.sleep(10)

ここから、(1)Cryptowatchから最新価格を取得する部分と、(2)ローソク足の日時・始値・終値を表示する部分、を関数にしてみましょう! 今回は以下のような名前の関数を2つ作ります。

・get_price()関数 … CryptowatchAPIで価格を取得
・print_price()関数 … 日時・終値・始値を表示

以下のようになります。


import requests
from datetime import datetime
import time


# Cryptowatchから 〇分足のデータを取得する関数を作成
def get_price(min):

	# APIで価格を取得する
	response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = { "periods" : min })
	response = response.json()
	data = response["result"][str(min)][-2]
	
	close_time = data[0]
	open_price = data[1]
	close_price = data[4]

	# 日時・終値・始値の3つを返す
	return close_time, open_price, close_price


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


# ここからがメイン処理
last_time = 0
while True:
	# get_price()関数を使って最新のローソク足の日時・始値・終値を取得する
	close_time , open_price , close_price = get_price(60)
	
	if close_time != last_time:
		last_time = close_time
		
		# print_price()関数を使って価格データを表示する
		print_price( close_time,open_price,close_price )
	
	time.sleep(10)

関数化する意味

プログラミング初心者の方は「…うん? むしろコードが長くて複雑になったぞ…?」と思うかもしれません(笑)しかし大事なのはメイン処理の部分です。

Cryptowatchから価格を取得する処理と、その内容を表示する処理、の2つを関数にして外に出したことで、今まで書いていた長いメイン処理の部分を以下のようにシンプルに7行にまとめることができました。

# ここからがメイン処理
last_time = 0
while True:
	close_time , open_price , close_price = get_price(60)
	if close_time != last_time:
		last_time = close_time
		print_price( close_time,open_price,close_price )
	time.sleep(10)

このように、ややこしい処理や何度も使う処理は、関数にして外に出しておくと、メイン処理の部分が非常に読みやすくなります。後で読み返したときに、「コード全体がどういう流れで何をやっているのか?」がわかりやすくなります。

自動売買BOTで関数を使う場面

今回はただ単にBitflyerFXのローソク足データを表示するだけなので、別に敢えて関数にする必要はありません。ですが、これが自動売買BOTになると、以下のように全体の流れも複雑になります。

1)While文でループする
2)Cryptowatchから最新価格を取得する
3)エントリー条件を判定する
4)Bitflyerにエントリー注文を出す
5)指値注文が約定したかを確認する
6)約定しなければキャンセルする
7)約定したら手仕舞いの条件を満たしたか判定する
8)手仕舞いの条件を満たしたら決済注文を出す

このような一連の流れをすべてメイン処理の中に書こうとすると、何がなんだかわからなくなります。このような場合には、

・エントリー条件を判定する関数
・サーバーに注文を出す関数
・手仕舞いの決済をする関数

など、役割ごとに関数に分けて外に出すようにしておくと、メイン処理を読みやすくシンプルに保つことができます。

コードの解説

まずは get_price()関数から見ておきましょう。
以下のように定義しています。

def get_price(min):

	# APIで価格を取得する
	response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = { "periods" : min })
	response = response.json()
	data = response["result"][str(min)][-2]
	
	close_time = data[0]
	open_price = data[1]
	close_price = data[4]

	# 日時・終値・始値の3つを返す
	return close_time, open_price, close_price

最初の1行目の def get_price( min ) で、get_priceという関数名を使うことを定義しています。

またこの()の中にある min というのが「引数」です。このように自作の関数には、好きなデータを外部から渡すことができます。今回は、この関数を1分足、5分足、15分足でも共通で使いたいので、「何分足か?」という部分を変数にしておき、これを外部から指定できるようにしておきます。

そのため、()の中に指定した数字を受け取って、それをパラメーター( params = { “periods” : min } )に指定しています。

メイン処理で以下のように記述すれば、1分足、5分足、15分足のデータを取得できます。

# メイン処理での呼び出し

# 1分足を使う
get_price(60)

# 5分足を使う
get_price(300)

# 1時間足を使う
get_price(3600)

またこの関数には、返り値があります。それが以下の部分ですね。

	return close_time, open_price, close_price

これは 日時(close_time)、始値(open_price)、終値(close_price)の3つの値を返す、という意味です。

関数に返り値がある場合は、メイン処理の方で関数を実行したときに、この3つの返り値を受け取る変数を用意しなければなりません。以下のような具合です。

# メイン処理(呼び出す側)
# これだとエラーになる
get_price(60)

# 3つの返り値を受け取る変数を用意する。
# 何でもいい

a,b,c = get_price(60)

同じようにprint_price()のコードも見ておきましょう。

def print_price( close_time, open_price, close_price ):
	print( "時間: " + datetime.fromtimestamp(close_time).strftime('%Y/%m/%d %H:%M')
		+ " 始値: " + str(open_price)
		+ " 終値: " + str(close_price) )

こちらも def print_price() で print_price という名前の関数を作成しています。

この関数は3つの変数(日時・始値・終値)を受け取って、それをテキスト形式に変換してprintで表示するだけの関数です。返り値はありません。このように返り値のない関数も定義することができます。

返り値のない関数を呼ぶときは、メイン処理に以下のように書けばOKです。


# メイン処理(呼び出す側)
print_price(a,b,c)

このときに指定された引数を渡さないとエラーになるので注意してください。関数側で定義した引数は、必ず呼び出すときに渡さなければなりません。例えば、3つの引数が定義されているのに、メイン処理で2つしか引数を渡さない場合もエラーになります。

実行結果

これをpythonファイルに保存して実行してみましょう!
以下のようになります。

これで、何度も使う処理を関数にまとめる方法を習得することができました! 次からはいよいよ赤三兵の買いシグナルのロジックを定義して実装していきましょう!

次回記事:酒田五法の赤三兵・黒三兵の売買ロジックを作る

Bitflyerのローソク足の情報(始値・高値・安値・終値)をpythonで取得する

さて、前回の記事で考えた自動売買BOTのロジックを実装していきましょう。まず必要なのは、1分足の価格データです。

Bitflyerでは、各時間軸のローソク足の情報(始値・高値・安値・終値)が、APIでは提供されていません。そのため、ローソク足のデータを使いたい場合は、Cryptowatchという外部APIを利用する必要があります。

なお、CryptowatchのAPIの基本的な使い方は以下の記事で説明しています。以下の内容は前提として進めさせてください。

・APIって何?CryptowatchのAPIを使って説明
・APIで取得したJSONデータから欲しい数字を取り出す方法

さて、やっていきましょう!

まずBitflyerの1分足の価格データの取得する

ローソク足の価格データのセット(始値・高値・安値・終値)のことを、よく英語やAPIの仕様書では OHLC と表記します。

これは Open-High-Low-Close の略ですが、よく見かけるので覚えておくといいです。ここに出来高(Volume)を加えて、OHLCVと表記することもあります。OHLCVという単語を知っていると、API仕様書を読むのが少し楽になります。

さて、BitflyerのOHLCを取得するためのCryptowatchのAPIは以下になります。

例)1分足のBitflyerFXのOHLC価格

https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc?periods=60

1分足以外の情報を取得したい場合は「60」の部分を秒換算で変更します。5分足のデータが欲しい場合は「?periods=300」を指定します。1時間足なら「?periods=3600」、日足なら「?periods=86400」です。

これを10秒おきにAPIで取得して、それを黒い画面に表示していくpythonコードを書いてみましょう。

始値と終値を取得する

前回の記事の自動売買ロジックは、『始値と終値の両方が切り上がっている』ことがエントリーの判定条件になっていました。なので、まずは最新の1分足の価格データだけを取り出して、始値と終値だけを抽出してみましょう。

pythonで書くと以下のようなコードになります。


import requests
from datetime import datetime

# CryptowatchのAPIで1分足を取得
response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc?periods=60")
response = response.json()

# 最後から2番目のローソク足を取り出す
data = response["result"]["60"][-2]

# ローソク足から日時・始値・終値を取り出す
close_time = datetime.fromtimestamp(data[0]).strftime('%Y/%m/%d %H:%M')
open_price = data[1]
close_price = data[4]

print( "時間: " + close_time
	+ " 始値: " + str(open_price)
	+ " 終値: " + str(close_price) )

時間を表示する部分以外は、今まで勉強した内容で書けるのではないでしょうか!

コードの解説

3行目では、request.get()でAPIのリクエストを送り、返ってきた内容を response という変数に入れています。4行目では、そのJSON形式のデータをパースしています。これは、データを扱うための準備のようなものです。

5行目では、1分足の価格データのうち1番新しいものを取得しています。Cryptowatchの価格データは古い順に並んでいるので、一番新しいものを取り出すには、[-1]と指定します。ただし1番新しいローソク足だとまだ終値が固まっていないため、その1つ前を指定するために、[-2]としています。これを data という変数に入れます。

なぜ response[“result”][“60”][-2] になるのかわからない方は、以下の記事を読んでください。

・CryptowatchのAPIで取得したbitcoin価格のJSONデータから欲しい数字を取り出す

さて、data に入れた最新の価格データは以下の順番で並んでいます。

[ 日時, 始値, 高値, 安値, 終値, 出来高 ]

ここから日時、始値、終値の3つを取り出します。ただし日時はUNIX時間なので、これを人間が見やすい時間に変換します。

6行目では、取得したデータの先頭 [0] にある日時(UNIXタイムスタンプ)を、datetime.fromtimestamp() という関数で、datetime型に変換しています。datetime型というのは、pythonで時間という概念を扱うための形式だと考えてください。

さらにこのdatetime型を、strftime()という関数で、以下のように文字列に変換しています。

strftime(‘%Y/%m/%d %H:%M’)
→ 「年/月/日 時間:分」の形式の文字列に変換

7行目では、始値が[1]番目、終値が[4]番目に入っているので、それぞれを取得して open_price、close_priceという2つの変数に入れています。最後の8行目は、それらをprint()で見やすく表示しています。

実行結果

さて、上記のコードを実行してみましょう。
以下のような結果になりました。

このコードを実行したのは7時6分です。
ちゃんと1つ前のローソク足の、始値と終値が取得できていました!

While文を使って自動ループにしてみる

では、今度は10秒おきに自動的にAPIから1分足の価格データを取得して、それをプロンプト(黒い画面)に表示していくpythonコードを書いてみましょう。今回、ループ処理をさせるために、はじめて「while文」を実践で使います!

pythonコードは以下のようになります。

import requests
from datetime import datetime
import time

while True:

	response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc?periods=60")
	response = response.json()

	data = response["result"]["60"][-2]

	close_time = datetime.fromtimestamp(data[0]).strftime('%Y/%m/%d %H:%M')
	open_price = data[1]
	close_price = data[4]

	print( "時間: " + close_time
		+ " 始値: " + str(open_price)
		+ " 終値: " + str(close_price) )

	time.sleep(10)

コードの解説

変更したのは3箇所です。

まずsleep()という関数を使うために、最初に「import time」を新しく読み込んでいます。sleep()というのは、ループ中の処理と処理の間に休止を挟みたいときに使います。コードの一番最後に、time.sleep(10)と書くことで、次の処理までに10秒間の待機を挟んでいます。

さらにこの処理全体を、while True: と書いた行の下にインデント付きで入れています。while True: というのは、無限ループという意味で、エラーが出たり手動で止めたりしない限り、while True:の下にインデント(先頭の空白)付きで書かれた処理を、繰り返し永久に実行し続けます。

さて、このコードを実行してみましょう。

実行結果

このコードを実行すると以下のようになります。

眺めていると、次々と一番下に新しい価格情報が追加されていくのがわかります。プログラム初心者の方からすると、いよいよ「プログラミングをしてる!」という実感が湧いてきたのではないでしょうか(笑)

無限ループの実行を止める

なお、こちらは While True: の無限ループで書いているので、何もしなければずっと動き続けます。止めたい場合は、キーボードの「Ctrl + C」を押してください。

「Ctrl + C」を押すと以下のように、KeyboardInterrupt と出て、プログラムが停止します。

if文を使って条件判定してみよう!

ここまでの記事で、10秒おきにAPIから価格データを取得してそれを表示するpythonプログラムができました。しかし実際には、10秒おきに価格データを取得しても、それを全て表示する必要はありません。

10秒おきでの表示だと、同じ価格データが何行にも渡って並んでしまいます。1分足であれば1分間に1度だけ、5分足であれば5分間に1度だけ、ローソク足が更新された場合に価格データを表示されるようにしたいですよね。

このような条件判定をするには「if文」を使います。if文を聞いたことがない方は、まず先に以下の記事に目を通してください。

最低限必要な3つの構文(While文/if文/for文)の使い方

では、While文のループの中にif文を入れて、価格データに更新があった場合のみ、それをprintするコードを書いてみましょう!

「価格データに更新があったかどうか?」を判定するためには、(前回のデータ)と(今回のデータ)の2つを比較する必要があります。つまり、変数が2つ必要だということを意識しながら読んでみてください。

以下のようなコードになります。

Pythonコード


import requests
from datetime import datetime
import time


response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc?periods=60")
response = response.json()

# 前回の時間を last_time に入れる
last_data = response["result"]["60"][-2]
last_time = datetime.fromtimestamp(last_data[0]).strftime('%Y/%m/%d %H:%M')
time.sleep(10)


while True:
	
	response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc?periods=60")
	response = response.json()

	data = response["result"]["60"][-2]
	
	# 今回の時間を close_time に入れる
	close_time = datetime.fromtimestamp(data[0]).strftime('%Y/%m/%d %H:%M')
	open_price = data[1]
	close_price = data[4]

	# 前回の時間(last_time)と今回の時間(close_time)が違う場合のみprintする
	if close_time != last_time:
		print( "時間: " + close_time
			+ " 始値: " + str(open_price)
			+ " 終値: " + str(close_price) )
		
		# 前回の時間(last_time)を今回の時間(close_time)で上書きする
		last_time = close_time

	time.sleep(10)

取得した最新のローソク足データから、日時・終値・始値を取得して、close_time/open_price/close_price の3つの変数に入れるところまでは全く同じです。

違うのは if close_time != last_time の部分です。今回は、価格データに変更があった場合のみprint()を実行したいので、以下のように条件判定をします。

if文の仕組み

(1)前回取得したローソク足の日時を変数(last_time)に入れておく
(2)今回新しく取得したローソク足の日時(close_time)に入れる
(3)if文の条件判定で、変数(last_time)と(close_time)を比べる

(4)日時に変更があれば、if文の中身が実行されて価格データがprintされる。さらに今回新しく取得したローソク足の日時(close_time)が前回のローソク足の日時(last_time)に上書きされる
(5)変更がなければif文の中身は実行されず、10秒待機してまたWhile文の先頭に戻る

上記のコードを実行すると、1分足なら1分間ごと、5分足なら5分間ごとに価格データが表示されるようになります。試してみてください。

「関数」の使い方

さて、プログラムを書くために必要な構文は(for文/while文/if文)の3つだけだと説明しましたが、実はあと1つだけどうしても「お勉強」しならないことがあります。それが「関数」です。

関数というのは、プログラムの中で何度も登場するようなコード(処理)を1つの部品としてまとめておいて、後から1行で呼び出せるようにする記述方法のことです。

例えば、今回勉強した「APIで価格を取得する」というような処理もそうですし、「Bitflyerに注文を出す」というような処理もそうです。何度も使いそうな処理は、そのたびにプログラムを書くのが面倒ですし、何回も同じコードを書いていると修正するときにも不便です。

まずは練習として、今回の記事で作った以下の処理を自作の関数にまとめてしまいましょう!

(1)CryptowatchのAPIで最新の価格を取得する処理
(2)ローソク足データの日時・始値・終値の3つを表示する処理

以下の2つの処理を関数にして、外部から呼び出せるようにしています。以下の記事でその方法を説明するので、関数の作り方を何となく理解してみてください。何となくで大丈夫です!

・(練習)何度も使いそうな処理を関数にまとめる

次回以降の記事では、この2つの点で改良を施したコードを用います。

次回

次は、今回取得した始値と終値を使って、「3回連続で終値が切りあがっている」「3回連続で始値が切りあがっている」「上ヒゲ・下ヒゲが付いている(付いていない)」というのを、どうpythonのプログラムで判定するのかを解説します!

ビットコインFXの自動売買BOTのアルゴリズムを考える

さてここまでの記事で、あなたはpythonを使って以下のことが出来るようになりました。

1)Crytowatchから価格情報を取得
2)Bitflyerからティッカー情報を取得
3)Bitflyerに買い注文・売り注文を出す
4)Bitflyerに出した注文の状況を確認する
5)Bitflyerに出した注文をキャンセルする

あとは、これを基本的な最低限のプログラミング構文(if文/for文/while文)と組み合わせるだけで、自動売買BOTを作ることができます。

・自動売買BOTを作る上で最低限必要な構文

今回はついに、動かしておくだけで自動的に売買の判断をして、エントリーと利確・損切をしながら動き続けるBOTの作成に取り掛かります。

売買ロジックを考える

具体的なプログラムに着手する前に考えなければならないのは、売買のロジック(アルゴリズム)です。

BOTに安定した自動売買をさせるためには、なるべくシンプルなロジックを考えなければなりません。これは初心者にとって特に大事なことです。ルールが少なければ少ないほど、想定外のエラーや例外を招く可能性が低くなり、安定性が高くなります。

また「エントリーの条件」「手仕舞いの条件」が、明確に数字(= APIで取得可能な数値データ)を使って定義できるかどうか?を考えることも大事です。数字で定義できないようなエントリーや利確・損切の条件は、自動売買BOTでは使えません。

今回の売買ロジック

今回は練習として、1分足を使って、以下のような超絶シンプルなルールを考えます。

買いの場合

1)3本連続で陽線が続いている
2)3本連続で始値と終値が上がっている
3)3本目の足の終値付近で指値の「買い」注文を出す
4)次の足で注文が約定しなければキャンセルする
5)約定したら、終値が1つ前の足の終値を下回ったところで決済する

売りの場合

1)3本連続で陰線が続いている
2)3本連続で始値と終値が下がっている
3)3本目の足の終値付近で指値の「売り」注文を出す
4)次の足で注文が約定しなければキャンセルする
5)約定したら、終値が1つ前の足の終値を上回ったところで決済する

注意

これは「利益を出す」ためのロジックではありません。あくまでアルゴリズムの実装の練習用です。このロジックだけでは全く勝てないでしょう。

この章では最小限の資金(0.01BTC単位)で、プログラミングの勉強として実装していきますが、損を出したくない方は、読むだけにして実際に動かすのは辞めてください。

ロジックの解説

エントリー条件

今回の売買ロジックでは、いわゆる酒田五法の「赤三兵」(3回連続の陽線)を買いのシグナル、「三羽鳥」(3回連続の陰線)を売りのシグナルとしてエントリーする自動売買BOTを考えます。

・酒田五法の「三兵」

実際の売買BOTでは、もっとエントリーの条件を絞りこむべきですが、今回はテストなので、なるべく頻繁に売買シグナルが点灯するようにします。

また本当であれば、最低限の実装として、1時間足の移動平均線の傾きを見て、その流れに逆らわない方向でのみエントリーする仕様にしたいところです。ですが、今回は練習なので、今まで勉強した内容だけで実装することを優先します。移動平均線の判定はBOTの中に組み込みません。

利確・損切の条件

今回は売買ロジックをシンプルにするために、手仕舞いの条件を「利確」「損切」に場合分けして考えることはしません。

一律に「終値が前の足から1度でも下がったら(上がったら)手仕舞う」とだけ定義します。これで長くポジションを持つことはなくなるので、儲からないですが、あまり大きな損失が出ることもないでしょう。

今後は、pythonでテクニカル指標やオシレーターを使う方法や、バックテストで過去の成績を検証する方法などもお伝えしていくので楽しみにしながら読み進めてください。

次回:Bitflyerのローソク足の情報を取得する