Backtesting.pyをはじめから ~Backtesting 0.1.7~
バックテストのpythonライブラリであるBacktestingについて公式の内容を使いながらまとめてみました。主に暗号資産のデータに対してライブラリを使用していますが、データ形式が要件を満たせばどのようなアセットに対してもバックテストが行えます。バックテストはバックキャストで過去のデータに当てはめてストラテジーの有用性や実績を検証しているだけなので、未来についての将来実績を約束するものではありません。有意であった指標が使えなくなる可能性はありますので参考までに留めています。こちらのリポジトリにいくつかJupyter Notebookのサンプルも置いたので書き換えて使えます。Backtesting 0.1.7やインジケータを計算するライブラリであるTa-libをぶち込んであるAnaconda3のDockerイメージを上げてあるので基本的にはそのコンテナでJupyter Notebookを書きかきしています。イメージはこちらです。
この記事の内容は以下の公式ドキュメントに基づいておりますが、最新版である 0.2.0
ではなく 0.1.7
で確認しております。※ Anaconda3の環境へpipで 0.2.0
をぶち込んだところ誤動作したので戻しました(泣
以下のマニュアルやチュートリアルをざっと確認した内容をまとめております。[1]
目次です。
- データ形式について
- Strategyクラス
- Backtestクラス
- パラメータヒートマップ
データ形式について
BacktestingではOHLCまたはOHLCVデータをPandasのDataFrameの形式で用意します。カラム名は最初の文字が大文字で正確に 'Open', 'High', 'Low', 'Close'
である必要があります。 'open', 'high', 'low', 'close'
で渡してもエラーとなるので注意してください。暗号資産であればccxtなどのライブラリで簡単に情報を抜けます。ccxtのfetch_ohlcv
は各取引所のAPIで取得する情報と差異がある場合があります。BitMexの例ですが以下の記事が参考になりました。またDataFrameのカラムは上記の4つ以外に存在してもBacktetingに渡すことが可能なようです。[2]
ccxtでOHLCVデータを取得するサンプルです。交換所はbitbankでペアはETH/JPYです。今ETH/JPYのメイカー・テイカー手数料が無料なので利益を上げやすくなっております。※bitbankの回し者ではございません
このデータフレームを Bactest
の data
へ引数として渡します。
class Backtest(data, strategy, *, cash=10000, commission=0.0, margin=1.0, trade_on_close=False, hedging=False, exclusive_orders=False)
ccxtやAPIからOHLCVのデータは簡単に取得できますが、板情報や約定情報を用いるような戦略のテストはBacktestingでは難しいです(例えば高頻度MM botで採用されているようなもの)。※厳密にできないわけではないが、バックテストにおける売買がDataFrameの行に沿って時系列に処理される点に注意、等間隔の時間の売買によるバックテストとなる
Strategyクラス
次にバックテストに使用するトレード戦略をコーディングします。backtesting.Strategyクラスを継承して init()
メソッドと next()
メソッドを書き換えましょう。 init()
メソッドと next()
メソッドが何をするのかは以下の通り。基本的にこの2つを定義しておくだけでバックテストを行えます。めちゃくちゃ簡単です。
init()
バックテストが実行される最初に呼びだされる関数で、トレードで使用する指標やシグナルを計算しておけます、単純移動平均値やRSIなどをTa-libで計算しておいてトレードのシグナルとして使用できますnext()
Backtestingインスタンスによって継続的に呼び出される関数で、各データポイント(ここではDataFrameのOHLCVの各行)ごとにトレードを実行し計算します
以下がEMA(指数平滑移動平均線)のゴールデンクロス・デッドクロスでのドテン売買のストラテジーコードです。
init
で短期と長期2種類のEMAを計算しています。 self.I
というのはインジケータ宣言のことでこの関数は np.ndarray
型の値を返します。EMAの計算期間はクラス変数で定義しています。このクラス変数はBacktestingの強力な関数であるoptimizeで操作することができるため後で重要になってきます。optimizeについては後ほど説明します。基本的にデータを素のOHLCVデータで渡して self.I
で呼び出した関数側でデータ操作を行い、シグナルとなる指標だけ init
側に返すのがベターなプラクティスです。最初からデータフレームへ様々なカラムを追加しておくこともできると思われますが、コードとしてあまりスッキリと収まりません。以下はチャネルブレイクアウト(いわゆるドテン君)の例です。
self.data.Close
などと呼ぶと通常の1次元配列が渡されます。 self.data
のタイプはSeriesやDataFrameではなくリストみたいです。SeriesやDataFrameとして取り扱いたい場合には変換してあげる必要があります。公式サイトには data.Close.s
や data.df
というアクセサでSeriesおよびDataFrame化できると書いてありましたが、これはなぜかうまくいきませんでした。
上記のチャネルブレイクアウトの戦略ではインジケータ作成時に self.data
を渡し、シグナルを計算して返却する関数側でデータフレームに組み立てなおして処理しています。以下のようにしてSeriesやDataFrameへ変換できます。
# Series
close = pd.Series(self.data.Close)# DataFrame
df = pd.DataFrame({'Open': self.data.Open, 'High': self.data.High, 'Low': self.data.Low, 'Close': self.data.Close})
次に next
の説明です。 next()
ではアセットの売買やポジションの決済を行います。ここで先に init
で用意したインジケータを利用します。 self.buy
と self.sell
で新規オーダーを建てることができます。既存のオーダーに対するクエリーはOrderクラスを使用します。self.buy
や self.sell
はサイズ size=0.9999999999999998 の成り行き注文になりますが、サイズが 0 と 1 の間の値である場合は、現在利用可能な流動性の割合として解釈されます(現金と Position.pl から使用済みマージンを差し引いたもの)。1 以上の数値の場合には売買ユニットの表現です。[3]
便利な関数にPositionクラスの self.position.close()
があります。Backtestingではポジションを管理できますが、この関数で現在のポジションを決済することができます。チャネルブレイクアウトの例でいうと現在のポジションを決済してから、逆方向へドテンでエントリーするというロジックを表現することができています。
また試してはいないのですが以下のリンクのように backtesting.lib
に格納されているSignalStrategyやTrailingStrategyでシグナルベースやトレーリングストップによる注文処理を行うこともできるようです。[4]
Backtestクラス
DataFrame化したOHLCデータとStrategyクラスを使用したトレード戦略の定義が完了しました。もうBacktestクラスを呼び出してバックテストが行える状態です。Backtestクラスでは主に run
plot
optimize
という関数を使用します。以下は run
でEMA(指数平滑移動平均線)のゴールデンクロス・デッドクロスをバックテストした結果です。この指標は短期の5日間のEMAと長期の10日間のEMAのラインがクロスしたことをシグナルとします。 cash
はユニットですがここでは1 BTCと想定して1と入力しました。このトレード戦略に基づくと最終的なEquity Final [$]は83 BTCほどとなるようです。ほぼ億り人ですねw
backtest.run
のオプションをいくつかピックアップして説明しておきます。[5]
class Backtest(data, strategy, *, cash=10000, commission=0.0, margin=1.0, trade_on_close=False, hedging=False, exclusive_orders=False)
margin
はレバレッジ取引を指定します。2倍のレバレッジの場合は0.5、4倍のレバレッジの場合は0.25と1を対象レバレッジで除算した比率で表現します。
trade_on_close
はトレードをどのタイミングで行うかです。BacktestingはDataFrameの各行ごとに処理されていきますが、 trade_on_close
をTrueにしておくと対象行のClose価格で注文を建ててくれます。デフォルトでは次の行のOpen価格での注文です。Close価格とOpen価格に顕著な乖離がある場合には、このオプションをTrueにするかどうか検討してください。
hedging
をTrueにすると両建てのポジションを許可できます。デフォルトでFalseですが、この場合に反対売買のオーダーを建てると先のトレードをFIFOで決済してから注文を行う動作となります。
exclusive_orders
はドキュメントに説明がありませんでした。しかしサンプルのJupyter Notebookの説明によると self.position.close()
を回避する指示のようです。※動作未確認
直近のバックテストの結果を描画してくれる関数が backtest.plot
です。この関数も複数のオプションがありますが、素で呼び出すだけで十分な気がします。以下は bt_ema.plot()
と実行してオプションなしで呼び出した結果です。 backtest.plot
のオプションには以下のようなものが存在します。
def plot(self, *, results=None, filename=None, plot_width=None, plot_equity=True, plot_pl=True, plot_volume=True, plot_drawdown=False, smooth_equity=False, relative_equity=True, superimpose=True, resample=True, reverse_indicators=False, show_legend=True, open_browser=True)
Backtestクラスの最後は optimize
関数の紹介です。この関数はトレード戦略におけるパラメータを網羅的に走査することで指定する結果を最大化するパラメータによるバックテストを返します。ここでトレード戦略におけるパラメータとは指標を作成する際に使用したクラス変数などです。EMAの例では移動平均の日数を短期5日、長期10日と指定しましたが、この日数を変更することでリターン(Equity Final [$])が最大となるバックテストを発見し、実行することができます。実際に最適化を実行したところ、短期が6日、長期が16日のEMAによって102 BTCが最大化されたリターンとなることが分かりました。億を超えてきましたね。
def optimize(self, maximize=’SQN’, constraint=None, return_heatmap=False, **kwargs)
backtest.optimize
で考慮すべきオプションは maximize
constraint
の2つです。 maximize
には最大化したい要素を指定します。Equity Final [$] や SQN あたりが候補となるでしょう。 constraint
はパラメータに対する制限を指定できます。上記の例では短期のEMAが長期のEMAの期間より大きい値を取らないようにしています。 return_heatmap
については次のパラメータヒートマップで取り上げます。
パラメータヒートマップ
backtest.optimize
である要素を最大化するパラメータの組み合わせが分かりました。ここではEMAの例に return_heatmap
で各パラメータごとの結果を取得して分析する方法を適用してみます。内容は公式のこちらのサンプルとほぼ同じです。このトレード戦略は4つのパラメータを使用し、Jupyter Notebookでは最適なパラメータの組み合わせを可視化しています。[6]
このトレードは短期・長期のEMA以外にエントリーとイグジットに異なるEMAのトレンドライン、計4本のEMAを使用します。backtest.optimize
を呼び出す際に return_heatmap=True
を指定しておくとSeriesでマルチインデックスの最適化実行の結果が返されます。
seabornなどのライブラリで描画できる他 backtesting.lib
にもplot_heatmapsという描画用の関数が用意されています。
左が短期・長期とリターンの関係をseabornのヒートマップで描画したもので、右がplot_heatmapsへheatmapをそのまま渡して描画した図です。ヒートマップの濃淡によって良い結果が得られるパラメータの範囲や傾向が分かるようになりました。視覚化した分析を行いたい場合は return_heatmap
で取得した値が活用できます。
以上、OHLCVデータが基礎とはなりますがトレード手法をStrategyでコーディングしバックテストを実行することで、簡単に過去の実データに対するテスト結果やパラメータの最適化を行うことができました。この記事内で作成したJupyter Notebookはこちらのリポジトリに上げてあります。