[Python] Essayez d'optimiser les paramètres de la systole FX avec une recherche aléatoire
C'est une continuation de. Implémentons un algorithme génétique (GA) au lieu d'une recherche aléatoire.
La création des données horaires est la même que la dernière fois.
import numpy as np
import pandas as pd
import indicators as ind #indicators.Importation de 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) #Décalage de 7 heures
ohlc = ind.TF_ohlc(dataM1, 'H') #Création de données horaires
Vous aurez besoin des indicateurs.py et backtest.py téléchargés sur GitHub. Pour backtest.py, il y a une légère correction dans BacktestReport
.
Cette fois, afin d'augmenter la combinaison de valeurs de paramètres à optimiser, nous ajouterons un signal de tassement au système d'intersection de deux moyennes mobiles. Le signal de paiement est défini comme suit.
Dans ce système, il y a 3 paramètres. Cette fois, nous rechercherons chaque paramètre dans la plage suivante.
SlowMAperiod = np.arange(7, 151) #Gamme de période moyenne mobile à long terme
FastMAperiod = np.arange(5, 131) #Gamme de période moyenne mobile à court terme
ExitMAperiod = np.arange(3, 111) #Gamme de période moyenne mobile pour le règlement
Il existe environ 2 millions de combinaisons. Il est possible de tout frapper, mais cela prendra plusieurs heures.
La routine principale de l'algorithme génétique est presque la même que la recherche aléatoire précédente. Remplacez simplement la recherche aléatoire de paramètres par le traitement génétique décrit ci-dessous. De plus, le signal d'achat et de vente ajoute un signal de règlement comme dans la règle ci-dessus.
def Optimize(ohlc, Prange):
def shift(x, n=1): return np.concatenate((np.zeros(n), x[:-n])) #Fonction Shift
SlowMA = np.empty([len(Prange[0]), len(ohlc)]) #Moyenne mobile à long terme
for i in range(len(Prange[0])):
SlowMA[i] = ind.iMA(ohlc, Prange[0][i])
FastMA = np.empty([len(Prange[1]), len(ohlc)]) #Moyenne mobile à court terme
for i in range(len(Prange[1])):
FastMA[i] = ind.iMA(ohlc, Prange[1][i])
ExitMA = np.empty([len(Prange[2]), len(ohlc)]) #Moyenne mobile pour le paiement
for i in range(len(Prange[2])):
ExitMA[i] = ind.iMA(ohlc, Prange[2][i])
Close = ohlc['Close'].values #le dernier prix
M = 20 #Nombre d'individus
Eval = np.zeros([M, 6]) #Élément d'évaluation
Param = InitParam(Prange, M) #Initialisation des paramètres
gens = 0 #Nombre de générations
while gens < 100:
for k in range(M):
i0 = Param[k,0]
i1 = Param[k,1]
i2 = Param[k,2]
#Acheter le signal d'entrée
BuyEntry = (FastMA[i1] > SlowMA[i0]) & (shift(FastMA[i1]) <= shift(SlowMA[i0]))
#Vendre un signal d'entrée
SellEntry = (FastMA[i1] < SlowMA[i0]) & (shift(FastMA[i1]) >= shift(SlowMA[i0]))
#Acheter un signal de sortie
BuyExit = (Close < ExitMA[i2]) & (shift(Close) >= shift(ExitMA[i2]))
#Vendre un signal de sortie
SellExit = (Close > ExitMA[i2]) & (shift(Close) <= shift(ExitMA[i2]))
#Backtest
Trade, PL = Backtest(ohlc, BuyEntry, SellEntry, BuyExit, SellExit)
Eval[k] = BacktestReport(Trade, PL)
#Changement de génération
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'])
Un autre changement par rapport à la dernière fois est que la plage des trois paramètres «SlowMAperiod», «FastMAperiod» et «ExitMAperiod »est regroupée dans une liste appelée« Prange »et transmise à chaque fonction. En faisant cela, même si le nombre de paramètres augmente, il peut être manipulé tel quel.
Dans la fonction ci-dessus, les fonctions ajoutées pour GA sont ʻInitParam () et ʻEvolution ()
. Premièrement, ʻInitParam () `est l'initialisation des paramètres de chaque individu.
from numpy.random import randint,choice
#Initialisation des paramètres
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 () `contient un traitement génétique comme suit:
#Traitement génétique
def Evolution(Param, Eval, Prange):
#Sélectionnez la roulette avec stockage élite
#1 point de passage
#Génération de quartier
#mutation
return Param
Chaque processus est expliqué ci-dessous.
Tout d'abord, sélectionnez l'individu à laisser pour la prochaine génération parmi les individus actuels. Il existe plusieurs façons de le sélectionner, mais il existe une fonction Numpy qui est utile pour la sélection de la roulette, alors utilisons-la. La sélection à la roulette est une méthode de sélection probabiliste des individus à laisser pour la prochaine génération en fonction du degré d'adaptabilité, qui est la valeur d'évaluation du test arrière. Plus l'adaptabilité est élevée, plus il est facile de rester.
La fonction utilisée cette fois est une fonction appelée numpy.random.choice ()
, qui sélectionne aléatoirement le nombre requis dans la liste, mais si vous ajoutez une liste de probabilités de sélection appelée p
à l'argument optionnel, cela Il choisira en fonction de la probabilité. C'est la sélection de roulette elle-même. Le code ressemble à ceci:
#Sélectionnez la roulette avec stockage élite
Param = Param[np.argsort(Eval)[::-1]] #Trier
R = Eval-min(Eval)
R = R/sum(R)
idx = choice(len(Eval), size=len(Eval), replace=True, p=R)
idx[0] = 0 #Sauvegarde élite
Param = Param[idx]
Cependant, il n'est pas pratique que la probabilité soit négative, elle a donc été corrigée pour que la valeur minimale d'adaptabilité soit de 0. De plus, si vous ne sélectionnez que la roulette, vous ne serez peut-être pas malencontreusement sélectionné même si l'adaptabilité est élevée, nous trions donc l'adaptabilité afin que l'individu le plus élevé (élite) reste toujours dans la génération suivante.
Ensuite, les gènes sont croisés. Il s'agit de sélectionner deux individus et d'échanger une partie de leurs informations génétiques entre eux. Il existe plusieurs méthodes de croisement, mais ici j'ai choisi l'un des paramètres et j'ai échangé l'avant et l'arrière.
#1 point de passage
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]))
Encore une fois, utilisez «choice ()» pour générer une séquence de nombres aléatoires pour le nombre d'individus se croisant. En ajoutant replace = False
, vous pouvez obtenir une séquence unique de nombres aléatoires. Ensuite, en sélectionnant deux individus comme «x» et en échangeant les données dans la dernière moitié de l'intersection «p», un croisement est réalisé.
En GA normale, l'évolution est simulée par sélection, croisement et mutation, mais si le point de croisement est limité à la rupture du paramètre comme cette fois, il sera plein des mêmes individus avant que vous ne le sachiez. L'évolution s'arrêtera. Cependant, s'il y a beaucoup de mutations, ce sera proche d'une recherche aléatoire, donc ce n'est pas très efficace. Par conséquent, cette fois, nous changerons certains des paramètres en +1 ou -1. Il s'agit d'une solution dite de voisinage, souvent utilisée dans les algorithmes de recherche locaux.
#Génération de quartier
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])
Sélectionnez un individu qui produit un quartier ainsi qu'une croix. Ensuite, décidez quel paramètre changer avec un nombre aléatoire et changez ce paramètre de 1.
Enfin, effectuez la mutation. Il existe plusieurs façons de procéder, mais certains des paramètres de l'individu sélectionné sont réécrits avec de nouveaux nombres aléatoires. Dans le cas de GA, les mutations sont importantes pour sortir de la solution locale, mais si vous les utilisez beaucoup, le caractère aléatoire augmentera, donc ici nous en définirons environ deux.
#mutation
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]))
Exécutons un algorithme génétique en utilisant la fonction définie ci-dessus.
result = Optimize(ohlc, [SlowMAperiod, FastMAperiod, ExitMAperiod])
result.sort_values('Profit', ascending=False)
GA utilise également des nombres aléatoires, les résultats seront donc différents à chaque fois. Voici un exemple des résultats, montrant la plus grande adaptabilité pour chaque génération. Puisqu'il est enregistré en tant qu'élite, la haute adaptabilité sera mise à jour séquentiellement.
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
Les individus qui sont restés dans la dernière génération sont les suivants.
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 |
Je n'ai pas étudié la solution optimale à ce problème, donc je ne sais pas, mais je pense que ce résultat est raisonnablement élevé. En premier lieu, le but de GA n'est pas de trouver la solution optimale, mais de trouver la solution quasi optimale dans un temps plus court que le round-robin.
En fait, trouver la solution optimale avec les paramètres Systre ne signifie pas que le système produira les mêmes résultats sur des périodes de temps différentes. Donc, si vous obtenez une solution décente dans 2000 essais sur 2 millions de combinaisons, tout devrait bien se passer.
Recommended Posts