Discussing the article: "Analyzing Price Time Gaps in MQL5 (Part II): Creating a Heat Map of Liquidity Distribution Over Time"
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
- 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
You are missing trading opportunities:
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
Registration
Log in
You agree to website policy and terms of use
If you do not have an account, please register

Check out the new article: Analyzing Price Time Gaps in MQL5 (Part II): Creating a Heat Map of Liquidity Distribution Over 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