English
preview
Изучение стандартной библиотеки MQL5 (Часть 11): Как создать матричный индикатор структуры рынка в MQL5

Изучение стандартной библиотеки MQL5 (Часть 11): Как создать матричный индикатор структуры рынка в MQL5

MetaTrader 5Трейдинг |
20 0
Clemence Benjamin
Clemence Benjamin

Содержание

  1. Введение
  2. Понимание работы библиотеки matrix
  3. Реализация - два этапа
  4. Тестирование и результаты
  5. Заключение
  6. Вложения


Введение

Для построения надежных моделей стратегий требуется больше, чем просто красивые графические построения: необходимы четкие, проверяемые правила, которые выдерживают рыночный шум и волатильность. Многие традиционные индикаторы либо сильно запаздывают, либо генерируют большое количество ложных сигналов во время быстрых движений. В этой статье мы используем целенаправленный инженерный подход: с помощью заголовка ALGLIB matrix.mqh в MQL5 вычисляется единый, постоянно обновляемый рыночный показатель (Market Score), который объединяет тренд, импульс и волатильность, а затем этот показатель преобразуется в действенные сигналы без перерисовки на основном графике.

Конкретно, наши цели заключаются в следующем:

  • формализировать рыночный показатель, вычисляемый из скользящего окна с помощью CMatrixDouble (тренд как наклон линейной регрессии, импульс как дельта окна, волатильность как стандартное отклонение),
  • предоставить два инструмента для проверки и практического применения — осциллятор с отдельным окном для проверки математических расчетов и индикатор на основном графике, который выдает стрелки на покупку/продажу
  • обеспечить свойства, необходимые для рабочего использования: сигналы, основанные только на закрытых барах (без утечки в будущее), одиночные оповещения на бар, опциональная фильтрация тренда EMA и пауза между сигналами.

Рабочий процесс состоит из двух этапов: На этапе 1 проверяются математические расчеты изолированно; на этапе 2 реализована логика пересечения пороговых значений, фильтрация EMA, пауза MinBarsBetween и всплывающие оповещения, позволяющие компилировать, устанавливать на график и немедленно наблюдать формализованные сигналы, которые можно использовать в автоматизации.

Понимание работы библиотеки matrix

Для разработки сложных торговых алгоритмов необходимо понимать используемые инструменты. Файл matrix.mqh является ключевым компонентом, напрямую перенесенным из известного проекта ALGLIB (созданного Сергеем Бочкановым) и интегрированным в MQL5 компанией MetaQuotes. В отличие от простых массивов, этот файл служит основой для сложного численного анализа, включая линейную алгебру, быстрые преобразования Фурье, численное интегрирование и классификацию данных.

Прежде чем перейти к коду, давайте найдем этот важный заголовочный файл. Откройте MetaEditor (нажмите F4 в MetaTrader 5), затем на панели Navigator разверните папку Include, затем Math, затем Alglib. Внутри есть matrix.mqh вместе с другими компонентами ALGLIB. На анимированном GIF ниже показан этот путь навигации — от начального экрана MetaEditor до файла. Если запомнить этот путь, позже будет проще обратиться к исходникам библиотеки или сослаться на них в своих проектах.

Accessing the matrix library in MetaEditor

Рис. 1. Доступ к библиотеке matrix в MetaEditor

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

Концепция индикатора Smart Market Structure Matrix

Концептуальная основа нашего индикатора Smart Market Structure Matrix базируется на сочетании трех различных рыночных сил: тренда, импульса и волатильности. Вместо того чтобы рассматривать эти элементы по отдельности, мы объединяем их в единый "рыночный показатель".

Используя матричные операции, мы извлекаем скользящее окно последних цен. Тренд рассчитывается как наклон линейной регрессии этого ценового вектора; импульс — это исходная дельта цены за окно; волатильность — это стандартное отклонение окна. Составной показатель вычисляется по формуле:
Показатель = TrendWeight * тренд + MomentumWeight * импульс - VolatilityWeight * волатильность
Положительный показатель указывает на бычью структуру (рост цен с низкой волатильностью), а отрицательный показатель — на медвежью структуру. На этапе 2 мы добавляем триггеры пересечения пороговых значений: сигнал на покупку возникает, когда показатель, находившийся в положительной области, падает ниже отрицательного порога (например, -10), а сигнал на продажу — когда он, находясь ранее в отрицательной области, поднимается выше положительного порога (например, +10). Дополнительная 200-периодная EMA помогает фильтровать сделки в направлении долгосрочного тренда, а всплывающее оповещение уведомляет вас о появлении нового сигнала на последнем баре.


Реализация - два этапа

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

Этап 1: Осциллятор в отдельном окне (базовая версия) — SmartMarketStructureMatrixIndicator_Separate

Первая версия работает в отдельном окне индикатора (#property indicator_separate_window). Она использует класс CMatrixDouble из matrix.mqh для обработки скользящих окон данных о ценах, вычисляет тренд, импульс и волатильность как ненормализованные значения, а затем объединяет их с фиксированными весами в составной показатель. Показатель отображается как в виде линии, так и в виде гистограммы. Простая пороговая логика (показатель > 0 — сигнал на покупку, показатель < 0 — сигнал на продажу) генерирует сигналы, которые отображаются в виде положительных/отрицательных баров гистограммы.

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

1. Объявление свойств и буферов

Каждый индикатор MQL5 начинается с директив #property. Это инструкции времени компиляции, которые указывают терминалу, как обрабатывать индикатор. Директива indicator_separate_window размещает индикатор в отдельном подокне под графиком цены. Директива indicator_buffers определяет, сколько внутренних массивов данных мы будем использовать — здесь нам нужно четыре. Директива indicator_plots определяет, сколько визуальных элементов будет отображаться (два: линия и гистограмма). Директива #include подключает библиотеку matrix, делая доступными все её классы.

Входные параметры отображаются пользователю в диалоговом окне свойств индикатора. Мы выбираем обоснованные значения по умолчанию: окно в 50 баров и веса, сумма которых равна 1 (0,4 + 0,3 + 0,3). Веса отражают тот факт, что тренд немного важнее импульса или волатильности, но трейдер может скорректировать их на основе тестирования. Четыре буфера объявлены как динамические массивы типа double; позже мы связываем их с отображаемыми графическими элементами с помощью SetIndexBuffer.

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

//+------------------------------------------------------------------+
//|                 SmartMarketStructureMatrixIndicator_Separate.mq5 |
//|                                Copyright 2026, Clemence Benjamin |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Clemence Benjamin"
#property link      "https://www.mql5.com"
#property version   "1.00"

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   2

#property indicator_label1  "MarketScore"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue

#property indicator_label2  "Signal"
#property indicator_type2   DRAW_HISTOGRAM
#property indicator_color2  clrSilver

#include <Math/Alglib/matrix.mqh>

//--- inputs
input int    WindowSize       = 50;
input double TrendWeight     = 0.4;
input double MomentumWeight  = 0.3;
input double VolatilityWeight= 0.3;

//--- buffers
double ScoreBuffer[];
double HistBuffer[];
double BuyBuffer[];
double SellBuffer[];

2. Инициализация

//+------------------------------------------------------------------+
//| Initialises indicator buffers                                    |
//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0, ScoreBuffer);
   SetIndexBuffer(1, HistBuffer);
   SetIndexBuffer(2, BuyBuffer);
   SetIndexBuffer(3, SellBuffer);

   PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, WindowSize);

   return(INIT_SUCCEEDED);
  }

3. Основные математические функции с использованием CMatrixDouble

Мы реализуем три вспомогательные функции, которые работают с объектом CMatrixDouble (вектор-столбец цен). CalculateTrend выполняет линейную регрессию с использованием матричных методов. Чтобы понять, почему это работает, линейная регрессия находит наклон (b) в уравнении y = a + b*x, который минимизирует сумму квадратов ошибок. Аналитическое решение использует ковариацию и дисперсию. Вместо написания циклов по массивам мы создаем вторую матрицу x, которая содержит индексы 0, 1, ..., n-1. Далее вычисляем meanX, meanY и сумму произведений. Наконец, наклон равен отношению ковариации(x,y) к дисперсии(x). Это классическое применение матричной арифметики: мы рассматриваем вектор цен как матрицу-столбец и выполняем поэлементные операции.

Функция CalculateMomentum тривиальна: она вычитает первую цену из последней цены в периоде. Функция CalculateVolatility использует встроенный метод Std() класса CMatrixDouble, который эффективно вычисляет стандартное отклонение выборки, используя двухпроходный алгоритм для обеспечения численной устойчивости. Эти три функции разделены, чтобы сделать код модульным и простым в модификации — например, заменить простой импульс процентным изменением или использовать другую меру волатильности, такую​как средний истинный диапазон (ATR).

//+------------------------------------------------------------------+
//| Calculates linear regression slope (trend)                       |
//+------------------------------------------------------------------+
double CalculateTrend(CMatrixDouble &prices)
  {
   int n = prices.Rows();

   CMatrixDouble x(n, 1);
   CMatrixDouble y = prices;

   for(int i = 0; i < n; i++)
      x.Set(i, 0, i);

   double meanX = x.Mean();
   double meanY = y.Mean();

   double num = 0.0;
   double den = 0.0;

   for(int i = 0; i < n; i++)
     {
      double xi = i - meanX;
      double yi = prices.Get(i, 0) - meanY;

      num += xi * yi;
      den += xi * xi;
     }

   if(den == 0.0)
      return(0.0);

   return(num / den);
  }

double CalculateMomentum(CMatrixDouble &prices)
  {
   int n = prices.Rows();
   if(n < 2) return(0.0);
   return(prices.Get(n - 1, 0) - prices.Get(0, 0));
  }

double CalculateVolatility(CMatrixDouble &prices)
  {
   return(prices.Std());
  }

4. Основной цикл вычислений — создание составного показателя

Вся магия происходит в функции OnCalculate. Она получает массив цен (в зависимости от выбранной применяемой цены; по умолчанию — цена закрытия). Сначала мы убеждаемся, что существует достаточное количество баров. Затем мы перебираем циклом от WindowSize до rates_total-1. Для каждой позиции i мы создаём матрицу CMatrixDouble размером WindowSize x 1. Значения заполняются в обратном порядке: мы берём текущий бар и двигаемся назад, чтобы гарантировать, что период заканчивается на текущем баре. Это важно, потому что в техническом анализе индикаторы всегда рассчитываются, используя только прошлые данные — без использования будущих данных.

После вычисления тренда, импульса и волатильности мы объединяем их, используя предоставленные пользователем веса. Обратите внимание, что волатильность вычитается, поскольку более высокая волатильность должна снижать показатель (мы хотим покупать, когда тренд и импульс сильны, а волатильность низка). Полученный показатель сохраняется в ScoreBuffer (синяя линия), а также копируется в HistBuffer (серебряная гистограмма). Наконец, мы записываем положительные значения в BuyBuffer[i], а отрицательные — в SellBuffer[i]. Однако, поскольку существует только одно построение гистограммы, терминал строит гистограмму из HistBuffer. Сигналы на этом этапе элементарны: любой положительный показатель формирует сигнал на покупку, а любой отрицательный — сигнал на продажу. Это, очевидно, слишком упрощенно, но этого достаточно для проверки математической основы.

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

//+------------------------------------------------------------------+
//| Calculates indicator values                                      |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   if(rates_total < WindowSize)
      return(0);

//--- determine start position
   int start;

   if(prev_calculated > WindowSize)
      start = prev_calculated - 1;
   else
      start = WindowSize;

//--- initialize reusable matrix
   CMatrixDouble mat(WindowSize, 1);

//--- main calculation loop
   for(int i = start; i < rates_total; i++)
     {
      //--- build matrix (sliding window)
      for(int j = 0; j < WindowSize; j++)
         mat.Set(j, 0, price[i - j]);

      //--- compute components
      double trend      = CalculateTrend(mat);
      double momentum   = CalculateMomentum(mat);
      double volatility = CalculateVolatility(mat);

      //--- composite score
      double score = TrendWeight * trend +
                     MomentumWeight * momentum -
                     VolatilityWeight * volatility;

//--- store values
      ScoreBuffer[i] = score;
      HistBuffer[i]  = score;

//--- signal buffers
      if(score > 0.0)
        {
         BuyBuffer[i]  = score;
         SellBuffer[i] = EMPTY_VALUE;
        }
      else if(score < 0.0)
        {
         BuyBuffer[i]  = EMPTY_VALUE;
         SellBuffer[i] = score;
        }
      else
        {
         BuyBuffer[i]  = EMPTY_VALUE;
         SellBuffer[i] = EMPTY_VALUE;
        }
     }

   return(rates_total);
  }

Ограничение этапа 1: Значения тренда, импульса и волатильности не нормализованы — их масштабы сильно различаются в зависимости от символа и таймфрейма. Фиксированные веса часто дают показатель, в котором доминирует один компонент. Более того, отдельное окно заставляет трейдера отвлекаться от ценового графика. Этап 2 решает эти проблемы, вводя сигналы пересечения порога непосредственно на ценовом графике, добавляя необязательный фильтр тренда и всплывающие оповещения.

Fig. 2. SmartMarketStructureMatrixIndicator_Separate.

Рис. 2. SmartMarketStructureMatrixIndicator_Separate.

Этап 2: Индикатор для основного окна со стрелками, фильтром EMA и оповещениями — SmartMarketStructureMatrixIndicator_Main

Логичным следующим шагом стало перемещение индикатора непосредственно на график цен (#property indicator_chart_window). Отдельное окно осциллятора заставляет постоянно переключать внимание – взгляд отрывается от свечей, и к тому времени, как вы снова смотрите на них, возможность может быть упущена. Поэтому я переписал индикатор, чтобы стрелки покупки и продажи отображались непосредственно на графике, именно там, где это необходимо. Основная математика, лежащая в основе составного показателя, осталась неизменной, но логика сигнала теперь стала более четкой: стрелка сигнала на покупку появляется, когда показатель, будучи положительным (бычий), падает ниже отрицательного порога (по умолчанию -10). Сигнал на продажу срабатывает, когда показатель, будучи отрицательным (медвежий сигнал), резко поднимается выше положительного порога (по умолчанию +10). Эту идею пересечения с противоположных сторон я использую в реальной торговле уже много лет — она улавливает истощение движения, а не преследует его.

Я также добавил опциональную 200-периодную EMA в качестве фильтра тренда. Это не обязательно — вы можете отключить эту функцию, установив EMAPeriod равным 0, — но при включении сигнал на покупку требует, чтобы цена находилась выше EMA, а сигнал на продажу — чтобы цена находилась ниже неё. Это небольшое дополнительное условие отсеивает многие контртрендовые выбросы. А поскольку я не люблю весь день смотреть в экраны, я встроил всплывающие оповещения с помощью функции Alert(). Когда новый сигнал появляется на самом последнем (еще формирующемся) баре, вы получаете четкое сообщение со временем, ценой и значением показателя. Оповещение срабатывает только один раз за бар — небольшая переменная состояния lastAlertBar предотвращает надоедливые повторные срабатывания, которые являются проблемой плохо написанных индикаторов.

1. Свойства и входные параметры — основной график и стрелочные построения

Первое изменение — это директива #property indicator_chart_window — она переносит индикатор из отдельной панели на сам ценовой график. Объявлены четыре буфера: два для стрелок (покупка и продажа), один для расчета невидимой EMA и один для линии MarketScore. Для стрелок используется тип отрисовки DRAW_ARROW с кодами 233 (вверх) и 234 (вниз), которые являются привычными символами Wingdings, распознаваемыми MetaTrader без дополнительных настроек.

Параметры ввода заслуживают более подробного рассмотрения. WindowSize по умолчанию равен 50 — это скользящее окно для тренда, импульса и волатильности. Меньшее окно реагирует быстрее, но создает больше шума; большее окно реагирует плавнее, но медленнее. Сумма весов равна 1 (тренд 0,4, импульс 0,3, волатильность 0,3). Я немного увеличил значение тренда, потому что, по моему опыту, наклон линейной регрессии говорит больше о направлении в среднесрочной перспективе, чем простая разница в ценах. Но вы можете их скорректировать — некоторые трейдеры предпочитают больший вес импульса для стратегий пробоя.

Пороговые значения необычны: BuyThreshold является отрицательным (-10), SellThreshold - положительным (+10). Это неслучайно. Сигнал на покупку — это не просто "показатель опускается ниже -10" — он должен был быть положительным непосредственно перед этим. Это означает, что рынок рос, а затем внезапно обвалился. Этот обвал часто превращается в резкий отскок. Та же логика для продаж: скачок от отрицательного значения до уровня выше +10 указывает на медвежье истощение. Значение MinBarsBetween (по умолчанию 3) предотвращает отображение стрелок индикатором на каждом баре во время волатильного свинга. EMAPeriod = 200 — это классический долгосрочный фильтр. Его можно полностью отключить, установив значение 0, если предпочитаете получать сырые сигналы показателей.

//+------------------------------------------------------------------+
//|                     SmartMarketStructureMatrixIndicator_Main.mq5 |
//|                                Copyright 2026, Clemence Benjamin |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Clemence Benjamin"
#property description "Main chart with MarketScore line + threshold‑cross arrows + alerts"
#property version   "1.1"
#property indicator_chart_window
#property indicator_buffers 4
#property indicator_plots   3

//--- Plot 1: Buy signals (Wingdings up arrow)
#property indicator_label1  "Buy Signal"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrLimeGreen
#property indicator_width1  2

//--- Plot 2: Sell signals (Wingdings down arrow)
#property indicator_label2  "Sell Signal"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrRed
#property indicator_width2  2

//--- Plot 3: MarketScore line
#property indicator_label3  "MarketScore"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrDodgerBlue
#property indicator_width3  1

#include <Math/Alglib/matrix.mqh>

//--- INPUT PARAMETERS
input int    WindowSize        = 50;       // sliding window size
input double TrendWeight       = 0.4;      // trend weight
input double MomentumWeight    = 0.3;      // momentum weight
input double VolatilityWeight  = 0.3;      // volatility weight (subtracted)
input double BuyThreshold      = -10;      // buy when score crosses below this (must be negative)
input double SellThreshold     = 10;       // sell when score crosses above this (must be positive)
input int    MinBarsBetween    = 3;        // minimum bars after a signal before new signal
input int    EMAPeriod         = 200;      // EMA period for trend filter (0 = disabled)

//--- Buffers
double BuyArrowBuffer[];    // plot 0
double SellArrowBuffer[];   // plot 1
double ScoreBuffer[];       // plot 2 – the line
double EMABuffer[];         // hidden (calculation)

//--- State
int lastSignalBar = -1;
int lastAlertBar  = -1;      // for suppressing repeated alerts on same bar

2. Основные математические функции (без изменений по сравнению с этапом 1)

Мы повторно используем те же три вспомогательные функции: CalculateTrend, CalculateMomentum и CalculateVolatility. Их реализации идентичны реализациям на этапе 1, поэтому я не буду повторять их здесь. Полный код находится в листинге выше. Следует отметить, что для расчета тренда используется метод наименьших квадратов на основе ненормализованных цен. Это работает отлично, но если вы торгуете активами с очень разными ценовыми шкалами (например, криптовалютой и форексом), вам может потребоваться нормализовать входные данные. Для простоты я оставил расчет на сырых ценах, но модульная конструкция позволяет легко добавить версию с логарифмической ценой.

3. Расчет EMA — простая, повторно используемая функция

Я намеренно избегал использования встроенного хэндла iMA(). Почему? Потому что хэндлы индикаторов могут быть неудобными — их необходимо освобождать в OnDeinit, и иногда они возвращают неполные данные на первых барах. Пользовательская функция CalculateEMA() является самодостаточной: она вычисляет SMA для первых баров 'периода', а затем рекурсивно применяет множитель EMA. Формула стандартная: множитель = 2 / (период + 1). Она быстрая и предсказуемая. Один нюанс: если EMAPeriod установлен в 0, функция никогда не вызывается, поэтому индикатор работает без какого-либо трендового фильтра.

//+------------------------------------------------------------------+
//| Calculates Exponential Moving Average (EMA)                     |
//+------------------------------------------------------------------+
void CalculateEMA(const double &price[], int period, int rates_total, double &emaBuffer[])
  {
   if(period <= 1 || rates_total < period)
     {
      ArrayInitialize(emaBuffer, 0.0);
      return;
     }

   double multiplier = 2.0 / (period + 1);

   //--- first value = SMA of first 'period' bars
   double sum = 0.0;
   for(int i = 0; i < period; i++)
      sum += price[i];
   emaBuffer[period - 1] = sum / period;

   //--- subsequent EMAs
   for(int i = period; i < rates_total; i++)
      emaBuffer[i] = (price[i] - emaBuffer[i - 1]) * multiplier + emaBuffer[i - 1];
  }

4. Инициализация – настройка буфера, проверка входных данных и свойства построения графика

Функция OnInit() теперь включает в себя базовые проверки входных параметров, отсутствовавшие на этапе 1. Если WindowSize меньше 2, индикатор отказывается запускаться и выводит четкое сообщение об ошибке. Это избавит вас от головной боли, когда на графике ничего не отображается. Я также добавил предупреждение для отрицательных весов – код все равно будет работать, но вы должны знать, что отрицательный вес инвертирует вклад (например, отрицательный вес волатильности фактически будет способствовать её росту). Буферы сопоставлены, коды стрелок установлены, а PLOT_DRAW_BEGIN установлен на WindowSize - 1. Это первый индекс, где существует полное окно истории; более ранние бары остаются пустыми, что позволяет избежать вводящих в заблуждение частичных значений. Переменные состояния lastSignalBar и lastAlertBar начинаются с -1, что означает "сигнала пока нет".

//+------------------------------------------------------------------+
//| Custom indicator initialization                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Input validation – fail gracefully if invalid parameters
   if(WindowSize < 2)
     {
      Print("Error: WindowSize must be at least 2. Indicator will not start.");
      return(INIT_PARAMETERS_INCORRECT);
     }

   if(TrendWeight < 0.0 || MomentumWeight < 0.0 || VolatilityWeight < 0.0)
     {
      Print("Warning: Weights should be non‑negative. Continuing anyway.");
     }

   //--- Map buffers to plots
   SetIndexBuffer(0, BuyArrowBuffer, INDICATOR_DATA);
   SetIndexBuffer(1, SellArrowBuffer, INDICATOR_DATA);
   SetIndexBuffer(2, ScoreBuffer, INDICATOR_DATA);
   SetIndexBuffer(3, EMABuffer, INDICATOR_CALCULATIONS);

   //--- Plot 0: Buy arrows
   PlotIndexSetInteger(0, PLOT_ARROW, 233);
   PlotIndexSetInteger(0, PLOT_LINE_COLOR, clrLimeGreen);
   PlotIndexSetString(0, PLOT_LABEL, "Buy");
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, WindowSize - 1);

   //--- Plot 1: Sell arrows
   PlotIndexSetInteger(1, PLOT_ARROW, 234);
   PlotIndexSetInteger(1, PLOT_LINE_COLOR, clrRed);
   PlotIndexSetString(1, PLOT_LABEL, "Sell");
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, WindowSize - 1);

   //--- Plot 2: MarketScore line
   PlotIndexSetInteger(2, PLOT_DRAW_TYPE, DRAW_LINE);
   PlotIndexSetInteger(2, PLOT_LINE_COLOR, clrDodgerBlue);
   PlotIndexSetInteger(2, PLOT_LINE_WIDTH, 1);
   PlotIndexSetString(2, PLOT_LABEL, "MarketScore");
   PlotIndexSetInteger(2, PLOT_DRAW_BEGIN, WindowSize - 1);

   //--- Initialize buffers
   ArrayInitialize(BuyArrowBuffer, EMPTY_VALUE);
   ArrayInitialize(SellArrowBuffer, EMPTY_VALUE);
   ArrayInitialize(ScoreBuffer, 0.0);
   ArrayInitialize(EMABuffer, 0.0);
   lastSignalBar = -1;
   lastAlertBar  = -1;

   return(INIT_SUCCEEDED);
  }

5. Основные вычисления – показатель, логика пересечения, фильтр EMA и оповещения

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

Во-первых, мы проверяем, что rates_total не меньше WindowSize – нет смысла продолжать, если баров недостаточно. Далее копируем период EMA в локальную переменную emaPeriod, поскольку входные параметры доступны только для чтения. Если EMAPeriod > 0, мы вызываем CalculateEMA для заполнения скрытого буфера.

Начальный индекс вычисляется эффективно: при первом запуске (prev_calculated == 0) мы начинаем с WindowSize - 1 – первого бара, имеющего полное окно истории. При последующих запусках мы начинаем с prev_calculated - 1, то есть пересчитываем только самый последний бар (и любые новые появившиеся бары). Это значительно повышает скорость по сравнению с пересчетом всего графика при каждом тике.

Внутри основного цикла мы создаём окно типа CMatrixDouble, используя последние значения цены WindowSize. Обратите внимание на порядок: мы идём в обратном порядке от i до i - WindowSize + 1, но при заполнении строк матрицы j=0 получает самую старую цену (i - WindowSize + 1), а j=WindowSize-1 — самую новую (i). Это нормально — матричным методам не важен порядок, если последовательность согласована.

Логика сигналов — вот где проявляется характер стратегии. Условие покупки — (prev > 0.0 && curr < BuyThreshold). Это означает, что показатель был положительным в предыдущем баре (бычий импульс), а затем упал ниже отрицательного порога. Почему не просто "curr < BuyThreshold"? Потому что это также сработает, когда показатель был отрицательным в течение некоторого времени и становится ещё более отрицательным — это просто продолжающийся нисходящий тренд, а не разворот. Условие пересечения с противоположной стороны делает этот сигнал сигналом типа возврата к среднему. Аналогично, для сигнала на продажу необходимо, чтобы предыдущий отрицательный показатель внезапно превысил положительный порог.

Фильтр EMA применяется путем переопределения параметров buyCond и sellCond, если цена находится на "неправильной" стороне EMA. Некоторые трейдеры могут предпочитать сравнивать показатель с EMA, а не с ценой; это легко изменить.

Проверка времени ожидания (timeOk) использует MinBarsBetween. Я обнаружил, что без этого индикатор может отображать несколько стрелок за один свинг, особенно когда показатель колеблется вокруг порогового значения. Установка значения в 3 бара дает рынку немного пространства для маневра.

Часть оповещения намеренно ограничена самым последним баром (i == rates_total - 1). Это бар, который формируется в данный момент. Нет смысла оповещать по историческим барам — вы не сможете торговать на них. А поскольку lastAlertBar хранит индекс последнего бара, на котором сработало оповещение, вас не будут засыпать множеством оповещений на одном и том же баре при пересчете индикатора (например, при каждом тике).

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

//+------------------------------------------------------------------+
//| Main calculation routine                                         |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])   // standard signature – no extra arrays
  {
   //--- Not enough bars to compute the first window
   if(rates_total < WindowSize)
      return(0);

   //--- Determine effective EMA period (input cannot be modified, so use a local variable)
   int emaPeriod = (EMAPeriod > 0) ? EMAPeriod : 0;
   bool useEMA = (emaPeriod > 0);

   //--- Calculate EMA if enabled
   if(useEMA)
      CalculateEMA(price, emaPeriod, rates_total, EMABuffer);

   //--- Determine first index to recalc
   int start = (prev_calculated == 0) ? WindowSize - 1 : prev_calculated - 1;

   CMatrixDouble window(WindowSize, 1);

   for(int i = start; i < rates_total; i++)
     {
      //--- Fill price window with 'WindowSize' bars: i, i-1, ..., i-WindowSize+1
      for(int j = 0; j < WindowSize; j++)
         window.Set(j, 0, price[i - j]);   // safe because i >= WindowSize-1

      double trend      = CalculateTrend(window);
      double momentum   = CalculateMomentum(window);
      double volatility = CalculateVolatility(window);
      double score = TrendWeight * trend + MomentumWeight * momentum - VolatilityWeight * volatility;

      ScoreBuffer[i] = score;
      BuyArrowBuffer[i]  = EMPTY_VALUE;
      SellArrowBuffer[i] = EMPTY_VALUE;

      //--- Skip the very first valid bar – cannot compute crossing yet
      if(i > (WindowSize - 1))
        {
         double prev = ScoreBuffer[i - 1];
         double curr = score;

         //--- Buy: previous positive AND crosses below BuyThreshold
         bool buyCond  = (prev > 0.0 && curr < BuyThreshold);
         //--- Sell: previous negative AND crosses above SellThreshold
         bool sellCond = (prev < 0.0 && curr > SellThreshold);

         //--- EMA trend filter (if enabled)
         if(useEMA)
           {
            if(buyCond && price[i] <= EMABuffer[i])
               buyCond = false;
            if(sellCond && price[i] >= EMABuffer[i])
               sellCond = false;
           }

         //--- Minimum bar spacing filter
         bool timeOk = (MinBarsBetween <= 1 || lastSignalBar == -1 || (i - lastSignalBar) >= MinBarsBetween);

         //--- Draw arrows and trigger alert ONLY on the latest bar (i == rates_total-1)
         if(buyCond && timeOk)
           {
            BuyArrowBuffer[i] = price[i];
            lastSignalBar = i;

            if(i == rates_total - 1 && lastAlertBar != i)
              {
               // Get bar time using iTime()
               datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i);
               Alert("BUY signal at ", TimeToString(barTime), " | Price: ", DoubleToString(price[i], _Digits), " | Score: ", DoubleToString(score, 2));
               lastAlertBar = i;
              }
           }

         if(sellCond && timeOk)
           {
            SellArrowBuffer[i] = price[i];
            lastSignalBar = i;

            if(i == rates_total - 1 && lastAlertBar != i)
              {
               datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i);
               Alert("SELL signal at ", TimeToString(barTime), " | Price: ", DoubleToString(price[i], _Digits), " | Score: ", DoubleToString(score, 2));
               lastAlertBar = i;
              }
           }
        }
     }

   return(rates_total);
  }

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


Тестирование и результаты

Скомпилируйте оба индикатора в MetaEditor (F7). Установите SmartMarketStructureMatrixIndicator_Separate на график: вы увидите отдельное окно с синей линией и серебряной гистограммой. Это подтверждает базовый рыночный показатель. Далее установите SmartMarketStructureMatrixIndicator_Main на тот же график. Синяя линия остаётся (вы можете скрыть её, отключив графическое построение "MarketScore"), и теперь зелёные стрелки сигнала на покупку и красные стрелки сигнала на продажу появляются непосредственно на ценовых барах, когда выполняются условия пересечения пороговых значений. Кроме того, при появлении нового сигнала на последнем баре всплывающее уведомление отображает тип сигнала, время, цену и показатель — идеально для трейдеров, которые не могут постоянно следить за экраном.

На индексе Volatility 75(1), таймфрейм M15, с настройками по умолчанию (WindowSize=50, BuyThreshold=-10, SellThreshold=10, EMAPeriod=200), сигнал обычно появляется после того, как исчерпан устойчивый тренд: стрелка сигнала на покупку часто появляется после того, как серия бычьих движений сменяется резкой распродажей, а стрелка на продажу — после того, как продолжительное медвежье движение разворачивается вверх. Фильтр EMA помогает избежать стрелок, указывающих на контртренд, а параметр MinBarsBetween снижает уровень шума. Поскольку индикатор использует только прошлые бары (цены закрытия) и не зависит от будущих данных, он никогда не перерисовывается. Бэктестирование показывает, что хотя не каждый сигнал является выигрышным, сочетание составного показателя и логики пересечения создает полезное преимущество, особенно в сочетании с надлежащим управлением рисками.

Fig. 3. SmartMarketStructureMatrixIndicator_Main – buy and sell arrows on the main chart, with alerts on new signals.

Рис. 3. SmartMarketStructureMatrixIndicator_Main – стрелки сигналов на покупку и продажу на основном графике с оповещениями о новых сигналах.


Заключение

Мы разработали два взаимодополняющих, ориентированных на практическое применение, индикатора и четкий путь валидации. На этапе 1 (SmartMarketStructureMatrixIndicator Separate) изолируется и проверяется рыночный показатель с помощью matrix.mqh, позволяя подтвердить статистическое поведение тренда, импульса и волатильности в скользящем окне. На этапе 2 (SmartMarketStructureMatrixIndicator Main) этот показатель преобразуется в конкретные события без перерисовки на ценовом графике: сигналы на покупку требуют наличия положительного показателя до этого, за которым следует падение ниже отрицательного порога BuyThreshold (то есть prev > 0 && curr < BuyThreshold, в соответствии с кодом индикатора). Основной индикатор также поддерживает дополнительный фильтр тренда EMA (EMAPeriod; 0 отключает), задержку MinBarsBetween для уменьшения лишнего шума и всплывающие оповещения, которые срабатывают не более одного раза за формирующийся бар.

Ограничения и дальнейшие шаги: на рынках с низкой волатильностью и рваной, боковой динамикой показатель может колебаться и приводить к ложным пересечениям — используйте EMA, увеличьте MinBarsBetween или скорректируйте веса/пороговые значения. Рассмотрите возможность нормализации входных данных (логарифмические цены или z-показатели) при работе с инструментами, имеющими разные ценовые шкалы, добавления фильтров объема или ATR, или интеграции сигналов в набор правил с управляемыми рисками выходами (трейлинг-стопы, выходы по времени, вторичные подтверждения). Оба исходных файла прилагаются и готовы к компиляции; используйте этап 1 для проверки математических расчетов, затем используйте индикатор этапа 2 на нужном символе и таймфрейме и настройте WindowSize, веса и пороговые значения в соответствии со своей торговой целью.

Вложения

Название файла Тип Версия Описание
SmartMarketStructureMatrixIndicator_Separate.mq5 Индикатор MQL5 1.00 Осциллятор с отдельным окном, линией и гистограммой. Демонстрирует базовое использование матриц.
SmartMarketStructureMatrixIndicator_Main.mq5 Индикатор MQL5 1.1 Индикатор основного графика со стрелками сигналов на покупку/продажу, фильтром тренда EMA, паузой между сигналами и всплывающими оповещениями. Сигналы при пересечении порогового значения (положительные → ниже BuyThreshold для покупки, отрицательные → выше SellThreshold для продажи)

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

Создание HTML-дашборда для Тестера стратегий и анализа проп-фирменных испытаний в MQL5 Создание HTML-дашборда для Тестера стратегий и анализа проп-фирменных испытаний в MQL5
В этой статье показано, как создать многоразовый модуль оценки проп-фирм для советников MQL5 и экспортировать результаты на HTML-дашборд. Модуль отслеживает баланс и капитал во время тестирования стратегий на исторических данных, имитирует одиночные или скользящие серии испытаний, проверяет целевую прибыль, дневную и общую просадку, а также минимальное количество торговых дней, после чего выводит как итоговый отчет, так и отчет в HTML-формате, который можно открыть в браузере.
Нейросети в трейдинге: Двухуровневая адаптация торговой политики (D2Skill) Нейросети в трейдинге: Двухуровневая адаптация торговой политики (D2Skill)
Начинаем адаптацию фреймворка D2Skill к нейронной торговой политике: навыки формируются из повторяющихся псевдоостатков и применяются как аддитивные коррекции в латентном пространстве. Реализуется элемент CD2SkillItem на MQL5/OpenCL с GPU‑конвейером обновления в двух представлениях (полный остаток и направление + масштаб). Контролируемый эксперимент оценивает косинусное сходство, ошибку реконструкции и вклад коррекции в функцию потерь. Читатель получает готовый блок и методику проверки для последующего банка навыков.
Создание торговых систем с искусственным интеллектом на MQL5 (Часть 2): Разработка программы с интеграцией ChatGPT и пользовательским интерфейсом Создание торговых систем с искусственным интеллектом на MQL5 (Часть 2): Разработка программы с интеграцией ChatGPT и пользовательским интерфейсом
В этой статье разрабатывается программа на языке MQL5 с интеграцией ChatGPT и пользовательским интерфейсом. Она задействует модуль разбора JSON из части 1 для отправки промптов в API OpenAI и отображения ответов на графике MetaTrader 5. Реализуется панель управления с полем ввода, кнопкой отправки и областью отображения ответа. Для пользовательского взаимодействия программа обрабатывает обмен данными с API и перенос текста по строкам.
Создание торговых систем с искусственным интеллектом на MQL5 (Часть 1): Реализация обработки JSON для API ИИ Создание торговых систем с искусственным интеллектом на MQL5 (Часть 1): Реализация обработки JSON для API ИИ
В этой статье разрабатывается модуль разбора JSON на языке MQL5 для обмена данными при интеграции с API ИИ. Основное внимание уделяется классу JsonValue для обработки JSON-структур. Реализуются методы сериализации и десериализации JSON-данных с поддержкой различных типов данных, таких как строки, числа и объекты. Это необходимо для взаимодействия с ИИ-сервисами, такими как ChatGPT, и служит основой для будущих торговых систем на базе ИИ, обеспечивая точную обработку данных.