PythonでFXシストレパラメータの最適化

[PythonでFXシストレのバックテスト](http://qiita.com/toyolab/items/e8292d2f051a88517cb2)
で、バックテストのコードを書いたので、こんどはシストレのパラメータの最適化をやってみます。トレードシステムの最適化といっても、今流行りのディープラーニングをやるわけではなく、単にテクニカル指標のパラメータの値を色々と変えて、最も評価値の高くなるものを見つけるだけです。Pythonのプログラミングの練習のためです。

# 準備
[PythonでFXシストレのバックテスト](http://qiita.com/toyolab/items/e8292d2f051a88517cb2)
と同じく、FXのヒストリカルデータを準備します。前と同じくEUR/USDの2015年の1時間足のデータを作っておきます。

````py
import numpy as np
import pandas as pd
import indicators as ind #indicators.pyのインポート

dataM1 = pd.read_csv('DAT_ASCII_EURUSD_M1_2015.csv', sep=';',
                     names=('Time','Open','High','Low','Close', ''),
                     index_col='Time', parse_dates=True)
dataM1.index += pd.offsets.Hour(7) #7時間のオフセット
ohlc = ind.TF_ohlc(dataM1, 'H') #１時間足データの作成
````

indicators.pyは[GitHub](https://github.com/toyolab/MT5IndicatorsPy)に上げてあるものを使います。

# バックテストとその評価
バックテストの関数は前回と同じものを使います。ヒストリカルデータと売買シグナルを入れて売買結果と損益を算出します。

````py
def Backtest(ohlc, BuyEntry, SellEntry, BuyExit, SellExit, lots=0.1, spread=2):
    Open = ohlc['Open'].values #始値
    Point = 0.0001 #1pipの値
    if(Open[0] > 50): Point = 0.01 #クロス円の1pipの値
    Spread = spread*Point #スプレッド
    Lots = lots*100000 #実際の売買量
    N = len(ohlc) #FXデータのサイズ
    BuyExit[N-2] = SellExit[N-2] = True #最後に強制エグジット
    BuyPrice = SellPrice = 0.0 # 売買価格
    
    LongTrade = np.zeros(N) # 買いトレード情報
    ShortTrade = np.zeros(N) # 売りトレード情報
    
    LongPL = np.zeros(N) # 買いポジションの損益
    ShortPL = np.zeros(N) # 売りポジションの損益

    for i in range(1,N):
        if BuyEntry[i-1] and BuyPrice == 0: #買いエントリーシグナル
            BuyPrice = Open[i]+Spread
            LongTrade[i] = BuyPrice #買いポジションオープン
        elif BuyExit[i-1] and BuyPrice != 0: #買いエグジットシグナル
            ClosePrice = Open[i]
            LongTrade[i] = -ClosePrice #買いポジションクローズ
            LongPL[i] = (ClosePrice-BuyPrice)*Lots #損益確定
            BuyPrice = 0

        if SellEntry[i-1] and SellPrice == 0: #売りエントリーシグナル
            SellPrice = Open[i]
            ShortTrade[i] = SellPrice #売りポジションオープン
        elif SellExit[i-1] and SellPrice != 0: #売りエグジットシグナル
            ClosePrice = Open[i]+Spread
            ShortTrade[i] = -ClosePrice #売りポジションクローズ
            ShortPL[i] = (SellPrice-ClosePrice)*Lots #損益確定
            SellPrice = 0

    return pd.DataFrame({'Long':LongTrade, 'Short':ShortTrade}, index=ohlc.index),\
            pd.DataFrame({'Long':LongPL, 'Short':ShortPL}, index=ohlc.index)
````

システムの評価としては、次の関数で、 総損益、取引数、平均損益、プロフィットファクター、最大ドローダウン、リカバリーファクターを算出します。だいたいMetaTraderの最適化で出力されるのと同じものです。

````py
def BacktestReport(Trade, PL):
    LongPL = PL['Long']
    ShortPL = PL['Short']
    LongTrades = np.count_nonzero(Trade['Long'])//2
    ShortTrades = np.count_nonzero(Trade['Short'])//2
    GrossProfit = LongPL.clip_lower(0).sum()+ShortPL.clip_lower(0).sum()
    GrossLoss = LongPL.clip_upper(0).sum()+ShortPL.clip_upper(0).sum()
    #総損益
    Profit = GrossProfit+GrossLoss
    #取引数
    Trades = LongTrades+ShortTrades
    #平均損益
    if Trades==0: Average = 0
    else: Average = Profit/Trades
    #プロフィットファクター
    if GrossLoss==0: PF=100
    else: PF = -GrossProfit/GrossLoss
    #最大ドローダウン
    Equity = (LongPL+ShortPL).cumsum()
    MDD = (Equity.cummax()-Equity).max()
    #リカバリーファクター
    if MDD==0: RF=100
    else: RF = Profit/MDD
    return np.array([Profit, Trades, Average, PF, MDD, RF])
````

# 最適化するパラメータとその範囲

前回のバックテストでは、長期移動平均の期間を30、短期移動平均の期間を10、と固定してテストしましたが、今回の最適化では、この二つの期間を変えてみます。

変化させる期間は、長期移動平均で10から50、短期移動平均で5から30としてみます。次のように配列に入れておきます。

````py
SlowMAperiod = np.arange(10, 51) #長期移動平均期間の範囲
FastMAperiod = np.arange(5, 31)  #短期移動平均期間の範囲
````

それぞれの期間は41通り、26通りですが、二つの期間の組み合わせだと、$41\times 26=1066$通りとなります。

# 最適化

このパラメータの期間の範囲を代入して最適化を行います。期間の組み合わせの数が増えてくると計算時間が無視できなくなるので、極力、無駄な計算を省かなくてはいけません。

とりあえず、41通り、26通りの移動平均の時系列をあらかじめ計算しておきます。そして、1066通りの組み合わせに対して、売買シグナルの生成、バックテスト、評価を行い、パラメータの値、評価値を出力します。コードの一例は以下のようになります。

````py
def Optimize(ohlc, SlowMAperiod, FastMAperiod):
    SlowMA = np.empty([len(SlowMAperiod), len(ohlc)]) #長期移動平均
    for i in range(len(SlowMAperiod)):
        SlowMA[i] = ind.iMA(ohlc, SlowMAperiod[i])

    FastMA = np.empty([len(FastMAperiod), len(ohlc)]) #短期移動平均
    for i in range(len(FastMAperiod)):
        FastMA[i] = ind.iMA(ohlc, FastMAperiod[i])
    
    N = len(SlowMAperiod)*len(FastMAperiod)
    Eval = np.empty([N, 6]) #評価項目
    Slow = np.empty(N) #長期移動平均期間
    Fast = np.empty(N) #短期移動平均期間
    def shift(x, n=1): return np.concatenate((np.zeros(n), x[:-n])) #シフト関数
    k = 0
    for i in range(len(SlowMAperiod)):
        for j in range(len(FastMAperiod)):
            #買いエントリーシグナル
            BuyEntry = (FastMA[j] > SlowMA[i]) & (shift(FastMA[j]) <= shift(SlowMA[i]))
            #売りエントリーシグナル
            SellEntry = (FastMA[j] < SlowMA[i]) & (shift(FastMA[j]) >= shift(SlowMA[i]))
            #買いエグジットシグナル
            BuyExit = SellEntry.copy()
            #売りエグジットシグナル
            SellExit = BuyEntry.copy()
            #バックテスト
            Trade, PL = Backtest(ohlc, BuyEntry, SellEntry, BuyExit, SellExit) 
            Eval[k] = BacktestReport(Trade, PL)
            Slow[k] = SlowMAperiod[i]
            Fast[k] = FastMAperiod[j]
            k += 1
    return pd.DataFrame({'Slow':Slow, 'Fast':Fast, 'Profit': Eval[:,0], 'Trades':Eval[:,1],
                         'Average':Eval[:,2],'PF':Eval[:,3], 'MDD':Eval[:,4], 'RF':Eval[:,5]},
                         columns=['Slow','Fast','Profit','Trades','Average','PF','MDD','RF'])
            
result = Optimize(ohlc, SlowMAperiod, FastMAperiod)
````

計算時間が気になっていましたが、Core i5-3337U 1.8GHzのCPUで、12秒ほどでした。同じ条件の最適化をMetaTrader5でやってみましたが、50秒近くかかったので、Pythonとしては、まあまあ実用可能な速度だったと思います。

# 最適化の結果

最適化の結果は、好みの項目でソートすることで、最適なパラメータの値がわかります。例えば、総損益でソートすると以下のようになります。

````py
result.sort_values('Profit', ascending=False).head(20)
````

            Slow  Fast  Profit  Trades   Average        PF     MDD        RF
       445  27.0   8.0  2507.1   264.0  9.496591  1.423497   485.1  5.168213
       470  28.0   7.0  2486.0   260.0  9.561538  1.419642   481.2  5.166251
       446  27.0   9.0  2263.3   252.0  8.981349  1.376432   624.7  3.623019
       444  27.0   7.0  2171.4   272.0  7.983088  1.341276   504.7  4.302358
       471  28.0   8.0  2102.3   250.0  8.409200  1.359030   540.3  3.890986
       497  29.0   8.0  2093.3   242.0  8.650000  1.365208   603.8  3.466876
       495  29.0   6.0  2063.5   256.0  8.060547  1.342172   620.6  3.325008
       498  29.0   9.0  2053.5   238.0  8.628151  1.362451   686.5  2.991260
       546  31.0   5.0  1959.4   254.0  7.714173  1.344256   529.7  3.699075
       520  30.0   5.0  1940.3   276.0  7.030072  1.313538   681.7  2.846267
       496  29.0   7.0  1931.5   248.0  7.788306  1.322891   611.3  3.159660
       422  26.0  11.0  1903.4   248.0  7.675000  1.309702   708.7  2.685763
       523  30.0   8.0  1903.0   232.0  8.202586  1.327680   823.9  2.309746
       524  30.0   9.0  1875.8   234.0  8.016239  1.328598   908.6  2.064495
       573  32.0   6.0  1820.8   242.0  7.523967  1.320688   639.8  2.845889
       420  26.0   9.0  1819.1   258.0  7.050775  1.282035   667.0  2.727286
       572  32.0   5.0  1808.2   256.0  7.063281  1.313564   522.9  3.458023
       598  33.0   5.0  1799.6   248.0  7.256452  1.317183   613.2  2.934768
       419  26.0   8.0  1777.4   274.0  6.486861  1.273817   552.7  3.215849
       434  26.0  23.0  1739.6   368.0  4.727174  1.241049  1235.5  1.408013

これより、総損益が最大となるパラメータの値は、長期移動平均の期間が27、短期移動平均の期間が8ということになります。

試しに、この期間でバックテストした資産曲線は以下のようになります。
![chart.png](https://qiita-image-store.s3.amazonaws.com/0/131169/88fc9f6a-c148-1a7d-7287-b226d781d854.png)

いいですね。ただ、パラメータの最適化をすれば、この程度の結果が出るのは当たり前で、あまり喜べるものではありません。別の期間のバックテストをしてがっかりするだけです。

今回はMetaTraderのバックテストより速い結果が出ただけでよしとします。MetaTraderではティック単位のバックテストもできるのですが、それをPythonでやろうとすると、やっぱりかなり時間がかかるんじゃないかと思います。まだまだ先は長いです。