Visuelle Auswertung der Optimierungsergebnisse
Einführung
Ein nutzerdefiniertes Optimierungskriterium bietet eine sehr bequeme Einrichtung für die Optimierung von Expert Advisors. Wenn wir jedoch mehrere Kriterien prüfen müssen, sollten wir mehrere Optimierungen durchführen, was zeitaufwändig sein kann. Eine bessere Lösung wäre die Möglichkeit, mehrere nutzerdefinierte Kriterien während einer Optimierung zu testen. Außerdem wäre es schön, wenn man sofort die Diagramme von Saldo (eng: balance) und Kapitalwert (eng: equity) sehen könnte.
Es ist immer gut, wenn man verschiedene Visualisierungsmöglichkeiten hat. Unser Gehirn nimmt mehr als achtzig Prozent der Informationen über die Augen auf. In diesem Artikel befassen wir uns also mit der Erstellung von Optimierungsdiagrammen und der Auswahl des optimalen nutzerdefinierten Kriteriums.
Wir werden auch sehen, wie man eine gewünschte Lösung mit wenig MQL5-Kenntnissen erstellen kann, indem man die auf der Website veröffentlichten Artikel und Forumskommentare verwendet.
Formulierung der Aufgabe
- Sammeln Sie die Daten der einzelnen Optimierungsdurchgänge.
- Erstellen Sie Salden-/Kapitaldiagramme für jeden Optimierungsdurchgang.
- Berechnen Sie mehrere nutzerdefinierte Optimierungskriterien.
- Sortieren Sie die Diagramme nach dem nutzerdefinierten Optimierungskriterium in aufsteigender Reihenfolge.
- Zeigen Sie die besten Ergebnisse für alle nutzerdefinierten Kriterien.
Schritte zur Problemlösung
Da wir den Code des Expert Advisors ohnehin ändern müssen, wollen wir versuchen, diese Änderungen zu minimieren.
- Daher wird der gesamte Datenerfassungscode in einer separaten Include-Datei SkrShotOpt.mqh implementiert, während das nutzerdefinierte Kriterium in der Datei CustomCriterion.mqh berechnet wird.
- Der Screenshot ScreenShotOptimization.mq5 dient der Erstellung von Diagrammen und der Speicherung von Screenshots.
Daher müssen wir nur wenige Codezeilen in den Expert Advisor einfügen.
1. Das Sammeln von Daten. SkrShotOpt.mqh
Die maximalen und minimalen Kapitalwerte werden in die Funktion OnTick() geschrieben.
double _Equity = AccountInfoDouble(ACCOUNT_EQUITY); if(tempEquityMax < _Equity) tempEquityMax = _Equity; if(tempEquityMin > _Equity) tempEquityMin = _Equity;
Um zu vermeiden, dass Positionsänderungen bei jedem Tick überprüft werden müssen, werden Positionsänderungen in der Funktion OnTradeTransaction() verfolgt:
void IsOnTradeTransaction(const MqlTradeTransaction & trans, const MqlTradeRequest & request, const MqlTradeResult & result) { if(trans.type == TRADE_TRANSACTION_DEAL_ADD) if(HistoryDealSelect(trans.deal)) { if(_deal_entry != DEAL_ENTRY_OUT && _deal_entry != DEAL_ENTRY_OUT_BY) _deal_entry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY); if(trans.deal_type == DEAL_TYPE_BUY || trans.deal_type == DEAL_TYPE_SELL) if(_deal_entry == DEAL_ENTRY_IN || _deal_entry == DEAL_ENTRY_OUT || _deal_entry == DEAL_ENTRY_INOUT || _deal_entry == DEAL_ENTRY_OUT_BY) allowed = true; } }
Wenn sich die Anzahl der offenen Positionen ändert, werden die Felder für den Saldo und das Eigenkapital gefüllt.
if(allowed) // if there was a trade { double accBalance = AccountInfoDouble(ACCOUNT_BALANCE); double accEquity = AccountInfoDouble(ACCOUNT_EQUITY); ArrayResize(balance, _size + 1); ArrayResize(equity, _size + 1); balance[_size] = accBalance; if(_deal_entry != DEAL_ENTRY_OUT && _deal_entry != DEAL_ENTRY_OUT_BY) // if a new position appeared equity[_size] = accEquity; else // if position closed { if(changesB < accBalance) equity[_size] = tempEquityMin; else switch(s_view) { case min_max_E: equity[_size] = tempEquityMax; break; default: equity[_size] = tempEquityMin; break; } tempEquityMax = accEquity; tempEquityMin = accEquity; } _size = _size + 1; changesPos = PositionsTotal(); changesB = accBalance; _deal_entry = -1; allowed = false; }
Die Größe der Datei mit Frames ist begrenzt. Bei einer großen Anzahl von Handelspositionen wird die Datei immer größer und ist schwer zu verarbeiten. Daher sollten nur die notwendigsten Informationen in die Datei geschrieben werden.
Wenn eine Position eröffnet wird, speichern wir den Saldo und den Kapitalwert:
- Am Ende schreiben wir den maximalen Kapitalwert, wenn die Position mit einem Verlust geschlossen wurde.
- den kleinsten Kapitalwert, wenn die Position mit einem Gewinn abgeschlossen wurde.
Somit hat fast jede Position vier Werte, die in Arrays geschrieben werden: Saldo und Kapitalwert bei Eröffnung, Saldo und Max/Min -Kapitalwert bei Schließung.
Es kann vorkommen, dass eine Position geschlossen und eine andere im selben Tick eröffnet wird. In diesem Fall wird nur eine Position geschrieben. Dies hat keinen Einfluss auf die Visualisierung der Diagramme, reduziert aber die Anzahl der Arrays erheblich.
Speichern der gesammelten Daten in einer Datei.
Es ist nur sinnvoll, gewinnbringende Optimierungsdurchläufe zu sammeln. Dieser Parameter ist in den Einstellungen implementiert, sodass Sie bei Bedarf zusätzlich verlustbringende Durchgänge registrieren können. Was die Vorwärtsdurchläufe betrifft, so werden sie alle aufgezeichnet.
Mit der Funktion FrameAdd() werden die gesammelten Daten am Ende jedes einzelnen Durchlaufs in eine Datei geschrieben, wenn das Ereignis Tester eintritt. Das Tester-Ereignis wird wiederum von der Funktion OnTester() verarbeitet.
bool FrameAdd( const string name, // public name/tag long id, // public id double value, // value const void& data[] // array of any type );
Ein ausführliches und anschauliches Beispiel für die Arbeit mit der Funktion FrameAdd() finden Sie hier: https://www.mql5.com/ru/forum/11277/page4#comment_469771
Da FrameAdd() nur ein Array und einen numerischen Wert 'value' schreiben kann, es aber gut ist, zusätzlich zu Saldo und Kapitalwert alle Werte der Aufzählung ENUM_STATISTICS zu übergeben, werden die Daten sequentiell in ein Array geschrieben, während die Arraygröße in den übergebenen numerischen Wert 'value' geschrieben wird.
if(id == 1) // if it is a backward pass { // if profit % and the number of trades exceed those specified in the settings, the pass is written into the file if(TesterStatistics(STAT_PROFIT) / TesterStatistics(STAT_INITIAL_DEPOSIT) * 100 > _profit && TesterStatistics(STAT_TRADES) >= trades) { double TeSt[42]; // total number of elements in the ENUM_STATISTICS enumeration is 41 IsRecordStat(TeSt); // writing testing statistics to the array IsCorrect(); // adjusting balance and equity arrays if(m_sort != none) { while((sort)size_sort != none) size_sort++; double LRB[], LRE[], coeff[]; Coeff = Criterion(balance, equity, LRB, LRE, TeSt, coeff, 3);// calculating custom criterion ArrayInsert(balance, equity, _size + 1, 0); // joining balance and equity arrays into one ArrayInsert(balance, TeSt, (_size + 1) * 2, 0); // add to the resulting array the array with the ENUM_STATISTICS data FrameAdd(name, id, _size + 1, balance); // write the frame into the file } else { ArrayInsert(balance, equity, _size + 1, 0); // joining balance and equity arrays into one ArrayInsert(balance, TeSt, (_size + 1) * 2, 0); // add to the resulting array the array with the ENUM_STATISTICS data FrameAdd(name, id, _size + 1, balance); // write the frame into the file } } }
Vorwärtsduchläufe werden ähnlich wie Rückwärtsdurchläufe behandelt, sind aber eigentlich die Folge der Optimierung. Aus diesem Grund werden für sie nur Salden und Kapitalwerte geschrieben, ohne ENUM_STATISTICS-Werte.
Wenn zum Endzeitpunkt des Tests eine offene Position besteht, wird sie vom Tester geschlossen.
Das bedeutet, dass wir eine Position praktisch schließen (den aktuellen Saldo und Kapitalwert schreiben), wenn die Variable, die die Anzahl der offenen Geschäfte speichert, zum Zeitpunkt des Testendes nicht gleich ist.
void IsCorrect() { if(changesPos > 0) // if there is an open position by the testing end time, it should be virtually closed as the tester will close such a position { _size++; ArrayResize(balance, _size + 1); ArrayResize(equity, _size + 1); if(balance[_size - 2] > AccountInfoDouble(ACCOUNT_BALANCE)) { balance[_size - 1] = AccountInfoDouble(ACCOUNT_BALANCE); switch(s_view) { case min_max_E: equity[_size - 1] = tempEquityMax; break; default: equity[_size - 1] = tempEquityMin; break; } } else { balance[_size - 1] = AccountInfoDouble(ACCOUNT_BALANCE); equity[_size - 1] = tempEquityMin; } balance[_size] = AccountInfoDouble(ACCOUNT_BALANCE); equity[_size] = AccountInfoDouble(ACCOUNT_EQUITY); } else { ArrayResize(balance, _size + 1); ArrayResize(equity, _size + 1); balance[_size] = AccountInfoDouble(ACCOUNT_BALANCE); equity[_size] = AccountInfoDouble(ACCOUNT_EQUITY); } }
Damit ist das Schreiben der Daten ist abgeschlossen.
Lesen der Daten aus der Datei. ScreenShotOptimization.mq5
Nach der Optimierung wird eine Datei mit Frames unter dem folgenden Pfad erstellt: C:\Users\*User-Name*\AppData\Roaming\MetaQuotes\Terminal\Terminalkennung\MQL5\Files\Tester. Die Datei trägt den Namen EA_name.symbol.timeframe.mqd. Auf die Datei kann nicht sofort nach der Optimierung zugegriffen werden. Wenn Sie jedoch das Terminal neu starten, kann auf die Datei mit regulären Dateifunktionen zugegriffen werden.
Suchen Sie die Datei unter C:\Users\*User-Name*\AppData\Roaming\MetaQuotes\Terminal\terminal ID\MQL5\Files\Tester.
int count = 0; long search_handle = FileFindFirst("Tester\\*.mqd", FileName); do { if(FileName != "") count++; FileName = "Tester\\" + FileName; } while(FileFindNext(search_handle, FileName)); FileFindClose(search_handle);
Zunächst wird das Lesen in die Struktur eingeordnet.
FRAME Frame = {0}; FileReadStruct(handle, Frame); struct FRAME { ulong Pass; long ID; short String[64]; double Value; int SizeOfArray; long Tmp[2]; void GetArrayB(int handle, Data & m_FB) { ArrayFree(m_FB.Balance); FileReadArray(handle, m_FB.Balance, 0, (int)Value); ArrayFree(m_FB.Equity); FileReadArray(handle, m_FB.Equity, 0, (int)Value); ArrayFree(m_FB.TeSt); FileReadArray(handle, m_FB.TeSt, 0, (SizeOfArray / sizeof(m_FB.TeSt[0]) - (int)Value * 2)); } void GetArrayF(int handle, Data & m_FB, int size) { FileReadArray(handle, m_FB.Balance, size, (int)Value); FileReadArray(handle, m_FB.Equity, size, (int)Value); } };
In den FRAME-Strukturfunktionen werden Datenstrukturfunktionen gefüllt, aus denen weitere Diagramme aufgebaut werden.
struct Data { ulong Pass; long id; int size; double Balance[]; double Equity[]; double LRegressB[]; double LRegressE[]; double coeff[]; double TeSt[]; }; Data m_Data[];
Da das Zeichnen von Tausenden von Screenshots sehr zeitintensiv ist, geben Sie in den Skripteinstellungen einen Parameter an, der das Speichern von Screenshots deaktiviert, wenn der Gewinn unter dem angegebenen Prozentsatz liegt.
Die Datei mit den Bildern wird in einer Schleife verarbeitet.
Um ein Diagramm zu zeichnen, benötigen wir ein Datenfeld. Daher werden zuerst alle Durchläufe, die das Kriterium des Mindestgewinns erfüllen, geschrieben.
Dann werden alle Rückwärtsdurchläufe iteriert, und die entsprechenden Vorwärtsdurchläufe werden entsprechend der Durchlaufnummer für sie ausgewählt. Das Array mit den Durchläufen in Vorwärtsrichtung wird dem Array mit den Durchläufen in Rückwärtsrichtung hinzugefügt.
Die Lösung kann zwei Arten von Diagrammen zeichnen. Einer davon ähnelt dem Graphen im Strategietester, d.h. der Durchlauf beginnt mit der Starteinlage.
Die zweite Variante des Vorwärtsdurchlaufs beginnt mit dem Depot, mit dem der Rückwärtsdurchlauf endete. In diesem Fall wird der Gewinnwert des Rückwärtsdurchlaufs zum Saldo und zum Kapitalwert des Vorwärtsdurchlaufs addiert und an das Ende des Arrays des Rückwärtsdurchlaufs geschrieben.
Dies geschieht natürlich nur, wenn die Optimierung mit einer Vorwärtsperiode durchgeführt wird.
int handle = FileOpen(FileName, FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_BIN); if(handle != INVALID_HANDLE) { FileSeek(handle, 260, SEEK_SET); while(Res && !IsStopped()) { FRAME Frame = {0}; // read from the file to the Frame structure Res = (FileReadStruct(handle, Frame) == sizeof(Frame)); if(Res) if(Frame.ID == 1) // if it is a Backward pass, write data to the m_Data structure { ArrayResize(m_Data, size + 1); m_Data[size].Pass = Frame.Pass; m_Data[size].id = Frame.ID; m_Data[size].size = (int)Frame.Value; Frame.GetArrayB(handle, m_Data[size]); // write data to the m_Data structure arrays // if profit of this pass corresponds to the input settings, immediately calculate optimization criteria if(m_Data[size].TeSt[STAT_PROFIT] / m_Data[size].TeSt[STAT_INITIAL_DEPOSIT] * 100 >= profitPersent) { Criterion(m_Data[size].Balance, m_Data[size].Equity, m_Data[size].LRegressB, m_Data[size].LRegressE, m_Data[size].TeSt, m_Data[size].coeff, m_lineR); size++; } } else // if it is a Forward pass, write to the end of the m_Data data structures if(m_Forward != BackOnly) // if drawing of only Backward passes is not selected in settings for(int i = 0; i < size; i++) { if(Frame.Pass == m_Data[i].Pass) // if Back and Forward pass numbers match { int m = 0; if(m_Forward == Back_Next_Forward) // if selected drawing of Forward graph as a continuation of Backward { Frame.GetArrayF(handle, m_Data[i], m_Data[i].size - 1); // write data at the end of the the m_Data structure array, with a one-trade shift for(int x = m_Data[i].size - 1; x < m_Data[i].size + (int)Frame.Value - 1; x++) { m_Data[i].Balance[x] = m_Data[i].Balance[x] + m_Data[i].TeSt[STAT_PROFIT]; // add profit of the Backward test to the Forward pass m_Data[i].Equity[x] = m_Data[i].Equity[x] + m_Data[i].TeSt[STAT_PROFIT]; } m = 1; } else Frame.GetArrayF(handle, m_Data[i], m_Data[i].size); // if drawing of a Forward pass from a starting balance is selected m_Data[i].coeff[Forward_Trade] = (int)(Frame.Value / 2); // number of forward trades (not exact)) m_Data[i].coeff[Profit_Forward] = m_Data[i].Balance[m_Data[i].size + (int)Frame.Value - m - 1] - m_Data[i].Balance[m_Data[i].size - m]; break; } if(i == size - 1) // if no Backward is found for this Forward pass, move the file pointer to the end of writing FileSeek(handle, Frame.SizeOfArray, SEEK_CUR); // of this frame as if we read array data from the file } } FileClose(handle); //---
Konstruktion der Diagramme
Das Plotten der Graphen.
string _GraphPlot(double& y1[], double& y2[], double& LRegressB[], double& LRegressE[], double& coeff[], double& TeSt[], ulong pass) { CGraphic graphic; //--- create graphic bool res = false; if(ObjectFind(0, "Graphic") >= 0) res = graphic.Attach(0, "Graphic"); else res = graphic.Create(0, "Graphic", 0, 0, 0, _width, _height); if(!res) return(NULL); graphic.BackgroundMain(FolderName); // print the Expert Advisor name graphic.BackgroundMainSize(FontSet + 1); // font size for the Expert Advisor name graphic.IndentLeft(FontSet); graphic.HistoryNameSize(FontSet); // font size for the line names graphic.HistorySymbolSize(FontSet); graphic.XAxis().Name("pass " + IntegerToString(pass)); // show the pass number along the X axis graphic.XAxis().NameSize(FontSet + 1); graphic.XAxis().ValuesSize(12); // price font size graphic.YAxis().ValuesSize(12); //--- add curves CCurve *curve = graphic.CurveAdd(y1, ColorToARGB(clrBlue), CURVE_POINTS_AND_LINES, "Balance"); // plot the balance graph curve.LinesWidth(widthL); // graph line width curve.PointsSize(widthL + 1); // size of dots on the balance graph CCurve *curve1 = graphic.CurveAdd(y2, ColorToARGB(clrGreen), CURVE_LINES, "Equity"); // plot the equity graph curve1.LinesWidth(widthL); int size = 0; switch(m_lineR) // plot the regression line { case lineR_Balance: // balance regression line { size = ArraySize(LRegressB); CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance"); curve2.LinesWidth(widthL); } break; case lineR_Equity: // equity regression line { size = ArraySize(LRegressE); CCurve *curve2 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity"); curve2.LinesWidth(widthL); } break; case lineR_BalanceEquity: // balance and equity regression line { size = ArraySize(LRegressB); CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance"); curve2.LinesWidth(widthL); CCurve *curve3 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity"); curve2.LinesWidth(widthL); } break; default: break; } //--- plot curves graphic.CurvePlotAll(); // Important!!! All lines and captions must be created after creating the graph; otherwise, the graph will override them if(size == 0) { size = ArraySize(LRegressE); if(size == 0) size = ArraySize(LRegressB); } int x1 = graphic.ScaleX(size - 1); //Scales the value of the number of trades along the X axis graphic.LineAdd(x1, 30, x1, _height - 45, ColorToARGB(clrBlue), LINE_END_BUTT); // construct the vertical line denoting the end of the Backward period string txt = ""; int txt_x = 70;// text indent along the X axis int txt_y = 30;// text indent along the Y axis graphic.FontSet("Arial", FontSet);// Set current font parameters for(int i = 0; i < size_sort; i++) // Write all coefficients and criteria on the chart { if(coeff[i] == 0) continue; if(i == 1 || i == 3) txt = StringFormat("%s = %d", EnumToString((sort)i), (int)coeff[i]); else if(i == 0 || i == 2) txt = StringFormat("%s = %.2f", EnumToString((sort)i), coeff[i]); else txt = StringFormat("%s = %.4f", EnumToString((sort)i), coeff[i]); graphic.TextAdd(txt_x, txt_y + FontSet * i, txt, ColorToARGB(clrGreen)); } txt_y = txt_y + FontSet * (size_sort - 1); txt = StringFormat("Profitability = %.2f", TeSt[STAT_PROFIT_FACTOR]); graphic.TextAdd(txt_x, txt_y + FontSet, txt, ColorToARGB(clrGreen)); txt = StringFormat("Expected payoff = %.2f", TeSt[STAT_EXPECTED_PAYOFF]); graphic.TextAdd(txt_x, txt_y + FontSet * 2, txt, ColorToARGB(clrGreen)); graphic.Update(); //--- return resource name return graphic.ChartObjectName(); }
Weitere Informationen über die Arbeit mit CGraphic finden Sie in den folgenden Artikeln:
- Wir schreiben eine Scalping-Markttiefe aufgrund der graphischen Bibliothek CGraphic
- Visualisierung! Eine grafische MQL5 Bibliothek ähnlich 'plot' der Sprache R
- Die Visualisierung von Optimierungsergebnissen nach dem ausgewählten Kriterium
- Die Behandlung der Ergebnisse der Optimierung mit einem grafischen Interface
- Grafisches Interface XI: Integration der graphischen Standardbibliothek (build 16)
Die Screenshots der Diagramme werden in einem separaten Ordner unter dem Verzeichnis der Dateien Files gespeichert. Der Ordnername lautet EA_name.Symbol.Zeitrahmen.
bool BitmapObjectToFile(const string ObjName, const string _FileName, const bool FullImage = true) { if(ObjName == "") return(true); const ENUM_OBJECT Type = (ENUM_OBJECT)ObjectGetInteger(0, ObjName, OBJPROP_TYPE); bool Res = (Type == OBJ_BITMAP_LABEL) || (Type == OBJ_BITMAP); if(Res) { const string Name = __FUNCTION__ + (string)MathRand(); ObjectCreate(0, Name, OBJ_CHART, 0, 0, 0); ObjectSetInteger(0, Name, OBJPROP_XDISTANCE, -5e3); const long chart = ObjectGetInteger(0, Name, OBJPROP_CHART_ID); Res = ChartSetInteger(chart, CHART_SHOW, false) && ObjectCreate(chart, Name, OBJ_BITMAP_LABEL, 0, 0, 0) && ObjectSetString(chart, Name, OBJPROP_BMPFILE, ObjectGetString(0, ObjName, OBJPROP_BMPFILE)) && (FullImage || (ObjectSetInteger(chart, Name, OBJPROP_XSIZE, ObjectGetInteger(0, ObjName, OBJPROP_XSIZE)) && ObjectSetInteger(chart, Name, OBJPROP_YSIZE, ObjectGetInteger(0, ObjName, OBJPROP_YSIZE)) && ObjectSetInteger(chart, Name, OBJPROP_XOFFSET, ObjectGetInteger(0, ObjName, OBJPROP_XOFFSET)) && ObjectSetInteger(chart, Name, OBJPROP_YOFFSET, ObjectGetInteger(0, ObjName, OBJPROP_YOFFSET)))) && ChartScreenShot(chart, FolderName + "\\" + _FileName, (int)ObjectGetInteger(chart, Name, OBJPROP_XSIZE), (int)ObjectGetInteger(chart, Name, OBJPROP_YSIZE)); ObjectDelete(0, Name); } return(Res); }
Dies sind die resultierenden Diagramme.
Die Diagramme im Ordner.
Wenn die Speicherung aller Screenshots gewählt worden ist, bestehen die Screenshotnamen aus dem nutzerdefinierten Kriterium, das für die Sortierung ausgewählt wurde + Gewinn + Durchlaufnummer.
Wenn nur die besten Durchläufe ausgewählt werden, setzen sich die Screenshotnamen aus dem nutzerdefinierten Kriterium + Gewinn zusammen.
So sieht das vom Skript erstellte Diagramm aus.
Unten sehen Sie das gleiche Diagramm aus dem Strategietester
Ich habe hier sehr ähnliche Diagramme gezeigt, aber in den meisten Fällen werden sie unterschiedlich sein. Das liegt daran, dass im Strategietester die Positionen entlang der X-Achse an die Zeit gebunden sind, während das Skript Diagramme erstellt, in denen die X-Achse an die Anzahl der Positionen gebunden ist. Außerdem sind die Kapitalwerte für die Analyse der vom Skript erstellten Diagramme nicht ausreichend, da wir nur ein Minimum an Informationen in einen Frame schreiben müssen, um die Datei klein genug zu halten. Gleichzeitig reichen diese Daten aus, um eine erste Bewertung der Effizienz eines Optimierungsdurchlaufs vorzunehmen. Sie sind auch ausreichend für die Berechnung eines nutzerdefinierten Optimierungskriteriums.
Starten Sie nach der Optimierung, bevor Sie die ScreenShotOptimization ausführen, das Terminal neu!
Ursprünglich wollte ich nur alle Optimierungsgraphen visualisieren. Aber als ich das Skript implementierte und siebentausend Screenshots im Ordner sah, wurde mir klar, dass es unmöglich ist, mit so vielen Graphen zu arbeiten. Stattdessen müssen wir die besten von ihnen anhand bestimmter Kriterien auswählen.
Ich habe schon vor langer Zeit festgestellt, dass algorithmische Händler in zwei Kategorien fallen:
- Die einen glauben, dass ein EA in einem sehr großen Zeitraum optimiert werden sollte, der mehreren Jahren oder sogar Dutzenden von Jahren entspricht und nach dem der EA funktionieren wird.
- Andere sind der Meinung, dass ein EA regelmäßig reoptimiert werden muss, und zwar in einem kleinen Zeitraum, z. B. ein Optimierungsmonat + eine Handelswoche, oder drei Optimierungsmonate + ein Handelsmonat oder ein anderer geeigneter Reoptimierungszeitplan.
Ich gehöre zum zweiten Typ.
Deshalb habe ich mich auf die Suche nach Optimierungskriterien gemacht, die als Filter für die Auswahl der besten Durchläufe dienen sollen.
Erstellen von nutzerdefinierten Optimierungskriterien
Alle nutzerdefinierten Optimierungskriterien werden in einer separaten Include-Datei CustomCriterion.mqh berechnet, da diese Berechnungen sowohl in der Skript-Operation zum Zeichnen der Diagramme als auch im Expert Advisor, den wir optimieren, verwendet werden.
Bevor ich mein eigenes nutzerdefiniertes Optimierungskriterium erstellt habe, habe ich eine Menge verwandtes Material gefunden.
R² als Gütemaß der Saldenkurve einer Strategie
Der Artikel enthält eine detaillierte Beschreibung einer linearen Regression und ihrer Berechnung mit Hilfe der AlgLib-Bibliothek. Er enthält auch eine gute Beschreibung des Bestimmtheitsmaßes R^2 und seiner Anwendung bei der Prüfung von Ergebnissen. Ich empfehle die Lektüre dieses Artikels.
Die Funktion zur Berechnung der linearen Regression, R^2, und ProfitStability:
void Coeff(double& Array[], double& LR[], double& coeff[], double& TeSt[], int total, int c) { //-- Fill the matrix: Y - Array value, X - ordinal number of the value CMatrixDouble xy(total, 2); for(int i = 0; i < total; i++) { xy[i].Set(0, i); xy[i].Set(1, Array[i]); } //-- Find coefficients a and b of the linear model y = a*x + b; int retcode = 0; double a, b; CLinReg::LRLine(xy, total, retcode, b, a); //-- Generate the linear regression values for each X; ArrayResize(LR, total); for(int x = 0; x < total; x++) LR[x] = x * a + b; if(m_calc == c) { //-- Find the coefficient of correlation of values with their linear regression corr = CAlglib::PearsonCorr2(Array, LR); //-- Find R^2 and its sign coeff[r2] = MathPow(corr, 2.0); int sign = 1; if(Array[0] > Array[total - 1]) sign = -1; coeff[r2] *= sign; //-- Find LR Standard Error if(total - 2 == 0) stand_err = 0; else { for(int i = 0; i < total; i++) { double delta = MathAbs(Array[i] - LR[i]); stand_err = stand_err + delta * delta; } stand_err = MathSqrt(stand_err / (total - 2)); } } //-- Find ProfitStability = Profit_LR/stand_err if(stand_err == 0) coeff[ProfitStability] = 0; else coeff[ProfitStability] = (LR[total - 1] - LR[0]) / stand_err; }
Aus diesem Artikel habe ich die Berechnung des individuellen Optimierungskriteriums ProfitStability übernommen. Seine Berechnung ist einfach: Zunächst berechnen wir den LR-Standardfehler, d.h. die durchschnittliche Abweichung der Regressionslinie von der Gleichgewichts- oder Kapitalwertlinie. Dann wird vom resultierenden Wert der Regressionslinie der Ausgangswert abgezogen, um TrendProfit zu erhalten.
Die ProfitStability wird als Verhältnis von TrendProfit und LR Standardabweichung berechnet:
Der Artikel beschreibt detailliert alle Vor- und Nachteile dieses Optimierungskriteriums. Er enthält auch eine Reihe von Tests zum Vergleich von ProfitStability mit anderen Optimierungskriterien.
Da die lineare Regression sowohl für den Saldo als auch für den Kapitalwert berechnet werden kann und die ProfitStability an die lineare Regressionsrechnung gebunden ist, wird die Berechnung der ProfitStability in die lineare Regressionsrechnung implementiert.
Erstellen von nutzerdefinierten Kriterien zur Optimierung von Expert Advisors
Es ist ein ziemlich alter Artikel aus dem Jahr 2011, aber er ist interessant und immer noch relevant. Ich habe eine Formel zur Berechnung des Trading System Safety Factor (TSSF) aus diesem Artikel verwendet.
TSSF = Avg.Win / Avg.Loss ((110% - %Win) / (%Win-10%) + 1)
if(TeSt[STAT_PROFIT_TRADES] == 0 || TeSt[STAT_LOSS_TRADES] == 0 || TeSt[STAT_TRADES] == 0) coeff[TSSF] = 0; else { double avg_win = TeSt[STAT_GROSS_PROFIT] / TeSt[STAT_PROFIT_TRADES]; double avg_loss = -TeSt[STAT_GROSS_LOSS] / TeSt[STAT_LOSS_TRADES]; double win_perc = 100.0 * TeSt[STAT_PROFIT_TRADES] / TeSt[STAT_TRADES]; // Calculate the secure ratio for this percentage of profitable deals: if((win_perc - 10.0) + 1.0 == 0) coeff[TSSF] = 0; else { double teor = (110.0 - win_perc) / (win_perc - 10.0) + 1.0; // Calculate the real ratio: double real = avg_win / avg_loss; if(teor != 0) coeff[TSSF] = real / teor; else coeff[TSSF] = 0; } }
Optimale Vorgehensweise für Entwicklung und Analyse von Handelssystemen
In diesem Artikel habe ich den LinearFactor verwendet, der wie folgt berechnet wird:
- LinearFactor = MaxDeviation/EndBalance
- MaxDeviaton = Max(MathAbs(Balance[i]-AverageLine))
- AverageLine=StartBalance+K*i
- K=(EndBalance-StartBalance)/n
- n - Anzahl der Positionen im Test
Einzelheiten finden Sie in dem oben genannten Artikel. Der Artikel ist sehr interessant und bietet viele nützliche Informationen.
Mit Blick auf die Zukunft möchte ich sagen, dass es mir nicht gelungen ist, ein universelles nutzerdefiniertes Optimierungskriterium zu finden, das zu jedem Expert Advisor passt. Verschiedene Kriterien liefern die besten Ergebnisse für verschiedene Expert Advisor.
In einigen EAs hat LinearFactor wunderbare Ergebnisse.
double MaxDeviaton = 0; double K = (Balance[total - 1] - Balance[0]) / total; for(int i = 0; i < total; i++) { if(i == 0) MaxDeviaton = MathAbs(Balance[i] - (Balance[0] + K * i)); else if(MathAbs(Balance[i] - (Balance[0] + K * i) > MaxDeviaton)) MaxDeviaton = MathAbs(Balance[i] - (Balance[0] + K * i)); } if(MaxDeviaton ==0 || Balance[0] == 0) coeff[LinearFactor] = 0; else coeff[LinearFactor] = 1 / (MaxDeviaton / Balance[0]);
In der persönlichen Korrespondenz erwähnte der Autor, dass dieses Kriterium weiter verstärkt werden kann, und er erklärte, wie, aber ich konnte diese Tipps nicht codieren.
Also fügte ich vier nutzerdefinierte Optimierungskriterien in den Code ein.
- R^2 - Bestimmtheitskoeffizient.
- ProfitStability (GewinnStabilität).
- TSSF - Handelssystem-Sicherheitsfaktor
- LinearFactor.
Alle diese Optimierungskriterien sind in unserem Projekt enthalten.
Leider konnte ich "Komplexes Kriterium, Maximum" nicht hinzufügen, weil ich nicht herausfinden konnte, wie es berechnet wird.
Mein Optimierungskriterium
Auf der Grundlage all dieser Artikel können wir mit der Erstellung unseres eigenen Optimierungskriteriums fortfahren.
Welche Saldenkurve wollen wir sehen? Ideal wäre natürlich eine stetig wachsende gerade Linie.
Schauen wir uns ein Diagramm mit Gewinn an.
Bei der Optimierung vergleichen wir nicht die Ergebnisse mehrerer EAs, sondern es handelt sich um die Ergebnisse ein und desselben EAs, weshalb ich beschlossen habe, die Zeit, in der der EA Gewinn erzielt hat, nicht zu berücksichtigen.
Auch die Volumina berücksichtige ich nicht. Aber wenn das Lot dynamisch berechnet wird, ist es notwendig, die Volumina irgendwie in die Berechnung des nutzerdefinierten Kriteriums einzubeziehen (dies ist nicht implementiert).
Wie viele Positionen? Es ist mir egal, wie viele Positionen mir Tausende einbringen, eine oder hundert Positionen, also werde ich auch die Anzahl der Positionen nicht berücksichtigen. Bitte beachten Sie jedoch, dass bei zu wenigen Positionen die lineare Regression falsch berechnet wird.
Was ist in diesem Diagramm wichtig? Zunächst einmal ist es der Gewinn. Ich habe mich entschieden, den relativen Gewinn auszuwerten, d.h. den Gewinn im Verhältnis zum Ausgangsbestand.
Relative_Prof = TeSt[STAT_PROFIT] / TeSt[STAT_INITIAL_DEPOSIT];
Ein weiterer sehr wichtiger Parameter ist der Drawdown.
Wie wird der Drawdown im Tester berechnet? Der maximale Kapitalwert auf der linken Seite wird mit dem minimalen Kapitalwert auf der rechten Seite verglichen.
Die Werte oberhalb des Saldos sind eher unangenehm - das Geld, das wir nicht verdienen konnten. Aber wenn die Werte unter dem Saldo liegen, tut es richtig weh.
Für mich ist der maximale Drawdown unterhalb des Saldos am wichtigsten. Der Handel sollte also nicht allzu sehr schmerzen.
double equityDD(const double & Balance[], const double & Equity[], const double & TeSt[], const double & coeff[], const int total) { if(TeSt[STAT_INITIAL_DEPOSIT] == 0) return(0); double Balance_max = Balance[0]; double Equity_min = Equity[0]; difference_B_E = 0; double Max_Balance = 0; switch((int)TeSt[41]) { case 0: difference_B_E = TeSt[STAT_EQUITY_DD]; break; default: for(int i = 0; i < total - 1; i++) { if(Balance_max < Balance[i]) Balance_max = Balance[i]; if(Balance[i] == 10963) Sleep(1); if(Balance_max - Equity[i + 1] > difference_B_E) { Equity_min = Equity[i + 1]; difference_B_E = Balance_max - Equity_min; Max_Balance = Balance_max; } } break; } return(1 - difference_B_E / TeSt[STAT_INITIAL_DEPOSIT]); }
Da die Werte der nutzerdefinierten Kriterien in aufsteigender Reihenfolge berücksichtigt werden sollten, habe ich den Drawdown von einem abgezogen. Je höher also der Wert, desto geringer der Drawdown.
Den daraus resultierenden Wert nannte ich equity_rel, d.h. Drawdown relativ zum Startguthaben.
Es stellte sich heraus, dass für die korrekte Berechnung von equity_rel die bisher verwendete Methode der Kapitalwerterfassung nicht geeignet ist. Da einige der minimalen Kapitalwerte verloren gehen, musste ich zwei Varianten implementieren, um die Kapitalwerte zu speichern. Die erste Variante speichert die maximalen Kapitalwerte, wenn mit einem Verlust abgeschlossen wird, und die minimalen Werte, wenn mit einem Gewinn abgeschlossen wird. Bei der zweiten Variante werden nur die minimalen Kapitalwerte gespeichert.
Um das Skript über die verwendete Methode zur Erfassung der Kapitalwerte zu informieren, wurden diese Optionen in das Array mit der Testerstatistik TeSt[41] geschrieben. Außerdem werden in der Funktion EquityDD() der Kapitalwert und die Differenz_B_E entsprechend der Methode der Kapitalerfassung berechnet.
//---
Als Nächstes habe ich beschlossen, verschiedene Daten zu kombinieren und das Ergebnis zu überprüfen.
//---
Auf der Grundlage von equity_rel ist es möglich, einen alternativen Erholungsfaktor zu berechnen.
difference_B_E — maximaler Kapitaldrawdown in Geldwerten.
coeff[c_recovery_factor] = coeff[Profit_Bak] / difference_B_E;
Um das Diagramm einer geraden Linie anzunähern, habe ich R^2 zum zweiten alternativen Erholungsfaktor hinzugefügt
coeff[c_recovery_factor_r2] = coeff[Profit_Bak] / difference_B_E * coeff[r2];
Da in den Einstellungen die Berechnung der Korrelation auf der Grundlage des Saldos oder des Kapitalwerts ausgewählt werden kann, wird R^2 mit dem Drawdown korrelieren, wenn wir nur die Mindestwerte des Kapitalwerts erfassen.
Die Formel "relativer Gewinn * R^2" kann interessante Ergebnisse für das nutzerdefinierte Kriterium liefern.
coeff[profit_r2] = relative_prof * coeff[r2];
Es wäre auch sinnvoll, zu berücksichtigen, wie groß die Korrelation war. Daher lautet das nächste nutzerdefinierte Kriterium wie folgt.
Relativer Gewinn * R^2 / Standardfehler
if(stand_err == 0) coeff[profit_r2_Err] = 0; else coeff[profit_r2_Err] = relative_prof * coeff[r2] / stand_err;
Da wir nun den relativen Gewinn, den Rückgang des Kapitalwerts im Verhältnis zum Startguthaben und R^2 kennen, können wir eine Formel erstellen, die den Gewinn, den Rückgang des Kapitalwerts und die Nähe der Kurve zu einer Geraden berücksichtigt:
relative_prof + equity_rel + r2;
Was aber, wenn wir einem dieser Parameter mehr Bedeutung beimessen wollen? Also habe ich die Gewichtungsvariable 'ratio' hinzugefügt.
Jetzt haben wir drei weitere nutzerdefinierte Optimierungskriterien.
coeff[profit_R_equity_r2] = relative_prof * ratio + coeff[equity_rel] + coeff[r2]; coeff[profit_equity_R_r2] = relative_prof + coeff[equity_rel] * ratio + coeff[r2]; coeff[profit_equity_r2_R] = relative_prof + coeff[equity_rel] + coeff[r2] * ratio;
Insgesamt haben wir zwölf nutzerdefinierte Optimierungskriterien.
1. R^2 - Bestimmungskoeffizient
2. ProfitStability
3. TSSF - der Sicherheitsfaktor des Handelssystems
4. LinearFactor
5. equity_rel
6. c_recovery_factor
7. c_recovery_factor_r2
8. profit_r2
9. profit_r2_Err
10. profit_R_equity_r2
11. profit_equity_R_r2
12. profit_equity_r2_R
Prüfen des Ergebnisses
Um das Ergebnis zu überprüfen, müssen wir einen einfachen Expert Advisor erstellen...
Laut dem vorläufigen Artikelplan sollte hier ein einfacher Expert Advisor Code stehen. Leider haben zwei einfache EAs nicht die gewünschten Ergebnisse gezeigt.
Daher musste ich einen der erstellten EAs nehmen, um die Ergebnisse mit ihm zu zeigen (ich habe den Namen des EAs versteckt).
Nehmen wir an, es ist Ende April und wir planen, den EA auf einem echten Konto einzusetzen. Wie findet man heraus, welches Kriterium man optimieren muss, damit er mit Gewinn handelt?
Starten wir eine dreimonatige Vorwärtsoptimierung.
Starten Sie das Terminal nach der Optimierung neu.
Führen Sie das Skript aus und wählen Sie in den Einstellungen nur die besten Ergebnisse aus. Hier sind die Ergebnisse im Ordner.
Dann wähle ich visuell den besten Durchlauf aus allen Durchläufen aus. Da ich mehrere ähnliche Ergebnisse hatte, wählte ich profit_equity_R_r2, da bei dieser Optimierung der geringere Drawdown Priorität hat.
Der gleiche Zeitraum sieht im Strategietester wie folgt aus:
Hier ist der maximale Saldo zum Vergleich:
Hier ist das Komplexe Kriterium max:
Wie Sie sehen können, gibt es mit dem besten profit_equity_R_r2 Durchlauf viel weniger Handelspositionen auf dem Chart, als mit der maximalen Balance und dem maximalen Complex, der Gewinn ist ungefähr der gleiche, aber der Chart ist viel glatter.
Wir haben also das nutzerdefinierte Kriterium bestimmt: profit_equity_R_r2. Schauen wir uns nun an, was passieren würde, wenn wir die Optimierung für die letzten drei Monate durchführen und, nachdem wir während der Optimierung die besten Einstellungen erhalten haben, beschließen würden, dieses Setup im Mai zu handeln.
Führen wir eine Vorwärtsoptimierung durch und prüfen wir.
Einstellungen für die Optimierung.
Legen Sie in den EA-Einstellungen das nutzerdefinierte Kriterium fest, für das die Optimierung durchgeführt werden soll.
Wenn wir also den EA für die letzten drei Monate mit dem nutzerdefinierten Kriterium profit_equity_R_r2 optimiert haben,
und dann mit den erhaltenen Einstellungen vom 1. April bis zum 1. Mai gehandelt hätten, würden wir 750 Einheiten mit einem Kapitalwert von 300 Einheiten verdienen.
Prüfen wir nun die Leistung des Validate EA von fxsaber!
Prüfen wir, wie der EA vier Monate lang handeln würde. Einstellungen von Validate: drei Optimierungsmonate und ein Handelsmonat.
Wie Sie sehen können, hat der EA diesen Stresstest überstanden!
Vergleichen wir ihn mit dem Chart, der die gleichen Einstellungen hat, aber für Complex Criterion max optimiert ist.
Der EA hat überlebt, aber...
Schlussfolgerung
Vorteile:
- Sie können alle Graphen der Optimierungsergebnisse gleichzeitig sehen.
- Die Möglichkeit, ein optimales nutzerdefiniertes Optimierungskriterium für Ihren EA zu finden.
Nachteile:
Aufgrund einer Begrenzung der aufgezeichneten Daten sind die Diagramme weniger aussagekräftig als die im Strategietester.
Bei einer großen Anzahl von Positionen wächst die Datei mit den Frames enorm an und wird unlesbar.
//---
Wie die Experimente gezeigt haben, gibt es nicht das einzige Superkriterium: Verschiedene Kriterien liefern die besten Ergebnisse für verschiedene Expert Advisor.
Aber wir haben eine ganze Reihe solcher Kriterien. Wenn die Leser die Idee unterstützen, wird die Auswahl noch größer sein.
//---
Einer der Tester hat vorgeschlagen, die Skripteinstellung im Artikel zu beschreiben, damit die Nutzer, die nicht bereit sind, das Material zu studieren, einfach den Code verwenden können, ohne die Details zu studieren.
Die Verwendung
Um diesen Code zu verwenden, laden Sie die unten angehängte Zip-Datei herunter, entpacken Sie sie und kopieren Sie sie in den MQL5-Ordner.
Wählen Sie im Terminal Datei -> Datenordner öffnen -> rechter Mausklick an einer leeren Stelle im neuen Ordner -> "Einfügen". Wenn Sie gefragt werden, ob Sie Dateien im Zielordner ersetzen wollen, wählen Sie "Ersetzen".
Starten Sie dann MetaEditor, öffnen Sie Ihren EA und nehmen Sie die folgenden Änderungen vor:
1. Fügen Sie IsOnTick() in die OnTick()-Funktion ein;
2. Fügen Sie den folgenden Code am Ende Ihres EAs ein:
#include <SkrShotOpt.mqh> double OnTester() {return(IsOnTester());} void OnTradeTransaction(const MqlTradeTransaction & trans, const MqlTradeRequest & request,const MqlTradeResult & result) { IsOnTradeTransaction(trans, request, result); }Wenn der EA bereits über die Funktion OnTradeTransaction() verfügt, fügen Sie ihr IsOnTradeTransaction(trans, request, result) hinzu;
3. Drücken Sie "Kompilieren".
Wenn Fehler generiert werden, die auf übereinstimmende Variablennamen hinweisen, müssen Sie die Namen ändern.
Einstellungen
Sobald Sie den Code eingefügt haben, erscheinen einige zusätzliche Zeilen in den EA-Einstellungen.
Aktivieren Sie keinesfalls die Kästchen zur Optimierung dieser Einstellungen!!! Diese Einstellungen haben keinen Einfluss auf die Optimierungsergebnisse, also optimieren Sie sie nicht.
- Write the pass if trades more than — Wenn Ihr EA viele Positionen handelt, kann dieser Parameter erhöht werden, um die Menge der in die Frames-Dateien geschriebenen Daten zu reduzieren.
- Write the pass if profit exceeds % — Verlustbringende Durchläufe werden standardmäßig entfernt. Sie können diesen Parameter ändern, wenn Sie nicht möchten, dass der Gewinn unter einem bestimmten Prozentsatz des Startguthabens liegt.
- Select equity values — wählen Sie "save only min equity" ein, wenn Sie die korrekte Berechnung der folgenden nutzerdefinierten Kriterien benötigen: quity_rel, c_recovery_factor, c_recovery_factor_r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R
Wenn Sie Diagramme ähnlich denen im Strategietester haben möchten, wählen Sie "save min and max equity"
- Custom max — wenn Sie "none" wählen, werden Frames in der Datei aufgezeichnet, aber es wird kein nutzerdefiniertes Kriterium berechnet (um die Optimierungszeit nicht zu erhöhen).
In diesem Fall können Sie jedoch keine Optimierung nach nutzerdefinierten Kriterien auswählen. Außerdem haben alle unten aufgeführten Parameter keine Wirkung.
Sie sollten ein nutzerdefiniertes Kriterium in diesem Parameter auswählen, wenn Sie die Optimierung nach einem nutzerdefinierten Kriterium durchführen möchten.
Beachten Sie, dass die Berechnung eines nutzerdefinierten Kriteriums von den Parametern "Kapitalwerte auswählen" und "Kriterium berechnen nach" abhängt.
- Calculate criterion by - R^2 auf der Grundlage von Bilanz oder Kapitalwert berechnen; wirkt sich auf alle nutzerdefinierten Kriterien aus, bei denen R^2 verwendet wird.
//----
Einstellungen des Skripts
- Draw regression line: Wählen Sie, welche Regressionslinie gezeichnet werden soll - Saldo, Kapitalwert, Saldo und Kapitalwert, keine.
- Profit percent more than - das Drucken von Screenshots ist zeitintensiv, daher können Sie wählen, dass nur Screenshots gedruckt werden, deren Gewinn den Parameter übersteigt.
- Only best results - wenn dies zutrifft, werden nur Screenshots mit dem besten Ergebnis jedes nutzerdefinierten Kriteriums gespeichert; andernfalls werden alle gespeichert.
- Custom criterion - wenn alle Screenshots ausgewählt sind, kann dieser Parameter verwendet werden, um das nutzerdefinierte Kriterium festzulegen, nach dem die Screenshots im Ordner sortiert werden sollen.
- ratio - das Gewicht zur Berechnung der nutzerdefinierten Kriterien Gewinn_R_Eigenkapital_r2, Gewinn_Eigenkapital_R_r2, Gewinn_Eigenkapital_r2_R.
- Calculate criterion by - R^2 auf der Grundlage von Bilanz oder Kapitalwert berechnen; wirkt sich auf alle nutzerdefinierten Kriterien aus, bei denen R^2 verwendet wird.
r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R.
- Graph - wählen Sie zwischen Graph wie im Tester "Rückwärts getrennt von Vorwärts", d.h. Vorwärts beginnt mit dem Startsaldo,
oder "Back continued by Forward" - Vorwärts beginnt mit dem letzten Saldo des Rückwärtsdurchlaufs.
//---
Die auf dieser Website veröffentlichten Artikel waren beim Schreiben des Programms sehr hilfreich.
Ich möchte mich bei allen Autoren der hier erwähnten Artikel bedanken!
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/9922
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.