English
preview
Automatisieren von Handelsstrategien in MQL5 (Teil 43): Adaptive lineare Regressionskanalstrategie

Automatisieren von Handelsstrategien in MQL5 (Teil 43): Adaptive lineare Regressionskanalstrategie

MetaTrader 5Handelssysteme |
16 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Einführung

In unserem vorigen Artikel (Teil 42) haben wir ein sitzungsbasiertes System Opening-Range-Breakout (ORB) in MetaQuotes Language 5 (MQL5) entwickelt, das nutzerdefinierte Sitzungsstartzeiten und Eröffnungsbereichsdauern in Minuten zulässt, automatisch das wahre Hoch und Tief auf einem ausgewählten Zeitrahmen bestimmt und Trades nur in der Ausbruchsrichtung ausführt. In Teil 43 entwickeln wir eine adaptive Strategie des linearen Regressionskanals.

Dieses System berechnet eine lineare Regressionslinie mit den Bändern der Standardabweichung über eine nutzerdefinierte Periodenlänge. Sie wird nur aktiviert, wenn die absolute Steigung eine Mindestschwelle überschreitet, um einen Markt in einem Trend zu gewährleisten. Darüber hinaus wird der Kanal automatisch neu erstellt, wenn der Kurs über einen konfigurierbaren Prozentsatz der Kanalbreite hinaus abweicht, und es werden Positionen bei sauberen Ausbrüchen aus dem Kanal eröffnet. Wir werden die folgenden Themen behandeln:

  1. Verständnis des Systems für den adaptiven linearen Regressionskanal
  2. Implementation in MQL5
  3. Backtests
  4. Schlussfolgerung

Am Ende werden Sie ein funktionierendes MQL5-Programm haben, das einen dynamischen Regressionskanal mit gefüllten Abweichungszonen, Ausbruchserkennung, Kreuzen der Mittellinie und normalen/umgekehrten Handelsmodi unterhält – fangen wir an!


Verständnis des Systems für den adaptiven linearen Regressionskanal

Die Strategie des linearen Regressionskanals legt eine lineare Regressionsgerade nach der Methode der kleinsten Quadrate über eine festgelegte Anzahl von Balken an, um die Richtung und Stärke des zugrunde liegenden Trends zu ermitteln, und fügt anschließend parallele Bänder in einer definierten Anzahl von Standardabweichungen oberhalb und unterhalb der Regressionsgeraden hinzu, um eine obere und eine untere Grenze zu bilden. Auf diese Weise entsteht ein dynamischer Preiskanal, der die erwartete Preisspanne in einem Trendmarkt darstellt: Ein innerhalb des Kanals oszillierender Preis deutet auf eine Fortsetzung hin, Berührungen oder leichte Durchbrüche der Grenzen bieten Rücksetzereinstiege, während erhebliche Abweichungen eine potenzielle Erschöpfung oder die Notwendigkeit einer Neuberechnung der Regression anhand neuerer Daten signalisieren. In der Regel kaufen wir, wenn der Kurs unterhalb des unteren Kanals schließt, und verkaufen, wenn der Kurs oberhalb des oberen Kanals schließt.

In unserer Implementierung berechnen wir die Regressionssteigung, den Achsenabschnitt und die Standardabweichung über einen konfigurierbaren Zeitraum und erstellen den Kanal nur dann, wenn die absolute Steigung den Mindestschwellenwert überschreitet, um eine Richtungsänderung zu bestätigen. Der Kanal wird vom ältesten Balken des Zeitraums bis zu einem zukünftigen Punkt verankert, der um einen Prozentsatz seiner Dauer verlängert wird und in zwei farbigen Zonen (rosa obere Hälfte, hellgrüne untere Hälfte) mit durchgezogenen Trendlinien für die obere, mittlere und untere Grenze gefüllt ist. Bei jedem neuen Balken wird der Kanal entweder um einen Balken nach rechts erweitert, wenn der Preis in seinem Bereich bleibt, oder er wird vollständig neu erstellt, wenn der Preis über einen bestimmten Prozentsatz der Kanalbreite hinaus abweicht.

Der Handel wird bei sauberen Ausbrüchen innerhalb des Kanals mit festem Pip-Stop-Loss/Take-Profit, maximal gleichzeitigen Positionen pro Richtung und einer Option für den inversen Modus eröffnet; alle Positionen in einer Richtung werden sofort geschlossen, wenn die Mittellinie überschritten wird. Die Beschriftungen für die oberen, mittleren und unteren Kanäle bewegen sich mit dem rechten Rand, und Pfeile markieren jeden Eintrag. Im inversen Modus wird einfach die Kauf- und Verkaufslogik vertauscht, sodass dasselbe Programm mit Ausbrüchen die Rückkehr zum Mittelwert anstelle von Fortsetzungs-Rücksetzer handeln kann. Wir dachten uns, dass dies für den Fall, dass wir das Gegenteil machen wollen, hilfreich sein würde. Kurz gesagt, hier ist eine visuelle Darstellung unserer Ziele.

SYSTEM DER LINEARE REGRESSIONSKANÄLE


Implementation in MQL5

Um das Programm in MQL5 zu erstellen, öffnen wir den MetaEditor, gehen zum Navigator, suchen den Ordner Experts, klicken auf die Registerkarte „Neu“ und folgen den Anweisungen, um die Datei zu erstellen. Sobald sie erstellt ist, müssen wir in der Programmierumgebung einige Eingabeparameter und globale Variablen deklarieren, die wir im gesamten Programm verwenden werden.

//+------------------------------------------------------------------+
//|                                 Linear Regression Channel EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade obj_Trade;                                                 //--- Trade object

//+------------------------------------------------------------------+
//| Enums                                                            |
//+------------------------------------------------------------------+
enum TradeMode {                                                  // Define trade mode enum
   Normal,                                                        // Normal
   Inverse                                                        // Inverse
};

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input int      RegressionPeriod       = 100;                      // Period for regression calculation
input double   Deviations             = 2.0;                      // Standard deviation multiplier for channel
input double   MinSlopeThreshold      = 0.00001;                  // Min absolute slope to identify clear trend (low for detection)
input int      UpdateThresholdPercent = 30;                       // Update threshold in percent of channel width (e.g., 30 for 30%)
input double   ExtensionPercent       = 50.0;                     // Initial extension percent of channel length to the right
input TradeMode TradeDirection        = Normal;                   // Trade Mode
input double   Lots                   = 0.01;                     // Lot size
input int      StopLossPips           = 100;                      // Stop loss in pips
input int      TakeProfitPips         = 100;                      // Take profit in pips
input int      MaxBuys                = 2;                        // Maximum open buy positions
input int      MaxSells               = 2;                        // Maximum open sell positions
input int      MagicNumber            = 123456;                   // Magic number for positions
input int      Slippage               = 3;                        // Slippage

Wir beginnen die Implementierung, indem wir die Handelsbibliothek mit „#include <Trade\Trade.mqh>“ einbinden, die die Klasse CTrade für die Auftragsausführung und das Positionsmanagement bereitstellt. Wir deklarieren das Objekt „obj_Trade“ global in der Klasse „CTrade“, um es im gesamten Programm zum Senden von Aufträgen und Ändern von Positionen zu verwenden.

Wir definieren die Enumeration „TradeMode“ mit zwei Optionen: “Normal“ für den standardmäßigen Handel des Rücksetzers-zum-Kanal (Kauf bei Einbrüchen unter das untere Band in Aufwärtstrends, Verkauf bei Erholungen über das obere Band in Abwärtstrends) und „Inverse“ zur Umkehrung der Logik für den Handel mit Ausbrüchen der Rückkehr zum Mittelwert. Anschließend legen wir die Eingabeparameter fest, die wir direkt in den Programmeigenschaften anpassen können. Dazu gehören „RegressionPeriod“, um festzulegen, wie viele Balken für die Berechnung der linearen Regression verwendet werden, „Deviations“ als Standardabweichungsmultiplikator für die Kanalbreite (in der Regel 2,0 für ca. 95 % Eingrenzung), „MinSlopeThreshold“ als minimaler absoluter Steigungswert, der erforderlich ist, um den Markttrend zu berücksichtigen und einen Kanal zu erstellen, und der Rest, den wir mit Kommentaren versehen haben, um ihn leicht verständlich zu machen. Diese Eingaben geben uns die volle Kontrolle über das Kanalverhalten, das Risikomanagement und den Handelsstil, ohne dass wir das Programm ändern müssen. Die von uns verwendeten Werte sind Standardwerte und können zur Anpassung jederzeit geändert werden. Wenn Sie kompilieren, sollten Sie folgendes Fenster sehen.

EINGABEPARAMETER-FENSTER

Nachdem wir die Eingaben gemacht haben, können wir nun einige globale Variablen für die Verwendung im Programm definieren.

//--- Global variables
datetime lastBarTime    = 0;                                      //--- Last bar time
double   channelUpper, channelLower, channelMiddle;               //--- Channel levels
string   channelName    = "LRC_Channel";                          //--- Channel name
string   upperLabelName = "LRC_Upper_Label";                      //--- Upper label name
string   middleLabelName = "LRC_Middle_Label";                    //--- Middle label name
string   lowerLabelName = "LRC_Lower_Label";                      //--- Lower label name
bool     hasValidChannel = false;                                 //--- Valid channel flag
datetime fixedTimeOld   = 0;                                      //--- Fixed old time
double   slope_global   = 0, intercept_global = 0, stdDev_global = 0; //--- Global slope, intercept, stdDev
long     period_sec     = PeriodSeconds(_Period);                 //--- Period seconds
int      arrowCounter   = 0;                                      //--- Arrow counter
double   current_right_x = 0;                                     //--- Current right x

Wir fahren fort, indem wir zusätzliche globale Variablen deklarieren, um die adaptive Kanallogik und die Visualisierung zu unterstützen. Wir verwenden „lastBarTime“, um den Zeitstempel des zuletzt verarbeiteten Balkens zu ermitteln. Die aktuellen projizierten Kanalpegel werden in den Feldern „channelUpper“, „channelLower“ und „channelMiddle“ gespeichert, um bei Ausbruchsprüfungen schnell darauf zugreifen zu können. Konstante Strings definieren Objektnamen: „channelName“ als Basis „LRC_Channel“ für das mehrteilige Kanalobjekt, mit „upperLabelName“, „middleLabelName“ und „lowerLabelName“ für die beweglichen Textbeschriftungen, die jede Begrenzung kennzeichnen.

Das boolesche Flag „hasValidChannel“ zeigt an, ob ein Trend-Regressionskanal derzeit aktiv ist, während „fixedTimeOld“ den Datumsanker des ältesten Balkens im Regressionszeitraum für konsistente x-Koordinatenberechnungen enthält. Wir speichern die berechneten Regressionsparameter global als „slope_global“, „intercept_global“ und „stdDev_global“, sodass sie für Projektionen wiederverwendet werden können, ohne dass sie bei jedem Tick neu berechnet werden müssen. „period_sec“ erfasst die Dauer eines Balkens in Sekunden über „PeriodSeconds(_Period)“ für genaue Zeit-zu-X-Umrechnungen. Das ganzzahlige „arrowCounter“ sorgt für eine eindeutige Benennung der Eingangspfeile, und „current_right_x“ verfolgt die x-Koordinate des äußersten rechten Punktes des Kanals, während er Balken für Balken erweitert wird, was präzise Erweiterungen um einen Balken ermöglicht, ohne dass das gesamte Kanalobjekt neu erstellt werden muss. Dies ist wichtig, damit der Kanal nicht flackert. Wenn das erledigt ist, beginnen wir mit der Implementierung, indem wir Hilfsfunktionen für das Rendern des Kanals definieren.

//+-----------------------------------------------------------------------------------+
//| Create Linear Regression Channel using channels for fill and trendlines for lines |
//+-----------------------------------------------------------------------------------+
bool ChannelCreate(const long chart_ID, const string name, const int sub_window, datetime time1, datetime time2) {
   double price1_middle = intercept_global;                       //--- Price1 middle
   double price2_middle = intercept_global + slope_global * current_right_x; //--- Price2 middle
   double price1_upper = price1_middle + Deviations * stdDev_global; //--- Price1 upper
   double price2_upper = price2_middle + Deviations * stdDev_global; //--- Price2 upper
   double price1_lower = price1_middle - Deviations * stdDev_global; //--- Price1 lower
   double price2_lower = price2_middle - Deviations * stdDev_global; //--- Price2 lower
   // Upper-middle fill channel
   string um_name = name + "_um";                                 //--- UM name
   if (!ObjectCreate(chart_ID, um_name, OBJ_CHANNEL, sub_window, time1, price1_upper, time2, price2_upper, time1, price1_middle)) { //--- Create UM channel
      Print(__FUNCTION__, ": failed to create upper-middle channel! Error code = ", GetLastError()); //--- Log error
      return(false);                                              //--- Return failure
   }
   ObjectSetInteger(chart_ID, um_name, OBJPROP_COLOR, clrPink);   //--- Set color
   ObjectSetInteger(chart_ID, um_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style
   ObjectSetInteger(chart_ID, um_name, OBJPROP_WIDTH, 1);         //--- Set width
   ObjectSetInteger(chart_ID, um_name, OBJPROP_FILL, true);       //--- Set fill
   ObjectSetInteger(chart_ID, um_name, OBJPROP_BACK, true);       //--- Set back
   ObjectSetInteger(chart_ID, um_name, OBJPROP_SELECTABLE, true); //--- Set selectable
   ObjectSetInteger(chart_ID, um_name, OBJPROP_SELECTED, false);  //--- Set not selected
   ObjectSetInteger(chart_ID, um_name, OBJPROP_RAY_RIGHT, false); //--- Set no ray
   ObjectSetInteger(chart_ID, um_name, OBJPROP_HIDDEN, false);    //--- Set not hidden
   ObjectSetInteger(chart_ID, um_name, OBJPROP_ZORDER, 0);        //--- Set zorder
   // Middle-lower fill channel
   string ml_name = name + "_ml";                                 //--- ML name
   if (!ObjectCreate(chart_ID, ml_name, OBJ_CHANNEL, sub_window, time1, price1_middle, time2, price2_middle, time1, price1_lower)) { //--- Create ML channel
      Print(__FUNCTION__, ": failed to create middle-lower channel! Error code = ", GetLastError()); //--- Log error
      return(false);                                              //--- Return failure
   }
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_COLOR, clrLightGreen); //--- Set color
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_WIDTH, 1);         //--- Set width
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_FILL, true);       //--- Set fill
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_BACK, true);       //--- Set back
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_SELECTABLE, true); //--- Set selectable
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_SELECTED, false);  //--- Set not selected
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_RAY_RIGHT, false); //--- Set no ray
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_HIDDEN, false);    //--- Set not hidden
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_ZORDER, 0);        //--- Set zorder
   // Upper trendline
   string upper_name = name + "_upper";                           //--- Upper name
   if (!ObjectCreate(chart_ID, upper_name, OBJ_TREND, sub_window, time1, price1_upper, time2, price2_upper)) { //--- Create upper trend
      Print(__FUNCTION__, ": failed to create upper trendline! Error code = ", GetLastError()); //--- Log error
      return(false);                                              //--- Return failure
   }
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_COLOR, clrRed); //--- Set color
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_WIDTH, 1);      //--- Set width
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_BACK, false);   //--- Set foreground
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_SELECTABLE, true); //--- Set selectable
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_SELECTED, false); //--- Set not selected
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_RAY_RIGHT, false); //--- Set no ray
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_HIDDEN, false); //--- Set not hidden
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_ZORDER, 1);     //--- Set zorder
   // Middle trendline
   string middle_name = name + "_middle";                         //--- Middle name
   if (!ObjectCreate(chart_ID, middle_name, OBJ_TREND, sub_window, time1, price1_middle, time2, price2_middle)) { //--- Create middle trend
      Print(__FUNCTION__, ": failed to create middle trendline! Error code = ", GetLastError()); //--- Log error
      return(false);                                              //--- Return failure
   }
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_COLOR, clrBlue); //--- Set color
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_WIDTH, 1);     //--- Set width
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_BACK, false);  //--- Set foreground
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_SELECTABLE, true); //--- Set selectable
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_SELECTED, false); //--- Set not selected
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_RAY_RIGHT, false); //--- Set no ray
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_HIDDEN, false); //--- Set not hidden
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_ZORDER, 1);    //--- Set zorder
   // Lower trendline
   string lower_name = name + "_lower";                           //--- Lower name
   if (!ObjectCreate(chart_ID, lower_name, OBJ_TREND, sub_window, time1, price1_lower, time2, price2_lower)) { //--- Create lower trend
      Print(__FUNCTION__, ": failed to create lower trendline! Error code = ", GetLastError()); //--- Log error
      return(false);                                              //--- Return failure
   }
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_COLOR, clrGreen); //--- Set color
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_WIDTH, 1);      //--- Set width
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_BACK, false);   //--- Set foreground
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_SELECTABLE, true); //--- Set selectable
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_SELECTED, false); //--- Set not selected
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_RAY_RIGHT, false); //--- Set no ray
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_HIDDEN, false); //--- Set not hidden
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_ZORDER, 1);     //--- Set zorder
   return(true);                                                  //--- Return success
}

//+------------------------------------------------------------------+
//| Delete the channel                                               |
//+------------------------------------------------------------------+
bool ChannelDelete(const long chart_ID, const string name) {
   bool success = true;                                           //--- Init success
   if (!ObjectDelete(chart_ID, name + "_um")) {                   //--- Delete um
      Print(__FUNCTION__, ": failed to delete um! Error code = ", GetLastError()); //--- Log error
      success = false;                                            //--- Set failure
   }
   if (!ObjectDelete(chart_ID, name + "_ml")) {                   //--- Delete ml
      Print(__FUNCTION__, ": failed to delete ml! Error code = ", GetLastError()); //--- Log error
      success = false;                                            //--- Set failure
   }
   if (!ObjectDelete(chart_ID, name + "_upper")) {                //--- Delete upper
      Print(__FUNCTION__, ": failed to delete upper! Error code = ", GetLastError()); //--- Log error
      success = false;                                            //--- Set failure
   }
   if (!ObjectDelete(chart_ID, name + "_middle")) {               //--- Delete middle
      Print(__FUNCTION__, ": failed to delete middle! Error code = ", GetLastError()); //--- Log error
      success = false;                                            //--- Set failure
   }
   if (!ObjectDelete(chart_ID, name + "_lower")) {                //--- Delete lower
      Print(__FUNCTION__, ": failed to delete lower! Error code = ", GetLastError()); //--- Log error
      success = false;                                            //--- Set failure
   }
   return(success);                                               //--- Return success
}

Hier implementieren wir die Funktion „ChannelCreate“, um den visuellen linearen Regressionskanal mit einer Kombination aus gefüllten Kanalobjekten und Trendlinien zu erstellen, wodurch wir farbige Zonen und klare Begrenzungslinien erhalten, ohne uns auf ein einziges eingebautes Kanalobjekt zu verlassen. Sie haben vielleicht schon bemerkt, dass wir auch der Visualisierung große Aufmerksamkeit schenken, um alles klar zu simulieren. Wir beginnen mit der Berechnung der genauen Preiskoordinaten für beide Enden des Kanals unter Verwendung der gespeicherten globalen Regressionswerte: Wir setzen den linken mittleren Preis auf „intercept_global“, den rechten mittleren auf „intercept_global + slope_global * current_right_x“, und leiten dann die oberen und unteren Preise an beiden Enden ab, indem wir „Deviations * stdDev_global“ addieren oder subtrahieren.

Um die gefüllten Bereiche zu erstellen, konstruieren wir zunächst den oberen mittleren Bereich: Wir bilden einen eindeutigen Namen, indem wir „_um“ an den Basisnamen anhängen, und verwenden dann ObjectCreate mit OBJ_CHANNEL, um einen Drei-Punkt-Kanal von links oben nach rechts oben nach links Mitte zu zeichnen. Wir konfigurieren diesen Kanal einfarbig in Rosa, Breite 1, Füllung aktiviert, Hintergrundplatzierung auswählbar, aber nicht ausgewählt, kein rechter Strahl, nicht ausgeblendet und z-order 0. Wir wiederholen den Vorgang für den mittleren und unteren Abschnitt mit dem Suffix „_ml“: Wir erstellen einen weiteren OBJ_CHANNEL von links Mitte nach rechts Mitte nach links unten, diesmal mit hellgrüner Farbe und identischem Styling, um den gewünschten zweifarbigen Fülleffekt zu erzielen.

Dann zeichnen wir die drei sichtbaren Begrenzungslinien als separate Trendlinien für ein schärferes Erscheinungsbild: Wir erstellen die obere Trendlinie mit dem Suffix „_upper“ mit OBJ_TREND von links oben nach rechts oben und setzen sie auf rot, solid, width 1, foreground (not back), selectable, no ray, z-order 1; dasselbe machen wir für die mittlere Trendlinie in blau mit dem Suffix „_middle“ und die untere in grün mit dem Suffix „_lower“. Durch die Trennung von Füllungen und Linien erhalten wir sowohl farbige Zonen als auch klare Grenzen, die auch dann deutlich bleiben, wenn sich der Kurs innerhalb der Linien bewegt. Wenn ein „ObjectCreate“-Aufruf fehlschlägt, protokollieren wir den Fehler mit GetLastError und geben false zurück; andernfalls geben wir bei vollem Erfolg true zurück.

Wir definieren auch die Begleitfunktion „ChannelDelete“, um alle fünf Komponenten sauber zu entfernen, wenn wir den Kanal neu erstellen oder zurücksetzen müssen: Wir versuchen, jedes Objekt mit seinem vollen Namen zu löschen („_um“, „_ml“, „_upper“, „_middle“, „_lower“), protokollieren alle Fehler, fahren aber fort und geben „true“ zurück, wenn alles gelöscht wurde, oder „false“, wenn ein Fehler auftrat. Zu diesem Zweck verwenden wir die Funktion ObjectDelete. Diese gepaarten Funktionen ermöglichen es uns, den visuellen Kanal vollständig neu zu erstellen, wenn der Preis deutlich ausbricht oder sich ein neuer Trend bildet. Außerdem müssen wir die Signale durch Pfeile anzeigen und die Kanalbeschriftungen aktualisieren.

//+------------------------------------------------------------------+
//| Draw arrow on chart for signal                                   |
//+------------------------------------------------------------------+
void DrawArrow(bool isBuy, datetime time, double price) {
   string name = "SignalArrow_" + IntegerToString(arrowCounter++);  //--- Arrow name
   ObjectCreate(0, name, OBJ_ARROW, 0, time, price);                //--- Create arrow
   ObjectSetInteger(0, name, OBJPROP_ARROWCODE, isBuy ? 233 : 234); //--- Set code
   ObjectSetInteger(0, name, OBJPROP_COLOR, isBuy ? clrGreen : clrRed); //--- Set color
   ObjectSetInteger(0, name, OBJPROP_WIDTH, 2);                     //--- Set width
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, isBuy ? ANCHOR_TOP : ANCHOR_BOTTOM); //--- Set anchor
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);            //--- Set not selectable
   ChartRedraw(0);                                                  //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Update channel labels                                            |
//+------------------------------------------------------------------+
void UpdateLabels(datetime labelTime) {
   double label_x = (double)(labelTime - fixedTimeOld) / period_sec; //--- Calc label x
   double middlePrice = intercept_global + slope_global * label_x;   //--- Calc middle
   double upperPrice = middlePrice + Deviations * stdDev_global;     //--- Calc upper
   double lowerPrice = middlePrice - Deviations * stdDev_global;     //--- Calc lower
   // Upper label
   if (ObjectFind(0, upperLabelName) < 0) {                          //--- Check no upper label
      ObjectCreate(0, upperLabelName, OBJ_TEXT, 0, labelTime, upperPrice); //--- Create upper label
   } else {                                                          //--- Exists
      ObjectMove(0, upperLabelName, 0, labelTime, upperPrice);       //--- Move upper label
   }
   ObjectSetString(0, upperLabelName, OBJPROP_TEXT, "Upper Channel"); //--- Set text
   ObjectSetInteger(0, upperLabelName, OBJPROP_COLOR, clrRed);       //--- Set color
   ObjectSetInteger(0, upperLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER); //--- Set anchor
   ObjectSetInteger(0, upperLabelName, OBJPROP_SELECTABLE, false);   //--- Set not selectable
   // Middle label
   if (ObjectFind(0, middleLabelName) < 0) {                         //--- Check no middle label
      ObjectCreate(0, middleLabelName, OBJ_TEXT, 0, labelTime, middlePrice); //--- Create middle label
   } else {                                                          //--- Exists
      ObjectMove(0, middleLabelName, 0, labelTime, middlePrice);     //--- Move middle label
   }
   ObjectSetString(0, middleLabelName, OBJPROP_TEXT, "Middle Channel"); //--- Set text
   ObjectSetInteger(0, middleLabelName, OBJPROP_COLOR, clrBlue);     //--- Set color
   ObjectSetInteger(0, middleLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Set anchor
   ObjectSetInteger(0, middleLabelName, OBJPROP_SELECTABLE, false);  //--- Set not selectable
   // Lower label
   if (ObjectFind(0, lowerLabelName) < 0) {                          //--- Check no lower label
      ObjectCreate(0, lowerLabelName, OBJ_TEXT, 0, labelTime, lowerPrice); //--- Create lower label
   } else {                                                          //--- Exists
      ObjectMove(0, lowerLabelName, 0, labelTime, lowerPrice);       //--- Move lower label
   }
   ObjectSetString(0, lowerLabelName, OBJPROP_TEXT, "Lower Channel"); //--- Set text
   ObjectSetInteger(0, lowerLabelName, OBJPROP_COLOR, clrGreen);     //--- Set color
   ObjectSetInteger(0, lowerLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER); //--- Set anchor
   ObjectSetInteger(0, lowerLabelName, OBJPROP_SELECTABLE, false);   //--- Set not selectable
   ChartRedraw(0);                                                   //--- Redraw chart
}

Wir implementieren die Funktion „DrawArrow“, um eine klare visuelle Markierung auf dem Chart zu setzen, wenn ein Handelssignal auftritt. Wir erzeugen einen eindeutigen Objektnamen, indem wir „SignalArrow_“ mit einem inkrementierenden „arrowCounter“ kombinieren, und erstellen dann einen OBJ_ARROW zum angegebenen Zeitpunkt und Preis. Wir wählen das Symbol 233 aus Wingdings für Kaufsignale (nach oben gerichteter Pfeil) oder 234 für Verkaufssignale (nach unten gerichteter Pfeil), verwenden die grüne Farbe für Käufe und die rote Farbe für Verkäufe, setzen die Breite auf 2 für die Sichtbarkeit, verankern den Pfeil oben für Käufe oder unten für Verkäufe, sodass er korrekt von der Kerze aus zeigt, machen ihn nicht auswählbar, um versehentliche Bewegungen zu vermeiden, und zeichnen den Chart sofort neu. MQL5 liefert Code. Sie können unten sehen, wie Sie zu den gewünschten wechseln.

MQL5 WINGDINGS

Im weiteren Verlauf der Implementierung erstellen wir die Funktion „UpdateLabels“, um beschreibende Textbeschriftungen genau an der aktuellen rechten Kante des Kanals zu positionieren und sie sanft zu verschieben, wenn der Kanal erweitert oder neu erstellt wird. Wir berechnen zunächst das x-Koordinatenäquivalent der angegebenen „labelTime“, indem wir „fixedTimeOld“ abziehen und durch „period_sec“ dividieren, und verwenden dann diesen x-Wert mit den globalen Regressionsparametern, um die genauen mittleren, oberen und unteren Preise an diesem exakten Punkt zu berechnen.

Für jede der drei Beschriftungen (obere, mittlere, untere) prüfen wir zunächst mit ObjectFind, ob sie bereits existiert; wenn nicht, erstellen wir mit OBJ_TEXT ein neues Objekt zum Zeitpunkt der Beschriftung und berechnen den Preis, andernfalls verschieben wir das vorhandene einfach mit ObjectMove an die neue Position. Wir setzen den entsprechenden Text („Upper Channel“, „Middle Channel“, „Lower Channel“), wenden die passenden Farben an (rot, blau, grün), positionieren den Anker als links-oben, links oder links-unten, sodass der Text sauber neben den Linien sitzt, ohne sie zu überlappen, machen sie nicht auswählbar und zeichnen schließlich das Chart neu. Mit diesen Funktionen können wir nun den Kanal identifizieren und zeichnen. Wir werden die Logik auch als Funktion haben, um den Code wie unten zu modularisieren.

//+------------------------------------------------------------------+
//| Create channel if clear trend                                    |
//+------------------------------------------------------------------+
void CreateChannelIfTrend() {
   if (Bars(_Symbol, _Period) < RegressionPeriod + 1) return;     //--- Return if insufficient bars
   double closeArray[];                                           //--- Close array
   ArraySetAsSeries(closeArray, true);                            //--- Set as series
   if (CopyClose(_Symbol, _Period, 1, RegressionPeriod, closeArray) != RegressionPeriod) return; //--- Copy close or return
   double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;               //--- Init sums
   int n = RegressionPeriod;                                      //--- Set n
   for (int i = 0; i < n; i++) {                                  //--- Iterate
      double x = (double)(n - 1 - i);                             //--- Calc x
      double y = closeArray[i];                                   //--- Get y
      sumX += x;                                                  //--- Add x
      sumY += y;                                                  //--- Add y
      sumXY += x * y;                                             //--- Add xy
      sumX2 += x * x;                                             //--- Add x2
   }
   double slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); //--- Calc slope
   // Check for clear trend
   if (MathAbs(slope) < MinSlopeThreshold) {                      //--- Check no trend
      hasValidChannel = false;                                    //--- Reset channel
      ChannelDelete(0, channelName);                              //--- Delete channel
      ObjectDelete(0, upperLabelName);                            //--- Delete upper label
      ObjectDelete(0, middleLabelName);                           //--- Delete middle label
      ObjectDelete(0, lowerLabelName);                            //--- Delete lower label
      return;                                                     //--- Return
   }
   double intercept = (sumY - slope * sumX) / n;                  //--- Calc intercept
   // Calculate stdDev
   double sumRes2 = 0;                                            //--- Init res2 sum
   for (int i = 0; i < n; i++) {                                  //--- Iterate
      double x = (double)(n - 1 - i);                             //--- Calc x
      double predicted = intercept + slope * x;                   //--- Calc predicted
      double res = closeArray[i] - predicted;                     //--- Calc res
      sumRes2 += res * res;                                       //--- Add res2
   }
   double variance = sumRes2 / (n - 2);                           //--- Calc variance
   double stdDev = MathSqrt(variance);                            //--- Calc stdDev
   // Store for projection
   slope_global = slope;                                          //--- Set global slope
   intercept_global = intercept;                                  //--- Set global intercept
   stdDev_global = stdDev;                                        //--- Set global stdDev
   // Fixed anchors with initial extension (future time2)
   fixedTimeOld = iTime(_Symbol, _Period, RegressionPeriod);      //--- Set old time
   datetime fixedTimeNew = iTime(_Symbol, _Period, 1);            //--- Set new time
   long channel_sec = fixedTimeNew - fixedTimeOld;                //--- Calc channel sec
   long extension_sec = (long)(channel_sec * (ExtensionPercent / 100.0)); //--- Calc extension
   datetime time_extended = fixedTimeNew + (datetime)extension_sec; //--- Calc extended time
   current_right_x = (double)(time_extended - fixedTimeOld) / period_sec; //--- Calc right x
   // Delete old and create new
   ChannelDelete(0, channelName);                                 //--- Delete channel
   if (!ChannelCreate(0, channelName, 0, fixedTimeOld, time_extended)) return; //--- Create channel or return
   hasValidChannel = true;                                        //--- Set valid channel
   double channelWidth = 2 * Deviations * stdDev_global;          //--- Calc width
   double channelWidthPoints = channelWidth / _Point;             //--- Calc width points
   Print("Channel created: slope=" + DoubleToString(slope, 8) + ", range=" + DoubleToString(channelWidth, _Digits) + " (" + DoubleToString(channelWidthPoints, 0) + " points), times: " + TimeToString(fixedTimeOld) + " to " + TimeToString(time_extended)); //--- Log channel
   UpdateLabels(time_extended);                                   //--- Update labels
   ChartRedraw(0);                                                //--- Redraw chart
}

Hier implementieren wir die Funktion „CreateChannelIfTrend“, um die vollständige lineare Regressionsberechnung durchzuführen und zu entscheiden, ob der Kanal erstellt oder aktualisiert werden soll, um sicherzustellen, dass er nur während eindeutiger Trendperioden angezeigt wird. Zunächst wird überprüft, ob genügend historische Balken vorhanden sind (mindestens „RegressionPeriod + 1“) – falls nicht, wird sofort zurückgegangen, um Fehler zu vermeiden. Dann deklarieren wir ein dynamisches Array für Schlusskurse, setzen es mit ArraySetAsSeries als Serie, sodass Index 0 der jüngste Balken ist, und kopieren genau die „RegressionPeriod“-Schlusskurse ab Index 1 über CopyClose – und kehren vorzeitig zurück, wenn das Kopieren fehlschlägt.

Wir initialisieren die Summationsvariablen für die Formel der kleinsten Quadrate und durchlaufen die Periode: Für jeden Balken i weisen wir x als (n-1-i) zu, sodass der älteste Balken x = n-1 und der jüngste x = 0 erhält, y als Schlusskurs, und akkumulieren sumX, sumY, sumXY und sumX2 entsprechend. Wir benötigen dies unbedingt für die Formel zur Trendberechnung. Wir berechnen die Steigung anhand der Standardformel. Wenn die absolute Steigung unter „MinSlopeThreshold“ liegt, betrachten wir den Markt als flach, setzen „hasValidChannel“ auf false, löschen alle bestehenden Kanäle und Kennzeichnungen mit „ChannelDelete“ und ObjectDelete und kehren zurück – dies verhindert das Zeichnen von sinnlosen horizontalen Kanälen unter Bereichsbedingungen. Sie können diese Prüfung jedoch auslassen, wenn Sie mit schwankenden Märkten handeln wollen.

Wenn die Steigung ausreichend ist, wird der Achsenabschnitt als (sumY - Steigung × sumX) / n berechnet. Anschließend berechnen wir die Standardabweichung: In der zweiten Schleife ermitteln wir den vorausgesagten Preis für jedes x, berechnen das quadrierte Residuum aus dem tatsächlichen Abschluss, summieren es, teilen durch (n-2), um die Varianz zu erhalten, und ziehen die Quadratwurzel für die Standardabweichung. Wir speichern Steigung, Achsenabschnitt und Abweichung in den globalen Variablen für spätere Projektionen. Wir verankern die linke Seite des Kanals an der Zeit des ältesten Balkens über iTime bei der Verschiebung „RegressionPeriod“ in „fixedTimeOld“, setzen den rechten Basisanker auf den letzten abgeschlossenen Balken (Index 1), berechnen die Kanaldauer in Sekunden, verlängern ihn nach rechts um „ExtensionPercent“ dieser Dauer und berechnen „current_right_x“ als exakte x-Koordinate des verlängerten Endpunkts.

Wir löschen alle vorherigen Kanäle mit „ChannelDelete“ und rufen dann „ChannelCreate“ auf, um den neuen mehrteiligen Kanal von „fixedTimeOld“ zur erweiterten Zeit zu erstellen. Bei Erfolg setzen wir „hasValidChannel“ auf true, protokollieren die neuen Kanaldetails (Steigung, Breite in Preis und Punkten, Zeitbereich), aktualisieren die gleitenden Beschriftungen am erweiterten rechten Rand mit „UpdateLabels“ und zeichnen den Chart neu. Wir können diese Funktion nun in der Ereignishandlung von OnInit aufrufen, um den ersten Kanal zu zeichnen, wenn wir genügend Daten haben.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(MagicNumber);                   //--- Set magic number
   CreateChannelIfTrend();                                        //--- Initial try
   return(INIT_SUCCEEDED);                                        //--- Return success
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ChannelDelete(0, channelName);                                 //--- Delete channel
   ObjectDelete(0, upperLabelName);                               //--- Delete upper label
   ObjectDelete(0, middleLabelName);                              //--- Delete middle label
   ObjectDelete(0, lowerLabelName);                               //--- Delete lower label
}

In OnInit weisen wir zunächst dem Objekt „obj_Trade“ mit „SetExpertMagicNumber“ die „MagicNumber“ zu und stellen so sicher, dass jeder Handel, den wir eröffnen, diese Kennung für eine ordnungsgemäße Verwaltung trägt. Dann rufen wir „CreateChannelIfTrend“ auf, um zu versuchen, den Regressionskanal mit den neuesten Daten zu erstellen, die beim Start verfügbar sind, und so einen ersten Kanal zu erstellen, wenn der Markt bereits eine ausreichende Trendstärke aufweist. Zum Abschluss wird INIT_SUCCEEDED zurückgegeben, um die erfolgreiche Initialisierung zu bestätigen.

Außerdem führen wir in der Funktion OnDeinit eine vollständige Bereinigung durch: Wir rufen „ChannelDelete“ auf, um alle fünf Komponenten des Regressionskanals (die zwei gefüllten Zonen und drei Trendlinien) zu entfernen, und löschen dann einzeln die drei beweglichen Textbeschriftungen mit ObjectDelete unter Verwendung von „upperLabelName“, „middleLabelName“ und „lowerLabelName“. Dadurch wird sichergestellt, dass keine verwaisten Objekte auf dem Chart verbleiben, wenn das Programm nicht mehr läuft. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

STARTKANAL DER LINEAREN REGRESSION

Aus dem Bild ist ersichtlich, dass wir den Kanal initialisieren, wenn wir einen Trend und genügend Balken haben, um ihn zu berechnen. Jetzt können wir mit der Verwaltung und Aktualisierung fortfahren, wenn wir mehr Balken in der Tick-Funktion haben. Wir müssen dies nur bei neuen Balken machen, um das Programm nicht unnötig zu überlasten. Wir haben die folgende Funktion definiert, um dies zu handhaben.

//+------------------------------------------------------------------+
//| Check if new bar has opened                                      |
//+------------------------------------------------------------------+
bool IsNewBar() {
   datetime currentBarTime = iTime(_Symbol, _Period, 0);          //--- Get current time
   if (currentBarTime != lastBarTime) {                           //--- Check new
      lastBarTime = currentBarTime;                               //--- Update last
      return true;                                                //--- Return true
   }
   return false;                                                  //--- Return false
}

Hier definieren wir die Funktion „IsNewBar“, um zu erkennen, wann sich ein neuer Balken im Zeitrahmen des Charts vollständig gebildet hat, sodass wir die Hauptberechnung und die Handelslogik nur einmal pro Balken anstatt bei jedem Tick ausführen können. Wir ermitteln die Eröffnungszeit des aktuellen Balkens (Index 0) mithilfe von iTime mit dem aktuellen Symbol und Zeitrahmen und speichern sie in „currentBarTime“. Dann vergleichen wir dies mit der gespeicherten globalen Variablen „lastBarTime“: Wenn sie sich unterscheiden, haben wir einen neuen Balken, also aktualisieren wir „lastBarTime“ auf den aktuellen Wert und geben true zurück, um zu signalisieren, dass die Verarbeitung fortgesetzt werden soll. Wenn die Zeiten übereinstimmen, geben wir einfach false zurück und überspringen die schwere Logik für den Rest des Ticks. Wir können dies nun in der Ereignishandler von OnTick verwenden, wenn neue Balken geboren werden.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   // Check for new bar
   if (!IsNewBar()) return;                                       //--- Return if not new bar
   // Scan every bar if no channel
   if (!hasValidChannel) {                                        //--- Check no channel
      CreateChannelIfTrend();                                     //--- Create if trend
      if (!hasValidChannel) return;                               //--- Return if no channel
   }
   // Project values manually for completed bar (shift 1)
   datetime previousTime = iTime(_Symbol, _Period, 1);            //--- Get previous time
   int x = Bars(_Symbol, _Period, fixedTimeOld, previousTime) - 1; //--- Calc x
   channelMiddle = intercept_global + slope_global * x;           //--- Calc middle
   channelUpper = channelMiddle + Deviations * stdDev_global;     //--- Calc upper
   channelLower = channelMiddle - Deviations * stdDev_global;     //--- Calc lower
   // Project for shift 2
   datetime time2 = iTime(_Symbol, _Period, 2);                   //--- Get time 2
   if (time2 <= fixedTimeOld) return;                             //--- Return if invalid
   int x2 = Bars(_Symbol, _Period, fixedTimeOld, time2) - 1;      //--- Calc x2
   double middle2 = intercept_global + slope_global * x2;         //--- Calc middle2
   double upper2 = middle2 + Deviations * stdDev_global;          //--- Calc upper2
   double lower2 = middle2 - Deviations * stdDev_global;          //--- Calc lower2
   // Get closes
   double closePrevious = iClose(_Symbol, _Period, 1);            //--- Get close previous
   double close2 = iClose(_Symbol, _Period, 2);                   //--- Get close 2
   if (closePrevious == 0 || close2 == 0) return;                 //--- Return if invalid
   // Check if beyond end
   datetime current_time2 = (datetime)ObjectGetInteger(0, channelName + "_middle", OBJPROP_TIME, 1); //--- Get current time2
   if (previousTime > current_time2) {                            //--- Check beyond end
      Print("Bars beyond channel end: previousTime=" + TimeToString(previousTime) + ", current_time2=" + TimeToString(current_time2) + " - recreating channel"); //--- Log recreate
      CreateChannelIfTrend();                                     //--- Recreate channel
      return;                                                     //--- Return
   }
}

In OnTick rufen wir zunächst „IsNewBar“ auf, um festzustellen, ob sich im aktuellen Zeitrahmen ein neuer Balken gebildet hat. Wenn kein neuer Balken vorhanden ist, kehren wir sofort zurück, um zu vermeiden, dass die Logik innerhalb derselben Kerze mehrfach ausgeführt wird, und halten alles nur mit abgeschlossenen Balken synchronisiert. Wenn „hasValidChannel“ falsch ist – d. h., wir haben derzeit keinen aktiven Regressionskanal aufgrund eines unzureichenden Trends oder einer früheren Löschung –, rufen wir sofort „CreateChannelIfTrend“ auf, um die neuesten Daten zu scannen und einen neuen Kanal zu erstellen, wenn die Steigung nun den Mindestschwellenwert erfüllt. Sollte die Funktion immer noch „hasValidChannel“ falsch lassen (Seitwärtsmarkt), kehren wir vorzeitig zurück und warten auf den nächsten Balken.

Sobald wir bestätigen, dass ein gültiger Kanal existiert, projizieren wir manuell die Regressionsstufen speziell für den gerade abgeschlossenen Balken (Index 1). Wir rufen seinen Zeitstempel mit iTime bei Index 1 in „previousTime“ ab, berechnen seine genaue x-Koordinate relativ zu unserem festen linken Anker mit Bars zwischen „fixedTimeOld“ und „previousTime“ minus 1 und berechnen dann den mittleren, oberen und unteren Kanal. So erhalten wir präzise Kanalpositionen genau zum Schluss des vorherigen Balkens. Wir wiederholen die Projektion für den Balken davor (Index 2). Anhand dieser Werte lässt sich feststellen, ob der Schlusskurs des vorangegangenen Balkens innerhalb des Kanals des vorherigen Balkens ausbrach. Anschließend werden die tatsächlichen Schlusskurse abgefragt: „closePrevious“ mit Index 1 und „close2“ mit Index 2, wobei eine vorzeitige Rückkehr erfolgt, wenn einer der beiden Werte ungültig (Null) ist.

Schließlich führen wir eine Sicherheitsprüfung durch: Wir lesen die aktuelle rechte Endpunktzeit des mittleren Trendlinienobjekts (channelName + "_middle") über die Funktion ObjectGetInteger mit OBJPROP_TIME-Index 1 in „current_time2“. Wenn die Zeit des vorherigen Balkens diesen Endpunkt bereits überschreitet – was bedeutet, dass sich der Preis über den Bereich hinaus bewegt hat, den unser Kanal derzeit erreicht –, protokollieren wir die Situation und erzwingen eine sofortige Neuerstellung mit „CreateChannelIfTrend“, um sicherzustellen, dass der Kanal nie hinter der Preisentwicklung zurückbleibt. Nach der Kompilierung erhalten wir folgendes Ergebnis.

TICK-UPDATES DES LINEAREN REGRESSION KANALS

So weit, so gut. Wir können sehen, dass wir den Kanal bei neuen Balken aktualisieren, wenn der Trend bestätigt wird. Jetzt können wir die Ausbrüche erkennen, also entweder den Kanal verlängern oder neu zeichnen. Hier ist der Codeausschnitt, den wir verwenden, um dies zu erreichen.

// Check if breakout (deviation > threshold * width) for recreation
double channelWidth = channelUpper - channelLower;             //--- Calc width
double channelWidthPoints = channelWidth / _Point;             //--- Calc width points
double updateThreshold = UpdateThresholdPercent / 100.0;       //--- Calc threshold
double deviation = MathMax(closePrevious - channelUpper, channelLower - closePrevious); //--- Calc deviation
double deviationPercent = (deviation / channelWidth) * 100;    //--- Calc percent
if (deviation > updateThreshold * channelWidth) {              //--- Check breakout
   Print("Breakout detected - deviation: " + DoubleToString(deviation, _Digits) + " (" + DoubleToString(deviationPercent, 2) + "%), threshold: " + DoubleToString(updateThreshold * channelWidth, _Digits) + " (" + IntegerToString(UpdateThresholdPercent) + "%) - recreating channel"); //--- Log breakout
   CreateChannelIfTrend();                                     //--- Recreate channel
   return;                                                     //--- Return
} else {                                                       //--- No breakout
   // Extend right by one bar if within
   datetime new_time2 = current_time2 + (datetime)period_sec;  //--- Calc new time2
   current_right_x += 1.0;                                     //--- Increment x
   double new_price_middle = intercept_global + slope_global * current_right_x; //--- Calc new middle
   double new_price_upper = new_price_middle + Deviations * stdDev_global; //--- Calc new upper
   double new_price_lower = new_price_middle - Deviations * stdDev_global; //--- Calc new lower
   // Move channels
   ObjectMove(0, channelName + "_um", 1, new_time2, new_price_upper); //--- Move um
   ObjectMove(0, channelName + "_ml", 1, new_time2, new_price_middle); //--- Move ml
   // Move trendlines
   ObjectMove(0, channelName + "_upper", 1, new_time2, new_price_upper); //--- Move upper
   ObjectMove(0, channelName + "_middle", 1, new_time2, new_price_middle); //--- Move middle
   ObjectMove(0, channelName + "_lower", 1, new_time2, new_price_lower); //--- Move lower
   UpdateLabels(new_time2);                                    //--- Update labels
   ChartRedraw(0);                                             //--- Redraw chart
}

Wir befassen uns nun mit dem adaptiven Verhalten des Kanals: Wir entscheiden, ob wir ihn barweise verlängern oder ihn vollständig neu erstellen, wenn der Preis einen signifikanten Ausbruch aus der aktuellen Regression zeigt. Zunächst wird die gesamte Kanalbreite als Abstand zwischen „channelUpper“ und „channelLower“ berechnet, in Referenzpunkte umgerechnet und die Aktualisierungsschwelle als „UpdateThresholdPercent“ geteilt durch 100 berechnet. Anschließend bestimmen wir mit MathMax die Abweichung der vorherigen Schließung von der näheren Grenze und drücken diese Abweichung als Prozentsatz der gesamten Kanalbreite aus. Wenn diese Abweichung den Schwellenwert überschreitet (z. B. standardmäßig 30 % der Breite), betrachten wir dies als bedeutsamen Ausbruch oder Erschöpfung der aktuellen Regression. Wir protokollieren die Details einschließlich der Abweichung in Preis und Prozentsatz zusammen mit dem Schwellenwert und rufen sofort „CreateChannelIfTrend“ auf, um die Regression anhand der neuesten Daten neu zu berechnen und einen neuen Kanal zu erstellen, der besser zum aktualisierten Trend passt – und kehren dann vorzeitig zurück, da der Kanal neu erstellt wurde. Die Logik für die Erholung kann nach Belieben geändert werden; dies ist nur eine willkürliche Logik, die wir uns beim Aufbau des Systems ausgedacht haben.

Bleibt der Preis dann einigermaßen im Rahmen (Abweichung <= Schwellenwert), verlängern wir stattdessen den bestehenden Kanal um einen Balken nach rechts, um Kontinuität zu gewährleisten. Wir verschieben die Zeit des rechten Endpunkts um eine volle Balkenperiode mit „current_time2 + period_sec“ in „new_time2“, erhöhen „current_right_x“ um 1,0 und projizieren die neuen mittleren, oberen und unteren Preise unter Verwendung der gespeicherten Globals. Wir aktualisieren dann jede Komponente, indem wir ihren rechten Ankerpunkt (Index 1) auf die neue Zeit und den entsprechenden Preis verschieben: die obere-mittlere Füllung mit „_um“, die mittlere-untere mit „_ml“ und die drei Trendlinien „_upper“, „_middle“ und „_lower“. Schließlich rufen wir „UpdateLabels“ mit der neuen richtigen Zeit auf, um die Textbeschriftungen neu zu positionieren und das Chart neu zu zeichnen. Diese Erweiterung sorgt dafür, dass der Kanal dem Preis bei einem Trend ohne unnötige Neuberechnungen folgt, und stellt gleichzeitig sicher, dass er zu einer neuen Regression übergeht, wenn sich der Preis zu weit außerhalb des erwarteten Bereichs bewegt. Wenn wir einen Ausbruch haben, wissen wir das jetzt anhand des Ausdrucks wie unten.

PROTOKOLLIERUNG DER DURCHBRUCHSERKENNUNG

Da wir den Kanal nun vollständig verwalten können, können wir Signale generieren, wenn der Preis den Kanal selbst verlässt, obwohl wir auch bestehende Positionen schließen wollen, wenn die Mittellinie gekreuzt wird. Eine weitere willkürliche Verwaltungslogik, die Sie überspringen können, wenn Sie möchten, dass Positionen nur bei Stop-Loss- oder Take-Profit-Treffern vollständig geschlossen werden.

// Count existing positions
int buyCount = 0, sellCount = 0;                               //--- Init counts
int total = PositionsTotal();                                  //--- Get total positions
for (int i = total - 1; i >= 0; i--) {                         //--- Iterate reverse
   ulong ticket = PositionGetTicket(i);                        //--- Get ticket
   if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) { //--- Check symbol magic
      if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy
         buyCount++;                                              //--- Increment buy
      } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell
         sellCount++;                                             //--- Increment sell
      }
   }
}
// Close logic: Close all buys if crossed above middle, all sells if below
if (closePrevious > channelMiddle) {                           //--- Check close above middle
   for (int i = PositionsTotal() - 1; i >= 0; i--) {           //--- Iterate reverse
      ulong ticket = PositionGetTicket(i);                     //--- Get ticket
      if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy
         obj_Trade.PositionClose(ticket, Slippage);                 //--- Close position
         Print("Closing Buy: Price " + DoubleToString(closePrevious, _Digits) + " crossed above middle channel " + DoubleToString(channelMiddle, _Digits)); //--- Log close
      }
   }
}
if (closePrevious < channelMiddle) {                           //--- Check close below middle
   for (int i = PositionsTotal() - 1; i >= 0; i--) {           //--- Iterate reverse
      ulong ticket = PositionGetTicket(i);                     //--- Get ticket
      if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell
         obj_Trade.PositionClose(ticket, Slippage);                 //--- Close position
         Print("Closing Sell: Price " + DoubleToString(closePrevious, _Digits) + " crossed below middle channel " + DoubleToString(channelMiddle, _Digits)); //--- Log close
      }
   }
}
// Open on clear breakout if room (with inverse option)
bool buySignal = (close2 >= lower2) && (closePrevious < channelLower); //--- Buy signal
bool sellSignal = (close2 <= upper2) && (closePrevious > channelUpper); //--- Sell signal
if (TradeDirection == Inverse) {                               //--- Check inverse
   bool temp = buySignal;                                      //--- Temp buy
   buySignal = sellSignal;                                     //--- Swap buy
   sellSignal = temp;                                          //--- Swap sell
}
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);            //--- Get ask
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);            //--- Get bid
if (buySignal && buyCount < MaxBuys) {                         //--- Check buy signal
   // Buy
   double sl = (StopLossPips == 0) ? 0 : NormalizeDouble(ask - StopLossPips * _Point, _Digits); //--- Calc SL
   double tp = (TakeProfitPips == 0) ? 0 : NormalizeDouble(ask + TakeProfitPips * _Point, _Digits); //--- Calc TP
   if (obj_Trade.Buy(Lots, _Symbol, 0, sl, tp, "LRC Buy")) {      //--- Open buy
      Print("Buy signal: Price " + DoubleToString(closePrevious, _Digits) + " broke below lower channel from inside"); //--- Log signal
      DrawArrow(true, previousTime, closePrevious);            //--- Draw arrow
   } else {                                                    //--- Failed
      Print("Buy order failed: " + obj_Trade.ResultRetcodeDescription()); //--- Log failure
   }
}
if (sellSignal && sellCount < MaxSells) {                      //--- Check sell signal
   // Sell
   double sl = (StopLossPips == 0) ? 0 : NormalizeDouble(bid + StopLossPips * _Point, _Digits); //--- Calc SL
   double tp = (TakeProfitPips == 0) ? 0 : NormalizeDouble(bid - TakeProfitPips * _Point, _Digits); //--- Calc TP
   if (obj_Trade.Sell(Lots, _Symbol, 0, sl, tp, "LRC Sell")) {    //--- Open sell
      Print("Sell signal: Price " + DoubleToString(closePrevious, _Digits) + " broke above upper channel from inside"); //--- Log signal
      DrawArrow(false, previousTime, closePrevious);           //--- Draw arrow
   } else {                                                    //--- Failed
      Print("Sell order failed: " + obj_Trade.ResultRetcodeDescription()); //--- Log failure
   }
}

Wir wenden uns nun dem Positionsmanagement und der Handelsausführung bei jedem neuen Balken zu. Zunächst zählen wir, wie viele Kauf- und Verkaufspositionen derzeit offen sind, die zu diesem Programm gehören: Wir ziehen eine Schleife rückwärts durch PositionsTotal, rufen jedes Ticket mit „PositionGetTicket“ ab und prüfen, ob Symbol und „MagicNumber“ übereinstimmen. Wir erhöhen „buyCount“ für POSITION_TYPE_BUY oder „sellCount“ für „POSITION_TYPE_SELL“ und erhalten so genaue Limits, bevor wir neue Geschäfte eröffnen. Dann implementieren wir die Logik für den Ausstieg aus der Mittellinie: Wenn der Schluss des vorherigen Balkens („closePrevious“) über „channelMiddle“ liegt, schließen wir sofort jede offene Kaufposition, indem wir erneut iterieren, den Typ prüfen und „obj_Trade.PositionClose“ mit dem Ticket und dem erlaubten „Slippage“ aufrufen, während wir den Grund protokollieren. Wenn der vorherige Schlusskurs unter „channelMiddle“ liegt, werden alle Verkaufspositionen mit demselben Prozess und derselben Protokollierung geschlossen. Dadurch werden Gewinne aggressiv abgesichert oder Verluste reduziert, sobald der Kurs die Regressionslinie selbst überschreitet, unabhängig davon, wie viele Abweichungen er davon entfernt ist.

Dann definieren wir Einstiegssignale, die auf sauberen Ausbrüchen aus dem Kanal basieren: Ein Kaufsignal tritt auf, wenn der Schlusskurs des zweiten Balkens („close2“) zu diesem Zeitpunkt am oder über dem unteren Band lag („lower2“), der Schlusskurs des vorherigen Balkens jedoch unter dem aktuellen unteren Band („channelLower“) liegt, was bedeutet, dass der Preis entscheidend aus dem Kanal nach unten ausgebrochen ist. Ein Verkaufssignal wird bei inversen Bedingungen ausgelöst. Wenn „TradeDirection“ auf „Inverse“ steht, tauschen wir einfach die beiden Signale aus und wandeln das Programm sofort von trendbeständigen Rücksetzern in den Seitwärtshandel der Rückkehr zum Mittelwert um.

Wir rufen den aktuellen Geld- und Briefkurs ab, prüfen dann, ob ein Kaufsignal und ein Raum unter „MaxBuys“ vorliegt: Wenn beide Bedingungen erfüllt sind, berechnen wir Stop-Loss und Take-Profit und senden eine Marktkauforder über „obj_Trade.Buy“ mit festen „Lots“, keinem vordefinierten Preis (Marktausführung), dem berechneten SL/TP und dem Kommentar „LRC Buy“. Im Erfolgsfall protokollieren wir die Signaldetails und rufen „DrawArrow“ mit true auf, um einen grünen Aufwärtspfeil zum Zeitpunkt und zum Abschluss des vorherigen Balkens zu erzeugen; im Fehlerfall protokollieren wir die Retcode-Beschreibung. Wir spiegeln den Prozess für Verkaufssignale wider. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

TEST DES LINEAREN REGRESSION KANALS GIF

Anhand der Visualisierung können wir sehen, dass wir den linearen Regressionskanal definieren, ihn bei Bedarf aktualisieren und bei Ausbrüchen Positionen eröffnen und somit unsere Ziele erreichen. Bleibt nur noch der Backtest des Programms, und der wird im nächsten Abschnitt behandelt.


Backtests

Nach einem gründlichen Backtest erhalten wir folgende Ergebnisse.

Backtest-Grafik:

GRAPH

Bericht des Backtests:

BERICHT


Schlussfolgerung

Zusammenfassend lässt sich sagen, dass wir ein adaptives System mit dem linearen Regressionskanal in MQL5 entwickelt haben. Es berechnet die Regressionslinie und die Bänder mit der Standardabweichung über eine vom Nutzer definierten Periodenlänge. Das System wird nur aktiviert, wenn die absolute Steigung einen Mindestwert überschreitet. Der Kanal verlängert sich automatisch, Balken für Balken, während der Kurs innerhalb des Kanals bleibt. Der Kanal wird vollständig neu erstellt, wenn sich der Kurs über einen konfigurierbaren Prozentsatz der Kanalbreite hinaus bewegt. Das System unterstützt sowohl den normalen (Rücksetzer) als auch den inversen (seitwärts) Handelsmodus. Es eröffnet Positionen bei sauberen Ausbrüchen aus dem Inneren des Kanals. Optisch zeigt es gefüllte zweifarbige Zonen, durchgezogene Trendlinien für die obere, mittlere und untere Grenze sowie bewegliche Etiketten und Einstiegspfeile.

Haftungsausschluss: Dieser Artikel ist nur für Bildungszwecke gedacht. Der Handel ist mit erheblichen finanziellen Risiken verbunden, und die Volatilität der Märkte kann zu Verlusten führen. Gründliche Backtests und sorgfältiges Risikomanagement sind entscheidend, bevor Sie dieses Programm auf den Live-Märkten einsetzen.

Diese adaptive Strategie für lineare Regressionskanäle bietet dynamische Aktualisierungen, normale oder inverse Modi und das Kreuzen der Mittellinie. Mit kanalbasierten Signalen sind Sie für den Handel mit Trendmärkten gerüstet. Die Strategie ist bereit für weitere Optimierungen auf Ihrer Handelsreise. Viel Spaß beim Handeln!

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

Beigefügte Dateien |
Entwicklung des Price Action Analysis Toolkit (Teil 53): Pattern Density Heatmap zur Entdeckung von Unterstützungs- und Widerstandszonen Entwicklung des Price Action Analysis Toolkit (Teil 53): Pattern Density Heatmap zur Entdeckung von Unterstützungs- und Widerstandszonen
In diesem Artikel wird die Pattern Density Heatmap vorgestellt, ein Zuordnungsinstrument des Preis-Aktions-Mappings, das wiederholte Erkennungen von Kerzenmustern in statistisch signifikante Unterstützungs- und Widerstandszonen umwandelt. Anstatt jedes Signal isoliert zu behandeln, fasst der EA die Erkennungen in festen Preisbereichen (bins) zusammen, bewertet ihre Dichte mit einer optionalen Aktualitätsgewichtung und bestätigt die Werte anhand von Daten mit höherem Zeitrahmen. Die sich daraus ergebende Heatmap zeigt, wo der Markt in der Vergangenheit reagiert hat – Werte, die proaktiv für das Handels-Timing, das Risikomanagement und das Vertrauen in die Strategie für jeden Handelsstil genutzt werden können.
Implementierung von praktischen Modulen aus anderen Sprachen in MQL5 (Teil 04): Zeit-, Datums- und Datetime-Module aus Python Implementierung von praktischen Modulen aus anderen Sprachen in MQL5 (Teil 04): Zeit-, Datums- und Datetime-Module aus Python
Im Gegensatz zu MQL5 bietet die Programmiersprache Python Kontrolle und Flexibilität, wenn es um den Umgang mit und die Manipulation von Zeit geht. In diesem Artikel werden wir ähnliche Module zur besseren Handhabung von Datum und Uhrzeit in MQL5 wie in Python implementieren.
Statistische Arbitrage durch kointegrierte Aktien (Teil 8): Rolling-Windows-Eigenvektor-Vergleich für Portfolio-Rebalancing Statistische Arbitrage durch kointegrierte Aktien (Teil 8): Rolling-Windows-Eigenvektor-Vergleich für Portfolio-Rebalancing
In diesem Artikel wird die Verwendung des Rolling-Windows-Eigenvektor-Vergleichs für die frühzeitige Diagnose von Ungleichgewichten und das Rebalancing des Portfolios in einer statistischen Arbitragestrategie der Rückkehr zum Mittelwert (Mean-Reversion) auf der Grundlage kointegrierter Aktien vorgeschlagen. Sie stellt diese Technik der traditionellen In-Sample/Out-of-Sample-ADF-Validierung gegenüber und zeigt, dass Eigenvektorverschiebungen die Notwendigkeit einer Neugewichtung signalisieren können, selbst wenn die IS/OOS-ADF immer noch eine stationäre Streuung anzeigt. Obwohl die Methode hauptsächlich für die Überwachung des Live-Handels gedacht ist, kommt der Artikel zu dem Schluss, dass der Eigenvektorvergleich auch in das Scoring-System integriert werden könnte – obwohl sein tatsächlicher Beitrag zur Leistung noch getestet werden muss.
Analytical Volume Profile Trading (AVPT): Liquiditätsarchitektur, Marktgedächtnis und algorithmische Ausführung Analytical Volume Profile Trading (AVPT): Liquiditätsarchitektur, Marktgedächtnis und algorithmische Ausführung
Analytical Volume Profile Trading (AVPT) untersucht, wie die Liquiditätsarchitektur und das Marktgedächtnis das Preisverhalten beeinflussen, und ermöglicht so einen tieferen Einblick in die institutionelle Positionierung und die volumengesteuerte Struktur. Durch die Zuordnung von POC, HVNs, LVNs und Value Areas können Händler Annahme-, Ablehnungs- und Ungleichgewichtszonen präzise identifizieren.