AI

BTCFXの約定データで強化学習してみる (3)

前回前々回と全然ダメでしたので、先人の知恵を探しておりました。基本的に下記のアリさんの過去のつぶやきを沢山拝見しました。ありがとうございますm(. .)m またアリさんがつぶやかれていた下記論文に関しても一応ぼんやりと眺めました。

sigfinいくつかみたけど、個人的には「高頻度注文情報の符号化と深層学習による短期株価予測」がよかったというか、やっていることがbotとだいだい同じで、入力の正規化どうするかずっと悩んでたり、分類問題にしてたりで、単に同意したいところが多かった。

id:ultraist (@ultraistter) 2018年10月17日

前提

その上で、改めて考えた前提が下記になります。

予測に使うデータ

直近5分間の約定データ(サイド、価格、サイズ、約定時間)

データ数

データ数が合計で12,000になるように集約する。 0.05秒置きにBuy/Sell別々に集約したデータを5分間分、を想定してます。

サイド

成行注文のBuy or Sellです。BitflyerはNullの場合もあるっぽいので、それは無視を想定してます。

価格

価格は、カテゴライズされた相対価格です。とりあえず下記8カテゴリを想定しました。 マイナス側にも同様に8カテゴリあるので、全体で16カテゴリあります。

  • 〜10
  • 10 〜 30
  • 30 〜 50
  • 50 〜 100
  • 100 〜 200
  • 200 〜 500
  • 500 〜 1000
  • 1000 〜

どこからの相対価格にするのかというと、下記を想定しました。

  • 基準価格: 直近5分間の最後のBuy価格とSell価格の真ん中の価格

サイズ

サイズは離散値にした方がいいのかよく分かりませんが、最大値は決められないし、とりあえず離散値にすることにしました。下記を想定しております。

  • 〜 0.05
  • 0.05 〜 0.1
  • 0.1 〜 0.5
  • 0.5 〜 1
  • 1 〜 5
  • 5 〜 10
  • 10 〜 20
  • 20 〜 40
  • 40 〜 60
  • 60 〜 100
  • 100 〜

約定時間

これは、0.05秒間隔でcsvつくるので、配列の順番で相対的時間が分かる気がするので、データとしては削除することにしました。

アクション

下記を想定しております。

  • 買う
  • 売る
  • 何もしない

報酬

  • 利確成功: 1
  • ポジ有効時間切れ: -1

売買ルール

  • 10秒おきに、アクションを選択
  • IN注文は、必ず約定できるものとし、約定価格は、上記の基準価格とする
  • 利確幅: 50円
  • ポジション有効時間: 60秒

今回目指すこと

前回までの反省

  • 前回までは、適当に高難易度の結果を最初から求めていたので、どこをとってもめちゃくちゃであり、何がどう悪いのかすら分からなかった、というのがございます。
  • 前回、前々回ともに、先に進みた過ぎてコードも適当すぎた。なおかつ学習結果も「何も取引しないことを学んだだけ」という結果でした。

今回の目標

  • そこで今回は、報酬やアクションをできるだけシンプルにして、最重要課題である、超短期的値動き予測の精度のみを求めていきたいと思います。
  • とりあえず、学習結果が、プラスになって、全てのアクションをしっかり使っている(一応売買する)状態を見たいです。

データ

今回はごちゃごちゃしたJSONからやっとシンプルなCSVになりました。CSV作成段階から、サイズをカテゴライズしております。Side, Size, Priceの順です。

データの形式

1,0,0
2,0,0
1,3,392299.0
2,0,0
1,0,0
2,3,392268.0
1,0,0
2,3,392270.0
1,0,0
2,1,392270.0
1,1,392299.0
2,4,392271.4000000001
1,3,392295.81

データ生成コード

Numbaというのを使ってみたけど、引数と戻り値をしっかり規定通りの書き方で設定したりしないと有効にならないのかな??なんか全然速度が変わらなくて悲しい。

import pandas as pd
import datetime as dt
from numba import jit
import csv
import time
@jit('int8(float64)')
def _size_to_num(size):
if size > 100:
return 11
elif size > 60:
return 10
elif size > 40:
return 9
elif size > 20:
return 8
elif size > 10:
return 7
elif size > 5:
return 6
elif size > 1:
return 5
elif size > 0.5:
return 4
elif size > 0.1:
return 3
elif size > 0.05:
return 2
elif size > 0:
return 1
else:
return 0
# @jit
def create_csv_data(end_time_str):
df = pd.read_csv('raw_0213.csv')
val = ['exec_date', 'side', 'size', 'price']
df = df[val]
df['exec_date'] = pd.to_datetime(df['exec_date'])
newData = []
end = dt.datetime.strptime(end_time_str, '%Y-%m-%dT%H:%M:%S.%f')
start = end - dt.timedelta(milliseconds=50)
buyTotalPrice, buyTotalSize, sellTotalPrice, sellTotalSize = [0, 0, 0, 0]
df_rows = df.values
for idx in range(df.shape[0]):
row = df_rows[idx]
exec_date, side, size, price = row
if not size > 0:
continue
if not (start <= exec_date < end):
# date = start.strftime('%H:%M:%S.%f')[:-3]
buyAveragePrice = buyTotalPrice / buyTotalSize if buyTotalSize else 0
sellAveragePrice = sellTotalPrice / sellTotalSize if sellTotalSize else 0
newData.append([1, _size_to_num(buyTotalSize), buyAveragePrice])
newData.append([2, _size_to_num(sellTotalSize), sellAveragePrice])
buyTotalPrice = 0
buyTotalSize = 0
sellTotalPrice = 0
sellTotalSize = 0
end = start
start = end - dt.timedelta(milliseconds=50)
if side == 'BUY':
buyTotalSize += size
buyTotalPrice += price * size
elif side == 'SELL':
sellTotalSize += size
sellTotalPrice += price * size
return newData
print('start!')
t = time.time()
csvData = create_csv_data('2019-02-14T00:00:00.000')
with open('bf_data/0213.csv', 'w') as f:
writer = csv.writer(f)
writer.writerows(csvData)
print('finish!')
print(time.time() - t)

env.py

import gym
import numpy as np
import gym.spaces
import sys
import pandas as pd
from random import randint
import copy
from numba import jit
import time
def _read_csv():
df = pd.read_csv('bf_data/0213.csv')
return df.values
@jit('int8(int64)')
def _price_category(price_diff):
price_abs = abs(price_diff)
category = 0
if price_abs > 1000:
category = 8
if price_abs > 500:
category = 7
elif price_abs > 200:
category = 6
elif price_abs > 100:
category = 5
elif price_abs > 50:
category = 4
elif price_abs > 30:
category = 3
elif price_abs > 10:
category = 2
elif price_abs > 0:
category = 1
if price_diff < 0:
category *= -1
return category
class BfEnv(gym.Env):
def __init__(self):
super(BfEnv, self).__init__()
self.action_space = gym.spaces.Discrete(3)
self.observation_space = gym.spaces.Box(low=-8, high=11, shape=(12000, 3), dtype=np.int16)
self.reward_range = [-1, 1]
self.executions = _read_csv()
self.one_sec_data_num = 40 # 1秒間のデータ数
self.train_data_num = self.one_sec_data_num * 60 * 5 # 学習に必要なデータ数
self.train_order_blank_num = self.one_sec_data_num * 3 # 学習から注文までのブランクを3秒間とする
self.simulate_data_num = self.one_sec_data_num * 60 * 1 # シミュレーションに必要なデータ数
self.max_step_num = 200 # 1エポック当たりのステップ数
self.action_time_range = 10 # アクション発生の間隔(秒)
self.rikaku_price_range = 50 # 利確値幅(円)
self.idx = 0
self.done = False
self.step_count = 0
self.base_price = 0 # 基準価格
self.actions = []
self.rewards = []
self.reset()
def _reset_idx(self):
arr_len = self.executions.shape[0]
actions_num = (self.one_sec_data_num * self.action_time_range + 1) * (self.max_step_num - 1)
min_idx = actions_num + self.simulate_data_num + self.train_order_blank_num - 1
max_idx = arr_len - 1 - self.train_data_num
self.idx = randint(min_idx, max_idx)
def _next_step(self):
self.idx -= self.one_sec_data_num * self.action_time_range + 1
self._observe()
def reset(self):
t = time.time()
self._reset_idx()
self.done = False
self.step_count = 0
self.actions = []
self.rewards = []
self._observe()
# print('reset: ', time.time() - t, ' sec')
return self.observation
def _observe(self):
t = time.time()
train_data_start_idx = self.idx + 1 # start_idxの方が新しいデータ
train_data_end_idx = train_data_start_idx + self.train_data_num - 1
self._base_price(train_data_start_idx)
observation_temp = []
for idx in range(train_data_start_idx, train_data_end_idx + 1):
price_category = 0
if self.executions[idx][2] > 0:
price_diff = self.executions[idx][2] - self.base_price
price_category = _price_category(price_diff)
execution = copy.deepcopy(self.executions[idx])
execution[2] = price_category
observation_temp.append(execution)
self.observation = np.array(observation_temp)
# print('_observe: ', time.time() - t, ' sec')
def _base_price(self, idx):
buy_price, sell_price = [0, 0]
while True:
if buy_price > 0 and sell_price > 0:
break
if self.executions[idx][2] > 0:
if self.executions[idx][0] == 1 and buy_price == 0:
buy_price = self.executions[idx][2]
elif self.executions[idx][0] == 2 and sell_price == 0:
sell_price = self.executions[idx][2]
idx += 1
self.base_price = int((sell_price - buy_price) / 2 + buy_price)
# action 0 = hold, 1 = buy, 2 = sell
def step(self, action):
t = time.time()
self.step_count += 1
reward = 0
if action != 0:
reward = self._simulate(action)
# self.rewards.append(reward)
self.actions.append(action)
self._next_step()
self.done = self.max_step_num <= self.step_count
# print('step: ', time.time() - t, ' sec')
return self.observation, reward, self.done, {}
def _simulate(self, side):
t = time.time()
posi_price = self.base_price
rikaku_price = posi_price + self.rikaku_price_range
if side == 2:
rikaku_price = posi_price - self.rikaku_price_range
end_idx = self.idx - self.train_order_blank_num
start_idx = end_idx - self.simulate_data_num + 1
max_price = 0
min_price = 100000000
for idx in range(start_idx, end_idx + 1):
if 0 < self.executions[idx][2] < min_price:
min_price = self.executions[idx][2]
elif self.executions[idx][2] > max_price:
max_price = self.executions[idx][2]
if min_price <= rikaku_price <= max_price:
return 1
# print('_simulate: ', time.time() - t, ' sec')
return -1
def render(self, mode='human', close=False):
if self.step_count % 200 == 0:
out = sys.stdout
out.write(' '.join(str(num) for num in self.actions))
out.write('\n')
def _close(self):
pass
def _seed(self, seed=None):
pass

bf2,py

import gym
from keras.layers import Dense, Activation, Flatten
from keras.models import Sequential
from keras.optimizers import Adam
from rl.agents.dqn import DQNAgent
from rl.memory import SequentialMemory
from rl.policy import EpsGreedyQPolicy, LinearAnnealedPolicy
import bf2
ENV_NAME = 'bf-v2'
env = gym.make(ENV_NAME)
nb_actions = env.action_space.n
model = Sequential()
model.add(Flatten(input_shape=(1,) + env.observation_space.shape))
model.add(Dense(64))
model.add(Activation('relu'))
model.add(Dense(256))
model.add(Activation('relu'))
model.add(Dense(64))
model.add(Activation('relu'))
model.add(Dense(nb_actions))
model.add(Activation('linear'))
# print(model.summary())
memory = SequentialMemory(limit=300000, window_length=1)
policy = LinearAnnealedPolicy(
EpsGreedyQPolicy(),
attr='eps',
value_max=1.0,
value_min=0.1,
value_test=0.05,
nb_steps=2500
)
dqn = DQNAgent(
model=model,
nb_actions=nb_actions,
gamma=0.99,
memory=memory,
enable_double_dqn=False,
enable_dueling_network=False,
nb_steps_warmup=100,
target_model_update=1e-2,
policy=policy
)
dqn.compile(Adam(lr=1e-3), metrics=['mae'])
dqn.fit(env, nb_steps=50000, visualize=False, verbose=2)
print('TEST')
dqn.test(env, nb_episodes=10, visualize=True)

学習結果

一応取引をしてくれました。ただ、1step目と50000ステップ目でほぼrewardが変わってない気もします。

f:id:edo1z:20190224182802g:plain