English
preview
Создание индикатора профиля объема с перекрестием на MQL5

Создание индикатора профиля объема с перекрестием на MQL5

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

Введение

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

В этой статье мы создадим индикатор профиля объема с перекрестием на MQL5. Он превращает перетаскивание с нажатой правой кнопкой мыши в MetaTrader 5 в интерактивный инструмент анализа объема. После выбора области графика индикатор определяет границы области анализа и формирует адаптивные ценовые интервалы. Затем он накапливает тиковый или реальный объем, определяет уровень максимального объема (POC) и отображает на графике профиль объема с окраской по цветовой шкале. Чтобы дать более полное представление об активности участников рынка, эта реализация поддерживает несколько моделей расчета цены — цену закрытия, медианную цену, типичную цену, взвешенную цену закрытия и OHLC4, — а не ограничивается только ценой закрытия. К концу этой статьи вы узнаете, как:
  • Интерактивно получать заданные пользователем участки графика с помощью событий графика.
  • Реализовать собственный интерфейс с перекрестием на MQL5.
  • Динамически определять границы области анализа.
  • Обеспечить поддержку различных ценовых моделей, не ограничиваясь традиционной ценой закрытия.

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


Обзор проекта

В отличие от традиционных инструментов построения профиля объема, которые используют заранее заданные или фиксированные области анализа, эта реализация позволяет пользователям строить профиль объема по запросу для любой области графика. Индикатор профиля объема с перекрестием — это событийно-ориентированный индикатор на MQL5, сочетающий привычный инструмент «Перекрестие» платформы MetaTrader 5 с аналитическими возможностями профиля объема. При включении с помощью кнопки меню этот индикатор активирует события перемещения мыши и отключает контекстное меню по умолчанию. Таким образом, перетаскивание с нажатой правой кнопкой мыши используется исключительно для выбора области анализа. При выключении индикатора эти настройки графика автоматически восстанавливаются, что позволяет избежать конфликтов со встроенными элементами управления MetaTrader 5 и сохранить привычный рабочий процесс пользователя. Затем выбранная область используется для расчета и отображения профиля объема с окраской по цветовой шкале вместе с уровнем максимального объема (POC).

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

Этот индикатор также поддерживает функцию быстрого удаления профиля. Дважды нажмите клавишу «C», чтобы удалить текущий профиль объема, уменьшить перегруженность графика и освободить ценовой график от перекрывающих его элементов. Ниже представлена визуальная модель индикатора, описанного в данном проекте:

Обзор проекта

Рабочий процесс проектирования

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

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

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

Событийно-ориентированное взаимодействие

Функция OnChartEvent() координирует интерактивное поведение индикатора. Она реагирует на нажатия кнопки меню и переключение ее состояния, обрабатывает перетаскивание с нажатой правой кнопкой мыши для задания области анализа и распознает двойное нажатие клавиши «C» для удаления текущего профиля объема. Такая событийно-ориентированная архитектура гарантирует, что действия пользователя обрабатываются только в момент их совершения, что повышает как отзывчивость системы, так и эффективность вычислений.

Выбор области анализа

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

Выбор области анализа

Такое перетаскивание с нажатой правой кнопкой мыши во многом напоминает работу со встроенным инструментом «перекрестие» в MetaTrader 5, обеспечивая привычный и интуитивно понятный способ взаимодействия и позволяя мгновенно анализировать объем по запросу в любой выбранной пользователем области графика.

Адаптивное построение интервалов

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

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

Накопление объема

После определения области анализа и формирования структуры адаптивных ценовых интервалов индикатор начинает накапливать объем в соответствующих ценовых интервалах. Для каждой свечи в выбранной области индикатор рассчитывает репрезентативную исходную цену по выбранной модели: цена закрытия, медианная цена (H + L) / 2, типичная цена (H + L + C) / 3, взвешенная цена закрытия (H + L + 2C) / 4 или OHLC4 (O + H + L + C) / 4. Затем индикатор определяет, в какой ценовой интервал попадает рассчитанная исходная цена, и добавляет в этот интервал тиковый объем или реальный объем свечи. Этот процесс повторяется для каждой свечи в пределах области анализа, постепенно формируя распределение торговой активности по всем ценовым уровням.

Визуализация профиля объема

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

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


Реализация на MQL5

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

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

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

//+------------------------------------------------------------------+
//|                                     Crosshair Volume Profile.mq5 |
//|                                             © 2026, ChukwuBuikem |
//|                             https://www.mql5.com/en/users/bikeen |
//+------------------------------------------------------------------+
#property copyright "© 2026, ChukwuBuikem"
#property link      "https://www.mql5.com/en/users/bikeen"
#property indicator_chart_window
#property indicator_plots 0
//--- MOUSE RIGHT-CLICK MACRO
#define MOUSE_RIGHT  0x02
//--- PROFILE MACRO
#define _PROG_NAME "Crosshair Volume Profile"
#define _MENU_BUTTON _PROG_NAME + "BUTTON"
#define _V_START_LINE _PROG_NAME + "V_START_LINE"
#define _V_END_LINE _PROG_NAME + "V_END_LINE"
#define _H_LINE _PROG_NAME + "HLINE"
#define _VOLUME_RANGE _PROG_NAME + "RECTANGLE"
#define _PROFILE _PROG_NAME + "PROFILE"
#define _PROFILE_POC _PROG_NAME + "PROFILE_POC"

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

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

//--- CUSTOM ENUMERATION
enum ENUM_VOLUME_PRICE
  {
//---
   VOLUME_PRICE_CLOSE,     //PRICE CLOSE
   VOLUME_PRICE_MEDIAN,    //PRICE MEDIAN
   VOLUME_PRICE_TYPICAL,   //PRICE TYPICAL
   VOLUME_PRICE_WEIGHTED,  //PRICE WEIGHTED
   VOLUME_PRICE_OHLC4      //PRICE OHLC4
  };

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

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

//--- DATA STRUCTURE
struct st_priceVolume
  {
   //---
   datetime          time;
   double            price;
   long              volume;
   //---CONSTRUCTOR
                     st_priceVolume(): time(0), price(EMPTY_VALUE), volume(0) {}
  };

Входные параметры

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

//--- INPUT SETTINGS
input ENUM_VOLUME_PRICE inpPriceType = VOLUME_PRICE_CLOSE;//Price source

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

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

//--- GLOBAL VARIABLES
bool isButtonOn = false;
datetime startTime = 0;
datetime endTime = 0;

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

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

Расчет исходной цены

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

//+------------------------------------------------------------------+
//|                 PRICE SOURCE COMPUTATION                         |
//+------------------------------------------------------------------+
double getPrice(const MqlRates & rate)
  {
//---
   switch(inpPriceType)
     {
      case VOLUME_PRICE_MEDIAN:
         return (rate.high + rate.low) / 2;

      case VOLUME_PRICE_TYPICAL:
         return (rate.high + rate.low + rate.close) / 3;

      case VOLUME_PRICE_WEIGHTED:
         return (rate.high + rate.low + (2 * rate.close)) / 4;

      case VOLUME_PRICE_OHLC4:
         return (rate.open + rate.high + rate.low + rate.close) / 4;
     }
//--- FALLBACK TO CLOSE PRICE
   return NormalizeDouble(rate.close, _Digits);
  }
Нормализация времени и объема

Точность визуализации зависит как от точной привязки ко времени, так и от согласованного масштабирования объема. Первая функция, normalizeToChartTime(), приводит любое заданное время к ближайшему допустимому значению времени на текущем графике, обеспечивая привязку вертикальной линии к соответствующей свече. Вторая функция, normalizeMinMax(), нормализует значения объема в диапазон от 0 до 1, что позволяет пропорционально масштабировать полосы профиля объема независимо от абсолютной величины объема.

//+------------------------------------------------------------------+
//|                      TIME NORMALIZATION                          |
//+------------------------------------------------------------------+
datetime normalizeToChartTime(const datetime time)
  {
//---
   int shift = iBarShift(_Symbol, PERIOD_CURRENT, time);
   if(shift < 0)
      shift = 0;

   return iTime(_Symbol, PERIOD_CURRENT, shift);
  }
//+------------------------------------------------------------------+
//|                   VOLUME NORMALIZATION                           |
//+------------------------------------------------------------------+
double normalizeMinMax(const long value, const long maxVol, const long minVol)
  {
//---
   if(maxVol <= minVol)
      return 0;

   return (double)(value - minVol) / (maxVol - minVol);
  }
Управление графическими объектами
Для построения интерактивного профиля объема с перекрестием индикатор должен динамически создавать, обновлять и удалять графические объекты в ответ на действия пользователя. Приведенные ниже вспомогательные функции инкапсулируют создание графических объектов, таких как кнопки, линии и прямоугольники, управление ими, а также процедуры очистки, делая код более понятным, пригодным для повторного использования и удобным в сопровождении.
//+------------------------------------------------------------------+
//|                           BUTTON CREATION                        |
//+------------------------------------------------------------------+
void createMenuButton(void)
  {
//---
   if(ObjectFind(0, _MENU_BUTTON) != -1)
      ObjectDelete(0, _MENU_BUTTON);

   ObjectCreate(0, _MENU_BUTTON, OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
   ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_XDISTANCE, 70);
   ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_YDISTANCE, 50);
   ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_XSIZE, 40);
   ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_YSIZE, 40);
   ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_COLOR, clrWhite);
   ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_BGCOLOR, clrRed);
   ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_BORDER_COLOR, clrBlack);
   ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_STATE, false);
   ObjectSetString(0, _MENU_BUTTON, OBJPROP_TOOLTIP, "Menu Button");
   ObjectSetString(0, _MENU_BUTTON, OBJPROP_TEXT, "OFF");
   ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_FONTSIZE, 10);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//|                     BUTTON TOGGLE SYSTEM                         |
//+------------------------------------------------------------------+
void toggleMenuButton(void)
  {
//---
   isButtonOn = !isButtonOn;

   if(isButtonOn)
     {
      ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_BGCOLOR, clrBlue);
      ObjectSetString(0, _MENU_BUTTON, OBJPROP_TEXT, "ON");
      ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_STATE, false);
      //--- CHANGE CHART SETTINGS
      ChartSetInteger(0, CHART_CONTEXT_MENU, false);
      ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
     }
   else
     {
      ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_BGCOLOR, clrRed);
      ObjectSetString(0, _MENU_BUTTON, OBJPROP_TEXT, "OFF");
      ObjectSetInteger(0, _MENU_BUTTON, OBJPROP_STATE, false);
      
      //--- RESTORE CHART SETTINGS
      ChartSetInteger(0, CHART_CONTEXT_MENU, true);
      ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false);
     }
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//|                      VERTICAL LINE CREATION                      |
//+------------------------------------------------------------------+
void createVLine(const string objName, const datetime vTime)
  {
//---
   if(ObjectCreate(0, objName, OBJ_VLINE, 0, vTime, 0))
     {
      color clr = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
      ObjectSetInteger(0, objName, OBJPROP_COLOR, (clr == clrBlack) ? clrWhite : clrBlack);
      ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);
      ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
      ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
      ObjectSetString(0, objName, OBJPROP_TOOLTIP, "\n");
     }
  }
//+------------------------------------------------------------------+
//|                   HORIZONTAL LINE CREATION                       |
//+------------------------------------------------------------------+
void createHLine(const string objName, const double price1,
                 const color clr = clrBlack, const string toolTip = "\n")
  {
//---
   if(ObjectCreate(0, objName, OBJ_HLINE, 0, 0, price1))
     {
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
      ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);
      ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
      ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
      ObjectSetString(0, objName, OBJPROP_TOOLTIP, toolTip);
     }
  }
//+------------------------------------------------------------------+
//|                      RECTANGLE CREATION                          |
//+------------------------------------------------------------------+
void createRect(const string objName, const datetime time1,
                const double price1, const datetime time2,
                const double price2, const color clr,
                const string tooltip = "\n")
  {
//---
   if(ObjectCreate(0, objName, OBJ_RECTANGLE, 0, time1, price1, time2, price2))
     {
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
      ObjectSetInteger(0, objName, OBJPROP_BACK, true);
      ObjectSetInteger(0, objName, OBJPROP_FILL, true);
      ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
      ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);
      ObjectSetString(0, objName, OBJPROP_TOOLTIP, tooltip);
     }
  }
//+------------------------------------------------------------------+
//|                        OBJECT DELETION                           |
//+------------------------------------------------------------------+
void  clearChart(void)
  {
//---
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false);
   ChartSetInteger(0, CHART_CONTEXT_MENU, true);
   ObjectsDeleteAll(0, _PROG_NAME);
   ChartRedraw();
  }

Пояснение

  • createMenuButton()
    Создает и инициализирует кнопку управления «Вкл./Выкл.», которая включает или отключает функциональность индикатора профиля объема с перекрестием.

  • toggleMenuButton()

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

  • createVLine()
    Отвечает за создание вертикальной опорной линии в заданный момент времени, позволяя перекрестию точно отмечать выбранную свечу на графике.
  • createHLine()
    Создает горизонтальную опорную линию на заданной цене, позволяя точно обозначать ценовые уровни перекрестия и уровня максимального объема (POC).
  • createRect()
    Создает залитый прямоугольник между двумя точками с координатами «время — цена»; такие прямоугольники служат графическими элементами для отображения полос профиля объема.
  • clearChart()
    Выполняет очистку при удалении индикатора: удаляет все созданные графические объекты, восстанавливает настройки взаимодействия с графиком по умолчанию и возвращает график в исходное состояние.
Обработчик координат курсора
Далее мы определяем функцию, которая отслеживает перемещение курсора по графику, преобразуя его экранные координаты X и Y в нормализованные значения времени и цены и возвращая их в виде структуры st_priceVolume, что позволяет пользователю точно задать область анализа профиля объема.
//+------------------------------------------------------------------+
//|               CURSOR COORDINATE PROCESSOR                        |
//+------------------------------------------------------------------+
st_priceVolume getCursorPriceTime(short x, short y)
  {
//---
   int sub;
   st_priceVolume tp;
   double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   ChartXYToTimePrice(0, x, y, sub, tp.time, tp.price);
//--- NORMALIZE PRICE AND TIME
   tp.price = NormalizeDouble(MathRound(tp.price / tickSize) * tickSize, _Digits);
   tp.time = normalizeToChartTime(tp.time);
   return tp;
  }
Расчет объема

Функция calcVolume() координирует весь процесс построения профиля объема: анализирует выбранные рыночные данные, распределяет объем по адаптивным ценовым интервалам, определяет уровень максимального объема (POC) и отображает результаты на графике.

//+------------------------------------------------------------------+
//|                   VOLUME COMPUTATION                             |
//+------------------------------------------------------------------+
void calcVolume(void)
  {
//---
   MqlRates rates[];

   if(CopyRates(_Symbol, PERIOD_CURRENT, startTime, endTime, rates) > 0)
     {
      double rangeHigh = rates[0].high, rangeLow = rates[0].low;
      int  count = MathAbs(iBarShift(_Symbol, PERIOD_CURRENT, startTime) -
                           iBarShift(_Symbol, PERIOD_CURRENT, endTime)) + 1;

      //---VERTICAL PRICE RANGE
      for(int b = 0; b < ArraySize(rates); b++)
        {
         if(rates[b].high > rangeHigh)
            rangeHigh = rates[b].high;

         if(rates[b].low < rangeLow)
            rangeLow = rates[b].low;
        }
      //--- SET ANALYSIS RANGE RECTANGLE PROPERLY
      ObjectSetDouble(0, _VOLUME_RANGE, OBJPROP_PRICE, 0, rangeHigh);
      ObjectSetDouble(0, _VOLUME_RANGE, OBJPROP_PRICE, 1, rangeLow);
      ChartRedraw();
      //--- ADAPTIVE BIN CONSTRUCTION
      int numberOfBins = int(count / 4);
      numberOfBins = MathMax(5, numberOfBins);
      double step = (rangeHigh - rangeLow) / numberOfBins;
      st_priceVolume bins[];
      ArrayResize(bins, numberOfBins);
      double binHigh = EMPTY_VALUE,  binLow = EMPTY_VALUE;
      bool useRealVol = (rates[0].real_volume > 0);

      //--- VOLUME ACCUMULATION USING SELECTED PRICE MODEL
      for(int w = 0; w < numberOfBins; w++)
        {
         binLow = rangeLow + step * w;
         binHigh = binLow + step;
         for(int c = 0; c < ArraySize(rates); c++)
           {
            if(getPrice(rates[c]) >= binLow && getPrice(rates[c]) <= binHigh)
              {
               bins[w].volume += (useRealVol) ? rates[c].real_volume : rates[c].tick_volume;
               bins[w].price = NormalizeDouble(getPrice(rates[c]), _Digits);
              }
           }        
        }
      //--- VOLUME EXTREMES
      long maxVol = 0, minVol = LONG_MAX;
      for(int b = 0; b < ArraySize(bins); b++)
        {
         if(bins[b].volume > maxVol)
            maxVol = bins[b].volume;

         if(bins[b].volume < minVol)
            minVol = bins[b].volume;
        }
      //--- PROFILE SCALING
      int extendBars = 0;
      int maxProfileLength =  int(count * 0.4);
      int minProfileLength = (int)MathRound(maxProfileLength * 0.05);
      minProfileLength = MathMax(2, minProfileLength);// CLAMP
      color clr = clrRed;
      double dominance = EMPTY_VALUE;
      //--- PROFILE RENDERING
      bool pocDrawn = false;
      for(int p = 0; p < ArraySize(bins); p++)
        {
         binLow = rangeLow + step * p;
         binHigh = binLow + step;

         extendBars = minProfileLength + (int)MathRound(normalizeMinMax(bins[p].volume, maxVol, minVol)
                      * (maxProfileLength - minProfileLength));
         dominance = (double)extendBars / maxProfileLength;
         clr = (dominance >= 0.8) ? clrBlue :
               (dominance >= 0.6) ? clrPurple :
               (dominance >= 0.5) ? clrBrown :
               (dominance >= 0.4) ? clrLime :
               (dominance >= 0.2) ? clrTeal :
               (dominance >= 0.15) ? clrBlueViolet :
               clrGray;

         createRect(_PROFILE + (string)p, startTime, binHigh, startTime + (PeriodSeconds()*extendBars), binLow, clr);
         if(bins[p].volume == maxVol && !pocDrawn)
           {
            //--- DRAW MAXIMUM VOLUME'S POC
            createHLine(_PROFILE_POC + (string)p, NormalizeDouble(bins[p].price, _Digits), clr, "POC");
            pocDrawn = true;
           }
        }
      ChartRedraw();
     }
  }

Инициализация и очистка

После создания вспомогательных функций приступим к реализации стандартных обработчиков событий MQL5, начиная с OnInit() и OnDeinit(). При инициализации индикатор создает кнопку меню «Вкл./Выкл.», позволяющую пользователям удобно включать или отключать интерактивный индикатор профиля объема с перекрестием. При удалении индикатора, напротив, выполняется процедура очистки, которая удаляет все созданные им графические объекты и восстанавливает настройки взаимодействия с графиком по умолчанию, оставляя график в чистом и согласованном состоянии.
//+------------------------------------------------------------------+
//|                   INITIALIZATION FUNCTION                        |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   createMenuButton();
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//|                  DEINITIALIZATION FUNCTION                       |
//+------------------------------------------------------------------+
void OnDeinit(const int32_t reason)
  {
//---
   clearChart();
  }

Механизм итерации (OnCalculate)

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

//+------------------------------------------------------------------+
//| 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[])
  {
//--- EMPTY
   return(rates_total);
  }

Интерактивный механизм

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

//+------------------------------------------------------------------+
//|                      CHARTEVENT FUNCTION                         |
//+------------------------------------------------------------------+
void OnChartEvent(const int32_t id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   st_priceVolume tp;
   static bool isDragging = false;
   int key = (int)lparam;
   static ulong lastClick = 0;
   switch(id)
     {
      case CHARTEVENT_OBJECT_CLICK:
         //--- TOGGLE MENU BUTTON
         if(sparam == _MENU_BUTTON)
           {
            toggleMenuButton();
           }
         break;
      case CHARTEVENT_KEYDOWN:
         //--- PROFILE DELETION: "C" DOUBLE-CLICK
         if(key == 67)
           {
            ulong now = GetTickCount();
            bool doubleClick = (now - lastClick <= 300);
            lastClick = now;
            if(doubleClick && ObjectFind(0, _VOLUME_RANGE) > -1)
              {
               ObjectsDeleteAll(0, _PROFILE);
               ObjectDelete(0, _VOLUME_RANGE);
               if(ObjectGetString(0, _MENU_BUTTON, OBJPROP_TEXT) == "ON")
                 {
                  toggleMenuButton();
                 }
               ChartRedraw();
              }
           }
         break;
      case CHARTEVENT_MOUSE_MOVE:
         if(isDragging)
           {
            //--- UPDATE OBJECTS ALONG THE CURSOR POSITION USING getCursorPriceTime()
            tp = getCursorPriceTime((short)lparam, (short)dparam);
            tp.time = (tp.time >= iTime(_Symbol, PERIOD_CURRENT, 0)) ?
                      iTime(_Symbol, PERIOD_CURRENT, 1) : tp.time;
            ObjectMove(0, _V_END_LINE, 0, tp.time, 0);
            ObjectMove(0, _H_LINE, 0, 0, tp.price);
            ObjectMove(0, _VOLUME_RANGE, 1, tp.time, tp.price);
            endTime = tp.time;
            ChartRedraw();
           }
         //--- RIGHT-CLICK
         if(((uchar)sparam & MOUSE_RIGHT) != 0)
           {
            tp = getCursorPriceTime((short)lparam, (short)dparam);

            if(!isDragging)
              {
               //--- CREATE CROSSHAIR
               isDragging = true;
               color clr = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
               ObjectsDeleteAll(0, _PROFILE);
               ChartRedraw();
               startTime = tp.time;
               createVLine(_V_START_LINE, tp.time);
               createVLine(_V_END_LINE, 0);
               createHLine(_H_LINE, 0, (clr == clrBlack) ?
                           clrWhite : clrBlack);
               //--- POSITION RECTANGLE ANCHOR (0) AT THE HIGH OF BAR AT RIGHT-CLICK POSITION
               int index = iBarShift(_Symbol, PERIOD_CURRENT, tp.time);
               double price = iHigh(_Symbol, PERIOD_CURRENT, index);
               createRect(_VOLUME_RANGE, tp.time, price, 0, 0, clrOldLace, "ANALYSIS RANGE");
               ChartRedraw();
              }
           }
         else
           {
            //--- RIGHT-CLICK RELEASE
            if(isDragging)
              {
               //--- DELETE CROSSHAIR
               ObjectDelete(0, _V_START_LINE);
               ObjectDelete(0, _V_END_LINE);
               ObjectDelete(0, _H_LINE);
               ChartRedraw();
               isDragging = false;
               //--- RENDER VOLUME PROFILE
               calcVolume();
              }
           }
         break;
     }
  }

Пояснение

  • Нажатие кнопки меню

    Когда пользователь нажимает кнопку меню «Вкл./Выкл.», вызывается функция toggleMenuButton(), которая включает или выключает интерактивный режим, одновременно обновляя внешний вид кнопки и настройки взаимодействия с графиком.

  • Удаление профиля объема

    Чтобы упростить повторный анализ и избежать загромождения графика, двойное нажатие клавиши «C» удаляет текущий профиль объема и прямоугольник области анализа, а также возвращает индикатор в исходное состояние, готовое к выбору новой области.

  • Событие щелчка правой кнопкой мыши

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

  • Перетаскивание

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

  • Отпускание правой кнопки мыши

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


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

Как только индикатор успешно скомпилируется, он будет готов к тестированию на графике. На приведенной ниже GIF-анимации показан полный процесс интерактивной работы: включение индикатора с помощью кнопки меню ON/OFF, выбор области анализа перекрестием путем перетаскивания с зажатой правой кнопкой мыши, а также автоматический расчет и отображение профиля объема при отпускании кнопки мыши. Это подтверждает корректность событийно-ориентированной архитектуры индикатора и то, что интерактивный индикатор профиля объема с перекрестием работает так, как задумано.

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


Заключение

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

Основные реализованные возможности:

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

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

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

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

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

Прикрепленные файлы |
Учет тональности новостей для защиты торговой стратегии на MQL5 Учет тональности новостей для защиты торговой стратегии на MQL5
Представляем динамический фильтр тональности новостей для советников MQL5. Он использует WebRequest для отправки запросов к REST API, извлекает из JSON тональность и оценку уверенности и на их основе разрешает или блокирует входы в позицию и выходы из позиции с учетом сигналов скользящей средней и индекса относительной силы (RSI). Вы получите пример советника, инструкции по интеграции, обновления по таймеру, а также способы обхода ограничений тестера стратегий. В результате сокращается число сделок против преобладающего направления новостного потока, а при смене тональности риск снижается раньше.
Конструирование признаков для машинного обучения (Часть 14): Признаки на основе сканирования трендов в MQL5 Конструирование признаков для машинного обучения (Часть 14): Признаки на основе сканирования трендов в MQL5
При наивном переносе вычисления признаков сканирования трендов на MQL5 каждое окно-кандидат пересчитывается на каждом баре с общей вычислительной сложностью O(H·L). В статье представлен файл CTrendScanningFeatures.mqh, в котором для каждого горизонта поддерживаются три обновляемые суммы с обновлением за O(1) на бар; результаты проверены по эталонной реализации на Python. Индикатор предоставляет четыре каузальных буфера — window, slope, t_value, rsquared — на баре подтверждения и исправляет инверсию знака, присутствующую в исходном обратном режиме разметки.
Нейросети в трейдинге: AlphaGen — поиск торговых факторов Нейросети в трейдинге: AlphaGen — поиск торговых факторов
Начинаем изучение AlphaGen — фреймворка автоматического поиска формульных факторов для прогнозирования рыночной доходности и формирования торговых сигналов. Разбираем, как генератор создаёт выражения из цен и объёмов, а совместная оценка факторов помогает отбирать полезные признаки для торговой модели. Рассматриваем адаптацию алгоритма к финансовым временным рядам. В практической части реализуем OpenCL-примитивы типизированного стека и маскирования токенов, обеспечивающие корректное построение формул с учётом ограничений доступной истории.
Пользовательские инструменты отладки и профилирования для разработки на MQL5 (Часть III): Регрессионный контроль производительности и торговых правил Пользовательские инструменты отладки и профилирования для разработки на MQL5 (Часть III): Регрессионный контроль производительности и торговых правил
В этой статье в процесс отладки и профилирования при разработке на MQL5 добавляется барьер регрессионного контроля. При этом сохраняются контракты профилировщика, средства запуска тестов TestLite и вспомогательного модуля торговых расчетов из Части II, а текущие диагностические данные профилировщика сравниваются с принятым эталоном. В рабочий процесс также добавляются проверки утверждений с учетом свойств символа, компактные файлы статусов и таблицы отчетов, чтобы изменения производительности, отсутствующие тесты и проблемы с допущениями об условиях брокера были видны еще до принятия сборки.