[Python]FXシストレパラメータの最適化をランダムサーチでやってみる

[PythonでFXシストレパラメータの最適化](http://qiita.com/toyolab/items/3dfa94a4543dd1eb5239)
で、トレードシステムのパラメータの組み合わせをすべて試して最適解を求めてみました。

組み合わせの数が少なければ、総当たりでも問題ないのですが、組み合わせの数が多くなると、なかなか大変になってきます。そのための最適化アルゴリズムが、[メタヒューリスティクス](https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%BF%E3%83%92%E3%83%A5%E3%83%BC%E3%83%AA%E3%82%B9%E3%83%86%E3%82%A3%E3%82%AF%E3%82%B9)として色々と知られています。そのなかでも有名な遺伝的アルゴリズムを実装しようかと思ったのですが、その前にもっと簡単なランダムサーチをPythonで書いてみました。

# 準備

１分足データから別のタイムフレームに変換する関数とテクニカル指標関数はindicators.pyに、前回のバックテストとその評価を行う関数は、backtest.pyに入れておきます。ソースは[GitHub](https://github.com/toyolab/MT5IndicatorsPy)にあります。以下のコードで`ohlc`に1時間足のデータができます。

````py
import numpy as np
import pandas as pd
import indicators as ind #indicators.pyのインポート
from backtest import Backtest,BacktestReport

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') #１時間足データの作成
````
# 最適化するトレードシステムのパラメータ
前回の最適化では、以下のように二つのパラメータをある範囲で変化させて最適化しました。

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

このままだと1066通りしかないですが、組み合わせの数は後で変えてみます。

# ランダムサーチ

単純なランダムサーチだと、パラメータの値を順次ランダムに発生させて、バックテストの評価の最も高いものを採用すればいいのですが、ここでは、遺伝的アルゴリズムにも応用できるよう、１回の世代で20個ほどのパラメータの組み合わせの試行を行い、それを何世代か繰り返す方法を取ってみました。

````py
def Optimize(ohlc, SlowMAperiod, FastMAperiod):
    def shift(x, n=1): return np.concatenate((np.zeros(n), x[:-n])) #シフト関数

    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])
    
    M = 20 #個体数
    Eval = np.zeros([M, 6])  #評価項目
    Param = np.zeros([M, 2], dtype=int) #パラメータ
    RandomSearch(Param, Eval[:,0], SlowMAperiod, FastMAperiod) #パラメータ初期化
    gens = 0 #世代数
    while gens < 100:
        for k in range(M):
            i = Param[k,0]
            j = Param[k,1]
            #買いエントリーシグナル
            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)
        #世代の交代
        Param = RandomSearch(Param, Eval[:,0], SlowMAperiod, FastMAperiod)
        gens += 1
    Slow = SlowMAperiod[Param[:,0]]
    Fast = FastMAperiod[Param[:,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'])
````
前回と異なるのは、各世代でパラメータの組み合わせを保存しておく配列`Param`を用意した点です。そして、各世代で評価値（ここでは総損益）が最も大きいパラメータのセットを保存（エリート保存）しておき、残りのパラメータをランダムに生成することを繰り返します。

エリート保存付きでパラメータをランダムに生成する関数は次のように書けます。

````py
#エリート保存付きランダムサーチ
def RandomSearch(Param, Eval, SlowMAperiod, FastMAperiod):
    Param = Param[np.argsort(Eval)[::-1]] #ソート
    NewParam = np.vstack((np.random.randint(len(SlowMAperiod), size=len(Eval)),
                          np.random.randint(len(FastMAperiod), size=len(Eval)))).T
    NewParam[0] = Param[0] #エリート保存
    return NewParam
````
評価値をソートして0番目のパラメータを再度セットするという方法です。
これを実行すると、以下のようになります。

````py
result = Optimize(ohlc, SlowMAperiod, FastMAperiod)
result.sort_values('Profit', ascending=False)
````
<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>Slow</th>
      <th>Fast</th>
      <th>Profit</th>
      <th>Trades</th>
      <th>Average</th>
      <th>PF</th>
      <th>MDD</th>
      <th>RF</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>27</td>
      <td>8</td>
      <td>2507.1</td>
      <td>264.0</td>
      <td>9.496591</td>
      <td>1.423497</td>
      <td>485.1</td>
      <td>5.168213</td>
    </tr>
    <tr>
      <th>15</th>
      <td>18</td>
      <td>5</td>
      <td>944.1</td>
      <td>428.0</td>
      <td>2.205841</td>
      <td>1.110187</td>
      <td>693.1</td>
      <td>1.362141</td>
    </tr>
    <tr>
      <th>19</th>
      <td>48</td>
      <td>14</td>
      <td>825.4</td>
      <td>238.0</td>
      <td>3.468067</td>
      <td>1.131883</td>
      <td>927.7</td>
      <td>0.889727</td>
    </tr>
    <tr>
      <th>16</th>
      <td>42</td>
      <td>29</td>
      <td>720.4</td>
      <td>308.0</td>
      <td>2.338961</td>
      <td>1.094974</td>
      <td>1011.8</td>
      <td>0.711998</td>
    </tr>
    <tr>
      <th>8</th>
      <td>44</td>
      <td>25</td>
      <td>589.3</td>
      <td>246.0</td>
      <td>2.395528</td>
      <td>1.089205</td>
      <td>1141.1</td>
      <td>0.516432</td>
    </tr>
    <tr>
      <th>6</th>
      <td>60</td>
      <td>25</td>
      <td>588.8</td>
      <td>120.0</td>
      <td>4.906667</td>
      <td>1.126946</td>
      <td>1025.0</td>
      <td>0.574439</td>
    </tr>
    <tr>
      <th>4</th>
      <td>22</td>
      <td>5</td>
      <td>493.5</td>
      <td>124.0</td>
      <td>3.979839</td>
      <td>1.105354</td>
      <td>1106.3</td>
      <td>0.446082</td>
    </tr>
    <tr>
      <th>11</th>
      <td>26</td>
      <td>7</td>
      <td>391.2</td>
      <td>206.0</td>
      <td>1.899029</td>
      <td>1.069326</td>
      <td>1099.8</td>
      <td>0.355701</td>
    </tr>
    <tr>
      <th>17</th>
      <td>13</td>
      <td>7</td>
      <td>343.2</td>
      <td>152.0</td>
      <td>2.257895</td>
      <td>1.069697</td>
      <td>990.9</td>
      <td>0.346352</td>
    </tr>
    <tr>
      <th>18</th>
      <td>45</td>
      <td>20</td>
      <td>174.5</td>
      <td>160.0</td>
      <td>1.090625</td>
      <td>1.032210</td>
      <td>1118.4</td>
      <td>0.156026</td>
    </tr>
    <tr>
      <th>12</th>
      <td>38</td>
      <td>20</td>
      <td>169.8</td>
      <td>170.0</td>
      <td>0.998824</td>
      <td>1.029754</td>
      <td>939.8</td>
      <td>0.180677</td>
    </tr>
    <tr>
      <th>3</th>
      <td>21</td>
      <td>11</td>
      <td>148.6</td>
      <td>460.0</td>
      <td>0.323043</td>
      <td>1.016830</td>
      <td>1203.1</td>
      <td>0.123514</td>
    </tr>
    <tr>
      <th>5</th>
      <td>34</td>
      <td>13</td>
      <td>-342.0</td>
      <td>170.0</td>
      <td>-2.011765</td>
      <td>0.939258</td>
      <td>1160.8</td>
      <td>-0.294624</td>
    </tr>
    <tr>
      <th>7</th>
      <td>42</td>
      <td>22</td>
      <td>-374.4</td>
      <td>156.0</td>
      <td>-2.400000</td>
      <td>0.933744</td>
      <td>1189.6</td>
      <td>-0.314728</td>
    </tr>
    <tr>
      <th>1</th>
      <td>51</td>
      <td>20</td>
      <td>-441.4</td>
      <td>160.0</td>
      <td>-2.758750</td>
      <td>0.922788</td>
      <td>1089.9</td>
      <td>-0.404991</td>
    </tr>
    <tr>
      <th>9</th>
      <td>58</td>
      <td>27</td>
      <td>-1485.5</td>
      <td>222.0</td>
      <td>-6.691441</td>
      <td>0.787506</td>
      <td>2394.0</td>
      <td>-0.620510</td>
    </tr>
    <tr>
      <th>10</th>
      <td>19</td>
      <td>25</td>
      <td>-1620.0</td>
      <td>234.0</td>
      <td>-6.923077</td>
      <td>0.775828</td>
      <td>1845.7</td>
      <td>-0.877716</td>
    </tr>
    <tr>
      <th>13</th>
      <td>33</td>
      <td>16</td>
      <td>-1625.0</td>
      <td>400.0</td>
      <td>-4.062500</td>
      <td>0.827957</td>
      <td>2307.7</td>
      <td>-0.704164</td>
    </tr>
    <tr>
      <th>2</th>
      <td>44</td>
      <td>24</td>
      <td>-1885.7</td>
      <td>508.0</td>
      <td>-3.712008</td>
      <td>0.828287</td>
      <td>2652.2</td>
      <td>-0.710995</td>
    </tr>
    <tr>
      <th>14</th>
      <td>55</td>
      <td>12</td>
      <td>-2048.3</td>
      <td>398.0</td>
      <td>-5.146482</td>
      <td>0.776908</td>
      <td>2813.6</td>
      <td>-0.728000</td>
    </tr>
  </tbody>
</table>
</div>

このように最適解は求まっていますが、20個体を100世代分、つまり2000回近く試行しているので、当然といえば当然です。

今回はパラメータが２個しかないので、それほど組み合わせの数は増えないのですが、

````py
SlowMAperiod = np.arange(10, 161) #長期移動平均期間の範囲
FastMAperiod = np.arange(5, 131)  #短期移動平均期間の範囲
````
のように19,026通りに増やしてみます。結果は

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>Slow</th>
      <th>Fast</th>
      <th>Profit</th>
      <th>Trades</th>
      <th>Average</th>
      <th>PF</th>
      <th>MDD</th>
      <th>RF</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>52</td>
      <td>48</td>
      <td>2003.4</td>
      <td>190.0</td>
      <td>10.544211</td>
      <td>1.426773</td>
      <td>1106.4</td>
      <td>1.810738</td>
    </tr>
    <tr>
      <th>5</th>
      <td>125</td>
      <td>26</td>
      <td>1312.6</td>
      <td>58.0</td>
      <td>22.631034</td>
      <td>1.513939</td>
      <td>896.4</td>
      <td>1.464302</td>
    </tr>
    <tr>
      <th>16</th>
      <td>49</td>
      <td>47</td>
      <td>1203.9</td>
      <td>62.0</td>
      <td>19.417742</td>
      <td>1.448046</td>
      <td>773.0</td>
      <td>1.557439</td>
    </tr>
    <tr>
      <th>11</th>
      <td>56</td>
      <td>55</td>
      <td>989.6</td>
      <td>116.0</td>
      <td>8.531034</td>
      <td>1.245266</td>
      <td>922.0</td>
      <td>1.073319</td>
    </tr>
    <tr>
      <th>14</th>
      <td>52</td>
      <td>34</td>
      <td>704.0</td>
      <td>62.0</td>
      <td>11.354839</td>
      <td>1.261632</td>
      <td>527.2</td>
      <td>1.335357</td>
    </tr>
    <tr>
      <th>13</th>
      <td>152</td>
      <td>36</td>
      <td>545.5</td>
      <td>75.0</td>
      <td>7.273333</td>
      <td>1.133198</td>
      <td>936.5</td>
      <td>0.582488</td>
    </tr>
    <tr>
      <th>2</th>
      <td>140</td>
      <td>58</td>
      <td>457.4</td>
      <td>196.0</td>
      <td>2.333673</td>
      <td>1.085960</td>
      <td>867.1</td>
      <td>0.527505</td>
    </tr>
    <tr>
      <th>18</th>
      <td>40</td>
      <td>109</td>
      <td>308.1</td>
      <td>88.0</td>
      <td>3.501136</td>
      <td>1.073977</td>
      <td>1435.0</td>
      <td>0.214704</td>
    </tr>
    <tr>
      <th>6</th>
      <td>75</td>
      <td>107</td>
      <td>255.1</td>
      <td>78.0</td>
      <td>3.270513</td>
      <td>1.066815</td>
      <td>1145.1</td>
      <td>0.222775</td>
    </tr>
    <tr>
      <th>3</th>
      <td>158</td>
      <td>85</td>
      <td>172.7</td>
      <td>122.0</td>
      <td>1.415574</td>
      <td>1.036899</td>
      <td>1530.6</td>
      <td>0.112832</td>
    </tr>
    <tr>
      <th>12</th>
      <td>68</td>
      <td>32</td>
      <td>-622.3</td>
      <td>90.0</td>
      <td>-6.914444</td>
      <td>0.836838</td>
      <td>1629.9</td>
      <td>-0.381803</td>
    </tr>
    <tr>
      <th>10</th>
      <td>120</td>
      <td>111</td>
      <td>-638.0</td>
      <td>37.0</td>
      <td>-17.243243</td>
      <td>0.787057</td>
      <td>1233.4</td>
      <td>-0.517269</td>
    </tr>
    <tr>
      <th>8</th>
      <td>153</td>
      <td>30</td>
      <td>-667.3</td>
      <td>35.0</td>
      <td>-19.065714</td>
      <td>0.776284</td>
      <td>1558.8</td>
      <td>-0.428086</td>
    </tr>
    <tr>
      <th>7</th>
      <td>43</td>
      <td>74</td>
      <td>-749.0</td>
      <td>39.0</td>
      <td>-19.205128</td>
      <td>0.770245</td>
      <td>1766.8</td>
      <td>-0.423930</td>
    </tr>
    <tr>
      <th>15</th>
      <td>63</td>
      <td>104</td>
      <td>-774.3</td>
      <td>92.0</td>
      <td>-8.416304</td>
      <td>0.835339</td>
      <td>1692.6</td>
      <td>-0.457462</td>
    </tr>
    <tr>
      <th>9</th>
      <td>154</td>
      <td>56</td>
      <td>-1296.9</td>
      <td>58.0</td>
      <td>-22.360345</td>
      <td>0.675945</td>
      <td>1733.3</td>
      <td>-0.748226</td>
    </tr>
    <tr>
      <th>1</th>
      <td>152</td>
      <td>24</td>
      <td>-1315.2</td>
      <td>74.0</td>
      <td>-17.772973</td>
      <td>0.664627</td>
      <td>2130.3</td>
      <td>-0.617378</td>
    </tr>
    <tr>
      <th>19</th>
      <td>155</td>
      <td>27</td>
      <td>-1363.8</td>
      <td>60.0</td>
      <td>-22.730000</td>
      <td>0.672006</td>
      <td>1474.4</td>
      <td>-0.924986</td>
    </tr>
    <tr>
      <th>17</th>
      <td>109</td>
      <td>40</td>
      <td>-1478.5</td>
      <td>62.0</td>
      <td>-23.846774</td>
      <td>0.651379</td>
      <td>1784.5</td>
      <td>-0.828523</td>
    </tr>
    <tr>
      <th>4</th>
      <td>148</td>
      <td>122</td>
      <td>-1957.4</td>
      <td>100.0</td>
      <td>-19.574000</td>
      <td>0.626400</td>
      <td>2171.4</td>
      <td>-0.901446</td>
    </tr>
  </tbody>
</table>
</div>
となり、最適解は求まりませんでした。ただ、これを何度か繰り返せば、そのうち、最適解が求まります。まあ、総当たりでもなんとかできるくらいの組み合わせなら、ランダムサーチでもそれなりの解が求まるということでしょう。