English Русский 中文 Español 日本語 Português
preview
Verwendung von Optimierungsalgorithmen zur Konfiguration von EA-Parametern im laufenden Betrieb

Verwendung von Optimierungsalgorithmen zur Konfiguration von EA-Parametern im laufenden Betrieb

MetaTrader 5Tester | 20 Juni 2024, 10:09
188 0
Andrey Dik
Andrey Dik

Inhalt

1. Einführung
2. Architektur eines EA mit Selbstoptimierung
3. Virtualisierung von Indikatoren
4. Virtualisierung der Strategie
5. Prüfung der Funktionsweise



1. Einführung

Ich werde oft gefragt, wie man Optimierungsalgorithmen bei der Arbeit mit EAs und Strategien im Allgemeinen anwendet. In diesem Artikel möchte ich mich mit den praktischen Aspekten der Verwendung von Optimierungsalgorithmen befassen.

In der heutigen Finanzwelt, in der jede Millisekunde einen großen Unterschied machen kann, wird der algorithmische Handel immer notwendiger. Optimierungsalgorithmen spielen eine Schlüsselrolle bei der Entwicklung effizienter Handelsstrategien. Vielleicht glauben einige Skeptiker, dass Optimierungsalgorithmen und Handel keine Gemeinsamkeiten haben. In diesem Artikel werde ich jedoch aufzeigen, wie diese beiden Bereiche zusammenwirken können und welche Vorteile sich aus diesem Zusammenspiel ergeben.

Für unerfahrene Händler kann das Verständnis der Grundprinzipien von Optimierungsalgorithmen ein mächtiges Werkzeug sein, um profitable Geschäfte zu finden und Risiken zu minimieren. Für erfahrene Fachleute können fundierte Kenntnisse in diesem Bereich neue Horizonte eröffnen und dazu beitragen, ausgefeilte Handelsstrategien zu entwickeln, die die Erwartungen übertreffen.

Bei der Selbstoptimierung in einem EA handelt es sich um einen Prozess, bei dem der EA seine Handelsstrategieparameter anpasst, um auf der Grundlage historischer Daten und aktueller Marktbedingungen eine bessere Leistung zu erzielen. Das Verfahren kann die folgenden Aspekte umfassen:

  • Datenerhebung und -analyse. Der EA sollte historische Marktdaten sammeln und analysieren. Dabei können verschiedene Datenanalysetechniken wie statistische Analysen, maschinelles Lernen und künstliche Intelligenz eingesetzt werden.
  • Ziele setzen. Die EA sollte klar definierte Ziele haben, die sie anstrebt. Dies kann die Maximierung des Gewinns, die Minimierung des Risikos oder das Erreichen eines bestimmten Rentabilitätsniveaus sein.
  • Anwendung von Optimierungsalgorithmen. Um die besten Ergebnisse zu erzielen, kann ein EA verschiedene Optimierungsalgorithmen verwenden. Diese Algorithmen helfen dem EA, die optimalen Werte für die Strategieparameter zu finden.
  • Prüfung und Validierung. Nach der Optimierung sollte der EA einem Backtesting unterzogen und anhand aktueller Marktbedingungen validiert werden, um seine Effizienz sicherzustellen. Das Testen hilft bei der Bewertung der Leistung des EA und seiner Fähigkeit, sich an veränderte Marktbedingungen anzupassen.
  • Überwachung und Aktualisierung. Der EA sollte seine Leistung ständig überwachen und seine Strategieparameter bei Bedarf aktualisieren. Die Märkte verändern sich ständig, und der EA sollte bereit sein, sich an neue Bedingungen und Veränderungen bei Trends und Volatilität anzupassen.

Es gibt mehrere Hauptszenarien für den Einsatz von Optimierungsalgorithmen im Handel:

  • Optimierung der Parameter der Handelsstrategie. Optimierungsalgorithmen können zur Anpassung der Parameter von Handelsstrategien verwendet werden. Mit diesen Methoden können wir die besten Werte für Parameter wie gleitende Durchschnittsperioden, Stop-Loss- und Take-Profit-Niveaus oder andere Parameter im Zusammenhang mit Handelssignalen und -regeln ermitteln.
  • Optimierung des Zeitpunkts des Markteintritts/-austritts. Optimierungsalgorithmen können dabei helfen, auf der Grundlage historischer Daten und aktueller Marktbedingungen den optimalen Zeitpunkt für den Ein- und Ausstieg aus Positionen zu bestimmen. So können beispielsweise Optimierungsalgorithmen eingesetzt werden, um optimale Zeitintervalle für Handelssignale zu ermitteln.
  • Verwaltung des Portfolios. Optimierungsalgorithmen können dabei helfen, die optimale Vermögensaufteilung in einem Portfolio zu bestimmen, um bestimmte Ziele zu erreichen. So können wir beispielsweise Optimierungstechniken wie die Mean-Variance-Optimierung einsetzen, um die effizienteste Mischung von Vermögenswerten angesichts der erwarteten Erträge und Risiken zu finden. Dies kann die Bestimmung der optimalen Mischung zwischen Aktien, Anleihen und anderen Vermögenswerten sowie die Optimierung der Positionsgrößen und der Portfoliodiversifizierung umfassen.
  • Entwicklung von Handelsstrategien. Optimierungsalgorithmen können zur Entwicklung neuer Handelsstrategien eingesetzt werden. So können wir beispielsweise mit Hilfe der genetischen Programmierung auf der Grundlage historischer Daten evolutionär nach optimalen Regeln für den Ein- und Ausstieg aus Positionen suchen.
  • Risikomanagement. Optimierungsalgorithmen können beim Risikomanagement im Handel helfen. So können wir beispielsweise Optimierungsalgorithmen einsetzen, um die optimale Positionsgröße zu berechnen oder ein dynamisches Stop-Loss-Niveau zu bestimmen, das potenzielle Verluste minimiert.
  • Auswahl der besten Handelsinstrumente. Optimierungsalgorithmen können bei der Auswahl der besten Handelsinstrumente oder Vermögenswerte für den Handel helfen. So können beispielsweise Optimierungsalgorithmen verwendet werden, um Vermögenswerte nach verschiedenen Kriterien wie Rentabilität, Volatilität oder Liquidität zu bewerten.
  • Vorhersage der Finanzmärkte. Optimierungsalgorithmen können zur Vorhersage von Finanzmärkten verwendet werden. Optimierungsalgorithmen können zur Abstimmung der Parameter von Vorhersagemodellen oder zur Auswahl optimaler Kombinationen von Vorhersagemodellen verwendet werden.

Dies sind nur einige Beispiele für den Einsatz von Optimierungsalgorithmen im Handel. Insgesamt können Optimierungsalgorithmen dazu beitragen, verschiedene Aspekte des Handels zu automatisieren und zu verbessern, von der Suche nach optimalen Strategien bis hin zum Risiko- und Portfoliomanagement.


2. Architektur eines EA mit Selbstoptimierung

Um die Selbstoptimierung von EA zu gewährleisten, sind mehrere Schemata möglich, aber eines der einfachsten und am wenigsten notwendigen, um alle erforderlichen Fähigkeiten und Funktionen zu implementieren, ist das in Abbildung 1 dargestellte.

Auf der Zeitachse „History“ befindet sich der EA an dem Punkt „time now“, an dem die Optimierungsentscheidung getroffen wird. Der „EA“ ruft die „Managerfunktion“ auf, die den Optimierungsprozess verwaltet. Der EA übergibt die „Optimierungsparameter“ an diese Funktion.

Im Gegenzug fordert der Manager von der „Optimierung ALGO“ oder „AO“ eine Reihe von Parametern an, die im Folgenden als „Set“ bezeichnet werden. Danach überträgt der Manager die Einstellungen an die virtuelle Handelsstrategie „EA Virt“, die ein vollständiges Analogon der realen Strategie ist, die funktioniert und Handelsoperationen durchführt, „EA“.

„EA Virt“ führt den virtuellen Handel von der „Vergangenheit“ bis zur „Gegenwart“ durch. Der Manager führt „EA Virt“ so oft aus, wie in der Populationsgröße der „Optimierungsparameter“ angegeben. “EA Virt“ wiederum gibt das Ergebnis des Laufs auf History in Form von „ff result“ zurück.

“ff result“ ist das Ergebnis einer Fitnessfunktion oder Fitness oder eines Optimierungskriteriums, das nach dem Ermessen des Nutzers festgelegt werden kann. Dabei kann es sich beispielsweise um einen Saldo, einen Gewinnfaktor, eine mathematische Erwartung oder ein komplexes Kriterium handeln, aber auch um ein Integral oder ein kumulatives Differential, das zu vielen Zeitpunkten der „Geschichte“ gemessen wird. Das Ergebnis der Fitnessfunktion, oder „ff-Ergebnis“, ist also das, was der Nutzer als wichtigen Indikator für die Qualität der Handelsstrategie betrachtet.

Anschließend übergibt der Manager dem Optimierungsalgorithmus das „ff-Ergebnis“, das eine Bewertung einer bestimmten Menge darstellt.

Wenn die Stop-Bedingung erreicht ist, überträgt der Manager die besten Einstellungen an den „EA“, woraufhin der EA mit neuen, aktualisierten Parametern zum Zeitpunkt „time now“ bis zum Reoptimierungspunkt „reoptimiz“ weiterarbeitet (handelt), wo er erneut bis zu einer bestimmten Historientiefe optimiert wird.

Der Re-Optimierungspunkt kann aus verschiedenen Gründen gewählt werden. Dabei kann es sich um eine genau definierte Anzahl von historischen Balken handeln, wie im folgenden Beispiel, oder um eine bestimmte Bedingung, z. B. einen Rückgang der Handelsindikatoren auf ein bestimmtes kritisches Niveau.

Schema

Abbildung 1. EA-Selbstoptimierungsstruktur

Nach dem Schema des Optimierungsalgorithmus „ALGO“ kann er als „Black Box“ betrachtet werden, der seine Arbeit autonom ausführt (allerdings ist alles, was außerhalb liegt, auch eine „Black Box“ für ihn), unabhängig von der spezifischen Handelsstrategie, dem Manager und der virtuellen Strategie. Der Manager fordert vom Optimierungsalgorithmus Einstellungen an und sendet eine Bewertung dieser zurück. Diese Bewertung wird vom Optimierungsalgorithmus verwendet, um den nächsten Satz zu bestimmen. Dieser Zyklus wird so lange fortgesetzt, bis der beste Parametersatz gefunden ist, der den Anforderungen des Nutzers entspricht. So sucht der Optimierungsalgorithmus nach den optimalen Parametern - eben jenen, die den über die Fitnessfunktion in „EA Virt“ festgelegten Bedürfnissen des Nutzers entsprechen.


3. Virtualisierung von Indikatoren

Um den EA auf historischen Daten laufen zu lassen, müssen wir eine virtuelle Kopie einer Handelsstrategie erstellen, die dieselben Handelsoperationen durchführt wie bei der Arbeit mit einem Handelskonto. Wenn keine Indikatoren verwendet werden, ist die Virtualisierung der logischen Bedingungen innerhalb des EA relativ einfach. Wir brauchen nur die logischen Aktionen zu einem bestimmten Zeitpunkt in der Preisreihe zu beschreiben. Gleichzeitig ist die Verwendung von Indikatoren eine komplexere Aufgabe, und in den meisten Fällen stützen sich die Handelsstrategien auf verschiedene Indikatoren.

Das Problem besteht darin, dass bei der Suche nach optimalen Indikatorparametern in einer bestimmten Iteration Indikator-Handles mit dem aktuellen Einstellungen erstellt werden müssen. Nach Beendigung des Laufs auf historischen Daten sollten diese Handles wieder gelöscht werden, da sonst der Arbeitsspeicher schnell voll werden kann, insbesondere wenn es eine große Anzahl von möglichen Optionen für einen Parametereinstellungen (Sets) gibt. Dies ist kein Problem, wenn dieser Vorgang auf einer Symboltabelle durchgeführt wird, aber das Löschen von Handles ist im Tester nicht erlaubt.

Um das Problem zu lösen, müssen wir die Berechnung des Indikators innerhalb des ausführbaren EA „virtualisieren“, um die Verwendung von Handles zu vermeiden. Nehmen wir den Stochastik-Indikator als Beispiel.

Der Berechnungsteil eines jeden Indikators enthält die Standardfunktion OnCalculate. Diese Funktion sollte z. B. in „Calculate“ umbenannt und praktisch unverändert gelassen werden. 

Wir müssen den Indikator als Klasse entwerfen (eine Struktur wäre auch geeignet). Nennen wir sie „C_Stochastic“. In der Klassendeklaration müssen wir die Hauptindikatorpuffer als öffentliche Felder registrieren (zusätzliche Berechnungspuffer können privat sein) und die Init-Initialisierungsfunktion deklarieren, an die wir die Indikatorparameter übergeben müssen.

//——————————————————————————————————————————————————————————————————————————————
class C_iStochastic
{
  public: void Init (const int InpKPeriod,       // K period
                     const int InpDPeriod,       // D period
                     const int InpSlowing)       // Slowing
  {
    inpKPeriod = InpKPeriod;
    inpDPeriod = InpDPeriod;
    inpSlowing = InpSlowing;
  }

  public: int Calculate (const int rates_total,
                         const int prev_calculated,
                         const double &high  [],
                         const double &low   [],
                         const double &close []);

  //--- indicator buffers
  public:  double ExtMainBuffer   [];
  public:  double ExtSignalBuffer [];
  private: double ExtHighesBuffer [];
  private: double ExtLowesBuffer  [];

  private: int inpKPeriod; // K period
  private: int inpDPeriod; // D period
  private: int inpSlowing; // Slowing
};
//——————————————————————————————————————————————————————————————————————————————

Sowie die Berechnung des Indikators selbst in der Methode „Calculate“. Die Berechnung des Indikators unterscheidet sich in keiner Weise von dem Indikator, der in der Standardauslieferung des Terminals enthalten ist. Der einzige Unterschied ist die Größenverteilung der Indikatorpuffer und deren Initialisierung

Dies ist ein sehr einfaches Beispiel, um das Prinzip der Indikatorvirtualisierung zu verstehen. Die Berechnung erfolgt für die gesamte Tiefe der in den Indikatorparametern angegebenen Perioden. Es ist möglich, die Fähigkeit, zusätzlich nur den letzten Balken zu berechnen und Ringpuffer zu implementieren, zu organisieren, aber der Zweck des Artikels ist es, ein einfaches Beispiel zu zeigen, das minimale Eingriffe bei der Umwandlung des Indikators in eine virtuelle Form erfordert und für Nutzer mit minimalen Programmierkenntnissen zugänglich ist.

//——————————————————————————————————————————————————————————————————————————————
int C_iStochastic::Calculate (const int rates_total,
                              const int prev_calculated,
                              const double &high  [],
                              const double &low   [],
                              const double &close [])
{
  if (rates_total <= inpKPeriod + inpDPeriod + inpSlowing) return (0);

  ArrayResize (ExtHighesBuffer, rates_total);
  ArrayResize (ExtLowesBuffer,  rates_total);
  ArrayResize (ExtMainBuffer,   rates_total);
  ArrayResize (ExtSignalBuffer, rates_total);

  ArrayInitialize (ExtHighesBuffer, 0.0);
  ArrayInitialize (ExtLowesBuffer,  0.0);
  ArrayInitialize (ExtMainBuffer,   0.0);
  ArrayInitialize (ExtSignalBuffer, 0.0);

  int i, k, start;

  start = inpKPeriod - 1;

  if (start + 1 < prev_calculated)
  {
    start = prev_calculated - 2;
    Print ("start ", start);
  }
  else
  {
    for (i = 0; i < start; i++)
    {
      ExtLowesBuffer  [i] = 0.0;
      ExtHighesBuffer [i] = 0.0;
    }
  }

  //--- calculate HighesBuffer[] and ExtHighesBuffer[]
  for (i = start; i < rates_total && !IsStopped (); i++)
  {
    double dmin =  1000000.0;
    double dmax = -1000000.0;

    for (k = i - inpKPeriod + 1; k <= i; k++)
    {
      if (dmin > low  [k]) dmin = low  [k];
      if (dmax < high [k]) dmax = high [k];
    }

    ExtLowesBuffer  [i] = dmin;
    ExtHighesBuffer [i] = dmax;
  }

  //--- %K
  start = inpKPeriod - 1 + inpSlowing - 1;

  if (start + 1 < prev_calculated) start = prev_calculated - 2;
  else
  {
    for (i = 0; i < start; i++) ExtMainBuffer [i] = 0.0;
  }

  //--- main cycle
  for (i = start; i < rates_total && !IsStopped (); i++)
  {
    double sum_low  = 0.0;
    double sum_high = 0.0;

    for (k = (i - inpSlowing + 1); k <= i; k++)
    {
      sum_low  += (close [k] - ExtLowesBuffer [k]);
      sum_high += (ExtHighesBuffer [k] - ExtLowesBuffer [k]);
    }

    if (sum_high == 0.0) ExtMainBuffer [i] = 100.0;
    else                 ExtMainBuffer [i] = sum_low / sum_high * 100;
  }

  //--- signal
  start = inpDPeriod - 1;

  if (start + 1 < prev_calculated) start = prev_calculated - 2;
  else
  {
    for (i = 0; i < start; i++) ExtSignalBuffer [i] = 0.0;
  }

  for (i = start; i < rates_total && !IsStopped (); i++)
  {
    double sum = 0.0;
    for (k = 0; k < inpDPeriod; k++) sum += ExtMainBuffer [i - k];
    ExtSignalBuffer [i] = sum / inpDPeriod;
  }

  //--- OnCalculate done. Return new prev_calculated.
  return (rates_total);
}
//——————————————————————————————————————————————————————————————————————————————

Außerdem werde ich ein Beispiel für die Virtualisierung des MACD-Indikators geben:

//——————————————————————————————————————————————————————————————————————————————
class C_iMACD
{
  public: void Init (const int InpFastEMA,       // Fast   EMA period
                     const int InpSlowEMA,       // Slow   EMA period
                     const int InpSignalSMA)     // Signal SMA period
  {
    inpFastEMA   = InpFastEMA;
    inpSlowEMA   = InpSlowEMA;
    inpSignalSMA = InpSignalSMA;

    maxPeriod = InpFastEMA;
    if (maxPeriod < InpSlowEMA)   maxPeriod = InpSlowEMA;
    if (maxPeriod < InpSignalSMA) maxPeriod = InpSignalSMA;
  }

  public: int Calculate (const int rates_total,
                         const int prev_calculated,
                         const double &close []);

  //--- indicator buffers
  public:  double ExtMacdBuffer   [];
  public:  double ExtSignalBuffer [];
  private: double ExtFastMaBuffer [];
  private: double ExtSlowMaBuffer [];

  private: int ExponentialMAOnBuffer (const int rates_total, const int prev_calculated, const int begin, const int period, const double& price [],double& buffer []);
  private: int SimpleMAOnBuffer      (const int rates_total, const int prev_calculated, const int begin, const int period, const double& price [],double& buffer []);

  private: int inpFastEMA;   // Fast EMA period
  private: int inpSlowEMA;   // Slow EMA period
  private: int inpSignalSMA; // Signal SMA period
  private: int maxPeriod;
};
//——————————————————————————————————————————————————————————————————————————————

Die Spezifikationen des Indikators:

//——————————————————————————————————————————————————————————————————————————————
int C_iMACD::Calculate (const int rates_total,
                        const int prev_calculated,
                        const double &close [])
{
  if (rates_total < maxPeriod) return (0);

  ArrayResize (ExtMacdBuffer,   rates_total);
  ArrayResize (ExtSignalBuffer, rates_total);
  ArrayResize (ExtFastMaBuffer, rates_total);
  ArrayResize (ExtSlowMaBuffer, rates_total);

  ArrayInitialize (ExtMacdBuffer,   0.0);
  ArrayInitialize (ExtSignalBuffer, 0.0);
  ArrayInitialize (ExtFastMaBuffer, 0.0);
  ArrayInitialize (ExtSlowMaBuffer, 0.0);

  ExponentialMAOnBuffer (rates_total, prev_calculated, 0, inpFastEMA, close, ExtFastMaBuffer);
  ExponentialMAOnBuffer (rates_total, prev_calculated, 0, inpSlowEMA, close, ExtSlowMaBuffer);

  int start;
  if (prev_calculated == 0) start = 0;

  else start = prev_calculated - 1;

  //--- calculate MACD
  for (int i = start; i < rates_total && !IsStopped (); i++) ExtMacdBuffer [i] = ExtFastMaBuffer [i] - ExtSlowMaBuffer [i];

  //--- calculate Signal
  SimpleMAOnBuffer (rates_total, prev_calculated, 0, inpSignalSMA, ExtMacdBuffer, ExtSignalBuffer);

  return (rates_total);
}
//——————————————————————————————————————————————————————————————————————————————

Die Berechnung der exponentiellen Glättung muss überhaupt nicht geändert werden:

//——————————————————————————————————————————————————————————————————————————————
int C_iMACD::ExponentialMAOnBuffer (const int rates_total, const int prev_calculated, const int begin, const int period, const double& price [],double& buffer [])
{
  //--- check period
  if (period <= 1 || period > (rates_total - begin)) return (0);

  //--- save and clear 'as_series' flags
  bool as_series_price  = ArrayGetAsSeries (price);
  bool as_series_buffer = ArrayGetAsSeries (buffer);

  ArraySetAsSeries (price, false);
  ArraySetAsSeries (buffer, false);

  //--- calculate start position
  int    start_position;
  double smooth_factor = 2.0 / (1.0 + period);

  if (prev_calculated == 0) // first calculation or number of bars was changed
  {
    //--- set empty value for first bars
    for (int i = 0; i < begin; i++) buffer [i] = 0.0;

    //--- calculate first visible value
    start_position = period + begin;
    buffer [begin] = price [begin];

    for (int i = begin + 1; i < start_position; i++) buffer [i] = price [i] * smooth_factor + buffer [i - 1] * (1.0 - smooth_factor);
  }
  else start_position = prev_calculated - 1;

  //--- main loop
  for (int i = start_position; i < rates_total; i++) buffer [i] = price [i] * smooth_factor + buffer [i - 1] * (1.0 - smooth_factor);

  //--- restore as_series flags
  ArraySetAsSeries (price,  as_series_price);
  ArraySetAsSeries (buffer, as_series_buffer);
  //---
  return (rates_total);
}
//——————————————————————————————————————————————————————————————————————————————

Die Berechnung der einfachen Glättung erfordert ebenfalls keine Änderungen:

//——————————————————————————————————————————————————————————————————————————————
int C_iMACD::SimpleMAOnBuffer (const int rates_total, const int prev_calculated, const int begin, const int period, const double& price [],double& buffer [])
{
  //--- check period
  if (period <= 1 || period > (rates_total - begin)) return (0);

  //--- save as_series flags
  bool as_series_price = ArrayGetAsSeries (price);
  bool as_series_buffer = ArrayGetAsSeries (buffer);

  ArraySetAsSeries (price, false);
  ArraySetAsSeries (buffer, false);

  //--- calculate start position
  int start_position;

  if (prev_calculated == 0) // first calculation or number of bars was changed
  {
    //--- set empty value for first bars
    start_position = period + begin;

    for (int i = 0; i < start_position - 1; i++) buffer [i] = 0.0;

    //--- calculate first visible value
    double first_value = 0;

    for (int i = begin; i < start_position; i++) first_value += price [i];

    buffer [start_position - 1] = first_value / period;
  }
  else start_position = prev_calculated - 1;

  //--- main loop
  for (int i = start_position; i < rates_total; i++) buffer [i] = buffer [i - 1] + (price [i] - price [i - period]) / period;

  //--- restore as_series flags
  ArraySetAsSeries (price, as_series_price);
  ArraySetAsSeries (buffer, as_series_buffer);

  //---
  return (rates_total);
}
//——————————————————————————————————————————————————————————————————————————————


4. Virtualisierung der Strategie

Einer der Leser meiner Artikel über Optimierungsalgorithmen, LUIS ALBERTO BIANUCCI, hat mir freundlicherweise den Code für einen EA zur Verfügung gestellt, der auf dem Stochastik-Indikator basiert. Er bat mich, auf der Grundlage dieses Codes ein Beispiel zu erstellen, um eine Möglichkeit zu demonstrieren, wie das Selbstlernen eines EAs mit der AO-Core-Bibliothek organisiert werden kann, und dieses Beispiel in dem Artikel zu berücksichtigen. So können andere Nutzer diese Methode bei der Verbindung von Optimierungsalgorithmen in ihren eigenen Entwicklungen nutzen. Ich möchte betonen, dass diese Methode geeignet ist, alle in meiner Serie „Populationsoptimierungsalgorithmen“ besprochenen Optimierungsalgorithmen miteinander zu verbinden, da die Algorithmen in einer universellen Form konzipiert sind und in beliebigen Anwenderprojekten erfolgreich eingesetzt werden können.

Wir haben uns bereits mit der Virtualisierung eines Indikators als Teil eines EAs beschäftigt. Wir gehen nun dazu über, die Virtualisierung einer Strategie zu betrachten. Am Anfang des EA-Codes werden wir den Import der Bibliothek, die Include-Dateien der Standard-Handelsbibliothek und die Include-Datei der virtuellen Stochastik deklarieren.

Als Nächstes kommt der „Input“ - die EA-Parameter, von denen InpKPeriod_P und InpUpperLevel_P die wichtigsten sind. Sie stellen die Periode und die Niveaus des Stochastik-Indikators dar und müssen optimiert werden.

input string   InpKPeriod_P        = "18|9|3|24";  //STO K period:      it is necessary to optimize
input string   InpUpperLevel_P  = "96|88|2|98"; //STO upper level: it is necessary to optimize

Die Parameter werden mit einem String-Typ deklariert, die Parameter sind zusammengesetzt und umfassen Standardwerte, Optimierungsanfangswert, Schrittweite und Endwert.

//——————————————————————————————————————————————————————————————————————————————
#import "\\Market\\AO Core.ex5"
bool   Init (int colonySize, double &range_min [], double &range_max [], double &range_step []);
//------------------------------------------------------------------------------
void   Preparation    ();
void   GetVariantCalc (double &variant [], int pos);
void   SetFitness     (double value,       int pos);
void   Revision       ();
//------------------------------------------------------------------------------
void   GetVariant     (double &variant [], int pos);
double GetFitness     (int pos);
#import
//——————————————————————————————————————————————————————————————————————————————

#include <Trade\Trade.mqh>;
#include "cStochastic.mqh"


input group         "==== GENERAL ====";
sinput long         InpMagicNumber      = 132516;       //Magic Number
sinput double       InpLotSize          = 0.01;         //Lots

input group         "==== Trading ====";
input int           InpStopLoss         = 1450;         //Stoploss
input int           InpTakeProfit       = 1200;         //Takeprofit

input group         "==== Stochastic ==|value|start|step|end|==";
input string        InpKPeriod_P        = "18|9|3|24";  //STO K period   : it is necessary to optimize
input string        InpUpperLevel_P     = "96|88|2|98"; //STO upper level: it is necessary to optimize

input group         "====Self-optimization====";
sinput bool         SelfOptimization    = true;
sinput int          InpBarsOptimize     = 18000;        //Number of bars in the history for optimization
sinput int          InpBarsReOptimize   = 1440;         //After how many bars, EA will reoptimize
sinput int          InpPopSize          = 50;           //Population size
sinput int          NumberFFlaunches    = 10000;        //Number of runs in the history during optimization
sinput int          Spread              = 10;           //Spread

MqlTick Tick;
CTrade  Trade;

C_iStochastic IStoch;

double Set        [];
double Range_Min  [];
double Range_Step [];
double Range_Max  [];

double TickSize = 0.0;

Bei der Initialisierung des EA in der OnInit-Funktion wird die Größe der Parameter-Arrays entsprechend der Anzahl der zu optimierenden Parameter festgelegt: Set - ein Reihe von Parametereinstellungen, Range_Min - minimale Parameterwerte (Startwerte), Range_Step - Parameterschrittweite und Range_Max - maximale Parameterwerte. Wir extrahieren die entsprechenden Werte aus den String-Parametern und weisen sie Arrays zu.

//——————————————————————————————————————————————————————————————————————————————
int OnInit ()
{
  TickSize = SymbolInfoDouble (_Symbol, SYMBOL_TRADE_TICK_SIZE);

  ArrayResize (Set,        2);
  ArrayResize (Range_Min,  2);
  ArrayResize (Range_Step, 2);
  ArrayResize (Range_Max,  2);

  string result [];
  if (StringSplit (InpKPeriod_P, StringGetCharacter ("|", 0), result) != 4) return INIT_FAILED;

  Set        [0] = (double)StringToInteger (result [0]);
  Range_Min  [0] = (double)StringToInteger (result [1]);
  Range_Step [0] = (double)StringToInteger (result [2]);
  Range_Max  [0] = (double)StringToInteger (result [3]);

  if (StringSplit (InpUpperLevel_P, StringGetCharacter ("|", 0), result) != 4) return INIT_FAILED;

  Set        [1] = (double)StringToInteger (result [0]);
  Range_Min  [1] = (double)StringToInteger (result [1]);
  Range_Step [1] = (double)StringToInteger (result [2]);
  Range_Max  [1] = (double)StringToInteger (result [3]);

  IStoch.Init ((int)Set [0], 1, 3);

  //  set magicnumber to trade object
  Trade.SetExpertMagicNumber (InpMagicNumber);

  //---
  return (INIT_SUCCEEDED);
}
//——————————————————————————————————————————————————————————————————————————————

In die OnTick-Funktion des EA-Codes fügen wir einen Aufrufblock für die Selbstoptimierung ein - die Funktion „Optimize“, die im Diagramm in Abbildung 1 der „manager“ ist und die Optimierung startet. Wir verwenden die Werte aus dem Array „Set“, wenn externe Variablen, die optimiert werden müssen, verwendet werden sollen.

//——————————————————————————————————————————————————————————————————————————————
void OnTick ()
{
  //----------------------------------------------------------------------------
  if (!IsNewBar ())
  {
    return;
  }

  //----------------------------------------------------------------------------
  if (SelfOptimization)
  {
    //--------------------------------------------------------------------------
    static datetime LastOptimizeTime = 0;

    datetime timeNow  = iTime (_Symbol, PERIOD_CURRENT, 0);
    datetime timeReop = iTime (_Symbol, PERIOD_CURRENT, InpBarsReOptimize);

    if (LastOptimizeTime <= timeReop)
    {
      LastOptimizeTime = timeNow;
      Print ("-------------------Start of optimization----------------------");

      Print ("Old set:");
      ArrayPrint (Set);

      Optimize (Set,
                Range_Min,
                Range_Step,
                Range_Max,
                InpBarsOptimize,
                InpPopSize,
                NumberFFlaunches,
                Spread * SymbolInfoDouble (_Symbol, SYMBOL_TRADE_TICK_SIZE));

      Print ("New set:");
      ArrayPrint (Set);

      IStoch.Init ((int)Set [0], 1, 3);
    }
  }

  //----------------------------------------------------------------------------
  if (!SymbolInfoTick (_Symbol, Tick))
  {
    Print ("Failed to get current symbol tick"); return;
  }

  //data preparation------------------------------------------------------------
  MqlRates rates [];
  int dataCount = CopyRates (_Symbol, PERIOD_CURRENT, 0, (int)Set [0] + 1 + 3 + 1, rates);

  if (dataCount == -1)
  {
    Print ("Data get error");
    return;
  }

  double hi [];
  double lo [];
  double cl [];

  ArrayResize (hi, dataCount);
  ArrayResize (lo, dataCount);
  ArrayResize (cl, dataCount);

  for (int i = 0; i < dataCount; i++)
  {
    hi [i] = rates [i].high;
    lo [i] = rates [i].low;
    cl [i] = rates [i].close;
  }

  int calc = IStoch.Calculate (dataCount, 0, hi, lo, cl);
  if (calc <= 0) return;

  double buff0 = IStoch.ExtMainBuffer [ArraySize (IStoch.ExtMainBuffer) - 2];
  double buff1 = IStoch.ExtMainBuffer [ArraySize (IStoch.ExtMainBuffer) - 3];

  //----------------------------------------------------------------------------
  // count open positions
  int cntBuy, cntSell;
  if (!CountOpenPositions (cntBuy, cntSell))
  {
    Print ("Failed to count open positions");
    return;
  }

  //----------------------------------------------------------------------------
  // check for buy
  if (cntBuy == 0 && buff1 <= (100 - (int)Set [1]) && buff0 > (100 - (int)Set [1]))
  {
    ClosePositions (2);

    double sl = NP (Tick.bid - InpStopLoss   * TickSize);
    double tp = NP (Tick.bid + InpTakeProfit * TickSize);

    Trade.PositionOpen (_Symbol, ORDER_TYPE_BUY, InpLotSize, Tick.ask, sl, tp, "Stochastic EA");
  }

  //----------------------------------------------------------------------------
  // check for sell
  if (cntSell == 0 && buff1 >= (int)Set [1] && buff0 < (int)Set [1])
  {
    ClosePositions (1);

    double sl = NP (Tick.ask + InpStopLoss   * TickSize);
    double tp = NP (Tick.ask - InpTakeProfit * TickSize);

    Trade.PositionOpen (_Symbol, ORDER_TYPE_SELL, InpLotSize, Tick.bid, sl, tp, "Stochastic EA");
  }
}
//——————————————————————————————————————————————————————————————————————————————

Die Funktion Optimieren führt die gleichen Aktionen aus, die normalerweise in den Skripten zum Testen von Optimierungsalgorithmen in der Artikelserie „Populationsoptimierungsalgorithmen“ durchgeführt werden:

1. Initialisierung des Optimierungsalgorithmus.
2.1. Vorbereitung der Population.
2.2. Gewinnung einer Reihe von Parametern aus dem Optimierungsalgorithmus.
2.3. Berechnung der Fitnessfunktion mit den ihr übergebenen Parametern.
2.4. Aktualisierung der besten Lösung.
2.5. Erhalt der besten Lösung durch den Algorithmus.

//——————————————————————————————————————————————————————————————————————————————
void Optimize (double      &set        [],
               double      &range_min  [],
               double      &range_step [],
               double      &range_max  [],
               const int    inpBarsOptimize,
               const int    inpPopSize,
               const int    numberFFlaunches,
               const double spread)
{
  //----------------------------------------------------------------------------
  double parametersSet [];
  ArrayResize(parametersSet, ArraySize(set));

  //----------------------------------------------------------------------------
  int epochCount = numberFFlaunches / inpPopSize;

  Init(inpPopSize, range_min, range_max, range_step);

  // Optimization-------------------------------------------------------------
  for (int epochCNT = 1; epochCNT <= epochCount && !IsStopped (); epochCNT++)
  {
    Preparation ();

    for (int set = 0; set < inpPopSize; set++)
    {
      GetVariantCalc (parametersSet, set);
      SetFitness     (VirtualStrategy (parametersSet, inpBarsOptimize, spread), set);
    }

    Revision ();
  }

  Print ("Fitness: ", GetFitness (0));
  GetVariant (parametersSet, 0);
  ArrayCopy (set, parametersSet, 0, 0, WHOLE_ARRAY);
}
//——————————————————————————————————————————————————————————————————————————————

Die Funktion VirtualStrategy führt Strategietests mit historischen Daten durch (im Diagramm in Abbildung 1 ist dies „EA Virt“). Sie nimmt das Array der Parameter „set“, „barsOptimize“ - die Anzahl der zu optimierenden Balken und den Wert „spread“.

Zunächst werden die Daten aufbereitet. Historische Daten werden in das Array „rates“ geladen. Dann werden die Arrays „hi“, „lo“ und „cl“, die für die Berechnung der Stochastik benötigt werden, erstellt.

Anschließend wird der Stochastik-Indikator initialisiert und auf der Grundlage historischer Daten berechnet. Wenn die Berechnung fehlschlägt, gibt die Funktion den Wert „-DBL_MAX“ zurück (den schlechtest möglichen Wert der Fitnessfunktion).

Anschließend wird die Strategie anhand historischer Daten getestet, wobei die Logik vollständig mit dem Hauptcode des EA übereinstimmt. Das Objekt „deals“ wird erstellt, um Angebote zu speichern. Anschließend werden die historischen Daten durchlaufen, wobei die Bedingungen für die Eröffnung und Schließung von Positionen für jeden Balken auf der Grundlage des Indikatorwerts und der Ebenen „upLevel“ und „dnLevel“ geprüft werden. Wenn die Bedingungen erfüllt sind, wird die Position geöffnet oder geschlossen.

Nach Abschluss des Durchlaufs durch die historischen Daten prüft die Funktion die Anzahl der abgeschlossenen Geschäfte. Wenn es keinen Handel gab, gibt die Funktion den Wert „-DBL_MAX“ zurück. Andernfalls gibt die Funktion den Endsaldo zurück.

Der Rückgabewert von VirtualStrategy ist der Wert der Fitnessfunktion. In diesem Fall ist dies der Wert des Endsaldos in Punkten (wie bereits erwähnt, kann die Fitnessfunktion ein Saldo, ein Gewinnfaktor oder ein beliebiger anderer Indikator für die Ergebnisqualität von Handelsgeschäften auf der Grundlage historischer Daten sein).

Es ist wichtig zu beachten, dass die virtuelle Strategie so genau wie möglich mit der Strategie des EA übereinstimmen sollte. In diesem Beispiel wird zu Eröffnungskursen gehandelt, was der Steuerung der Balkeneröffnung im Haupt-EA entspricht. Wenn die Logik der Handelsstrategie bei jedem Tick ausgeführt wird, muss der Nutzer dafür sorgen, dass die Tick-Historie während des virtuellen Tests heruntergeladen wird, und die VirtualStrategy-Funktion entsprechend anpassen.

//——————————————————————————————————————————————————————————————————————————————
double VirtualStrategy (double &set [], int barsOptimize, double spread)
{
  //data preparation------------------------------------------------------------
  MqlRates rates [];
  int dataCount = CopyRates(_Symbol, PERIOD_CURRENT, 0, barsOptimize + 1, rates);

  if (dataCount == -1)
  {
    Print ("Data get error");
    return -DBL_MAX;
  }

  double hi [];
  double lo [];
  double cl [];

  ArrayResize (hi, dataCount);
  ArrayResize (lo, dataCount);
  ArrayResize (cl, dataCount);

  for (int i = 0; i < dataCount; i++)
  {
    hi [i] = rates [i].high;
    lo [i] = rates [i].low;
    cl [i] = rates [i].close;
  }

  C_iStochastic iStoch;
  iStoch.Init ((int)set [0], 1, 3);

  int calc = iStoch.Calculate (dataCount, 0, hi, lo, cl);
  if (calc <= 0) return -DBL_MAX;

  //============================================================================
  //test of strategy on history-------------------------------------------------
  S_Deals deals;

  double iStMain0 = 0.0;
  double iStMain1 = 0.0;
  double upLevel  = set [1];
  double dnLevel  = 100.0 - set [1];
  double balance  = 0.0;

  //running through history-----------------------------------------------------
  for (int i = 2; i < dataCount; i++)
  {
    if (i >= dataCount)
    {
      deals.ClosPos (-1, rates [i].open, spread);
      deals.ClosPos (1, rates [i].open, spread);
      break;
    }

    iStMain0 = iStoch.ExtMainBuffer [i - 1];
    iStMain1 = iStoch.ExtMainBuffer [i - 2];

    if (iStMain0 == 0.0 || iStMain1 == 0.0) continue;

    //buy-------------------------------
    if (iStMain1 <= dnLevel && dnLevel < iStMain0)
    {
      deals.ClosPos (-1, rates [i].open, spread);

      if (deals.GetBuys () == 0) deals.OpenPos (1, rates [i].open, spread);
    }

    //sell------------------------------
    if (iStMain1 >= upLevel && upLevel > iStMain0)
    {
      deals.ClosPos (1, rates [i].open, spread);

      if (deals.GetSels () == 0) deals.OpenPos (-1, rates [i].open, spread);
    }
  }
  //----------------------------------------------------------------------------

  if (deals.histSelsCNT + deals.histBuysCNT <= 0) return -DBL_MAX;
  return deals.balance;
}
//——————————————————————————————————————————————————————————————————————————————

Wenn wir den Optimierungsalgorithmus aus der Reihe „Populationsoptimierungsalgorithmen“ verwenden wollen (der Algorithmus „Evolution of Social Groups“, kurz ESG, ist als Beispiel im Artikelarchiv zu finden), dann müssen wir den Pfad zum Algorithmus im EA angeben:

#include "AO_ESG.mqh"

In der Funktion Optimieren deklarieren wir das Objekt ESG-Algorithmus und konfigurieren die Grenzwerte der optimierten Parameter. Dann würde die Funktion Optimieren bei Verwendung von ESG wie folgt aussehen:

//——————————————————————————————————————————————————————————————————————————————
void Optimize (double      &set        [],
               double      &range_min  [],
               double      &range_step [],
               double      &range_max  [],
               const int    inpBarsOptimize,
               const int    inpPopSize,
               const int    numberFFlaunches,
               const double spread)
{
  //----------------------------------------------------------------------------
  int epochCount = numberFFlaunches / inpPopSize;

  C_AO_ESG AO;
  
  int    Population_P     = 200;   //Population size
  int    Groups_P         = 100;   //Number of groups
  double GroupRadius_P    = 0.1;   //Group radius
  double ExpansionRatio_P = 2.0;   //Expansion ratio
  double Power_P          = 10.0;  //Power
  
  AO.Init (ArraySize (set), Population_P, Groups_P, GroupRadius_P, ExpansionRatio_P, Power_P);
  
  for (int i = 0; i < ArraySize (set); i++)
  {
    AO.rangeMin  [i] = range_min  [i];
    AO.rangeStep [i] = range_step [i];
    AO.rangeMax  [i] = range_max  [i];
  }
  
  // Optimization-------------------------------------------------------------
  for (int epochCNT = 1; epochCNT <= epochCount && !IsStopped (); epochCNT++)
  {
    AO.Moving ();
    
    for (int set = 0; set < ArraySize (AO.a); set++)
    {
      AO.a [set].f = VirtualStrategy (AO.a [set].c, inpBarsOptimize, spread);
    }

    AO.Revision ();
  }

  Print ("Fitness: ", AO.fB);
  ArrayCopy (set, AO.cB, 0, 0, WHOLE_ARRAY);
}
//——————————————————————————————————————————————————————————————————————————————

Die in meiner Optimierungsserie vorgestellten Suchalgorithmus-Strategien bieten einen einfachen und klaren Ansatz, um sie in ihrer reinsten Form - „wie sie sind“ - zu analysieren und miteinander zu vergleichen. Sie enthalten keine Methoden, die die Suche beschleunigen, wie z. B. die Beseitigung von Duplikaten und einige andere Techniken, und erfordern mehr Iterationen und Zeit.


5. Prüfung der Funktionsweise

Testen wir unseren selbstoptimierenden EA auf Basis des Stochastik-Indikators über einen Zeitraum von einem Jahr mit den unten im Screenshot gezeigten Parametern, zunächst im Modus „false“ des Parameters SelfOptimization, d.h. ohne Selbstoptimierung.

foto_

Bild 2. EA-Einstellungen

OriginalTest

Abb. 3. Ergebnisse bei deaktivierter Selbstoptimierung

SelfOpt

Abb. 4. Ergebnisse bei aktivierter Selbstoptimierung


Zusammenfassung

In diesem Artikel haben wir uns mit der Selbstoptimierung in einem EA befasst. Diese Methode ist sehr einfach und erfordert nur minimale Eingriffe in den EA-Quellcode. Für jede spezifische Strategie empfiehlt es sich, eine Reihe von Experimenten durchzuführen, um die optimale Länge der historischen Segmente zu bestimmen, auf denen die Optimierung und der Handel stattfinden. Diese Werte sind individuell und strategieabhängig.

Es ist wichtig zu verstehen, dass eine Optimierung nicht zu positiven Ergebnissen führen kann, wenn es für die Strategie selbst keine profitablen Einstellungen gibt. Es ist unmöglich, aus Sand Gold zu gewinnen, wenn dort kein Gold vorhanden ist. Die Optimierung ist ein nützliches Instrument zur Verbesserung der Leistung einer Strategie, aber sie kann keine profitablen Sets schaffen, wo es keine gibt. Daher sollten Sie zunächst eine Strategie entwickeln, die ein Gewinnpotenzial hat, und diese dann durch Optimierung verbessern.

Die Vorteile dieses Ansatzes liegen in der Möglichkeit, eine Strategie anhand historischer Daten mit Hilfe von Walk-Forward-Tests zu testen und geeignete Optimierungskriterien zu finden, die einer bestimmten Strategie entsprechen. Walk-Forward-Tests ermöglichen es uns, die Effizienz einer Strategie anhand historischer Daten zu bewerten und dabei Veränderungen der Marktbedingungen im Laufe der Zeit zu berücksichtigen. Dies hilft, eine Überoptimierung zu vermeiden, wenn eine Strategie nur über einen bestimmten Zeitraum hinweg gut funktioniert, aber nicht in Echtzeit erfolgreich angewendet werden kann. Daher bietet der Walk-Forward-Test eine zuverlässigere Bewertung der Strategieleistung.

Der Walk-Forward-Test (WFT) ist eine Technik zur Bewertung und zum Testen von Handelsstrategien auf den Finanzmärkten. Er wird verwendet, um die Effizienz und Nachhaltigkeit von Handelsstrategien auf der Grundlage historischer Daten und ihrer Fähigkeit, in der Zukunft Gewinne zu erzielen, zu bestimmen.

Die Grundidee von WFT besteht darin, die verfügbaren Daten in mehrere Perioden zu unterteilen: eine historische Periode, die zur Entwicklung und Abstimmung einer Strategie verwendet wird (Trainingsperiode), und nachfolgende Perioden, die zur Bewertung und Prüfung der Strategie verwendet werden (Testperioden). Dieser Vorgang wird mehrmals wiederholt. Jedes Mal wird der Trainingszeitraum um einen Schritt nach vorne verschoben, und der Testzeitraum wird ebenfalls nach vorne verschoben. So wird die Strategie über verschiedene Zeiträume hinweg getestet, um sicherzustellen, dass sie sich an veränderte Marktbedingungen anpassen kann.

Walk-Forward-Tests sind eine realistischere Methode zur Bewertung von Strategien, da sie die Veränderungen der Marktbedingungen im Laufe der Zeit berücksichtigen. Sie hilft auch dabei, ein Übertraining einer Strategie auf der Grundlage historischer Daten zu vermeiden, und vermittelt ein genaueres Verständnis ihrer Leistung in der realen Welt.

Im Archiv, das diesem Artikel beigefügt ist, finden Sie Beispiele, die zeigen, wie Optimierungsalgorithmen mit dem EA verbunden werden können. Sie werden in der Lage sein, den Code zu studieren und ihn auf Ihre spezifische Strategie anzuwenden, um optimale Ergebnisse zu erzielen.

Das angegebene Beispiel ist nicht für den Handel auf realen Konten gedacht, da es keine notwendigen Kontrollen gibt, und soll lediglich die Möglichkeit der Selbstoptimierung demonstrieren.

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/14183

Beigefügte Dateien |
Risikobalance beim gleichzeitigen Handel von mehreren Handelsinstrumenten Risikobalance beim gleichzeitigen Handel von mehreren Handelsinstrumenten
Dieser Artikel ermöglicht es Anfängern, ein Skript für den Risikoausgleich beim gleichzeitigen Handel von mehreren Handelsinstrumenten von Grund auf zu schreiben. Darüber hinaus können erfahrene Nutzer neue Ideen für die Umsetzung ihrer Lösungen in Bezug auf die in diesem Artikel vorgeschlagenen Optionen erhalten.
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 3): Überarbeitung der Architektur Entwicklung eines Expertenberaters für mehrere Währungen (Teil 3): Überarbeitung der Architektur
Wir haben bereits einige Fortschritte bei der Entwicklung eines Mehrwährungs-EAs mit mehreren parallel arbeitenden Strategien gemacht. In Anbetracht der gesammelten Erfahrungen sollten wir die Architektur unserer Lösung überprüfen und versuchen, sie zu verbessern, bevor wir zu weit vorpreschen.
Statistische Arbitrage mit Vorhersagen Statistische Arbitrage mit Vorhersagen
Wir werden uns mit statistischer Arbitrage beschäftigen, wir werden mit Python nach Korrelations- und Kointegrationssymbolen suchen, wir werden einen Indikator für den Pearson-Koeffizienten erstellen und wir werden einen EA für den Handel mit statistischer Arbitrage mit Vorhersagen erstellen, die mit Python und ONNX-Modellen gemacht werden.
Algorithmen zur Optimierung mit Populationen: Künstliche multisoziale Suchobjekte (MSO) Algorithmen zur Optimierung mit Populationen: Künstliche multisoziale Suchobjekte (MSO)
Dies ist eine Fortsetzung des vorangegangenen Artikels, der sich mit dem Konzept der sozialen Gruppen befasst. In dem Artikel wird die Entwicklung sozialer Gruppen anhand von Bewegungs- und Gedächtnisalgorithmen untersucht. Die Ergebnisse werden dazu beitragen, die Entwicklung sozialer Systeme zu verstehen und sie bei der Optimierung und Suche nach Lösungen anzuwenden.