English Русский 中文 Español 日本語 Português
preview
Neuronale Netze im Trading: Actor–Director–Critic (Letzter Teil)

Neuronale Netze im Trading: Actor–Director–Critic (Letzter Teil)

MetaTrader 5Handelssysteme |
26 2
Dmitriy Gizlyk
Dmitriy Gizlyk

Einführung

Im vorherigen Artikel haben wir die theoretischen Aspekte des Frameworks Actor–Director–Critic kennengelernt, einer erweiterten Version der Architektur Actor–Critic. Die klassische Architektur von Actor–Critic ist zu einem Eckpfeiler vieler erfolgreicher Algorithmen des verstärkenden Lernens (RL) geworden. In diesem Aufbau ist der Agent in zwei Komponenten unterteilt: Der Actor schlägt Aktionen vor, während der Critic sie auf der Grundlage von Belohnungen bewertet, die er aus der Umgebung erhält. Diese Kombination ermöglicht die schrittweise Entwicklung einer Strategie, bei der Aktionen zunehmend zielgerichteter und Bewertungen fortschreitend genauer werden. Trotz ihrer Eleganz und Effektivität stößt diese Architektur jedoch in realen Anwendungen, insbesondere im Handel, auf mehrere signifikante Einschränkungen.

Das Hauptproblem tritt in den frühen Phasen des Trainings auf. Zu diesem Zeitpunkt hat der Actor noch nicht gelernt, welche Aktionen vorteilhaft sind, während der Critic nicht in der Lage ist, sie angemessen zu bewerten, da er ebenfalls gerade erst mit dem Lernen beginnt. Dies erzeugt eine Art blindes Umhertasten: Der Agent führt zahlreiche zufällige, ineffiziente und manchmal sogar schädliche Aktionen aus, während er uninformative oder verzögerte Rückmeldungen erhält. In Marktumgebungen, in denen Fehler kostspielig sein können, wird ein solcher Ansatz übermäßig riskant und instabil.

Um dieses grundlegende Problem anzugehen, führten die Autoren des Frameworks von Actor–Director–Critic eine zusätzliche Komponente ein – den Director –, der dem System einen zusätzlichen Bewertungskanal hinzufügt. Im Gegensatz zum Critic, der eine kontinuierliche Bewertung einer Aktion auf der Grundlage von Belohnungen aus der Umgebung erstellt, klassifiziert der Director Aktionen binär: ob sie zur gegebenen Strategie passen oder nicht. Dies ermöglicht es dem Agenten, den Entscheidungsraum schneller und effektiver zu durchqueren.

Es ist wichtig zu betonen, dass der Director kein Filter ist und die Handlungsfreiheit des Agenten nicht einschränkt. Im Gegenteil, er ergänzt die Bewertung des Critics, indem er kategorischere Rückmeldungen liefert. Während der Critic sich über die Qualität einer Aktion unsicher sein mag (insbesondere in den frühen Phasen des Trainings), kann der Director sofort anzeigen, ob die Aktion dem Verhaltensmodell entspricht, das er gelernt hat. Dies hilft dem Actor, die Wiederholung von Aktionen zu vermeiden, die bereits als ungeeignet oder falsch eingestuft wurden, wodurch Trainingsressourcen geschont und die Entwicklung einer robusten Strategie beschleunigt werden.

Das Ergebnis ist eine synergetische Interaktion zwischen drei Komponenten: Der Actor lernt, Aktionen auszuwählen, der Critic lernt, sie im Hinblick auf die erwartete Belohnung zu bewerten, und der Director identifiziert explizit Aktionen, die gänzlich vermieden werden sollten. Dies schafft ein duales Feedback-System – kontinuierlich und binär –, das es dem Agenten ermöglicht, wenig vielversprechende Richtungen schneller zu eliminieren und sich auf produktive Strategien zu konzentrieren.

Die Visualisierung des Actor–Director–Critic-Frameworks durch die Autoren wird unten dargestellt.

Die Visualisierung des Frameworks Actor–Director–Critic durch die Autoren

Im praktischen Teil des vorherigen Artikels wurde eine detaillierte Beschreibung der Architektur der trainierbaren Modelle bereitgestellt. Es ist anzumerken, dass im Vergleich zur ursprünglichen Implementierung des Frameworks durch die Autoren einige wesentliche Modifikationen eingeführt wurden.

In erster Linie haben wir die vorgeschlagenen Konzepte an das zuvor diskutierte Multi-Agenten-Framework HiSSD angepasst. Darüber hinaus werden in unserer Implementierung sowohl der Director als auch der Critic auf latenten Repräsentationen der gemeinsamen Fähigkeiten des Agenten trainiert – komprimierte Merkmale, die aus den internen Schichten des Umgebungszustands-Encoders extrahiert wurden, anstatt auf den rohen Umgebungsmerkmalen selbst. Dieser Ansatz ermöglicht die Bildung einer verallgemeinerten Repräsentation von Handlungsmustern, wodurch Aktionen im Kontext einer globalen Verhaltenslogik bewertet werden können anstatt nur im Kontext einer konkreten Marktsituation. Infolgedessen erhält das System ein stabileres und strategisch aussagekräftigeres Feedback, was unter Bedingungen begrenzter Informationen und hoher Unsicherheit, wie sie an Finanzmärkten häufig vorkommen, von entscheidender Bedeutung ist.

In diesem Artikel verlagert sich der Fokus auf das Modelltraining. Auch in diesen Prozess wurden im Vergleich zum ursprünglichen Ansatz einige Modifikationen eingeführt.

Einer der wesentlichen Unterschiede ist die Aufteilung des Trainings in zwei Phasen. Während der ersten Phase durchlaufen alle Systemkomponenten ein Offline-Training unter Verwendung eines zuvor gesammelten Trainingsdatensatzes. Diese Phase ermöglicht es dem Agenten, erste Erfahrungen zu sammeln und eine grundlegende Verhaltensstrategie zu etablieren, ohne den Live-Handelsbetrieb zu beeinträchtigen. Jedes Modell wird unter Verwendung seiner eigenen Zielfunktion trainiert, die speziell auf seine Rolle innerhalb der Architektur zugeschnitten ist.

Die zweite Phase besteht aus einem Online-Feintuning. An diesem Punkt beginnt der Agent, direkt mit der Umgebung zu interagieren. Der Actor verfeinert seine Policy in Echtzeit, während der Critic und der Director weiter lernen und die Qualität ihrer Bewertungen und Klassifizierungen auf Basis aktualisierter Daten verbessern. Diese Phase ermöglicht es dem Agenten, sich an die aktuellen Marktbedingungen anzupassen und gleichzeitig die während des Offline-Trainings festgelegte strategische Ausrichtung beizubehalten.

Dieser zweistufige Ansatz bietet ein Gleichgewicht zwischen Stabilität und Anpassungsfähigkeit. Der Agent entwickelt zunächst eine zuverlässige Anfangsstrategie und verfeinert diese anschließend unter realen Bedingungen. Als Ergebnis erwarten wir ein robustes und effizient trainierbares System, das in der Lage ist, sich an verändernde Marktdynamiken anzupassen, ohne die strategische Konsistenz zu opfern.


Offline-Training

Der Algorithmus für die erste Trainingsphase (Offline-Training) ist als der Expert Advisor …\Experts\ADC\Study.mq5 implementiert. Der Großteil seines Codes wurde vom vorherigen Projekt übernommen, was nicht überraschend ist. Das architektonische Design der trainierbaren Modelle basiert weitgehend auf Konzepten, die innerhalb des Frameworks HiSSD entwickelt wurden, mit dem wir bereits vertraut sind.

Diese Kontinuität ermöglichte es uns, bewährte Lösungen zu nutzen, ohne das Rad neu zu erfinden. Dennoch waren einige Modifikationen unvermeidlich.

Die Hinzufügung von zwei neuen Komponenten (Critic und Director) erweiterte die Funktionalität des Systems erheblich. Deren Integration erforderte Änderungen an der Logik des Modelltrainings. Im Rahmen dieses Artikels werden wir uns ausschließlich auf die Train-Methode konzentrieren, die nahezu den gesamten Modelltrainingsprozess enthält.

Wie zuvor akzeptiert diese Methode keine Parameter und bezieht alle erforderlichen Daten aus zuvor initialisierten globalen Variablen. Zu Beginn der Methode werden eine Reihe lokaler Variablen erstellt und zur temporären Speicherung von Zwischeninformationen während des Trainings initialisiert.

void Train(void)
  {
//---
   vector<float> probability = vector<float>::Full(Buffer.Size(), 1.0f / Buffer.Size());
//---
   vector<float> result, target, state;
   matrix<float> fstate = matrix<float>::Zeros(1, NForecast * BarDescr);
   bool Stop = false;
//---
   uint ticks = GetTickCount();

Sobald die Vorbereitungsphase abgeschlossen ist, gehen wir zum eigentlichen Trainingsprozess über, der als System verschachtelter Schleifen organisiert ist. Die äußere Schleife iteriert über Mini-Batches für eine festgelegte Anzahl von Trainingsiterationen.

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

In der Anfangsphase des Offline-Trainings wird eine einzelne Trajektorie aus dem Experience-Replay-Puffer entnommen. Ein spezifischer Umgebungszustand wird dann zufällig aus dieser Trajektorie ausgewählt und als Ausgangspunkt für die Erstellung eines neuen Mini-Batches von Trainingsdaten verwendet. Dieser Ansatz stellt die Vielfalt der Trainingsbeispiele sicher.

Vor der Verarbeitung des neu gebildeten Mini-Batches führen wir ein obligatorisches Zurücksetzen der internen temporären Datenpuffer aller trainierbaren Modelle durch. Dies ist besonders wichtig für rekurrente Module, die bekanntlich über ein Gedächtnis verfügen und kontextbezogene Informationen aus früheren Zuständen beibehalten können, wodurch Wissen über frühere Zeitschritte hinweg akkumuliert wird. Wenn jedoch zwischen nicht zusammenhängenden Trajektoriensegmenten gewechselt wird, kann dieser Speicher problematisch werden.

Der in den verborgenen Zuständen des vorherigen Mini-Batches gespeicherte Kontext ist nicht mehr relevant. Er hat keine Beziehung zum neuen Beispiel und kann die generierten Signale verzerren. Daher ist das Zurücksetzen temporärer Zustände vor der Verarbeitung jedes neuen Mini-Batches nicht nur eine Vorsichtsmaßnahme – es ist eine notwendige Bedingung für die korrekte und unabhängige Analyse des aktuellen historischen Datensegments.

if(
   !cEncoder.Clear()
   || !cTask.Clear()
   || !cActor.Clear()
   || !cProbability.Clear()
   || !cDirector.Clear()
   || !cCritic.Clear()
)
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
result = vector<float>::Zeros(NActions);

Nach der Initialisierung des Mini-Batches fahren wir mit der nächsten Stufe fort: der Organisation einer verschachtelten Schleife, die sequenziell durch die Umgebungszustände iteriert. Diese Zustände werden strikt in chronologischer Reihenfolge verarbeitet, genau so, wie sie ursprünglich vom Agenten während seiner Interaktion mit der Handelsumgebung beobachtet wurden.

Die Wahrung der historischen Reihenfolge ist nicht einfach eine Formalität, sondern eine grundlegende Anforderung für das effektive Training rekurrent-modellbasierter Komponenten. Im Gegensatz zu herkömmlichen vollständig verbundenen Schichten, die auf isolierten Eingaben operieren, halten rekurrente neuronale Netze verborgene Zustände aufrecht, die auf akkumulierten Informationen aus vorherigen Zeitschritten basieren. Ihre Stärke liegt in ihrer Fähigkeit, zeitliche Abhängigkeiten, Verhaltensmuster und wiederkehrende Signale innerhalb von Zeitreihendaten zu identifizieren.

Wenn die zeitliche Struktur der Daten gestört wird – auch nur teilweise –, kann das Modell seine Fähigkeit verlieren, kausale Zusammenhänge zu erkennen, die spezifisch durch zeitliche Dynamiken entstehen. Folglich wird der Wert solcher Trainingsbeispiele erheblich gemindert.

Aus diesem Grund wird jeder Zeitschritt innerhalb eines Mini-Batches strikt nacheinander verarbeitet – ohne Mischen, Auslassungen oder Abweichungen. Dieser Ansatz ermöglicht es rekurrenten Komponenten, schrittweise eine interne kontextuelle Repräsentation aufzubauen und Wissen über die Entwicklung des Marktumfelds aufzubauen.

for(int i = start; i < MathMin(Buffer[tr].Total, start + Batch); i++)
  {
   if(!state.Assign(Buffer[tr].States[i].state) ||
      MathAbs(state).Sum() == 0 ||
      !bState.AssignArray(state))
     {
      iter -= Batch + start - i;
      break;
     }
   //---

Innerhalb des Körpers der verschachtelten Schleife laden wir die Beschreibung des analysierten Umgebungszustands aus dem Experience-Replay-Puffer und generieren die entsprechenden zeitbezogene harmonische Merkmale.

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

Als Nächstes konstruieren wir den Repräsentationsvektor des Kontozustands.

//--- Account
float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0];
float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1];
float profit = float(bState[0] / _Point * (result[0] - result[3]));
bAccount.Clear();
bAccount.Add(1);
bAccount.Add((PrevEquity + profit) / PrevEquity);
bAccount.Add(profit / PrevEquity);
bAccount.Add(MathMax(result[0] - result[3], 0));
bAccount.Add(MathMax(result[3] - result[0], 0));
bAccount.Add((bAccount[3] > 0 ? profit / PrevEquity : 0));
bAccount.Add((bAccount[4] > 0 ? profit / PrevEquity : 0));
bAccount.Add(0);
bAccount.AddArray(GetPointer(bTime));
if(bAccount.GetIndex() >= 0)
   bAccount.BufferWrite();

An dieser Stelle ist es wichtig, eine methodische Technik hervorzuheben, die während des Offline-Trainings eingesetzt wird. Dies bezieht sich auf die Verwendung einer sogenannten „nahezu perfekten Trajektorie“. Bei diesem Ansatz lockern wir bewusst die chronologische Isolation, was es dem Algorithmus ermöglicht, zukünftige Umgebungszustände vorwegzunehmen, die bereits im Trainingsdatensatz verfügbar sind.

Unter Verwendung dieser Informationen konstruieren wir einen verbesserten Aktionstensor, der strategisch fundiertere Entscheidungen widerspiegelt. Natürlich werden sich diese Entscheidungen mit hoher Wahrscheinlichkeit von den Aktionen unterscheiden, die der Agent ursprünglich während seiner Interaktion mit dem Markt getroffen hat. Anstatt das Verhalten des Agenten zu replizieren, dient dieser Tensor als approximativer Maßstab, dem sich der Agent während des Trainings annähern sollte. Genau deshalb bezeichnen wir sie als „beinahe perfekte“ Trajektorie.

Eine solche Strategie schafft jedoch zwangsläufig Diskrepanzen zwischen den tatsächlichen Aktionen, die vom Agenten ausgeführt werden, und den idealisierten Aktionen, die retrospektiv generiert werden. Infolgedessen müssen auch entsprechende Anpassungen an anderen Teilen des Modells vorgenommen werden.

Insbesondere müssen wir den Repräsentationsvektor des Kontozustands neu berechnen, damit er mit den Aktionen konsistent bleibt, die aus der beinahe perfekten Trajektorie abgeleitet wurden. Andernfalls würde der Agent mit inkonsistenten Daten trainiert, bei denen Aktionen und ihre Konsequenzen nicht mehr miteinander korrespondieren. Solche Inkonsistenzen würden die Trainingssignale zwangsläufig verzerren.

Nach der Vorbereitung des Eingabedatensatzes führen wir nacheinander die Forward-Pass-Methoden der trainierbaren Modelle aus, während derer verschiedene vorhergesagte Werte generiert werden. Das Ziel des Modelltrainings ist es, die Abweichung zwischen diesen vorhergesagten Werten und den gewünschten Zielergebnissen zu minimieren.

//--- Feed Forward
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cTask.feedForward((CBufferFloat*)GetPointer(bState), 1, false, GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cActor.feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, GetPointer(cTask), -1))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cProbability.feedForward(GetPointer(cEncoder), LatentLayer, (CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Es ist erwähnenswert, dass wir in dieser Phase keine Forward-Passes durch die Aktionsbewertungsmodelle des Agenten durchführen – Critic und Director. Dies ist eine direkte Konsequenz des Ansatzes der „beinahe perfekten Trajektorie“. Wir werden jedoch in Kürze auf dieses Thema zurückkommen.

Sobald die Feed-Forward-Operationen der trainierbaren Modelle erfolgreich abgeschlossen sind, fahren wir mit der Generierung der Zielwerte fort. Zunächst wird eine Sequenz zukünftiger Umgebungszustände über einen vordefinierten Planungshorizont aus dem Experience-Replay-Puffer geladen.

//--- Look for target
target = vector<float>::Zeros(NActions);
bActions.AssignArray(target);
if(!state.Assign(Buffer[tr].States[i + NForecast].state) ||
   !state.Resize(NForecast * BarDescr) ||
   MathAbs(state).Sum() == 0)
  {
   iter -= Batch + start - i;
   break;
  }
if(!fstate.Resize(1, NForecast * BarDescr) ||
   !fstate.Row(state, 0) ||
   !fstate.Reshape(NForecast, BarDescr))
  {
   iter -= Batch + start - i;
   break;
  }
for(int j = 0; j < NForecast / 2; j++)
  {
   if(!fstate.SwapRows(j, NForecast - j - 1))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }
  }

Diese Daten dienen als Zielwerte für unseren Umgebungszustands-Encoder. Folglich können wir eine Backpropagation durch dieses Modell durchführen, um dessen Parameter zu optimieren und die Diskrepanz zwischen vorhergesagten und Zielwerten zu minimieren.

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

Die Informationen über zukünftige Umgebungszustände bilden zudem die Grundlage für die Konstruktion des „beinahe perfekten“ Handelsoperationstensors. Wichtig ist, dass jede neue Operation unter Berücksichtigung der vorherigen generiert wird, was es uns ermöglicht, eine kohärente Handelsstrategie anstelle von isolierten impulsiven Aktionen zu modellieren. Im Ergebnis entsteht eine kontextuell verknüpfte Handelssequenz, in der jede nachfolgende Aktion durch die Logik der vorangegangenen bestimmt wird. Dies ist besonders wichtig während des Offline-Trainings, bei dem der Agent kein Echtzeit-Feedback aus der Umgebung erhält und stattdessen Muster aus bereits aufgezeichneten Daten extrahieren muss.

Diese Methode hilft dabei, den Einfluss von Marktgeräuschen zu mindern, die sich in realen Daten durch zufällige Schwankungen äußern. Innerhalb der „nahezu perfekten“ Trajektorie wird das Verhalten des Agenten flüssiger, rationaler und strategisch konsistenter. Potenziell kann dies das Modelltraining beschleunigen und gleichzeitig eine robuste Handelslogik fördern, die später während des Online-Betriebs angepasst werden kann.

Der Prozess der Konstruktion einer „nahezu perfekten“ Handelsoperation wurde im vorherigen Artikel ausführlich beschrieben, und es ist wenig sinnvoll, ihn hier zu wiederholen.

Der resultierende Tensor einer „nahezu perfekten“ Handelsoperation dient als Zielwert für das Training unseres übergeordneten Actor–Moduls.

//--- Actor Policy
if(!cActor.backProp(GetPointer(bActions), (CNet*)GetPointer(cTask), -1))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

In dieser Phase gehen wir zur wichtigsten Komponente der Trainingsarchitektur über – dem Training der Aktionsbewertungsmodelle des Actors, und zwar Critic und Director. Diese Komponenten sind für die Generierung der Feedback-Signale verantwortlich, die die Strategie des Agenten leiten und ihm helfen, produktive Aktionen von ineffektiven zu unterscheiden.

Hier stoßen wir jedoch auf eine grundlegende Herausforderung: Der Raum der möglichen Aktionen in einem gegebenen Umgebungszustand ist extrem groß. Die Bewertung der gesamten Menge an Entscheidungen, die der Actor potenziell treffen könnte, ist mit vertretbaren Rechenressourcen praktisch nicht durchführbar. Darüber hinaus werden viele dieser Aktionen vom Agenten nie tatsächlich ausgeführt und bieten daher kaum oder gar keinen Trainingswert.

Aus diesem Grund folgt das Training des Critic in der Regel einer lokalen Bewertungsstrategie. Anstatt zu versuchen, den gesamten Aktionsraum abzudecken, konzentrieren wir uns auf eine Umgebung der tatsächlich getroffenen Entscheidungen, die der Agent zum aktuellen Zeitschritt getroffen hat. Innerhalb dieses lokalen Unterraums werden Optimierungsrichtungen gesucht – Vektorverschiebungen, die möglicherweise die Zielfunktion erhöhen und folglich die Rentabilität verbessern können.

Hier bietet der Ansatz der „nahezu perfekten Trajektorie“ einen erheblichen Vorteil. Da wir Zugriff auf ein Referenzverhalten haben, das durch begrenzten Vorausblick in zukünftige Zustände generiert wurde, können wir den Fokus der Bewertung von dem, was der Agent tatsächlich getan hat, auf das verlagern, was er hätte tun sollen. Mit anderen Worten: Wir trainieren den Critic nicht nur, um gute von schlechten Aktionen zu unterscheiden, sondern auch, um seine Bewertungen an Aktionen auszurichten, die dem strategischen Ideal nahekommen, das durch die „nahezu perfekte“ Trajektorie repräsentiert wird.

Innerhalb dieses Paradigmas führen wir einen Vorwärtsdurchlauf durch den Critic durch, um die „nahezu perfekte“ Handelsoperation zu bewerten.

//--- Critic
if(!cCritic.feedForward(GetPointer(bActions), 1, false, (CNet*)GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Anschließend wird sofort ein Backpropagation-Durchlauf ausgeführt, um die Abweichung zwischen der Bewertung des Critic und dem aus tatsächlichen historischen Marktdaten berechneten Wert zu minimieren.

float reward = float((result[0] - result[3]) * fstate[0, 0] / Point());
Result.Clear();
if(!Result.Add(reward)
   || !cCritic.backProp(Result, (CNet*)GetPointer(cEncoder), LatentLayer)
   || !cEncoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer, true)
  )
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Der Trainingsprozess des Directors ist konzeptionell ähnlich dem des Critics, weist jedoch einige wichtige Unterschiede auf. Wie jeder Klassifikator benötigt der Director einen ausgewogenen Satz positiver und negativer Beispiele, aus denen er die Grenze zwischen „guten“ und „schlechten“ Agentenaktionen lernen kann.

Die positiven Beispiele sind relativ unkompliziert. Diese bestehen aus den Handelsoperationen, die im Rahmen der „nahezu perfekten“ Trajektorie generiert wurden. Solche Aktionen stellen strategisch fundierte Entscheidungen dar, die aus der Analyse zukünftiger Umgebungszustände abgeleitet wurden, und sollten daher als positiv klassifiziert werden.

Ein zuverlässiger Klassifikator kann jedoch nicht allein mit positiven Beispielen trainiert werden. Er erfordert zudem einen repräsentativen Pool an negativen Beispielen, die ein Verhalten aufweisen, das nicht mit unseren Zielen vereinbar ist. Dies führt zu einer methodischen Herausforderung: Der Trainingsdatensatz deckt naturgemäß nicht den gesamten Raum potenzieller Aktionen ab. Er enthält nur diejenigen Entscheidungen, die der Agent in der Vergangenheit tatsächlich getroffen hat.

Um dieses Problem anzugehen, haben wir eine einfache, aber effektive Heuristik angewandt. Als negative Beispiele verwenden wir Mengen von Zufallswerten, die innerhalb des zulässigen Bereichs des Aktionsraums des Agenten generiert wurden. Mit hoher Wahrscheinlichkeit stimmen solche zufälligen Aktionen nicht mit den strategischen Zielen des Modells überein und können daher als Verhaltensrauschen oder Fehler betrachtet werden.

Es ist wichtig zu beachten, dass der Wechsel zwischen positiven und negativen Beispielen während des Trainings ebenfalls randomisiert ist. Dies verhindert, dass das Modell eine Überanpassung an eine der beiden Kategorien vornimmt, und trägt dazu bei, eine robustere Klassifizierungsgrenze zu etablieren. Ein solcher Ansatz macht den Director flexibler und verallgemeinerungsfähiger, während er schärfere und verlässlichere Signale erzeugt – eine besonders wertvolle Eigenschaft angesichts der hohen Unsicherheit, die für Finanzmärkte charakteristisch ist.

Als Ergebnis wird der Director zu einer leistungsstarken binären Orientierungshilfe, die in der Lage ist, unproduktive Aktionen entschieden abzulehnen und den Agenten in vielversprechendere Bereiche des strategischen Entscheidungsraums zu lenken.

//--- Director
Result.Clear();
if((MathRand() / 32767.0) > 0.5)
   Result.Add(1);
else
  {
   target = vector<float>::Zeros(NActions);
   for(int i = 0; i < NActions; i++)
      target[i] = float(MathRand() / 32767.0);
   bActions.AssignArray(target);
   Result.Add(0);
  }
if(!cDirector.feedForward(GetPointer(bActions), 1, false, (CNet*)GetPointer(cEncoder), LatentLayer)
   || !cDirector.backProp(Result, (CNet*)GetPointer(cEncoder), LatentLayer)
   || !cEncoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer, true)
  )
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Als Nächstes trainieren wir den niedrigstufigen Controller und das Vorhersagemodell, das für die Schätzung der Wahrscheinlichkeiten der zukünftigen Preisbewegungsrichtung verantwortlich ist. Diese Codeblöcke wurden unverändert aus dem vorherigen Projekt übernommen, in dem sie detailliert beschrieben wurden. Daher schlage ich vor, sie zum eigenständigen Studium zu belassen. Der vollständige Quellcode des Expert Advisors für das Offline-Modelltraining befindet sich im Anhang.



Online-Training

Die nächste Phase unserer Arbeit ist die Feinabstimmung der trainierbaren Modelle in einer Online-Umgebung, in der wir auf eine völlig andere Klasse von Herausforderungen und Einschränkungen stoßen. Während die Offline-Phase auf tiefgreifendem, verallgemeinertem Lernen auf Basis historischer Daten und dem Ansatz der „nahezu perfekten Trajektorie“ beruhte, konzentriert sich die Online-Phase ausschließlich auf die Anpassung an Echtzeitbedingungen.

Einer der Hauptvorteile des Online-Lernens ist die direkte Verfügbarkeit von Feedback aus der Umgebung. Der Actor trifft eine Entscheidung, die Aktion wird ausgeführt und ihr Ergebnis wird fast unmittelbar bekannt. Dies ermöglicht es, das Verhalten des Agenten zeitnah anzupassen, erfolgreiche Strategien zu verstärken und ineffektive zu verwerfen.

Dieser Ansatz ist jedoch nicht ohne erhebliche Einschränkungen. Die wichtigste davon ist die Unfähigkeit, in zukünftige Zustände vorauszublicken, wie es bei der Konstruktion der „nahezu perfekten Trajektorie“ während des Offline-Trainings möglich war. In der Online-Umgebung muss der Agent Entscheidungen ausschließlich auf Basis des aktuellen Zustands und seiner bestehenden Policy treffen, ohne Zugriff auf „Wissen aus der Zukunft“. Dies verändert die Lernbedingungen grundlegend und erfordert einen Übergang zu einem klassischeren Reinforcement-Learning-Paradigma.

Das Lernen erfolgt daher durch Versuch und Irrtum, und die Qualität der Entscheidungen kann nur retrospektiv bewertet werden. Infolgedessen nimmt die Bedeutung der Bewertungsmodelle – Critic und Director – erheblich zu. Diese Komponenten dienen als internes Beratungssystem für den Actor und leiten dessen Verhalten. Im Gegensatz zur Offline-Phase werden sie jedoch nun während der Live-Handelsaktivität kontinuierlich feinabgestimmt und passen sich in Echtzeit an sich ändernde Marktbedingungen an.

Lassen Sie uns die Implementierung Schritt für Schritt untersuchen. Der Online-Trainingsalgorithmus ist innerhalb des Expert Advisors ...\Experts\ADC\StudyOnline.mq5 implementiert. Aufgrund des begrenzten Umfangs dieses Artikels konzentrieren wir uns nur auf eine detaillierte Untersuchung der OnTick-Methode. Diese Methode verarbeitet eingehende Tick-Ereignisse. Hier werden wir auch den Haupttrainingsalgorithmus implementieren.

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

Zunächst ist es wichtig anzumerken, dass unsere Modelle historische Daten nur auf Basis geschlossener Bars analysieren und nicht darauf ausgelegt sind, auf jeden eingehenden Tick zu reagieren. Folglich besteht bis zum Schluss der nächsten Bar keine Notwendigkeit, eine detaillierte Analyse des Umgebungszustands durchzuführen. Das Ergebnis würde unverändert bleiben. Um unnötige Berechnungen zu minimieren, beginnt die Methode daher mit der Überprüfung, ob eine neue Bar geschlossen wurde. Falls nicht, warten wir auf den nächsten Tick.

Sobald eine neue Bar geschlossen wurde, fordern wir historische Daten vom Terminal über eine bestimmte Tiefe an und erstellen die Eingabedatenpuffer.

   int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, 1), HistoryBars, Rates);
   if(!ArraySetAsSeries(Rates, true))
      return;
//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   Symb.Refresh();
   Symb.RefreshRates();
//---
   float atr = 0;
   for(int b = 0; b < (int)HistoryBars; b++)
     {
      float open = (float)Rates[b].open;
      float rsi = (float)RSI.Main(b);
      float cci = (float)CCI.Main(b);
      atr = (float)ATR.Main(b);
      float macd = (float)MACD.Main(b);
      float sign = (float)MACD.Signal(b);
      if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE ||
         macd == EMPTY_VALUE || sign == EMPTY_VALUE)
         continue;
      //---
      int shift = b * BarDescr;
      sState.state[shift] = (float)(Rates[b].close - open);
      sState.state[shift + 1] = (float)(Rates[b].high - open);
      sState.state[shift + 2] = (float)(Rates[b].low - open);
      sState.state[shift + 3] = (float)(Rates[b].tick_volume / 1000.0f);
      sState.state[shift + 4] = rsi;
      sState.state[shift + 5] = cci;
      sState.state[shift + 6] = atr;
      sState.state[shift + 7] = macd;
      sState.state[shift + 8] = sign;
     }
//---

Anschließend laden wir Informationen über den Kontostand und die aktuell offenen Positionen.

   sState.account[0] = (float)AccountInfoDouble(ACCOUNT_BALANCE);
   sState.account[1] = (float)AccountInfoDouble(ACCOUNT_EQUITY);
//---
   double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0;
   double position_discount = 0;
   double multiplyer = 1.0 / (60.0 * 60.0 * 10.0);
   int total = PositionsTotal();
   datetime current = TimeCurrent();
   for(int i = 0; i < total; i++)
     {
      if(PositionGetSymbol(i) != Symb.Name())
         continue;
      double profit = PositionGetDouble(POSITION_PROFIT);
      switch((int)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
            buy_value += PositionGetDouble(POSITION_VOLUME);
            buy_profit += profit;
            break;
         case POSITION_TYPE_SELL:
            sell_value += PositionGetDouble(POSITION_VOLUME);
            sell_profit += profit;
            break;
        }
      position_discount += profit - (current - PositionGetInteger(POSITION_TIME)) * multiplyer * MathAbs(profit);
     }
   sState.account[2] = (float)buy_value;
   sState.account[3] = (float)sell_value;
   sState.account[4] = (float)buy_profit;
   sState.account[5] = (float)sell_profit;
   sState.account[6] = (float)position_discount;
   sState.account[7] = (float)Rates[0].time;

Als Nächstes generieren wir zeitbezogene harmonische Merkmale.

   bTime.Clear();
   double time = (double)Rates[0].time;
   double x = time / (double)(D'2024.01.01' - D'2023.01.01');
   bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
   x = time / (double)PeriodSeconds(PERIOD_MN1);
   bTime.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
   x = time / (double)PeriodSeconds(PERIOD_W1);
   bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
   x = time / (double)PeriodSeconds(PERIOD_D1);
   bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
   if(bTime.GetIndex() >= 0)
      bTime.BufferWrite();
//---
   bAccount.Clear();
   bAccount.Add((float)((sState.account[0] - PrevBalance) / PrevBalance));
   bAccount.Add((float)(sState.account[1] / PrevBalance));
   bAccount.Add((float)((sState.account[1] - PrevEquity) / PrevEquity));
   bAccount.Add(sState.account[2]);
   bAccount.Add(sState.account[3]);
   bAccount.Add((float)(sState.account[4] / PrevBalance));
   bAccount.Add((float)(sState.account[5] / PrevBalance));
   bAccount.Add((float)(sState.account[6] / PrevBalance));
   bAccount.AddArray(GetPointer(bTime));
//---
   if(bAccount.GetIndex() >= 0)
      if(!bAccount.BufferWrite())
         return;
//---
   bState.AssignArray(sState.state);

Es ist wichtig zu beachten, dass die resultierende Darstellung des aktuellen Umgebungszustands für zwei verschiedene Zwecke verwendet wird. Natürlich dient sie als Eingabe für einen Feed-Forward-Durchlauf durch den Agenten, was zur Generierung neuer Handelsaktionen führt. Belohnungen aus der Umgebung für diese Aktionen werden jedoch erst verfügbar, nachdem die nächste Bar gebildet wurde. Dies erzeugt eine zeitliche Lücke.

Andererseits sind wir in diesem Stadium in der Lage, die Wirksamkeit der vom Agenten im vorherigen Zeitschritt ergriffenen Maßnahmen zu bewerten. Es liegt in unserem Interesse, diese Bewertung durchzuführen, bevor die Modellzustände aktualisiert werden, die noch die Ergebnisse der Analyse des vorherigen Umgebungszustands enthalten.

Daher speisen wir zunächst die neu erstellte Darstellung des Umgebungszustands in die Zielmodelle ein und generieren eine vorhergesagte Schätzung des Zustands unter der Annahme, dass der Agent seiner aktuellen Policy folgt.

if(!bFirstRun)
  {
   //--- Target Nets
   if(!cEncoder[1].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL)
      || !cTask[1].feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                                    (CNet*)GetPointer(cEncoder[1]), LatentLayer)
      || !cActor[1].feedForward((CBufferFloat*)GetPointer(bAccount), 1, false,
                                                                        GetPointer(cTask[1]), -1)
      || !cCritic[2].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer)
      || !cCritic[3].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer)
      || !cCritic[4].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer)
      || !cCritic[5].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer)
     )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return;
     }

Die Verwendung von Zielmodellen spielt eine wichtige Rolle bei der Aufrechterhaltung einer kohärenten und stabilen Verhaltensstrategie bei gleichzeitiger Minimierung des Einflusses von Marktgeräuschen. Dies ermöglicht es dem Agenten, nicht nur unmittelbare Belohnungen, sondern auch erwartete zukünftige Erträge zu berücksichtigen.

Anschließend fahren wir mit dem Training von Critic fort. Es sei daran erinnert, dass die Autoren des Frameworks ActorDirectorCritic die Verwendung von zwei parallel arbeitenden Critics vorschlugen, von denen jeder über ein eigenes Paar von Zielmodellen verfügt. Zunächst konstruieren wir den Zielwert für die Bewertung der jüngsten Aktionen des Agenten, wobei wir die in der aktuellen Phase erhaltene Belohnung für den ersten Critic einbeziehen, und führen einen Feed-Forward- und Backpropagation-Durchlauf durch das Modell durch.

//--- Critic 1
cCritic[2].getResults(Result);
float reward = Result[0];
cCritic[4].getResults(Result);
reward = (reward + Result[0]) / 2 * DiscFactor + float(sState.account[1] - PrevEquity);
Result.Clear();
if(!Result.Add(reward)
   || !cCritic[0].backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   return;
  }

Es ist zu beachten, dass das ursprüngliche Framework vorschlägt, den Fehlergradienten basierend auf der Mindestschätzung der durchgeführten Aktionen zu propagieren. In unserer Implementierung verfolgen wir jedoch einen etwas anderen Ansatz. Wir werden den Fehlergradienten von dem Modell mit dem niedrigsten durchschnittlichen Bewertungsfehler propagieren. Somit gehen wir von der Mindestschätzung zu einer genaueren über.

Der zweite wichtige Aspekt des Online-Trainings betrifft die rechtzeitige Aktualisierung der Policy des Actors. Die offensichtlichste und technisch einfachste Lösung ist die Verwendung eines festen Iterationszählers. Nach einer vordefinierten Anzahl von Schritten wird die Strategie des Agenten aktualisiert. Dieser Ansatz ist in einer echten Online-Lernumgebung, in der jeder Umgebungszustand einzigartig ist und nicht erneut besucht werden kann, durchaus sinnvoll.

Wir beabsichtigen jedoch, die leistungsstarken Simulationsfunktionen des MetaTrader 5 Strategietesters zu nutzen. Dies ermöglicht es uns, dieselbe Ereignissequenz mehrmals abzuspielen, wodurch ein Online-Prozess effektiv simuliert wird, während wiederholte Trainingsdurchläufe möglich sind.

Dies führt zu einem potenziellen Problem. Wenn wir uns auf einen naiven Zeitplan für Aktualisierungen mit festen Iterationen verlassen, aktualisiert jeder Trainingslauf die Policy des Actors bei exakt denselben Umgebungszuständen. Dies verringert die Variabilität des Trainingsprozesses erheblich, führt künstliche Verzerrungen ein und behindert die Fähigkeit des Agenten, robuste Muster zu erlernen.

Um diesen Effekt zu vermeiden, haben wir einen stochastischen Ansatz zur Auslösung von Policy-Aktualisierungen implementiert. Anstatt einen deterministischen Zähler zu verwenden, generieren wir eine zufällige Ganzzahl und führen eine Policy-Aktualisierung nur dann durch, wenn dieser Wert durch eine vordefinierte Zahl teilbar ist. Dieser Mechanismus bewahrt die erforderliche Regelmäßigkeit der Optimierung und macht Aktualisierungen zeitlich nur schwach vorhersehbar. Dadurch wird ein Overfitting auf bestimmte Datensegmente verhindert.

if(cCritic[0].getRecentAverageError() <= cCritic[1].getRecentAverageError() &&
   (MathRand() % ActorUpdate) == 0)
   if(!cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
      || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
     )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return;
     }

Dasselbe Verfahren wird für den zweiten Critic wiederholt.

//--- Critic 2
cCritic[3].getResults(Result);
reward = Result[0];
cCritic[5].getResults(Result);
reward = (reward + Result[0]) / 2 * DiscFactor + float(sState.account[1] - PrevEquity);
Result.Clear();
if(!Result.Add(reward)
   || !cCritic[1].backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   return;
  }
if(cCritic[0].getRecentAverageError() > cCritic[1].getRecentAverageError() &&
   (MathRand() % ActorUpdate) == 0)
   if(!cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
      || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
     )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return;
     }

Der Trainingsprozess des Directors ist wesentlich einfacher. Profitable Handelsoperationen werden als positive Beispiele behandelt, während alle anderen Trades als negative Beispiele behandelt werden.

//--- Director
Result.Clear();
if((sState.account[1] - PrevEquity) > 0)
   Result.Add(1);
else
   Result.Add(0);
if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
   || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
   || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
  )
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   return;
  }

Hier aktualisieren wir auch die Parameter des Vorhersagemodells basierend auf der Richtung der kürzlich geschlossenen Bar.

 //--- Probability
 vector<float> target = vector<float>::Zeros(NActions / 3);
 if(sState.state[0] > 0)
    target[0] = 1;
 else
    if(sState.state[0] < 0)
       target[1] = 1;
 if(!Result.AssignArray(target)
    || !cProbability.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
    || !cEncoder[0].backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer)
   )
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    return;
   }
}

Sobald die Modelloptimierungsiterationen abgeschlossen sind, fahren wir mit der Generierung einer neuen Handelsoperation fort. Die zuvor erstellte Darstellung des aktuellen Umgebungszustands wird nun an die trainierbaren Modelle weitergegeben, einschließlich des Modells, das die Aktionen des Actors bewertet.

//--- New state
   if(!cEncoder[0].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL)
      || !cTask[0].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(cEncoder[0]),
                                                                                                   LatentLayer)
      || !cActor[0].feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, (CNet*)GetPointer(cTask[0]), -1)
      || !cProbability.feedForward((CNet*)GetPointer(cEncoder[0]), LatentLayer, (CBufferFloat*)NULL)
      || !cDirector.feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
      || !cCritic[0].feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
      || !cCritic[1].feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
     )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return;
     }

Anschließend speichern wir in globalen Variablen die Informationen, die bei der Verarbeitung der nächsten geschlossenen Bar benötigt werden.

PrevBalance = sState.account[0];
PrevEquity = sState.account[1];

Danach gehen wir zur Durchführung von Handelsoperationen über. Zuerst erhalten wir den vom Actor erzeugten Ausgabevektor.

   vector<float> temp;
   cActor[0].getResults(temp);
//---
   if(temp.Size() < NActions)
      temp = vector<float>::Zeros(NActions);

Sich gegenseitig aufhebende Positionsvolumina werden aus dem resultierenden Tensor entfernt.

double min_lot = Symb.LotsMin();
double step_lot = Symb.LotsStep();
double stops = (MathMax(Symb.StopsLevel(), 1) + Symb.Spread()) * Symb.Point();
if(temp[0] >= temp[3])
  {
   temp[0] -= temp[3];
   temp[3] = 0;
  }
else
  {
   temp[3] -= temp[0];
   temp[0] = 0;
  }

Wir fahren dann mit der Dekodierung der Ausgabe des Actors fort. Wenn sie kein Volumen an Long-Positionen enthalten, werden alle bestehenden Positionen, die möglicherweise zuvor eröffnet wurden, geschlossen.

//--- buy control
   if(temp[0] < min_lot || (temp[1] * MaxTP * Symb.Point()) <= 2 * stops ||
                                 (temp[2] * MaxSL * Symb.Point()) <= stops)
     {
      if(buy_value > 0)
         CloseByDirection(POSITION_TYPE_BUY);
     }

Wenn wir Long-Positionen eröffnen oder halten müssen, werden die generierten Werte in tatsächliche Handelsvolumina und Preisniveaus umgewandelt.

else
  {
   double buy_lot = min_lot + MathRound((double)(temp[0] - min_lot) / step_lot) * step_lot;
   double buy_tp = NormalizeDouble(Symb.Ask() + temp[1] * MaxTP * Symb.Point(), Symb.Digits());
   double buy_sl = NormalizeDouble(Symb.Ask() - temp[2] * MaxSL * Symb.Point(), Symb.Digits());

Wenn zuvor eröffnete Positionen vorhanden sind, passen wir die relevanten Handelsniveaus an.

if(buy_value > 0)
   TrailPosition(POSITION_TYPE_BUY, buy_sl, buy_tp);

Das Volumen der aktuellen Position wird dann angepasst, indem die Position entweder teilweise geschlossen oder der erforderliche Betrag hinzugefügt wird. Letzterer Fall umfasst auch die Eröffnung einer neuen Position.

 if(buy_value != buy_lot)
   {
    if((buy_value - buy_lot) >= min_lot)
       ClosePartial(POSITION_TYPE_BUY, buy_value - buy_lot);
    else
       if((buy_lot - buy_value) >= min_lot)
          if(!Trade.Buy(buy_lot - buy_value, Symb.Name(), Symb.Ask(), buy_sl, buy_tp))
             if(Trade.CheckResultRetcode() == 10019)
               {
                Result.Clear();
                Result.Add(0);
                if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
                   || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
                   || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
                  )
                  {
                   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
                   return;
                  }
               }
   }
}

Es ist wichtig zu beachten, dass bei einem Fehler „Nicht genügend Mittel“ während der Eröffnung einer neuen Position oder der Erhöhung einer bestehenden Position sofort eine Rückmeldung über den Director erfolgt, die darauf hinweist, dass die Entscheidung als negativ betrachtet werden sollte.

Short-Positionen werden auf ähnliche Weise angepasst.

//--- sell control
   if(temp[3] < min_lot || (temp[4] * MaxTP * Symb.Point()) <= 2 * stops ||
                                  (temp[5] * MaxSL * Symb.Point()) <= stops)
     {
      if(sell_value > 0)
         CloseByDirection(POSITION_TYPE_SELL);
     }
   else
     {
      double sell_lot = min_lot + MathRound((double)(temp[3] - min_lot) / step_lot) * step_lot;;
      double sell_tp = NormalizeDouble(Symb.Bid() - temp[4] * MaxTP * Symb.Point(), Symb.Digits());
      double sell_sl = NormalizeDouble(Symb.Bid() + temp[5] * MaxSL * Symb.Point(), Symb.Digits());
      if(sell_value > 0)
         TrailPosition(POSITION_TYPE_SELL, sell_sl, sell_tp);
      if(sell_value != sell_lot)
        {
         if((sell_value - sell_lot) >= min_lot)
            ClosePartial(POSITION_TYPE_SELL, sell_value - sell_lot);
         else
            if((sell_lot - sell_value) >= min_lot)
               if(!Trade.Sell(sell_lot - sell_value, Symb.Name(), Symb.Bid(), sell_sl, sell_tp))
                  if(Trade.CheckResultRetcode() == 10019)
                    {
                     Result.Clear();
                     Result.Add(0);
                     if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
                        || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
                        || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
                       )
                       {
                        PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
                        return;
                       }
                    }
        }
     }

Abschließend prüft die Methode, ob es an der Zeit ist, die Zielmodelle zu aktualisieren. Bei Bedarf werden Soft-Update-Prozeduren aufgerufen, um Parameter von den trainierbaren Modellen auf ihre entsprechenden Zielmodelle zu kopieren.

   bFirstRun = false;
//---
   if((int(Rates[0].time / PeriodSeconds(TimeFrame)) % TragetUpdate) == 0)
     {
      if(MathRand() / 32767.0 > 0.5)
         cCritic[2].WeightsUpdate(GetPointer(cCritic[0]), tau);
      else
         cCritic[4].WeightsUpdate(GetPointer(cCritic[0]), tau);
      if(MathRand() / 32767.0 > 0.5)
         cCritic[3].WeightsUpdate(GetPointer(cCritic[1]), tau);
      else
         cCritic[5].WeightsUpdate(GetPointer(cCritic[1]), tau);
      cEncoder[1].WeightsUpdate(GetPointer(cEncoder[0]), tau);
      cTask[1].WeightsUpdate(GetPointer(cTask[0]), tau);
      cActor[1].WeightsUpdate(GetPointer(cActor[0]), tau);
     }
   if(PrevBalance < 50)
      ExpertRemove();
  }

Die Methode endet dann und wartet auf das Schließen der nächsten Bar.

Der vollständige Quellcode des Expert Advisors für das Online-Modelltraining ist im Anhang enthalten.



Tests

Wir haben erheblichen Aufwand in die Anpassung und Implementierung der Kernkonzepte des Frameworks Actor–Director–Critic in MQL5 investiert und dessen Komponenten in die Architektur der trainierbaren Modelle integriert. Die Interaktionslogik zwischen Actor, Director und Critic wurde sorgfältig entworfen und mehrere originelle Ansätze für das Agententraining wurden implementiert. Nun ist es Zeit für die letzte – und vielleicht spannendste – Phase: die Bewertung der Wirksamkeit der vorgeschlagenen Lösungen anhand realer historischer Marktdaten.

Das Framework wird anhand historischer Daten unter Bedingungen bewertet, die dem realen Handel sehr nahekommen. Dies ermöglicht eine objektive Beurteilung, ob die gewählten architektonischen und algorithmischen Lösungen erfolgreich mit der Dynamik und Unsicherheit der Finanzmärkte umgehen können. Darüber hinaus deckt ein solches Testen sowohl die Stärken als auch die Schwächen der aktuellen Implementierung auf und hilft zudem, Richtungen für weitere Verbesserungen und Optimierungen aufzuzeigen.

Der Trainingsdatensatz wurde mithilfe zufälliger Agentenläufe im MetaTrader 5 Strategietester generiert, was es uns ermöglichte, ein breites Spektrum an Verhaltensszenarien zu sammeln. Der Datensatz enthält historische Daten von EURUSD des Zeitrahmens M1 für das gesamte Jahr 2024.

Das anfängliche Modelltraining wurde offline durchgeführt, ohne den Trainingsdatensatz zu aktualisieren, bis sich die Vorhersagefehler stabilisierten. Wir wechselten dann zum MetaTrader 5 Strategietester und setzten die Feinabstimmung der Modellparameter fort, bis eine stabile Leistung erreicht wurde.

Eine objektive Bewertung der erlernten Handelsstrategie kann nur durch die Evaluierung der trainierten Modelle mit Daten außerhalb der Trainingsstichprobe erreicht werden. Um die Leistung zu testen, verwendeten wir historische Daten von Januar bis März 2025. Da dieser Zeitraum während des Trainings nicht verwendet wurde, ist das Risiko einer Überanpassung eliminiert, was den Ergebnissen eine echte praktische Bedeutung verleiht.

Alle anderen Parameter, einschließlich des Marktumfelds, des Zeitrahmens, des Ausführungssimulationsmodells und der Terminaleinstellungen, wurden unverändert gelassen. Dies stellte eine saubere Bewertung der erlernten Strategie selbst sicher, ohne Beeinflussung durch externe Faktoren.

Die Testergebnisse werden unten dargestellt und bieten eine klare Veranschaulichung des Verhaltensmodells des Agenten.

Während des Testzeitraums führte das Modell 684 Trades aus. Davon wurden 268 profitabel geschlossen, was zu einer Gewinnrate von etwas über 39 % führte. Dennoch erzielte das Modell während des Testzeitraums einen Gesamtgewinn, da der durchschnittliche profitable Trade fast doppelt so groß war wie der durchschnittliche verlustbringende Trade.



Schlussfolgerung

In dieser Arbeit untersuchten wir die theoretischen Grundlagen des Frameworks Actor–Director–Critic und implementierten unsere eigene Interpretation der vorgeschlagenen Konzepte unter Verwendung von MQL5. Das Framework wurde vollständig in eine bestehende Multi-Agenten-Architektur integriert, was zu einem modularen, flexiblen und effizient trainierbaren Agenten führte, der nicht nur lokale Aktionsbewertungen (durch den Critic), sondern auch den breiteren strategischen Kontext der Verhaltenslogik (durch den Director) berücksichtigen kann. Dieser Ansatz bietet dem Actor ein genaueres und robusteres Feedback, wodurch der Agent in der Lage ist, ineffektive Aktionen schneller zu verwerfen und produktive Bereiche des Strategieraums effizienter zu erkunden.

Die durchgeführten Tests bestätigten die Tragfähigkeit des vorgeschlagenen Ansatzes und zeigten, dass das Framework Actor–Director–Critic in der Lage ist, ausgewogenere Entscheidungen zu treffen und dabei selbst unter Bedingungen von Marktunsicherheit ein souveränes Verhalten an den Tag zu legen.

Es ist jedoch wichtig zu beachten, dass die in diesem Artikel vorgestellten Programme ausschließlich als demonstrative Beispiele gedacht sind, die die Fähigkeiten des Frameworks aufzeigen. Bevor die vorgeschlagenen Lösungen in Live-Handelsumgebungen angewendet werden, sollten die Modelle mit einem repräsentativeren Datensatz trainiert und umfassenden Tests und Validierungen unterzogen werden.


Referenzen


In diesem Artikel verwendete Programme

# Name Typ Beschreibung
1 Research.mq5 Expert Advisor Expert Advisor für die Datenerfassung
2 ResearchRealORL.mq5
Expert Advisor
Expert Advisor für die Datenerfassung mit der Methode Real-ORL
3 Study.mq5 Expert Advisor Expert Advisor für das Offline-Modelltraining
4 StudyOnline.mq5
Expert Advisor
Expert Advisor für das Online-Modelltraining
4 Test.mq5 Expert Advisor Expert Advisor für Modelltests
5 Trajectory.mqh Klassenbibliothek Struktur der Beschreibung des Systemzustands und der Modellarchitektur
6 NeuroNet.mqh Klassenbibliothek Eine Bibliothek von Klassen zur Erstellung eines neuronalen Netzes
7 NeuroNet.cl Codebibliothek OpenCL-Programmcode

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

Letzte Kommentare | Zur Diskussion im Händlerforum (2)
Sergey Chalyshev
Sergey Chalyshev | 21 Apr. 2025 in 17:58
Unwissenheit führt zu Unwissenheit

https://nukadeti.ru/basni/krylov-kvartet

Dmitriy Gizlyk
Dmitriy Gizlyk | 24 Apr. 2025 in 13:52
Sergey Chalyshev #:
Unwissenheit führt zu Unwissenheit

https://nukadeti.ru/basni/krylov-kvartet

Könnt ihr mir vielleicht sagen, wie es richtig geht?

Von der Grundstufe zur Mittelstufe: Objekte (III) Von der Grundstufe zur Mittelstufe: Objekte (III)
Im heutigen Artikel werden wir uns ansehen, wie man ein sehr attraktives und interessantes Interaktionssystem implementiert, insbesondere für diejenigen, die gerade erst anfangen, das Programmieren in MQL5 zu üben. Hier gibt es nichts grundlegend Neues. Dank meiner Herangehensweise an das Thema wird es viel einfacher sein, alles zu verstehen, da wir in der Praxis sehen werden, wie man ein Programm unter Verwendung eines strukturierten Ansatzes mit einem praktischen und ansprechenden Ziel entwickelt.
Marktsimulation (Teil 23): Positionsansicht (I) Marktsimulation (Teil 23): Positionsansicht (I)
Der Inhalt, den wir ab diesem Punkt behandeln werden, ist in Bezug auf Theorie und Konzepte weitaus komplexer. Ich werde versuchen, den Stoff so einfach wie möglich zu gestalten. Der Programmierteil selbst ist recht einfach und unkompliziert. Aber wenn Sie die Theorie dahinter nicht verstehen, werden Sie überhaupt keine praktische Grundlage haben, um das Replay-/Simulationssystem für andere Aufgaben als die, die ich zeigen werde, zu verfeinern oder anzupassen. Ich möchte nicht, dass Sie den von mir präsentierten Code lediglich kompilieren und verwenden. Ich möchte, dass Sie lernen, verstehen und, wenn möglich, in der Lage sind, etwas noch Besseres zu schaffen.
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.
Von den Grundlagen zur Mittelstufe: Umgang von Mausereignissen Von den Grundlagen zur Mittelstufe: Umgang von Mausereignissen
Dieser Artikel gehört zur Kategorie der Materialien, bei denen das bloße Durchsehen und Studieren des Codes definitiv nicht ausreicht, um die beteiligten Prozesse zu verstehen. Tatsächlich müssen Sie eine ausführbare Anwendung erstellen und diese auf einem beliebigen Chart ausführen. Dies geschieht, damit Sie kleine Details verstehen können, die ansonsten überaus schwer zu erfassen wären, wie etwa die gleichzeitige Verwendung von Tastatur und Maus zur Erstellung bestimmter Elemente.