前回の記事「Pandasを使ってBTCFXの自動売買BOTの月別の成績を集計しよう!」の練習問題の回答コードです。
1時間足での30期間ドンチャン・ブレイクアウトBOTの月別の成績を集計してみましょう! なお、今回のコードではPandasの集計方法を使って以下のような指標も加えておきました。
・全トレードの回数と勝率
・全トレードの平均リターン
・最大の勝ちトレードでの利益
・最大の負けトレードでの損失
・最終的な利益合計
・最終的な損失合計
これらも自動売買BOTの成績を評価する際に参考にしてみてください。
import requests from datetime import datetime import time import matplotlib.pyplot as plt import pandas as pd #-----設定項目 chart_sec = 3600 # 1時間足を使用 term = 30 # 過去n期間の設定 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"]) + "\n" flag["records"]["log"].append(log) return flag # ドンチャンブレイクを判定する関数 def donchian( data,last_data ): highest = max(i["high_price"] for i in last_data) if data["high_price"] > highest: return {"side":"BUY","price":highest} lowest = min(i["low_price"] for i in last_data) if data["low_price"] < 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(term,signal["price"],data["high_price"])) flag["records"]["log"].append(str(data["close_price"]) + "円で買いの指値注文を出します\n") # ここに買い注文のコードを入れる flag["order"]["exist"] = True flag["order"]["side"] = "BUY" flag["order"]["price"] = round(data["close_price"] * lot) if signal["side"] == "SELL": flag["records"]["log"].append("過去{0}足の最安値{1}円を、直近の安値が{2}円でブレイクしました\n".format(term,signal["price"],data["low_price"])) flag["records"]["log"].append(str(data["close_price"]) + "円で売りの指値注文を出します\n") # ここに売り注文のコードを入れる flag["order"]["exist"] = True flag["order"]["side"] = "SELL" flag["order"]["price"] = round(data["close_price"] * lot) 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"]["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": flag["records"]["log"].append("過去{0}足の最安値{1}円を、直近の安値が{2}円でブレイクしました\n".format(term,signal["price"],data["low_price"])) flag["records"]["log"].append(str(data["close_price"]) + "円あたりで成行注文を出してポジションを決済します\n") # 決済の成行注文コードを入れる records( flag,data ) flag["position"]["exist"] = False flag["position"]["count"] = 0 flag["records"]["log"].append("さらに" + str(data["close_price"]) + "円で売りの指値注文を入れてドテンします\n") # ここに売り注文のコードを入れる flag["order"]["exist"] = True flag["order"]["side"] = "SELL" flag["order"]["price"] = round(data["close_price"] * lot) if flag["position"]["side"] == "SELL": if signal["side"] == "BUY": flag["records"]["log"].append("過去{0}足の最高値{1}円を、直近の高値が{2}円でブレイクしました\n".format(term,signal["price"],data["high_price"])) flag["records"]["log"].append(str(data["close_price"]) + "円あたりで成行注文を出してポジションを決済します\n") # 決済の成行注文コードを入れる records( flag,data ) flag["position"]["exist"] = False flag["position"]["count"] = 0 flag["records"]["log"].append("さらに" + str(data["close_price"]) + "円で買いの指値注文を入れてドテンします\n") # ここに買い注文のコードを入れる flag["order"]["exist"] = True flag["order"]["side"] = "BUY" flag["order"]["price"] = round(data["close_price"] * lot) return flag # 各トレードのパフォーマンスを記録する関数 def records(flag,data): # 取引手数料等の計算 entry_price = flag["position"]["price"] exit_price = round(data["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"] ) # 値幅の計算 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"], "Periods" : flag["records"]["holding-periods"], "Slippage" : flag["records"]["slippage"] }) # 総損益の列を追加する 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("-----------------------------------") 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("-----------------------------------") 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("") print("最大の勝ちトレード : {}円".format(records.Profit.max())) print("最大の負けトレード : {}円".format(records.Profit.min())) 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=1483228800) flag = { "order":{ "exist" : False, "side" : "", "price" : 0, "count" : 0 }, "position":{ "exist" : False, "side" : "", "price": 0, "count":0 }, "records":{ "date":[], "profit":[], "return":[], "side":[], "holding-periods":[], "slippage":[], "log":[] } } last_data = [] i = 0 while i < len(price): # ドンチャンの判定に使う過去30期間分の安値・高値データを準備する if len(last_data) < 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 = close_position( data,last_data,flag ) else: flag = entry_signal( data,last_data,flag ) # 過去データを30個に保つために先頭を削除 del last_data[0] 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時間足でシンプルな30期間のドンチャンブレイクBOTを、2017年8月~2018年4月にかけて運用した場合の成績です。
損益グラフ
ドンチャン・ブレイクアウトBOTの成績
(base) C:\Pydoc>python test.py -------------------------- テスト期間: 開始時点 : 2017/08/13 08:00 終了時点 : 2018/04/20 15:00 6000件のローソク足データで検証 -------------------------- バックテストの結果 ----------------------------------- 買いエントリの成績 ----------------------------------- トレード回数 : 49回 勝率 : 51.0% 平均リターン : 3.04% 総損益 : 1090811円 平均保有期間 : 62.2足分 ----------------------------------- 売りエントリの成績 ----------------------------------- トレード回数 : 49回 勝率 : 42.9% 平均リターン : 0.85% 総損益 : 670134円 平均保有期間 : 56.7足分 ----------------------------------- 総合の成績 ----------------------------------- 全トレード数 : 98回 勝率 : 46.9% 平均リターン : 1.94% 平均保有期間 : 59.5足分 最大の勝ちトレード : 545284円 最大の負けトレード : -296151円 最大ドローダウン : -483332円 / -24.4% 利益合計 : 4591094円 損失合計 : -2830149円 最終損益 : 1760945円 手数料合計 : -104961円 ----------------------------------- 月別の成績 ----------------------------------- 2017年8月の成績 ----------------------------------- トレード数 : 6回 月間損益 : 1552円 平均リターン : 0.08% 月間ドローダウン : -20319円 ----------------------------------- 2017年9月の成績 ----------------------------------- トレード数 : 10回 月間損益 : 139626円 平均リターン : 3.02% 月間ドローダウン : -20785円 ----------------------------------- 2017年10月の成績 ----------------------------------- トレード数 : 12回 月間損益 : 119928円 平均リターン : 2.12% 月間ドローダウン : -50845円 ----------------------------------- 2017年11月の成績 ----------------------------------- トレード数 : 11回 月間損益 : 405276円 平均リターン : 4.89% 月間ドローダウン : -138695円 ----------------------------------- 2017年12月の成績 ----------------------------------- トレード数 : 10回 月間損益 : 175018円 平均リターン : 1.68% 月間ドローダウン : -364745円 ----------------------------------- 2018年1月の成績 ----------------------------------- トレード数 : 14回 月間損益 : 425925円 平均リターン : 1.08% 月間ドローダウン : -247323円 ----------------------------------- 2018年2月の成績 ----------------------------------- トレード数 : 13回 月間損益 : 284263円 平均リターン : 2.58% 月間ドローダウン : -483332円 ----------------------------------- 2018年3月の成績 ----------------------------------- トレード数 : 11回 月間損益 : 155253円 平均リターン : 1.07% 月間ドローダウン : -425155円 ----------------------------------- 2018年4月の成績 ----------------------------------- トレード数 : 11回 月間損益 : 54104円 平均リターン : 0.3% 月間ドローダウン : -216376円
月別の成績で見ても、すべての月でプラスの成績が出ています。
これはなかなか悪くない結果ですね。
こちらの月別の成績は、あくまで「ポジションを手仕舞った日時」を基準に区切っている点に注意してください。例えば、2月の成績はプラスになっていますが、これは1月から持ち越したポジションで大きな利益が出ているからです。もし2月からBOTの稼働を開始していたら、2月の損益はマイナスになります。
コードの解説
基本的には、前回の記事「Pandasを使って自動売買BOTの成績を月別に集計しよう!」で、例として解説したコードをそのまま使っています。特に難しいところは無かったのではないでしょうか。
以下のところだけ、前回の記事には登場していなかった書き方なので、追加で解説しておきます。
最大ドローダウン率
最大ドローダウン率とは、ある行の最大ドローダウンの金額を、その行までの最大資産額で割った数字です。特定の行までの最大値は、cummax()で取得できます。そのため、以下のような式になります。
records["DrawdownRate"] = round(records.Drawdown / records.Gross.cummax() * 100,1)
ただし実際に最大ドローダウン率を表示するときには注意が必要です。単にドローダウン率の中から最大値を選ぶだけだと、最大ドローダウンの金額と時期が一致するとは限らないからです。
例えば、「最大ドローダウン率でみると序盤の2月の30%が最大だけど、金額ベースでみると4月の300万円が最大ドローダウンだ」ということもあり得ます。
このとき私たちが知りたいのは、最大ドローダウン率ではなく、「最大ドローダウン金額が最大だったときのドローダウン率」であるはずです。そのため、以下のように記述します。
# 最大ドローダウンの行番号を取得 records.Drawdown.idxmax() # 最大ドローダウンと同じ行のドローダウン率を取得 records.DrawdownRate.loc[records.Drawdown.idxmax()]
これで最大ドローダウンと同じ時期のドローダウン率を取得することができました。
pandasの表データのfor文処理
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) ))
pandasの表データを1行ずつループ処理したい場合、以下のように書くことができます。
for index,row in 表データ変数.iterrows(): # 1行ずつ処理したい内容
index (インデックス)というのは、表データから特定の「行」を検索するときの「索引」のことです。
行には通常、1行ごとに先頭から 0,1,2,3,4,5.....などの行番号が割り振られていますが、それだと検索するときに不便です。縦の列にカラム名(ここでは Gross/Rate/Drawdownなど)があるのと同じように、行にも名前を付けることができます。それが index です。
今回のコードでは、pandas の groupby()で「月別」にデータをグループ化しているため、index には「2017/08」「2017/09」「2017/10」などのDate型のデータが指定されています。そのため、for文の中で index.year / index.month を指定することで、これらの数字を print できます。