English 中文 Español Deutsch 日本語 Português
preview
Автоматизация торговых стратегий на MQL5 (Часть 13): Создание торгового алгоритма для паттерна "Голова и Плечи"

Автоматизация торговых стратегий на MQL5 (Часть 13): Создание торгового алгоритма для паттерна "Голова и Плечи"

MetaTrader 5Трейдинг |
583 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В предыдущей статье (Часть 12) мы реализовали стратегию Смягчения ордер-блоков (Mitigation Order Blocks, MOB) на языке программирования MetaQuotes Language 5 (MQL5) для использования институциональных ценовых зон для торговли. Теперь, в Части 13, мы сосредоточимся на построении торгового алгоритма «Голова-Плечи», автоматизирующего классический разворотный паттерн для точного определения разворотов рынка. В статье рассмотрим следующие темы:

  1. Изучение архитектуры паттерна Голова-Плечи
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

К концу настоящей статьи у вас будет полностью функциональный советник, готовый к торговле по паттерну «Голова-плечи» — давайте погрузимся в работу!


Изучение архитектуры паттерна Голова-Плечи

Паттерн Голова-Плечи это классическое образование в виде графика, широко признанное в техническом анализе для прогнозирования разворотов тренда, проявляющееся как в стандартных (медвежьих), так и в обратных (бычьих) вариациях, каждая из которых определяется уникальной последовательностью ценовых пиков или впадин. В стандартном паттерне, в нашей программе, восходящий тренд сменяется тремя пиками: левое плечо достигает максимума, голова поднимается заметно выше в момент кульминации тренда (значительно превосходя оба плеча), а правое плечо опускается ниже головы, но близко по высоте левому плечу. Все они связаны линией шеи, соединяющей две впадины — как только цена пробьет эту линию, мы откроем медвежью сделку на пробое, установим стоп-лосс выше правого плеча и нацелимся на тейк-профит, спроецировав высоту от головы до линии шеи вниз, как показано на рисунке ниже.

BEARISH HEAD & SHOULDERS PATTERN

Для обратного паттерна нисходящий тренд приведет к трем впадинам: левое плечо обозначит минимум, голова опустится заметно глубже (ниже обоих плеч), а правое плечо остановится вблизи уровня левого. Линия шеи пройдет через пики — пробой цены выше этого уровня вызовет бычий вход со стоп-лоссом ниже правого плеча и тейк-профитом, расширяющимся вверх на расстояние от линии шеи до головы. Все построено на выделяющейся высоте головы и практически симметричности плеч, что является нашими руководящими правилами. Ниже приведена иллюстрация.

BULLISH HEAD & SHOULDERS PATTERN

Что касается управления рисками, мы внедрим дополнительную функцию трейлинг-стопа, чтобы максимально зафиксировать прибыль. Поехали!


Реализация средствами MQL5

Чтобы создать программу наMQL5, откройте  MetaEditor, перейдите в Навигатор, найдите папку «Индикаторы» (Indicators), перейдите на вкладку "Создать" (New) и следуйте инструкциям по созданию файла. Как только это будет сделано, в среде программирования нам нужно будет объявить некоторые  глобальные переменные, которые будем использовать во всей программе.

//+------------------------------------------------------------------+
//|                                  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

Здесь мы начинаем с #include <Trade\Trade.mqh>" и объекта "CTrade", "obj_Trade", чтобы включить дополнительные торговые файлы для управления сделками. Мы устанавливаем такие входные данные, как "LookbackBars" (по умолчанию 50) для исторического анализа, "ThresholdPoints" (по умолчанию 70.0) для подтверждения разворота и "ShoulderTolerancePoints" (по умолчанию 15.0), а также "TroughTolerancePoints" (по умолчанию 30.0) для симметрии. Остальные исходные данные не требуют пояснений. Мы добавили подробные комментарии для удобства понимания. Далее нам нужно определить некоторые структуры, которые мы будем использовать для поиска паттернов и управления рассматриваемыми сделками.

// 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
};

Мы создали две ключевые структуры, используя ключевое слово struct  для управления нашим торговым алгоритмом «Голова-Плечи»: В "Extremum" будут сохраняться пики и впадины с помощью "bar" (индекс), "time" (временная метка), "price" (значение) и "isPeak" (true для пиков, false для впадин), чтобы точно определить компоненты паттерна, в то время как "TradedPattern" будет отслеживать совершенные сделки, используя "leftShoulderTime" и "leftShoulderPrice", чтобы предотвратить дублирование. Чтобы гарантировать, что мы торгуем один раз за бар и отслеживаем текущие сделки, мы объявляем переменную и массив следующим образом.

// 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

После выполнения этих действий у нас выполнены все настройки. Однако, поскольку нам нужно будет отобразить паттерн на графике, следует создать архитектуру графика и компоненты баров, чтобы обеспечить его соответствие требованиям паттерна.

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
}

Мы подготавливаем и оснащаем программу визуализацией, определяя такие переменные, как "chart_width" и "chart_height", используя функцию ChartGetInteger для размеров графика, "chart_scale" для масштабирования, "chart_first_vis_bar" и "chart_vis_bars" для сведений о барах, а также "chart_prcmin" и "chart_prcmax" с помощью ChartGetDouble для ценового диапазона. Мы используем функцию "BarWidth" с параметром pow для вычисления расстояния между барами из "chart_scale", функцию "ShiftToX" для преобразования индексов баров в x-координаты с помощью "chart_first_vis_bar" и "chart_scale", а функцию "PriceToY" с параметром round для сопоставления цен с y-координатами на основании "chart_height", "chart_prcmax" и "chart_prcmin", что обеспечивает точное отображение паттерна. Теперь настройки завершены. Можем приступить к инициализации программы в обработчике событий OnInit

//+------------------------------------------------------------------+
//| 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
}

В OnInit мы используем метод "SetExpertMagicNumber" для объекта "obj_Trade", чтобы присвоить "MagicNumber" в качестве уникального идентификатора для всех сделок, гарантируя, что позиции нашей программы различимы, и вызываем функцию ArrayResize, чтобы установить нулевой размер массива "tradedPatterns", очищая его от любых предыдущих данных для нового старта. Затем завершаем работу, возвращая INIT_SUCCEEDED для подтверждения успешной настройки, подготавливая советник к эффективному обнаружению паттерна и торговле по нему. Теперь мы можем перейти к обработчику событий OnTick и убедиться, что мы проводим анализ один раз для каждого бара.

//+------------------------------------------------------------------+
//| 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
}

В обработчике событий OnTick, который активируется при каждом обновлении цены, чтобы отслеживать рыночные изменения и реагировать на них, мы используем функцию iTime, чтобы получить "currentBarTime" для последнего бара и сравнить его с "lastBarTime" во избежание повторной обработки, обновляя "lastBarTime" только для новых баров. Затем обновляем визуальные элементы графика, вызывая ChartGetInteger для обновления "chart_width", "chart_height", "chart_scale", "chart_first_vis_bar" и "chart_vis_bars", а также ChartGetDouble для "chart_prcmin" и "chart_prcmax". Также используем функцию PositionsTotal для проверки открытых сделок и досрочного закрытия, если таковые имеются, чтобы предотвратить перекрытие позиций, создавая условия для обнаружения паттернов и торговли. Затем можем определить функцию для нахождения точек экстремума или ключевых точек паттерна.

//+------------------------------------------------------------------+
//| 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
         }
      }
   }
}

Здесь мы точно определяем пики и впадины, которые определяют наш паттерн «голова-плечи», реализуя функцию "FindExtrema", которая анализирует последние "lookback" бары для построения массива "extrema" критических ценовых точек. Начинаем со сброса массива "extrema" с помощью функции ArrayFree, чтобы обеспечить "чистый лист", затем используем функцию "Bars" для получения общего количества доступных баров и ограничиваем "lookback", если он превышает этот предел, гарантируя, что мы останемся в пределах диапазона данных графика. Далее готовим массивы "highs" и "lows" для хранения ценовых данных, установив их в качестве временных рядов с помощью функции ArraySetAsSeries (сначала новые), и заполнения их с помощью CopyHigh и CopyLow, чтобы извлечь максимум и минимум цены за "lookback + 1" баров.

В цикле от самого старого бара к самому новому определяем тренд с помощью "isUpTrend" на основе начального движения цены, затем отслеживаем "lastHigh" или "lastLow" и их "lastExtremumBar". Когда разворот превышает "ThresholdPoints", мы расширяем "extrema" с помощью функции ArrayResize, сохраняем такие элементы, как "bar", "time" (посредством "iTime"), "price" и "isPeak" (true для пиков, false для впадин), а также включаем тренд, что позволяет точно идентифицировать паттерн. Теперь мы можем взять определенные уровни цен и сохранить их для дальнейшего использования.

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

Здесь объявляем массив "extrema" типа "Extremum" для хранения идентифицированных пиков и впадин, в которых будут храниться плечи и голова паттерна. Затем вызываем функцию "FindExtrema", передавая "extrema" и "LookbackBars" в качестве аргументов, чтобы просканировать последние бары "LookbackBars" и заполнить массив ключевыми экстремумами, закладывая основу для распознавания паттернов и последующих торговых решений. Когда мы выводим значения массива с помощью функции ArrayPrint, у нас получается нечто, что отображает нижеприведенную структуру.

STORED PRICE DATA

Это подтверждает, что у нас есть необходимые точки данных. Итак, мы можем перейти к определению компонентов паттерна. Чтобы сделать код модульным, мы используем функции.

//+------------------------------------------------------------------+
//| 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
}

Здесь мы определяем стандартный паттерн с помощью функции "DetectHeadAndShoulders", которая проверяет массив "extrema", чтобы найти действительную последовательность из шести точек: впадина, пик (левое плечо), впадина, пик (голова), впадина и пик (правое плечо), требуется как минимум шесть записей, которые проверяются функцией ArraySize.  Мы перебираем "extrema", начиная с "размера - 6", проверяя структуру паттерна с чередующимися вершинами и впадинами, затем извлекаем цены для впадин "leftShoulder", "head", "rightShoulder" и линии шеи ("trough1", "trough2"); вложенный цикл гарантирует, что голова является самым высоким пиком в пределах диапазона, что определяется с помощью функций MathMax и MathMin,  в то время как расстояния между точками определяются параметрами "MinBarRange" и "MaxBarRange", а равномерность - с помощью "BarRangeMultiplier".

Подтверждаем пробой правого плеча, проверяя функцию iLow на соответствие "ThresholdPoints" через "ValidationBars", и если голова превышает оба плеча и допуски ("ShoulderTolerancePoints", "TroughTolerancePoints") соблюдены, мы присваиваем индексы, такие как "leftShoulderIdx", "headIdx" и "necklineStartIdx", регистрируем диапазоны баров с помощью функции Print для отладки и возвращаем значение true, чтобы сигнализировать об обнаруженном паттерне. В противном случае возвращаем значение false. Используем ту же логику, чтобы найти обратный паттерн.

//+------------------------------------------------------------------+
//| 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
}

Определяем функцию "DetectInverseHeadAndShoulders" для определения обратного паттерна, которая просматривает массив "extrema", чтобы найти последовательность из шести точек — пик, впадина (левое плечо), пик, впадина (голова), пик, впадина (правое плечо) — для которых требуется как минимум шесть записей, подтвержденных функцией ArraySize.  Выполняем перебор от "size - 6" вниз, подтверждая чередование пик-впадина, затем тянем цены на "leftShoulder", "head", "rightShoulder" и пики линии шеи ("peak1", "peak2»). Вложенный цикл проверяет, является ли голова самой низкой впадиной в диапазоне в пять баров вокруг "headBar" с использованием функций MathMax, MathMin и iLow, в то время как "Bars" гарантирует, что мы останемся в пределах графика.

Устанавливаем расстояние между барами с помощью "MinBarRange" и "MaxBarRange", вычисляем равномерность с помощью функции MathMin и "BarRangeMultiplier" и проверяем пробой правого плеча, используя функцию iHigh в соответствии с "ThresholdPoints" по сравнению с "ValidationBars". Если "head" находится ниже обоих плечей, а допуски ("ShoulderTolerancePoints", "TroughTolerancePoints") соблюдены, мы устанавливаем индексы типа "leftShoulderIdx" и "necklineStartIdx", регистрируем диапазоны и возвращаем значение true, в противном случае возвращаем false. Теперь, используя эти 2 функции, мы можем перейти к определению паттернов, как показано ниже.

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

      //---
   }
}

Здесь мы продвигаемся вперед, объявляя переменные "leftShoulderIdx", "headIdx", "rightShoulderIdx", "necklineStartIdx" и "necklineEndIdx" для хранения индексов компонентов паттернов, затем используем функцию "DetectHeadAndShoulders" для проверки массива "extrema" на наличие стандартного паттерна, передавая эти индексы в качестве справочных данных. При обнаружении извлекаем "closePrice" с помощью функции iClose для предыдущего бара и "necklinePrice" из "extrema[necklineEndIdx].price", запуская сигнал на продажу, если "closePrice" опускается ниже "necklinePrice". Затем извлекаем "lsTime" и "lsPrice" из "extrema[leftShoulderIdx]" для подготовки к исполнению сделки на основе положения левого плеча. В этот момент необходимо убедиться, что паттерн не торгуется. Определяем функцию для проверки.

//+------------------------------------------------------------------+
//| 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
}

Здесь мы гарантируем, что наша программа избежит дублирования сделок путем реализации функции "IsPatternTraded", которая проверяет, существует ли паттерн, идентифицируемый как "lsTime" и "lsPrice", в массиве "tradedPatterns". Используем функцию ArraySize для получения "size" массива, а затем проходим по нему циклом, сравнивая каждую запись по параметру "leftShoulderTime" с "lsTime" и "leftShoulderPrice" с "lsPrice" в диапазоне "PriceTolerance" с помощью функции MathAbs. Если совпадение найдено, регистрируем его в логе с помощью функции Print, в том числе TimeToString и DoubleToString для удобства чтения, и возвращаем значение true, в противном случае возвращаем значение false, чтобы разрешить новую сделку. Затем вызываем функцию для выполнения проверки и продолжаем, если ничего не найдено.

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

Здесь мы улучшаем распознавание паттернов, проверяя обнаруженный стандартный паттерн и настраивая сделку на продажу, начиная с функции "IsPatternTraded", чтобы проверить, соответствуют ли "lsTime" и "lsPrice" предыдущей сделке в "tradedPatterns", завершая выполнение в случае значения true, чтобы избежать дублирования. Затем используем функцию iTime, чтобы назначить "breakoutTime" в качестве временной метки предыдущего бара и получить индексы бара, такие как "lsBar", "headBar", "rsBar", "necklineStartBar" и "necklineEndBar", из "extrema", вычисляя диапазоны, такие как "lsToHead", "headToRs" и "rsToBreakout". Если "rsToBreakout" превышает значение "avgPatternRange", умноженное на "RightShoulderBreakoutMultiplier", отклоняем паттерн и регистрируем его с помощью функции Print

Затем определяем "slope" линии шеи, используя "necklineStartPrice" и "necklineEndPrice" относительно "barDiff", вычисляем "breakoutNecklinePrice" и расширяем линию шеи в обратном направлении с помощью цикла, используя функцию "NecklineCrossesBar", чтобы найти пересечение в "avgPatternRange * RightShoulderBreakoutMultiplier", обновляя "extendedBar", "extendedNecklineStartTime" (посредством "iTime") и "extendedNecklineStartPrice". Если пересечение не подходит, возвращаемся к "2 * lsToNeckStart", ограничиваясь общим количеством "Bars", и регистрируем все детали — индексы баров, цены и диапазоны — с помощью функций Print, TimeToString and DoubleToString для тщательного документирования. Фрагмент кода пользовательской функции выглядит так.

//+------------------------------------------------------------------+
//| 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
}

Функция проверяет, пересекает ли "necklinePrice" ценовой диапазон бара в "barIndex", чтобы обеспечить точное расширение линии шеи. Мы используем функцию iHigh,  чтобы получить цену "high" бара, а также функцию "iLow", чтобы получить цену "low". Затем возвращаем значение true, если "necklinePrice" находится между значениями "low" и "high", подтверждая, что линия шеи пересекает диапазон бара для проверки паттерна. Если есть подтверждение правильности паттерна, визуализируем его на графике. Нам понадобятся функции, чтобы нарисовать и отметить его.

//+------------------------------------------------------------------+
//| 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
   }
}

Здесь мы обогащаем программу с помощью средств визуализации, чтобы выделить паттерн на графике, начиная с функции "DrawTrendLine", использующей функцию ObjectCreate для построения линии от "timeStart" и "priceStart" к "timeEnd" и "priceEnd", задавая свойства типа "lineColor", "style" и "width" посредством ObjectSetInteger, отрисовывая её позади баров с помощью OBJPROP_BACK и обновляя дисплей с помощью функции ChartRedraw, в случае необходимости регистрируя ошибки в логе с помощью "Print" и GetLastError

Затем реализуем функцию "DrawTriangle", чтобы заштриховать структуру паттерна, вызывая функцию "ObjectCreate" с тремя точками ("time1", "price1" и т.д.), применяя "fillColor" и сплошную границу с помощью ObjectSetInteger, заполняя ее с помощью OBJPROP_FILL, помещая ее позади графика и обновляя внешний вид с помощью ChartRedraw, снова регистрируя ошибки с помощью функции "Print", если создание завершается неудачей.

Наконец, добавляем функцию "DrawText" для отметки ключевых точек, используя функцию ChartGetInteger для настройки "dynamicFontSize" на основе "chartscale", позиционируя текст по "time" и "price", а также со смещением с помощью "ObjectCreate", настраивая его с помощью "ObjectSetString" - для "text", "ObjectSetInteger" - для "textColor" и "FONTSIZE", а также "ObjectSetDouble" - для "angle", рисуя их на переднем плане с помощью ChartRedraw и подтверждая создание с помощью "Print" и "DoubleToString" или отмечая ошибки. Теперь можем вызывать функции для добавления функции видимости, и первое, что мы делаем, это добавляем строки следующим образом.

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

Здесь мы визуально представляем стандартный паттерн, создавая уникальный префикс на основе временной метки головы с помощью функции TimeToString и рисуя линии тренда с помощью функции "DrawTrendLine", чтобы соединить левое плечо с началом линии шеи, начало линии шеи с головой, голову с концом линии шеи и конец линии шеи с правым плечом красным цветом, шириной 3. В то время, как линия шеи от своего расширенного начала до точки пробоя выделена синим цветом шириной 2, а дополнительные линии соединяют правое плечо с пробоем и расширенную линию шеи обратно с левым плечом красным цветом. Все это в однотонном стиле для отображения паттерна на графике. После компиляции получаем следующий результат.

WITH LINES

Для добавления треугольников используем функцию "DrawTriangle". Технически мы создаем его в области плеч и головы.

// 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

Здесь мы улучшаем визуализацию, используя функцию "DrawTriangle", чтобы заштриховать ключевые области светло-коралловым цветом, формируя треугольник для левого плеча от точки левого плеча до начала линии шеи и начала расширенной линии шеи. Другой треугольник для головы - от точки головы до начала и конца линии шеи, а третий - для правого плеча от точки правого плеча до конца линии шеи и точки пробоя, выделяя структуру паттерна на графике. После компиляции получаем следующий результат.

WITH TRIANGLES

Наконец, нам нужно добавить метки к паттерну, чтобы сделать его полностью визуально привлекательным и наглядным.

// 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

Наконец, мы аннотируем паттерн, используя функцию "DrawText", чтобы поместить красные метки "LS", "HEAD" и "RS" над точками левого плеча, головы и правого плеча в соответствующие моменты времени и цены, улучшая читаемость графика. Затем вычисляем среднюю точку линии шеи, усредняя значения "extendedNecklineStartTime" и "breakoutTime" для "necklineMidTime", а также корректируем "extendedNecklineStartPrice" с учетом "slope" и разницы баров с помощью функции iBarShift для "necklineMidPrice". Чтобы выровнять метку, мы преобразуем время в x-пиксели с помощью функции "ShiftToX", а цены в y-пиксели с помощью функции "PriceToY" в начале линии шеи и точке её пробоя, вычисляем "pixelSlope" и получаем "necklineAngle" в градусах с помощью функции atan и "M_PI", регистрируя их с помощью функции "Print" и функции DoubleToString для верификации.

Затем рисуем синюю метку "NECKLINE" (ЛИНИЯ ШЕИ) в средней точке с помощью функции "DrawText", расположенную ниже и повернутую так, чтобы она соответствовала "necklineAngle", гарантируя, что аннотация повторяет наклон линии шеи. Ниже представлен результат.

FINAL PATTERN OUTCOME

На изображении видно, что паттерн полностью визуализирован. Теперь нам нужно обнаружить его пробой, по сути, расширенную линию, открыть позицию на продажу и изменить диапазон расширения на бар пробоя. Проще простого. Для этого мы используем следующую логику.

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
   }
}

Как только паттерн подтвержден, исполняем сделку на продажу, устанавливая для рыночного ордера значение "entryPrice" равным 0, вычисляя "sl" выше цены правого плеча с помощью "BufferPoints", определяя "patternHeight" как разницу между ценами головы и линии шеи, и устанавливая "tp" ниже "closePrice" с помощью "patternHeight".

Проверяем направление сделки, убедившись, что "sl" находится выше, а "tp" ниже "closePrice", прежде чем использовать функцию "Sell" в "obj_Trade", чтобы открыть сделку, указав "LotSize", "sl", "tp" и комментарий. В случае успешного выполнения вызываем функцию "AddTradedPattern" с помощью "lsTime" и "lsPrice" для регистрации паттерна в логе и используем функцию Print с DoubleToString для записи сведений "sl" и "tp". Фрагмент кода пользовательской функции для пометки паттерна как торгуемого приведен ниже.

//+------------------------------------------------------------------+
//| 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
}

Определяем функцию "AddTradedPattern" для отслеживания торговых настроек. Она использует "lsTime" и "lsPrice" для регистрации деталей левого плеча, поскольку левое плечо не перерисовывается. Проверяем размер "tradedPatterns" с помощью функции ArraySize.  Если он попадает в "MaxTradedPatterns", сдвигаем элементы влево, чтобы удалить самые старые. Изменяем размер "tradedPatterns" с помощью функции "ArrayResize", чтобы уменьшить его. Мы регистрируем это, а затем расширяем "tradedPatterns", используя функцию ArrayResize для новой записи. Устанавливаем "leftShoulderTime" на "lsTime", а "leftShoulderPrice" - на "lsPrice". Регистрируем добавление с помощью функции Print, функции TimeToString и функции DoubleToString.  После компиляции получаем следующий результат.

TRADED SETUP

На изображении видно, что мы не только визуализируем настройки, но и торгуем ими соответствующим образом. Обратное распознавание паттерна «голова-плечи», визуализация и торговые операции используют ту же логику, но в обратном порядке. Вот его логика.

// 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
         }
      }
   }
}

Теперь остается только управлять открытыми позициями, применяя логику трейлинг-стопа для максимизации прибыли. Мы создаем функцию для обработки логики трейлинга, как показано ниже.

//+------------------------------------------------------------------+
//| 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
                     }
                  }
               }
            }
         }
      }
   }
}

Здесь мы добавляем функцию трейлинг-стопа с функцией "ApplyTrailingStop". Для настройки открытых позиций она использует "minTrailPoints" и "trailingPoints" . Мы получаем цены "bid" и "ask" с помощью функции SymbolInfoDouble.  Перебираем позиции, используя функцию PositionsTotal.  Для каждой из них получаем "ticket" с помощью функции PositionGetTicket и выбираем её с помощью функции PositionSelectByTicket.  Мы проверяем символ и "magicNo", используя функции "PositionGetString" и "PositionGetInteger". Извлекаем "openPrice", "currentSL" и "currentProfit" с помощью функции PositionGetDouble

Для покупки мы рассчитываем прибыль с помощью "bid" и сверяем с "minTrailPoints" плюс "trailingPoints". Если условие выполняется, устанавливаем новый "newSL" с помощью функции NormalizeDouble и обновляем его с помощью метода "PositionModify" на объекте "trade_object". Для продажи используем вместо этого "ask" и корректируем "newSL" ниже. Успешное изменение цены регистрируется в логе. Затем можем вызвать эту функцию в обработчике событий OnTick

// 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
}

Вызов функции с входными параметрами - это все, что нам нужно, чтобы обеспечить включение трейлинг-стопа. Теперь, как только программа перестанет использоваться, остается только освободить массивы хранения и удалить отображенные нами визуальные объекты. Обрабатываем это в обработчике событий 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
}

В обработчике событий OnDeinit , который запускается при выключении советника, мы очищаем программу и график, к которому она привязана. Используем функцию ArrayFree, чтобы освободить память от "tradedPatterns". Затем удаляем все объекты графика. Функция ObjectsDeleteAll удаляет элементы с префиксом "HS_" для стандартных паттернов. Она также удаляет паттерны с префиксом "IHS_" для обратных паттернов. Наконец, обновляем график. Функция ChartRedraw обновляет отображение, чтобы отразить эти изменения перед полным закрытием. После компиляции получаем следующий результат.

FINAL OUTCOME WITH TRAILING STOP

На изображении видно, что мы применяем трейлинг-стоп к торговым настройкам, тем самым достигая нашей цели. Осталось провести повторное тестирование программы, и это будет выполнено в следующем разделе.


Тестирование на истории

После тщательного тестирования на истории мы получили следующие результаты.

График тестирования на истории:

График

Отчет о тестировании на истории:

REPORT


Заключение

В заключение, мы успешно разработали торговый алгоритм Голова-Плечи на MQL5. Он обеспечивает точное обнаружение паттернов, детальную визуализацию и автоматическое исполнение сделок по классическому разворотному сигналу. Используя правила проверки, построение графиков линии шеи и трейлинг-стопы, наш советник эффективно адаптируется к изменениям на рынке. Можно использовать иллюстрации, сделанные в качестве отправной точки, чтобы улучшать его дополнительными шагами, такими как настройка параметров или расширенный контроль рисков. Кроме того, обратите внимание, что это редкая настройка паттерна.

Отказ от ответственности: Содержание настоящей статьи предназначено только для целей обучения. Торговля сопряжена со значительным финансовым риском, а рыночные условия могут быть непредсказуемыми. Перед началом использования в реальных условиях необходимы тщательное тестирование на истории и управление рисками.

С помощью этого фундамента вы сможете усовершенствовать свои навыки торговли и улучшать этот алгоритм. Продолжайте тестировать и оптимизировать систему для достижения успеха. Желаем удачи!

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17618

Прикрепленные файлы |
От начального до среднего уровня: Struct (III) От начального до среднего уровня: Struct (III)
В этой статье мы рассмотрим, что такое структурированный код. Многие люди путают структурированный код с организованным кодом, однако между этими двумя понятиями есть разница. Об этом и будет рассказано в этой статье. Несмотря на кажущуюся сложность, которую вы почувствуете при первом знакомстве с этим типом написания кода, я постарался подойти к этому вопросу как можно проще. Но данная статья - лишь первый шаг к чему-то большему.
От начального до среднего уровня: Индикатор (IV) От начального до среднего уровня: Индикатор (IV)
В этой статье мы рассмотрим, как легко создать и внедрить операционную методологию для окрашивания свечей. Данная концепция высоко ценится трейдерами. При реализации такого рода вещей необходимо проявлять осторожность, чтобы бары или свечи сохраняли свой первоначальный вид и не затрудняли чтение свечи за свечой.
Нейросети в трейдинге: Спайково-семантический подход к пространственно-временной идентификации (Основные компоненты) Нейросети в трейдинге: Спайково-семантический подход к пространственно-временной идентификации (Основные компоненты)
В статье мы подробно рассмотрели интеграцию модуля SSAM в блок SEW‑ResNeXt, демонстрируя, как фреймворк S3CE‑Net позволяет эффективно объединять спайковое внимание с остаточными блоками. Такая архитектура обеспечивает точную обработку временных и пространственных потоков данных и высокую стабильность обучения. Модульность и гибкость компонентов упрощают расширение модели и повторное использование проверенных методов.
Возможности Мастера MQL5, которые вам нужно знать (Часть 53): Market Facilitation Index Возможности Мастера MQL5, которые вам нужно знать (Часть 53): Market Facilitation Index
Market Facilitation Index (индекс облегчения рынка) — еще один индикатор Билла Вильямса, предназначенный для измерения эффективности движения цен в сочетании с объемом. Как всегда, мы рассматриваем различные паттерны этого индикатора в рамках класса сигналов Мастера и представляем ряд отчетов по тестам и результаты анализа различных паттернов.