前回の記事「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 できます。