【 今回やること! 】
- Pythonのライブラリの『Backtesting.py』を使って、FXのバックテストを行います。
『Backtesting.py』を使ってバックテストをする方法についてまとめました。
自動売買システムをヒストリカルデータ(過去の相場データ)で試します。
ライブラリを使うことで、最低限必要な機能がそろったバックテストプログラムが楽に作れます。
FXの知識が浅い私にとっては、ライブラリの仕様を見ることがバックテストの評価方法や指標の種類を知るきっかけになっています。
最初に別のライブラリを使おうとしたのですが、私の知識が足りなすぎて分かりづらかったため、比較的簡単という情報があった『Backtesting.py』を使ってみました。
『Backtesting.py』は、ヒストリカルデータを設定して、バックテストを実行し、分析結果を見るというバックテストの基本的な部分が、シンプルに出来ていると思います。結果が簡単に一覧表示できるのも分かりやすいです。
目次
『Backtesting.py』の公式サイト
今回の作業の前提
『Google ドライブ』の『hst_(日付、例:hst_20191029)』フォルダ内に、使用するヒストリカルデータの『USDJPY_20190930.csv』がある状態で作業を始めています。
『Google Colaboratory』の使い方や、ヒストリカルデータの入手方法は下記の記事を見て下さい。
『Backtesting.py』のインストール
- 『Google Colaboratory』で『Python 3 の新しいノートブック(または前回のノートブック)』を開きます。
- 『Google ドライブ』をマウントします。
- 『コードセル』に下記のコードを貼り付けて実行します。
『Linuxコマンド』でライブラリをインストールしします。
『コードセル』では、先頭に”!(エクスクラメーション)”を付けることで『Linuxコマンド』を実行できます。!pip3 install backtesting
この記事を書いている時は求められませんでしたが、グラフ表示のために『pip3 install mpl_finance』が必要な場合があるかもしれません。
サンプルプログラムのストラテジーを動かす
コード
データの読み込み、ストラテジーの設定、バックテストの設定、実行と結果出力を行っています。
公式のサンプルプログラムは、ライブラリに含まれるデータを使用していますが、下記のコードは自分で用意したデータを使っています。
- 『コードセル』に下記のコードを貼り付けて実行します。
%%time # ↑セルの処理時間を計算 %%timeはセルの最初に単独で書く import os # パス結合 import pandas as pd # pandasデータフレームを使用 # Googleドライブのマイドライブのパス mydrive = '/content/drive/My Drive' # フォルダ名 hst_dir = 'hst_20191029' # 入力ファイル名 input_csv = 'USDJPY_20190930.csv' # CSVファイルをPandasデータフレームに読み込む df = pd.read_csv(os.path.join(mydrive, hst_dir, input_csv)) # DateTime列をdatatime型に変換→インデックスに設定して、元の列は削除する df = df.set_index(pd.to_datetime(df['DateTime'])).drop('DateTime', axis=1) from backtesting import Backtest, Strategy # バックテスト実行、ストラテジー作成 from backtesting.lib import crossover from backtesting.test import SMA # SMAインジケータ使用 class myStrategy(Strategy): n1 = 10 # 終値のSMA(単純移動平均)の期間 n2 = 30 # 終値のSMA(単純移動平均)の期間 def init(self): # ストラテジーの事前処理 self.sma1 = self.I(SMA, self.data.Close, self.n1) # 終値のSMA(単純移動平均)をインジケータとして追加 self.sma2 = self.I(SMA, self.data.Close, self.n2) # 終値のSMA(単純移動平均)をインジケータとして追加 def next(self): # ヒストリカルデータの行ごとに呼び出される(データの2行目から開始) if crossover(self.sma1, self.sma2): # sma1がsma2を上回った時 self.buy() # 現在のポジションを閉じて、所持金分買う elif crossover(self.sma2, self.sma1): self.sell() # 現在のポジションを閉じて、所持金分売る # バックテストを設定 bt = Backtest( df, # ヒストリカルデータ myStrategy, # ストラテジー cash=10000, # 所持金 commission=0.0005, # 取引手数料(為替価格に対する倍率で指定、為替価格100円でcommission=0.0005なら0.05円) margin=1.0, # 取引金額に対する所持金の割合、cash=10000でmargin=0.2なら50000分取引する trade_on_close=True # True:現在の終値で取引する、False:次の時間の始値で取引する ) output = bt.run() # バックテスト実行 print(output) # 実行結果を表示 bt.plot() # 実行結果のグラフを表示
- 下記のような実行結果とグラフが表示されます。
Start 2019-09-30 00:01:00 End 2019-09-30 23:59:00 Duration 0 days 23:58:00 Exposure [%] 92.7677 Equity Final [$] 9715.39 Equity Peak [$] 10006.7 Return [%] -2.84615 Buy & Hold Return [%] 0.101896 Max. Drawdown [%] -2.94051 Avg. Drawdown [%] -0.0155023 Max. Drawdown Duration 0 days 00:30:00 Avg. Drawdown Duration 0 days 00:10:00 # Trades 54 Win Rate [%] 9.25926 Best Trade [%] 0.0639561 Worst Trade [%] -0.149003 Avg. Trade [%] -0.0529873 Max. Trade Duration 0 days 01:50:00 Avg. Trade Duration 0 days 00:25:00 Expectancy [%] 0.0566947 SQN -10.6372 Sharpe Ratio -1.45463 Sortino Ratio -1.87251 Calmar Ratio -0.0180198 _strategy myStrategy
- ※グラフを表示した際の Warningについて
『bt.plot()』でグラフを表示すると『BokehDeprecationWarning: 'legend' keyword is deprecated, use explicit 'legend_label', 'legend_field', or 'legend_group' keywords instead』という警告文が最近出るようになりました。気になりますがまだ対処方法がわかっていません。バックテスト自体は最後まで実行できているようです。
結果表示について
各パラメータについて調べてみました。意味は理解しきれてないものもありますが、参考まで。
Start 2019-09-30 00:01:00 # ヒストリカルデータの開始日時 End 2019-09-30 23:59:00 # ヒストリカルデータの終了日時 Duration 0 days 23:58:00 # ヒストリカルデータの期間 Exposure [%] 92.7677 # ポジションを持っていた期間の割合(ポジションを持っていた期間÷全期間×100) Equity Final [$] 9715.39 # 所持金の最終値 Equity Peak [$] 10006.7 # 所持金の最高値 Return [%] -2.84615 # 利益率=損益÷開始時所持金×100 Buy & Hold Return [%] 0.101896 # ((終了時の終値 - 開始時の終値)÷ 開始時の終値)の絶対値×100 Max. Drawdown [%] -2.94051 # 最大下落率 Avg. Drawdown [%] -0.0155023 # 平均下落率 Max. Drawdown Duration 0 days 00:30:00 # 最大下落期間 Avg. Drawdown Duration 0 days 00:10:00 # 平均下落期間 # Trades 54 # 取引回数 Win Rate [%] 9.25926 # 勝率=勝ち取引回数÷全取引回数×100 Best Trade [%] 0.0639561 # 1回の取引での利益の最大値÷所持金×100 Worst Trade [%] -0.149003 # 1回の取引での損失の最大値÷所持金×100 Avg. Trade [%] -0.0529873 # 損益の平均値÷所持金×100 Max. Trade Duration 0 days 01:50:00 # 1回の取引での最長期間 Avg. Trade Duration 0 days 00:25:00 # 1回の取引での平均期間 Expectancy [%] 0.0566947 # 期待値=平均利益×勝率+平均損失×敗率 # 期待値とは、1回の取引で期待できる利益のことです。 # 売買ルールの優劣を判断する指標で、勝率、平均利益、平均損失から求めます。 # 期待値が正の場合は資産が増えていき、負の場合は資産が減っていくと考えます。 SQN -10.6372 # SQN(SystemQualityNumber) # SQNとは、Van K. Tharp氏によって定義された取引システムの分類(評価)方法です。 # SQN値によって下記のように分類されます。 # 1.6-1.9:平均以下 # 2.0-2.4:平均 # 2.5-2.9:良い # 3.0-5.0:素晴らしい # 5.1-6.9:最高 # 7.0~:聖杯? # 取引数が30以上の場合、SQN値は信頼できるとみなされるでしょう。 Sharpe Ratio -1.45463 # 標準偏差に対するリターンの比率 # シャープレシオとは利益とリスクの比率のことで、値が大きいほど資産曲線がなめらかになり安定性のある利益が見込めます。 Sortino Ratio -1.87251 # 下方リスクに対するリターンの比率 # シャープレシオだけでは分からない下方リスクの抑制度合いを判断する場合に使われます。 # 通常、この数値が大きいほど優れている(下落局面に強い)ことを示します。 Calmar Ratio -0.0180198 # 最大損失率に対する年間平均収益の比率 # 値が低いほど指定された期間に渡ってリスク調整ベースで実行された投資は悪化します。 # 値が高いほどパフォーマンスが向上します。 _strategy myStrategy # 使用したストラテジーの関数名
補足
- ヒストリカルデータは、csvファイルを Pandas データフレームに読み込んでから使用しています。
- ヒストリカルデータは、日時インデックス、Open、High、Low、Close、Volumeの列を含んでいます。
Volume列は無くても良いようです。標準偏差等、上記以外の列を独自に加えた場合でも動作しました。
- ストラテジーの関数は、『init()』と『next()』をオーバーライド(再定義)する必要があります。
class myStrategy(Strategy): def init(self): # ストラテジーの事前処理 print('init') def next(self): # ヒストリカルデータの行ごとに呼び出される(データの2行目から開始) print('next')
試したところ『next()』は、ヒストリカルデータの2行目から実行されました。
- インジケータのデータが出るまで取引が実行されません。
def init(self): # ストラテジーの事前処理 self.sma1 = self.I(SMA, self.data.Close, 30)
上記では、ヒステリカルデータの終値の30行分の平均値をインジケータとして加えています。
平均値が出る30行目までは、取引が行われません。
- 取引量は設定出来ません。
注文は常にその時の所持金分すべてを使います。
- 同時に複数のポジションを持てません。
『buy()』、『sell()』ともに現在のポジションを閉じてから発注します。
- バックテストの設定について
# バックテストを設定 bt = Backtest( df, # ヒストリカルデータ myStrategy, # ストラテジー cash=10000, # 所持金 commission=0.0005, # 取引手数料(為替価格に対する倍率で指定、為替価格100円でcommission=0.0005なら0.05円) margin=1.0, # 取引金額に対する所持金の割合、cash=10000円でmargin=0.2なら50000円取引する trade_on_close=True # True:現在の終値で取引する、False:次の時間の始値で取引する、デフォルトはFalse )
ストラテジーの例
- 値が2連続で上がっていたら、利確と損切りの価格を設定して買い注文を入れるストラテジーです。
class myStrategy(Strategy): def init(self): # ストラテジーの事前処理 return # 何もしない def next(self): # ヒストリカルデータの行ごとに呼び出される(データの2行目から開始) if not self.position: # ポジションを持っていない場合 if len(self.data) >= 3: # ヒストリカルデータの3行目以降の場合 if self.data.Close[-1] > self.data.Close[-2]: # 現在の終値が1つ前の終値より上回っている場合 if self.data.Close[-2] > self.data.Close[-3]: # 1つ前の終値が2つ前の終値より上回っている場合 self.buy( tp=self.data.Close[-1] + 0.30, # tp(take profit)利確する価格を設定、現在の価格+0.3 sl=self.data.Close[-1] - 0.20 # sl(stop losst)損切りする価格を設定、現在の価格-0.3 )
- 現在の価格は『self.data.Close[-1]』で取得できます。
- 買い注文(『buy()』)では、sl(stop losst) < 取引価格 < tp(take profit)にする必要があります。売り注文(『sell()』)では、sl(stop losst) > 取引価格 > tp(take profit)にする必要があります。
- 現在の価格は『self.data.Close[-1]』で取得できます。
- ポジションがないなら買い注文して、時間が10行分経過したら現在の価格でクローズするストラテジーです。
class myStrategy(Strategy): def init(self): # ストラテジーの事前処理 return # 何もしない def next(self): # ヒストリカルデータの行ごとに呼び出される(データの2行目から開始) if not self.position: # ポジションを持っていない場合 self.orderd_bar = len(self.data) # 現在の行数を取得 self.buy() # 買い注文を出す else: # ポジションを持っている場合 if len(self.data) > self.orderd_bar + 10: # 買い注文から10行分経過した場合 self.position.close() # 現在の価格でポジションをクローズする