
仮想通貨bot 勉強記録㉑
~ドンチャンの損益確認コードを作る~
◆前回までのあらすじ
ドンチャンチャネルブレイクアウトのロジックを作ってみました。
◆今回やること
・損益確認コードの作成
勉強記録⑲と同じように、損益確認コードを作りました!
作成したコードはこちら↓
from datetime import datetime
from rich import print as pp
import pybybit
import time
import json
import numpy as np
#====================API設定====================
apis = [
'プライベートキー',
'シークレットキー'
]
bybit = pybybit.API(*apis, testnet=True)
#===============================================
#====================バックテスト用の初期設定値====================
lot = 1000 # 1トレードのロット
slippage = 0.00075 # 手数料やスリッページ(0.075%初期値)
chart_min = 30 # (1 3 5 15 30 60 120 240 360 720 "D" "M" "W")
test_start = 0 # 何番目のデータからテストするか
test_end = int(60*24*14/chart_min) # 何番目のデータまでテストするか(この例この例だと14日間)
path = "backdata/**********.json" # 読み込むファイルのパス
term = 20 # 過去n期間
wait = 0 # 待機時間
#====================APIから価格データ取得====================
def get_price_from_API(chart_min):
#取得開始時刻の設定
get_start = {
"year" : int(2021), #年
"month" : int(3), #月
"day" : int(1), #日
"hour" : int(00), #時
"minute" : int(00) #分
}
get_start = int(datetime(**get_start).timestamp())
#取得終了時刻の設定
get_end = {
"year" : int(2021), #年
"month" : int(4), #月
"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"])
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 )
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 )
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)
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("総損益 : {}$".format(round((total_return_buy + total_return_sell),2)))
print("手数料合計 : {}$".format( np.sum(flag["records"]["slippage"]) ))
print("==============================")
return False
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":[], # 各トレードでの利益・損失額を記録
"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()
コピペで動くよ!
※この.pyファイルがあるフォルダ内に"log"というフォルダを作成する必要があります
◆解説
ほとんど勉強記録⑲と同じなので、省略しながら解説します。
・flag
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":[], # 各トレードでの利益・損失額を記録
"slippage":[], # 各トレードで生じた手数料を記録
"log":[] # あとでテキストファイルに出力したい内容を記録
}
}
最初にメイン関数内に設定している変数:flagを確認しておきましょう。
オーダーの状況、ポジションの状況、損益等の集計のための記録の3項目があり、そこから更に各項目にブレイクダウンしてます。
基本的にはコメントアウトの通りです。
保有期間はカウントしてますが、集計には使ってません。
集計結果がおかしいときは、flagの中身を確認してみると、修正すべき箇所が分かることが多いと思われまする。
◆実行結果
省略しながら解説って書いたけど、勉強記録⑲と内容かぶり過ぎなので解説終わり!
実行するとこんな感じです↓
・集計結果
めっちゃマイナスやないかーいッ
まぁでも動作確認だし、ちゃんと集計できてるし、成功です
・ログファイルの出力
実行すると、コードを保存してあるファルダの下位フォルダにlogが保存されます。(実行しすぎ)
僕の場合だとPydocにコードを保存してるので、Pydoc/logにlogファイルが出力されます。
ちなみに各パラメータをいじると総合成績がプラスになることもあります。
例えば30分足で、2021/3/1から14日間に設定すると、手数料を差し引いても損益はプラスでした。
実はこの行為(結果が良くなるようにパラメータを調整すること)は、過剰最適化といい、あんまり良くないらしいです。
そりゃ結果が良くなるよう手を加えてたら、バックテストの意味もなくなりますよね。
今回はここまで!
次回は最終損益結果が出るまでのグラフを作成するか、最近使っているSpyderの紹介をしようかな。