Discussing the article: "Analyzing Price Time Gaps in MQL5 (Part II): Creating a Heat Map of Liquidity Distribution Over Time"

 

Check out the new article: Analyzing Price Time Gaps in MQL5 (Part II): Creating a Heat Map of Liquidity Distribution Over Time.

A detailed guide on how to create a heat map indicator for MetaTrader 5 that visualizes the price distribution over time. The article reveals the mathematical basis of time density analysis, where each price level is colored from red (minimum stay time) to blue (maximum stay time).

In the first part, we examined the concept of time gaps and their connection with institutional trading activity. However, detecting these gaps is only half the task. To trade effectively, a trader needs to see the full picture: where the price spends a lot of time, where it spends little time, and how these zones interact with each other.

This is where our indicator comes in – a tool that turns invisible time patterns into a visual heat map. While in the first part we were looking for anomalies (gaps), now we are constructing a complete map of normal price behavior.

The basic idea is simple: represent the entire price range as a set of micro-zones and, for each zone, calculate how long the price was in it. The longer the time, the "hotter" the zone, the more important it is for the market. The result is visualized through a color scheme from cool red to hot blue.


Author: Yevgeniy Koshtenko

 

It reminds me: I used to do this a long time ago,

In my opinion, you have just a little bit incomplete - it is left to reveal the regular structure (or show that it does not exist).

This is not a heat-map, of course - just the extrema of the regular part of a similar temperature map.

 
How does this differ from the well-known "market profile" (other than colouring)?
 

By the way, the map is calculated using the most labour-intensive and error-prone method.

Everything is simpler - for each candle 2 pairs {price,weight} : { price=high, weight=-1; } { price=low,weight=+1;} The collection is sorted by price, the sum with accumulation by weight is the heat map. Then it is quantised as you like.

 
I decided to experiment with ChatGPT to bolt on a couple of "improvements" and create something useful for myself - this is what came out of it. The commented out part is my attempt to detect when the price enters one of the coloured zones and visually show its reaction. But the idea was to catch the shades of these colours: for example, when the zone is slightly brighter orange, according to my observations, the price goes sideways.
//+------------------------------------------------------------------+
//| Heat Map Plus - v3.00 modificada |
//| Mantém buffer/alerts e adiciona ADX, stats e classificação |
//+------------------------------------------------------------------+
#property copyright "Chart Coloring by Time and Volume Distribution"
#property link      "https://www.mql5.com"
#property version   "3.00"
#property indicator_chart_window
#property strict

#include <Math\Stat\Math.mqh>    // MathMean, MathStdDev
#include <Indicators\Trend.mqh>     // ADX para filtrar tendência

// buffers para EA
#property indicator_buffers 3
#property indicator_plots   0
double LevelPriceBuffer[];
double LevelStrengthBuffer[];
double LevelTypeBuffer[];

//--- Input parameters
sinput group "=== Heat Map Settings ==="
input int      AnalysisPeriod   = 500;
input int      MaxHistory       = 10000;
input double   TickSize         = 0;
input bool     UseTickVolume    = true;
input double   VolumeWeight     = 0.5;
input int      SessStartHour    = 8;
input int      SessEndHour      = 17;
input double   MinBarRange      = 10;
input bool     EnableAlerts     = true;

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
sinput group "=== Colors (1%→100%) ==="
input color    Color1Percent    = clrRed;
input color    Color25Percent   = clrOrange;
input color    Color50Percent   = clrYellow;
input color    Color75Percent   = clrAqua;
input color    Color100Percent  = clrBlue;
input int      Transparency     = 70;

//--- globals
struct PriceLevel {
 double price, price_high, price_low;
 long   time_spent;
 long   volume;
 double presence_percent;
 color  level_color;
 string object_name; };
PriceLevel      levels[];
datetime        startTime, endTime, lastUpdate;
int             totalPriceLevels = 0;
long            maxTimeSpent = 0, minTimeSpent = LONG_MAX;
long            maxVolume = 0,    minVolume    = LONG_MAX;
bool            calculated = false;

//--- ADX handle
CiADX   adx;

//+------------------------------------------------------------------+
//| Initialisation|
//+------------------------------------------------------------------+
int OnInit() {
// buffers para EA
 SetIndexBuffer(0, LevelPriceBuffer);
 SetIndexBuffer(1, LevelStrengthBuffer);
 SetIndexBuffer(2, LevelTypeBuffer);
 if(AnalysisPeriod < 50 || MaxHistory < AnalysisPeriod) {
  Print("Parâmetros incorretos: 50 <= AnalysisPeriod <= MaxHistory");
  return(INIT_PARAMETERS_INCORRECT); }
 if(VolumeWeight < 0 || VolumeWeight > 1) {
  Print("VolumeWeight deve estar em [0,1]");
  return(INIT_PARAMETERS_INCORRECT); }
// ADX(14)
 if(!adx.Create(_Symbol, _Period, 14)) {
  Print("Erro ao criar ADX");
  return(INIT_FAILED); }
 ArrayResize(levels, 0);
 lastUpdate = 0;
 calculated = false;
 totalPriceLevels = 0;
 return(INIT_SUCCEEDED); }

//+------------------------------------------------------------------+
//| Calculation|
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &/*open*/[],
                const double &high[],
                const double &low[],
                const double &/*close*/.[],
                const long &tick_volume[],
                const long &volume[],
                const int &/*spread*/.[]) {
 if(rates_total < AnalysisPeriod) return(0);
// só recalcula em novo candle
 if(lastUpdate == time[rates_total - 1]) return(rates_total);
 lastUpdate = time[rates_total - 1];
 calculated = false;
 if(!calculated) {
  // atualiza ADX
  adx.Refresh(1);
  // cálculo principal
  CalculateTimeDistribution(time, high, low, tick_volume, volume, rates_total);
  ColorChart();
  ExportBuffers(rates_total);
  calculated = true; }
 return(rates_total); }

//+------------------------------------------------------------------+
//| Sliding-window + filtros de sessão e volatilidade |
//+------------------------------------------------------------------+
void CalculateTimeDistribution(const datetime &time[],
                               const double &high[],
                               const double &low[],
                               const long &tick_volume[],
                               const long &volume[],
                               int rates_total) {
 ArrayResize(levels, 0);
 int historyStart = MathMax(0, rates_total - MaxHistory);
 startTime = time[historyStart];
 endTime   = time[rates_total - 1];
// price range
 double maxP = high[ArrayMaximum(high, historyStart, MaxHistory)];
 double minP = low [ArrayMinimum(low,  historyStart, MaxHistory)];
 double priceRange = maxP - minP;
 if(priceRange <= 0) return;
// tick size
 double tk = (TickSize > 0 ? TickSize : Point());
 totalPriceLevels = (int)(priceRange / tk);
 totalPriceLevels = MathMin(1000, MathMax(50, totalPriceLevels));
 double realTick = priceRange / totalPriceLevels;
// init levels
 ArrayResize(levels, totalPriceLevels);
 for(int i = 0; i < totalPriceLevels; i++) {
  levels[i].price       = minP + i * realTick;
  levels[i].price_high  = levels[i].price + realTick / 2;
  levels[i].price_low   = levels[i].price - realTick / 2;
  levels[i].time_spent  = 0;
  levels[i].volume      = 0;
  levels[i].object_name = "HeatLevel_" + IntegerToString(i); }
// percorre barras
 for(int bar = historyStart; bar < rates_total; bar++) {
  // filtro de sessão
  int hr = TimeHour(time[bar]);
  if(hr < SessStartHour || hr > SessEndHour) continue;
  // filtro de volatilidade
  double amp = (high[bar] - low[bar]) / Point();
  if(amp < MinBarRange) continue;
  long barVol = UseTickVolume ? tick_volume[bar] : volume[bar];
  // find impacted levels
  int st = MathMax(0, (int)((low[bar]  - minP) / realTick));
  int en = MathMin(totalPriceLevels - 1,
                   (int)((high[bar] - minP) / realTick));
  for(int li = st; li <= en; li++) {
   if(DoesBarTouchLevel(high[bar], low[bar], levels[li])) {
    levels[li].time_spent++;
    levels[li].volume    += barVol; } } }
// acha min/max
 maxTimeSpent = 0; minTimeSpent = LONG_MAX;
 maxVolume    = 0; minVolume    = LONG_MAX;
 for(int i = 0; i < totalPriceLevels; i++) {
  long t = levels[i].time_spent;
  long v = levels[i].volume;
  if(t > maxTimeSpent) maxTimeSpent = t;
  if(t > 0 && t < minTimeSpent) minTimeSpent = t;
  if(v > maxVolume)    maxVolume    = v;
  if(v > 0 && v < minVolume)    minVolume    = v; }
 if(minTimeSpent == LONG_MAX) minTimeSpent = 0;
 if(minVolume   == LONG_MAX) minVolume   = 0;
// percentuais e cores
 CalculatePercentsAndColors(); }

//+------------------------------------------------------------------+
//| Percents & cores|
//+------------------------------------------------------------------+
void CalculatePercentsAndColors() {
 long tr = maxTimeSpent - minTimeSpent;  if(tr <= 0) tr = 1;
 long vr = maxVolume    - minVolume;     if(vr <= 0) vr = 1;
 for(int i = 0; i < totalPriceLevels; i++) {
  double tp = levels[i].time_spent > 0 ?
              ((levels[i].time_spent - minTimeSpent) / (double)tr) * 100.0 : 0;
  double vp = levels[i].volume > 0 ?
              ((levels[i].volume - minVolume) / (double)vr) * 100.0 : 0;
  double comb = (1.0 - VolumeWeight) * tp + VolumeWeight * vp;
  levels[i].presence_percent =
   MathMax(1.0, MathMin(100.0, comb));
  levels[i].level_color =
   GetPercentageColor(levels[i].presence_percent); } }

//+------------------------------------------------------------------+
//| Chart Coloring + Alerts|
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
void ColorChart() {
 ClearPreviousObjects();
 datetime nowTime = TimeCurrent();
 double lastBid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Array de nomes de zona
 string zoneNames[5] = {"", "Reversão.", "Tendência.", "Lateralização.", "Barreira" };
// 1) Armazena ztype e preço médio
 int    types[]; ArrayResize(types, totalPriceLevels);
 double mids[];  ArrayResize(mids,  totalPriceLevels);
 for(int i = 0; i < totalPriceLevels; i++) {
  types[i] = (int)LevelTypeBuffer[ArraySize(LevelTypeBuffer) - 1 - i];
  if(types[i] < 1 || types[i] > 4)
   types[i] = 4;
  mids[i] = (levels[i].price_low + levels[i].price_high) / 2.0; }
// 2) Cria objetos de zona (retângulos colouridos)
 for(int i = 0; i < totalPriceLevels; i++) {
  string nm = levels[i].object_name;
  // retângulo da zona
  if(ObjectCreate(ChartID(), nm, OBJ_RECTANGLE, 0,
                  startTime, levels[i].price_low,
                  nowTime,   levels[i].price_high)) {
   ObjectSetInteger(ChartID(), nm, OBJPROP_COLOR, levels[i].level_color);
   ObjectSetInteger(ChartID(), nm, OBJPROP_BACK, true);
   ObjectSetInteger(ChartID(), nm, OBJPROP_FILL, true);
   ENUM_LINE_STYLE style = (Transparency > 50) ? STYLE_DOT : STYLE_SOLID;
   ObjectSetInteger(ChartID(), nm, OBJPROP_STYLE, style);
   ObjectSetInteger(ChartID(), nm, OBJPROP_WIDTH, 1); }
  // alertas
  if(EnableAlerts) {
   static double lastPrice = 0;
   bool cross = ((lastPrice < levels[i].price && lastBid >= levels[i].price) ||
                 (lastPrice > levels[i].price && lastBid <= levels[i].price));
   if(cross) {
    Alert("HeatMap: preço cruzou nível ",
          DoubleToString(levels[i].price, _Digits),
          " [", zoneNames[types[i]], "]"); }
   lastPrice = lastBid; } }
//// 3) Agrupa por tipo contíguo e desenha uma linha por grupo
// struct Segment {
// int type;
// int start;
// int end; };
// Segment segs[];
// ArrayResize(segs, 0);
// int currentType = types[0];
// int segStart = 0;
// for(int i = 1; i < totalPriceLevels; i++) {
// if(types[i] != currentType) {
// Segment seg;
// seg.type = currentType;
// seg.start = segStart;
// seg.end = i - 1;
// ArrayResize(segs, ArraySize(segs) + 1);
// segs[ArraySize(segs) - 1] = seg;
// currentType = types[i];
// segStart = i; } }
//// último segmento
// Segment lastSeg;
// lastSeg.type = currentType;
// lastSeg.start = segStart;
// lastSeg.end = totalPriceLevels - 1;
// ArrayResize(segs, ArraySize(segs) + 1);
// segs[ArraySize(segs) - 1] = lastSeg;
//// 4) Desenha linha horizontal média por segmento
// for(int j = 0; j < ArraySize(segs); j++) {
// int s = segs[j].start;
// int e = segs[j].end;
// int type = segs[j].type;
// // média dos preços médios
// double avgPrice = 0;
// for(int k = s; k <= e; k++)
// avgPrice += mids[k];
// avgPrice /= (e - s + 1);
//// Criar linha horizontal
// string lineName = StringFormat(zoneNames[types[s]] + "linha", s, e);
// if(ObjectCreate(ChartID(), lineName, OBJ_HLINE, 0, 0, avgPrice)) {
// ObjectSetInteger(ChartID(), lineName, OBJPROP_COLOR, clrWhite);
// ObjectSetInteger(ChartID(), lineName, OBJPROP_WIDTH, 4);
// ObjectSetInteger(ChartID(), lineName, OBJPROP_STYLE, STYLE_SOLID); }
//// Criar texto à direita (último candle visível)
// double textOffset = SymbolInfoDouble(_Symbol, SYMBOL_POINT) * 100; // deslocamento vertical acima da linha
// double textPrice = avgPrice + textOffset;
// datetime timeRight = iTime(_Symbol, _Period, 0); // tempo do candle mais recente
// string textName = StringFormat(zoneNames[types[s]] + "texto", s, e);
// if(ObjectCreate(ChartID(), textName, OBJ_TEXT, 0, timeRight, textPrice)) {
// ObjectSetInteger(ChartID(), textName, OBJPROP_COLOR, clrBlack);
// ObjectSetInteger(ChartID(), textName, OBJPROP_FONTSIZE, 10);
// ObjectSetInteger(ChartID(), textName, OBJPROP_CORNER, CORNER_RIGHT_LOWER); // opcional se quiser em pixel
// ObjectSetString(ChartID(), textName, OBJPROP_TEXT, zoneNames[types[s]]);
// ObjectSetInteger(ChartID(), textName, OBJPROP_ANCHOR, ANCHOR_RIGHT); // ancora à direita do ponto
// } }
}
//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
int CountZLineObjects() {
 int total = ObjectsTotal(ChartID());
 int count = 0;
 for(int i = 0; i < total; i++) {
  string name = ObjectName(ChartID(), i);
  if(StringFind(name, "ZLine_") == 0) // verifica se começa com "ZLine_"
   count++; }
 return count; }
//+------------------------------------------------------------------+
//| Exporta buffers para EA + classificação de zona |
//+------------------------------------------------------------------+
void ExportBuffers(int rates_total) {
// monta array de stats
 double stats[]; ArrayResize(stats, totalPriceLevels);
 for(int i = 0; i < totalPriceLevels; i++)
  stats[i] = levels[i].presence_percent;
 double mean = MathMean(stats);
 double sd   = MathStdDev(stats, mean);
 if(sd <= 0) sd = mean * 0.1;
 double adxValue = adx.Main(0);
 int cnt = MathMin(totalPriceLevels,
                   ArraySize(LevelPriceBuffer));
 for(int i = 0; i < cnt; i++) {
  // tipo de zona
  double p = levels[i].presence_percent;
  int ztype = 4; // BARREIRA por padrão.
  if(p < mean - sd)                                 ztype = 1; // REVERSÃO
  else if(p > mean + sd && adxValue >= 25.0)        ztype = 2; // TENDÊNCIA
  else if(adxValue < 20.0)                          ztype = 3; // LATERALIZAÇÃO
  LevelPriceBuffer   [rates_total - 1 - i] = levels[i].price;
  LevelStrengthBuffer[rates_total - 1 - i] = p;
  LevelTypeBuffer    [rates_total - 1 - i] = ztype; } }

//+------------------------------------------------------------------+
//| Helpers|
//+------------------------------------------------------------------+
bool DoesBarTouchLevel(double high, double low,
                       const PriceLevel &L) {
 return(high >= L.price_low && low <= L.price_high); }

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
void ClearPreviousObjects() {
 int tot = ObjectsTotal(ChartID());
 for(int i = tot - 1; i >= 0; i--) {
  string nm = ObjectName(ChartID(), i);
  if(StringFind(nm, "HeatLevel_") == 0)
   ObjectDelete(ChartID(), nm); } }

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
color GetPercentageColor(double p) {
 if(p <= 1.0)      return Color1Percent;
 else if(p <= 25.) return InterpolateColor(
                            Color1Percent,
                            Color25Percent,
                            (p - 1) / 24);
 else if(p <= 50.) return InterpolateColor(
                            Color25Percent,
                            Color50Percent,
                            (p - 25) / 25);
 else if(p <= 75.) return InterpolateColor(
                            Color50Percent,
                            Color75Percent,
                            (p - 50) / 25);
 else              return InterpolateColor(
                            Color75Percent,
                            Color100Percent,
                            (p - 75) / 25); }

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
color InterpolateColor(color c1, color c2, double f) {
 int r1 = (c1 >> 16) & 0xFF, g1 = (c1 >> 8) & 0xFF,
     b1 = c1 & 0xFF;
 int r2 = (c2 >> 16) & 0xFF, g2 = (c2 >> 8) & 0xFF,
     b2 = c2 & 0xFF;
 int r = int(r1 + (r2 - r1) * f),
     g = int(g1 + (g2 - g1) * f),
     b = int(b1 + (b2 - b1) * f);
 return (r << 16) | (g << 8) | b; }

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
int TimeHour(const datetime time) {
 MqlDateTime dt;
 TimeToStruct(time, dt);
 return dt.hour; }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| DrawAndExport|
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Calcula a média de um array de double |
//+------------------------------------------------------------------+
double MathMean(const double &a[], const int count) {
 if(count <= 0) return 0.0;
 double sum = 0.0;
 for(int i = 0; i < count; i++)
  sum += a[i];
 return sum / count; }
//+------------------------------------------------------------------+
//| Calcula o desvio padrão amostral de um array de double |
//+------------------------------------------------------------------+
double MathStdDev(const double &a[], const int count) {
 if(count < 2) return 0.0;
 double mean = MathMean(a, count);
 double sumSq = 0.0;
 for(int i = 0; i < count; i++)
  sumSq += MathPow(a[i] - mean, 2);
 return MathSqrt(sumSq / (count - 1));  // Desvio padrão amostral.
}
//+------------------------------------------------------------------+
Discover new MetaTrader 5 opportunities with MQL5 community and services
Discover new MetaTrader 5 opportunities with MQL5 community and services
  • 2025.07.15
  • www.mql5.com
MQL5: language of trade strategies built-in the MetaTrader 5 Trading Platform, allows writing your own trading robots, technical indicators, scripts and libraries of functions