教師あり機械学習を使ってBTC価格を予想してみる⑥
どうもお久しぶりです、alumiです。大学が始まって忙しくて全然更新できてませんでした。すみません。前回はこちら
いつの間にかビットコイン大暴落してましたね…。僕からするとこの荒れ相場でむしろbotが息を吹き返したのでありがたいことなのですが、このまま20万とかまで死んでしまうのはちょっと嫌かも…。botについては最後に少し宣伝をしますが、まずは本編から。
前回は特徴量の追加の候補を挙げたところで終わったと思うのでそれをひとつずつ試していきましょう。
前回のあらすじ
現時点での採用モデルの情報整理をしたよ!平均53%くらいは安定して予測できてるっぽい。
bot化してみてわかったこと
前回の記事を書いた後、現状のロジックを用いて1分足のbotを作りました。
具体的なアルゴリズムは以下です。
①過去4000件の1分足データで教師あり機械学習をして5~30分後の価格が現在価格に比べて「上昇」「下降」のどちらかかにクラス分類をする。
②次の1分足1000件分は①でfitしたモデルに沿って予測をし「上昇」予想なら買い、「下降」予想なら売りで入る(つまり毎分どちらかの注文は行います)。同時にキューの構造で決済注文リストを用意しておき、指定時間後に決済注文を行うようにする。
③注文拒否などによるポジションの予期せぬ予定量の超過を制御する。
つまり、毎分上か下かの予想にしたがって成り行きで売買する単純なアルゴリズムです(他にエラー対策や値幅の閾値設定などありますが一部省略してます)。上下予測が53%でできて、予測の成功失敗と値幅が無相関(と思っていた)ならば、これでも利益が出るだろうということでしばらく動かしていたのですが、どうもうまく利益が出なかったので、バックテストの損益分布を手元の1分足データを使って調べてみました。
すると面白いことがわかりました。
見ればわかるのですが、確かに損益の山は少しだけ右に寄っています。しかし5000以上の値幅になるときの累積度数が明らかに損失の方が回数が多くなっているのです。十分な回数試行した上でこの差が出ているため偶然ではないです。これは閾値を5000から広げても狭めても変わりませんでした。
つまり予測はできても外したときの損失が大きいため利益につながっていないという状況です。botの指標で言うなら勝率が高くても損益率が低いということです。
では何故このようになってしまったのか。これに関してはまだ確認していませんが、おそらく教師あり機械学習で学習したものは普通の値動きに対して強いが仕掛けに弱いのだろう、という仮説を立てました。つまり急激な値動きはあまり起こらないがゆえに現在の教師あり学習の方法では対応できていない!のでしょう。しかもみてる足が1分足なので簡単に予想外の動きが仕掛けられます。そういった理由でちょっとこの戦略は厳しいのかな、と思ってます。また進捗は報告します。
コードの供養
書いたコードの供養です。ところどころ省略しているのでコピーしただけでは動かないですし、自分で一から作った方が早いと思いますが、アルゴリズムは公開しておきます。
# coding: UTF-8
#-----------------------
# 教師あり機械学習による価格の上下予測に基づいた1分足戦略
#-----------------------
# 必要ライブラリimport
import hashlib
import hmac
import requests
import datetime
import json
from key import API_KEY,API_SECRET
import ccxt
from pprint import pprint
import time
import numpy as np
import pandas as pd
import smtplib
from email.mime.text import MIMEText
from email.header import Header
import logging.config
# sklearnのimport
from sklearn import linear_model
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import confusion_matrix,f1_score
logging.config.fileConfig('logging.conf')
logger = logging.getLogger()
logger.log(100,"====bot使用再開====")
period = 60
# ===============================================================
# 機械学習
# モデル決定
def machine_learning():
response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = { "periods" : period , "after" : 1 })
response = response.json()
best = 0
k_range = [5,10,20,30] # kの値の候補
l_range = [10,20] # lの値の候補
for k in k_range:
for l in l_range:
x_data = []
y_data = []
p_diff = []
# データダウンロード
for i in range(len(response['result'][str(period)])-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の要素
diff = response['result'][str(period)][i+k+l][4] - response['result'][str(period)][i+k+1][1]
if diff > 0:
target = 1
else:
target = -1
y_data.append(target)
tr = 3000 # 学習データの数
ts = 1000 # テストデータの数
x = np.array(x_data)
x_train = x[-tr-ts:-ts]
x_train_test = x[-ts:]
y = np.array(y_data)
y_train = y[-tr-ts:-ts]
y_train_test = y[-ts:]
# 識別器の選択
clf = linear_model.LogisticRegression()
# スケーリング
sc = StandardScaler()
sc.fit(x_train)
x_train = sc.transform(x_train)
x_train_test = sc.transform(x_train_test)
# グリッドサーチ
C_range = 10**np.arange(-10.0,10.0)
param = {'C':C_range}
gs = GridSearchCV(clf,param)
gs.fit(x_train,y_train)
clf = gs.best_estimator_
# 学習とテスト
clf.fit(x_train,y_train)
train_test_score = clf.score(x_train_test,y_train_test)
# 分析
y_train_pred = clf.predict(x_train_test)
cmat = confusion_matrix(y_train_pred,y_train_test)
f = f1_score(y_train_test,y_train_pred,average='macro')
if f > best:
print(cmat)
best = f
best_train_test_score = train_test_score
best_l = l
best_k = k
best_x_train = x_train
best_y_train = y_train
print("best param:k={0},l={1}".format(best_k,best_l))
return best_k,best_l,best_x_train,best_y_train
# 教師あり学習による予測
def prediction(k,l,x_train,y_train,x_data):
# 識別器の選択
from sklearn import linear_model
clf = linear_model.LogisticRegression()
# スケーリング
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
sc.fit(x_train)
x_train = sc.transform(x_train)
# グリッドサーチ
from sklearn.model_selection import GridSearchCV
C_range = 10**np.arange(-10.0,10.0)
param = {'C':C_range}
gs = GridSearchCV(clf,param)
gs.fit(x_train,y_train)
clf = gs.best_estimator_
# 学習とテスト
clf.fit(x_train,y_train)
pred = clf.predict([x_data])
return pred
# 現在の特徴量ベクトルの作成
def make_data(k):
response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params = { "periods" : period , "after" : 1 })
response = response.json()
arr = np.array(response['result'][str(period)][-k:])
arr = arr[:,1:5].ravel()
arr = (arr - arr.min())/(arr.max()-arr.min())-0.5
# print(len(arr))
return arr
# 取引所API関連の関数は省略
# ===============================================================
# アルゴリズム
cnt = 0
max_size = 0.2
flag = {
'operation':True
}
cm = np.ones((2,2))
while flag['operation']:
# 学習の更新
if cnt % 1000 == 0:
# 残っているポジションの解消
code = 0
try:
# 建玉の確認(あれば[{},{},...]、なければ[]が返ってくる)
while code != 200:
code,pos = position()
time.sleep(2)
# 残っている建玉のside確認(残っていなければここでIndexError発生)
ws = pos[0]['side']
if ws == "BUY":
sid = "SELL"
else:sid = "BUY"
# 残っている建玉の合計sizeを計算し決済
siz = 0
for i in range(len(pos)):
siz += pos[i]['size']
if int(siz*100) == 0:
logger.warning("端数が残っているので都合上最小注文をします")
siz = 0.01
code = cnt_m = 0
while code != 200:
code = market(sid,int(siz*100)/100)
time.sleep(3)
cnt_m += 1
if cnt_m > 40:
flag['operation'] = False
logger.warning("一定時間注文が通らなかったので稼働停止します")
break
logger.log(100,"全決済注文をしました")
except (IndexError,KeyError):
logger.log(100,"ポジションは残っていません")
except ValueError:
logger.warning("ポジションの取得に失敗しJSONDecodeErrorが発生したと考えられます")
# 全注文のキャンセル
code = 0
while code != 200:
code = cancelorders()
time.sleep(3)
logger.log(100,"全注文をキャンセルしました")
# 機械学習
k,l,x_train,y_train = machine_learning()
logger.log(100,"学習の結果k={0},l={1}が選択されました".format(k,l))
# 適切な注文サイズの計算
size = int(max_size*100/l)/100
# 格納用配列の準備
reserve = np.zeros(l)
ID_list = np.zeros(l)
price_list = np.zeros(l)
side_list = np.zeros(l)
cm = np.ones((2,2))
time.sleep(3)
# 決済注文
code = cnt_m = 0
if reserve[0] == 1:
while code != 200:
code = market("BUY",size)
time.sleep(3)
cnt_m += 1
if cnt_m > 40:
flag['operation'] = False
logger.warning("2分間注文が通らなかったので稼働停止します")
break
code = 0
while code != 200:
code = cancel_order(ID_list[0])
time.sleep(2)
print("買い決済しました")
elif reserve[0] == -1:
while code != 200:
code = market("SELL",size)
time.sleep(3)
cnt_m += 1
if cnt_m > 40:
flag['operation'] = False
logger.warning("2分間注文が通らなかったので稼働停止します")
break
code = 0
while code != 200:
code = cancel_order(ID_list[0])
time.sleep(2)
print("売り決済しました")
else:
time.sleep(4)
pass
# 使用済みデータの破棄
reserve = np.delete(reserve,0)
ID_list = np.delete(ID_list,0)
side_list = np.delete(side_list,0)
price_list = np.delete(price_list,0)
# 新規注文
if cnt % 1000 < 1000-l:
# 予測
data = make_data(k)
judge = prediction(k,l,x_train,y_train,data)[0]
# 予測に基づいて成行注文(IFDOCO)
price = get_price()
code = cnt_m = 0
if judge == 1:
while code != 200:
code,id = IFDOCO_order("BUY",size,price+40000,price-40000)
time.sleep(2)
cnt_m += 1
if cnt_m > 60:
flag['operation'] = False
logger.warning("2分間注文が通らなかったので稼働停止します")
id['parent_order_acceptance_id'] = "error"
break
ID_list = np.append(ID_list,id['parent_order_acceptance_id'])
reserve = np.append(reserve,-1)
side_list = np.append(side_list,1)
price_list = np.append(price_list,price)
print("買い注文しました")
elif judge == -1:
while code != 200:
code,id = IFDOCO_order("SELL",size,price-40000,price+40000)
time.sleep(2)
cnt_m += 1
if cnt_m > 60:
flag['operation'] = False
logger.warning("2分間注文が通らなかったので稼働停止します")
id['parent_order_acceptance_id'] = "error"
break
ID_list = np.append(ID_list,id['parent_order_acceptance_id'])
reserve = np.append(reserve,1)
side_list = np.append(side_list,-1)
price_list = np.append(price_list,price)
print("売り注文しました")
else:
ID_list = np.append(ID_list,0)
reserve = np.append(reserve,0)
side_list = np.append(side_list,0)
price_list = np.append(price_list,price)
time.sleep(2)
pass
# 調整
try:
# 建玉の確認(あれば[{},{},...]、なければ[]が返ってくる)
code = 0
while code != 200:
code,pos = position()
print(code)
time.sleep(2)
# 残っている建玉のside確認(残っていなければここでIndexError発生)
ws = pos[0]['side']
if ws == "BUY":
sid = "SELL"
else:sid = "BUY"
# 残っている建玉の合計sizeを計算
siz = 0
for i in range(len(pos)):
siz += pos[i]['size']
code = cnt_m = 0
if int(siz*100)/100 > max_size:
print(siz-max_size)
while code != 200:
code = market(sid,int((siz-max_size)*100)/100)
time.sleep(3)
cnt_m += 1
if cnt_m > 20:
flag['operation'] = False
logger.warning("2分間調整注文が通らなかったので稼働停止します")
break
logger.log(100,"調整注文しました")
except (IndexError,KeyError):
logger.log(100,"ポジションはありません")
except ValueError:
logger.log(100,"ポジションの取得に失敗しJSONDecodeErrorが発生したと考えられます")
time.sleep(5)
# logger.log(100,"タイム計測テスト")
cnt += 1
print(cnt)
time.sleep(44)
# CM結果log出力
if cnt % 1000 == 0:
logger.log(100,cm)
logger.log(100,sc)
# 緊急停止プログラム
try:
code = 0
while code != 200:
code,res = get_asset()
time.sleep(2)
if res['open_position_pnl'] < -200000*size or res['open_position_pnl'] > 200000*size:
flag['operation'] = False
logger.warning("botを緊急停止します")
else:
pass
except (IndexError,KeyError):
logger.warning("資産状況の取得の際エラーが発生しました")
# ===============================================================
# 緊急停止処理
logger.log(100,"====ポジションと注文を解消します====")
# 省略
これからの展望
今まではscikit-learnで扱えるような教師あり機械学習しか試していませんでした。しかし、いつまでもそれでやっていてもこっちがつまらないので、とりあえず大学の復習も兼ねてDNNを実装してみようと思います。流行りのディープラーニングです。これはただの計算ゴリ押しなので特に期待はしていませんが、ここから見えてくる何かがあるかもしれないのでライブラリなど使わず実装したいなと思ってます(想像より大変です)。その後強化学習を試してみようかと思います。強化学習とは、またその時に説明しますが、囲碁や将棋、または迷路やマリオなどゲームの世界で今躍進しているアルゴリズムです。これを応用して凄腕トレーダーbotが作れないかな?と思ったので色々試していこうと思います。こういう風に色々試せるのは楽しいですね。個人的にはmmbotやSFDbotブームの次は機械学習botのブームくるんじゃないかな?って期待してます(または原点回帰のスイングbot)。11月初めにあった天下一bot会のオフ会で色々な刺激をもらってモチベーションが上がったので、年内にもう何記事か更新できたらと思っています。よろしくお願いします。
おまけ(宣伝)
宣伝ですが、8月ごろに売っていた僕のbotが9月ごろからのボラの低下によって成績がヨコヨコになっていたのですが、最近の値動きがまた得意な相場に戻ってきて、この間バックテストにおいて最高利益を更新したため再販しようと思います。(以前までに買っていただいていた人は、ずっと動かし続けていれば少なくとも収支プラスにはなっていると思います)
このままビットコインが大暴落するという未来もなくはないと思っているので初めの5部だけ特別価格で15000円にしておきます(以前は5万円で売っていたものなので僕的には特価のつもりです)。その後は様子を見てまた値上げしていきます。
ご興味のある方は、無料説明部分をよく読んでご購入いただけたらと思います。以上宣伝でした(●´_ _)ペコ
最後まで読んでいただきありがとうございました。