深層強化学習（DQN）で学習するFXトレードエージェントを作ってみた

どうも、オリィ研究所の ryo_grid こと神林です。
こんにちは。

時系列データに対するディープラーニング適用の一例として、深層強化学習（DQN）させたトレードエージェント（まともなパフォーマンスを発揮する）のモデルを作成し、FX自動トレード（のシミュレーション）をするということにトライしてきました。

[　深層強化学習でのFX自動トレード（のシミュレーション）がうまくいかないのでオレオレ手法を考えた - Qiita](https://qiita.com/ryo_grid/items/f700285549a8b195d918)
[【続】深層強化学習でのFX自動トレード（のシミュレーション）がうまくいかないのでオレオレ手法を考えた - Qiita](https://qiita.com/ryo_grid/items/bf89ea76aecc45ab39f4)
[【成功】深層強化学習でのFX自動トレード（のシミュレーション）がうまくいかないのでオレオレ手法を考えた - Qiita](https://qiita.com/ryo_grid/items/ae78835f9a61d020145f)
[【LSTM導入版】深層強化学習でのFX自動トレード（のシミュレーション）がうまくいかないのでオレオレ手法を考えた - Qiita](https://qiita.com/ryo_grid/items/e9af54f61701ffdf81b6)

このテーマに関しては5記事目となりました（今回はこれまでとタイトルを変えました）。

ひとまずこんなものかな、という成果は得られたので、この記事でまとめておきます。

# 実装について
- **参考にした論文（以降では、「論文」と呼称）**
  - ["Deep Reinforcement Learning for Trading,", Zihao Zhang & Stefan Zohren & Stephen Roberts, 2019.  Papers 1911.10107, arXiv.org.](https://www.researchgate.net/profile/Zihao_Zhang14/publication/337484696_Deep_Reinforcement_Learning_for_Trading/links/5dde6cc3a6fdcc2837eddaa7/Deep-Reinforcement-Learning-for-Trading.pdf)
- **これまでのオレオレ手法はあきらめた**
  - オレオレ手法で頑張るのは、結局、汎化させることができず、うまくういかなかったためあきらめました
  - そもそも、別にオリジナルの手法を開発したかったわけではなく、本記事で参考にした論文で提案されている手法の実装がしんどそうだったので、オレオレのもっとラクに実装できる方法を試していただけなので、あきらめること自体は別に構わない
  - とはいえ、論文で提案されている内容を（自身の読解に基づいて）そのまま実装するとうまくいかなかったため、実験的にうまくいった形にいじったりはしています（読解があやまっていただけという可能性はあると思います）
  - また、実装するにあたって必要な情報が、明に記述されていない箇所もあったので、そこらへんは自分でよろしく決めて実装しました
- **state**
  - 為替の値（close)
  - 過去の return
      - episodeに対応する時刻tから各期間さかのぼった時点の価格、時刻tでの価格の差額
         - 論文の提案手法はそもそもFXだけを対象としたものではない。そこで、一例として株式を対象とした場合を考えると、価格変化をreturnと解釈することになると考えた
      - 1年、1か月、2か月、3か月
         - 対象期間の日数の2乗根と、直近60日でのExponentially wighted moving starndard deviation（EMSD）で正規化
             - 本記事の実装では、1エピソードを半日足に対応させる形に代えているため、算出にあたって用いる日数相当の値は2倍した値を利用）
         - https://en.wikipedia.org/wiki/Moving_average#Exponentially_weighted_moving_variance_and_standard_deviation
         - https://ja.wikipedia.org/wiki/%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87#%E6%8C%87%E6%95%B0%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87
         - 詳細は論文と、本記事での実装をご参照下さい
  - Moving Average Convergence Divergence（MACD）
     - 一般的に知られているものと意味するところは同じようですが、算出方法は異なるようなので、論文での算出式と本記事での実装をご参照ください
  - Relative Strength Index（RSI）
     - 30日過去まで見た時のもの 
  - **以降は独自に追加したもの（LSTMで複数時系列データを扱わせることをやめたため、代わりに、過去の為替推移の要約データとして各種テクニカル指標を戻した）**
  - 前の足と現在の足での価格を比較した際の変化率
  - 各種テクニカル指標
     - Moving Avarage（MA）
     - MA乖離率
     - ボリンジャーバンドの値内側
     - ボリンジャーバンドの値外側
     - Percentage Price Oscilator（PPO）
     - Chande Momentum Oscillator（CMO）
     - ボラティリティ
          - 直近60日での Exponentially wighted moving starndard deviation（EMSD）
          - 特徴量 "過去のreturn" を算出する際に用いる値と同じ。また、rewardの算出で用いる値とも同じ
- **action**
  - sell=-1、donot=0、buy=1 という値で表現する
  - environmentは選択されたactionに対応する売買を行い、actionに対応する一つ上のポチで示した値をreward算出式に入れて、その値をagentに返す
    - donot: 何もしない
    - buy: 保有ポジションがshortであればcloseし、longポジションをopen。longポジションを持っている場合は何もしない
    - sell: 保有ポジションがlongであればcloseし、shortポジションをopen。shortポジションを持っている場合は何もしない


- **rewardの算出式**

```math
R_{t} = μ\bigr[ A_{t-1}\frac{σ_{tgt}}{σ_{t-1}}r_t - bp \hspace{2pt} p_{t-1} \bigr|\frac{σ_{tgt}}{σ_{t-1}}A_{t-1} - \frac{σ_{tgt}}{σ_{t-2}}A_{t-2}\bigl|\bigl]
```

- **rewardについて**
  - 算出式
      - 上記の通り
  - 各定数・変数
     - A{t}: 時刻tで選択したアクションに対応する値。値は sell=-1, donot=0, buy=1 となる。算出に用いる場合、t が現在処理しているエピソードの時刻を示すため、rewardの算出式では前エピソードと前々エピソードで選択したアクションは用いるが、現在のエピソードで選択したアクションの情報は用いないということになる
     - μ: FXの場合、購入する通貨数に相当。値は1で良いらしい
     - σ{t}: 時刻tにおけるボラティリティ。算出方法は論文および本記事での実装を参照
     - σ{tgt}: 正直なところ良く分かっていない。特定の期間のreturnの値の一定割合の値というのがリファーされた論文の内容も踏まえての読解であるが、パフォーマンスがダメダメになったので、σ{t}がとる値の範囲を確認した上で、5.0と適当な値に固定した
         - 一応、A{t-1} が donot で、A{t-2} が buy もしくは sell の場合に、reward を算出すると 取引コストだけが発生するが（ポジションをキープするだけであるため、FXでは不要であるというのはさておき、同じ売買が続いた場合は、保有商品をキープするだけという振る舞いは変わらなかったはずにも関わらず、発生せず不可解ではある）、その場合の額の多寡が変化するという点では、設定する値の大小は無意味ではない
         - その他の場合では、ポジション保有に対する評価値が生じる場合はそちらも同様にスケールされるため大小に意味はなく、どちらも生じない場合も当然意味は無い
     - p{t}: 時刻tでの価格（スプレッドは考慮しない値）
     - r{t}: p{t} - p{t-1}
     - bp: 取引手数料の割合。FXの場合はスプレッドという形になる（はず）。本記事での実装では1ドル100円の場合に1ドルあたり0.3銭のスプレッドがあった場合に、片道で0.15銭分の評価損益が生じるとおき、bp=0.0015 とした
  - reward算出式の大枠での解釈
     - 基本は学習データにおける取引結果から入力特徴量に対応する適切なactionを学習するというもの（為替予測などと本質的には同じようなものだと認識しています）
     - ただし、単純に取引結果をrewardとするのではなく、直近のボラティリティからポジション保有のリスクを求め、rewardの値をスケールさせる（論文で算出式の導出の根拠としてリファーさている関連研究の論文を参照するに、ポートフォリオ管理等に適用される手法であったりするようです）
     - ある時点tでのrewardの算出にあたって、とったaction{t}の種別が利用されていないのは不思議に見えるが、Q学習の更新式に基づくreward値の更新により、1つ先のエピソードにおけるrewardの一定割合が時刻tでとったアクションのrewardとして加算（正の値の場合もあれば負の値の場合もある）されるため、結果的には、選択したaction{t}に対応する取引によりポジションの保有種別・状態が変化（変化が無い場合もある）し、state{t}におけるaction{t}を選択した場合のrewardに一定程度反映されて学習されることになる
  - 具体的な式の解釈
     - 中括弧の中身が（FXの場合で言えば、）1エピソード前に売買した1通貨分のlongもしくはshortのポジションの保有状態の変化（変化が無い場合も含む）に対応するもので、μ が売買した通貨数に対応するため、全体としては1エピソード前に売買した通貨数全体を1ポジションとした場合の、保有状態の変化に対する評価値となる
     - 中括弧の中身の第一項は1エピソード前のアクションを評価するものである。A{t-1} は {-1, 0, 1}の値をとり、r{t}は前のエピソードの時点と現在時点の価格の差分であるため、価格が上がっていれば正の値、下がっていれば負の値をとる。ここで、A{t-1} * r{t} は、買いをして、価格が上がった場合および、売りをして、価格が下がった場合に正の値となる。取引が逆であった場合は負の値である。従って、正しい予測で売買をしたかどうかを評価するものとなる。アクションが donotであった場合は A{t-1} は0 となるため第一項の値は0となる
     - σ{tgt} / {t-1} は上記の評価をボラティリティに応じてスケールするための係数である。ボラティリティが小さい状況では評価値は大きくなり、大きい状況では評価値が小さくなる。これにより、エージェントが市場のボラティリティの高い状況での取引を避けるような振る舞いをするようになるのだと思われる。第二項にもボラティリティの値を用いた分数が現れるが、これは第一項のスケーリングとの整合性をとるためのものと思われる
     - 第二項は、-bp * p{t-1} が1通貨あたりの片道の取引コストを表しており、絶対値をとっている部分で前エピソードで取引コストが生じたかを表している。従って、それらを乗算することで前エピソードでかかった1通貨あたりの取引コストとなる
     - 第二項の絶対値をとっている部分については、ボラティリティによるスケーリングの部分を除いて考えると、前エピソードと前々エピソードでのアクションが売りと買い（買いと売り）であれば値は2となり、ポジションのクローズとオープンで2回分の決済のコストがかかったことを表現んできる。donotと売りもしくは買い（順序が逆の場合も）では値は1となり、決済コストは1回分となる。ただし、この場合、ただポジションをキープしただけの場合もあるはずで、その場合は取引コストは発生しないが、とにかく1回分の取引コストはとられたと見なすようである。donotとdonotの場合は値は0で取引コストは発生しない
- **論文との差分（そもそも誤読している箇所もあるかもしれません）**
  - rewardにおけるσ{tgt}
     - 私の読解が正しければ（そんなに自信は無いです）、論文では、固定値ではなく、エピソードによって変化する値のようですが、そのように実装したところ、ダメダメになってしまったので、固定値にしています
  - MACDを求める際の標準偏差を求める際に期間を半年にしている
  - 論文では、ボラティリティを用いている箇所全てにおいて、基本的には価格を対象とするのではなく、価格変化の差分（2つの足の間での差分）を対象に算出したボラティリティを利用するようであるが、本記事での実装では価格を対象としてボラティリティを求めて、それを用いている
      - 利用箇所としては、少なくとも特徴量として用いるMACD、return 値の算出処理、rewwardの算出処理がある
      - 価格変化の差分を対象とする実装も試したが、パフォーマンスが落ちただけであったのでやめた
  - replayを1イテレーション分全てまとめて行っている（TFでのfitの呼び出しは1イテレーションで1回のみ）
  - 学習期間・テスト期間
     - 本記事では2003年半ば～2016年半ばの13年間のデータを利用しているのに対し、論文では2005年～2019年の15年間のデータを用いている
     - 論文では学習期間を5年間（本記事の実装ではテストセットの先頭1年強は特徴量の算出には用いるが、学習時のイテレーションでなめる範囲には含まないため、それに合わせると4年間と解釈するのが正しいはず）、続く5年間をテスト期間として評価している
     - （ちょっと自信がないですが、）5年・5年で学習・テストとしていて、次の5年でのパフォーマンスを見る際は、学習期間を5年後ろにずらして、再度モデルを作成しなおすということをしたと思われる
  - 一日足ではなく半日足
     - 取引の機会を増やし、取引回数を増やすため
     - 取引回数を増やすのは、そちらの方が運の要素が減らせると考えるため
  - BatchNormalizationを入れている（論文には説明されているレイヤ以外に何かレイヤを加えたか否かについては言及がない）
  - イテレーションをまたいだreplayをしていない
     - 今回の実装では、memoryは1イテレーションでクリアしており、replayはランダムreplayを行っている。ただし、利用されるエピソードの記録は、同一イテレーションのものだけであり、重複が生じないように選択される
     - 論文ではmemoryのサイズを5000とし、replayは1000エピソード毎に行うと記述があるため、イテレーションをまたいでmemoryの内容は保持され、その内容を用いてreplayをしていると思われる（replayに用いるエピソードのデータをどう選択しているかは不明）
  - 入力する特徴量を scikit-learn の StandardScaler を用いて**全て**正規化してしまっている
     - 論文では入力する為替の値のみ正規化したと記述がある
     - なお、評価を行うにあたってはテストデータの特徴量も正規化せねばならないため、学習データの特徴量を正規化する際にフィットさせたStandardScalerのインスタンスを保持しておき、それを用いてテストデータの特徴量も正規化している（学習データからのリークといったことにはならない認識）
  - 論文では取引量を固定通貨数（のはず。固定額だったかもしれない・・・）としていたが、複利効果を得るため、資産額に応じてポジションの通貨数が変化するようにしてある
      - なお、論文では複利となるように取引をする場合はrewardの算出式を変更する必要があると記述されているが、特に変更しなくても、複利効果が得られるよう実装した方が高いパフォーマンスが得られたので、そのようにしている 

# 運用を想定した場合のモデルの作成手順
- 利用するモデルをどのように決定するか
  - **学習中に定期的に実行されるバックテストの結果を見て、過学習が起きる前のちょうど良いイテレーション数を見つける**
     - 1年程度のバックテストで安定した推移を見せていれば、その後1年間程度は相場の急激な変化がない限りは、似たパフォーマンスが得られる可能性が高い。明らかに相場の動きや、価格のレンジが学習期間および、テストデータの期間と異なっているようだと分かった場合は運用を停止するしかないと思われる
     - 良い感じの結果が得られたら、その時点でのモデルが得られるよう早期終了させる。なお、現在の実装では、総イテレーション数はグリーディー法のパラメータとして用いているため、それを減らすことで早期終了させることはできない（修正すればいいだけですが・・・）。そのため、適当にコードを挿入してプログラムを終了させる必要あり
  - **良い感じの結果がいくら回しても現れなければ、学習期間を変える（長くしたり、短くしたり）**
     - 学習期間を伸ばす場合はユニット数を増やす必要があり。短くする場合は減らす必要があることが、今回作成したプログラムにおいては、実験的に分かっている
     - NNはユニット数によって表現力が変化するが、少なすぎては表現力不足により（過学習が始まる以前に）学習データ・テストデータで共通するルールすら獲得することができず、多すぎては過剰な表現力により学習データへの過学習が早期から進みやすくなるということだと認識している
     - 学習期間とテスト期間に間が空くとパフォーマンスが落ちる傾向があるため、短くする場合は学習データの先頭を後ろにずらし、長くする場合は学習期間の先頭を過去の方向にずらすか（データが余分にあれば）、テスト期間の開始時点を未来方向にずらす形になる（テスト期間が短くなっても良ければ）

# 評価（USDJPY, EURJPY, GBPJPY）
- データセットの最初の1年強を特徴量生成の都合で除き（特徴量の生成には利用している）、その後6年間（取引不可な週末等は除外）を学習データとし、残りの期間（取引不可な週末等は除外）をテストデータとして、評価
- 評価のためのバックテストではオプションをクローズした場合のログのみ出力しているため、結果のグラフも横軸は何回目のクローズか、となっており、バックテスト期間とは一致していません（クローズが発生しなかった期間は、結果のグラフにおいては現れない）

## 実行環境
- Windows10 Home 64bit
- python 3.7.2
- pip 20.0.2
- pip module
  - tensorflow 2.1.0
  - scikit-learn 0.22.1
  - scipy 1.4.2
  - numpy 1.18.1
  - TA-Lib 0.4.17
- (GeForce GTX 1660 Super は搭載しているが、今回のモデル規模だと利用すると遅くなるのに加えて、シードを固定しても再現性が無くなるらしいので、プログラム起動時に無効化している)

## 各データセットの為替推移（CLOSEの値）
※**本記事での実装においては、ロングポジションの価格はCLOSEの値 + 0.0015円、ショートポジションの価格は CLOSEの値 - 0.0015円です。決済する場合は、両者を逆にした価格です。取引差益への課税は考慮していません。また通貨ペアごとのスプレッドは手抜きで全て前述の値となっています**
### USDJPY (2003-05-04_2016-07-09)
![USDJPY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12325/18f86b21-6c44-5acf-7597-0ef98c5a71b5.png)

### EURJPY (2003-08-03_2016-07-09)
![EURJPY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12325/90a97c62-491d-2130-7aec-23038e71d87a.png)

### GBPJPY (2003-08-03_2016-07-09)
![GBPJPY.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12325/4ca36880-6144-0178-b2f5-bec5a1c01be5.png)


## ハイパーパラメータ
- 学習率: 0.0001
- ミニバッチのサイズ: 64
- 学習データのサイズ: 約3024足 （半日足で、およそ6年間。取引不可な時間帯のデータは含まない）
- 保持可能なポジション数（通貨の塊）: 1
- NN構造: 80ユニットのDenseと40ユニットのDenseが主
    - dueling network も実装している

## NN構造
![de4bb4b7b40297e4ee0d5ebf5823ec4c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12325/003e7ff0-010c-95a9-2c3f-ea09584661cb.png)



## 各通貨ペアにおけるテストデータでのバックテスト結果
- 初期資産を100万円としバックテストを開始
- 学習データの期間は、いずれも、データセット全体の先頭一年強を除いた後の約6年間（除かれた先頭1年強も特徴量の生成には用いられている）
- テスト期間の長さはそれぞれ微妙に異なるが、いずれもデータセットの最後の5年弱（学習データの期間と連続している）

※**以下はあくまでいい感じの推移をしたイテレーション数で早期終了させたものの場合で、それ以外のイテレーション数だと少ない場合でも、多い場合でも全然ダメというものはたくさんありました。**
### USDJPY
120イテレーション時の推移
![USDJPY2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12325/b16890f4-e0a6-1bcc-88b4-b0d2210b43bf.png)

### EURJPY
50イテレーション時の推移
![EURJPY2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12325/01f3282d-0740-3d70-9dc9-025d19ec1147.png)

### GBPJPY
50イテレーション時の推移
![GBPJPY2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12325/701a46d6-8059-d24c-50e4-4dd331cac155.png)




# ソースコード
 - リポジトリ（のブランチ）: [github](https://github.com/ryogrid/fx_systrade/tree/thesis-based-dqn-trade-ryogird-special-qiita-2006)
 - ソースファイル: [agent](https://github.com/ryogrid/fx_systrade/blob/f61f95506ad7406cc532615031c0b284f37f186f/thesis_based_dqn_trade_agent.py), [environment](https://github.com/ryogrid/fx_systrade/blob/ffe5877e1a31564c33a2cd2d6de8aaaff94dca60/thesis_based_dqn_trade_environment.py)
 - **※これはUSDJPYでの評価に用いたコードですが、データセットのファイルをenvironmentでロードしている箇所に、他の通貨ペアのファイルをロードする場合のコードがコメントアウトされているので、そこをよろしくやってもらえれば他の通貨ペアの場合も試せます**

# 実行方法
- Windowsなら以下のように実行すればOKです（標準出力とエラー出力をhoge.txtに出力しています）
  - pip install -r requirements.py
  - pip install  TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl （Windows環境でなければ不要）
  - python thesis_based_dqn_trade_agent.py train > hoge.txt 2>&1
  - 10イテレーションおきにその時点のモデルでバックテストを行い、ポジションをクローズした時のログが auto_backtest_log_{開始日時}.csv に出力されます。先に学習データの期間でのバックテストを行い、その後でテスト期間でのバックテストを行うので、どちらの結果かは開始日時で判別してください。
  - 出力されたcsvファイルは2列目が何足目（0オリジン）か、9列目がクローズした時点での資産額になります。他の列の値が何を表しているかはコードをご参照ください

#余談的なもの
- 何故LSTMを用いていないか
  - LSTMを使った場合では汎化させることができなかった
  - 知識不足なだけかもしれないですが、ユニット数やハイパーパラメータ（後ろに並べたもののパラメータも含む）の調整、L1・L2正則化、weight decay（重み減衰）、勾配クリッピング、BatchNormalization、Dropout等の設定・利用など試行錯誤してみたもののうまくいかず
- 状態遷移が一本道になるがうまくいくのはどういう理屈か良く分からない
- LSTMを用いている場合、学習が進んだ場合に、学習データでもパフォーマンスが落ちていくことがあった（一時的に、といった感じではなかった）
    - 強化学習の枠組みの側が要因か、構成したNNの特性が要因かは良く分からなかった
    - 同じ特徴量、NN構成で為替のUp or Downの予測を行う、単純な教師あり学習も試してみたが、同様の現象が起きた
    - なお、LSTMを用いていない場合では検証していない
- **学習期間のデータセットについて**
  - 運用する期間と学習する期間はなるべく近くし、学習期間もあまり長くし過ぎない方が良さそう
  - 長くすると市場の動きやレンジが変わっても対応できるようになるだろうが、そういった場合を除くと実験的には勝率は下がったように見えた
     - 参考: [FXシステムトレードのプログラムをいくつか作ってみて分かった課題とその解決法について - Qiita](https://qiita.com/ryo_grid/items/f13ab8a2bb708713396b)
    - 3年程度に抑えられれば望ましい感じがするが、上記の裏返しになる。また、価格の位置するレンジが学習データとかけ離れていると取引自体が行われないようである
    - 学習期間を短くとったものと、長くとったもので2つモデルを用意しておき、相場の動向を見て、どちらを運用するか切り替えるのがよいのかもしれない。もしくは、２つのモデルがそれぞれ出力した3つのreward値をよろしくアンサンブル（単純には平均値をとる等）して、その結果からアクションを決定するか
- このプログラムによるモデルには損切りという概念が存在しないため、実運用を考えるとモデルの外で良いので組み込んだ方が良いのかもしれないが、単純にやるとダメダメなパフォーマンスになったりするので難しいところ
- バックテスト結果の推移の評価について
  - シャープレシオを見るだけでは不十分
  - [Twitterのこの件に関する連ツイ](https://twitter.com/ryo_grid/status/1266896657624358912)

# 最後に
- 参考にさせていただいた論文の著者である Zihao Zhang氏、Stefan Zohren氏、Stephen Roberts氏 に敬意と感謝の意を表します
- 論文の読解に誤りがあればご指摘いただければ幸いです
- バグが潜んでいたりする可能性も無きにしにあらずなので、見つけた場合はリポジトリに issueとしてあげておいていただけると助かります
- アドバイス等あれば些細なことでもコメントいただければ幸いです


　
以上です！