preview
Heatmap Visualization of Intraday Return Patterns in MQL5 Using CCanvas

Heatmap Visualization of Intraday Return Patterns in MQL5 Using CCanvas

MetaTrader 5Indicators |
140 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Introduction

Experienced traders often observe that certain instruments behave differently depending on the time of day and the day of the week. EURUSD tends to exhibit higher volatility during the London-New York overlap. XAUUSD frequently shows directional movement during Asian-session hours. These tendencies are recognizable to practitioners but rarely quantified systematically, because MetaTrader 5 provides no native tool for aggregating and visualizing return distributions across time dimensions simultaneously.

A heatmap solves this problem directly. The heatmap computes the average bar return for each hour-of-day and day-of-week over a configurable lookback period. It then maps these averages to a color-interpolated grid, turning thousands of data points into an interpretable two-dimensional view. A trader can identify at a glance which hour-day combinations have historically produced positive average returns and which have not — without claiming that this information predicts future behavior.


Bar Return Definition

The return of a single bar is defined as the percentage change from the previous close to the current close:

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

Percentage normalization is used rather than raw price differences because raw differences are instrument-dependent and non-comparable. A one-pip move on EURUSD and a one-pip move on XAUUSD carry entirely different economic significance. Percentage returns express both in the same unit, making the heatmap visually meaningful when compared across instruments.

Arithmetic averaging is used rather than geometric or log-return averaging. For small returns over short holding periods — individual bars — arithmetic and log-return averages converge, and arithmetic averaging is computationally simpler and more interpretable in a visualization context.


Two-Dimensional Aggregation

The aggregation matrix has dimensions 5 × 24, where rows index weekdays (Monday through Friday) and columns index hours of the day (0 through 23).

Axis Dimension Values
Rows (Y) 5 Monday=0, Tuesday=1, Wednesday=2, Thursday=3, Friday=4
Columns (X) 24 Hour 0 through Hour 23

For each historical bar, two values are extracted from its timestamp: the hour of day and the day of week. Weekend bars are discarded. The bar's percentage return is accumulated into the corresponding cell, and a sample counter for that cell is incremented. After the full historical scan, each cell's average return is computed by dividing the accumulated sum by the sample count.

The variable notation used throughout is:

Symbol Meaning
R_i Bar return for bar i
H Hour index (0–23)
D Day index (0–4, Monday–Friday)
Sum(H,D) Accumulated returns for cell (H,D)
Count(H,D) Number of samples in cell (H,D)
Avg(H,D) Mean return = Sum(H,D) / Count(H,D)


Color Mapping

The color scale maps average return values to RGB colors using a red-to-green interpolation centered at zero.

Color Meaning
Dark Green Strong positive average return
Light Green Mild positive average return
Gray Near-zero average return
Light Red Mild negative average return
Dark Red Strong negative average return

The normalization divides each cell's average return by the maximum absolute return observed anywhere in the matrix. This relative approach ensures the full color range is always utilized regardless of whether the instrument has large or small absolute returns.

EURUSD H1 heatmap

EURUSD H1 heatmap showing average return distribution across 5,000 historical bars. Green cells indicate hours and weekdays with historically positive average returns; red cells indicate negative. Color intensity reflects return magnitude relative to the strongest signal in the dataset.

Historical return averages are descriptive, not predictive. Markets are non-stationary: tendencies that produced a pattern in the lookback window may not persist. Structural events — central bank policy changes, market microstructure shifts — can invalidate historical patterns without warning.

The indicator does not perform statistical significance testing. A cell with an average return of +0.02% derived from 8 samples is not meaningfully different from zero. Cells with low sample counts should be interpreted cautiously. The visualization is an exploratory analytical tool, not a signal generator.


Computational Complexity

Phase Complexity Notes
Historical bar scan O(N) N = number of lookback bars
Return accumulation O(1) per bar Direct matrix indexing
Average computation O(120) Fixed matrix size
Color interpolation O(120)
Fixed matrix size
Canvas rendering O(120)
Fixed matrix size

The dominant cost is the historical scan, which executes once in OnCalculate() when the indicator first loads and again when a new bar closes. All rendering operates at the fixed cost O(120) regardless of how many bars were analyzed.


Expected Log Output

[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

The 'Cells Rendered = 120' line confirms that every combination of weekday and hour contains at least one sample. On EURUSD H1, a 5,000-bar lookback covers roughly seven months of history. Under these conditions, each of the 120 cells typically has a meaningful sample size. On faster timeframes such as M1, the same lookback covers only a few days, leaving many cells empty and reducing the rendered cell count accordingly. Max Abs Return is used as the normalization denominator for the color scale. The strongest positive or negative cell is rendered with maximum saturation, and all other cells are scaled proportionally.


Code Listings and Architectural Breakdown

ReturnMatrix.mqh

CReturnMatrix is the central data structure of the indicator. It accumulates percentage bar returns into a 5×24 grid indexed by day-of-week and hour-of-day, and computes arithmetic averages per cell after the full historical scan completes. It has no awareness of disk, display, or color — it only manages numbers.

One important implementation note: the parameter name matrix cannot be used in recent versions of MQL5 because matrix was introduced as a built-in reserved keyword for mathematical matrix operations. All parameters referring to CReturnMatrix objects use the name ret_matrix throughout this implementation to avoid this conflict.

The file opens with its include guard and dimension constants:

//+------------------------------------------------------------------+
//|                                              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 and HEATMAP_HOURS are defined as preprocessor constants rather than class members so that all other modules — HeatmapStatistics.mqh, HeatmapRenderer.mqh — can use them in their own loop bounds without depending on a CReturnMatrix instance.

The class declaration follows:

//+------------------------------------------------------------------+
//| 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;
  };

Three parallel 5×24 arrays serve distinct roles. m_sum accumulates the raw sum of percentage returns contributed to each cell. m_count records how many bars have been accumulated into each cell. m_avg stores the computed arithmetic average, populated only after ComputeAverages() is called. m_total_samples tracks the global count of valid bars processed, used for the diagnostic log output.

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

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

The constructor initializes m_total_samples and immediately calls Reset() to zero all array cells. The destructor is empty because all three arrays are fixed-size members allocated as part of the object itself — no heap memory is involved and no explicit release is needed. The total memory occupied by a CReturnMatrix instance is two double[5][24] arrays at 8 bytes each (1,920 bytes) plus one int[5][24] at 4 bytes (480 bytes), totaling approximately 2.4 KB.

//+------------------------------------------------------------------+
//| 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() zeroes all three arrays and the total sample counter in a single O(120) pass. It is called from the constructor and from BuildMatrix() in the main indicator file at the start of every recalculation cycle. This ensures that a second call — triggered by a new bar closing — does not accumulate on top of results from the previous scan.

//+------------------------------------------------------------------+
//| 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() is called once per valid historical bar. It validates both indices before writing to prevent out-of-range array access in the event that timestamp decomposition produces unexpected values. This bounds check is a second line of defense supplementing the weekend rejection performed in CTimeBucketAnalyzer. On a valid bar, the return is added to the cell's sum and both the cell counter and the global sample counter are incremented. The method has O(1) complexity — it performs a constant number of operations regardless of the matrix size.

//+------------------------------------------------------------------+
//| 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() performs the final division across all 120 cells in a single O(120) pass. This deferred computation strategy avoids performing thousands of redundant divisions during the scan — the averages are meaningless until all bars have been processed. Cells with zero sample counts receive an average of 0.0, which the color map renders as neutral gray.

//+------------------------------------------------------------------+
//| 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

Both accessors repeat the bounds check before returning, producing a safe neutral value on any out-of-range index. GetCount() is used by CHeatmapStatistics::Compute() to skip empty cells when computing summary statistics, and by CHeatmapRenderer::Render() implicitly through the average values. GetTotalSamples() returns the global bar count for the diagnostic log.

TimeBucketAnalyzer.mqh

CTimeBucketAnalyzer isolates all timestamp processing behind a static interface. Every method is static — timestamp decomposition is a pure function that depends only on its input and carries no state between calls. Instantiating the class is never necessary.

//+------------------------------------------------------------------+
//|                                         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() is the primary entry point called once per bar during the historical scan. IsWeekend() is a pre-filter called by Decompose() before any other processing. DayName() provides the three-character abbreviations used by the renderer to label the Y axis.

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

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

Both are empty. The class holds no member data and allocates no resources. They are defined explicitly to comply with the common MetaQuotes convention that requires every class to declare its constructor and destructor.

//+------------------------------------------------------------------+
//| 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() uses TimeToStruct() to decompose the timestamp into an MqlDateTime struct and checks the day_of_week field against Sunday (0) and Saturday (6). The MQL4-era functions are not available in MQL5; TimeToStruct() is the preferred replacement. Some brokers include weekend candles in their history for certain instruments; rejecting them here prevents those bars from being accumulated into Monday or Friday cells through an incorrect day mapping.

//+------------------------------------------------------------------+
//| 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() initializes both output parameters to -1 before any processing. If the method returns false — due to a weekend bar or an unexpected day_of_week value — the caller receives index values that will fail the bounds check in CReturnMatrix::Accumulate(), providing a second layer of defense against data corruption.

The switch statement remaps remaps the day of week convention (Sunday=0, Monday=1, Saturday=6) to a zero-based Monday-first index. This remapping ensures that the matrix row for Monday corresponds to index 0, consistent with how the renderer labels the Y axis from top to bottom.

//+------------------------------------------------------------------+
//| 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() provides human-readable labels for the renderer's Y axis. The renderer calls StringSubstr(name, 0, 3) on the result to produce the three-character abbreviations Mon, Tue, Wed, Thu, Fri that fit within the 64-pixel label margin.

HeatmapColorMap.mqh

CHeatmapColorMap performs the normalization and RGB interpolation that determines what each cell looks like on screen. All methods are static — color conversion is a pure function of its inputs. The file includes <Canvas\Canvas.mqh> to gain access to the ARGB() macro, which is defined there.

//+------------------------------------------------------------------+
//|                                              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() is the primary conversion method called for each of the 120 cells during rendering. NeutralColor() returns a fixed light gray used for cells with no data or a zero return range.

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

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

Both are empty. The class holds no state and all methods are static.

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

NeutralColor() returns a fully opaque light gray in ARGB format. It is called when max_abs_return is effectively zero — meaning all cells in the matrix have zero average return, which can occur on synthetic price series or when the lookback contains no valid bars. It is also the visual output for any cell whose average return maps to zero after normalization.

//+------------------------------------------------------------------+
//| 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() performs three sequential operations. First, it normalizes avg_return to the range [-1, +1] by dividing by max_abs_return. This relative normalization ensures the full color range is always utilized — a matrix where all returns fall between -0.01% and +0.01% will use the same color span as one where returns range from -0.5% to +0.5%. The clamp guards against floating-point imprecision that could push norm marginally outside the valid range.

Second, it computes the RGB components using two-branch interpolation. For positive returns it shifts from gray toward green; for negative returns it shifts from gray toward red. Component values are then clamped to [0, 255]. For positive returns, green is held constant at 200 and red and blue both fade from 200 toward 50 as norm approaches 1 — producing a progression from light gray at zero to saturated green at the maximum. For negative returns, red is held at 200 and green and blue fade symmetrically — producing light gray at zero and saturated red at the minimum. The result is a visually balanced bidirectional scale.

Third, the RGB components are clamped to [0, 255] and assembled using ARGB(255, r, g, b), which packs the four bytes into a 32-bit unsigned integer with the alpha channel set to 255 (fully opaque). CCanvas uses ARGB format — specifying the bytes in the wrong order would swap the positive and negative visual signals.

HeatmapStatistics.mqh

CHeatmapStatistics performs a single O(120) pass over the completed matrix to extract four summary values: minimum average return, maximum average return, maximum absolute return, and the count of populated cells. It is a separate class from CReturnMatrix because statistical summarization is a distinct responsibility — it consumes the matrix rather than managing it.

//+------------------------------------------------------------------+
//|                                            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 is the value consumed by CHeatmapRenderer as the normalization denominator for the color scale. m_cell_count reports how many cells had at least one sample, which appears in the diagnostic log as Cells Rendered. The Compute() parameter is named ret_matrix rather than matrix to avoid the MQL5 reserved keyword conflict.

//+------------------------------------------------------------------+
//| 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)
  {
  }

The constructor initializes m_min_avg to a large positive value and m_max_avg to a large negative value. This ensures that the first valid cell encountered will always update both extremes correctly — regardless of whether its average is positive or negative. The destructor is empty.

//+------------------------------------------------------------------+
//| 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() resets all four statistics at the start of each call to prevent accumulation across multiple recalculation cycles. The inner loop skips any cell where GetCount() returns zero — this is critical because empty cells hold a default average of 0.0, and including them would corrupt the minimum value by pulling it toward zero even when all non-empty cells are positive.

The guard block after the loop handles the pathological case where the entire matrix is empty — which occurs when the indicator loads on a chart with insufficient history — setting all statistics to zero to prevent undefined behavior in the color mapper.

//+------------------------------------------------------------------+
//| 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

The four accessors return their respective private members directly. MaxAbs() is the most frequently consumed — it is called by CHeatmapRenderer::Render() before the drawing loop to establish the normalization denominator for the color scale.

PrintDiagnostics() produces the five-line summary visible in the Experts tab on each recalculation. The %% in the format string produces a literal percent sign in the output. DoubleToString() with four decimal places gives sufficient precision for the small return magnitudes typical of forex and crypto instruments.

HeatmapRenderer.mqh

CHeatmapRenderer manages the full lifecycle of the CCanvas object and translates abstract matrix data into visible pixels. It is the only module in the system that interacts with the MQL5 chart object layer.

//+------------------------------------------------------------------+
//|                                              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);
  };

The layout reserves RENDERER_LABEL_WIDTH = 64 pixels on the left for day-name labels and RENDERER_LABEL_HEIGHT = 20 pixels at the top for hour labels. The remaining canvas area is divided into a 5×24 grid whose cell dimensions adapt automatically to the canvas size.

Render() uses mixed parameter passing. ret_matrix is passed by reference because it contains fixed-size arrays. stats is passed as a pointer because CHeatmapStatistics does not have this restriction. This mixed approach is the solution that compiles correctly in the MQL5 environment.

//+------------------------------------------------------------------+
//| 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();
  }

The constructor sets m_initialized to false, preventing any drawing method from executing before Create() succeeds. The destructor calls Destroy() to ensure the bitmap label object is removed from the chart even if the caller forgets to call Destroy() explicitly in 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() subtracts the label margin and padding from the total canvas width, then divides by 24 to obtain the pixel width per cell. CellHeight() applies the same calculation vertically. Both enforce a minimum of 4 pixels to prevent invisible cells on extremely narrow subwindows. CellX() and CellY() convert grid coordinates to pixel coordinates by adding the respective label margin offset.

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

DrawBackground() clears the entire bitmap to a dark gray before any other drawing. Without this clear, remnants from a previous render frame bleed through when cells are redrawn at different sizes after a window resize.

//+------------------------------------------------------------------+
//| 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() renders hour indices at even positions only (0, 2, 4, ... 22) to avoid text overlap on cells that may be as narrow as 20–30 pixels. The x coordinate centers the label within the cell by offsetting by half the cell width minus an approximate half-character correction of 6 pixels.

//+------------------------------------------------------------------+
//| 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() renders three-character abbreviations — Mon, Tue, Wed, Thu, Fri — to fit within the 64-pixel label margin. The y coordinate vertically centers the label within the row using the same half-height offset pattern as the hour labels.

//+------------------------------------------------------------------+
//| 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() computes the four pixel corners of the cell rectangle, obtains the interpolated color from CHeatmapColorMap::ReturnToColor(), floods the cell interior with FillRectangle(), and then draws a 1-pixel dark border with Rectangle(). The border uses the same dark gray as the background, making the grid structure visible without introducing a competing color.

//+------------------------------------------------------------------+
//| 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() calls m_canvas.CreateBitmapLabel() with COLOR_FORMAT_ARGB_NORMALIZE, interpreted correctly as 8-bit-per-channel ARGB. The canvas dimensions default to 800×200 when the caller passes invalid values. m_initialized is set to true only after the canvas creation succeeds — if CreateBitmapLabel() fails, all subsequent drawing calls are blocked by the guard at the top of 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() calls m_canvas.Destroy() to release the bitmap resource and then ObjectDelete() to remove the chart label object by name. Without the explicit ObjectDelete(), the bitmap label persists visibly on the chart after the indicator is removed or recompiled, requiring the user to delete it manually.

//+------------------------------------------------------------------+
//| 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() sequences all drawing methods and calls m_canvas.Update() exactly once at the end. Every intermediate drawing step operates on the in-memory bitmap; only this final call transfers the completed frame to the display, eliminating visible construction artifacts. The O(120) drawing loop is fast enough that the update appears instantaneous at any normal screen refresh rate.

IntradayReturnHeatmap.mq5

The main indicator file wires all modules together. It declares the three global objects, implements the historical scan in BuildMatrix(), and manages the indicator lifecycle across OnInit(), OnDeinit(), OnCalculate(), and 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 places the canvas in an independent subwindow below the price chart. #property indicator_plots 0 suppresses standard buffer rendering — this indicator draws entirely through CCanvas and has no buffer-based plots. The Data Window will show Indicator window 1 with no values beneath it, which is the correct behavior for a pure canvas renderer.

//--- 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

All three objects are declared at global scope so they are shared across all event handlers without parameter passing. ExtReady tracks whether at least one successful render has completed, used by OnChartEvent() to decide whether to re-render after a window resize.

//+------------------------------------------------------------------+
//| 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() is a free function — not a class method — so standard library calls within it require no :: prefix. The function iterates from index 0 to scan_limit - 1. For each bar at index i, the percentage return is computed relative to close[i+1] — the immediately preceding bar — because MQL5 indicator arrays are indexed with the most recent bar at index 0. The guard close[i + 1] <= 0.0 prevents division by zero on history gaps. ComputeAverages() is called once after all bars have been scanned.

//+------------------------------------------------------------------+
//| 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() queries the subwindow height using ChartGetInteger() with the subwindow index to obtain the actual pixel height allocated to the indicator window — which differs from the main chart window height. The defaults of 800×200 are applied if the returned values are unreasonably small, which can occur when the indicator initializes before the chart window has fully rendered.

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

OnDeinit() calls ExtRenderer.Destroy() to remove the bitmap label from the chart. Without this call, the heatmap bitmap object persists visibly on the chart after the indicator is detached or recompiled.

//+------------------------------------------------------------------+
//| 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);
  }

The prev_calculated > 0 && rates_total == prev_calculated guard prevents the full matrix rebuild from running on every tick. It runs only when rates_total changes, which occurs on each new bar close. On M1 charts this means one recalculation per minute; on H1 charts one per hour.

GetPointer(ExtStats) is used to pass ExtStats to Render(), which expects a CHeatmapStatistics * pointer. For a global or stack-allocated object, GetPointer() is the correct MQL5 way to obtain a usable pointer — it is safer than the & address-of operator for non-heap objects in this context.

//+------------------------------------------------------------------+
//| 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() responds to CHARTEVENT_CHART_CHANGE, which fires when the user resizes the terminal window or adjusts the chart panel layout. The renderer is destroyed and recreated at the new pixel dimensions. If ExtReady is true — meaning at least one successful render has occurred — the existing matrix and statistics are re-rendered immediately without rescanning history. This makes window resizing fast and avoids redundant computation.


Conclusion

This article presented a complete implementation of an intraday return heatmap for MetaTrader 5, transforming historical price data into a compact visual representation of average returns across weekdays and hours of the day. By combining percentage return normalization, a fixed 5×24 aggregation matrix, relative color scaling, and CCanvas rendering, the indicator provides a fast and intuitive way to explore historical intraday behavior directly within the trading platform.

The implementation is intentionally modular, separating time decomposition, return aggregation, statistical summarization, color mapping, and rendering into independent components. Runtime diagnostics verify correct data processing, while the computational cost remains predictable: the historical scan is O(N), whereas aggregation, normalization, and rendering operate on a fixed 120-cell matrix.

The resulting heatmap is designed as a practical analytical tool for exploring session-level tendencies, evaluating time-based filters, and validating hypotheses about intraday market behavior. Beyond this specific implementation, the same architecture can serve as a foundation for richer visual analytics in MetaTrader 5, including alternative aggregation methods, additional statistical metrics, or more advanced market behavior studies.


Programs used in the article:

# Name Type Description
1 ReturnMatrix.mqh Include File Defines the CReturnMatrix class, which accumulates percentage bar returns into a 5×24 grid and computes the arithmetic average for each weekday-hour cell after the historical scan completes.
2 TimeBucketAnalyzer.mqh Include File Defines the CTimeBucketAnalyzer class, which decomposes a bar timestamp into a weekday index and an hour index, and rejects weekend bars before they reach the accumulation stage.
3 HeatmapColorMap.mqh Include File
Defines the CHeatmapColorMap class, which normalizes an average return value relative to the matrix maximum and converts it to an ARGB color ranging from dark red through neutral gray to dark green.
4 HeatmapStatistics.mqh Include File
Defines the CHeatmapStatistics class, which scans the completed matrix to extract the minimum average return, maximum average return, maximum absolute return, and the count of populated cells used for color scaling and diagnostic output.
5 HeatmapRenderer.mqh Include File
Defines the CHeatmapRenderer class, which manages a CCanvas bitmap object in the indicator subwindow and draws the background, axis labels, and all 120 color cells before committing the frame to the display.
6 IntradayReturnHeatmap.mq5 Custom Indicator The main indicator file that scans historical bars, builds the return matrix, computes statistics, and coordinates the rendering pipeline across OnInit(), OnCalculate(), OnDeinit(), and OnChartEvent().
7 Heatmap_Visualization.zip Zip Archive Zip archive containing all the attached files and their paths relative to the terminal's root folder.


Digital Signal Processing for Traders: Building Ehlers' Filter Library in MQL5 Digital Signal Processing for Traders: Building Ehlers' Filter Library in MQL5
We implement Ehlers-style DSP filters in a single reusable MQL5 library and use it to build two indicators. The Roofing Filter applies a 2‑pole high‑pass followed by a Super Smoother to isolate the tradeable 10–48‑bar band. The Even Better Sinewave normalizes the wave to about ±1, oscillating in cycle regimes and railing in trends, so you can read cycles and detect regime shifts in charts and EAs.
Market Microstructure in MQL5 (Part 7): Regime Classification Market Microstructure in MQL5 (Part 7): Regime Classification
We integrate eleven one-minute microstructure measurements from Parts 2–6 into a composite regime label with confidence and direction. A rule-based RegimeClassifier() assigns one of six regimes—Normal, Stressed, Noisy, Informed, Trending, Mean-Reverting—using empirically derived thresholds from 514 NQ M1 sessions (May 2024–May 2026). The deliverable includes MARKET_REGIME, RegimeAnalysis, and PopulateRegimeAnalysis(), enabling position sizing, stop placement, and signal filtering from a single call.
Beyond GARCH (Part VII): Monte Carlo Volatility Forecasting in MQL5 Beyond GARCH (Part VII): Monte Carlo Volatility Forecasting in MQL5
We implement the CMonteCarlo module that turns the fitted MMAR parameters into a volatility forecast via Monte Carlo. It runs N independent simulations over a chosen horizon and reports mean, median, standard deviation, and a percentile-based 95% confidence interval, with access to per-run values if needed. Adaptive cascade depth selects the minimal k such that b^k covers the horizon, keeping the run fast and consistent.
Automating Trading Strategies in MQL5 (Part 49): The Quasimodo (QM) Reversal Pattern Automating Trading Strategies in MQL5 (Part 49): The Quasimodo (QM) Reversal Pattern
In this article, we build an automated trading program in MQL5 that detects the Quasimodo reversal pattern from a zig-zag of confirmed swing pivots. We work through swing detection, pattern arming, retrace entries at the QM line, and structural stop placement with risk-based sizing. We also add trade management with breakeven, trailing, and partial closing to handle open positions.