Automatisieren von Handelsstrategien in MQL5 (Teil 43): Adaptive lineare Regressionskanalstrategie
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:
- Verständnis des Systems für den adaptiven linearen Regressionskanal
- Implementation in MQL5
- Backtests
- 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.

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.

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.

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:

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.

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.

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:

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:

Bericht des Backtests:

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
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Entwicklung des Price Action Analysis Toolkit (Teil 53): Pattern Density Heatmap zur Entdeckung von Unterstützungs- und Widerstandszonen
Implementierung von praktischen Modulen aus anderen Sprachen in MQL5 (Teil 04): Zeit-, Datums- und Datetime-Module aus Python
Statistische Arbitrage durch kointegrierte Aktien (Teil 8): Rolling-Windows-Eigenvektor-Vergleich für Portfolio-Rebalancing
Analytical Volume Profile Trading (AVPT): Liquiditätsarchitektur, Marktgedächtnis und algorithmische Ausführung
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.