この記事はミクシィ20新卒 Advent Calendar 2019の7日目の記事です。
人工知能でロウソク足予測
皆さんこんにちはところで株はやってますか?株はいいぞ。
最近株に明るいニュースが続いているようです。日経平均株価は11月に年初来高値(2万3520円)を更新しています。私自身も初心者にしては結構なプラス益を手に入れることができました。
ロウソク足
そんな株ですが、ローソク足と呼ばれる分析手法があります。
ローソク足とは、一定期間の相場の4本値(始値、高値、安値、終値)を用いて一本の棒状の足を生成したもので、このローソク足を並べていくことで、相場の状態や流れを一目でわかるようにしたチャートをローソク足チャートといいます。
ローソク足チャートは、日本の江戸時代に生まれた伝統のあるチャートで、現在では海外でも広く使われています。
簡単に言うと始値、高値、安値、終値という4つの値を用いた分析手法のことです。
ローソク足は江戸時代に本間宗久が米相場で考案した手法で、世界最古のテクニカル分析手法と言われています。ちなみにいくつかの法則があるようです。(参照:酒田五法)
人工知能での学習
このローソク足(始値、高値、安値、終値)を使った分析手法で人工知能に株価を学習させて見たいと思います。
データの収集
こちらのサイトを参考にしてロウソク足に必要な始値、高値、安値、終値のデータを収集しました。対象はこちらのサイト。
株価データ・株主優待情報・先物データ・ランキングデータ・CSVダウンロード無料
API があったようなのでそちらを使用させていただきました。もしかしたらグレーな方法かもしれません。
以下がサンプルコードです。(csvはshift_jisだったので注意)
import urllib.request
req = urllib.request.Request("https://kabuoji3.com/stock/file.php")
req.add_header('authority', 'kabuoji3.com')
data = urllib.parse.urlencode({"code":"1301", "year":"2019"}).encode("utf-8")
with urllib.request.urlopen(req, data) as res:
html = res.read().decode("shift_jis")
with open("1301.csv", mode='a') as f:
for i, csv in enumerate(html.split('\n')):
if i > 2: # 1,2行目はヘッダー
f.write(csv + '\n')
1301 東証1部 (株)極洋(水産・農林業),,,,,
日付,始値,高値,安値,終値,出来高,終値調整値
"2019-01-04","2806","2861","2760","2852","26200","2852"
"2019-01-07","2890","2937","2843","2859","35400","2859"
...
こんな感じで日足データを取得できました。
これを2000年〜2019年まで、2076社分の日足データを取得しました。人工知能くんのミッションはこれらの膨大なデータから次の日の始値、高値、安値、終値を予想することです。それでは早速始めましょう。
データの正規化
株価といっても、その金額はまちまちです。例えば株価が数千円や数万円の高いものから、数百円のものまで色々あります。
そのため前日と当日の差分に変換します。今回は当日の株価(始値、高値、安値、終値)- 前日の終値 を計算しました。
また、ただ差分を求めるだけでなく、対数(log)に変換してから差分を求める(対数差分系列)といいらしいのでそうしました。ニュースとかで見る変動率を近似したものになっているそうです。(詳しくは知らない)
np.log(row[0]) - np.log(old_row[3])
下が通常の株価(終値)です。
下が対数(log)に変換してから差分を求めたものです。
データの分散が小さくなって扱いやすくなっていることが分かります。
LSTMを使った学習
KerasでLSTMを用いて学習しました。LSTM(Long short-term memory)は長期的な依存関係を学習することのできる、特別なRNN(Recurrent Neural Network)の一種です。
データの作成
predata.csvに先程の正規化をしたデータをまとめました。(分かりやすいように各社株価の先頭に[START]という印を付けました。)一番最初のデータは前日のデータがないので使いません。
inputs = list()
with open("./predata.csv", 'r', encoding='utf-8') as f:
inp = []
for line in f:
line = line.replace('\n', '')
if line == '[START]':
if len(inp) != 0:
inputs.append(inp[1:])
inp = []
else:
inp.append([float(i) for i in line.split(',')])
今回は16日分の株価データを入力として、1 日後の株価を予測するようにしました。
以下のようにデータを作っていきます。output_data_updown は前日と比べて株価(終値)が上がったか下がったかを学習するためのデータです。
input_data = []
output_data = []
output_data_updown = []
for inp in inputs:
for i in range(len(inp)-16):
input_data.append(inp[i:i+16])
output_data.append(inp[i+16])
output_data_updown.append(int((inp[i+16-1][3] - inp[i+16][3]) > 0))
input_data = np.array(input_data)
output_data = np.array(output_data)
output_data_updown = np.expand_dims(np.array(output_data_updown), axis=-1)
print(input_data.shape)
print(output_data.shape)
print(output_data_updown.shape)
(9144468, 16, 4)
(9144468, 4)
(9144468, 1)
約900万サンプルくらいになりました。np.appendを使ったら激遅だったので注意。このうち10%を検証用データに使います。以下のように分割しました。
train_input_data = input_data[:8229888]
train_output_data = output_data[:8229888]
train_output_updown_data = output_data_updown[:8229888]
validation_input_data = input_data[8229888:9144320]
validation_output_data = output_data[8229888:9144320]
validation_output_updown_data = output_data_updown[8229888:9144320]
バッチサイズで割り切れる必要があるので注意。
モデルの作成
以下のようにモデルを作ります。
def create_model():
lstm_unit_size = 256
inputs = tf.keras.layers.Input(shape=(16, 4))
outputs, h, c = tf.keras.layers.LSTM(lstm_unit_size, return_sequences=True, return_state=True)(inputs)
outputs = tf.keras.layers.Dropout(0.2)(outputs)
outputs = tf.keras.layers.LSTM(lstm_unit_size, return_sequences=True)(outputs, initial_state=[h, c])
outputs = tf.keras.layers.TimeDistributed(tf.keras.layers.Dense( 4, activation='relu' ))(outputs)
outputs = tf.keras.layers.Flatten()(outputs)
predicted = tf.keras.layers.Dense( 4, activation='linear', name='price' )(outputs)
updown = tf.keras.layers.Dense(1, activation='sigmoid', name='updown')(outputs)
model = tf.keras.models.Model(inputs=inputs, outputs=[predicted, updown])
model.compile(optimizer='Adam',
loss={'price':'mse', 'updown':'binary_crossentropy'},
loss_weights={'price': 1., 'updown':0.1},
metrics={'price':'accuracy', 'updown':'accuracy'})
return model
LSTMを2つ使いました。出力がいくつかありますが、priceが株価を予測する方、updownが前日から上がったか下がったかを学習する部分です。
学習
学習の様子です。10epochくらいしました。historyに学習時のログが残ります。
batch_size = 256
history = model.fit( train_input_data, train_output_data,
validation_data=(validation_input_data, validation_output_data),
batch_size=batch_size, epochs=max_epoch, callbacks=callbacks )
株価の予想については accuracy: 0.6575 となりました。最初の方で学習しきっていてほぼ動きはありませんでした。株価の上下に対しては accuracy: 0.7610 と株価自体の予想よりはいい感じで、こちらは少し伸びしろがありそうでした。
予測
最後に 2497 ユナイテッド(株) の株価を実際に予想してみました。こちら2006年上場の企業で、学習したデータには含まれていない企業です。
今回は2019年10/1~10/24日までの株価を入力として、11/29日までの株価を人工知能の推論結果のみで予測してみました。
では早速始めます。入力するデータを作っていきます。学習のときと同様のデータの正規化をしてあります。
input_data = []
for inp in inputs:
input_data.append(inp[0:16])
input_data = np.array(input_data)
print(input_data.shape)
print(input_data)
(1, 16, 4)
[[[-0.00168634 0.01587998 -0.00931027 0.00922438]
[-0.0100672 0.01573531 -0.01259991 -0.00502093]
[-0.01351372 -0.00842465 -0.04197618 -0.03761127]
[ 0.0008707 0.00953627 -0.00787061 0.00694447]
[ 0.00345423 0.01203798 -0.00956113 0.00172861]
[-0.00086393 0.02389192 -0.00432714 0.0213592 ]
[-0.0144745 -0.0101955 -0.02395324 -0.0153325 ]
[-0.00343938 -0.00171821 -0.02169282 -0.02169282]
[ 0.00873368 0.02169282 -0.00615928 0.00524936]
[ 0. 0.02669121 0. 0.01386504]
[ 0.01027406 0.0321789 0.00343643 0.00686109]
[ 0.00170794 0.0051151 -0.01030937 0.00170794]
[ 0.00595493 0.01103108 -0.00427534 -0.00085361]
[-0.0025652 0.00255864 -0.00857638 -0.00085434]
[ 0.00085434 0.01189479 -0.00085507 0.00681434]
[ 0.00169635 0.01683541 0.00169635 0.01600034]]]
これを入力して推論させます。途中で入力データの一番古いものを削除し、推論結果追加することで、人工知能の推論のみで予測しています。
outputs = []
for i in range(25):
out = model.predict(input_data)
input_data = np.delete(input_data, 0, 1)
input_data = np.append(input_data, np.reshape(out[0], [1,1,4]), axis=1)
outputs.append(out[0])
print(outputs)
[array([[ 0.00132819, 0.01139224, -0.00968045, 0.0012033 ]],
dtype=float32), array([[-1.5926454e-04, 1.0682505e-02, -1.1488163e-02, -6.4508058e-06]],
dtype=float32), array([[ 0.00127331, 0.01170186, -0.01039623, 0.00069969]],
dtype=float32), array([[ 0.00027916, 0.01070059, -0.01091884, 0.00012368]],
dtype=float32), array([[ 0.00050769, 0.01077457, -0.01174999, -0.00071645]],
dtype=float32), array([[ 0.00111121, 0.01154042, -0.00996058, 0.0012894 ]],
dtype=float32), array([[ 0.0009697 , 0.01154 , -0.01030977, 0.00080856]],
dtype=float32), array([[ 0.00101198, 0.0116825 , -0.01077132, 0.00053665]],
dtype=float32), array([[ 0.00069706, 0.01124728, -0.01157157, -0.00036084]],
dtype=float32), array([[ 0.00154834, 0.01216084, -0.00977372, 0.00157495]],
dtype=float32), array([[ 0.00126158, 0.0121403 , -0.01036783, 0.00101024]],
dtype=float32), array([[ 0.00050072, 0.01097398, -0.01090129, 0.00016591]],
dtype=float32), array([[ 0.00023197, 0.01036121, -0.01203129, -0.00111604]],
dtype=float32), array([[ 0.00083902, 0.0110049 , -0.01011042, 0.00090828]],
dtype=float32), array([[ 4.6287850e-04, 1.0940313e-02, -1.1158437e-02, -1.2882054e-05]],
dtype=float32), array([[ 0.00012015, 0.01106888, -0.01229248, -0.00068957]],
dtype=float32), array([[ 0.00120305, 0.01264553, -0.01065658, 0.00163803]],
dtype=float32), array([[ 0.00102521, 0.01279328, -0.011617 , 0.00062591]],
dtype=float32), array([[-8.5204374e-05, 1.1174697e-02, -1.3360392e-02, -1.3588690e-03]],
dtype=float32), array([[ 0.00091006, 0.01223093, -0.01100137, 0.00123645]],
dtype=float32), array([[ 6.5742014e-04, 1.2348325e-02, -1.2196045e-02, 8.5171312e-05]],
dtype=float32), array([[ 2.9950868e-05, 1.1805771e-02, -1.3623042e-02, -1.1899523e-03]],
dtype=float32), array([[ 0.00114615, 0.01286497, -0.01096623, 0.00165228]],
dtype=float32), array([[ 0.0010013 , 0.01311738, -0.01210236, 0.00052181]],
dtype=float32), array([[-0.00024819, 0.0114615 , -0.01435088, -0.00192848]],
dtype=float32)]
何やら怪しげなデータが出てきました。これは対数の差分になっているので元の数値に戻します。もとに戻すには np.cumsum を使って全ての和を計算して、一番最初の値を足してやれば良いです。(分かりやすいように終値のみでやっています)
まずは入力値と出力値を合体させます。(終値のみ)
outputs = np.array(outputs).reshape(25,4)
result = [i[3] for i in inp[0:16]]
result.extend([i[3] for i in outputs])
そしてもとに戻すため exp します。rootはこの例だと10/1日の1日前の9/30日分の株価(始値、高値、安値、終値)をただ対数に変換したものです。
np.exp(np.cumsum(result) + (root[3]))
array([1198.00012618, 1192.00012561, 1148.00011953, 1156.00012042,
1158.00012063, 1183.00012223, 1165.00012034, 1140.00011732,
1146.00011807, 1162.00011956, 1170.00011993, 1172.00012009,
1171.00011996, 1170.00011986, 1178.00012066, 1197.00012339,
1198.44133774, 1198.43360685, 1199.27242903, 1199.42076322,
1198.56174579, 1200.10816839, 1201.07891501, 1201.7236475 ,
1201.29009968, 1203.18356029, 1204.39967309, 1204.59950881,
1203.25587169, 1204.34925667, 1204.33374228, 1203.50355461,
1205.47654016, 1206.23129645, 1204.59329927, 1206.08363413,
1206.18636223, 1204.75191169, 1206.74415004, 1207.37401034,
1205.04785923])
このように出力されました!これは終値のみですが、グラフを作って本当に当たっているのか比較してみたいと思います。
うーん、全然的中してませんね。出力の差分の値が小さすぎる?と思いとりあえず全て20倍してみました。(グラフが重なるところまでは同じデータです)
outputs = outputs * 20.0
あれ?意外といけてる?なぜなのかは分かりませんが詳しい方いれば教えてください!
まとめ
なんとなくそれっぽいグラフを出力できた。出力の値が小さすぎるのは対数変換がいらなかった?
値段自体を当てるのは結構難しい、株価の上下の判定だけだったらもっといい感じの精度が出ていたかも。
株の話出来る方募集中です。@tonotech 一緒に大儲けしましょう!
*この記事は素人の意見です。内容等について生じたついていかなる損害等も責任を負いかねます。