目次。
- 目次。
- 先にこちらを読んでください。
- はじめに。
- LSTMで日経平均株価予測を行うプログラムを書いてみた話。
- 上場企業、約4000社の株価予測。
- 時系列データを実際に可視化してみる。
- ソースコード(プログラムコード)。
- おわりに。
この文章を読んで、面白い!役に立った!...と思った分だけ、投げ銭していただけると嬉しいです。
【宣伝】ギターも歌も下手だけど、弾き語りをやっているので、よければ聴いてください。
先にこちらを読んでください。
『株式自動売買プログラムを実践に投入すると精度が急落する謎現象の原因究明に奮闘していた話。』を先に読んでください。
特に...
- 原因部分:正規化処理(min-max normalization)を行うコード。
- 『【株式自動売買×ディープラーニング】LSTMで上場企業、約4000社の株価予測をしてみた話。』の考察。
を読んでください。
はじめに。
株式自動売買プログラム開発関連の文章は、以下のカテゴリーにまとめているので、興味のある方はどうぞ。
この文章を読む前に『株式自動売買とディープラーニング(ニューラルネットワーク)の話。』を読んで頂けると嬉しいです。
LSTMで日経平均株価予測を行うプログラムを書いてみた話。
前回、Long Short-Term Memory、略して、LSTMという再帰的ニューラルネットワーク(RNN)を改良したネットワークを使って、日経平均株価の予測を行った。詳しくは、以下の文章を参照のこと。
LSTMのハイパーパラメータ(入力データ長、隠れ層のパーセプトロン数とか)は、論文を参考にして決めたので、興味のある方は、『論文版はてなブックマーク(その10:ディープラーニング×株価予測)の話。』もどうぞ。
上場企業、約4000社の株価予測。
約4年間の日経平均株価を使って、LSTMを学習させ、上場企業、約4000社の株価予測(終値予測)をした。入力データから1営業日先を予測するパターン(1営業日後予測)と入力データから5営業日先を予測するパターン(5営業日後予測)の2パターン。5営業日後予測の場合、買った日から5営業日経つまで株式は保有したままとなる。
入力データから取引が行われなかった(出来高0の)日のデータは、あらかじめ全て取り除いた。また、出来高0のデータを取り除いた後、データ数が120営業日以下の銘柄は、今回の予測対象から取り除いた。
残ったのは、3948銘柄。それぞれの銘柄の~5年分の株価データ(日足)(始値、高値、低値、終値、出来高)を使って、株価予測を行った。
株価データの取得の仕方は、昔の記事で一般の人向けに紹介したので、よければどうぞ。
証券会社のwebサイトで株価チャートを見ていると、株価チャートが自動更新されることに気づくだろう。あれは、webサイトに埋め込まれているJavaScriptで書かれたスクリプトが証券会社のサーバと通信して、株価データを勝手に取ってくるからである。僕の文章で紹介したのは、スクリプトとサーバのHTTP通信を監視して、スクリプトが株価データを取ってきている場所を突き止め、そこから直接、データを取ってくるという方法。これに関しては、XMLHttpRequest (XHR)という言葉も調べてほしい。
値上がり・値下がり正答率。
「n営業日後の予測値から現在の株価(真値)を引いたものの傾きの符号」と「n営業日後の株価(真値)から現在の株価(真値)を引いたものの傾きの符号」が一緒ならば、値上がり・値下がり予測は、成功したと考える。
例えば、現在100円の株価が明日120円になると予測し、実際は110円になったとする。その場合、前者の傾きは20円/日、後者の傾きは10円/日となり、符号が一致しているので、値上がりを正しく予測することができている。
終値(真値)が現在とn営業日後で変わらない場合、その株価データ、予測値は、正答率計算から取り除いた。株価が値上がりすると予測して、株式を買って、次の日、株価が上がりも、下がりもしなかった場合、得も損もしない(手数料は考慮していない)。また、今のモデルの場合、株価が上昇も下落もしないという出力を出すのは、ほぼ不可能である(出力の型は、floatなので、「今日が100円で明日が100.000000円です」っていう出力を得るのは難しい)。
1営業日後予測。
正答率は、75%~80%くらい。思った以上に予測精度は高い。
5営業日後予測。
正答率は、80%~85%くらい。月曜日の株価から火曜日の株価を予測する(1営業日後予測)よりも、月曜日の株価から来週の月曜日の株価を予測する(5営業日後予測)の方が正答率は高い。
1営業日予測だと、明日、値上がりすると予想して、株価が値下がりすれば、予測失敗となる。一方で、5営業日後予測だと、たとえ明日、明後日、明々後日の株価が下落しようが、その後、上昇に転じ、最終的な株価が値上がりしていれば、予測成功と考える。1営業日予測だと、上昇・下落トレンド内のノイズを正確に予測しなければいけないのに対し、5営業日予測だと上昇・下落トレンドさえ、予測できればいいので、後者の方が高い正答率になったのだと思う。
元本増加率。
ある元本である銘柄を売り買いし、~5年後に元本が何倍になるか表した指標。ある銘柄を買って、売った後、再び買うときは、元本+先ほどの利益分の株式を購入する。そのため、ずっと予測が当たれば、元本は指数関数的に増加する。
例えば、100万円で銘柄Aを買って、120万円で売って、120万円で銘柄Aを買って、140万円で売ったとすると、銘柄Aの元本増加率は、(120/100)× (140/120)=1.4となる。
この元本増加率は、簡単のためにいくつかの仮定を置いている。
・仮定:1万円の元本で、一株99円の株式を1万円分買うことができる。
→現実:株式は、100株単位で売られていることがほとんど。1万円の元本で、99×100=9900円分の株式を保有することはできるが、元本ピッタリに保有することはできない。
・仮定:売買手数料・税金がかからない。
→現実:言わずもがな、どちらもかかる。
・売買注文は、一瞬で成立する。
→現実:売り手、買い手がいないと株式の取引は成立しない。流動性が低い銘柄の取引や大量の株式を取引することは、流動性が高い銘柄、少量の取引よりも難しい。
赤いヒストグラムがLSTMを使って、n日後の株価を予測し、それを元に株式売買すると、元本が何倍になるか表している。一方で、黒いヒストグラムはn日後の株価(真値)を未来予知(もちろん現実では、そんなことはできない)して、株式売買をしたときに元本が何倍になるか表している。
1営業日後予測。
完全に未来を予測できるならば、5年間、ある銘柄を売買すれば、おおよそ元本は、1000倍になる(黒いグラフ)。一方で、LSTMが予測した結果を使って、株式売買をした場合、おおよそ元本は、100倍になる(赤いグラフ)。実際は、「手元のお金を余らすことなく株式を買うことは不可能」、「手数料・税金がかかる」、「売買はすぐには成立しない」ので、この結果は、かなり過大評価されている。
グラフを見ると、元本増加率が倍の銘柄がある。 5年間、ある銘柄を売買し続ければ、元本が1円から1000兆円になるということである。もちろん、そういったことは起こりえない。このようなとんでもない結果は、上の3つの仮定の導入によって引き起こされる。この話に関しては、実際に時系列データを見ながら、もう一度、触れる。
5営業日後予測。
先ほどの結果とは違い、完全に未来を予測できる場合よりもLSTMでの予測結果を使った方が元本増加率は高い。
今回のアルゴリズムは、株価が上がるという予測が得られれば、株式を買い、株価が下がるという予測が得られれば、株式を売る。未来を完全に予測できる場合、細かな株価の変動に応答して、株式を売買してしまう。そして買った株式は、5営業日後まで保有し続ける。一度、小さな株価変動に反応して、株式を買うと、その後、どれだけ大きな利益チャンスが訪れようと、5営業日過ぎるまで対応することができない。
一方、LSTMの予測結果で株式売買した場合、小さな株価変動を捉えることはできないが、大きな株価のトレンドは捉えることができる。小さな利益チャンスには反応せず、大きな利益チャンス時のみに株式を買うことができるので、結果的に未来が完全に予測できるものよりも元本を増加させることができたのだと思う。
SN比。
株価予測値から現在の株価を引いたもの(株価の差分)を株価予測値から株価真値を引いたもの(予測の誤差)で割って絶対値をとったものをSN比と定義した。
例えば、「100円の株が明日、110円に値上がりする」と予測して、実際には120円になったとする。するとSN比は、(120-100)/(110-100)=2となる。
SN比が1より大きければ、予測の誤差は、株式の価格変動よりも小さく、SN比が1より小さければ、誤差が価格変動よりも大きくなって、予測の役に立たない。SN比は、大きい方がいい。
1営業日後予測。
SN比は、4くらい。株価の変動幅に対して、予測の誤差は4分の1くらい。
5営業日後予測。
SN比は、6くらい。株価の変動幅に対して、予測の誤差は6分の1くらい。入力データから5営業日後の株価を予測するので、当然、誤差は大きくなるが、現在と5営業日後の株価変動も同様に、現在と1営業日後の株価変動よりも大きくなるので、5営業日後の方がSNが高くなったのだと思う。
時系列データを実際に可視化してみる。
3948銘柄中、元本増加率が1000番目、2000番目、3000番目、3948番目(最下位)、1番目に高い銘柄の予測値と真値の時系列データを図示してみた。1番目に高い銘柄の元本増加率は、倍。これに関しては、正確に株価を予測できているとは言えないので、一番最後に回した。
グラフのタイトルは、銘柄コード。グラフの線の太さは、前回に比べてかなり細めにした。ぱっと見、グラフは見にくくなってしまったが、真値と予測値の株価の値上がり・値下がりタイミングがちゃんと一致しているのか、していないのかを目視で確認することができる。グリット線は、5営業日ごとに引いている。
元本増加率が1000番目に高い銘柄。
1営業日後予測。
5営業日後予測。
元本増加率が2000番目に高い銘柄。
1営業日後予測。
5営業日後予測。
元本増加率が3000番目に高い銘柄。
1営業日後予測。
5営業日後予測。
元本増加率が3948番目(最下位)に高い銘柄。
1営業日後予測。
5営業日後予測。
元本増加率が1番目に高い銘柄。
1営業日後予測。
5営業日後予測。
考察とか。
1営業日後予測の場合も、5営業日後予測の場合も元本増加率が1番目に高いのは、「1689 WisdomTree 天然ガス上場投資信託」。1営業日予測をもとに株式売買をすると、元本増加率が倍になる銘柄である。
倍という数字自体は、ありえない数字でもなくない。株価が前日よりも高くなる予測が得られれば、株式を買い、前日よりも低くなる予測が得られれば、株式を売る。こういった基準で株式売買を行うと、5年間で200回~300回くらいの取引が行われる。銘柄コード1689は、1株10円以下で推移し、今では1円から2円の間をさまよっている。1株1円でこの銘柄を1株買って、2円で売ったとすると、元本増加率は2倍になる。その後、1株1円でこの銘柄を2株買って、1株2円で2株売ったとすると、元本増加率は、4倍になる。これを10回続けると、元本増加率は、倍、おおよそ倍になる。50回続けると、元本は、倍、おおよそ倍に増加する。倍は、数式だけで考えると、ありえない話ではない。
しかし、もちろん言うまでもなく、これには、色々と問題がある。700営業日から900営業日の間、株価は3円と4円の間を行ったり来たりしている。一方で、予測値は、3円から4円の間を小さく振動しながら推移している。そのため、株価の真値が下がるときの予測値は必ず現在値よりも低く、株価の真値が上がるときの予測値は必ず現在値よりも高いという状況になる。3円と4円の間で、適当に株価の予測値を出しておけば、この状況では、どんなへっぽこなモデルでも株価値上がり・値下がりの正答率が100%になってしまう。3円と4円以外にもこの銘柄は、x円、x+1円の間を行ったり来たりしており、予測値は、(2x+1)/2円、近辺をうろついている。元本増加率のみならず、真値と予測値の平均自乗誤差なども評価指標に加えると、このような銘柄ははじけると思う。
1円で買って2円で売るという行為を繰り返し、元本を1円から1000兆円にするためには、最終的には、1株1円の株式を500兆株買って、2円で500兆株売らなければならない。もちろん銘柄コード1689の株式は、500兆株も市場に出回っていない。
また、この銘柄の「板」を見てみると、2円での売り注文よりも1円での買い注文の数が圧倒的に多い。1円で買い注文を入れたとしても、それが成約するまでには、何日、下手したら何週間もかかりそう。明日、株価が1円から2円に上がるから、買い注文したとしても、実際に株式を保有するまでにものすごく時間がかかり、またいつ手に入るか分からない。
また1円の株式を2円で売ったとしても2円がそのままポケットに入るわけではない。売買手数料と税金が引かれる。手数料+税金で2円が1.9円になったとしよう。今まで、原本が2倍→4倍→8倍に増えていたのが、1.9倍→3.61倍→6.859倍にしか増えない。最初は、0.1しかなかった差がさらに取引を2回繰り返すことによって、1.141まで膨れ上がっている。そのため、手数料と税金を引かれることによって、実際の元本増加率は、今回の結果よりもかなり小さくなる。
ソースコード(プログラムコード)。
前回、書いたソースコードに色々とイケていない部分があったので、修正しつつ、上場企業、約4000社の株価を予測するソースコードを書いた。
プログラムを書くのが難しいと感じる今日この頃。複数人で分担して、開発を行ったこともないし、数万行を超えるような大規模な開発も行ったことがないので、プログラムの分かりやすさや、保守性に重きを置いて、プログラムを書く経験は乏しい。
今まで我流でやってきたが、その方法にも限界を感じつつある。
入力する株価データの用意(data_loader.py)。
n営業日の株価データをwindow(window sizeはw)で分割する。windowは1営業日ずつスライドさせていく。
前回は、w個の株価データ+x日後の株価データ(正解)が常にペアになるようにしか入力データを生成することができなかった。これは、モデルの学習、検証には支障をきたさない。
しかし、実際に株価を予測するとなると、x日後の株価データ(正解)がないと入力データが生成できないというのは不都合である。そのため、正解データのないw個の株価データも入力データとして用意することができるようにした。
import glob import pandas as pd import re from sklearn.metrics import mean_absolute_error import torch import torch.nn as nn from torch.utils.data import DataLoader import numpy as np import matplotlib.pyplot as plt import matplotlib.ticker as tick from matplotlib import rcParams storage_uri = YYYYY rcParams['font.sans-serif'] = ['Hiragino Maru Gothic Pro', 'Yu Gothic', 'Meirio', 'Takao', 'IPAexGothic', 'IPAPGothic', 'VL PGothic', 'Noto Sans CJK JP'] device = torch.device("cuda" if torch.cuda.is_available() else "cpu") class Data_holder(): def __init__(self, stock_prices_raw, train_val_divided, val_flag): self.stock_close_price_index = stock_prices_raw.loc[:,'open':'volume'].columns.get_loc('close') self.stock_prices = stock_prices_raw.loc[:,'open':'volume'].values self.train_val_divided = train_val_divided self.val_flag = val_flag self.feature_num = self.stock_prices.shape[1] if self.val_flag != True: self.stock_prices = self.stock_prices[:int(self.stock_prices.shape[0]*self.train_val_divided)] if self.val_flag == True: self.stock_prices = self.stock_prices[int(self.stock_prices.shape[0]*self.train_val_divided):] @staticmethod def prepare_data(file_source): """saveStockPriceDataSet()で取得した株価データのcsvファイルをpandas.core.frame.DataFrame形式でloadする関数。 Args: str: csvファイルのuri。 Return: pandas.core.frame.DataFrame: 株価データ。「列名:日付,始値,高値,低値,終値,出来高,取引総額」、「行名:日付」の表。 """ stock_data = pd.read_csv(file_source, engine = "python") stock_data = stock_data.set_index('date') temp = file_source.replace(storage_uri, '') temp = re.sub(r'YYYYY', '', temp) latest_data = re.sub(r'.csv', '', temp) if len(latest_data) > 5: latest_data = [float(x.strip()) for x in latest_data.split(',')] else: latest_data = [0]*7 temp = pd.DataFrame([latest_data[1:]], columns=stock_data.columns, index=[int(latest_data[0])], dtype=float) stock_data = stock_data.append(temp,ignore_index=False) return stock_data def fragment_normalize_data(self, window_size, predicted_day_num): fragment_num = self.stock_prices.shape[0] - window_size + 1 stock_price_flagments = np.zeros((fragment_num, window_size+predicted_day_num, self.feature_num)) XXXXX = YYYYY #test時に出力を株価に変換するために必要。 #正規化。 """ 詳細は割愛。 """ stock_price_labels = torch.tensor(stock_price_flagments[:, window_size+predicted_day_num-1, self.stock_close_price_index], dtype=torch.float, device=device).view(-1, 1) stock_price_flagments = torch.tensor(stock_price_flagments[:, :window_size, :], dtype=torch.float, device=device) if self.val_flag != True: return stock_price_labels[:-predicted_day_num], stock_price_flagments[:-predicted_day_num] if self.val_flag == True: return stock_price_labels, stock_price_flagments, XXXXX class StockDataset(torch.utils.data.Dataset): def __init__(self, x, y, phase): self.sequence = x self.label = y self.phase = phase def __len__(self): return len(self.label) def __getitem__(self, idx): out_data = self.sequence[idx] out_label = self.label[idx] return out_data, out_label
ニューラルネットワーク部分(LSTM_ver1.py)。
前回と一緒だと思う。多分。
import glob import pandas as pd import re from sklearn.metrics import mean_absolute_error import torch import torch.nn as nn from torch.utils.data import DataLoader import numpy as np import matplotlib.pyplot as plt import matplotlib.ticker as tick from matplotlib import rcParams import data_loader rcParams['font.sans-serif'] = ['Hiragino Maru Gothic Pro', 'Yu Gothic', 'Meirio', 'Takao', 'IPAexGothic', 'IPAPGothic', 'VL PGothic', 'Noto Sans CJK JP'] device = torch.device("cuda" if torch.cuda.is_available() else "cpu") class LSTM_full_connected_layer(nn.Module): def __init__(self, input_size, hidden_layer_size, num_layers): super().__init__() self.input_size = input_size self.hidden_layer_size = hidden_layer_size self.num_layers = num_layers self.lstm = nn.LSTM(input_size=self.input_size, hidden_size=self.hidden_layer_size, num_layers=self.num_layers, batch_first=True ) self.dense = nn.Linear(self.hidden_layer_size, 1) def forward(self, x_data, batch_size): h0 = torch.zeros(self.num_layers, batch_size, self.hidden_layer_size) c0 = torch.zeros(self.num_layers, batch_size, self.hidden_layer_size) #隠れ層、cellの内部状態は使用しない。 lstm_out, (hn, cn) = self.lstm(x_data, (h0, c0)) linear_out = self.dense(lstm_out[:,-1,:].view(x_data.size(0), -1)) return torch.sigmoid(linear_out)
学習と検証(main_ver2.py)。
ニューラルネットワークに日経平均株価データを与え、学習を行い、それをもとに上場企業、約4000社の株価データを予測するコード。
全上場企業の株価予測(1営業日、5営業日)が終わるまで、2時間から3時間くらいかかる。途中で予期せぬエラーが発生して止まってしまっても、途中から始められるように得られたデータは、一回一回、保存している。
import glob import re from sklearn.metrics import mean_absolute_error import torch from torch.utils.data import DataLoader import numpy as np import matplotlib.pyplot as plt import matplotlib.ticker as tick from matplotlib import rcParams import data_loader import LSTM_ver1 rcParams['font.sans-serif'] = ['Hiragino Maru Gothic Pro', 'Yu Gothic', 'Meirio', 'Takao', 'IPAexGothic', 'IPAPGothic', 'VL PGothic', 'Noto Sans CJK JP'] device = torch.device("cuda" if torch.cuda.is_available() else "cpu") storage_uri = YYYYY gl = glob.glob(YYYYY) def loss_func(pred_value, true_value): pred_value_flattened = pred_value.view(-1) true_value_flattened = true_value.view(-1) squared_diff_sum = torch.sum((pred_value_flattened - true_value_flattened)**2) loss = squared_diff_sum/pred_value_flattened.size(0) return loss def train_model(dataloaders_dict, feature_num, hidden_layer_size, num_layers, batch_size): model = LSTM_ver1.LSTM_full_connected_layer(feature_num, hidden_layer_size, num_layers) model.train() optimizer = torch.optim.Adam(model.parameters(), lr=0.01) loss_train = np.zeros((epochs, 2)) loss_val = np.zeros((int(epochs/5), 2)) for i in range(epochs): total_loss = 0.0 for phase in ['train', 'val']: if phase == 'train': model.train() optimizer.zero_grad() print('(train)') else: if((i+1) % 5 == 0): model.eval() print('-------------') print('(val)') else: continue count = 0 for x, y in dataloaders_dict[phase]: count += 1 with torch.set_grad_enabled(phase == 'train'): y_pred = model(x, batch_size) loss = loss_func(y_pred, y) total_loss += loss.item() if (phase == 'train'): loss.backward() optimizer.step() total_loss /= count if phase == 'train': loss_train[i, 0] = i+1 loss_train[i, 1] = total_loss if phase == 'val' and (i+1) % 5 == 0: loss_val[int((i+1)/5-1), 0] = i+1 loss_val[int((i+1)/5)-1, 1] = total_loss print(f'epoch: {i+1:3} loss: {total_loss:10.8f}') return model, loss_train, loss_val def train_NN(stock_prices_raw1, stock_prices_raw2, window_size, feature_num, hidden_layer_size, num_layers, batch_size, predicted_day_num, epochs, train_val_divided): if train_val_divided == -1: data_holder_t = data_loader.Data_holder(stock_prices_raw1, train_val_divided=1, val_flag=False) data_holder_v = data_loader.Data_holder(stock_prices_raw2, train_val_divided=0, val_flag=True) else: data_holder_t = data_loader.Data_holder(stock_prices_raw1, train_val_divided, val_flag=False) data_holder_v = data_loader.Data_holder(stock_prices_raw2, train_val_divided, val_flag=True) stock_price_labels_t, stock_price_flagments_t = data_holder_t.fragment_normalize_data(window_size, predicted_day_num) stock_price_labels_v, stock_price_flagments_v, _ = data_holder_v.fragment_normalize_data(window_size, predicted_day_num) train_dataset = data_loader.StockDataset(stock_price_flagments_t, stock_price_labels_t, 'train') val_dataset = data_loader.StockDataset(stock_price_flagments_v, stock_price_labels_v, 'val') train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, drop_last=True) val_dataloader = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=False, drop_last=True) dataloaders_dict = {'train': train_dataloader, 'val': val_dataloader} model, loss_train, loss_val = train_model(dataloaders_dict, feature_num, hidden_layer_size, num_layers, batch_size) return model, loss_train, loss_val def test_NN(model, stock_prices_raw, window_size, feature_num, hidden_layer_size, num_layers, batch_size, predicted_day_num, epochs, train_val_divided): if train_val_divided == -1: data_holder = data_loader.Data_holder(stock_prices_raw, train_val_divided=0, val_flag=True) else: data_holder = data_loader.Data_holder(stock_prices_raw, train_val_divided, val_flag=True) stock_price_labels, stock_price_flagments, XXXXX = data_holder.fragment_normalize_data(window_size, predicted_day_num) test_dataset = data_loader.StockDataset(stock_price_flagments, stock_price_labels, 'val') test_dataloader = torch.utils.data.DataLoader(test_dataset, batch_size=1, shuffle=False, drop_last=False) prices_pred = np.zeros((len(test_dataloader))) prices_true = np.zeros((len(test_dataloader))) count = 0 for x, y in test_dataloader: y_pred = model(x, batch_size=1).item()**(YYYYY) prices_pred[count] = y_pred prices_true[count] = y.item()**(YYYYY) count += 1 return prices_pred, prices_true[:-predicted_day_num] if __name__ == "__main__": window_size = Y feature_num = Y hidden_layer_size = Y num_layers = Y batch_size = Y epochs = Y train_val_divided = Y predicted_days = 2 stock_prices_raw1 = data_loader.Data_holder.prepare_data(gl[0]) stock_prices_raw1 = stock_prices_raw1[stock_prices_raw1['open'] > 0] model = [None]*predicted_days for i, predicted_day_num in enumerate([1, 5]): model[i], loss_train, loss_val = train_NN( stock_prices_raw1, stock_prices_raw1, window_size, feature_num, hidden_layer_size, num_layers, batch_size, predicted_day_num, epochs, train_val_divided) for code_i in range(1, len(gl)): stock_prices_raw2 = data_loader.Data_holder.prepare_data(gl[code_i]) stock_prices_raw2 = stock_prices_raw2[stock_prices_raw2['open'] > 0] if stock_prices_raw2.shape[0] < 121: continue file_name = gl[code_i] file_name = file_name.replace(storage_uri, '') file_name = re.sub(r'YYYYY', '', file_name) file_name = re.sub(r'-.*', '', file_name) for i, predicted_day_num in enumerate([1, 5]): prices_pred, prices_true = test_NN( model[i], stock_prices_raw2, window_size, feature_num, hidden_layer_size, num_layers, batch_size, predicted_day_num, epochs, train_val_divided) np.save(YYYYY, prices_pred)
データ解析(analysis_ver2.py)。
main_ver2.pyで得られた株価予測データの解析。この文章に張り付けているグラフは、analysis_ver2.pyで作成した。
import glob import re import numpy as np import matplotlib.pyplot as plt import matplotlib.ticker as tick import data_loader import csv import pandas as pd predicted_day_num = 1 storage_uri = YYYYY gl = glob.glob(YYYYY) gl_2 = glob.glob(YYYYY) prices_pred = None stock_prices_raw = None def draw_graph(): accuracy_rate = pd.read_csv(YYYYY, engine='python', header=None) fig, ax = plt.subplots() ax.set_xlabel('成功確率') ax.set_ylabel('頻度') ax.hist(accuracy_rate.iloc[:, 3], bins=100, range=(0, 1), log=False, color='red') fig.savefig(str(predicted_day_num) + '-accuracy_rate.png', format='png', dpi=1000) increase_rate = pd.read_csv(YYYYY, engine='python', header=None) fig, ax = plt.subplots() ax.set_xlabel('元本増加率'+'($10^{x}$倍)') ax.set_ylabel('頻度') labels = [str(predicted_day_num)+'営業日後予測', '真値'] ax.hist([np.log10(increase_rate.iloc[:, 2]), np.log10(increase_rate.iloc[:, 4])], bins=100, log=False, stacked=False, color=['red', 'black'], label=labels) ax.legend() fig.savefig(str(predicted_day_num) + '-increase_rate.png', format='png', dpi=1000) S_N_ratio = pd.read_csv(YYYYY, engine='python', header=None) fig, ax = plt.subplots() ax.set_xlabel('S/N'+'($10^{x}$)') ax.set_ylabel('頻度') ax.hist(np.log10(S_N_ratio.iloc[:, 1]), bins=100, log=False, color='red') fig.savefig(str(predicted_day_num) + '-S_N_ratio.png', format='png', dpi=1000) def draw_time_series_graph(): increase_rate = pd.read_csv(YYYYY, engine='python', header=None).iloc[:, [0, 2]].values increase_rate = increase_rate[np.argsort(increase_rate[:, 1])[::-1]] draw_ranking_list = [0, 999, 1999, 2999, len(increase_rate)-1] increase_rate = increase_rate[draw_ranking_list] for code_i in range(1, len(gl)): stock_code = gl[code_i] stock_code = stock_code.replace(storage_uri, '') stock_code = re.sub(r'YYYYY', '', stock_code) stock_code = re.sub(r'-.*', '', stock_code) stock_prices_raw = None prices_pred = None if int(stock_code) in increase_rate: tmp_uri = YYYYY stock_prices_raw = stock_prices_raw = data_loader.Data_holder.prepare_data(gl[code_i]) stock_prices_raw = stock_prices_raw[stock_prices_raw['open'] > 0]['close'].values prices_pred = np.load(tmp_uri)[:-predicted_day_num] window_size = stock_prices_raw.shape[0] - prices_pred.shape[0] - predicted_day_num + 1 fig, ax = plt.subplots() x = np.linspace(predicted_day_num-1, len(prices_pred)+predicted_day_num-2, len(prices_pred)) ax.plot(x, stock_prices_raw[window_size+predicted_day_num-1:], label='真値', color='black', linewidth = 0.2) ax.plot(x, prices_pred, label=str(predicted_day_num)+'営業日後予測', color='red', linewidth = 0.2) ax.set_xlabel('営業日') ax.set_ylabel('株価(円)') tmp = np.where(increase_rate[:, 0] == int(stock_code))[0][0] ax.set_title('銘柄コード:' + str(int(increase_rate[tmp][0]))) ax.legend() ax.xaxis.set_minor_locator(tick.MultipleLocator(5)) ax.grid(which='minor', linewidth=0.1) fig.savefig(YYYYY, format='png', dpi=1000) continue def save_data(stock_code, buy_sell_num_p, increase_rate_p, buy_sell_num_t, increase_rate_t, right_wrong_bool, S_N_ratio): with open(YYYYY, 'a') as fout: writer = csv.writer(fout, lineterminator='\n') tmp = [stock_code, np.sum(right_wrong_bool), right_wrong_bool.shape[0]-np.sum(right_wrong_bool), np.sum(right_wrong_bool)/right_wrong_bool.shape[0]] writer.writerow(tmp) with open(YYYYY, 'a') as fout: writer = csv.writer(fout, lineterminator='\n') tmp = [stock_code, buy_sell_num_p, increase_rate_p, buy_sell_num_t, increase_rate_t] writer.writerow(tmp) with open(YYYYY, 'a') as fout: writer = csv.writer(fout, lineterminator='\n') tmp = [stock_code, S_N_ratio] writer.writerow(tmp) def get_increase_rate(diff_array, window_size, stock_prices_raw): holding_flag = False buy_price = 0 increase_rate = 1 skip_flag = False skip_num = 0 buy_sell_num = 0 for dpt_i, dpt in enumerate(diff_array): if skip_flag == True and skip_num < predicted_day_num: skip_num += 1 continue if holding_flag == False and dpt > 0: holding_flag = True skip_flag = True skip_num = 1 buy_price = stock_prices_raw[window_size-1:-predicted_day_num][dpt_i] if holding_flag == True and (dpt < 0 or dpt_i == len(diff_array) -1): holding_flag = False skip_flag = False skip_num = 0 increase_rate *= stock_prices_raw[window_size-1:-predicted_day_num][dpt_i]/buy_price buy_sell_num += 1 return increase_rate, buy_sell_num def analyze_data(): for code_i in range(1, len(gl)): stock_prices_raw = data_loader.Data_holder.prepare_data(gl[code_i]) stock_prices_raw = stock_prices_raw[stock_prices_raw['open'] > 0]['close'].values[:] if stock_prices_raw.shape[0] < 121: continue stock_code = gl[code_i] stock_code = stock_code.replace(storage_uri, '') stock_code = re.sub(r'YYYYY', '', stock_code) stock_code = re.sub(r'-.*', '', stock_code) for code_i_1 in range(len(gl_2)): stock_code_1 = gl_2[code_i_1] stock_code_1 = stock_code_1.replace(storage_uri, '') stock_code_1 = re.sub(r'YYYYY', '', stock_code_1) stock_code_1 = re.sub(r'\\', '', re.sub(r'.npy', '', stock_code_1)) if stock_code == stock_code_1: prices_pred = np.load(gl_2[code_i_1])[:-predicted_day_num] break window_size = stock_prices_raw.shape[0] - prices_pred.shape[0] - predicted_day_num + 1 diff_pred_true = prices_pred - stock_prices_raw[window_size-1:-predicted_day_num] diff_true = stock_prices_raw[window_size+predicted_day_num-1:] - stock_prices_raw[window_size-1:-predicted_day_num] tmp = diff_pred_true * diff_true right_wrong_bool = (tmp[tmp != 0] > 0).astype(np.int) tmp = abs(diff_true/(diff_pred_true-diff_true)) S_N_ratio = np.sum(tmp)/tmp[tmp != 0].shape[0] increase_rate_p, buy_sell_num_p = get_increase_rate(diff_pred_true, window_size, stock_prices_raw) increase_rate_t, buy_sell_num_t = get_increase_rate(diff_true, window_size, stock_prices_raw) save_data(stock_code, buy_sell_num_p, increase_rate_p, buy_sell_num_t, increase_rate_t, right_wrong_bool, S_N_ratio) analyze_data() draw_graph() draw_time_series_graph()
おわりに。
日経平均株価を使って、LSTMを学習させ、上場企業、約4000社の株価予測(1営業日先、5営業日先)を行った。
株価の値上がり・値下がりの正答率は、75%~85%くらい。元本は、5年間の株式売買でおおよそ100倍になる(色々と仮定を置いているので、実際はもっと少ない)。
このプログラムをもとにした実際の売買に使えるプログラムは、この文章を書いている段階で粗々とではあるが、書き終えている。もう少し洗練させた後、このブログで紹介したいと思う。
この文章を読んで、面白い!役に立った!...と思った分だけ、投げ銭していただけると嬉しいです。