English Deutsch 日本語
preview
Создание пользовательских индикаторов в MQL5 (Часть 8): Интеграция объёма для углублённого анализа рыночного профиля (Market Profile)

Создание пользовательских индикаторов в MQL5 (Часть 8): Интеграция объёма для углублённого анализа рыночного профиля (Market Profile)

MetaTrader 5Торговые системы |
186 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В нашей предыдущей статье (Часть 7), мы разработали гибридный Time Price Opportunity индикатор рыночного профиля (TPO) в MetaQuotes Language 5 (MQL5), который поддерживал несколько таймфреймов сессий, включая внутридневные, дневные, недельные, месячные и фиксированные периоды с учётом часового пояса, дискретизировал цены по сетке, отслеживал данные сессии по максимумам, минимумам, ценам открытия и закрытия, рассчитывал точку контроля (POC) и область стоимости по количеству TPO, а также отрисовывал профиль на графике с настраиваемыми цветами для детального анализа структуры сессий.

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

  1. Интеграция объёма и расширенных возможностей в гибридные рыночные TPO-профили
  2. Реализация в MQL5
  3. Бэктестинг
  4. Заключение

К концу статьи у вас будет расширенный индикатор MQL5 для гибридных рыночных профилей Time Price Opportunity с расширенным анализом на основе объёма, готовый к дальнейшей настройке. Приступим!


Интеграция объёма и расширенных возможностей в гибридные рыночные TPO-профили

Интеграция данных объёма в гибридные Time Price Opportunity рыночные профили (TPO) расширяет традиционный анализ цены и времени за счёт учёта торгового объёма на каждом ценовом уровне. Это позволяет определять точку контроля по объёму (volume-based POC), то есть уровень, где прошёл наибольший объём, и корректировать область стоимости так, чтобы она отражала реальное участие рынка, а не только время, проведённое ценой на уровне. Такое улучшение позволяет оценить степень убеждённости участников рынка и то, насколько движение подтверждено объёмом: например, позволяет отличать высокообъёмные зоны справедливой цены (fair value), которые могут выступать поддержкой или сопротивлением, от низкообъёмных областей, указывающих на слабый интерес или возможные пробои. При этом такие элементы, как средневзвешенная по объёму цена (VWAP), дают динамический ориентир для оценки внутридневного направления рынка.

Мы расширим базовое управление сессиями накоплением объёма на каждом ценовом уровне, добавим опциональный расчёт объёмной точки контроля и средневзвешенной по объёму цены (VWAP), определим начальный баланс по ранним периодам, добавим отрисовку линий продолжения для ключевых уровней, таких как максимум, минимум, границы области стоимости и срединный уровень, включим варианты символов и способы обозначения цен открытия, а также включим режим split-профиля с отступами для более понятного отображения букв. Всё это будет поддерживать фоны фиксированных диапазонов и метки объёма для полной визуализации профиля. Ниже кратко показано визуальное представление наших целей.

VOLUME MARKET PROFILE ROADMAP


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

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

//+------------------------------------------------------------------+
//|                             Hybrid TPO Market Profile PART 2.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots 0

//+------------------------------------------------------------------+
//| Enums                                                            |
//+------------------------------------------------------------------+
enum MarketProfileTimeframe { // Define market profile timeframe enum
   INTRADAY,                  // Intraday
   DAILY,                     // Daily
   WEEKLY,                    // Weekly
   MONTHLY,                   // Monthly
   FIXED                      // Fixed
};

enum TpoCharacterType {      // Define TPO character type enum
   SQUARE,                   // ■ Square
   CIRCLE,                   // ● Circle
   ALPHABETIC                // A-Za-z
};

enum MidpointAlgorithm {     // Define midpoint algorithm enum
   HIGH_LOW_MID,             // High/Low mid
   TPO_COUNT_BASED           // Number of TPOs
};

enum MarkPeriodOpens {       // Define mark period opens enum
   NONE,                     // No
   SWAP_CASE,                // Swap case
   USE_ZERO                  // Use '0'
};

enum TextSize {              // Define text size enum
   TINY,                     // Tiny
   SMALL,                    // Small
   NORMAL                    // Normal
};

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
sinput group "Settings"
input double ticksPerTpoLetter = 10;             // Ticks per letter
input bool highlightVolumeProfilePoc = true;     // Highlight POC based on VP?
input bool useVolumeProfilePocForValueArea = true; // Use VP POC for Value Area?
input bool highlightSessionVwap = false;         // Highlight session VWAP?
input bool showExtensionLines = false;           // Show extension lines?
input bool splitProfile = false;                 // Split MP?

sinput group "Time"
input MarketProfileTimeframe profileTimeframe = DAILY; // Timeframe
input string timezone = "Exchange";              // Timezone
input string dailySessionRange = "0830-1500";    // Daily session
input int intradayProfileLengthMinutes = 60;     // Profile length in minutes (Intraday)
input datetime fixedTimeRangeStart = D'2026.02.01 08:30'; // From (Fixed)
input datetime fixedTimeRangeEnd = D'2026.02.02 15:00'; // Till (Fixed)
input bool renderVolumes = true;                 // Render volume numbers? (Fixed)

sinput group "Rendering"
input TpoCharacterType tpoCharacterType = SQUARE; // TPO characters
input int valueAreaPercent = 70;                 // Value Area Percent
input int initialBalancePeriods = 2;             // IB periods
input int initialBalanceLineWidth = 2;           // IB line width
input int priceMarkerWidth = 2;                  // Price marker width
input int priceMarkerLength = 1;                 // Price marker length
input TextSize textSize = NORMAL;                // Font size
input MarkPeriodOpens markPeriodOpens = NONE;    // Mark period open?
input MidpointAlgorithm midpointAlgorithm = HIGH_LOW_MID; // Midpoint algo

sinput group "Colors"
input color defaultTpoColor = clrGray;           // Default
input color singlePrintColor = 0xd56a6a;         // Single Print
input color valueAreaColor = clrBlack;           // Value Area
input color pointOfControlColor = 0x3f7cff;      // POC
input color volumeProfilePocColor = 0x87c74c;    // VP POC
input color openColor = clrDodgerBlue;           // Open
input color closeColor = clrRed;                 // Close
input color initialBalanceHighlightColor = clrDodgerBlue; // IB
input color initialBalanceBackgroundColor = 0x606D79;     // IB background
input color sessionVwapColor = 0xFF9925;                  // Session VWAP
input color pocExtensionColor = 0x87c74c;                 // POC extension
input color valueAreaHighExtensionColor = clrBlack;       // VAH extension
input color valueAreaLowExtensionColor = clrBlack;        // VAL extension
input color highExtensionColor = clrRed;                  // High extension
input color lowExtensionColor = clrGreen;                 // Low extension
input color midpointExtensionColor = 0x7649ff;            // Midpoint extension
input color fixedRangeBackgroundColor = 0x3179f5;         // Fixed range background

//+------------------------------------------------------------------+
//| Constants                                                        |
//+------------------------------------------------------------------+
#define MAX_BARS_BACK 5000
#define TPO_CHARACTERS_STRING "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

//+------------------------------------------------------------------+
//| Structures                                                       |
//+------------------------------------------------------------------+
struct TpoPriceLevel {       // Define TPO price level structure
   double price;             // Store price level
   string tpoString;         // Store TPO string
   int tpoCount;             // Store TPO count
   double volume;            // Store volume
};

struct ProfileSessionData {  // Define profile session data structure
   datetime startTime;       // Store start time
   datetime endTime;         // Store end time
   double sessionOpen;       // Store session open price
   double sessionClose;      // Store session close price
   double sessionHigh;       // Store session high price
   double sessionLow;        // Store session low price
   double initialBalanceHigh;// Store initial balance high
   double initialBalanceLow; // Store initial balance low
   double vwap;              // Store VWAP
   int pointOfControlIndex;  // Store point of control index
   int volumeProfilePocIndex;// Store volume profile POC index
   TpoPriceLevel levels[];   // Store array of price levels
   int periodCount;          // Store period count
   double periodHighs[];     // Store array of period highs
   double periodLows[];      // Store array of period lows
   double periodOpens[];     // Store array of period opens
   double volumeProfilePrices[]; // Store array of volume profile prices
   double volumeProfileVolumes[]; // Store array of volume profile volumes
};

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
string objectPrefix = "HTMP_";     //--- Set object prefix
ProfileSessionData sessions[];     //--- Declare sessions array
int activeSessionIndex = -1;       //--- Initialize active session index
double tpoPriceGridStep = 0;       //--- Initialize TPO price grid step
string tpoCharacterSet[];          //--- Declare TPO character set array
datetime previousBarTime = 0;      //--- Initialize previous bar time
datetime lastCompletedBarTime = 0; //--- Initialize last completed bar time
bool isNewSession = false;         //--- Initialize new session flag
int labelFontSize = 10;            //--- Set label font size
int maxSessionHistory = 20;        //--- Set maximum session history
int timezoneOffsetSeconds = 0;     //--- Initialize timezone offset in seconds

Мы начинаем реализацию с определения дополнительных перечислений для расширения параметров настройки улучшенного гибридного рыночного профиля Time Price Opportunity. Перечисление "TpoCharacterType" предлагает варианты вроде "SQUARE" для блочных символов, "CIRCLE" для отображения точками и "ALPHABETIC" для буквенной маркировки, чтобы подстроиться под разные визуальные предпочтения вместо единственного ранее доступного варианта меток. Далее "MidpointAlgorithm" задаёт алгоритмы расчёта срединного уровня, например "HIGH_LOW_MID" на основе диапазона сессии или "TPO_COUNT_BASED" с использованием накопленного количества Time Price Opportunity. Мы добавляем "MarkPeriodOpens" с вариантами "NONE" для отключения маркировки, "SWAP_CASE" для смены регистра букв на открытиях и "USE_ZERO" для обозначения их символом нуля. Наконец, "TextSize" позволяет масштабировать текст с настройками "TINY", "SMALL" и "NORMAL".

Мы расширяем пользовательские входные параметры, сгруппированные по разделам, добавляя новые настройки: "highlightVolumeProfilePoc" и "useVolumeProfilePocForValueArea" для включения возможностей точки контроля по объёму, "highlightSessionVwap" для выделения средневзвешенной по объёму цены (VWAP), "showExtensionLines" для проекции ключевых уровней и "splitProfile" для раздельного отображения буквенного профиля. В группе времени добавляется "renderVolumes" для управления метками объёма в фиксированном диапазоне. В разделе отрисовки мы добавляем выбор типа TPO-символов, периоды начального баланса и ширину линии, размеры ценовых маркеров, размер текста, маркировку открытия периода и метод расчёта срединного уровня. Группа цветов расширяется входными параметрами для точки контроля объёмного профиля, маркеров открытия, подсветки и фона начального баланса, сессионной средневзвешенной по объёму цены (VWAP), различных линий продолжения и фонов фиксированного диапазона.

Мы обновляем структуры для поддержки интеграции объёма: "TpoPriceLevel" теперь включает поле объёма наряду с ценой, строкой Time Price Opportunity и счётчиком; "ProfileSessionData" добавляет максимум и минимум начального баланса, средневзвешенную цену, индекс точки контроля объёмного профиля, массивы максимумов и минимумов периодов, а также отдельные массивы цен и объёмов объёмного профиля. Соответственно корректируются глобальные переменные: вводится флаг "isNewSession", а "labelFontSize" будет задаваться динамически позже. При этом сохраняются такие элементы, как префикс объектов, массив сессий, активный индекс, шаг сетки, набор символов, временные метки, лимит истории и смещение часового пояса. Далее обновим обработчик инициализации, задав размер шрифта следующим образом. Для наглядности мы выделили конкретные изменённые строки.

//+------------------------------------------------------------------+
//| Initialize custom indicator                                      |
//+------------------------------------------------------------------+
int OnInit() {
   IndicatorSetString(INDICATOR_SHORTNAME, "Hybrid TPO Market Profile"); //--- Set indicator short name
   
   tpoPriceGridStep = ticksPerTpoLetter * _Point; //--- Calculate TPO price grid step
   
   ArrayResize(tpoCharacterSet, 52);              //--- Resize TPO character set array
   for(int i = 0; i < 52; i++) {                  //--- Loop through characters
      tpoCharacterSet[i] = StringSubstr(TPO_CHARACTERS_STRING, i, 1); //--- Assign character to array
   }
   
   switch(textSize) {                             //--- Switch on text size
      case TINY: labelFontSize = 7; break;        //--- Set tiny font size
      case SMALL: labelFontSize = 9; break;       //--- Set small font size
      case NORMAL: labelFontSize = 11; break;     //--- Set normal font size
   }
   
   if(timezone != "Exchange") {                    //--- Check if timezone is not exchange
      string tzString = StringSubstr(timezone, 3); //--- Extract timezone string
      int offset = (int)StringToInteger(tzString); //--- Convert offset to integer
      timezoneOffsetSeconds = offset * 3600;       //--- Calculate timezone offset in seconds
   }
   
   ArrayResize(sessions, 0);                       //--- Resize sessions array to zero
   
   return(INIT_SUCCEEDED);                         //--- Return initialization success
}

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

//+------------------------------------------------------------------+
//| Delete session objects                                           |
//+------------------------------------------------------------------+
void DeleteSessionObjects(datetime sessionTime) {
   string sessionString = IntegerToString(sessionTime); //--- Convert session time to string
   
   int total = ObjectsTotal(0, 0, -1);            //--- Get total number of objects
   for(int i = total - 1; i >= 0; i--) {          //--- Loop through objects in reverse
      string name = ObjectName(0, i, 0, -1);      //--- Get object name
      if(StringFind(name, objectPrefix) == 0 && StringFind(name, sessionString) > 0) //--- Check if matches session
         ObjectDelete(0, name);                   //--- Delete object
   }
}

//+------------------------------------------------------------------+
//| Create new session                                               |
//+------------------------------------------------------------------+
int CreateNewSession() {
   int size = ArraySize(sessions);                //--- Get size of sessions array
   
   if(size >= maxSessionHistory) {                //--- Check if size exceeds history limit
      DeleteSessionObjects(sessions[0].startTime); //--- Delete old session objects
      
      for(int i = 0; i < size - 1; i++) {         //--- Loop to shift sessions
         sessions[i] = sessions[i + 1];           //--- Copy next session to current
      }
      ArrayResize(sessions, size - 1);            //--- Resize sessions array
      size = size - 1;                            //--- Update size
   }
   
   ArrayResize(sessions, size + 1);               //--- Resize sessions array for new session
   int newIndex = size;                           //--- Set new index
   
   sessions[newIndex].startTime = 0;              //--- Initialize start time
   sessions[newIndex].endTime = 0;                //--- Initialize end time
   sessions[newIndex].sessionOpen = 0;            //--- Initialize session open
   sessions[newIndex].sessionClose = 0;           //--- Initialize session close
   sessions[newIndex].sessionHigh = 0;            //--- Initialize session high
   sessions[newIndex].sessionLow = 0;             //--- Initialize session low
   sessions[newIndex].initialBalanceHigh = 0;     //--- Initialize initial balance high
   sessions[newIndex].initialBalanceLow = 0;      //--- Initialize initial balance low
   sessions[newIndex].vwap = 0;                   //--- Initialize VWAP
   sessions[newIndex].pointOfControlIndex = -1;   //--- Initialize point of control index
   sessions[newIndex].volumeProfilePocIndex = -1; //--- Initialize volume profile POC index
   sessions[newIndex].periodCount = 0;            //--- Initialize period count
   ArrayResize(sessions[newIndex].levels, 0);     //--- Resize levels array
   ArrayResize(sessions[newIndex].periodHighs, 0);//--- Resize period highs array
   ArrayResize(sessions[newIndex].periodLows, 0); //--- Resize period lows array
   ArrayResize(sessions[newIndex].periodOpens, 0);//--- Resize period opens array
   ArrayResize(sessions[newIndex].volumeProfilePrices, 0); //--- Resize volume profile prices array
   ArrayResize(sessions[newIndex].volumeProfileVolumes, 0); //--- Resize volume profile volumes array
   
   return newIndex;                               //--- Return new index
}

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

Затем мы расширяем функцию "CreateNewSession" для управления новыми профилями, получая текущий размер массива сессий с помощью ArraySize; если достигнут лимит истории, мы вызываем "DeleteSessionObjects" для времени начала самой старой сессии, чтобы очистить визуальные элементы, сдвигаем оставшиеся сессии вперёд и уменьшаем размер массива. После этого расширяем массив для новой записи, инициализируем её поля, включая новые элементы вроде начального баланса, максимумов и минимумов, средневзвешенной по объёму цены (VWAP), индекса точки контроля объёмного профиля, а также сбрасываем счётчики и индексы. Дополнительно изменяем размеры массивов максимумов и минимумов периодов, цен и объёмов объёмного профиля, после чего возвращаем новый индекс. Теперь нам понадобятся вспомогательные функции для изменения регистра букв, которые будут использоваться при маркировке открытий периодов.

//+------------------------------------------------------------------+
//| Convert string to upper case                                     |
//+------------------------------------------------------------------+
string ConvertToUpperCase(string str) {
   string result = str;                           //--- Copy string
   StringToUpper(result);                         //--- Convert to upper case
   return result;                                 //--- Return result
}

//+------------------------------------------------------------------+
//| Convert string to lower case                                     |
//+------------------------------------------------------------------+
string ConvertToLowerCase(string str) {
   string result = str;                           //--- Copy string
   StringToLower(result);                         //--- Convert to lower case
   return result;                                 //--- Return result
}

//+------------------------------------------------------------------+
//| Check if character is upper case                                 |
//+------------------------------------------------------------------+
bool IsUpperCaseCharacter(string character) {
   return character == ConvertToUpperCase(character) && character != ConvertToLowerCase(character); //--- Check and return if upper case
}

Здесь мы добавляем вспомогательные функции для работы с регистром строк, чтобы поддержать такие возможности, как обозначение цен открытия периодов сменой регистра. Функция "ConvertToUpperCase" копирует входную строку и применяет StringToUpper для полного преобразования в верхний регистр перед возвратом результата. Аналогично "ConvertToLowerCase" дублирует строку и использует StringToLower для полного преобразования в нижний регистр. Чтобы определить регистр, "IsUpperCaseCharacter" проверяет, совпадает ли отдельный символ со своей версией в верхнем регистре через "ConvertToUpperCase" и отличается ли от формы в нижнем регистре через "ConvertToLowerCase"; для заглавных букв функция возвращает true. Далее мы изменим способ добавления TPO-символов к уровням, поскольку теперь доступны варианты с точками и квадратами.

//+------------------------------------------------------------------+
//| Add TPO character to level                                       |
//+------------------------------------------------------------------+
void AddTpoCharacterToLevel(int sessionIndex, int levelIndex, int periodIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if session index invalid
   if(levelIndex < 0 || levelIndex >= ArraySize(sessions[sessionIndex].levels)) return; //--- Return if level index invalid
   
   string tpoCharacter = "";                      //--- Initialize TPO character
   
   switch(tpoCharacterType) {                     //--- Switch on TPO character type
      case SQUARE:                                //--- Handle square case
         tpoCharacter = "■";                      //--- Set square character
         break;                                   //--- Exit case
      case CIRCLE:                                //--- Handle circle case
         tpoCharacter = "●";                      //--- Set circle character
         break;                                   //--- Exit case
      case ALPHABETIC:                            //--- Handle alphabetic case
         tpoCharacter = tpoCharacterSet[periodIndex % 52]; //--- Get alphabetic character
         
         if(markPeriodOpens != NONE && periodIndex < ArraySize(sessions[sessionIndex].periodOpens)) { //--- Check if mark opens and valid index
            double periodOpen = sessions[sessionIndex].periodOpens[periodIndex]; //--- Get period open
            double levelPrice = sessions[sessionIndex].levels[levelIndex].price; //--- Get level price
            
            if(MathAbs(levelPrice - periodOpen) < tpoPriceGridStep / 2) { //--- Check if matches open
               if(markPeriodOpens == SWAP_CASE) {                         //--- Check swap case
                  if(IsUpperCaseCharacter(tpoCharacter))                  //--- Check if upper
                     tpoCharacter = ConvertToLowerCase(tpoCharacter);     //--- Convert to lower
                  else                                                    //--- Handle lower
                     tpoCharacter = ConvertToUpperCase(tpoCharacter);     //--- Convert to upper
               } else if(markPeriodOpens == USE_ZERO) {                   //--- Check use zero
                  tpoCharacter = "0";                                     //--- Set zero character
               }
            }
         }
         break;                                                           //--- Exit case
   }
   
   sessions[sessionIndex].levels[levelIndex].tpoString += tpoCharacter;   //--- Append character to TPO string
   sessions[sessionIndex].levels[levelIndex].tpoCount++;                  //--- Increment TPO count
}

Здесь мы расширяем функцию "AddTpoCharacterToLevel", добавляя настраиваемые представления Time Price Opportunity и маркировку открытия. Сначала проверяем индексы сессии и уровня с помощью ArraySize и выходим раньше, если они недействительны. Мы инициализируем пустую строку TPO-символа, затем используем switch по перечислению "tpoCharacterType": для "SQUARE" или "CIRCLE" задаём фиксированные символы вроде квадрата или точки; для "ALPHABETIC" выбираем символ из набора по индексу периода с остатком от деления на 52. Для символов мы решили использовать жёстко заданные варианты, потому что они отображались лучше, чем символы из Wingdings, хотя при необходимости их всё равно можно использовать для большей гибкости.

Если выбран буквенный режим, "markPeriodOpens" не равен "NONE" и размер массива открытий периодов допустим, мы сравниваем цену уровня с ценой открытия периода с помощью MathAbs в пределах допуска в половину шага сетки. Если есть совпадение, применяем "SWAP_CASE", проверяя символ через "IsUpperCaseCharacter" и переключая регистр через "ConvertToLowerCase" или "ConvertToUpperCase", либо задаём ноль для "USE_ZERO". В конце добавляем символ к строке уровня и увеличиваем счётчик Time Price Opportunity для построения профиля. Теперь определим ещё одну вспомогательную функцию, которая добавляет пробелы в TPO-строки для разделения профилей, если эта возможность включена. Это создаёт split-профиль с более понятным разделением периодов при использовании букв.

//+------------------------------------------------------------------+
//| Pad levels for split profile                                     |
//+------------------------------------------------------------------+
void PadLevelsForSplitProfile(int sessionIndex) {
   if(!splitProfile || tpoCharacterType != ALPHABETIC) return;         //--- Return if not split or not alphabetic
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   
   int periodCount = sessions[sessionIndex].periodCount;               //--- Get period count
   int levelCount = ArraySize(sessions[sessionIndex].levels);          //--- Get level count
   
   for(int i = 0; i < levelCount; i++) {                               //--- Loop through levels
      int currentLength = StringLen(sessions[sessionIndex].levels[i].tpoString); //--- Get current length
      if(currentLength < periodCount) {                                //--- Check if needs padding
         for(int j = currentLength; j < periodCount; j++) {            //--- Loop to pad
            sessions[sessionIndex].levels[i].tpoString += " ";         //--- Add space
         }
      }
   }
}

Мы вводим функцию "PadLevelsForSplitProfile" для подготовки буквенных строк Time Price Opportunity к разделённому отображению профиля. Она сразу завершается, если режим split-профиля отключён, выбран небуквенный тип символов или индекс сессии выходит за допустимые границы массива sessions. Мы получаем количество периодов и уровней, затем проходим по каждому уровню, проверяя текущую длину строки с помощью StringLen; если она меньше количества периодов, дополняем её пробелами во внутреннем цикле, чтобы выровнять все строки до одинаковой длины и обеспечить постоянные интервалы для визуального разделения в отрисованном профиле. Далее нам нужно построить функцию, которая создаёт объёмный профиль по уровням и находит POC на основе объёма.

//+------------------------------------------------------------------+
//| Build volume profile and find POC                                |
//+------------------------------------------------------------------+
void BuildVolumeProfileAndFindPoc(int sessionIndex) {
   if(!highlightVolumeProfilePoc) {                //--- Check if not highlight VP POC
      if(sessionIndex >= 0 && sessionIndex < ArraySize(sessions)) //--- Check valid session
         sessions[sessionIndex].volumeProfilePocIndex = -1; //--- Reset VP POC index
      return;                                     //--- Return
   }
   
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   if(sessions[sessionIndex].startTime == 0) return; //--- Return if no start time
   
   ENUM_TIMEFRAMES lowerTimeframe = PERIOD_M1;    //--- Set default lower timeframe
   int currentTimeframeSeconds = PeriodSeconds(_Period); //--- Get current timeframe seconds
   
   if(currentTimeframeSeconds < 30 * 60) {        //--- Check for M1 lower
      lowerTimeframe = PERIOD_M1;                 //--- Set M1
   } else if(currentTimeframeSeconds < 60 * 60) { //--- Check for M3 lower
      lowerTimeframe = PERIOD_M3;                 //--- Set M3
   } else if(currentTimeframeSeconds == 60 * 60) {//--- Check for M10 lower
      lowerTimeframe = PERIOD_M10;                //--- Set M10
   } else {                                       //--- Default to H1
      lowerTimeframe = PERIOD_H1;                 //--- Set H1
   }
   
   if(PeriodSeconds(_Period) <= 60) {             //--- Check for 1-minute timeframe
      int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size
      for(int i = 0; i < size; i++) {             //--- Loop through levels
         int volumeProfileIndex = -1;             //--- Initialize VP index
         for(int j = 0; j < ArraySize(sessions[sessionIndex].volumeProfilePrices); j++) { //--- Loop through VP prices
            if(MathAbs(sessions[sessionIndex].volumeProfilePrices[j] - sessions[sessionIndex].levels[i].price) < _Point / 2) { //--- Check match
               volumeProfileIndex = j;            //--- Set index
               break;                             //--- Exit loop
            }
         }
         
         if(volumeProfileIndex == -1) {         //--- Check if new
            int volumeProfileSize = ArraySize(sessions[sessionIndex].volumeProfilePrices); //--- Get VP size
            ArrayResize(sessions[sessionIndex].volumeProfilePrices, volumeProfileSize + 1); //--- Resize prices
            ArrayResize(sessions[sessionIndex].volumeProfileVolumes, volumeProfileSize + 1); //--- Resize volumes
            sessions[sessionIndex].volumeProfilePrices[volumeProfileSize] = sessions[sessionIndex].levels[i].price; //--- Set price
            sessions[sessionIndex].volumeProfileVolumes[volumeProfileSize] = sessions[sessionIndex].levels[i].volume; //--- Set volume
         } else {                                 //--- Handle existing
            sessions[sessionIndex].volumeProfileVolumes[volumeProfileIndex] += sessions[sessionIndex].levels[i].volume; //--- Add volume
         }
      }
   } else {                                       //--- Handle higher timeframes
      datetime sessionEnd = (sessions[sessionIndex].endTime > 0) ? sessions[sessionIndex].endTime : TimeCurrent(); //--- Get session end
      
      int startBar = iBarShift(_Symbol, lowerTimeframe, sessions[sessionIndex].startTime); //--- Get start bar
      int endBar = iBarShift(_Symbol, lowerTimeframe, sessionEnd); //--- Get end bar
      
      if(startBar < 0 || endBar < 0) return;      //--- Return if invalid bars
      
      int barCount = startBar - endBar + 1;       //--- Calculate bar count
      if(barCount <= 0) return;                   //--- Return if no bars
      
      double highs[], lows[];                     //--- Declare highs and lows arrays
      long volumes[];                             //--- Declare volumes array
      
      ArraySetAsSeries(highs, true);              //--- Set highs as series
      ArraySetAsSeries(lows, true);               //--- Set lows as series
      ArraySetAsSeries(volumes, true);            //--- Set volumes as series
      
      if(CopyHigh(_Symbol, lowerTimeframe, endBar, barCount, highs) <= 0) return; //--- Copy highs
      if(CopyLow(_Symbol, lowerTimeframe, endBar, barCount, lows) <= 0) return; //--- Copy lows
      if(CopyTickVolume(_Symbol, lowerTimeframe, endBar, barCount, volumes) <= 0) return; //--- Copy volumes
      
      for(int i = 0; i < barCount; i++) {         //--- Loop through bars
         double quantizedHigh = QuantizePriceToGrid(highs[i]); //--- Quantize high
         double quantizedLow = QuantizePriceToGrid(lows[i]); //--- Quantize low
         
         int priceCount = (int)MathMax(1, (quantizedHigh - quantizedLow) / tpoPriceGridStep + 1); //--- Calculate price count
         double volumePerLevel = (double)volumes[i] / priceCount; //--- Calculate volume per level
         
         for(double price = quantizedLow; price <= quantizedHigh; price += tpoPriceGridStep) { //--- Loop through prices
            int volumeProfileIndex = -1;          //--- Initialize VP index
            for(int j = 0; j < ArraySize(sessions[sessionIndex].volumeProfilePrices); j++) { //--- Loop through VP prices
               if(MathAbs(sessions[sessionIndex].volumeProfilePrices[j] - price) < _Point / 2) { //--- Check match
                  volumeProfileIndex = j;         //--- Set index
                  break;                          //--- Exit loop
               }
            }
            
            if(volumeProfileIndex == -1) {      //--- Check if new
               int volumeProfileSize = ArraySize(sessions[sessionIndex].volumeProfilePrices); //--- Get VP size
               ArrayResize(sessions[sessionIndex].volumeProfilePrices, volumeProfileSize + 1); //--- Resize prices
               ArrayResize(sessions[sessionIndex].volumeProfileVolumes, volumeProfileSize + 1); //--- Resize volumes
               sessions[sessionIndex].volumeProfilePrices[volumeProfileSize] = price; //--- Set price
               sessions[sessionIndex].volumeProfileVolumes[volumeProfileSize] = volumePerLevel; //--- Set volume
            } else {                              //--- Handle existing
               sessions[sessionIndex].volumeProfileVolumes[volumeProfileIndex] += volumePerLevel; //--- Add volume
            }
         }
      }
   }
   
   double maxVolume = 0;                          //--- Initialize max volume
   double volumeProfilePocPrice = 0;              //--- Initialize VP POC price
   
   for(int i = 0; i < ArraySize(sessions[sessionIndex].volumeProfileVolumes); i++) { //--- Loop through volumes
      if(sessions[sessionIndex].volumeProfileVolumes[i] > maxVolume) { //--- Check if max
         maxVolume = sessions[sessionIndex].volumeProfileVolumes[i]; //--- Update max
         volumeProfilePocPrice = sessions[sessionIndex].volumeProfilePrices[i]; //--- Update price
      }
   }
   
   sessions[sessionIndex].volumeProfilePocIndex = -1; //--- Reset VP POC index
   for(int i = 0; i < ArraySize(sessions[sessionIndex].levels); i++) { //--- Loop through levels
      if(MathAbs(sessions[sessionIndex].levels[i].price - volumeProfilePocPrice) < tpoPriceGridStep / 2) { //--- Check match
         sessions[sessionIndex].volumeProfilePocIndex = i; //--- Set index
         break;                                    //--- Exit loop
      }
   }
}

Мы реализуем функцию "BuildVolumeProfileAndFindPoc" для построения отдельного объёмного профиля и подсветки точки контроля по объёму. Сначала проверяем, включена ли функция через "highlightVolumeProfilePoc"; если нет, сбрасываем индекс для сессий с корректным индексом и сразу завершаем функцию. После проверки индекса сессии через "ArraySize" и наличия времени начала мы динамически выбираем более низкий таймфрейм с помощью PeriodSeconds на текущем периоде: устанавливаем его в PERIOD_M1 для периодов меньше 30 минут, "PERIOD_M3" для периодов меньше часа, "PERIOD_M10" ровно для часа или "PERIOD_H1" в остальных случаях, чтобы получить более детальные данные для точного распределения объёма. Если текущий период составляет 60 секунд или меньше, мы проходим по существующим уровням, ищем совпадающие цены в массивах объёмного профиля с MathAbs и допуском в половину пункта; если совпадение не найдено, расширяем массивы с помощью ArrayResize и добавляем цену и объём либо накапливаем объём при совпадении.

Для старших таймфреймов мы определяем конец сессии как заданное время окончания или текущее время из TimeCurrent, получаем индексы начального и конечного баров через iBarShift на младшем таймфрейме и, если они допустимы и количество положительное, объявляем массивы максимумов, минимумов и объёмов и задаём их как series-массивы. Данные копируются с помощью CopyHigh, "CopyLow" и CopyTickVolume начиная с конечного бара, после чего мы перебираем бары: дискретизируем максимум и минимум через "QuantizePriceToGrid", рассчитываем ценовые шаги, гарантируя минимум один шаг, равномерно распределяем объём бара по уровням и проходим по диапазону цен, чтобы аналогично добавить или накопить значения в массивах объёмного профиля с проверкой допуска.

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

//+------------------------------------------------------------------+
//| Calculate session VWAP                                           |
//+------------------------------------------------------------------+
void CalculateSessionVwap(int sessionIndex) {
   if(!highlightSessionVwap) return;              //--- Return if not highlight VWAP
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   
   double sumPriceVolume = 0;                     //--- Initialize sum price volume
   double sumVolume = 0;                          //--- Initialize sum volume
   
   int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size
   for(int i = 0; i < size; i++) {                //--- Loop through levels
      sumPriceVolume += sessions[sessionIndex].levels[i].price * sessions[sessionIndex].levels[i].volume; //--- Accumulate price volume
      sumVolume += sessions[sessionIndex].levels[i].volume; //--- Accumulate volume
   }
   
   if(sumVolume > 0)                              //--- Check if volume positive
      sessions[sessionIndex].vwap = sumPriceVolume / sumVolume; //--- Calculate VWAP
}

Чтобы определить средневзвешенную по объёму цену для сессии, мы реализуем функцию "CalculateSessionVwap", которая сразу завершается, если условие не выполнено или индекс сессии выходит за допустимые границы. Мы задаём накопители для суммы произведений цены на объём и общего объёма, затем проходим по уровням, умножая каждую цену на её объём и добавляя результат к сумме произведений, а объёмы — к общей сумме. Если общий объём больше нуля, вычисляем и сохраняем средневзвешенную цену в поле vwap сессии как сумму произведений, делённую на сумму объёмов. То есть фактически это средняя цена, взвешенная по объёму торгов. Теперь у нас есть все вспомогательные функции для расчётов; осталось заняться визуальной отрисовкой данных. Сначала добавим функцию для подсветки открывающего TPO-символа в буквенном режиме. Это как подсветить стартовую букву прожектором — необязательно, но помогает быстрее визуально её найти.

//+------------------------------------------------------------------+
//| Render open TPO highlight                                        |
//+------------------------------------------------------------------+
void RenderOpenTpoHighlight(int sessionIndex, int openLevelIndex, string &displayStrings[]) {
   if(tpoCharacterType != ALPHABETIC) return;                          //--- Return if not alphabetic
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if session invalid
   if(openLevelIndex < 0 || openLevelIndex >= ArraySize(sessions[sessionIndex].levels)) return; //--- Return if level invalid
   
   string fullString = displayStrings[openLevelIndex];                 //--- Get full display string
   if(StringLen(fullString) == 0) return;                              //--- Return if empty string
   
   string openCharacter = StringSubstr(fullString, 0, 1);              //--- Extract open character
   string remainingCharacters = StringSubstr(fullString, 1);           //--- Extract remaining characters
   
   string objectName = objectPrefix + "OpenTPO_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create object name
   int barIndex = iBarShift(_Symbol, _Period, sessions[sessionIndex].startTime); //--- Get bar index
   if(barIndex < 0) return;                                            //--- Return if invalid bar index
   
   datetime labelTime = iTime(_Symbol, _Period, barIndex);             //--- Get label time
   int x, y;                                                           //--- Declare coordinates
   ChartTimePriceToXY(0, 0, labelTime, sessions[sessionIndex].levels[openLevelIndex].price, x, y); //--- Convert to XY
   
   if(ObjectFind(0, objectName) < 0) {                                 //--- Check if object not found
      ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0);                 //--- Create label object
   }
   
   ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, x);              //--- Set X distance
   ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, y);              //--- Set Y distance
   ObjectSetInteger(0, objectName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
   ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_LEFT);       //--- Set anchor
   ObjectSetInteger(0, objectName, OBJPROP_COLOR, openColor);          //--- Set color
   ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize);   //--- Set font size
   ObjectSetString(0, objectName, OBJPROP_FONT, "Arial");              //--- Set font
   ObjectSetString(0, objectName, OBJPROP_TEXT, openCharacter);        //--- Set text
   ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);         //--- Set selectable false
   ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true);              //--- Set hidden true
   
   if(StringLen(remainingCharacters) > 0) {                            //--- Check if remaining
      displayStrings[openLevelIndex] = " " + remainingCharacters;      //--- Prepend space
   } else {                                                            //--- Handle no remaining
      displayStrings[openLevelIndex] = " ";                            //--- Set space
   }
}

Здесь мы добавляем функцию "RenderOpenTpoHighlight" для визуального выделения открывающего символа Time Price Opportunity в буквенном режиме. Она сразу завершается, если выбран небуквенный тип символов либо индексы сессии и уровня открытия выходят за допустимые границы массивов. Мы получаем строку отображения для уровня открытия и выходим, если она пустая, согласно StringLen, затем извлекаем первый символ как маркер открытия с помощью StringSubstr, а остальную часть строки — как оставшуюся. Имя объекта формируется из префикса и времени начала сессии через IntegerToString, затем мы получаем индекс бара через "iBarShift" и завершаем функцию, если индекс некорректен. Получаем время метки через "iTime", преобразуем время и цену в координаты с помощью ChartTimePriceToXY, и если объект отсутствует по "ObjectFind", создаём метку с помощью ObjectCreate и OBJ_LABEL.

Мы задаём её позицию, угол размещения, точку привязки, цвет маркера открытия, заданный во входных параметрах, динамический размер шрифта, шрифт Arial, текст символа открытия и свойства недоступности выбора/скрытия через ObjectSetInteger и "ObjectSetString". В конце обновляем строку отображения: добавляем пробел перед оставшимися символами, если они есть, или задаём один пробел в противном случае, чтобы сохранить выравнивание при отрисовке профиля. Далее мы отрисуем линии границ профиля, линии начального баланса и продления ключевых уровней.

//+------------------------------------------------------------------+
//| Render profile border line                                       |
//+------------------------------------------------------------------+
void RenderProfileBorderLine(int sessionIndex, int barIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   if(sessions[sessionIndex].sessionHigh == 0 || sessions[sessionIndex].sessionLow == 0) return; //--- Return if no high low
   
   datetime startTime = iTime(_Symbol, _Period, barIndex);             //--- Get start time
   
   color backgroundEdgeColor = 0x606D79;                               //--- Set background edge color
   color initialBalanceEdgeColor = initialBalanceHighlightColor;       //--- Set IB edge color
   
   if(initialBalancePeriods > 0 && sessions[sessionIndex].initialBalanceHigh > 0 && sessions[sessionIndex].initialBalanceLow > 0) { //--- Check IB
      
      string edge1Name = objectPrefix + "Edge1_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create edge1 name
      if(ObjectFind(0, edge1Name) < 0) {                               //--- Check if not found
         ObjectCreate(0, edge1Name, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionLow, 
                      startTime, sessions[sessionIndex].initialBalanceLow); //--- Create trend
         ObjectSetInteger(0, edge1Name, OBJPROP_RAY_RIGHT, false);     //--- Set ray right false
         ObjectSetInteger(0, edge1Name, OBJPROP_SELECTABLE, false);    //--- Set selectable false
         ObjectSetInteger(0, edge1Name, OBJPROP_HIDDEN, true);         //--- Set hidden true
      }
      ObjectSetDouble(0, edge1Name, OBJPROP_PRICE, 0, sessions[sessionIndex].sessionLow); //--- Set price 0
      ObjectSetDouble(0, edge1Name, OBJPROP_PRICE, 1, sessions[sessionIndex].initialBalanceLow); //--- Set price 1
      ObjectSetInteger(0, edge1Name, OBJPROP_COLOR, backgroundEdgeColor); //--- Set color
      ObjectSetInteger(0, edge1Name, OBJPROP_WIDTH, 3);                //--- Set width
      ObjectSetInteger(0, edge1Name, OBJPROP_STYLE, STYLE_SOLID);      //--- Set style
      
      string edge2Name = objectPrefix + "Edge2_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create edge2 name
      if(ObjectFind(0, edge2Name) < 0) {                               //--- Check if not found
         ObjectCreate(0, edge2Name, OBJ_TREND, 0, startTime, sessions[sessionIndex].initialBalanceLow, 
                      startTime, sessions[sessionIndex].initialBalanceHigh); //--- Create trend
         ObjectSetInteger(0, edge2Name, OBJPROP_RAY_RIGHT, false);     //--- Set ray right false
         ObjectSetInteger(0, edge2Name, OBJPROP_SELECTABLE, false);    //--- Set selectable false
         ObjectSetInteger(0, edge2Name, OBJPROP_HIDDEN, true);         //--- Set hidden true
      }
      ObjectSetDouble(0, edge2Name, OBJPROP_PRICE, 0, sessions[sessionIndex].initialBalanceLow);  //--- Set price 0
      ObjectSetDouble(0, edge2Name, OBJPROP_PRICE, 1, sessions[sessionIndex].initialBalanceHigh); //--- Set price 1
      ObjectSetInteger(0, edge2Name, OBJPROP_COLOR, initialBalanceEdgeColor);                     //--- Set color
      ObjectSetInteger(0, edge2Name, OBJPROP_WIDTH, 3);                //--- Set width
      ObjectSetInteger(0, edge2Name, OBJPROP_STYLE, STYLE_SOLID);      //--- Set style
      
      string edge3Name = objectPrefix + "Edge3_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create edge3 name
      if(ObjectFind(0, edge3Name) < 0) {                               //--- Check if not found
         ObjectCreate(0, edge3Name, OBJ_TREND, 0, startTime, sessions[sessionIndex].initialBalanceHigh, 
                      startTime, sessions[sessionIndex].sessionHigh);  //--- Create trend
         ObjectSetInteger(0, edge3Name, OBJPROP_RAY_RIGHT, false);     //--- Set ray right false
         ObjectSetInteger(0, edge3Name, OBJPROP_SELECTABLE, false);    //--- Set selectable false
         ObjectSetInteger(0, edge3Name, OBJPROP_HIDDEN, true);         //--- Set hidden true
      }
      ObjectSetDouble(0, edge3Name, OBJPROP_PRICE, 0, sessions[sessionIndex].initialBalanceHigh);   //--- Set price 0
      ObjectSetDouble(0, edge3Name, OBJPROP_PRICE, 1, sessions[sessionIndex].sessionHigh);          //--- Set price 1
      ObjectSetInteger(0, edge3Name, OBJPROP_COLOR, backgroundEdgeColor);                           //--- Set color
      ObjectSetInteger(0, edge3Name, OBJPROP_WIDTH, 3);                                             //--- Set width
      ObjectSetInteger(0, edge3Name, OBJPROP_STYLE, STYLE_SOLID);                                   //--- Set style
      
   } else {                                                                                         //--- Handle no IB
      string edgeName = objectPrefix + "Edge_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create edge name
      if(ObjectFind(0, edgeName) < 0) {                                                             //--- Check if not found
         ObjectCreate(0, edgeName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionLow, 
                      startTime, sessions[sessionIndex].sessionHigh);                               //--- Create trend
         ObjectSetInteger(0, edgeName, OBJPROP_RAY_RIGHT, false);                                   //--- Set ray right false
         ObjectSetInteger(0, edgeName, OBJPROP_SELECTABLE, false);                                  //--- Set selectable false
         ObjectSetInteger(0, edgeName, OBJPROP_HIDDEN, true);                                       //--- Set hidden true
      }
      ObjectSetDouble(0, edgeName, OBJPROP_PRICE, 0, sessions[sessionIndex].sessionLow);            //--- Set price 0
      ObjectSetDouble(0, edgeName, OBJPROP_PRICE, 1, sessions[sessionIndex].sessionHigh);           //--- Set price 1
      ObjectSetInteger(0, edgeName, OBJPROP_COLOR, backgroundEdgeColor);                            //--- Set color
      ObjectSetInteger(0, edgeName, OBJPROP_WIDTH, 3);                                              //--- Set width
      ObjectSetInteger(0, edgeName, OBJPROP_STYLE, STYLE_SOLID);                                    //--- Set style
   }
}

//+------------------------------------------------------------------+
//| Render initial balance lines                                     |
//+------------------------------------------------------------------+
void RenderInitialBalanceLines(int sessionIndex, int barIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return;       //--- Return if index invalid
   if(sessions[sessionIndex].initialBalanceHigh == 0 || sessions[sessionIndex].initialBalanceLow == 0) return; //--- Return if no IB
   
   datetime startTime = iTime(_Symbol, _Period, barIndex);                   //--- Get start time
   
   string initialBalanceHighName = objectPrefix + "IB_High_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create IB high name
   if(ObjectFind(0, initialBalanceHighName) < 0) { //--- Check if not found
      ObjectCreate(0, initialBalanceHighName, OBJ_TREND, 0, startTime, sessions[sessionIndex].initialBalanceHigh, 
                   startTime, sessions[sessionIndex].initialBalanceHigh);     //--- Create trend
      ObjectSetInteger(0, initialBalanceHighName, OBJPROP_RAY_RIGHT, false);  //--- Set ray right false
      ObjectSetInteger(0, initialBalanceHighName, OBJPROP_SELECTABLE, false); //--- Set selectable false
      ObjectSetInteger(0, initialBalanceHighName, OBJPROP_HIDDEN, true);      //--- Set hidden true
   }
   ObjectSetInteger(0, initialBalanceHighName, OBJPROP_COLOR, initialBalanceHighlightColor); //--- Set color
   ObjectSetInteger(0, initialBalanceHighName, OBJPROP_WIDTH, initialBalanceLineWidth);      //--- Set width
   
   string initialBalanceLowName = objectPrefix + "IB_Low_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create IB low name
   if(ObjectFind(0, initialBalanceLowName) < 0) { //--- Check if not found
      ObjectCreate(0, initialBalanceLowName, OBJ_TREND, 0, startTime, sessions[sessionIndex].initialBalanceLow, 
                   startTime, sessions[sessionIndex].initialBalanceLow);      //--- Create trend
      ObjectSetInteger(0, initialBalanceLowName, OBJPROP_RAY_RIGHT, false);   //--- Set ray right false
      ObjectSetInteger(0, initialBalanceLowName, OBJPROP_SELECTABLE, false);  //--- Set selectable false
      ObjectSetInteger(0, initialBalanceLowName, OBJPROP_HIDDEN, true);       //--- Set hidden true
   }
   ObjectSetInteger(0, initialBalanceLowName, OBJPROP_COLOR, initialBalanceHighlightColor); //--- Set color
   ObjectSetInteger(0, initialBalanceLowName, OBJPROP_WIDTH, initialBalanceLineWidth);      //--- Set width
}

//+------------------------------------------------------------------+
//| Render key level extensions                                      |
//+------------------------------------------------------------------+
void RenderKeyLevelExtensions(int sessionIndex, int barIndex, int valueAreaUpperIndex, int valueAreaLowerIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   if(ArraySize(sessions[sessionIndex].levels) == 0) return;           //--- Return if no levels
   
   datetime startTime = iTime(_Symbol, _Period, barIndex);             //--- Get start time
   
   bool isCurrentSession = (sessionIndex == ArraySize(sessions) - 1);  //--- Check if current session
   
   datetime endTime;                                                   //--- Declare end time
   bool rayRight;                                                      //--- Declare ray right
   
   if(isCurrentSession) {                                              //--- Handle current session
      endTime = startTime + PeriodSeconds(_Period) * 100;              //--- Set end time
      rayRight = true;                                                 //--- Set ray right true
   } else {                                                            //--- Handle past session
      if(sessionIndex + 1 < ArraySize(sessions)) {                     //--- Check next session
         int nextBarIndex = iBarShift(_Symbol, _Period, sessions[sessionIndex + 1].startTime); //--- Get next bar
         endTime = iTime(_Symbol, _Period, nextBarIndex);              //--- Set end time
      } else {                                                         //--- Handle last
         endTime = startTime + PeriodSeconds(_Period) * 100;           //--- Set end time
      }
      rayRight = false;                                                //--- Set ray right false
   }
   
   if(sessions[sessionIndex].volumeProfilePocIndex >= 0 && sessions[sessionIndex].volumeProfilePocIndex < ArraySize(sessions[sessionIndex].levels)) { //--- Check VP POC
      double pocPrice = sessions[sessionIndex].levels[sessions[sessionIndex].volumeProfilePocIndex].price; //--- Get POC price
      string pocExtensionName = objectPrefix + "POC_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create POC ext name
      if(ObjectFind(0, pocExtensionName) < 0) {                             //--- Check if not found
         ObjectCreate(0, pocExtensionName, OBJ_TREND, 0, startTime, pocPrice, endTime, pocPrice); //--- Create trend
         ObjectSetInteger(0, pocExtensionName, OBJPROP_SELECTABLE, false);  //--- Set selectable false
         ObjectSetInteger(0, pocExtensionName, OBJPROP_HIDDEN, true);       //--- Set hidden true
      } else {                                    //--- Handle existing
         ObjectSetInteger(0, pocExtensionName, OBJPROP_TIME, 0, startTime); //--- Set time 0
         ObjectSetDouble(0, pocExtensionName, OBJPROP_PRICE, 0, pocPrice);  //--- Set price 0
         ObjectSetInteger(0, pocExtensionName, OBJPROP_TIME, 1, endTime);   //--- Set time 1
         ObjectSetDouble(0, pocExtensionName, OBJPROP_PRICE, 1, pocPrice);  //--- Set price 1
      }
      ObjectSetInteger(0, pocExtensionName, OBJPROP_RAY_RIGHT, rayRight);   //--- Set ray right
      ObjectSetInteger(0, pocExtensionName, OBJPROP_COLOR, pocExtensionColor); //--- Set color
      ObjectSetInteger(0, pocExtensionName, OBJPROP_WIDTH, 2);              //--- Set width
      ObjectSetInteger(0, pocExtensionName, OBJPROP_STYLE, STYLE_SOLID);    //--- Set style
   }
   
   if(valueAreaUpperIndex >= 0 && valueAreaUpperIndex < ArraySize(sessions[sessionIndex].levels)) { //--- Check VAH
      double valueAreaHighPrice = sessions[sessionIndex].levels[valueAreaUpperIndex].price;         //--- Get VAH price
      string valueAreaHighExtensionName = objectPrefix + "VAH_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create VAH ext name
      if(ObjectFind(0, valueAreaHighExtensionName) < 0) {                                           //--- Check if not found
         ObjectCreate(0, valueAreaHighExtensionName, OBJ_TREND, 0, startTime, valueAreaHighPrice, endTime, valueAreaHighPrice); //--- Create trend
         ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_SELECTABLE, false);                //--- Set selectable false
         ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_HIDDEN, true);                     //--- Set hidden true
      } else {                                    //--- Handle existing
         ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_TIME, 0, startTime);               //--- Set time 0
         ObjectSetDouble(0, valueAreaHighExtensionName, OBJPROP_PRICE, 0, valueAreaHighPrice);      //--- Set price 0
         ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_TIME, 1, endTime);                 //--- Set time 1
         ObjectSetDouble(0, valueAreaHighExtensionName, OBJPROP_PRICE, 1, valueAreaHighPrice);      //--- Set price 1
      }
      ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_RAY_RIGHT, rayRight);                 //--- Set ray right
      ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_COLOR, valueAreaHighExtensionColor);  //--- Set color
      ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_WIDTH, 1);                            //--- Set width
      ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_STYLE, STYLE_DOT);                    //--- Set style
   }
   
   if(valueAreaLowerIndex >= 0 && valueAreaLowerIndex < ArraySize(sessions[sessionIndex].levels)) { //--- Check VAL
      double valueAreaLowPrice = sessions[sessionIndex].levels[valueAreaLowerIndex].price;          //--- Get VAL price
      string valueAreaLowExtensionName = objectPrefix + "VAL_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create VAL ext name
      if(ObjectFind(0, valueAreaLowExtensionName) < 0) {                                            //--- Check if not found
         ObjectCreate(0, valueAreaLowExtensionName, OBJ_TREND, 0, startTime, valueAreaLowPrice, endTime, valueAreaLowPrice); //--- Create trend
         ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_SELECTABLE, false);                 //--- Set selectable false
         ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_HIDDEN, true);                      //--- Set hidden true
      } else {                                                                                      //--- Handle existing
         ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_TIME, 0, startTime);                //--- Set time 0
         ObjectSetDouble(0, valueAreaLowExtensionName, OBJPROP_PRICE, 0, valueAreaLowPrice);        //--- Set price 0
         ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_TIME, 1, endTime);                  //--- Set time 1
         ObjectSetDouble(0, valueAreaLowExtensionName, OBJPROP_PRICE, 1, valueAreaLowPrice);        //--- Set price 1
      }
      ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_RAY_RIGHT, rayRight);                  //--- Set ray right
      ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_COLOR, valueAreaLowExtensionColor);    //--- Set color
      ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_WIDTH, 1);                             //--- Set width
      ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_STYLE, STYLE_DOT);                     //--- Set style
   }
   
   string highExtensionName = objectPrefix + "High_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create high ext name
   if(ObjectFind(0, highExtensionName) < 0) {                                                       //--- Check if not found
      ObjectCreate(0, highExtensionName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionHigh, 
                   endTime, sessions[sessionIndex].sessionHigh);                                    //--- Create trend
      ObjectSetInteger(0, highExtensionName, OBJPROP_SELECTABLE, false);                            //--- Set selectable false
      ObjectSetInteger(0, highExtensionName, OBJPROP_HIDDEN, true);                                 //--- Set hidden true
      ObjectSetInteger(0, highExtensionName, OBJPROP_STYLE, STYLE_DOT);                             //--- Set style
   } else {                                                                                         //--- Handle existing
      ObjectSetInteger(0, highExtensionName, OBJPROP_TIME, 0, startTime);                           //--- Set time 0
      ObjectSetDouble(0, highExtensionName, OBJPROP_PRICE, 0, sessions[sessionIndex].sessionHigh);  //--- Set price 0
      ObjectSetInteger(0, highExtensionName, OBJPROP_TIME, 1, endTime);                             //--- Set time 1
      ObjectSetDouble(0, highExtensionName, OBJPROP_PRICE, 1, sessions[sessionIndex].sessionHigh);  //--- Set price 1
   }
   ObjectSetInteger(0, highExtensionName, OBJPROP_RAY_RIGHT, rayRight);       //--- Set ray right
   ObjectSetInteger(0, highExtensionName, OBJPROP_COLOR, highExtensionColor); //--- Set color
   ObjectSetInteger(0, highExtensionName, OBJPROP_WIDTH, 1);                  //--- Set width
   
   string lowExtensionName = objectPrefix + "Low_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create low ext name
   if(ObjectFind(0, lowExtensionName) < 0) {                                                                //--- Check if not found
      ObjectCreate(0, lowExtensionName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionLow, 
                   endTime, sessions[sessionIndex].sessionLow);                                             //--- Create trend
      ObjectSetInteger(0, lowExtensionName, OBJPROP_SELECTABLE, false);                                     //--- Set selectable false
      ObjectSetInteger(0, lowExtensionName, OBJPROP_HIDDEN, true);                                          //--- Set hidden true
      ObjectSetInteger(0, lowExtensionName, OBJPROP_STYLE, STYLE_DOT);                                      //--- Set style
   } else {                                                                                                 //--- Handle existing
      ObjectSetInteger(0, lowExtensionName, OBJPROP_TIME, 0, startTime);                                    //--- Set time 0
      ObjectSetDouble(0, lowExtensionName, OBJPROP_PRICE, 0, sessions[sessionIndex].sessionLow);            //--- Set price 0
      ObjectSetInteger(0, lowExtensionName, OBJPROP_TIME, 1, endTime);                                      //--- Set time 1
      ObjectSetDouble(0, lowExtensionName, OBJPROP_PRICE, 1, sessions[sessionIndex].sessionLow);            //--- Set price 1
   }
   ObjectSetInteger(0, lowExtensionName, OBJPROP_RAY_RIGHT, rayRight);      //--- Set ray right
   ObjectSetInteger(0, lowExtensionName, OBJPROP_COLOR, lowExtensionColor); //--- Set color
   ObjectSetInteger(0, lowExtensionName, OBJPROP_WIDTH, 1);                 //--- Set width
   
   double midpointPrice = 0;                      //--- Initialize midpoint price
   if(midpointAlgorithm == HIGH_LOW_MID) {        //--- Check high low mid
      midpointPrice = sessions[sessionIndex].sessionHigh - (sessions[sessionIndex].sessionHigh - sessions[sessionIndex].sessionLow) / 2; //--- Calculate midpoint
   } else {                                       //--- Handle TPO count based
      int totalTpoCount = GetTotalTpoCount(sessionIndex); //--- Get total TPO
      int targetTpoCount = totalTpoCount / 2;     //--- Calculate target
      int currentTpoCount = 0;                    //--- Initialize current
      
      for(int i = 0; i < ArraySize(sessions[sessionIndex].levels); i++) { //--- Loop through levels
         currentTpoCount += sessions[sessionIndex].levels[i].tpoCount; //--- Accumulate TPO
         if(currentTpoCount >= targetTpoCount) {  //--- Check if reached
            midpointPrice = sessions[sessionIndex].levels[i].price; //--- Set midpoint
            break;                                //--- Exit loop
         }
      }
   }
   
   if(midpointPrice > 0) {                        //--- Check if midpoint set
      string midpointExtensionName = objectPrefix + "Mid_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create mid ext name
      if(ObjectFind(0, midpointExtensionName) < 0) { //--- Check if not found
         ObjectCreate(0, midpointExtensionName, OBJ_TREND, 0, startTime, midpointPrice, endTime, midpointPrice); //--- Create trend
         ObjectSetInteger(0, midpointExtensionName, OBJPROP_SELECTABLE, false);  //--- Set selectable false
         ObjectSetInteger(0, midpointExtensionName, OBJPROP_HIDDEN, true);       //--- Set hidden true
         ObjectSetInteger(0, midpointExtensionName, OBJPROP_STYLE, STYLE_DOT);   //--- Set style
      } else {                                                                   //--- Handle existing
         ObjectSetInteger(0, midpointExtensionName, OBJPROP_TIME, 0, startTime); //--- Set time 0
         ObjectSetDouble(0, midpointExtensionName, OBJPROP_PRICE, 0, midpointPrice); //--- Set price 0
         ObjectSetInteger(0, midpointExtensionName, OBJPROP_TIME, 1, endTime);   //--- Set time 1
         ObjectSetDouble(0, midpointExtensionName, OBJPROP_PRICE, 1, midpointPrice); //--- Set price 1
      }
      ObjectSetInteger(0, midpointExtensionName, OBJPROP_RAY_RIGHT, rayRight);   //--- Set ray right
      ObjectSetInteger(0, midpointExtensionName, OBJPROP_COLOR, midpointExtensionColor); //--- Set color
      ObjectSetInteger(0, midpointExtensionName, OBJPROP_WIDTH, 1);              //--- Set width
   }
}

Мы вводим функцию "RenderProfileBorderLine" для отрисовки вертикальных граничных линий, обозначающих ценовой диапазон сессии на графике. Сначала проверяется индекс сессии и наличие максимумов и минимумов цены. Мы получаем время начала с помощью iTime и задаём цвета для фоновых краёв и подсветки начального баланса. Если периоды начального баланса положительны и значения заданы, создаём или обновляем три сегмента трендовых линий через "ObjectCreate" с OBJ_TREND: первый — от минимума сессии до минимума начального баланса фоновым цветом, второй — от минимума до максимума начального баланса цветом подсветки, третий — от максимума начального баланса до максимума сессии фоновым цветом. Каждый настраивается через ObjectFind, "ObjectSetDouble" для цен, ObjectSetInteger для отключения луча, запрета выбора, скрытия, сплошного стиля и ширины 3. Если начального баланса нет, рисуется одна трендовая линия от минимума до максимума фоновым цветом с теми же свойствами.

Далее функция "RenderInitialBalanceLines" визуализирует диапазон начального баланса, если значения доступны: получает время начала и создаёт или обновляет две горизонтальные трендовые линии для максимума и минимума через ObjectCreate с "OBJ_TREND" в тех же координатах, задавая отсутствие луча, запрет выбора, скрытие, входной цвет подсветки и ширину линии.

Для проекции важных уровней вперёд мы реализуем функцию "RenderKeyLevelExtensions". Сначала она проверяет допустимость данных и получает время начала, затем определяет, является ли сессия текущей: для неё устанавливается расширенное время окончания с включённым лучом, а для прошлых сессий используется начало следующей сессии без луча. В расчётах применяются PeriodSeconds, "TimeCurrent" и "iBarShift". Для точки контроля объёмного профиля, если она допустима, рисуется сплошная линия на её цене с заданным цветом продления и шириной 2. Аналогично для верхней и нижней границ области стоимости, если индексы в диапазоне, создаются пунктирные линии с соответствующими цветами и шириной 1.

Мы всегда добавляем пунктирные продолжения максимумов и минимумов сессии цветами, указанными во входных параметрах. Срединный уровень рассчитывается на основе перечисления "midpointAlgorithm" — либо как простой срединный уровень, либо через накопление количества Time Price Opportunity с помощью "GetTotalTpoCount" для поиска уровня половины объёма TPO. Если значение положительное, отрисовывается пунктирная линия цветом срединного уровня. Всё это выполняется через "ObjectSetInteger" для времени, ObjectSetDouble для цен, луча, стиля и других свойств после проверки или создания объекта тем же способом, что и для остальных линий. Далее нужно определить логику отображения чисел объёма рядом с уровнями в фиксированном режиме, окрашенных по интенсивности, то есть показать, насколько активно шла торговля на каждом ценовом уровне.

//+------------------------------------------------------------------+
//| Render fixed range volume labels                                 |
//+------------------------------------------------------------------+
void RenderFixedRangeVolumeLabels(int sessionIndex, int barIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return;     //--- Return if index invalid
   if(ArraySize(sessions[sessionIndex].volumeProfilePrices) == 0) return;  //--- Return if no VP prices
   
   double maxVolume = 0;                                                   //--- Initialize max volume
   for(int i = 0; i < ArraySize(sessions[sessionIndex].volumeProfileVolumes); i++) { //--- Loop through volumes
      if(sessions[sessionIndex].volumeProfileVolumes[i] > maxVolume)       //--- Check if max
         maxVolume = sessions[sessionIndex].volumeProfileVolumes[i];       //--- Update max
   }
   
   if(maxVolume == 0) return;                                              //--- Return if no volume
   
   datetime labelTime = iTime(_Symbol, _Period, barIndex);                 //--- Get label time
   
   for(int i = 0; i < ArraySize(sessions[sessionIndex].volumeProfilePrices); i++) { //--- Loop through VP prices
      double price = sessions[sessionIndex].volumeProfilePrices[i];        //--- Get price
      double volumeValue = sessions[sessionIndex].volumeProfileVolumes[i]; //--- Get volume
      double ratio = volumeValue / maxVolume;     //--- Calculate ratio
      
      color volumeColor;                          //--- Declare color
      int percent = (int)(100 * ratio);           //--- Calculate percent
      
      if(percent <= 8) {                          //--- Check low percent
         volumeColor = singlePrintColor;          //--- Set single print color
      } else {                                    //--- Handle higher
         int transparency = (int)MathMax(0, 80 - percent);                     //--- Calculate transparency
         volumeColor = ApplyTransparencyToColor(valueAreaColor, transparency); //--- Apply transparency
      }
      
      string objectName = objectPrefix + "Vol_" + IntegerToString(sessions[sessionIndex].startTime) + "_" + IntegerToString(i); //--- Create object name
      
      int x, y;                                           //--- Declare coordinates
      ChartTimePriceToXY(0, 0, labelTime, price, x, y);   //--- Convert to XY
      
      if(ObjectFind(0, objectName) < 0) {                 //--- Check if not found
         ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0); //--- Create label
      }
      
      ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, x - 50);         //--- Set X distance
      ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, y);              //--- Set Y distance
      ObjectSetInteger(0, objectName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
      ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_RIGHT);      //--- Set anchor
      ObjectSetInteger(0, objectName, OBJPROP_COLOR, volumeColor);        //--- Set color
      ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize);   //--- Set font size
      ObjectSetString(0, objectName, OBJPROP_FONT, "Arial");              //--- Set font
      ObjectSetString(0, objectName, OBJPROP_TEXT, IntegerToString((int)MathRound(volumeValue))); //--- Set text
      ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);         //--- Set selectable false
      ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true);              //--- Set hidden true
   }
}

//+------------------------------------------------------------------+
//| Apply transparency to color                                      |
//+------------------------------------------------------------------+
color ApplyTransparencyToColor(color baseColor, int transparency) {
   int red = (int)(baseColor & 0xFF);             //--- Extract red
   int green = (int)((baseColor >> 8) & 0xFF);    //--- Extract green
   int blue = (int)((baseColor >> 16) & 0xFF);    //--- Extract blue
   
   transparency = (int)MathMin(100, MathMax(0, transparency)); //--- Clamp transparency
   int alpha = 255 - (transparency * 255 / 100); //--- Calculate alpha
   
   return (color)((alpha << 24) | (blue << 16) | (green << 8) | red); //--- Return color with alpha
}

Мы реализуем функцию "RenderFixedRangeVolumeLabels" для отображения значений объёма в виде меток рядом с профилем в режиме фиксированного таймфрейма. Сначала проверяется индекс сессии и наличие цен объёмного профиля. Максимальный объём определяется проходом по массиву объёмов с обновлением наибольшего значения; если он равен нулю, функция выходит, чтобы избежать проблем с делением. После получения времени метки через iTime мы проходим по каждой записи объёмного профиля: рассчитываем отношение к максимуму, получаем процент и задаём цвет метки — используем цвет single print для 8% или меньше либо применяем прозрачность к цвету области стоимости через "ApplyTransparencyToColor" для более высоких значений. Прозрачность меняется обратно пропорционально от 0 до 80 в зависимости от процента через функцию MathMax.

Для каждой записи строится уникальное имя объекта из префикса, времени начала и индекса через IntegerToString, затем время и цена преобразуются в координаты через "ChartTimePriceToXY". Если метка отсутствует по ObjectFind, она создаётся с помощью "ObjectCreate" и "OBJ_LABEL" и настраивается со смещением X влево, верхним левым углом, правой точкой привязки, вычисленным цветом, динамическим размером шрифта, шрифтом Arial, округлённым текстом объёма из MathRound и "IntegerToString", а также свойствами недоступности выбора и скрытия, заданными функциями "ObjectSetInteger" и "ObjectSetString".

Для поддержки переменной прозрачности визуальных элементов мы определяем функцию "ApplyTransparencyToColor", извлекая красную, зелёную и синюю составляющие базового цвета с помощью битового AND и сдвигов. Входная прозрачность ограничивается диапазоном от 0 до 100 с помощью MathMin и "MathMax"; затем вычисляется альфа-канал как 255 минус масштабированный процент, и возвращается новый цвет с альфа-каналом, сдвинутым на 24 бита влево и объединённым через OR с расположением синего, зелёного и красного компонентов. Наконец, мы можем нарисовать цветной фоновый прямоугольник для фиксированных таймфреймов, чтобы визуально отделить профили фиксированного диапазона на графике.

//+------------------------------------------------------------------+
//| Render fixed range background rectangle                          |
//+------------------------------------------------------------------+
void RenderFixedRangeBackgroundRectangle(int sessionIndex) {
   if(profileTimeframe != FIXED) return;                               //--- Return if not fixed
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   
   string objectName = objectPrefix + "FixedBG_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create object name
   
   if(ObjectFind(0, objectName) < 0) {                                 //--- Check if not found
      ObjectCreate(0, objectName, OBJ_RECTANGLE, 0, sessions[sessionIndex].startTime, sessions[sessionIndex].sessionHigh,
                   sessions[sessionIndex].endTime > 0 ? sessions[sessionIndex].endTime : TimeCurrent(),
                   sessions[sessionIndex].sessionLow);                 //--- Create rectangle
      ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);      //--- Set selectable false
      ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true);           //--- Set hidden true
      ObjectSetInteger(0, objectName, OBJPROP_BACK, true);             //--- Set back true
   }
   
   ObjectSetInteger(0, objectName, OBJPROP_COLOR, fixedRangeBackgroundColor); //--- Set color
   ObjectSetInteger(0, objectName, OBJPROP_FILL, true); //--- Set fill true
}

Мы добавляем функцию "RenderFixedRangeBackgroundRectangle" для отрисовки залитого фона профилей фиксированного таймфрейма, улучшая визуальное разделение на графике. Функция сразу завершается, если таймфрейм не равен "FIXED" или индекс сессии выходит за допустимые границы массива. Имя объекта формируется из префикса и времени начала сессии через IntegerToString, затем, если по ObjectFind объект отсутствует, создаётся прямоугольник с помощью ObjectCreate и OBJ_RECTANGLE от времени начала на максимуме сессии до времени окончания (с возвратом к TimeCurrent если время окончания равно нулю) на минимуме сессии. Он задаётся как недоступный для выбора, скрытый и отрисовываемый позади других элементов. В любом случае применяем входной цвет фона фиксированного диапазона и включаем сплошную заливку через ObjectSetInteger чтобы получить цветную подложку для диапазона профиля. Теперь мы можем вызвать эти функции в основной функции отрисовки профиля, которая выполняет основную работу, а затем вызывать всю логику в обработчике расчёта по тикам.

//+------------------------------------------------------------------+
//| Render session profile                                           |
//+------------------------------------------------------------------+
void RenderSessionProfile(int sessionIndex) {
   if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid
   
   int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size
   if(size == 0 || sessions[sessionIndex].startTime == 0) return; //--- Return if no levels or no start time
   
   int barIndex = iBarShift(_Symbol, _Period, sessions[sessionIndex].startTime); //--- Get bar index
   if(barIndex < 0) return;                       //--- Return if invalid
   
   PadLevelsForSplitProfile(sessionIndex);        //--- Pad levels for split
   SortPriceLevelsDescending(sessionIndex);       //--- Sort levels descending
   CalculatePointOfControl(sessionIndex);         //--- Calculate POC
   BuildVolumeProfileAndFindPoc(sessionIndex);    //--- Build VP and find POC
   
   int totalTpoCount = GetTotalTpoCount(sessionIndex); //--- Get total TPO count
   int pointOfControlIndex = (useVolumeProfilePocForValueArea && sessions[sessionIndex].volumeProfilePocIndex >= 0) ? 
                             sessions[sessionIndex].volumeProfilePocIndex : sessions[sessionIndex].pointOfControlIndex; //--- Select POC index
   
   int valueAreaUpperIndex = pointOfControlIndex; //--- Initialize value area upper index
   int valueAreaLowerIndex = pointOfControlIndex; //--- Initialize value area lower index
   
   if(pointOfControlIndex >= 0) {                 //--- Check valid POC index
      int targetTpoCount = (int)(totalTpoCount * valueAreaPercent / 100.0); //--- Calculate target TPO count
      int currentTpoCount = sessions[sessionIndex].levels[pointOfControlIndex].tpoCount; //--- Set current TPO count
      
      while(currentTpoCount < targetTpoCount && (valueAreaUpperIndex > 0 || valueAreaLowerIndex < size - 1)) { //--- Loop to expand value area
         int upperTpoCount = (valueAreaUpperIndex > 0) ? sessions[sessionIndex].levels[valueAreaUpperIndex - 1].tpoCount : 0; //--- Get upper TPO count
         int lowerTpoCount = (valueAreaLowerIndex < size - 1) ? sessions[sessionIndex].levels[valueAreaLowerIndex + 1].tpoCount : 0; //--- Get lower TPO count
         
         if(upperTpoCount >= lowerTpoCount && valueAreaUpperIndex > 0) { //--- Check upper expansion
            valueAreaUpperIndex--;                //--- Decrement upper index
            currentTpoCount += upperTpoCount;     //--- Add upper TPO
         } else if(valueAreaLowerIndex < size - 1) { //--- Check lower expansion
            valueAreaLowerIndex++;                //--- Increment lower index
            currentTpoCount += lowerTpoCount;     //--- Add lower TPO
         } else if(valueAreaUpperIndex > 0) {     //--- Fallback upper expansion
            valueAreaUpperIndex--;                //--- Decrement upper index
            currentTpoCount += upperTpoCount;     //--- Add upper TPO
         } else {                                 //--- Break if no more
            break;                                //--- Exit loop
         }
      }
   }
   
   string displayStrings[];                       //--- Declare display strings array
   ArrayResize(displayStrings, size);             //--- Resize display strings
   for(int i = 0; i < size; i++) {                //--- Loop through levels
      displayStrings[i] = sessions[sessionIndex].levels[i].tpoString; //--- Copy TPO string
   }
   
   int openLevelIndex  = -1;                      //--- Initialize open level index
   int closeLevelIndex = -1;                      //--- Initialize close level index
   
   if(tpoCharacterType == ALPHABETIC) {           //--- Check alphabetic
      double openPrice  = sessions[sessionIndex].sessionOpen; //--- Get open price
      double closePrice = sessions[sessionIndex].sessionClose; //--- Get close price
      
      for(int i = 0; i < size; i++) {             //--- Loop to find levels
         if(openLevelIndex < 0  && MathAbs(sessions[sessionIndex].levels[i].price - openPrice)  < tpoPriceGridStep / 2) //--- Check open match
            openLevelIndex = i;                   //--- Set open index
         if(closeLevelIndex < 0 && MathAbs(sessions[sessionIndex].levels[i].price - closePrice) < tpoPriceGridStep / 2) //--- Check close match
            closeLevelIndex = i;                  //--- Set close index
      }
      
      RenderOpenTpoHighlight(sessionIndex, openLevelIndex, displayStrings); //--- Render open highlight
      RenderCloseTpoHighlight(sessionIndex, closeLevelIndex, displayStrings); //--- Render close highlight
   }
   
   for(int i = 0; i < size; i++) {                //--- Loop to render levels
      string objectName = objectPrefix + "TPO_" + IntegerToString(sessions[sessionIndex].startTime) + "_" + IntegerToString(i); //--- Create object name
      
      color textColor = defaultTpoColor;          //--- Set default color
      
      if(sessions[sessionIndex].levels[i].tpoCount == 1) { //--- Check single print
         textColor = singlePrintColor;            //--- Set single print color
      }
      
      if(i >= valueAreaUpperIndex && i <= valueAreaLowerIndex) { //--- Check value area
         textColor = valueAreaColor;              //--- Set value area color
      }
      
      if(i == sessions[sessionIndex].pointOfControlIndex && sessions[sessionIndex].volumeProfilePocIndex != sessions[sessionIndex].pointOfControlIndex) { //--- Check TPO POC
         textColor = pointOfControlColor;         //--- Set POC color
      }
      
      if(highlightVolumeProfilePoc && i == sessions[sessionIndex].volumeProfilePocIndex) { //--- Check VP POC
         textColor = volumeProfilePocColor;       //--- Set VP POC color
      }
      
      if(highlightSessionVwap && MathAbs(sessions[sessionIndex].levels[i].price - sessions[sessionIndex].vwap) < tpoPriceGridStep / 2) { //--- Check VWAP
         textColor = sessionVwapColor;            //--- Set VWAP color
      }
      
      if(ObjectFind(0, objectName) < 0) {         //--- Check if object not found
         ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0);       //--- Create label
         ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, 0); //--- Set X distance
         ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, 0); //--- Set Y distance
      }
      
      datetime labelTime = iTime(_Symbol, _Period, barIndex); //--- Get label time
      int x, y;                                               //--- Declare coordinates
      ChartTimePriceToXY(0, 0, labelTime, sessions[sessionIndex].levels[i].price, x, y); //--- Convert to XY
      
      ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, x); //--- Set X distance
      ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, y); //--- Set Y distance
      ObjectSetInteger(0, objectName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
      ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_LEFT);       //--- Set anchor
      ObjectSetInteger(0, objectName, OBJPROP_COLOR, textColor);          //--- Set color
      ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize);   //--- Set font size
      ObjectSetString(0, objectName, OBJPROP_FONT, "Arial");              //--- Set font
      ObjectSetString(0, objectName, OBJPROP_TEXT, displayStrings[i]);    //--- Set text
      ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);         //--- Set selectable false
      ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true);              //--- Set hidden true
   }
   
   RenderOpenCloseMarkers(sessionIndex, barIndex);  //--- Render open close markers
   RenderProfileBorderLine(sessionIndex, barIndex); //--- Render profile border
   
   if(initialBalancePeriods > 0)                    //--- Check if IB periods
      RenderInitialBalanceLines(sessionIndex, barIndex); //--- Render IB lines
   
   if(showExtensionLines)                           //--- Check if show extensions
      RenderKeyLevelExtensions(sessionIndex, barIndex, valueAreaUpperIndex, valueAreaLowerIndex); //--- Render extensions
   
   if(profileTimeframe == FIXED && renderVolumes)   //--- Check fixed and render volumes
      RenderFixedRangeVolumeLabels(sessionIndex, barIndex); //--- Render volume labels
   
   RenderFixedRangeBackgroundRectangle(sessionIndex); //--- Render fixed background
}

Здесь мы расширяем функцию "RenderSessionProfile", добавляя расширенные возможности отрисовки рыночного профиля. Сначала выполняются проверки индекса и данных: функция сразу завершается, если индекс некорректен, уровни отсутствуют или не задано время начала, после чего получает индекс бара через функцию iBarShift. Мы подготавливаем профиль, вызывая "PadLevelsForSplitProfile" для выравнивания в режиме разделения, сортируем уровни по убыванию через "SortPriceLevelsDescending", рассчитываем точку контроля Time Price Opportunity с помощью "CalculatePointOfControl" и строим объёмный профиль для поиска его точки контроля через "BuildVolumeProfileAndFindPoc".

После получения общего количества Time Price Opportunity через "GetTotalTpoCount" мы условно выбираем индекс точки контроля: если "useVolumeProfilePocForValueArea" равно true и точка контроля по объёму существует, используем её; иначе возвращаемся к TPO-точке контроля. Затем инициализируем и расширяем индексы области стоимости вокруг неё, накапливая счётчики в цикле и выбирая соседний уровень с большим количеством Time Price Opportunities до достижения цели на основе "valueAreaPercent". Для подготовки визуального отображения изменяем размер и копируем строки Time Price Opportunity в массив отображения через ArrayResize. Если выбран буквенный режим, находим индексы уровней открытия и закрытия проходом по уровням с MathAbs и допуском в половину шага сетки, а затем отрисовываем подсветки, вызывая "RenderOpenTpoHighlight" и "RenderCloseTpoHighlight".

В основном цикле отрисовки для каждого уровня мы формируем уникальное имя объекта, начинаем с цвета по умолчанию и переопределяем его для одиночных принтов, диапазона области стоимости, отдельной точки контроля Time Price Opportunity, выделенной объёмной точки контроля или совпадения закрытия со средневзвешенной ценой в пределах допуска. Если объекта нет по "ObjectFind", создаём или обновляем метки с помощью "ObjectCreate" и "OBJ_LABEL", преобразуем время и цену в координаты через ChartTimePriceToXY после получения времени метки из "iTime" и применяем позицию, угол размещения, точку привязки, цвет, размер шрифта, шрифт, отображаемый текст и свойства недоступности выбора/скрытия.

Наконец, мы добавляем вспомогательные элементы, вызывая "RenderOpenCloseMarkers", "RenderProfileBorderLine", "RenderInitialBalanceLines", если заданы периоды, "RenderKeyLevelExtensions" с индексами области стоимости, если включены продления, "RenderFixedRangeVolumeLabels" для фиксированных таймфреймов с объёмами и "RenderFixedRangeBackgroundRectangle" для фонов. Теперь осталось обновить обработчик расчёта по тикам, чтобы сохранять H/L/O каждого периода, рассчитывать IB при попадании в заданные периоды, добавлять объём к уровням и вызывать VWAP для всего расчёта.

//+------------------------------------------------------------------+
//| Calculate custom indicator                                       |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int 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 int &spread[]) {
   
   if(rates_total < 2) return 0;                              //--- Return if insufficient rates
   
   datetime currentBarTime = time[rates_total - 1];           //--- Get current bar time
   bool isNewBar = (currentBarTime != lastCompletedBarTime);  //--- Check if new bar
   
   if(IsNewSessionStarted(currentBarTime, previousBarTime) || previousBarTime == 0) { //--- Check new session
      if(activeSessionIndex >= 0 && activeSessionIndex < ArraySize(sessions)) { //--- Check active session
         sessions[activeSessionIndex].endTime = previousBarTime; //--- Set end time
         RenderSessionProfile(activeSessionIndex);             //--- Render session profile
      }
      
      activeSessionIndex = CreateNewSession();                 //--- Create new session
      sessions[activeSessionIndex].startTime = currentBarTime; //--- Set start time
      sessions[activeSessionIndex].sessionOpen = open[rates_total - 1]; //--- Set session open
      sessions[activeSessionIndex].sessionHigh = high[rates_total - 1]; //--- Set session high
      sessions[activeSessionIndex].sessionLow = low[rates_total - 1]; //--- Set session low
      lastCompletedBarTime = currentBarTime;                   //--- Update last completed bar time
   }
   
   previousBarTime = currentBarTime;                           //--- Update previous bar time
   
   if(isNewBar && IsBarEligibleForProcessing(currentBarTime) && activeSessionIndex >= 0) { //--- Check if process bar
      sessions[activeSessionIndex].sessionHigh = MathMax(sessions[activeSessionIndex].sessionHigh, high[rates_total - 1]); //--- Update session high
      sessions[activeSessionIndex].sessionLow = MathMin(sessions[activeSessionIndex].sessionLow, low[rates_total - 1]); //--- Update session low
      sessions[activeSessionIndex].sessionClose = close[rates_total - 1]; //--- Update session close
      
      int periodIndex = sessions[activeSessionIndex].periodCount;             //--- Get period index
      ArrayResize(sessions[activeSessionIndex].periodHighs, periodIndex + 1); //--- Resize period highs
      ArrayResize(sessions[activeSessionIndex].periodLows, periodIndex + 1);  //--- Resize period lows
      ArrayResize(sessions[activeSessionIndex].periodOpens, periodIndex + 1); //--- Resize period opens
      
      sessions[activeSessionIndex].periodHighs[periodIndex] = high[rates_total - 1]; //--- Set period high
      sessions[activeSessionIndex].periodLows[periodIndex] = low[rates_total - 1];   //--- Set period low
      sessions[activeSessionIndex].periodOpens[periodIndex] = open[rates_total - 1]; //--- Set period open
      sessions[activeSessionIndex].periodCount++; //--- Increment period count
      
      if(periodIndex < initialBalancePeriods) {   //--- Check if within IB periods
         if(periodIndex == 0) {                   //--- Handle first period
            sessions[activeSessionIndex].initialBalanceHigh = high[rates_total - 1]; //--- Set IB high
            sessions[activeSessionIndex].initialBalanceLow = low[rates_total - 1];   //--- Set IB low
         } else {                                 //--- Handle subsequent
            sessions[activeSessionIndex].initialBalanceHigh = MathMax(sessions[activeSessionIndex].initialBalanceHigh, high[rates_total - 1]); //--- Update IB high
            sessions[activeSessionIndex].initialBalanceLow = MathMin(sessions[activeSessionIndex].initialBalanceLow, low[rates_total - 1]); //--- Update IB low
         }
      }
      
      double quantizedHigh = QuantizePriceToGrid(high[rates_total - 1]); //--- Quantize high
      double quantizedLow = QuantizePriceToGrid(low[rates_total - 1]);   //--- Quantize low
      
      for(double price = quantizedLow; price <= quantizedHigh; price += tpoPriceGridStep) { //--- Loop through prices
         int levelIndex = GetOrCreatePriceLevel(activeSessionIndex, price);                 //--- Get or create level
         if(levelIndex >= 0) {                    //--- Check valid level
            AddTpoCharacterToLevel(activeSessionIndex, levelIndex, periodIndex);            //--- Add TPO character
            sessions[activeSessionIndex].levels[levelIndex].volume += (double)tick_volume[rates_total - 1] / 
                                                       MathMax(1, (quantizedHigh - quantizedLow) / tpoPriceGridStep + 1); //--- Add volume
         }
      }
      
      CalculateSessionVwap(activeSessionIndex);   //--- Calculate VWAP
      lastCompletedBarTime = currentBarTime;      //--- Update last completed bar time
   }
   
   if(IsBarEligibleForProcessing(currentBarTime) && activeSessionIndex >= 0) { //--- Check if update session
      sessions[activeSessionIndex].sessionClose = close[rates_total - 1];      //--- Update close
      sessions[activeSessionIndex].sessionHigh = MathMax(sessions[activeSessionIndex].sessionHigh, high[rates_total - 1]); //--- Update high
      sessions[activeSessionIndex].sessionLow = MathMin(sessions[activeSessionIndex].sessionLow, low[rates_total - 1]); //--- Update low
   }
   
   for(int i = 0; i < ArraySize(sessions); i++) { //--- Loop through sessions
      RenderSessionProfile(i);                    //--- Render profile
   }
   
   return rates_total;                            //--- Return rates total
}

В обработчике события OnCalculate мы обновляем логику для интеграции новых возможностей, таких как отслеживание начального баланса и накопление объёма при обработке данных. Сначала возвращаем ноль, если доступно недостаточно ценовых данных. Мы извлекаем время текущего бара и проверяем появление нового бара; затем, если начинается новая сессия по "IsNewSessionStarted" или предыдущего времени нет, фиксируем время окончания активной сессии и отрисовываем её через "RenderSessionProfile", если она допустима по границам через ArraySize, создаём новую с помощью "CreateNewSession", инициализируем основные поля по текущему бару и обновляем время последнего завершённого бара так же, как в предыдущей версии.

После установки предыдущего времени бара, если появился новый подходящий бар по "IsBarEligibleForProcessing" и активная сессия существует, мы обновляем экстремумы сессии с помощью MathMax и MathMin плюс цену закрытия, изменяем размеры массивов и сохраняем максимумы, минимумы и цены открытия периодов, увеличиваем счётчик периодов и, если период находится в пределах "initialBalancePeriods", задаём или обновляем максимум и минимум начального баланса на основе индекса периода. Мы дискретизируем максимум и минимум бара, проходим по диапазону, получая или создавая уровни через "GetOrCreatePriceLevel", добавляем символы Time Price Opportunity через "AddTpoCharacterToLevel" и накапливаем объём пропорционально, используя тиковый объём, делённый на количество ценовых шагов, где минимум один шаг гарантируется через "MathMax". Затем рассчитываем средневзвешенную цену через "CalculateSessionVwap" и обновляем время последнего завершённого бара. Если бар подходит для обработки, обновляем закрытие, максимум и минимум сессии для данных в реальном времени, после чего отрисовываем все сессии в цикле и возвращаем общее количество баров. После компиляции получаем следующий результат.

HYBRID VOLUME-BASED MARKET PROFILE - DOT CHARACTERS

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


Бэктестинг

Мы провели тестирование, и ниже представлена итоговая визуализация в виде одной анимированной картинки в формате Graphics Interchange Format (GIF).

BACKTEST GIF


Заключение

В заключение мы улучшили гибридный Time Price Opportunity индикатор рыночного профиля (TPO) в MQL5 за счёт интеграции данных объёма для расчёта точки контроля по объёму, области стоимости и средневзвешенной по объёму цены с настраиваемой подсветкой. Система добавляет расширенные возможности: определение начального баланса, линии продления ключевых уровней, режим split-профиля, альтернативные TPO-символы в виде квадратов или кругов, линии границ, фоны фиксированных диапазонов и динамические метки объёма для комплексного визуального анализа профиля на разных таймфреймах. С этим обновлённым гибридным индикатором рыночного профиля Time Price Opportunity вы сможете глубже анализировать структуру рынка и динамику объёма, а также продолжить его оптимизацию в своей торговой практике. Удачной торговли!

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

Прикрепленные файлы |
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Нейросети в трейдинге: От единственного прогноза к пространству рыночных сценариев (ORION) Нейросети в трейдинге: От единственного прогноза к пространству рыночных сценариев (ORION)
В статье начинается адаптация фреймворка ORION к анализу финансовых рынков. Вместо единственной прогнозной траектории модель формирует несколько сценариев будущего движения, оценивает их вероятность и неопределённость. Практическая часть посвящена OpenCL-кернелам для генерации сценариев, расчёта ответственности и подготовки обучающих сигналов для роутера, генератора и блока неопределённости.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Оптимизация торговли на снятии ликвидности: Как различать снятие ликвидности и сдвиги рыночной структуры Оптимизация торговли на снятии ликвидности: Как различать снятие ликвидности и сдвиги рыночной структуры
Эта статья посвящена специализированному трендовому советнику, цель которого — подробно показать, как использовать торговые сетапы после снятий ликвидности. В ней рассматривается советник, специально разработанный для трейдеров, стремящихся оптимизировать применение рейдов и снятий ликвидности как критериев входа в рынок и принятия торговых решений. Также рассматривается, как правильно различать снятия ликвидности и сдвиги рыночной структуры, проверять и использовать каждый из этих сценариев при его возникновении и тем самым снижать потери, вызванные их смешением.