
仮想通貨bot 勉強記録㉒
~損益結果をグラフを表示する~
◆前回までのあらすじ
ドンチャンチャネルブレイクアウトのロジックでバックテストを行うコードを作りました。
◆今回やること
・損益をグラフ表示&最大ドローダウンを確認する
先に結果ですが、こんな感じです↓
かっけぇぇぇ!めっちゃプログラミング感が強い。
これこれ、これがやりたかったんですよ。
コードはこちら↓
from datetime import datetime
from rich import print as pp
import pybybit
import time
import json
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
#====================API設定====================
apis = [
'プライベートキー',
'シークレットキー'
]
bybit = pybybit.API(*apis, testnet=True)
#===============================================
#====================バックテスト用の初期設定値====================
lot = 1000 # 1トレードのロット
slippage = 0.00075 # 手数料やスリッページ(0.075%初期値)
chart_min = 60 # (1 3 5 15 30 60 120 240 360 720 "D" "M" "W")
test_start = 0 # 何番目のデータからテストするか
test_end = int(60*24*364/chart_min) # 何番目のデータまでテストするか
path = "backdata/******************.json" # 読み込むファイルのパス
term = 20 # 過去n期間
wait = 0 # 待機時間
#====================APIから価格データ取得====================
def get_price_from_API(chart_min):
#取得開始時刻の設定
get_start = {
"year" : int(2019), #年
"month" : int(1), #月
"day" : int(1), #日
"hour" : int(00), #時
"minute" : int(00) #分
}
get_start = int(datetime(**get_start).timestamp())
#取得終了時刻の設定
get_end = {
"year" : int(2020), #年
"month" : int(1), #月
"day" : int(1), #日
"hour" : int(00), #時
"minute" : int(00) #分
}
get_end = int(datetime(**get_end).timestamp())
#データを格納する変数
price = []
#tがnowより小さければ実行
while get_start < get_end:
#pybybitでローソク足取得
k = bybit.rest.inverse.public_kline_list(
symbol = "BTCUSD",
interval= chart_min,
from_ = get_start
).json()
#klinesに取得したデータを入れる
price += k["result"]
#200本x足の長さ分だけタイムスタンプを進める
get_start += 200*60*chart_min
return price
#====================ファイルから価格データを読み込む====================
def get_price_from_file(path):
file = open(path,'r',encoding='utf-8')
price = json.load(file)
return price
#====================画面出力関====================
def print_price(data):
pp( " 時間: " + datetime.fromtimestamp(data['open_time']).strftime('%Y/%m/%d %H:%M')
+ " 始値: " + str(data['open'])
+ " 終値: " + str(data['close']))
#====================時間と始値・終値をログに記録====================
def log_price( data,flag ):
log = "時間: " + datetime.fromtimestamp(data["open_time"]).strftime('%Y/%m/%d %H:%M') + " 始値: " + str(data["open"]) + " 終値: " + str(data["close"]) + "\n"
flag["records"]["log"].append(log)
return flag
#====================ロジック判定====================
def donchian( data,last_data ):
highest = max(i["high"] for i in last_data)
if data["high"] > highest:
return {"side":"BUY","price":highest}
lowest = min(i["low"] for i in last_data)
if data["low"] < 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":
log = "過去{0}足の最高値{1}$を、直近の高値が{2}$でブレイク\n".format(term,signal["price"],data["high"]) + str(data["close"]) + "$で買い指値\n"
flag["records"]["log"].append(log)
# ここに買い注文のコードを入れる
flag["order"]["exist"] = True
flag["order"]["side"] = "BUY"
flag["order"]["price"] = float(data["close"])
if signal["side"] == "SELL":
log = "過去{0}足の最安値{1}$を、直近の安値が{2}$でブレイク\n".format(term,signal["price"],data["low"]) + str(data["close"]) + "$で売り指値\n"
flag["records"]["log"].append(log)
# ここに売り注文のコードを入れる
flag["order"]["exist"] = True
flag["order"]["side"] = "SELL"
flag["order"]["price"] = data["close"]
return flag
#====================注文状況確認====================
def check_order( flag ):
"""ここに注文状況確認コード"""
flag["order"]["exist"] = False
flag["position"]["exist"] = True
flag["position"]["side"] = flag["order"]["side"]
flag["position"]["price"] = flag["order"]["price"]
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":
log = "過去{0}足の最安値{1}$を、直近の安値が{2}$でブレイク\n".format(term,signal["price"],data["low"]) + "成行注文を出してポジションを決済します\n"
flag["records"]["log"].append(log)
# 決済の成行注文コードを入れる
records( flag,data )
flag["position"]["exist"] = False
flag["position"]["count"] = 0
log = "さらに" + str(data["close"]) + "$で売りの指値注文を入れてドテンします\n"
flag["records"]["log"].append(log)
# ここに売り注文のコードを入れる
flag["order"]["exist"] = True
flag["order"]["side"] = "SELL"
flag["order"]["price"] = float(data["close"])
if flag["position"]["side"] == "SELL":
if signal["side"] == "BUY":
log = "過去{0}足の最高値{1}$を、直近の高値が{2}$でブレイク\n".format(term,signal["price"],data["high"]) + "成行注文を出してポジションを決済します\n"
flag["records"]["log"].append(log)
# 決済の成行注文コードを入れる
records( flag,data )
flag["position"]["exist"] = False
flag["position"]["count"] = 0
log = "さらに" + str(data["close"]) + "$で買いの指値注文を入れてドテンします\n"
flag["records"]["log"].append(log)
# ここに買い注文のコードを入れる
flag["order"]["exist"] = True
flag["order"]["side"] = "BUY"
flag["order"]["price"] = float(data["close"])
drawdown = max(flag["records"]["gross-profit"]) - flag["records"]["gross-profit"][-1]
if drawdown > flag["records"]["drawdown"]:
flag["records"]["drawdown"] = drawdown
return flag
#====================トレードパフォーマンス確認====================
def records(flag,data):
entry_price = float(flag["position"]["price"])
exit_price = round(float(data["close"]))
trade_cost = lot * slippage
log ="スリッページ・手数料として" + str(trade_cost) + "$を考慮\n"
flag["records"]["log"].append(log)
flag["records"]["slippage"].append(trade_cost)
# 値幅の計算
buy_profit = exit_price - entry_price
sell_profit = entry_price - exit_price
# 利益率の計算
buy_return = buy_profit/entry_price
sell_return = sell_profit/entry_price
#利益・損失の確認
if flag["position"]["side"] == "BUY":
flag["records"]["buy-count"] += 1
flag["records"]["buy-profit"].append( buy_profit )
flag["records"]["buy-return"].append( buy_return )
#追加(損益合計の記録)
flag["records"]["gross-profit"].append( flag["records"]["gross-profit"][-1] + (buy_return*lot) )
if buy_profit > 0:
flag["records"]["buy-winning"] += 1
log = str(round(buy_return * lot , 4 )) + "$の利益です\n"
flag["records"]["log"].append(log)
else:
log = str(abs(round(buy_return * lot , 4 ))) + "$の損失です\n"
flag["records"]["log"].append(log)
if flag["position"]["side"] == "SELL":
flag["records"]["sell-count"] += 1
flag["records"]["sell-profit"].append( sell_profit )
flag["records"]["sell-return"].append( sell_return )
#追加(損益合計の記録)
flag["records"]["gross-profit"].append( flag["records"]["gross-profit"][-1] + (sell_return*lot) )
if sell_profit > 0:
flag["records"]["sell-winning"] += 1
log = str(round( sell_return * lot , 4 )) + "$の利益です\n"
flag["records"]["log"].append(log)
else:
log = str(abs(round( sell_return * lot , 4 ))) + "$の損失です\n"
flag["records"]["log"].append(log)
# 決済日時の記録
flag["records"]["date"].append(datetime.fromtimestamp(data["open_time"]))
return flag
#====================バックテストの集計====================
def backtest(flag):
total_return_buy = np.sum((flag["records"]["buy-return"])*lot)
total_return_sell = np.sum((flag["records"]["sell-return"])*lot)
print("\nバックテスト結果")
print("==============================")
print("--------買いエントリの成績--------")
print("トレード回数 : {}回".format(flag["records"]["buy-count"]))
print("勝率 : {}%".format(round(flag["records"]["buy-winning"] / flag["records"]["buy-count"] * 100,1)))
print("平均リターン : {}%".format(round(np.average(flag["records"]["buy-return"])*100,4)))
print("総損益 : {}$".format(round((total_return_buy),2)))
print("\n--------売りエントリの成績--------")
print("トレード回数 : {}回".format(flag["records"]["sell-count"] ))
print("勝率 : {}%".format(round(flag["records"]["sell-winning"] / flag["records"]["sell-count"] * 100,1)))
print("平均リターン : {}%".format(round(np.average(flag["records"]["sell-return"])*100,4)))
print("総損益 : {}$".format(round((total_return_sell),2)))
print("\n------------総合成績-------------")
print("最大ドローダウン : {0}$ / {1}%".format(-1 * round(flag["records"]["drawdown"],2), -1 * round(flag["records"]["drawdown"]/max(flag["records"]["gross-profit"])*100,1) ))
print("総損益 : {}$".format(round((total_return_buy + total_return_sell),2)))
print("手数料合計 : {}$".format( np.sum(flag["records"]["slippage"]) ))
print("==============================")
# 損益曲線をプロット
del flag["records"]["gross-profit"][0] # X軸/Y軸のデータ数を揃えるため、先頭の0を削除
date_list = flag["records"]["date"] #日付のリストを指定
plt.plot( date_list, flag["records"]["gross-profit"] ) # データをプロット
plt.xlabel("Date") # X軸ラベルを記載
plt.ylabel("Balance") # Y軸ラベルを記載
plt.xticks(rotation=60) # X軸の目盛りを60度回転
plt.show()
file = open("log/donchian-{0}-log.txt".format(datetime.now().strftime("%Y-%m-%d-%H-%M")),'wt',encoding='utf-8')
file.writelines(flag["records"]["log"])
#====================メイン====================
def main():
price = get_price_from_API(chart_min)
last_data = []
print("テスト期間 ")
print("==============================")
print("開始時点 : " + str(datetime.fromtimestamp(float(price[test_start]["open_time"]))))
print("終了時点 : " + str(datetime.fromtimestamp(float(price[test_end]["open_time"]))))
print(str(test_end-test_start) + "件のローソク足データで検証")
print("==============================")
flag = {
"order":{
"exist" : False, #オーダーの有り無し
"side" : "", #買いか売りか
"count" : 0, #保有期間
"price" : 0 #オーダー価格
},
"position":{
"exist" : False, #ポジションの有り無し
"side" : "", #買いか売りか
"count" : 0, #保有期間
"price" : 0 #ポジション価格
},
"records":{
"buy-count": 0, # 買いエントリのトレード数を記録
"buy-winning" : 0, # 勝った数を記録
"buy-return":[], # 各トレードでのリターン(利益率)を記録
"buy-profit": [], # 各トレードでの利益・損失額を記録
"sell-count": 0, # 売りエントリのトレード数を記録
"sell-winning" : 0, # 勝った数を記録
"sell-return":[], # 各トレードでのリターン(利益率)を記録
"sell-profit":[], # 各トレードでの利益・損失額を記録
"drawdown": 0, # ドローダウンの記録
"date":[], # 決済日時の記録
"gross-profit":[0], # 総利益の記録
"slippage":[], # 各トレードで生じた手数料を記録
"log":[] # あとでテキストファイルに出力したい内容を記録
}
}
i = 0
while i < test_end:
# ドンチャンの判定に使う過去term足分の安値・高値データを準備する
if len(last_data) < term:
last_data.append(price[i])
log_price(price[i],flag)
time.sleep(wait)
i += 1
#if文の条件を満たすまでループ処理
continue
data = price[i]
flag = log_price(data,flag) #iの価格を記
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 )
# 過去データをterm個ピッタリに保つために先頭を削除
del last_data[0]
last_data.append( data )
i += 1
time.sleep(wait)
backtest(flag)
main()
◆解説
前回からの変化点を解説します。
・import
import matplotlib.pyplot as plt
import pandas as pd
インポートするモジュールです。
グラフを表示するモジュールとして、matplotlib.pyplotをインポートします。
pandasも形式変換に使うのでインポートしておきます。
・flag
"records":{
"buy-count": 0, # 買いエントリのトレード数を記録
"buy-winning" : 0, # 勝った数を記録
"buy-return":[], # 各トレードでのリターン(利益率)を記録
"buy-profit": [], # 各トレードでの利益・損失額を記録
"sell-count": 0, # 売りエントリのトレード数を記録
"sell-winning" : 0, # 勝った数を記録
"sell-return":[], # 各トレードでのリターン(利益率)を記録
"sell-profit":[], # 各トレードでの利益・損失額を記録
"drawdown": 0, # ドローダウンの記録
"date":[], # 決済日時の記録
"gross-profit":[0], # 総利益の記録
"slippage":[], # 各トレードで生じた手数料を記録
"log":[] # あとでテキストファイルに出力したい内容を記録
}
flag["records"]の中に"drawdown": 0,"date":[], "gross-profit":[0], の3項目を追加します。内容はコメントアウトの通りです。
・close_position( data,last_data,flag )
drawdown = max(flag["records"]["gross-profit"]) - flag["records"]["gross-profit"][-1]
if drawdown > flag["records"]["drawdown"]:
flag["records"]["drawdown"] = drawdown
最大ドローダウンの定義を追加します。
ドローダウンは損益額が最大になった時の値から、今回決済時の値を引きます。
もし今回のドローダウンが過去の最大ドローダウンを超えたら、今回の値を最大ドローダウンとして上書きします。
・records(flag,data)
#利益・損失の確認
if flag["position"]["side"] == "BUY":
flag["records"]["buy-count"] += 1
flag["records"]["buy-profit"].append( buy_profit )
flag["records"]["buy-return"].append( buy_return )
#追加(損益合計の記録)
flag["records"]["gross-profit"].append( flag["records"]["gross-profit"][-1] + (buy_return*lot) )
if buy_profit > 0:
flag["records"]["buy-winning"] += 1
log = str(round(buy_return * lot , 4 )) + "$の利益です\n"
flag["records"]["log"].append(log)
else:
log = str(abs(round(buy_return * lot , 4 ))) + "$の損失です\n"
flag["records"]["log"].append(log)
if flag["position"]["side"] == "SELL":
flag["records"]["sell-count"] += 1
flag["records"]["sell-profit"].append( sell_profit )
flag["records"]["sell-return"].append( sell_return )
#追加(損益合計の記録)
flag["records"]["gross-profit"].append( flag["records"]["gross-profit"][-1] + (sell_return*lot) )
if sell_profit > 0:
flag["records"]["sell-winning"] += 1
log = str(round( sell_return * lot , 4 )) + "$の利益です\n"
flag["records"]["log"].append(log)
else:
log = str(abs(round( sell_return * lot , 4 ))) + "$の損失です\n"
flag["records"]["log"].append(log)
# 決済日時の記録
flag["records"]["date"].append(datetime.fromtimestamp(data["open_time"]))
records(flag,data)の中に3行追加します。
①if flag["position"]["side"] == "BUY"の後にflag["records"]["gross-profit"].append( flag["records"]["gross-profit"][-1] + (buy_return*lot) )
②if flag["position"]["side"] == "SELL"の後にflag["records"]["gross-profit"].append( flag["records"]["gross-profit"][-1] + (sell_return*lot) )
③関数内の最後にflag["records"]["date"].append(datetime.fromtimestamp(data["open_time"]))
①、②はその時点での損益の記録です。前回決済時の損益(flag["records"]["gross-profit"][-1])に、リターン*ロットを足すことで今回決済時の損益を算出しています。
③は決済を行ったローソク足のタイムスタンプを、datetime型に変換して記録しています。
・backtest(flag)
# 損益曲線をプロット
del flag["records"]["gross-profit"][0] # X軸/Y軸のデータ数を揃えるため、先頭の0を削除
date_list = flag["records"]["date"] #日付のリストを指定
plt.plot( date_list, flag["records"]["gross-profit"] ) # データをプロット
plt.xlabel("Date") # X軸ラベルを記載
plt.ylabel("Balance") # Y軸ラベルを記載
plt.xticks(rotation=60) # X軸の目盛りを60度回転
plt.show()
backtest(flag)内に追記します。
del flag["records"]["gross-profit"][0] # X軸/Y軸のデータ数を揃えるため、先頭の0を削除
1行目は、初期値に0を入れてあるflag["records"]["gross-profit"][0]の最初の値を削除しています。グラフを作成するときに、X軸/Y軸のデータの数が合わずにエラーになるのを回避する為です。
最後のplt.show()でグラフを表示します。
print("\n------------総合成績-------------")
print("最大ドローダウン : {0}$ / {1}%".format(-1 * round(flag["records"]["drawdown"],2), -1 * round(flag["records"]["drawdown"]/max(flag["records"]["gross-profit"])*100,1) ))
print("総損益 : {}$".format(round((total_return_buy + total_return_sell),2)))
print("手数料合計 : {}$".format( np.sum(flag["records"]["slippage"]) ))
print("==============================")
画面表示する総合成績の部分に、最大ドローダウンの項目を追加してます。
金額、割合をそれぞれ計算して表示します。
解説終わり!
◆実行
・Spyderを使う
今回はAnacondaプロンプトではなく、Spyderを使ってコードを実行します。
まず、スタートボタンからAnaconda3(64-bit)を展開し、Spyder(Anaconda3)を選択して起動します
起動したらこんな感じになると思います↓(ちょっと違うかも)
使用言語を日本語にしたい場合は、
①画面上の方にある工具マークをクリック
②General
③Advanced settings
④Language
⑤englishu⇒日本語に変更
で日本語にできます。
起動したらコードを書く部分(真ん中もしくは左側のエリア)に、コードを記載します。
F5または実行ボタンで、コードを実行します。
すると、右下のエリアにコードの実行結果、右上にグラフが表示されます。
グラフが表示されない場合は、ctrl+shift+gを押してみてください。
グラフの表示エリアが現れます。
・結果
期間:2019/01/01~2020/12/31、60分足で実行した結果です。
勝率:買いエントリーの方が高いが、それでも36.9%程度
平均リターン:買いエントリーはプラスだが、売りエントリーはマイナス
総損益:買いエントリーはプラスだが、売りエントリーはマイナス
最大ドローダウン:-50%くらい(アカン)
売りエントリーの結果が酷い。
試しに買いエントリーしかしないように修正したら、ドローダウンがかなり小さくなり、グラフも右肩上がりになりました。
テスト期間が上げ相場だったから売りエントリーの結果がダメダメだったのかな?
てことは、移動平均線の傾きとかでエントリー条件にフィルターを掛ければ上げ・下げどっちの相場でも使えるようになるのでは・・・?
楽しくなってきましたね・・・(^ω^ )
今回はここまで!