準備

今回からもう少しコンパクトなタイムフレームにしてみました。

In [15]:
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_GBPUSD_M1_201705.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') #1時間足データの作成

最適化するトレードシステムのパラメータ

前回の最適化では、以下のように二つのパラメータをある範囲で変化させて最適化しました。 このままだと1066通りしかないですが、組み合わせの数は後で変えてみます。

In [8]:
SlowMAperiod = np.arange(10, 61) #長期移動平均期間の範囲
FastMAperiod = np.arange(5, 31)  #短期移動平均期間の範囲

ランダムサーチ

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

In [9]:
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を用意した点です。そして、各世代で評価値(ここでは総損益)が最も大きいパラメータのセットを保存(エリート保存)しておき、残りのパラメータをランダムに生成することを繰り返します。

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

In [10]:
#エリート保存付きランダムサーチ
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番目のパラメータを再度セットするという方法です。 これを実行すると、以下のようになります。

In [11]:
import warnings;warnings.filterwarnings('ignore')
result = Optimize(ohlc, SlowMAperiod, FastMAperiod)
result.sort_values('Profit', ascending=False)
Out[11]:
Slow Fast Profit Trades Average PF MDD RF
0 10 22 456.7 29.0 15.748276 2.850486 119.3 3.828164
9 10 12 456.7 29.0 15.748276 2.850486 119.3 3.828164
1 11 28 132.5 8.0 16.562500 2.241799 106.7 1.241799
15 16 11 120.4 27.0 4.459259 1.340594 163.1 0.738197
6 38 24 94.6 64.0 1.478125 1.157195 242.1 0.390748
19 14 19 54.0 14.0 3.857143 1.247820 81.8 0.660147
17 56 12 -105.4 14.0 -7.528571 0.662396 152.4 -0.691601
13 10 20 -137.5 18.0 -7.638889 0.566520 156.9 -0.876354
16 11 25 -149.7 18.0 -8.316667 0.624247 227.8 -0.657155
3 23 21 -173.2 31.0 -5.587097 0.639692 219.8 -0.787989
8 54 7 -190.4 16.0 -11.900000 0.519556 190.4 -1.000000
5 38 21 -199.0 21.0 -9.476190 0.552608 199.0 -1.000000
14 59 15 -202.0 47.0 -4.297872 0.710394 407.1 -0.496193
18 26 30 -212.3 18.0 -11.794444 0.434921 241.9 -0.877635
7 15 10 -221.7 19.0 -11.668421 0.537354 240.8 -0.920681
11 41 11 -231.3 14.0 -16.521429 0.375034 231.3 -1.000000
12 51 28 -234.8 25.0 -9.392000 0.510833 234.8 -1.000000
2 50 16 -253.0 14.0 -18.071429 0.345915 253.0 -1.000000
10 25 22 -280.7 38.0 -7.386842 0.537257 303.3 -0.925486
4 19 16 -488.1 25.0 -19.524000 0.303212 542.0 -0.900554

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

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

In [16]:
SlowMAperiod = np.arange(10, 161) #長期移動平均期間の範囲
FastMAperiod = np.arange(5, 131)  #短期移動平均期間の範囲

のように19,026通りに増やしてみます。結果は

In [17]:
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'])
In [18]:
#エリート保存付きランダムサーチ
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
In [19]:
import warnings;warnings.filterwarnings('ignore')
result = Optimize(ohlc, SlowMAperiod, FastMAperiod)
result.sort_values('Profit', ascending=False)
Out[19]:
Slow Fast Profit Trades Average PF MDD RF
0 10 22 456.7 29.0 15.748276 2.850486 119.3 3.828164
19 74 7 322.9 18.0 17.938889 3.339855 69.3 4.659452
12 22 33 271.5 9.0 30.166667 4.940493 37.3 7.278820
1 126 81 185.5 9.0 20.611111 7.648746 19.3 9.611399
7 150 12 172.6 18.0 9.588889 1.782766 194.3 0.888317
18 10 61 132.0 4.0 33.000000 121.000000 1.1 120.000000
6 10 84 54.4 8.0 6.800000 1.301887 180.2 0.301887
15 148 81 34.8 14.0 2.485714 1.155705 137.1 0.253829
9 85 27 -2.4 3.0 -0.800000 0.974249 93.2 -0.025751
17 124 53 -18.6 3.0 -6.200000 0.764259 78.9 -0.235741
10 26 64 -23.2 3.0 -7.733333 0.770977 101.3 -0.229023
11 62 24 -37.9 3.0 -12.633333 0.671007 115.2 -0.328993
3 44 103 -124.5 32.0 -3.890625 0.701510 213.6 -0.582865
2 98 48 -125.4 3.0 -41.800000 0.040551 130.7 -0.959449
5 23 40 -153.0 7.0 -21.857143 0.189189 178.3 -0.858104
4 121 63 -222.6 5.0 -44.520000 0.073272 240.2 -0.926728
8 129 124 -239.2 8.0 -29.900000 0.190798 241.4 -0.990886
16 155 28 -251.8 21.0 -11.990476 0.499702 268.8 -0.936756
14 48 76 -338.1 33.0 -10.245455 0.498591 458.8 -0.736922
13 29 53 -402.6 17.0 -23.682353 0.232119 402.6 -1.000000

となり、最適解は求まりませんでした。ただ、これを何度か繰り返せば、そのうち、最適解が求まります。

まあ、総当たりでもなんとかできるくらいの組み合わせなら、ランダムサーチでもそれなりの解が求まるということでしょうね。

In [ ]: