English Русский Español 日本語 Português
preview
Neuronale Netze leicht gemacht (Teil 69): Dichte-basierte Unterstützungsbedingung für die Verhaltenspolitik (SPOT)

Neuronale Netze leicht gemacht (Teil 69): Dichte-basierte Unterstützungsbedingung für die Verhaltenspolitik (SPOT)

MetaTrader 5Handelssysteme | 10 Juni 2024, 11:03
85 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Einführung

Offline-Verstärkungslernen ermöglicht das Trainieren von Modellen auf der Grundlage von Daten, die durch Interaktionen mit der Umwelt gesammelt wurden. Dadurch lässt sich der Prozess der Interaktion mit der Umwelt erheblich reduzieren. Darüber hinaus können wir angesichts der Komplexität der Umweltmodellierung Echtzeitdaten von mehreren Forschungsagenten sammeln und das Modell dann anhand dieser Daten trainieren.

Gleichzeitig werden durch die Verwendung eines statischen Trainingsdatensatzes die uns zur Verfügung stehenden Umweltinformationen erheblich eingeschränkt. Aufgrund der begrenzten Ressourcen können wir nicht die gesamte Vielfalt der Umgebung im Trainingsdatensatz erhalten.

Beim Lernen der optimalen Strategie des Agenten besteht jedoch eine hohe Wahrscheinlichkeit, dass seine Aktionen über die Verteilung des Trainingsdatensatzes hinausgehen. Da es keine Rückmeldungen aus der Umwelt gibt, können wir natürlich keine wirkliche Bewertung solcher Maßnahmen vornehmen. Aufgrund des Mangels an Daten im Trainingsdatensatz kann auch unser Kritiker (Critic) keine adäquate Bewertung abgeben. In diesem Fall können wir sowohl hohe als auch niedrige Erwartungen haben.

Es muss gesagt werden, dass hohe Erwartungen viel gefährlicher sind als niedrige. Bei unterschätzten Schätzungen kann das Modell die Durchführung dieser Aktionen verweigern, was zum Erlernen einer suboptimalen Agentenpolitik führt. Im Falle einer Überschätzung neigt das Modell dazu, ähnliche Aktionen zu wiederholen, was zu erheblichen Verlusten im Betrieb führen kann. Daher ist die Beibehaltung der Agentenpolitik innerhalb des Trainingsdatensatzes ein wichtiger Aspekt, um die Zuverlässigkeit des Offline-Trainings zu gewährleisten.

Verschiedene Offline-Verstärkungslernmethoden zur Lösung dieses Problems verwenden Parametrisierung oder Regularisierung, die die Strategie des Agenten darauf beschränken, Aktionen innerhalb der Unterstützungsmenge des Trainingsdatensatzes durchzuführen. Detaillierte Konstruktionen stören in der Regel die Agentenmodelle, was zu zusätzlichen Betriebskosten führen kann und die volle Nutzung etablierter Online-Verstärkungslernmethoden verhindert. Regularisierungsmethoden verringern die Diskrepanz zwischen der gelernten Strategie und dem Trainingsdatensatz, der möglicherweise nicht der Definition der dichtebasierten Unterstützung entspricht und somit ineffektiv ein Handeln außerhalb der Verteilung verhindert.

In diesem Zusammenhang schlage ich vor, die Anwendbarkeit der Methode Supported Policy OpTimization (SPOT) zu prüfen, die in dem Artikel „Supported Policy Optimization for Offline Reinforcement Learning“ vorgestellt wurde. Seine Ansätze ergeben sich direkt aus einer theoretischen Formalisierung der politischen Nebenbedingung auf der Grundlage der Dichteverteilung des Trainingsdatensatzes. SPOT verwendet einen Dichteschätzer, der auf einem Variational AutoEncoder (VAE) basiert, der ein einfaches, aber effektives Regularisierungselement ist. Sie kann in fertige Algorithmen des verstärkten Lernens eingebaut werden. SPOT erreicht die beste Leistung in seiner Klasse bei Standard-Offline-RL-Benchmarks. Dank des flexiblen Designs können mit SPOT offline trainierte Modelle auch online feinabgestimmt werden.


1. Algorithmus für die unterstützte Politikoptimierung (SPOT)

Die Durchführung einer Unterstützungsbeschränkung ist die typische Methode zur Abschwächung von Fehlern beim Offline-Verstärkungslernen. Die Nebenbedingung der Unterstützung kann wiederum auf der Grundlage der Dichte der Verhaltensstrategie formalisiert werden. Die Autoren der Methode Supported Policy OpTimization schlagen einen Regularisierungsalgorithmus aus der Perspektive einer expliziten Schätzung der Verhaltensdichte vor. SPOT enthält einen Regularisierungsterm, der sich direkt aus der theoretischen Formalisierung der Dichteunterstützungsbeschränkung ergibt. Das Regularisierungselement verwendet einen bedingten Variations-Auto-Encoder (CVAE), der die Dichte des Trainingsdatensatzes erlernt.

Ähnlich wie eine optimale Strategie aus einer optimalen Q-Funktion extrahiert werden kann, kann eine unterstützte optimale Strategie auch mit Hilfe der gierigen Auswahl wiederhergestellt werden:

Im Falle der Funktionsannäherung entspricht dies einem eingeschränkten Strategieoptimierungsproblem.

Im Gegensatz zu einer spezifischen Parametrisierung der Agentenpolitik oder Divergenzstrafen, die in anderen Methoden zur Einschränkung der Unterstützung verwendet werden, schlagen die Autoren von SPOT vor, die Dichte des Trainingsdatensatzes direkt als Nebenbedingung zu verwenden:

wobei ϵ'=log ϵ zur Vereinfachung der Schreibweise.

Die auf der Verhaltensdichte basierende Nebenbedingung ist im Zusammenhang mit der Unterstützungsbeschränkung einfach und unkompliziert. Die Autoren der Methode schlagen vor, die Log-Likelihood-Funktion anstelle der probabilistischen Funktion zu verwenden, weil sie mathematisch einfacher ist.

Daraus ergibt sich wiederum die zusätzliche Nebenbedingung, dass die Dichte der Verhaltensstrategie an jedem Punkt des Zustandsraums nach unten begrenzt ist. Es ist praktisch unmöglich, ein solches Problem zu lösen, da es eine große, sogar unendliche Anzahl von Nebenbedingungen gibt. Stattdessen verwenden die Autoren des SPOT-Algorithmus eine heuristische Annäherung, die die durchschnittliche Verhaltensdichte berücksichtigt:

Wandeln wir das eingeschränkte Optimierungsproblem in ein uneingeschränktes um. Zu diesem Zweck behandeln wir den Term der Nebenbedingung als Strafe. Daraus ergibt sich das Lernziel für die Politik als:

wobei λ ein Lagrangescher Multiplikator ist.

Der einfache Regularisierungsterm in der oben dargestellten Verlustfunktion erfordert den Zugang zu den Verhaltensregeln, die bei der Erfassung des Trainingsdatensatzes verwendet wurden. Aber wir haben nur Offline-Daten, die durch diese Politik erzeugt werden. Wir können die Wahrscheinlichkeitsdichte an einem beliebigen Punkt explizit schätzen, indem wir verschiedene Methoden zur Dichteschätzung verwenden. Der Variations-Autoencoder (VAE) ist eines der besten Modelle für die neuronale Dichteschätzung. Die Autoren der Methode haben sich für die Verwendung eines bedingten Variations-Autoencoders als Dichte-Schätzer entschieden. Nachdem wir die VAE trainiert haben, können wir sie einfach als untere Grenze.

Der oben vorgestellte allgemeine Rahmen kann mit minimalen Änderungen auf verschiedene Algorithmen des Reinforcement Learning aufgebaut werden. In ihrer Veröffentlichung verwenden die Autoren der Methode TD3 als Basisalgorithmus.


2. Implementierung mit MQL5

Nach der Betrachtung der theoretischen Aspekte der Methode der Supported Policy Optimization gehen wir zu ihrer Implementierung mit MQL5 über. Wir werden unser Modell auf der Grundlage der Expert Advisors aus dem Artikel über die Methode Real-ORL implementieren. Ich möchte daran erinnern, dass das verwendete Basismodell auf der Methode Soft Actor-Critic basiert, die der TD3-Methode ähnelt und von den SPOT-Autoren verwendet wird. Unser Modell wird jedoch durch eine Reihe von Ansätzen ergänzt, die in früheren Artikeln diskutiert wurden.

Zunächst einmal sollten wir beachten, dass die SPOT-Methode die Politik des Agenten auf der Grundlage der Datendichte in der Trainingsmenge reguliert. Diese Regularisierung wird in der Phase des Offline-Trainings der Agentenpolitik angewendet. Sie hat keinen Einfluss auf den Prozess der Interaktion mit der Umwelt. Folglich wird der Trainingsdatensatz, der die Expert Advisors sammelt und testet, ohne Änderungen verwendet. In der Anlage können Sie sich mit ihnen vertraut machen.

Wir können also sofort mit dem Modelltraining Expert Advisor fortfahren. Es sollte jedoch beachtet werden, dass wir vor dem Training der Strategie den Autoencoder der Dichtefunktion des Trainingsdatensatzes trainieren müssen. Daher werden wir den Lernprozess in 2 Phasen unterteilen. Der Autoencoder wird in einem separaten Expert Advisor „...\SPOT\StudyCVAE.mq5“ trainiert.

2.1 Training des Dichtemodells

Bevor wir mit dem Aufbau des Dichtemodell-Trainings EA beginnen, sollten wir zunächst erörtern, was und wie wir trainieren werden. Die Autoren der SPOT-Methode schlugen vor, einen erweiterten Autoencoder zu verwenden, um die Dichte des Trainingsdatensatzes zu untersuchen. Was bedeutet das für die Praxis?

Wir haben bereits über die Eigenschaften des Autoencoders gesprochen, der Daten komprimiert und wiederherstellt. Wir haben auch erwähnt, dass neuronale Netze nur in einer Umgebung stabil arbeiten können, die dem Trainingsdatensatz ähnlich ist. Wenn wir das Modell mit Ausgangsdaten füttern, die weit von der Verteilung des Trainingsdatensatzes entfernt sind, werden die Ergebnisse des Modells daher nahe an Zufallswerten liegen. Dies führt zu einem erheblichen Anstieg des Dekodierungsfehlers der Daten. Wir werden die Kombination dieser Eigenschaften des Autoencoder-Modells nutzen.

Wir trainieren den Autoencoder anhand der Verteilung der Agentenaktionen aus dem Trainingsdatensatz. Während des Trainings des Agenten werden wir die von der aktualisierten Agentenpolitik vorgeschlagenen Aktionen in den Autoencoder einspeisen. Der Fehler bei der Datendekodierung zeigt indirekt den Abstand der prädiktiven Aktionen von der Verteilung des Trainingsdatensatzes an.

Jetzt haben wir also ein gewisses Verständnis für die Funktionalität, und sie passt in die Architektur des Autoencoders. Aber reicht das aus, um das Vorhandensein der Aktion eines Agenten im Trainingsdatensatz zu verstehen? Wir wissen sehr wohl, dass ein und dieselbe Handlung unter verschiedenen Umweltbedingungen zu völlig entgegengesetzten Ergebnissen führen kann. Daher müssen wir den Autoencoder trainieren, um die Verteilungen der Handlungen in verschiedenen Umweltzuständen zu extrahieren. Wir kommen also zu dem Schluss, dass wir ein Paar „State-Action“ (Zustand-Aktion) in den Autoencoder-Eingang einspeisen müssen. In diesem Fall erwarten wir am Ausgang des Autoencoders die Agentenaktion, die dem Eingang zugeführt wurde.

Beachten Sie, dass wir bei der Eingabe des Paares „Zustand-Aktion“ in den Autoencoder erwarten, dass in dessen latentem Zustand komprimierte Informationen über den Zustand und die Aktion enthalten sind. Wenn wir den Autoencoder jedoch darauf trainieren, nur die Aktion zu dekodieren, ist die Wahrscheinlichkeit groß, dass wir den Autoencoder darauf trainieren, Informationen über den Zustand der Umgebung zu ignorieren. Außerdem wird die gesamte Größe des latenten Zustands genutzt, um die gewünschte Aktion zu übertragen. Damit sind wir wieder bei der Kodierung und Dekodierung zustandsloser Aktionen angelangt, was äußerst unerwünscht ist. Daher ist es für uns wichtig, die Aufmerksamkeit des Autoencoders auf beide Komponenten der ursprünglichen Zustandsdaten zu richten. Um dieses Ergebnis zu erreichen, verwenden die Autoren der Methode einen erweiterten Autoencoder, dessen Architektur die Eingabe eines bestimmten Schlüssels zur Dekodierung von Daten vorsieht. Dieser Schlüssel wird zusammen mit der latenten Repräsentation an den Eingang des Decoders geleitet. In unserem Fall werden wir den Zustand der Umgebung als Schlüssel verwenden.

Wir müssen also ein Autoencoder-Modell erstellen, das 3 Tensoren für die Eingabe des Feed-Forward-Durchgangs erhalten soll:

  • Zustand der Umgebung (Encoder-Eingang)
  • Agentenaktion (Encoder-Eingang)
  • Zustand der Umgebung (Decodereingang Taste)

Zuvor haben wir Modelle mit nur Ausgangsdaten von 2 Tensoren erstellt. Nun müssen wir die Ausgangsdaten aus 3 Tensoren implementieren. Dieses Problem kann auf verschiedene Weise gelöst werden.

Zunächst können wir das Zustands-Aktions-Paar zu einem einzigen Tensor kombinieren. Dann wird der Schlüssel der zweite Tensor der Quelldaten sein, und das passt zu dem Modell, das wir zuvor mit 2 Tensoren der Quelldaten verwendet haben. Die Kombination unterschiedlicher Umweltdaten und Maßnahmen des Agenten kann sich jedoch negativ auf die Modellleistung auswirken und unsere Möglichkeiten zur Vorverarbeitung von Umweltrohdaten einschränken.

Die zweite Möglichkeit besteht darin, eine Methode für die Arbeit mit dem Modell mit 3 Tensoren der Originaldaten hinzuzufügen. Dies ist ein arbeitsintensiver Prozess, der zu einer endlosen Erstellung von Methoden für jede spezifische Aufgabe führen kann. Das macht unsere Bibliothek schwerfällig und schwierig zu verstehen und zu pflegen.

In diesem Artikel habe ich mich für die dritte Option entschieden, die mir am einfachsten erscheint. Wir werden separate Encoder- und Decoder-Modelle erstellen. Jeder wird mit 2 Tensoren der Ausgangsdaten arbeiten. Ihre Umsetzung entspricht voll und ganz den Methoden, die wir zuvor entwickelt haben.

Dies ist eine theoretische Entscheidung. Lassen Sie uns nun zur Beschreibung der Architektur unserer Autoencoder-Modelle übergehen. Dies wird in der Methode CreateCVAEDescriptions geschehen. Wir füttern die Methode mit Zeigern zu 2 dynamischen Arrays, in denen wir die Architektur von 2 Modellen, Encoder und Decoder, zusammenstellen werden. Im Hauptteil der Methode werden die empfangenen Zeiger überprüft und gegebenenfalls neue Instanzen von dynamischen Array-Objekten erstellt.

bool CreateCVAEDescriptions(CArrayObj *encoder, CArrayObj *decoder)
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!decoder)
     {
      decoder = new CArrayObj();
      if(!decoder)
         return false;
     }

Zunächst beschreiben wir die Architektur des Encoders. Wir füttern das Modell mit historischen Preisbewegungsdaten und analysierten Indikatorwerten. Die dem Modell zugeführten Eingangsdaten sind roh und unverarbeitet. Daher führen wir als Nächstes die primäre Vorverarbeitung in der Stapeldaten-Normalisierungsschicht durch.

//--- Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Anschließend komprimieren wir die Daten und extrahieren gleichzeitig etablierte Muster mithilfe eines Blocks von Faltungsschichten.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.step = BarDescr;
   int prev_wout = descr.window_out = BarDescr / 2;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = prev_wout;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = prev_wout;
   prev_wout = descr.window_out = 8;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = prev_wout;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Die auf diese Weise erhaltene Einbettung des Umweltzustands wird mit dem Vektor der Aktionen des Agenten kombiniert.

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count;
   descr.step = NActions;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Dann komprimieren wir die Daten mit Hilfe von 2 vollständig verbundenen Schichten.

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 2 * EmbeddingSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Am Ausgang des Encoders erstellen wir eine stochastische latente Repräsentation unter Verwendung der internen Schicht des Variations-Autoencoders.

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronVAEOCL;
   descr.count = EmbeddingSize;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Im Folgenden wird die Architektur des Decoders beschrieben. Die Modelleingabe ist eine latente Darstellung, die vom Encroder erzeugt wird.

//--- Decoder
   decoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

Der resultierende Tensor wird sofort mit dem Umgebungsvektor verknüpft.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = EmbeddingSize;
   descr.window = prev_count;
   descr.step = (HistoryBars * BarDescr);
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

Wir haben unverarbeitete Rohdaten, die den Zustand der Umgebung beschreiben, in den Encoder eingegeben und ihre primäre Verarbeitung in der Batch-Normalisierungsschicht durchgeführt. Im Decoder haben wir jedoch nicht die Möglichkeit, eine solche Normalisierung vorzunehmen. Ich habe beschlossen, die Daten nicht 2 Mal zu normalisieren. Stattdessen werde ich im Trainings- und Betriebsprozess Daten aus dem Encoder nach der Normalisierung übernehmen. Dies wird es uns ermöglichen, den Decoder etwas einfacher zu gestalten und die Datenverarbeitungszeit zu reduzieren.

Als Nächstes verwenden wir vollständig verbundene Schichten, um den Aktionsvektor aus den erhaltenen Ausgangsdaten zu rekonstruieren.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

Nachdem wir die Architektur unseres Autoencoders beschrieben haben, gehen wir dazu über, einen Expert Advisor zu konstruieren, um diesen Autoencoder zu trainieren. Wie oben erwähnt, werden wir 2 Modelle trainieren: Kodierer und Dekodierer.

CNet                 Encoder;
CNet                 Decoder;

In der Initialisierungsmethode OnInit des Programms wird zunächst der Trainingsdatensatz geladen. Vergessen Sie nicht, das Ergebnis der Operation zu überprüfen, da im Falle eines Fehlers beim Laden der Daten keine Daten zum Trainieren des Modells vorhanden sind.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      return INIT_FAILED;
     }

Als Nächstes versuchen wir, bereits trainierte Modelle zu laden und, falls erforderlich, neue Modelle mit zufälligen Parametern zu erstellen.

//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true) ||
      !Decoder.Load(FileName + "Dec.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Init new CVAE");
      CArrayObj *encoder = new CArrayObj();
      CArrayObj *decoder = new CArrayObj();
      if(!CreateCVAEDescriptions(encoder,decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      if(!Encoder.Create(encoder) || !Decoder.Create(decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
         delete encoder;
         delete decoder;
     }

Dann verschieben wir beide Modelle in einen einzigen OpenCL-Kontext, der es uns ermöglicht, Daten zwischen den Modellen auszutauschen, ohne sie in den Speicher des Hauptprogramms zu laden.

   OpenCL = Encoder.GetOpenCL();
   Decoder.SetOpenCL(OpenCL);

Hier führen wir die minimal notwendige Kontrolle über die Architektur der geladenen (oder erstellten) Modelle durch. Achten Sie darauf, die Ergebnisse der Operationen zu überprüfen.

   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(), 
                                                                                          (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   Encoder.getResults(Result);
   int latent_state = Result.Total();
   Decoder.GetLayerOutput(0, Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Decoder doesn't match result of Encoder (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }

Anschließend wird die Erstellung eines Ereignisses initiiert, um den Modellbildungsprozess zu starten. Danach schließen wir die Programminitialisierungsmethode mit dem Ergebnis INIT_SUCCEEDED ab.

   if(!EventChartCustom(ChartID(), 1, 0, 0, "Init"))
     {
      PrintFormat("Error of create study event: %d", GetLastError());
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

In der Deinitialisierungsmethode OnDeinit des Programms werden die trainierten Modelle gespeichert und der Speicher der im Programm erstellten Objekte gelöscht.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   Encoder.Save(FileName + "Enc.nnw", 0, 0, 0, TimeCurrent(), true);
   Decoder.Save(FileName + "Dec.nnw", Decoder.getRecentAverageError(), 0, 0, TimeCurrent(), true);
   delete Result;
   delete OpenCL;
  }

Beachten Sie, dass wir alle Modelle im allgemeinen Terminalkatalog speichern. Damit sind sie sowohl bei der Verwendung von Programmen im Terminal als auch im Strategietester verfügbar.

Das Modelltraining wird mit der Methode Train durchgeführt. Im Hauptteil der Methode werden zunächst die erforderlichen lokalen Variablen angelegt.

//+------------------------------------------------------------------+
//| Train function                                                   |
//+------------------------------------------------------------------+
void Train(void)
  {
   int total_tr = ArraySize(Buffer);
   uint ticks = GetTickCount();
   int bar = (HistoryBars - 1) * BarDescr;

Dann erstellen wir eine Trainingsschleife.

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++)
     {
      int tr = int((MathRand() * MathRand() / MathPow(32767, 2)) * (total_tr));
      int i = int((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2));
      if(i < 0)
         continue;

Beachten Sie, dass wir im Gegensatz zu unserer jüngsten Arbeit hier keine Priorisierung der Trajektorien verwenden. Dies ist ein ganz bewusster und gewollter Schritt. Der Grund dafür ist, dass wir in dieser Phase versuchen, die wahre Datendichte im Trainingsdatensatz zu untersuchen. Die Priorisierung von Trajektorien kann zwar Informationen zugunsten von Trajektorien mit höherer Priorität verzerren. Daher verwenden wir einheitliche Stichproben von Trajektorien und Zuständen.

Nachdem wir die Trajektorie und den Zustand abgetastet haben, füllen wir die Beschreibungspuffer für den Zustand der Umgebung und die Aktionen des Agenten aus dem Trainingsdatensatz.

      State.AssignArray(Buffer[tr].States[i].state);
      Actions.AssignArray(Buffer[tr].States[i].action);
      if(Actions.GetIndex() >= 0)
         Actions.BufferWrite();

Ich erinnere mich, dass wir normalerweise im Konzept der „Umgebungsbeschreibung“ einen Vektor einbeziehen, der den Zustand des Kontos und der offenen Positionen beschreibt. Hier habe ich mich nicht auf den Stand des Kontos konzentriert, da die Richtung der eröffneten oder gehaltenen Position durch den Stand des Marktes bestimmt wird. Der Kontostand wird analysiert, um Risiken zu steuern und den Umfang der Position zu bestimmen. In dieser Phase habe ich beschlossen, mich auf die Untersuchung der Handlungsdichte in einzelnen Marktsituationen zu beschränken und mich nicht auf das Risikomanagementmodell zu konzentrieren.

Nach der Vorbereitung der anfänglichen Datenpuffer führen wir einen Feed-forward-Durchlauf des Autoencoders durch. Wie oben beschrieben, geben wir am Decoder-Eingang zweimal einen Zeiger an den Encoder. In diesem Fall verwenden wir die Modellausgabe als Haupteingangsdatenstrom. Für einen zusätzlichen Strom von Eingabedaten entfernen wir die Ergebnisse aus der Encoder-Batch-Normalisierungsschicht. Achten Sie darauf, den gesamten Prozess zu überwachen.

      if(!Encoder.feedForward(GetPointer(State), 1,false, GetPointer(Actions)) ||
         !Decoder.feedForward(GetPointer(Encoder), -1, GetPointer(Encoder),1))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Während des Trainings des Autoencoders müssen wir die Ergebnisse seiner Arbeit nicht analysieren oder verarbeiten. Wir müssen lediglich die Zielwerte angeben, wofür wir den Aktionsvektor des Agenten verwenden. Dies ist derselbe Vektor, den wir zuvor in den Encoder eingegeben haben. Mit anderen Worten: Wir haben bereits einen Ergebnispuffer vorbereitet und rufen die Backpropagation-Methoden der beiden Autoencoder-Modelle auf.

      if(!Decoder.backProp(GetPointer(Actions), GetPointer(Encoder), 1) ||
         !Encoder.backPropGradient(GetPointer(Actions), GetPointer(Actions)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Beachten Sie, dass der Encoder seine Parameter auf der Grundlage des vom Decoder empfangenen Fehlergradienten aktualisiert. Und wir brauchen keinen separaten Zielpuffer für den Encoder zu erzeugen.

Damit sind die Operationen einer Iteration des Autoencoder-Trainings abgeschlossen. Alles, was wir tun müssen, ist, den Nutzer über den Fortschritt der Operationen zu informieren und mit der nächsten Iteration der Modelltrainingsschleife fortzufahren.

      if(GetTickCount() - ticks > 500)
        {
         string str = StringFormat("%-15s %5.2f%% -> Error %15.8f\n", "Decoder", iter * 100.0 / (double)(Iterations), 
                                                                                    Decoder.getRecentAverageError());
         Comment(str);
         ticks = GetTickCount();
        }
     }

Hier werden nur Fehlerinformationen für den Decoder angezeigt, da der Fehler für den Encoder nicht berechnet wird.

Nach erfolgreichem Abschluss aller Iterationen der Autoencoder-Trainingsschleife löschen wir das Kommentarfeld des Charts und leiten den Prozess der Beendigung des EA ein.

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Decoder", Decoder.getRecentAverageError());
   ExpertRemove();
//---
  }

Den vollständigen Code des Expert Advisors finden Sie im Anhang. Alle in diesem Artikel verwendeten Programme werden dort ebenfalls vorgestellt.

2.2 Schulung der Agentenpolitik

Nach dem Training des Dichtemodells fahren wir mit dem Expert Advisor „...\SPOT\Study.mq5“ für das Training der Agentenpolitik fort. Der Ausbildungsprozess für Agenten ist praktisch unverändert. Sie wurde nur geringfügig ergänzt, was die Regulierung ihrer Verhaltenspolitik betrifft. Die Architektur aller trainierten Modelle wurde ebenfalls ohne Änderungen übernommen. Betrachten wir daher nur einige der Methoden des EA „...\SPOT\Study.mq5“. Den vollständigen Code finden Sie im Anhang.

Unabhängig davon, wie klein die Änderungen im Trainingsalgorithmus des Agenten sind, sind sie mit höher trainierten Autoencoder-Modellen verbunden. Wir müssen sie in das Programm aufnehmen.

STrajectory          Buffer[];
CNet                 Actor;
CNet                 Critic1;
CNet                 Critic2;
CNet                 TargetCritic1;
CNet                 TargetCritic2;
CNet                 Convolution;
CNet                 Encoder;
CNet                 Decoder;

In der Programminitialisierungsmethode OnInit laden wir, wie zuvor, den Trainingsdatensatz und steuern die Ausführung der Operationen.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      return INIT_FAILED;
     }

Dann, noch vor dem Laden der trainierten Modelle, laden wir den Autoencoder. Wenn es nicht möglich ist, Modelle zu laden, informieren wir den Nutzer und brechen die Initialisierungsmethode mit dem Ergebnis INIT_FAILED ab.

//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true) ||
      !Decoder.Load(FileName + "Dec.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Cann't load CVAE");
      return INIT_FAILED;
     }

Es ist zu beachten, dass wir keine neuen Modelle mit zufälligen Parametern erstellen, wenn es keine vortrainierten Modelle gibt. Da nicht trainierte Modelle den Lernprozess nur verzerren und die Ergebnisse eines solchen Trainings unvorhersehbar sind.

Andererseits könnten wir ein Flag hinzufügen und in Ermangelung von trainierten Autoencoder-Modellen die Politik des Agenten trainieren, ohne seine Aktionen zu regulieren, wie es zuvor getan wurde. Wenn ich an einem echten Problem arbeite, würde ich wahrscheinlich so vorgehen. Aber in diesem Fall wollen wir die Arbeit der Regularisierung bewerten. Daher dient die Unterbrechung des Programms als zusätzlicher Kontrollpunkt für den „menschlichen Faktor“.

Als Nächstes laden wir die trainierten Modelle und erstellen bei Bedarf neue Modelle, die mit Zufallsparametern initialisiert werden.

   if(!Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !Critic1.Load(FileName + "Crt1.nnw", temp, temp, temp, dtStudied, true) ||
      !Critic2.Load(FileName + "Crt2.nnw", temp, temp, temp, dtStudied, true) ||
      !TargetCritic1.Load(FileName + "Crt1.nnw", temp, temp, temp, dtStudied, true) ||
      !TargetCritic2.Load(FileName + "Crt2.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Init new models");
      CArrayObj *actor = new CArrayObj();
      CArrayObj *critic = new CArrayObj();
      CArrayObj *convolution = new CArrayObj();
      if(!CreateDescriptions(actor, critic, convolution))
        {
         delete actor;
         delete critic;
         delete convolution;
         return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !Critic1.Create(critic) || !Critic2.Create(critic) ||
         !Convolution.Create(convolution))
        {
         delete actor;
         delete critic;
         delete convolution;
         return INIT_FAILED;
        }
      if(!TargetCritic1.Create(critic) || !TargetCritic2.Create(critic))
        {
         delete actor;
         delete critic;
         delete convolution;
         return INIT_FAILED;
        }
      delete actor;
      delete critic;
      delete convolution;
      //---
      TargetCritic1.WeightsUpdate(GetPointer(Critic1), 1.0f);
      TargetCritic2.WeightsUpdate(GetPointer(Critic2), 1.0f);
      StartTargetIter = StartTargetIteration;
     }
   else
      StartTargetIter = 0;

   if(!Convolution.Load(FileName + "CNN.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Init new Encoder model");
      CArrayObj *actor = new CArrayObj();
      CArrayObj *critic = new CArrayObj();
      CArrayObj *convolution = new CArrayObj();
      if(!CreateDescriptions(actor, critic, convolution))
        {
         delete actor;
         delete critic;
         delete convolution;
         return INIT_FAILED;
        }
      if(!Convolution.Create(convolution))
        {
         delete actor;
         delete critic;
         delete convolution;
         return INIT_FAILED;
        }
      delete actor;
      delete critic;
      delete convolution;
     }

Sobald neue Modelle erfolgreich geladen und/oder initialisiert sind, werden sie in einen einzigen OpenCL-Kontext verschoben. Außerdem deaktivieren wir beim Lernen von Modellen den Modus der Parameteraktualisierung. Das heißt, dass wir in diesem Stadium kein zusätzliches Training des Autoencoders durchführen.

   OpenCL = Actor.GetOpenCL();
   Critic1.SetOpenCL(OpenCL);
   Critic2.SetOpenCL(OpenCL);
   TargetCritic1.SetOpenCL(OpenCL);
   TargetCritic2.SetOpenCL(OpenCL);
   Convolution.SetOpenCL(OpenCL);
   Encoder.SetOpenCL(OpenCL);
   Decoder.SetOpenCL(OpenCL);
   Encoder.TrainMode(false);
   Decoder.TrainMode(false);

Obwohl der Zufallscodierer ebenfalls nicht trainiert ist, haben wir sein Flag des Trainingsmodus‘ nicht geändert. Dafür besteht keine Notwendigkeit. Bei der Methode zur Änderung des Lernmodus werden ungenutzte Puffer nicht entfernt. Daher wird der Speicher nicht gelöscht. Es wird lediglich das Flag geändert, das den Backpropagation-Algorithmus steuert. Wir rufen die Backpropagation-Methode des Encoders im Programm nicht auf. Dies bedeutet, dass die Auswirkung einer Änderung des Trainings-Flags für den Zufallscodierer gegen Null geht. Im Falle eines Autoencoders ist die Situation etwas anders. Wir werden dies später im Rahmen der Trainingsmethode des Models Train berücksichtigen. Kehren wir nun zu der Methode der Initialisierung des EA zurück.

Nachdem wir die Modelle erstellt und in einen einzigen OpenCL-Kontext übertragen haben, führen wir eine minimale Kontrolle über die Übereinstimmung ihrer Architektur mit den im Programm verwendeten Konstanten durch.

Zunächst prüfen wir, ob die Größe der Ergebnisschicht des Akteurs mit der Größe des Aktionsvektors des Agenten übereinstimmt.

   Actor.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the actor does not match the actions count (%d <> %d)", NActions, Result.Total());
      return INIT_FAILED;
     }

Die Größe der Ausgangsdaten des Akteurs muss der Größe des Vektors entsprechen, der den Zustand der Umgebung beschreibt.

   Actor.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Actor doesn't match state description (%d <> %d)", Result.Total(), (HistoryBars * BarDescr));
      return INIT_FAILED;
     }

Wir stellen auch sicher, dass die Größe der latenten Schicht des Akteurs mit der Größe des Quelldatenpuffers des Kritikers übereinstimmt.

   Actor.GetLayerOutput(LatentLayer, Result);
   int latent_state = Result.Total();
   Critic1.GetLayerOutput(0, Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Critic doesn't match latent state Actor (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }

Wir führen ähnliche Prüfungen für die Encoder- und Decoder-Modelle des Autoencoders durch.

   Decoder.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the Decoder does not match the actions count (%d <> %d)", NActions, Result.Total());
      return INIT_FAILED;
     }

   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(), 
                                                                                          (HistoryBars * BarDescr));
      return INIT_FAILED;
     }

   Encoder.getResults(Result);
   latent_state = Result.Total();
   Decoder.GetLayerOutput(0, Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Decoder doesn't match result of Encoder (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }

Damit sind die Arbeiten zur Vorbereitung der Modelle abgeschlossen. Lassen Sie uns den Hilfspuffer initialisieren und ein Ereignis erzeugen, um den Lernprozess zu starten.

   Gradient.BufferInit(AccountDescr, 0);
//---
   if(!EventChartCustom(ChartID(), 1, 0, 0, "Init"))
     {
      PrintFormat("Error of create study event: %d", GetLastError());
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

Dann schließen wir die Methode der Initialisierung des Expert Advisors mit einem positiven Ergebnis ab.

Da wir die Parameter der Autoencoder-Modelle während des Trainingsprozesses nicht ändern werden, brauchen wir sie nach Abschluss des Programms nicht zu speichern. Daher bleibt die Methode OnDeinit unverändert. Den Code dazu finden Sie im Anhang. Als Nächstes gehen wir zum Training der Modelle über. Betrachten wir also die Methode Train.

Der Algorithmus der Actor-Policy-Trainingsmethode ist umfassender und komplexer als die oben beschriebene Dichtemodell-Trainingsmethode. Gehen wir näher darauf ein.

Zu Beginn der Methode bereiten wir mehrere lokale Variablen und Matrizen vor, die wir später beim Training der Modelle verwenden werden. 

void Train(void)
  {
   int total_tr = ArraySize(Buffer);
   uint ticks = GetTickCount();
//---
   int total_states = Buffer[0].Total;
   for(int i = 1; i < total_tr; i++)
      total_states += Buffer[i].Total;
   vector<float> temp, next;
   Convolution.getResults(temp);
   matrix<float> state_embedding = matrix<float>::Zeros(total_states, temp.Size());
   matrix<float> rewards = matrix<float>::Zeros(total_states, NRewards);
   matrix<float> actions = matrix<float>::Zeros(total_states, NActions);

Als Nächstes erstellen wir ein System von Schleifen zur Erzeugung von Einbettungen aller Zustände aus dem Erfahrungswiedergabepuffer. Die äußere Schleife unseres Systems iteriert über die Trajektorien im Trainingsdatensatz. Die verschachtelte Schleife durchläuft die Umgebungszustände, die der Agent beim Durchlaufen der Flugbahn besucht hat.

   int state = 0;
   for(int tr = 0; tr < total_tr; tr++)
     {
      for(int st = 0; st < Buffer[tr].Total; st++)
        {
         State.AssignArray(Buffer[tr].States[st].state);

Im Hauptteil des Schleifensystems laden wir einen Vektor, der einen bestimmten Zustand der Umgebung aus dem Trainingsbeispiel beschreibt. Ergänzen Sie ihn durch eine Beschreibung des Kontostands und der offenen Positionen.

         float PrevBalance = Buffer[tr].States[MathMax(st - 1, 0)].account[0];
         float PrevEquity = Buffer[tr].States[MathMax(st - 1, 0)].account[1];
         State.Add((Buffer[tr].States[st].account[0] - PrevBalance) / PrevBalance);
         State.Add(Buffer[tr].States[st].account[1] / PrevBalance);
         State.Add((Buffer[tr].States[st].account[1] - PrevEquity) / PrevEquity);
         State.Add(Buffer[tr].States[st].account[2]);
         State.Add(Buffer[tr].States[st].account[3]);
         State.Add(Buffer[tr].States[st].account[4] / PrevBalance);
         State.Add(Buffer[tr].States[st].account[5] / PrevBalance);
         State.Add(Buffer[tr].States[st].account[6] / PrevBalance);

 Hier fügen wir die Welleneigenschaft (harmonics) des Zeitstempels in den Puffer ein.

         double x = (double)Buffer[tr].States[st].account[7] / (double)(D'2024.01.01' - D'2023.01.01');
         State.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = (double)Buffer[tr].States[st].account[7] / (double)PeriodSeconds(PERIOD_MN1);
         State.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
         x = (double)Buffer[tr].States[st].account[7] / (double)PeriodSeconds(PERIOD_W1);
         State.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = (double)Buffer[tr].States[st].account[7] / (double)PeriodSeconds(PERIOD_D1);
         State.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         State.AddArray(vector<float>::Zeros(NActions));

Um die Zustände unabhängig von den Aktionen des Agenten zu bewerten, füllen wir den Rest des Puffers mit Nullwerten.

Nach erfolgreicher Befüllung des Quelldatenpuffers rufen wir die Feed-Forward-Pass-Methode des Zufallscodierers auf.

         if(!Convolution.feedForward((CBufferFloat *)GetPointer(State), 1, false, (CBufferFloat *)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            ExpertRemove();
            return;
           }

Die Ergebnisse seiner Arbeit speichern wir in der Einbettungsmatrix.

         Convolution.getResults(temp);
         if(!state_embedding.Row(temp, state))
            continue;

Gleichzeitig speichern wir abgeschlossene Aktionen und Belohnungen, die wir bei späteren Übergängen erhalten.

         if(!temp.Assign(Buffer[tr].States[st].action) ||
            !actions.Row(temp, state))
            continue;
         if(!temp.Assign(Buffer[tr].States[st].rewards) ||
            !next.Assign(Buffer[tr].States[st + 1].rewards) ||
            !rewards.Row(temp - next * DiscFactor, state))
            continue;

Nachdem alle Entitäten erfolgreich zu den lokalen Matrizen hinzugefügt wurden, wird der Zähler der verarbeiteten Zustände erhöht. Wir informieren den Nutzer über den Fortschritt des Zustandseinbettungsprozesses und gehen zur nächsten Iteration des Schleifensystems über.

         state++;
         if(GetTickCount() - ticks > 500)
           {
            string str = StringFormat("%-15s %6.2f%%", "Embedding ", state * 100.0 / (double)(total_states));
            Comment(str);
            ticks = GetTickCount();
           }
        }
     }

Nachdem alle Iterationen des Schleifensystems erfolgreich abgeschlossen wurden, passen wir die Größe der lokalen Matrizen gegebenenfalls an die tatsächliche Größe der verwendeten Daten an.

   if(state != total_states)
     {
      rewards.Resize(state, NRewards);
      actions.Resize(state, NActions);
      state_embedding.Reshape(state, state_embedding.Cols());
      total_states = state;
     }

Dann gehen wir zur nächsten Phase der Vorbereitungsarbeiten über, in der wir eine Reihe lokaler Variablen vorbereiten und die Priorität der Stichproben von Trajektorien aus dem Trainingsdatensatz im Modelltrainingsprozess festlegen.

   vector<float> rewards1, rewards2, target_reward;
   STarget target;
   int bar = (HistoryBars - 1) * BarDescr;
//---
   vector<float> probability = GetProbTrajectories(Buffer, 0.9);

Damit sind die vorbereitenden Arbeiten abgeschlossen, und wir können direkt mit dem Training der Modelle beginnen. Zu diesem Zweck erstellen wir eine Trainingsschleife mit der in den externen Parametern des EA angegebenen Anzahl von Iterationen.

Im Hauptteil der Schleife wird die Flugbahn unter Berücksichtigung der Prioritäten abgetastet und ein zufälliger Zustand auf ihr ausgewählt.

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++)
     {
      int tr = SampleTrajectory(probability);
      int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2));
      if(i < 0)
        {
         iter--;
         continue;
        }

Als Nächstes müssen wir gemäß der SAC-Methode die erwartete Belohnung bis zum Ende der Episode berechnen. Zu diesem Zweck verwenden wir Zielmodelle der Kritiker (Critics). Wir werden diese Operationen jedoch nur mit vorher trainierten Modellen durchführen. Bevor wir mit der Arbeit beginnen, überprüfen wir daher, ob die erforderliche Mindestanzahl von Trainingsiterationen abgeschlossen ist.

      target_reward = vector<float>::Zeros(NRewards);
      //--- Target
      if(iter >= StartTargetIter)
        {
         State.AssignArray(Buffer[tr].States[i + 1].state);

Nach erfolgreicher Übergabe der Kontrolle füllen wir den anfänglichen Datenpuffer mit einer Beschreibung des späteren Zustands der Umgebung.

Getrennt davon füllen wir den Puffer mit der Beschreibung des Kontostatus und der offenen Positionen.

         float PrevBalance = Buffer[tr].States[i].account[0];
         float PrevEquity = Buffer[tr].States[i].account[1];
         Account.Clear();
         Account.Add((Buffer[tr].States[i + 1].account[0] - PrevBalance) / PrevBalance);
         Account.Add(Buffer[tr].States[i + 1].account[1] / PrevBalance);
         Account.Add((Buffer[tr].States[i + 1].account[1] - PrevEquity) / PrevEquity);
         Account.Add(Buffer[tr].States[i + 1].account[2]);
         Account.Add(Buffer[tr].States[i + 1].account[3]);
         Account.Add(Buffer[tr].States[i + 1].account[4] / PrevBalance);
         Account.Add(Buffer[tr].States[i + 1].account[5] / PrevBalance);
         Account.Add(Buffer[tr].States[i + 1].account[6] / PrevBalance);

Außerdem fügen wir demselben Puffer die Zeitstempel-Welleneigenschaft hinzu.

         double x = (double)Buffer[tr].States[i + 1].account[7] / (double)(D'2024.01.01' - D'2023.01.01');
         Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = (double)Buffer[tr].States[i + 1].account[7] / (double)PeriodSeconds(PERIOD_MN1);
         Account.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
         x = (double)Buffer[tr].States[i + 1].account[7] / (double)PeriodSeconds(PERIOD_W1);
         Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = (double)Buffer[tr].States[i + 1].account[7] / (double)PeriodSeconds(PERIOD_D1);
         Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         //---
         if(Account.GetIndex() >= 0)
            Account.BufferWrite();

Die gesammelten Daten reichen aus, um den Feedforward-Durchgang des Akteurs zu vervollständigen.

         if(!Actor.feedForward(GetPointer(State), 1, false, GetPointer(Account)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }

Beachten Sie, dass wir die Feed-Forward-Durchgangs-Methode für das Actor-Modell aufrufen, das mit dem folgenden Zustand der Umgebung trainiert wird. Dadurch wird eine Aktion des Akteurs (Actor) gemäß der aktualisierten Richtlinie erzeugt. So bewerten die Zielkritiker die erwartete Belohnung durch die aktualisierte Politik bis zum Ende der Episode.

         if(!TargetCritic1.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)) ||
            !TargetCritic2.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }

Wir verwenden die Mindestpunktzahl, die wir von den 2 Zielkritikern erhalten haben, als erwarteten Wert für die folgenden Operationen.

         TargetCritic1.getResults(rewards1);
         TargetCritic2.getResults(rewards2);
         target_reward.Assign(Buffer[tr].States[i + 1].rewards);
         if(rewards1.Sum() <= rewards2.Sum())
            target_reward = rewards1 - target_reward;
         else
            target_reward = rewards2 - target_reward;
         target_reward *= DiscFactor;
         target_reward[NRewards - 1] = EntropyLatentState(Actor);
        }

Im nächsten Schritt schulen wir unsere Kritiker. Um die Korrektheit ihrer Einschätzungen zu gewährleisten, basiert das Training auf einem Vergleich von tatsächlichen Aktionen und Belohnungen aus dem Trainingsdatensatz. Ich möchte Sie daran erinnern, dass wir in unserem Modell den Akteur verwenden, um den Zustand der Umgebung vorzuverarbeiten. Daher füllen wir wie zuvor die anfänglichen Datenpuffer mit einer Beschreibung des abgetasteten Zustands der Umgebung.

      //--- Q-function study
      State.AssignArray(Buffer[tr].States[i].state);

Wir füllen den Puffer mit der Beschreibung des Kontostands und der offenen Positionen.

      float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0];
      float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1];
      Account.Clear();
      Account.Add((Buffer[tr].States[i].account[0] - PrevBalance) / PrevBalance);
      Account.Add(Buffer[tr].States[i].account[1] / PrevBalance);
      Account.Add((Buffer[tr].States[i].account[1] - PrevEquity) / PrevEquity);
      Account.Add(Buffer[tr].States[i].account[2]);
      Account.Add(Buffer[tr].States[i].account[3]);
      Account.Add(Buffer[tr].States[i].account[4] / PrevBalance);
      Account.Add(Buffer[tr].States[i].account[5] / PrevBalance);
      Account.Add(Buffer[tr].States[i].account[6] / PrevBalance);

Zeitstempel-Status hinzufügen.

      double x = (double)Buffer[tr].States[i].account[7] / (double)(D'2024.01.01' - D'2023.01.01');
      Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
      x = (double)Buffer[tr].States[i].account[7] / (double)PeriodSeconds(PERIOD_MN1);
      Account.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
      x = (double)Buffer[tr].States[i].account[7] / (double)PeriodSeconds(PERIOD_W1);
      Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
      x = (double)Buffer[tr].States[i].account[7] / (double)PeriodSeconds(PERIOD_D1);
      Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
      if(Account.GetIndex() >= 0)
         Account.BufferWrite();

Führen Sie als nächstes den Feed-Forward-Durchgang für den Actor aus.

      if(!Actor.feedForward(GetPointer(State), 1, false, GetPointer(Account)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Bitte beachten Sie, dass wir in diesem Stadium über einen vollständigen Datensatz verfügen, um einen Feedforward-Durchlauf des Autoencoders durchzuführen. Wir verschieben nicht auf später, was jetzt getan werden kann. Daher nennen wir die Feedforward-Methoden Encoder und Decoder.

      if(!Encoder.feedForward((CBufferFloat *)GetPointer(State), 1, false, (CNet *)GetPointer(Actor)) ||
         !Decoder.feedForward(GetPointer(Encoder), -1, GetPointer(Encoder), 1))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Wie bereits erwähnt, werden die Kritiker anhand der tatsächlichen Handlungen des Akteurs aus dem Trainingsdatensatz trainiert. Wir laden sie also in den Datenpuffer und rufen die Feed-Forward-Methoden der beiden Kritiker auf.

      Actions.AssignArray(Buffer[tr].States[i].action);
      if(Actions.GetIndex() >= 0)
         Actions.BufferWrite();
      //---
      if(!Critic1.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actions)) ||
         !Critic2.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actions)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Als Nächstes ergänzen wir den aktuellen Zustandsbeschreibungspuffer mit den erforderlichen Daten und führen die Einbettung des analysierten Zustands mit einem Zufallscodierer durch.

      if(!State.AddArray(GetPointer(Account)) || !State.AddArray(vector<float>::Zeros(NActions)) ||
         !Convolution.feedForward((CBufferFloat *)GetPointer(State), 1, false, (CBufferFloat *)NULL))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Auf der Grundlage der Einbettungsergebnisse erstellen wir Zielwerte für Akteur und Kritiker.

      Convolution.getResults(temp);
      target = GetTargets(Quant, temp, state_embedding, rewards, actions);

Danach aktualisieren wir die Parameter unserer Kritiker. Wie wir bereits gesehen haben, wird die Methode CAGrad zur Anpassung des Gradientenvektors verwendet, um die Modellkonvergenz zu verbessern.

      Critic1.getResults(rewards1);
      Result.AssignArray(CAGrad(target.rewards + target_reward - rewards1) + rewards1);
      if(!Critic1.backProp(Result, GetPointer(Actions), GetPointer(Gradient)) ||
         !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient), LatentLayer))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

      Critic2.getResults(rewards2);
      Result.AssignArray(CAGrad(target.rewards + target_reward - rewards2) + rewards2);
      if(!Critic2.backProp(Result, GetPointer(Actions), GetPointer(Gradient)) ||
         !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient), LatentLayer))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Nachdem wir die kritischen Modelle erfolgreich aktualisiert haben, gehen wir zur Optimierung der Akteurspolitik über. Dieser Prozess kann in 3 Blöcke unterteilt werden. Im ersten Block passen wir die Strategie des Agenten so an, dass er eine bestimmte Aktion wiederholt, die aus den Aktionen im Trainingsdatensatz, die in ähnlichen Zuständen durchgeführt wurden, gesammelt und nach der erhaltenen Belohnung gewichtet wurde.

      //--- Policy study
      Actor.getResults(rewards1);
      Result.AssignArray(CAGrad(target.actions - rewards1) + rewards1);
      if(!Actor.backProp(Result, GetPointer(Account), GetPointer(Gradient)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

In der zweiten Stufe verwenden wir die Ergebnisse des Autoencoders und überprüfen die Abweichung der generierten Agentenaktionen von den Trainingsdaten. Wenn die Dekodierfehlerschwelle der Aktion überschritten wird, versuchen wir, die Politik des Akteurs auf die Verteilung des Trainingsdatensatzes zurückzuführen. Dazu führen wir den Backpropagation-Durchgang des Autoencoders durch, und der Kodierungsfehler wird als Fehlergradient direkt an den Actor weitergegeben, ähnlich wie bei der Übergabe des Fehlergradienten des Kritikers. Um diese Operation sicher zu implementieren, haben wir den Lernmodus im Encoder und Decoder während der Programminitialisierung deaktiviert.

      Decoder.getResults(rewards2);
      if(rewards2.Loss(rewards1, LOSS_MSE) > MeanCVAEError)
        {
         Actions.AssignArray(rewards1);
         if(!Decoder.backProp(GetPointer(Actions), GetPointer(Encoder), 1) ||
            !Encoder.backPropGradient((CNet*)GetPointer(Actor)) ||
            !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }
        }

In der nächsten Phase des Trainings der Politik des Akteurs überprüfen wir die Zuverlässigkeit der Prognosen unserer Kritiker. Wenn die Vorhersagen zuverlässig genug sind, passen wir die Politik des Akteurs in Richtung der wahrscheinlichsten maximalen Belohnung an. In dieser Phase deaktivieren wir auch den Aktualisierungsmodus der kritischen Parameter, um den Effekt der gegenseitigen Anpassung der Modelle zu vermeiden. 

      CNet *critic = NULL;
      if(Critic1.getRecentAverageError() <= Critic2.getRecentAverageError())
         critic = GetPointer(Critic1);
      else
         critic = GetPointer(Critic2);
      if(MathAbs(critic.getRecentAverageError()) <= MaxErrorActorStudy)
        {
         if(!critic.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }
         critic.getResults(rewards1);
         Result.AssignArray(CAGrad(target.rewards + target_reward - rewards1) + rewards1);
         critic.TrainMode(false);
         if(!critic.backProp(Result, GetPointer(Actor)) ||
            !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            critic.TrainMode(true);
            break;
           }
         critic.TrainMode(true);
        }

Als Nächstes müssen wir die kritischen Zielmodelle aktualisieren.

      //--- Update Target Nets
      if(iter >= StartTargetIter)
        {
         TargetCritic1.WeightsUpdate(GetPointer(Critic1), Tau);
         TargetCritic2.WeightsUpdate(GetPointer(Critic2), Tau);
        }
      else
        {
         TargetCritic1.WeightsUpdate(GetPointer(Critic1), 1);
         TargetCritic2.WeightsUpdate(GetPointer(Critic2), 1);
        }

Wir müssen den Nutzer auch über den Fortschritt des Lernprozesses informieren.

      if(GetTickCount() - ticks > 500)
        {
         string str = StringFormat("%-15s %5.2f%% -> Error %15.8f\n", "Critic1", iter * 100.0 / (double)(Iterations),
                                                                                    Critic1.getRecentAverageError());
         str += StringFormat("%-15s %5.2f%% -> Error %15.8f\n", "Critic2", iter * 100.0 / (double)(Iterations), 
                                                                                    Critic2.getRecentAverageError());
         str += StringFormat("%-14s %5.2f%% -> Error %15.8f\n", "Actor", iter * 100.0 / (double)(Iterations), 
                                                                                      Actor.getRecentAverageError());
         Comment(str);
         ticks = GetTickCount();
        }
     }

Nachdem alle Iterationen des Modelltrainingszyklus abgeschlossen sind, wird das Kommentarfeld im Chart gelöscht. Außerdem geben wir Informationen über die Ergebnisse des Modelltrainings in das Protokoll ein und leiten die Beendigung von EA ein.

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Critic1", Critic1.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Critic2", Critic2.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError());
   ExpertRemove();
//---
  }

Damit ist unsere Arbeit an der Implementierung der unterstützten Policy-Optimierungsmethode mit MQL5 abgeschlossen. Im Anhang finden Sie den vollständigen Code aller im Artikel verwendeten Programme. Nun kommen wir zum nächsten Teil unseres Artikels, in dem wir die Ergebnisse anhand eines praktischen Falles überprüfen werden.


3. Test

Wir haben die SPOT-Methode (Supported Policy OpTimization) mit MQL5-Tools implementiert. Nun ist es an der Zeit, die Ergebnisse unserer Arbeit in der Praxis zu testen. Wie immer möchte ich Sie darauf aufmerksam machen, dass diese Arbeit meine eigene Sicht der von den Autoren der Methode vorgeschlagenen Ansätze darstellt. Außerdem überlagern sie sich mit Entwicklungen, die zuvor mit anderen Methoden erstellt wurden. Das Ergebnis war ein Modell, das aus einem Konglomerat verschiedener Ideen bestand, die durch meine Vision des Prozesses gesammelt wurden. Folglich können alle möglicherweise festgestellten Unzulänglichkeiten nicht vollständig auf eine der verwendeten Methoden projiziert werden.

Wie zuvor werden die Modelle anhand historischer Daten für EURUSD H1 trainiert und getestet. Alle Indikatoren werden mit Standardparametern verwendet. Das Modell wird mit den Daten der ersten 7 Monate des Jahres 2023 trainiert. Um das trainierte Modell zu testen, verwenden wir historische Daten vom August 2023.

Wie bereits erwähnt, werden die Modelle der Interaktion mit der Umwelt unverändert übernommen. Daher können wir für die erste Stufe des Trainings den im Rahmen des Artikels über Real-ORL gesammelten Trainingsdatensatz verwenden, der als Spender der Modelle diente. Ich habe den Trainingsdatensatz kopiert und als „SPOT.bd“ gespeichert.

In der ersten Phase trainieren wir den Autoencoder. Der Trainingsdatensatz umfasst 500 Trajektorien mit jeweils 3591 Umweltzuständen. Insgesamt handelt es sich um fast 1,8 Millionen „State-Action-Reward“-Sets. Ich habe 5 Autoencoder-Trainingsschleifen mit jeweils 0,5 Millionen Iterationen durchgeführt, was 40 % mehr ist als die Größe des Trainingsdatensatzes.

Nach dem anfänglichen Training des Autoencoders beginnen wir mit dem Training der Modelle in der EA „...\SPOT\Study.mq5“. Beachten Sie, dass die Dauer des Modelltrainings die Autoencoder-Trainingszeit deutlich übersteigt.

Es sollte auch beachtet werden, dass die Politik des Agenten innerhalb des Trainingsdatensatzes bleibt und keine Hoffnung besteht, Ergebnisse zu erhalten, die besser sind als die Pässe im Trainingsdatensatz. Um optimale Strategien zu erhalten, müssen wir daher den Erfahrungswiedergabepuffer iterativ aktualisieren und die Modelle, einschließlich des Autoencoders, aktualisieren.

Daher führe ich parallel zum Modelltraining die Optimierung des EA „ResearchExORL.mq5“ im Strategietester durch , um Strategien jenseits des Trainingssets zu untersuchen.

Nach Abschluss der Modelltrainingsschleife führen wir die Optimierung mit 200 Durchgängen des EA „Research.mq5“ durch, der die Umgebung in einer Umgebung mit gelernten Akteurspolitiken erkundet.

Auf der Grundlage des aktualisierten Trainingssatzes wiederholen wir das Autoencoder-Training für 0,5 Millionen Iterationen. Dann führen wir das nachgelagerte Training der Politik des Akteurs durch.

In mehreren Trainingsschleifen ist es mir gelungen, die Akteurs-Politik zu trainieren, die in der Lage ist, während des historischen Trainings- und Testzeitraums Gewinne zu erzielen. Die Modellergebnisse für August 2023 werden im Folgenden dargestellt.

Testergebnisse

Testergebnisse

Wie Sie aus den dargestellten Daten ersehen können, hat das Modell während des Testmonats der Strategie 124 Positionen gehandelt (92 Verkaufs- und 32 Kaufpositionen). Davon wurden fast 47 % mit Gewinn abgeschlossen. Es ist bemerkenswert, dass der Anteil der profitablen Verkaufs- und Kaufpositionen nahe beieinander liegt (50 % bzw. 46 %). Außerdem ist der durchschnittliche Gewinn um 25 % höher als der durchschnittliche Verlust. Der größte Gewinn ist fast 2 Mal größer als der größte Verlust. Im Allgemeinen lag der Gewinnfaktor auf der Grundlage der Handelsergebnisse bei 1,15.


Schlussfolgerung

In diesem Artikel haben wir die Methode des Supported Policy OpTimization (SPOT) kennengelernt, die eine erfolgreiche Lösung für das Problem des Offline-Lernens unter den Bedingungen eines begrenzten Trainingsdatensatzes darstellt. Seine Fähigkeit zur Anpassung der Politik bei geschätzter Verhaltensstrategiedichte zeigt eine überlegene Leistung bei Standard-Testszenarien. SPOT lässt sich leicht in bestehende Offline-RL-Algorithmen integrieren und bietet so Flexibilität für den Einsatz in unterschiedlichen Kontexten. Der modulare Aufbau ermöglicht den Einsatz bei unterschiedlichen Lernansätzen.

Ein einzigartiges Merkmal von SPOT ist die Verwendung einer Regularisierung, die auf einer expliziten Schätzung der Dichte der Trainingsdaten basiert. Dies ermöglicht eine genaue Kontrolle der zulässigen Maßnahmen und verhindert effektiv eine Extrapolation über den Trainingsdatensatz hinaus.

Im praktischen Teil haben wir unsere Vision der vorgeschlagenen Ansätze mit MQL5 umgesetzt. Anhand der Testergebnisse können wir eine Schlussfolgerung über die Wirksamkeit dieser Methode ziehen. Während des Trainingsprozesses können wir auch die Stabilität des Prozesses feststellen. Auf der Grundlage der Trainingsergebnisse ist es uns gelungen, eine profitable Strategie für das Verhalten des Akteurs zu finden.

Es ist jedoch zu beachten, dass die Beibehaltung der Politik des Akteurs innerhalb des Trainingsdatensatzes die Anregung der Forschung außerhalb dieses Datensatzes einschränkt. Einerseits wird dadurch der Lernprozess stabiler. Andererseits schränkt es die Möglichkeiten zur Erkundung unbekannter Teilräume der Umgebung ein. Daraus lässt sich schließen, dass diese Methode am effektivsten eingesetzt werden kann, wenn der Trainingsdatensatz suboptimale Pässe aufweist.

Gleichzeitig kann man versuchen, die Methode umzukehren und die Untersuchung von Aktionen außerhalb des Trainingsdatensatzes anzuregen, um die Erkundung der Umgebung zu fördern. Aber das ist ein Thema für zukünftige Untersuchungen.


Referenzen

  • Supported Policy Optimization for Offline Reinforcement Learning
  • Neuronale Netze leicht gemacht (Teil 67): Nutzung früherer Erfahrungen zur Lösung neuer Probleme

  • Programme, die im diesem Artikel verwendet werden

    # Name Typ Beschreibung
    1 Research.mq5 EA Beispielsammlung EA
    2 ResearchRealORL.mq5
    EA
    EA zum Sammeln von Beispielen mit der Real-ORL-Methode
    3 ResearchExORL.mq5 EA EA für die Sammlung von Beispielen mit der ExORL-Methode
    4 Study.mq5  EA Trainings-EA des Agenten
    5 StudyCVAE.mq5 EA
    Autoencoder, Lernender Expert Advisor
    6 Test.mq5 EA Testmodel des EA
    7 Trajectory.mqh Klassenbibliothek Struktur der Systemzustandsbeschreibung
    8 NeuroNet.mqh Klassenbibliothek Eine Bibliothek von Klassen zur Erstellung eines neuronalen Netzes
    9 NeuroNet.cl Code Base Die Bibliothek des Programmcodes von OpenCL


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

    Beigefügte Dateien |
    MQL5.zip (653.77 KB)
    MQL5 beherrschen, vom Anfänger zum Profi (Teil II): Grundlegende Datentypen und die Verwendung von Variablen MQL5 beherrschen, vom Anfänger zum Profi (Teil II): Grundlegende Datentypen und die Verwendung von Variablen
    Dies ist eine Fortsetzung der Serie für Anfänger. In diesem Artikel werden wir uns ansehen, wie man Konstanten und Variablen erstellt, Daten, Farben und andere nützliche Daten schreibt. Wir werden lernen, wie man Enumerationen (Aufzählungen) wie Wochentage oder Linienstile (durchgezogen, gepunktet usw.) erstellt. Variablen und Ausdrücke sind die Grundlage der Programmierung. Sie sind definitiv in 99 % der Programme vorhanden, daher ist es wichtig, sie zu verstehen. Wenn Sie also neu in der Programmierung sind, kann dieser Artikel sehr nützlich für Sie sein. Erforderliche Programmierkenntnisse: sehr einfach, innerhalb der Grenzen meines vorherigen Artikels (siehe den Link am Anfang).
    Die Gruppenmethode der Datenverarbeitung: Implementierung des Kombinatorischen Algorithmus in MQL5 Die Gruppenmethode der Datenverarbeitung: Implementierung des Kombinatorischen Algorithmus in MQL5
    In diesem Artikel setzen wir unsere Untersuchung der Algorithmenfamilie Group Method of Data Handling mit der Implementierung des Kombinatorischen Algorithmus und seiner verfeinerten Variante, dem Kombinatorischen Selektiven Algorithmus in MQL5 fort.
    Neuronale Netze leicht gemacht (Teil 70): Operatoren der Closed-Form Policy Improvement (CFPI) Neuronale Netze leicht gemacht (Teil 70): Operatoren der Closed-Form Policy Improvement (CFPI)
    In diesem Artikel werden wir uns mit einem Algorithmus vertraut machen, der geschlossene Operatoren zur Verbesserung der Politik verwendet, um die Aktionen des Agenten im Offline-Modus zu optimieren.
    MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 17): Handel mit mehreren Währungen MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 17): Handel mit mehreren Währungen
    Der Handel mit mehreren Währungen ist nicht standardmäßig verfügbar, wenn ein Expertenberater über den Assistenten zusammengestellt wird. Wir untersuchen 2 mögliche Hacks, die Händler machen können, wenn sie ihre Ideen mit mehr als einem Symbol gleichzeitig testen wollen.