Создание пользовательских индикаторов в MQL5 (Часть 8): Интеграция объёма для углублённого анализа рыночного профиля (Market Profile)
Введение
В нашей предыдущей статье (Часть 7), мы разработали гибридный Time Price Opportunity индикатор рыночного профиля (TPO) в MetaQuotes Language 5 (MQL5), который поддерживал несколько таймфреймов сессий, включая внутридневные, дневные, недельные, месячные и фиксированные периоды с учётом часового пояса, дискретизировал цены по сетке, отслеживал данные сессии по максимумам, минимумам, ценам открытия и закрытия, рассчитывал точку контроля (POC) и область стоимости по количеству TPO, а также отрисовывал профиль на графике с настраиваемыми цветами для детального анализа структуры сессий.
В части 8 мы расширяем этот индикатор за счёт интеграции объёма, чтобы получить более глубокие сведения из рыночного профиля, включая расчёты точки контроля по объёму, области стоимости и средневзвешенной по объёму цены с вариантами подсветки. Это обновление добавляет расширенные возможности: определение начального баланса, линии продления ключевых уровней, режим split-профиля, альтернативные TPO-символы в виде квадратов или кругов, линии границ, фоновые прямоугольники для фиксированных диапазонов и динамические метки объёма, сохраняя гибкость работы на разных таймфреймах. Мы рассмотрим следующие темы:
- Интеграция объёма и расширенных возможностей в гибридные рыночные TPO-профили
- Реализация в MQL5
- Бэктестинг
- Заключение
К концу статьи у вас будет расширенный индикатор MQL5 для гибридных рыночных профилей Time Price Opportunity с расширенным анализом на основе объёма, готовый к дальнейшей настройке. Приступим!
Интеграция объёма и расширенных возможностей в гибридные рыночные TPO-профили
Интеграция данных объёма в гибридные Time Price Opportunity рыночные профили (TPO) расширяет традиционный анализ цены и времени за счёт учёта торгового объёма на каждом ценовом уровне. Это позволяет определять точку контроля по объёму (volume-based POC), то есть уровень, где прошёл наибольший объём, и корректировать область стоимости так, чтобы она отражала реальное участие рынка, а не только время, проведённое ценой на уровне. Такое улучшение позволяет оценить степень убеждённости участников рынка и то, насколько движение подтверждено объёмом: например, позволяет отличать высокообъёмные зоны справедливой цены (fair value), которые могут выступать поддержкой или сопротивлением, от низкообъёмных областей, указывающих на слабый интерес или возможные пробои. При этом такие элементы, как средневзвешенная по объёму цена (VWAP), дают динамический ориентир для оценки внутридневного направления рынка.
Мы расширим базовое управление сессиями накоплением объёма на каждом ценовом уровне, добавим опциональный расчёт объёмной точки контроля и средневзвешенной по объёму цены (VWAP), определим начальный баланс по ранним периодам, добавим отрисовку линий продолжения для ключевых уровней, таких как максимум, минимум, границы области стоимости и срединный уровень, включим варианты символов и способы обозначения цен открытия, а также включим режим split-профиля с отступами для более понятного отображения букв. Всё это будет поддерживать фоны фиксированных диапазонов и метки объёма для полной визуализации профиля. Ниже кратко показано визуальное представление наших целей.

Реализация в 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" и обновляем время последнего завершённого бара. Если бар подходит для обработки, обновляем закрытие, максимум и минимум сессии для данных в реальном времени, после чего отрисовываем все сессии в цикле и возвращаем общее количество баров. После компиляции получаем следующий результат.

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

Заключение
В заключение мы улучшили гибридный Time Price Opportunity индикатор рыночного профиля (TPO) в MQL5 за счёт интеграции данных объёма для расчёта точки контроля по объёму, области стоимости и средневзвешенной по объёму цены с настраиваемой подсветкой. Система добавляет расширенные возможности: определение начального баланса, линии продления ключевых уровней, режим split-профиля, альтернативные TPO-символы в виде квадратов или кругов, линии границ, фоны фиксированных диапазонов и динамические метки объёма для комплексного визуального анализа профиля на разных таймфреймах. С этим обновлённым гибридным индикатором рыночного профиля Time Price Opportunity вы сможете глубже анализировать структуру рынка и динамику объёма, а также продолжить его оптимизацию в своей торговой практике. Удачной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21390
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Нейросети в трейдинге: От единственного прогноза к пространству рыночных сценариев (ORION)
Оптимизация торговли на снятии ликвидности: Как различать снятие ликвидности и сдвиги рыночной структуры
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования