BitflyerでBTCFXの単純移動平均線と指数移動平均線をpythonで実装しよう!

この記事では、Bitflyerの価格データからBTCFXの単純移動平均線(MA)と、指数平滑移動平均線(EMA)の作り方を勉強します!移動平均線の説明はいらないと思うので早速本題に入りましょう!

なお、今回は「移動平均線をグラフで描画する」といったあまり実践的でないことはしません。グラフはチャートで見れますからね。BOTの売買ルールで使うことを想定して「指定した足の移動平均の数値を返す」だけの関数を作りましょう!

Bitflyerから価格データを取得する

BTCFXの価格データはCryptowatchを使って取得します。
CryptowatchのAPIの使い方や仕様については以下の記事を読んでください。

Bitflyerのローソク足の情報をpythonで取得する
バックテストに必要なローソク足データを集める

▽ BitflyerFXのローソク足データを取得する関数


import requests
from datetime import datetime

# 使用する時間足
chart_sec = 3600

# CryptowatchでBTCFXの価格データを取得
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)]:
			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

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


特に引数を設定しなければ、直近の500件のローソク足データがセットされます。移動平均線を作るにはこれで十分なので、500件で進めます。

単純移動平均線の値を返す関数

単純移動平均線は、例えば、10期間の移動平均(10MA)であれば、直近の10足の終値の平均値を取るだけです。非常にシンプルで使いやすく、変な加工がされていないので統計的にも最も信頼できる平均値です。

Pythonコード


# 移動平均線の値を返す関数
def calculate_MA( value ):
	MA = sum(i["close_price"] for i in price[-1*value:]) / value
	return round(MA)

単純移動平均の関数は上記のように3行で書けます。
特別なライブラリも何も必要ありません。

では、計算結果を確認しておきましょう!


#---- ここからメインの実行処理----

price = get_price(chart_sec)
MA10 = calculate_MA(10)     # 現在の10期間移動平均の数値
MA21 = calculate_MA(21)     # 現在の21期間移動平均の数値
MA100 = calculate_MA(100)   # 現在の100期間移動平均の数値
print( MA10,MA21,MA100 )


最初のCrytowatchの関数と、さきほどの移動平均を計算する関数をまるまるコピペして、その下に上記のコードを加えて実行してみてください。そしてBitflyerFXの画面のSMA(単純移動平均線)と比較してみましょう。同じ数値になっているはずです。

実行結果

▽ チャート画面との一致を確認しよう

なお、一番新しいまだリアルタイムで変動中の足の移動平均を計算しているので、計算結果は実行するたびに変わります。

コードの解説

例えば、10期間の移動平均線の場合、pythonではprice[-10:] と記載することで直近の10期間の価格データを取りだすことができます(配列のスライスというテクニックです)。なので、for文で10期間の終値を合計してそれを10で割れば、単純移動平均を計算できます。

ここでは自由に期間を変更できるようにしたいので、例としてあげた数値の10を value という変数に置き換えて、price[-1*value:] としています。

さらに実践的な単純移動平均の関数

例えば、エントリー条件として「X期間の移動平均線がn日前よりも上がっている場合」などのフィルターをかけることがあります。そのため、現在の移動平均線の値だけでなく、n足前の移動平均線の値も取得できるようにしましょう。

以下のように変更するだけです。


# 単純移動平均を計算する関数
def calculate_MA( value,before=None ):
	if before is not None:
		MA = sum(i["close_price"] for i in price[-1*value + before: before]) / value
	else:
		MA = sum(i["close_price"] for i in price[-1*value:]) / value
	return round(MA)

何も引数を与えなければ、先ほどと同じ直近(まだ形成中)の足の移動平均線の数値を返します。「-10」などの数値を与えると、10期間前の移動平均線の値を返します。

ではやってみましょう!


#---- ここからメインの実行処理----

price = get_price(chart_sec)    # 価格データを取得
MA10 = calculate_MA(10,-10)     # 10期間前の10MAの数値
MA21 = calculate_MA(21,-10)     # 10期間前の21MAの数値
MA100 = calculate_MA(100,-10)   # 10期間前の100MAの数値
print( MA10,MA21,MA100 )

実行結果

さきほどと同じようにチャートでも確認しておきましょう。
10期間前というのは、リアルタイムで動いている足から遡って10期間なので、以下のことです。

少し目盛り幅がわかりにくいですが、ちゃんと計算できてるのがわかりますね。移動平均線の基本的な使い方(2本のクロス判定やMACDの計算など)は、全てこの関数をベースにできますので、ひとまずこれで完成でいいと思います。

では次に指数平滑移動平均線を計算してみましょう。

指数平滑移動平均線の値を返す関数

ご存知だと思いますが、指数平滑移動平均線は直近の価格データにより大きいウエイトを置いた移動平均線です。昔の価格が計算期間から外れても、その影響が出ないように特別に加工された平均値の計算方法です。

よりシンプルな売買ロジックを好む人は、単純移動平均を使うことが多い気がしますが、指数移動平均もかなり人気があると思うので一応、解説しておきましょう!

EMAの計算式

指数平滑移動平均(EMA)の計算は、以下の式になります。

こういうのは具体的な数字を入れた方がわかりやすいと思うので、10期間のEMAの計算式を見てみましょう!

これは何を計算しているのかというと、要するに直近の価格だけを2倍して2回分のデータとして扱って、平均値を取っているわけですね。だから分母が(10+1)になり、終値のところの分子だけ(2)になっています。

なお、計算式の中に「前回のEMA」が入っていますが、これは初回だけは単純移動平均(MA)の値を使います。

EMAは計算期間によって数値が変わる

最初にEMAの性質として理解して欲しいところですが、EMAは単純移動平均と違って完全に1つの数値に定まるわけではありません。

上記の計算式を見ればわかりますが、指数平滑移動平均(EMA)は、過去のEMAをずっと参照し続けています。そのため、古い価格データの割合はどんどん小さくなるものの、完全に無くなるわけではありません。

そのため、どこから計算を開始するかによって同じ期間のEMAでも若干値が変わります。例えば、同じ30日間の指数移動平均でも「今日から計算を始めた人」と「数カ月前から計算し続けている人」では数値が一致しません。

Pythonコード

では先ほどと同様、まずは直近の足の指数平滑移動平均(EMA)を計算するコードを書いてみましょう。


# 指数移動平均を計算する関数
def calculate_EMA( value ):
	MA = sum(i["close_price"] for i in price[-2*value: -1*value]) / value
	EMA = (price[-1*value]["close_price"] * 2 / (value+1)) + (MA * (value-1) / (value+1))
	for i in range(value-1):
		EMA = (price[-1*value+1+i]["close_price"] * 2 /(value+1)) + (EMA * (value-1) / (value+1))
	return round(EMA)

コードの解説

最初の2行では、「初回のEMA」の値を計算しています。 例えば、20期間EMAであれば、最初に過去の20期間の単純移動平均(MA)を計算し、そこに次の足の終値を加重して初回のEMA(指数平滑移動平均)を計算します。

これで完成でも別に構わないのですが、せっかくわざわざ指数平滑移動平均を使うわけですから、直近の価格データに重みづけされた平均値が欲しいはずです。そのため、さらに20期間多く遡って、直近の20期間のデータを使ってEMAを20回計算しています。

(例)20期間EMAの場合

・直近40期間のデータを使う
・一番古い20期間で単純移動平均(MA)を作り、EMAを計算する
・残りの20期間を使って指数平滑移動平均を20回計算する

(例)50期間EMAの場合

・直近100期間のデータを使う
・一番古い50期間で単純移動平均(MA)を作り、EMAを計算する
・残りの50期間を使って指数移動平均を50回計算する

これでどの時点からEMAの計算を開始しても、長い期間計測している人と同じくらい、しっかり直近の価格に重みづけされた平均値になっているはずです。

実行してみよう!

では先ほどと同様、テストしてみましょう。


#---- ここからメインの実行処理----

price = get_price(chart_sec)
EMA10 = calculate_EMA(10)
EMA21 = calculate_EMA(21)
EMA100 = calculate_EMA(100)
print( EMA10,EMA21,EMA100 )


実行結果

今回も実際のBitflyerのチャートに照らし合わせて確認してみましょう。

実用上問題のない程度にしっかり一致していますね! 何らかの判定に使う際でも、このくらいの精度なら問題ないでしょう。

より実践的な指数平滑移動平均の関数

では先ほどと全く同じように、過去の期間を指定してその時点のEMAの数値を取得できるように修正してみましょう。関数の中の数式のすべての期間を指定分ズラすだけなので、それほど難しくありません。


# 指数移動平均を計算する関数
def calculate_EMA( value,before=None ):
	if before is not None:
		MA = sum(i["close_price"] for i in price[-2*value + before : -1*value + before]) / value
		EMA = (price[-1*value + before]["close_price"] * 2 / (value+1)) + (MA * (value-1) / (value+1))
		for i in range(value-1):
			EMA = (price[-1*value+before+1 + i]["close_price"] * 2 /(value+1)) + (EMA * (value-1) / (value+1))
	else:
		MA = sum(i["close_price"] for i in price[-2*value: -1*value]) / value
		EMA = (price[-1*value]["close_price"] * 2 / (value+1)) + (MA * (value-1) / (value+1))
		for i in range(value-1):
			EMA = (price[-1*value+1 + i]["close_price"] * 2 /(value+1)) + (EMA * (value-1) / (value+1))
	return round(EMA)

先ほどと同様、何も引数を与えなければ直近の(リアルタイムで変動中の)足の指数移動平均の数値を返し、「-10」などの期間を与えると、10足前の時点での指数移動平均の数値を返します。

では、実際にこれでBitflyerFXの20期間足前の指数移動平均を確認してみましょう!


#---- ここからメインの実行処理----

price = get_price(chart_sec)     # 価格データを取得
EMA10 = calculate_EMA(10,-20)    # 20期間前の10EMAの数値を計算
EMA21 = calculate_EMA(21,-20)    # 20期間前の21EMAの数値を計算
EMA100 = calculate_EMA(100,-20)  # 20期間前の100EMAの数値を計算
print( EMA10,EMA21,EMA100 )

実行結果

20期間前の足というとこれですね。また少し目盛りの単位がわかりにくいですが、しっかり一致しています。

この関数だと、どの時点からでも正確なEMAを計算できますし、遡って過去の時点のEMAを計算することもできます。ただしそれなりにローソク足データが必要な点に注意してください。

例えば、100期間の指数移動平均(EMA)となると、直近の足でも計算に200期間のデータが必要です。さらに100足前の時点のEMAを計算するとなると、合計300期間遡ることになるため、最低300期間分のデータが必要です。

BOTで運用するとき

なお実際にBOTを運用するときは、コード内の変数に前回のEMAの値を記録しておいて、While文で新しいローソク足を処理するたびにEMAの値を更新することが一般的だと思います。その場合は、最初の起動時に1回だけ、上記の関数を使ってEMAを計算することになります。

ループのたびに過去の単純移動平均(MA)や指数平滑移動平均(EMA)を配列に記録しておくスタイルであれば、上記で解説した「過去の足のMAやEMAを取得する箇所」のコードは必要ありません。リアルタイムの足で計算したものを全部保存しておけばいいだけだからです。

BitflyerのEMAを実践で使う場合の注意点

先ほども述べたように、指数移動平滑平均(EMA)は「誰が計算しても1つの値に定まる」という性質の数値ではありません。どれだけの過去データを考慮に入れるかによって数値が変動します。

これについては、以下の英文記事を読んでいただくとよくわかります。以下の英文記事は、実際にBitflyerのチャート画面のボタンから参照されている解説ページです。

▽ 引用 (記事リンク

Therefore, the current EMA value will change depending on how much past data you use in your EMA calculation. Ideally, for a 100% accurate EMA, you should use every data point the stock has ever had in calculating the EMA, starting your calculations from the first day the stock existed. This is not always practical, but the more data points you use, the more accurate your EMA will be.

この記事によると、最も正確なEMAとは、「市場が開始した1日目からの価格をすべて考慮に入れた計算結果」だそうですが、そのような計算は実質的に不可能です。

例えば、私の解説した関数では200EMAの計算に過去400期間のデータを使っています。しかしBitflyerの画面に表示される200EMAの計算期間はわかりません。一般的に短期EMAの場合ほど計測期間によるズレは少ないですが、長期EMAになるほど計測期間の違いによって数値にズレが生じる可能性があります。

売買シグナルにBitflyer画面に表示されるEMAとピッタリ一致する数値を使いたい場合は、何らかの参考サイトをスクレイピングするしかないかもしれません。あるいは単純移動平均を使うかです。

今回使ったコード


import requests
from datetime import datetime

# 使用する時間足
chart_sec = 3600

# CryptowatchでBTCFXの価格データを取得
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)]:
			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


# 単純移動平均を計算する関数
def calculate_MA( value,before=None ):
	if before is not None:
		MA = sum(i["close_price"] for i in price[-1*value + before: before]) / value
	else:
		MA = sum(i["close_price"] for i in price[-1*value:]) / value
	return round(MA)

# 指数移動平均を計算する関数
def calculate_EMA( value,before=None ):
	if before is not None:
		MA = sum(i["close_price"] for i in price[-2*value + before : -1*value + before]) / value
		EMA = (price[-1*value + before]["close_price"] * 2 / (value+1)) + (MA * (value-1) / (value+1))
		for i in range(value-1):
			EMA = (price[-1*value+before+1 + i]["close_price"] * 2 /(value+1)) + (EMA * (value-1) / (value+1))
	else:
		MA = sum(i["close_price"] for i in price[-2*value: -1*value]) / value
		EMA = (price[-1*value]["close_price"] * 2 / (value+1)) + (MA * (value-1) / (value+1))
		for i in range(value-1):
			EMA = (price[-1*value+1 + i]["close_price"] * 2 /(value+1)) + (EMA * (value-1) / (value+1))
	return round(EMA)

#---- ここからメインの実行処理----

price = get_price(chart_sec)     # 価格データを取得
EMA10 = calculate_EMA(10,-20)
EMA21 = calculate_EMA(21,-20)
EMA100 = calculate_EMA(100,-20)
print( EMA10,EMA21,EMA100 )