Создание пользовательских индикаторов в MQL5 (Часть 7): Гибридные рыночные профили Time Price Opportunity (TPO) для анализа сессий
Введение
В предыдущей статье (Часть 6) мы разработали усовершенствованную систему расчёта Relative Strength Index на языке MetaQuotes Language 5 (MQL5), в которой были реализованы методы сглаживания, сдвиги оттенков для улучшения визуализации и поддержка нескольких таймфреймов для комплексного анализа рынка. В части 7 мы разрабатываем гибридный индикатор Time Price Opportunity (TPO) рыночного профиля с поддержкой разных таймфреймов сессий, включая внутридневные, дневные, недельные, месячные и фиксированные периоды с учётом часового пояса. Этот индикатор дискретизирует цены по сетке, отслеживает данные сессии, включая максимум, минимум, цены открытия и закрытия, рассчитывает POC и область стоимости по количеству TPO, а также отрисовывает профиль на графике с настраиваемыми цветами для подробного анализа сессии. Мы рассмотрим следующие темы:
- Изучение концепции гибридного рыночного профиля Time Price Opportunity
- Реализация в MQL5
- Бэктестирование
- Заключение
В итоге у вас будет рабочий индикатор MQL5 для гибридных рыночных TPO-профилей, готовый к дальнейшей настройке. Приступим!
Изучение концепции гибридного рыночного профиля Time Price Opportunity
Гибридный рыночный профиль Time Price Opportunity (TPO) — это инструмент визуализации, который отображает распределение цены во времени в рамках заданных торговых сессий. Для представления временных интервалов на конкретных ценовых уровнях используются буквы, прямоугольные маркеры или просто точки. Такой профиль показывает зоны повышенной активности, например область стоимости и точку контроля, где происходила основная часть торговли. Этот подход помогает определять поддержку, сопротивление и зоны справедливой стоимости, агрегируя ценовое движение в профильную гистограмму: более плотные скопления TPO указывают на сбалансированную торговлю, а более тонкие — на потенциальные пробои или дисбалансы. Обычно мы применяем его по сессиям, чтобы оценивать рыночные настроения, открывать позиции вблизи границ области стоимости или отслеживать смещения POC для продолжения тренда.
Наш план состоит в том, чтобы определять сессии на основе выбранных таймфреймов с учётом часового пояса. Мы будем дискретизировать цены по сетке для назначения TPO и отслеживать метрики сессии, например максимумы и минимумы. Также мы рассчитаем POC как уровень с наибольшим количеством TPO. Затем определим область стоимости, покрывающую заданный процент от общего числа TPO. Наконец, мы визуализируем профиль с помощью меток, точек и квадратов с цветовым кодированием для более глубокого анализа графика. Ниже приведено визуальное представление наших целей.

Реализация в MQL5
Чтобы создать индикатор в MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку Indicators, нажмите вкладку "New" и следуйте подсказкам для создания файла. После его создания в среде разработки мы определим свойства и настройки индикатора, например количество буферов и построений индикатора.
//+------------------------------------------------------------------+ //| Hybrid TPO Market Profile PART 1.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 }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ sinput group "Settings" input double ticksPerTpoLetter = 10; // Ticks per letter input int valueAreaPercent = 70; // Value Area Percent 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) sinput group "Rendering" input int labelFontSize = 10; // Font size 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 closeColor = clrRed; // Close //+------------------------------------------------------------------+ //| 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 }; 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 TpoPriceLevel levels[]; // Store array of price levels int periodCount; // Store period count double periodOpens[]; // Store array of period opens int pointOfControlIndex; // Store point of control index }; //+------------------------------------------------------------------+ //| 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 int maxSessionHistory = 20; //--- Set maximum session history int timezoneOffsetSeconds = 0; //--- Initialize timezone offset in seconds
Мы начинаем реализацию с настройки свойств индикатора с помощью директив #property, задавая отображение в основном окне графика с помощью indicator_chart_window и указывая ноль буферов и построений, поскольку этот индикатор ориентирован на пользовательскую отрисовку объектов, а не на построение линий или гистограмм. Далее мы определяем перечисление "MarketProfileTimeframe", которое позволяет выбирать периоды профиля: "INTRADAY" для краткосрочных сессий, "DAILY" для стандартных торговых дней, "WEEKLY", "MONTHLY" и "FIXED" для пользовательских диапазонов. Это обеспечивает гибкость при анализе сессий.
Затем мы объявляем пользовательские входные параметры, сгруппированные по разделам с помощью директив sinput group. Начинаем с настроек "ticksPerTpoLetter", управляющих ценовой детализацией на одну букву Time Price Opportunity и "valueAreaPercent", задающего процент от общего числа Time Price Opportunities, определяющий область стоимости. В группе времени входные параметры включают выбор таймфрейма профиля, строку часового пояса для корректировки смещения, диапазон дневной сессии в виде строки, например "0830-1500", длину внутридневного профиля в минутах, а также значения datetime для начала и конца фиксированного диапазона. Группа отрисовки добавляет "labelFontSize" для отображения текста, а группа цветов задаёт настраиваемые цвета: "defaultTpoColor" для обычных букв, "singlePrintColor" для одиночных принтов, "valueAreaColor", "pointOfControlColor" и "closeColor" для визуального разграничения элементов профиля.
Для единообразной работы мы вводим константы с помощью #define, включая "MAX_BARS_BACK" для ограничения обработки исторических баров и "TPO_CHARACTERS_STRING" как алфавитную последовательность для назначения букв периодам Time Price Opportunity. Для организации данных мы создаём две структуры: "TpoPriceLevel" для хранения сведений об отдельном ценовом уровне с полями цены, строки символов Time Price Opportunity и счётчика; и "ProfileSessionData" для управления информацией по сессии, включая время начала и окончания, цены открытия, закрытия, максимума и минимума, массив ценовых уровней, количество периодов, массив открытий периодов и индекс POC.
Наконец, мы инициализируем глобальные переменные необходимые для управления во время выполнения: "objectPrefix" для именования графических объектов, массив "sessions" для хранения данных профиля, "activeSessionIndex" со стартовым значением -1 для отслеживания текущей сессии, "tpoPriceGridStep" для квантования цены, массив "tpoCharacterSet" для назначения букв, временные метки "previousBarTime" и "lastCompletedBarTime", "maxSessionHistory" для ограничения истории 20 сессиями и "timezoneOffsetSeconds" для корректировки времени. После этого можно инициализировать индикатор.
//+------------------------------------------------------------------+ //| Initialize custom indicator | //+------------------------------------------------------------------+ int OnInit() { IndicatorSetString(INDICATOR_SHORTNAME, "Hybrid TPO Market Profile - Part 1"); //--- 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 } 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 }
Продолжаем реализацию с обработчика события OnInit, который запускается при подключении индикатора к графику и задаёт основные настройки гибридного рыночного профиля Time Price Opportunity. Сначала мы присваиваем индикатору короткое имя с помощью функции IndicatorSetString с параметром INDICATOR_SHORTNAME чтобы в интерфейсе платформы отображалось имя "Hybrid TPO Market Profile - Part 1".
Далее мы рассчитываем шаг ценовой сетки, умножая пользовательский параметр "ticksPerTpoLetter" на значение пункта символа _Point, и сохраняем результат в "tpoPriceGridStep", чтобы определить вертикальный интервал между буквами Time Price Opportunity на основе ценовых приращений. Затем мы подготавливаем набор символов для маркировки Time Price Opportunities: изменяем размер массива "tpoCharacterSet" до 52 элементов и заполняем его в цикле, извлекая каждую букву из константы "TPO_CHARACTERS_STRING" с помощью StringSubstr для использования как заглавных, так и строчных букв при идентификации периодов.
Чтобы учесть корректировку времени, мы проверяем, отличается ли входной параметр "timezone" от "Exchange". Если да, извлекаем часть со смещением, начиная с четвёртого символа, с помощью "StringSubstr", преобразуем её в целое число с помощью StringToInteger, и рассчитываем "timezoneOffsetSeconds", умножая смещение на 3600 для представления часов в секундах. Мы сбрасываем массив "sessions", изменяя его размер до нуля с помощью ArrayResize, очищая предыдущие данные для нового построения сессионных профилей. Наконец, возвращаем INIT_SUCCEEDED чтобы сообщить платформе об успешной инициализации. Теперь нужно выполнять расчёты индикатора на каждом тике. Чтобы код был модульным и удобным для сопровождения, организуем логику во вспомогательных функциях. Начнём с создания сессии и разбора диапазонов сессий.
//+------------------------------------------------------------------+ //| Create new session | //+------------------------------------------------------------------+ int CreateNewSession() { int size = ArraySize(sessions); //--- Get size of sessions array if(size >= maxSessionHistory) { //--- Check if size exceeds history limit 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].periodCount = 0; //--- Initialize period count sessions[newIndex].pointOfControlIndex = -1; //--- Initialize point of control index ArrayResize(sessions[newIndex].levels, 0); //--- Resize levels array ArrayResize(sessions[newIndex].periodOpens, 0);//--- Resize period opens array return newIndex; //--- Return new index } //+------------------------------------------------------------------+ //| Quantize price to grid | //+------------------------------------------------------------------+ double QuantizePriceToGrid(double price) { return MathRound(price / tpoPriceGridStep) * tpoPriceGridStep; //--- Calculate and return quantized price } //+------------------------------------------------------------------+ //| Parse daily session time range | //+------------------------------------------------------------------+ bool ParseDailySessionTimeRange(int &startHour, int &startMinute, int &endHour, int &endMinute) { string parts[]; //--- Declare parts array int count = StringSplit(dailySessionRange, '-', parts); //--- Split daily session range if(count != 2) return false; //--- Return false if invalid count startHour = (int)StringToInteger(StringSubstr(parts[0], 0, 2)); //--- Parse start hour startMinute = (int)StringToInteger(StringSubstr(parts[0], 2, 2)); //--- Parse start minute endHour = (int)StringToInteger(StringSubstr(parts[1], 0, 2)); //--- Parse end hour endMinute = (int)StringToInteger(StringSubstr(parts[1], 2, 2)); //--- Parse end minute return true; //--- Return true }
Сначала мы определяем функцию "CreateNewSession", которая добавляет новую сессию профиля в массив. Для начала получаем текущий размер массива с помощью ArraySize Если размер достиг максимального лимита истории или превышает его, мы сдвигаем существующие сессии вперёд в цикле, удаляя самую старую, а затем уменьшаем размер массива с помощью ArrayResize перед обновлением размера. Затем мы расширяем массив на один элемент для новой сессии, задаём её индекс и инициализируем все поля значениями по умолчанию, например 0 или -1, включая очистку массивов уровней и открытий периодов. Чтобы цены соответствовали сетке Time Price Opportunity, мы реализуем функцию "QuantizePriceToGrid", которая делит входную цену на шаг сетки, округляет результат с помощью функции MathRound и умножает обратно, привязывая цену к ближайшей точке сетки.
Для обработки дневных сессий функция "ParseDailySessionTimeRange" разбивает входную строку диапазона с помощью функции StringSplit с разделителем-дефисом на части. Если найдены ровно две части, мы извлекаем и преобразуем часы и минуты из каждой части с помощью "StringSubstr" и StringToInteger, присваивая их параметрам, переданным по ссылке. В противном случае функция возвращает false, указывая на ошибку разбора. Далее нам понадобятся функции для фильтрации баров сессии и управления ценовыми уровнями внутри сессии.
//+------------------------------------------------------------------+ //| Check if bar is within daily session | //+------------------------------------------------------------------+ bool IsBarWithinDailySession(datetime barTime) { if(profileTimeframe != DAILY) return true; //--- Return true if not daily timeframe int startHour, startMinute, endHour, endMinute; //--- Declare time variables if(!ParseDailySessionTimeRange(startHour, startMinute, endHour, endMinute)) return true; //--- Parse and return true if fail MqlDateTime dateTimeStruct; //--- Declare date time struct TimeToStruct(barTime + timezoneOffsetSeconds, dateTimeStruct); //--- Convert time to struct int barMinutes = dateTimeStruct.hour * 60 + dateTimeStruct.min; //--- Calculate bar minutes int startMinutes = startHour * 60 + startMinute; //--- Calculate start minutes int endMinutes = endHour * 60 + endMinute; //--- Calculate end minutes if(endMinutes > startMinutes) { //--- Check if end after start return barMinutes >= startMinutes && barMinutes <= endMinutes; //--- Return if within range } else { //--- Handle overnight case return barMinutes >= startMinutes || barMinutes <= endMinutes; //--- Return if within range } } //+------------------------------------------------------------------+ //| Check if new session started | //+------------------------------------------------------------------+ bool IsNewSessionStarted(datetime currentTime, datetime previousTime) { if(previousTime == 0) return true; //--- Return true if no previous time datetime adjustedCurrent = currentTime + timezoneOffsetSeconds; //--- Adjust current time datetime adjustedPrevious = previousTime + timezoneOffsetSeconds; //--- Adjust previous time MqlDateTime currentDateTime, previousDateTime; //--- Declare date time structs TimeToStruct(adjustedCurrent, currentDateTime); //--- Convert current to struct TimeToStruct(adjustedPrevious, previousDateTime); //--- Convert previous to struct switch(profileTimeframe) { //--- Switch on profile timeframe case DAILY: { //--- Handle daily case int startHour, startMinute, endHour, endMinute; //--- Declare time variables if(!ParseDailySessionTimeRange(startHour, startMinute, endHour, endMinute)) return false; //--- Parse and return false if fail datetime sessionStart = StringToTime(TimeToString(adjustedCurrent, TIME_DATE) + " " + IntegerToString(startHour, 2, '0') + ":" + IntegerToString(startMinute, 2, '0')); //--- Calculate session start datetime prevSessionStart = StringToTime(TimeToString(adjustedPrevious, TIME_DATE) + " " + IntegerToString(startHour, 2, '0') + ":" + IntegerToString(startMinute, 2, '0')); //--- Calculate previous session start return adjustedCurrent >= sessionStart && adjustedPrevious < prevSessionStart; //--- Return if new session } case WEEKLY: //--- Handle weekly case return currentDateTime.day_of_week < previousDateTime.day_of_week || currentDateTime.day_of_year < previousDateTime.day_of_year; //--- Return if new week case MONTHLY: //--- Handle monthly case return currentDateTime.mon != previousDateTime.mon; //--- Return if new month case FIXED: //--- Handle fixed case return currentTime >= fixedTimeRangeStart && previousTime < fixedTimeRangeStart; //--- Return if new fixed range case INTRADAY: { //--- Handle intraday case long currentMinute = (adjustedCurrent / 60) * 60; //--- Calculate current minute long prevMinute = (adjustedPrevious / 60) * 60; //--- Calculate previous minute return (currentMinute % (intradayProfileLengthMinutes * 60)) == 0 && currentMinute != prevMinute; //--- Return if new intraday profile } } return false; //--- Return false } //+------------------------------------------------------------------+ //| Check if bar is eligible for processing | //+------------------------------------------------------------------+ bool IsBarEligibleForProcessing(datetime barTime) { if(profileTimeframe == FIXED) { //--- Check fixed timeframe return barTime >= fixedTimeRangeStart && barTime <= fixedTimeRangeEnd; //--- Return if within fixed range } if(profileTimeframe == DAILY) { //--- Check daily timeframe return IsBarWithinDailySession(barTime); //--- Return if within daily session } return true; //--- Return true } //+------------------------------------------------------------------+ //| Get or create price level | //+------------------------------------------------------------------+ int GetOrCreatePriceLevel(int sessionIndex, double price) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return -1; //--- Return invalid if index out of range int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size for(int i = 0; i < size; i++) { //--- Loop through levels if(MathAbs(sessions[sessionIndex].levels[i].price - price) < _Point / 2) //--- Check if price matches return i; //--- Return index } ArrayResize(sessions[sessionIndex].levels, size + 1); //--- Resize levels array sessions[sessionIndex].levels[size].price = price; //--- Set new price sessions[sessionIndex].levels[size].tpoString = ""; //--- Initialize TPO string sessions[sessionIndex].levels[size].tpoCount = 0; //--- Initialize TPO count return size; //--- Return new index }
Далее мы реализуем функцию "IsBarWithinDailySession", которая определяет, попадает ли заданный бар в указанные дневные торговые часы, и сразу возвращает true для недневных таймфреймов. Если разбор диапазона сессии через функцию "ParseDailySessionTimeRange" не удаётся, по умолчанию возвращается true. В противном случае мы преобразуем скорректированное время бара в структуру MqlDateTime с помощью TimeToStruct, рассчитываем общее количество минут для бара, времени начала и окончания, а затем проверяем, находится ли время бара внутри диапазона. При этом условная логика учитывает как обычные, так и ночные сессии.
Далее функция "IsNewSessionStarted" проверяет начало новой сессии профиля, сравнивая скорректированное текущее и предыдущее время; если предыдущего времени нет, она возвращает true. Мы корректируем временные метки с учётом смещения часового пояса, преобразуем их в структуры "MqlDateTime" и используем оператор switch по таймфрейму профиля: для дневного режима разбирается диапазон сессии и формируется время начала с помощью StringToTime, "TimeToString" и "IntegerToString", чтобы проверить переход к новому дню; для недельного режима сравниваются день недели и день года; месячный режим проверяет изменение месяца; фиксированный режим сравнивает время с входным временем начала; а внутридневной режим проверяет, совпадает ли текущая минута с границей длины профиля и не совпадает ли она с предыдущей.
Для фильтрации включаемых баров мы определяем функцию "IsBarEligibleForProcessing": для фиксированных таймфреймов она проверяет, находится ли время бара между входными значениями начала и окончания; для дневного режима вызывает "IsBarWithinDailySession"; в остальных случаях возвращает true для всех баров. Наконец, функция "GetOrCreatePriceLevel" управляет ценовыми уровнями внутри сессии, проверяя индекс сессии относительно размера массива sessions с помощью ArraySize, и возвращает -1, если индекс недопустим. Она перебирает существующие уровни, чтобы найти близкое совпадение с помощью MathAbs с допуском в половину пункта и возвращает индекс при успешном совпадении. Если совпадение не найдено, функция изменяет размер массива уровней, инициализирует цену нового уровня, пустую строку Time Price Opportunity и счётчик со значением ноль, а затем возвращает новый индекс. Поскольку сначала нам нужно отрисовать буквы профиля в определённом порядке, потребуется определить алгоритм пузырьковой сортировки, чтобы правильно упорядочить буквы перед отображением.
//+------------------------------------------------------------------+ //| 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 = tpoCharacterSet[periodIndex % 52]; //--- Get TPO character sessions[sessionIndex].levels[levelIndex].tpoString += tpoCharacter; //--- Append character to TPO string sessions[sessionIndex].levels[levelIndex].tpoCount++; //--- Increment TPO count } //+------------------------------------------------------------------+ //| Sort price levels descending | //+------------------------------------------------------------------+ void SortPriceLevelsDescending(int sessionIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size for(int i = 0; i < size - 1; i++) { //--- Outer loop for sorting for(int j = 0; j < size - i - 1; j++) { //--- Inner loop for comparison if(sessions[sessionIndex].levels[j].price < sessions[sessionIndex].levels[j + 1].price) { //--- Check if swap needed TpoPriceLevel temp = sessions[sessionIndex].levels[j]; //--- Store temporary level sessions[sessionIndex].levels[j] = sessions[sessionIndex].levels[j + 1]; //--- Swap levels sessions[sessionIndex].levels[j + 1] = temp; //--- Assign temporary back } } } } //+------------------------------------------------------------------+ //| Calculate point of control | //+------------------------------------------------------------------+ void CalculatePointOfControl(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) return; //--- Return if no levels int maxTpoCount = 0; //--- Initialize max TPO count int pointOfControlIndex = 0; //--- Initialize POC index for(int i = 0; i < size; i++) { //--- Loop through levels if(sessions[sessionIndex].levels[i].tpoCount > maxTpoCount) { //--- Check if higher TPO count maxTpoCount = sessions[sessionIndex].levels[i].tpoCount; //--- Update max TPO count pointOfControlIndex = i; //--- Update POC index } } sessions[sessionIndex].pointOfControlIndex = pointOfControlIndex; //--- Set POC index } //+------------------------------------------------------------------+ //| Get total TPO count | //+------------------------------------------------------------------+ int GetTotalTpoCount(int sessionIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return 0; //--- Return zero if index invalid int total = 0; //--- Initialize total int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size for(int i = 0; i < size; i++) { //--- Loop through levels total += sessions[sessionIndex].levels[i].tpoCount; //--- Accumulate TPO count } return total; //--- Return total }
Сначала мы реализуем функцию "AddTpoCharacterToLevel", которая назначает буквы Time Price Opportunity конкретным ценовым уровням. Сначала она проверяет индексы сессии и уровня относительно размеров массивов с помощью "ArraySize", чтобы избежать ошибок, и досрочно завершает работу при недопустимых значениях. Затем получает нужный символ из набора с использованием остатка от деления индекса периода на 52, добавляет его к строке уровня и увеличивает счётчик для отслеживания активности на этой цене. Чтобы упорядочить уровни от самой высокой цены к самой низкой, функция "SortPriceLevelsDescending" проверяет корректность индекса сессии, получает размер массива уровней и применяет алгоритм пузырьковой сортировки во вложенных циклах, меняя соседние элементы местами, если текущая цена ниже следующей. Для обмена используется временная структура "TpoPriceLevel".
Функция "CalculatePointOfControl" определяет ценовой уровень с наибольшим количеством Time Price Opportunities: она проверяет сессию, инициализирует максимальный счётчик и индекс нулём, затем перебирает уровни и обновляет эти значения при обнаружении большего количества, после чего сохраняет индекс в данных сессии. Мы добавляем функцию "GetTotalTpoCount", которая суммирует все значения Time Price Opportunity по уровням сессии: при недопустимом индексе она возвращает ноль, иначе инициализирует общую сумму, накапливает значения в цикле и возвращает итог для расчётов области стоимости. Теперь можно перейти к логике визуальной отрисовки этих компонентов рыночного профиля. Сначала определим логику подсветки TPO закрытия и маркеров открытия/закрытия.
//+------------------------------------------------------------------+ //| Render close TPO highlight | //+------------------------------------------------------------------+ void RenderCloseTpoHighlight(int sessionIndex, int closeLevelIndex, string &displayStrings[]) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if session invalid if(closeLevelIndex < 0 || closeLevelIndex >= ArraySize(sessions[sessionIndex].levels)) return; //--- Return if level invalid string fullString = displayStrings[closeLevelIndex]; //--- Get full display string int stringLength = StringLen(fullString); //--- Get string length if(stringLength == 0) return; //--- Return if empty string string closeCharacter = StringSubstr(fullString, stringLength - 1, 1); //--- Extract close character string remainingCharacters = StringSubstr(fullString, 0, stringLength - 1); //--- Extract remaining characters string objectName = objectPrefix + "CloseTPO_" + 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[closeLevelIndex].price, x, y); //--- Convert to XY int characterWidth = 8; //--- Set character width int offsetX = (stringLength - 1) * characterWidth; //--- Calculate offset X 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 + offsetX); //--- 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, closeColor); //--- Set color ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize); //--- Set font size ObjectSetString(0, objectName, OBJPROP_FONT, "Arial"); //--- Set font ObjectSetString(0, objectName, OBJPROP_TEXT, closeCharacter + "◄"); //--- Set text ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true); //--- Set hidden true displayStrings[closeLevelIndex] = remainingCharacters; //--- Update display string } //+------------------------------------------------------------------+ //| Render open close markers | //+------------------------------------------------------------------+ void RenderOpenCloseMarkers(int sessionIndex, int barIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid if(sessions[sessionIndex].sessionOpen == 0) return; //--- Return if no open datetime startTime = iTime(_Symbol, _Period, barIndex); //--- Get start time string openObjectName = objectPrefix + "Open_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create open object name if(ObjectFind(0, openObjectName) < 0) { //--- Check if not found ObjectCreate(0, openObjectName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionOpen, startTime, sessions[sessionIndex].sessionOpen); //--- Create trend object ObjectSetInteger(0, openObjectName, OBJPROP_RAY_RIGHT, false); //--- Set ray right false ObjectSetInteger(0, openObjectName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, openObjectName, OBJPROP_HIDDEN, true); //--- Set hidden true } ObjectSetInteger(0, openObjectName, OBJPROP_COLOR, clrDodgerBlue); //--- Set color ObjectSetInteger(0, openObjectName, OBJPROP_WIDTH, 2); //--- Set width string closeObjectName = objectPrefix + "Close_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create close object name if(ObjectFind(0, closeObjectName) < 0) { //--- Check if not found ObjectCreate(0, closeObjectName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionClose, startTime, sessions[sessionIndex].sessionClose); //--- Create trend object ObjectSetInteger(0, closeObjectName, OBJPROP_RAY_RIGHT, false); //--- Set ray right false ObjectSetInteger(0, closeObjectName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, closeObjectName, OBJPROP_HIDDEN, true); //--- Set hidden true } ObjectSetInteger(0, closeObjectName, OBJPROP_COLOR, closeColor); //--- Set color ObjectSetInteger(0, closeObjectName, OBJPROP_WIDTH, 2); //--- Set width }
Мы определяем функцию "RenderCloseTpoHighlight", которая выделяет символ Time Price Opportunity закрытия в отображении профиля. Она начинается с проверки индексов сессии и уровня с помощью ArraySize чтобы пропустить недопустимые случаи. Мы получаем строку отображения на уровне закрытия, вычисляем её длину с помощью StringLen, и, если строка не пуста, извлекаем последний символ с помощью StringSubstr для подсветки, сохраняя оставшуюся часть строки.
Имя объекта формируется объединением префикса со временем начала сессии, преобразованным через IntegerToString, а индекс бара мы получаем с помощью iBarShift; если индекс недопустим, функция завершается досрочно. Затем получаем время метки с помощью iTime, преобразуем цену и время в координаты графика с помощью ChartTimePriceToXY, и вычисляем смещение по X на основе ширины символа и длины строки. Если объект не найден через ObjectFind, мы создаём метку с помощью ObjectCreate с типом OBJ_LABEL, затем задаём её позицию, угол размещения и точку привязки, цвет, размер шрифта, шрифт, текст с добавленным маркером, а также свойства недоступности для выбора и скрытия с помощью ObjectSetInteger и "ObjectSetString", после чего обновляем строку отображения, исключая из неё выделенный символ.
Для визуализации границ сессии мы реализуем функцию "RenderOpenCloseMarkers", которая проверяет индекс сессии и пропускает обработку, если цена открытия не задана. Мы получаем время начала с помощью iTime на основе индекса бара, затем аналогично формируем имена объектов открытия и закрытия. Для маркера открытия, если он не найден через "ObjectFind", мы создаём объект трендовой линии через "ObjectCreate" с использованием OBJ_TREND на цене открытия, отключаем продление луча вправо, делаем объект недоступным для выбора и скрытым, а также задаём синий цвет и ширину 2. Аналогично для маркера закрытия мы создаём или обновляем трендовую линию на цене закрытия, устанавливая входной цвет закрытия и ту же ширину. В результате оба маркера отображаются как короткие горизонтальные линии на графике у бара начала сессии. Теперь можно объединить всю эту логику для отрисовки полного рыночного профиля.
//+------------------------------------------------------------------+ //| 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 SortPriceLevelsDescending(sessionIndex); //--- Sort levels descending CalculatePointOfControl(sessionIndex); //--- Calculate POC int totalTpoCount = GetTotalTpoCount(sessionIndex); //--- Get total TPO count int pointOfControlIndex = sessions[sessionIndex].pointOfControlIndex; //--- Get 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 closeLevelIndex = -1; //--- Initialize close level index double closePrice = sessions[sessionIndex].sessionClose; //--- Get close price for(int i = 0; i < size; i++) { //--- Loop to find close level if(MathAbs(sessions[sessionIndex].levels[i].price - closePrice) < tpoPriceGridStep / 2) { //--- Check price match closeLevelIndex = i; //--- Set close level index break; //--- Exit loop } } 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) { //--- Check POC textColor = pointOfControlColor; //--- Set POC 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 }
Здесь мы реализуем функцию "RenderSessionProfile", которая рисует полный рыночный профиль для заданной сессии на графике. Сначала выполняется проверка индекса относительно размера массива sessions с помощью ArraySize чтобы досрочно завершить работу при недопустимом индексе, а затем дополнительно проверяется наличие уровней и корректное время начала. Индекс стартового бара мы получаем с помощью iBarShift на основе символа, периода и времени начала сессии; при недопустимом значении функция завершается. Затем вызываем "SortPriceLevelsDescending" для сортировки ценовых уровней от высокого к низкому и "CalculatePointOfControl" для определения уровня с наибольшим количеством Time Price Opportunity.
Далее мы получаем общее количество Time Price Opportunity через "GetTotalTpoCount" и индекс POC, инициализируя им верхнюю и нижнюю границы области стоимости. Если POC корректна, мы рассчитываем целевое количество как процент от общего числа с использованием входного процента области стоимости, начиная с текущего количества на этом уровне. Затем расширяем область стоимости в цикле while, сравнивая и добавляя значения с соседних верхних или нижних уровней с приоритетом той стороны, где больше Time Price Opportunities, уменьшая или увеличивая индексы до достижения цели или границ.
Для подготовки к отрисовке мы объявляем массив строк отображения и изменяем его размер под количество уровней с помощью ArrayResize, копируя в него строку Time Price Opportunity каждого уровня в цикле. Затем мы ищем индекс уровня закрытия, перебирая уровни и используя MathAbs чтобы найти ближайшее совпадение с ценой закрытия сессии в пределах допуска, равного половине шага сетки, и прерываем цикл после нахождения.
После вызова "RenderCloseTpoHighlight" с индексом сессии, индексом уровня закрытия и строками отображения для обработки выделения закрытия мы проходим по каждому уровню, создавая или обновляя объекты-метки: формируем уникальное имя из префикса, времени начала и индекса уровня с помощью IntegerToString; задаём цвет текста по умолчанию и переопределяем его для одиночных принтов, диапазона области стоимости или POC на основе входных цветов; проверяем наличие объекта через ObjectFind и при необходимости создаём его через "ObjectCreate" с типом OBJ_LABEL с начальными нулевыми отступами; получаем время метки через "iTime" и преобразуем время-цену в координаты с помощью "ChartTimePriceToXY"; затем применяем положение, угол размещения и точку привязки, цвет, размер шрифта, шрифт, текст из строки отображения и свойства недоступности для выбора/скрытия с помощью "ObjectSetInteger" и ObjectSetString функций.
Наконец, мы вызываем "RenderOpenCloseMarkers" с индексом сессии и индексом бара, чтобы добавить на график визуальные индикаторы открытия и закрытия. Теперь эту функцию можно вызывать в обработчике расчётов на тиках, условно выполняя основную часть отрисовки.
//+------------------------------------------------------------------+ //| 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].periodOpens, periodIndex + 1); //--- Resize period opens sessions[activeSessionIndex].periodOpens[periodIndex] = open[rates_total - 1]; //--- Set period open sessions[activeSessionIndex].periodCount++; //--- Increment period count 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 } } 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, который обрабатывает массивы ценовых данных пользовательского индикатора при каждом обновлении. Сначала он возвращает ноль, если доступно меньше двух баров, чтобы обеспечить достаточный объём данных. Мы извлекаем время текущего бара из массива time по последнему индексу и определяем, является ли это новым баром, сравнивая его с последним завершённым временем. Если через "IsNewSessionStarted" обнаружена новая сессия или предыдущего времени нет, мы завершаем активную сессию, задавая её время окончания и отрисовывая её через "RenderSessionProfile", если она допустима по границам ArraySize затем создаём новую сессию с помощью "CreateNewSession", инициализируем её время начала, открытие, максимум и минимум по данным текущего бара и обновляем время последнего завершённого бара.
После обновления времени предыдущего бара, если это новый бар, подходящий для обработки через "IsBarEligibleForProcessing", и активная сессия существует, мы обновляем максимум и минимум сессии с помощью MathMax и MathMin используя значения текущего бара, задаём закрытие, увеличиваем счётчик периодов после изменения размера массива и сохранения открытия периода, квантуем максимум и минимум бара через "QuantizePriceToGrid" и проходим от минимума к максимуму с шагом сетки, получая или создавая уровни через "GetOrCreatePriceLevel" перед добавлением символов Time Price Opportunity с помощью "AddTpoCharacterToLevel". В конце обновляем время последнего завершённого бара. Кроме того, если текущий бар подходит для обработки и сессия активна, мы непрерывно обновляем закрытие, максимум и минимум, чтобы отражать изменения в реальном времени.
Наконец, мы перебираем все сессии, отрисовываем каждый профиль и возвращаем общее количество баров, показывая, что обработка выполнена полностью. Остаётся обработать ручное изменение размера графика, чтобы уровни перерисовывались и не выглядели «подвешенными», а также удалять наши объекты при деинициализации индикатора. Для этого используется следующий подход.
//+------------------------------------------------------------------+ //| Handle chart event | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_CHART_CHANGE) { //--- Check chart change event for(int i = 0; i < ArraySize(sessions); i++) { //--- Loop through sessions RenderSessionProfile(i); //--- Render profile } } } //+------------------------------------------------------------------+ //| Deinitialize custom indicator | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { DeleteAllIndicatorObjects(); //--- Delete all indicator objects } //+------------------------------------------------------------------+ //| Delete all indicator objects | //+------------------------------------------------------------------+ void DeleteAllIndicatorObjects() { 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) //--- Check if name starts with prefix ObjectDelete(0, name); //--- Delete object } }
Взаимодействие с графиком обрабатывается в обработчике события OnChartEvent который срабатывает при различных событиях графика. Он проверяет, совпадает ли идентификатор события с CHARTEVENT_CHART_CHANGE чтобы обнаружить изменения, например переключение таймфрейма или изменение размера; если да, мы проходим по всем сессиям, используя ArraySize для получения их количества, и повторно отрисовываем каждый профиль через "RenderSessionProfile", чтобы обновить визуальное отображение.
При удалении индикатора выполняется обработчик события OnDeinit который вызывает "DeleteAllIndicatorObjects" для очистки всех пользовательских элементов графика и предотвращения появления оставшихся объектов.
В "DeleteAllIndicatorObjects" мы получаем общее количество объектов графика через функцию ObjectsTotal по всем окнам и типам, затем перебираем их в обратном порядке от последнего индекса до нуля, получая имя каждого объекта с помощью ObjectName, и если оно начинается с префикса согласно StringFind возвращающей ноль, удаляем объект с помощью ObjectDelete чтобы обеспечить полную очистку. При запуске индикатора получаем следующий результат.

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

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