English
preview
Тепловая карта внутридневной доходности в MQL5: Визуализация с помощью CCanvas

Тепловая карта внутридневной доходности в MQL5: Визуализация с помощью CCanvas

MetaTrader 5 — Индикаторы |
59 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Введение

Опытные трейдеры часто замечают, что некоторые инструменты ведут себя по-разному в зависимости от времени суток и дня недели. Пара EURUSD, как правило, демонстрирует более высокую волатильность в период перекрытия лондонской и нью-йоркской сессий. Пара XAUUSD часто демонстрирует направленное движение в часы азиатской сессии. Эти тенденции знакомы практикующим трейдерам, но их редко систематически оценивают количественно, поскольку в MetaTrader 5 нет встроенного инструмента для агрегирования и визуализации распределений доходности одновременно по нескольким временным измерениям.

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


Определение доходности бара

Доходность отдельного бара определяется как процентное изменение цены закрытия текущего бара относительно цены закрытия предыдущего бара:

R_i = (Close_i - Close_{i-1}) / Close_{i-1} × 100

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

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


Двумерное агрегирование

Матрица агрегирования имеет размер 5 × 24, где строки соответствуют дням недели (с понедельника по пятницу), а столбцы — часам суток (от 0 до 23).

Ось Размер Значения
Строки (Y) 5 Понедельник = 0, вторник = 1, среда = 2, четверг = 3, пятница = 4
Столбцы (X) 24 От 0-го до 23-го часа

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

Во всем тексте используются следующие обозначения переменных:

Обозначение Значение
R_i Доходность бара i
H Индекс часа (0–23)
D Индекс дня (0–4, понедельник–пятница)
Sum(H,D) Сумма доходностей для ячейки (H,D)
Count(H,D) Число наблюдений в ячейке (H,D)
Avg(H,D) Средняя доходность = Sum(H,D) / Count(H,D)


Сопоставление значений цветам

Цветовая шкала сопоставляет значения средней доходности цветам RGB с помощью цветовой интерполяции от красного к зеленому с центром в нуле.

Цвет Значение
Тёмно-зелёный Высокая средняя положительная доходность
Светло-зеленый Незначительная положительная средняя доходность
Серый Средняя доходность, близкая к нулю
Светло-красный Незначительная отрицательная средняя доходность
Тёмно-красный Большая по модулю отрицательная средняя доходность

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

Тепловая карта EURUSD на графике H1

Тепловая карта пары EURUSD на графике H1, отражающая распределение средней доходности по 5 000 историческим барам. Зеленые ячейки обозначают часы и дни недели с положительной исторической средней доходностью, красные — с отрицательной. Интенсивность цвета отражает модуль доходности относительно самого сильного сигнала в наборе данных.

Средние исторические показатели доходности носят описательный, а не прогностический характер. Рынки нестационарны: тенденции, сформировавшие закономерность за анализируемый период, могут не сохраниться. Структурные события — изменения в политике центрального банка, сдвиги в микроструктуре рынка — могут без предупреждения лишить исторические закономерности актуальности.

Данный индикатор не проводит проверку статистической значимости. Среднюю доходность ячейки в размере +0,02%, рассчитанную по 8 наблюдениям, нельзя считать существенно отличающейся от нуля. Значения в ячейках с малым числом наблюдений следует интерпретировать с осторожностью. Визуализация представляет собой инструмент для предварительного анализа, а не генератор сигналов.


Вычислительная сложность

Этап Сложность Примечания
Обход исторических баров O(N) N = число баров анализируемой истории
Суммирование доходностей O(1) на бар Прямое индексирование матрицы
Вычисление средних значений O(120) Фиксированный размер матрицы
Цветовая интерполяция O(120)
Фиксированный размер матрицы
Отрисовка на холсте O(120)
Фиксированный размер матрицы

Основные вычислительные затраты приходятся на обход исторических баров, который выполняется в OnCalculate() при первоначальной загрузке индикатора, а затем при закрытии каждого нового бара. Вся отрисовка выполняется с фиксированной сложностью O(120) независимо от количества проанализированных баров.


Ожидаемый вывод в журнал

[ReturnHeatmap] Initialized — Lookback: 5000 bars
[ReturnHeatmap] Processed Bars    = 5000
[ReturnHeatmap] Max Avg Return    = 0.0700%
[ReturnHeatmap] Min Avg Return    = -0.0568%
[ReturnHeatmap] Max Abs Return    = 0.0700%
[ReturnHeatmap] Cells Rendered    = 120
[ReturnHeatmap] Canvas Updated Successfully

Строка "Cells Rendered = 120" подтверждает, что для каждой комбинации дня недели и часа имеется как минимум одно наблюдение. На EURUSD H1 анализируемый период в 5 000 баров охватывает примерно семь месяцев. В этих условиях каждая из 120 ячеек обычно содержит достаточно большое число наблюдений. На меньших таймфреймах, таких как M1, тот же период выборки охватывает лишь несколько дней, поэтому многие ячейки остаются пустыми, а число отрисованных ячеек соответственно уменьшается. Максимальная по модулю средняя доходность используется в качестве знаменателя при нормализации цветовой шкалы. Ячейка с максимальной по модулю доходностью отрисовывается с максимальной насыщенностью цвета, а насыщенность цвета остальных ячеек изменяется пропорционально.


Листинги кода и разбор архитектуры

ReturnMatrix.mqh

CReturnMatrix — это основная структура данных индикатора. Он накапливает доходности баров, выраженные в процентах, в таблице размером 5×24 с индексами по дням недели и часам суток и вычисляет средние арифметические значения для каждой ячейки после завершения полного обхода исторических баров. Он не работает с диском, отображением или цветами — только с числами.

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

Файл начинается с защиты от повторного включения и констант, задающих размеры:

//+------------------------------------------------------------------+
//|                                              ReturnMatrix.mqh    |
//|               Intraday Return Heatmap — Matrix Aggregation Layer |
//+------------------------------------------------------------------+
#ifndef RETURN_MATRIX_MQH
#define RETURN_MATRIX_MQH

//--- Matrix dimensions
#define HEATMAP_DAYS  5   // Monday through Friday
#define HEATMAP_HOURS 24  // Hours 0 through 23

HEATMAP_DAYS и HEATMAP_HOURS определены как константы препроцессора, а не как члены класса, чтобы все остальные модули — HeatmapStatistics.mqh и HeatmapRenderer.mqh — могли использовать их для задания границ своих циклов, не завися от экземпляра CReturnMatrix.

Ниже приводится объявление класса:

//+------------------------------------------------------------------+
//| CReturnMatrix                                                    |
//| Accumulates percentage bar returns into a 5x24 matrix indexed    |
//| by day-of-week (0=Monday..4=Friday) and hour-of-day (0..23).     |
//| Provides arithmetic average computation per cell after the       |
//| full historical scan is complete.                                |
//+------------------------------------------------------------------+
class CReturnMatrix
  {
private:
   double            m_sum[HEATMAP_DAYS][HEATMAP_HOURS];    // Accumulated returns
   int               m_count[HEATMAP_DAYS][HEATMAP_HOURS];  // Sample counts
   double            m_avg[HEATMAP_DAYS][HEATMAP_HOURS];    // Computed averages
   int               m_total_samples;                       // Valid bars processed

public:
                     CReturnMatrix(void);
                    ~CReturnMatrix(void);

   void              Reset(void);
   bool              Accumulate(int day_index, int hour_index, double ret);
   void              ComputeAverages(void);

   double            GetAverage(int day_index, int hour_index) const;
   int               GetCount(int day_index, int hour_index)   const;
   int               GetTotalSamples(void)                     const;
  };

Три параллельных массива размером 5×24 выполняют разные функции. m_sum накапливает сумму доходностей, выраженных в процентах, для каждой ячейки. m_count хранит число баров, учтенных в каждой ячейке. m_avg хранит вычисленные средние арифметические значения и заполняется только после вызова ComputeAverages(). m_total_samples отслеживает общее число обработанных баров, прошедших проверку; это число используется при выводе диагностических сообщений в журнал.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CReturnMatrix::CReturnMatrix(void)
  {
   m_total_samples = 0;
   Reset();
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CReturnMatrix::~CReturnMatrix(void)
  {
  }

Конструктор инициализирует переменную `m_total_samples` и сразу же вызывает метод `Reset()`, чтобы обнулить все элементы массивов. Деструктор пустой, поскольку все три массива являются членами фиксированного размера, выделенными в составе самого объекта — память кучи не задействована, и явное освобождение памяти не требуется. Общий объём памяти, занимаемый экземпляром CReturnMatrix, складывается из двух массивов double[5][24] с элементами по 8 байт (1 920 байт) и одного массива int[5][24] с элементами по 4 байта (480 байт), что в сумме составляет примерно 2,4 КБ.

//+------------------------------------------------------------------+
//| Clear all accumulators and counters                              |
//+------------------------------------------------------------------+
void CReturnMatrix::Reset(void)
  {
   m_total_samples = 0;

   for(int d = 0; d < HEATMAP_DAYS; d++)
     {
      for(int h = 0; h < HEATMAP_HOURS; h++)
        {
         m_sum[d][h]   = 0.0;
         m_count[d][h] = 0;
         m_avg[d][h]   = 0.0;
        }
     }
  }

Метод Reset() обнуляет все три массива и общий счетчик наблюдений за один проход со сложностью O(120). Он вызывается из конструктора и из функции BuildMatrix() в основном файле индикатора в начале каждого цикла пересчёта. Это гарантирует, что при повторном вызове после закрытия нового бара данные не добавляются к результатам предыдущего обхода.

//+------------------------------------------------------------------+
//| Accumulate one bar return into the correct cell                  |
//+------------------------------------------------------------------+
bool CReturnMatrix::Accumulate(int day_index, int hour_index, double ret)
  {
//--- validate cell indices before writing
   if(day_index < 0 || day_index >= HEATMAP_DAYS)
      return(false);
   if(hour_index < 0 || hour_index >= HEATMAP_HOURS)
      return(false);

//--- add return to accumulator and increment sample count
   m_sum[day_index][hour_index]   += ret;
   m_count[day_index][hour_index] += 1;
   m_total_samples++;

   return(true);
  }

Метод Accumulate() вызывается один раз для каждого исторического бара, прошедшего проверку. Перед записью метод проверяет оба индекса, чтобы предотвратить выход за границы массива, если разложение временной метки на компоненты даст неожиданные значения. Эта проверка границ индексов служит второй линией защиты, дополняющей отбрасывание баров выходных дней в CTimeBucketAnalyzer. При обработке бара, прошедшего проверку, его доходность добавляется к сумме в соответствующей ячейке, а счетчик наблюдений этой ячейки и общий счетчик наблюдений увеличиваются на единицу. Сложность этого метода составляет O(1) — он выполняет постоянное число операций независимо от размера матрицы.

//+------------------------------------------------------------------+
//| Compute average returns for all cells after the full scan        |
//+------------------------------------------------------------------+
void CReturnMatrix::ComputeAverages(void)
  {
   for(int d = 0; d < HEATMAP_DAYS; d++)
     {
      for(int h = 0; h < HEATMAP_HOURS; h++)
        {
         if(m_count[d][h] > 0)
            m_avg[d][h] = m_sum[d][h] / (double)m_count[d][h];
         else
            m_avg[d][h] = 0.0; // Neutral for cells with no samples
        }
     }
  }

Метод ComputeAverages() выполняет итоговое деление для всех 120 ячеек за один проход со сложностью O(120). Эта стратегия отложенных вычислений позволяет избежать тысяч избыточных операций деления во время обхода исторических баров — средние значения не имеют смысла, пока не обработаны все бары. Ячейкам с нулевым числом наблюдений присваивается среднее значение 0.0, которое при сопоставлении значений цветам отрисовывается нейтральным серым цветом.

//+------------------------------------------------------------------+
//| Return the computed average for a given cell                     |
//+------------------------------------------------------------------+
double CReturnMatrix::GetAverage(int day_index, int hour_index) const
  {
   if(day_index < 0 || day_index >= HEATMAP_DAYS)
      return(0.0);
   if(hour_index < 0 || hour_index >= HEATMAP_HOURS)
      return(0.0);
   return(m_avg[day_index][hour_index]);
  }

//+------------------------------------------------------------------+
//| Return the sample count for a given cell                         |
//+------------------------------------------------------------------+
int CReturnMatrix::GetCount(int day_index, int hour_index) const
  {
   if(day_index < 0 || day_index >= HEATMAP_DAYS)
      return(0);
   if(hour_index < 0 || hour_index >= HEATMAP_HOURS)
      return(0);
   return(m_count[day_index][hour_index]);
  }

//+------------------------------------------------------------------+
//| Return the total number of valid bars accumulated                |
//+------------------------------------------------------------------+
int CReturnMatrix::GetTotalSamples(void) const
  {
   return(m_total_samples);
  }

#endif // RETURN_MATRIX_MQH

Оба метода доступа повторяют проверку границ индексов перед возвратом результата и возвращают безопасное нейтральное значение при любом индексе вне допустимого диапазона. Метод GetCount() используется в CHeatmapStatistics::Compute() для пропуска пустых ячеек при вычислении сводных статистических показателей, а в CHeatmapRenderer::Render() — неявно, через средние значения. Метод GetTotalSamples() возвращает общее число баров для записи в диагностический журнал.

TimeBucketAnalyzer.mqh

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

//+------------------------------------------------------------------+
//|                                         TimeBucketAnalyzer.mqh   |
//|               Intraday Return Heatmap — Time Decomposition Layer |
//+------------------------------------------------------------------+
#ifndef TIME_BUCKET_ANALYZER_MQH
#define TIME_BUCKET_ANALYZER_MQH

//+------------------------------------------------------------------+
//| CTimeBucketAnalyzer                                              |
//| Extracts the hour-of-day (0-23) and day-of-week index (0-4,      |
//| Monday=0 through Friday=4) from a bar timestamp.                 |
//| Weekend timestamps (Saturday and Sunday) are rejected.           |
//+------------------------------------------------------------------+
class CTimeBucketAnalyzer
  {
public:
                     CTimeBucketAnalyzer(void);
                    ~CTimeBucketAnalyzer(void);

   static bool       Decompose(datetime bar_time,
                               int &out_day_index,
                               int &out_hour_index);

   static bool       IsWeekend(datetime bar_time);
   static string     DayName(int day_index);
  };

Decompose() — основная точка входа, вызываемая один раз для каждого бара при обходе исторических баров. IsWeekend() — это предварительный фильтр, вызываемый методом Decompose() до начала любой другой обработки. Метод DayName() возвращает трехсимвольные сокращения, которые модуль отрисовки использует для подписей по оси Y.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTimeBucketAnalyzer::CTimeBucketAnalyzer(void)
  {
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CTimeBucketAnalyzer::~CTimeBucketAnalyzer(void)
  {
  }

Конструктор и деструктор пусты: класс не содержит полей данных и не выделяет ресурсов. Они определены явно в соответствии с принятым в MetaQuotes соглашением, согласно которому каждый класс должен объявлять конструктор и деструктор.

//+------------------------------------------------------------------+
//| Return true if the timestamp falls on a weekend                  |
//+------------------------------------------------------------------+
bool CTimeBucketAnalyzer::IsWeekend(datetime bar_time)
  {
   MqlDateTime dt;
   TimeToStruct(bar_time, dt);
   return(dt.day_of_week == 0 || dt.day_of_week == 6); // Sunday=0, Saturday=6
  }

Функция IsWeekend() использует функцию TimeToStruct() для преобразования временной метки в структуру MqlDateTime и проверяет поле day_of_week на соответствие значениям "воскресенье" (0) и "суббота" (6). Соответствующие функции работы со временем из MQL4 недоступны в MQL5. Вместо них рекомендуется использовать функцию TimeToStruct(). Некоторые брокеры включают в историю по определенным инструментам свечи за выходные дни. Их исключение на этом этапе предотвращает попадание таких баров в ячейки понедельника или пятницы из-за неверного сопоставления дней недели.

//+------------------------------------------------------------------+
//| Decompose a timestamp into day and hour bucket indices           |
//+------------------------------------------------------------------+
bool CTimeBucketAnalyzer::Decompose(datetime bar_time,
                                    int &out_day_index,
                                    int &out_hour_index)
  {
   out_day_index  = -1;
   out_hour_index = -1;

//--- reject weekend bars before further processing
   if(IsWeekend(bar_time))
      return(false);

//--- decompose timestamp into calendar components
   MqlDateTime dt;
   TimeToStruct(bar_time, dt);

//--- extract hour of day directly from struct
   out_hour_index = dt.hour;

//--- day_of_week: Sunday=0, Monday=1 ... Saturday=6
   switch(dt.day_of_week)
     {
      case 1:
         out_day_index = 0;
         break; // Monday
      case 2:
         out_day_index = 1;
         break; // Tuesday
      case 3:
         out_day_index = 2;
         break; // Wednesday
      case 4:
         out_day_index = 3;
         break; // Thursday
      case 5:
         out_day_index = 4;
         break; // Friday
      default:
         return(false);
     }

   return(true);
  }

Метод Decompose() перед началом обработки инициализирует оба выходных параметра значением -1. Если метод возвращает false (из-за бара выходного дня или неожиданного значения day_of_week), вызывающий код получает значения индексов, которые не пройдут проверку границ индексов в CReturnMatrix::Accumulate(), что обеспечивает второй уровень защиты от повреждения данных.

Оператор switch преобразует стандартную нумерацию дней недели (воскресенье = 0, понедельник = 1, суббота = 6) в индексы, начинающиеся с нуля, где понедельнику соответствует индекс 0. Такое преобразование обеспечивает соответствие строки понедельника индексу 0, согласуясь с порядком подписей оси Y сверху вниз в модуле отрисовки.

//+------------------------------------------------------------------+
//| Return the display name for a day index (0=Monday..4=Friday)     |
//+------------------------------------------------------------------+
string CTimeBucketAnalyzer::DayName(int day_index)
  {
   switch(day_index)
     {
      case 0:
         return("Monday");
      case 1:
         return("Tuesday");
      case 2:
         return("Wednesday");
      case 3:
         return("Thursday");
      case 4:
         return("Friday");
      default:
         return("Unknown");
     }
  }

#endif // TIME_BUCKET_ANALYZER_MQH

Метод DayName() возвращает удобочитаемые подписи для оси Y модуля отрисовки. Модуль отрисовки применяет StringSubstr(name, 0, 3) к полученному результату, формируя трёхсимвольные сокращения Mon, Tue, Wed, Thu, Fri, которые помещаются в отведённое для подписей поле шириной 64 пикселя.

HeatmapColorMap.mqh

CHeatmapColorMap выполняет нормализацию и цветовую интерполяцию в пространстве RGB, определяющие внешний вид каждой ячейки на экране. Все методы являются статическими — преобразование цвета является чистой функцией входных параметров. Файл подключает <Canvas\Canvas.mqh>, чтобы получить доступ к определённому в нём макросу ARGB().

//+------------------------------------------------------------------+
//|                                              HeatmapColorMap.mqh |
//|              Intraday Return Heatmap — Color Interpolation Layer |
//+------------------------------------------------------------------+
#ifndef HEATMAP_COLOR_MAP_MQH
#define HEATMAP_COLOR_MAP_MQH

#include <Canvas\Canvas.mqh>

//+------------------------------------------------------------------+
//| CHeatmapColorMap                                                 |
//| Converts an average return value into an ARGB color suitable     |
//| for CCanvas rendering. Normalization is relative to the maximum  |
//| absolute return observed in the full matrix, ensuring the color  |
//| scale uses its full dynamic range for any instrument.             |
//+------------------------------------------------------------------+
class CHeatmapColorMap
  {
public:
                     CHeatmapColorMap(void);
                    ~CHeatmapColorMap(void);

   static uint       ReturnToColor(double avg_return, double max_abs_return);
   static uint       NeutralColor(void);
  };

ReturnToColor() — основной метод преобразования, вызываемый для каждой из 120 ячеек при отрисовке. Функция NeutralColor() возвращает фиксированный светло-серый цвет, используемый для ячеек без данных или при нулевом диапазоне доходности.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CHeatmapColorMap::CHeatmapColorMap(void)
  {
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CHeatmapColorMap::~CHeatmapColorMap(void)
  {
  }

Конструктор и деструктор пусты: класс не содержит состояния, и все методы статические.

//+------------------------------------------------------------------+
//| Return the neutral color used for zero-return or empty cells     |
//+------------------------------------------------------------------+
uint CHeatmapColorMap::NeutralColor(void)
  {
   return(ARGB(255, 200, 200, 200)); // Light gray
  }

Функция NeutralColor() возвращает полностью непрозрачный светло-серый цвет в формате ARGB. Она вызывается, когда значение max_abs_return практически равно нулю — то есть средняя доходность во всех ячейках матрицы равна нулю, что возможно на синтетических ценовых рядах или если в периоде выборки нет пригодных для расчёта баров. Этим же цветом отображается любая ячейка, средняя доходность которой после нормализации равна нулю.

//+------------------------------------------------------------------+
//| Map an average return to an ARGB color value                      |
//+------------------------------------------------------------------+
uint CHeatmapColorMap::ReturnToColor(double avg_return, double max_abs_return)
  {
//--- if no meaningful range exists, return neutral color
   if(max_abs_return < 1e-10)
      return(NeutralColor());

//--- normalize return to [-1, +1] range relative to observed maximum
   double norm = avg_return / max_abs_return;

//--- clamp to valid interpolation range
   if(norm > 1.0)
      norm = 1.0;
   if(norm < -1.0)
      norm = -1.0;

   int r = 0;
   int g = 0;
   int b = 0;

   if(norm >= 0.0)
     {
      //--- positive: interpolate from gray toward pure green as norm → 1
      r = (int)(50.0 + (1.0 - norm) * 150.0);
      g = 200;
      b = (int)(50.0 + (1.0 - norm) * 150.0);
     }
   else
     {
      //--- negative: interpolate from gray toward pure red as norm → -1
      r = 200;
      g = (int)(50.0 + (1.0 + norm) * 150.0);
      b = (int)(50.0 + (1.0 + norm) * 150.0);
     }

//--- clamp RGB components to valid byte range
   if(r < 0)
      r = 0;
   if(r > 255)
      r = 255;
   if(g < 0)
      g = 0;
   if(g > 255)
      g = 255;
   if(b < 0)
      b = 0;
   if(b > 255)
      b = 255;

//--- return as ARGB with full opacity
   return(ARGB(255, r, g, b));
  }

Метод ReturnToColor() выполняет три последовательные операции. Сначала метод нормализует avg_return до диапазона [-1, +1], деля его на max_abs_return. Такая относительная нормализация гарантирует, что всегда используется полный цветовой диапазон: матрица, в которой все показатели доходности находятся в диапазоне от -0,01% до +0,01%, будет использовать тот же цветовой диапазон, что и матрица, в которой показатели доходности варьируются от -0,5% до +0,5%. Ограничение диапазоном защищает от последствий погрешностей вычислений с плавающей запятой, из-за которых значение norm может незначительно выйти за допустимые пределы.

Затем метод вычисляет компоненты RGB, используя отдельные формулы для положительной и отрицательной доходности. При положительной доходности цвет меняется от серого к зелёному, а при отрицательной — от серого к красному. Затем значения компонентов ограничиваются диапазоном [0, 255]. Для положительных значений доходности зелёный компонент остаётся равным 200, а красный и синий компоненты постепенно уменьшаются от 200 до 50 по мере приближения norm к 1. Таким образом, цвет плавно меняется от светло-серого при нулевой доходности до насыщенного зелёного при максимальной. Для отрицательных значений доходности красный компонент остаётся равным 200, а зелёный и синий компоненты уменьшаются по той же схеме. Цвет также начинается со светло-серого при нулевой доходности, но по мере её снижения переходит в насыщенный красный. В результате получается визуально сбалансированная двунаправленная шкала.

В-третьих, компоненты RGB ограничиваются диапазоном [0, 255] и объединяются с помощью макроса ARGB(255, r, g, b), который упаковывает четыре байта в 32-битное целое число без знака с альфа-каналом, равным 255 (полная непрозрачность). CCanvas использует формат ARGB — неверный порядок байтов поменяет местами цветовые обозначения положительной и отрицательной доходности.

HeatmapStatistics.mqh

Класс CHeatmapStatistics выполняет один проход со сложностью O(120) по готовой матрице для получения четырёх сводных показателей: минимальной средней доходности, максимальной средней доходности, максимальной по модулю средней доходности и числа непустых ячеек. Этот класс отделён от CReturnMatrix, поскольку вычисление сводной статистики — отдельная задача: он использует матрицу, а не управляет ею.

//+------------------------------------------------------------------+
//|                                            HeatmapStatistics.mqh |
//|              Intraday Return Heatmap — Statistical Summary Layer |
//+------------------------------------------------------------------+
#ifndef HEATMAP_STATISTICS_MQH
#define HEATMAP_STATISTICS_MQH

#include "ReturnMatrix.mqh"

//+------------------------------------------------------------------+
//| CHeatmapStatistics                                               |
//| Scans a completed CReturnMatrix to extract the summary values    |
//| required for color normalization and diagnostic logging:         |
//| minimum average return, maximum average return, and maximum      |
//| absolute average return used as the normalization denominator.   |
//+------------------------------------------------------------------+
class CHeatmapStatistics
  {
private:
   double            m_min_avg;    // Minimum average return in matrix
   double            m_max_avg;    // Maximum average return in matrix
   double            m_max_abs;    // Maximum absolute average return
   int               m_cell_count; // Number of cells with samples

public:
                     CHeatmapStatistics(void);
                    ~CHeatmapStatistics(void);

   void              Compute(CReturnMatrix &ret_matrix);

   double            MinAvg(void)    const;
   double            MaxAvg(void)    const;
   double            MaxAbs(void)    const;
   int               CellCount(void) const;

   void              PrintDiagnostics(int total_samples) const;
  };

m_max_abs — это значение, используемое CHeatmapRenderer в качестве знаменателя при нормализации цветовой шкалы. m_cell_count указывает число ячеек хотя бы с одним наблюдением; в диагностическом журнале этот показатель отображается как Cells Rendered. Параметр метода Compute() назван ret_matrix, а не matrix, чтобы избежать конфликта с зарезервированным ключевым словом MQL5.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CHeatmapStatistics::CHeatmapStatistics(void)
  {
   m_min_avg    =  1e38;
   m_max_avg    = -1e38;
   m_max_abs    =  0.0;
   m_cell_count =  0;
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CHeatmapStatistics::~CHeatmapStatistics(void)
  {
  }

Конструктор инициализирует переменную `m_min_avg`, присваивая ей большое положительное значение, а переменную `m_max_avg` — большое отрицательное значение. Это гарантирует, что первая найденная допустимая ячейка всегда будет правильно обновлять оба крайних значения — независимо от того, является ли её среднее значение положительным или отрицательным. Деструктор пустой.

//+------------------------------------------------------------------+
//| Scan the matrix and compute summary statistics                   |
//+------------------------------------------------------------------+
void CHeatmapStatistics::Compute(CReturnMatrix &ret_matrix)
  {
//--- reset before scan to avoid accumulation from prior calls
   m_min_avg    =  1e38;
   m_max_avg    = -1e38;
   m_max_abs    =  0.0;
   m_cell_count =  0;

   for(int d = 0; d < HEATMAP_DAYS; d++)
     {
      for(int h = 0; h < HEATMAP_HOURS; h++)
        {
         if(ret_matrix.GetCount(d, h) == 0)
            continue; // Skip empty cells to avoid distorting extremes

         double avg = ret_matrix.GetAverage(d, h);
         m_cell_count++;

         if(avg < m_min_avg)
            m_min_avg = avg;
         if(avg > m_max_avg)
            m_max_avg = avg;

         double abs_avg = MathAbs(avg);
         if(abs_avg > m_max_abs)
            m_max_abs = abs_avg;
        }
     }

//--- guard against a matrix where no cells had any samples
   if(m_cell_count == 0)
     {
      m_min_avg = 0.0;
      m_max_avg = 0.0;
      m_max_abs = 0.0;
     }
  }

Метод Compute() сбрасывает все четыре статистических показателя в начале каждого вызова, чтобы предотвратить их накопление при повторных циклах пересчёта. Внутренний цикл пропускает все ячейки, для которых метод GetCount() возвращает ноль. Это крайне важно, поскольку среднее значение в пустых ячейках по умолчанию равно 0.0, и их включение исказило бы минимум, сместив его к нулю, даже если средние значения всех непустых ячеек положительны.

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

//+------------------------------------------------------------------+
//| Return the minimum average return observed in the matrix          |
//+------------------------------------------------------------------+
double CHeatmapStatistics::MinAvg(void) const
  {
   return(m_min_avg);
  }

//+------------------------------------------------------------------+
//| Return the maximum average return observed in the matrix          |
//+------------------------------------------------------------------+
double CHeatmapStatistics::MaxAvg(void) const
  {
   return(m_max_avg);
  }

//+------------------------------------------------------------------+
//| Return the maximum absolute average return for normalization      |
//+------------------------------------------------------------------+
double CHeatmapStatistics::MaxAbs(void) const
  {
   return(m_max_abs);
  }

//+------------------------------------------------------------------+
//| Return the number of non-empty cells in the matrix                |
//+------------------------------------------------------------------+
int CHeatmapStatistics::CellCount(void) const
  {
   return(m_cell_count);
  }

//+------------------------------------------------------------------+
//| Print diagnostic statistics to the Experts tab                   |
//+------------------------------------------------------------------+
void CHeatmapStatistics::PrintDiagnostics(int total_samples) const
  {
   PrintFormat("[ReturnHeatmap] Processed Bars    = %d", total_samples);
   PrintFormat("[ReturnHeatmap] Max Avg Return    = %s%%",
               DoubleToString(m_max_avg, 4));
   PrintFormat("[ReturnHeatmap] Min Avg Return    = %s%%",
               DoubleToString(m_min_avg, 4));
   PrintFormat("[ReturnHeatmap] Max Abs Return    = %s%%",
               DoubleToString(m_max_abs, 4));
   PrintFormat("[ReturnHeatmap] Cells Rendered    = %d", m_cell_count);
  }

#endif // HEATMAP_STATISTICS_MQH

Эти четыре метода доступа непосредственно возвращают значения соответствующих закрытых полей. Метод MaxAbs() используется чаще всего. CHeatmapRenderer::Render() вызывает его перед циклом отрисовки, чтобы задать знаменатель нормализации цветовой шкалы.

Метод PrintDiagnostics() при каждом пересчёте выводит пятистрочную сводку на вкладке "Эксперты". Последовательность %% в строке форматирования выводится как обычный знак процента. Функция DoubleToString() с четырьмя знаками после запятой обеспечивает достаточную точность для малых величин доходности, характерных для валютных и криптовалютных инструментов.

HeatmapRenderer.mqh

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

//+------------------------------------------------------------------+
//|                                              HeatmapRenderer.mqh |
//|                 Intraday Return Heatmap — Canvas Rendering Layer |
//+------------------------------------------------------------------+
#ifndef HEATMAP_RENDERER_MQH
#define HEATMAP_RENDERER_MQH

//--- Include
#include <Canvas\Canvas.mqh>
#include "ReturnMatrix.mqh"
#include "TimeBucketAnalyzer.mqh"
#include "HeatmapColorMap.mqh"
#include "HeatmapStatistics.mqh"

//+------------------------------------------------------------------+
//| Layout constants                                                 |
//+------------------------------------------------------------------+
#define RENDERER_LABEL_WIDTH  64  // Pixels reserved for Y-axis day labels
#define RENDERER_LABEL_HEIGHT 20  // Pixels reserved for X-axis hour labels
#define RENDERER_PADDING       4  // General padding in pixels

//+------------------------------------------------------------------+
//| CHeatmapRenderer                                                 |
//| Manages a CCanvas object attached to a chart label object inside |
//| the indicator subwindow. Draws the background, weekday labels,   |
//| hour labels, and all 120 color cells, then commits the frame.    |
//+------------------------------------------------------------------+
class CHeatmapRenderer
  {
private:
   CCanvas           m_canvas;         // CCanvas bitmap object
   string            m_obj_name;       // Chart object name for bitmap
   long              m_chart_id;       // Target chart identifier
   int               m_subwindow;      // Indicator subwindow index
   int               m_canvas_width;   // Canvas width in pixels
   int               m_canvas_height;  // Canvas height in pixels
   bool              m_initialized;    // True after successful Create()

   int               CellWidth(void)  const;
   int               CellHeight(void) const;
   int               CellX(int hour_index) const;
   int               CellY(int day_index)  const;

   void              DrawBackground(void);
   void              DrawHourLabels(void);
   void              DrawDayLabels(void);
   void              DrawCell(int day_index, int hour_index,
                              double avg_return, double max_abs);

public:
                     CHeatmapRenderer(void);
                    ~CHeatmapRenderer(void);

   bool              Create(long chart_id, int subwindow,
                            int width, int height);
   void              Destroy(void);

   bool              Render(CReturnMatrix &ret_matrix,
                            CHeatmapStatistics *stats);
  };

В макете слева отведено RENDERER_LABEL_WIDTH = 64 пикселя для подписей с названиями дней, а вверху — RENDERER_LABEL_HEIGHT = 20 пикселей для подписей часов. Оставшаяся область холста разделена на сетку размером 5×24, размеры ячеек которой автоматически подстраиваются под размер холста.

В методе Render() используются разные способы передачи параметров. Параметр ret_matrix передается по ссылке, поскольку содержит массивы фиксированного размера. Параметр stats передается как указатель, поскольку на класс CHeatmapStatistics это ограничение не распространяется. Именно этот комбинированный подход позволяет получить код, который корректно компилируется в среде MQL5.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CHeatmapRenderer::CHeatmapRenderer(void)
  {
   m_obj_name      = "HeatmapCanvas";
   m_chart_id      = 0;
   m_subwindow     = 0;
   m_canvas_width  = 0;
   m_canvas_height = 0;
   m_initialized   = false;
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CHeatmapRenderer::~CHeatmapRenderer(void)
  {
   Destroy();
  }

Конструктор устанавливает для m_initialized значение false, предотвращая выполнение любых методов отрисовки до успешного вызова Create(). Деструктор вызывает метод Destroy(), чтобы гарантировать удаление объекта графической метки с графика, даже если вызывающий код забудет явно вызвать Destroy() в OnDeinit().

//+------------------------------------------------------------------+
//| Compute the pixel width of a single heatmap cell                 |
//+------------------------------------------------------------------+
int CHeatmapRenderer::CellWidth(void) const
  {
   int usable = m_canvas_width - RENDERER_LABEL_WIDTH - RENDERER_PADDING;
   int w      = usable / HEATMAP_HOURS;
   return(w < 4 ? 4 : w);
  }

//+------------------------------------------------------------------+
//| Compute the pixel height of a single heatmap cell                |
//+------------------------------------------------------------------+
int CHeatmapRenderer::CellHeight(void) const
  {
   int usable = m_canvas_height - RENDERER_LABEL_HEIGHT - RENDERER_PADDING;
   int h      = usable / HEATMAP_DAYS;
   return(h < 4 ? 4 : h);
  }

//+------------------------------------------------------------------+
//| Compute the left pixel coordinate of a cell column               |
//+------------------------------------------------------------------+
int CHeatmapRenderer::CellX(int hour_index) const
  {
   return(RENDERER_LABEL_WIDTH + hour_index * CellWidth());
  }

//+------------------------------------------------------------------+
//| Compute the top pixel coordinate of a cell row                   |
//+------------------------------------------------------------------+
int CHeatmapRenderer::CellY(int day_index) const
  {
   return(RENDERER_LABEL_HEIGHT + day_index * CellHeight());
  }

Метод CellWidth() вычитает из общей ширины холста ширину области подписей и внутренние отступы, затем делит результат на 24, получая ширину одной ячейки в пикселях. Метод CellHeight() выполняет аналогичный расчет по вертикали. В обоих случаях установлено минимальное значение в 4 пикселя, чтобы избежать появления невидимых ячеек в чрезвычайно узких подокнах. Методы CellX() и CellY() преобразуют координаты сетки в пиксельные координаты, добавляя соответствующее смещение, учитывающее область подписей.

//+------------------------------------------------------------------+
//| Fill the canvas background with a dark color                     |
//+------------------------------------------------------------------+
void CHeatmapRenderer::DrawBackground(void)
  {
   m_canvas.Erase(ARGB(255, 30, 30, 30));
  }

Метод DrawBackground() перед любой другой отрисовкой очищает все растровое изображение, заполняя его темно-серым цветом. Без такой очистки после изменения размера окна при перерисовке ячеек с новыми размерами будут видны остатки предыдущего кадра отрисовки.

//+------------------------------------------------------------------+
//| Draw hour labels along the top X axis                            |
//+------------------------------------------------------------------+
void CHeatmapRenderer::DrawHourLabels(void)
  {
   uint text_color = ARGB(255, 210, 210, 210);
   int  cw         = CellWidth();

   for(int h = 0; h < HEATMAP_HOURS; h++)
     {
      if(h % 2 != 0)
         continue;

      int    x   = CellX(h) + cw / 2 - 6;
      int    y   = 2;
      string lbl = IntegerToString(h);

      m_canvas.TextOut(x, y, lbl, text_color);
     }
  }

Метод DrawHourLabels() отрисовывает подписи часов только на четных позициях (0, 2, 4, ... 22), чтобы избежать наложения текста в ячейках, ширина которых может составлять всего 20–30 пикселей. Координата x задает положение подписи по центру ячейки: смещение составляет половину ширины ячейки за вычетом поправки в 6 пикселей, приблизительно соответствующей половине ширины символа.

//+------------------------------------------------------------------+
//| Draw weekday labels along the left Y axis                        |
//+------------------------------------------------------------------+
void CHeatmapRenderer::DrawDayLabels(void)
  {
   uint text_color = ARGB(255, 210, 210, 210);
   int  ch         = CellHeight();

   for(int d = 0; d < HEATMAP_DAYS; d++)
     {
      int    y    = CellY(d) + ch / 2 - 6;
      string name = CTimeBucketAnalyzer::DayName(d);
      string lbl  = StringSubstr(name, 0, 3);

      m_canvas.TextOut(RENDERER_PADDING, y, lbl, text_color);
     }
  }

Функция DrawDayLabels() отрисовывает трехсимвольные сокращения дней недели (Mon, Tue, Wed, Thu, Fri) так, чтобы они помещались в поле для меток шириной 64 пикселя. Координата y задаётся так, чтобы метка располагалась по центру строки по вертикали. Для этого используется то же смещение на половину высоты, что и для меток часов.

//+------------------------------------------------------------------+
//| Draw a single heatmap cell with its interpolated color           |
//+------------------------------------------------------------------+
void CHeatmapRenderer::DrawCell(int day_index, int hour_index,
                                double avg_return, double max_abs)
  {
   int x1 = CellX(hour_index);
   int y1 = CellY(day_index);
   int x2 = x1 + CellWidth()  - 1;
   int y2 = y1 + CellHeight() - 1;

   uint cell_color   = CHeatmapColorMap::ReturnToColor(avg_return, max_abs);
   uint border_color = ARGB(255, 30, 30, 30);

   m_canvas.FillRectangle(x1, y1, x2, y2, cell_color);
   m_canvas.Rectangle(x1, y1, x2, y2, border_color);
  }

Метод DrawCell() вычисляет координаты четырёх углов прямоугольника ячейки в пикселях, получает интерполированный цвет с помощью CHeatmapColorMap::ReturnToColor(), закрашивает внутреннюю часть ячейки с помощью FillRectangle(), а затем рисует тёмную рамку шириной 1 пиксель с помощью Rectangle(). Для рамки используется тот же тёмно-серый цвет, что и для фона. Это позволяет сделать сетку видимой, не вводя дополнительный цвет, который отвлекал бы внимание.

//+------------------------------------------------------------------+
//| Create the CCanvas object and attach it to the chart             |
//+------------------------------------------------------------------+
bool CHeatmapRenderer::Create(long chart_id, int subwindow,
                              int width, int height)
  {
   m_chart_id      = chart_id;
   m_subwindow     = subwindow;
   m_canvas_width  = width  > 0 ? width  : 800;
   m_canvas_height = height > 0 ? height : 200;

   if(!m_canvas.CreateBitmapLabel(m_chart_id, m_subwindow,
                                  m_obj_name,
                                  0, 0,
                                  m_canvas_width,
                                  m_canvas_height,
                                  COLOR_FORMAT_ARGB_NORMALIZE))
     {
      PrintFormat("[HeatmapRenderer] CreateBitmapLabel failed: %d",
                  GetLastError());
      return(false);
     }

   m_initialized = true;
   return(true);
  }

Метод Create() вызывает m_canvas.CreateBitmapLabel() с параметром COLOR_FORMAT_ARGB_NORMALIZE, который корректно интерпретируется как формат ARGB с 8 битами на канал. Если вызывающая сторона передаёт недопустимые значения, для холста устанавливаются размеры по умолчанию — 800×200. Переменная m_initialized принимает значение true только после успешного создания холста: если вызов CreateBitmapLabel() завершается с ошибкой, все последующие вызовы отрисовки блокируются проверкой в начале функции Render().

//+------------------------------------------------------------------+
//| Release the canvas object and delete the chart label             |
//+------------------------------------------------------------------+
void CHeatmapRenderer::Destroy(void)
  {
   if(m_initialized)
     {
      m_canvas.Destroy();
      ObjectDelete(m_chart_id, m_obj_name);
      m_initialized = false;
     }
  }

Метод Destroy() вызывает m_canvas.Destroy() для освобождения ресурса растрового изображения, а затем — ObjectDelete() для удаления объекта графической метки с графика по имени. Если явно не вызвать функцию ObjectDelete(), графическая метка останется видимой на графике после удаления или перекомпиляции индикатора, и пользователю придётся удалить её вручную.

//+------------------------------------------------------------------+
//| Render the complete heatmap from matrix and statistics           |
//+------------------------------------------------------------------+
bool CHeatmapRenderer::Render(CReturnMatrix &ret_matrix,
                              CHeatmapStatistics *stats)
  {
   if(!m_initialized)
      return(false);

   if(CheckPointer(&ret_matrix) == POINTER_INVALID ||
      CheckPointer(stats)  == POINTER_INVALID)
      return(false);

   double max_abs = stats.MaxAbs();

//--- clear canvas and draw structural elements first
   DrawBackground();
   DrawHourLabels();
   DrawDayLabels();

//--- draw all 120 color cells
   for(int d = 0; d < HEATMAP_DAYS; d++)
     {
      for(int h = 0; h < HEATMAP_HOURS; h++)
        {
         double avg = ret_matrix.GetAverage(d, h);
         DrawCell(d, h, avg, max_abs);
        }
     }

//--- commit the completed bitmap to the chart display
   m_canvas.Update();

   Print("[ReturnHeatmap] Canvas Updated Successfully");
   return(true);
  }

#endif // HEATMAP_RENDERER_MQH

Метод Render() последовательно вызывает все методы отрисовки и в конце вызывает m_canvas.Update() ровно один раз. Все промежуточные этапы отрисовки выполняются с растровым изображением в памяти. Только этот заключительный вызов выводит готовый кадр на экран, исключая видимые артефакты построения. Цикл отрисовки со сложностью O(120) работает достаточно быстро, чтобы обновление выглядело мгновенным при любой стандартной частоте обновления экрана.

IntradayReturnHeatmap.mq5

Главный файл индикатора связывает все модули воедино. В нём объявляются три глобальных объекта, реализуется обход исторических баров в функции BuildMatrix() и осуществляется управление жизненным циклом индикатора через OnInit(), OnDeinit(), OnCalculate() и OnChartEvent().

//+------------------------------------------------------------------+
//|                                        IntradayReturnHeatmap.mq5 |
//|                 Intraday Return Heatmap — Main Indicator Module  |
//+------------------------------------------------------------------+
#property indicator_separate_window
#property indicator_plots 0

//--- Include
#include <Heatmap_Visualization\ReturnMatrix.mqh>
#include <Heatmap_Visualization\TimeBucketAnalyzer.mqh>
#include <Heatmap_Visualization\HeatmapColorMap.mqh>
#include <Heatmap_Visualization\HeatmapStatistics.mqh>
#include <Heatmap_Visualization\HeatmapRenderer.mqh>

Директива #property indicator_separate_window размещает холст в отдельном подокне под ценовым графиком. Директива #property indicator_plots 0 отключает стандартную отрисовку по буферам: этот индикатор полностью выполняет отрисовку средствами CCanvas и не использует графические построения на основе буферов. В окне данных будет отображаться "Окно индикатора 1" без значений под ним — это нормальное поведение для модуля отрисовки, использующего исключительно холст.

//--- Inputs
input int  inp_lookback_bars = 5000; // Number of Historical Bars Used For Analysis
input bool inp_print_stats   = true; // Print Summary Statistics To The Experts Tab

//--- Global Variables
CReturnMatrix      ExtMatrix;    // 5x24 return accumulation matrix
CHeatmapStatistics ExtStats;     // Statistical summary of matrix
CHeatmapRenderer   ExtRenderer;  // CCanvas rendering engine
bool               ExtReady;     // True after successful first render

Все три объекта объявлены в глобальной области видимости, поэтому они доступны всем обработчикам событий без передачи в качестве параметров. ExtReady хранит признак того, что хотя бы одна отрисовка успешно завершена. OnChartEvent() использует его, чтобы определить, нужна ли повторная отрисовка после изменения размера окна.

//+------------------------------------------------------------------+
//| Scan historical bars and populate the return matrix              |
//+------------------------------------------------------------------+
bool BuildMatrix(const datetime &time[],
                 const double   &close[])
  {
   ExtMatrix.Reset();

   int bars = ArraySize(close);
   if(bars < 2)
      return(false);

//--- determine the scan limit from the lookback input
   int scan_limit = bars - 1;
   if(inp_lookback_bars > 0 && inp_lookback_bars < scan_limit)
      scan_limit = inp_lookback_bars;

   for(int i = 0; i < scan_limit; i++)
     {
      if(close[i + 1] <= 0.0)
         continue;

      //--- compute percentage return relative to the previous bar close
      double ret = (close[i] - close[i + 1]) / close[i + 1] * 100.0;

      //--- decompose timestamp into day and hour bucket indices
      int day_idx  = -1;
      int hour_idx = -1;
      if(!CTimeBucketAnalyzer::Decompose(time[i], day_idx, hour_idx))
         continue;

      ExtMatrix.Accumulate(day_idx, hour_idx, ret);
     }

   ExtMatrix.ComputeAverages();
   return(true);
  }

BuildMatrix() — это свободная функция (а не метод класса), поэтому при вызове функций стандартной библиотеки внутри неё не требуется префикс ::. Функция перебирает индексы от 0 до scan_limit - 1. Для каждого бара с индексом i процентная доходность рассчитывается относительно close[i+1] — цены закрытия непосредственно предшествующего бара, — поскольку в массивах индикаторов MQL5 самый последний бар имеет индекс 0. Проверка close[i + 1] <= 0.0 предотвращает деление на ноль при пропусках в исторических данных. Функция ComputeAverages() вызывается один раз после завершения обхода всех баров.

//+------------------------------------------------------------------+
//| Indicator initialization function                                |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   ExtReady = false;

   if(inp_lookback_bars < 2)
     {
      Print("[ReturnHeatmap] Error: inp_lookback_bars must be >= 2");
      return(INIT_PARAMETERS_INCORRECT);
     }

//--- locate the indicator subwindow
   int subwindow = ChartWindowFind(0, "");
   if(subwindow < 0)
      subwindow = 1;

//--- retrieve chart pixel dimensions for the subwindow
   int chart_width  = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS,  0);
   int chart_height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, subwindow);

   if(chart_width < 100)
      chart_width = 800;
   if(chart_height < 50)
      chart_height = 200;

   if(!ExtRenderer.Create(0, subwindow, chart_width, chart_height))
     {
      Print("[ReturnHeatmap] Renderer creation failed");
      return(INIT_FAILED);
     }

   PrintFormat("[ReturnHeatmap] Initialized — Lookback: %d bars",
               inp_lookback_bars);
   return(INIT_SUCCEEDED);
  }

Функция OnInit() запрашивает высоту подокна с помощью ChartGetInteger(), передавая индекс подокна, чтобы получить фактическую высоту в пикселях, выделенную для подокна индикатора, которая отличается от высоты основного окна графика. Если возвращаемые значения неоправданно малы, применяются значения по умолчанию 800×200; такая ситуация может возникнуть, когда индикатор инициализируется до завершения отрисовки окна графика.

//+------------------------------------------------------------------+
//| Indicator deinitialization function                              |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ExtRenderer.Destroy();
   PrintFormat("[ReturnHeatmap] Deinitialized — reason: %d", reason);
  }

Функция OnDeinit() вызывает ExtRenderer.Destroy(), чтобы удалить графическую метку с графика. Без этого вызова растровый объект тепловой карты остается видимым на графике после удаления с него индикатора или его перекомпиляции.

//+------------------------------------------------------------------+
//| Indicator calculation function                                   |
//+------------------------------------------------------------------+
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[])
  {
//--- skip recalculation when no new bars have arrived
   if(prev_calculated > 0 && rates_total == prev_calculated)
      return(rates_total);

   if(rates_total < 3)
      return(0);

   if(!BuildMatrix(time, close))
     {
      Print("[ReturnHeatmap] BuildMatrix failed");
      return(0);
     }

//--- compute statistics from the completed matrix (passing object by reference)
   ExtStats.Compute(ExtMatrix);

   if(inp_print_stats)
      ExtStats.PrintDiagnostics(ExtMatrix.GetTotalSamples());

//--- render the heatmap to the canvas
   if(!ExtRenderer.Render(ExtMatrix, GetPointer(ExtStats)))
     {
      Print("[ReturnHeatmap] Render failed");
      return(0);
     }

   ExtReady = true;
   return(rates_total);
  }

Проверка условия prev_calculated > 0 && rates_total == prev_calculated предотвращает полное перестроение матрицы на каждом тике. Полное перестроение матрицы выполняется только при изменении значения rates_total, что происходит при закрытии каждого нового бара. На графиках M1 это означает один пересчёт в минуту; на графиках H1 — один раз в час.

Функция GetPointer(ExtStats) используется для передачи объекта ExtStats в функцию Render(), которая ожидает указатель типа CHeatmapStatistics *. Для глобального объекта или объекта, размещённого в стеке, GetPointer() — корректный в MQL5 способ получить пригодный для использования указатель; в данном контексте он безопаснее оператора взятия адреса & для объектов, размещённых не в куче.

//+------------------------------------------------------------------+
//| Chart event handler — resize canvas when window dimensions change|
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   if(id == CHARTEVENT_CHART_CHANGE)
     {
      int subwindow = ChartWindowFind(0, "");
      if(subwindow < 0)
         subwindow = 1;

      int new_width  = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS,  0);
      int new_height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, subwindow);

      if(new_width < 100)
         new_width = 800;
      if(new_height < 50)
         new_height = 200;

      ExtRenderer.Destroy();
      if(ExtRenderer.Create(0, subwindow, new_width, new_height))
        {
         if(ExtReady)
            ExtRenderer.Render(ExtMatrix, GetPointer(ExtStats));
        }
     }
  }

Функция OnChartEvent() реагирует на событие CHARTEVENT_CHART_CHANGE, которое срабатывает, когда пользователь изменяет размер окна терминала или настраивает расположение панелей графиков. Модуль отрисовки уничтожается и создаётся заново с новыми размерами в пикселях. Если ExtReady имеет значение true (то есть хотя бы одна отрисовка выполнена успешно), существующая матрица и статистические данные немедленно отрисовываются заново без повторного обхода исторических баров. Благодаря этому изменение размера окна происходит быстро, а излишние вычисления исключаются.


Заключение

В данной статье представлена полная реализация тепловой карты внутридневной доходности для MetaTrader 5, преобразующей исторические ценовые данные в компактное визуальное представление средней доходности по дням недели и часам суток. Благодаря сочетанию нормализации процентной доходности, фиксированной матрицы агрегирования 5×24, относительного масштабирования цветовой шкалы и отрисовки с помощью CCanvas индикатор позволяет быстро и интуитивно понятно исследовать историческое внутридневное поведение рынка непосредственно в торговой платформе.

Реализация намеренно построена по модульному принципу: разложение временной метки на компоненты, агрегирование доходности, вычисление сводных статистических показателей, сопоставление значений цветам и отрисовка выделены в независимые компоненты. Диагностика во время выполнения проверяет корректность обработки данных, при этом вычислительная сложность остается предсказуемой: обход исторических баров имеет сложность O(N), тогда как агрегирование, нормализация и отрисовка выполняются на фиксированной матрице из 120 ячеек.

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


Программы, использованные в статье:

# Название Тип Описание
1 ReturnMatrix.mqh Подключаемый файл Определяет класс CReturnMatrix, который накапливает процентные доходности баров в сетке размером 5×24 и вычисляет среднее арифметическое для каждой ячейки, соответствующей дню недели и часу, после завершения обхода исторических баров.
2 TimeBucketAnalyzer.mqh Подключаемый файл Определяет класс CTimeBucketAnalyzer, который извлекает из временной метки бара индекс дня недели и индекс часа и отбрасывает бары выходных дней до этапа накопления.
3 HeatmapColorMap.mqh Подключаемый файл
Определяет класс CHeatmapColorMap, который нормализует значение средней доходности относительно максимальной по модулю доходности в матрице и преобразует его в цвет ARGB в диапазоне от тёмно-красного через нейтральный серый до тёмно-зелёного.
4 HeatmapStatistics.mqh Подключаемый файл
Определяет класс CHeatmapStatistics, который обходит готовую матрицу и определяет минимальную среднюю доходность, максимальную среднюю доходность, максимальную по модулю среднюю доходность и число непустых ячеек для масштабирования цветовой шкалы и вывода диагностической информации.
5 HeatmapRenderer.mqh Подключаемый файл
Определяет класс CHeatmapRenderer, который управляет растровым объектом CCanvas в подокне индикатора и отрисовывает фон, подписи осей и все 120 цветных ячеек перед выводом кадра на экран.
6 IntradayReturnHeatmap.mq5 Пользовательский индикатор Основной файл индикатора, который выполняет обход исторических баров, строит матрицу доходностей, вычисляет статистические показатели и координирует этапы отрисовки в обработчиках OnInit(), OnCalculate(), OnDeinit() и OnChartEvent().
7 Heatmap_Visualization.zip Архив в формате ZIP ZIP-архив, содержащий все прилагаемые файлы и их пути относительно корневой папки терминала.


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

Прикрепленные файлы |
ReturnMatrix.mqh (5.4 KB)
От прогноза цены к узкому торговому уравнению От прогноза цены к узкому торговому уравнению
Исследуется прогноз следующего часового движения EURUSD H1 на длинной истории. Сравнивается исходное ценовое уравнение с простейшим ориентиром, после чего задача переносится на направление и величину следующего изменения с хронологическим разделением данных. Отдельно анализируется переход 23:00→00:00 и его поведение на 100 последовательных двухмесячных участках вне обучения. Читатель получает воспроизводимую схему подготовки данных и отбора сигналов по уверенности.
Создание интерактивного индикатора профиля объема AnchorFlow (MTF) на языке MQL5 Создание интерактивного индикатора профиля объема AnchorFlow (MTF) на языке MQL5
Мы реализуем интерактивный мультитаймфреймовый (MTF) профиль объема с якорной привязкой на языке MQL5 для MetaTrader 5. Индикатор строит профиль текущего таймфрейма на основном графике и профиль старшего таймфрейма (HTF) в подокне индикатора; оба профиля согласованы по положению общей линии с якорной привязкой и видимому диапазону. Вы освоите настройка количества ценовых интервалов (бинов) профиля с помощью клавиатуры (двойное нажатие клавиши E или S), надежную проверку таймфреймов, обновления с учетом видимой области графика и восстановление графических объектов, чтобы организовать надежную, синхронизированную работу с объемами.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Конструирование признаков для машинного обучения (Часть 9): Tесты на структурные сдвиги на Python Конструирование признаков для машинного обучения (Часть 9): Tесты на структурные сдвиги на Python
Мы представляем реализацию тестов на структурные сдвиги из главы 17 AFML, готовую для использования в реальных системах. Модуль включает тест Чу — Стинчкомба — Уайта (односторонний и двусторонний варианты), тест Дики — Фуллера типа Чоу (DFC), SADF для шести моделей (линейная, квадратичная, sm poly 1, sm poly 2, sm exp, sm power), а также квантильную статистику ADF (QADF) с параметрами q и v и условное среднее ADF (CADF) с параметром q; результаты возвращаются в виде скалярных признаков с индексацией по барам. Мы устраняем проблемы масштабируемости фрагментов кода из книги и ошибку, связанную с порядком аргументов, а также показываем, как фиксированная глубина ретроспективы (L=504) ограничивает вычислительную сложность SADF величиной O(L²) на бар при выявлении рыночных режимов.