31
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[Python]FXシストレパラメータの最適化を遺伝的アルゴリズムでやってみる

Posted at

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

の続きです。ランダムサーチの代わりに遺伝的アルゴリズム(GA)を実装してみます。

準備

1時間足データの作成については、前回と同じです。

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') #1時間足データの作成

GitHubにアップしてあるindicators.pyとbacktest.pyが必要です。backtest.pyについては、BacktestReportで若干修正があります。

最適化するトレードシステム

今回は最適化するパラメータの値の組み合わせを増やすために、2本の移動平均の交差システムに決済用のシグナルを追加します。決済用のシグナルは、以下のように定義します。

  • 買いポジションの決済:終値が決済用移動平均を下抜けたとき
  • 売りポジションの決済:終値が決済用移動平均を上抜けたとき

このシステムでは、パラメータは3個となります。今回はそれぞれのパラメータを以下のような範囲で探索することにします。

SlowMAperiod = np.arange(7, 151) #長期移動平均期間の範囲
FastMAperiod = np.arange(5, 131) #短期移動平均期間の範囲
ExitMAperiod = np.arange(3, 111) #決済用移動平均期間の範囲

だいたい200万通りの組み合わせとなります。総当たりできないこともありませんが、数時間はかかると思われます。

メインルーチン

遺伝的アルゴリズムのメインルーチンは、前回のランダムサーチとほぼ同じです。パラメータをランダムに探すところを、後述の遺伝的処理に置き換えるだけです。また売買シグナルは上記のルールのように決済用のシグナルを追加しています。

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

    SlowMA = np.empty([len(Prange[0]), len(ohlc)]) #長期移動平均
    for i in range(len(Prange[0])):
        SlowMA[i] = ind.iMA(ohlc, Prange[0][i])

    FastMA = np.empty([len(Prange[1]), len(ohlc)]) #短期移動平均
    for i in range(len(Prange[1])):
        FastMA[i] = ind.iMA(ohlc, Prange[1][i])
    
    ExitMA = np.empty([len(Prange[2]), len(ohlc)]) #決済用移動平均
    for i in range(len(Prange[2])):
        ExitMA[i] = ind.iMA(ohlc, Prange[2][i])
    
    Close = ohlc['Close'].values #終値
    
    M = 20 #個体数
    Eval = np.zeros([M, 6])  #評価項目
    Param = InitParam(Prange, M) #パラメータ初期化
    gens = 0 #世代数
    while gens < 100:
        for k in range(M):
            i0 = Param[k,0]
            i1 = Param[k,1]
            i2 = Param[k,2]
            #買いエントリーシグナル
            BuyEntry = (FastMA[i1] > SlowMA[i0]) & (shift(FastMA[i1]) <= shift(SlowMA[i0]))
            #売りエントリーシグナル
            SellEntry = (FastMA[i1] < SlowMA[i0]) & (shift(FastMA[i1]) >= shift(SlowMA[i0]))
            #買いエグジットシグナル
            BuyExit = (Close < ExitMA[i2]) & (shift(Close) >= shift(ExitMA[i2]))
            #売りエグジットシグナル
            SellExit = (Close > ExitMA[i2]) & (shift(Close) <= shift(ExitMA[i2]))
            #バックテスト
            Trade, PL = Backtest(ohlc, BuyEntry, SellEntry, BuyExit, SellExit) 
            Eval[k] = BacktestReport(Trade, PL)
        # 世代の交代
        Param = Evolution(Param, Eval[:,0], Prange)
        gens += 1
        print(gens, Eval[0,0])
    Slow = Prange[0][Param[:,0]]
    Fast = Prange[1][Param[:,1]]
    Exit = Prange[2][Param[:,2]]
    return pd.DataFrame({'Slow':Slow, 'Fast':Fast, 'Exit':Exit, 'Profit': Eval[:,0], 'Trades':Eval[:,1],
                         'Average':Eval[:,2],'PF':Eval[:,3], 'MDD':Eval[:,4], 'RF':Eval[:,5]},
                         columns=['Slow','Fast','Exit','Profit','Trades','Average','PF','MDD','RF'])

もう一つ、前回からの変更点は、SlowMAperiod, FastMAperiod, ExitMAperiodの三つのパラメータの範囲をPrangeというリストにまとめて各関数に渡すようにした点です。こうすることで、パラメータの数が増えてもそのまま対応することができます。

遺伝的処理

上の関数で、GA用に追加した関数は、InitParam()Evolution()です。まず、InitParam()は、各個体のパラメータの初期化です。

from numpy.random import randint,choice

#パラメータ初期化
def InitParam(Prange, M):
    Param = randint(len(Prange[0]), size=M)
    for i in range(1,len(Prange)):
        Param = np.vstack((Param, randint(len(Prange[i]), size=M)))
    return Param.T

Evolution()は、次のようにいくつかの遺伝的処理を含みます。

#遺伝的処理
def Evolution(Param, Eval, Prange):
    #エリート保存付きルーレット選択
    #1点交叉
    #近傍生成
    #突然変異
    return Param

それぞれの処理について説明します。

エリート保存付きルーレット選択

まず、現在の個体の中から次世代に残す個体を選択します。選択の方法はいくつかあるのですが、Numpyの関数でルーレット選択に便利な関数があったので、それを使ってみます。ルーレット選択はバックテストの評価値である適応度の大きさに応じて、確率的に次世代に残す個体を選択する方法です。適応度が高いほど残りやすくなります。

今回使った関数は、numpy.random.choice()という関数で、リストから必要な個数分ランダムに選択するものですが、オプションの引数にpという選択確率のリストを付けると、その確率に合わせて選択してくれるのです。これはルーレット選択そのものです。次のようなコードになります。

    #エリート保存付きルーレット選択
    Param = Param[np.argsort(Eval)[::-1]] #ソート
    R = Eval-min(Eval)
    R = R/sum(R)
    idx = choice(len(Eval), size=len(Eval), replace=True, p=R)
    idx[0] = 0 #エリート保存
    Param = Param[idx]

ただ、確率がマイナスだと都合が悪いので、適応度の最小値が0になるように補正してあります。またルーレット選択だけだと、適応度が高くても運悪く選ばれないこともあるので、適応度をソートして最も高い個体(エリート)は必ず次世代に残るようにしています。

1点交叉

次に遺伝子の交叉を行います。これは、二つの個体を選んで、遺伝子情報の一部を互いに交換することです。交叉の方法もいくつかありますが、ここではパラメータの並びの1か所を選んで、その前後を交換する方法にしました。

    #1点交叉
    N = 10
    idx = choice(np.arange(1,len(Param)), size=N, replace=False)
    for i in range(0,N,2):
        ix = idx[i:i+2]
        p = randint(1,len(Prange))
        Param[ix] = np.hstack((Param[ix][:,:p], Param[ix][:,p:][::-1]))

ここでもchoice()を使って交叉する個体の個数分の乱数列を生成します。replace=Falseを付けることで、重複のない乱数列が得られます。そして、2個ずつの個体をixとして選択して、交叉点pの後半のデータを入れ換えることで交叉を実現しています。

近傍生成

通常のGAでは、選択、交叉、突然変異で進化のシミュレーションを行うのですが、今回のように交叉点をパラメータの切れ目のところに限定していると、いつの間にか同じ個体だらけになってしまい、進化が止まってしまいます。かといって、突然変異を多くすると、ランダムサーチに近くなってしまうので、あまり効率がよくありません。そこで、今回は、パラメータの一部を+1、あるいは-1する変化を施します。いわゆる近傍解というやつで、局所探索アルゴリズムでよく利用されるものです。

    #近傍生成
    N = 10
    idx = choice(np.arange(1,len(Param)), size=N, replace=False)
    diff = choice([-1,1], size=N).reshape(N,1)
    for i in range(N):
        p = randint(len(Prange))
        Param[idx[i]][p:p+1] = (Param[idx[i]][p]+diff[i]+len(Prange[p]))%len(Prange[p])

交叉と同じく近傍を生成する個体を選択します。そして、どのパラメータを変化させるかをまた乱数で決めて、そのパラメータを1だけ変化させます。

突然変異

最後に突然変異を実行します。これもいくつかの方法がありますが、選択した個体のパラメータの一部を新しく乱数で書き換えます。GAの場合、局所解から抜け出すには、突然変異が重要なのですが、これを多用するとランダム性が高くなってしまうので、ここでは、2個程度にしておきます。

    #突然変異
    N = 2
    idx = choice(np.arange(1,len(Param)), size=N, replace=False)
    for i in range(N):
        p = randint(len(Prange))
        Param[idx[i]][p:p+1] = randint(len(Prange[p]))

実行結果

以上のように定義した関数を使って遺伝的アルゴリズムを実行してみます。

result = Optimize(ohlc, [SlowMAperiod, FastMAperiod, ExitMAperiod])
result.sort_values('Profit', ascending=False)

GAも乱数を使っているので、結果は毎回異なります。以下は結果の一例で、世代毎に最も高い適応度を示したものです。エリート保存しているので、高い適応度は順次更新されます。

1 -94.9
2 958.2
3 958.2
4 958.2
5 1030.3
6 1030.3
7 1030.3
8 1454.0
9 1550.9
10 1550.9
11 1850.8
12 1850.8
13 1850.8
14 1850.8
15 1850.8
16 1850.8
17 2022.5
18 2076.5
19 2076.5
20 2076.5
:
61 2076.5
62 2076.5
63 2076.5
64 2076.5
65 2076.5
66 2316.2
67 2316.2
68 2316.2
69 2316.2
70 2316.2
:
95 2316.2
96 2316.2
97 2316.2
98 2316.2
99 2316.2
100 2316.2

最後の世代に残った個体は以下のようになります。

Slow Fast Exit Profit Trades Average PF MDD RF
0 126 17 107 2316.2 75.0 30.882667 2.306889 387.1 5.983467
18 126 15 107 2316.2 75.0 30.882667 2.306889 387.1 5.983467
8 105 18 106 2210.2 76.0 29.081579 2.247080 387.1 5.709636
17 126 18 108 2130.9 75.0 28.412000 2.158098 424.9 5.015062
10 126 18 107 2078.4 79.0 26.308861 1.980794 448.3 4.636181
9 127 18 107 2074.5 73.0 28.417808 2.184819 371.3 5.587126
6 126 15 7 2030.3 76.0 26.714474 2.007143 415.7 4.884051
16 126 14 107 2024.9 76.0 26.643421 2.100489 424.9 4.765592
5 126 17 107 1954.7 74.0 26.414865 1.917441 448.3 4.360250
13 126 17 105 1878.7 79.0 23.781013 1.888694 414.2 4.535732
2 127 18 107 1872.4 75.0 24.965333 1.878813 448.3 4.176667
12 126 17 101 1869.6 76.0 24.600000 2.063300 420.4 4.447193
11 92 15 107 1859.5 73.0 25.472603 2.006223 358.8 5.182553
14 125 14 108 1843.1 84.0 21.941667 1.811938 473.6 3.891681
4 124 14 107 1839.8 75.0 24.530667 1.975245 420.4 4.376308
3 42 19 107 1796.8 75.0 23.957333 1.912405 410.7 4.374970
1 125 15 106 1614.7 81.0 19.934568 1.711729 386.9 4.173430
19 104 18 107 1583.7 94.0 16.847872 1.654746 393.4 4.025674
7 125 17 106 1421.7 81.0 17.551852 1.629015 574.4 2.475104
15 92 16 107 539.8 103.0 5.240777 1.150513 605.1 0.892084

この問題の最適解は調べていないのでわかりませんが、この結果は、まあまあ高い値だと思います。そもそもGAの目的は、最適解を求めることではなく、総当たりするより短い時間で準最適解を求めることです。

実際、シストレのパラメータで最適解を求めたところで、そのシステムが別の期間でも同じ結果を出すわけではありません。そういうことなので、200万通りの組み合わせの中から2000回の試行でまあまあの解が得られたのであれば、よしとするべきでしょう。

31
42
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?