English 日本語
preview
Selbstoptimierende Expert Advisors in MQL5 (Teil 15): Identifizierung linearer Systeme

Selbstoptimierende Expert Advisors in MQL5 (Teil 15): Identifizierung linearer Systeme

MetaTrader 5Handelssysteme |
20 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Handelssysteme sind komplexe Anwendungen, von denen erwartet wird, dass sie in chaotischen, dynamischen Umgebungen funktionieren – eine Herausforderung selbst für die erfahrensten Entwickler. Es ist nahezu unmöglich, alle richtigen Maßnahmen zu definieren, die eine Handelsanwendung ergreifen sollte, da es praktisch unendlich viele Marktergebnisse gibt. Die Aufrechterhaltung der Kontrolle und die Sicherstellung einer gleichbleibenden Rentabilität unter solchen Unsicherheiten bleibt eine der größten Herausforderungen im algorithmischen Handel.

Einfache Strategien mögen in ruhigen Marktphasen zuverlässig erscheinen, doch sowohl einfache als auch komplexe Systeme versagen häufig, wenn die Volatilität zunimmt. Trotzdem scheinen die Bereiche der Kontrolltheorie und der Signalverarbeitung bei der Bewältigung dieser Herausforderungen nicht ausreichend genutzt zu werden. Die Kontrolltheorie, die sich mit der Aufrechterhaltung der Stabilität in dynamischen und unsicheren Systemen befasst, ist eng mit den Problemen verknüpft, mit denen unsere Gemeinschaft der algorithmischen Händler täglich konfrontiert ist.

Die klassische Steuerungstheorie geht von einem ersten prinzipiellen Verständnis des Systems aus – explizite Formeln, die die Beziehung zwischen Eingängen und Ausgängen beschreiben. Die modernen Finanzmärkte entziehen sich jedoch einer solch klaren mathematischen Struktur. Dies hat zu einem wachsenden Interesse an der Integration von Kontrolltheorie und maschinellem Lernen geführt, das diese Beziehungen direkt aus den Daten annähern kann, anstatt sich auf explizite Gleichungen zu verlassen.

Dieses Konzept hat es in sich: Auch ohne die genauen Steuerungsgleichungen zu kennen, können Praktiker lernen, das Systemverhalten anhand von Daten zu regulieren. Die Kontrolltheorie und der algorithmische Handel haben das gleiche Ziel: die Unsicherheit zu bewältigen und gleichzeitig die Stabilität zu erhalten. Ein rückgekoppelter Regler sagt keine Preise voraus, sondern regelt die Reaktionen des Systems, unterdrückt Überreaktionen auf Störungen und gewährleistet eine gleichmäßige Leistung.

Feedback-Controller verbessern auch die Kapitaleffizienz, indem sie lernen, wann das Kapital effektiv eingesetzt wird, und unnötige Handelsgeschäfte reduzieren. In Verbindung mit maschinellem Lernen können sich diese Systeme autonom anpassen und so ihre Präzision, Kontrolle und Zuverlässigkeit verbessern. Trotz der eindeutigen Überschneidungen klafft zwischen der Kontrolltheorie und dem algorithmischen Handel noch eine erhebliche Forschungslücke – eine Lücke mit großem Potenzial.

In diesem Artikel zeigen wir, wie die Kontrolltheorie selbst die einfachsten Handelssysteme verjüngen kann. Anhand einer einfachen Strategie des gleitenden Durchschnitts – Kaufen, wenn der Kurs über den Durchschnitt steigt, und Verkaufen, wenn er unter den Durchschnitt fällt – untersuchen wir, wie die Rückkopplungskontrolle einer Strategie, die oft als veraltet abgetan wird, Stabilität und Rentabilität verleihen kann. Viele behaupten zwar, dass solche Methoden nicht funktionieren, weil sie „zu gut bekannt“ sind, aber solche Argumente sind empirisch nicht stichhaltig. Unser Ansatz verwendet stattdessen eine Rückkopplungskontrolle, um festzustellen, wann und warum die Strategie erfolgreich ist oder scheitert.

Für unser Experiment haben wir die klassische Strategie des gleitenden Durchschnitts angewandt und alle Parameter festgelegt. Unter Verwendung von zwei Jahren historischer Daten (Januar 2023 – Mai 2025) haben wir den gleitenden Durchschnittszeitraum für die erste Hälfte optimiert und die Leistung für die zweite Hälfte getestet, um eine Vergleichsgrundlage zu schaffen. Sobald dieser Benchmark gesetzt war, lernte der Rückkopplungsregler vollständig aus dem Verhalten des Systems während des Backtestings, ohne jegliche Parameteranpassung.

Anfänglich funktionierten das geregelte und das ungeregelte System identisch, da der Regler noch beobachtete. Sobald das rückkopplungsgesteuerte System jedoch aktiv war, führte es zu erheblichen Verbesserungen:

  • Der Gesamtverlust sank von -$575 auf -$333 (eine Verringerung der ineffizienten Kapitalverwendung um 42%)
  • Der Nettogewinn stieg von -$49 auf +$57
  • Die Zahl der Abschlüsse sank von 78 auf 51 (eine Effizienzsteigerung von 34 %). 
  • Die Gewinnquote stieg von 44 % auf 53 %.
  • Der Gewinnfaktor stieg von 0,91 auf 1,17 – eine Steigerung der Rentabilität um 28 %.

Diese Ergebnisse, die unter identischen Marktbedingungen und Systembeschränkungen erzielt wurden, zeigen die stabilisierende Kraft der Rückkopplungssteuerung. Wo menschliche Intuition und traditionelle Modellierung an ihre Grenzen stoßen, bietet die Kontrolltheorie einen prinzipienfesten Weg nach vorn – sie offenbart tiefere Zusammenhänge und ungenutztes Potenzial in Strategien, die lange als erschöpft galten.


Erste Schritte in MQL5

Um mit der Entwicklung unserer Anwendung zu beginnen, legen wir zunächst wichtige Systemkonstanten fest, die während aller Übungen unverändert bleiben. In späteren Versionen wird die Zahl der Konstanten zunehmen, aber wir beabsichtigen, sie von einer Version zur nächsten zu übertragen.
//+------------------------------------------------------------------+
//|                                  Feedback Control Benchmark .mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define SYMBOL Symbol()
#define MA_SHIFT 0
#define MA_MODE MODE_EMA
#define MA_APPLIED_PRICE PRICE_CLOSE
#define SYSTEM_TIME_FRAME PERIOD_D1
#define MIN_VOLUME SymbolInfoDouble(SYMBOL,SYMBOL_VOLUME_MIN)

Der Leser sollte beachten, dass der Zweck dieser Benchmark-Version darin besteht, einen guten Anfangszeitraum für unsere technischen Indikatoren festzulegen. Wir geben daher einen Tuning-Parameter vor, den wir später mit einem genetischen Algorithmus optimieren wollen.

//+------------------------------------------------------------------+
//| Tuning parameters                                                |
//+------------------------------------------------------------------+
input group "Technical Indicators"
input int  MA_PERIOD = 10;//Moving average period

Als nächstes laden wir die für diese Übung erforderlichen Bibliotheken. Die Handelsbibliothek ist ausreichend. 

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;

Wir definieren auch wichtige globale Variablen, wie z. B. Puffer für die Indikatoren Gleitender Durchschnitt und Average True Range (ATR). Die ATR definiert unser Stop-Loss- und Risikoniveau, das bei allen Übungen gleich bleibt. Außerdem enthalten wir globale Variablen zur Verfolgung der Marktpreise (Eröffnungs-, Höchst-, Tiefst- und Schlusskurs) sowie Handles für unsere technischen Indikatoren.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
double ma[],atr[];
double ask,bid,open,high,low,close,padding;
int    ma_handler,atr_handler;

Wenn die Anwendung zum ersten Mal geladen wird, initialisieren wir Handler für die Indikatoren – einen für den gleitenden Durchschnitt und einen für die ATR. Die ATR misst die Marktvolatilität und legt entsprechend Stop-Loss- und Take-Profit-Levels fest. 

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize the indicator
   ma_handler = iMA(SYMBOL,SYSTEM_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_MODE,MA_APPLIED_PRICE);
   atr_handler = iATR(SYMBOL,SYSTEM_TIME_FRAME,14);
   return(INIT_SUCCEEDED);
  }

Wenn die Anwendung geschlossen wird, deinitialisieren wir die Indikatoren und geben ihre Ressourcen frei, wie es sich in MQL5 bewährt hat.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Release the indicator
   IndicatorRelease(ma_handler);
   IndicatorRelease(atr_handler);
  }

Immer wenn das Terminal neue Preisniveaus empfängt, führen wir unsere Handelslogik aus. Wir prüfen zunächst, ob sich eine neue Tageskerze gebildet hat. Wenn ja, eröffnen wir eine Position, die einmal pro Tag gehandelt wird.

Unsere Handelsstrategie vergleicht die Kursniveaus mit dem Gleitenden Durchschnitt. Während der Backtests in MetaTrader 5 bildet sich die erste Tageskerze jedoch um Mitternacht, wenn die Kurswerte oft flach und unzuverlässig sind. Um dies zu vermeiden, bezieht sich unser Algorithmus auf die Kerzen- und Indikatorwerte des Vortages. Im Wesentlichen entscheidet die Anwendung über die Aktionen von heute auf der Grundlage der Ereignisse von gestern.

Wir führen dann die Handelslogik aus, indem wir prüfen, ob der Schlusskurs über oder unter dem gleitenden Durchschnitt liegt, um zu entscheiden, ob wir kaufen oder verkaufen sollen. 

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new candle has formed
   datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0);
   static datetime time_stamp;

   if(current_time != time_stamp)
     {
      //--- Update the time
      time_stamp = current_time;

      //--- If we have no open positions
      if(PositionsTotal()==0)
        {
         //--- Update indicator buffers
         CopyBuffer(ma_handler,0,1,1,ma);
         CopyBuffer(atr_handler,0,0,1,atr);
         padding = atr[0] * 2;

         //--- Fetch current market prices
         ask = SymbolInfoDouble(SYMBOL,SYMBOL_ASK);
         bid = SymbolInfoDouble(SYMBOL,SYMBOL_BID);
         close = iClose(SYMBOL,SYSTEM_TIME_FRAME,0);

         //--- Check trading signal
         if(close > ma[0])
            Trade.Buy(MIN_VOLUME,SYMBOL,ask,ask-padding,ask+padding);

         if(close < ma[0])
            Trade.Sell(MIN_VOLUME,SYMBOL,bid,ask+padding,ask-padding);
        }
     }
  }
//+------------------------------------------------------------------+

Sobald die Ausführung von abgeschlossen ist, werden alle zuvor definierten Systemkonstanten zurückgesetzt.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef SYMBOL
#undef SYSTEM_TIME_FRAME
#undef MA_APPLIED_PRICE
#undef MA_MODE
#undef MA_SHIFT
#undef MIN_VOLUME
//+------------------------------------------------------------------+

Zusammengesetzt ergibt sich daraus die Benchmark-Version der Anwendung.

//+------------------------------------------------------------------+
//|                                  Feedback Control Benchmark .mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define SYMBOL Symbol()
#define MA_SHIFT 0
#define MA_MODE MODE_EMA
#define MA_APPLIED_PRICE PRICE_CLOSE
#define SYSTEM_TIME_FRAME PERIOD_D1
#define MIN_VOLUME SymbolInfoDouble(SYMBOL,SYMBOL_VOLUME_MIN)

//+------------------------------------------------------------------+
//| Tuning parameters                                                |
//+------------------------------------------------------------------+
input group "Technical Indicators"
input int  MA_PERIOD = 10;//Moving average period

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
double ma[],atr[];
double ask,bid,open,high,low,close,padding;
int    ma_handler,atr_handler;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize the indicator
   ma_handler = iMA(SYMBOL,SYSTEM_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_MODE,MA_APPLIED_PRICE);
   atr_handler = iATR(SYMBOL,SYSTEM_TIME_FRAME,14);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Release the indicator
   IndicatorRelease(ma_handler);
   IndicatorRelease(atr_handler);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new candle has formed
   datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0);
   static datetime time_stamp;

   if(current_time != time_stamp)
     {
      //--- Update the time
      time_stamp = current_time;

      //--- If we have no open positions
      if(PositionsTotal()==0)
        {
         //--- Update indicator buffers
         CopyBuffer(ma_handler,0,1,1,ma);
         CopyBuffer(atr_handler,0,0,1,atr);
         padding = atr[0] * 2;

         //--- Fetch current market prices
         ask = SymbolInfoDouble(SYMBOL,SYMBOL_ASK);
         bid = SymbolInfoDouble(SYMBOL,SYMBOL_BID);
         close = iClose(SYMBOL,SYSTEM_TIME_FRAME,0);

         //--- Check trading signal
         if(close > ma[0])
            Trade.Buy(MIN_VOLUME,SYMBOL,ask,ask-padding,ask+padding);

         if(close < ma[0])
            Trade.Sell(MIN_VOLUME,SYMBOL,bid,ask+padding,ask-padding);
        }
     }

  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef SYMBOL
#undef SYSTEM_TIME_FRAME
#undef MA_APPLIED_PRICE
#undef MA_MODE
#undef MA_SHIFT
#undef MIN_VOLUME
//+------------------------------------------------------------------+



Identifizierung guter Ausgangsbedingungen

Wir können nun unsere Benchmark-Handelsstrategie auswählen und die historischen Daten für die Backtests festlegen. Für diese Übung verwenden wir Daten aus den Jahren 2023 bis 2025 und führen auch Vorwärtstests durch. Beim Vorwärtstest wird die Backtest-Periode in Segmente unterteilt, die gleich groß oder ungleich groß sein können, falls der Begriff nicht bekannt ist. Hier wird der Datensatz in zwei Hälften geteilt und der Vorwärtsparameter auf ½ gesetzt. Auf diese Weise kann der genetische Optimierer die Parameter auf der ersten Hälfte der Daten abstimmen und sie auf der zweiten Hälfte testen. Die zweite Hälfte des Datensatzes wird vor dem Modell verborgen und als endgültige Auswertungsmenge verwendet, die dem Optimierer erst nach dem Training angezeigt wird.

Abbildung 1: Auswahl der Backtest-Tage für unser Optimierungsverfahren

Da wir nun wissen, wie wichtig Vorwärtstests sind, können wir die Modellierungsbedingungen festlegen, unter denen wir unsere Strategie bewerten wollen. Aufgrund von Netzwerkbeschränkungen habe ich den Modellierungsmodus „jeder Tick“ anstelle von „jeder Tick auf der Grundlage echter Ticks“ verwendet, obwohl letzterer realistischere Ergebnisse liefert und für Personen mit stabilen Verbindungen empfohlen wird. Für die Optimierung haben wir aus Gründen der Effizienz den schnellen genetischen Algorithmus verwendet; für eine gründlichere Suche kann eine langsamere, vollständige Version verwendet werden. Die Eingabeparameter wurden durch ihr Minimum, ihr Maximum und ihre Schrittweite definiert, um den Bereich des Optimierers zu kontrollieren.

Abbildung 2: Wählen Sie den schnellen genetischen Algorithmus, der uns hilft, gute Anfangsindikatoren zu identifizieren.

Wir begannen damit, den Verzögerungsparameter auf eine zufällige Verzögerung einzustellen, um die reale Marktlatenz zu simulieren. 

Abbildung 3: Die Tuning-Parameter unserer Handelsanwendung sind denkbar einfach

Die Backtest-Ergebnisse waren schlecht – keine der Konfigurationen war profitabel. 

Abbildung 4: Die Backtest-Ergebnisse erscheinen instabil und müssen verbessert werden

Streudiagramme bestätigten, dass die Verluste in allen Versuchen gleich sind.

Abbildung 5: Es scheint, dass keine der von uns getesteten Konfigurationen in der ersten Hälfte des Optimierungsverfahrens rentabel war

Überraschenderweise erwies sich die Strategie im Vorwärtstest als profitabel, insbesondere wenn der Zeitraum auf 42 festgelegt wurde. Bei der Auswahl des besten Ergebnisses besteht zwar die Gefahr einer Überanpassung, aber lange Bewertungszeiträume verringern diese Wahrscheinlichkeit.

Abbildung 6: Die Ergebnisse des Vorwärtstests waren profitabler als die Ergebnisse des Backtests, die wir erhalten haben.

Im Vorwärtstest wurden viele besonders rentable Konfigurationen ermittelt, was darauf hindeutet, dass die Strategie unter bestimmten Bedingungen gut funktionieren kann, aber in ihrer derzeitigen Form möglicherweise unzuverlässig ist. Eine verlässliche Strategie sollte über die Zeit hinweg konsistent funktionieren, was bei unserer nicht der Fall war.

Abbildung 7: Visualisierung der Leistung unserer Strategie außerhalb der Stichprobe über Datenperioden, die der genetische Optimierer nicht beobachtet hat.


Die Festlegung unserer Benchmark

Um einen Richtwert für die Rentabilität festzulegen, wählen wir zunächst die kompilierte Anwendung aus unserer IDE aus und legen dann die Backtest-Daten fest – denselben Zeitraum, der zuvor verwendet und später für die Vorwärtsprüfung herangezogen wurde.

Abbildung 8: Durchführung eines vollständigen historischen Backtests unserer Handelsanwendung unter Verwendung des besten Zeitraums, den wir ermittelt haben

In einem vollständigen Backtest mit der Einstellung von 42 Perioden erwirtschaftete die Strategie einen Gesamtverlust von 559 $ bei 78 Handelsgeschäften und einem Gewinnfaktor von 0,97, was eher auf einen langfristigen Kapitalverfall als auf ein Wachstum hinweist.

Abbildung 9: Prüfung der detaillierten statistischen Leistung unseres Richtwerts anhand historischer Daten

Die Kapitalkurve dieser Version unserer Handelsstrategie ist sehr volatil, wobei die Strategie insgesamt eine schlechte Performance aufweist. Selbst im anfänglichen Backtest, der die besten Ergebnisse des genetischen Optimierers zeigen sollte, war die Strategie nach zwei Jahren Handel kaum kostendeckend.

Abbildung 10: Die Kapitalkurve, die durch das Kontroll-Setup unserer Handelsanwendung erzeugt wird, scheint sehr instabil zu sein


Die Verbesserung unserer ersten Ergebnisse

Wir sind nun bereit, mit der Verbesserung unserer Benchmark-Rentabilität zu beginnen. Für den Anfang definieren wir zusätzliche Systemkonstanten, die die bereits eingeführten erweitern. Diese neuen Konstanten bestimmen die Parameter, die unser Modell benötigt – zum Beispiel, wie viele Beobachtungen der Feedback-Controller sammeln muss, bevor er das Verhalten der Strategie anpasst. 
//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define SYMBOL            Symbol()
#define MA_PERIOD         42
#define MA_SHIFT          0
#define MA_MODE           MODE_EMA
#define MA_APPLIED_PRICE  PRICE_CLOSE
#define SYSTEM_TIME_FRAME PERIOD_D1
#define MIN_VOLUME        SymbolInfoDouble(SYMBOL,SYMBOL_VOLUME_MIN)
#define OBSERVATIONS      90
#define FEATURES          7
#define MODEL_INPUTS      8  

Wir definieren auch neue globale Variablen, um die Prognosen unseres linearen Systems zusammen mit seinen Eingaben, Zielen und einer Matrix historischer Beobachtungen zu speichern.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
double ma[],atr[];
double ask,bid,open,high,low,close,padding;
int    ma_handler,atr_handler,scenes;
bool   forecast;
matrix snapshots,b,X,y,U,S,VT,current_forecast;
vector s;

Die Initialisierungsfunktion des Expert Advisors wurde leicht geändert, um diese globalen Variablen vorzubereiten. 

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize the indicator
   ma_handler = iMA(SYMBOL,SYSTEM_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_MODE,MA_APPLIED_PRICE);
   atr_handler = iATR(SYMBOL,SYSTEM_TIME_FRAME,14);

//--- Prepare global variables
   forecast = false;
   snapshots = matrix::Zeros(FEATURES,OBSERVATIONS);
   scenes = -1;
   return(INIT_SUCCEEDED);
  }

Wenn sich die Preisniveaus aktualisieren, prüft unsere Handelslogik, ob eine Prognose aus dem linearen System erforderlich ist. Wenn das System noch Beobachtungen sammelt, überspringt es die Prognosen und führt die Handelsgeschäfte auf der Grundlage der vorherigen Logik aus. Sobald genügend Daten gesammelt sind, werden die Prognosen aktiviert. Unabhängig von den offenen Positionen zeichnet das System bei jeder neuen Kerze periodische Schnappschüsse auf, die den Zustand des Modells im Laufe der Zeit festhalten.

Wenn Positionen offen sind, wird die Modellvorhersagemethode aufgerufen, um eine lineare Vorhersage zu erhalten, die später beim Timing von Ausstiegen helfen kann. Im Moment wollen wir einfach beobachten, ob dieses lineare Rückkopplungssystem das Verhalten der Strategie regulieren kann.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new candle has formed
   datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0);
   static datetime time_stamp;

   if(current_time != time_stamp)
     {
      //--- Update the time
      time_stamp = current_time;
      scenes = scenes+1;

      //--- Check how many scenes have elapsed
      if(scenes == (OBSERVATIONS-1))
        {
         forecast   = true;
        }

      //--- If we have no open positions
      if(PositionsTotal()==0)
        {
         //--- Update indicator buffers
         CopyBuffer(ma_handler,0,1,1,ma);
                     CopyBuffer(atr_handler,0,0,1,atr);
            padding = atr[0] * 2;

         //--- Fetch current market prices
         ask = SymbolInfoDouble(SYMBOL,SYMBOL_ASK);
         bid = SymbolInfoDouble(SYMBOL,SYMBOL_BID);
         close = iClose(SYMBOL,SYSTEM_TIME_FRAME,1);

         //--- Do we need to forecast?
         if(!forecast)
           {
            //--- Check trading signal
            check_signal();
           }

         //--- We need a forecast
         else
            if(forecast)
              {
               model_forecast();
              }
        }

      //--- Take a snapshot
      if(!forecast)
         take_snapshot();

      //--- Otherwise, we have positions open
      else
        {
         //--- Let the model decide if we should close or hold our position
         if(forecast)
            model_forecast();

         //--- Otherwise record all observations on the performance of the application
         else
            if(!forecast)
               take_snapshot();
        }
     }
  }
//+------------------------------------------------------------------+

Die Handelslogik wurde in eine separate CheckSignal-Methode umgewandelt, die denselben Regeln folgt: Wenn keine Positionen offen sind, wird gekauft, wenn der Kurs über dem gleitenden Durchschnitt liegt, und verkauft, wenn er darunter liegt.

//+------------------------------------------------------------------+
//| Check for our trading signal                                     |
//+------------------------------------------------------------------+
void check_signal(void)
  {
   if(PositionsTotal() == 0)
     {
      if(close > ma[0])
        {
         Trade.Buy(MIN_VOLUME,SYMBOL,ask,ask-padding,ask+padding);
        }

      if(close < ma[0])
        {
         Trade.Sell(MIN_VOLUME,SYMBOL,bid,ask+padding,ask-padding);
        }
     }
  }

Die Erstellung einer Prognose umfasst die Vorbereitung und Aktualisierung von Momentaufnahmen. Zunächst werden die vorhandenen Snapshots kopiert, dann werden sie mit take_snapshots() aktualisiert. Dann werden die Eingaben (X) und das Ziel (y) unseres linearen Systems vorbereitet: Die erste Zeile von X ist ein Vektor von Einsen (der Achsenabschnitt), während die übrigen Zeilen Systembeobachtungen enthalten. Das Ziel ist der Kontostand, der einen Schritt vor den Schnappschüssen liegt.

Anschließend führen wir die Singulärwertzerlegung (SVD) durch – ein unbeaufsichtigter Algorithmus, der eine Matrix in eine Reihe von Rang-1-Komponenten faktorisiert und so die dominanten Korrelationsstrukturen in den Daten aufdeckt. Der Algorithmus liefert einen Vektor und zwei Matrizen, die wir zur Rekonstruktion unseres linearen Systems verwenden. Der Vektor wird mit der Methode Diag() in eine Diagonalmatrix umgewandelt, und anschließend wird sein Rang überprüft. Wenn der Wert ungleich Null ist, berechnen wir die pseudoinverse Lösung, um die Koeffizienten des Systems zu schätzen, die in B gespeichert sind.

Als Nächstes rufen wir die aktuellen Marktinputs ab und multiplizieren sie mit B, um eine Schätzung aus unserem linearen System zu erhalten. Wenn der prognostizierte Saldo den aktuellen übersteigt, wird gehandelt; andernfalls wird auf bessere Marktbedingungen gewartet. Eine abschließende Prüfung fängt alle Fälle ab, in denen bei der Matrixinversion Fehler auftreten können.

//+------------------------------------------------------------------+
//| Obtain a forecast from our model                                 |
//+------------------------------------------------------------------+
void model_forecast(void)
  {

   Print(scenes);
   Print(snapshots);

//--- Create a copy of the current snapshots
   matrix temp;
   temp.Copy(snapshots);
   snapshots = matrix::Zeros(FEATURES,scenes+1);

   for(int i=0;i<FEATURES;i++)
     {
      snapshots.Row(temp.Row(i),i);
     }

//--- Attach the latest readings to the end
   take_snapshot();

//--- Obtain a forecast for our trading signal
//--- Define the model inputs and outputs

//--- Implement the inputs and outputs
   X = matrix::Zeros(FEATURES+1,scenes);
   y = matrix::Zeros(1,scenes);

//--- The first row is the intercept.
   X.Row(vector::Ones(scenes),0);

//--- Filling in the remaining rows
   for(int i =0; i<scenes;i++)
     {
      //--- Filling in the inputs
      X[1,i] = snapshots[0,i]; //Open
      X[2,i] = snapshots[1,i]; //High
      X[3,i] = snapshots[2,i]; //Low
      X[4,i] = snapshots[3,i]; //Close
      X[5,i] = snapshots[4,i]; //Moving average
      X[6,i] = snapshots[5,i]; //Account equity
      X[7,i] = snapshots[6,i]; //Account balance

      //--- Filling in the target
      y[0,i] = snapshots[6,i+1];//Future account balance
     }

   Print("Finished implementing the inputs and target: ");
   Print("Snapshots:\n",snapshots);
   Print("X:\n",X);
   Print("y:\n",y);

//--- Singular value decomposition
   X.SingularValueDecompositionDC(SVDZ_S,s,U,VT);

//--- Transform s to S, that is the vector to a diagonal matrix
   S = matrix::Zeros(s.Size(),s.Size());
   S.Diag(s,0);

//--- Done
   Print("U");
   Print(U);
   Print("S");
   Print(s);
   Print(S);
   Print("VT");
   Print(VT);

//--- Learn the system's coefficients

//--- Check if S is invertible
   if(S.Rank() != 0)
     {
      //--- Invert S
      matrix S_Inv = S.Inv();
      Print("S Inverse: ",S_Inv);

      //--- Obtain psuedo inverse solution
      b = VT.Transpose().MatMul(S_Inv);
      b = b.MatMul(U.Transpose());
      b = y.MatMul(b);

      //--- Prepare the current inputs
      matrix inputs = matrix::Ones(MODEL_INPUTS,1);
      for(int i=1;i<MODEL_INPUTS;i++)
        {
         inputs[i,0] = snapshots[i-1,scenes];
        }

      //--- Done
      Print("Coefficients:\n",b);
      Print("Inputs:\n",inputs);
      current_forecast = b.MatMul(inputs);
      Print("Forecast:\n",current_forecast[0,0]);

      //--- The next trade may be expected to be profitable
      if(current_forecast[0,0] > AccountInfoDouble(ACCOUNT_BALANCE))
        {
         //--- Feedback
         Print("Next trade expected to be profitable. Checking for trading singals.");
         //--- Check for our trading signal
         check_signal();
        }
        
        //--- Next trade may be expected to be unprofitable
        else
         {
            Print("Next trade expected to be unprofitable. Waiting for better market conditions");
         }
     }

//--- S is not invertible!
   else
     {
      //--- Error
      Print("[Critical Error] Singular values are not invertible.");
     }
  }

Wir definieren auch eine Methode zur Aufzeichnung von System-Snapshots. Jeder Snapshot speichert die Werte von Interesse in einer Matrix (in MQL5 werden Matrizen nach Zeilen und dann nach Spalten referenziert).

//+------------------------------------------------------------------+
//| Take a snapshot of the market                                    |
//+------------------------------------------------------------------+
void take_snapshot(void)
  {
//--- Record system state
   snapshots[0,scenes]=iOpen(SYMBOL,SYSTEM_TIME_FRAME,1); //Open
   snapshots[1,scenes]=iHigh(SYMBOL,SYSTEM_TIME_FRAME,1); //High
   snapshots[2,scenes]=iLow(SYMBOL,SYSTEM_TIME_FRAME,1);  //Low
   snapshots[3,scenes]=iClose(SYMBOL,SYSTEM_TIME_FRAME,1);//Close
   snapshots[4,scenes]=ma[0];                             //Moving average
   snapshots[5,scenes]=AccountInfoDouble(ACCOUNT_EQUITY); //Equity
   snapshots[6,scenes]=AccountInfoDouble(ACCOUNT_BALANCE);//Balance

   Print("Scene: ",scenes);
   Print(snapshots);
  }

Wenn die Anwendung die Ausführung beendet, sind alle Systemkonstanten undefiniert.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef SYMBOL
#undef SYSTEM_TIME_FRAME
#undef MA_APPLIED_PRICE
#undef MA_MODE
#undef MA_SHIFT
#undef MIN_VOLUME
#undef MODEL_INPUTS
#undef FEATURES
#undef OBSERVATIONS
//+------------------------------------------------------------------+
Zusammengenommen ergibt dies unsere Feedback-Controller-Version der Handelsstrategie.
//+------------------------------------------------------------------+
//|                                  Feedback Control Benchmark .mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define SYMBOL Symbol()
#define MA_PERIOD 42
#define MA_SHIFT 0
#define MA_MODE MODE_EMA
#define MA_APPLIED_PRICE PRICE_CLOSE
#define SYSTEM_TIME_FRAME PERIOD_D1
#define MIN_VOLUME SymbolInfoDouble(SYMBOL,SYMBOL_VOLUME_MIN)
#define OBSERVATIONS 90
#define FEATURES     7
#define MODEL_INPUTS 8  

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
double ma[],atr[];
double ask,bid,open,high,low,close,padding;
int    ma_handler,atr_handler,scenes;
bool   forecast;
matrix snapshots,b,X,y,U,S,VT,current_forecast;
vector s;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize the indicator
   ma_handler = iMA(SYMBOL,SYSTEM_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_MODE,MA_APPLIED_PRICE);
   atr_handler = iATR(SYMBOL,SYSTEM_TIME_FRAME,14);

//--- Prepare global variables
   forecast = false;
   snapshots = matrix::Zeros(FEATURES,OBSERVATIONS);
   scenes = -1;
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Release the indicator
   IndicatorRelease(ma_handler);
   IndicatorRelease(atr_handler);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new candle has formed
   datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0);
   static datetime time_stamp;

   if(current_time != time_stamp)
     {
      //--- Update the time
      time_stamp = current_time;
      scenes = scenes+1;

      //--- Check how many scenes have elapsed
      if(scenes == (OBSERVATIONS-1))
        {
         forecast   = true;
        }

      //--- If we have no open positions
      if(PositionsTotal()==0)
        {
         //--- Update indicator buffers
         CopyBuffer(ma_handler,0,1,1,ma);
                     CopyBuffer(atr_handler,0,0,1,atr);
            padding = atr[0] * 2;

         //--- Fetch current market prices
         ask = SymbolInfoDouble(SYMBOL,SYMBOL_ASK);
         bid = SymbolInfoDouble(SYMBOL,SYMBOL_BID);
         close = iClose(SYMBOL,SYSTEM_TIME_FRAME,1);

         //--- Do we need to forecast?
         if(!forecast)
           {
            //--- Check trading signal
            check_signal();
           }

         //--- We need a forecast
         else
            if(forecast)
              {
               model_forecast();
              }
        }

      //--- Take a snapshot
      if(!forecast)
         take_snapshot();

      //--- Otherwise, we have positions open
      else
        {
         //--- Let the model decide if we should close or hold our position
         if(forecast)
            model_forecast();

         //--- Otherwise record all observations on the performance of the application
         else
            if(!forecast)
               take_snapshot();
        }
     }
  }
//+------------------------------------------------------------------+


//+------------------------------------------------------------------+
//| Check for our trading signal                                     |
//+------------------------------------------------------------------+
void check_signal(void)
  {
   if(PositionsTotal() == 0)
     {
      if(close > ma[0])
        {
         Trade.Buy(MIN_VOLUME,SYMBOL,ask,ask-padding,ask+padding);
        }

      if(close < ma[0])
        {
         Trade.Sell(MIN_VOLUME,SYMBOL,bid,ask+padding,ask-padding);
        }
     }
  }

//+------------------------------------------------------------------+
//| Obtain a forecast from our model                                 |
//+------------------------------------------------------------------+
void model_forecast(void)
  {

   Print(scenes);
   Print(snapshots);

//--- Create a copy of the current snapshots
   matrix temp;
   temp.Copy(snapshots);
   snapshots = matrix::Zeros(FEATURES,scenes+1);

   for(int i=0;i<FEATURES;i++)
     {
      snapshots.Row(temp.Row(i),i);
     }

//--- Attach the latest readings to the end
   take_snapshot();

//--- Obtain a forecast for our trading signal
//--- Define the model inputs and outputs

//--- Implement the inputs and outputs
   X = matrix::Zeros(FEATURES+1,scenes);
   y = matrix::Zeros(1,scenes);

//--- The first row is the intercept.
   X.Row(vector::Ones(scenes),0);

//--- Filling in the remaining rows
   for(int i =0; i<scenes;i++)
     {
      //--- Filling in the inputs
      X[1,i] = snapshots[0,i]; //Open
      X[2,i] = snapshots[1,i]; //High
      X[3,i] = snapshots[2,i]; //Low
      X[4,i] = snapshots[3,i]; //Close
      X[5,i] = snapshots[4,i]; //Moving average
      X[6,i] = snapshots[5,i]; //Account equity
      X[7,i] = snapshots[6,i]; //Account balance

      //--- Filling in the target
      y[0,i] = snapshots[6,i+1];//Future account balance
     }

   Print("Finished implementing the inputs and target: ");
   Print("Snapshots:\n",snapshots);
   Print("X:\n",X);
   Print("y:\n",y);

//--- Singular value decomposition
   X.SingularValueDecompositionDC(SVDZ_S,s,U,VT);

//--- Transform s to S, that is the vector to a diagonal matrix
   S = matrix::Zeros(s.Size(),s.Size());
   S.Diag(s,0);

//--- Done
   Print("U");
   Print(U);
   Print("S");
   Print(s);
   Print(S);
   Print("VT");
   Print(VT);

//--- Learn the system's coefficients

//--- Check if S is invertible
   if(S.Rank() != 0)
     {
      //--- Invert S
      matrix S_Inv = S.Inv();
      Print("S Inverse: ",S_Inv);

      //--- Obtain psuedo inverse solution
      b = VT.Transpose().MatMul(S_Inv);
      b = b.MatMul(U.Transpose());
      b = y.MatMul(b);

      //--- Prepare the current inputs
      matrix inputs = matrix::Ones(MODEL_INPUTS,1);
      for(int i=1;i<MODEL_INPUTS;i++)
        {
         inputs[i,0] = snapshots[i-1,scenes];
        }

      //--- Done
      Print("Coefficients:\n",b);
      Print("Inputs:\n",inputs);
      current_forecast = b.MatMul(inputs);
      Print("Forecast:\n",current_forecast[0,0]);

      //--- The next trade may be expected to be profitable
      if(current_forecast[0,0] > AccountInfoDouble(ACCOUNT_BALANCE))
        {
         //--- Feedback
         Print("Next trade expected to be profitable. Checking for trading singals.");
         //--- Check for our trading signal
         check_signal();
        }
        
        //--- Next trade may be expected to be unprofitable
        else
         {
            Print("Next trade expected to be unprofitable. Waiting for better market conditions");
         }
     }

//--- S is not invertible!
   else
     {
      //--- Error
      Print("[Critical Error] Singular values are not invertible.");
     }
  }

//+------------------------------------------------------------------+
//| Take a snapshot of the market                                    |
//+------------------------------------------------------------------+
void take_snapshot(void)
  {
//--- Record system state
   snapshots[0,scenes]=iOpen(SYMBOL,SYSTEM_TIME_FRAME,1); //Open
   snapshots[1,scenes]=iHigh(SYMBOL,SYSTEM_TIME_FRAME,1); //High
   snapshots[2,scenes]=iLow(SYMBOL,SYSTEM_TIME_FRAME,1);  //Low
   snapshots[3,scenes]=iClose(SYMBOL,SYSTEM_TIME_FRAME,1);//Close
   snapshots[4,scenes]=ma[0];                             //Moving average
   snapshots[5,scenes]=AccountInfoDouble(ACCOUNT_EQUITY); //Equity
   snapshots[6,scenes]=AccountInfoDouble(ACCOUNT_BALANCE);//Balance

   Print("Scene: ",scenes);
   Print(snapshots);
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef SYMBOL
#undef SYSTEM_TIME_FRAME
#undef MA_APPLIED_PRICE
#undef MA_MODE
#undef MA_SHIFT
#undef MIN_VOLUME
#undef MODEL_INPUTS
#undef FEATURES
#undef OBSERVATIONS
//+------------------------------------------------------------------+

Wenn man es über dasselbe Backtest-Fenster wie zuvor laufen lässt, zeigt sich eine dramatische Verbesserung. 

Abbildung 11: Festlegung eines Verbesserungsmaßstabs mit Hilfe unseres linearen Rückkopplungsreglers

Das System geht von anhaltenden Verlusten zu nachhaltiger Rentabilität über. Die Genauigkeit klettert vom mittleren 40 %-Bereich auf über 50 %, während die Anzahl der Abschlüsse sinkt – ein Zeichen für mehr Effizienz. Unsere Sharpe Ratio und unser Erholungsfaktor haben sich deutlich verbessert. Und all diese Verbesserungen wurden erzielt, ohne dass wir der Anwendung ausdrücklich gesagt hätten, was genau sie tun soll, um besser zu funktionieren.

Abbildung 12: Eine detaillierte Analyse der Verbesserungen durch das lineare System, die wir anhand der von uns aufgezeichneten Beobachtungen ermittelt haben

Die neue Kapitalkurve ersetzt die frühere Instabilität durch stetiges Wachstum, und frühe Rückschläge treten in den folgenden Perioden nicht mehr auf. Selbst in Verlustphasen fällt das System nicht mehr so tief wie früher.

Abbildung 13: Visualisierung der Kapitalkurve, die von der verfeinerten Version unserer Handelsanwendung erzeugt wird



Schlussfolgerung

Damit sind wir am Ende unserer heutigen Diskussion angelangt. Wir glauben, dass sein Artikel Ihnen gezeigt hat, wie Feedback-Controller mit maschinellem Lernen ergänzt werden können, um Unsicherheiten zu managen, die Kapitaleffizienz zu verbessern und Handelssysteme in volatilen oder sich verändernden Märkten mit Hilfe der MQL5-API zu stabilisieren. Die Leser erhalten ein klares Verständnis dafür, warum herkömmliche Strategien häufig scheitern und wie sie mithilfe datengestützter Methoden systematisch verbessert werden können. Gleichzeitig lernen sie praktische Möglichkeiten zur Implementierung von Feedback-Controllern, zur Leistungsoptimierung und zum Risikomanagement kennen. Die vorgestellten Konzepte versetzen Sie in die Lage, instabile oder leistungsschwache Strategien in kontrollierte, profitable Systeme umzuwandeln.

Dateiname Beschreibung der Datei
Feedback Control Benchmark 1.mq5 Die klassische Version der Strategie, die wir durch Beobachtung ihrer Beziehung zum Markt übertreffen wollten. 
Feedback Control Benchmark 2.mq5 Der Feedback-Controller, den wir eingesetzt haben, um die Beziehung zwischen unserer Strategie und den aktuellen Marktbedingungen zu erfahren.

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/19891

Statistische Arbitrage durch kointegrierte Aktien (Teil 6): Bewertungssystem Statistische Arbitrage durch kointegrierte Aktien (Teil 6): Bewertungssystem
In diesem Artikel schlagen wir ein Bewertungssystem für die Strategien der Rückkehr zum Mittelwert vor, das auf der statistischen Arbitrage von kointegrierten Aktien basiert. In dem Artikel werden Kriterien vorgeschlagen, die von der Liquidität und den Transaktionskosten bis zur Anzahl der Kointegrationsränge und der Zeit bis zur Umkehrung des Mittelwerts reichen, wobei die strategischen Kriterien der Datenhäufigkeit (Zeitrahmen) und des Rückblickzeitraums für die Kointegrationstests berücksichtigt werden, die vor der Bewertung der Rangfolge richtig bewertet werden. Die für die Reproduktion des Backtests erforderlichen Dateien werden zur Verfügung gestellt, und ihre Ergebnisse werden ebenfalls kommentiert.
Der MQL5 Standard Library Explorer (Teil 2): Verbinden mit Bibliothekskomponenten Der MQL5 Standard Library Explorer (Teil 2): Verbinden mit Bibliothekskomponenten
Heute machen wir einen wichtigen Schritt, damit jeder Entwickler versteht, wie man Klassenstrukturen liest und schnell Expert Advisors mit der MQL5-Standardbibliothek erstellt. Die Bibliothek ist reichhaltig und ausbaufähig, aber es kann sich anfühlen, als würde man ein komplexes Toolkit ohne Handbuch in die Hand bekommen. Hier wird eine alternative Integrationsroutine vorgestellt und diskutiert – ein prägnanter, wiederholbarer Arbeitsablauf, der zeigt, wie sich Klassen in realen Projekten zuverlässig verbinden lassen.
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.
Statistische Arbitrage durch kointegrierte Aktien (Teil 5): Screening Statistische Arbitrage durch kointegrierte Aktien (Teil 5): Screening
In diesem Artikel wird ein Verfahren zum Screening von Vermögenswerten für eine statistische Arbitragestrategie durch kointegrierte Aktien vorgeschlagen. Das System beginnt mit der regulären Filterung nach wirtschaftlichen Faktoren, wie z. B. Vermögensbereich und Branche, und endet mit einer Liste von Kriterien für ein Scoring-System. Für jeden statistischen Test, der beim Screening verwendet wurde, wurde eine entsprechende Python-Klasse entwickelt: Pearson-Korrelation, Engle-Granger-Kointegration, Johansen-Kointegration und ADF/KPSS-Stationarität. Diese Python-Klassen werden zusammen mit einer persönlichen Anmerkung des Autors über den Einsatz von KI-Assistenten für die Softwareentwicklung bereitgestellt.