[Python] Versuchen Sie, die FX-Systolenparameter mit einer zufälligen Suche zu optimieren
Es ist eine Fortsetzung von. Lassen Sie uns einen genetischen Algorithmus (GA) anstelle einer zufälligen Suche implementieren.
Die Erstellung von Stundendaten erfolgt wie beim letzten Mal.
import numpy as np
import pandas as pd
import indicators as ind #indicators.Import von 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 Stunden Offset
ohlc = ind.TF_ohlc(dataM1, 'H') #Erstellung von Stundendaten
Sie benötigen Indikatoren.py und backtest.py, die auf GitHub hochgeladen wurden. Für backtest.py gibt es eine leichte Korrektur in BacktestReport
.
Dieses Mal fügen wir dem Schnittpunktsystem zweier gleitender Durchschnitte ein Signal zur Abrechnung hinzu, um die Kombination der zu optimierenden Parameterwerte zu erhöhen. Das Zahlungssignal ist wie folgt definiert.
In diesem System gibt es 3 Parameter. Dieses Mal werden wir jeden Parameter im folgenden Bereich durchsuchen.
SlowMAperiod = np.arange(7, 151) #Bereich der langfristigen gleitenden Durchschnittsperiode
FastMAperiod = np.arange(5, 131) #Bereich des kurzfristigen gleitenden Durchschnittszeitraums
ExitMAperiod = np.arange(3, 111) #Bereich des gleitenden Durchschnittszeitraums für die Abrechnung
Es gibt ungefähr 2 Millionen Kombinationen. Es ist möglich, alle zu treffen, aber es wird mehrere Stunden dauern.
Die Hauptroutine des genetischen Algorithmus ist fast dieselbe wie bei der vorherigen Zufallssuche. Ersetzen Sie einfach die zufällige Suche nach Parametern durch die unten beschriebene genetische Verarbeitung. Zusätzlich fügt das Kauf- und Verkaufssignal ein Abwicklungssignal wie in der obigen Regel hinzu.
def Optimize(ohlc, Prange):
def shift(x, n=1): return np.concatenate((np.zeros(n), x[:-n])) #Schaltfunktion
SlowMA = np.empty([len(Prange[0]), len(ohlc)]) #Langfristiger gleitender Durchschnitt
for i in range(len(Prange[0])):
SlowMA[i] = ind.iMA(ohlc, Prange[0][i])
FastMA = np.empty([len(Prange[1]), len(ohlc)]) #Kurzfristiger gleitender Durchschnitt
for i in range(len(Prange[1])):
FastMA[i] = ind.iMA(ohlc, Prange[1][i])
ExitMA = np.empty([len(Prange[2]), len(ohlc)]) #Mobiler Durchschnitt für die Zahlung
for i in range(len(Prange[2])):
ExitMA[i] = ind.iMA(ohlc, Prange[2][i])
Close = ohlc['Close'].values #Schlusskurs
M = 20 #Anzahl der Personen
Eval = np.zeros([M, 6]) #Bewertungsgegenstand
Param = InitParam(Prange, M) #Parameterinitialisierung
gens = 0 #Anzahl der Generationen
while gens < 100:
for k in range(M):
i0 = Param[k,0]
i1 = Param[k,1]
i2 = Param[k,2]
#Einstiegssignal kaufen
BuyEntry = (FastMA[i1] > SlowMA[i0]) & (shift(FastMA[i1]) <= shift(SlowMA[i0]))
#Eingangssignal verkaufen
SellEntry = (FastMA[i1] < SlowMA[i0]) & (shift(FastMA[i1]) >= shift(SlowMA[i0]))
#Ausgangssignal kaufen
BuyExit = (Close < ExitMA[i2]) & (shift(Close) >= shift(ExitMA[i2]))
#Ausgangssignal verkaufen
SellExit = (Close > ExitMA[i2]) & (shift(Close) <= shift(ExitMA[i2]))
#Backtest
Trade, PL = Backtest(ohlc, BuyEntry, SellEntry, BuyExit, SellExit)
Eval[k] = BacktestReport(Trade, PL)
#Generationswechsel
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'])
Eine weitere Änderung gegenüber dem letzten Mal besteht darin, dass der Bereich der drei Parameter "SlowMAperiod", "FastMAperiod" und "ExitMAperiod" in einer Liste mit dem Namen "Prange" zusammengefasst und an jede Funktion übergeben wird. Selbst wenn die Anzahl der Parameter zunimmt, kann dies auf diese Weise so behandelt werden, wie es ist.
In der obigen Funktion sind die für GA hinzugefügten Funktionen "InitParam ()" und "Evolution ()". Erstens ist InitParam () die Initialisierung der Parameter jedes Einzelnen.
from numpy.random import randint,choice
#Parameterinitialisierung
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 ()
enthält einige genetische Prozesse wie folgt:
#Genetische Verarbeitung
def Evolution(Param, Eval, Prange):
#Wählen Sie Roulette mit Elite-Speicher
#1 Punkt Kreuzung
#Nachbarschaftsgeneration
#Mutation
return Param
Jeder Prozess wird unten erklärt.
Wählen Sie zunächst aus den aktuellen Personen die Person aus, die für die nächste Generation übrig bleiben soll. Es gibt verschiedene Möglichkeiten, es auszuwählen, aber es gab eine Numpy-Funktion, die für die Roulette-Auswahl nützlich war. Verwenden wir sie also. Die Roulette-Auswahl ist eine Methode zur probabilistischen Auswahl von Personen, die für die nächste Generation übrig bleiben sollen, entsprechend dem Grad der Anpassungsfähigkeit, der der Bewertungswert des Rückentests ist. Je höher die Anpassungsfähigkeit, desto leichter bleibt es.
Die diesmal verwendete Funktion ist eine Funktion namens "numpy.random.choice ()", die zufällig die erforderliche Anzahl aus der Liste auswählt. Wenn Sie jedoch dem optionalen Argument eine Liste von Auswahlwahrscheinlichkeiten mit dem Namen "p" hinzufügen, ist dies der Fall Es wird nach der Wahrscheinlichkeit wählen. Dies ist die Roulette-Auswahl selbst. Der Code sieht folgendermaßen aus:
#Wählen Sie Roulette mit Elite-Speicher
Param = Param[np.argsort(Eval)[::-1]] #Sortieren
R = Eval-min(Eval)
R = R/sum(R)
idx = choice(len(Eval), size=len(Eval), replace=True, p=R)
idx[0] = 0 #Elite speichern
Param = Param[idx]
Es ist jedoch unpraktisch, wenn die Wahrscheinlichkeit negativ ist, weshalb sie so korrigiert wurde, dass der Mindestwert für die Anpassungsfähigkeit 0 ist. Wenn Sie nur Roulette auswählen, werden Sie möglicherweise auch bei hoher Anpassungsfähigkeit nicht unglücklich ausgewählt. Daher sortieren wir die Anpassungsfähigkeit so, dass die höchste Person (Elite) immer in der nächsten Generation verbleibt.
Als nächstes werden die Gene gekreuzt. Dies dient dazu, zwei Individuen auszuwählen und einen Teil ihrer genetischen Informationen miteinander auszutauschen. Es gibt verschiedene Methoden zum Überqueren, aber hier habe ich einen der Parameter ausgewählt und die Vorder- und Rückseite ausgetauscht.
#1 Punkt Kreuzung
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]))
Verwenden Sie erneut "choice ()", um eine Folge von Zufallszahlen für die Anzahl sich überschneidender Personen zu generieren. Durch Hinzufügen von "replace = False" erhalten Sie eine eindeutige Folge von Zufallszahlen. Dann wird die Kreuzung realisiert, indem zwei Personen als "ix" ausgewählt und die Daten in der zweiten Hälfte des Kreuzungspunkts "p" ausgetauscht werden.
Bei normaler GA wird die Evolution durch Auswahl, Kreuzung und Mutation simuliert. Wenn der Kreuzungspunkt jedoch wie diesmal auf die Unterbrechung des Parameters beschränkt ist, ist er voll von denselben Personen, bevor Sie ihn kennen. Die Evolution wird aufhören. Wenn es jedoch viele Mutationen gibt, kommt es einer zufälligen Suche nahe, so dass es nicht sehr effizient ist. Daher werden wir diesmal einige der Parameter auf +1 oder -1 ändern. Es handelt sich um eine sogenannte Nachbarschaftslösung, die häufig in lokalen Suchalgorithmen verwendet wird.
#Nachbarschaftsgeneration
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])
Wählen Sie eine Person aus, die sowohl eine Nachbarschaft als auch ein Kreuz erzeugt. Entscheiden Sie dann, welcher Parameter mit einer Zufallszahl geändert werden soll, und ändern Sie diesen Parameter um 1.
Führen Sie schließlich die Mutation durch. Es gibt verschiedene Möglichkeiten, dies zu tun, aber einige der Parameter der ausgewählten Person werden mit neuen Zufallszahlen neu geschrieben. Im Fall von GA sind Mutationen wichtig, um aus der lokalen Lösung herauszukommen. Wenn Sie sie jedoch häufig verwenden, nimmt die Zufälligkeit zu, sodass wir hier etwa zwei festlegen.
#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]))
Lassen Sie uns einen genetischen Algorithmus mit der oben definierten Funktion ausführen.
result = Optimize(ohlc, [SlowMAperiod, FastMAperiod, ExitMAperiod])
result.sort_values('Profit', ascending=False)
GA verwendet auch Zufallszahlen, sodass die Ergebnisse jedes Mal anders sind. Das Folgende ist ein Beispiel für die Ergebnisse, die die höchste Anpassungsfähigkeit für jede Generation zeigen. Da es als Elite gespeichert ist, wird die hohe Anpassungsfähigkeit nacheinander aktualisiert.
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
Die Individuen, die in der letzten Generation geblieben sind, sind wie folgt.
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 |
Ich habe nicht die optimale Lösung für dieses Problem untersucht, daher weiß ich es nicht, aber ich denke, dieses Ergebnis ist ziemlich hoch. Erstens besteht der Zweck von GA nicht darin, die optimale Lösung zu finden, sondern die quasi-optimale Lösung in kürzerer Zeit als Round-Robin zu finden.
Das Finden der optimalen Lösung mit Systre-Parametern bedeutet nicht, dass das System über verschiedene Zeiträume dieselben Ergebnisse liefert. Wenn Sie also in 2000 Versuchen aus 2 Millionen Kombinationen eine anständige Lösung erhalten, sollte es Ihnen gut gehen.
Recommended Posts