中国・武漢で発生した「新型コロナウイルス(新型肺炎)」が、世界に拡散中です。
これにより投資家心理を圧迫し、アジアの主な株式市場が春節(旧正月)で休場となり商いが薄いなか、日本株のヘッジ売りが目立つ状況です(483円安)。
会社の取引先の社員もコロナウイルスに感染したらしく、社内でもマスク人口が拡大しています。
Pythonのバックテストライブラリ「Backtrader」は開発・コミュニティも活発で自由度も高そうです。
ですが、海外含めてマトモにストラテジー組んで運用している人は少ないのでは?
と思うほどサンプルを超えた情報が少ないです。
FX用なのか、ショート・ロング、pipsなどの情報もあり、複数銘柄売買もよく分かりません。
まず知りたいのは、次の機能です。
- 機械学習済のデータのバックテストが可能か?
加えて、バックテスト結果を見るために大事なのは、
「Protraレベルの分析機能を持っているのか?」
だと思ってます。
- バックテスト結果の各種計算算出
- 複数銘柄にまたがるバックテスト
- 市場全体の傾向指標の追加(騰落レシオ等)
- 空売りや指値買い
- 単利計算、複利計算
この辺りを調査するために、Backtraderを利用してみます。
- 【3回目】機械学習で株価予測(Pythonのバックテストライブラリ調査)
- 【2回目】機械学習で株価予測(TA-LibとLightGBMを使った学習モデル構築)
- 【1回目】機械学習で株価予測(Two SigmaのKaggleコンペを確認)
ただし、のめり込むと目的(学習データの利益確認)と施策(Backtraderの理解)が変わってしまうので注意です。
※ 私はFFS理論のD因子(拡散型)タイプ
sklearnの学習データの保存&ロード
今回は学習済みのモデルを利用する方法の一つとして pickle を扱い方を調べてみます。
学習済みモデルを保存する
1 2 3 4 5 |
import pickle def save_model(clf): with open('model.pickle', mode='wb') as fp: pickle.dump(clf, fp, protocol=2) |
30MB程度のファイルが出来ました。
別のバージョンの Python からも読み込むことが考えられるときは protocol オプションに 2 を指定します。
これでPython 2.3 以降であれば保存したモデルを復元できるようになります。
学習済みモデルをロードする
ロードも簡単です。
1 2 3 4 5 |
def load_model(): clf = None with open('model.pickle', mode='rb') as fp: clf = pickle.load(fp) return clf |
実際にロードした学習データで計算可能な事が確認できました。
Backtraderでpickleの利用方法
・・・。
調べてもサンプルがありません。
Backtraderの売買方法を完全に理解すれば可能だと思いますが・・・目的と施策が変わってしまう・・・。
Backtraderで複数銘柄の扱い方
可能なようです。
bactrader_sample.pyを見る限り、銘柄数読み込んで「cerebro.adddata」に渡しているように見えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
df1 = pd.read_csv(input_csv1, skiprows=2000, names=("DateTime", "Open", "High", "Low", "Close", "Volume")) # csvファイルをPandasデータフレームに読み込む df2 = pd.read_csv(input_csv2, skiprows=2000, names=("DateTime", "Open", "High", "Low", "Close", "Volume")) # csvファイルをPandasデータフレームに読み込む #display(df.head(-10)) #日時列をdatatime型にしてインデックスにして、元の列は削除する df1 = df1.set_index(pd.to_datetime(df1['DateTime'])).drop('DateTime', axis=1) df2 = df2.set_index(,pd.to_datetime(df2['DateTime'])).drop('DateTime', axis=1) data1 = btfeed.PandasData(dataname=df1) # PandasのデータをBacktraderの形式に変換する data2 = btfeed.PandasData(dataname=df2) # PandasのデータをBacktraderの形式に変換する data1 = btfeed.PandasData(dataname=df1) # Cerebro形式にデータを変換 data2 = btfeed.PandasData(dataname=df2) # Cerebro形式にデータを変換 cerebro.adddata(data1) # データをCerebroエンジンに追加 cerebro.adddata(data2) # データをCerebroエンジンに追加 |
ただ、読み込んだ銘柄数だけグラフ表示が別になります。
Multi Exampleでは、フォルダ内のCSVを読み込んで利用する方法が書かれています。
結構、面倒なんだね。
Backtraderを使った学習データ利用
ドキュメントは下記にあります。
が、説明が少なくて何の数値が算出されているのか分かりにくいです。
Protra風に出力する
とりあえずやってみました。
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 |
import pprint def convert_to_protra(thestrat, df): ana = thestrat.analyzers.myTradeAnalyzer.get_analysis() sqn = thestrat.analyzers.mySQN.get_analysis() dd = thestrat.analyzers.myDrawDown.get_analysis() anr = thestrat.analyzers.myAnnualReturn.get_analysis() # Python3.6以降のfプリフィクスを用いた書き方(簡潔・低負荷) print() print(f'{pd.to_datetime(df.index.values[0]):%Y/%m/%d}~{pd.to_datetime(df.index.values[-1]):%Y/%m/%d}における成績です。') print("----------------------------------------") print(f'全トレード数 {ana.total.closed:}') print(f'勝ちトレード数(勝率) {ana.won.total}({ana.won.total / ana.total.closed * 100:.2f}%)') print(f'負けトレード数(負率) {ana.lost.total}({ana.lost.total / ana.total.closed * 100:.2f}%)') print() print(f'全トレード平均利益 {ana.pnl.net.average:,.0f}円') print(f'勝ちトレード平均利益 {ana.won.pnl.average:,.0f}円') print(f'負けトレード平均損益 {ana.lost.pnl.average:,.0f}円') print() print(f'勝ちトレード最大利率 ') print(f'負けトレード最大損率 ') print() print(f'全トレード平均期間 {ana.len.average:.2f}') print(f'勝ちトレード平均期間 {ana.len.won.average:.2f}') print(f'負けトレード平均期間 {ana.len.lost.average:.2f}') print("----------------------------------------") print(f'必要資金 {startcash:,.0f}円') print(f'最大ポジション(簿価) ') print(f'最大ポジション(時価) ') print() print(f'純利益 {ana.pnl.net.total:,.0f}円') print(f'勝ちトレード総利益 {ana.won.pnl.total:,.0f}円') print(f'負けトレード総損失 {ana.lost.pnl.total:,.0f}円') print() print(f'全トレード平均利益 {ana.pnl.net.average:,.0f}円') print(f'勝ちトレード平均利益 {ana.won.pnl.average:,.0f}円') print(f'負けトレード平均損失 {ana.lost.pnl.average:,.0f}円') print() print(f'勝ちトレード最大利益 {ana.won.pnl.max:,.0f}円') print(f'負けトレード最大損益 {ana.lost.pnl.max:,.0f}円') print() print(f'プロフィットファクター {abs(ana.won.pnl.total / ana.lost.pnl.total):.2f}') print(f'最大ドローダウン(簿価) {dd.moneydown:,.0f}円') print(f'最大ドローダウン(時価) {dd.max.moneydown:,.0f}円') print("----------------------------------------") print(f'現在進行中のトレード数 {ana.total.open}') print("----------------------------------------") print(f'平均年利 {ana.pnl.net.total / startcash * 100:,.2f}%') print(f'最大連勝 ') print(f'最大連敗 ') print("----------------------------------------") print(f'SQN(システムの評価値) {sqn.sqn:.2f}') print("----------------------------------------") print(f'[年度別レポート] ') print(f'年度 取引回数 運用損益 年利 勝率 PF 最大DD') for year in anr: print(f'{year}年\t-\t\t{anr[year] * startcash:,.0f}円 ') print() print() #pprint.pprint(thestrat.analyzers.myTradeAnalyzer.get_analysis()) import backtrader.analyzers as btanalyzers # バックテストの解析用ライブラリ cerebro.addanalyzer(btanalyzers.DrawDown, _name='myDrawDown') # ドローダウン cerebro.addanalyzer(btanalyzers.SQN, _name='mySQN') # SQN cerebro.addanalyzer(btanalyzers.TradeAnalyzer, _name='myTradeAnalyzer') # トレードの勝敗等の結果 cerebro.addanalyzer(btanalyzers.AnnualReturn, _name='myAnnualReturn') # 年次報告 thestrats = cerebro.run() # バックテストを実行 thestrat = thestrats[0] # 解析結果の取得 convert_to_protra(thestrat, df) |
結果は次のようになります。
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 |
2006/02/20~2020/01/24における成績です。 ---------------------------------------- 全トレード数 147 勝ちトレード数(勝率) 48(32.65%) 負けトレード数(負率) 99(67.35%) 全トレード平均利益 -26円 勝ちトレード平均利益 301円 負けトレード平均損益 -185円 勝ちトレード最大利率 負けトレード最大損率 全トレード平均期間 22.80 勝ちトレード平均期間 40.15 負けトレード平均期間 14.39 ---------------------------------------- 必要資金 10,000円 最大ポジション(簿価) 最大ポジション(時価) 純利益 -3,808円 勝ちトレード総利益 14,465円 負けトレード総損失 -18,273円 全トレード平均利益 -26円 勝ちトレード平均利益 301円 負けトレード平均損失 -185円 勝ちトレード最大利益 1,445円 負けトレード最大損益 -671円 プロフィットファクター 0.79 最大ドローダウン(簿価) 5,549円 最大ドローダウン(時価) 5,746円 ---------------------------------------- 現在進行中のトレード数 1 ---------------------------------------- 平均年利 -38.08% 最大連勝 最大連敗 ---------------------------------------- SQN(システムの評価値) -1.03 ---------------------------------------- [年度別レポート] 年度 取引回数 運用損益 年利 勝率 PF 最大DD 2006年 - 504円 2007年 - 323円 2008年 - -277円 2009年 - -1,702円 2010年 - -311円 2011年 - -595円 2012年 - 1,180円 2013年 - 358円 2014年 - -802円 2015年 - 1,269円 2016年 - -213円 2017年 - -166円 2018年 - -1,969円 2019年 - -1,489円 2020年 - -124円 |
まだ知識が乏しいので合っているかどうか分かりません。
が、なんかやり切った感です。
まとめ
出力結果がProtraっぽくなりました。
よし、じゃあ三点チャージ法作ったり、今までのストラテジー移植方法でも調べるか!
と、売買部分を調査し始めて気づきました。
肝心の「機械学習の結果を用いたバックテスト」が分かっていない・・・
この施策だと目的達成には遠そうな気がします。
・・・
本質を見失わないようにしよう。
うん、そうだ!
よく気づいた自分!
・・・で、更に上位の目的は何だっけ?
さらに、機械学習で株価予想するのは何のためだっけ?
そもそも、株価予想は何のためだっけ?
あれ??
ソースコード
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 189 190 191 192 193 194 195 196 197 198 199 |
%%time # ↑セルの処理時間を計算 セルの最初に単独で書く import os # ディレクトリ作成、パス結合、ファイル削除 import pandas as pd # pandasデータフレームを使用 dir_name = '.' # ヒストリカルデータのフォルダ名 input_csv1 = os.path.join(os.getcwd(), dir_name, '7203.csv') # csvファイルのフルパス input_csv2 = os.path.join(os.getcwd(), dir_name, '6758.csv') # csvファイルのフルパス df1 = pd.read_csv(input_csv1, skiprows=2000, names=("DateTime", "Open", "High", "Low", "Close", "Volume")) # csvファイルをPandasデータフレームに読み込む df2 = pd.read_csv(input_csv2, skiprows=2000, names=("DateTime", "Open", "High", "Low", "Close", "Volume")) # csvファイルをPandasデータフレームに読み込む #display(df.head(-10)) #日時列をdatatime型にしてインデックスにして、元の列は削除する df1 = df1.set_index(pd.to_datetime(df1['DateTime'])).drop('DateTime', axis=1) df2 = df2.set_index(pd.to_datetime(df2['DateTime'])).drop('DateTime', axis=1) import backtrader as bt # Backtrader import backtrader.feeds as btfeed # データ変換 data1 = btfeed.PandasData(dataname=df1) # PandasのデータをBacktraderの形式に変換する data2 = btfeed.PandasData(dataname=df2) # 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('買い約定, 銘柄{4}, 取引量:{0:.2f}, 価格:{1:.2f}, 取引額:{2:.2f}, 手数料:{3:.2f}'.format( order.executed.size, # 取引量 order.executed.price, # 価格 order.executed.value, # 取引額 order.executed.comm, # 手数料 order.data._name, ), dt=bt.num2date(order.executed.dt), # 約定の日時をdatetime型に変換 doprint=True # Trueの場合出力 ) elif order.issell(): # 売り注文の場合 self.log('売り約定, 銘柄{4}, 取引量:{0:.2f}, 価格:{1:.2f}, 取引額:{2:.2f}, 手数料:{3:.2f}'.format( order.executed.size, # 取引量 order.executed.price, # 価格 order.executed.value, # 取引額 order.executed.comm, # 手数料 order.data._name, ), 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) # ストラテジーを追加 data1 = btfeed.PandasData(dataname=df1) # Cerebro形式にデータを変換 data2 = btfeed.PandasData(dataname=df2) # Cerebro形式にデータを変換 cerebro.adddata(data1) # データをCerebroエンジンに追加 cerebro.adddata(data2) # データを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 # バックテストの解析用ライブラリ #from bt_analyzers import stats cerebro.addanalyzer(btanalyzers.DrawDown, _name='myDrawDown') # ドローダウン cerebro.addanalyzer(btanalyzers.SQN, _name='mySQN') # SQN cerebro.addanalyzer(btanalyzers.TradeAnalyzer, _name='myTradeAnalyzer') # トレードの勝敗等の結果 cerebro.addanalyzer(btanalyzers.AnnualReturn, _name='myAnnualReturn') # 年次報告 #cerebro.addanalyzer(btanalyzers.PyFolio, _name='myPyFolio') # 年次報告 #cerebro.addanalyzer(stats.ForexStats, _name='myForexStats') thestrats = cerebro.run() # バックテストを実行 thestrat = thestrats[0] # 解析結果の取得 # グラフの設定 %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' # 時間軸のマウスオーバー時の表示フォーマット ) import pprint def convert_to_protra(thestrat, df): ana = thestrat.analyzers.myTradeAnalyzer.get_analysis() sqn = thestrat.analyzers.mySQN.get_analysis() dd = thestrat.analyzers.myDrawDown.get_analysis() anr = thestrat.analyzers.myAnnualReturn.get_analysis() # Python3.6以降のfプリフィクスを用いた書き方(簡潔・低負荷) print() print(f'{pd.to_datetime(df.index.values[0]):%Y/%m/%d}~{pd.to_datetime(df.index.values[-1]):%Y/%m/%d}における成績です。') print("----------------------------------------") print(f'全トレード数 {ana.total.closed:}') print(f'勝ちトレード数(勝率) {ana.won.total}({ana.won.total / ana.total.closed * 100:.2f}%)') print(f'負けトレード数(負率) {ana.lost.total}({ana.lost.total / ana.total.closed * 100:.2f}%)') print() print(f'全トレード平均利益 {ana.pnl.net.average:,.0f}円') print(f'勝ちトレード平均利益 {ana.won.pnl.average:,.0f}円') print(f'負けトレード平均損益 {ana.lost.pnl.average:,.0f}円') print() print(f'勝ちトレード最大利率 ') print(f'負けトレード最大損率 ') print() print(f'全トレード平均期間 {ana.len.average:.2f}') print(f'勝ちトレード平均期間 {ana.len.won.average:.2f}') print(f'負けトレード平均期間 {ana.len.lost.average:.2f}') print("----------------------------------------") print(f'必要資金 {startcash:,.0f}円') print(f'最大ポジション(簿価) ') print(f'最大ポジション(時価) ') print() print(f'純利益 {ana.pnl.net.total:,.0f}円') print(f'勝ちトレード総利益 {ana.won.pnl.total:,.0f}円') print(f'負けトレード総損失 {ana.lost.pnl.total:,.0f}円') print() print(f'全トレード平均利益 {ana.pnl.net.average:,.0f}円') print(f'勝ちトレード平均利益 {ana.won.pnl.average:,.0f}円') print(f'負けトレード平均損失 {ana.lost.pnl.average:,.0f}円') print() print(f'勝ちトレード最大利益 {ana.won.pnl.max:,.0f}円') print(f'負けトレード最大損益 {ana.lost.pnl.max:,.0f}円') print() print(f'プロフィットファクター {abs(ana.won.pnl.total / ana.lost.pnl.total):.2f}') print(f'最大ドローダウン(簿価) {dd.moneydown:,.0f}円') print(f'最大ドローダウン(時価) {dd.max.moneydown:,.0f}円') print("----------------------------------------") print(f'現在進行中のトレード数 {ana.total.open}') print("----------------------------------------") print(f'平均年利 {ana.pnl.net.total / startcash * 100:,.2f}%') print(f'最大連勝 ') print(f'最大連敗 ') print("----------------------------------------") print(f'SQN(システムの評価値) {sqn.sqn:.2f}') print("----------------------------------------") print(f'[年度別レポート] ') print(f'年度 取引回数 運用損益 年利 勝率 PF 最大DD') for year in anr: print(f'{year}年\t-\t\t{anr[year] * startcash:,.0f}円 ') print() print() #pprint.pprint(thestrat.analyzers.myTradeAnalyzer.get_analysis()) #pprint.pprint(thestrat.analyzers.myAnnualReturn.get_analysis()) convert_to_protra(thestrat, df) |