#はじめに
理系大学生で現在修士2年生です。
専攻は情報系で金融に関してはほぼ知識はないので勉強中です。
今回Smart Tradeでインターンすることになり、QuantXで機械学習を使ったアルゴリズムを開発しので、まとめてみます。
#目標
・QuantX上で簡単な機械学習を試してみる。
・QuantXで機械学習使って株価予測してみたpart1を改良してみます
###前回からの改良点1
前回の作った機械学習モデルは「t日後の価格が上がっているか(下がっているか)どうか」の2値分類するモデルでした。今回は「t日以内に先にn%上がるか先にn%下がるか、またはどちらでもないか」の3値分類をする機械学習モデルを作ります。単純にt日後の価格が上がっているか予測するよりも、ある程度動くときだけ予測するモデルの方が精度上がると思いました。
###前回からの改良点2
前回の手順2(株価データから特徴抽出)をより良いものに改善します。
具体的には後ほど、前回の問題点を述べ改善策を書きます。
#今回の大まかな手順
- 教師データの生成
- 株価データから特徴抽出
- 学習
- QuantX上でバックテストと結果を可視化
基本的にはQuantXで機械学習使って株価予測してみたpart1と手順2~4は同じです。
完成したサンプルコードはこちら
#解説
手順に沿って、重要な部分だけ解説します。
##0.準備
今回使用するライブラリ一覧です。
5行目で使用する機械学習モデルをインポートしています。
import numpy as np
import pandas as pd
import talib as ta
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
使用する銘柄と要素の選択。今回は日経225連動型上場投資信託と終値のみを使用。
ここで扱う銘柄を変更したり、複数選択できます。
ctx.logger.debug("initialize() called")
ctx.configure(
channels={ # 利用チャンネル
"jp.stock": {
"symbols": [
"jp.stock.1321", #日経225連動型上場投資信託
],
"columns": [
"close_price_adj", # 終値(株式分割調整後)
]
}
}
)
##1. 教師データの生成
今回の教師データは終値を基準として、ある時点からt日以内に先に(n%下落する前に)n%上昇したら1、(n%上昇する前に)n%下落したら-1、どちらにも移動しなかったら0というラベルをつけます。
例えば画像のような日毎の株価データの終値があったとします。
01/01での終値は1,000で、01/02の終値は1,050で5%上昇しています。先に2%以上上昇したので、01/01のラベルは+1とします。
01/02での終値は1,050で、01/03の終値は1,030で01/02の時と比較して1.9%下落してます。この時点ではまだラベルはつきません。次に01/04の終値は1,020なので01/02での終値と比べて2.8%下落したので、先に2%以上下落したのでラベルは-1になります。
01/06での終値は1,000で、直後の3日間(01/07~01/09)と比較しても±2%どちらにも変動しないので、ラベルは0になります。
(01/09の場合はそれ以降のデータを書いてないため、空白にしてあります。)
今回は「ある時刻からt日以内に先にn%以上上昇するか、n%以上下落するか、またはどちらでもないか」を予測する3値分類になります。
以上の教師データを生成するためのコードは以下になります。periodとpercentはそれぞれ上記いう「t」と「n」に対応しています。ですのでperiodとpercentを変更することで、好きな「期間」と「%」を設定することができます。例えば長期で大きなリターンを予測したい場合はperiod、percentをそれぞれ大きくするとできます。
# ここから手順1(教師データの生成)
# label(教師データ)の作成
label = []
period = 7 # 何日以内
percent = 0.02 # 何パーセント予測するか
# 正解データの生成
for i in range(len(cp["jp.stock.1321"])-period): # 最後のperiod日間は無視
for j in range(1, period+1):
rate = 0
rate = (cp["jp.stock.1321"][i+j]-cp["jp.stock.1321"][i])/cp["jp.stock.1321"][i] # 時刻iと時刻(i+j)の変化率を計算
if rate>=percent: # rateがpercentを超えた時,教師データ1を追加
label.append(1)
break
elif rate<=-percent: # rateが-percentを下回った時,教師データを-1
label.append(-1)
break
elif j == period: # どちらにも動かなかった時教師データ0
label.append(0)
# 最後のperiod個は値を入れられてないので代わりに0を入れとく(必須)
for i in range(period):
label.append(0)
##2. 株価データから特徴量を抽出
特徴量を入れるための型を準備します。(今回は適当に選びました。)
使用したいテクニカル指標がある場合は、自由に指標を追加しても大丈夫です。
# ここから手順2(株価データから特徴抽出)
# 特徴量を入れるための準備
rsi7 = pd.DataFrame(0, index=cp.index, columns=["rsi7"])
sma14 = pd.DataFrame(0,index=cp.index, columns=["sma14"])
sma7 = pd.DataFrame(0,index=cp.index, columns=["sma7"])
sma7_diff = pd.DataFrame(0,index=cp.index, columns=["sma7_diff"])
sma14_diff = pd.DataFrame(0,index=cp.index, columns=["sma14_diff"])
sma_diff = pd.DataFrame(0,index=cp.index, columns=["sma_diff"])
bb_mid = pd.DataFrame(0,index=cp.index, columns=["bb_mid"])
bb_up = pd.DataFrame(0,index=cp.index, columns=["bb_up"])
bb_low = pd.DataFrame(0,index=cp.index, columns=["bb_low"])
bb_diff = pd.DataFrame(0,index=cp.index, columns=["bb_diff"])
上で用意した型に、はじめにimportしたtalib(テクニカル指標を計算してくれるライブラリ)を使って値を代入します。talibに関してはこちらを参照ください。
# Ta-libを使って指標を追加
rsi7['rsi7'] = ta.RSI(cp["jp.stock.1321"].values.astype(np.double), timeperiod=7)
sma14['sma14'] = ta.SMA(cp["jp.stock.1321"].values.astype(np.double), timeperiod=14)
sma7['sma7'] = ta.SMA(cp["jp.stock.1321"].values.astype(np.double), timeperiod=7)
bb_up['bb_up'], bb_mid['bb_mid'], bb_low['bb_low'] = ta.BBANDS(cp["jp.stock.1321"].values.astype(np.double), timeperiod=14)
ここまでは前回と同じです。
前回は移動平均線の1つ前の時刻との差分、2つの異なる期間の移動平均線の差分を取っていました。しかしこれは株価の変動を考慮しておらず適切ではないと思いました。ですので今回は移動平均線の上昇率、2つの異なる期間の移動平均線を現在価格で割ってスケールを調整しました。(ボリンジャーバンドに関しても同様です。)
RSIとかはもともと、0~100の間しか取らないのでそのまま使えると思います。(識別器がランダムフォレストの場合は)
イメージとしては以下の図のようにしました。前回は「移動平均線の差」で、今回は「移動平均線の変動率」を特徴量にしました。
sma7_diff['sma7_diff'] = sma7.pct_change()
sma14_diff['sma14_diff'] = sma14.pct_change()
sma_diff['sma_diff'] = (sma7['sma7'] - sma14['sma14'])/cp['jp.stock.1321']
bb_diff['bb_diff'] = bb_mid['bb_mid'] - bb_up['bb_up']
bb_diff['bb_diff'] = bb_diff.pct_change()
上で用意した特徴量を1つのデータフレームに統一。これは用意した特徴量を学習ができる形に変換します。
# 特徴量の結合
df_x = pd.concat([rsi7, sma7_diff, sma14_diff, sma_diff, bb_diff], axis = 1)
何が起きてるか分からない方は、以下のコードをコメントアウト外して実行してみると、df_xの中身が見れるのでイメージが掴めるかもしれません。
# ここで確認できるよ。
# ctx.logger.debug(df_x)
##3. 学習
モデルの汎化性能を測るために、学習データとテストデータに分割します。今回は前半5割を学習データにします。shuffleは必ずFalseにして下さい。
(ちなみにQuantXは、(バックテストの期限+1年)分のデータを読み込んでいます。また現状では、バックテスト期間の最長は3年なので、一つの銘柄につき4年分のデータしか読み込むこどができません…。今後のアップデートでもっと長期間読み込めるようになるかもです。)
# ここから手順3
# 学習データとテストデータに分割(前半5割を学習データ,後半5割をテストデータ)
X_train, X_test, y_train, y_test = train_test_split(df_x, y, train_size=0.5,shuffle = False)
X_trainの中身を見て貰えれば分かるのですが、最初のn行には値が入っておらずNANになっています。これは各時刻で指標を計算するとき、過去のn日分のデータを使用します。ですので、最初のn行は指標を計算する過去データがないため、NANになります。学習をする際、このデータが混じっているとエラーになってしまうので消しときます。
(今回の場合でいうと、手順2でテクニカル指標を計算する際、最大過去14日分のデータを使ったので、nは14になります。)
# はじめのNAN削除
X_train = X_train[14:]
y_train = y_train[14:]
最後に学習データを使ってモデルを学習します。
今回はランダムフォレストを使いましたが、色々試してみるといいと思います。ただ、他の代表的な機械学習システムはハイパーパラメータの調整であったり、データの標準化が必要になってきます。一方ランダムフォレストは、決めるハイパーパラメータも少なくデータの標準化も不要なので手軽に使えて精度もいいので今回は使用しました。
# 学習データを使って学習(モデルはランダムフォレスト)
clf = RandomForestClassifier(random_state=1, n_estimators = 300,
max_leaf_nodes = 10, max_depth=12, max_features=None, min_samples_split = 0.1)
clf = clf.fit(X_train, y_train)
##4. 予測
最後に学習したモデルを使って、テストデータに対して予測しています。
QuantX上でバックテストを行う場合、最終的なsignal(今回は+1,-1,0の入った予測)を初めに読み込んだ「cp」とインデックスを合わせないとエラーになってしまいます。ですので「test」にはテストデータに使ったデータと手順3で取り除いだNANの分のsignalを用意します。(全てFalse)
これに予測したpredを結合し、QuantX上でエラーを出さない形に変換します。
# ここから手順4
# テストデータを使って予測
pred = clf.predict(X_test)
# signalのインデックスを合わせる
test = np.ones(len(X_train)+14, dtype=np.bool) * False
signal = np.hstack((test, pred))
signal = pd.DataFrame(data = signal, columns=cp.columns, index=cp.index)
#注文方法の設定
「ある時刻からt日以内に先にn%以上上昇するか、n%以上下落するか、またはどちらでもないか」を予測する3値分類でした。また、それぞれ+1、-1、0を割り当てました。
ですので、シグナルには+1、-1、0のいずれかの値が入っています。
今回は+1の時はt日以内にn%先に上がると予測するので買い注文、-1の時はt日以内に先にn%下がると予測するので売り注文を出します。0の時はポジションのリターンに応じて利確、損切りをします。以下がソースコードになります。
# 1321を取引するため
bull = ctx.getSecurity("jp.stock.1321")
# シグナルを取得
signal = current["signal"][0]
# signal毎に注文をだす
if signal == 1:
bull.order_target_percent(0.5, comment="BULL BUY")
elif signal == -1:
bull.order_target_percent(0, comment="BULL SELL")
else:
for (sym,val) in ctx.portfolio.positions.items():
returns = val["returns"]
if returns < -0.01:
sec = ctx.getSecurity(sym)
sec.order_target_percent(0, comment="損切り(%f)" % returns)
elif returns > 0.03:
sec = ctx.getSecurity(sym)
sec.order_target_percent(0, comment="利確(%f)" % returns)
#結果
今回はバックテスト期間を2015/01/01~2017/12/31で行いました。QuantXでは実行時に、バックテスト期間+1年分のデータを読み込むため、読み込むデータは2014/01/01~2017/12/31になります。また、前半5割をテストデータとしため、実際に検証を行ってる期間は2016/01/01~2017/12/31になっています。(多少のずれはあります。)
以下が結果の画像ですがなんとも言えない結果になってます。
取引履歴の詳細が以下になります。赤丸が買い注文、青色が売り注文になります。レンジ相場では取引が多いものの、右上の綺麗に上昇している時には取引していなかったりします。こういった部分は学習データに依存するので、今後「どういった時に取引するアルゴリズムを作りたいか」を決め、教師データから考え直す必要があると思います。
(具体的にはトレンドが出てる時に取引したいのであれば、トレンドが出ていると判断できるようなルールを決め、ルールベースでその条件を満たしたときのデータのみで学習し予測とか…。色々方法はあると思います。)
#まとめ
最後まで読んでいただきありがとうございました。
今回は前回の記事から少し改善してみました。(したつもりです。)
まだまだ良いアルゴリズムとは言えないので今後もQuantXと機械学習を使った記事を挙げていきます。記事に関する誤った知識がごいざいましたらご指摘よろしくお願いします。
#QuantX勉強会の宣伝
SmartTrade社では毎週水曜日と金曜日に18:00から勉強会を行っています。
https://python-algo.connpass.com/