前回、機械学習で解いた予測結果がどれだけ利益が出ているのか詳しく知ることが出来ませんでした。
そこで、Pythonを使った株価予想(バックテスト)ツールを調べてみます。
オンライン・サービス
QuantX
Smart Tradeのサービスであるクオンテックス(QuantX)という、株式投資をPythonで行うための無料プラットフォームがあります。
開発したアルゴリズムはSmart Tradeのマーケットプレイスでも販売できます。
都内の大学生をインターンシップに迎え、Qiita上に大量の記事を公開させています。
登録してバックテストするまでは分かりやすいですが、肝心のシステムトレード分析が浅い印象を受けます。
同時に販売されているストラテジーもイザナミなどと比べると・・・なイメージです。
とはいえ、いつか試そう。
バックテストライブラリ
pythonのアルゴリズムトレードライブラリをいくつか紹介します。
「The Top 21 Python Trading Tools for 2020」というサイトにPythonのトレードツールのTop21が紹介されていました。
21個もあるのか・・・と思いましたが、バックテスト可能なツール紹介は次の4種類です。
- Top1:Zipline(Quantopian提供)
- Top2:Backtrader
- Top3:LEAN(QuantConnect提供)
- Top4:Moonshot(QuantRocket)
また、日本人ブロガーが「簡単に使える!」とよく紹介しているのが次のライブラリです。
その他、backtraderが「代替ライブラリ」と紹介していたものは次の通りです。
- PyAlgoTrade
- pybacktest
- Ultra-Finance
- ProfitPy
- prophet
- quant
- AlephNull
- Trading with Python
- visualize-wealth
- tia: Toolkit for integration and analysis
- QuantSoftware Toolkit
- Pinkfish
- bt
- PyThalesians
- QSTrader
- QSForex
- pysystemtrade
- QTPyLib
- RQalpha
Googleトレンドでは次のような人気になっています。
「Backtrader」「Backtesting.py」が比較的人気のようです。
代表的なライブラリを簡単に紹介しつつ、「Backtrader」「Backtesting.py」を実際に使ってみます。
Zipline
githubでは一番星の数が多いライブラリです。
2018年7月16日が最新で「1.3.0」で、最近は更新がありません。
歴史も比較的長く、大規模なデータ分析でも可能です。
また、コミュニティは活発です。
LEAN
日本人ブロガーの紹介は皆無です。
2017年8月9日が最新で「2.4.0.1」です。
LEANエンジンのコアはC#で記述されているため高速で、かつマルチ言語(F#言語、R言語など)に対応しています。
バックテストだけでなく、実際の取引もサポートしています。
また、コミュニティサイトも活発で、複数のサンプル戦略も公開されています。
Moonshot(QuantRocket)
日本では、一人だけツイッターで呟いているだけのライブラリです。
RT
@fx_kirin
: Pandasを使った複数ロジックのバックテストができるし、IBに直接同じストラテジをライブ運用できるって書いてあってとても気になる。 // QuantRocket – Documentation and Usage Guide https://t.c…
2019年9月24日が最新で「1.9.1」です。活発です。
Ziplineによるバックテストもサポートしています。
が、このライブラリはデータ取得等が月額料金制となっています。
PyAlgoTrade
以前は最も利用されていると言われているバックテストライブラリです。
2018年8月20日が最新で「0.20」です。
まだマイナーバージョンアップしかしていませんが、サポートしている注文方法が豊富で成行注文はもちろんのこと、指値や逆指値なども使えます。
またドキュメントも豊富なので使いやすそうな印象を受けます。
Twitter動向、企業ニュースのスクレイピングなどもサポートしていると記載があります。
pybacktest
こちらも軽量なライブラリです。
2013年5月3日が最新で「0.1」、開発は止まってそうです。
テクニカル指標の計算などの機能は用意されてなく、シンプルに機能をまとめたライブラリです。
Backtesting.py
初心者におすすめなライブラリです。
2019年9月23日のライブラリが最新で「0.1.2」。
2019年1月17日にリリースされて3回のアップデートがありましたが、今は開発がストップしている可能性があります。
機能的にはかなり劣りますが、逆にシンプルなため初心者には手っ取り早く使えるライブラリです。
特徴として次があります。
- バックテストの処理がとても高速
- テクニカル指標のライブラリTa-Libをサポート
Backtesting.pyを利用してみる
インストールは簡単でした。
1 2 3 4 5 6 |
# # /c/Python38/Scripts/pip install backtesting Collecting backtesting Downloading https://files.pythonhosted.org/packages/cb/08/78f3cddd05554662737a05a085adaa2aec492272d5cc30cec519b6acde3b/Backtesting-0.1.2.tar.gz (158kB) Requirement already satisfied: numpy in c:\python38\lib\site-packages (from backtesting) (1.17.4) Requirement already satisfied: pandas!=0.25.0,>=0.21.0 in c:\python38\lib\site-packages (from backtesting) (0.25.3) ...(省略) |
次にバックテストに必要な時系列の価格データを用意します。
次の条件を満たす必要があります。
- pandas DataFrame形式であること
- ‘Open’, ‘High’, ‘Low’, ‘Close’, 列を持っている事(’Volume’はオプション)
- indexはpandasのDatetime形式
- 他の列は存在していても問題なし
Protraのデータがそのまま使えそうです。
次のようなコードを書きました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import datetime import pandas as pd from backtesting import Strategy from backtesting import Backtest class myCustomStrategy(Strategy): def init(self): pass def next(self): self.buy if self.data.Close[-1]> self.data.Open[-1] else self.sell() df = pd.read_csv("7203.csv", skiprows=2000, names=("Datetime", "Open", "High", "Low", "Close", "Volume"), index_col='Datetime') # 欠損値を埋める df = df.interpolate() df.index = pd.to_datetime(df.index) bt = Backtest(df, myCustomStrategy, cash=100000, commission=.001) bt.run() # bt.plot() |
Backtestクラスのインスタンスを作成して、run()を呼ぶだけです。
コンストラクタの引数には、テストで利用するデータ(df)と、上記で作成したcustom strategyクラス、cash(予算)、comission(売買手数料)(0~10%で指定可能)を指定します。
予算は10万円を、手数料は0.1%としています。
結果は次のように出力されます。
BokehJS 1.4.0を使っているようです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
Start 2006-02-20 00:00:00 # 対象期間の開始日 End 2020-01-24 00:00:00 # 対象期間の終了日 Duration 5086 days 00:00:00 # バックテスト実施期間 Exposure [%] 99.9214 # ポジションを持っていた期間の割合 Equity Final [$] 4169.13 # 所持金の最終値 Equity Peak [$] 116167 # 所持金の最高値 Return [%] -95.8309 # 利益率 Buy & Hold Return [%] 23.3856 # (終了時の終値 - 開始時の終値)÷ 開始時の終値 Max. Drawdown [%] -96.4111 # 最大ドローダウン Avg. Drawdown [%] -9.75228 # 平均ドローダウン Max. Drawdown Duration 848 days 00:00:00 # 最大ドローダウン期間 Avg. Drawdown Duration 122 days 00:00:00 # 平均ドローダウン期間 # Trades 1805 # 全トレード数 Win Rate [%] 53.4626 # 勝率=勝ち取引回数÷全取引回数×100 Best Trade [%] 17.5874 # 1回の取引での利益の最大値÷所持金×100 Worst Trade [%] -20.7905 # 1回の取引での損失の最大値÷所持金×100 Avg. Trade [%] -0.159077 # 損益の平均値÷所持金×100 Max. Trade Duration 15 days 00:00:00 # 1回の取引での最長期間 Avg. Trade Duration 3 days 00:00:00 # 1回の取引での平均期間 Expectancy [%] 1.68481 # 期待値=平均利益×勝率+平均損失×敗率 SQN -1.38561 # システムの評価値 Sharpe Ratio -0.0622533 # シャープ・レシオ # 利益とリスクの比率のことで、値が大きいほど資産曲線がなめらかになり安定性のある利益が見込める Sortino Ratio -0.0695105 # ソルティノレシオ # シャープレシオだけでは分からない下方リスクの抑制度合いを判断する場合に使われる Calmar Ratio -0.00164999 # カルマー比 # 値が低いほど指定された期間に渡ってリスク調整ベースで実行された投資は悪化 _strategy myCustomStrategy #ストラテジー名 dtype: object |
SQN(SystemQualityNumber)とは、Van K. Tharp氏によって定義された取引システムの分類(評価)方法のようです。
SQN値によって下記のように分類され、取引数が30以上の場合、SQN値は信頼できるとみなされています。
# 1.6-1.9:平均以下
# 2.0-2.4:平均
# 2.5-2.9:良い
# 3.0-5.0:素晴らしい
# 5.1-6.9:最高
# 7.0~:聖杯?
グラフを出力する場合は次を呼びます(bt.run()の出力は表示されなくなります)
1 |
bt.plot() |
かなり簡単にバックテストが可能です。
ちょっと試すなら、このライブラリが良さそうです。
Backtrader
PyAlgoTraderと同様に海外で人気のライブラリです。
2019年5月30日のライブラリが最新で「1.9.74.123」です。
アップデートも多く、ドキュメントも豊富&コミュニティも非常に活発です。
また、インジケータの種類が多く、取引量などの設定も可能です。
Backtraderを利用してみる
インストールを行います。
1 2 3 4 5 |
# # /c/Python38/Scripts/pip install backtrader Collecting backtrader Downloading https://files.pythonhosted.org/packages/a6/35/6ed3fbb771712d457011680970f3f0bcf38bfbc4cedd447d62705a6523c8/backtrader-1.9.74.123-py2.py3-none-any.whl (411kB) Installing collected packages: backtrader ...(省略) |
売買のソースコードとしてBacktesting.pyの出力を真似て実装されたものを見つけました。
[引用] 『Backtrader』でFXのバックテストをする!:Python
世の中に公開されているサンプルより初動が早そうなので、こちらを使って日本株のバックテストを行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
%%time # ↑セルの処理時間を計算 セルの最初に単独で書く import os # ディレクトリ作成、パス結合、ファイル削除 import pandas as pd # pandasデータフレームを使用 dir_name = '.' # ヒストリカルデータのフォルダ名 input_csv = os.path.join(os.getcwd(), dir_name, '7203.csv') # csvファイルのフルパス df = pd.read_csv(input_csv, skiprows=2000, names=("DateTime", "Open", "High", "Low", "Close", "Volume")) # csvファイルをPandasデータフレームに読み込む display(df.head(-10)) #日時列をdatatime型にしてインデックスにして、元の列は削除する df = df.set_index(pd.to_datetime(df['DateTime'])).drop('DateTime', axis=1) import backtrader as bt # Backtrader import backtrader.feeds as btfeed # データ変換 data = btfeed.PandasData(dataname=df) # PandasのデータをBacktraderの形式に変換する class myStrategy(bt.Strategy): # ストラテジー n1 = 10 # 終値のSMA(単純移動平均)の期間 n2 = 30 # 終値のSMA(単純移動平均)の期間 def log(self, txt, dt=None, doprint=False): # ログ出力用のメソッド if doprint: print('{0:%Y-%m-%d %H:%M:%S}, {1}'.format( dt or self.datas[0].datetime.datetime(0), txt )) def __init__(self): # 事前処理 self.sma1 = bt.indicators.SMA(self.data.close, period=self.n1) # SMA(単純移動平均)のインジケータを追加 self.sma2 = bt.indicators.SMA(self.data.close, period=self.n2) # SMA(単純移動平均)のインジケータを追加 self.crossover = bt.indicators.CrossOver(self.sma1, self.sma2) # sma1がsma2を上回った時に1、下回ったときに-1を返す def next(self): # 行ごとに呼び出される if self.crossover > 0: # SMA1がSMA2を上回った場合 if self.position: # ポジションを持っている場合 self.close() # ポジションをクローズする self.buy() # 買い発注 elif self.crossover < 0: # SMA1がSMA2を下回った場合 if self.position: # ポジションを持っている場合 self.close() # ポジションをクローズする self.sell() # 売り発注 def notify_order(self, order): # 注文のステータスの変更を通知する if order.status in [order.Submitted, order.Accepted]: # 注文の状態が送信済or受理済の場合 return # 何もしない if order.status in [order.Completed]: # 注文の状態が完了済の場合 if order.isbuy(): # 買い注文の場合 self.log('買い約定, 取引量:{0:.2f}, 価格:{1:.2f}, 取引額:{2:.2f}, 手数料:{3:.2f}'.format( order.executed.size, # 取引量 order.executed.price, # 価格 order.executed.value, # 取引額 order.executed.comm # 手数料 ), dt=bt.num2date(order.executed.dt), # 約定の日時をdatetime型に変換 doprint=True # Trueの場合出力 ) elif order.issell(): # 売り注文の場合 self.log('売り約定, 取引量:{0:.2f}, 価格:{1:.2f}, 取引額:{2:.2f}, 手数料:{3:.2f}'.format( order.executed.size, # 取引量 order.executed.price, # 価格 order.executed.value, # 取引額 order.executed.comm # 手数料 ), dt=bt.num2date(order.executed.dt), # 約定の日時をdatetime型に変換 doprint=True # Trueの場合ログを出力する ) # 注文の状態がキャンセル済・マージンコール(証拠金不足)・拒否済の場合 elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('注文 キャンセル・マージンコール(証拠金不足)・拒否', doprint=True ) def notify_trade(self, trade): # 取引の開始/更新/終了を通知する if trade.isclosed: # トレードが完了した場合 self.log('取引損益, 総額:{0:.2f}, 純額:{1:.2f}'.format( trade.pnl, # 損益 trade.pnlcomm # 手数料を差し引いた損益 ), doprint=True # Trueの場合ログを出力する ) # バックテストの設定 cerebro = bt.Cerebro() # Cerebroエンジンをインスタンス化 cerebro.addstrategy(myStrategy) # ストラテジーを追加 data = btfeed.PandasData(dataname=df) # Cerebro形式にデータを変換 cerebro.adddata(data) # データをCerebroエンジンに追加 cerebro.broker.setcash(10000.0) # 所持金を設定 cerebro.broker.setcommission(commission=0.0005) # 手数料(スプレッド)を0.05%に設定 cerebro.addsizer(bt.sizers.PercentSizer, percents=50) # デフォルト(buy/sellで取引量を設定していない時)の取引量を所持金に対する割合で指定する startcash = cerebro.broker.getvalue() # 開始時の所持金 cerebro.broker.set_coc(True) # 発注時の終値で約定する # 解析の設定 import backtrader.analyzers as btanalyzers # バックテストの解析用ライブラリ cerebro.addanalyzer(btanalyzers.DrawDown, _name='myDrawDown') # ドローダウン cerebro.addanalyzer(btanalyzers.SQN, _name='mySQN') # SQN cerebro.addanalyzer(btanalyzers.TradeAnalyzer, _name='myTradeAnalyzer') # トレードの勝敗等の結果 thestrats = cerebro.run() # バックテストを実行 thestrat = thestrats[0] # 解析結果の取得 # 評価値の表示 print('Start :{0:%Y/%m/%d %H:%M:%S}'.format( # ヒストリカルデータの開始日時 pd.to_datetime(df.index.values[0]) )) print('End :{0:%Y/%m/%d %H:%M:%S}'.format (# ヒストリカルデータの開始日時 pd.to_datetime(df.index.values[-1]) )) print('Duration :{0}'.format( # ヒストリカルデータの期間の長さ pd.to_datetime(df.index.values[-1]) - pd.to_datetime(df.index.values[0]) )) print('Exposure[%] :{0:.2f}'.format( # ポジションを持っていた期間の割合(ポジションを持っていた期間÷全期間×100) thestrat.analyzers.myTradeAnalyzer.get_analysis().len.total / len(df) * 100 )) print('Equity Final[$] :{0:.2f}'.format( # 所持金の最終値(closedした取引) startcash + thestrat.analyzers.myTradeAnalyzer.get_analysis().pnl.net.total )) print('Return[%] :{0:.2f}'.format( # 利益率=損益÷開始時所持金×100 thestrat.analyzers.myTradeAnalyzer.get_analysis().pnl.net.total / startcash * 100 )) print('Buy & Hold Return[%] :{0:.2f}'.format( # ((終了時の終値 - 開始時の終値)÷ 開始時の終値)の絶対値×100 (df['Close'][-1] - df['Close'][0]) / df['Close'][0] * 100 )) print('Max. Drawdown[%] :{0:.2f}'.format( # 最大下落率 thestrat.analyzers.myDrawDown.get_analysis().max.drawdown )) print('Max. Drawdown Duration:{0}'.format( # 最大下落期間 pd.Timedelta(minutes=thestrat.analyzers.myDrawDown.get_analysis().max.len) )) print('Trades :{0}'.format( # 取引回数 thestrat.analyzers.myTradeAnalyzer.get_analysis().total.closed )) winrate = ( # 勝率 thestrat.analyzers.myTradeAnalyzer.get_analysis().won.total / thestrat.analyzers.myTradeAnalyzer.get_analysis().total.closed ) lostrate = ( # 敗率 thestrat.analyzers.myTradeAnalyzer.get_analysis().lost.total / thestrat.analyzers.myTradeAnalyzer.get_analysis().total.closed ) print('Win Rate[%] :{0:.2f}'.format( # 勝率=勝ち取引回数÷全取引回数×100 winrate * 100 )) print('Best Trade[%] :{0:.2f}'.format( # 1回の取引での利益の最大値÷所持金×100 thestrat.analyzers.myTradeAnalyzer.get_analysis().won.pnl.max / startcash * 100 )) print('Worst Trade[%] :{0:.2f}'.format( # 1回の取引での損失の最大値÷所持金×100 thestrat.analyzers.myTradeAnalyzer.get_analysis().lost.pnl.max / startcash * 100 )) print('Avg. Trade[%] :{0:.2f}'.format( # 損益の平均値÷所持金×100 thestrat.analyzers.myTradeAnalyzer.get_analysis().pnl.net.average / startcash * 100 )) print('Max. Trade Duration :{0}'.format( # 1回の取引での最長期間 pd.Timedelta(minutes=thestrat.analyzers.myTradeAnalyzer.get_analysis().len.max) )) print('Avg. Trade Duration :{0}'.format( # 1回の取引での平均期間 pd.Timedelta(minutes=thestrat.analyzers.myTradeAnalyzer.get_analysis().len.average) )) print('Expectancy[%] :{0:.2f}'.format( # 期待値=平均利益×勝率+平均損失×敗率 thestrat.analyzers.myTradeAnalyzer.get_analysis().won.pnl.average * winrate + thestrat.analyzers.myTradeAnalyzer.get_analysis().lost.pnl.average * lostrate )) print('SQN :{0:.2f}'.format( # SQN システムの評価値 thestrat.analyzers.mySQN.get_analysis().sqn )) # グラフの設定 %matplotlib inline # ↑グラフをNotebook内に描画する import matplotlib.pylab as pylab # グラフ描画用ライブラリ pylab.rcParams['figure.figsize'] = 12, 8 # グラフのサイズ cerebro.plot( style = 'candle', # ロウソク表示にする barup = 'green', barupfill = False, # 陽線の色、塗りつぶし設定 bardown = 'red', bardownfill = False, # 陰線の色、塗りつぶし設定 fmt_x_data = '%Y-%m-%d %H:%M:%S' # 時間軸のマウスオーバー時の表示フォーマット ) |
出力結果です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
2006-05-12 00:00:00, 売り約定, 取引量:-0.78, 価格:6370.00, 取引額:-5000.00, 手数料:2.50 2006-07-07 00:00:00, 買い約定, 取引量:0.78, 価格:5980.00, 取引額:-5000.00, 手数料:2.35 2006-07-07 00:00:00, 買い約定, 取引量:0.78, 価格:5980.00, 取引額:4693.88, 手数料:2.35 2006-07-10 00:00:00, 取引損益, 総額:306.12, 純額:301.28 2006-07-24 00:00:00, 売り約定, 取引量:-0.78, 価格:5780.00, 取引額:4693.88, 手数料:2.27 2006-07-24 00:00:00, 売り約定, 取引量:-0.78, 価格:5780.00, 取引額:-4536.89, 手数料:2.27 ... 2020-01-07 00:00:00, 売り約定, 取引量:-0.78, 価格:7715.00, 取引額:6076.14, 手数料:3.03 2020-01-07 00:00:00, 売り約定, 取引量:-0.78, 価格:7715.00, 取引額:-6055.73, 手数料:3.03 2020-01-08 00:00:00, 取引損益, 総額:-20.41, 純額:-26.47 2020-01-23 00:00:00, 買い約定, 取引量:0.78, 価格:7836.00, 取引額:-6055.73, 手数料:3.08 2020-01-23 00:00:00, 買い約定, 取引量:0.78, 価格:7836.00, 取引額:6150.71, 手数料:3.08 2020-01-24 00:00:00, 取引損益, 総額:-94.98, 純額:-101.08 Duration :5086 days 00:00:00 Exposure[%] :98.33 Equity Final[$] :6191.55 Return[%] :-38.08 Buy & Hold Return[%] :23.39 Max. Drawdown[%] :48.84 Max. Drawdown Duration:2 days 02:38:00 Trades :147 Win Rate[%] :32.65 Best Trade[%] :14.45 Worst Trade[%] :-6.71 Avg. Trade[%] :-0.26 Max. Trade Duration :0 days 01:45:00 Avg. Trade Duration :0 days 00:22:48.163265 Expectancy[%] :-25.91 SQN :-1.03 |
グラフも出力可能です・・・・赤い。
その他、株関連ライブラリ
TA-Lib
テクニカル分析における代表的な指標を算出することができます。
既に紹介済です。
Quandl
金融、経済などの数値データの検索エンジンで、さまざまなソースから得られたデータを検索し、グラフや表を表示させることができます。
まとめ
比較的簡単にバックテストを行える事は分かりました。
が、Protraを改造する必要があったように他視点から分析するには、大幅な改造が必要な気がします。
独自実装している声はよく聞きますが、満足するフレームワーク作った頃に大体において飽きが来るので躊躇しています。