見出し画像

教師あり機械学習を使ってBTC価格を予想してみる③

こんにちは、alumiです。前回の記事に対して少し反響があって嬉しいです。今回も続きやっていきます。

前回のあらすじ
30分足のOHLCのデータk本分を、MinMaxScalerを使ってスケーリングして、ロジスティック回帰したらスケーリングなしの時に比べてわずかに精度が上がったよ!

第2回の誤りについて

まずは訂正とお詫びから。
前回スケーリングという作業をscikit-learnで行ってきましたが、あの作業で実行できていたことは厳密には説明とは違いました。例えば[[a,b],[c,d]]という2次元配列があったときにあの作業でスケーリングしたのはaとc、bとdであるのに、説明ではaとb、cとdでスケールを合わせるような書き方でした。私自身勘違いしていたので気をつけます。修正コードを以下のようにデータダウンロードのところに付け足すことで当初の意図通りにスケーリングできます。

arr = (arr - arr.min())/(arr.max()-arr.min())-0.5

なお、こちらの方に変えても認識率はほぼ変化なしでした(ある意味残念)。ここからはどちらの方法でもスケーリングした状態をデフォルトにしていきます。

他のスケーリングについて

MinMaxScalerは各変数の最大値最小値を指定した同じ範囲に直すことで全体をスケーリングする手法でしたが、scikit-learnには他にもスケーリング方法が用意されているので、試しておきます。
まずは標準化。統計を少しでも勉強したことがあれば知っていると思いますが、これは正規分布の標準化と同じ作業です。ある数値データが(ここではOHLCの値)が正規分布に従うと仮定し、それを標準偏差1,平均0の正規分布に変換することを標準化と言います。ここで詳しく説明はしませんが、分散σ,平均μの正規分布に従う確率変数xに対してz=(x-μ)/σという新たな確率変数を考えることに相当します。
大事なことは分布スケールの違う数値データを同じくらいの範囲に収まる数値データに変換できるということです。つまり方法は違えどMinMaxScalerとやっていることは似ています。
コードは前回のMinMaxScalerの部分を以下のように差し替えるだけ。

from sklearn.preprocessing import StandardScaler
sc = StandardScaler()

実行してみます。前回の最後にやったようにkの値を変えながらスケーリング有りと無しで認識率を比べてみます。

いい感じです!標準化でも精度は上がりそうです。
さらに他の方法としてベクトルの正規化というものもあります。これはその名の通り各特徴ベクトルを正規化(簡単に言って仕舞えばベクトルの大きさ(ノルム)で各要素を割る作業です)するわけですが、今回は割愛します。

特徴量を増やしてみる

さて、スケーリングには少し意味があることが判明しました。他に精度を上げるためには何ができるでしょうか。
それはやはり情報を増やすことだろうと思います。今の段階ではまだ機械学習に使う特徴量が少なすぎます。裁量トレーダーにしたって一つの指標だけを見ている人より様々な市場の情報を踏まえてトレードしている人の方が成績はいいですよね多分。多分・・・。
ここで特徴量について少し考えてみましょう。私たちは今、過去の価格情報から未来の価格を予測しようとしています。他に価格に影響を与えそうなものとして、有名人の発言やニュース、インサイダー情報などなどありますがひとまず置いておきます。
前回までは過去数本分のロウソク足データを特徴ベクトルにして予測しましたが、これを100本、1000本と増やしていけば精度が上がるのでしょうか。100本前のロウソク足データが今の価格に”直接”影響してくるとはちょっと考えづらいですよね。ではもう特徴量を増やすことができないのかと言われるとそんなことはないわけです。
今まではOHLCという変数単体で、つまり1次の情報のみ考えてきました。しかし、例えばOとCを掛け合わせた新たな特徴量を定義してもいいですよね。つまり2次項、3次項も考えていくことで特徴量を擬似的に増やせます。(ただしやりすぎても計算が重くなるだけなのでやっても3次くらいまでですね)
これに近い発想でテクニカル指標も特徴量に追加していきます。ほとんどのテクニカル指標は過去のOHLCの値になんらかの計算処理を施して導いた数値です。テクニカルとして今も使われているものは多少なりとも価格決定に対して意味を持った数値なわけなので、単純に多次元項を総当たりで考えるより優先度は高いと思います。

というわけでこの二つの方針で特徴量を増やしていきたいと思います。
まずはOHLCの多次元項を考察していきます。
コードは前回のものからあまり変えません。下のコードを適切なところに追加して、リストやラベルの名前をわかりやすいように調整してやるだけです。degree=2という部分が2次項まで考えることを意味しています。

from sklearn.preprocessing import PolynomialFeatures
        polf = PolynomialFeatures(degree=2)
        polf.fit(x_train)
        x_train_polf = polf.transform(x_train)
        x_test_polf = polf.transform(x_test)

実行してみます。

・・・・・・・・・・・。

・・・・・・・・・・・・・・・・・・・・。

長いっっっっっっっ!!

考えてみれば、k=30のとき特徴ベクトルは120次元になるので、2次項まで考えるとひとつのベクトルだけで120+120_C_2=7260次元にもなってしまうのです。これでは私のメモリ4GBパソコンでは永遠に計算が終わりません。もはやパソコンに対する拷問です。ノーパソ愛護団体から訴えられてしまいます。
ということでk=5,10ときに減らして計算させてみます。それでも560秒かかりました。結果はこんな感じ。

なんとも判断しずらいですね・・・。時間短縮のためひとまずk=5に絞ります。そしてもう一度。ループ回数を30回に増やして平均を比べます。

結果
degree=1
0.5095873651429208
degree=2
0.5078078078078079

変わりません。残念。
ここでひとつ大事なことを見落としているのに気付きました。2次項を作ったら当然オーダーが変わってくるのでスケーリングが大事です。両方にスケーリングも施した上で比べてみましょう。

結果(両方Standardization済み)
degree=1
0.521921921921922
degree=2
0.5172839506172839

はい。スケーリングによりどちらも精度は上がりましたが両者で差はありませんね。むしろ2次項を増やした方が精度低くなってます。やはりOHLCの2次項を考えても意味はなさそうです。
しかし本命がまだです。テクニカル指標を可能なだけ特徴量に加えましょう。bot作成用に作った関数が存在する都合で今回加える特徴量は、RSI(14)、EMA(5)、EMA(13)、EMA(25)、RCI(5)、RCI(9)、RCI(22)、MACD(12,26)、MACDのシグナル(9)、ボリンジャーバンド、ATR(14)
にします。カッコ内の数値は期間です(GMOのアプリを参考に一般的な数値を選びました)。
コード書くのめんどくさ・・・・いえ楽しみですね!

ーーーー1日経過ーーーー

はい、少し他のことに時間を取られてましたが書けました。とりあえずとても乱雑なコードで認識率が上がりそうか確かめてみます。

結果(テクニカル指標11個を特徴量として足してStandardization)
0.5152342433434446

テクニカルを足す前でも0.51前後の認識率が出ていたのでほぼ変わってません。次元はかなり増えましたが・・・。オシレーター系の数を絞ったりEMAをなくしてみたりとしましたが、その後も目立った向上は見られず。テクニカルの使用については一旦断念ということで。絶対何か使えると思うのでまた後で試したいと思います。


正解の定義を変えてみる

次は正解ラベルに工夫をします。今の正解ラベルの定義は直後のロウソク足が陽線かどうかでした。しかし、次の1本を予測するよりひょっとすると3本くらい後の価格上下を予測する方がやりやすいかもしれません。実際凄腕トレーダーだって次の1本で上がるか下がるか予測しているのではなくもう少し長い目での利益を期待してますよね?
コードはデータダウンロードのyの部分を少し変えてやればいいです。今回は「l本先の値段の上下を予測する」としてlの値を[1,5,10,15,20,25,30]で変えて試してみます。また、k(何本前までのロウソク足データを特徴量にするか)の値も[5,10,15,20]で変えてみます。計28通り調べるわけです。結果はpandasを利用して表にまとめます。すると1分足のときになかなか良い結果が現れました。それが以下。

横軸がl、縦軸がkです。こう見ると55%超えがちらほら見えます。すごい進歩です!kもlも値が小さい方がいいのでk=10,l=20を選ぶことにします。
ちなみにl=20固定でkを変化させた時のグラフは以下。

そして逆にk=10固定でlを変化させた時のグラフは以下です。

一つ目のグラフがなかなか面白いです。なめらかな曲線になっておりなおかつ極大点(もしかしたら最大点)が存在します。つまり、1分足において、「20本後のデータを最もよく予測できるのは過去10本分程度のデータ」というわけです。
1分足なので4日も経てば6000件程度のデータはすべて入れ替わります。なのでこの最適パラメータが長い期間続くとは限りませんが、こうやって正解ラベルの定義を見直したことで大きく認識率が上がったのは大きな収穫だと言えるでしょう。

ここまでのまとめと次への展望

今回は内容が多かったので整理します。
今回はまず第2回で行おうとしたスケーリングを意図通りの実行となるよう正しく修正しました。また、他のスケーリング(第2回の意図とは少しちがうスケーリングですが)手法も紹介しました。
次に特徴量を増やすべく2次の多項式化やテクニカル指標を試しました。しかしこれはうまく効果を発揮しませんでした。
そこで今度は正解ラベルの定義の見直しをしました。1本後が陽線かどうか判断することは難しくても、複数本後なら認識率を高められる可能性を見出しました。

では自ずと次への展望も見えてくるでしょう。まずは他の時間足で同じようにテストを行ってみます。1分足はすぐにデータがすり替わってしまうので5分足くらいでこの精度を出せるのが理想です。そして次にそのもとで多次元多項式化とテクニカルの追加をもう一度試してみます。特にテクニカル指標は値動きに対してなんらかの相関を持っているはずなのでうまくやれば認識率をもっとあげられるのではないかと思います。
また一方で正解ラベルの見直しももう少し改善の余地があるかもしれません。今は価格が「上がった」「下がった」の2択で予測していますが「どれくらい上がったのか」というように値幅を考慮することで一気に実用性が増す可能性があります。そのへんはまたいずれ。

ではまた次回お会いしましょう!最後まで読んでいただきありがとうございました。

第4回はこちら

記録用コード
正解ラベルの見直しにおいて使ったコードをフルで残しておきます。少し急ぎで書いたので乱雑なのはお許しください。

# coding: UTF-8

import requests
import numpy as np
import pandas as pd

period = 60 # 時間足(秒単位)
response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = { "periods" : period ,"after" : 1})
response = response.json()


data_k = []
k_range = [5,10,15,20] # kの値の候補
l_range = [1,5,10,15,20,25,30] # lの値の候補
for k in k_range:
    data_l = []
    for l in l_range:
        x_data = []
        y_data = []

        # データダウンロード
        for i in range(6000-k-l):
            # xの要素
            arr = np.array(response['result'][str(period)][i:i+k])
            arr = arr[:,1:5].ravel()
            arr = (arr - arr.min())/(arr.max()-arr.min())-0.5
            x_data.append(arr)

            # yの要素
            if response['result'][str(period)][i+k+l][4] - response['result'][str(period)][i+k+1][1] > 0:
                target = 1
            else:
                target = 0
            y_data.append(target)

        x = np.array(x_data)
        y = np.array(y_data)

        # 識別器の選択
        from sklearn import linear_model
        clf = linear_model.LogisticRegression()

        # 層化してシャッフル
        from sklearn.model_selection import StratifiedShuffleSplit
        ss = StratifiedShuffleSplit(n_splits = 100, train_size = 0.5, test_size = 0.5)

        # ひとつの(k,l)に対して100回繰り返して平均と標準偏差を出す
        score = []
        for train_index, test_index in ss.split(x,y):
            x_train,x_test = x[train_index],x[test_index]
            y_train,y_test = y[train_index],y[test_index]
            # スケーリング
            from sklearn.preprocessing import StandardScaler
            sc = StandardScaler()
            sc.fit(x_train)
            x_train = sc.transform(x_train)
            x_test = sc.transform(x_test)
            # 学習とテスト
            clf.fit(x_train,y_train)
            score.append(clf.score(x_test,y_test))

        data_l.append("{0:.2f}+/-{1:.2f}".format(np.array(score).mean()*100,np.array(score).std()*100))
    data_k.append(data_l)

print(pd.DataFrame(data_k,index=k_range,columns=l_range))

この記事が気に入ったらサポートをしてみませんか?