アルゴリズムトレードの強化学習アルゴリズムについて調べてみた

  • 90
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

Nekopuniさんのブログに触発されて趣味と実益を兼ねて強化学習を用いたアルゴリズムトレードについて調べてみました。
書いている内容には全く自信がありません(特にコード)。自分用のメモが主な用途ですが、しょぼいサーベイ資料として参考になればありがたいです。

参考資料

調査概要

強化学習を使用する意味(参考文献4参照)

強化学習と教師あり学習の違いはザックリ書くと以下のとおり。

  • 強化学習は政策の最適化により環境から獲得する期待報酬の最大化が目的
  • 教師あり学習は教師データに対する予測誤差を小さくするのが目的

教師あり学習では統一的なトレーディングポリシー無しに、条件を最適化してしまう。
一方、強化学習では環境や政策といった部分を含めてアルゴリズム構築が可能となるため強化学習を使用する事による有効性が高いと考えられる。

Value Function RLとDirect RLの違い

■Value Function RL
状態または状態行動対により行動価値が割り当てられる。この価値をもとに政策(ある状態が得られた時にとる行動の方針)の最適化を行う。これにより長期的な期待報酬の最大化を行う。
Q-learningはこちらに分類される。Q-learningの詳細は参考文献5を見ていただきたく。

■Direct RL
環境から得られた経験(観測値)をもとに直接的に報酬関数(価値観数)の調整を行っていく。Q-learningとは異なりQ-tableが必要ないため、時間的/空間的計算量は小さくなる。ただし、期待報酬の最大化は短期的なものとなる。
Recurrent Reinforcement Learning(RRL)はこちらに分類される。RRLの詳細につきましても参考文献5を参照してただきたく。
(今回はこちらのRRLを使用したコードを作成しています。)

RRL Financial Trading Framework

  • Agent: RRL-trader
  • State: テクニカル指標または過去のリターンで定義される市場の状態
  • Reward: tからt+1の間のリスクと手数料を加味した獲得報酬
  • Action: trading signal(buy/sell/short/long/neutral/hold)

エージェントの評価値

Differential Sharpe Ratio(DSR)を使用。weightの更新時に使用。

実運用する場合の注意点

  • 損切り
  • リスクに対する嗜好性
  • 異常な挙動が確認された時のシステムダウン処理

アルゴリズム(参考文献1参照)

【時刻t時点でのポジション決定(long or short)】

$F_t=sign(\sum_{i=0}^{M}w_{i,t}r_{t-i}+w_{M+1,t}F_{t-1}+v_t)$
$F_t ∈ [-1,1]; (short=-1, long=1)$
$w_t$: weight vector
$v_t$: threshold of the neural network
$r_t$: $p_t - p_{t-1}$(時系列におけるリターン)

数式の形は単純な1層だけのニューラルネットワークと同じ。
実際はニューラルネットワークの手法を適用し、閾値$v_t$もweight vectorに含ませて最適化する。

【時点Tでの利益(profit)】

$P_t=\sum_{t=0}^{T}R_t$
$R_t:=F_{t-1}r_t-δ|F_t-F_{t-1}|$
δ: transaction cost(手数料)
※各取引において1単位のみの取引がされる事が前提となっています。

【最適化評価値】

■sharpe ratio
$\hat{S}(t):=\frac{A_t}{B_t}$
$A_t=A_{t-1}+η(R_t-A_{t-1}), A_0=0$
$B_t=B_{t-1}+η(R_t^2-B_{t-1}), B_0=0$
η: adaptation parameter

■Differential Sharpe Ratio(DSR)
sharpe ratioの移動平均版をオンライン学習用に改善したもの。
DSRの方が計算が軽く、収束も早い(らしい)。
上の$\hat{S}$をη=0周りでテイラー展開し第一項までを取得。
$D_t:=\frac{d\hat{S}}{dη}|_{η=0}$

  $=\frac{B_{t-1}\Delta A_t-\frac{1}{2}A_{t-1}\Delta B_t}{(B_{t-1}-A_{t-1}^2)^\frac{3}{2}}$
$\Delta A_t=R_t-A_{t-1}$
$\Delta B_t=R_t^2-B_{t-1}$

$D_t$を即時的なパフォーマンス尺度とみなし、これを最大化するようにweightの更新を行う。

【weightの更新】

$w_{i,t}=w_{i,t-1}+\rho \Delta w_{i,t}$
$\Delta w_{i,t}=\frac{dD_t}{dw_i}$
    $\approx \frac{dD_t}{dR_t}${ $\frac{dR_t}{dF_t}\frac{dF_t}{dw_{i,t}} + \frac{dR_t}{dF_{t-1}}\frac{dF_{t-1}}{dw_{i,t-1}}$}

$\frac{dF_t}{dw_{i,t}}\approx\frac{\partial F_t}{\partial w_{i,t}}+\frac{\partial F_t}{\partial F_{t-1}}\frac{dF_{t-1}}{dw_{i,t-1}}$

$\frac{dD_t}{dR_t}=\frac{B_{t-1}-A_{t-1}R_t}{(B_{t-1}-A_{t-1}^2)^{3/2}}$
$\frac{dR_t}{dF_t}=-\delta$
$\frac{dR_t}{dF_{t-1}}=r_t-\delta$

【損切り、signal設定の閾値、システム異常の判定】

パラメタとして設定しましょう、とあるが具体的な記載はない。経験的な値で設定するしかなさそう。。

参考コード

sign関数は一旦tanhとして計算し、$F_t$が0より大きいかそうでないかでsignal化しています。
損切りなどの機能は書いていません。
自分でいうのもなんですが相当怪しいコードです。参考としてご覧ください。

# coding: utf-8

import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from math import tanh, copysign

class RRLAgentForFX:
    TRADING_COST = 0.003
    EPS = 1e-6

    def __init__(self,M,rho=0.01,eta=0.1,bias=1.0):
        np.random.seed(555)
        self.M = M # number of lags
        self.weights = np.zeros(self.M+3,dtype=np.float64)
        self.bias = bias # bias term
        self.rho = rho
        self.eta = eta
        self.price_diff = np.zeros(self.M+1) # r_t

        self.pre_price = None
        self.pre_signal = 0

        self.pre_A = 0.0
        self.pre_B = 0.0
        self.pre_gradient_F = 0.0

        # result store
        self.signal_store = []
        self.profit_store = []
        self.dsr_store = []
        self.sr_store = []
        self.cumulative_profit = 0.0

    def train_online(self, price):
        self.calculate_price_diff(price)
        signal, self.F_t_value = self.select_signal()
        print "signal",signal
        self.calculate_return(signal)
        self.update_parameters()
        self.pre_price = price
        self.pre_signal = signal

        # store result
        self.signal_store.append(signal)

    def calculate_price_diff(self,price):
        r = price - self.pre_price if self.pre_price is not None else 0
        self.price_diff[:self.M] = self.price_diff[1:]
        self.price_diff[self.M] = r

    def calculate_return(self,signal):
        R_t = self.pre_signal*self.price_diff[-1]
        R_t -= self.TRADING_COST*abs(signal - self.pre_signal)
        self.return_t = R_t

        self.cumulative_profit += R_t
        self.profit_store.append(self.cumulative_profit)

    def select_signal(self):
        values_sum = (self.weights[:self.M+1]*self.price_diff).sum()
        values_sum += self.weights[-2]*self.pre_signal
        values_sum += self.bias*self.weights[-1]

        F_t_value = tanh(values_sum)
        return copysign(1, F_t_value ), F_t_value

    def update_parameters(self):
        # update weight
        self.weights += self.rho*self.calculate_gradient_weights()
        print "weight",self.weights

        # update moment R_t
        self.update_R_moment()

    def calculate_gradient_weights(self):
        """ differentiate between D_t and w_t """
        denominator = self.pre_B-self.pre_A**2
        if denominator!=0:
            diff_D_R = self.pre_B-self.pre_A*self.return_t
            diff_D_R /= (denominator)**1.5
        else:
            diff_D_R = 0

        gradient_F = self.calculate_gradient_F()
        print "gradient_F",gradient_F

        #diff_R_F = -self.TRADING_COST
        #diff_R_F_{t-1} = self.price_diff[-1] - self.TRADING_COST
        delta_weights = -self.TRADING_COST*gradient_F
        delta_weights += ( self.price_diff[-1] - self.TRADING_COST) \
                                                    *self.pre_gradient_F
        delta_weights *= diff_D_R
        self.pre_gradient_F = gradient_F
        return delta_weights

    def calculate_gradient_F(self):
        """ differentiate between F_t and w_t """
        diff_tnah = 1-self.F_t_value**2

        diff_F_w = diff_tnah*( np.r_[ self.price_diff, self.pre_signal, self.bias ] )
        diff_F_F = diff_tnah*self.weights[-2]

        return diff_F_w + diff_F_F*self.pre_gradient_F

    def update_R_moment(self):
        delta_A = self.return_t - self.pre_A
        delta_B = self.return_t**2 - self.pre_B
        A_t = self.pre_A + self.eta*delta_A # A_t. first moment of R_t.
        B_t = self.pre_B + self.eta*delta_B # B_t. second moment of R_t.
        self.sr_store.append(A_t/B_t)
        self.calculate_dsr(delta_A, delta_B)

        self.pre_A = A_t
        self.pre_B = B_t

    def calculate_dsr(self,delta_A,delta_B):
        dsr = self.pre_B*delta_A - 0.5*self.pre_A*delta_B
        dsr /= (self.pre_B-self.pre_A**2)**1.5
        self.dsr_store.append(dsr)

if __name__=='__main__':
    M = 8
    fx_agent = RRLAgentForFX(M,rho=0.01,eta=0.01,bias=0.25)

    ifname = os.getcwd()+'/input/quote.csv'
    data = pd.read_csv(ifname)
    train_data = data.ix[:3000,'USD']

    for price in train_data.values:
        fx_agent.train_online(price)

実験

みずほ銀行ヒストリカルデータのページから外国為替相場(/円)の日時データのcsvファイルをダウンロードし使用しています。使用したのはUSD/円の2002年4月1日から3,000個のデータを使用し学習しました。

実験結果

USD/円レート

USD.png

累積利益(1日1単位のみ購入時)

profit.png

DSR

DSR_.png

SR

SR.png

コメント

ρとηの値によって全然結果が変わってきます。不安定過ぎます。。間違いに気付き次第コードの更新をしていきたいと思います。
おかしな点にお気づきの方がおられましたらコメント頂けますと大変助かります。