Обсуждение статьи "Анализ временных разрывов цен в MQL5 (Часть II): Создаем тепловую карту распределения ликвидности во времени"
напомнило: когда-то очень давно такое делал,
на мой взгляд у вас совсем чуть-чуть недоделано - осталось выявить регулярную структуру (или показать что её нет).
это конечно не heat-map - просто выведены экстремумы регулярной части подобной температурной карты
Чем это отличается от широко известного "профиля рынка" (кроме как цветовой раскраской)?
кстати и карта посчитана максимально трудоёмким и чреватым ошибками методом.
всё проще делается - для каждой свечи в коллекцию загоняется 2 пары {price,weight} : { price=high, weight=-1; } { price=low,weight=+1;} , коллекция сортируется по price, сумма с накоплением по weight это и есть тепловая карта. Дальше квантизуется по как вам нравится
Я решил поэкспериментировать с ChatGPT, чтобы прикрутить пару «улучшений» и создать что-то полезное для себя — вот что из этого вышло. Закомментированная часть — это моя попытка определить, когда цена заходит в одну из цветных зон, и наглядно показать её реакцию. Но сама идея была в том, чтобы ловить оттенки этих цветов: например, когда зона чуть ярче оранжевая, по моим наблюдениям цена уходит в боковик.
//+------------------------------------------------------------------+ //| 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; //+------------------------------------------------------------------+ //| Initialization | //+------------------------------------------------------------------+ 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 coloridos) 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
Вы упускаете торговые возможности:
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Регистрация
Вход
Вы принимаете политику сайта и условия использования
Если у вас нет учетной записи, зарегистрируйтесь

Опубликована статья Анализ временных разрывов цен в MQL5 (Часть II): Создаем тепловую карту распределения ликвидности во времени:
Подробное руководство по созданию индикатора тепловой карты для MetaTrader 5, который визуализирует временное распределение цены в виде тепловой карты. Статья раскрывает математическую основу анализа временной плотности, где каждый ценовой уровень окрашивается от красного (минимальное время пребывания) до синего (максимальное время пребывания).
В первой части статьи мы разобрали концепцию временных разрывов и их связь с институциональной активностью. Однако, обнаружение этих разрывов — лишь половина задачи. Для эффективной торговли трейдеру необходимо видеть полную картину: где цена проводит много времени, где мало, и как эти зоны взаимодействуют между собой.
Именно здесь на помощь приходит наш индикатор — инструмент, который превращает невидимые временные паттерны в наглядную тепловую карту. Если в первой части мы искали аномалии (разрывы), то теперь мы строим полную карту нормального поведения цены.
Основная идея проста: представить весь ценовой диапазон в виде множества микрозон и для каждой зоны подсчитать, сколько времени в ней находилась цена. Чем больше времени — тем "горячее" зона, тем важнее она для рынка. Результат визуализируется через цветовую схему от холодного красного до горячего синего.
Автор: Yevgeniy Koshtenko