Edited at

OpenAI GymでFXのトレーディング環境を構築する

More than 1 year has passed since last update.

(このプログラムはまだ動作確認もまともに行っていません。随時アップデートしますので気になる方はいいねなりストックなりしてアップデートをお待ちください。)

元記事とリポジトリはこちらからどうぞ。


はじめに

FXのトレーディングアルゴリズムを強化学習で作ってみたい!

というわけで、界隈では人気のOpenAI Gymのenvでトレーディング環境を作ってみようと思います。


下準備

まずはgymをインストールしておきます。

pip install gym


環境の仕様

こちらのEUR/USDの1分足ヒストリカルデータを4ヶ月分ダウンロードし、

1分足、5分足、30分足、4時間足と4つの時間足を64本分作成します。

なぜこれらの時間足にしたかというと、MT4で指定可能な時間足で馴染みがあり、1分足->5分足は5倍、5分足->30分足は6倍と、各々5~8倍長期に設定されているためです。

とりあえず観測できる情報を自分と同じにしてみようというコンセプトです。

本当は自分が裁量でトレードするときは5分足と1時間足くらいしか見ないのですが、ここらへんは後々調整してみます。


コーディング

OpenAI gymの環境はgym.Envを継承して行います。

今回はFxEnvというクラスを作成します。

class FxEnv(gym.Env):

Envは _reset、_stepとaction_space、observation_spaceが必要なようです。

また、_renderがあると学習の様子をレンダリングできます。


__init__

action_spaceやobservation_space、その他の初期情報は__init__で指定します。

ヒストリカルデータはカレントディレクトリにダウンロードされていることが前提です。

(後々スクレイピングするように改良してもよいのですが月1のことなので…)

    def __init__(self):

# 定数
self.STAY = 0
self.BUY = 1
self.SELL = 2
self.CLOSE = 3
# 対象となる通貨ペアの最大値
self.MAX_VALUE = 2
# 初期の口座資金
self.initial_balance = 10000
# CSVファイルのパス配列(最低4ヶ月分を昇順で)
self.csv_file_paths = []
now = datetime.datetime.now()
for _ in range(4):
now = now - relativedelta.relativedelta(months=1)
filename = 'DAT_MT_EURUSD_M1_{}.csv'.format(now.strftime('%Y%m'))
if not os.path.exists(filename):
print('ファイルが存在していません。下記からダウンロードしてください。', filename)
print('http://www.histdata.com/download-free-forex-historical-data/?/metatrader/1-minute-bar-quotes/EURUSD/')
else:
self.csv_file_paths.append(filename)
# スプレッド
self.spread = 0.5
# Point(1pipsの値)
self.point = 0.0001
# 利食いpips
self.take_profit_pips = 30
# 損切りpips
self.stop_loss_pips = 15
# ロット数
self.lots = 0.01
# ロット数
self.lot_base = 100000
# 0~3のアクション。定数に詳細は記載している
self.action_space = gym.spaces.Discrete(4)
# 観測できる足数
self.visible_bar = 32
# 1分足、5分足、30分足、4時間足の5時系列データを足数分作る
self._reset()
self.observation_space = spaces.Box(low=0, high=self.MAX_VALUE, shape=numpy.shape(self.make_obs('ohlc_array')))


_reset

_resetでは4ヶ月分のCSVを読み込んでpandasのDataFrameに変換します。

observationを返さないとkeras-rlが初期化で止まってしまうためmake_obsの戻り値を返しておきます。

        self.info = AccountInformation(self.initial_balance)

# CSVを読み込む
self.data = pandas.DataFrame()
for path in self.csv_file_paths:
csv = pandas.read_csv(path,
names=['date', 'time', 'open', 'high', 'low', 'close', 'v'],
parse_dates={'datetime': ['date', 'time']},
)
csv.index = csv['datetime']
csv = csv.drop('datetime', axis=1)
csv = csv.drop('v', axis=1)
self.data = self.data.append(csv)
# 最後に読んだCSVのインデックスを開始インデックスとする
self.read_index = len(self.data) - len(csv)
# そこから開始位置をランダムにずらす(5日分(7220分)は残す)
# self.read_index += numpy.random.randint(0, (len(csv) - 7220))
# チケット一覧
self.tickets = []
return self.make_obs('ohlc_array')


_step

_stepではエージェントの行動を受けとり、環境や報酬を返します。

__initで指定したとおりaction_spaceは0~3の4通りなので、それに沿って報酬を決めていきます。

    def _step(self, action):

current_data = self.data.iloc[self.read_index]
ask = current_data['close'] + self.spread * self.point
bid = current_data['close'] - self.spread * self.point

if action == self.STAY:
for ticket in self.tickets:
if ticket.order_type == self.BUY:
if bid > ticket.take_profit:
# 買いチケットを利確
profit = (ticket.take_profit - ticket.open_price) * ticket.lots * self.lot_base
self.info.balance += profit
self.info.total_pips_buy += profit
elif bid < ticket.stop_loss:
# 買いチケットを損切り
profit = (ticket.stop_loss - ticket.open_price) * ticket.lots * self.lot_base
self.info.balance += profit
self.info.total_pips_buy += profit
elif ticket.order_type == self.SELL:
if ask < ticket.take_profit:
# 売りチケットを利確
profit = (ticket.open_price - ticket.take_profit) * ticket.lots * self.lot_base
self.info.balance += profit
self.info.total_pips_sell += profit
elif bid < ticket.stop_loss:
# 売りチケットを損切り
profit = (ticket.open_price - ticket.stop_loss) * ticket.lots * self.lot_base
self.info.balance += profit
self.info.total_pips_sell += profit
elif action == self.BUY:
ticket = Ticket(self.BUY, ask, ask + self.take_profit_pips * self.point,
ask - self.stop_loss_pips * self.point, self.lots)
self.tickets.append(ticket)
pass
elif action == self.SELL:
ticket = Ticket(self.SELL, bid, bid - self.take_profit_pips * self.point,
bid + self.stop_loss_pips * self.point, self.lots)
self.tickets.append(ticket)
pass
elif action == self.CLOSE:
for ticket in self.tickets:
if ticket.order_type == self.BUY:
# 買いチケットをクローズ
profit = (bid - ticket.open_price) * ticket.lots * self.lot_base
self.info.balance += profit
self.info.total_pips_buy += profit
elif ticket.order_type == self.SELL:
# 売りチケットをクローズ
profit = (ticket.open_price - ask) * ticket.lots * self.lot_base
self.info.balance += profit
self.info.total_pips_sell += profit

# インデックスをインクリメント
self.read_index += 1
# obs, reward, done, infoを返す
return self.make_obs('ohlc_array'), self.info.total_pips_buy + self.info.total_pips_sell, self.info.balance <= 0 or self.read_index >= len(self.data), self.info


観測情報の作成

先述の通り、1分足~4時間足を観測情報とするため、1分足からリサンプリングを行います。

pandasのohlc()は1つの列からohlcを作成してしまうため、元の1分足がohlc形式だとohlcそれぞれにohlcを作成してしまい16列に分裂してしまいます。

元がohlcの場合の集約方法はこちらに書いてありました。

m1 = numpy.array(target.iloc[-1 * self.visible_bar:][target.columns])

m5 = numpy.array(target.resample('5min').agg({'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last'}).dropna().iloc[-1 * self.visible_bar:][target.columns])
m30 = numpy.array(target.resample('30min').agg({'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last'}).dropna().iloc[-1 * self.visible_bar:][target.columns])
h4 = numpy.array(target.resample('4H').agg({'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last'}).dropna().iloc[-1 * self.visible_bar:][target.columns])
return numpy.array([m1, m5, m30, h4])


gym.envに登録する

最初にpip installしたgymのディレクトリにenvディレクトリがあるので、

FxEnvというディレクトリを作成し、fx_env.pyと下記の__init__.pyを置きます。


__init.py

from gym.envs.fx_env.fx_env import FxEnv


また、envディレクトリに存在する__init.pyを開き、FxEnvを登録するコードを追記します。

とりあえず動作検証なので7200step(5日間分)だけ動かします。

# FxEnv

register(
id='FxEnv-v0',
entry_point='fx_env:FxEnv',
max_episode_steps=7200,
)


とりあえず動かしてみる

ここでは、ランダムで傍観、買い、売り、手仕舞いを繰り返すという単純なロジックで環境を利用してみましょう。

import gym

import random

env = gym.make('FxEnv-v0')

Episodes = 1

obs = []

for _ in range(Episodes):
observation = env.reset()
done = False
count = 0
while not done:
action = random.choice([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3])
observation, reward, done, info = env.step(action)
obs = obs + [observation]
# print observation,reward,done,info
count += 1
if done:
print('reward:', reward)
print('steps:', count)

実行すると、以下のような出力が得られます。

reward:10119.1893579

steps:7200

今回はランダムでもやや勝ったみたいですね。

次回以降は学習するロジックを組みながら環境のバグを取っていこうと思います。

学習のロジックは別の記事に上げますが、環境に関してはこの記事をアップデートする予定です。