BTCFXのドンチャンチャネルブレイクアウトの勝率をバックテストで検証する

前回までの記事で、過去チャートのバックテストで自動売買BOTの勝率や平均リターン、総損益を検証する基本的な方法がわかったと思います。

今回の記事では、「ドンチャン・ブレイクアウト」というより実践的な売買ロジックを使って、さらに高度な自動売買BOTの評価方法を解説していきたいと思います!

ドンチャンブレイクアウトとは

ブレイクアウト手法とは、狭い値幅での揉み合い(レンジ相場)をブレイクアウトした瞬間を捕まえてエントリーし、その後のトレンドに乗って利益を出す順張り(トレンドフォロー型)の手法のことをいいます。

ブレイクアウトを捉えるための手法はさまざまなものが開発されていますが、シストレなどの自動売買BOTの世界では、以下の2つが最も定番です。

1.ドンチャン・チャネル・ブレイクアウト

過去n期間の最高値・最安値を更新したときに、その方向にエントリーする手法です。

例えば、過去20期間の最高値を更新したときに「買い」でエントリーします。次に過去20日の最安値を更新したときに決済して手仕舞いますが、このときさらに反対方向にそのままエントリー(ドテン)する場合も多いです。

このシンプルな戦略の優位性は、トレーダーの必読本である「タートル流投資の魔術」や、ジョン・ヒルの「究極のトレーディングガイド」などの本に詳しい解説があります。

非常に単純な売買ロジックなので、プログラミングしやすく、シストレや自動売買BOTの勉強にも最適です。

2.オープニングレンジ・ブレイクアウト

オープニングレンジ・ブレイクアウトも同じく「どうやったらブレイクアウトの開始点を、シンプルな数字とロジックで発見できるか?」という視点で開発された手法です。

こちらは、過去n期間の値幅の平均値(ATR)を基準値として利用し、ある足が始値から(基準値の)X%以上動いたらその方向にエントリーします。例えば、過去3期間の安値–高値の平均値が10万円でXが70%とすると、次の足で始値から7万円上に動いた時点で買いでエントリーします。

前の足が陰線か陽線かで場合分けをして、買いと売りとで別々のX%のパラメーターを使用することが多いです。

この戦略については、ラリーウィリアムズの「短期売買法」や、先ほどのジョンヒルの「究極のトレーディングガイド」などの本に詳しい解説があります。

3.戦略の使い方

どちらも多くのトレーダーが知っている手法ですが、実際には、どの時間軸でどういうパラメーターを使うか、どういうトレンド判定の条件やフィルターと組み合わて「騙し」を除去するか、どういう手仕舞いの方法を選択するか、などによって全く違う成績になります。

これらの手法は、あくまで売買ロジックを考えるときにベースのアイデアとして使いやすいというだけで、それ自体が必勝法というわけではありません。詳しい戦略のカスタマイズ方法などは、今後、解説していく予定です。

今回はバックテストの方法の解説記事なので、まずは一番シンプルな「ドンチャン・ブレイクアウト」のロジックをpythonでコーディングするところから始めます。

ドンチャンブレイクアウトは非常に単純なロジックなので、今まで勉強した知識だけでも、pythonのプログラムコードを書くことができます。早速、やっていきましょう!

ドンチャン・ブレイクアウトのpythonコード

プログラミング初心者の方は、いきなり複雑なロジックを書こうとしてはいけません。まずは必要最小限のシンプルな骨組みの部分を作り、そこから徐々に条件などを足していくのがお勧めです。

例えば、買いと売りの両方を考えて混乱するならまずは買いエントリーだけ実装しましょう。エントリーと手仕舞いの両方を考えて混乱するなら、まずは「n足後に無条件で手仕舞う」といったシンプルなロジックで実装しましょう。

ではドンチャン・ブレイクアウトの最もシンプルなロジックとは何でしょうか? 今回は以下のように定義してみます。

1.今回実装するロジック

1)過去n期間の最高値を直近の足の高値が上回ったら(その足の終値の指値で)買いエントリーする。過去n期間の最安値を直近の足の安値が下回ったら売りエントリーする。
2)買いエントリーをした場合、過去n期間の最安値を更新したら手仕舞い、さらに売りエントリーする
3)売りエントリーをした場合、過去n期間の最高値を更新したら手仕舞い、さらに買いエントリーする

これだけです!

ではこれを「第4回 BOT作成編」で勉強した内容を前提としながらコーディングしてみましょう。

2.pythonコード

以下は、Cryptowatchから過去のローソク足を取得して、20期間のドンチャンブレイクアウトで売買シグナルや手仕舞いのタイミングを確認するためのコードです。



import requests
from datetime import datetime
import time


#-----設定項目

chart_sec = 3600  # 1時間足を使用
term = 20         # 過去n期間の設定
wait = 0          # ループの待機時間



# CryptowatchのAPIを使用する関数
def get_price(min, before=0, after=0):
	price = []
	params = {"periods" : min }
	if before != 0:
		params["before"] = before
	if after != 0:
		params["after"] = after

	response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params)
	data = response.json()
	
	if data["result"][str(min)] is not None:
		for i in data["result"][str(min)]:
			if i[1] != 0 and i[2] != 0 and i[3] != 0 and i[4] != 0:
				price.append({ "close_time" : i[0],
					"close_time_dt" : datetime.fromtimestamp(i[0]).strftime('%Y/%m/%d %H:%M'),
					"open_price" : i[1],
					"high_price" : i[2],
					"low_price" : i[3],
					"close_price": i[4] })
		return price
		
	else:
		print("データが存在しません")
		return None


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


# ドンチャンブレイクを判定する関数
def donchian( data,last_data ):
	
	highest = max(i["high_price"] for i in last_data)
	if data["high_price"] > highest:
		return {"side":"BUY","price":highest}
	
	lowest = min(i["low_price"] for i in last_data)
	if data["low_price"] < lowest:
		return {"side":"SELL","price":lowest}
	
	return {"side" : None , "price":0}


# ドンチャンブレイクを判定してエントリー注文を出す関数
def entry_signal( data,last_data,flag ):
	signal = donchian( data,last_data )
	if signal["side"] == "BUY":
		print("過去{0}足の最高値{1}円を、直近の高値が{2}円でブレイクしました".format(term,signal["price"],data["high_price"]))
		print(str(data["close_price"]) + "円で買いの指値注文を出します")

		# ここに買い注文のコードを入れる
		
		flag["order"]["exist"] = True
		flag["order"]["side"] = "BUY"

	if signal["side"] == "SELL":
		print("過去{0}足の最安値{1}円を、直近の安値が{2}円でブレイクしました".format(term,signal["price"],data["low_price"]))
		print(str(data["close_price"]) + "円で売りの指値注文を出します")

		# ここに売り注文のコードを入れる
		
		flag["order"]["exist"] = True
		flag["order"]["side"] = "SELL"

	return flag



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


# 手仕舞いのシグナルが出たら決済の成行注文 + ドテン注文 を出す関数
def close_position( data,last_data,flag ):
	
	flag["position"]["count"] += 1
	signal = donchian( data,last_data )
	
	if flag["position"]["side"] == "BUY":
		if signal["side"] == "SELL":
			print("過去{0}足の最安値{1}円を、直近の安値が{2}円でブレイクしました".format(term,signal["price"],data["low_price"]))
			print("成行注文を出してポジションを決済します")
			
			# 決済の成行注文コードを入れる
			
			flag["position"]["exist"] = False
			flag["position"]["count"] = 0
			
			print("さらに" + str(data["close_price"]) + "円で売りの指値注文を入れてドテンします")
			
			# ここに売り注文のコードを入れる
			
			flag["order"]["exist"] = True
			flag["order"]["side"] = "SELL"
			
			

	if flag["position"]["side"] == "SELL":
		if signal["side"] == "BUY":
			print("過去{0}足の最高値{1}円を、直近の高値が{2}円でブレイクしました".format(term,signal["price"],data["high_price"]))
			print("成行注文を出してポジションを決済します")
			
			# 決済の成行注文コードを入れる
			
			flag["position"]["exist"] = False
			flag["position"]["count"] = 0
			
			print("さらに" + str(data["close_price"]) + "円で買いの指値注文を入れてドテンします")
			
			# ここに買い注文のコードを入れる
			
			flag["order"]["exist"] = True
			flag["order"]["side"] = "BUY"
			
	return flag


# ------------------------------
# ここからメイン処理
# ------------------------------

price = get_price(chart_sec)
last_data = []

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

i = 0
while i < len(price):

	# ドンチャンの判定に使う過去n足分の安値・高値データを準備する
	if len(last_data) < term:
		last_data.append(price[i])
		print_price(price[i])
		time.sleep(wait)
		i += 1
		continue
	
	data = price[i]
	print_price(data)
	
	
	if flag["order"]["exist"]:
		flag = check_order( flag )
	elif flag["position"]["exist"]:
		flag = close_position( data,last_data,flag )
	else:
		flag = entry_signal( data,last_data,flag )
	
	
	# 過去データをn個ピッタリに保つために先頭を削除
	del last_data[0]
	last_data.append( data )
	i += 1
	time.sleep(wait)

基本的な仕組みは、第4回で作成した練習用のBOTと同じです。

data という変数に最新のローソク足のデータを入れて、last_data という変数に過去のローソク足のデータを入れて、それを比べることでエントリーシグナルの条件を満たしたどうかを判定しています。

1.ドンチャンブレイクアウトのデータ準備

ただしドンチャンブレイクアウトでは、過去n足(以下、20期間として説明します)の最高値をブレイクしたかどうかを判定する必要があるため、過去20期間分のローソク足データを保持する変数が必要です。

そこでメイン処理のループ文の最初に以下のような処理を入れます。

while i < len(price):
	# ドンチャンの判定に使う過去n足分の安値・高値データを準備する
	if len(last_data) < term:
		last_data.append(price[i])
		print_price(price[i])
		time.sleep(wait)
		i += 1
		continue

まず3行目の if len(last_data) < term: の部分で、last_dataに含まれているローソク足データの数が、20足分に達するまで上記の処理を実行するように指示しています。

continue は「これより下は実行せずにwhile文の先頭に戻る」という意味です。つまり、last_dataという変数に過去20足のローソク足データが保持されるまでは、この先の処理はおこなわず、この部分の処理だけをループで実行します。

いったん20足分のデータが溜まった後は、ループのたびに以下の処理を実行します。

# 過去データをn個ピッタリに保つために先頭を削除
del last_data[0]
last_data.append( data )

1行目のdel 変数名[0] で、last_data に含まれる先頭のデータを削除しています。さらに、2行目の last_data.append(data)で、最新のローソク足のデータを last_data の末尾に追加しています。

メイン処理のループのたびにこれを実行することで、last_data という変数の中身を、常に過去の新しい20期間分のローソク足データにぴったり揃えることができます。

2.ドンチャンブレイクアウトを判定する関数

# ドンチャンブレイクを判定する関数
def donchian( data,last_data ):
	
	highest = max(i["high_price"] for i in last_data)
	if data["high_price"] > highest:
		return {"side":"BUY","price":highest}
	
	lowest = min(i["low_price"] for i in last_data)
	if data["low_price"] < lowest:
		return {"side":"SELL","price":lowest}
	
	return {"side" : None , "price":0}

この関数で、ドンチアン・ブレイクアウトが発生したかどうか、どちらの方向に発生したかを判定しています。

ロングの方向にブレイクアウトが発生した場合は「BUY」シグナルを返し、ショートの方向にブレイクアウトが発生した場合は「SELL」シグナルを返します。また一緒に、ブレイクアウトされた最高値・最安値の価格を返す(return)ようにしておきます。

過去20足の最高値を更新したかどうか、を判定するロジックが以下の部分です。

highest = max(i["high_price"] for i in last_data)
if data["high_price"] > highest:
	return {"side":"BUY","price":highest}

1行目で、last_data に含まれる変数を1個ずつチェックし、["high_price"]だけを取り出した配列を作り、max()でそこから最大値を取得しています。

これはリスト内包表記というfor文を1行で書くための書き方ですが、少し難しく感じる方は、今まで勉強した知識を使って以下のように書いても構いません。

# 「リスト内包表記」で1行で書いた場合
highest = max(i["high_price"] for i in last_data)

# わかりやすく書いた場合
highest = 0
for i in last_data:
	if highest < i["high_price"]:
		highest = i["high_price"]

また、この最高値(highest)を、現在の最新のローソク足の高値(data["high_price"])と比べ、もし現在の最新のローソク足の高値の方が高ければ、ブレイクアウトの条件を満たしたことになります。

ブレイクアウトされていた場合は、その方向と価格をセットで返します。

3.エントリー注文を出す関数

def entry_signal( data,last_data,flag ):
	signal = donchian( data,last_data )
	if signal["side"] == "BUY":
		print("過去{0}足の最高値{1}円を、直近の高値が{2}円でブレイクしました".format(term,signal["price"],data["high_price"]))
		print(str(data["close_price"]) + "円で買いの指値注文を出します")

		# ここに買い注文のコードを入れる
		
		flag["order"]["exist"] = True
		flag["order"]["side"] = "BUY"

最初に1行目でさきほど作成した donchian() 関数を呼び、ドンチャンブレイクアウトが発生しているかどうかを確認します。返ってきたデータは signal という変数で受け取ります。

signal = donchian( data,last_data )

これで"BUY"が返ってくれば買いでエントリーし、"SELL"が返ってくれば売りでエントリーしています。

4.決済注文とドテン注文を出す関数

def close_position( data,last_data,flag ):
	flag["position"]["count"] += 1 # ポジションを持って何足経過してるかを計測
	signal = donchian( data,last_data )

	if flag["position"]["side"] == "BUY":
		if signal["side"] == "SELL":
			print("過去{0}足の最安値{1}円を、直近の安値が{2}円でブレイクしました".format(term,signal["price"],data["low_price"]))
			print("成行注文を出してポジションを決済します")
			
			# 決済の成行注文コードを入れる
			
			flag["position"]["exist"] = False
			flag["position"]["count"] = 0
			
			print("さらに" + str(data["close_price"]) + "円で売りの指値注文を入れてドテンします")
			
			# ここに売り注文のコードを入れる
			
			flag["order"]["exist"] = True
			flag["order"]["side"] = "SELL"

こちらも基本的な手順は同じです。最初の1行目に signal = donchian() を実行して、ドンチャンブレイクアウトのシグナルを確認します。

もし買いポジションを持っている場合は、売り方向にブレイクアウトが発生した場合のみ、ポジションを決済します。

if flag["position"]["side"] == "BUY":
	if signal["side"] == "SELL":
		print("過去{0}足の最安値{1}円を、直近の安値が{2}円でブレイクしました".format(term,signal["price"],data["low_price"]))
		print("成行注文を出してポジションを決済します")
		
		# 決済の成行注文コードを入れる
		
		flag["position"]["exist"] = False
		flag["position"]["count"] = 0

ポジションを手仕舞ったので、flag変数の[exist]をFalseに更新しておきます。

さらに、そのまま売り方向でのエントリーを試みます。これがドテン注文です。

print("さらに" + str(data["close_price"]) + "円で売りの指値注文を入れてドテンします")

# ここに売り注文のコードを入れる

flag["order"]["exist"] = True
flag["order"]["side"] = "SELL"

ドテンでエントリー注文を出したので、またflag変数の[order][exist]をTrueに更新しておきます。これで、次のwhile文では「注文が通ったかどうか確認する関数」が呼ばれることになります。

それでは最後に実行結果を見ておきましょう。

実行結果

練習問題

このドンチャンブレイクアウトのバックテスト(勝率の検証用)のコードを作り、以下の項目を計算してみましょう。コードの書き方は前回の記事で説明したのと全く同じです。

・トレード回数
・勝率
・平均リターン
・総損益
・平均保有期間

解答サンプルのコード

次回記事

上記の練習問題で、こちらのドンチャンブレイクアウトの勝率をバックテストで検証すると、以下のような成績になります。

これだけシンプルな売買ロジックでも、成績はそれほど悪くないように見えますね。

トレンドフォロー戦略は、一般論として勝率が低いです。トレードの相場の大半がレンジ相場なので、ブレイクアウトの方向に順張りするロジックは「騙し」に引っかかりやすく、勝率が低くなりがちです。その代わり、1度トレンドに乗れれば大きな利益を得られるため、勝率が50%を下回っても期待値は正(プラス)になる、というのが典型的な教科書の考え方です。

バックテストの注意点

しかしこのような戦略には、特有の弱点もあります。それはドローダウンが大きいことです。平均保有期間が長いため、途中で一時的に大きく資産が目減りする時期があるかもしれません。

また月別のリターンにかなり大きなバラつきがある可能性もあります。最終的な平均リターンが2%でも、例えば、その範囲がマイナス数十%までバラついている可能性もあります。

これらのリスクは、上記の成績表を見ているだけではわかりません。

そのため、次回の記事では、バックテストのコードをさらに改良して、資産カーブ(曲線グラフ)を描画したり、平均リターンのバラツキ具合をグラフにする方法を解説します!

「BTCFXのドンチャンチャネルブレイクアウトの勝率をバックテストで検証する」への1件のフィードバック

  1. こんにちは
    BOT作成とプログラミング勉強の参考にさせていただいております。
    質問なのですが、
    こちらのプログラムですと、ポジションを持った次のロウソク足では
    注文の判断しかしておらず、ドテンの判断をしていないように思うのですがどうでしょうか。勘違いでしたらすみません。

コメントを残す

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