BitflyerFXでBTCの平均ボラティリティ(ATR)を基準に自動で損切りを入れる

より実践的な自動売買BOTでは、エントリーや手仕舞いの条件とは別に、損切り(ストップ)を設定することが多いです。

1.損切りは必要なのか?

純粋に売買ロジックの期待値だけを考えれば、損切りを入れない方が期待リターンは高くなる場合も多いです。ラリー・コナーズ氏の著書「短期売買入門」では、「大半の売買ロジックでは損切りを用いると統計的なエッジ(優位性)が消える」と記載されています。

しかし資金管理の面では、損切りを設定しないと1回のトレードでの最大損失額を予測したりコントロールすることが難しくなります。

前回の記事で説明したように、多くの資金管理アルゴリズムでは、1回のトレードで取れるポジションの数量を「1回のトレードで許容できる最大損失額」から自動的に計算します。そのため、基本的には多少リターンを犠牲にしてでも損切りを設定することが推奨されます。

2.損切り(ストップ)の金額を設定する

「ストップの金額をいくらにすべきか?」にも色々な考え方があります。ここではATR(平均レンジ)を基準にそのときの市場の状況に応じた値幅でストップを入れる方法を解説します。

ATRとは

ATRとは、簡単にいうと一定期間における「1足の高値と安値のレンジ(値幅)」の平均値のことです。要するに、1期間で動く可能性のある値幅(ボラティリティ)の平均値のことです。

※ 正確には「高値と安値」「前の足の終値と安値」「前の足の終値と高値」の3つの値幅のうち最も大きいもののこととをATRといいます。ですが、この記事では単に高値と安値のレンジを使います。

ATRは(高値 – 安値)で計算できます。

早速、練習で1時間足のX期間における平均レンジ(ATR)を計算するpythonコードを書いてみましょう。前回までの章のコードとの繋がりを考えて、ここでは last_data という変数に過去のローソク足(OHLCV)が、配列で古い順に入っていると仮定します。

すると以下のようになります。

pythonコード


# 平均レンジを計算する期間
volatility_term = 5


# BitflyerFXのローソク足データを持つ変数(サンプル)
last_data = [
 {'close_price': 1080800,
  'close_time': 1524978000,
  'close_time_dt': '2018/04/29 14:00',
  'high_price': 1085000,
  'low_price': 1078550,
  'open_price': 1083601},
 {'close_price': 1081900,
  'close_time': 1524981600,
  'close_time_dt': '2018/04/29 15:00',
  'high_price': 1084750,
  'low_price': 1076900,
  'open_price': 1080366},
 {'close_price': 1083802,
  'close_time': 1524985200,
  'close_time_dt': '2018/04/29 16:00',
  'high_price': 1085000,
  'low_price': 1078888,
  'open_price': 1081922},
 {'close_price': 1082500,
  'close_time': 1524988800,
  'close_time_dt': '2018/04/29 17:00',
  'high_price': 1086750,
  'low_price': 1081300,
  'open_price': 1084250},
 {'close_price': 1071383,
  'close_time': 1524992400,
  'close_time_dt': '2018/04/29 18:00',
  'high_price': 1083639,
  'low_price': 1070530,
  'open_price': 1082500},
 {'close_price': 1070549,
  'close_time': 1524996000,
  'close_time_dt': '2018/04/29 19:00',
  'high_price': 1074696,
  'low_price': 1063100,
  'open_price': 1071266},
 {'close_price': 1062000,
  'close_time': 1524999600,
  'close_time_dt': '2018/04/29 20:00',
  'high_price': 1071700,
  'low_price': 1054140,
  'open_price': 1070549},
 {'close_price': 1065516,
  'close_time': 1525003200,
  'close_time_dt': '2018/04/29 21:00',
  'high_price': 1066200,
  'low_price': 1055999,
  'open_price': 1062000},
 {'close_price': 1068330,
  'close_time': 1525006800,
  'close_time_dt': '2018/04/29 22:00',
  'high_price': 1071250,
  'low_price': 1063000,
  'open_price': 1065800},
 {'close_price': 1070222,
  'close_time': 1525010400,
  'close_time_dt': '2018/04/29 23:00',
  'high_price': 1071500,
  'low_price': 1066301,
  'open_price': 1068330}]


# 期間の平均ボラティリティを計算する関数
def calculate_volatility( last_data ):

	high_sum = sum(i["high_price"] for i in last_data[-1 * volatility_term :])
	low_sum  = sum(i["low_price"]  for i in last_data[-1 * volatility_term :])
	volatility = round((high_sum - low_sum) / volatility_term)
	return volatility


# メイン処理
print( calculate_volatility(last_data) )

これを計算して実行してみると、例えば、5期間の平均レンジ(ボラティリティ値)は10561円、10期間の平均レンジは9178円とわかります。大体、平均して1時間に1万円くらいの値動きがあった、ということですね。

BTCFXはボラティリティの変動が大きいため、固定値(5000円とか3%とか)で損切りを入れるより、そのときどきの市場ボラティリティの変動に合わせてストップを設定した方がリスクを柔軟にコントロールできます。

コードの解説

前述のように、ATR(ボラティリティ)とは高値と安値の差を1足ごとに計算し、それを合計したものを期間で割って平均値を求めたものです。ただし算数的には、先にすべての高値の合計とすべての安値の合計の差を計算してそれを期間で割っても同じです。

そのため、以下のようにシンプルに3行で書くことができます。

high_sum = sum(i["high_price"] for i in last_data[-1 * volatility_term :])
low_sum  = sum(i["low_price"]  for i in last_data[-1 * volatility_term :])
volatility = round((high_sum - low_sum) / volatility_term)

1行目は、last_data という配列データの後ろからX個( volatility_term で設定した個数 )だけ取り出して、その中で高値([high_price])だけを合計しています。

この書き方には「スライス」と「リスト内包表記」という2つのテクニックを使っています。どちらも前の章で、「ドンチアンブレイクアウトの判定をする関数」を作ったときに解説しましたが、一応参考リンクを貼っておきます。

リスト内包表記について
スライスについての記事

3.自動的にストップを入れるコードを書く

Bitflyerでストップ注文を入れる方法としては、特殊注文のAPIを使う方法もあります。ですが、スキャルや高頻度売買BOTでない限り、普通に成行注文を出す設計にしておいた方が、実際の運用時にトラブルが減っていいと思います。

手順としては以下になります。

手順

1)エントリー時に上記の関数を使って、直近の1期間の平均ボラティリティを計算する
2)平均ボラティリティを基準にストップ金額を決める(例:2ATR)
3)エントリー注文の時に執行された価格を変数に記録しておく
4)全体ループのたびに新たに取得した足の安値(または高値)が損切にかかってないかチェックする
5)損切にかかっていたら決済の成行注文を出す

今後の記事では、エントリー後に損切りラインを有利な方向に動かしていく「トレイリングストップ」や、徐々にポジションを買い増ししていく「増し玉」なども解説していく予定です。

そこで毎回、ストップの逆指値注文を出したり、キャンセルしたりしていると、とてつもなくコードが複雑になります。そのため、このブログでは基本的に成行注文で損切りする前提で解説を進めます。

注意点1)実際の運用時

上記の方法の場合、逆指値のストップ注文を入れる方法に比べると、注文の執行が遅れる可能性があります。

実際の運用では、成行注文を出す場合でも、リアルタイムで形成中の足やより短い時間軸の足を使って損切りの判定をすることになります。つまり、エントリーや手仕舞いの判定には直近の確定足を使い、損切りの判定には形成中の足(または1分足)を使います。

注意点2)バックテスト時

一方、バックテストでは、短い時間軸の過去データが同じ期間分だけ揃わないため、上記の状況を再現することはできません。

そこでバックテストでは、「1時間足の終値が損切りラインに掛かっていたら、本来のストップ位置で約定したもの」と仮定します。ただし実際には、理想の価格よりも不利な位置で約定する可能性が高いため、平均ボラティリティを基準にして、1~2ティック不利な位置で約定したと仮定します。

pythonコードを書こう!

では実際のコードを見ていきましょう。

前回の章までで作成したコードに追記していくことを前提に解説していきます。まずはエントリー注文を出す関数に以下を追記します。

1)エントリー注文時


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

def entry_signal( data,last_data,flag ):
	signal = donchian( data,last_data )
	if signal["side"] == "BUY":

		# ここに買い注文のコードを入れる
		
		flag["order"]["ATR"] = calculate_volatility( last_data ) # 追記部分
		flag["order"]["price"] = data["close_price"]

	return flag

flag変数に新しく[“ATR”]という項目を作成し、ここにエントリー時の平均ボラティリティを記録します。

2)ポジションの取得時

指値注文が約定したら、以下のように[position][ATR]に、エントリー時のボラティリティの値を引き継ぎます。


# サーバーに出した注文が約定したか確認する関数

def check_order( flag,last_data ):

	flag["position"]["price"] = flag["order"]["price"]
	flag["position"]["ATR"] = flag["order"]["ATR"]
	return flag

なお、成行注文でエントリーする場合は上記は必要ありません。

前回はバックテストの章だったので説明しませんでしたが、実際の運用では、ドンチアン・ブレイクアウトのようなBOTは成行注文でエントリーする方が現実的です。

指値注文だと注文が刺さらなかった場合は諦めることになりますが、ブレイクアウトBOTでは「数回のトレード機会を見逃す」ことが、最終的に致命的なパフォーマンスの差に繋がる可能性があるからです。

成行注文でエントリーした場合に、執行価格を取得する方法は以下の記事を参考にしてください。

参考:Bitflyerの成行注文の執行価格をAPIで取得する

3)損切りを判定する関数

いったんポジションを保有したら、全体ループの中で以下のような「損切りの判定をする関数」で損切ラインを割っていないかを確認します。


# 損切ラインにかかったら成行注文で決済する関数

def stop_position( data,flag ):

	if flag["position"]["side"] == "BUY":
		stop_price = flag["position"]["price"] - flag["position"]["ATR"] * stop_range
		if data["low_price"] < stop_price:
			flag["records"]["log"].append("{0}円の損切ラインに引っかかりました。\n".format( stop_price ))
			stop_price = round( stop_price - 2 * calculate_volatility(last_data) / ( chart_sec / 60) )
			flag["records"]["log"].append(str(stop_price) + "円あたりで成行注文を出してポジションを決済します\n")
			
			# 決済の成行注文コードを入れる
			
			records( flag,data,stop_price,"STOP" )
			flag["position"]["exist"] = False
			flag["position"]["count"] = 0
	
	
	if flag["position"]["side"] == "SELL":
		stop_price = flag["position"]["price"] + flag["position"]["ATR"] * stop_range
		if data["high_price"] > stop_price:
			flag["records"]["log"].append("{0}円の損切ラインに引っかかりました。\n".format( stop_price ))
			stop_price = round( stop_price + 2 * calculate_volatility(last_data) / (chart_sec / 60) )
			flag["records"]["log"].append(str(stop_price) + "円あたりで成行注文を出してポジションを決済します\n")
			
			# 決済の成行注文コードを入れる
			
			records( flag,data,stop_price,"STOP" )
			flag["position"]["exist"] = False
			flag["position"]["count"] = 0
			
	return flag

以下の式で損切りのラインを計算しています。

▽ 損切りラインの計算式


# 買いエントリーの場合
stop_price = flag["position"]["price"] - flag["position"]["ATR"] * stop_range

# 売りエントリーの場合
stop_price = flag["position"]["price"] + flag["position"]["ATR"] * stop_range

stop_range という変数は、何レンジ幅にストップを入れるかという設定項目です。例えば「2」であれば、エントリー方向から平均ボラティリティの2期間分、逆に動いたときに損切りします。これは最初に倍率を設定しておきます。

損切りラインを計算したら、直近の安値(売りなら直近の高値)が、損切りラインを割っていないかどうか判定します。損切りラインにかかっていた場合は、成行注文を出してポジションを決済します。

なお、バックテストの場合は、以下のように1分足で2ティック不利な位置で約定したと仮定します。

▽ バックテストでの約定価格


# 買いエントリーの場合
stop_price = round( stop_price - 2 * calculate_volatility(last_data) / ( chart_sec / 60) )

# 売りエントリーの場合
stop_price = round( stop_price + 2 * calculate_volatility(last_data) / ( chart_sec / 60) )

ここでは、平均ボラティリティの値幅を1分間に換算して、それを2倍しています。

つまり実際の運用時に1分足で2ティック分程度、注文が遅れたと仮定しています。ここは好みで調整してください。

なお、上記の「損切りを判定する関数」は、「手仕舞いの条件を判定する関数」より前に呼ばれる必要があります。そのため、「手仕舞いの条件を判定する関数」の方では、二重に決済されることがないように、以下のコードを入れておいてください。

# 手仕舞いとドテン注文 を出す関数
def close_position( data,last_data,flag ):

	# すでに損切りに掛かっていたら何もしない
	if flag["position"]["exist"] == False:
		return flag

4)全体のメイン処理

最後に全体のループ部分です。

last_data = []
need_term = max(buy_term,sell_term,volatility_term)
i = 0
while i < len(price):

	# ドンチャンの判定に使う期間分の安値・高値データを準備する
	if len(last_data) < need_term:
		last_data.append(price[i])
		flag = log_price(price[i],flag)
		time.sleep(wait)
		i += 1
		continue
	
	data = price[i]
	flag = log_price(data,flag)
	
	
	if flag["order"]["exist"]:
		flag = check_order( flag )
	elif flag["position"]["exist"]:
		flag = stop_position( data,flag ) # ストップの関数を先に呼ぶ
		flag = close_position( data,last_data,flag )
	else:
		flag = entry_signal( data,last_data,flag )
	
	last_data.append( data )
	i += 1
	time.sleep(wait)

今回からは「平均ボラティリティを計算する期間」も考慮する必要があります。

そのため、最初に設定項目の中から(上値ブレイクアウト期間・下値ブレイクアウト期間・平均レンジの計算期間のうち)最も長いものを調べ、その期間分のローソク足データが溜まるまで次に進まないようにしています。

またポジションがある場合(flag[position][exist]==True)に、さきほど作成した損切を判定する関数(stop_position())を呼ぶようにしておきます。これで完成です!

実行結果

最後にこれらを追記して「1時間足の30期間ドンチアン・チャネルブレイクアウトで2ATRの損切りを使った場合」の結果を示しておきましょう。

今回は平均ボラティリティ(ATR)も30期間で計算しています。つまり直近の30期間の平均ボラティリティを使って、ブレイクアウトのポイントから2足分、逆に動いたら損切りする、というロジックを組み込んだ場合の成績です。

設定項目

・1時間足を使用(2017/8~2018/4)
・上値ブレイクアウト判定期間 30期間
・下値ブレイクアウト判定期間 30期間
・平均ボラティリティ計算期間 30期間
・ブレイクアウトの判定基準(高値/安値)
・ストップのレンジ幅 2ATR

結果

(base) C:\Pydoc>python test.py
--------------------------
テスト期間:
開始時点 : 2017/09/03 16:00
終了時点 : 2018/05/11 23:00
6000件のローソク足データで検証
--------------------------
バックテストの結果
-----------------------------------
買いエントリの成績
-----------------------------------
トレード回数       :  55回
勝率               :  43.6%
平均リターン       :  2.57%
総損益             :  1110155円
平均保有期間       :  49.9足分
損切りの回数       :  24回
-----------------------------------
売りエントリの成績
-----------------------------------
トレード回数       :  55回
勝率               :  32.7%
平均リターン       :  0.95%
総損益             :  837891円
平均保有期間       :  40.9足分
損切りの回数       :  28回
-----------------------------------
総合の成績
-----------------------------------
全トレード数       :  110回
勝率               :  38.2%
平均リターン       :  1.76%
平均保有期間       :  45.4足分
損切りの回数       :  52回

最大の勝ちトレード :  545284円
最大の負けトレード :  -240272円
最大連敗回数       :  10回
最大ドローダウン   :  -308866円 / -29.0%
利益合計           :  4507994円
損失合計           :  -2559948円

最終損益           :  1948046円
手数料合計         :  -121351円
-----------------------------------
月別の成績
-----------------------------------
2017年9月の成績
-----------------------------------
トレード数         :  11回
月間損益           :  46625円
平均リターン       :  0.94%
月間ドローダウン   :  -31743円
-----------------------------------
2017年10月の成績
-----------------------------------
トレード数         :  14回
月間損益           :  99914円
平均リターン       :  1.62%
月間ドローダウン   :  -61226円
-----------------------------------
2017年11月の成績
-----------------------------------
トレード数         :  12回
月間損益           :  283306円
平均リターン       :  3.25%
月間ドローダウン   :  -100079円
-----------------------------------
2017年12月の成績
-----------------------------------
トレード数         :  9回
月間損益           :  330126円
平均リターン       :  3.34%
月間ドローダウン   :  -308866円
-----------------------------------
2018年1月の成績
-----------------------------------
トレード数         :  17回
月間損益           :  398919円
平均リターン       :  0.89%
月間ドローダウン   :  -206301円
-----------------------------------
2018年2月の成績
-----------------------------------
トレード数         :  12回
月間損益           :  511775円
平均リターン       :  4.22%
月間ドローダウン   :  -238405円
-----------------------------------
2018年3月の成績
-----------------------------------
トレード数         :  12回
月間損益           :  108046円
平均リターン       :  0.64%
月間ドローダウン   :  -216829円
-----------------------------------
2018年4月の成績
-----------------------------------
トレード数         :  17回
月間損益           :  147572円
平均リターン       :  0.93%
月間ドローダウン   :  -170708円
-----------------------------------
2018年5月の成績
-----------------------------------
トレード数         :  6回
月間損益           :  21763円
平均リターン       :  0.37%
月間ドローダウン   :  -119398円

注) ここでの月間ドローダウンは、前月から継続中の最大ドローダウンのことです。その月単独の損失ではありません。

ログファイルを見ると、どこで損切りに掛かっているのかもわかります。

比較検証してみよう!

では、以下の4つの条件で最終成績とドローダウンを比較してみましょう。

1)損切り(ストップ)を用いない場合
2)1レンジ幅に損切りを置いた場合
3)2レンジ幅に損切りを置いた場合
4)3レンジ幅に損切りを置いた場合

※ 損切りを用いない場合の検証は、損切りの関数を「#」でコメントアウトするだけで検証できます。

1)損切りを用いない場合

-----------------------------------
総合の成績
-----------------------------------
全トレード数       :  98回
勝率               :  46.9%
平均リターン       :  2.06%
平均保有期間       :  59.7足分
損切りの回数       :  0回

最大の負けトレード :  -296151円
最大連敗回数       :  5回
最大ドローダウン   :  -483332円 / -25.3%
最終損益           :  1929010円

2)損切りを1レンジ幅に置いた場合

-----------------------------------
総合の成績
-----------------------------------
全トレード数       :  154回
勝率               :  22.1%
平均リターン       :  0.85%
平均保有期間       :  26.1足分
損切りの回数       :  115回

最大の負けトレード :  -123445円
最大連敗回数       :  15回
最大ドローダウン   :  -740325円 / -75.9%
最終損益           :  1277551円

3)損切りを2レンジ幅に置いた場合

-----------------------------------
総合の成績
-----------------------------------
全トレード数       :  110回
勝率               :  38.2%
平均リターン       :  1.76%
平均保有期間       :  45.4足分
損切りの回数       :  52回

最大の負けトレード :  -240272円
最大連敗回数       :  10回
最大ドローダウン   :  -308866円 / -29.0%
最終損益           :  1948046円

4)損切りを3レンジ幅に置いた場合

-----------------------------------
総合の成績
-----------------------------------
全トレード数       :  100回
勝率               :  45.0%
平均リターン       :  2.05%
平均保有期間       :  54.7足分
損切りの回数       :  24回

最大の負けトレード :  -357078円
最大連敗回数       :  9回
最大ドローダウン   :  -425672円 / -39.2%
最終損益           :  1927639円

検証結果

ストップ幅 損切なし 1ATR 2ATR 3ATR
トレード回数 98 154 110 100
勝率 46.9% 22.1% 38.2% 45.0%
平均リターン 2.06% 0.85% 1.76% 2.05%
損切りの回数 0 115 52 24
最大連敗回数 5 15 10 9
最大DD -25.3% -75.9% -29.0% -39.2%
最終損益 192万9010円 127万7551円 194万8046円 192万7639円

 

損切りの位置を近くに置けばおくほど、損切りにかかる回数は増え、その結果1トレード当たりの勝率や平均リターンが悪くなる、という明確な傾向が見てとれます。

期待値への影響

損切りのパラメーターは、恣意的に決められる自由度の高い数字なので、どうしてもカーブフィッティングに繋がりやすくなります。そのため、売買ロジックが持つ期待値の検証とは切り離して考えた方がいいと思います。

重要なのは、バックテスト編で検証したような、本来の売買ロジックが持つ期待値を「できるだけ邪魔しない」ことです。その意味で考えると1ATRのストップ幅は近すぎます。154回のトレードのうち115回も損切りにかかっていては、元々の売買ロジックの期待値を再現できません。

売買ロジックの持つ期待値を邪魔しない範囲で、2~3ATRのラインに損切りを置くのが適切だと思います。

資金管理への影響

売買ロジックの期待値を検証するときには、あまり損切りのパラメータを調整するべきではありません。ですが、売買ロジックを決定した後に、資金管理の方法を考える上では、損切りラインをどこに設定するかは重要な問題になります。

前回の記事の公式で説明したように、損切りラインを近くに設定するほどポジションサイズを大きく取ることができ、損切りラインを遠くに設定するほどポジションサイズは小さくなるからです。ポジションサイズの設計は、売買ロジックの期待値とは違う角度からパフォーマンスに大きな影響を与えます。

上記の例だと、2ATRの位置に損切りラインを置く場合と、3ATRの位置に損切りを置く場合とでは、ポジションを取れる量が1.5倍も違ってきます。2ATRでも3ATRでも、そこまで期待値が変わらないと判断できるのであれば、2ATRを用いて大きくポジションを取った方が、利益の絶対額は大きくなります。

次回の記事では、今回の記事で計算したストップから自動的に注文枚数を計算する部分のコードを実装していきます。

今回の練習問題

今回のコードでは、ついでに新しく以下の2つを実装しました。

1)損切りに掛かった回数を集計する
2)最大連敗回数を集計する

いずれも前回の章(バックテスト編)で勉強した内容で簡単にかけるコードなので、興味がある方は練習問題として自身でコードを書いてみてください。連敗回数の取得にはfor文を使いましたが、本当はもっとスマートな書き方もあるかもしれません。

▽ 今回の記事で使ったコード



import requests
from datetime import datetime
import time
import matplotlib.pyplot as plt
import pandas as pd


#--------設定項目--------

chart_sec = 3600           #  1時間足を使用
buy_term =  30             #  買いエントリーのブレイク期間の設定
sell_term = 30             #  売りエントリーのブレイク期間の設定

judge_price={
  "BUY" : "high_price",    #  ブレイク判断 高値(high_price)か終値(close_price)を使用
  "SELL": "low_price"      #  ブレイク判断 安値 (low_price)か終値(close_price)を使用
}

volatility_term = 30       # 平均ボラティリティの計算に使う期間
stop_range = 2             # 何レンジ幅に損切(ストップ)を置くか

wait = 0                   #  ループの待機時間
lot = 1                    #  BTCの注文枚数
slippage = 0.001           #  手数料・スリッページ

#------------------------


# 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 log_price( data,flag ):
	log =  "時間: " + datetime.fromtimestamp(data["close_time"]).strftime('%Y/%m/%d %H:%M') + " 高値: " + str(data["high_price"]) + " 安値: " + str(data["low_price"]) + " 終値: " + str(data["close_price"]) + "\n"
	flag["records"]["log"].append(log)
	return flag



# 平均ボラティリティを計算する関数
def calculate_volatility( last_data ):

	high_sum = sum(i["high_price"] for i in last_data[-1 * volatility_term :])
	low_sum  = sum(i["low_price"]  for i in last_data[-1 * volatility_term :])
	volatility = round((high_sum - low_sum) / volatility_term)
	return volatility


# ドンチャンブレイクを判定する関数
def donchian( data,last_data ):
	
	highest = max(i["high_price"] for i in last_data[ (-1* buy_term): ])
	if data[ judge_price["BUY"] ] > highest:
		return {"side":"BUY","price":highest}
	
	lowest = min(i["low_price"] for i in last_data[ (-1* sell_term): ])
	if data[ judge_price["SELL"] ] < 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":
		flag["records"]["log"].append("過去{0}足の最高値{1}円を、直近の価格が{2}円でブレイクしました\n".format(buy_term,signal["price"],data[judge_price["BUY"]]))
		flag["records"]["log"].append(str(data["close_price"]) + "円で買いの指値注文を出します\n")

		# ここに買い注文のコードを入れる
		
		flag["order"]["ATR"] = calculate_volatility( last_data )
		flag["order"]["price"] = data["close_price"]
		flag["order"]["exist"] = True
		flag["order"]["side"] = "BUY"

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

		# ここに売り注文のコードを入れる
		
		flag["order"]["ATR"] = calculate_volatility( last_data )
		flag["order"]["price"] = 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"]
	flag["position"]["ATR"] = flag["order"]["ATR"]
	flag["position"]["price"] = flag["order"]["price"]
	
	return flag


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

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


# 損切ラインにかかったら成行注文で決済する関数
def stop_position( data,flag ):
	
	if flag["position"]["side"] == "BUY":
		stop_price = flag["position"]["price"] - flag["position"]["ATR"] * stop_range
		if data["low_price"] < stop_price:
			flag["records"]["log"].append("{0}円の損切ラインに引っかかりました。\n".format( stop_price ))
			stop_price = round( stop_price - 2 * calculate_volatility(last_data) / ( chart_sec / 60) )
			flag["records"]["log"].append(str(stop_price) + "円あたりで成行注文を出してポジションを決済します\n")
			
			# 決済の成行注文コードを入れる
			
			records( flag,data,stop_price,"STOP" )
			flag["position"]["exist"] = False
			flag["position"]["count"] = 0
	
	
	if flag["position"]["side"] == "SELL":
		stop_price = flag["position"]["price"] + flag["position"]["ATR"] * stop_range
		if data["high_price"] > stop_price:
			flag["records"]["log"].append("{0}円の損切ラインに引っかかりました。\n".format( stop_price ))
			stop_price = round( stop_price + 2 * calculate_volatility(last_data) / (chart_sec / 60) )
			flag["records"]["log"].append(str(stop_price) + "円あたりで成行注文を出してポジションを決済します\n")
			
			# 決済の成行注文コードを入れる
			
			records( flag,data,stop_price,"STOP" )
			flag["position"]["exist"] = False
			flag["position"]["count"] = 0
			
	return flag


# 各トレードのパフォーマンスを記録する関数
def records(flag, data, close_price, close_type=None):
	
	# 取引手数料等の計算
	entry_price = int(round(flag["position"]["price"] * lot))
	exit_price = int(round( close_price * lot ))
	trade_cost = round( exit_price * slippage )
	
	log = "スリッページ・手数料として " + str(trade_cost) + "円を考慮します\n"
	flag["records"]["log"].append(log)
	flag["records"]["slippage"].append(trade_cost)
	
	# 手仕舞った日時と保有期間を記録
	flag["records"]["date"].append(data["close_time_dt"])
	flag["records"]["holding-periods"].append( flag["position"]["count"] )
	
	# 損切りにかかった回数をカウント
	if close_type == "STOP":
		flag["records"]["stop-count"].append(1)
	else:
		flag["records"]["stop-count"].append(0)
	
	# 値幅の計算
	buy_profit = exit_price - entry_price - trade_cost
	sell_profit = entry_price - exit_price - trade_cost
	
	# 利益が出てるかの計算
	if flag["position"]["side"] == "BUY":
		flag["records"]["side"].append( "BUY" )
		flag["records"]["profit"].append( buy_profit )
		flag["records"]["return"].append( round( buy_profit / entry_price * 100, 4 ))
		if buy_profit  > 0:
			log = str(buy_profit) + "円の利益です\n"
			flag["records"]["log"].append(log)
		else:
			log = str(buy_profit) + "円の損失です\n"
			flag["records"]["log"].append(log)
	
	if flag["position"]["side"] == "SELL":
		flag["records"]["side"].append( "SELL" )
		flag["records"]["profit"].append( sell_profit )
		flag["records"]["return"].append( round( sell_profit / entry_price * 100, 4 ))
		if sell_profit > 0:
			log = str(sell_profit) + "円の利益です\n"
			flag["records"]["log"].append(log)
		else:
			log = str(sell_profit) + "円の損失です\n"
			flag["records"]["log"].append(log)
	
	return flag

# バックテストの集計用の関数
def backtest(flag):
	
	# 成績を記録したpandas DataFrameを作成
	records = pd.DataFrame({
		"Date"     :  pd.to_datetime(flag["records"]["date"]),
		"Profit"   :  flag["records"]["profit"],
		"Side"     :  flag["records"]["side"],
		"Rate"     :  flag["records"]["return"],
		"Stop"     :  flag["records"]["stop-count"],
		"Periods"  :  flag["records"]["holding-periods"],
		"Slippage" :  flag["records"]["slippage"]
	})
	
	# 連敗回数をカウントする
	consecutive_defeats = []
	defeats = 0
	for p in flag["records"]["profit"]:
		if p < 0:
			defeats += 1
		else:
			consecutive_defeats.append( defeats )
			defeats = 0
	
	
	# 総損益の列を追加する
	records["Gross"] = records.Profit.cumsum()
	
	# 最大ドローダウンの列を追加する
	records["Drawdown"] = records.Gross.cummax().subtract(records.Gross)
	records["DrawdownRate"] = round(records.Drawdown / records.Gross.cummax() * 100,1)
	
	# 買いエントリーと売りエントリーだけをそれぞれ抽出する
	buy_records = records[records.Side.isin(["BUY"])]
	sell_records = records[records.Side.isin(["SELL"])]
	
	# 月別のデータを集計する
	records["月別集計"] = pd.to_datetime( records.Date.apply(lambda x: x.strftime('%Y/%m')))
	grouped = records.groupby("月別集計")
	
	month_records = pd.DataFrame({
		"Number"   :  grouped.Profit.count(),
		"Gross"    :  grouped.Profit.sum(),
		"Rate"     :  round(grouped.Rate.mean(),2),
		"Drawdown" :  grouped.Drawdown.max(),
		"Periods"  :  grouped.Periods.mean()
		})
	
	print("バックテストの結果")
	print("-----------------------------------")
	print("買いエントリの成績")
	print("-----------------------------------")
	print("トレード回数       :  {}回".format( len(buy_records) ))
	print("勝率               :  {}%".format(round(len(buy_records[buy_records.Profit>0]) / len(buy_records) * 100,1)))
	print("平均リターン       :  {}%".format(round(buy_records.Rate.mean(),2)))
	print("総損益             :  {}円".format( buy_records.Profit.sum() ))
	print("平均保有期間       :  {}足分".format( round(buy_records.Periods.mean(),1) ))
	print("損切りの回数       :  {}回".format( buy_records.Stop.sum() ))
	
	print("-----------------------------------")
	print("売りエントリの成績")
	print("-----------------------------------")
	print("トレード回数       :  {}回".format( len(sell_records) ))
	print("勝率               :  {}%".format(round(len(sell_records[sell_records.Profit>0]) / len(sell_records) * 100,1)))
	print("平均リターン       :  {}%".format(round(sell_records.Rate.mean(),2)))
	print("総損益             :  {}円".format( sell_records.Profit.sum() ))
	print("平均保有期間       :  {}足分".format( round(sell_records.Periods.mean(),1) ))
	print("損切りの回数       :  {}回".format( sell_records.Stop.sum() ))
	
	print("-----------------------------------")
	print("総合の成績")
	print("-----------------------------------")
	print("全トレード数       :  {}回".format(len(records) ))
	print("勝率               :  {}%".format(round(len(records[records.Profit>0]) / len(records) * 100,1)))
	print("平均リターン       :  {}%".format(round(records.Rate.mean(),2)))
	print("平均保有期間       :  {}足分".format( round(records.Periods.mean(),1) ))
	print("損切りの回数       :  {}回".format( records.Stop.sum() ))
	print("")
	print("最大の勝ちトレード :  {}円".format(records.Profit.max()))
	print("最大の負けトレード :  {}円".format(records.Profit.min()))
	print("最大連敗回数       :  {}回".format( max(consecutive_defeats) ))
	print("最大ドローダウン   :  {0}円 / {1}%".format(-1 * records.Drawdown.max(), -1 * records.DrawdownRate.loc[records.Drawdown.idxmax()]  ))
	print("利益合計           :  {}円".format( records[records.Profit>0].Profit.sum() ))
	print("損失合計           :  {}円".format( records[records.Profit<0].Profit.sum() ))
	print("")
	print("最終損益           :  {}円".format( records.Profit.sum() ))
	print("手数料合計         :  {}円".format( -1 * records.Slippage.sum() ))
	
	print("-----------------------------------")
	print("月別の成績")
	
	for index , row in month_records.iterrows():
		print("-----------------------------------")
		print( "{0}年{1}月の成績".format( index.year, index.month ) )
		print("-----------------------------------")
		print("トレード数         :  {}回".format( row.Number.astype(int) ))
		print("月間損益           :  {}円".format( row.Gross.astype(int) ))
		print("平均リターン       :  {}%".format( row.Rate ))
		print("月間ドローダウン   :  {}円".format( -1 * row.Drawdown.astype(int) ))

	
	# ログファイルの出力
	file =  open("./{0}-log.txt".format(datetime.now().strftime("%Y-%m-%d-%H-%M")),'wt',encoding='utf-8')
	file.writelines(flag["records"]["log"])

	# 損益曲線をプロット
	plt.plot( records.Date, records.Gross )
	plt.xlabel("Date")
	plt.ylabel("Balance")
	plt.xticks(rotation=50) # X軸の目盛りを50度回転
	
	plt.show()
	


# ここからメイン処理

# 価格チャートを取得
price = get_price(chart_sec,after=1451606400)

flag = {
	"order":{
		"exist" : False,
		"side" : "",
		"price" : 0,
		"ATR"  : 0,
		"count" : 0
	},
	"position":{
		"exist" : False,
		"side" : "",
		"price": 0,
		"ATR" :0,
		"count":0
	},
	"records":{
		"date":[],
		"profit":[],
		"return":[],
		"side":[],
		"stop-count":[],
		"holding-periods":[],
		"slippage":[],
		"log":[]
	}
}


last_data = []
need_term = max(buy_term,sell_term,volatility_term)
i = 0
while i < len(price):

	# ドンチャンの判定に使う期間分の安値・高値データを準備する
	if len(last_data) < need_term:
		last_data.append(price[i])
		flag = log_price(price[i],flag)
		time.sleep(wait)
		i += 1
		continue
	
	data = price[i]
	flag = log_price(data,flag)
	
	
	if flag["order"]["exist"]:
		flag = check_order( flag )
	elif flag["position"]["exist"]:
		flag = stop_position( data,flag )
		flag = close_position( data,last_data,flag )
	else:
		flag = entry_signal( data,last_data,flag )
	
	last_data.append( data )
	i += 1
	time.sleep(wait)


print("--------------------------")
print("テスト期間:")
print("開始時点 : " + str(price[0]["close_time_dt"]))
print("終了時点 : " + str(price[-1]["close_time_dt"]))
print(str(len(price)) + "件のローソク足データで検証")
print("--------------------------")

backtest(flag)

コメントを残す

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