English 日本語
preview
Automatisieren von Handelsstrategien in MQL5 (Teil 13): Aufbau eines Kopf-Schulter-Handelsalgorithmus

Automatisieren von Handelsstrategien in MQL5 (Teil 13): Aufbau eines Kopf-Schulter-Handelsalgorithmus

MetaTrader 5Handel |
175 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Einführung

In unserem letzten Artikel (Teil 12) haben wir die Strategie Mitigation Order Blocks (MOB) in MetaQuotes Language 5 (MQL5) implementiert, um institutionelle Preiszonen für den Handel zu nutzen. In Teil 13 verlagern wir unseren Schwerpunkt auf die Entwicklung des Handelsalgorithmus Kopf und Schultern, der ein klassisches Umkehrmuster automatisiert, um Marktschwankungen präzise zu erfassen. Wir werden die folgenden Themen behandeln:

  1. Verstehen des Musters von Kopf und Schultern
  2. Implementation in MQL5
  3. Backtests
  4. Schlussfolgerung

Am Ende dieses Artikels werden Sie einen voll funktionsfähigen Expert Advisor haben, mit dem Sie das Kopf-Schulter-Muster handeln können - legen Sie los!


Verstehen des Musters von Kopf und Schultern

Das Muster Kopf und Schultern ist eine klassische Chartformation, die in der technischen Analyse zur Vorhersage von Trendumkehrungen weithin anerkannt ist und sowohl in einer Standard- (abwärts) als auch in einer inversen (aufwärts) Variante auftritt, die jeweils durch eine einzigartige Abfolge von Kursspitzen oder -tälern definiert sind. Im Standardmuster, in unserem Programm, wird ein Aufwärtstrend zu drei Spitzen führen: Die linke Schulter bildet ein Hoch, der Kopf wird sich als Höhepunkt des Trends deutlich höher auftürmen (und beide Schultern deutlich übertreffen), und die rechte Schulter wird sich niedriger als der Kopf, aber in der Höhe ähnlich wie die linke Schulter ausbilden, alles verbunden durch eine Nackenlinie, die die beiden Tiefs miteinander verbindet - sobald der Preis unter diese Linie bricht, werden wir beim Ausbruch einen Verkauf eingehen, einen Stop-Loss über der rechten Schulter setzen und einen Take-Profit anstreben, indem wir die Höhe des Kopfes zur Nackenlinie nach unten projizieren, wie unten dargestellt.

ABWÄRTS- KOPF-SCHULTER-MUSTER

Für das inverse Muster ergibt ein Abwärtstrend drei Tiefs: die linke Schulter markiert ein Tief, der Kopf taucht deutlich tiefer (unterhalb beider Schultern), und die rechte Schulter wird sich in der Nähe des linken Niveaus ausrichten, mit einer Nackenlinie über den Spitzen - ein Kursdurchbruch darüber löst einen Aufwärts-Einstieg aus, mit einem Stop-Loss unterhalb der rechten Schulter und einem Take-Profit, der sich um den Abstand zwischen Nackenlinie und Kopf nach oben erstreckt, alles auf der Grundlage der herausragenden Höhe des Kopfes und der Beinahe-Symmetrie der Schultern als unseren Leitregeln. Hier ist die Visualisierung.

AUFWÄRTS-KOPF- UND SCHULTERMUSTER

Was das Risikomanagement betrifft, so werden wir eine optionale Trailing-Stop-Funktion integrieren, um die Gewinne zu sichern und zu maximieren. Los geht's.


Implementation in MQL5

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

//+------------------------------------------------------------------+
//|                                  Head & Shoulders Pattern EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://youtube.com/@ForexAlgo-Trader?"
#property version   "1.00"

#include <Trade\Trade.mqh>                    //--- Include the Trade.mqh library for trading functions
CTrade obj_Trade;                            //--- Trade object for executing and managing trades

// Input Parameters
input int LookbackBars = 50;                   // Number of historical bars to analyze for pattern detection
input double ThresholdPoints = 70.0;           // Minimum price movement in points to identify a reversal
input double ShoulderTolerancePoints = 15.0;   // Maximum allowable price difference between left and right shoulders
input double TroughTolerancePoints = 30.0;     // Maximum allowable price difference between neckline troughs or peaks
input double BufferPoints = 10.0;              // Additional points added to stop-loss for safety buffer
input double LotSize = 0.1;                    // Volume of each trade in lots
input ulong MagicNumber = 123456;              // Unique identifier for trades opened by this EA
input int MaxBarRange = 30;                    // Maximum number of bars allowed between key pattern points
input int MinBarRange = 5;                     // Minimum number of bars required between key pattern points
input double BarRangeMultiplier = 2.0;         // Maximum multiple of the smallest bar range for pattern uniformity
input int ValidationBars = 3;                  // Number of bars after right shoulder to validate breakout
input double PriceTolerance = 5.0;             // Price tolerance in points for matching traded patterns
input double RightShoulderBreakoutMultiplier = 1.5; // Maximum multiple of pattern range for right shoulder to breakout distance
input int MaxTradedPatterns = 20;              // Maximum number of patterns stored in traded history
input bool UseTrailingStop = false;             // Toggle to enable or disable trailing stop functionality
input int MinTrailPoints = 50;                 // Minimum profit in points before trailing stop activates
input int TrailingPoints = 30;                 // Distance in points to maintain behind current price when trailing

Hier beginnen wir mit „#include <Trade\Trade.mqh>“ und einem „CTrade“-Objekt, „obj_Trade“, um zusätzliche Handelsdateien für die Handelsverwaltung aufzunehmen. Wir setzen Eingaben wie „LookbackBars“ (Standardwert 50) für die historische Analyse, „ThresholdPoints“ (Standardwert 70.0) für die Umkehrbestätigung und „ShoulderTolerancePoints“ (Standardwert 15.0) und „TroughTolerancePoints“ (Standardwert 30.0) für die Symmetrie. Der Rest der Eingaben ist selbsterklärend. Zum besseren Verständnis haben wir ausführliche Kommentare hinzugefügt. Als Nächstes müssen wir einige Strukturen definieren, die wir verwenden werden, um die Muster zu finden und die in Frage kommenden Geschäfte zu verwalten.

// Structure to store peaks and troughs
struct Extremum {
   int bar;           //--- Bar index where extremum occurs
   datetime time;     //--- Timestamp of the bar
   double price;      //--- Price at extremum (high for peak, low for trough)
   bool isPeak;       //--- True if peak (high), false if trough (low)
};

// Structure to store traded patterns
struct TradedPattern {
   datetime leftShoulderTime;  //--- Timestamp of the left shoulder
   double leftShoulderPrice;   //--- Price of the left shoulder
};

Wir haben zwei Schlüsselstrukturen mit dem Schlüsselwort struct eingerichtet, um unseren Handelsalgorithmus Kopf und Schultern zu steuern: „Extremum“ speichert Spitzen und Talsohlen mit „bar“ (Index), „time“ (Zeitstempel), „price“ (Wert) und „isPeak“ (true für Peaks, false für Talsohlen), um die Komponenten des Musters zu identifizieren, während „TradedPattern“ ausgeführte Handelsgeschäfte mit „leftShoulderTime“ und „leftShoulderPrice“ verfolgt, um Duplikate zu vermeiden. Um sicherzustellen, dass wir nur einmal pro Balken handeln und die laufenden Handelsgeschäfte im Auge behalten, deklarieren wir eine Variable und ein Array wie folgt.

// Global Variables
static datetime lastBarTime = 0;         //--- Tracks the timestamp of the last processed bar to avoid reprocessing
TradedPattern tradedPatterns[];          //--- Array to store details of previously traded patterns

Damit sind wir bereit. Da wir das Muster jedoch im Chart anzeigen müssen, müssen wir die Chartarchitektur und die Balkenkomponenten so gestalten, dass sie sich an die Anforderungen des Musters anpassen.

int chart_width         = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);        //--- Width of the chart in pixels for visualization
int chart_height        = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);       //--- Height of the chart in pixels for visualization
int chart_scale         = (int)ChartGetInteger(0, CHART_SCALE);                  //--- Zoom level of the chart (0-5)
int chart_first_vis_bar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);      //--- Index of the first visible bar on the chart
int chart_vis_bars      = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);           //--- Number of visible bars on the chart
double chart_prcmin     = ChartGetDouble(0, CHART_PRICE_MIN, 0);                 //--- Minimum price visible on the chart
double chart_prcmax     = ChartGetDouble(0, CHART_PRICE_MAX, 0);                 //--- Maximum price visible on the chart

//+------------------------------------------------------------------+
//| Converts the chart scale property to bar width/spacing           |
//+------------------------------------------------------------------+
int BarWidth(int scale) { return (int)pow(2, scale); }                           //--- Calculates bar width in pixels based on chart scale (zoom level)

//+------------------------------------------------------------------+
//| Converts the bar index (as series) to x in pixels                |
//+------------------------------------------------------------------+
int ShiftToX(int shift) { return (chart_first_vis_bar - shift) * BarWidth(chart_scale) - 1; } //--- Converts bar index to x-coordinate in pixels on the chart

//+------------------------------------------------------------------+
//| Converts the price to y in pixels                                |
//+------------------------------------------------------------------+
int PriceToY(double price) {                                                     //--- Function to convert price to y-coordinate in pixels
   if (chart_prcmax - chart_prcmin == 0.0) return 0;                             //--- Return 0 if price range is zero to avoid division by zero
   return (int)round(chart_height * (chart_prcmax - price) / (chart_prcmax - chart_prcmin) - 1); //--- Calculate y-pixel position based on price and chart dimensions
}

Wir bereiten das Programm vor und statten es mit einer Visualisierung aus, indem wir Variablen wie „chart_width“ und „chart_height“ mit der Funktion ChartGetInteger für die Chartabmessungen, „chart_scale“ für den Zoom, „chart_first_vis_bar“ und „chart_vis_bars“ für die Details der Balken und „chart_prcmin“ und „chart_prcmax“ über ChartGetDouble für den Preisbereich. Wir verwenden die Funktion „BarWidth“ mit pow, um die Balkenabstände aus „chart_scale“ zu berechnen, die Funktion „ShiftToX“, um die Balkenindizes in x-Koordinaten umzuwandeln, die auf „chart_first_vis_bar“ und „chart_scale“ in x-Koordinaten umzuwandeln, und die Funktion „PriceToY“ mit round, um die Preise auf y-Koordinaten auf der Grundlage von „chart_height“, „chart_prcmax“ und „chart_prcmin“ umzuwandeln und so eine präzise Musteranzeige zu ermöglichen. Wir sind jetzt vollständig vorbereitet. Wir können mit der Initialisierung des Programms in OnInit fortfahren.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {                                                           //--- Expert Advisor initialization function
   obj_Trade.SetExpertMagicNumber(MagicNumber);                          //--- Set the magic number for trades opened by this EA
   ArrayResize(tradedPatterns, 0);                                       //--- Initialize tradedPatterns array with zero size
   return(INIT_SUCCEEDED);                                               //--- Return success code to indicate successful initialization
}

In OnInit verwenden wir die Methode „SetExpertMagicNumber“ mit dem „obj_Trade“, um „MagicNumber“ als eindeutige Kennung für alle Handelsgeschäfte zuzuweisen, um sicherzustellen, dass die Positionen unseres Programms unterscheidbar sind, und rufen die Funktion ArrayResize auf, um das Array „tradedPatterns“ auf eine Größe von Null zu setzen, wodurch alle vorherigen Daten für einen Neustart gelöscht werden. Abschließend geben wir INIT_SUCCEEDED zurück, um das erfolgreiche Setup zu bestätigen und den Expert Advisor darauf vorzubereiten, das Muster zu erkennen und effektiv zu handeln. Wir können nun zur Ereignishandlung von OnTick übergehen und sicherstellen, dass wir die Analyse einmal pro Balken durchführen.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {                                                          //--- Main tick function executed on each price update
   datetime currentBarTime = iTime(_Symbol, _Period, 0);                 //--- Get the timestamp of the current bar
   if (currentBarTime == lastBarTime) return;                            //--- Exit if the current bar has already been processed

   lastBarTime = currentBarTime;                                         //--- Update the last processed bar time

   // Update chart properties
   chart_width         = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Update chart width in pixels
   chart_height        = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Update chart height in pixels
   chart_scale         = (int)ChartGetInteger(0, CHART_SCALE);           //--- Update chart zoom level
   chart_first_vis_bar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); //--- Update index of the first visible bar
   chart_vis_bars      = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);    //--- Update number of visible bars
   chart_prcmin        = ChartGetDouble(0, CHART_PRICE_MIN, 0);          //--- Update minimum visible price on chart
   chart_prcmax        = ChartGetDouble(0, CHART_PRICE_MAX, 0);          //--- Update maximum visible price on chart

   // Skip pattern detection if a position is already open
   if (PositionsTotal() > 0) return;                                     //--- Exit function if there are open positions to avoid multiple trades
}

In OnTick, das bei jeder Preisaktualisierung aktiviert wird, um Marktveränderungen zu überwachen und darauf zu reagieren, verwenden wir die Funktion iTime, um „currentBarTime“ für den letzten Balken abzurufen und mit der „lastBarTime“ zu vergleichen, um eine erneute Verarbeitung zu vermeiden, und aktualisieren „lastBarTime“. Anschließend aktualisieren wir das Chart, indem wir ChartGetInteger aufrufen, um „chart_width“, „chart_height“, „chart_scale“, „chart_first_vis_bar“ und „chart_vis_bars“ zu aktualisieren, und ChartGetDouble für „chart_prcmin“ und „chart_prcmax“. Wir verwenden auch die Funktion PositionsTotal, um zu prüfen, ob offene Positionen vorhanden sind, und diese frühzeitig zu schließen, um Überschneidungen zu vermeiden und so die Voraussetzungen für die Mustererkennung und den Handel zu schaffen. Wir können dann eine Funktion definieren, um die Extrempunkte oder die Schlüsselmusterpunkte zu finden.

//+------------------------------------------------------------------+
//| Find extrema in the last N bars                                  |
//+------------------------------------------------------------------+
void FindExtrema(Extremum &extrema[], int lookback) {                    //--- Function to identify peaks and troughs in price history
   ArrayFree(extrema);                                                   //--- Clear the extrema array to start fresh
   int bars = Bars(_Symbol, _Period);                                    //--- Get total number of bars available
   if (lookback >= bars) lookback = bars - 1;                            //--- Adjust lookback if it exceeds available bars

   double highs[], lows[];                                               //--- Arrays to store high and low prices
   ArraySetAsSeries(highs, true);                                        //--- Set highs array as time series (newest first)
   ArraySetAsSeries(lows, true);                                         //--- Set lows array as time series (newest first)
   CopyHigh(_Symbol, _Period, 0, lookback + 1, highs);                   //--- Copy high prices for lookback period
   CopyLow(_Symbol, _Period, 0, lookback + 1, lows);                     //--- Copy low prices for lookback period

   bool isUpTrend = highs[lookback] < highs[lookback - 1];               //--- Determine initial trend based on first two bars
   double lastHigh = highs[lookback];                                    //--- Initialize last high price
   double lastLow = lows[lookback];                                      //--- Initialize last low price
   int lastExtremumBar = lookback;                                       //--- Initialize last extremum bar index

   for (int i = lookback - 1; i >= 0; i--) {                             //--- Loop through bars from oldest to newest
      if (isUpTrend) {                                                   //--- If currently in an uptrend
         if (highs[i] > lastHigh) {                                      //--- Check if current high exceeds last high
            lastHigh = highs[i];                                         //--- Update last high price
            lastExtremumBar = i;                                         //--- Update last extremum bar index
         } else if (lows[i] < lastHigh - ThresholdPoints * _Point) {     //--- Check if current low indicates a reversal (trough)
            int size = ArraySize(extrema);                               //--- Get current size of extrema array
            ArrayResize(extrema, size + 1);                              //--- Resize array to add new extremum
            extrema[size].bar = lastExtremumBar;                         //--- Store bar index of the peak
            extrema[size].time = iTime(_Symbol, _Period, lastExtremumBar); //--- Store timestamp of the peak
            extrema[size].price = lastHigh;                              //--- Store price of the peak
            extrema[size].isPeak = true;                                 //--- Mark as a peak
            //Print("Extrema added: Bar ", lastExtremumBar, ", Time ", TimeToString(extrema[size].time), ", Price ", DoubleToString(lastHigh, _Digits), ", IsPeak true"); //--- Log new peak
            isUpTrend = false;                                           //--- Switch trend to downtrend
            lastLow = lows[i];                                           //--- Update last low price
            lastExtremumBar = i;                                         //--- Update last extremum bar index
         }
      } else {                                                        //--- If currently in a downtrend
         if (lows[i] < lastLow) {                                     //--- Check if current low is below last low
            lastLow = lows[i];                                        //--- Update last low price
            lastExtremumBar = i;                                      //--- Update last extremum bar index
         } else if (highs[i] > lastLow + ThresholdPoints * _Point) {  //--- Check if current high indicates a reversal (peak)
            int size = ArraySize(extrema);                            //--- Get current size of extrema array
            ArrayResize(extrema, size + 1);                           //--- Resize array to add new extremum
            extrema[size].bar = lastExtremumBar;                      //--- Store bar index of the trough
            extrema[size].time = iTime(_Symbol, _Period, lastExtremumBar); //--- Store timestamp of the trough
            extrema[size].price = lastLow;                            //--- Store price of the trough
            extrema[size].isPeak = false;                             //--- Mark as a trough
            //Print("Extrema added: Bar ", lastExtremumBar, ", Time ", TimeToString(extrema[size].time), ", Price ", DoubleToString(lastLow, _Digits), ", IsPeak false"); //--- Log new trough
            isUpTrend = true;                                         //--- Switch trend to uptrend
            lastHigh = highs[i];                                      //--- Update last high price
            lastExtremumBar = i;                                      //--- Update last extremum bar index
         }
      }
   }
}

Hier ermitteln wir die Spitzen und Tiefpunkte, die unser Kopf-Schulter-Muster definieren, indem wir die Funktion „FindExtrema“ implementieren, die die letzten „Lookback“-Balken analysiert, um das Array „extrema“ der kritischen Kurspunkte zu erstellen. Zunächst wird das Array „extrema“ mit der Funktion „ArrayFree“ zurückgesetzt, um einen sauberen Zustand zu gewährleisten. Anschließend wird die Funktion „Bars“ verwendet, um die Gesamtzahl der verfügbaren Balken abzurufen und den „Lookback“ zu begrenzen, wenn diese Grenze überschritten wird, um sicherzustellen, dass der Datenbereich des Charts nicht überschritten wird. Als Nächstes bereiten wir die Arrays „highs“ und „lows“ für die Preisdaten vor, indem wir sie mit der Funktion ArraySetAsSeries als Zeitreihen festlegen (die neueste zuerst) und sie mit CopyHigh und CopyLow auffüllen, um die Höchst- und Tiefstpreise über „lookback + 1“ Bars zu extrahieren.

In einer Schleife vom ältesten zum neuesten Balken bestimmen wir den Trend mit „isUpTrend“ auf der Grundlage der anfänglichen Kursbewegung, verfolgen dann „lastHigh“ oder „lastLow“ und deren „lastExtremumBar“; wenn ein Umschwung „ThresholdPoints“ überschreitetWenn eine Umkehrung „ThresholdPoints“ überschreitet, erweitern wir „extrema“ mit der Funktion ArrayResize, speichern Details wie „bar“, „time“ (über „iTime“), „price“ und „isPeak“ (true für Spitzen, false für Talsohlen) und schalten den Trend um, was eine präzise Mustererkennung ermöglicht. Nun können wir die ermittelten Preisniveaus für die weitere Verwendung speichern.

Extremum extrema[];                                                   //--- Array to store identified peaks and troughs
FindExtrema(extrema, LookbackBars);                                   //--- Find extrema in the last LookbackBars bars

Hier deklarieren wir das Array „extrema“ vom Typ „Extremum“, um identifizierte Höchst- und Tiefstwerte zu speichern, die die Schultern und den Kopf des Musters darstellen. Anschließend rufen wir die Funktion „FindExtrema“ auf, wobei wir „extrema“ und „LookbackBars“ als Argumente übergeben, um die letzten „LookbackBars“-Balken zu scannen und das Array mit den Schlüsselextremen aufzufüllen, was die Grundlage für die Mustererkennung und die nachfolgenden Handelsentscheidungen bildet. Wenn wir die Array-Werte mit der Funktion ArrayPrint ausdrucken, erhalten wir Folgendes.

GESPEICHERTE PREISDATEN

Dies bestätigt, dass wir über die erforderlichen Datenpunkte verfügen. Wir können also mit der Identifizierung der Musterkomponenten fortfahren. Um den Code zu modularisieren, verwenden wir Funktionen.

//+------------------------------------------------------------------+
//| Detect standard Head and Shoulders pattern                       |
//+------------------------------------------------------------------+
bool DetectHeadAndShoulders(Extremum &extrema[], int &leftShoulderIdx, int &headIdx, int &rightShoulderIdx, int &necklineStartIdx, int &necklineEndIdx) { //--- Function to detect standard H&S pattern
   int size = ArraySize(extrema);                                        //--- Get the size of the extrema array
   if (size < 6) return false;                                           //--- Return false if insufficient extrema for pattern (need at least 6 points)

   for (int i = size - 6; i >= 0; i--) {                                 //--- Loop through extrema to find H&S pattern (start at size-6 to ensure enough points)
      if (!extrema[i].isPeak && extrema[i+1].isPeak && !extrema[i+2].isPeak && //--- Check sequence: trough, peak (LS), trough
          extrema[i+3].isPeak && !extrema[i+4].isPeak && extrema[i+5].isPeak) { //--- Check sequence: peak (head), trough, peak (RS)
         double leftShoulder = extrema[i+1].price;                       //--- Get price of left shoulder
         double head = extrema[i+3].price;                               //--- Get price of head
         double rightShoulder = extrema[i+5].price;                      //--- Get price of right shoulder
         double trough1 = extrema[i+2].price;                            //--- Get price of first trough (neckline start)
         double trough2 = extrema[i+4].price;                            //--- Get price of second trough (neckline end)

         bool isHeadHighest = true;                                      //--- Flag to verify head is the highest peak in range
         for (int j = MathMax(0, i - 5); j < MathMin(size, i + 10); j++) { //--- Check surrounding bars (5 before, 10 after) for higher peaks
            if (extrema[j].isPeak && extrema[j].price > head && j != i + 3) { //--- If another peak is higher than head
               isHeadHighest = false;                                    //--- Set flag to false
               break;                                                    //--- Exit loop as head is not highest
            }
         }

         int lsBar = extrema[i+1].bar;                                   //--- Get bar index of left shoulder
         int headBar = extrema[i+3].bar;                                 //--- Get bar index of head
         int rsBar = extrema[i+5].bar;                                   //--- Get bar index of right shoulder
         int lsToHead = lsBar - headBar;                                 //--- Calculate bars from left shoulder to head
         int headToRs = headBar - rsBar;                                 //--- Calculate bars from head to right shoulder

         if (lsToHead < MinBarRange || lsToHead > MaxBarRange || headToRs < MinBarRange || headToRs > MaxBarRange) continue; //--- Skip if bar ranges are out of bounds

         int minRange = MathMin(lsToHead, headToRs);                     //--- Get the smaller of the two ranges for uniformity check
         if (lsToHead > minRange * BarRangeMultiplier || headToRs > minRange * BarRangeMultiplier) continue; //--- Skip if ranges exceed uniformity multiplier

         bool rsValid = false;                                           //--- Flag to validate right shoulder breakout
         int rsBarIndex = extrema[i+5].bar;                              //--- Get bar index of right shoulder for validation
         for (int j = rsBarIndex - 1; j >= MathMax(0, rsBarIndex - ValidationBars); j--) { //--- Check bars after right shoulder for breakout
            if (iLow(_Symbol, _Period, j) < rightShoulder - ThresholdPoints * _Point) { //--- Check if price drops below RS by threshold
               rsValid = true;                                           //--- Set flag to true if breakout confirmed
               break;                                                    //--- Exit loop once breakout is validated
            }
         }
         if (!rsValid) continue;                                         //--- Skip if right shoulder breakout not validated

         if (isHeadHighest && head > leftShoulder && head > rightShoulder && //--- Verify head is highest and above shoulders
             MathAbs(leftShoulder - rightShoulder) < ShoulderTolerancePoints * _Point && //--- Check shoulder price difference within tolerance
             MathAbs(trough1 - trough2) < TroughTolerancePoints * _Point) { //--- Check trough price difference within tolerance
            leftShoulderIdx = i + 1;                                     //--- Set index for left shoulder
            headIdx = i + 3;                                             //--- Set index for head
            rightShoulderIdx = i + 5;                                    //--- Set index for right shoulder
            necklineStartIdx = i + 2;                                    //--- Set index for neckline start (first trough)
            necklineEndIdx = i + 4;                                      //--- Set index for neckline end (second trough)
            Print("Bar Ranges: LS to Head = ", lsToHead, ", Head to RS = ", headToRs); //--- Log bar ranges for debugging
            return true;                                                 //--- Return true to indicate pattern found
         }
      }
   }
   return false;                                                         //--- Return false if no pattern detected
}

Hier erkennen wir das Standardmuster durch die Funktion „DetectHeadAndShoulders“, die das Array „extrema“ untersucht, um eine gültige Abfolge von sechs Punkten zu finden: einen Tiefpunkt, einen Höchstwert (linke Schulter), einen Tiefpunkt, einen Höchstwert (Kopf), einen Tiefpunkt und einen Höchstwert (rechte Schulter), wofür mindestens sechs Einträge erforderlich sind, wie von der Funktion ArraySize geprüft. Wir gehen in einer Schleife durch „extrema“, beginnend bei „size - 6“, und überprüfen die Spannbreite der abwechselnde hohen und tiefen Spitzen des Musters, dann extrahieren wir die Preise für „leftShoulder“, „head“, „rightShoulder“ und die Tiefpunkte der Halslinie („trough1“, „trough2“); eine verschachtelte Schleife stellt sicher, dass der Kopf der höchste Peak innerhalb eines Bereichs ist, indem die Funktionen MathMax und MathMin verwendet werden, während die Balkenabstände zwischen den Punkten durch „MinBarRange“ und „MaxBarRange“ und die Einheitlichkeit durch „BarRangeMultiplier“ eingeschränkt werden.

Wir bestätigen den Ausbruch durch die rechten Schulter, indem wir die Funktion iLow gegen „ThresholdPoints“ über „ValidationBars“ prüfen, und wenn der Kopf beide Schultern übersteigt und die Toleranzen („ShoulderTolerancePoints“, „TroughTolerancePoints“) erfüllt sind, weisen wir Indizes wie „leftShoulderIdx“, „headIdx“ und „necklineStartIdx“ zu, protokollieren die Balkenbereiche mit Print zur Fehlersuche und geben „true“ zurück, um ein erkanntes Muster zu signalisieren, andernfalls „false“. Wir verwenden dieselbe Logik, um das umgekehrte Muster zu finden.

//+------------------------------------------------------------------+
//| Detect inverse Head and Shoulders pattern                        |
//+------------------------------------------------------------------+
bool DetectInverseHeadAndShoulders(Extremum &extrema[], int &leftShoulderIdx, int &headIdx, int &rightShoulderIdx, int &necklineStartIdx, int &necklineEndIdx) { //--- Function to detect inverse H&S pattern
   int size = ArraySize(extrema);                                        //--- Get the size of the extrema array
   if (size < 6) return false;                                           //--- Return false if insufficient extrema for pattern (need at least 6 points)

   for (int i = size - 6; i >= 0; i--) {                                 //--- Loop through extrema to find inverse H&S pattern
      if (extrema[i].isPeak && !extrema[i+1].isPeak && extrema[i+2].isPeak && //--- Check sequence: peak, trough (LS), peak
          !extrema[i+3].isPeak && extrema[i+4].isPeak && !extrema[i+5].isPeak) { //--- Check sequence: trough (head), peak, trough (RS)
         double leftShoulder = extrema[i+1].price;                       //--- Get price of left shoulder
         double head = extrema[i+3].price;                               //--- Get price of head
         double rightShoulder = extrema[i+5].price;                      //--- Get price of right shoulder
         double peak1 = extrema[i+2].price;                              //--- Get price of first peak (neckline start)
         double peak2 = extrema[i+4].price;                              //--- Get price of second peak (neckline end)

         bool isHeadLowest = true;                                       //--- Flag to verify head is the lowest trough in range
         int headBar = extrema[i+3].bar;                                 //--- Get bar index of head for range check
         for (int j = MathMax(0, headBar - 5); j <= MathMin(Bars(_Symbol, _Period) - 1, headBar + 5); j++) { //--- Check 5 bars before and after head
            if (iLow(_Symbol, _Period, j) < head) {                      //--- If any low is below head
               isHeadLowest = false;                                     //--- Set flag to false
               break;                                                    //--- Exit loop as head is not lowest
            }
         }

         int lsBar = extrema[i+1].bar;                                   //--- Get bar index of left shoulder
         int rsBar = extrema[i+5].bar;                                   //--- Get bar index of right shoulder
         int lsToHead = lsBar - headBar;                                 //--- Calculate bars from left shoulder to head
         int headToRs = headBar - rsBar;                                 //--- Calculate bars from head to right shoulder

         if (lsToHead < MinBarRange || lsToHead > MaxBarRange || headToRs < MinBarRange || headToRs > MaxBarRange) continue; //--- Skip if bar ranges are out of bounds

         int minRange = MathMin(lsToHead, headToRs);                     //--- Get the smaller of the two ranges for uniformity check
         if (lsToHead > minRange * BarRangeMultiplier || headToRs > minRange * BarRangeMultiplier) continue; //--- Skip if ranges exceed uniformity multiplier

         bool rsValid = false;                                           //--- Flag to validate right shoulder breakout
         int rsBarIndex = extrema[i+5].bar;                              //--- Get bar index of right shoulder for validation
         for (int j = rsBarIndex - 1; j >= MathMax(0, rsBarIndex - ValidationBars); j--) { //--- Check bars after right shoulder for breakout
            if (iHigh(_Symbol, _Period, j) > rightShoulder + ThresholdPoints * _Point) { //--- Check if price rises above RS by threshold
               rsValid = true;                                           //--- Set flag to true if breakout confirmed
               break;                                                    //--- Exit loop once breakout is validated
            }
         }
         if (!rsValid) continue;                                         //--- Skip if right shoulder breakout not validated

         if (isHeadLowest && head < leftShoulder && head < rightShoulder && //--- Verify head is lowest and below shoulders
             MathAbs(leftShoulder - rightShoulder) < ShoulderTolerancePoints * _Point && //--- Check shoulder price difference within tolerance
             MathAbs(peak1 - peak2) < TroughTolerancePoints * _Point) { //--- Check peak price difference within tolerance
            leftShoulderIdx = i + 1;                                     //--- Set index for left shoulder
            headIdx = i + 3;                                             //--- Set index for head
            rightShoulderIdx = i + 5;                                    //--- Set index for right shoulder
            necklineStartIdx = i + 2;                                    //--- Set index for neckline start (first peak)
            necklineEndIdx = i + 4;                                      //--- Set index for neckline end (second peak)
            Print("Bar Ranges: LS to Head = ", lsToHead, ", Head to RS = ", headToRs); //--- Log bar ranges for debugging
            return true;                                                 //--- Return true to indicate pattern found
         }
      }
   }
   return false;                                                         //--- Return false if no pattern detected
}

Wir definieren die Funktion „DetectInverseHeadAndShoulders“ zur Ermittlung des inversen Musters, die das „extrema“-Array durchsucht, um eine Sequenz von sechs Punkten zu finden - Höchstwert, Tiefstwert (linke Schulter), Höchstwert, Tiefstwert (Kopf), Höchstwert, Tiefstwert (rechte Schulter) -, für die mindestens sechs Einträge erforderlich sind, wie mit der Funktion ArraySize überprüft wird. Wir iterieren von „size - 6“ abwärts, um den Wechsel von Höchst- und Tiefstkursen des Musters zu bestätigen, und ziehen dann die Preise für „leftShoulder“, „head“, „rightShoulder“ und die Höchstkurse der Halslinie („peak1“, „peak2“); eine verschachtelte Schleife prüft, ob der Kopf der niedrigste Tiefpunkt innerhalb eines Fünf-Bar-Bereichs um „headBar“ ist, wobei die Funktionen MathMax, MathMin und iLow verwendet werden, während „Bars“ sicherstellt, dass wir innerhalb der Chartgrenzen bleiben.

Wir erzwingen den Abstand der Balken mit „MinBarRange“ und „MaxBarRange“, berechnen die Gleichmäßigkeit mit der Funktion MathMin und „BarRangeMultiplier“ und validieren den Ausbruch der rechten Schulter mit der Funktion iHigh gegen „ThresholdPoints“ über „ValidationBars“. Wenn Der Kopf „head“ unter beiden Schultern liegt und die Toleranzen („ShoulderTolerancePoints“, „TroughTolerancePoints“) erfüllt sind, setzen wir Indizes wie „leftShoulderIdx“ und „necklineStartIdx“, protokollieren die Bereiche und geben „true“ zurück, ansonsten „false“. Mit diesen 2 Funktionen können wir nun wie folgt vorgehen, um die Muster zu identifizieren.

int leftShoulderIdx, headIdx, rightShoulderIdx, necklineStartIdx, necklineEndIdx; //--- Indices for pattern components

// Standard Head and Shoulders (Sell)
if (DetectHeadAndShoulders(extrema, leftShoulderIdx, headIdx, rightShoulderIdx, necklineStartIdx, necklineEndIdx)) { //--- Check for standard H&S pattern
   double closePrice = iClose(_Symbol, _Period, 1);                   //--- Get the closing price of the previous bar
   double necklinePrice = extrema[necklineEndIdx].price;              //--- Get the price of the neckline end point

   if (closePrice < necklinePrice) {                                  //--- Check if price has broken below the neckline (sell signal)
      datetime lsTime = extrema[leftShoulderIdx].time;                //--- Get the timestamp of the left shoulder
      double lsPrice = extrema[leftShoulderIdx].price;                //--- Get the price of the left shoulder

      //---
   }
}

Hier deklarieren wir zunächst die Variablen „leftShoulderIdx“, „headIdx“, „rightShoulderIdx“, „necklineStartIdx“ und „necklineEndIdx“ deklarieren, um die Indizes der Komponenten des Musters zu speichern, und dann die Funktion „DetectHeadAndShoulders“ verwenden, um das Array „extrema“ auf ein Standardmuster zu überprüfen, wobei diese Indizes als Referenzen übergeben werden. Wenn dies der Fall ist, rufen wir „closePrice“ mit der Funktion iClose für den vorherigen Balken und der „necklinePrice“ aus „extrema[necklineEndIdx].price“ ab und lösen ein Verkaufssignal aus, wenn „closePrice“ unter „necklinePrice“ fällt. Anschließend extrahieren wir „lsTime“ und „lsPrice“ aus „extrema[leftShoulderIdx]“, um die Ausführung des Handels auf der Grundlage der Position der linken Schulter vorzubereiten. An diesem Punkt müssen wir sicherstellen, dass das Muster nicht gehandelt wird. Wir definieren eine Funktion, die diese Prüfung durchführt.

//+------------------------------------------------------------------+
//| Check if pattern has already been traded                         |
//+------------------------------------------------------------------+
bool IsPatternTraded(datetime lsTime, double lsPrice) {                  //--- Function to check if a pattern has already been traded
   int size = ArraySize(tradedPatterns);                                 //--- Get the current size of the tradedPatterns array
   for (int i = 0; i < size; i++) {                                      //--- Loop through all stored traded patterns
      if (tradedPatterns[i].leftShoulderTime == lsTime &&                //--- Check if left shoulder time matches
          MathAbs(tradedPatterns[i].leftShoulderPrice - lsPrice) < PriceTolerance * _Point) { //--- Check if left shoulder price is within tolerance
         Print("Pattern already traded: Left Shoulder Time ", TimeToString(lsTime), ", Price ", DoubleToString(lsPrice, _Digits)); //--- Log that pattern was previously traded
         return true;                                                    //--- Return true to indicate pattern has been traded
      }
   }
   return false;                                                         //--- Return false if no match found
}

Hier stellen wir sicher, dass unser Programm doppelte Abschlüsse vermeidet, indem wir die Funktion „IsPatternTraded“ implementieren, die prüft, ob ein Muster, das durch „lsTime“ und „lsPrice“ identifiziert wird, bereits im Array „tradedPatterns“ existiert. Wir verwenden die Funktion ArraySize, um die „Größe“ des Arrays zu ermitteln, dann durchlaufen wir es in einer Schleife und vergleichen die „leftShoulderTime“ jedes Eintrags mit „lsTime“ und „leftShoulderPrice“ mit „lsPrice“ innerhalb eines „PriceTolerance“-Bereichs über die Funktion MathAbs; wenn eine Übereinstimmung gefunden wird, protokollieren wir sie mit Print, einschließlich TimeToString und DoubleToString zur besseren Lesbarkeit, und geben true zurück, andernfalls false, um einen neuen Handel zuzulassen. Wir rufen dann die Funktion auf, um die Prüfung durchzuführen und fahren fort, wenn keine gefunden wird.

if (IsPatternTraded(lsTime, lsPrice)) return;                   //--- Exit if this pattern has already been traded

datetime breakoutTime = iTime(_Symbol, _Period, 1);             //--- Get the timestamp of the breakout bar (previous bar)
int lsBar = extrema[leftShoulderIdx].bar;                       //--- Get the bar index of the left shoulder
int headBar = extrema[headIdx].bar;                             //--- Get the bar index of the head
int rsBar = extrema[rightShoulderIdx].bar;                      //--- Get the bar index of the right shoulder
int necklineStartBar = extrema[necklineStartIdx].bar;           //--- Get the bar index of the neckline start
int necklineEndBar = extrema[necklineEndIdx].bar;               //--- Get the bar index of the neckline end
int breakoutBar = 1;                                            //--- Set breakout bar index (previous bar)

int lsToHead = lsBar - headBar;                                 //--- Calculate number of bars from left shoulder to head
int headToRs = headBar - rsBar;                                 //--- Calculate number of bars from head to right shoulder
int rsToBreakout = rsBar - breakoutBar;                         //--- Calculate number of bars from right shoulder to breakout
int lsToNeckStart = lsBar - necklineStartBar;                   //--- Calculate number of bars from left shoulder to neckline start
double avgPatternRange = (lsToHead + headToRs) / 2.0;           //--- Calculate average bar range of the pattern for uniformity check

if (rsToBreakout > avgPatternRange * RightShoulderBreakoutMultiplier) { //--- Check if breakout distance exceeds allowed range
   Print("Pattern rejected: Right Shoulder to Breakout (", rsToBreakout, 
         ") exceeds ", RightShoulderBreakoutMultiplier, "x average range (", avgPatternRange, ")"); //--- Log rejection due to excessive breakout range
   return;                                                      //--- Exit function if pattern is invalid
}

double necklineStartPrice = extrema[necklineStartIdx].price;    //--- Get the price of the neckline start point
double necklineEndPrice = extrema[necklineEndIdx].price;        //--- Get the price of the neckline end point
datetime necklineStartTime = extrema[necklineStartIdx].time;    //--- Get the timestamp of the neckline start point
datetime necklineEndTime = extrema[necklineEndIdx].time;        //--- Get the timestamp of the neckline end point
int barDiff = necklineStartBar - necklineEndBar;                //--- Calculate bar difference between neckline points for slope
double slope = (necklineEndPrice - necklineStartPrice) / barDiff; //--- Calculate the slope of the neckline (price change per bar)
double breakoutNecklinePrice = necklineStartPrice + slope * (necklineStartBar - breakoutBar); //--- Calculate neckline price at breakout point

// Extend neckline backwards
int extendedBar = necklineStartBar;                             //--- Initialize extended bar index with neckline start
datetime extendedNecklineStartTime = necklineStartTime;         //--- Initialize extended neckline start time
double extendedNecklineStartPrice = necklineStartPrice;         //--- Initialize extended neckline start price
bool foundCrossing = false;                                     //--- Flag to track if neckline crosses a bar within range

for (int i = necklineStartBar + 1; i < Bars(_Symbol, _Period); i++) { //--- Loop through bars to extend neckline backwards
   double checkPrice = necklineStartPrice - slope * (i - necklineStartBar); //--- Calculate projected neckline price at bar i
   if (NecklineCrossesBar(checkPrice, i)) {                     //--- Check if neckline intersects the bar's high-low range
      int distance = i - necklineStartBar;                      //--- Calculate distance from neckline start to crossing bar
      if (distance <= avgPatternRange * RightShoulderBreakoutMultiplier) { //--- Check if crossing is within uniformity range
         extendedBar = i;                                       //--- Update extended bar index
         extendedNecklineStartTime = iTime(_Symbol, _Period, i); //--- Update extended neckline start time
         extendedNecklineStartPrice = checkPrice;              //--- Update extended neckline start price
         foundCrossing = true;                                  //--- Set flag to indicate crossing found
         Print("Neckline extended to first crossing bar within uniformity: Bar ", extendedBar); //--- Log successful extension
         break;                                                 //--- Exit loop after finding valid crossing
      } else {                                                  //--- If crossing exceeds uniformity range
         Print("Crossing bar ", i, " exceeds uniformity (", distance, " > ", avgPatternRange * RightShoulderBreakoutMultiplier, ")"); //--- Log rejection of crossing
         break;                                                 //--- Exit loop as crossing is too far
      }
   }
}

if (!foundCrossing) {                                           //--- If no valid crossing found within range
   int barsToExtend = 2 * lsToNeckStart;                        //--- Set fallback extension distance as twice LS to neckline start
   extendedBar = necklineStartBar + barsToExtend;               //--- Calculate extended bar index
   if (extendedBar >= Bars(_Symbol, _Period)) extendedBar = Bars(_Symbol, _Period) - 1; //--- Cap extended bar at total bars if exceeded
   extendedNecklineStartTime = iTime(_Symbol, _Period, extendedBar); //--- Update extended neckline start time
   extendedNecklineStartPrice = necklineStartPrice - slope * (extendedBar - necklineStartBar); //--- Update extended neckline start price
   Print("Neckline extended to fallback (2x LS to Neckline Start): Bar ", extendedBar, " (no crossing within uniformity)"); //--- Log fallback extension
}

Print("Standard Head and Shoulders Detected:");                 //--- Log detection of standard H&S pattern
Print("Left Shoulder: Bar ", lsBar, ", Time ", TimeToString(lsTime), ", Price ", DoubleToString(lsPrice, _Digits)); //--- Log left shoulder details
Print("Head: Bar ", headBar, ", Time ", TimeToString(extrema[headIdx].time), ", Price ", DoubleToString(extrema[headIdx].price, _Digits)); //--- Log head details
Print("Right Shoulder: Bar ", rsBar, ", Time ", TimeToString(extrema[rightShoulderIdx].time), ", Price ", DoubleToString(extrema[rightShoulderIdx].price, _Digits)); //--- Log right shoulder details
Print("Neckline Start: Bar ", necklineStartBar, ", Time ", TimeToString(necklineStartTime), ", Price ", DoubleToString(necklineStartPrice, _Digits)); //--- Log neckline start details
Print("Neckline End: Bar ", necklineEndBar, ", Time ", TimeToString(necklineEndTime), ", Price ", DoubleToString(necklineEndPrice, _Digits)); //--- Log neckline end details
Print("Close Price: ", DoubleToString(closePrice, _Digits));    //--- Log closing price at breakout
Print("Breakout Time: ", TimeToString(breakoutTime));           //--- Log breakout timestamp
Print("Neckline Price at Breakout: ", DoubleToString(breakoutNecklinePrice, _Digits)); //--- Log neckline price at breakout
Print("Extended Neckline Start: Bar ", extendedBar, ", Time ", TimeToString(extendedNecklineStartTime), ", Price ", DoubleToString(extendedNecklineStartPrice, _Digits)); //--- Log extended neckline start details
Print("Bar Ranges: LS to Head = ", lsToHead, ", Head to RS = ", headToRs, ", RS to Breakout = ", rsToBreakout, ", LS to Neckline Start = ", lsToNeckStart); //--- Log bar ranges for pattern analysis

Hier verbessern wir die Mustererkennung, indem wir ein erkanntes Standardmuster validieren und einen Verkaufshandel einrichten, beginnend mit der Funktion „IsPatternTraded“, die prüft, ob „lsTime“ und „lsPrice“ mit einem früheren Handel in „tradedPatterns“ übereinstimmen, und die bei „true“ beendet wird, um Duplikate zu vermeiden. Wir verwenden dann die Funktion iTime, um „breakoutTime“ als Zeitstempel des vorherigen Balkens zuzuweisen und Bar-Indizes wie „lsBar“, „headBar“, „rsBar“, „necklineStartBar“ und „necklineEndBar“ von „extrema“ abzurufen zur Berechnung der Bereiche wie „lsToHead“, „headToRs“ und „rsToBreakout“; wenn „rsToBreakout“ „avgPatternRange“ mal „RightShoulderBreakoutMultiplier“ überschreitet, wird das Muster zurückgewiesen und mit der Funktion Print protokolliert.

Als Nächstes bestimmen wir die „Steigung“ der Nackenlinie mit „necklineStartPrice“ und „necklineEndPrice“ über „barDiff“, berechnen „breakoutNecklinePrice“ und verlängern die Nackenlinie in einer Schleife nach hinten, indem wir die Funktion „NecklineCrossesBar“ verwenden, um eine Kreuzung innerhalb von „avgPatternRange * RightShoulderBreakoutMultiplier“ zu finden, wobei „extendedBar“, „extendedNecklineStartTime“ (über „iTime“) und „extendedNecklineStartPrice“ aktualisiert werden; wenn keine Kreuzung passt, wird auf „2 * lsToNeckStart“ zurückgegangen, wobei die Obergrenze bei der Gesamtzahl der „Bars“ liegt, und protokollieren alle Details - Bar-Indizes, Preise und Bereiche - mit den Funktionen Print, TimeToString und DoubleToString für eine umfassende Dokumentation. Das Codefragment der nutzerdefinierten Funktion lautet wie folgt.

//+------------------------------------------------------------------+
//| Check if neckline crosses a bar's high-low range                 |
//+------------------------------------------------------------------+
bool NecklineCrossesBar(double necklinePrice, int barIndex) {            //--- Function to check if neckline price intersects a bar's range
   double high = iHigh(_Symbol, _Period, barIndex);                      //--- Get the high price of the specified bar
   double low = iLow(_Symbol, _Period, barIndex);                        //--- Get the low price of the specified bar
   return (necklinePrice >= low && necklinePrice <= high);               //--- Return true if neckline price is within bar's high-low range
}

Die Funktion prüft, ob „necklinePrice“ die Preisspanne eines Balkens bei „barIndex“ schneidet, um eine genaue Verlängerung der Halslinie zu gewährleisten. Wir verwenden die Funktion iHigh, um das Hoch des Balkens zu ermitteln, und die Funktion iLow, um das Tief zu ermitteln, und geben dann true zurück, wenn necklinePrice zwischen dem Tief und dem Hoch liegt, was bestätigt, dass die Nackenlinie den Bereich des Balkens für die Mustervalidierung durchquert. Wenn es eine Bestätigung des Musters gibt, visualisieren wir es auf dem Chart. Wir brauchen Funktionen, um sie zu zeichnen und zu beschriften.

//+------------------------------------------------------------------+
//| Draw a trend line for visualization                              |
//+------------------------------------------------------------------+
void DrawTrendLine(string name, datetime timeStart, double priceStart, datetime timeEnd, double priceEnd, color lineColor, int width, int style) { //--- Function to draw a trend line on the chart
   if (ObjectCreate(0, name, OBJ_TREND, 0, timeStart, priceStart, timeEnd, priceEnd)) { //--- Create a trend line object if possible
      ObjectSetInteger(0, name, OBJPROP_COLOR, lineColor);               //--- Set the color of the trend line
      ObjectSetInteger(0, name, OBJPROP_STYLE, style);                   //--- Set the style (e.g., solid, dashed) of the trend line
      ObjectSetInteger(0, name, OBJPROP_WIDTH, width);                   //--- Set the width of the trend line
      ObjectSetInteger(0, name, OBJPROP_BACK, true);                     //--- Set the line to draw behind chart elements
      ChartRedraw();                                                     //--- Redraw the chart to display the new line
   } else {                                                              //--- If line creation fails
      Print("Failed to create line: ", name, ". Error: ", GetLastError()); //--- Log the error with the object name and error code
   }
}

//+------------------------------------------------------------------+
//| Draw a filled triangle for visualization                         |
//+------------------------------------------------------------------+
void DrawTriangle(string name, datetime time1, double price1, datetime time2, double price2, datetime time3, double price3, color fillColor) { //--- Function to draw a filled triangle on the chart
   if (ObjectCreate(0, name, OBJ_TRIANGLE, 0, time1, price1, time2, price2, time3, price3)) { //--- Create a triangle object if possible
      ObjectSetInteger(0, name, OBJPROP_COLOR, fillColor);               //--- Set the fill color of the triangle
      ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID);             //--- Set the border style to solid
      ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);                       //--- Set the border width to 1 pixel
      ObjectSetInteger(0, name, OBJPROP_FILL, true);                     //--- Enable filling of the triangle
      ObjectSetInteger(0, name, OBJPROP_BACK, true);                     //--- Set the triangle to draw behind chart elements
      ChartRedraw();                                                     //--- Redraw the chart to display the new triangle
   } else {                                                              //--- If triangle creation fails
      Print("Failed to create triangle: ", name, ". Error: ", GetLastError()); //--- Log the error with the object name and error code
   }
}

//+------------------------------------------------------------------+
//| Draw text label for visualization                                |
//+------------------------------------------------------------------+
void DrawText(string name, datetime time, double price, string text, color textColor, bool above, double angle = 0) { //--- Function to draw a text label on the chart
   int chartscale = (int)ChartGetInteger(0, CHART_SCALE);                //--- Get the current chart zoom level
   int dynamicFontSize = 5 + int(chartscale * 1.5);                      //--- Calculate font size based on zoom level for visibility
   double priceOffset = (above ? 10 : -10) * _Point;                     //--- Set price offset above or below the point for readability
   if (ObjectCreate(0, name, OBJ_TEXT, 0, time, price + priceOffset)) {  //--- Create a text object if possible
      ObjectSetString(0, name, OBJPROP_TEXT, text);                      //--- Set the text content of the label
      ObjectSetInteger(0, name, OBJPROP_COLOR, textColor);               //--- Set the color of the text
      ObjectSetInteger(0, name, OBJPROP_FONTSIZE, dynamicFontSize);      //--- Set the font size based on chart scale
      ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_CENTER);          //--- Center the text at the specified point
      ObjectSetDouble(0, name, OBJPROP_ANGLE, angle);                    //--- Set the rotation angle of the text in degrees
      ObjectSetInteger(0, name, OBJPROP_BACK, false);                    //--- Set the text to draw in front of chart elements
      ChartRedraw();                                                     //--- Redraw the chart to display the new text
      Print("Text created: ", name, ", Angle: ", DoubleToString(angle, 2)); //--- Log successful creation of the text with its angle
   } else {                                                              //--- If text creation fails
      Print("Failed to create text: ", name, ". Error: ", GetLastError()); //--- Log the error with the object name and error code
   }
}

Hier bereichern wir das Programm mit Visualisierungswerkzeugen, um das Muster im Chart hervorzuheben, beginnend mit der Funktion „DrawTrendLine“, die die Funktion ObjectCreate verwendet, um eine Linie von „timeStart“ und „priceStart“ zu „timeEnd“ und „priceEnd“ eine Linie zu zeichnen, Eigenschaften wie „lineColor“, „style“ und „width“ über ObjectSetInteger zu setzen, sie mit OBJPROP_BACK hinter Balken zu zeichnen und die Anzeige mit der Funktion ChartRedraw zu aktualisieren, wobei Fehler mit „Print“ und GetLastError protokolliert werden, falls erforderlich.

Anschließend implementieren wir die Funktion „DrawTriangle“, um die Struktur des Musters zu schattieren, indem wir die Funktion „ObjectCreate“ mit drei Punkten („time1“, „price1“ usw.) aufrufen.), wenden „fillColor“ und einen festen Rahmen mit ObjectSetInteger an, füllen es mit OBJPROP_FILL, platzieren es hinter dem Chart und aktualisieren die Ansicht mit ChartRedraw, wobei wir wiederum Fehler mit „Print“ protokollieren, wenn die Erstellung fehlschlägt.

Schließlich fügen wir die Funktion „DrawText“ hinzu, um Schlüsselpunkte zu beschriften, verwenden die Funktion „ChartGetInteger“, um „dynamicFontSize“ auf der Grundlage von „chartscale“ anzupassen, positionieren den Text bei „time“ und „price“ plus einem Offset über „ObjectCreate“ und passen ihn mit „ObjectSetString“ für „Text“, „ObjectSetInteger“ für „textColor“ und „FONTSIZE“, und „ObjectSetDouble“ für „angle“ anpassen, es mit ChartRedraw einzeichnen und die Erstellung mit „Print“ und „DoubleToString“ bestätigen oder Fehler vermerken. Jetzt können wir die Funktionen aufrufen, um das Sichtbarkeitsmerkmal hinzuzufügen, und als Erstes fügen wir die Zeilen wie folgt hinzu.

string prefix = "HS_" + TimeToString(extrema[headIdx].time, TIME_MINUTES); //--- Create unique prefix for chart objects based on head time
// Lines
DrawTrendLine(prefix + "_LeftToNeckStart", lsTime, lsPrice, necklineStartTime, necklineStartPrice, clrRed, 3, STYLE_SOLID); //--- Draw line from left shoulder to neckline start
DrawTrendLine(prefix + "_NeckStartToHead", necklineStartTime, necklineStartPrice, extrema[headIdx].time, extrema[headIdx].price, clrRed, 3, STYLE_SOLID); //--- Draw line from neckline start to head
DrawTrendLine(prefix + "_HeadToNeckEnd", extrema[headIdx].time, extrema[headIdx].price, necklineEndTime, necklineEndPrice, clrRed, 3, STYLE_SOLID); //--- Draw line from head to neckline end
DrawTrendLine(prefix + "_NeckEndToRight", necklineEndTime, necklineEndPrice, extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, clrRed, 3, STYLE_SOLID); //--- Draw line from neckline end to right shoulder
DrawTrendLine(prefix + "_Neckline", extendedNecklineStartTime, extendedNecklineStartPrice, breakoutTime, breakoutNecklinePrice, clrBlue, 2, STYLE_SOLID); //--- Draw neckline from extended start to breakout
DrawTrendLine(prefix + "_RightToBreakout", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, breakoutTime, breakoutNecklinePrice, clrRed, 3, STYLE_SOLID); //--- Draw line from right shoulder to breakout
DrawTrendLine(prefix + "_ExtendedToLeftShoulder", extendedNecklineStartTime, extendedNecklineStartPrice, lsTime, lsPrice, clrRed, 3, STYLE_SOLID); //--- Draw line from extended neckline to left shoulder

Hier bilden wir das Standardmuster visuell ab, indem wir ein eindeutiges Präfix basierend auf dem Zeitstempel des Kopfes mit der Funktion TimeToString erstellen und mit der Funktion „DrawTrendLine“ Trendlinien zeichnen, die die linke Schulter, den Kopf und die rechte Schulter vom Beginn bis zum Ende der Nackenlinie in Rot mit einer Breite von 3 nachzeichnen, während die Nackenlinie von ihrem verlängerten Beginn bis zum Ausbruchspunkt in Blau mit einer Breite von 2 gezeichnet wird und weitere Linien die rechte Schulter mit dem Ausbruch und die verlängerte Nackenlinie zurück mit der linken Schulter in Rot verbinden, alles in einem durchgehenden Stil, um das Muster im Chart darzustellen. Nach der Kompilierung erhalten wir folgendes Ergebnis.

MIT LINIEN

Um die Dreiecke hinzuzufügen, verwenden wir die Funktion „DrawTriangle“. Technisch gesehen konstruieren wir sie innerhalb der Schultern und des Kopfes.

// Triangles
DrawTriangle(prefix + "_LeftShoulderTriangle", lsTime, lsPrice, necklineStartTime, necklineStartPrice, extendedNecklineStartTime, extendedNecklineStartPrice, clrLightCoral); //--- Draw triangle for left shoulder area
DrawTriangle(prefix + "_HeadTriangle", extrema[headIdx].time, extrema[headIdx].price, necklineStartTime, necklineStartPrice, necklineEndTime, necklineEndPrice, clrLightCoral); //--- Draw triangle for head area
DrawTriangle(prefix + "_RightShoulderTriangle", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, necklineEndTime, necklineEndPrice, breakoutTime, breakoutNecklinePrice, clrLightCoral); //--- Draw triangle for right shoulder area

Hier verbessern wir die Visualisierung, indem wir die Funktion „DrawTriangle“ verwenden, um die Schlüsselbereiche in hellem Korallenrot zu schattieren. So bilden wir ein Dreieck für die linke Schulter vom Punkt der linken Schulter bis zum Beginn der Nackenlinie und dem Beginn der verlängerten Nackenlinie, ein weiteres für den Kopf vom Kopfpunkt bis zum Beginn und Ende der Nackenlinie und ein drittes für die rechte Schulter vom Punkt der rechten Schulter bis zum Ende der Nackenlinie und dem Ausbruchspunkt, um die Struktur des Musters auf dem Chart hervorzuheben. Nach der Kompilierung erhalten wir folgendes Ergebnis.

MIT DREIECKEN

Schließlich müssen wir dem Muster Beschriftungen hinzufügen, um es optisch ansprechend und selbsterklärend zu machen.

// Text Labels
DrawText(prefix + "_LS_Label", lsTime, lsPrice, "LS", clrRed, true); //--- Draw "LS" label above left shoulder
DrawText(prefix + "_Head_Label", extrema[headIdx].time, extrema[headIdx].price, "HEAD", clrRed, true); //--- Draw "HEAD" label above head
DrawText(prefix + "_RS_Label", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, "RS", clrRed, true); //--- Draw "RS" label above right shoulder
datetime necklineMidTime = extendedNecklineStartTime + (breakoutTime - extendedNecklineStartTime) / 2; //--- Calculate midpoint time of the neckline
double necklineMidPrice = extendedNecklineStartPrice + slope * (iBarShift(_Symbol, _Period, extendedNecklineStartTime) - iBarShift(_Symbol, _Period, necklineMidTime)); //--- Calculate midpoint price of the neckline
// Calculate angle in pixel space
int x1 = ShiftToX(iBarShift(_Symbol, _Period, extendedNecklineStartTime)); //--- Convert extended neckline start to x-pixel coordinate
int y1 = PriceToY(extendedNecklineStartPrice);                          //--- Convert extended neckline start price to y-pixel coordinate
int x2 = ShiftToX(iBarShift(_Symbol, _Period, breakoutTime));           //--- Convert breakout time to x-pixel coordinate
int y2 = PriceToY(breakoutNecklinePrice);                               //--- Convert breakout price to y-pixel coordinate
double pixelSlope = (y2 - y1) / (double)(x2 - x1);                     //--- Calculate slope in pixel space (rise over run)
double necklineAngle = -atan(pixelSlope) * 180 / M_PI;                  //--- Calculate neckline angle in degrees, negated for visual alignment
Print("Pixel X1: ", x1, ", Y1: ", y1, ", X2: ", x2, ", Y2: ", y2, ", Pixel Slope: ", DoubleToString(pixelSlope, 4), ", Neckline Angle: ", DoubleToString(necklineAngle, 2)); //--- Log pixel coordinates and angle
DrawText(prefix + "_Neckline_Label", necklineMidTime, necklineMidPrice, "NECKLINE", clrBlue, false, necklineAngle); //--- Draw "NECKLINE" label at midpoint with calculated angle

Abschließend wird das Muster mit der Funktion „DrawText“ kommentiert, indem die roten Beschriftungen „LS“, „HEAD“ und „RS“ über den Punkten der linken Schulter, des Kopfes und der rechten Schulter zu den jeweiligen Zeitpunkten und Preisen platziert werden, um die Lesbarkeit des Charts zu verbessern. Dann berechnen wir den Mittelpunkt der Nackenlinie, indem wir den Durchschnitt von „extendedNecklineStartTime“ und „breakoutTime“ für „necklineMidTime“ bilden und „extendedNecklineStartPrice“ mit „Slope“ und Balkendifferenzen über die Funktion iBarShift für „necklineMidPrice“ an; um den Text auszurichten, konvertieren wir Zeiten in x-Pixel mit der Funktion „ShiftToX“ in x-Pixel und die Preise mit der Funktion „PriceToY“ in y-Pixel am Anfang und am Ende der Nackenlinie um, berechnen eine „pixelSlope“ und leiten „necklineAngle“ in Grad mit der Funktion atan und „M_PI“ ab und protokollieren diese mit der Funktion „Print“ und der Funktion DoubleToString zur Überprüfung.

Dann zeichnen wir mit der Funktion „DrawText“ eine blaue Beschriftung „NECKLINE“ in der Mitte, die unten positioniert und entsprechend dem „necklineAngle“ gedreht wird, um sicherzustellen, dass die Beschriftung der Neigung des Ausschnitts folgt. Hier ist das Ergebnis.

ENDGÜLTIGES ERGEBNIS DES MUSTERS

Auf dem Bild ist zu erkennen, dass das Muster vollständig visualisiert ist. Wir müssen nun den Ausbruch, d. h. die verlängerte Linie, erkennen, eine Verkaufsposition eröffnen und den Verlängerungsbereich an den Ausbruchsstab anpassen. Ganz einfach. Das erreichen wir durch die folgende Logik.

double entryPrice = 0;                                                  //--- Set entry price to 0 for market order (uses current price)
double sl = extrema[rightShoulderIdx].price + BufferPoints * _Point;    //--- Calculate stop-loss above right shoulder with buffer
double patternHeight = extrema[headIdx].price - necklinePrice;          //--- Calculate pattern height from head to neckline
double tp = closePrice - patternHeight;                                 //--- Calculate take-profit below close by pattern height
if (sl > closePrice && tp < closePrice) {                               //--- Validate trade direction (SL above, TP below for sell)
   if (obj_Trade.Sell(LotSize, _Symbol, entryPrice, sl, tp, "Head and Shoulders")) { //--- Attempt to open a sell trade
      AddTradedPattern(lsTime, lsPrice);                                //--- Add pattern to traded list
      Print("Sell Trade Opened: SL ", DoubleToString(sl, _Digits), ", TP ", DoubleToString(tp, _Digits)); //--- Log successful trade opening
   }
}

Sobald das Muster bestätigt ist, führen wir einen Verkaufshandel aus, indem wir „entryPrice“ für eine Marktorder auf 0 setzen, „sl“ über dem rechten Schulterpreis mit „BufferPoints“ berechnen, „patternHeight“ als Differenz zwischen Kopf- und Halslinienpreis bestimmen und „tp“ um „patternHeight“ unter „closePrice“ setzen.

Wir überprüfen die Handelsrichtung, indem wir sicherstellen, dass „sl“ über und „tp“ unter „closePrice“ liegt, bevor wir die Funktion „Sell“ auf „obj_Trade“ anwenden, um den Handel mit „LotSize“, „sl“, „tp“ und Kommentar zu eröffnen; bei Erfolg rufen wir die Funktion „AddTradedPattern“ mit „lsTime“ und „lsPrice“ auf, um das Muster zu protokollieren, und verwenden die Funktion Print mit DoubleToString, um die Details von „sl“ und „tp“ aufzuzeichnen. Der Codeschnipsel der nutzerdefinierten Funktion zur Kennzeichnung des Musters als gehandelt lautet wie folgt.

//+------------------------------------------------------------------+
//| Add pattern to traded list with size management                  |
//+------------------------------------------------------------------+
void AddTradedPattern(datetime lsTime, double lsPrice) {                 //--- Function to add a new traded pattern to the list
   int size = ArraySize(tradedPatterns);                                 //--- Get the current size of the tradedPatterns array
   if (size >= MaxTradedPatterns) {                                      //--- Check if array size exceeds maximum allowed
      for (int i = 0; i < size - 1; i++) {                               //--- Shift all elements left to remove the oldest
         tradedPatterns[i] = tradedPatterns[i + 1];                      //--- Copy next element to current position
      }
      ArrayResize(tradedPatterns, size - 1);                            //--- Reduce array size by 1
      size--;                                                           //--- Decrement size variable
      Print("Removed oldest traded pattern to maintain max size of ", MaxTradedPatterns); //--- Log removal of oldest pattern
   }
   ArrayResize(tradedPatterns, size + 1);                                //--- Increase array size to add new pattern
   tradedPatterns[size].leftShoulderTime = lsTime;                       //--- Store the left shoulder time of the new pattern
   tradedPatterns[size].leftShoulderPrice = lsPrice;                     //--- Store the left shoulder price of the new pattern
   Print("Added traded pattern: Left Shoulder Time ", TimeToString(lsTime), ", Price ", DoubleToString(lsPrice, _Digits)); //--- Log addition of new pattern
}

Wir definieren die Funktion „AddTradedPattern“, um den Überblick über die gehandelten Setups zu behalten. Es verwendet „lsTime“ und „lsPrice“, um die Details der linken Schulter zu protokollieren, da die linke Schulter nicht neu gezeichnet wird. Wir überprüfen die Größe von „tradedPatterns“ mit der Funktion ArraySize. Wenn der Wert „MaxTradedPatterns“ erreicht wird, werden die ältesten Elemente nach links verschoben und fallengelassen. Wir ändern die Größe von „tradedPatterns“ mit der Funktion „ArrayResize“, um es zu verkleinern. Wir protokollieren dies und erweitern dann „tradedPatterns“ mit der Funktion ArrayResize für einen neuen Eintrag. Wir setzen „leftShoulderTime“ auf „lsTime“ und „leftShoulderPrice“ auf „lsPrice“. Wir protokollieren die Addition mit der Funktion Print, der Funktion TimeToString und der Funktion DoubleToString. Nach der Kompilierung erhalten wir folgendes Ergebnis.

HANDELSEINSTELLUNG

Anhand des Bildes können wir sehen, dass wir diese Einstellung nicht nur visualisieren, sondern auch entsprechend handeln. Die Mustererkennung des umgekehrten Kopf-Schulter, die Visualisierung und die Handelsoperationen verwenden dieselbe Logik, allerdings in umgekehrter Weise. Hier ist die Logik.

// Inverse Head and Shoulders (Buy)
if (DetectInverseHeadAndShoulders(extrema, leftShoulderIdx, headIdx, rightShoulderIdx, necklineStartIdx, necklineEndIdx)) { //--- Check for inverse H&S pattern
   double closePrice = iClose(_Symbol, _Period, 1);                   //--- Get the closing price of the previous bar
   double necklinePrice = extrema[necklineEndIdx].price;              //--- Get the price of the neckline end point

   if (closePrice > necklinePrice) {                                  //--- Check if price has broken above the neckline (buy signal)
      datetime lsTime = extrema[leftShoulderIdx].time;                //--- Get the timestamp of the left shoulder
      double lsPrice = extrema[leftShoulderIdx].price;                //--- Get the price of the left shoulder

      if (IsPatternTraded(lsTime, lsPrice)) return;                   //--- Exit if this pattern has already been traded

      datetime breakoutTime = iTime(_Symbol, _Period, 1);             //--- Get the timestamp of the breakout bar (previous bar)
      int lsBar = extrema[leftShoulderIdx].bar;                       //--- Get the bar index of the left shoulder
      int headBar = extrema[headIdx].bar;                             //--- Get the bar index of the head
      int rsBar = extrema[rightShoulderIdx].bar;                      //--- Get the bar index of the right shoulder
      int necklineStartBar = extrema[necklineStartIdx].bar;           //--- Get the bar index of the neckline start
      int necklineEndBar = extrema[necklineEndIdx].bar;               //--- Get the bar index of the neckline end
      int breakoutBar = 1;                                            //--- Set breakout bar index (previous bar)

      int lsToHead = lsBar - headBar;                                 //--- Calculate number of bars from left shoulder to head
      int headToRs = headBar - rsBar;                                 //--- Calculate number of bars from head to right shoulder
      int rsToBreakout = rsBar - breakoutBar;                         //--- Calculate number of bars from right shoulder to breakout
      int lsToNeckStart = lsBar - necklineStartBar;                   //--- Calculate number of bars from left shoulder to neckline start
      double avgPatternRange = (lsToHead + headToRs) / 2.0;           //--- Calculate average bar range of the pattern for uniformity check

      if (rsToBreakout > avgPatternRange * RightShoulderBreakoutMultiplier) { //--- Check if breakout distance exceeds allowed range
         Print("Pattern rejected: Right Shoulder to Breakout (", rsToBreakout, 
               ") exceeds ", RightShoulderBreakoutMultiplier, "x average range (", avgPatternRange, ")"); //--- Log rejection due to excessive breakout range
         return;                                                      //--- Exit function if pattern is invalid
      }

      double necklineStartPrice = extrema[necklineStartIdx].price;    //--- Get the price of the neckline start point
      double necklineEndPrice = extrema[necklineEndIdx].price;        //--- Get the price of the neckline end point
      datetime necklineStartTime = extrema[necklineStartIdx].time;    //--- Get the timestamp of the neckline start point
      datetime necklineEndTime = extrema[necklineEndIdx].time;        //--- Get the timestamp of the neckline end point
      int barDiff = necklineStartBar - necklineEndBar;                //--- Calculate bar difference between neckline points for slope
      double slope = (necklineEndPrice - necklineStartPrice) / barDiff; //--- Calculate the slope of the neckline (price change per bar)
      double breakoutNecklinePrice = necklineStartPrice + slope * (necklineStartBar - breakoutBar); //--- Calculate neckline price at breakout point

      // Extend neckline backwards
      int extendedBar = necklineStartBar;                             //--- Initialize extended bar index with neckline start
      datetime extendedNecklineStartTime = necklineStartTime;         //--- Initialize extended neckline start time
      double extendedNecklineStartPrice = necklineStartPrice;         //--- Initialize extended neckline start price
      bool foundCrossing = false;                                     //--- Flag to track if neckline crosses a bar within range

      for (int i = necklineStartBar + 1; i < Bars(_Symbol, _Period); i++) { //--- Loop through bars to extend neckline backwards
         double checkPrice = necklineStartPrice - slope * (i - necklineStartBar); //--- Calculate projected neckline price at bar i
         if (NecklineCrossesBar(checkPrice, i)) {                     //--- Check if neckline intersects the bar's high-low range
            int distance = i - necklineStartBar;                      //--- Calculate distance from neckline start to crossing bar
            if (distance <= avgPatternRange * RightShoulderBreakoutMultiplier) { //--- Check if crossing is within uniformity range
               extendedBar = i;                                       //--- Update extended bar index
               extendedNecklineStartTime = iTime(_Symbol, _Period, i); //--- Update extended neckline start time
               extendedNecklineStartPrice = checkPrice;              //--- Update extended neckline start price
               foundCrossing = true;                                  //--- Set flag to indicate crossing found
               Print("Neckline extended to first crossing bar within uniformity: Bar ", extendedBar); //--- Log successful extension
               break;                                                 //--- Exit loop after finding valid crossing
            } else {                                                  //--- If crossing exceeds uniformity range
               Print("Crossing bar ", i, " exceeds uniformity (", distance, " > ", avgPatternRange * RightShoulderBreakoutMultiplier, ")"); //--- Log rejection of crossing
               break;                                                 //--- Exit loop as crossing is too far
            }
         }
      }

      if (!foundCrossing) {                                           //--- If no valid crossing found within range
         int barsToExtend = 2 * lsToNeckStart;                        //--- Set fallback extension distance as twice LS to neckline start
         extendedBar = necklineStartBar + barsToExtend;               //--- Calculate extended bar index
         if (extendedBar >= Bars(_Symbol, _Period)) extendedBar = Bars(_Symbol, _Period) - 1; //--- Cap extended bar at total bars if exceeded
         extendedNecklineStartTime = iTime(_Symbol, _Period, extendedBar); //--- Update extended neckline start time
         extendedNecklineStartPrice = necklineStartPrice - slope * (extendedBar - necklineStartBar); //--- Update extended neckline start price
         Print("Neckline extended to fallback (2x LS to Neckline Start): Bar ", extendedBar, " (no crossing within uniformity)"); //--- Log fallback extension
      }

      Print("Inverse Head and Shoulders Detected:");                  //--- Log detection of inverse H&S pattern
      Print("Left Shoulder: Bar ", lsBar, ", Time ", TimeToString(lsTime), ", Price ", DoubleToString(lsPrice, _Digits)); //--- Log left shoulder details
      Print("Head: Bar ", headBar, ", Time ", TimeToString(extrema[headIdx].time), ", Price ", DoubleToString(extrema[headIdx].price, _Digits)); //--- Log head details
      Print("Right Shoulder: Bar ", rsBar, ", Time ", TimeToString(extrema[rightShoulderIdx].time), ", Price ", DoubleToString(extrema[rightShoulderIdx].price, _Digits)); //--- Log right shoulder details
      Print("Neckline Start: Bar ", necklineStartBar, ", Time ", TimeToString(necklineStartTime), ", Price ", DoubleToString(necklineStartPrice, _Digits)); //--- Log neckline start details
      Print("Neckline End: Bar ", necklineEndBar, ", Time ", TimeToString(necklineEndTime), ", Price ", DoubleToString(necklineEndPrice, _Digits)); //--- Log neckline end details
      Print("Close Price: ", DoubleToString(closePrice, _Digits));    //--- Log closing price at breakout
      Print("Breakout Time: ", TimeToString(breakoutTime));           //--- Log breakout timestamp
      Print("Neckline Price at Breakout: ", DoubleToString(breakoutNecklinePrice, _Digits)); //--- Log neckline price at breakout
      Print("Extended Neckline Start: Bar ", extendedBar, ", Time ", TimeToString(extendedNecklineStartTime), ", Price ", DoubleToString(extendedNecklineStartPrice, _Digits)); //--- Log extended neckline start details
      Print("Bar Ranges: LS to Head = ", lsToHead, ", Head to RS = ", headToRs, ", RS to Breakout = ", rsToBreakout, ", LS to Neckline Start = ", lsToNeckStart); //--- Log bar ranges for pattern analysis

      string prefix = "IHS_" + TimeToString(extrema[headIdx].time, TIME_MINUTES); //--- Create unique prefix for chart objects based on head time
      // Lines
      DrawTrendLine(prefix + "_LeftToNeckStart", lsTime, lsPrice, necklineStartTime, necklineStartPrice, clrGreen, 2, STYLE_SOLID); //--- Draw line from left shoulder to neckline start
      DrawTrendLine(prefix + "_NeckStartToHead", necklineStartTime, necklineStartPrice, extrema[headIdx].time, extrema[headIdx].price, clrGreen, 2, STYLE_SOLID); //--- Draw line from neckline start to head
      DrawTrendLine(prefix + "_HeadToNeckEnd", extrema[headIdx].time, extrema[headIdx].price, necklineEndTime, necklineEndPrice, clrGreen, 2, STYLE_SOLID); //--- Draw line from head to neckline end
      DrawTrendLine(prefix + "_NeckEndToRight", necklineEndTime, necklineEndPrice, extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, clrGreen, 2, STYLE_SOLID); //--- Draw line from neckline end to right shoulder
      DrawTrendLine(prefix + "_Neckline", extendedNecklineStartTime, extendedNecklineStartPrice, breakoutTime, breakoutNecklinePrice, clrBlue, 2, STYLE_SOLID); //--- Draw neckline from extended start to breakout
      DrawTrendLine(prefix + "_RightToBreakout", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, breakoutTime, breakoutNecklinePrice, clrGreen, 2, STYLE_SOLID); //--- Draw line from right shoulder to breakout
      DrawTrendLine(prefix + "_ExtendedToLeftShoulder", extendedNecklineStartTime, extendedNecklineStartPrice, lsTime, lsPrice, clrGreen, 2, STYLE_SOLID); //--- Draw line from extended neckline to left shoulder
      // Triangles
      DrawTriangle(prefix + "_LeftShoulderTriangle", lsTime, lsPrice, necklineStartTime, necklineStartPrice, extendedNecklineStartTime, extendedNecklineStartPrice, clrLightGreen); //--- Draw triangle for left shoulder area
      DrawTriangle(prefix + "_HeadTriangle", extrema[headIdx].time, extrema[headIdx].price, necklineStartTime, necklineStartPrice, necklineEndTime, necklineEndPrice, clrLightGreen); //--- Draw triangle for head area
      DrawTriangle(prefix + "_RightShoulderTriangle", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, necklineEndTime, necklineEndPrice, breakoutTime, breakoutNecklinePrice, clrLightGreen); //--- Draw triangle for right shoulder area
      // Text Labels
      DrawText(prefix + "_LS_Label", lsTime, lsPrice, "LS", clrGreen, false); //--- Draw "LS" label below left shoulder
      DrawText(prefix + "_Head_Label", extrema[headIdx].time, extrema[headIdx].price, "HEAD", clrGreen, false); //--- Draw "HEAD" label below head
      DrawText(prefix + "_RS_Label", extrema[rightShoulderIdx].time, extrema[rightShoulderIdx].price, "RS", clrGreen, false); //--- Draw "RS" label below right shoulder
      datetime necklineMidTime = extendedNecklineStartTime + (breakoutTime - extendedNecklineStartTime) / 2; //--- Calculate midpoint time of the neckline
      double necklineMidPrice = extendedNecklineStartPrice + slope * (iBarShift(_Symbol, _Period, extendedNecklineStartTime) - iBarShift(_Symbol, _Period, necklineMidTime)); //--- Calculate midpoint price of the neckline
      // Calculate angle in pixel space
      int x1 = ShiftToX(iBarShift(_Symbol, _Period, extendedNecklineStartTime)); //--- Convert extended neckline start to x-pixel coordinate
      int y1 = PriceToY(extendedNecklineStartPrice);                          //--- Convert extended neckline start price to y-pixel coordinate
      int x2 = ShiftToX(iBarShift(_Symbol, _Period, breakoutTime));           //--- Convert breakout time to x-pixel coordinate
      int y2 = PriceToY(breakoutNecklinePrice);                               //--- Convert breakout price to y-pixel coordinate
      double pixelSlope = (y2 - y1) / (double)(x2 - x1);                     //--- Calculate slope in pixel space (rise over run)
      double necklineAngle = -atan(pixelSlope) * 180 / M_PI;                  //--- Calculate neckline angle in degrees, negated for visual alignment
      Print("Pixel X1: ", x1, ", Y1: ", y1, ", X2: ", x2, ", Y2: ", y2, ", Pixel Slope: ", DoubleToString(pixelSlope, 4), ", Neckline Angle: ", DoubleToString(necklineAngle, 2)); //--- Log pixel coordinates and angle
      DrawText(prefix + "_Neckline_Label", necklineMidTime, necklineMidPrice, "NECKLINE", clrBlue, true, necklineAngle); //--- Draw "NECKLINE" label at midpoint with calculated angle

      double entryPrice = 0;                                                  //--- Set entry price to 0 for market order (uses current price)
      double sl = extrema[rightShoulderIdx].price - BufferPoints * _Point;    //--- Calculate stop-loss below right shoulder with buffer
      double patternHeight = necklinePrice - extrema[headIdx].price;          //--- Calculate pattern height from neckline to head
      double tp = closePrice + patternHeight;                                 //--- Calculate take-profit above close by pattern height
      if (sl < closePrice && tp > closePrice) {                               //--- Validate trade direction (SL below, TP above for buy)
         if (obj_Trade.Buy(LotSize, _Symbol, entryPrice, sl, tp, "Inverse Head and Shoulders")) { //--- Attempt to open a buy trade
            AddTradedPattern(lsTime, lsPrice);                                //--- Add pattern to traded list
            Print("Buy Trade Opened: SL ", DoubleToString(sl, _Digits), ", TP ", DoubleToString(tp, _Digits)); //--- Log successful trade opening
         }
      }
   }
}

Nun gilt es, die eröffneten Positionen durch Anwendung eines Trailing-Stop zu verwalten, um die Gewinne zu maximieren. Wir erstellen eine Funktion, um die Logik des Trailing-Stop wie folgt zu behandeln.

//+------------------------------------------------------------------+
//| Apply trailing stop with minimum profit threshold                |
//+------------------------------------------------------------------+
void ApplyTrailingStop(int minTrailPoints, int trailingPoints, CTrade &trade_object, ulong magicNo = 0) { //--- Function to apply trailing stop to open positions
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);                           //--- Get current bid price
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);                           //--- Get current ask price

   for (int i = PositionsTotal() - 1; i >= 0; i--) {                             //--- Loop through all open positions from last to first
      ulong ticket = PositionGetTicket(i);                                       //--- Retrieve position ticket number
      if (ticket > 0 && PositionSelectByTicket(ticket)) {                        //--- Check if ticket is valid and select the position
         if (PositionGetString(POSITION_SYMBOL) == _Symbol &&                    //--- Verify position is for the current symbol
             (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)) {  //--- Check if magic number matches or no magic filter applied
            double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);           //--- Get position opening price
            double currentSL = PositionGetDouble(POSITION_SL);                   //--- Get current stop-loss price
            double currentProfit = PositionGetDouble(POSITION_PROFIT) / (LotSize * SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE)); //--- Calculate profit in points
            
            if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) {         //--- Check if position is a Buy
               double profitPoints = (bid - openPrice) / _Point;                 //--- Calculate profit in points for Buy position
               if (profitPoints >= minTrailPoints + trailingPoints) {            //--- Check if profit exceeds minimum threshold for trailing
                  double newSL = NormalizeDouble(bid - trailingPoints * _Point, _Digits); //--- Calculate new stop-loss price
                  if (newSL > openPrice && (newSL > currentSL || currentSL == 0)) { //--- Ensure new SL is above open price and better than current SL
                     if (trade_object.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP))) { //--- Attempt to modify position with new SL
                        Print("Trailing Stop Updated: Ticket ", ticket, ", New SL: ", DoubleToString(newSL, _Digits)); //--- Log successful SL update
                     }
                  }
               }
            } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check if position is a Sell
               double profitPoints = (openPrice - ask) / _Point;                 //--- Calculate profit in points for Sell position
               if (profitPoints >= minTrailPoints + trailingPoints) {            //--- Check if profit exceeds minimum threshold for trailing
                  double newSL = NormalizeDouble(ask + trailingPoints * _Point, _Digits); //--- Calculate new stop-loss price
                  if (newSL < openPrice && (newSL < currentSL || currentSL == 0)) { //--- Ensure new SL is below open price and better than current SL
                     if (trade_object.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP))) { //--- Attempt to modify position with new SL
                        Print("Trailing Stop Updated: Ticket ", ticket, ", New SL: ", DoubleToString(newSL, _Digits)); //--- Log successful SL update
                     }
                  }
               }
            }
         }
      }
   }
}

Hier fügen wir mit der Funktion „ApplyTrailingStop“ ein Trailing-Stop hinzu. Sie verwendet „minTrailPoints“ und „trailingPoints“, um offene Positionen anzupassen. Wir rufen die Geld- und Briefkurse mit der Funktion SymbolInfoDouble ab. Wir durchlaufen die Positionen mit der Funktion PositionsTotal. Für jede dieser Positionen wird das „Ticket“ mit der Funktion PositionGetTicket ermittelt und mit der Funktion PositionSelectByTicket ausgewählt. Wir überprüfen das Symbol und „magicNo“ mit den Funktionen „PositionGetString“ und „PositionGetInteger“. Wir rufen „openPrice“, „currentSL“ und „currentProfit“ mit der Funktion PositionGetDouble ab.

Bei einem Kauf berechnen wir den Gewinn mit „bid“ und vergleichen ihn mit „minTrailPoints“ plus „trailingPoints“. Wenn dies der Fall ist, wird mit der Funktion NormalizeDouble eine neue „newSL“ festgelegt und über die Methode „PositionModify“ auf dem Objekt „trade_object“ aktualisiert. Für einen Verkauf verwenden wir stattdessen „ask“ und passen „newSL“ weiter unten an. Die erfolgreiche Preisänderung wird protokolliert. Wir können diese Funktion dann in OnTick aufrufen.

// Apply trailing stop if enabled and positions exist
if (UseTrailingStop && PositionsTotal() > 0) {                        //--- Check if trailing stop is enabled and there are open positions
   ApplyTrailingStop(MinTrailPoints, TrailingPoints, obj_Trade, MagicNumber); //--- Apply trailing stop to positions with specified parameters
}

Der Aufruf der Funktion mit den Eingabeparametern ist alles, was wir brauchen, um sicherzustellen, dass der Trailing-Stop aktiviert ist. Was jetzt noch bleibt, ist das Freigeben der Speicherbereiche, sobald das Programm nicht mehr verwendet wird, und das Löschen der visuellen Objekte, die wir zugeordnet haben. Wir behandeln das in der Ereignisbehandlung durch OnDeinit.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {                                        //--- Expert Advisor deinitialization function
   ArrayFree(tradedPatterns);                                            //--- Free memory used by tradedPatterns array
   ObjectsDeleteAll(0, "HS_");                                           //--- Delete all chart objects with "HS_" prefix (standard H&S)
   ObjectsDeleteAll(0, "IHS_");                                          //--- Delete all chart objects with "IHS_" prefix (inverse H&S)
   ChartRedraw();                                                        //--- Redraw the chart to remove deleted objects
}

In OnDeinit, das ausgeführt wird, wenn der Expert Advisor heruntergefahren wird, bereinigen wir das Programm und den Chart, auf dem er läuft. Wir verwenden die Funktion ArrayFree, um den Speicher von „tradedPatterns“ freizugeben. Dann entfernen wir alle Chartobjekte. Die Funktion ObjectsDeleteAll löscht Elemente mit dem Präfix „HS_“ für Standardmuster. Es löscht auch diejenigen mit dem Präfix „IHS_“ für inverse Muster. Schließlich aktualisieren wir das Chart. Die Funktion ChartRedraw aktualisiert die Anzeige, um diese Änderungen vor dem vollständigen Schließen wiederzugeben. Nach der Kompilierung erhalten wir folgendes Ergebnis.

ENDRESULTAT MIT TRAILING-STOP

Aus dem Bild können wir ersehen, dass wir den Trailing-Stop mit den Einstellungen anwenden und somit unser Ziel erreichen. Bleiben nur noch die Backtests des Programms, und das wird im nächsten Abschnitt behandelt.


Backtests

Nach einem gründlichen Backtest erhalten wir folgende Ergebnisse.

Backtest-Grafik:

Grafik

Bericht des Backtest:

BERICHT


Schlussfolgerung

Zusammenfassend lässt sich sagen, dass wir erfolgreich einen Handelsalgorithmus für Kopf und Schultern in MQL5 entwickelt haben. Es bietet eine präzise Mustererkennung, detaillierte Visualisierung und automatische Handelsausführung für das klassische Umkehrsignal. Durch den Einsatz von Validierungsregeln, dem Zeichnen der Nackenlinie und Trailing Stops passt sich unser Expert Advisor effektiv an Marktveränderungen an. Sie können die erstellten Illustrationen als Sprungbrett nutzen, um sie durch zusätzliche Schritte wie Parameteroptimierung oder erweiterte Risikokontrollen zu erweitern. Beachten Sie auch, dass es sich um ein seltenes Muster handelt.

Haftungsausschluss: Dieser Artikel ist nur für Bildungszwecke gedacht. Der Handel ist mit erheblichen finanziellen Risiken verbunden, und die Marktbedingungen können unvorhersehbar sein. Ordnungsgemäße Backtests und ein Risikomanagement sind vor dem Live-Einsatz unerlässlich.

Auf dieser Grundlage können Sie Ihre Handelsfähigkeiten verfeinern und diesen Algorithmus verbessern. Testen und optimieren Sie weiter für den Erfolg. Viel Glück!

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

Beigefügte Dateien |
Larry Connors‘ Strategien RSI2 Mean-Reversion im Day-Trading Larry Connors‘ Strategien RSI2 Mean-Reversion im Day-Trading
Larry Connors ist ein renommierter Händler und Autor, der vor allem für seine Arbeit im Bereich des quantitativen Handels und für Strategien wie den 2-Perioden-RSI (RSI2) bekannt ist, der dabei hilft, kurzfristig überkaufte und überverkaufte Marktbedingungen zu erkennen. In diesem Artikel werden wir zunächst die Motivation für unsere Forschung erläutern, dann drei von Connors' berühmtesten Strategien in MQL5 nachbilden und sie auf den Intraday-Handel mit dem S&P 500 Index CFD anwenden.
Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 19): ZigZag Analyzer Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 19): ZigZag Analyzer
Jeder, der Preisaktionen handelt, verwendet Trendlinien manuell, um Trends zu bestätigen und potenzielle Wende- oder Fortsetzungsniveaus zu erkennen. In dieser Serie über die Entwicklung eines Preisaktionsanalyse-Toolkits stellen wir ein Tool vor, das sich auf das Zeichnen von schrägen Trendlinien zur einfachen Marktanalyse konzentriert. Dieses Tool vereinfacht den Prozess für Händler, indem es die wichtigsten Trends und Niveaus, die für eine wirksame Bewertung der Preisaktionen unerlässlich sind, klar umreißt.
Meistern der Log-Einträge (Teil 6): Speichern von Protokollen in der Datenbank Meistern der Log-Einträge (Teil 6): Speichern von Protokollen in der Datenbank
Dieser Artikel befasst sich mit der Verwendung von Datenbanken zur strukturierten und skalierbaren Speicherung von Protokollen. Es behandelt grundlegende Konzepte, wesentliche Operationen, Konfiguration und Implementierung eines Datenbank-Handlers in MQL5. Schließlich werden die Ergebnisse validiert und die Vorteile dieses Ansatzes für die Optimierung und effiziente Überwachung hervorgehoben.
Automatisieren von Handelsstrategien in MQL5 (Teil 12): Umsetzung der Strategie der Mitigation Order Blocks (MOB) Automatisieren von Handelsstrategien in MQL5 (Teil 12): Umsetzung der Strategie der Mitigation Order Blocks (MOB)
In diesem Artikel bauen wir ein MQL5-Handelssystem auf, das die Orderblock-Erkennung für den Handel des Smart Money automatisiert. Wir skizzieren die Regeln der Strategie, implementieren die Logik in MQL5 und integrieren das Risikomanagement für eine effektive Handelsausführung. Schließlich führen wir Backtests durch, um die Leistung des Systems zu bewerten und es für optimale Ergebnisse zu verfeinern.