1回のトレードの許容資金を使って分割エントリー(増し玉)してみよう!

前回の記事では、「1回のトレードで口座の何%までの損失(リスク)を許容できるか?」から逆算して、ポジションサイズを計算する方法を紹介しました。

これを応用すると、1回のトレードで取れるリスクの範囲から逆算して数回に分けてポジションを取得することもできます。いわゆる「増し玉」(ピラミッティング)のことですね。

今回は、1度に許容範囲の全額でポジションを取るのではなく、数回に分けてポジションを取った場合のパフォーマンスを検証してみましょう。

1.今回検証すること

今回はドンチャン・チャネルブレイクアウト戦略で以下の3つのエントリー方法をテストしてみましょう。

1)ブレイクアウトの時点で許容リスク全額分の注文を出す
2)ブレイクアウトの時点で半分だけポジションを取り、その後、ブレイクアウトの方向に1ATR進んだらもう半分のポジションを取る
3)ブレイクアウトの時点では1/4だけポジションを取り、その後、ブレイクアウトの方向に1/2ATR進むたびに、1/4ずつ追加ポジションを取る

2)と3)を図にすると以下のような感じです。

チャネルブレイクアウトについて詳しく解説している「タートル流投資の魔術」という有名な本のトレーディング規則では、1トレードの資金を4つのユニットに分割して、基準レンジ(1/2ATR)ごとにポジションを追加する方法(3のパターン)が、実際に紹介されています。

今回の検証では、ブレイクアウトの地点から2レンジ以上離れる前に全ポジションを取得しますが、何レンジ離れるごとに追加ポジションを取得するかは、あとから自由に変更できるように変数にしておきましょう。

2.実装の仕組みの考え方

複数回に分けてエントリーする場合でも、前回の記事と同じように「理論上のサイズ」と「実際に購入可能なサイズ」の両方を計算する必要があります。

1)理論サイズの計算

理論サイズの方は、簡単に計算できます。口座資金のうち1トレードで許容できる損失の割合(n%)を、分割でエントリーしたい回数で割ればいいだけだからです。

例えば、1トレードで失ってもいい口座資金の割合が2%で、2回に分けてエントリーしたいなら、各トレードのリスク量を1%にして計算します。4回に分けてエントリーするなら0.5%で計算します。

こちらの計算式の中にBTC価格は含まれていません。そのため、何回に分けてエントリーしたとしても、こちらの「理論上の計算サイズ」はすべて同じになります。

例えば、以下の条件を仮定しましょう。

・口座資金50万円
・平均ボラティリティ(ATR)1万円
・損切レンジ2ATR
・1トレードでの許容リスク 口座の4%
・分割エントリーの回数 4回

この場合、4回のエントリーの各ポジションサイズは、以下の式で決まります。

・(50万円 × 0.04 ÷ 4) ÷ 2万円 = 0.25枚

2)実際に購入可能なサイズの計算

一方、「実際に購入可能なサイズ」の方は、ブレイクアウト後の価格の変動によっては、最初と同じ枚数を取得できなくなる可能性があります。特にレバレッジの比率が低い場合はそうです。

例えば、先ほどと同じ条件で1/2ATRごとに4回に分けてポジションを取得するケースを考えてみましょう。レバレッジは2倍、ブレイクアウト時のBTC価格は100万円とします。

・口座資金50万円
・レバレッジ2倍
・平均ボラティリティ(ATR)1万円
・BTC価格 100万円
・分割エントリーの回数 4回

この場合、各ポジションの取得時の価格は以下のようになるはずです。

・1回目・・・100万円(0.25枚)
・2回目・・・100万5000円(0.25枚)
・3回目・・・101万円(0.25枚)
・4回目・・・101万5000円(0.24枚)

このとき、各ポジション取得時の計算式は以下のようになります。

・(証拠金残高 – 既存のポジションの必要証拠金) × レバレッジ倍率 ÷ BTC価格

前回の記事と同様に、各ポジションを追加取得する際に、この「理論上の注文サイズ」と「実際に注文可能なサイズ」の両方を計算して、より小さい方のサイズで発注します。

3.バックテスト用のコード

ではバックテスト用のコードを作ってみましょう。

1)設定項目

今回のコードでは、以下の2つの項目を設定できるようにします。

entry_times = 2            # 何回に分けて追加ポジションを取るか
entry_range = 1            # 何レンジごとに追加ポジションを取るか

この例であれば、1ATRごとに2回に分けてポジションを取ります。
つまりブレイクアウト時に半分だけポジションを取り、さらに価格がブレイクアウト方向に1ATR進んだら、もう半分のポジションを取ります。

2)pythonコード

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

前回までに作成してきた関数やコードをあまり修正しなくていいように、追加ポジションの取得に関しては、全く別の変数と関数を用意することにします。

まずは以下のような変数を作りましょう。


# ["add-position"]という項目を追加
flag = {
	"add-position":{
		"count":0, # 何回目のエントリーかを管理
		"first-entry-price":0, # 最初のエントリー価格を記録
		"last-entry-price":0,  # 前回(n-1)回目のエントリー価格を記録
		"unit-range":0, # 何円ごとに買い増し(ポジション追加)するかを記録
		"unit-size":0,  # 1回あたりの理論上のポジションサイズを記録
		"stop":0        # 最初のエントリー時のストップ幅を記録
	},
	(略)
}

前述のように、各追加ポジションの理論上のサイズ(unit-size)と、何円幅ごとに追加ポジションを取得するかを示す基準レンジ(unit-range)の2つは、最初のブレイクアウトの時点で計算できます。

そのため、最初にこの2つを計算して上記の変数にセットするようにします。

あとは、新しいローソク足の終値を取得するたびに、最初のエントリー価格(first-entry-price)からどれだけ離れたかを計算して、基準レンジ(unit-range)以上に離れていたら、追加ポジションを取得する、という流れで実装していきます。

ストップ位置の更新

なお、ストップ(損切り)のラインは、追加ポジションを取得するたびに、その平均取得単価を基準にして、全ポジションの損切りラインを移動させていくことにします。例えば、以下のような感じです。

・最初のエントリー価格 100万円
・平均ボラティリティ(ATR) 1万円
・損切レンジ 2ATR

・1回目のエントリー 100万円
・2回目のエントリー 100万5000円
・3回目のエントリー 101万円
・4回目のエントリー 101万5000円

最初に設定した許容リスクを超えないように、どの時点でも平均取得単価の2レンジ下で損切りが入るようストップ位置を更新していきます。

・1回目の平均取得単価 100万円 / ストップ 98万円
・2回目の平均取得単価 100万2500円 / ストップ 98万2500円
・3回目の平均取得単価 100万5000円 / ストップ 98万5000円
・4回目の平均取得単価 100万7500円 / ストップ 98万7500円

損切りの計算に使う平均ボラティリティ(ATR)は、追加ポジションの取得のたびに再計算するのではなく、ブレイクアウト時に計算したものを使い回します。そのため、上記の[add-position]には、ストップ幅を記録する変数も用意しておきます。

約定価格の問題

今回のバックテストの検証では、すべて理想通りの価格で約定したものと仮定します。

例えば、上記の例なら、1時間足の終値が101万5000円を超えた時点で、各ポジションは 100万円/100万5000円/101万円/101万5000円でそれぞれ順調に約定したと仮定して検証します。

ただし実際の運用では、リアルタイムで形成される足(または短い時間軸の足)を使って成行注文を出すことになるので、もう少し不利な価格で約定することになります。そのため、バックテストでも約定価格にスリッページを考慮できるようにしておきます。

※ 逆指値注文で運用する場合は、前回までのコードは使えず、根本からプログラムの設計を作り直す必要があります。その方法は、この記事では解説しません。

3)注文サイズを計算する関数

# 注文ロットを計算する関数
def calculate_lot( last_data,data,flag ):

	# 口座残高を取得する
	balance = flag["records"]["funds"]

	# 最初のエントリーの場合
	if flag["add-position"]["count"] == 0:
		
		# 1回の注文単位と基準レンジを計算する
		volatility = calculate_volatility( last_data )
		stop = stop_range * volatility
		calc_lot = np.floor( balance * trade_risk / stop * 100 ) / 100
		
		flag["add-position"]["unit-size"] = np.floor( calc_lot / entry_times * 100 ) / 100
		flag["add-position"]["unit-range"] = round( volatility * entry_range )
		flag["add-position"]["stop"] = stop
		
		flag["records"]["log"].append("\n現在のアカウント残高は{}円です\n".format( balance ))
		flag["records"]["log"].append("許容リスクから購入できる枚数は最大{}BTCまでです\n".format( calc_lot ))
		flag["records"]["log"].append("{0}回に分けて{1}BTCずつ注文します\n".format( entry_times, flag["add-position"]["unit-size"] ))
		
	# 2回目以降のエントリーの場合
	else:
		balance = round( balance - flag["position"]["price"] * flag["position"]["lot"] / levarage )
	
	# ストップ幅には、最初のエントリー時に計算したボラティリティを使う
	stop = flag["add-position"]["stop"]
	
	# 実際に購入可能な枚数を計算する
	able_lot = np.floor( balance * levarage / data["close_price"] * 100 ) / 100
	lot = min(able_lot, flag["add-position"]["unit-size"])
	
	flag["records"]["log"].append("証拠金から購入できる枚数は最大{}BTCまでです\n".format( able_lot ))
	return lot,stop,flag

まずは注文サイズを計算する関数です。
前回の記事で作成したコードを、複数回の分割エントリーに対応できるよう修正しました。

3行目

ブレイクアウトの判定がされた場合、実際にエントリー注文を出す前に必ずこの「注文ロットを計算する関数」が呼ばれます。そのため、ここで最初に基準となる平均ボラティリティ(ATR)を1度だけ計算します。

さらに先ほど準備したflag変数に「理論上の注文サイズ」「基準レンジ」「ストップ幅」をまとめてセットします。最初のエントリーかどうかは、flag[add-position][count]で判定します。

13行目

# 2回目以降のエントリーの場合
	else:
		balance = round( balance - flag["position"]["price"] * flag["position"]["lot"] / levarage )

2回目以降のエントリーの場合は、注文に利用可能な証拠金の額を再計算しなければなりません。

注文に利用可能な証拠金とは、現在の証拠金残高から、すでに持っているポジションの必要証拠金を引いた金額です。この数字を balance に上書きしてから、「実際に購入可能なサイズ(able_lot)」を計算します。

あとは前回と同じように、「理論上の注文サイズ(unit-size)」と「実際に購入可能なサイズ(able_lot)」を比較して、より小さい方を返します。

4)複数回に分けてポジションを取る関数


# 複数回に分けて追加ポジションを取る関数
def add_position( data,flag ):
	
	# ポジションがない場合は何もしない
	if flag["position"]["exist"] == False:
		return flag
	
	
	# 最初(1回目)のエントリー価格を記録
	if flag["add-position"]["count"] == 0:
		flag["add-position"]["first-entry-price"] = flag["position"]["price"]
		flag["add-position"]["last-entry-price"] = flag["position"]["price"]
		flag["add-position"]["count"] += 1
	
	while True:
		
		# 以下の場合は、追加ポジションを取らない
		if flag["add-position"]["count"] >= entry_times:
			return flag
		
		# この関数の中で使う変数を用意
		first_entry_price = flag["add-position"]["first-entry-price"]
		last_entry_price = flag["add-position"]["last-entry-price"]
		unit_range = flag["add-position"]["unit-range"]
		current_price = data["close_price"]
		
		
		# 価格がエントリー方向に基準レンジ分だけ進んだか判定する
		should_add_position = False
		if flag["position"]["side"] == "BUY" and (current_price - last_entry_price) > unit_range:
			should_add_position = True
		elif flag["position"]["side"] == "SELL" and (last_entry_price - current_price) > unit_range:
			should_add_position = True
		else:
			break
		
		# 基準レンジ分進んでいれば追加注文を出す
		if should_add_position == True:
			flag["records"]["log"].append("\n前回のエントリー価格{0}円からブレイクアウトの方向に{1}ATR({2}円)以上動きました\n".format( last_entry_price, entry_range, round( unit_range ) ))
			flag["records"]["log"].append("{0}/{1}回目の追加注文を出します\n".format(flag["add-position"]["count"] + 1, entry_times))
			
			# 注文サイズを計算
			lot,stop,flag = calculate_lot( last_data,data,flag )
			if lot < 0.01:
				flag["records"]["log"].append("注文可能枚数{}が、最低注文単位に満たなかったので注文を見送ります\n".format(lot))
				flag["add-position"]["count"] += 1
				return flag
			
			# 追加注文を出す
			if flag["position"]["side"] == "BUY":
				entry_price = first_entry_price + (flag["add-position"]["count"] * unit_range) 	# バックテスト用
				#entry_price = round((1 + slippage) * entry_price)
				
				flag["records"]["log"].append("現在のポジションに追加して、{0}円で{1}BTCの買い注文を出します\n".format(entry_price,lot))
				
				# ここに買い注文のコードを入れる
				
				
			if flag["position"]["side"] == "SELL":
				entry_price = first_entry_price - (flag["add-position"]["count"] * unit_range) 	# バックテスト用
				#entry_price = round((1 - slippage) * entry_price)
				
				flag["records"]["log"].append("現在のポジションに追加して、{0}円で{1}BTCの売り注文を出します\n".format(entry_price,lot))
				
				# ここに売り注文のコードを入れる
				
				
			# ポジション全体の情報を更新する
			flag["position"]["stop"] = stop
			flag["position"]["price"] = int(round(( flag["position"]["price"] * flag["position"]["lot"] + entry_price * lot ) / ( flag["position"]["lot"] + lot )))
			flag["position"]["lot"] = np.round( (flag["position"]["lot"] + lot) * 100 ) / 100

			if flag["position"]["side"] == "BUY":
				flag["records"]["log"].append("{0}円の位置にストップを更新します\n".format(flag["position"]["price"] - stop))
			elif flag["position"]["side"] == "SELL":
				flag["records"]["log"].append("{0}円の位置にストップを更新します\n".format(flag["position"]["price"] + stop))

			flag["records"]["log"].append("現在のポジションの取得単価は{}円です\n".format(flag["position"]["price"]))
			flag["records"]["log"].append("現在のポジションサイズは{}BTCです\n\n".format(flag["position"]["lot"]))
			
			flag["add-position"]["count"] += 1
			flag["add-position"]["last-entry-price"] = entry_price
	
	return flag

上記のコードの判定方法のロジックは以下です。

判定ロジック

1)前回のエントリー価格を last_entry_price に入れる
2)現在の終値を取得して current_price に入れる
3)両者の差が基準レンジ(unit_range)以上離れていれば、追加ポジションを取る
4)平均建値と合計サイズを計算して、flag["position"] を更新する
5)追加ポジションの執行価格を last_entry_priceに入れる

この1)~5)までをWhile文でループし、3)の条件を満たさなくなったら、break してWhile文を抜けます。以下で具体例を見てみましょう。

例)
・最初のエントリー価格 100万円
・基準レンジ 5000円
・前回(2回目)のエントリー価格 100万5000円
・今回の終値 101万2000円

⇒ 101万円で3回目のポジションを追加取得したと仮定する

例2)
・最初のエントリー価格 100万円
・基準レンジ 5000円
・前回(2回目)のエントリー価格 100万5000円
・今回の終値 102万5000円

⇒ 101万円で3回目、101万5000円で4回目のポジションを追加取得したと仮定する

should_add_position という変数を作って、3)の箇所の条件の判定をしています。また例2)のように、1期間足の間に2回以上の買い増しをしている可能性もあるので、全体のロジックをWhile文で囲っています。

※ 実践でBOTを運用する際には、While文で囲む必要はありません。

4-1)エントリー価格

ではもう少し詳しく分解してみてみましょう。
エントリー価格を計算して注文を出す箇所のコードを抜粋してみます。

# 追加注文を出す
if flag["position"]["side"] == "BUY":
	entry_price = first_entry_price + (flag["add-position"]["count"] * unit_range) 
	#entry_price = round((1 + slippage) * entry_price)

	flag["records"]["log"].append("現在のポジションに追加して、{0}円で{1}BTCの買い注文を出します\n".format(entry_price,lot))
	
	# ここに買い注文のコードを入れる
	
	
if flag["position"]["side"] == "SELL":
	entry_price = first_entry_price - (flag["add-position"]["count"] * unit_range) 
	#entry_price = round((1 - slippage) * entry_price)

	flag["records"]["log"].append("現在のポジションに追加して、{0}円で{1}BTCの売り注文を出します\n".format(entry_price,lot))
	
	# ここに売り注文のコードを入れる
	

前述のように、バックテストでは理想通りの価格で約定したと仮定しています。

・理想の約定価格 = 最初のエントリー価格 + (基準レンジ × n回目のエントリー)

そのため、バックテスト用の約定価格は、以下のような計算式になっています。ただし下の式では、一応、約定価格にスリッページの影響を考慮できるようにしてあります。

	entry_price = first_entry_price + (flag["add-position"]["count"] * unit_range) 
	#entry_price = round((1 + slippage) * entry_price)

本番でBOTを運用する際には、実際の約定価格(例えば、成行注文の執行価格)を entry_price に代入すればOKです。

4-2)ポジションの平均取得価格を更新

次に、flag[position]を更新する箇所のコードを見ておきましょう。

flag[position]は、今までの記事で見てきたように、現在のポジションの価格とサイズを記録するための変数です。こちらは、複数の建玉の管理を意識しなくていいように、追加ポジションを取得するたびに「合計サイズ」と「平均建値」を計算し、その数字だけを記録するようにします。

こうしておけば、複数回に分けてエントリーする場合でも、今まで作成した手仕舞いや損切りのコードに手を加える必要がなくなります。


# ポジション全体の情報を更新する
flag["position"]["stop"] = stop
flag["position"]["price"] = int(round(( flag["position"]["price"] * flag["position"]["lot"] + entry_price * lot ) / ( flag["position"]["lot"] + lot )))
flag["position"]["lot"] = np.round( (flag["position"]["lot"] + lot) * 100 ) / 100

flag["records"]["log"].append("現在のポジションの取得単価は{}円です\n".format(flag["position"]["price"]))
flag["records"]["log"].append("現在のポジションサイズは{}BTCです\n\n".format(flag["position"]["lot"]))

flag["add-position"]["count"] += 1
flag["add-position"]["last-entry-price"] = entry_price

5)全体のメインループの箇所

最後に全体のメイン処理の部分を見ておきましょう。

いままでの全体ループでは、ポジションがある場合に「手仕舞いを判定する関数」「損切りを判定する関数」を順番に呼んでいました。この後ろに「追加ポジションを取得する関数」を付け足します。


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["position"]["exist"]:
		flag = stop_position( data,flag )
		flag = close_position( data,last_data,flag )
		flag = add_position( data,flag ) # ココに追加
	
	# ポジションがない場合
	else:
		flag = entry_signal( data,last_data,flag )
	
	last_data.append( data )
	i += 1
	time.sleep(wait)

これで今まで作成したpythonコードを「複数回に分けてのエントリー」に対応させることができました。

6)手仕舞いや損切りの箇所

最後に、ポジションを手仕舞ったり、損切りをするたびに、flag[add-position][count]をリセットするようにしておきます。これ以外に、手仕舞いの関数・損切りの関数を変更する必要はありません。

このflag[add-position][count]が唯一、今までのコードと今回新しく作成した関数を繋ぐ架け橋になる部分です。


# ポジションを手仕舞ったり損切した後

records( flag,data )
flag["position"]["exist"] = False
flag["position"]["count"] = 0
flag["add-position"]["count"] = 0 # 追記

以上でバックテスト用のコードは完成です!

実行結果

これを実行すると、以下のような感じでポジションの積み増しがシミュレーションできます。

▽ 出力ログファイル

実際のパフォーマンスは後ほど確認しましょう!

4.Bitflyer用のコード

ついでに、このコードをBitflyerFX(実践)で使えるようにする方法も見ておきましょう。今回、作成・修正した2つの関数は、成行注文を出す設計であれば、そのまま実践用のコードとして使えます。

置き換える必要があるのは、以下の2点だけです。

1)証拠金残高から購入可能サイズを計算する部分
2)約定価格を entry_price にセットする部分

1)証拠金残高の計算

証拠金残高の計算には、前回の記事と同じく「GET /v1/me/getcollateral/」というBitflyerの公式APIを使います。

Bitflyerの公式APIドキュメント

「getcollateral」というAPIは、collateral で現在の証拠金評価額、require_collateral で現在のポジション(建玉)を維持するための必要証拠金、を返します。

・collateral ... 現在の証拠金評価額
・require_collateral ... 建玉の必要証拠金

そのため、collateral から require_collateral を差し引くことで、注文に利用可能な証拠金を計算できます。


# 注文に利用可能な証拠金を取得する関数
def bitflyer_collateral():
	while True:
		try:
			collateral = bitflyer.private_get_getcollateral()
			
			# 追加注文で利用可能な証拠金を計算
			spendable_collateral = np.floor(collateral["collateral"] - collateral["require_collateral"])
			print("現在、新規注文に利用可能な証拠金の額は{}円です".format( int(spendable_collateral) ))
			return int( spendable_collateral )
			
		except ccxt.BaseError as e:
			print("BitflyerのAPIでの口座残高取得に失敗しました : ", e)
			print("20秒待機してやり直します")
			time.sleep(20)


# 注文ロットを計算する関数
def calculate_lot( last_data,data,flag ):

	# 口座残高を取得する
	balance = bitflyer_collateral()

	# 最初のエントリーの場合
	if flag["add-position"]["count"] == 0:
		
		# 1回の注文単位と基準レンジを計算する
		volatility = calculate_volatility( last_data )
		stop = stop_range * volatility
		calc_lot = np.floor( balance * trade_risk / stop * 100 ) / 100
		
		flag["add-position"]["unit-size"] = np.floor( calc_lot / entry_times * 100 ) / 100
		flag["add-position"]["unit-range"] = round( volatility * entry_range )
		flag["add-position"]["stop"] = stop
		
		flag["records"]["log"].append("\n現在のアカウント残高は{}円です\n".format( balance ))
		flag["records"]["log"].append("許容リスクから購入できる枚数は最大{}BTCまでです\n".format( calc_lot ))
		flag["records"]["log"].append("{0}回に分けて{1}BTCずつ注文します\n".format( entry_times, flag["add-position"]["unit-size"] ))
	
	# ストップ幅には、最初のエントリー時に計算したボラティリティを使う
	stop = flag["add-position"]["stop"]
	
	# 実際に購入可能な枚数を計算する
	able_lot = np.floor( balance * levarage / data["close_price"] * 100 ) / 100
	lot = min(able_lot, flag["add-position"]["unit-size"])
	
	flag["records"]["log"].append("証拠金から購入できる枚数は最大{}BTCまでです\n".format( able_lot ))
	return lot,stop,flag

2)約定価格の取得

前述のように、バックテストでは理想の価格で約定したものと仮定してその価格を計算しましたが、実践用のコードでは、成行注文を出した上で、その執行価格をAPIで取得して、それを entry_price にセットする必要があります。

・成行注文の執行価格を取得する方法

5.検証結果

それでは、ドンチャン・チャネルブレイクアウトで複数回に分けてポジションを取得した場合のパフォーマンスを検証してみましょう。今回は以下のような前提条件を使います。

売買ロジック

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

資金管理

・初期資金30万円
・レバレッジ3倍
・ストップのレンジ幅 2ATR
・1トレードで許容するリスク 4%

また追加ポジションの取得はすべて目標価格から0.1% 不利な方向に滑って約定したと仮定します。(slippage = 0.001)

1回で全ポジションを取得した場合

まずはブレイクアウトの時点で、許容額の限界まで1度にポジションを取得した場合のパフォーマンスを見てみます。


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

最大の勝ちトレード :  1277503円
最大の負けトレード :  -244647円
最大連敗回数       :  10回
最大ドローダウン   :  -1876398円 / -32.6%
利益合計           :  11429600円
損失合計           :  -6537527円
最終損益           :  4892073円

初期資金           :  300000円
最終資金           :  5192073円
運用成績           :  1731.0%

約9カ月で17倍(1731%)の運用成績ですから、かなり良い成績です。
しかし口座の4%のリスクを取るだけでも、ドローダウンは30%以上とかなり大きくなりました。許容リスクの割合を維持したまま、もう少しドローダウンを小さくできないでしょうか?

2回に分けてポジションを取得した場合

では次に、2回に分けてポジションを取得する場合を考えてみましょう。ブレイクアウトの時点では半分だけポジションを取り、その後、ブレイクアウトの方向に1レンジ進んだらもう半分のポジションを取ります。

-設定値
entry_times = 2
entry_range = 1


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

最大の勝ちトレード :  1336007円
最大の負けトレード :  -245197円
最大連敗回数       :  10回
最大ドローダウン   :  -1497132円 / -26.0%
利益合計           :  10776540円
損失合計           :  -5001125円
最終損益           :  5775415円

初期資金           :  300000円
最終資金           :  6075415円
運用成績           :  2025.0%

約9カ月の運用成績は20倍(2025%)と、先程よりパフォーマンスが向上しています。さらに最大ドローダウンも小さくなったため、資産グラフも先ほどより安定しています。

一方、平均リターンや勝率はさきほどより悪くなっているようです。

4回に分けてポジションを取得した場合

では4回に分けてポジションを取得する場合を見てみましょう。
ブレイクアウトの時点では許容リスクの1/4を上限にポジションを取り、その後、ブレイクアウトの方向に1/2レンジ進むたびに1/4ずつポジションを追加した場合です。

-設定値
entry_times = 4
entry_range = 0.5


-----------------------------------
総合の成績
-----------------------------------
全トレード数       :  114回
勝率               :  30.7%
平均リターン       :  0.86%
平均保有期間       :  43.2足分
損切りの回数       :  64回

最大の勝ちトレード :  1435541円
最大の負けトレード :  -254950円
最大連敗回数       :  10回
最大ドローダウン   :  -1143648円 / -19.7%
利益合計           :  10508068円
損失合計           :  -4105824円
最終損益           :  6402244円

初期資金           :  300000円
最終資金           :  6702244円
運用成績           :  2234.0%


4回に分けてエントリーすると、勝率・平均リターンはともに最低となりましたが、一方で運用成績は2234%となり今までで一番いい結果となりました! また最大ドローダウンも-19.7%と今までで最小となっています。

考察

チャネルブレイクアウトのようなトレンドフォロー戦略の場合、複数回に分けてポジションを取ると、その分だけ平均建値の面では不利になります。勝率や平均リターンなどの指標が悪化しているのは、おそらくそのためです。

平均建値が不利になると、許容リスクとの関係から損切りにかかる可能性も高くなります。4回に分割してエントリーするパターンでは、全トレード114回のうち63回が損切りに掛かっています。

一方で、ブレイクアウトに失敗した場合の損失は(サイズを抑えているため)比較的小さく済みます。つまりリターンの良いときに大きいポジションを持ち、リターンの悪いときに小さいポジションを持っているため、全体としてみれば、パフォーマンスが向上し、かつドローダウンを小さく抑えることができています。

この傾向を他の条件でも確認するために、検証期間を変えた場合、口座の許容リスクを5%にした場合、異なる時間軸(2時間足)を使用した場合、などもテストしてみましょう。

その他の条件テスト(1)

・1時間足を使用(2018/1~2018/5)
・口座の許容リスク3%

分割回数 1回 2回 4回
勝率 35.5% 32.3% 30.2%
平均リターン 1.41% 0.92% 0.65%
最大DD -25.7% -19.9% -15.0%
利益合計 1115589円 1071247円 1141549円
損失合計 -655812円 -495360円 -430326円
最終資金 759777円 875887円 1011223円
運用成績 253% 292% 337%

その他の条件テスト(2)

・1時間足を使用(2017/8~2018/5)
・口座の許容リスク5%

分割回数 1回 2回 4回
勝率 39.1% 34.2% 30.7%
平均リターン 1.8% 1.21% 0.87%
最大DD -37.8% -31.4% -24.1%
利益合計 18070662円 17389476円 17210344円
損失合計 -11052685円 -8769789円 -7316265円
最終資金 7317977円 8919687円 10194079円
運用成績 2439% 2973% 3398%

その他の条件テスト(3)

・2時間足を使用(2017/3~2018/5)
・口座の許容リスク3%

分割回数 1回 2回 4回
勝率 34.0% 33.7% 31.7%
平均リターン 2.22% 1.71% 1.16%
最大DD -22.4% -18.4% -15.2%
利益合計 6671857円 6773418円 7252069円
損失合計 -3813601円 -2969508円 -2858596円
最終資金 3158256円 4103910円 4693473円
運用成績 1053% 1368% 1564%

このように、どの期間や時間軸、口座のリスク率を用いてテストしても、分割エントリーをした方が、(勝率や平均リターンは下がるものの)運用成績が向上するという傾向が見られました。

やはり「タートル流」の積み増しエントリーはBTCFXのチャネルブレイクアウトでも機能するようです。

6.今回の勉強で使用したコード




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


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

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             # 何レンジ幅にストップを入れるか
trade_risk = 0.02          # 1トレードあたり口座の何%まで損失を許容するか
levarage = 3               # レバレッジ倍率の設定
start_funds = 300000       # シミュレーション時の初期資金

entry_times = 1            # 何回に分けて追加ポジションを取るか
entry_range = 0.5          # 何レンジごとに追加ポジションを取るか

wait = 0                   #  ループの待機時間
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:
		flag["records"]["log"].append("データが存在しません")
		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)
	flag["records"]["log"].append("現在の{0}期間の平均ボラティリティは{1}円です\n".format( volatility_term, volatility ))
	return volatility



#-------------資金管理の関数--------------


# 注文ロットを計算する関数
def calculate_lot( last_data,data,flag ):

	# 口座残高を取得する
	balance = flag["records"]["funds"]

	# 最初のエントリーの場合
	if flag["add-position"]["count"] == 0:
		
		# 1回の注文単位(ロット数)と、追加ポジの基準レンジを計算する
		volatility = calculate_volatility( last_data )
		stop = stop_range * volatility
		calc_lot = np.floor( balance * trade_risk / stop * 100 ) / 100
		
		flag["add-position"]["unit-size"] = np.floor( calc_lot / entry_times * 100 ) / 100
		flag["add-position"]["unit-range"] = round( volatility * entry_range )
		flag["add-position"]["stop"] = stop
		
		flag["records"]["log"].append("\n現在のアカウント残高は{}円です\n".format( balance ))
		flag["records"]["log"].append("許容リスクから購入できる枚数は最大{}BTCまでです\n".format( calc_lot ))
		flag["records"]["log"].append("{0}回に分けて{1}BTCずつ注文します\n".format( entry_times, flag["add-position"]["unit-size"] ))
		
	# 2回目以降のエントリーの場合
	else:
		balance = round( balance - flag["position"]["price"] * flag["position"]["lot"] / levarage )
	
	# ストップ幅には、最初のエントリー時に計算したボラティリティを使う
	stop = flag["add-position"]["stop"]
	
	# 実際に購入可能な枚数を計算する
	able_lot = np.floor( balance * levarage / data["close_price"] * 100 ) / 100
	lot = min(able_lot, flag["add-position"]["unit-size"])
	
	flag["records"]["log"].append("証拠金から購入できる枚数は最大{}BTCまでです\n".format( able_lot ))
	return lot,stop,flag



# 複数回に分けて追加ポジションを取る関数
def add_position( data,flag ):
	
	# ポジションがない場合は何もしない
	if flag["position"]["exist"] == False:
		return flag
	
	# 最初(1回目)のエントリー価格を記録
	if flag["add-position"]["count"] == 0:
		flag["add-position"]["first-entry-price"] = flag["position"]["price"]
		flag["add-position"]["last-entry-price"] = flag["position"]["price"]
		flag["add-position"]["count"] += 1
	
	while True:
		
		# 以下の場合は、追加ポジションを取らない
		if flag["add-position"]["count"] >= entry_times:
			return flag
		
		# この関数の中で使う変数を用意
		first_entry_price = flag["add-position"]["first-entry-price"]
		last_entry_price = flag["add-position"]["last-entry-price"]
		unit_range = flag["add-position"]["unit-range"]
		current_price = data["close_price"]
		
		
		# 価格がエントリー方向に基準レンジ分だけ進んだか判定する
		should_add_position = False
		if flag["position"]["side"] == "BUY" and (current_price - last_entry_price) > unit_range:
			should_add_position = True
		elif flag["position"]["side"] == "SELL" and (last_entry_price - current_price) > unit_range:
			should_add_position = True
		else:
			break
		
		# 基準レンジ分進んでいれば追加注文を出す
		if should_add_position == True:
			flag["records"]["log"].append("\n前回のエントリー価格{0}円からブレイクアウトの方向に{1}ATR({2}円)以上動きました\n".format( last_entry_price, entry_range, round( unit_range ) ))
			flag["records"]["log"].append("{0}/{1}回目の追加注文を出します\n".format(flag["add-position"]["count"] + 1, entry_times))
			
			# 注文サイズを計算
			lot,stop,flag = calculate_lot( last_data,data,flag )
			if lot < 0.01:
				flag["records"]["log"].append("注文可能枚数{}が、最低注文単位に満たなかったので注文を見送ります\n".format(lot))
				return flag
			
			# 追加注文を出す
			if flag["position"]["side"] == "BUY":
				entry_price = first_entry_price + (flag["add-position"]["count"] * unit_range)
				#entry_price = round((1 + slippage) * entry_price)
				
				flag["records"]["log"].append("現在のポジションに追加して、{0}円で{1}BTCの買い注文を出します\n".format(entry_price,lot))
				
				# ここに買い注文のコードを入れる
				
				
			if flag["position"]["side"] == "SELL":
				entry_price = first_entry_price - (flag["add-position"]["count"] * unit_range)
				#entry_price = round((1 - slippage) * entry_price)
				
				flag["records"]["log"].append("現在のポジションに追加して、{0}円で{1}BTCの売り注文を出します\n".format(entry_price,lot))
				
				# ここに売り注文のコードを入れる
				
				
			# ポジション全体の情報を更新する
			flag["position"]["stop"] = stop
			flag["position"]["price"] = int(round(( flag["position"]["price"] * flag["position"]["lot"] + entry_price * lot ) / ( flag["position"]["lot"] + lot )))
			flag["position"]["lot"] = np.round( (flag["position"]["lot"] + lot) * 100 ) / 100

			if flag["position"]["side"] == "BUY":
				flag["records"]["log"].append("{0}円の位置にストップを更新します\n".format(flag["position"]["price"] - stop))
			elif flag["position"]["side"] == "SELL":
				flag["records"]["log"].append("{0}円の位置にストップを更新します\n".format(flag["position"]["price"] + stop))

			flag["records"]["log"].append("現在のポジションの取得単価は{}円です\n".format(flag["position"]["price"]))
			flag["records"]["log"].append("現在のポジションサイズは{}BTCです\n\n".format(flag["position"]["lot"]))
			
			flag["add-position"]["count"] += 1
			flag["add-position"]["last-entry-price"] = entry_price
	
	return flag



#-------------売買ロジックの部分の関数--------------

# ドンチャンブレイクを判定する関数
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"]]))
		
		lot,stop,flag = calculate_lot( last_data,data,flag )
		if lot > 0.01:
			flag["records"]["log"].append("{0}円で{1}BTCの買い注文を出します\n".format(data["close_price"],lot))
			
			# ここに買い注文のコードを入れる
			
			flag["records"]["log"].append("{0}円にストップを入れます\n".format(data["close_price"] - stop))
			flag["position"]["lot"],flag["position"]["stop"] = lot,stop
			flag["position"]["exist"] = True
			flag["position"]["side"] = "BUY"
			flag["position"]["price"] = data["close_price"]
		else:
			flag["records"]["log"].append("注文可能枚数{}が、最低注文単位に満たなかったので注文を見送ります\n".format(lot))

	if signal["side"] == "SELL":
		flag["records"]["log"].append("過去{0}足の最安値{1}円を、直近の価格が{2}円でブレイクしました\n".format(sell_term,signal["price"],data[judge_price["SELL"]]))
		
		lot,stop,flag = calculate_lot( last_data,data,flag )
		if lot > 0.01:
			flag["records"]["log"].append("{0}円で{1}BTCの売り注文を出します\n".format(data["close_price"],lot))
			
			# ここに売り注文のコードを入れる
			
			flag["records"]["log"].append("{0}円にストップを入れます\n".format(data["close_price"] + stop))
			flag["position"]["lot"],flag["position"]["stop"] = lot,stop
			flag["position"]["exist"] = True
			flag["position"]["side"] = "SELL"
			flag["position"]["price"] = data["close_price"]
		else:
			flag["records"]["log"].append("注文可能枚数{}が、最低注文単位に満たなかったので注文を見送ります\n".format(lot))

	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["add-position"]["count"] = 0
			
			
			lot,stop,flag = calculate_lot( last_data,data,flag )
			if lot > 0.01:
				flag["records"]["log"].append("\n{0}円で{1}BTCの売りの注文を入れてドテンします\n".format(data["close_price"],lot))
				
				# ここに売り注文のコードを入れる
				
				flag["records"]["log"].append("{0}円にストップを入れます\n".format(data["close_price"] + stop))
				flag["position"]["lot"],flag["position"]["stop"] = lot,stop
				flag["position"]["exist"] = True
				flag["position"]["side"] = "SELL"
				flag["position"]["price"] = data["close_price"]
			


	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["add-position"]["count"] = 0
			
			
			lot,stop,flag = calculate_lot( last_data,data,flag )
			if lot > 0.01:
				flag["records"]["log"].append("\n{0}円で{1}BTCの買いの注文を入れてドテンします\n".format(data["close_price"],lot))
				
				# ここに買い注文のコードを入れる
				
				flag["records"]["log"].append("{0}円にストップを入れます\n".format(data["close_price"] - stop))
				flag["position"]["lot"],flag["position"]["stop"] = lot,stop
				flag["position"]["exist"] = True
				flag["position"]["side"] = "BUY"
				flag["position"]["price"] = data["close_price"]
			
	return flag



# 損切ラインにかかったら成行注文で決済する関数
def stop_position( data,flag ):
	
	if flag["position"]["side"] == "BUY":
		stop_price = flag["position"]["price"] - flag["position"]["stop"]
		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
			flag["add-position"]["count"] = 0
	
	
	if flag["position"]["side"] == "SELL":
		stop_price = flag["position"]["price"] + flag["position"]["stop"]
		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
			flag["add-position"]["count"] = 0
			
	return flag


#------------バックテストの部分の関数--------------


# 各トレードのパフォーマンスを記録する関数
def records(flag,data,close_price,close_type=None):
	
	# 取引手数料等の計算
	entry_price = int(round(flag["position"]["price"] * flag["position"]["lot"]))
	exit_price = int(round(close_price * flag["position"]["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 ))
		flag["records"]["funds"] = flag["records"]["funds"] + buy_profit
		if buy_profit  > 0:
			log = str(buy_profit) + "円の利益です\n\n"
			flag["records"]["log"].append(log)
		else:
			log = str(buy_profit) + "円の損失です\n\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 ))
		flag["records"]["funds"] = flag["records"]["funds"] + sell_profit
		if sell_profit > 0:
			log = str(sell_profit) + "円の利益です\n\n"
			flag["records"]["log"].append(log)
		else:
			log = str(sell_profit) + "円の損失です\n\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["Funds"] = records.Gross + start_funds
	
	# 最大ドローダウンの列を追加する
	records["Drawdown"] = records.Funds.cummax().subtract(records.Funds)
	records["DrawdownRate"] = round(records.Drawdown / records.Funds.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(),
		"Funds"    :  grouped.Funds.last(),
		"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("最終損益           :  {}円".format( records.Profit.sum() ))
	print("")
	print("初期資金           :  {}円".format( start_funds ))
	print("最終資金           :  {}円".format( records.Funds.iloc[-1] )) 
	print("運用成績           :  {}%".format( round(records.Funds.iloc[-1] / start_funds * 100),2 )) 
	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) ))
		print("月末資金           :  {}円".format( row.Funds.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.Funds )
	plt.xlabel("Date")
	plt.ylabel("Balance")
	plt.xticks(rotation=50) # X軸の目盛りを50度回転
	
	plt.show()
	

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

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

flag = {
	"position":{
		"exist" : False,
		"side" : "",
		"price": 0,
		"stop":0,
		"ATR"  : 0,
		"lot":0,
		"count":0
	},
	"add-position":{
		"count":0,
		"first-entry-price":0,
		"last-entry-price":0,
		"unit-range":0,
		"unit-size":0,
		"stop":0
	},
	"records":{
		"date":[],
		"profit":[],
		"return":[],
		"side":[],
		"stop-count":[],
		"funds" : start_funds,
		"holding-periods":[],
		"slippage":[],
		"log":[]
	}
}


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

	# ドンチャンの判定に使う期間分の安値・高値データを準備する
	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["position"]["exist"]:
		flag = stop_position( data,flag )
		flag = close_position( data,last_data,flag )
		flag = add_position( 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)

「1回のトレードの許容資金を使って分割エントリー(増し玉)してみよう!」への6件のフィードバック

  1. Ryotaさん、素晴らしいブログですね!
    食い入るように読ませてもらってます。
    実戦用にアップデートするのに苦戦してますが、ひとまずお礼まで。

    1. コメントありがとうございます!
      バックテストと実践用のコードを行ったり来たりするの頭使いますよね…、
      私もたまに間違えて上書きしてBOT止まったりしますw
      一緒にがんばりましょう^^

  2. 素晴らしいブログありがとうございます!
    このブログのおかげで、もの凄く勉強になっています!

    一つ質問なのですが、増し玉が理想価格で約定している個所について、ここはdata[“close_price”]では問題があったのでしょうか?

    特に、エントリレンジを小さくすればするほど、現実とはかけはなれた理想価格で約定してしまい、簡単に結果が向上するので、
    終値の方がより現実の約定に近いのかなと思いました。

    自分はFXでのバックテストになりますが、data[“close_price”]に変えることで、結果に大きく変化が見られたので、質問させていただきます。

    長々と申し訳ありません。

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

      今回のテストは1時間足のデータを使ったバックテストなので、例えば、最初のエントリーから15分後や80分後に2回目、3回目の増し玉をする場合をシミュレーションできません。 実際の取引BOTでは、毎分のようにAPIで価格を取得してポジションの積み増しの判定をするので、次の1時間足の終値を約定価格として想定するのは、あまり現実的なシミュレーションではないと思います。

      ただ仰るように、1分足のレベルでは注文が遅延したり不利な価格で約定する可能性は十分あります。 そのため、本記事の記載にもあるように2回目以降のエントリーにもスリッページの影響を組み込むことは可能です(コメントアウトしてある箇所です)。不利な価格で約定することを仮定する場合は、この slipppage の変数をかなり高めに設定すればいいと思います!

      1. 返信有難うございます!

        なるほど理解しました!
        ご丁寧な返答助かりました。ありがとうございます!

  3. 素晴らしい内容をありがとうございます。大変参考にさせていただいております。
    1点ご質問があります。
    前半の”約定価格の問題”の箇所で、「ただし実際の運用では、リアルタイムで形成される足(または短い時間軸の足)を使って成行注文を出すことになる」と書かれております。
    私の理解では、実際の運用でも1時間足を使う場合は、1時間の足が確定したときに売買等の決定を行うものと考えておりましたが、分割エントリーの判断にのみ実際には短い足を使うのということでしょうか?それともその他の売買含めて、1時間足とリアルタイムの短い時間足を同時に見ながら判断を行うということでしょうか?
    なにか私の根本的なところの理解が足りていないような気がしているのですが、教えていただけると助かります。よろしくお願い致します。

コメントを残す

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