English
preview
Создание индикатора Market Structure Sentinel на языке MQL5

Создание индикатора Market Structure Sentinel на языке MQL5

MetaTrader 5 — Примеры |
58 0
Chukwubuikem Okeke
Chukwubuikem Okeke

Введение

Такие понятия технического анализа, как слом структуры (BOS) и смена характера (CHOCH), становятся всё более популярными среди дискреционных трейдеров, стремящихся интерпретировать намерения рынка на основе ценового движения. Однако, хотя эти понятия относительно легко распознать визуально, их воплощение в виде надёжной логики MQL5 представляет собой серьёзную проблему. Для проведения анализа рыночной структуры недостаточно просто нанести на график максимумы и минимумы. Для этого требуются точное обнаружение свинговых точек, подтверждение сломов структуры, фильтрация ложных сигналов и стабильное поведение в любых рыночных условиях. То, что кажется интуитивно понятным для человеческого глаза, зачастую трудно определить программно с достаточной точностью и эффективностью.

В этой статье эти задачи решаются через создание полнофункционального индикатора Market Structure Sentinel на языке MQL5. Индикатор предназначен для обнаружения и визуализации ключевых структурных событий, таких как слом структуры (BOS) и смена характера (CHOCH), в режиме реального времени, помогая трейдерам лучше понимать намерения рынка, направленный уклон и поведенческие переходы непосредственно по ценовому движению. Помимо отображения сломов структуры на графике, индикатор также включает компактную мини-панель со сводкой текущих рыночных условий. На мини-панели стрелка вверх обозначает бычий тренд, а стрелка вниз — медвежий тренд. На ней отображаются обе стрелки, указывающие на консолидацию (флэт). Чтобы обеспечить удобство работы с графиком и избежать помех, мини-панель можно скрыть или снова отобразить с помощью простых действий с клавиатурой: двойное нажатие клавиши H скрывает мини-панель, а двойное нажатие клавиши S отображает её снова.



Понимание структуры рынка

Структура рынка — одно из старейших и фундаментальных понятий технического анализа. Задолго до появления концепций "умных денег" (SMC) трейдеры полагались на естественное формирование максимумов, минимумов, трендов и разворотов, чтобы определять направление рынка и направленность движения цены. По сути, структура рынка отражает направленное поведение цены, когда она проходит через циклы расширения, отката, продолжения и разворота. Основы структуры рынка восходят к классическим теориям ценового движения, таким как теория Доу, разработанная в конце XIX века Чарльзом Доу, согласно которой рынки движутся трендами, а эти тренды можно определять по формированию более высоких максимумов и более высоких минимумов в бычьих условиях либо более низких максимумов и более низких минимумов в медвежьих условиях. Современные методологии SMC расширили эти принципы, предложив более точные интерпретации институционального поведения через такие концепции, как слом структуры (BOS) и смена характера (CHOCH).

В практической торговле структура рынка служит основой для понимания намерений рынка. Вместо того чтобы полагаться исключительно на запаздывающие индикаторы, трейдеры анализируют реакцию цены вблизи предыдущих максимумов и минимумов, чтобы определить, кто в данный момент доминирует на рынке — покупатели или продавцы. Бычья рыночная структура, как правило, отражает сильное давление со стороны покупателей и последовательное обновление максимумов, тогда как медвежья структура свидетельствует об устойчивом давлении со стороны продавцов и ослаблении спроса.



Идеальная структура рынка

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

В восходящем тренде цена, как правило, формирует последовательность более высоких максимумов (HH) и более высоких минимумов (HL). Более высокий максимум формируется, когда цена пробивает предыдущий свинговый максимум, что свидетельствует о сохраняющейся силе покупателей. Более высокий минимум формируется, когда цена откатывается, но не опускается ниже предыдущего свингового минимума, что свидетельствует о том, что покупатели по-прежнему контролируют рынок. Именно эта повторяющаяся последовательность HH и HL является определяющей чертой бычьей рыночной структуры.

Структура рынка — восходящий тренд

Рис. 1. Структура рынка — восходящий тренд

И наоборот, нисходящий тренд характеризуется более низкими минимумами (LL) и более низкими максимумами (LH). Более низкий минимум формируется, когда цена опускается ниже предыдущего свингового минимума, подтверждая медвежий импульс, а более низкий максимум возникает, когда попытки отката не позволяют превысить предыдущий свинговый максимум. В совокупности эти формации свидетельствуют о сохраняющемся давлении со стороны продавцов и медвежьей направленности рынка.

Структура рынка — нисходящий тренд

Рис. 2. Структура рынка — нисходящий тренд

Когда рынок перестает формировать чёткие последовательности HH/HL или LL/LH, цена часто переходит в фазу консолидации, в которой покупатели и продавцы временно уравновешены.

Каждая последовательность более высоких максимумов (HH), более высоких минимумов (HL), более низких максимумов (LH) и более низких минимумов (LL) вносит свой вклад в формирующуюся картину рыночной структуры. На основе этих формаций естественным образом возникают события слома структуры (BOS) и смены характера (CHOCH), которые служат признаками институциональной активности и дают трейдерам ранние сигналы о возможном продолжении или развороте направления рынка.

Рыночная структура — BOS

Рис. 3. Рыночная структура — BOS

Рыночная структура — CHOCH

Рис. 4. Рыночная структура — CHOCH



Реализация

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

Директивы препроцессора

В начале индикатора мы определяем директивы препроцессора для метаданных, поведения графика, констант, управления с клавиатуры, идентификаторов объектов, предустановок цветов и необходимых библиотек. Это позволяет установить базовую конфигурацию программы.

//+------------------------------------------------------------------+
//|                                    Market Structure Sentinel.mq5 |
//|                                             © 2026, ChukwuBuikem |
//|                             https://www.mql5.com/en/users/bikeen |
//+------------------------------------------------------------------+
#property copyright "© 2026, ChukwuBuikem"
#property link      "https://www.mql5.com/en/users/bikeen"
#property version   "1.00"
#property indicator_chart_window
#property indicator_plots 0
#property strict

#include <ChartObjects\ChartObjectsTxtControls.mqh>
#include <ChartObjects\ChartObjectsLines.mqh>

#define PROG_NAME "Market Structure Sentinel"
//--- Mini Dashboard constants
#define OUTER_PANEL PROG_NAME + "MiniDashboard_OuterPanel"
#define MAIN_HEADER PROG_NAME + "MiniDashboard_Header"
#define SUB_HEADER PROG_NAME + "MiniDashboard_SubHeader"
#define HEARDER_LABEL PROG_NAME + "MiniDashboard_Direction"
#define DIRECTION_LABEL PROG_NAME + "MiniDashboard_Arrow"
//--- Market Structure Constants
#define TRENDLINE PROG_NAME + "_Trendline"
#define TEXT PROG_NAME + "_Text"
//--- Keystroke Constants
#define KEY_H 72
#define KEY_S 83

#define CLR_DARK_NAVY   C'10,20,50'
#define CHART_ID ChartID()

Пользовательское перечисление

Это пользовательское перечисление определяет три возможных состояния рынка — восходящий тренд, нисходящий тренд и флэт, — которые используются во всём индикаторе для стандартизации и упрощения классификации направления тренда.

//--- Custom Enumeration
enum ENUM_TREND
  {
//---
   TREND_UP,//Up trend
   TREND_DOWN,//Down trend
   TREND_RANGE,//Consolidation
  };

Структура данных

Мы определяем легковесную структуру для свинговых точек (время, цена и статус пробоя). Конструктор предоставляет безопасные значения по умолчанию, чтобы обеспечить согласованность управления состоянием.
//--- Data structure
struct st_SwingPoint
  {
   //---
   datetime          time;
   double            price;
   bool              isBroken;
   //--- Constructor
                     st_SwingPoint(): time(LONG_MIN),
                     price(EMPTY_VALUE), isBroken(false) {}

  };

Настраиваемые параметры

Пользовательские входные параметры задают основные поведенческие и визуальные настройки индикатора, позволяя регулировать чувствительность определения свинговых точек с помощью rightLeftBars и настраивать через bosColor и chochColor цвета для визуализации слома структуры (BOS) и смены характера (CHOCH).
//--- Input settings
input int rightLeftBars = 3;        //Pivot strength (bars on each side)
input color bosColor = clrRed;      //Color for BOS
input color chochColor = clrPurple; //Color for CHOCH

Глобальные переменные

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

//--- Global variables
st_SwingPoint swingHigh[2], swingLow[2];
int start = -1;
string marketContext = "";
color contextColor = clrNONE;
CChartObjectRectLabel rectLabel;
CChartObjectLabel label;
CChartObjectText text;
CChartObjectTrend trendLine;
ENUM_TREND currentTrend;

Вспомогательные функции

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

  • Обнаружение новой свечи

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

//+------------------------------------------------------------------+
//|                  New candle detection                            |
//+------------------------------------------------------------------+
bool isNewCandle(const datetime newOpenTime)
  {
//---
   static datetime lastOpenTime = LONG_MIN;
   if(lastOpenTime == LONG_MIN)
     {
      lastOpenTime = newOpenTime;
      return false;
     }
   if(lastOpenTime != newOpenTime)
     {
      lastOpenTime = newOpenTime;
      return true;
     }
   return false;
  }
  • Обнаружение свинговых точек

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

//+------------------------------------------------------------------+
//|                  Swing high detection                            |
//+------------------------------------------------------------------+
bool isSwingHigh(const int index, const double &high[], const double &close[])
  {
//---
   int size = ArraySize(high);
//--- Index boundary validation
   if(index < rightLeftBars)
      return false;
   if(index >= (size - (rightLeftBars + 1)))
      return false;

   for(int w = 1; w <= rightLeftBars && (index - w) >= 1; w++)
     {
      //--- Look right (newer candles)
      if(high[index] < high[index - w])
         return false;
      //--- Look left (older candles)
      if(high[index] < high[index + w])
         return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//|                   Swing low detection                            |
//+------------------------------------------------------------------+
bool isSwingLow(const int index, const double &low[], const double &close[])
  {
//---
   int size = ArraySize(low);
//--- Index boundary validation
   if(index < rightLeftBars)
      return false;
   if(index >= (size - (rightLeftBars + 1)))
      return false;

   for(int w = 1; w <= rightLeftBars && (index - w) >= 1; w++)
     {
      //--- Look right (newer candles)
      if(low[index] > low[index - w])
         return false;
      //--- Look left (older candles)
      if(low[index] > low[index + w])
         return false;
     }
   return true;
  }
  • Механизм определения направления тренда

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

//+------------------------------------------------------------------+
//|                 Trend direction detection                        |
//+------------------------------------------------------------------+
ENUM_TREND getTrendDirection(const st_SwingPoint &high[],
                             const st_SwingPoint &low[])
  {
//--- Most recent pair of swing points
   if(high[0].time > low[1].time && high[1].time > low[1].time)
     {
      //--- Determine trend direction using highs
      return(high[0].price > high[1].price) ?
            TREND_UP : (high[0].price < high[1].price) ? TREND_DOWN : TREND_RANGE;
     }
   if(low[0].time > high[1].time && low[1].time > high[1].time)
     {
      //--- Determine trend direction using highs
      return(low[0].price > low[1].price) ?
            TREND_UP : (low[0].price < low[1].price) ? TREND_DOWN : TREND_RANGE;
     }

   return TREND_RANGE;// Default value
  }

Пояснение:

На практике структура рынка редко бывает столь симметричной, как показано на рисунках 1 и 2. Чтобы оперативно реагировать на развивающееся ценовое движение и сократить структурное запаздывание, в данной реализации используется подход, основанный на попарном анализе свинговых точек. Вместо того чтобы опираться исключительно на свинговые максимумы или свинговые минимумы, алгоритм динамически оценивает самую последнюю действительную пару свинговых точек — в зависимости от того, какая структура в данный момент доминирует во времени. Этот адаптивный подход позволяет более точно учитывать контекст при оценке направления тренда, чем статический анализ, привязанный к одному типу свинговых точек.

  • Обнаружение двойного нажатия клавиши

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

//+------------------------------------------------------------------+
//|           Keystroke double click detection                       |
//+------------------------------------------------------------------+
bool isDoubleClick(ulong &lastClick, ulong thresholdMs)
  {
//---
   ulong now = GetTickCount();
   lastClick = now;

   return (now - lastClick <= thresholdMs);
  }
  • Расчёт нормализованного времени средней свечи
При нанесении текста на график очень важно правильно расположить его, чтобы обеспечить читаемость и избежать наложения графических объектов на активные свечи. Эта функция вычисляет нормализованную среднюю точку между двумя моментами времени свечей, что позволяет размещать текстовые и визуальные элементы в сбалансированных областях графика, точно совмещая их с существующей свечой на текущем таймфрейме.
//+------------------------------------------------------------------+
//|             Normalized middle time detection                     |
//+------------------------------------------------------------------+
datetime getMiddleCandleTime(const datetime time1, const datetime time2)
  {
//---
   datetime rawMiddleTime = (time1 + time2) / 2;

   int nearestBar = iBarShift(_Symbol, PERIOD_CURRENT, rawMiddleTime, false);

   if(nearestBar < 0)
      return rawMiddleTime;

   return iTime(_Symbol, PERIOD_CURRENT, nearestBar);// Normalized value
  }
  • Утилиты визуализации графика

Эффективные технические индикаторы зависят не только от точности анализа, но и от чёткого и структурированного визуального представления. Для повышения читаемости графика и обеспечения интуитивной интерпретации рыночной ситуации следующие служебные функции управляют созданием графических объектов, таких как линии тренда, текстовые аннотации, метки, стрелки направления и элементы интерфейса. Эти вспомогательные средства визуализации обеспечивают единообразное отображение результатов анализа на графике, сохраняя при этом упорядоченное расположение элементов, настраиваемый стиль и минимальное перекрытие ценового движения.
//+------------------------------------------------------------------+
//|                    Trendline creation                            |
//+------------------------------------------------------------------+
void drawTrendline(const string objName, const datetime time1,
                   const double price1, const datetime time2,
                   const double price2, const color clr,
                   const int width, const string tooltip)
  {
//---
   if(trendLine.Create(CHART_ID, objName, 0, time1, price1, time2, price2))
     {
      trendLine.Color(clr);
      trendLine.Width(width);
      trendLine.Tooltip(tooltip);
      trendLine.SetInteger(OBJPROP_HIDDEN, true);
     }
  }
//+------------------------------------------------------------------+
//|                      Text creation                               |
//+------------------------------------------------------------------+
void createText(const string objName, const datetime time, const double price, const color clr,
                const string display, const int fontSize = 10, const string font = "Arial")
  {
//---
   if(text.Create(CHART_ID, objName, 0, time, price))
     {
      text.Color(clr);
      text.Font(font);
      text.FontSize(fontSize);
      text.Tooltip(display);
      text.SetString(OBJPROP_TEXT, display);
      text.SetInteger(OBJPROP_HIDDEN, true);
     }
  }
//+------------------------------------------------------------------+
//|        Function to create rectangle labels                       |
//+------------------------------------------------------------------+
bool createRectLabel(const string objName, const int xDistance, const int yDistance,
                     const int xSize, const int ySize, const color clr, int borderWidth,
                     const color borderColor  = clrNONE, const ENUM_BORDER_TYPE borderType  = BORDER_FLAT,
                     const ENUM_LINE_STYLE  borderStyle = STYLE_SOLID)
  {
//---
   if(rectLabel.Create(CHART_ID, objName, 0, 0, 0, 0, 0))
     {
      rectLabel.X_Distance(xDistance);
      rectLabel.Y_Distance(yDistance);
      rectLabel.X_Size(xSize);
      rectLabel.Y_Size(ySize);
      rectLabel.BackColor(clr);
      rectLabel.SetInteger(OBJPROP_BORDER_COLOR, borderColor);
      rectLabel.SetInteger(OBJPROP_WIDTH, borderWidth);
      rectLabel.BorderType(borderType);
      rectLabel.Style(borderStyle);
      rectLabel.Corner(CORNER_RIGHT_UPPER);
      rectLabel.Tooltip("\n");
      rectLabel.SetInteger(OBJPROP_HIDDEN, true);
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//|        Function to create  labels                                |
//+------------------------------------------------------------------+
bool createLabel(const string objName, const int xDistance, const int yDistance,
                 const color clr, const string display, const int fontSize = 15,
                 const string font = "Arial", const string tooltip = "\n")
  {
//---
   if(label.Create(CHART_ID, objName, 0, 0, 0))
     {
      label.X_Distance(xDistance);
      label.Y_Distance(yDistance);
      label.Color(clr);
      label.Tooltip(tooltip);
      label.SetString(OBJPROP_TEXT, display);
      label.FontSize(fontSize);
      label.Font(font);
      label.SetInteger(OBJPROP_HIDDEN, true);
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//|             Trend direction arrows creation                      |
//+------------------------------------------------------------------+
void createDirectionalArrows(const ENUM_TREND trendDirection)
  {
//---
   string upArrow   = DIRECTION_LABEL + "_UP";
   string downArrow = DIRECTION_LABEL + "_DOWN";
//--- Create both arrows, then set color based on current market direction
   createLabel(upArrow, 710, 70, clrNONE, "▲", 30);
   createLabel(downArrow, 750, 70, clrNONE, "▼", 30);
//--- Up trend
   if(trendDirection == TREND_UP)
     {
      label.Attach(CHART_ID, upArrow, 0, 0);
      label.Tooltip("Up trend");
      label.Color(clrLimeGreen);
      return;
     }
//--- Down trend
   if(trendDirection == TREND_DOWN)
     {
      label.Attach(CHART_ID, downArrow, 0, 0);
      label.Tooltip("Down trend");
      label.Color(clrRed);
      return;
     }
//--- Ranging market
   label.Attach(CHART_ID, upArrow, 0, 0);
   label.Tooltip("Consolidation");
   label.Color(clrLimeGreen);
   label.Attach(CHART_ID, downArrow, 0, 0);
   label.Tooltip("Consolidation");
   label.Color(clrRed);
  }
  • Интерфейс мини-панели

Функции интерфейса отвечают за динамическое отображение и удаление мини-панели, позволяя пользователям показывать или скрывать её в зависимости от личных предпочтений или требований анализа. Эта мини-панель объединяет ключевую информацию, такую как направление тренда и визуальные элементы статуса, в единую структурированную панель, повышая читаемость без загромождения основного графика.

//+------------------------------------------------------------------+
//|                     Mini dashboard display                       |
//+------------------------------------------------------------------+
void showMiniDashboard(const ENUM_TREND trendDirection)
  {
//---
   createRectLabel(OUTER_PANEL, 300, 20, 250, 130, CLR_DARK_NAVY, 3, CLR_DARK_NAVY, BORDER_FLAT, STYLE_DASHDOTDOT);
   createLabel(MAIN_HEADER, 595, 27, clrWhite, "Market Structure Sentinel", 13);
   createLabel(SUB_HEADER, 600, 80, clrGold, "Trend: ", 20);
   createDirectionalArrows(trendDirection);
   ChartRedraw(CHART_ID);
  }
//+------------------------------------------------------------------+
//|                  Hide mini dashboard                             |
//+------------------------------------------------------------------+
void hideMiniDashboard()
  {
//---
   ObjectsDeleteAll(CHART_ID, PROG_NAME + "MiniDashboard");
   ChartRedraw(CHART_ID);
  }

  • Извлечение исторических свинговых точек

В ходе инициализации подпрограммы анализируют исторические данные, чтобы найти последние свинговые максимумы и минимумы, определить преобладающий тренд и обнаружить недавние события BOS/CHOCH, уже отмеченные на графике. Это гарантирует, что индикатор начнёт работу с полностью синхронизированным структурным контекстом, а не будет ждать появления новых рыночных формаций в реальном времени.

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

//+------------------------------------------------------------------+
//|       Detect swing high within MqlRates array[]                  |
//+------------------------------------------------------------------+
bool isRatesSwingHigh(const int index, const MqlRates & rates[])
  {
//---
   int size = ArraySize(rates);
//--- Index boundary validation
   if(index < rightLeftBars)
      return false;
   if(index >= size - (rightLeftBars + 1))
      return false;

   for(int w = 1; w <= rightLeftBars; w++)
     {
      if(index - w < 1)
         return false;
      //--- Look right (newer candles)
      if(rates[index].high < rates[index - w].high)
         return false;
      //--- Look left (older candles)
      if(rates[index].high < rates[index + w].high)
         return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//|       Detect swing low within MqlRates array[]                   |
//+------------------------------------------------------------------+
bool isRatesSwingLow(const int index, const MqlRates & rates[])
  {
//---
   int size = ArraySize(rates);
//--- Index boundary validation
   if(index < rightLeftBars)
      return false;
   if(index >= size - (rightLeftBars + 1))
      return false;

   for(int w = 1; w <= rightLeftBars; w++)
     {
      if(index - w < 1)
         return false;
      //--- Look right (newer candles)
      if(rates[index].low > rates[index - w].low)
         return false;
      //--- Look left (older candles)
      if(rates[index].low > rates[index + w].low)
         return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//|                  Initial structure state                         |
//+------------------------------------------------------------------+
void initialMarketStructure(const MqlRates &rates[])
  {
//---
   int lowCount = 0, highCount = 0;
//--- Detect last two swing highs and lows
   for(int w = rightLeftBars; w < ArraySize(rates) - rightLeftBars; w++)
     {
      //--- Detect swing low
      if(lowCount < 2 && isRatesSwingLow(w, rates))
        {
         //--- Save properties
         swingLow[lowCount].price = rates[w].low;
         swingLow[lowCount].time = rates[w].time;
         lowCount++;
        }
      //--- Detect swing high
      if(highCount < 2 && isRatesSwingHigh(w, rates))
        {
         //--- Save properties
         swingHigh[highCount].price = rates[w].high;
         swingHigh[highCount].time = rates[w].time;
         highCount++;
        }
      //--- Exit early when both buffers are filled
      if(lowCount >= 2 && highCount >= 2)
         break;
     }
//--- Determine trend direction using highs
   currentTrend = getTrendDirection(swingHigh, swingLow);
//--- Check break of recent swing high
   for(int w = iBarShift(_Symbol, PERIOD_CURRENT, swingHigh[0].time) - 1; w >= 0 && !swingHigh[0].isBroken; w--)
     {
      if(rates[w].close > swingHigh[0].price)
        {
         swingHigh[0].isBroken = true;
         marketContext = (currentTrend == TREND_UP) ? "BOS" : "CHOCH";
         contextColor = (marketContext == "BOS") ? bosColor : chochColor;
         drawTrendline(TRENDLINE, swingHigh[0].time, swingHigh[0].price,
                       rates[w].time, swingHigh[0].price, contextColor, 2, marketContext);
         createText(TEXT, getMiddleCandleTime(swingHigh[0].time, rates[w].time) - PeriodSeconds(),
                    swingHigh[0].price + (30 * _Point), contextColor, marketContext);
         ChartRedraw(CHART_ID);
        }
     }
//--- Check break of recent swing low
   for(int w = iBarShift(_Symbol, PERIOD_CURRENT, swingLow[0].time) - 1; w >= 0 && !swingLow[0].isBroken; w--)
     {
      if(rates[w].close < swingLow[0].price)
        {
         swingLow[0].isBroken = true;
         marketContext = (currentTrend == TREND_DOWN) ? "BOS" : "CHOCH";
         contextColor = (marketContext == "BOS") ? bosColor : chochColor;
         drawTrendline(TRENDLINE, swingLow[0].time, swingLow[0].price,
                       rates[w].time, swingLow[0].price, contextColor, 2, marketContext);
         createText(TEXT, getMiddleCandleTime(swingLow[0].time, rates[w].time) - PeriodSeconds(),
                    swingLow[0].price - (10 * _Point), contextColor, marketContext);
         ChartRedraw(CHART_ID);
        }
     }
  }
Примечание: MqlRates используется намеренно для повышения эффективности инициализации. В функции OnInit() обработка одного структурированного массива цен выполняется быстрее и с меньшим потреблением памяти, чем выполнение нескольких вызовов таймсерийных функций, таких как CopyOpen(), CopyHigh(), CopyLow() и CopyTime().

Логика инициализации (OnInit)

Функция OnInit() проверяет входные данные, ожидает синхронизации данных, загружает исторические данные в массив MqlRates, инициализирует контекст свингов, а затем отображает мини-панель.
//+------------------------------------------------------------------+
//|                Initialization function                           |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(rightLeftBars <= 1)
     {
      Print("[INIT]: Pivot strength (bars) input must be > 1");
      return INIT_PARAMETERS_INCORRECT;
     }
   while(!SeriesInfoInteger(_Symbol, PERIOD_CURRENT, SERIES_SYNCHRONIZED) && !IsStopped())
     {
      Sleep(100);
     }
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   if(CopyRates(_Symbol, PERIOD_CURRENT, 1, iBars(_Symbol, PERIOD_CURRENT), rates) > 0)
     {
      initialMarketStructure(rates);
     }
   showMiniDashboard(currentTrend);
   return(INIT_SUCCEEDED);
  }

Очистка (OnDeinit)

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

//+------------------------------------------------------------------+
//|              Cleanup function                                    |
//+------------------------------------------------------------------+
void OnDeinit(const int32_t reason)
  {
//---
   ObjectsDeleteAll(CHART_ID, PROG_NAME);
   hideMiniDashboard();
   ChartRedraw(CHART_ID);
  }

Интерактивная система переключения

Для повышения удобства использования и уменьшения загромождённости графика в индикаторе реализована событийно-ориентированная система управления мини-панелью с помощью двойного нажатия клавиши. Обработчик OnChartEvent отслеживает определённые события нажатия клавиш и использует механизм распознавания двойного нажатия клавиши, чтобы динамически отображать или скрывать мини-панель по запросу пользователя.
//+------------------------------------------------------------------+
//|                 Interactive toggle system                        |
//+------------------------------------------------------------------+
void OnChartEvent(const int32_t id, const long& lparam, const double& dparam, const string& sparam)
  {
//---
   int key = (int)lparam;
//--- Accept only "S" and "H" presses
   if(id != CHARTEVENT_KEYDOWN || (key != KEY_H && key != KEY_S))
      return;
   static ulong lastClick = 0;

   switch(key)
     {
      case KEY_S:
         //--- Double-click is set to happen under 500 milliseconds (ms)
         if(isDoubleClick(lastClick, 500))
            showMiniDashboard(currentTrend);
         break;
      case KEY_H:
         //--- Double-click is set to happen under 500 milliseconds (ms)
         if(isDoubleClick(lastClick, 500))
            hideMiniDashboard();
         break;
     }
  }

Основной вычислительный механизм (OnCalculate)

Ключевая логика индикатора находится в функции OnCalculate(), где выполняется вся обработка рыночной структуры в режиме реального времени. В отличие от реализаций, которые на каждом тике проходят по историческим барам, этот механизм работает только на новых барах и не перебирает ценовой ряд. Такая событийно-ориентированная архитектура значительно повышает эффективность выполнения, ограничивая вычисления только вновь завершёнными свечами и сокращая ненужные накладные расходы на обработку в условиях реального рынка.
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime & time[],
                const double & open[],
                const double & high[],
                const double & low[],
                const double & close[],
                const long & tick_volume[],
                const long & volume[],
                const int32_t &spread[])
  {
//---
   if(isNewCandle(time[rates_total - 1]))
     {
      ArraySetAsSeries(open, true);
      ArraySetAsSeries(high, true);
      ArraySetAsSeries(low, true);
      ArraySetAsSeries(close, true);
      ArraySetAsSeries(time, true);
      //--- Detect break of swing high
      if(!swingHigh[0].isBroken && swingHigh[0].price != EMPTY_VALUE && close[1] > swingHigh[0].price)
        {
         //--- Break of swing high
         swingHigh[0].isBroken = true;
         marketContext = (currentTrend == TREND_UP) ? "BOS" : "CHOCH";
         contextColor = (marketContext == "BOS") ? bosColor : chochColor;
         drawTrendline(TRENDLINE, swingHigh[0].time, swingHigh[0].price,
                       time[1], swingHigh[0].price, contextColor, 2, marketContext);
         createText(TEXT, getMiddleCandleTime(swingHigh[0].time, time[1]) - PeriodSeconds(),
                    swingHigh[0].price + (30 * _Point), contextColor, marketContext);
         ChartRedraw(CHART_ID);
         return rates_total;
        }
      //--- Detect break of swing low
      if(!swingLow[0].isBroken && swingLow[0].price != EMPTY_VALUE && close[1] < swingLow[0].price)
        {
         //--- Break of swing low
         swingLow[0].isBroken = true;
         marketContext = (currentTrend == TREND_UP) ? "CHOCH" : "BOS";
         contextColor = (marketContext == "BOS") ? bosColor : chochColor;
         drawTrendline(TRENDLINE, swingLow[0].time, swingLow[0].price,
                       time[1], swingLow[0].price, contextColor, 2, marketContext);
         createText(TEXT, getMiddleCandleTime(swingLow[0].time, time[1]) - PeriodSeconds(),
                    swingLow[0].price - (10 * _Point), contextColor, marketContext);
         ChartRedraw(CHART_ID);
         return rates_total;
        }
      if(rates_total < (rightLeftBars + 1) * 2)
         return rates_total;
      //--- Detect swing high
      if(isSwingHigh(rightLeftBars + 1, high, close) && swingHigh[0].time != time[rightLeftBars + 1])
        {
         //--- Update swing high array
         swingHigh[1] = swingHigh[0];
         swingHigh[0].time = time[rightLeftBars + 1];
         swingHigh[0].price = high[rightLeftBars + 1];
         swingHigh[0].isBroken = false;
         //--- Update trend direction
         currentTrend = getTrendDirection(swingHigh, swingLow);
         //--- Display mini dashboard with latest trend direction
         showMiniDashboard(currentTrend);
         return rates_total;
        }
      //--- Detect swing low
      if(isSwingLow(rightLeftBars + 1, low, close) && swingLow[0].time != time[rightLeftBars + 1])
        {
         //--- Update swing low array
         swingLow[1] = swingLow[0];
         swingLow[0].time = time[rightLeftBars + 1];
         swingLow[0].price = low[rightLeftBars + 1];
         swingLow[0].isBroken = false;
         //--- Update trend direction
         currentTrend = getTrendDirection(swingHigh, swingLow);
         //--- Display mini dashboard with latest trend direction
         showMiniDashboard(currentTrend);
         return rates_total;
        }
     }
   return(rates_total);
  }

Пояснение:

Как только обнаруживается новая свеча, поток выполнения продолжается следующим образом:

  • Обнаружение пробоя

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

  1. Закрытие выше активного свингового максимума подтверждает бычий слом структуры.
  2. Закрытие ниже активного свингового минимума подтверждает медвежий слом структуры.

Пробитый свинг помечается флагом, рыночный контекст классифицируется как слом структуры (BOS) или смена характера (CHOCH), а на графике отображаются соответствующие линия тренда и текстовая аннотация.

Этот этап обрабатывается первым, поскольку сломы структуры дают непосредственный рыночный контекст до начала формирования нового свинга.

  • Проверка данных
Перед началом анализа свингов механизм проверяет, достаточно ли свечей для подтверждения свинга слева и справа. Это предотвращает некорректную индексацию и обеспечивает надёжное подтверждение свингов.

  • Обнаружение свинговых точек

Механизм оценивает, формирует ли недавно завершённая свеча действительный свинговый максимум или свинговый минимум, используя логику сравнения цен слева и справа.

После подтверждения:

  1. Соответственно, предыдущая свинговая точка переносится в хранилище исторических данных.
  2. Новая свинговая точка становится активным структурным ориентиром.
  3. Статус пробоя сбрасывается.
  4. Направление тренда пересчитывается.
  5. Мини-панель обновляется соответствующим образом.

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


Тестирование программы

После успешной компиляции без ошибок индикатор был подключен к рабочему графику для оценки его поведения в режиме реального времени; полученный результат показан ниже.

Индикатор Market Structure Sentinel

Рис. 5. Индикатор Market Structure Sentinel



Заключение

В этой статье мы с нуля разработали полноценную концепцию анализа рыночной структуры, реализовав следующее:

  • Эффективное обнаружение новых свечей для событийно-ориентированного выполнения
  • Определение свинговых точек для структурного анализа рынка
  • Адаптивное определение направления тренда с использованием динамических взаимосвязей свингов
  • Извлечение исторической структуры и обнаружение структурных пробоев для анализа слома структуры (BOS) и смены характера (CHOCH)
  • Утилиты визуализации в реальном времени, нормализованное позиционирование текста, интерактивные элементы управления информационной панелью и оптимизированные процедуры инициализации с использованием MqlRates для повышения эффективности выполнения

Объединив эти компоненты, мы получили полнофункциональный индикатор Market Structure Sentinel, способный предоставлять аналитическую информацию о структуре рынка в режиме реального времени в рамках легковесной и расширяемой архитектуры.

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

Прикрепленные файлы |
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Построение системы обнаружения дивергенций (Часть IV): Создание переиспользуемого модуля обнаружения дивергенций для MQL5 Построение системы обнаружения дивергенций (Часть IV): Создание переиспользуемого модуля обнаружения дивергенций для MQL5
В статье логика обнаружения дивергенций, разработанная в этой серии, выносится в подключаемый файл DivergenceEngine.mqh, который можно повторно использовать в индикаторах и советниках на языке MQL5. В статье подробно описаны архитектура на основе структур, выбор осциллятора (MPO4 или RSI), работа с опорными точками и состоянием, а также минимальный API для доступа к данным. Советник на основе Parabolic SAR демонстрирует подключение модуля: коэффициент ускорения индикатора изменяется в зависимости от обнаруженной дивергенции. Этот пример дает понятную схему, которую можно повторно использовать без дублирования кода.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 5): Правила заливки, отверстия и рамки Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 5): Правила заливки, отверстия и рамки
В пятой части мы добавим в растеризатор правило заливки, а в путь — вспомогательную функцию для построения обводки. Теперь движок поддерживает заливку по правилу ненулевого числа обхода и по правилу "чет-нечет". Выбор задается одним параметром типа перечисления, что позволяет создавать кольца, полноценные рамки, не закрашивающие внутреннюю область, и внутренние просветы глифов. Функция AddThickLine() строит обводки из заливок в рамках одного пути и одного прохода, сохраняя равномерную непрозрачность на стыках. На ее примере показано, почему для таких обводок требуется правило ненулевого числа обхода.