English Русский 中文 Español 日本語 Português
preview
Neuronale Netze im Handel: Ein hybrider Handelsrahmen mit prädiktiver Kodierung (letzter Teil)

Neuronale Netze im Handel: Ein hybrider Handelsrahmen mit prädiktiver Kodierung (letzter Teil)

MetaTrader 5Handelssysteme |
83 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Einführung

Im vorangegangenen Artikel haben wir die theoretischen Aspekte des hybriden Handelssystems StockFormer eingehend untersucht, das prädiktive Kodierungs- und Verstärkungslernalgorithmen kombiniert, um Markttrends und die Dynamik von Finanzanlagen vorherzusagen. StockFormer ist ein hybrides Framework, das mehrere Schlüsseltechnologien und -ansätze zusammenführt, um komplexe Herausforderungen auf den Finanzmärkten zu bewältigen. Sein Hauptmerkmal ist die Verwendung von drei modifizierten Transformatorenzweigen, die jeweils für die Erfassung verschiedener Aspekte der Marktdynamik zuständig sind. Der erste Zweig extrahiert verborgene Interdependenzen zwischen Vermögenswerten, während der zweite und dritte Zweig sich auf kurz- und langfristige Prognosen konzentrieren, sodass das System sowohl aktuelle als auch zukünftige Markttrends berücksichtigen kann.

Die Integration dieser Zweige wird durch eine Kaskade von Aufmerksamkeitsmechanismen erreicht, die die Fähigkeit des Modells verbessern, aus Blöcken mit mehreren Köpfen zu lernen, und die Verarbeitung und Erkennung latenter Muster in den Daten verbessern. So kann das System nicht nur Trends auf der Grundlage historischer Daten analysieren und vorhersagen, sondern auch dynamische Beziehungen zwischen verschiedenen Vermögenswerten berücksichtigen. Dies ist besonders wichtig für die Entwicklung von Handelsstrategien, die sich an schnell wechselnde Marktbedingungen anpassen können.

Die originale Visualisierung des Systems von StockFormer finden Sie unten.

Im praktischen Teil des vorangegangenen Artikels haben wir die Algorithmen des Moduls Diversified Multi-Head Attention (DMH-Attn) implementiert, das als Grundlage für die Erweiterung des Standard-Aufmerksamkeitsmechanismus im Transformer-Modell dient. DMH-Attn verbessert die Effizienz bei der Erkennung von verschiedenen Mustern und Abhängigkeiten in Finanzzeitreihen erheblich, was besonders wertvoll ist, wenn man mit verrauschten und hochvolatilen Daten arbeitet.

In diesem Artikel werden wir die Arbeit fortsetzen, indem wir uns auf die Architektur der verschiedenen Teile des Modells und die Mechanismen ihrer Interaktion bei der Schaffung eines einheitlichen Zustandsraums konzentrieren. Darüber hinaus werden wir den Prozess der Ausbildung der Handelsstrategie des Entscheidungsagenten untersuchen.



Prädiktive Kodierungsmodelle

Wir beginnen mit prädiktiven Kodierungsmodellen. Die Autoren von StockFormer schlugen vor, drei Prognosemodelle zu verwenden. Die eine dient der Ermittlung von Abhängigkeiten innerhalb der Daten, die die Dynamik der analysierten Finanzanlagen beschreiben. Die beiden anderen werden trainiert, um die kommenden Bewegungen der zu untersuchenden multimodalen Zeitreihen zu prognostizieren, jeweils mit einem anderen Planungshorizont.

Alle drei Modelle basieren auf der Architektur von EncoderDecoder Transformer und verwenden modifizierte DMH-Attn-Module. In unserer Implementierung werden der Encoder und der Decoder als separate Modelle erstellt.


Modelle für die Suche nach Abhängigkeiten


Die Architektur der Modelle für die Suche nach Abhängigkeiten für Zeitreihen von Finanzanlagen wird in der Methode CreateRelationDescriptions definiert.

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

Die Parameter der Methode enthalten Zeiger auf zwei dynamische Arrays, in die wir die Architekturbeschreibungen des Encoders und des Decoders übertragen müssen. Innerhalb der Methode überprüfen wir die Gültigkeit der empfangenen Zeiger und erstellen gegebenenfalls neue Instanzen der dynamischen Array-Objekte.

Für die erste Schicht des Encoders verwenden wir eine voll verknüpfte Schicht, die groß genug ist, um alle Tensordaten aus der Roheingabe aufzunehmen.

Es sei daran erinnert, dass der Encoder historische Daten über die gesamte Tiefe des analysierten Verlaufs empfängt.

//--- Encoder
   encoder.Clear();
//---
   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;
     }

Die Rohdaten stammen aus dem Handelsterminal. Wie zu erwarten, gehören die multimodalen Zeitreihendaten, die Indikatoren und möglicherweise mehrere Finanzinstrumente umfassen, zu unterschiedlichen Verteilungen. Daher werden die Eingabedaten zunächst mit einer Batch-Normalisierungsschicht vorverarbeitet.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Die Autoren von StockFormer schlagen vor, bis zu 50 % der Eingabedaten während des Trainings der Modelle für die Suche nach Abhängigkeiten zufällig zu maskieren. Das Modell muss die maskierten Daten auf der Grundlage der verbleibenden Informationen rekonstruieren. In unserem Encoder wird diese Maskierung von einer Dropout-Schicht übernommen.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronDropoutOCL;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   descr.probability = 0.5f;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Anschließend fügen wir eine erlernbare Positionskodierungsschicht hinzu.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronLearnabledPE;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Der Encoder schließt mit einem diversifizierten Mehrkopf-Aufmerksamkeitsmodul ab, das aus drei verschachtelten Schichten besteht.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronDMHAttention;
   descr.window = BarDescr;
   descr.window_out = 32;
   descr.count = HistoryBars;
   descr.step = 4;               //Heads
   descr.layers = 3;             //Layers
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Die Eingabe für den Decoder im Modell der Abhängigkeitssuche ist die gleiche multimodale Zeitreihe mit identischer Maskierung und Positionskodierung. Daher sind die meisten Encoder- und Decoder-Architekturen identisch. Der Hauptunterschied besteht darin, dass wir das diversifizierte Mehrkopf-Aufmerksamkeits-Modul durch ein Kreuzaufmerksamkeits-Modul ersetzen, das die Datenströme von Decoder und Encoder aufeinander abstimmt.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronCrossDMHAttention;
//--- Windows
     {
      int temp[] = {BarDescr, BarDescr};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.window_out = 32;
//--- Units
     {
      int temp[] = {prev_count/descr.windows[0], HistoryBars};
      if(ArrayCopy(descr.units, temp) < (int)temp.Size())
         return false;
     }
   descr.step = 4;               //Heads
   descr.layers = 3;             //Layers
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

Da die Ausgabe des Decoders mit den ursprünglichen Eingabedaten verglichen wird, schließen wir das Modell mit einer umgekehrten Normalisierungsschicht ab.

//--- layer 5
   prev_count = descr.units[0] * descr.windows[0];
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronRevInDenormOCL;
   descr.count = prev_count;
   descr.layers = 1;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }


Vorhersage-Modelle


Beide Prognosemodelle haben trotz unterschiedlicher Planungshorizonte die gleiche Architektur, die in der Methode CreatePredictionDescriptions definiert ist. Es ist erwähnenswert, dass der Encoder so konzipiert ist, dass er dieselben multimodalen Zeitreihen empfängt, die zuvor vom Modell für die Suche nach Abhängigkeiten analysiert wurden. Daher verwenden wir die Encoder-Architektur vollständig wieder, mit Ausnahme der Dropout-Schicht, da die Eingabemaskierung während des Trainings der Vorhersagemodelle nicht angewendet wird.

Der Decoder des Vorhersagemodells erhält als Eingabe nur den Merkmalsvektor des letzten Balkens, dessen Werte durch eine vollständig verbundene Schicht geleitet werden.

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

Wie bei den zuvor beschriebenen Modellen folgt darauf eine Batch-Normalisierungsschicht, die wir für die erste Vorverarbeitung der rohen Eingabedaten verwenden.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

In diesem Artikel konzentrieren wir uns auf das Training des Modells zur Analyse historischer Daten für ein einzelnes Finanzinstrument. Angesichts dessen wird die Wirksamkeit der Positionskodierung minimiert, wenn die Eingabedaten nur den Beschreibungsvektor eines Balkens enthalten. Aus diesem Grund lassen wir ihn hier weg. Bei der Analyse mehrerer Finanzinstrumente empfiehlt es sich jedoch, die Eingabedaten um eine Positionskodierung zu ergänzen.

Als Nächstes folgt ein dreischichtiges, diversifiziertes Mehrkopf-Kreuzaufmerksamkeits-Modul, das den Ausgang des entsprechenden Encoders als zweite Informationsquelle nutzt.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronCrossDMHAttention;
//--- Windows
     {
      int temp[] = {BarDescr, BarDescr};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.window_out = 32;
//--- Units
     {
      int temp[] = {1, HistoryBars};
      if(ArrayCopy(descr.units, temp) < (int)temp.Size())
         return false;
     }
   descr.step = 4;               //Heads
   descr.layers = 3;             //Layers
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

Am Ausgang des Modells fügen wir eine vollständig verbundene Projektionsschicht ohne Aktivierungsfunktion hinzu.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = BarDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

Zwei wichtige Punkte sollten hier hervorgehoben werden. Erstens: Im Gegensatz zu traditionellen Modellen, die erwartete Werte für die Fortsetzung der analysierten Zeitreihen vorhersagen, schlagen die Autoren des Systems von StockFormer vor, die Veränderungskoeffizienten der Indikatoren vorherzusagen. Das bedeutet, dass die Größe des Ausgangsvektors mit dem Eingangstensor des Decoders übereinstimmt, unabhängig vom Planungshorizont. Ein solcher Ansatz ermöglicht es uns, die umgekehrte Normalisierungsschicht am Ausgang des Decoders zu eliminieren. Außerdem wird bei dieser Art der Vorhersage die umgekehrte Normalisierung überflüssig. Da Veränderungskoeffizienten und Rohindikatoren zu unterschiedlichen Verteilungen gehören.

Zweitens, die Verwendung einer vollständig verbundenen Schicht am Ausgang des Decoders. Wie bereits erwähnt, analysieren wir eine multimodale Zeitreihe für ein einzelnes Finanzinstrument. Daher erwarten wir, dass alle untersuchten unitären Sequenzen einen unterschiedlichen Grad an Korrelation aufweisen. Daher müssen ihre Veränderungskoeffizienten angeglichen werden. Daher ist in diesem Fall eine vollständig verbundene Schicht angemessen. Wenn Sie jedoch eine parallele Analyse mehrerer Finanzinstrumente planen, ist es ratsam, die vollständig verknüpfte Schicht durch eine Faltungsschicht zu ersetzen, die eine unabhängige Vorhersage der Veränderungskoeffizienten für jeden Vermögenswert ermöglicht.

Damit ist unser Überblick über die Architekturen der prädiktiven Kodierungsmodelle abgeschlossen. Eine ausführliche Beschreibung ihrer Gestaltung findet sich im Anhang.


Training prädiktiver Kodierungsmodelle


Im System StockFormer wird das Training von prädiktiven Kodierungsmodellen in einer eigenen Phase durchgeführt. Nachdem wir uns die Architekturen der Vorhersagemodelle angesehen haben, wenden wir uns nun der Konstruktion eines Expert Advisors für deren Training zu. Die Basismethoden des EA sind weitgehend ähnlichen Programmen entlehnt, die in früheren Artikeln dieser Reihe besprochen wurden. Daher werden wir uns in diesem Artikel hauptsächlich auf den direkten Trainingsalgorithmus konzentrieren, der in der Methode Train organisiert ist.

Zunächst werden wir ein wenig Vorbereitungsarbeit leisten. Hier bilden wir einen Wahrscheinlichkeitsvektor für die Auswahl von Trajektorien aus dem Erfahrungswiederholungspuffer, wobei wir den Trajektorien mit maximaler Rentabilität höhere Wahrscheinlichkeiten zuweisen. Auf diese Weise beeinflussen wir den Trainingsprozess in Richtung profitabler Läufe und füllen ihn mit positiven Beispielen.

void Train(void)
  {
//---
   vector<float> probability = GetProbTrajectories(Buffer, 0.9);
//---
   vector<float> result, target, state;
   matrix<float> predict;
   bool Stop = false;
//---
   uint ticks = GetTickCount();

In diesem Stadium werden auch die notwendigen lokalen Variablen deklariert, die zur Speicherung von Zwischendaten während des Trainings verwendet werden. Nach Abschluss der Vorbereitung beginnen wir mit der Iterationsschleife des Trainings. Die in den externen Parametern des EA festgelegte Gesamtzahl der Iterationen.

   for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter ++)
     {
      int tr = SampleTrajectory(probability);
      int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - NForecast));
      if(i <= 0)
        {
         iter --;
         continue;
        }
      if(!state.Assign(Buffer[tr].States[i].state) ||
         MathAbs(state).Sum() == 0 ||
         !bState.AssignArray(state))
        {
         iter --;
         continue;
        }
      if(!state.Assign(Buffer[tr].States[i + NForecast].state) ||
         !state.Resize((NForecast + 1)*BarDescr) ||
         MathAbs(state).Sum() == 0)
        {
         iter --;
         continue;
        }

Innerhalb der Schleife wird eine Trajektorie aus dem Erfahrungswiedergabepuffer zusammen mit ihrem anfänglichen Umgebungszustand ausgewählt. Anschließend prüfen wir, ob historische Daten im gewählten Zustand sowie aktuelle Daten über den angegebenen Planungshorizont vorliegen. Wenn diese Prüfungen erfolgreich sind, übertragen wir die historischen Werte der gewünschten Analysetiefe in den entsprechenden Datenpuffer und führen den Vorwärtsdurchlauf aller Vorhersagemodelle durch.

      //--- Feed Forward
      if(!RelateEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) ||
         !RelateDecoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(RelateEncoder)) ||
         !ShortEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) ||
         !ShortDecoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(ShortEncoder)) ||
         !LongEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) ||
         !LongDecoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(LongEncoder)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }

Es ist wichtig zu wissen, dass jedes Vorhersagemodell trotz seiner identischen Architektur einen eigenen Encoder hat. Dadurch erhöht sich die Gesamtzahl der trainierbaren Modelle und dementsprechend auch der Rechenaufwand für Training und Betrieb. Es ermöglicht aber auch jedem Modell, die für seine spezifische Aufgabe relevanten Abhängigkeiten zu erfassen.

Ein weiterer Punkt betrifft die Verwendung des Rohdaten-Tensors im Hauptstrom des Decoders. Wie bereits erwähnt, akzeptieren die Decoder der Vorhersagemodelle nur den letzten Balken als Eingabe. Beim Training wird jedoch in allen Fällen der historische Puffer über die gesamte Analysetiefe verwendet. Zur Verdeutlichung: Der im Wiedergabepuffer gespeicherte Umgebungszustand kann als Matrix dargestellt werden. In dieser Matrix entsprechen die Zeilen den Balken und die Spalten den Merkmalen (Preise und Indikatoren). Die erste Zeile enthält die Daten des letzten Balkens. Wenn also ein Tensor übergeben wird, der größer ist als die Eingangsgröße des Decoders, nimmt das Modell einfach das erste Segment, das der Größe der Eingangsschicht entspricht. Das ist genau das, was wir brauchen, denn so können wir zusätzliche Puffer und unnötige Datenkopien vermeiden.

Nach einem erfolgreichen Vorwärtsdurchlauf bereiten wir Zielwerte vor und führen den Rückwärtsdurchlauf durch. Bei den Modellen für die Suche nach Abhängigkeiten sind die Zielwerte die multimodalen Zeitreihen selbst. Daher können wir sofort Backpropagation durch den Decoder laufen lassen und den Fehlergradienten an den Encoder weitergeben. Auf der Grundlage des erhaltenen Gradienten aktualisieren wir die Encoder-Parameter entsprechend.

      //--- Relation
      if(!RelateDecoder.backProp(GetPointer(bState), (CNet *)GetPointer(RelateEncoder)) ||
         !RelateEncoder.backPropGradient((CBufferFloat*)NULL))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }

Für die Vorhersagemodelle müssen jedoch Zielwerte definiert werden. Wie bereits erwähnt, sind die Ziele hier die Änderungskoeffizienten der Parameter. Wir gehen davon aus, dass der Planungshorizont kürzer ist als die Analysetiefe der historischen Daten. Zur Berechnung der Zielwerte wird also der zukünftige Zustand der Umgebung, der in den erforderlichen Horizontschritten aufgezeichnet wurde, aus dem Wiedergabepuffer entnommen. Und dann wandeln wir diesen Tensor in eine Matrix um, bei der jede Zeile einem Balken entspricht.

      //--- Prediction
      if(!predict.Resize(1, state.Size()) ||
         !predict.Row(state, 0) ||
         !predict.Reshape(NForecast + 1, BarDescr)
        )
        {
         iter --;
         continue;
        }

Da die ersten Zeilen einer solchen Matrix spätere Balken darstellen, nehmen wir eine Zeile mehr als den Planungshorizont. Die letzte Zeile dieser verkürzten Matrix entspricht dem aktuell analysierten Balken.

Es ist wichtig, daran zu erinnern, dass der Wiedergabepuffer nicht normalisierte Daten speichert. Um die berechneten Veränderungskoeffizienten in einen sinnvollen Bereich zu bringen, normalisieren wir sie durch die maximalen absoluten Werte jedes Parameters in der Matrix der zukünftigen Werte. Infolgedessen erhalten wir Koeffizienten, die typischerweise im Bereich {-2,0, 2,0} liegen.

      result = MathAbs(predict).Max(0);

Für das kurzfristige Vorhersagemodell ist das Ziel der Änderungskoeffizient des Parameters beim nächsten Balken. Dieser wird als Differenz zwischen den letzten beiden Zeilen der Vorhersagematrix berechnet, durch den Vektor der Maximalwerte geteilt und dann im entsprechenden Puffer gespeichert.

      target = (predict.Row(NForecast - 1) - predict.Row(NForecast)) / result;
      if(!bShort.AssignArray(target))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }

Für das langfristige Prognosemodell summieren wir die Koeffizienten der Parameteränderung über alle Balken und wenden einen Diskontierungsfaktor an.

      for(int i = 0; i < NForecast - 1; i++)
         target += (predict.Row(i) - predict.Row(i + 1)) / result * 
                              MathPow(DiscFactor, NForecast - i - 1);
      if(!bLong.AssignArray(target))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }

Sobald der vollständige Satz von Zielwerten definiert ist, aktualisieren wir die Parameter der Vorhersagemodelle, um den Prognosefehler zu minimieren. Konkret führen wir zunächst eine Backpropagation durch den Decoder und den Encoder des Kurzzeitvorhersagemodells durch, gefolgt von dem Langzeitmodell.

      //--- Short prediction
      if(!ShortDecoder.backProp(GetPointer(bShort), (CNet *)GetPointer(ShortEncoder)) ||
         !ShortEncoder.backPropGradient((CBufferFloat*)NULL))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }
      //--- Long prediction
      if(!LongDecoder.backProp(GetPointer(bLong), (CNet *)GetPointer(LongEncoder)) ||
         !LongEncoder.backPropGradient((CBufferFloat*)NULL))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }

Nach der Aktualisierung aller Modelle, die in dieser Phase trainiert wurden, protokollieren wir den Fortschritt, um den Nutzer zu informieren, und fahren dann mit der nächsten Trainingsiteration fort.

      //---
      if(GetTickCount() - ticks > 500)
        {
         double percent = double(iter) * 100.0 / (Iterations);
         string str = StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Relate", 
                                  percent, RelateDecoder.getRecentAverageError());
         str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Short", percent, 
                                            ShortDecoder.getRecentAverageError());
         str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Long", percent, 
                                             LongDecoder.getRecentAverageError());
         Comment(str);
         ticks = GetTickCount();
        }
     }

Nach Abschluss aller Trainingsiterationen löschen wir die Kommentare auf dem Chart (das zuvor zur Anzeige von Trainingsaktualisierungen verwendet wurde).

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Relate", RelateDecoder.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Short", ShortDecoder.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Long", LongDecoder.getRecentAverageError());
   ExpertRemove();
//---
  }

Wir drucken die Ergebnisse in das Journal und leiten die Beendigung der EA-Operation ein.

Der vollständige Quellcode des Vorhersagemodell-Trainings EA befindet sich im Anhang (Datei: „ ...\MQL5\Experts\StockFormer\Study1.mq5“).

Abschließend sei darauf hingewiesen, dass wir beim Training des Modells für diesen Artikel die gleiche Struktur der Eingabedaten wie in früheren Arbeiten verwendet haben. Wichtig ist, dass das Training des Vorhersagemodells ausschließlich auf Umgebungszuständen beruht, die unabhängig von den Aktionen des Agenten sind. Daher kann das Training mit einem vorab gesammelten Datensatz gestartet werden. Wir gehen nun zur nächsten Phase unserer Arbeit über.



Training der Politik

Während die Vorhersagemodelle trainiert werden, wenden wir uns der nächsten Phase zu – dem Training der Verhaltenspolitik des Agenten.

Modell der Architektur


Wir beginnen mit der Vorbereitung der Architekturen der in dieser Phase verwendeten Modelle, wie sie in der Methode CreateDescriptions definiert sind. Es ist wichtig zu beachten, dass im System von StockFormer sowohl der Akteur als auch der Kritiker als Input die Ergebnisse der Vorhersagemodelle verwenden, die mithilfe einer Kaskade von Aufmerksamkeitsmodulen zu einem einheitlichen Unterraum kombiniert werden. In unserer Bibliothek können wir Modelle mit zwei Datenquellen erstellen. Daher haben wir die Aufmerksamkeitskaskade in zwei separate Modelle aufgeteilt. Im ersten Modell gleichen wir Daten aus zwei Planungshorizonten ab. Die Autoren empfehlen, langfristige Planungsdaten aus dem Hauptstrom zu verwenden, da diese weniger empfindlich gegenüber Rauschen sind.

Der Aufbau des Zwei-Horizont-Ausrichtungsmodells ist einfach. Hier erstellen wir zwei Ebenen:

  1. Eine vollständig verbundene Eingabeschicht.
  2. Ein diversifiziertes Kreuzaufmerksamkeits-Modul mit drei internen Schichten.

//--- Long to Short predict
   long_short.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!long_short.Add(descr))
     {
      delete descr;
      return false;
     }
//--- Layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronCrossDMHAttention;
//--- Windows
     {
      int temp[] = {BarDescr, BarDescr};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.window_out = 32;
//--- Units
     {
      int temp[] = {1, 1};
      if(ArrayCopy(descr.units, temp) < (int)temp.Size())
         return false;
     }
   descr.step = 4;               //Heads
   descr.layers = 3;             //Layers
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!long_short.Add(descr))
     {
      delete descr;
      return false;
     }

Hier wird keine Normalisierungsschicht verwendet, da die Modelleingabe die Ausgabe von zuvor trainierten Vorhersagemodellen und nicht von Rohdaten ist.

Die Ergebnisse des Zwei-Horizont-Abgleichs werden dann mit Informationen über den aktuellen Zustand der Umgebung angereichert, die vom Encoder des auf die Eingabedaten angewandten Abhängigkeits-Suchmodells stammen.

Erinnern Sie sich, dass das Modell für die Suche nach Abhängigkeiten darauf trainiert wurde, maskierte Teile der Eingabedaten zu rekonstruieren. In diesem Stadium gehen wir davon aus, dass jede univariate Zeitreihe eine prädiktive Zustandsdarstellung hat, die auf der Grundlage der anderen univariaten Sequenzen gebildet wurde. Daher ist die Ausgabe des Encoders ein entrauschter Tensor des Umgebungszustands, da Ausreißer, die nicht den Erwartungen des Modells entsprechen, durch statistische Werte aus anderen Sequenzen kompensiert werden.

Die Architektur des Modells, das die Vorhersagen mit Informationen über den Zustand der Umgebung anreichert, ist eng an das Zwei-Horizont-Ausrichtungsmodell angelehnt. Der einzige Unterschied besteht darin, dass wir die Sequenzlänge der zweiten Datenquelle ändern.

//--- Predict to Relate
   predict_relate.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = (BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!predict_relate.Add(descr))
     {
      delete descr;
      return false;
     }
//--- Layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronCrossDMHAttention;
//--- Windows
     {
      int temp[] = {BarDescr, BarDescr};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.window_out = 32;
//--- Units
     {
      int temp[] = {1, HistoryBars};
      if(ArrayCopy(descr.units, temp) < (int)temp.Size())
         return false;
     }
   descr.step = 4;               //Heads
   descr.layers = 3;             //Layers
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!predict_relate.Add(descr))
     {
      delete descr;
      return false;
     }

Nach der Konstruktion der Aufmerksamkeitskaskade, die die Ergebnisse der drei Vorhersagemodelle in einem einheitlichen Unterraum kombiniert, fahren wir mit der Konstruktion des Akteurs fort. Der Input für das Akteursmodell ist der Output der Aufmerksamkeitskaskade.

//--- Actor
   actor.Clear();
//--- Input Layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = (BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Die prädiktiven Erwartungen werden mit Informationen über den Kontostand kombiniert.

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

Diese kombinierten Informationen werden durch einen Entscheidungsblock geleitet, der als MLP mit einem stochastischen Ausgangskopf implementiert ist.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   descr.probability = Rho;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 2 * NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   descr.probability = Rho;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronVAEOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Am Ausgang des Modells werden die Handelsparameter für jede Richtung mithilfe einer Faltungsschicht mit einer Sigmoid-Aktivierungsfunktion angepasst.

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = NActions / 3;
   descr.window = 3;
   descr.step = 3;
   descr.window_out = 3;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   descr.probability = Rho;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Der Kritiker hat eine ähnliche Architektur, aber statt des Kontostands analysiert er die Aktionen des Agenten. Bei der Ausgabe wird kein stochastischer Kopf verwendet. Die vollständige Architektur aller Modelle finden Sie im Anhang.

Trainingsverfahren für die Politik


Sobald die Modellarchitekturen definiert sind, organisieren wir die Trainingsalgorithmen. In der zweiten Phase geht es darum, die optimale Strategie für das Agentenverhalten zu finden, um die Rendite zu maximieren und gleichzeitig das Risiko zu minimieren.

Wie zuvor beginnt die Trainingsmethode mit der Vorbereitung. Wir generieren einen Wahrscheinlichkeitsvektor für die Auswahl von Trajektorien aus dem Erfahrungswiedergabepuffer auf der Grundlage ihrer Leistung und der Deklaration lokaler Variablen.

void Train(void)
  {
//---
   vector<float> probability = GetProbTrajectories(Buffer, 0.9);
//---
   vector<float> result, target, state;
   bool Stop = false;
//---
   uint ticks = GetTickCount();

Anschließend wird die Trainingsschleife gestartet, wobei die Anzahl der Iterationen durch die externen Parameter des EAs festgelegt wird.

   for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter ++)
     {
      int tr = SampleTrajectory(probability);
      int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - NForecast));
      if(i <= 0)
        {
         iter --;
         continue;
        }
      if(!state.Assign(Buffer[tr].States[i].state) ||
         MathAbs(state).Sum() == 0 ||
         !bState.AssignArray(state))
        {
         iter --;
         continue;
        }

In jeder Iteration werden eine Trajektorie und ihr Zustand für die aktuelle Iteration ausgewählt. Vergewissern Sie sich, dass wir alle erforderlichen Daten haben.

Im Gegensatz zu Vorhersagemodellen erfordert das Training von Richtlinien zusätzliche Eingabedaten. Nach der Extraktion der Beschreibung des Umgebungszustands werden Kontostand und offene Positionen aus dem Replay-Puffer zum jeweiligen Zeitschritt erfasst.

      //--- Account
      bAccount.Clear();
      float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0];
      float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1];
      bAccount.Add((Buffer[tr].States[i].account[0] - PrevBalance) / PrevBalance);
      bAccount.Add(Buffer[tr].States[i].account[1] / PrevBalance);
      bAccount.Add((Buffer[tr].States[i].account[1] - PrevEquity) / PrevEquity);
      bAccount.Add(Buffer[tr].States[i].account[2]);
      bAccount.Add(Buffer[tr].States[i].account[3]);
      bAccount.Add(Buffer[tr].States[i].account[4] / PrevBalance);
      bAccount.Add(Buffer[tr].States[i].account[5] / PrevBalance);
      bAccount.Add(Buffer[tr].States[i].account[6] / PrevBalance);
      //---
      double time = (double)Buffer[tr].States[i].account[7];
      double x = time / (double)(D'2024.01.01' - D'2023.01.01');
      bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
      x = time / (double)PeriodSeconds(PERIOD_MN1);
      bAccount.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
      x = time / (double)PeriodSeconds(PERIOD_W1);
      bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
      x = time / (double)PeriodSeconds(PERIOD_D1);
      bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
      if(!!bAccount.GetOpenCL())
        {
         if(!bAccount.BufferWrite())
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }
        }

Ein Zeitstempel für den analysierten Zustand wird ebenfalls hinzugefügt.

Anhand dieser Informationen führen wir einen Vorwärtsdurchlauf durch die prädiktiven Kodierungsmodelle und die Aufmerksamkeitskaskade durch, um die prädiktiven Ausgaben in einen einheitlichen Unterraum zu transformieren.

      //--- Generate Latent state
      if(!RelateEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) ||
         !ShortEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) ||
         !ShortDecoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(ShortEncoder)) ||
         !LongEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) ||
         !LongDecoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(LongEncoder)) ||
         !LongShort.feedForward(GetPointer(LongDecoder), -1, GetPointer(ShortDecoder), -1) ||
         !PredictRelate.feedForward(GetPointer(LongShort), -1, GetPointer(RelateEncoder), -1)
        )
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }

Anmerkung: In diesem Stadium wird der Decoder für die Suche nach Abhängigkeiten nicht ausgeführt, da er nicht für das Training oder die Bereitstellung von Richtlinien verwendet wird.

Als Nächstes optimieren wir den Kritiker, um den Fehler bei der Bewertung der Agentenaktionen zu minimieren. Tatsächliche Aktionen aus dem ausgewählten Zustand werden aus dem Replay-Puffer abgerufen und durch den Kritiker geleitet.

      //--- Critic
      target.Assign(Buffer[tr].States[i].action);
      target.Clip(0, 1);
      bActions.AssignArray(target);
      if(!!bActions.GetOpenCL())
         if(!bActions.BufferWrite())
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }
      Critic.TrainMode(true);
      if(!Critic.feedForward(GetPointer(PredictRelate), -1, (CBufferFloat*)GetPointer(bActions)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }

Die geschätzte Aktion, die als Ergebnis der Werte des Vorwärtsdurchlauf des Kritikers erhalten wird, nähert sich zunächst einer Zufallsverteilung an. Der Erfahrungswiederholungspuffer speichert jedoch auch die tatsächlichen Belohnungen, die der Agent für seine tatsächlichen Aktionen während der Trajektoriensammlung erhalten hat. Daher können wir den Kritiker so trainieren, dass der Fehler zwischen der vorhergesagten und der tatsächlichen Belohnung minimiert wird.

Wir extrahieren die tatsächliche Belohnung aus dem Erfahrungswiedergabepuffer und führen den Rückwärtsdurchlauf des Kritikers aus.

      result.Assign(Buffer[tr].States[i + 1].rewards);
      target.Assign(Buffer[tr].States[i + 2].rewards);
      result = result - target * DiscFactor;
      Result.AssignArray(result);
      if(!Critic.backProp(Result, (CBufferFloat *)GetPointer(bActions), (CBufferFloat *)GetPointer(bGradient)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }

Als Nächstes gehen wir zum eigentlichen Training der Verhaltenspolitik des Akteurs über. Anhand der gesammelten Eingabedaten führen wir einen Vorwärtsdurchlauf durch den Akteur durch, um den Aktionstensor entsprechend der aktuellen Politik zu erzeugen.

      //--- Actor Policy
      if(!Actor.feedForward(GetPointer(PredictRelate), -1, (CBufferFloat*)GetPointer(bAccount)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }

Unmittelbar danach bewerten wir die generierten Aktionen mit Hilfe des Kritikers.

      Critic.TrainMode(false);
      if(!Critic.feedForward(GetPointer(PredictRelate), -1, (CNet*)GetPointer(Actor), -1))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }

Anmerkung: Während der Optimierung der Politik des Akteurs ist der Trainingsmodus des Kritikers deaktiviert. Auf diese Weise kann der Fehlergradient an den Akteur weitergegeben werden, ohne dass die Parameter des Kritikers aufgrund irrelevanter Daten geändert werden.

Das Training der Akteure erfolgt in zwei Phasen. In der ersten Phase bewerten wir die Wirksamkeit der tatsächlichen Aktionen, die im Erfahrungswiedergabepuffer aufgezeichnet wurden. Wenn die Belohnung positiv ist, minimieren wir den Fehler zwischen dem vorhergesagten und dem tatsächlichen Aktionstensor. So wird eine rentable Politik unter Aufsicht trainiert.

      if(result.Sum() >= 0)
         if(!Actor.backProp(GetPointer(bActions), (CBufferFloat*)GetPointer(bAccount), GetPointer(bGradient)) ||
            !PredictRelate.backPropGradient(GetPointer(RelateEncoder), -1, -1, false) ||
            !LongShort.backPropGradient(GetPointer(ShortDecoder), -1, -1, false) ||
            !ShortDecoder.backPropGradient((CNet *)GetPointer(ShortEncoder), -1, -1, false) ||
            !ShortEncoder.backPropGradient((CBufferFloat*)NULL) ||
            !LongDecoder.backPropGradient((CNet *)GetPointer(LongEncoder), -1, -1, false) ||
            !LongEncoder.backPropGradient((CBufferFloat*)NULL)
           )
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

Wichtig ist, dass in dieser Phase der Fehlergradient bis zu den Vorhersagemodellen weitergegeben wird. Dadurch werden sie fein abgestimmt, um die Aufgabe der Optimierung der Politik des Akteurs zu unterstützen.

Kritikergeleitete Phase: Die Verhaltenspolitik des Akteurs wird durch die Verbreitung des Fehlergradienten vom Kritiker optimiert. In dieser Phase wird die Politik unabhängig von den tatsächlichen Ergebnissen der Aktionen in der Umwelt angepasst, wobei man sich ausschließlich auf die Bewertung der aktuellen Politik durch den Kritiker stützt. Dafür erhöhen wir die Aktionsbewertung um 1%.

      Critic.getResults(Result);
      for(int c = 0; c < Result.Total(); c++)
        {
         float value = Result.At(c);
         if(value >= 0)
            Result.Update(c, value * 1.01f);
         else
            Result.Update(c, value * 0.99f);
        }

Diese angepasste Belohnung wird dann an den Kritiker als Ziel weitergegeben und durch den Rückwärtsdurchlauf wird der Fehlergradient an den Akteur weitergegeben. Dieser Vorgang erzeugt einen Fehlergradienten am Ausgang des Akteurs, der die Aktionen auf eine höhere Rentabilität ausrichtet.

      if(!Critic.backProp(Result, (CNet *)GetPointer(Actor), LatentLayer) ||
         !Actor.backPropGradient((CBufferFloat*)GetPointer(bAccount), GetPointer(bGradient)) ||
         !PredictRelate.backPropGradient(GetPointer(RelateEncoder), -1, -1, false) ||
         !LongShort.backPropGradient(GetPointer(ShortDecoder), -1, -1, false) ||
         !ShortDecoder.backPropGradient((CNet *)GetPointer(ShortEncoder), -1, -1, false) ||
         !ShortEncoder.backPropGradient((CBufferFloat*)NULL) ||
         !LongDecoder.backPropGradient((CNet *)GetPointer(LongEncoder), -1, -1, false) ||
         !LongEncoder.backPropGradient((CBufferFloat*)NULL)
        )
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }

Der resultierende Gradient wird ähnlich wie in der ersten Trainingsstufe durch alle relevanten Modelle propagiert.

Anschließend informieren wir den Nutzer über den Trainingsfortschritt und fahren mit der nächsten Iteration fort.

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

Nach Abschluss aller Trainingsiterationen löschen wir die Chartkommentare, protokollieren die Ergebnisse im Journal und leiten die Beendigung des Programms ein, genau wie in der ersten Trainingsphase.

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

Es ist anzumerken, dass sich die Algorithmusanpassungen nicht nur auf die Modellschulung der Expert Advisors auswirkten, sondern auch auf die Interaktion der EAs mit der Umgebung. Die Anpassungen der Algorithmen für die Interaktion mit der Umwelt spiegeln jedoch weitgehend den oben beschriebenen Vorwärtsdurchlauf des Akteurs wider und sind Gegenstand einer unabhängigen Studie. Daher werden wir hier nicht auf die detaillierte Logik dieser Algorithmen eingehen. Ich möchte Sie dazu ermutigen, ihre Implementierungen selbst zu erkunden. Der vollständige Quellcode für alle in diesem Artikel verwendeten Programme ist im Anhang enthalten.



Tests

Wir haben die umfassende Implementierung des System des StockFormers unter Verwendung von MQL5 abgeschlossen und die letzte Phase unserer Arbeit erreicht – das Training der Modelle und die Evaluierung ihrer Leistung anhand echter historischer Daten.

Wie bereits erwähnt, wurde in der ersten Phase des Trainings der Vorhersagemodelle ein in früheren Studien gesammelter Datensatz verwendet. Dieser Datensatz umfasst historische Daten des EURUSD für das gesamte Jahr 2023 im Zeitrahmen H1. Alle Indikatorparameter wurden auf ihre Standardwerte gesetzt.

Beim Training des Vorhersagemodells verwenden wir nur historische Daten, die den Zustand der Umgebung beschreiben und unabhängig vom Verhalten des Agenten sind. So können wir die Modelle trainieren, ohne den Trainingsdatensatz zu aktualisieren. Der Trainingsprozess wird so lange fortgesetzt, bis sich die Fehler in einem engen Bereich stabilisiert haben.

Die zweite Trainingsstufe – die Optimierung der Verhaltenspolitik des Akteurs – wird iterativ durchgeführt, wobei der Trainingsdatensatz regelmäßig aktualisiert wird, um die aktuelle Politik widerzuspiegeln.

Wir bewerten die Leistung des trainierten Modells mit dem MetaTrader 5 Strategy Tester anhand historischer Daten vom Januar 2024. Dieser Zeitraum schließt sich unmittelbar an den Zeitraum des Trainingsdatensatzes an. Die Ergebnisse werden im Folgenden vorgestellt.

Während des Testzeitraums führte das Modell 15 Trades aus, von denen 10 mit Gewinn abgeschlossen wurden – eine Erfolgsquote von über 66 %. Ein recht gutes Ergebnis. Bemerkenswert ist, dass der durchschnittliche Gewinn viermal so hoch ist wie der durchschnittliche Verlust. Daraus ergibt sich ein klarer Aufwärtstrend in der Saldenkurve.



Schlussfolgerung

In diesen beiden Artikeln haben wir das System vom StockFormer untersucht, das einen innovativen Ansatz für das Training von Handelsstrategien für Finanzmärkte bietet. StockFormer kombiniert „Predictive Coding“ mit „Reinforcement Learning“ und ermöglicht so die Entwicklung flexibler Strategien, die dynamische Abhängigkeiten zwischen mehreren Vermögenswerten erfassen und deren Verhalten sowohl kurz- als auch langfristig prognostizieren.

Die dreifach verzweigte prädiktive Kodierungsstruktur in StockFormer ermöglicht die Extraktion von latenten Darstellungen, die kurzfristige Trends, langfristige Veränderungen und Beziehungen zwischen den Vermögenswerten widerspiegeln. Die Integration dieser Darstellungen erfolgt über eine Kaskade von Mehrkopf-Aufmerksamkeitsmodulen, die einen einheitlichen Zustandsraum für die Optimierung von Handelsentscheidungen schaffen.

Im praktischen Teil haben wir die Schlüsselkomponenten des Rahmens in MQL5 implementiert, die Modelle trainiert und sie an realen historischen Daten getestet. Die experimentellen Ergebnisse bestätigen die Wirksamkeit der vorgeschlagenen Ansätze. Die Anwendung dieser Modelle im Live-Handel erfordert jedoch ein Training mit einem größeren historischen Datensatz und umfassende weitere Tests.


Referenzen


Programme, die im diesem Artikel verwendet werden

# Name Typ Beschreibung
1 Research.mq5 Expert Advisor Expert Advisor für die Probenahme
2 ResearchRealORL.mq5
Expert Advisor
Expert Advisor für die Probenahme mit der Real-ORL-Methode
3 Study1.mq5  Expert Advisor Predictive Learning Expert Advisor
4 Study2.mq5  Expert Advisor Expert Advisor für das Training der Politik
5 Test.mq5 Expert Advisor Expert Advisor für Modelltests
6 Trajectory.mqh Klassenbibliothek Struktur der Beschreibung des Systemzustands und der Modellarchitektur
7 NeuroNet.mqh Klassenbibliothek Eine Bibliothek von Klassen zur Erstellung eines neuronalen Netzes
8 NeuroNet.cl Code Base OpenCL-Programmcode-Bibliothek

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

Beigefügte Dateien |
MQL5.zip (2253.87 KB)
Quantencomputing und Handel: Ein neuer Ansatz für Preisprognosen Quantencomputing und Handel: Ein neuer Ansatz für Preisprognosen
Der Artikel beschreibt einen innovativen Ansatz zur Vorhersage von Kursbewegungen auf den Finanzmärkten mit Hilfe von Quantencomputern. Das Hauptaugenmerk liegt auf der Anwendung des Algorithmus Quantum Phase Estimation (QPE), um Prototypen von Preismustern zu finden, die es Händlern ermöglichen, die Analyse von Marktdaten erheblich zu beschleunigen.
Artificial Tribe Algorithm (ATA) Artificial Tribe Algorithm (ATA)
In diesem Artikel werden die wichtigsten Komponenten und Innovationen des ATA-Optimierungsalgorithmus ausführlich besprochen. Dabei handelt es sich um eine evolutionäre Methode mit einem einzigartigen dualen Verhaltenssystem, das sich je nach Situation anpasst. ATA kombiniert individuelles und soziales Lernen und nutzt Crossover für Erkundungen und Migration, um Lösungen zu finden, wenn sie in lokalen Optima stecken.
Black Hole Algorithmus (BHA) Black Hole Algorithmus (BHA)
Der Black Hole Algorithm (BHA) nutzt die Prinzipien der Schwerkraft von Schwarzen Löchern, um Lösungen zu optimieren. In diesem Artikel werden wir uns ansehen, wie BHA die besten Lösungen findet und dabei lokale Extreme vermeidet, und warum dieser Algorithmus zu einem leistungsstarken Werkzeug für die Lösung komplexer Probleme geworden ist. Erfahren Sie, wie einfache Ideen zu beeindruckenden Ergebnissen in der Welt der Optimierung führen können.
Visuelle Bewertung und Anpassung des Handels im MetaTrader 5 Visuelle Bewertung und Anpassung des Handels im MetaTrader 5
Mit dem Strategietester können Sie mehr tun, als nur die Parameter Ihres Handelsroboters zu optimieren. Ich zeige Ihnen, wie Sie den Handelsverlauf Ihres Kontos im Nachhinein auswerten und Anpassungen an Ihrem Handel im Tester vornehmen, indem Sie die Stop-Losses Ihrer offenen Positionen ändern.