English Русский
preview
Ereignisgesteuerte Architektur in MQL5: Wie aus einem Expert Advisor ein vollwertiges Handelssystem wird

Ereignisgesteuerte Architektur in MQL5: Wie aus einem Expert Advisor ein vollwertiges Handelssystem wird

MetaTrader 5Beispiele |
34 6
MetaQuotes
MetaQuotes

Einführung

Bei der Entwicklung eines EA in MQL5 beginnen viele Leute mit der offensichtlichsten Lösung: Sie setzen die gesamte Logik in die Methode OnTick. Auf diese Weise ist es wirklich einfacher, den Einstieg zu finden. Dieser Ansatz hat jedoch einen versteckten Nachteil. Wenn das Projekt wächst, werden Handelsregeln, Bedingungsprüfung, Order-Verarbeitung, Datenaktualisierung, Schnittstelle, Berechnungen und Protokollierung in einem einzigen Handler zusammengefasst. Infolgedessen wächst der Code bis zu einem Zustand, in dem er nicht mehr programmierbar ist und nur noch durch reines Glück zusammengehalten wird. Jede Veränderung an einer Stelle wirkt sich auf ganz andere Teile des Systems aus. Sie reparieren das Bedienpanel – und plötzlich bricht das Handelsszenario zusammen. Sie ändern den Eingabefilter – in der Hintergrundprüfung erscheint ein Fehler. Ein solcher EA wird schnell zu einem fragilen Monolithen, dessen Komplexität schneller wächst als das Vertrauen des Entwicklers.

MetaTrader 5 bietet mehr als nur den Datenstrom der Kurse. Er basiert auf Ereignissen. Das Terminal empfängt ständig Ticks, Timer-Signale, Nutzeraktionen, Änderungen des Handelsstatus und Ereignisse zur Markttiefe. Diese Meldungen sollten getrennt behandelt werden. Um dies zu erreichen, verfügt MQL5 über verschiedene Handler, die jeweils einen eigenen Verantwortungsbereich haben. OnTick ist für die Marktaktualisierung zuständig, OnTimer für periodische und Hintergrundaufgaben und OnChartEvent für die Reaktion auf die GUI und Nutzeraktionen. Wenn die Logik entsprechend ihrem Zweck verteilt wird, ist der Code nicht mehr unübersichtlich. Der Code wirkt dann wie ein sauber strukturiertes technisches System, in dem jedes Modul seine Aufgabe erfüllt und seine Nachbarn nicht beeinträchtigt.

Dieses Design ist besonders wichtig, wenn der EA über ein einzelnes Symbol hinausgeht und mehrere Funktionen gleichzeitig ausführt. Wir müssen den Markt überwachen, die Schnittstelle pflegen, auf Tastendruck reagieren, den internen Status synchronisieren, Signale zwischen den Komponenten übertragen und manchmal sogar mehrere Instrumente bedienen. An diesem Punkt sieht die Ereignisarchitektur nicht mehr wie eine schöne Theorie aus. Sie wird zu einer praktischen Notwendigkeit. Wenn wir die Hintergrundarbeit in OnTimer und die Reaktion auf die Aktionen des Nutzers in OnChartEvent verlagern, wird der Haupthandelskreis von unnötiger Last befreit. Dies macht das Systemverhalten vorhersehbarer und vereinfacht die Wartung erheblich.

Nutzerdefinierte Ereignisse und Dienste spielen in einer solchen Architektur eine gesonderte Rolle. Mit nutzerdefinierten Ereignissen können wir einen internen Nachrichtenbus zwischen Modulen organisieren, während wir mit Diensten Hilfslogik außerhalb eines bestimmten Charts verschieben können. Es handelt sich nicht mehr nur um einen EA, sondern um ein System von Komponenten, die Befehle und Signale austauschen können, ohne alles in einer Funktion zu vermischen. Dies ermöglicht den Aufbau von Lösungen auf der Ebene der Handelsanwendung mit Control Panels, Hintergrundanalysen, modulübergreifendem Austausch und einer klaren Rollentrennung.

Darüber hinaus verbessert der ereignisgesteuerte Ansatz die Testbarkeit erheblich. Wenn die Logik auf verschiedene Handler verteilt ist, ist es einfacher, sie einzeln zu testen. Wir können die Reaktion auf den Timer, die UI-Ereignisse und die Handelstransaktionen separat analysieren. Dies ist besonders wichtig bei Aufgaben, bei denen ein Fehler Zeit und Geld kosten kann. Je besser die Struktur ist, desto einfacher ist es, das Systemverhalten zu kontrollieren und die Ursachen von Fehlern schnell zu finden.

In diesem Artikel werden wir uns ansehen, wie man von dem Modell von „Alles-in- OnTick“ zu einer ausgereifteren Ereignisarchitektur übergehen kann. Betrachten wir nun die Rollen von vordefinierten Handlern und nutzerdefinierten Ereignissen sowie von Diensten, die nicht an ein Chart gebunden sind. Anschließend gehen wir auf typische Fehler ein, die die Architektur schon vor Beginn der eigentlichen Arbeit zerstören. Die Grundidee ist einfach: Wenn MQL5 für den vorgesehenen Zweck eingesetzt wird, können wir damit nicht nur Handelsroboter, sondern auch vollwertige Anwendungssysteme bauen.

MQL5-Ereignisse



Vordefinierte Ereignisse

Vordefinierte Ereignisse in MQL5 bilden das Gerüst, auf dem die gesamte Logik des Programms basiert. Dabei handelt es sich nicht nur um eine Reihe von Handler-Funktionen, sondern um ein genau definiertes Modell der Reaktion auf Veränderungen in der Laufzeitumgebung. Und je eher der Entwickler aufhört, sie als eine Ergänzung zu OnTick zu betrachten, desto schneller wird der Code eine Architektur erhalten.

Der Lebenszyklus eines jeden Programms beginnt mit OnInit und endet mit OnDeinit. Dies sind sozusagen die Ein- und Ausstiegspunkte. OnInit legt den Grundstein: Timer werden über EventSetTimergesetzt, grafische Objekte werden erstellt, interne Strukturen und externe Komponenten werden initialisiert. Es ist auch bequem, hier Dienste zu starten und die Umgebung vorzubereiten.

int OnInit()
  {
   EventSetTimer(30); // poll every 30 seconds
   return(INIT_SUCCEEDED);
  }

OnDeinit wiederum erfordert Disziplin. Alles, was erstellt wurde, muss korrekt freigegeben werden: Objekte werden gelöscht, der Timer wird über EventKillTimerdeaktiviert, Ressourcen werden geschlossen. Das Ignorieren dieser Symmetrie ist ein klassischer Fehler, der zu einer Beeinträchtigung der Laufzeitumgebung und subtilen Störungen führt.

void OnDeinit(const int reason)
  {
   EventKillTimer(); // disable the timer when unloading
  }

OnTick wird traditionell als das Herzstück des EA angesehen. Dies ist teilweise richtig. Sein Zweck ist es, auf Marktveränderungen eines bestimmten Symbols zu reagieren. Hier können Sie Signale berechnen, Einstiegsbedingungen prüfen und Positionen verwalten. Grundsätzlich gilt jedoch, dass das Vorhandensein von OnTick nicht zwingend erforderlich ist. Wenn die Logik auf andere Ereignisse verlagert wird, kann der EA auch ohne sie funktionieren. Außerdem wird OnTick in ausgereiften Architekturen oft zu einem leichtgewichtigen Router und nicht zum Zentrum des Universums.

OnTimer ist ein Werkzeug, das von vielen unterschätzt wird. Das Terminal generiert diese Ereignisse in einem bestimmten Intervall, sodass Hintergrundaufgaben von Marktaufgaben getrennt werden können. Das Sammeln von Statistiken, das Aktualisieren von Zwischenspeichern, das Abfragen mehrerer Symbole, das Neuberechnen von Indikatoren – all das passt logischerweise in den Timer. Dieser Ansatz entlastet OnTick und macht das Systemverhalten stabiler, insbesondere in Zeiten hoher Volatilität. Denken Sie an eine einfache, aber wichtige Regel: Der Timer sollte nicht nur in OnInit gesetzt, sondern auch unbedingt in OnDeinit wieder entfernt werden.

OnChartEvent öffnet die Tür zur Welt der interaktiven Anwendungen. Dies ist ein Handler für GUI-Ereignisse: Mausklicks, Tastenanschläge, Interaktionen mit Objekten und, am wichtigsten, nutzerdefinierte Ereignisse. Bedienfelder, Knöpfe und Betriebsartenschalter sind integriert. Hier wird die Reaktion auf externe Signale, die von anderen Programmen kommen, implementiert. Im Kontext der ereignisgesteuerten Architektur ist dies nicht länger ein einfacher UI-Handler, sondern ein vollwertiger Kommunikationskanal.

OnTradeTransaction ist ein Punkt der Handelsstatuskontrolle. Im Gegensatz zu vereinfachten Ansätzen, bei denen nur das Ergebnis einer Operation geprüft wird, ist die gesamte Struktur MqlTradeTransaction hier verfügbar. So lässt sich der Lebenszyklus eines Auftrags genau verfolgen: vom Versand bis zur Ausführung und späteren Änderungen. Dieser Detaillierungsgrad ist besonders wichtig bei komplexen Positionsmanagement-Szenarien, bei denen nicht nur das Ziel, sondern auch der Weg dorthin wichtig ist.

OnBookEvent wird seltener verwendet, ist aber bei bestimmten Aufgaben unersetzlich. Es reagiert auf Änderungen der Markttiefe und wird über MarketBookAdd aktiviert. Dies ist bereits das Gebiet der subtileren Strategien, bei denen nicht nur der Preis wichtig ist, sondern auch die Liquiditätsstruktur. Bei Hochfrequenz-Ansätzen oder bei der Analyse von Mikro-Marktbewegungen wird dieser Handler zu einer wichtigen Quelle von Signalen.

Das Wichtigste ist hier ganz einfach: Jeder Programmtyp in MQL5 empfängt seinen eigenen Satz von Ereignissen, und durch sie wird sein Verhalten geformt. Der Versuch, dieses Modell zu ignorieren und alles auf einen einzigen Handler zu reduzieren, führt unweigerlich zu Komplexität. Im Gegenteil, die richtige Verteilung der Logik auf die Ereignisse schafft ein klares, vorhersehbares und skalierbares System – die eigentliche Grundlage, ohne die es unmöglich ist, voranzukommen.



Nutzerdefinierte Ereignisse

Nutzerdefinierte Ereignisse in MQL5 sind eines der praktischsten Werkzeuge für den Aufbau einer Ereignisarchitektur. Mit ihrer Hilfe ist das Programm nicht mehr in sich geschlossen. Es erhält die Fähigkeit, Signale zwischen seinen Teilen so selbstverständlich zu übertragen, wie die Knoten eines einzelnen Systems Dienstnachrichten austauschen. Dies wird erreicht durch die Funktion EventChartCustom, die es uns ermöglicht, programmatisch unser eigenes Ereignis an ein bestimmtes Chart zu senden und es dann in OnChartEvent zu behandeln. Dies ist ein interner Nachrichtenbus. Es ist besonders wertvoll, wenn ein einziges Programm auf den Markt reagieren und die Arbeit der Schnittstelle, der Servicemodule und der Handelslogik koordinieren soll.

Der Hauptvorteil dieses Ansatzes besteht darin, dass er dazu beiträgt, die Verantwortlichkeiten zwischen den Modulen zu trennen. Eine Komponente kann in der Marktanalyse eingesetzt werden. Die andere – in der Anzeige des Status des Panels. Die dritte – bei der Durchführung von Handelsaktionen. Die vierte – bei der Wartung von Hintergrundrechnungen. Wenn wir versuchen, all dies mit direkten Aufrufen und gemeinsam genutzten globalen Variablen zu verknüpfen, wird der Code schnell unübersichtlich. Die Wartung wird wie die Reparatur eines Mechanismus, bei dem jedes Zahnrad mit allen anderen ineinandergreift. Nutzerdefinierte Ereignisse vermeiden diese Verwirrung: Das Modul greift nicht in die interne Logik eines anderen ein, sondern sendet einfach ein sauberes Signal dorthin, wo es empfangen werden soll.

Die Möglichkeit einer gezielten Ereignisübertragung ist gesondert zu erwähnen. In EventChartCustom können wir nicht nur das aktuelle Chart, sondern auch mit chartID die ID eines anderen Fensters angeben. Dies ermöglicht die Kommunikation zwischen verschiedenen Instanzen von EAs, Indikatoren und Diensten. Dieser Mechanismus ist besonders nützlich bei Systemen mit mehreren Symbolen und mehreren Charts. Eine Komponente sammelt oder erzeugt Informationen, die andere empfängt sie und führt eine Aktion aus. In diesem Fall wird das nutzerdefinierte Ereignis zum Bindeglied, das die verschiedenen Teile zu einer einzigen Architektur zusammenführt.

Darüber hinaus wird der Gedanke des Austauschs von Ereignissen in der Initialisierungsphase besonders deutlich. Manchmal muss OnInit mit vorbereitenden Aktionen überladen werden: Erstellen interner Objekte, Laden von Daten, Erstellen von Caches, Konfigurieren der Schnittstelle, Starten von Diensten. Wenn all dies sofort bei der Initialisierung geschieht, wird die Methode schwerfällig und verlangsamt den Programmstart. Wenn die Initialisierung zu lange dauert, besteht außerdem die Gefahr, dass ein Zeitlimit überschritten wird oder das System einfach nur langsam und unhandlich gestartet wird. Es ist viel sinnvoller, nur das Minimum an Vorbereitung in OnInit zu belassen und sofort ein nutzerdefiniertes Ereignis zu erstellen, das den arbeitsintensiven Teil in einen separaten Handler verschiebt. Dann startet das Programm schnell, und die Hauptvorbereitung wird im Ereignismodus durchgeführt.

Dies ist auch deshalb wichtig, weil in MQL5 die langfristige Ausführung einer Funktion die Behandlung anderer Ereignisse blockiert. Der Aufruf mehrerer kleiner Funktionen hintereinander löst das Problem nicht: Solange die Kette nicht vollständig ist, kann nichts in den Ablauf der Ereignisse eingreifen. Dies ist besonders bei Programmen mit einer Nutzeroberfläche zu beobachten. Der Nutzer interagiert mit dem Panel und wartet auf eine Antwort, aber das Programm reagiert nicht, weil es ein langes Skript ausführt. Es wirkt, als hätte sich das Programm aufgehängt, obwohl das System in Wirklichkeit einfach keine Zeit hat, die Kontrolle an den Ereignis-Handler zu übertragen. Genau aus diesem Grund ist das Ereignismodell hier architektonisch notwendig. Sie ermöglicht die Delegierung der Orchestrierung an einen Event-Handler, der die Schritte sequentiell ausführt, ohne die Schnittstelle zu blockieren oder das Programm reaktionsunfähig zu machen.

Diese Flexibilität hat einen Nachteil: Ohne ein System können keine nutzerdefinierten Ereignisse gesendet werden. Hier ist ein klares Protokoll erforderlich: Welche IDs bedeuten was, wo wird der Nachrichtencode gespeichert, wie wird festgestellt, ob ein Ereignis zur Anwendung gehört. In der Regel werden zu diesem Zweck nutzerdefinierte ID-Bereiche festgelegt. Verwenden Sie eindeutige Bezeichnungen in String-Parametern und stellen Sie sicher, dass das Ereignis tatsächlich für dieses Modul bestimmt ist. Andernfalls kann es leicht passieren, dass ein Signal ohne Relevanz die Logik in ein architektonisches Durcheinander stürzt.

Es gibt noch eine weitere Regel, die oft unterschätzt wird: Erzeugen Sie nicht zu viele Ereignisse, es sei denn, es besteht ein echter Bedarf. Die Terminal-Ereigniswarteschlange mag keinen unnötigen Lärm. Wenn Ereignisse zu häufig gesendet werden, wird die Nachrichtenwarteschlange überlastet und die Signalverarbeitung verschlechtert sich. Es ist besser, nur wichtige Zustände zu übertragen: die Tatsache, dass Daten bereit sind, das Auftreten eines Signals, ein Befehl zur Aktualisierung der Schnittstelle, die Bestätigung der Ausführung einer Aktion. Dann werden die Ereignisse wie ordentliche Notizen funktionieren und nicht wie ein chaotischer Strom von Telegrammen.

Aus diesem Grund sind nutzerdefinierte Ereignisse in einem ausgereiften System besonders wertvoll. Sie verbinden Module ohne starre Kopplung, ermöglichen den Aufbau klarer Interaktionsprotokolle und machen MQL5 zu einer Umgebung für vollwertige ereignisgesteuerte Entwicklung.



Dienste

Ein Dienst in MQL5 ist ein Hintergrund-Terminaldienst. Kein Chart, keine Symbolbindung, kein Warten auf Ticks. Nach dem Start funktioniert es von selbst, wie ein ordentlicher Diener, der nicht daran erinnert werden muss, was er zu tun hat.

Die gesamte Logik befindet sich in der Funktion OnStart. Meistens ist darin ein Zyklus organisiert: Prüfen des Status, Ausführen der Aufgabe, Pause durch Sleep, gefolgt vom nächsten Schritt. Dieser Rhythmus ist einfach und zuverlässig. Er erfordert keine Marktereignisse und hängt nicht von der Aktivität des Charts ab. Der Dienst bestimmt sein eigenes Tempo.

Die praktischen Vorteile werden schnell deutlich. Wenn Sie regelmäßig Daten aus einer externen Quelle herunterladen müssen, kann der Dienst dies übernehmen. Sie müssen Ticks für ein nutzerdefiniertes Symbol erzeugen? Auch das kann der Dienst leisten. Müssen Sie alle paar Sekunden Signale senden oder den Systemstatus aktualisieren? Auch dies ist Aufgabe des Dienstes. Alles, was sich wiederholt, alles, was nicht vom Marktrauschen abhängen sollte, sollte logischerweise hierher gebracht werden.

Es gibt noch ein weiteres Detail, das den Service besonders komfortabel macht. Wenn Sie ihn nicht manuell stoppen, wird er beim nächsten Start des Terminals automatisch gestartet. Dadurch wird es zu einem Werkzeug für Routinevorgänge: Überprüfung des Status beim Start, Vorbereitung von Daten, Synchronisierung – all dies kann ohne Nutzereingriff erfolgen. Einmal eingerichtet, funktioniert der Dienst jeden Tag wie ein Uhrwerk.

In der Architektur sieht das sehr robust aus. Der EA auf dem Chart reagiert auf den Markt. Gleichzeitig ist die Schnittstelle nutzerorientiert. In der Zwischenzeit verrichtet der Dienst still und regelmäßig im Hintergrund seine Arbeit. Kein Aufhebens, keine Überlastung von OnTick. Diese Rollenteilung entlastet das System von unnötigem Stress und macht es stabiler. Wenn jedes Teil seine Aufgabe erfüllt, arbeitet die gesamte Struktur spürbar sauberer.



Kommunikation zwischen Programmen

Wenn eine Handelslösung aus mehreren Komponenten besteht, benötigen sie einen Nachrichtenübermittlungsmechanismus. Ein Modul kann Daten sammeln. Das zweite zeigt sie auf dem Panel. Das dritte trifft eine Handelsentscheidung. Das vierte behandelt die Hintergrundlogik. Und wenn jeder von ihnen anfängt, in seinem eigenen Universum zu leben, wird das Ergebnis keine Architektur sein, sondern ein Gewirr von Flags, globalen Variablen und Zufallsprüfungen. Deshalb funktioniert die ereignisgesteuerte Struktur hier besonders gut.

Die saubere Version der ereignisbasierten Architektur besteht darin, dass Indikatoren zur Quelle von Signalen werden, wenn auch nicht auf herkömmliche Weise (einen Puffer abgefragt – einen Wert erhalten), sondern als vollwertige Ereignisgeneratoren.

Die Idee ist einfach. Jeder Indikator ist in einem leichten Adapter verpackt. Im Hintergrund erfüllt er genau eine Aufgabe: Er überwacht ausschließlich, ob ein Signal entsteht, und löst bei erfüllter Bedingung ein nutzerdefiniertes Ereignis aus. Alles ist bereit. Keine ständigen Anfragen von außen, kein unnötiges Rauschen.

int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
   if(prev_calculated==rates_total)
     return prev_calculated;
//---
   if(BarsCalculated(handle) < rates_total)
      return(prev_calculated);
   vector<double> sig, main;
   if(!main.CopyIndicatorBuffer(handle, 0, 1, 2) ||
      !sig.CopyIndicatorBuffer(handle, 1, 1, 2))
      return(prev_calculated);
   if(sig[0] > main[0] && sig[1] <= main[1])
     {
      if(!EventChartCustom(ChartID(), BuyID, MagicNumber, main[0], IndComment))
         return(prev_calculated);
     }
   if(sig[0] < main[0] && sig[1] >= main[1])
     {
      if(!EventChartCustom(ChartID(), SellID, MagicNumber, main[0], IndComment))
         return(prev_calculated);
     }
//--- return value of prev_calculated for the next call
   return(rates_total);
  }

Nehmen wir als Beispiel zwei klassische Indikatoren – MACD und RSI. Jeder von ihnen arbeitet unabhängig in ihrem eigenen Zeitrahmen. Sie bearbeiten nur eine neue Bar. Der MACD prüft den Schnittpunkt zwischen dem Histogramm und der Signallinie. Der RSI ist der Schnittpunkt zwischen den Überkauf- und Überverkaufsniveaus. Wenn ein Signal auftritt, erzeugen sie vordefinierte nutzerdefinierte Ereignisse. Dies ist ein grundlegender Punkt: Der Indikator speichert kein Signal im Puffer, er registriert ein Ereignis.

Dann kommt der EA ins Spiel, allerdings in einer ganz anderen Rolle. Die Indikatoren werden nicht bei jedem Tick abgefragt. Es geht nicht durch die Historie, synchronisiert keine Puffer, versucht nicht zu erraten, ob es ein Signal gab. Sie wartet einfach auf Ereignisse in OnChartEvent.

void OnChartEvent(const int32_t id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(lparam != MagicNumber)
      return;
   switch(id - CHARTEVENT_CUSTOM)
     {
      case MACD1Buy:
         indSignals[0] = 1;
         break;
      case MACD1Sell:
         indSignals[0] = -1;
         break;
      case MACD2Buy:
         indSignals[1] = 1;
         CloseSellSignal = true;
         CloseBuySignal = false;
         break;
      case MACD2Sell:
         indSignals[1] = -1;
         CloseBuySignal = true;
         CloseSellSignal = false;
         break;
      case RSI1CrossOverBoughtDown:
         indSignals[2] = -1;
         break;
      case RSI1CrossOverSoldUp:
         indSignals[2] = 1;
         break;
      case RSI2CrossOverBoughtDown:
         indSignals[3] = -1;
         CloseBuySignal = true;
         CloseSellSignal = false;
         break;
      case RSI2CrossOverSoldUp:
         indSignals[3] = 1;
         CloseSellSignal = true;
         CloseBuySignal = false;
         break;
     }
   BuySignal = true;
   SellSignal = true;
   for(uint i = 0; i < indSignals.Size() && (BuySignal || SellSignal); i++)
     {
      BuySignal = BuySignal && (indSignals[i] == 1);
      SellSignal = SellSignal && (indSignals[i] == -1);
     }
  }

Der EA registriert die eingehenden Ereignisse. Wenn nichts ankommt, passiert nichts.

Dadurch ändert sich die Art der Belastung drastisch. In der klassischen Struktur mit der Abfrage von Indikatoren geschieht bei jedem Tick das Gleiche – Puffer anfordern, Werte überprüfen. Manchmal brauchen wir vielleicht auch einen Gang durch die Historie, um das Signal nicht zu verpassen. Besonders schwierig wird dies bei mehreren Indikatoren und unterschiedlichen Zeitrahmen. Es gibt immer eine Zeitlücke zwischen den Signalen, und wir müssen ständig zurückschauen, um sie zu erfassen.

Das Ereignismodell beseitigt diesen Mehraufwand einfach. Jedes Signal ist ein unmittelbar vorliegendes Ereignis. Es ist nicht nötig, danach zu suchen, sie neu zu berechnen oder sie nachträglich zu rekonstruieren. Es erscheint im Moment des Entstehens.

Das Ergebnis ist ein übersichtliches und schnelles Diagramm:

  • der Indikator ist nur für seine eigene Berechnung und sein eigenes Signal verantwortlich;
  • das Ereignis hält den Moment des Zustandswechsels fest;
  • der EA aggregiert die Signale und trifft eine Entscheidung.

Dies wird besonders bei Strategien mit mehreren Indikatoren deutlich. Ein Signal kommt zum Beispiel von der M5, das andere von der M30. Im klassischen Modell müssen die Daten synchronisiert, Verzögerungen berücksichtigt und die Historie überprüft werden. Hier ist alles einfacher: Jeder Indikator hat zum richtigen Zeitpunkt ein Signal geliefert, der EA speicherte den Status und wartete, bis alle Bedingungen erfüllt waren.

In Bezug auf die Leistung ist dies ein spürbarer Vorteil. Es gibt kein ständiges Abrufen der Indikatoren, keine sinnlosen Prüfungen bei jedem Tick. OnTick wird einfach und schnell, d.h. die Reaktion auf den Markt ist schneller. Im Handel ist dies nicht nur eine Theorie, sondern ein ganz praktischer Vorteil: weniger Verzögerungen, klarere Logik, genauere Ausführung.

void OnTick()
  {
//---
   if(BuySignal)
     {
      cSymbol.Refresh();
      cSymbol.RefreshRates();
      if(!cTrade.Buy(InpLot, cSymbol.Name(), cSymbol.Ask(),
                     cSymbol.Ask() - SL * cSymbol.Point(),
                     cSymbol.Ask() + TP * cSymbol.Point(), "Event Example"))
        {
         PrintFormat("Error open Buy position: %d", GetLastError());
         return;
        }
      BuySignal = false;
      ArrayFill(indSignals, 0, indSignals.Size(), 0);
     }
//---
   if(SellSignal)
     {
      cSymbol.Refresh();
      cSymbol.RefreshRates();
      if(!cTrade.Sell(InpLot, cSymbol.Name(), cSymbol.Bid(),
                      cSymbol.Bid() + SL * cSymbol.Point(),
                      cSymbol.Bid() - TP * cSymbol.Point(), "Event Example"))
        {
         PrintFormat("Error open Sell position: %d", GetLastError());
         return;
        }
      SellSignal = false;
      ArrayFill(indSignals, 0, indSignals.Size(), 0);
     }
//---
   if(CloseBuySignal)
     {
      if(cPosition.SelectByMagic(cSymbol.Name(), MagicNumber) &&
         cPosition.PositionType() == POSITION_TYPE_BUY)
        {
         if(!cTrade.PositionClose(cPosition.Ticket()))
           {
            PrintFormat("Error close Buy position: %d", GetLastError());
            return;
           }
        }
     }
//---
   if(CloseSellSignal)
     {
      if(cPosition.SelectByMagic(cSymbol.Name(), MagicNumber) &&
         cPosition.PositionType() == POSITION_TYPE_SELL)
        {
         if(!cTrade.PositionClose(cPosition.Ticket()))
           {
            PrintFormat("Error close Sell position: %d", GetLastError());
            return;
           }
        }
     }
//---
  }

Ebenso wichtig ist, dass der Code transparenter wird. Das Signal ist nicht länger ein Wert in einem Puffer, der interpretiert werden muss, sondern wird zu einem eindeutigen Ereignis: passiert – registriert – bearbeitet. Dies ist genau dann der Fall, wenn die Architektur sowohl die Lesbarkeit als auch das Verhalten des Systems direkt verbessert.

Die Ausführung des EA im Strategietester des MetaTrader 5 ergab ein interessantes Bild. Wir haben ein System mit einem eindeutigen Charakter – es verdient aktiv, hält aber gleichzeitig die Risiken in vernünftigen Grenzen.

Testergebnisse Testergebnisse

Bei einem Anfangskapital von 1.000,0 USD betrug das Ergebnis 2.427,52 USD, d.h. +143% in 3 Jahren. Das Ergebnis kann sich sehen lassen, vor allem, wenn man bedenkt, dass der Drawdown moderat ist. Die Equity-Kurve sieht ordentlich aus: Das Wachstum erfolgt schrittweise, es gibt Rücksetzer, aber keine längeren Einbrüche. Der maximale Drawdown liegt bei etwa 12-13 %, während der Recovery Factor bei etwa 4,6 liegt – das System ist in der Lage, sich vom Drawdown zu erholen , ohne in ihnen stecken zu bleiben. Dies ist ein wichtiges Zeichen für Nachhaltigkeit.

Die konventionelle Gewinnmechanik funktioniert. Insgesamt 18 Trades mit einer Gewinnrate von 50%. Aber der durchschnittliche Gewinn ist fast viermal so hoch wie der durchschnittliche Verlust. Dies hat dazu geführt, dass der Profit Factor den Wert von 3,9 erreicht hat. Die Strategie verdient ihr Geld nicht durch die Häufigkeit, sondern durch die Qualität der Signale und das Halten der Positionen. Es gibt jedoch eine Einschränkung. Die geringe Zahl der Transaktionen macht die Statistiken unzuverlässig.

Die Equity-Kurve ist ziemlich glatt, ohne chaotische Sprünge. Dies bestätigt indirekt, dass die Signale nicht zufällig sind.

Und das ist ein wichtiger architektonischer Punkt. Das ereignisgesteuerte Modell macht die ständige Abfrage von Indikatoren überflüssig. Der EA reagiert nur auf das Signal. Weniger unnötige Vorgänge bedeuten eine schnellere Reaktion. Im Testprogramm ist dies nur eine Nuance, aber im Live-Handel wird es zu einem Vorteil.

Das Ergebnis ist einfach. Wir haben es hier mit einem Signalmodell zu tun, das ein gutes Risiko-Ertrags-Verhältnis und eine gut durchdachte Architektur aufweist. Aber im Moment ist dies ein Prototyp. Es werden Out-of-Sample-Tests und erweiterte Statistiken benötigt.



Typische Fehler

Wenn eine Architektur ereignisgesteuert wird, wird der Code sauberer und schneller. Gleichzeitig ändert sich aber auch die Art der Fehler. Sie liegen nicht mehr an der Oberfläche, sondern manifestieren sich in den Verbindungen zwischen den Handlern und in der Disziplin des Umgangs mit Ressourcen.

Das erste und dringlichste Problem ist das gleiche alte überladene OnTick. Selbst nachdem man sich mit dem Ereignismodell vertraut gemacht hat, ist man versucht, einen Teil der Logik dort zu belassen , nur für den Fall der Fälle. Das Ergebnis ist ein Hybrid: Es gibt Zeitgeber und Ereignisse, aber der Großteil der Arbeit wird immer noch von einer einzigen Methode erledigt. Dieser Kompromiss bringt uns schnell wieder zum Ausgangspunkt zurück: schwerer, schlecht gewarteter Code. Die Erfahrung zeigt, dass wir, wenn wir die Rollen bereits verteilt haben, dies auch bis zum Ende durchziehen müssen.

Die nächste Fehlerschicht bezieht sich auf die Ressourcenverwaltung. Das Ereignismodell umfasst mehr Einheiten: Zeitgeber, Objekte, Abonnements, Hilfsstrukturen. Und wenn sie nicht sorgfältig verwaltet werden, fängt das System an, ein Rauschen zu erzeugen. Vergessene EventKillTimer, nicht entfernte grafische Elemente, verlassene globale Variablen – all das macht das Programm nicht sofort kaputt, sondern verringert allmählich seine Vorhersagbarkeit. Das Ergebnis ist eine klassische Situation: Der Fehler erscheint an einem anderen Ort als dem, an dem er gemacht wurde.

void OnDeinit(const int reason)
  {
//---
   for(uint i = 0; i < handles.Size(); i++)
      if(handles[i] != INVALID_HANDLE)
         IndicatorRelease(handles[i]);
  }

Schon die Art der Ereignisse erfordert besondere Aufmerksamkeit. Handler werden nacheinander ausgeführt, und eine lange Operation innerhalb eines Ereignisses blockiert die anderen. Das macht sich vor allem in der Nutzeroberfläche bemerkbar: Der Nutzer interagiert mit dem Panel, aber das Programm reagiert nicht. Es ist mit Berechnungen beschäftigt. Auch hier wird der Grundgedanke der Architektur deutlich: die Ausführung in Phasen aufzuteilen und den Thread nicht länger als wirklich nötig zu beschäftigen.

Wenn in einem System mehrere Ereignisquellen auftauchen (Indikatoren, ein Panel, ein Dienst), tritt die Identifikationsdisziplin in den Vordergrund. Wenn wir keine klaren Konventionen für Objektnamen und Ereigniscodes einführen, entsteht Verwirrung: Signale überschneiden sich, Handler reagieren auf die falschen Dinge. Eine einfache Regel mit Präfixen und ID-Bereichen scheint fast trivial, aber sie sorgt dafür, dass das System in Ordnung bleibt, wenn es zu wachsen beginnt.

#define  MACD1Buy                   1
#define  MACD1Sell                  2
#define  MACD2Buy                   3
#define  MACD2Sell                  4
#define  RSI1CrossOverBoughtUp      5
#define  RSI1CrossOverBoughtDown    6
#define  RSI1CrossOverSoldUp        7
#define  RSI1CrossOverSoldDown      8
#define  RSI2CrossOverBoughtUp      9
#define  RSI2CrossOverBoughtDown    10
#define  RSI2CrossOverSoldUp        11
#define  RSI2CrossOverSoldDown      12

Und schließlich ist der heikelste Moment der Start des Programms. In einem ereignisgesteuerten Modell können Ereignisse fast sofort eintreffen, noch bevor die Initialisierung vollständig abgeschlossen ist. Wenn die Strukturen zu diesem Zeitpunkt noch nicht fertig sind, kann es leicht passieren, dass man auf leere Daten oder einen falschen Zustand zugreift. Daher muss die Initialisierung entweder vollständig abgeschlossen sein, bevor mit der Verarbeitung begonnen wird, oder – was oft bequemer ist – in Phasen mit Bereitschaftskontrolle durch dieselben nutzerdefinierten Ereignisse unterteilt werden.

Daraus ergibt sich, dass Ereignisse selbst ein System nicht zuverlässig machen. Sie liefern nur Werkzeuge. Zuverlässigkeit entsteht, wenn zu diesen Werkzeugen technische Disziplin hinzugefügt wird. Und wenn es sie gibt, ist die ereignisgesteuerte Architektur nicht mehr nur eine bequeme Technik – sie wird zur Grundlage eines stabilen und berechenbaren Handelssystems.



Schlussfolgerung

Wenn die Programmlogik auf Ereignissen aufbaut, hört die Plattform auf, nur eine Umgebung für EAs zu sein, und wird zur Grundlage für vollwertige Anwendungslösungen. In diesem Modell ähnelt die Programmierung bereits der Entwicklung einer klassischen Desktop-Anwendung: Eine Oberfläche mit Schaltflächen und Bedienfeldern erscheint, Hintergrunddienste werden gestartet, und für verschiedene Arten von Ereignissen werden separate Handler erstellt. Alles ist an seinem Platz, und deshalb ist das System auch deutlich stabiler in der Wartung.

Der ereignisbasierte Ansatz ist besonders wertvoll, wenn ein regulärer EA nicht mehr mit der Rolle einer einzigen Datei für alle Fälle zurechtkommt. Es ermöglicht die Erstellung von Assistenten mit mehreren Symbolen, flexiblen Bedienfeldern und komplexen Steuerungen, die auf semantische Zustandsänderungen reagieren. Es ist einfacher, neue Funktionen zu dieser Architektur hinzuzufügen: Wir müssen nur ein weiteres Ereignis oder einen weiteren Handler einführen, ohne den gesamten Code überarbeiten zu müssen.

Dies ist der Hauptvorteil der ereignisgesteuerten Architektur: Sie sorgt für Ordnung, ohne an Flexibilität zu verlieren. Die ordnungsgemäße Trennung von Handlern, Diensten und Kommunikationskanälen ermöglicht die Schaffung zuverlässiger, skalierbarer und wirklich lebendiger Handelssysteme. Hier geht MQL5 über das traditionelle EA-Format hinaus, nämlich als Umgebung für komplexe Handelsanwendungen, bei denen nicht nur die Marktreaktion wichtig ist, sondern auch die ausgereifte technische Organisation des gesamten Programms.


Programme, die in diesem Artikel verwendet werden

# Name Typ Beschreibung
1 EventExample.mq5 Expert Advisor Ereignisbehandlung EA
2 EventMACD.mq5 Indikator MACD-Indikator mit Generierung von Ereignissen beim Kreuzen von Linien
3 EventRSI.mq5 Indikator RSI-Indikator mit Ereignisgenerierung bei Überschreitung der Niveaus

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

Beigefügte Dateien |
MQL5.zip (5.12 KB)
Letzte Kommentare | Zur Diskussion im Händlerforum (6)
fxsaber
fxsaber | 5 Mai 2026 in 15:00
//+------------------------------------------------------------------+
//| Funktion Handel|
//+------------------------------------------------------------------+
void OnTrade()
  {
   Sleep(0);
//---
  }
//+------------------------------------------------------------------+
//| TradeTransaction-Funktion|
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {
   Sleep(0);
//---
  }
Wofür?
fxsaber
fxsaber | 5 Mai 2026 in 15:02
            if(!cTrade.PositionClose(cPosition.Ticket()))
              {
               PrintFormat("Error close Sell position: %d", GetLastError());
               return;
              }
Es scheint, dass cTrade im Falle eines Fehlers eine Ausgabe enthält.
fxsaber
fxsaber | 5 Mai 2026 in 15:33
fxsaber #:
Die Arbeit in einem Expert Advisor mit Indikatoren ohne Puffer ist für ein Beispiel wahrscheinlich in Ordnung.


In dem Beispiel wird eine strenge Aktualisierung der Symboldaten durchgeführt (es wäre gut, MQL_TESTER zu verwenden).

void OnTick()
  {
//---
   if(BuySignal)
     {
      cSymbol.Refresh();
      cSymbol.RefreshRates();


Aber es wird nicht die Relevanz des Signals und des Ticks überprüft, die durch die Ereignisse berechnet werden. Und das ist ein echtes Problem.


Es kann durch asynchrones OrderSend abgeschwächt, aber nicht gelöst werden. Daher müssen wir auch in einem solchen Beispiel in ChartEvent-Event zusätzlich die Daten des Ticks übergeben, bei dem die Ereigniserzeugung erfolgte.

Chacha Ian Maroa
Chacha Ian Maroa | 11 Mai 2026 in 12:46
Großartig, der Service-Typ von MQL5-Programmen ist so unterschätzt.
Winged Trading
Winged Trading | 11 Mai 2026 in 14:27
Tolles Beispiel, danke.
Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Marktsimulation (Teil 18): Erste Schritte mit SQL (I) Marktsimulation (Teil 18): Erste Schritte mit SQL (I)
Es spielt keine Rolle, welches SQL-Programm wir verwenden: MySQL, SQL Server, SQLite, OpenSQL oder andere. Allen gemeinsam ist die Sprache SQL. Auch wenn wir nicht vorhaben, Workbench zu verwenden, können wir die Datenbank direkt in MetaEditor oder über MQL5 manipulieren oder mit ihr arbeiten, um Aktionen in MetaTrader 5 auszuführen, aber dazu benötigen Sie SQL-Kenntnisse. Hier werden wir also zumindest die Grundlagen lernen.
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.
Hidden-Markov-Modelle in Handelssystemen mit maschinellem Lernen Hidden-Markov-Modelle in Handelssystemen mit maschinellem Lernen
Hidden-Markov-Modelle (HMMs) sind eine leistungsstarke Klasse probabilistischer Modelle, die für die Analyse sequenzieller Daten entwickelt wurden, bei denen beobachtete Ereignisse von einer Sequenz unbeobachteter (versteckter) Zustände abhängen, die einen Markov-Prozess bilden. Zu den wichtigsten Annahmen des HMM gehören die Markov-Eigenschaft für verborgene Zustände, was bedeutet, dass die Wahrscheinlichkeit des Übergangs zum nächsten Zustand nur vom aktuellen Zustand abhängt, und die Unabhängigkeit der Beobachtungen bei Kenntnis des aktuellen verborgenen Zustands.