RNN, LSTMを使った株価予測

RNNやLSTMは時系列データの予測のために利用されます。時系列データには、ある場所の気温や、来客数、商品の価格など多岐にわたりますが、最もデータを入手しやすい株価をRNNとLSTMで予測を行ってみたいと思います。

ただし、ニューラルネットはあくまでも得られたデータの範囲内でしか予測する事が出来ず、想定外の状況になった場合、そのモデルはほぼ意味をなしません。例えば、コロナショック前の1年前のデータを用いても、コロナショックを予測する事は出来ません。

また、株価の形成はテクニカルな要素だけでなく、ファンダメンタルズ、実需や先物などの複雑な要素もあり、LSTMで未来を予測するのは難しいとは思います。とはいえ、面白そうなので、年末の時間を利用してLSTMに慣れるためにもやってみようと思います。

あくまでもRNNやLSTMに慣れる練習の一環ですので、この結果をもって株価が予測できるなどとは思わないでください。

github

  • jupyter notebook形式のファイルはこちら

google colaboratory

  • google colaboratory で実行する場合はこちら

筆者の環境

筆者のOSはmacOSです。LinuxやUnixのコマンドとはオプションが異なります。

!sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.6
BuildVersion:	18G6032
!python -V
Python 3.8.5

基本的なライブラリとkerasをインポートしそのバージョンを確認しておきます。

%matplotlib inline
%config InlineBackend.figure_format = 'svg'

import matplotlib
import matplotlib.pyplot as plt
import scipy
import numpy as np
import pandas as pd

import tensorflow as tf
from tensorflow import keras

print('matplotlib version :', matplotlib.__version__)
print('scipy version :', scipy.__version__)
print('numpy version :', np.__version__)
print('tensorflow version : ', tf.__version__)
print('keras version : ', keras.__version__)
matplotlib version : 3.3.2
scipy version : 1.5.2
numpy version : 1.18.5
tensorflow version :  2.3.1
keras version :  2.4.0

データの取得

今回は日経平均とアメリカのS&P500の株価のデータの予測を行います。データはそれぞれ以下のサイトからダウンロードしました。

日経平均のデータ

SP500のデータ

日経平均の予測

データの確認

まず最初に日経のデータを見てみます。

!ls
files_bk             lstm_nb.md           lstm_nb.txt          nikkei.csv           sp500_2019.csv       sp500_2019_utf8.csve sp500_2020_utf8.csv
lstm_nb.ipynb        lstm_nb.py           lstm_nb_files        nikkei_utf8.csv      sp500_2019_utf8.csv  sp500_2020.csv       sp500_2020_utf8.csve
%%bash
head nikkei.csv
�f�[�^���t,�I�l,�n�l,���l,���l
"2017/01/04","19594.16","19298.68","19594.16","19277.93"
"2017/01/05","19520.69","19602.10","19615.40","19473.28"
"2017/01/06","19454.33","19393.55","19472.37","19354.44"
"2017/01/10","19301.44","19414.83","19484.90","19255.35"
"2017/01/11","19364.67","19358.64","19402.17","19325.46"
"2017/01/12","19134.70","19300.19","19300.19","19069.02"
"2017/01/13","19287.28","19174.97","19299.36","19156.93"
"2017/01/16","19095.24","19219.13","19255.41","19061.27"
"2017/01/17","18813.53","19038.45","19043.91","18812.86"

文字コードがshift-jisになっているので、utf-8に直します。

%%bash
nkf --guess nikkei.csv
Shift_JIS (LF)
%%bash
nkf -w nikkei.csv > nikkei_utf8.csv
%%bash
head nikkei_utf8.csv
データ日付,終値,始値,高値,安値
"2017/01/04","19594.16","19298.68","19594.16","19277.93"
"2017/01/05","19520.69","19602.10","19615.40","19473.28"
"2017/01/06","19454.33","19393.55","19472.37","19354.44"
"2017/01/10","19301.44","19414.83","19484.90","19255.35"
"2017/01/11","19364.67","19358.64","19402.17","19325.46"
"2017/01/12","19134.70","19300.19","19300.19","19069.02"
"2017/01/13","19287.28","19174.97","19299.36","19156.93"
"2017/01/16","19095.24","19219.13","19255.41","19061.27"
"2017/01/17","18813.53","19038.45","19043.91","18812.86"

問題ないようなので、pandasで読み込みます。

df = pd.read_csv('nikkei_utf8.csv')
df.head()
データ日付終値始値高値安値
02017/01/0419594.1619298.6819594.1619277.93
12017/01/0519520.6919602.1019615.4019473.28
22017/01/0619454.3319393.5519472.3719354.44
32017/01/1019301.4419414.8319484.9019255.35
42017/01/1119364.6719358.6419402.1719325.46
df.tail()
データ日付終値始値高値安値
9712020/12/2426668.3526635.1126764.5326605.26
9722020/12/2526656.6126708.1026716.6126638.28
9732020/12/2826854.0326691.2926854.0326664.60
9742020/12/2927568.1526936.3827602.5226921.14
975本資料は日経の著作物であり、本資料の全部又は一部を、いかなる形式によっても日経に無断で複写、...NaNNaNNaNNaN

最後の行に著作権に関する注意書きがありますが、これを削除します。複写や流布は行いません。

df.drop(index=975, inplace=True)
df.tail()
データ日付終値始値高値安値
9702020/12/2326524.7926580.4326585.2126414.74
9712020/12/2426668.3526635.1126764.5326605.26
9722020/12/2526656.6126708.1026716.6126638.28
9732020/12/2826854.0326691.2926854.0326664.60
9742020/12/2927568.1526936.3827602.5226921.14

データを可視化してみます。コロナショックで大きくへこんでいることがわかりますが、2020年の年末の時点では金融緩和の影響を受けて大幅に上がっています。

データの整形

最初のデータを基準に、その値からの変化率を計算し、そのリストに対して学習を行います。

def shape_data(data_list):
  return [d / data_list[0] - 1 for d in data_list]

df['data_list'] = shape_data(df['終値'])
ticks = 10
xticks = ticks * 5

plt.plot(df['データ日付'][::ticks], df['終値'][::ticks], label='nikkei stock')
plt.grid()
plt.legend()
plt.xticks(df['データ日付'][::xticks], rotation=60)
plt.show()

比率に直したグラフも示しておきます。

plt.plot(df.index.values[::ticks], df['data_list'][::ticks], label='nikkei stock')
plt.grid()
plt.legend()
plt.show()

定数の準備

# データとしては約四年分あるが、今回はこれを8このパートに分けて、それぞれの領域で予想を行う
TERM_PART_LIST = [0, 120, 240, 360, 480, 600, 720, 840]

# 予測に利用するデータ数
# 90個のデータから後の30個のデータを予測する
NUM_LSTM = 90

# 中間層の数
NUM_MIDDLE = 200

# ニューラルネットのモデルの定数
batch_size = 100
epochs = 2000
validation_split = 0.25

データの準備

kerasに投入するためにデータを整えます。

def get_x_y_lx_ly(term_part):

  date = np.array(df['データ日付'][TERM_PART_LIST[term_part]: TERM_PART_LIST[term_part + 1]])
  x = np.array(df.index.values[TERM_PART_LIST[term_part]: TERM_PART_LIST[term_part + 1]])
  y = np.array(df['data_list'][TERM_PART_LIST[term_part]: TERM_PART_LIST[term_part + 1]])

  n = len(y) - NUM_LSTM
  l_x = np.zeros((n, NUM_LSTM))
  l_y = np.zeros((n, NUM_LSTM))

  for i in range(0, n):
    l_x[i] = y[i: i + NUM_LSTM]
    l_y[i] = y[i + 1: i + NUM_LSTM + 1]

  l_x = l_x.reshape(n, NUM_LSTM, 1)
  l_y = l_y.reshape(n, NUM_LSTM, 1)

  return n, date, x, y, l_x, l_y

n, date, x, y, l_x, l_y = get_x_y_lx_ly(0)
print('shape : ', x.shape)
print('ndim : ', x.ndim)
print('data : ', x[:10])
shape :  (120,)
ndim :  1
data :  [0 1 2 3 4 5 6 7 8 9]
print('shape : ', y.shape)
print('ndim : ', y.ndim)
print('data : ', y[:10])
shape :  (120,)
ndim :  1
data :  [ 0.         -0.00374959 -0.00713631 -0.01493915 -0.01171216 -0.02344882
 -0.01566181 -0.02546269 -0.03983993 -0.03571421]
print(l_y.shape)
print(l_x.shape)
(30, 90, 1)
(30, 90, 1)

モデルの構築

モデルの構築を定義する関数です。デフォルトではRNNとします。

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import LSTM
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import Activation
from tensorflow.keras.layers import SimpleRNN
from tensorflow.keras.layers import GRU


def build_model(model_name='RNN'):
  # LSTMニューラルネットの構築
  model = Sequential()

  # RNN,LSTM、GRUを選択できるようにする
  if model_name == 'RNN':
    model.add(SimpleRNN(NUM_MIDDLE, input_shape=(NUM_LSTM, 1), return_sequences=True))

  if model_name == 'LSTM':
    model.add(LSTM(NUM_MIDDLE, input_shape=(NUM_LSTM, 1), return_sequences=True))

  if model_name == 'GRU':
    model.add(GRU(NUM_MIDDLE, input_shape=(NUM_LSTM, 1), return_sequences=True))

  model.add(Dense(1, activation="linear"))
  model.compile(loss="mean_squared_error", optimizer="sgd")

  return model


# ニューラルネットを深くした(今回は使わない)
def build_model_02():

  NUM_MIDDLE_01 = 100
  NUM_MIDDLE_02 = 120

  # LSTMニューラルネットの構築
  model = Sequential()
  model.add(LSTM(NUM_MIDDLE_01, input_shape = (NUM_LSTM, 1), return_sequences=True))
  model.add(Dropout(0.2))
  model.add(LSTM(NUM_MIDDLE_02, return_sequences=True))
  model.add(Dropout(0.2))
  model.add(Dense(1))
  model.add(Activation("linear"))
  model.compile(loss="mean_squared_error", optimizer="sgd")
  # model.compile(loss="mse", optimizer='rmsprop')

  return model

model = build_model('RNN')

モデルの詳細

print(model.summary())
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
simple_rnn (SimpleRNN)       (None, 90, 200)           40400
_________________________________________________________________
dense (Dense)                (None, 90, 1)             201
=================================================================
Total params: 40,601
Trainable params: 40,601
Non-trainable params: 0
_________________________________________________________________
None
# validation_split で最後の10%を検証用に利用します
history = model.fit(l_x, l_y, epochs=epochs, batch_size=batch_size, validation_split=validation_split, verbose=0)

損失関数の可視化

学習によって誤差が減少していく様子を可視化してみます。今のエポック数で収束しているように見えます。

loss = history.history['loss']
val_loss = history.history['val_loss']

plt.plot(np.arange(len(loss)), loss, label='loss')
plt.plot(np.arange(len(val_loss)), val_loss, label='val_loss')
plt.grid()
plt.legend()
plt.show()

RNNによる結果の確認

薄いオレンジに塗りつぶされた期間が予測のために利用した期間です。その期間は、実際の推移と予測が一致しています。オレンジの実線が実際の株価推移、青が予測です。

def plot_result():

  # 初期の入力値
  res = []
  res = np.append(res, l_x[0][0][0])
  res = np.append(res, l_y[0].reshape(-1))

  for i in range(0, n):
    _y = model.predict(res[- NUM_LSTM:].reshape(1, NUM_LSTM, 1))

    # 予測されたデータを次の予測のためのインプットデータとして利用
    res = np.append(res, _y[0][NUM_LSTM - 1][0])

  res = np.delete(res, -1)

  plt.plot(date, y, label="stock price", color='coral')
  plt.plot(date, res, label="prediction result", color='blue')
  plt.xticks(date[::12], rotation=60)

  plt.legend()
  plt.grid()

  plt.axvspan(0, NUM_LSTM, color="coral", alpha=0.2)

  plt.show()

print('{} - {} の結果'.format(date[0], date[NUM_LSTM - 1]))
plot_result()
2017/01/04 - 2017/05/16 の結果

結果としてはどうでしょうか?まぁトレンドは大きく外していないかなという程度でしょうか笑

他の期間の予測

これまでの関数を使って、他の期間の予測もしてみます。

for term in [1, 2, 3, 4, 5, 6]:
  n, date, x, y, l_x, l_y = get_x_y_lx_ly(term)
  model = build_model('RNN')
  history = model.fit(l_x, l_y, epochs=epochs, batch_size=batch_size, validation_split=validation_split, verbose=0)
  print('予測期間 : {} - {} の結果'.format(date[0], date[NUM_LSTM - 1]))
  plot_result()
予測期間 : 2017/06/28 - 2017/11/07 の結果
予測期間 : 2017/12/21 - 2018/05/08 の結果
予測期間 : 2018/06/20 - 2018/10/29 の結果
予測期間 : 2018/12/12 - 2019/04/26 の結果
予測期間 : 2019/06/18 - 2019/10/29 の結果
予測期間 : 2019/12/12 - 2020/04/27 の結果

LSTMによる予測

for term in [0, 1]:
  n, date, x, y, l_x, l_y = get_x_y_lx_ly(term)
  model = build_model('LSTM')
  history = model.fit(l_x, l_y, epochs=epochs, batch_size=batch_size, validation_split=validation_split, verbose=0)
  print('予測期間 : {} - {} の結果'.format(date[0], date[NUM_LSTM - 1]))
  plot_result()
予測期間 : 2017/01/04 - 2017/05/16 の結果
予測期間 : 2017/06/28 - 2017/11/07 の結果

LSTMでは今回の行った簡単なモデルでは、ほとんど予測できませんでした。よってグラフも二つしか示していません。もう少し考察すれば良さそうですが、今回の目的からはそれるので辞めておきます。

GRUによる予測

for term in [0, 1]:
  n, date, x, y, l_x, l_y = get_x_y_lx_ly(term)
  model = build_model('GRU')
  history = model.fit(l_x, l_y, epochs=epochs, batch_size=batch_size, validation_split=validation_split, verbose=0)
  print('予測期間 : {} - {} の結果'.format(date[0], date[NUM_LSTM - 1]))
  plot_result()
予測期間 : 2017/01/04 - 2017/05/16 の結果
予測期間 : 2017/06/28 - 2017/11/07 の結果

GRUでも意味のある結果が得られませんでした。

S&P500の予測

2019年

同じようにアメリカの代表的な株価指数であるS&P500についても予測してみます。 ファイルは上記のサイトからダウンロード出来ます。

!ls
files_bk             lstm_nb.md           lstm_nb.txt          nikkei.csv           sp500_2019.csv       sp500_2019_utf8.csve sp500_2020_utf8.csv
lstm_nb.ipynb        lstm_nb.py           lstm_nb_files        nikkei_utf8.csv      sp500_2019_utf8.csv  sp500_2020.csv       sp500_2020_utf8.csve

ファイルの中身を簡単に見てみます。

%%bash
head sp500_2019.csv
1557 ����ETF SPDR S&P500  ETF�iETF�j,,,,,
���t,�n�l,���l,���l,�I�l,�o����,�I�l�����l
"2019-01-04","26620","26830","26310","26780","7665","26780"
"2019-01-07","27710","27790","27450","27520","1568","27520"
"2019-01-08","27800","28020","27760","27910","2051","27910"
"2019-01-09","27960","28300","27960","28210","2557","28210"
"2019-01-10","28050","28050","27600","27830","7270","27830"
"2019-01-11","28300","28300","27950","28150","1584","28150"
"2019-01-15","28100","28300","28080","28210","7142","28210"
"2019-01-16","28430","28430","28260","28300","936","28300"

文字コードがShift-JISのようなので、utf-8に置換します。

%%bash
nkf -w sp500_2019.csv > sp500_2019_utf8.csv

さらに見てみると、1行目がpandasに入れるのに余計なので、削除します。

%%bash
head sp500_2019_utf8.csv
1557 東証ETF SPDR S&P500  ETF(ETF),,,,,
日付,始値,高値,安値,終値,出来高,終値調整値
"2019-01-04","26620","26830","26310","26780","7665","26780"
"2019-01-07","27710","27790","27450","27520","1568","27520"
"2019-01-08","27800","28020","27760","27910","2051","27910"
"2019-01-09","27960","28300","27960","28210","2557","28210"
"2019-01-10","28050","28050","27600","27830","7270","27830"
"2019-01-11","28300","28300","27950","28150","1584","28150"
"2019-01-15","28100","28300","28080","28210","7142","28210"
"2019-01-16","28430","28430","28260","28300","936","28300"
%%bash
sed -ie '1d' sp500_2019_utf8.csv
%%bash
head sp500_2019_utf8.csv
日付,始値,高値,安値,終値,出来高,終値調整値
"2019-01-04","26620","26830","26310","26780","7665","26780"
"2019-01-07","27710","27790","27450","27520","1568","27520"
"2019-01-08","27800","28020","27760","27910","2051","27910"
"2019-01-09","27960","28300","27960","28210","2557","28210"
"2019-01-10","28050","28050","27600","27830","7270","27830"
"2019-01-11","28300","28300","27950","28150","1584","28150"
"2019-01-15","28100","28300","28080","28210","7142","28210"
"2019-01-16","28430","28430","28260","28300","936","28300"
"2019-01-17","28500","28900","28420","28420","966","28420"

準備が整ったので、pandasに入れます。

df = pd.read_csv('sp500_2019_utf8.csv')
df.head()
日付始値高値安値終値出来高終値調整値
02019-01-0426620268302631026780766526780
12019-01-0727710277902745027520156827520
22019-01-0827800280202776027910205127910
32019-01-0927960283002796028210255728210
42019-01-1028050280502760027830727027830
df.tail()
日付始値高値安値終値出来高終値調整値
2362019-12-2435200352003515035150243235150
2372019-12-2535150352003505035050205235050
2382019-12-2635150352503515035200227635200
2392019-12-2735450355003535035500278735500
2402019-12-3035400354503525035250354235250

日経平均と同様に、終値を変化率に変換します。同じ関数を利用します。

df['data_list'] = shape_data(df['終値'])

また、先ほどの関数を再利用したいので、日付というカラム名をデータ日付と言うカラム名に変更します。

df = df.rename(columns={'日付':'データ日付'})
df.head()
データ日付始値高値安値終値出来高終値調整値data_list
02019-01-04266202683026310267807665267800.000000
12019-01-07277102779027450275201568275200.027633
22019-01-08278002802027760279102051279100.042196
32019-01-09279602830027960282102557282100.053398
42019-01-10280502805027600278307270278300.039208
df.tail()
データ日付始値高値安値終値出来高終値調整値data_list
2362019-12-24352003520035150351502432351500.312547
2372019-12-25351503520035050350502052350500.308813
2382019-12-26351503525035150352002276352000.314414
2392019-12-27354503550035350355002787355000.325616
2402019-12-30354003545035250352503542352500.316281

全体のグラフを俯瞰しています。

plt.plot(df['データ日付'][::ticks], df['終値'][::ticks], label='sp500 2019')
plt.grid()
plt.legend()
plt.xticks(df['データ日付'][::xticks], rotation=60)
plt.show()

予測を行って、結果をグラウかしてみます。

for term in [0, 1]:
  n, date, x, y, l_x, l_y = get_x_y_lx_ly(term)
  model = build_model('RNN')
  history = model.fit(l_x, l_y, epochs=epochs, batch_size=batch_size, validation_split=validation_split, verbose=0)
  print('予測期間 : {} - {} の結果'.format(date[0], date[NUM_LSTM - 1]))
  plot_result()
予測期間 : 2019-01-04 - 2019-05-22 の結果
予測期間 : 2019-07-04 - 2019-11-15 の結果

日経平均と同様、トレンドに沿って予測しており、逆張り防止にはなるかもしれません笑

2020年

次に2020年の株価について予測を行ってみます。データの前処理などは省略します。

%%bash
head sp500_2020_utf8.csv
nkf -w sp500_2020.csv > sp500_2020_utf8.csv
sed -ie '1d' sp500_2020_utf8.csv
日付,始値,高値,安値,終値,出来高,終値調整値
"2020-01-06","34800","34850","34700","34750","7632","34750"
"2020-01-07","35050","35200","35050","35200","3487","35200"
"2020-01-08","34550","34900","34200","34850","11349","34850"
"2020-01-09","35450","35600","35450","35600","6255","35600"
"2020-01-10","35850","35900","35800","35900","3461","35900"
"2020-01-14","36200","36250","36100","36150","4379","36150"
"2020-01-15","35950","36050","35900","35950","4270","35950"
"2020-01-16","36150","36250","36100","36250","2707","36250"
"2020-01-17","36500","36550","36450","36450","9618","36450"
df = pd.read_csv('sp500_2020_utf8.csv')
df.head()
日付始値高値安値終値出来高終値調整値
02020-01-0634800348503470034750763234750
12020-01-0735050352003505035200348735200
22020-01-08345503490034200348501134934850
32020-01-0935450356003545035600625535600
42020-01-1035850359003580035900346135900
df['data_list'] = shape_data(df['終値'])
df = df.rename(columns={'日付':'データ日付'})
df.head()
データ日付始値高値安値終値出来高終値調整値data_list
02020-01-06348003485034700347507632347500.000000
12020-01-07350503520035050352003487352000.012950
22020-01-083455034900342003485011349348500.002878
32020-01-09354503560035450356006255356000.024460
42020-01-10358503590035800359003461359000.033094
df.tail()
データ日付始値高値安値終値出来高終値調整値data_list
2342020-12-21382503830038100383006596383000.102158
2352020-12-22380003810037800379006080379000.090647
2362020-12-24380503820038050381002621381000.096403
2372020-12-25383003830038100382001945382000.099281
2382020-12-28382503845038200384004734384000.105036
plt.plot(df['データ日付'][::ticks], df['終値'][::ticks], label='sp500 2020')
plt.grid()
plt.legend()
plt.xticks(df['データ日付'][::xticks], rotation=60)
plt.show()
for term in [0, 1]:
  n, date, x, y, l_x, l_y = get_x_y_lx_ly(term)
  model = build_model('RNN')
  history = model.fit(l_x, l_y, epochs=epochs, batch_size=batch_size, validation_split=validation_split, verbose=0)
  print('予測期間 : {} - {} の結果'.format(date[0], date[NUM_LSTM - 1]))
  plot_result()
予測期間 : 2020-01-06 - 2020-05-20 の結果
予測期間 : 2020-07-02 - 2020-11-13 の結果

まとめ

特徴量抽出、モデル検討、ハイパーパラメタの調整などやれることはたくさんあると思いますが、目的はkerasに慣れる事で、サービスインなどの予定はないので、ここで終わりにします。 株価を決定する要素は様々あるので、単純なNNでは予測するのはかなり難しいだろうと思っています。