Русский Español Português
preview
Analyzing Price Time Gaps in MQL5 (Part II): Creating a Heat Map of Liquidity Distribution Over Time

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

MetaTrader 5Indicators |
525 4
Yevgeniy Koshtenko
Yevgeniy Koshtenko

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.


Mathematical foundation: From chaos to order

Any market can be viewed as a continuous struggle between supply and demand. Where this struggle is most intense, price tends to remain longer. Mathematically, this is expressed through the time density function:

T(p) = Σt_i, where the price is in the range [p-δ, p+δ]

Here p is the price level under study, δ is the size of the analysis zone, while t_i is the duration of each period the price is in this zone.

But the raw data tells little. We need normalization which will convert absolute time values into relative percentages. We use the formula:

P(p) = ((T(p) - T_min) / (T_max - T_min)) × 99% + 1%

This formula ensures that the "coldest" zones are assigned a 1% presence value, while the "hottest" ones reach 100%. All other zones will be distributed between these extremes in proportion to their importance.


Solution architecture: Modularity as the basis for reliability

Creating such an indicator requires a well-thought-out architecture. At the core is the PriceLevel structure, which encapsulates all information about each price level:

struct PriceLevel
{
    double      price;             // Central price level
    double      price_high;        // Zone upper boundary
    double      price_low;         // Zone lower boundary
    long        time_spent;        // Accumulated time in bars
    double      presence_percent;  // Presence percentage
    color       level_color;       // Dynamic color
    string      object_name;       // Unique ID
};

Each level has its own life: it accumulates time, recalculates percentages, and changes color. It is not just a data structure - it is the living essence of the market.

The key innovation was the use of a sliding analysis window. Instead of processing the entire available history (which could take seconds), we analyze only the latest MaxHistory bars through a window the size of AnalysisPeriod. This ensures that the results are relevant and the performance is acceptable.


Algorithm: Math meets reality

The process begins with determining the price range for analysis. The algorithm automatically finds the maximum and minimum for the studied period, which allows it to adapt to the volatility of any instrument.

This range is then divided into equal zones. The number of zones is calculated dynamically: if the tick size is specified, it is used; if not, the minimum Point of the instrument is taken. At the same time, the system balances between detail (at least 50 levels) and performance (no more than 1000 levels).

The most resource-intensive part is counting the time at each level. A naive approach would require checking each bar against each level (O(n²) complexity). We optimized this to O(n×k), where k is the average number of levels affected by one bar.

// Optimization: find only relevant levels for each bar
int startLevel = MathMax(0, (int)((lowPrice - minPrice) / realTickSize));
int endLevel = MathMin(totalPriceLevels - 1, (int)((highPrice - minPrice) / realTickSize) + 1);

for(int levelIdx = startLevel; levelIdx <= endLevel; levelIdx++)
{
    if(DoesBarTouchLevel(highPrice, lowPrice, levels[levelIdx]))
    {
        levels[levelIdx].time_spent++;
    }
}

The DoesBarTouchLevel function checks the intersection of the High-Low range of the bar with the boundaries of the price level. The logic is simple: if the maximum of the bar is above the lower boundary of the level and the minimum of the bar is below the upper boundary, there is an overlap.


Color alchemy: Transforming numbers into images

After the time is calculated, the most creative part begins: converting the raw data into a color scheme. We use a five-step system: red (1%), orange (25%), yellow (50%), light-blue (75%), blue (100%).

Smooth interpolation occurs between key points. For example, a level with 37% presence will receive a color between orange and yellow. Interpolation works in RGB space:

color InterpolateColor(color color1, color color2, double factor)
{
    // Decomposition into RGB components
    int r1 = (color1 >> 16) & 0xFF;
    int g1 = (color1 >> 8) & 0xFF;
    int b1 = color1 & 0xFF;
    
    // Linear interpolation of each channel
    int r = (int)(r1 + (r2 - r1) * factor);
    int g = (int)(g1 + (g2 - g1) * factor);
    int b = (int)(b1 + (b2 - b1) * factor);
    
    return (r << 16) | (g << 8) | b;
}

The result is smooth color transitions that create a natural heat map of the market.


Visualization: From algorithm to chart

Each price level is displayed as a rectangle on the chart. Creating thousands of graphical objects is a technically challenging task that requires optimization.

Rectangles are placed from the start time of the analysis to the current moment, covering the entire area of interest. Each object is configured to work in the background: it does not interfere with chart analysis, is not highlighted when clicked, and is automatically redrawn when the zoom level changes.

The object management system includes a mechanism for clearing previous results before rendering new ones. This prevents the accumulation of "garbage" on the chart and ensures correct updating of the visualization.

In real time, performance is critical. The indicator uses several levels of optimization:

  • The first level is lazy calculations. Recalculation occurs only when a new bar appears. The system tracks the time of the last update and starts calculations only when a change occurs.
  • The second level is memory optimization. Data arrays are allocated once and reused. Data structures are designed for minimal memory consumption: longs are used instead of doubles for counters, and unnecessary string variables are avoided.
  • The third level is algorithmic optimization. The sliding analysis window limits the amount of data processed. The adaptive level grid prevents the creation of an excessive number of zones.


Interpreting the results: What the colors mean

Red zones (1-25% presence) indicate areas that price passes through quickly. These are the potential time gap zones described in the previous article. Red zones often experience rebounds and false breakouts, so they require a cautious approach.

Orange and yellow zones (25-75%) represent areas of moderate activity. Here the price lingers periodically, but without obvious dominance. These are transition zones that can become support or resistance, depending on the market context. These are the zones where trend-following trades often work best.

The blue and light blue zones (75-100%) are the main focus of our analysis. This is where the price spends most of its time, indicating high trading activity. These levels have a strong magnetic force: the price regularly returns to them, using them as a support for movement or a barrier to overcome.

The most effective strategy is trading bounces from blue zones. When the price approaches the area of maximum presence, the probability of a reversal is significantly higher than average. This works especially well in sideways markets, where the blue zones clearly define the channel boundaries.

Breakouts through yellow zones often signal continued movement. If the price easily passes through the medium presence area on good volume, it indicates that there is no serious resistance ahead.

Reversal setups are often most effective in red zones.

Combination with volume analysis greatly enhances the signals. When the blue zone coincides in time with high volume in the volume profile, a zone of maximum significance is created.


Customization for different markets: Versatility through adaptation

Forex with its high liquidity requires large AnalysisPeriod (300-500 bars) and MaxHistory (5000-8000 bars) values. The movements here are smoother, so a greater depth of analysis is needed to identify significant areas.

For stocks, moderate settings tend to work well: AnalysisPeriod 200-300 bars, MaxHistory 3000-5000 bars. The session structure creates natural pauses that are reflected well in the heat map.

Tick size (TickSize) is critical for proper operation. Setting this value too low will result in excessive detail without any practical benefit. Too high a value will result in missing important nuances. The value of 0 (automatic mode) is usually optimal - the system will automatically select the size based on the instrument's characteristics.

Transparency affects not only visual perception, but also performance. High values (70-90%) create a semi-transparent map that does not interfere with candlestick analysis, but requires more resources to render.

The 1000 level limit was introduced for a reason. MetaTrader 5 has limitations on the number of graphical objects, and exceeding reasonable limits leads to a slowdown in the interface without significantly improving the quality of analysis.


Integration with other tools: Synergy of methods

The heat map indicator works especially well alongside the volume profile. The coincidence of blue time zones with volume peaks creates areas of exceptional importance. Such zones often become key for long-term price movement.

Combination with Fibonacci levels gives interesting results. When important Fibonacci levels fall into the blue zones of the heat map, their importance increases many times over. This is natural: mathematical levels are supported by real price behavior.

Volatility indicators (such as Bollinger Bands) work well in conjunction with a heat map. Widening of the bands in the red zones often precedes strong moves, while narrowing in the blue zones indicates the accumulation of energy for a future breakout.

The next version of the indicator is planned to include machine learning to automatically optimize parameters for a specific instrument. The algorithm will analyze level performance statistics and adjust settings for maximum efficiency.

Integration with alert systems will allow you to receive notifications when prices approach key zones. This is especially useful for swing traders who cannot constantly monitor charts.

Exporting data to external systems will open up opportunities for creating trading robots based on heat maps. Robots will be able to use the strength of levels as an additional filter for entering a position.


Method philosophy: Time as a market currency

The indicator is based on a profound idea: time is the currency of the market. Traders spend their time as consciously as they spend their money. The more time spent at a given level, the more emotions, decisions and capital are associated with it.

This emotional attachment creates market memory. Even when the price moves away from a significant level, the memory of it remains in the subconscious of the participants. When returning to this level, old memories awaken: for some, about profits, for others, about losses. These memories influence new trading decisions.

The heat map of time makes this invisible memory visible. It turns market psychology into math, emotions into algorithms, and intuition into data.


Conclusion: A new look at old truths

The indicator does not reveal new principles, it makes old ones clearer. Support and resistance levels have always existed, but now they can be measured and ranked by importance. The heat map shows where the market spends time, and therefore where the real strength lies.

Combined with the time gap indicator, this provides a comprehensive picture of the behavior of major participants: where they act quickly and where they linger. Together, these tools allow us to better understand market structure and make decisions based on logic rather than intuition.

In the next part, we will discuss how to combine all of this into a single trading system.

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/18661

Attached files |
Last comments | Go to discussion (4)
Maxim Kuznetsov
Maxim Kuznetsov | 3 Jul 2025 at 09:42

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.

Stanislav Korotky
Stanislav Korotky | 3 Jul 2025 at 14:27
How does this differ from the well-known "market profile" (other than colouring)?
Maxim Kuznetsov
Maxim Kuznetsov | 3 Jul 2025 at 16:33

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.

Lipe Ramos
Lipe Ramos | 15 Jul 2025 at 18:44
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.
}
//+------------------------------------------------------------------+
Market Simulation (Part 24): Getting Started with SQL (VII) Market Simulation (Part 24): Getting Started with SQL (VII)
In the previous article, we completed the necessary introduction to SQL. And, in my opinion, we properly clarified what we wanted to show and explain about SQL. This was done so that anyone who comes to look at the replication/simulation system being built can at least get an idea of what may be happening there. The point is that there is no sense in programming things that SQL handles perfectly.
From Basic to Intermediate: Function Pointers From Basic to Intermediate: Function Pointers
You have probably already heard about pointers when it comes to programming. But did you know that we can use this kind of data here in MQL5? Of course, this must be done in a way that keeps us in control and avoids strange program behavior during execution. Still, because this is a resource with a very specific purpose and aimed at particular kinds of tasks, it is rare to hear anyone discuss what a pointer is and how to use it in MQL5.
From Basic to Intermediate: Objects (II) From Basic to Intermediate: Objects (II)
In today's article, we will look at how to control some object properties in a simple way using code. We will also see how a custom application can place more than one object on the same chart. In addition, we will begin to understand the importance of assigning a short name to any indicator we plan to implement.
Market Simulation (Part 23): Getting Started with SQL (VI) Market Simulation (Part 23): Getting Started with SQL (VI)
In this article, we will see how to visualize a database and, from that, understand how it is structured. This is done by analyzing the database’s internal structure. Although this may seem unnecessary at first, it is fully justified if we really want to become database administrators. After all, some people make a living maintaining and designing databases.