Создание интерактивного индикатора профиля объема AnchorFlow (MTF) на языке MQL5
Введение
Большинство трейдеров, ориентированных на объемы, в конечном итоге сталкиваются с необходимостью просматривать объемы на нескольких таймфреймах, однако MetaTrader 5 изначально предоставляет только индикатор объема в виде гистограммы для одного таймфрейма. Хотя этот стандартный инструмент полезен, ему не хватает якорной привязки, адаптивности и мультитаймфреймового контекста — все это необходимо для серьезного анализа объемов. Однако эти ограничения не обусловлены самой платформой. MQL5 — это мощный событийно-ориентированный язык программирования, поддерживающий динамическую отрисовку объектов, синхронизацию между таймфреймами и взаимодействие с пользователем в режиме реального времени, однако большинство индикаторов объема не используют весь его потенциал.
В этой статье мы создадим интерактивный мультитаймфреймовый (MTF) индикатор профиля объема с якорной привязкой на языке MQL5, чтобы устранить эти ограничения. Индикатор строит профиль объема текущего таймфрейма на основном графике и отображает данные об объеме старшего таймфрейма (HTF) в отдельном подокне индикатора. Он адаптируется к изменению масштаба и прокрутке, реагирует на действия пользователя и выделяет преобладание объема за счет синхронизации профилей младшего таймфрейма (LTF) и старшего таймфрейма (HTF). В индикаторе предусмотрено простое управление с клавиатуры. Дважды нажмите клавишу "E", чтобы перейти в режим редактирования и задать количество ценовых интервалов. Дважды нажмите клавишу "S", чтобы сохранить и применить значение. Если пользователь не вводит значение, индикатор использует последнее допустимое значение, сохраненное в памяти, обеспечивая непрерывность анализа.
Для большего удобства пользователя при перетаскивании время линии с якорной привязкой приводится к времени допустимой свечи на графике, что предотвращает ее размещение в недопустимой точке в будущем; при необходимости линия автоматически возвращается в центр видимого диапазона.
К концу этой статьи вы узнаете, как:
- Реализовать перемещаемую линию с якорной привязкой, управляющую построением профиля объема
- Создать интерактивные ценовые интервалы профиля объема, настраиваемые пользователем, с пересчетом в режиме реального времени
- Обеспечить одновременное отображение одного индикатора в нескольких окнах на графике
- Разработать модель восстановления графических объектов после их удаления для обеспечения надежного взаимодействия с пользователем
- Строить синхронизированные мультитаймфреймовые (MTF) профили объема
- Применять надежную проверку заданного во входных параметрах таймфрейма для обеспечения стабильной и безопасной работы индикатора
В совокупности эти концепции демонстрируют, как с помощью MQL5 можно создавать профессиональные инструменты для анализа объемов, которые выходят далеко за рамки возможностей встроенных индикаторов MetaTrader 5.
Обзор проекта
Объем является одним из ключевых показателей рыночного анализа, позволяющим выявить, где сосредоточена торговая активность и как меняется активность участников рынка в зависимости от ценовых уровней и времени. Несмотря на всю важность этого показателя, большинство трейдеров, работающих в MetaTrader 5, по-прежнему ограничиваются гистограммой объема на одном таймфрейме, что затрудняет понимание контекста и снижает эффективность принятия решений на основе объема — особенно когда анализ охватывает несколько временных горизонтов.
В рамках данного проекта разработан и реализован интерактивный мультитаймфреймовый (MTF) индикатор профиля объема с якорной привязкой на языке MQL5, призванный устранить эти ограничения. Индикатор анализирует объем в видимой области графика, строит профиль текущего таймфрейма на основном графике и отображает профиль выбранного пользователем старшего таймфрейма (HTF) в отдельном подокне индикатора. Оба профиля синхронизируются с помощью общей линии с якорной привязкой, что обеспечивает точное временное выравнивание между таймфреймами и позволяет пользователю явно задать диапазон анализа объема, перетащив якорь в любую интересующую точку.
В отличие от платформы Viewport SnR Volume Profile, в данном проекте реализовано интерактивное построение ценовых интервалов. Количество ценовых интервалов профиля объема не фиксировано и не определяется автоматически; пользователь может в любой момент ввести нужное количество интервалов. Чтобы предотвратить непреднамеренный перехват ввода, режим редактирования активируется только при явном выделении линии с якорной привязкой, что однозначно определяет состояние взаимодействия. Двойное нажатие клавиши "E" включает режим редактирования для ввода числового значения, а двойное нажатие клавиши "S" мгновенно сохраняет и применяет это значение. Если пользователь ничего не ввел, индикатор безопасно возвращается к последнему допустимому значению, сохраненному в памяти, что обеспечивает непрерывность анализа и стабильную работу.
Индикатор полностью событийно-ориентированный и учитывает видимую область графика, реагируя в реальном времени на изменение масштаба графика, прокрутку, перемещение линии с якорной привязкой и действия пользователя. В нем используются автоматическое приведение времени линии с якорной привязкой к времени допустимого бара на графике, восстановление графических объектов для повышения надежности и синхронизированное отображение преобладания объема на младшем таймфрейме (LTF) и старшем таймфрейме (HTF), позволяющее наглядно определить ценовые уровни с наиболее значимой торговой активностью. Ниже представлена визуальная модель индикатора, описанного в данном проекте:

Обзор проекта
Рабочий процесс проектирования
Перед реализацией крайне важно четко определить порядок работы системы. Четко определенный порядок работы упрощает разработку и обеспечивает предсказуемое поведение при действиях пользователя, изменениях графика и синхронизации нескольких таймфреймов. В этом разделе работа индикатора разбита на отдельные этапы, каждый из которых отвечает за конкретный аспект его поведения.
Инициализация и проверка входных данных
Рабочий процесс начинается на этапе инициализации индикатора. На данном этапе система устанавливает количество ценовых интервалов профиля объема по умолчанию равным 20, задавая стабильное исходное состояние до начала взаимодействия с пользователем. Это значение по умолчанию остается активным до тех пор, пока пользователь явно не изменит его с помощью механизма интерактивного редактирования. Далее система проверяет выбранный старший таймфрейм, чтобы убедиться, что он строго больше текущего таймфрейма графика. Если выбранный таймфрейм недопустим, индикатор переключается на ближайший таймфрейм старше текущего таймфрейма графика. Это позволяет сохранить согласованность и избежать избыточных настроек.
После проверки настроек таймфреймов индикатор определяет границы текущей видимой области графика и вычисляет начальное положение линии с якорной привязкой, обычно в центре этой области. Это обеспечивает корректную и содержательно значимую отправную точку для анализа объема и позволяет индикатору отображать согласованные профили объема сразу после загрузки.
Управление якорем и определение границ диапазона
Перетаскиваемая вертикальная линия с якорной привязкой служит основным механизмом управления системой. Якорь задает начальную точку анализа объема, а крайняя правая видимая свеча автоматически служит конечной точкой анализа. Вместе эти две границы позволяют пользователю явно управлять диапазоном анализа объема без необходимости выбирать его вручную. Тот же заданный пользователем диапазон анализа используется при построении профиля объема старшего таймфрейма (HTF) в подокне индикатора, благодаря чему профили младшего таймфрейма (LTF) и старшего таймфрейма (HTF) остаются синхронизированными по времени и непосредственно сопоставимыми.
После задания диапазона анализа индикатор получает данные свечей и определяет ценовые границы по наибольшему максимуму и наименьшему минимуму в этом диапазоне. Эти значения задают рабочий ценовой диапазон, в пределах которого формируются ценовые интервалы профиля объема и накапливается объем в каждом из них. Для визуальной стабильности диапазон подокна индикатора расширяется на 100 пунктов в каждую сторону. Значения с учетом этого расширения используются в качестве максимального и минимального уровней подокна индикатора. Такое расширение диапазона предотвращает сжатие профиля по краям и сохраняет его визуальную четкость при различных рыночных условиях.

Определение границ диапазона
При перетаскивании время линии с якорной привязкой приводится к времени ближайшего допустимого бара графика, чтобы предотвратить размещение линии с якорной привязкой в недопустимой точке будущего. Если якорь удален, он немедленно восстанавливается, что сохраняет непрерывность рабочего процесса и целостность индикатора, а также гарантирует, что диапазон анализа всегда остается четко определенным.
Интерактивное построение ценовых интервалов
В отличие от моделей с фиксированными или автоматически масштабируемыми ценовыми интервалами, этот индикатор использует интерактивное построение ценовых интервалов. Пользователь может задать количество ценовых интервалов профиля объема только при явном выделении линии с якорной привязкой, что четко определяет состояние взаимодействия. Двойное нажатие клавиши "E" включает режим редактирования, в котором индикатор принимает последующий числовой ввод, задающий количество ценовых интервалов, а двойное нажатие клавиши "S" сохраняет и применяет изменение. Если пользователь ничего не вводит, система использует последнее сохраненное значение, обеспечивая стабильную и бесперебойную работу.
Накопление объема
После определения диапазона и структуры ценовых интервалов индикатор накапливает тиковый объем или реальный объем как для текущего таймфрейма (LTF), так и для выбранного старшего таймфрейма (HTF). Для каждой свечи в пределах диапазона анализа объем добавляется в ценовой интервал только в том случае, если цена закрытия свечи находится в его границах. Такой подход обеспечивает единообразное и детерминированное распределение объема по ценовым интервалам на разных таймфреймах.
Накопление объема выполняется параллельно для свечей младшего и старшего таймфреймов с использованием одного и того же заданного пользователем временного диапазона и структуры ценовых интервалов, что сохраняет согласованность профилей по времени. Затем значения накопленного объема нормализуются, чтобы избежать ошибок при делении и обеспечить пропорциональное масштабирование, позволяя получить стабильный показатель преобладания объема, сопоставимый для разных ценовых уровней.
Визуализация профиля объема
Наконец, индикатор отображает профили накопленного объема. Профили текущего таймфрейма отображаются непосредственно на основном графике, а профили старшего таймфрейма — в отдельном подокне индикатора. Длина профилей масштабируется пропорционально преобладанию объема, а цветовое кодирование визуально выделяет ценовые уровни с высокой торговой активностью. Точка контроля (POC) выделяется, чтобы обозначить области максимальной торговой активности.
Событийно-ориентированное обновление
Функция OnChartEvent() координирует событийно-ориентированное поведение индикатора. Она обрабатывает выделение и перетаскивание линии с якорной привязкой, распознавание двойного нажатия клавиш "E" и "S", числовой ввод, события масштабирования и прокрутки, а также восстановление линии с якорной привязкой после удаления. Мгновенно реагируя на действия пользователя и события графика, индикатор поддерживает синхронизацию профилей объема и обеспечивает быстрый отклик, интуитивно понятное взаимодействие и удобство работы на профессиональном уровне.
Реализация на MQL5
В этом разделе предложенная концепция реализуется на практике на языке MQL5. Мы поэтапно разрабатываем интерактивный профиль объема AnchorFlow, реализуя логику инициализации, обработку интерактивных событий, управление якорем, обработку видимой области графика, расчет объема, мультитаймфреймовую (MTF) синхронизацию и процедуры отрисовки.
Директивы компилятора
Начнем с задания директив препроцессора, определяющих параметры компиляции и метаданные программы MQL5. Затем объявим ключевые константы, которые будут использоваться во всей реализации, обеспечивая согласованность и удобство сопровождения кода.//+------------------------------------------------------------------+ //| Interactive AnchorFlow 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" #define _PROG_NAME "Interactive AnchorFlow Volume Profile (MTF)" #property description _PROG_NAME #property indicator_separate_window #property indicator_plots 0 //--- PROFILE MACROS #define _VP_ANCHOR _PROG_NAME + "VP Anchor" #define _VOLUME_PROFILE _PROG_NAME + "Profile" #define _LTF_VOLUME_PROFILE _VOLUME_PROFILE + "LTF" #define _HTF_VOLUME_PROFILE _VOLUME_PROFILE + "HTF" //--- KEYSTROKE MACROS #define _DBL_CLICK_TIME 500 //MILLISECONDS #define _KEY_E 69 // EDIT #define _KEY_S 83 // SAVE
Пояснение
- Хотя директива #property indicator_separate_window создает отдельное подокно индикатора, она не препятствует отображению объектов на основном графике. При использовании директивы #property indicator_plots 0 все визуальные элементы отрисовываются с помощью графических объектов, что позволяет одновременно отображать профили младшего таймфрейма (LTF) на основном графике, а профили старшего таймфрейма (HTF) — в подокне индикатора.
- Заданные макросы определяют идентификаторы программы, имена объектов и константы клавиш, упрощая сопровождение кода и обеспечивая единообразие именования и обработки взаимодействий во всем индикаторе.
Пользовательская структура данных
Далее введем пользовательскую структуру данных для хранения ценового уровня и накопленного на нем объема; она служит базовым контейнером при расчете профиля объема.
//--- DATA STRUCTURE struct st_priceVolume { //--- double price; long volume; //--- CONSTRUCTOR st_priceVolume(): price(EMPTY_VALUE), volume(0) {} };
Настройки индикатора
Индикатору требуется всего один входной параметр, позволяющий пользователю указать старший таймфрейм (HTF) для синхронизированного мультитаймфреймового (MTF) анализа объема.
//--- INPUT SETTING input ENUM_TIMEFRAMES inpHtf = PERIOD_H4;// Higher timeframe
Глобальные переменные
Задав входной параметр, перейдем к определению глобальных переменных программы. Эти переменные хранят состояние индикатора во время выполнения, включая положение линии с якорной привязкой, проверенный старший таймфрейм (HTF), интерактивно задаваемое количество ценовых интервалов, буфер пользовательского ввода и состояние режима редактирования.
//--- GLOBAL STATE VARIABLES datetime anchorTime; ENUM_TIMEFRAMES htf = inpHtf; int indSubWindow = -1; int interactiveBins = 20; string inputBuffer = ""; bool editMode = false;
Вспомогательные функции
Для повышения модульности и обеспечения четкого разделения обязанностей в ходе реализации используется ряд вспомогательных функций. Эти вспомогательные функции инкапсулируют повторно используемые операции, такие как проверка состояния свечей, анализ локальных экстремумов, управление массивами и визуализация событий снятия ликвидности.- Определение старшего таймфрейма (HTF)
Эта функция автоматически определяет ближайший допустимый таймфрейм старше текущего таймфрейма графика, обеспечивая содержательный мультитаймфреймовый (MTF) анализ. Если пользователь указывает недопустимый таймфрейм или таймфрейм, совпадающий с текущим, функция автоматически выбирает ближайший доступный старший таймфрейм (HTF), предотвращая некорректное поведение индикатора.
//+------------------------------------------------------------------+ //| NEXT HIGHER TIMEFRAME DETECTION | //+------------------------------------------------------------------+ ENUM_TIMEFRAMES getNextHFT() { //--- ENUM_TIMEFRAMES timeframes[] = {PERIOD_M1, PERIOD_M2, PERIOD_M3, PERIOD_M4, PERIOD_M5, PERIOD_M6, PERIOD_M10, PERIOD_M12, PERIOD_M15, PERIOD_M20, PERIOD_M30, PERIOD_H1, PERIOD_H2, PERIOD_H3, PERIOD_H4, PERIOD_H6, PERIOD_H8, PERIOD_H12, PERIOD_D1, PERIOD_W1, PERIOD_MN1 }; int chartSecs = PeriodSeconds(); //--- FIND NEXT HIGHER THAN CHART TF for(int w = 0; w < ArraySize(timeframes); w++) { if(PeriodSeconds(timeframes[w]) > chartSecs) return timeframes[w]; } //--- FALLBACK return timeframes[ArraySize(timeframes) - 1]; }
- Определение видимой области графика
Определение видимой области графика позволяет установить текущий видимый диапазон и ограничить все расчеты объема той частью графика, которую просматривает пользователь. Функция получает первый видимый бар и количество видимых баров, определяет соответствующие временные границы начала и конца диапазона и возвращает фактическое количество видимых баров. Эти значения задают динамический диапазон анализа, используемый во всем индикаторе, позволяя профилям объема автоматически адаптироваться при прокрутке графика или изменении его масштаба.
//+------------------------------------------------------------------+ //| VIEWPORT DETECTION | //+------------------------------------------------------------------+ int getViewportValues(datetime &start, datetime &end) { //--- int first = (int) ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); int visibleBars = (int) ChartGetInteger(0, CHART_VISIBLE_BARS); int last = first - visibleBars + 2; if(last < 0) last = 0; visibleBars = visibleBars - 2; start = iTime(_Symbol, PERIOD_CURRENT, last); end = iTime(_Symbol, PERIOD_CURRENT, first); return visibleBars; }
- Нормализация времени и объема
Надежное построение профиля объема зависит от точного выравнивания по времени и единообразного масштабирования объема. В этом разделе представлены вспомогательные функции для приведения временных меток к времени допустимых баров графика и масштабирования накопленного объема, что обеспечивает надежность вычислений, синхронизированную мультитаймфреймовую (MTF) обработку и стабильную отрисовку профиля.
//+------------------------------------------------------------------+ //| CHART ALIGNMENT | //+------------------------------------------------------------------+ datetime normalizeToChartTime(datetime time, ENUM_TIMEFRAMES tf = PERIOD_CURRENT) { //--- int shift = iBarShift(_Symbol, tf, time); if(shift < 0) shift = 0; return iTime(_Symbol, tf, shift); } //+------------------------------------------------------------------+ //| 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 } //+------------------------------------------------------------------+ //| VOLUME NORMALIZATION | //+------------------------------------------------------------------+ double normalizeMinMax(const long value, const long maxVol, const long minVol) { //--- if(maxVol <= minVol) return 0; return (double)(value - minVol) / (maxVol - minVol); }Пояснение
- Выравнивание по барам графика
Эта функция приводит произвольную временную метку к времени ближайшего существующего бара на указанном таймфрейме, обеспечивая синхронизацию всех операций, связанных со временем, с доступными рыночными данными. - Определение времени середины диапазона
Чтобы установить начальное положение линии с якорной привязкой в центре видимого диапазона, функция вычисляет время его середины и приводит его к времени ближайшего допустимого бара. Это также помогает избежать непреднамеренного размещения линии с якорной привязкой в будущем относительно текущего времени. - Нормализация объема
Здесь мы применяем минимаксную нормализацию к накопленному объему, масштабируя значения в диапазон [0, 1] и предотвращая ошибки деления на ноль для стабильного отображения профиля.
- Обнаружение двойного нажатия клавиши
Мы определяем функцию, которая проверяет, произошли ли два последовательных нажатия клавиши в течение заданного временного интервала, обеспечивая надежное обнаружение двойного нажатия для таких действий, как включение режима редактирования (E) и подтверждение пользовательского ввода (S).
//+------------------------------------------------------------------+ //| KEYSTROKE DOUBLE-CLICK DETECTION | //+------------------------------------------------------------------+ bool isDoubleClick(ulong &lastClick) { //--- ulong now = GetTickCount(); bool doubleClick = (now - lastClick <= _DBL_CLICK_TIME); lastClick = now; return doubleClick; }
- Визуализация
Эти вспомогательные функции инкапсулируют процесс создания графических объектов индикатора, включая интерактивную линию с якорной привязкой, границы профиля и прямоугольники профиля объема. Примечательно, что и createHLine(), и createRect() принимают в качестве параметра индекс подокна индикатора, что позволяет использовать одну и ту же логику отрисовки как для основного графика, так и для подокна индикатора.
//+------------------------------------------------------------------+ //| VERTICAL LINE CREATION | //+------------------------------------------------------------------+ bool createVLine(const string objName, const datetime time, const color clr, const string tooltip = "\n", const bool selected = false) { //--- if(ObjectCreate(0, objName, OBJ_VLINE, 0, time, 0)) { ObjectSetInteger(0, objName, OBJPROP_COLOR, clr); ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, true); ObjectSetInteger(0, objName, OBJPROP_SELECTED, selected); ObjectSetInteger(0, objName, OBJPROP_WIDTH, 2); ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true); ObjectSetString(0, objName, OBJPROP_TOOLTIP, tooltip); ChartRedraw(); return true; } return false; } //+------------------------------------------------------------------+ //| HORIZONTAL LINE CREATION | //+------------------------------------------------------------------+ bool createHLine(const string objName, const double price, const int subWind, const color clr, const string tooltip) { //--- if(ObjectCreate(0, objName, OBJ_HLINE, subWind, 0, price)) { ObjectSetInteger(0, objName, OBJPROP_COLOR, clr); ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1); ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true); ObjectSetString(0, objName, OBJPROP_TOOLTIP, tooltip); return true; } return false; } //+------------------------------------------------------------------+ //| RECTANGLE CREATION | //+------------------------------------------------------------------+ bool createRect(const string objName, const datetime time1, const double price1, const datetime time2, const double price2, const int subWind, const color clr) { //--- if(ObjectCreate(0, objName, OBJ_RECTANGLE, subWind, 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); ObjectSetString(0, objName, OBJPROP_TOOLTIP, "\n"); return true; } return false; }
- Расчет профиля объема
При наличии необходимых рыночных данных следующим шагом становится реализация основного вычислительного модуля индикатора. Используя заданный пользователем диапазон анализа и интерактивную настройку ценовых интервалов, этот модуль строит синхронизированные профили объема младшего таймфрейма (LTF) и старшего таймфрейма (HTF), накапливая объем, определяя преобладание объема и отображая пропорциональные блоки профилей вместе с соответствующими уровнями точки контроля (POC).
//+------------------------------------------------------------------+ //| HTF VOLUME PROFILE COMPUTATION | //+------------------------------------------------------------------+ void htfVolumeProfile(const datetime vpAnchor, const int maxProfile, const int minProfile) { //--- datetime viewStart = 0, viewEnd = 0; int count = getViewportValues(viewStart, viewEnd); viewStart = normalizeToChartTime(viewStart, htf); viewEnd = normalizeToChartTime(vpAnchor, htf); count = MathAbs(iBarShift(_Symbol, htf, viewStart) - iBarShift(_Symbol, htf, viewEnd)) + 1; int firstBar = -1, highestIndex = -1, lowestIndex = -1; int numberOfBins = -1; bool useRealVol = false; double rangeHigh = EMPTY_VALUE, rangeLow = EMPTY_VALUE; double binHigh = EMPTY_VALUE, binLow = EMPTY_VALUE; long maxVol = 0, minVol = LONG_MAX; int extendBars = 0; color clr = clrNONE; double dominance = -1; MqlRates rates[]; ArraySetAsSeries(rates, true); st_priceVolume bins[]; //--- ENSURE DATA IS SYNCHRONIZED while(!SeriesInfoInteger(_Symbol, htf, SERIES_SYNCHRONIZED) && !IsStopped()) { Sleep(100); } //--- CLEAR SUB WINDOW ObjectsDeleteAll(0, _HTF_VOLUME_PROFILE); ChartRedraw(); if(CopyRates(_Symbol, htf, viewStart, viewEnd, rates) > 0) { firstBar = iBarShift(_Symbol, htf, viewStart); highestIndex = iHighest(_Symbol, htf, MODE_HIGH, count, firstBar); lowestIndex = iLowest(_Symbol, htf, MODE_LOW, count, firstBar); //--- rangeHigh = iHigh(_Symbol, htf, highestIndex); rangeLow = iLow(_Symbol, htf, lowestIndex); //--- INTERACTIVE BIN CONSTRUCTION numberOfBins = interactiveBins; double step = (rangeHigh - rangeLow) / numberOfBins; ArrayResize(bins, numberOfBins); useRealVol = (rates[0].real_volume > 0); //--- VOLUME ACCUMULATION for(int w = 0; w < numberOfBins; w++) { binLow = rangeLow + step * w; binHigh = binLow + step; //--- for(int c = 0; c < ArraySize(rates); c++) { if(rates[c].close >= binLow && rates[c].close <= binHigh) { bins[w].volume += (useRealVol) ? rates[c].real_volume : rates[c].tick_volume; bins[w].price = rates[c].close; } } if(bins[w].volume < 1) bins[w].volume = 1; } //--- VOLUME EXTREMES 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; } bool pocDrawn = false; //--- PROFILE RENDERING for(int p = 0; p < ArraySize(bins); p++) { binLow = rangeLow + step * p; binHigh = binLow + step; extendBars = minProfile + (int)MathRound(normalizeMinMax(bins[p].volume, maxVol, minVol) * (maxProfile - minProfile)); dominance = (double)extendBars / maxProfile; clr = (dominance >= 0.85) ? clrGreen : (dominance >= 0.65) ? clrPurple : (dominance >= 0.45) ? clrCoral : (dominance >= 0.25) ? clrRed : (dominance >= 0.10) ? clrBurlyWood : clrDarkSlateGray; createRect(_HTF_VOLUME_PROFILE + (string)p, vpAnchor, binLow, vpAnchor + (PeriodSeconds()*extendBars), binHigh, indSubWindow, clr); if(bins[p].volume == maxVol && !pocDrawn) { //--- DRAW MAXIMUM VOLUME'S POC : ONCE createHLine(_HTF_VOLUME_PROFILE + "POC", bins[p].price, indSubWindow, clr, "HTF POC"); pocDrawn = true; } } } } //+------------------------------------------------------------------+ //| CORE VOLUME PROFILE COMPUTATIONS | //+------------------------------------------------------------------+ void calcVolumeProfile(const datetime vpAnchor, const bool vpSelected) { //--- datetime viewStart = 0, viewEnd = 0; int count = getViewportValues(viewStart, viewEnd); int firstBar = -1, highestIndex = -1, lowestIndex = -1; int numberOfBins = -1; bool useRealVol = false; double rangeHigh = EMPTY_VALUE, rangeLow = EMPTY_VALUE; double binHigh = EMPTY_VALUE, binLow = EMPTY_VALUE; long maxVol = 0, minVol = LONG_MAX; int extendBars = 0, maxProfileLength = 0, minProfileLength = 0; color clr = clrNONE; double dominance = -1; MqlRates rates[]; ArraySetAsSeries(rates, true); st_priceVolume bins[]; createVLine(_VP_ANCHOR, vpAnchor, clrBlack, "VP Anchor", vpSelected); count = MathAbs(iBarShift(_Symbol, PERIOD_CURRENT, viewStart) - iBarShift(_Symbol, PERIOD_CURRENT, vpAnchor)) + 1; //--- ENSURE DATA IS SYNCHRONIZED while(!SeriesInfoInteger(_Symbol, PERIOD_CURRENT, SERIES_SYNCHRONIZED) && !IsStopped()) { Sleep(100); } //--- CLEAR MAIN CHART ObjectsDeleteAll(0, _LTF_VOLUME_PROFILE); ChartRedraw(); if(CopyRates(_Symbol, PERIOD_CURRENT, viewStart, vpAnchor, rates) > 0) { firstBar = iBarShift(_Symbol, PERIOD_CURRENT, viewStart); highestIndex = iHighest(_Symbol, PERIOD_CURRENT, MODE_HIGH, count, firstBar); lowestIndex = iLowest(_Symbol, PERIOD_CURRENT, MODE_LOW, count, firstBar); //--- rangeHigh = iHigh(_Symbol, PERIOD_CURRENT, highestIndex); rangeLow = iLow(_Symbol, PERIOD_CURRENT, lowestIndex); //--- DYNAMIC SUB-WINDOW PRICE SCALING IndicatorSetDouble(INDICATOR_MAXIMUM, rangeHigh + (100 * _Point)); IndicatorSetDouble(INDICATOR_MINIMUM, rangeLow - (100 * _Point)); //--- INTERACTIVE BIN CONSTRUCTION numberOfBins = interactiveBins; double step = (rangeHigh - rangeLow) / numberOfBins; ArrayResize(bins, numberOfBins); useRealVol = (rates[0].real_volume > 0); //--- VOLUME ACCUMULATION for(int w = 0; w < numberOfBins; w++) { binLow = rangeLow + step * w; binHigh = binLow + step; //--- for(int c = 0; c < ArraySize(rates); c++) { if(rates[c].close >= binLow && rates[c].close <= binHigh) { bins[w].volume += (useRealVol) ? rates[c].real_volume : rates[c].tick_volume; bins[w].price = rates[c].close; } } if(bins[w].volume < 1) bins[w].volume = 1; } //--- VOLUME EXTREMES 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 maxProfileLength = (int)(count * 0.4); maxProfileLength = MathMax(4, maxProfileLength); minProfileLength = (int)MathRound(maxProfileLength * 0.05); minProfileLength = MathMax(2, minProfileLength); bool pocDrawn = false; //--- PROFILE RENDERING 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)); //--- AVOID ZERO DIVISION ERROR if(maxProfileLength > 0) dominance = (double)extendBars / maxProfileLength; clr = (dominance >= 0.85) ? clrFireBrick : (dominance >= 0.65) ? clrBlue : (dominance >= 0.45) ? clrChocolate : (dominance >= 0.25) ? clrBrown : (dominance >= 0.10) ? clrPaleVioletRed : clrOlive; createRect(_LTF_VOLUME_PROFILE + (string)p, vpAnchor, binLow, vpAnchor + (PeriodSeconds()*extendBars), binHigh, 0, clr); if(bins[p].volume == maxVol && !pocDrawn) { //--- DRAW MAXIMUM VOLUME'S POC : ONCE createHLine(_LTF_VOLUME_PROFILE + "POC", bins[p].price, 0, clr, "LTF POC"); pocDrawn = true; } } htfVolumeProfile(vpAnchor, maxProfileLength, minProfileLength); ChartRedraw(); } }
Пояснение
- htfVolumeProfile()
Вычисляет профиль объема старшего таймфрейма (HTF), синхронизируя данные старшего таймфрейма с заданным пользователем диапазоном анализа, накапливая объем по интерактивно настраиваемым ценовым интервалам, масштабируя длины элементов профиля на основе нормализованного преобладания объема и отображая полученный профиль вместе с его точкой контроля (POC) старшего таймфрейма в подокне индикатора.
- calcVolumeProfile()
Выполняет основную процедуру координации работы индикатора. Эта функция определяет диапазон анализа, вычисляет профиль объема младшего таймфрейма (LTF), динамически масштабирует подокно индикатора, отображает профиль LTF и точку контроля (POC) на основном графике и, наконец, запускает расчет для старшего таймфрейма (HTF), формируя синхронизированные мультитаймфреймовые (MTF) профили объема.
Логика инициализации (OnInit)
Создав вспомогательные функции, мы теперь реализуем стандартные обработчики событий MQL5, начиная с обработчика OnInit(). В ходе инициализации индикатор проверяет корректность старшего таймфрейма, настраивает параметры графика, определяет начальную видимую область графика и положение линии с якорной привязкой, а также отображает первый синхронизированный профиль объема, формируя рабочее состояние для последующего взаимодействия с пользователем.
//+------------------------------------------------------------------+ //| INITIALIZATION FUNCTION | //+------------------------------------------------------------------+ int OnInit() { //--- ROBUST INPUT TIMEFRAME VALIDATION if(htf <= _Period) htf = getNextHFT(); //--- CHART AND INDICATOR SETUP ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); IndicatorSetString(INDICATOR_SHORTNAME, _PROG_NAME + " " + EnumToString(htf)); indSubWindow = ChartWindowFind(0, _PROG_NAME + " " + EnumToString(htf)); //--- RENDER VOLUME PROFILE datetime start, end; getViewportValues(start, end); anchorTime = getMiddleCandleTime(start, end); calcVolumeProfile(anchorTime, false); return(INIT_SUCCEEDED); }
Очистка (OnDeinit)
При удалении индикатора функция OnDeinit() восстанавливает измененные настройки графика, удаляет все объекты, созданные программой, и обновляет график, обеспечивая корректное и согласованное завершение работы.
//+------------------------------------------------------------------+ //| DEINITIALIZATION FUNCTION | //+------------------------------------------------------------------+ void OnDeinit(const int32_t reason) { //--- CHART SETTING RESTORATION AND CLEAN UP ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); ObjectsDeleteAll(0, _PROG_NAME); ChartRedraw(); }
Основная функция индикатора
Функция OnCalculate() отслеживает поступление новых рыночных данных и пересчитывает профили объема только при формировании нового бара, сохраняя текущее состояние выбора линии с якорной привязкой и избегая ненужных вычислений.//+------------------------------------------------------------------+ //| 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(prev_calculated > 0 && rates_total != prev_calculated) { bool isSelected = (bool) ObjectGetInteger(0, _VP_ANCHOR, OBJPROP_SELECTED); calcVolumeProfile(anchorTime, isSelected); } return(rates_total); }
Интерактивный механизм
Интерактивный механизм реализован в рамках функции OnChartEvent(), что позволяет индикатору реагировать на действия пользователя и события на графике в режиме реального времени. Он координирует управление якорем, интерактивное настройка количества ценовых интервалов (бинов) профиля, синхронизацию с видимой областью графика и автоматическое восстановление графических объектов, обеспечивая быстрый отклик на действия пользователя.
//+------------------------------------------------------------------+ //| CHARTEVENT FUNCTION | //+------------------------------------------------------------------+ void OnChartEvent(const int32_t id, const long &lparam, const double &dparam, const string &sparam) { //--- CHART CHANGE if(id == CHARTEVENT_CHART_CHANGE) { bool isSelected = (bool) ObjectGetInteger(0, _VP_ANCHOR, OBJPROP_SELECTED); //--- OTHER SUB-INDICATOR DELETION, CHART SCROLL, AND ZOOM calcVolumeProfile(anchorTime, isSelected); //--- UPDATE INDICATOR SUB WINDOW indSubWindow = ChartWindowFind(0, _PROG_NAME + " " + EnumToString(htf)); } //--- INTERACTIVE BIN EDITING if(id == CHARTEVENT_KEYDOWN) { int key = (int)lparam; static ulong lastEClick = 0; static ulong lastSClick = 0; if((bool)ObjectGetInteger(0, _VP_ANCHOR, OBJPROP_SELECTED)) { //--- EDIT-SAVE MECHANISM if(key == _KEY_E || key == _KEY_S) { switch(key) { //--- EDIT MODE case _KEY_E: if(isDoubleClick(lastEClick)) { inputBuffer = ""; editMode = true; Print("Edit mode activated"); } break; //--- SAVE INPUT case _KEY_S: if(isDoubleClick(lastSClick)) { //--- GET NUMBER OF BINS FROM USER INPUT int temp = (int) StringToInteger(inputBuffer); //--- CLAMP (5 - 100) temp = (temp < 5) ? 5 : (temp > 100) ? 100 : temp; //--- IF NO INPUT, USE LAST VALUE IN MEMORY interactiveBins = (inputBuffer == "") ? interactiveBins : temp; Print("Saved: [", interactiveBins, "]"); editMode = false; } break; } } //--- CAPTURE USER INPUTS if(editMode) { //--- NUMERIC INPUTS (0 - 9) if(key >= 48 && key <= 57) { inputBuffer += CharToString((char)key); } } } } //--- if(sparam != _VP_ANCHOR) return; switch(id) { //--- ANCHOR LINE DRAG-AND-DROP case CHARTEVENT_OBJECT_DRAG: { anchorTime = (datetime)ObjectGetInteger(0, _VP_ANCHOR, OBJPROP_TIME); anchorTime = normalizeToChartTime(anchorTime); //--- ENHANCED USER-EXPERIENCE if(anchorTime > iTime(_Symbol, PERIOD_CURRENT, 1)) { datetime start = 0, end = 0; getViewportValues(start, end); anchorTime = getMiddleCandleTime(start, end); } calcVolumeProfile(anchorTime, true); break; } //--- ANCHOR RESTORATION case CHARTEVENT_OBJECT_DELETE: { Print("Volume Profile Anchor line deleted. RESTORED"); //--- RESTORE IMMEDIATELY createVLine(_VP_ANCHOR, anchorTime, clrBlack, "VP Anchor", true); break; } } }
Пояснение
- Обработка изменений на графике
Реагирует на масштабирование, прокрутку и изменения видимой области графика, пересчитывая профили объема и обновляя ссылку на подокно индикатора.
- Интерактивную настройку количества ценовых интервалов (бинов) профиля
Обрабатывает двойные нажатия клавиш E и S для включения режима редактирования и сохранения заданного пользователем количества ценовых интервалов, а также считывает последующие нажатия цифровых клавиш.
- Обработка перетаскивания линии с якорной привязкой
Обрабатывает события перетаскивания линии с якорной привязкой, выполняет приведение времени линии с якорной привязкой к времени допустимого бара графика, предотвращает размещение линии с якорной привязкой в будущем и немедленно перестраивает синхронизированные профили объема.
- Восстановление линии с якорной привязкой
Отслеживает события удаления линии с якорной привязкой и автоматически воссоздает линию. с якорной привязкой, сохраняя целостность индикатора и непрерывность взаимодействия с ним.
Тестирование индикатора
После успешной компиляции индикатора без ошибок на приведенном ниже GIF-изображении демонстрируется его интерактивное поведение в режиме реального времени. В нем демонстрируются построение профиля объема на основе линии с якорной привязкой, надежная проверка мультитаймфреймовых (MTF) настроек с автоматическим переходом на старший таймфрейм (HTF), а также синхронное обновление профилей объема младшего таймфрейма (LTF) и старшего таймфрейма (HTF) при перемещении линии с якорной привязкой, что подтверждает корректность реализации и быстроту ее отклика.

Тестирование индикатора
Заключение
В этой статье мы реализовали на MQL5 интерактивный мультитаймфреймовый (MTF) профиль объема AnchorFlow, который расширяет возможности встроенного индикатора объема MetaTrader 5 за счет синхронизированного событийно-ориентированного анализа объема. Разработанная система не ограничивается гистограммой одного таймфрейма: она позволяет пользователям интерактивно задавать диапазон анализа, строить синхронизированные профили объема на нескольких таймфреймах и наглядно отображать распределение объема непосредственно на графике.
Основные реализованные возможности:
- Построение профиля объема с якорной привязкой — перетаскиваемая якорная линия задает начало диапазона анализа, а видимая область графика определяет его конец, предоставляя пользователям полный контроль над областью построения профиля.
- Интерактивное формирование ценовых интервалов — механизм редактирования с помощью клавиатуры, позволяющий пользователям динамически задавать количество ценовых интервалов профиля объема двойными нажатиями клавиш E и S, с автоматическим возвратом к последнему допустимому значению.
- Надежная проверка мультитаймфреймовых (MTF) настроек — автоматическое определение и выбор ближайшего старшего таймфрейма (HTF) при указании недопустимого таймфрейма, что обеспечивает надежную синхронизацию.
- Синхронизированное мультитаймфреймовое (MTF) отображение объема — одновременное отображение профилей объема младшего таймфрейма (LTF) на основном графике и профилей объема старшего таймфрейма (HTF) в отдельном подокне индикатора, причем оба профиля строятся для одного и того же диапазона анализа.
- Событийно-ориентированные обновления — автоматический пересчет при перемещении линии с якорной привязкой, масштабировании и прокрутке графика, восстановлении графических объектов и формировании нового бара, обеспечивающий быстрый отклик и плавное взаимодействие с пользователем.
Благодаря сочетанию интерактивных элементов управления, надежной обработки таймфреймов, синхронизированного отображения и обновлений в режиме реального времени эта система демонстрирует, как с помощью MQL5 можно разрабатывать профессиональные мультитаймфреймовые (MTF) инструменты анализа объема, далеко выходящие за рамки возможностей встроенного индикатора объема MetaTrader 5. При этом система служит прочной основой для более сложных средств аналитики и торговых систем, использующих данные об объеме.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23391
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
По ряду причин этот индикатор пока не является полноценным:
1.) Не поддерживает профиль объема в режиме реального времени
2.) Он работает только на одном таймфрейме — H4
Это фиктивный индикатор