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

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

元記事とリポジトリは[こちら](http://tono-n-chi.com/blog/2017/09/openai-gym-fx-env/)からどうぞ。

## はじめに

FXのトレーディングアルゴリズムを強化学習で作ってみたい！
というわけで、界隈では人気の[OpenAI Gym](https://gym.openai.com/)のenvでトレーディング環境を作ってみようと思います。

## 下準備

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

```bash
pip install gym
```

## 環境の仕様

[こちら](http://www.histdata.com/download-free-forex-historical-data/?/metatrader/1-minute-bar-quotes/EURUSD/)の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というクラスを作成します。

```py
class FxEnv(gym.Env):
```

Envは \_reset、\_stepとaction_space、observation_spaceが必要なようです。
また、_renderがあると学習の様子をレンダリングできます。

### \_\_init\_\_

action_spaceやobservation_space、その他の初期情報は\_\_init\_\_で指定します。
ヒストリカルデータはカレントディレクトリにダウンロードされていることが前提です。
（後々スクレイピングするように改良してもよいのですが月1のことなので…）

```py
    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の戻り値を返しておきます。

```py
        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通りなので、それに沿って報酬を決めていきます。

```py
    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の場合の集約方法は[こちら](https://stackoverflow.com/questions/36222928/pandas-ohlc-aggregation-on-ohlc-data)に書いてありました。

```py
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日間分）だけ動かします。

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


## とりあえず動かしてみる

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

```py
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
```

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

次回以降は学習するロジックを組みながら環境のバグを取っていこうと思います。
学習のロジックは別の記事に上げますが、環境に関してはこの記事をアップデートする予定です。