今回からもう少しコンパクトなタイムフレームにしてみました。
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通りしかないですが、組み合わせの数は後で変えてみます。
SlowMAperiod = np.arange(10, 61) #長期移動平均期間の範囲
FastMAperiod = np.arange(5, 31) #短期移動平均期間の範囲
単純なランダムサーチだと、パラメータの値を順次ランダムに発生させて、バックテストの評価の最も高いものを採用すればいいのですが、ここでは、遺伝的アルゴリズムにも応用できるよう、1回の世代で20個ほどのパラメータの組み合わせの試行を行い、それを何世代か繰り返す方法を取ってみました。
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を用意した点です。そして、各世代で評価値(ここでは総損益)が最も大きいパラメータのセットを保存(エリート保存)しておき、残りのパラメータをランダムに生成することを繰り返します。
エリート保存付きでパラメータをランダムに生成する関数は次のように書けます。
#エリート保存付きランダムサーチ
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番目のパラメータを再度セットするという方法です。 これを実行すると、以下のようになります。
import warnings;warnings.filterwarnings('ignore')
result = Optimize(ohlc, SlowMAperiod, FastMAperiod)
result.sort_values('Profit', ascending=False)
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個しかないので、それほど組み合わせの数は増えないのですが、
SlowMAperiod = np.arange(10, 161) #長期移動平均期間の範囲
FastMAperiod = np.arange(5, 131) #短期移動平均期間の範囲
のように19,026通りに増やしてみます。結果は
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'])
#エリート保存付きランダムサーチ
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
import warnings;warnings.filterwarnings('ignore')
result = Optimize(ohlc, SlowMAperiod, FastMAperiod)
result.sort_values('Profit', ascending=False)
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 |
となり、最適解は求まりませんでした。ただ、これを何度か繰り返せば、そのうち、最適解が求まります。
まあ、総当たりでもなんとかできるくらいの組み合わせなら、ランダムサーチでもそれなりの解が求まるということでしょうね。