English Русский Español 日本語 Português
preview
Neuronale Netze im Trading: Effektive Merkmalsextraktion zur präzisen Klassifizierung (letzter Teil)

Neuronale Netze im Trading: Effektive Merkmalsextraktion zur präzisen Klassifizierung (letzter Teil)

MetaTrader 5Handelssysteme |
45 1
Dmitriy Gizlyk
Dmitriy Gizlyk

Einführung

Wir haben die letzte Phase unserer Einführung in das Mantis-Framework erreicht. Zuerst haben wir dessen theoretische Grundlage im Detail untersucht: wie das Modell mehrkanalige Zeitreihen verarbeitet, warum Differenzsignale benötigt werden und wie lokale Token gebildet werden. Anschließend sind wir tiefer in die Architektur eingetaucht, um zu sehen, wie die Patches strukturiert sind und wie das Modell lernt, stabile Muster aus dem Marktrauschen zu extrahieren. Jetzt ist es an der Zeit, alles zusammenzuführen.

Das Mantis-Framework ist als eine Sequenz unabhängiger, aber logisch zusammenhängender Module strukturiert. Jedes von ihnen übernimmt eine spezifische Funktion – von der anfänglichen Verarbeitung der Eingabedaten bis zur Token-Bildung und Informationsaggregation. Das zugrunde liegende Prinzip ist dieses: minimale Vorannahmen bei gleichzeitiger Extraktion so vieler Muster wie möglich aus den Daten selbst.

Alles beginnt damit, dass rohe Marktmerkmale in das Modell eingehen. Anstatt sie als eine einzige Matrix zu behandeln, trennt das Modell jeden Datentyp in einen separaten Kanal. Für jeden dieser Kanäle wird ein zusätzlicher Kanal der ersten Differenzen erstellt, wodurch das Modell kurzfristige Dynamiken explizit erfassen kann. Solche Differenzkanäle erhöhen die Empfindlichkeit des Modells gegenüber Änderungen in der Richtung der Preisbewegung. Zum Beispiel kann eine plötzliche Beschleunigung im Differenzkanal den Beginn einer Impulsbewegung anzeigen, lange bevor Indikatoren sie erkennen.

Jeder dieser Kanäle wird den Faltungsblöcken zugeführt. Hier beginnt die erste Analyse. Faltungen extrahieren lokale Merkmale, indem sie lernen, stabile Mikromuster zu unterscheiden. Jeder Kanal erzeugt 256 Merkmale, die sein lokales Verhalten repräsentieren.

Nach der Faltung beginnt die nächste Stufe: Die Daten werden in 32 nicht überlappende Patches unterteilt. Jeder von ihnen deckt ein spezifisches Segment der Zeitreihe ab. Für jedes einzelne wird eine kanalweise Mittelwertbildung (mean-pooling) durchgeführt. Dies ergibt 32 Token, jedes mit 256 Merkmalen. Dies ist ein wichtiger Punkt. Patches sind lokalisierte Segmente des Marktverhaltens – im Wesentlichen verdichtete Mini-Charts. Sie erfassen Phasen, die von Impuls-/Impulsbewegungen und Konsolidierungen bis hin zu Pullbacks und Divergenzen reichen. Diese Art der Partitionierung macht das Verhalten des Modells stabiler und anpassungsfähiger. Das Modell sieht den Markt nicht als einen Strom von Zahlen, sondern als eine Abfolge erkennbarer Szenarien.

In diesem Stadium wird für jeden Eingabedatenkanal ein Token aus dem ursprünglichen Strom und dem Differenzstrom entnommen. Diese Token werden auf einen gemeinsamen Bereich skaliert, normalisiert und kombiniert. Ein aggregiertes Token wird dann durch eine lineare Projektionsschicht erzeugt, die das Verhalten dieses Kanals über das betreffende Segment widerspiegelt. Ein solches Token enthält den Zustand, die Änderungsrate und die Stärke. Alles, was es einem Trader ermöglicht, die Marktstimmung intuitiv einzuschätzen.

Als Nächstes wird die gesamte Token-Sequenz in Attention-Blöcke eingespeist. Dies ist die kritischste Ebene, auf der Patches beginnen, miteinander zu kommunizieren. Das Modell bewertet, welche Segmente der Vergangenheit für den aktuellen Zustand wichtig sind. Vielleicht wird ein Signal, das vor 20 Bars entstand, nun entscheidend für die Bewertung einer Umkehr. Diese Fähigkeit, Verbindungen zwischen weit entfernten Segmenten zu erkennen, macht das Modell besonders nützlich für die Analyse komplexer Marktstrukturen.

Hinter alldem steckt eine einfache, aber wirkungsvolle Idee: lokale Präzision beizubehalten, ohne den globalen Kontext aus den Augen zu verlieren. Im Handel ist dies entscheidend. Zum Beispiel kann eine Abfolge kleiner Kerzen während einer Flaute nur wenige oder gar keine Informationen enthalten. Aber wenn sie nach einem starken Volumenanstieg erscheinen – insbesondere nahe einem signifikanten Kursniveau –, ändert sich der Kontext vollständig. Mantis berücksichtigt dies automatisch.

Das Mantis-Framework ist mehr als nur eine Architektur. Es ist ein flexibles Werkzeug, das sich an verschiedene Arten von Handelsstrategien anpassen kann.

Nachfolgend ist die vom Autor erstellte Visualisierung des Mantis-Frameworks dargestellt.


Modellarchitektur

In früheren Artikeln haben wir bereits die Hauptkomponenten des Mantis-Frameworks erstellt und sind nun an einem entscheidenden Punkt angelangt: dem Entwurf der Architektur eines trainierbaren Modells, das in der Lage ist, Handelsentscheidungen in Echtzeit zu treffen. Genau wie ein erfahrener Trader, der Preisverhalten, Volumina, Kerzenmuster und die allgemeine Marktstimmung beobachtet – und erst dann, unter Berücksichtigung des Kontextes, eine Entscheidung trifft, eine Position zu eröffnen –, muss unser Modell lernen, Marktdynamiken nicht als Strom von Zahlen, sondern als eine Reihe miteinander verbundener Prozesse wahrzunehmen. Wir zielen darauf ab, ein Verhaltensmodell zu replizieren, das Marktmuster unterscheidet, Verschiebungen in Marktphasen spürt und sich an neue Szenarien anpassen kann. Genau in diesem Kontext ist die Logik des gesamten Mantis-Frameworks aufgebaut.

Es sollte betont werden, dass das Mantis-Framework ursprünglich als Zeitreihenklassifikator entwickelt wurde. Seine Architektur ist für die Segmentierung und Identifizierung verborgener Muster in komplexen Sequenzen ausgelegt. Dieser Ansatz hat sich insbesondere im Finanzkontext als effektiv erwiesen, wo Preisbewegungen oft unter Schichten von Marktrauschen verborgen sind. Durch mehrstufige Verarbeitung – einschließlich Faltungen, lokaler Patch-Bildung und Kanalaggregation – lernt das Modell, Strukturen in Marktdaten zu erkennen, anstatt zufällige Schwankungen. Mantis ermöglicht es nicht nur, Signale zu erfassen, sondern auch, Marktregime zu identifizieren.

Im Kontext unserer aktuellen Aufgabe fungiert Mantis jedoch nicht mehr als eigenständiger Klassifikator, sondern als Fundament – als Fundament der Wahrnehmungsschicht des Handelsagenten. Wir verwenden die Mantis-Architektur, um ein Embedding zu erstellen – eine kompakte, aber informationsreiche Darstellung der Marktsituation –, die dann in die Steuerungskomponente des Modells eingespeist wird. Auf diese Weise wird der Klassifikator zu den Augen des Agenten und gibt ihm die Fähigkeit, das Verhaltensgefüge des Marktes zu erkennen.

Genau dieses Embedding wird an den nächsten Teil der Architektur weitergegeben, der auf dem Prinzip von Actor–Director–Critic basiert. Dieser Ansatz ermöglicht es, Verantwortlichkeiten auf Module aufzuteilen und die Stabilität des Verhaltens des Agenten zu gewährleisten. Der Actor empfängt ein Embedding und trifft eine Handelsentscheidung. Der Director fungiert als struktureller Filter und Verhaltenskorrektor, der die vom Actor vorgeschlagenen Aktionen entweder als gültig oder fehlerhaft klassifiziert und starke Rückkopplungssignale liefert. Sein Zweck ist es, eindeutig unvernünftige oder instabile Entscheidungen herauszufiltern, insbesondere in Zonen von Marktturbulenzen. Der Critic vervollständigt den Zyklus, indem er die strategische Solidität der Aktionen basierend auf dem aktuellen Zustand und der Historie der Interaktionen bewertet. Er fungiert als interner Berater: Selbst wenn eine Aktion möglich ist, lohnt es sich im aktuellen Kontext wirklich, dieses Risiko einzugehen?

Die Architektur aller Modellkomponenten wird mithilfe der Methode CreateDescriptions definiert, bei der die Schichten jedes Moduls sequenziell konfiguriert werden. Ein flexibles Parametersystem ermöglicht es, die Architektur zu skalieren, sie an verschiedene Marktinstrumente anzupassen und experimentelle Elemente hinzuzufügen oder zu deaktivieren, ohne die Kernlogik umschreiben zu müssen.

bool CreateDescriptions(CArrayObj *&encoder,
                        CArrayObj *&actor,
                        CArrayObj *&director,
                        CArrayObj *&critic
                       )
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!director)
     {
      director = new CArrayObj();
      if(!director)
         return false;
     }
   if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         return false;
     }

Über die Methodenparameter erhalten wir Zeiger auf vier dynamische Arrays, die jeweils dazu bestimmt sind, die architektonischen Beschreibungen der entsprechenden Modellkomponenten zu speichern: den Encoder, den Actor, den Director und den Critic. Diese Arrays fungieren als Container, in die Objekte, die die Struktur der neuronalen Netzwerkblöcke beschreiben, nacheinander hinzugefügt werden.

Innerhalb des Methodenkörpers besteht der erste Schritt darin, die empfangenen Zeiger zu validieren. Wenn einer von ihnen nicht initialisiert ist oder auf einen ungültigen Speicherbereich verweist, wird automatisch eine neue Instanz des entsprechenden Objekts erstellt. Dieser Ansatz macht es überflüssig, Arrays manuell im Voraus vorzubereiten, wodurch die Modularität des Codes und seine Eignung für skalierbare Lösungen erhöht werden.

Als Nächstes gehen wir direkt zur Beschreibung der Modellarchitekturen über. Es ist logisch, den Encoder zuerst in dieser Kette zu präsentieren – das Schlüsselmodul, durch das das Modell beginnt, den Markt wahrzunehmen. Hier beginnt der Prozess der Umwandlung von Rohzeitreihen in eine aussagekräftige Darstellung des Marktzustands.

In der ersten Stufe verwenden wir eine voll verbundene Schicht, die in diesem Fall keine Rechenfunktion, sondern lediglich eine Schnittstellenfunktion ausführt. Ihre Rolle besteht darin, eine bequeme Pufferschnittstelle zum Laden der Eingabedaten in das Modell bereitzustellen.

//--- 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;
     }

Unterschiedliche Eingabedaten mit verschiedenen Skalen werden an den Batch-Normalisierungsblock weitergeleitet. Hier werden alle Werte auf eine vergleichbare Skala gebracht und Skalenunterschiede reduziert, was besonders bei der Arbeit mit mehrdimensionalen Finanzzeitreihen kritisch ist. Die Normalisierung ermöglicht es dem Modell, die Daten nicht als zufällige Zahlenmenge, sondern als logisch kohärenten Informationsstrom wahrzunehmen.

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

Für eine leichte Datenaugmentation während des Trainings verwenden wir eine Batch-Normalisierungsschicht mit kontrolliertem Rauschen, was dazu beiträgt, das Modell robuster gegenüber Marktrauschen zu machen und eine Überanpassung an unbedeutende Schwankungen zu verhindern. Dieser Ansatz hilft dem System, Marktinformationen flexibler zu interpretieren und dabei ein Gleichgewicht zwischen Anpassung und Stabilität zu wahren.

Der nächste wichtige Schritt ist die Erstellung von Differenzkanälen erster Ordnung. Diese Schicht generiert Differenzmerkmale – Wertänderungen zwischen benachbarten Zeitpunkten –, die dem Modell helfen, die Dynamik und Richtung der Marktbewegung zu erfassen. Im Handel sind es genau diese Änderungen, die oft den Schlüssel zum Verständnis von Trendumkehrungen oder Trendfortsetzungen liefern, da ein Anstieg oder Fall des Preises zu einem bestimmten Zeitpunkt manchmal wichtiger sein kann als absolute Werte. Die Erstellung von Differenzkanälen verbessert den Informationsgehalt der Quelldaten erheblich und erhöht die Empfindlichkeit des Modells gegenüber lokalen Trends und schnellen Schwankungen.

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

Nachdem die Differenzkanäle erster Ordnung gebildet wurden, werden dem erweiterten Datensatz harmonische Zeitkodierungen hinzugefügt. Dieser Schritt ist äußerst wichtig, damit das Modell den Kontext zeitlicher Abhängigkeiten und die zyklische Natur von Marktprozessen verstehen kann. Harmonische sind eine Art zeitliches Leuchtfeuer, das es dem Modell ermöglicht, nicht nur einen bestimmten Zeitpunkt zu erkennen, sondern auch wiederkehrende Muster, saisonale Schwankungen und verschiedene Zeitskalen. Die zeitliche Kodierung mittels Harmonischer verschafft dem Modell ein tiefes Verständnis der zeitlichen Struktur.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defMamba4CastEmbeding;
   prev_count = descr.count = HistoryBars;
   descr.window = 2 * BarDescr;
   int prev_out = descr.window_out = NSkills;
     {
      int temp[] = {PeriodSeconds(PERIOD_H1), PeriodSeconds(PERIOD_D1)};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Um kanalübergreifende Beziehungen und Abhängigkeiten in den Daten effektiv zu berücksichtigen, verwenden wir einen Faltungsblock mit unterschiedlichen Fenstergrößen. Dieser Multi-Window-Ansatz ermöglicht es dem Modell, gleichzeitig sowohl lokale, kurzfristige Muster als auch längerfristige Trends zu erfassen, die in der Dynamik über mehrere Kanäle hinweg auftreten.

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

Die letzte Stufe der Datenvorbereitung ist die Patching-Schicht. Diese Schicht unterteilt den verarbeiteten Mehrkanal-Tensor in separate, nicht überlappende Patches – im Wesentlichen Fragmente der Zeitreihe, von denen jedes gruppierte Informationen über lokale Marktbedingungen enthält.

Diese Unterteilung ermöglicht es dem Modell, sich auf einzelne Datensegmente zu konzentrieren, was die Rechenkomplexität reduziert und die Identifizierung lokaler Muster erleichtert. Innerhalb jedes Patches wird eine zusätzliche Aggregation durchgeführt, die eine kompakte und dennoch informative Darstellung gewährleistet.

Dadurch wird jeder Patch zu einem separaten Token, das eine komprimierte und strukturierte Beschreibung der Marktsituation über ein begrenztes Zeitintervall enthält. Dies verbessert die Fähigkeit des Modells, lokale Muster zu erfassen, erheblich und bietet eine solide Grundlage für nachfolgende Verarbeitungs- und Entscheidungsphasen.

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMantisPatching;
   descr.count = prev_count;
   descr.layers = prev_out;
   descr.window = EmbeddingSize;
   descr.window_out = NSkills;
   descr.step = Segments;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_count = descr.step;
   prev_out = descr.layers;

Der Attention-Block in unserem Encoder ist als einzelnes Objekt implementiert: CNeuronMantisAttentionUnit. Diese Komponente spielt eine Schlüsselrolle bei der Erfassung wichtiger Beziehungen zwischen Patches und Kanälen. In den Parametern des Objekts geben wir die Anzahl der internen Cross-Attention-Module an, was es uns ermöglicht, die Tiefe und Breite der Informationsanalyse flexibel anzupassen.

Jedes Cross-Attention-Modul konzentriert sich darauf, die wichtigsten Elemente im Datenstrom zu identifizieren und sie mit einem lernbaren Klassentoken (Class Token) in Beziehung zu setzen. Diese Architektur bietet eine effektive Rauschfilterung und hilft dem Modell, sich speziell auf die Signale zu konzentrieren, die Handelsentscheidungen tatsächlich beeinflussen.

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMantisAttentionUnit;
   descr.step = 4;
   descr.count = prev_count;
     {
      int temp[] = {EmbeddingSize, EmbeddingSize};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.layers = 3;
   descr.window_out = NSkills;
   descr.window = prev_out;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Am Ausgang des Cross-Attention-Moduls wird ein Token generiert – eine kompakte und informative Repräsentation des analysierten Zustands der Umgebung. Im Kontext unserer Aufgabe ist eine genaue Klassifizierung von Zuständen keine Priorität. Es ist für uns wichtiger, ausreichend klare und deutliche latente Repräsentationen zu erhalten, die es dem Actor ermöglichen, wohlüberlegte und fundierte Handelsentscheidungen zu treffen.

Genau deshalb haben wir uns bewusst dazu entschieden, die Architektur des Encoders nicht zu verkomplizieren. Dieser Ansatz wahrt ein Gleichgewicht zwischen der Tiefe der Wahrnehmung des Modells und seiner Recheneffizienz, wodurch es sich flexibel an reale Marktbedingungen anpassen kann, ohne die Qualität der getroffenen Entscheidungen zu beeinträchtigen.

Als Nächstes gehen wir zur Beschreibung der Architektur des Actors über. Wie beim Encoder verwendet die Eingangsstufe eine Kombination aus einer vollständig verbundenen Schicht und einem Batch-Normalisierungsblock. Dieses Paar dient als Schnittstelle für den Empfang und die anfängliche Standardisierung der Eingabedaten. In diesem Fall überträgt der Hauptinformationskanal jedoch andere Informationen: Daten zum aktuellen Stand des Handelskontos, einschließlich des Saldos, offener Positionen, des Drawdown-Niveaus und anderer Kennzahlen, die den internen Betriebskontext des Handelsagenten widerspiegeln.

//---
   CLayerDescription *latent = encoder.At(7);
//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = AccountDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = AccountDescr;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Parallel dazu wird über einen Hilfskanal eine vom Encoder gebildete latente Repräsentation der Marktumgebung in das Modell eingespeist. Durch die Kombination dieser beiden Datenströme – interner Zustand und externer Kontext – ist der Actor in der Lage, Entscheidungen zu treffen, die bereits ein Element des strategischen Denkens und Risikomanagements beinhalten. Das Modell ist darauf trainiert, nicht nur die aktuelle Marktsituation zu berücksichtigen, sondern auch seine eigenen Ressourcen, seine Risikotoleranz und die Dynamik seiner aktuellen Position. Dieser Ansatz macht das Verhalten des Agenten überlegt und robust, selbst inmitten erhöhter Marktturbulenzen.

Daten aus zwei Informationsströmen – Marktkontext und Zustand des Handelskontos – werden in der Konkatenationsschicht zusammengeführt. Dieses Objekt erfüllt eine einfache, aber wichtige Funktion: Es kombiniert den vom Encoder empfangenen Embedding-Vektor mit den internen Merkmalen des Handelskontos zu einer einzigen Repräsentation. Auf diese Weise erhält das Modell ein vollständiges Bild dessen, was geschieht.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = AccountDescr;        // Inputs window
   descr.step = latent.windows[0];     // Cross window
   descr.batch = 1e4;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Nachdem die Daten kombiniert wurden, beginnt die tiefe Merkmalsverarbeitung, die durch eine Sequenz von drei vollständig verbundenen Schichten implementiert wird. Die erste Schicht ist für die Extraktion und Verallgemeinerung von Schlüsselmerkmalen verantwortlich. Die zweite Schicht stärkt signifikante Abhängigkeiten zwischen Parametern und deckt potenzielle Muster auf. In der dritten Schicht wird die Handelsentscheidung erzeugt. Diese sequentielle Transformation ermöglicht es dem Modell, nicht einfach nur auf Signale zu reagieren, sondern wohlüberlegte und strategisch fundierte Entscheidungen zu treffen.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.batch = 1e4;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SoftPlus;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NActions;
   descr.activation = SIGMOID;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Die Architektur der Modelle von Director und Critic spiegelt fast vollständig die Struktur des Actor-Modells wider. Der Hauptunterschied liegt in der Dimensionalität der Eingabedaten. Der Aktionstensor des Agent wird diesen Modellen über den Hauptkanal zugeführt, und am Ausgang wird eine Bewertung dieser Aktionen gebildet – eine klassifizierende Bewertung durch den Director und eine strategische Bewertung durch den Critic. Da die interne Logik dieser Modelle ähnlich ist, werden wir den Artikel nicht durch eine erneute Beschreibung überladen. Für eine detailliertere Untersuchung können Leser auf den diesem Artikel beigefügten Code verweisen, der eine vollständige Beschreibung der Architektur aller trainierbaren Komponenten des Systems bietet.


Kontrastives Lernen

Nachdem die Architekturen der Modelle erstellt wurden, gehen wir zur nächsten wichtigen Phase über – dem Training. Wie im theoretischen Teil ausführlich erörtert, ist eines der Hauptmerkmale des Mantis-Frameworks das selbstüberwachte kontrastive Lernen. Dieser Mechanismus ermöglicht es dem Modell, informative und klar unterscheidbare Token zu generieren, die den Zustand der Umgebung in einer kompakten und dennoch ausdrucksstarken Form beschreiben. So wie ein erfahrener Trader auf einen Blick eine Konsolidierungsphase von einer Impulsbewegung unterscheiden kann, muss unser Modell lernen, charakteristische Marktbedingungen zu identifizieren und sie in einer verdichteten Form für die spätere Verwendung durch andere Systemkomponenten darzustellen.

In diesem Fall dient kontrastives Lernen als Grundlage für die Entwicklung einer Marktintuition im Modell. Ohne strenge Überwachung, aber mit einem klaren Verständnis der Unterschiede zwischen Zustandspaaren, lernt der Encoder, Repräsentationen zu konstruieren, die ihm helfen, so effektiv wie möglich zwischen ähnlichen, aber grundlegend verschiedenen Marktszenarien zu unterscheiden.

Die Implementierung des entsprechenden Algorithmus ist im Expert Advisor (EA) „…\\MQL5\\Experts\\Mantis\\StudyContrast.mq5“ enthalten. Der EA verwaltet den Prozess der Bildung von Paaren aus positiven und negativen Beispielen, führt eine Trainingsiteration durch, sammelt Statistiken und speichert die trainierten Parameter.

Es ist erwähnenswert, dass es einen wichtigen – und ohne Übertreibung konzeptionellen – Unterschied zwischen unserer Implementierung des kontrastiven Lernens und traditionellen Ansätzen gibt. Wir haben uns bewusst dafür entschieden, den zeitaufwändigen Schritt der Bildung eines Trainingsdatensatzes in dieser Trainingsphase auszulassen. Stattdessen haben wir durch die Nutzung der vollen Leistung und Flexibilität der MetaTrader 5-Plattform eine dynamische Generierung von Trainingsdaten während des Trainingsprozesses implementiert.

Der Benutzer muss lediglich das Start- und Enddatum des Trainingszeitraums in den EA-Einstellungen angeben. Alle erforderlichen Marktdaten werden automatisch in Echtzeit vom Terminal angefordert und abgerufen. Dieser Ansatz vereinfacht nicht nur das Verfahren, sondern bietet auch weitaus breitere Möglichkeiten beim Training des Modells.

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input datetime             Start          = D'2020.01.01';
input datetime             End            = D'2025.01.01';
input int                  Iterations     = 100000;
input int                  Batch          = 50;
input group                "---- Indicators ----"
input ENUM_TIMEFRAMES      TimeFrame   =  PERIOD_M1;
//---
input group                "---- RSI ----"
input int                  RSIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   RSIPrice    =  PRICE_CLOSE;   //Applied price
//---
input group                "---- CCI ----"
input int                  CCIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   CCIPrice    =  PRICE_TYPICAL; //Applied price
//---
input group                "---- ATR ----"
input int                  ATRPeriod   =  14;            //Period
//---
input group                "---- MACD ----"
input int                  FastPeriod  =  12;            //Fast
input int                  SlowPeriod  =  26;            //Slow
input int                  SignalPeriod =  9;            //Signal
input ENUM_APPLIED_PRICE   MACDPrice   =  PRICE_CLOSE;   //Applied price

Als Nächstes möchte ich mir genauer ansehen, wie dieser Prozess in der Methode Train implementiert ist. Diese Methode startet den gesamten kontrastiven Lernzyklus und enthält die Logik für die Datenvorbereitung, die Pufferverwaltung sowie die Vorwärts- und Rückwärtsdurchläufe durch das Netzwerk.

Der Prozess beginnt mit der Definition der Grenzen der historischen Daten. Die Funktion iBarShift wird verwendet, um den Offset von der aktuellen Bar bis zum Beginn und Ende des Trainings zu bestimmen.

void Train(void)
  {
   int start = iBarShift(Symb.Name(), TimeFrame, Start);
   int end = iBarShift(Symb.Name(), TimeFrame, End);
   int bars = CopyRates(Symb.Name(), TimeFrame, 0, start, Rates);

Als Nächstes werden Puffer für alle Indikatoren zugewiesen, um die Länge der geladenen Historie aufzunehmen.

if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) ||
   !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }

Die nächste Phase ist das Laden der Daten. Dazu wird eine Schleife implementiert, die nacheinander die Methode Refresh aller Indikatoren aufruft und die Anzahl der berechneten Werte überprüft. Die Schleife endet entweder nach erfolgreichem Laden der Daten oder nach 100 Versuchen.

int count = -1;
bool load = false;
do
  {
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   count++;
   load = (RSI.BarsCalculated() >= bars &&
           CCI.BarsCalculated() >= bars &&
           ATR.BarsCalculated() >= bars &&
           MACD.BarsCalculated() >= bars
          );
   Sleep(100);
   count++;
  }
while(!load && count < 100);
if(!load)
  {
   PrintFormat("%s -> %d The training data has not been loaded",
                                        __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }

Am Ende des Datenvorbereitungsprozesses legen wir die gewünschte Indexreihenfolge für das Kurs-Array (ArraySetAsSeries) fest und deklarieren die notwendigen lokalen Variablen.

   if(!ArraySetAsSeries(Rates, true))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
   bars -= end + HistoryBars;
   if(bars < 0)
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
//---
   vector<float> result, target, neg_target;
   bool Stop = false;

Nun beginnt die Haupttrainingsschleife. In jeder Iteration wird zufällig eine Position aus dem Gesamtbereich ausgewählt – der Bar-Index, aus dem der Referenzzustand gebildet wird.

   uint ticks = GetTickCount();
//---
   for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch)
     {
      int posit = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
      if(!CreateBuffers(posit + end, GetPointer(bState), GetPointer(bTime)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         ExpertRemove();
         return;
        }

Für diesen Zustand werden Quelldatenpuffer erstellt und ein Vorwärtsdurchlauf durch den Encoder durchgeführt.

//--- Feed Forward
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                   (CBufferFloat*)GetPointer(bTime)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
cEncoder.getResults(Result);

Wir speichern das Ergebnis des Vorwärtsdurchlaufs als Referenz – eine Art Referenzzustand des Marktes, an dem das Modell seine Repräsentation ausrichtet. Als Nächstes, und das ist wichtig, wird erneut ein Vorwärtsdurchlauf durch den Encoder mit exakt denselben Eingabedaten durchgeführt. Hier gibt es jedoch eine Feinheit. Die in unsere Architektur integrierte Batch-Normalisierungsschicht mit hinzugefügtem Rauschen fungiert als eine milde Form der Datenerweiterung. Dies bedeutet, dass das Modell beim wiederholten Durchlauf das vorherige Ergebnis nicht mehr einfach wiederholt, sondern eine leicht veränderte, verrauschte Ausgabe erzeugt.

Genau diese Eigenschaft nutzen wir, um ein positives Paar zu bilden. Der Rückwärtsdurchlauf wird unter Verwendung der zuvor gespeicherten Referenz als Ziel durchgeführt. Auf diese Weise trainieren wir das Modell, zufälliges Rauschen zu ignorieren und sich auf aussagekräftige, stabile kontextuelle Merkmale zu konzentrieren. Einfach ausgedrückt lernt das Modell, durch das Rauschen hindurchzusehen und eine stabile Sicht auf die wichtigsten Marktmerkmale beizubehalten.

//--- Positive
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                 (CBufferFloat*)GetPointer(bTime)) ||
   !cEncoder.backProp(Result, (CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Als Nächstes kommt der vielleicht interessanteste Teil – die Bildung negativer Paare. Die Anzahl der negativen Paare wird in den EA-Parametern durch die Variable Batch festgelegt. Um die erforderliche Anzahl an negativen Paaren zu generieren, erstellen wir eine verschachtelte Schleife, nachdem wir zuvor das Referenz-Token in einen Vektor verschoben haben.

//--- Negative
if(!Result.GetData(target))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
for(int b = 0; b < Batch; b++)
  {
   int negot = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
   int count = 0;
   while(negot == posit)
     {
      negot = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
      count++;
      if(count > 100)
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }
     }
   if(Stop)
            break;

Im Körper der verschachtelten Schleife wählen wir zufällig einen weiteren Zustand aus dem Trainingsbereich aus – einen, der sich von dem zuvor verwendeten Referenzzustand unterscheidet. Hier ist ein wichtiger Punkt hervorzuheben: Wir akzeptieren jeden Zustand, solange er nicht mit dem Referenzzustand übereinstimmt. Theoretisch ist es sogar möglich, dass sich die Bereiche überschneiden.

Genau das macht den Trainingsprozess dynamischer und realistischer. Das Modell lernt, Unterschiede in sehr ähnlichen Zuständen zu erkennen. Wenn sich die Bereiche überschneiden, besteht die Aufgabe des Modells darin, genau die Nuancen hervorzuheben, die einen Zustand vom anderen unterscheiden. Unter realen Marktbedingungen sind es genau diese subtilen Unterschiede, die oft über den Erfolg einer Handelsentscheidung entscheiden.

Somit zwingt unser kontrastives Training den Encoder nicht nur dazu, Zustände zu erkennen, sondern auch Schlüsselmerkmale zu extrahieren und informative sowie unterscheidbare Tokens zu erstellen, die für den anschließenden Betrieb des Actors erforderlich sind.

Für den ausgewählten Zustand generieren wir die Quelldatenpuffer und führen einen Vorwärtsdurchlauf durch den Encoder durch.

if(!CreateBuffers(negot + end, GetPointer(bState), GetPointer(bTime)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }
//--- Feed Forward
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                   (CBufferFloat*)GetPointer(bTime)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Als Nächstes folgt die Phase der Bildung der Trainingsziele für das negative Trainingsbeispiel. Dazu berechnen wir die Differenz zwischen dem im aktuellen Schritt erhaltenen Token und der Referenzdarstellung, die wir zuvor gespeichert haben. Dies ist der entscheidende Punkt: Wir drängen das aktuelle Token von der Referenz weg und erstellen so einen Zielwert für das negative Beispiel.

Um diesen Effekt zu verstärken, verdoppeln wir den Abstand zwischen dem Referenzzustand und dem aktuellen Zustand, wodurch die abstoßende Kraft erhöht wird. Infolgedessen werden Zustände, die nahe am Referenzzustand liegen, nur geringfügig abgestoßen, während solche, die sich deutlich unterscheiden, mit weitaus größerer Intensität abgestoßen werden. Dies trägt dazu bei, den freien Raum um die Referenzrepräsentation zu erweitern, wodurch die Token unterscheidbarer und robuster gegenüber Rauschen werden.

Dieser Mechanismus führt ein dynamisches Element in den Lernprozess ein, das eine klare Trennung der latenten Räume sicherstellt. Das Modell lernt nicht einfach, Zustände zu erkennen; es lernt, eine Sicherheitszone um den Referenzzustand zu schaffen und andere, unterschiedliche Zustände in einem deutlichen Abstand zu halten. Dies verbessert die Qualität und Zuverlässigkeit der nachfolgenden Entscheidungsfindung erheblich.

cEncoder.getResults(result);
neg_target = result * 2 - target;
neg_target = neg_target - neg_target.Max();
if(!neg_target.Activation(neg_target, AF_SOFTMAX) ||
   !Result.AssignArray(neg_target))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Wir haben einen letzten, aber nicht weniger wichtigen Schritt vor uns: einen Rückwärtsdurchlauf durch den Encoder unter Verwendung des generierten negativen Ziels durchzuführen. Es ist diese Phase, die es ermöglicht, die Parameter des Modells zu optimieren und es so anzupassen, dass es unterschiedliche Zustände im latenten Raum effektiv voneinander wegdrückt.

if(!cEncoder.backProp(Result, (CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Nachdem nun alle Trainingsoperationen für diesen Schritt erfolgreich abgeschlossen wurden, hält das System den Benutzer auf dem Laufenden – der Prozessstatus wird aktualisiert. Wir geben Updates zum aktuellen Fortschritt, indem wir den Prozentsatz der Fertigstellung und die Fehlermetriken des Modells anzeigen. Dieser transparente Ansatz schafft nicht nur Vertrauen in die Leistung des Algorithmus, sondern hilft auch dabei, die Effektivität des Trainings schnell zu überwachen.

Danach gehen wir zur nächsten Iteration der Trainingsschleife über, in der der gesamte oben beschriebene Prozess mit neuen Daten erneut wiederholt wird. Dieser Zyklus stellt eine gründliche und konsistente Verbesserung des Modells sicher und ermöglicht es ihm, schrittweise immer genauere und informativere Repräsentationen aufzubauen, die für fundierte Handelsentscheidungen erforderlich sind.

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

Nach erfolgreichem Abschluss aller Iterationen der Trainingsschleifen protokollieren wir die Trainingsergebnisse. Als Nächstes wird der Prozess zum Herunterfahren des Programms eingeleitet.

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

Dies ist eine saubere und kontrollierte Beendigung der Sitzung, die sicherstellt, dass alle wichtigen Daten erhalten bleiben und Ressourcen ordnungsgemäß freigegeben werden.

Der vollständige Quellcode des Programms für das kontrastive Training des Encoders ist im Anhang enthalten. Im selben Archiv finden Sie Implementierungen der Offline- und Online-Trainingsprogramme für die Modelle Actor, Director und Critic. Diese Programme wurden mit minimalen Änderungen aus früheren Entwicklungen portiert; insbesondere wurde der Trainingsprozess für den Encoder entfernt, da er nun separat implementiert ist. Dieser Ansatz erleichtert die Organisation des Trainings und erhöht die Flexibilität sowie die Wartbarkeit des gesamten Frameworks.


Tests

Wir haben den Trainingsprozess selbst in drei aufeinanderfolgende Phasen unterteilt, um einen systematischen und zuverlässigen Arbeitsablauf für das gesamte Modell zu gewährleisten.

Die erste Phase ist das kontrastive Training des Encoders. Es wird auf historischen Daten der letzten fünf Jahre für das Währungspaar EURUSD auf dem Zeitrahmen M1 durchgeführt. Dieser Umfang und Detaillierungsgrad der Daten ermöglichen es dem Encoder, qualitativ hochwertige, informative latente Repräsentationen des Marktzustands zu entwickeln, die als Grundlage für den weiteren Betrieb des gesamten Systems dienen.

Als Nächstes folgt die zweite Phase – das Offline-Training der Schlüsselkomponenten des Systems: des Actors, des Directors und des Critics. Dazu wird ein Trainingsdatensatz verwendet, der aus Daten von 2024 gesammelt wurde, wobei alle zuvor festgelegten Parameter beibehalten werden. Der Prozess nutzt das Konzept einer nahezu perfekten Trajektorie, was es den Modellen ermöglicht, aus den zuverlässigsten Beispielen für Aktionen und Bewertungen zu lernen. Diese Phase ist wichtig für die Stärkung grundlegender Strategien und Entscheidungskriterien.

Die dritte Phase ist das Online-Fine-Tuning der Modelle, das direkt im Strategietester auf demselben historischen Intervall durchgeführt wird. Dies ermöglicht es, die Modelle an veränderte Marktbedingungen anzupassen und die Parameter so genau wie möglich feinabzustimmen.

Sobald alle Trainingsphasen abgeschlossen sind, wird das Modell anhand von Daten von Januar bis März 2025 getestet. Gleichzeitig bleiben alle während der Trainingsphasen verwendeten Parameter unverändert. Dieser Ansatz gewährleistet eine faire und objektive Bewertung der Modellleistung anhand eines neuen, bisher nicht verwendeten Datensatzes. Die Testergebnisse sind wie folgt:

Während des Testzeitraums führte das Modell 881 Trades aus, von denen 447 profitabel waren, was einer Erfolgsquote von 50,74 % entspricht. Dies deutet auf ein neutrales Gleichgewicht zwischen profitablen und verlustreichen Trades hin. Der Profitfaktor (Profit Factor) liegt bei 1,25.

Insgesamt hat die Strategie positive Ergebnisse mit moderatem Risiko und einem stetigen Wachstum der Equity während der ersten Hälfte des Testzeitraums geliefert. Nach Mitte Februar geht die Rentabilität jedoch zurück, und es ist eine deutliche Seitwärtsbewegung mit sinkendem Equity zu beobachten.

Somit ist die Strategie tragfähig und zeigt eine positive Performance über das Testintervall; einige Parameter bedürfen jedoch einer Verbesserung.


Schlussfolgerung

Als Ergebnis unserer Arbeit haben wir das Mantis-Framework vollständig in ein angepasstes algorithmisches Handelsmodell integriert, das in der Lage ist, informative Merkmale aus hochfrequenten Zeitreihen zu extrahieren und diese in Echtzeit in sinnvolle Entscheidungen umzuwandeln.

Besondere Aufmerksamkeit wurde der selbstüberwachten kontrastiven Lernphase des Encoders gewidmet, die im EA StudyContrast.mq5 implementiert ist. Wir haben uns von einem statischen Datensatz entfernt, indem wir ein dynamisches Laden der Daten aus dem Terminal implementiert und diesen mit realen Beispielen der letzten fünf Jahre für EURUSD im M1-Zeitrahmen angereichert. Positive und negative Paare, die durch Soft-Augmentation und kontrolliertes Rauschen generiert wurden, ermöglichten es dem Modell in der Praxis, durch das Marktrauschen hindurchzusehen.

Der abschließende Test des Modells im Zeitraum Januar–März 2025 belegte die Rentabilität der implementierten Lösungen. Die Strategie ist tragfähig und zeigt im Testintervall eine positive Performance; einige Parameter bedürfen jedoch einer Verbesserung.


Referenzen


Im Artikel verwendete Programme

# Name Typ Beschreibung
1 Research.mq5 EA EA zum Sammeln von Beispielen
2 ResearchRealORL.mq5
EA
EA zum Sammeln von Beispielen mit der Real-ORL-Methode
3 StudyContrast.mq5 EA EA für das kontrastive Training des Encoders
4 Study.mq5 EA EA für das Offline-Modelltraining
5 StudyOnline.mq5
EA
EA für das Online-Modelltraining
6 Test.mq5 EA EA zum Testen des Modells
7 Trajectory.mqh Klassenbibliothek Struktur zur Beschreibung des Systemzustands und der Modellarchitekturen
8 NeuroNet.mqh Klassenbibliothek Klassenbibliothek zur Erstellung eines neuronalen Netzes
9 NeuroNet.cl Bibliothek OpenCL-Bibliothek


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

Beigefügte Dateien |
MQL5.zip (2794.71 KB)
Letzte Kommentare | Zur Diskussion im Händlerforum (1)
[Gelöscht] | 30 Mai 2025 in 17:49
Seltsam, warum wird nur in eine Richtung gehandelt? Und wie entstehen die Signale zum Schließen von Positionen?
Von der Grundstufe bis zur Mittelstufe: Direkter Dateizugriff (I) Von der Grundstufe bis zur Mittelstufe: Direkter Dateizugriff (I)
Im heutigen Artikel werden wir zum ersten Mal den wahlfreien Zugriff auf Dateiinhalte untersuchen. Dies gilt sowohl für das Schreiben als auch für das Lesen von Informationen, die in einer Datei gespeichert sind. Da das Thema jedoch zu umfangreich ist, um es in einem einzigen Artikel zu behandeln, beschränken wir uns hier auf eine Einführung in den wahlfreien Zugriff.
Gauß-Prozesse im maschinellen Lernen (Teil 1): Klassifikationsmodell in MQL5 Gauß-Prozesse im maschinellen Lernen (Teil 1): Klassifikationsmodell in MQL5
Der Artikel befasst sich mit der Klassifikation mittels Gauß-Prozessen. Wir beginnen mit dem Studium der theoretischen Grundlagen und gehen dann zur praktischen Entwicklung der GP-Bibliothek in MQL5 über.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Implementierung eines Breakeven-Mechanismus in MQL5 (Teil 2): ATR- und RRR-basierter Breakeven Implementierung eines Breakeven-Mechanismus in MQL5 (Teil 2): ATR- und RRR-basierter Breakeven
Dieser Artikel vervollständigt die Implementierung von ATR- und RRR-basierten Breakeven-Mechanismen in MQL5 und entwickelt eine Klasse von Grund auf neu, die es einfach macht, zwischen Breakeven-Modi zu wechseln, ohne die Parameter erneut eingeben zu müssen. Um die Wirksamkeit jedes Breakeven-Typs zu bewerten, werden mehrere Backtests durchgeführt, bei denen deren Vor- und Nachteile im Kontext des algorithmischen Handels analysiert werden.