preview
Анализ временных разрывов цен в MQL5 (Часть II): Создаем тепловую карту распределения ликвидности во времени

Анализ временных разрывов цен в MQL5 (Часть II): Создаем тепловую карту распределения ликвидности во времени

MetaTrader 5Индикаторы |
501 4
Yevgeniy Koshtenko
Yevgeniy Koshtenko

В первой части статьи мы разобрали концепцию временных разрывов и их связь с институциональной активностью. Однако, обнаружение этих разрывов — лишь половина задачи. Для эффективной торговли трейдеру необходимо видеть полную картину: где цена проводит много времени, где мало, и как эти зоны взаимодействуют между собой.

Именно здесь на помощь приходит наш индикатор — инструмент, который превращает невидимые временные паттерны в наглядную тепловую карту. Если в первой части мы искали аномалии (разрывы), то теперь мы строим полную карту нормального поведения цены.

Основная идея проста: представить весь ценовой диапазон в виде множества микрозон и для каждой зоны подсчитать, сколько времени в ней находилась цена. Чем больше времени — тем "горячее" зона, тем важнее она для рынка. Результат визуализируется через цветовую схему от холодного красного до горячего синего.


Математическая основа: от хаоса к порядку

Любой рынок можно представить как непрерывную борьбу спроса и предложения. В местах, где эта борьба наиболее интенсивна, цена задерживается дольше. Математически это выражается через функцию временной плотности:

T(p) = Σt_i, где цена находится в диапазоне [p-δ, p+δ]

Здесь p — исследуемый ценовой уровень, δ — размер зоны анализа, а t_i — продолжительность каждого периода нахождения цены в этой зоне.

Но сырые данные мало что говорят. Нужна нормализация, которая превратит абсолютные значения времени в относительные проценты. Мы используем формулу:

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

Эта формула гарантирует, что самые "холодные" зоны получат 1% присутствия (красный цвет), а самые "горячие" — 100% (синий цвет). Все остальные зоны распределятся между этими крайностями пропорционально своей значимости.


Архитектура решения: модульность как основа надежности

Создание такого индикатора требует продуманной архитектуры. В основе лежит структура PriceLevel, которая инкапсулирует всю информацию о каждом ценовом уровне:

struct PriceLevel
{
    double      price;             // Центральная цена уровня
    double      price_high;        // Верхняя граница зоны
    double      price_low;         // Нижняя граница зоны
    long        time_spent;        // Накопленное время в барах
    double      presence_percent;  // Процент присутствия
    color       level_color;       // Динамический цвет
    string      object_name;       // Уникальный идентификатор
};

Каждый уровень живет своей жизнью: накапливает время, пересчитывает проценты, меняет цвет. Это не просто структура данных — это живая сущность рынка.

Ключевым нововведением стало использование скользящего окна анализа. Вместо обработки всей доступной истории (что могло бы занять секунды), мы анализируем только последние MaxHistory баров через окно размером AnalysisPeriod. Это дает актуальность результатов при приемлемой производительности.


Алгоритм: математика встречается с реальностью

Процесс начинается с определения ценового диапазона для анализа. Алгоритм автоматически находит максимум и минимум за исследуемый период, что позволяет адаптироваться к волатильности любого инструмента.

Затем этот диапазон разбивается на равные зоны. Количество зон вычисляется динамически: если указан размер тика, используется он; если нет — берется минимальный Point инструмента. При этом система балансирует между детализацией (не менее 50 уровней) и производительностью (не более 1000 уровней).

Самая ресурсоемкая часть — подсчет времени на каждом уровне. Наивный подход потребовал бы проверки каждого бара против каждого уровня (сложность O(n²)). Мы оптимизировали это до O(n×k), где k — среднее количество уровней, затрагиваемых одним баром.

// Оптимизация: находим только релевантные уровни для каждого бара
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++;
    }
}

Функция DoesBarTouchLevel проверяет пересечение диапазона High-Low бара с границами ценового уровня. Логика простая: если максимум бара выше нижней границы уровня и минимум бара ниже верхней границы — есть пересечение.


Цветовая алхимия: превращение чисел в образы

После подсчета времени, начинается самая творческая часть — преобразование сырых данных в цветовую схему. Мы используем пятиступенчатую систему: красный (1%), оранжевый (25%), желтый (50%), голубой (75%), синий (100%).

Между ключевыми точками происходит плавная интерполяция. Например, уровень с 37% присутствия получит цвет между оранжевым и желтым. Интерполяция работает в RGB-пространстве:

color InterpolateColor(color color1, color color2, double factor)
{
    // Разложение на RGB компоненты
    int r1 = (color1 >> 16) & 0xFF;
    int g1 = (color1 >> 8) & 0xFF;
    int b1 = color1 & 0xFF;
    
    // Линейная интерполяция каждого канала
    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;
}

Результат — плавные цветовые переходы, которые создают естественную тепловую карту рынка.


Визуализация: от алгоритма к графику

Каждый ценовой уровень отображается как прямоугольник на графике. Создание тысячи графических объектов — технически сложная задача, требующая оптимизации.

Прямоугольники размещаются от времени начала анализа до текущего момента, покрывая всю исследуемую область. Каждый объект настраивается на работу в фоновом режиме: он не мешает анализу графика, не выделяется при клике, автоматически перерисовывается при изменении масштаба.

Система управления объектами включает механизм очистки предыдущих результатов перед отрисовкой новых. Это предотвращает накопление "мусора" на графике и обеспечивает корректное обновление визуализации.

В режиме реального времени производительность критична. Индикатор использует несколько уровней оптимизации:

  • первый уровень — ленивые вычисления. Пересчет происходит только при появлении нового бара. Система отслеживает время последнего обновления и запускает вычисления только при изменении.
  • второй уровень — оптимизация памяти. Массивы данных выделяются один раз и переиспользуются. Структуры данных спроектированы для минимального потребления памяти: используются long вместо double для счетчиков, избегаются лишние строковые переменные.
  • третий уровень — алгоритмическая оптимизация. Скользящее окно анализа ограничивает объем обрабатываемых данных. Адаптивная сетка уровней предотвращает создание избыточного количества зон.


Интерпретация результатов: что говорят цвета

Красные зоны (1-25% присутствия) указывают на области, которые цена проходит быстро. Это потенциальные зоны временных разрывов из первой части статьи. В красных зонах часто формируются отбои и ложные пробои, поэтому они требуют осторожного подхода.

Оранжевые и желтые зоны (25-75%) представляют области умеренной активности. Здесь цена задерживается периодически, но без явного доминирования. Это переходные зоны, которые могут становиться поддержкой или сопротивлением, в зависимости от рыночного контекста. Именно в них лучше всего торговать по тренду.

Голубые и синие зоны (75-100%) — главные герои нашего анализа. Здесь цена проводит максимальное время, что указывает на высокую торговую активность. Эти уровни обладают сильной магнетической силой: цена регулярно возвращается к ним, используя как опору для движения или барьер для преодоления.

Самая эффективная стратегия — торговля отбоев от синих зон. Когда цена подходит к области максимального присутствия, вероятность разворота значительно выше среднего. Особенно это работает в боковых рынках, где синие зоны четко определяют границы канала.

Пробои через желтые зоны часто сигнализируют о продолжении движения. Если цена легко проходит область среднего присутствия при хорошем объеме, это указывает на отсутствие серьезного сопротивления впереди.

Ну а отбойные стратегии лучше всего реализуются через красные зоны.

Комбинация с объемным анализом многократно усиливает сигналы. Когда синяя зона по времени совпадает с высоким объемом в объемном профиле, получается зона максимальной значимости.


Настройка под разные рынки: универсальность через адаптацию

Форекс с его высокой ликвидностью требует больших значений AnalysisPeriod (300-500 баров) и MaxHistory (5000-8000 баров). Движения здесь более плавные, поэтому нужна большая глубина анализа для выявления значимых зон.

Фондовый рынок хорошо работает с умеренными настройками: AnalysisPeriod 200-300 баров, MaxHistory 3000-5000 баров. Сессионная структура создает естественные паузы, которые хорошо отражаются в тепловой карте.

Размер тика (TickSize) критично важен для корректной работы. Если установить слишком маленькое значение, получится избыточная детализация без практической пользы. Слишком большое значение приведет к потере важных нюансов. Значение 0 (автоматический режим) обычно оптимально — система сама подберет размер на основе характеристик инструмента.

Прозрачность (Transparency) влияет не только на визуальное восприятие, но и на производительность. Высокие значения (70-90%) создают полупрозрачную карту, которая не мешает анализу свечей, но требует больше ресурсов для отрисовки.

Лимит в 1000 уровней введен не случайно. MеtaTrader 5 имеет ограничения на количество графических объектов, а превышение разумных пределов приводит к замедлению интерфейса без существенного улучшения качества анализа.


Интеграция с другими инструментами: синергия методов

Индикатор тепловой карты прекрасно дополняет объемный профиль. Совпадение синих зон времени с пиками объема создает области исключительной важности. Такие зоны часто становятся ключевыми для долгосрочного движения цены.

Комбинация с уровнями Фибоначчи дает интересные результаты. Когда важные фибо-уровни попадают в синие зоны тепловой карты, их значимость многократно возрастает. Это естественно: математические уровни подкрепляются реальным поведением цены.

Индикаторы волатильности (например, полосы Боллинджера) хорошо работают в связке с тепловой картой. Расширение полос в красных зонах часто предшествует сильным движениям, а сужение в синих зонах указывает на накопление энергии для будущего броска.

В следующую версию индикатора планируется включить машинное обучение для автоматической оптимизации параметров под конкретный инструмент. Алгоритм будет анализировать статистику отработки уровней и подстраивать настройки для максимальной эффективности.

Интеграция с системами алертов позволит получать уведомления при приближении цены к ключевым зонам. Это особенно полезно для свинг-трейдеров, которые не могут постоянно следить за графиками.

Экспорт данных во внешние системы откроет возможности для создания торговых роботов на базе тепловой карты. Роботы смогут использовать силу уровней как дополнительный фильтр для входа в позицию.


Философия метода: время как валюта рынка

В основе индикатора лежит глубокая идея: время — это валюта рынка. Участники торгов тратят время так же осознанно, как тратят деньги. Чем больше времени потрачено на определенном уровне, тем больше эмоций, решений и капитала с ним связано.

Эта эмоциональная привязка создает рыночную память. Даже когда цена уходит от значимого уровня, память о нем остается в подсознании участников. При возвращении к этому уровню, просыпаются старые воспоминания: у кого-то — о прибыли, у кого-то — об убытках. Эти воспоминания влияют на новые торговые решения.

Тепловая карта времени делает эту невидимую память видимой. Она превращает психологию рынка в математику, эмоции — в алгоритмы, интуицию — в данные.


Заключение: новый взгляд на старые истины

Индикатор не открывает новые принципы — он делает старые понятнее. Уровни поддержки и сопротивления существовали всегда, но теперь их можно измерить и ранжировать по значимости. Тепловая карта показывает, где рынок проводит время, а значит — где скрывается настоящая сила.

В сочетании с индикатором временных разрывов это даёт целостную картину поведения крупных участников: где они действуют быстро, а где задерживаются. Вместе инструменты позволяют лучше понимать структуру рынка и принимать решения с опорой на логику, а не на интуицию.

В следующей части — как объединить всё это в единую торговую систему.
Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
Maxim Kuznetsov
Maxim Kuznetsov | 3 июл. 2025 в 09:42

напомнило: когда-то очень давно такое делал,

на мой взгляд у вас совсем чуть-чуть недоделано - осталось выявить регулярную структуру (или показать что её нет).

это конечно не heat-map - просто выведены экстремумы регулярной части подобной температурной карты

Stanislav Korotky
Stanislav Korotky | 3 июл. 2025 в 14:27
Чем это отличается от широко известного "профиля рынка" (кроме как цветовой раскраской)?
Maxim Kuznetsov
Maxim Kuznetsov | 3 июл. 2025 в 16:33

кстати и карта посчитана максимально трудоёмким и чреватым ошибками методом.

всё проще делается - для каждой свечи в коллекцию загоняется 2 пары {price,weight}  : { price=high, weight=-1;  } { price=low,weight=+1;} , коллекция сортируется по price, сумма с накоплением по weight это и есть тепловая карта. Дальше квантизуется по как вам нравится

Lipe Ramos
Lipe Ramos | 15 июл. 2025 в 18:44
Я решил поэкспериментировать с 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
}
//+------------------------------------------------------------------+
Применение модели машинного обучения CatBoost в качестве фильтра для трендовых стратегий Применение модели машинного обучения CatBoost в качестве фильтра для трендовых стратегий
CatBoost – это эффективная модель машинного обучения на основе деревьев, которая специализируется на принятии решений на основе статических признаков. Другие модели на основе деревьев, такие как XGBoost и Random Forest, обладают схожими характеристиками в плане надежности, интерпретируемости и способности работать со сложными паттернами. Эти модели имеют широкий спектр применения: от анализа признаков до управления рисками. В данной статье мы пройдемся по процедуре использования обученной модели CatBoost в качестве фильтра для классической трендовой стратегии на основе пересечения скользящих средних.
Возможности Мастера MQL5, которые вам нужно знать (Часть 47): Обучение с подкреплением (алгоритм временных различий) Возможности Мастера MQL5, которые вам нужно знать (Часть 47): Обучение с подкреплением (алгоритм временных различий)
Temporal Difference (TD, временные различия) — еще один алгоритм обучения с подкреплением, который обновляет Q-значения на основе разницы между прогнозируемыми и фактическими вознаграждениями во время обучения агента. Особое внимание уделяется обновлению Q-значений без учета их пар "состояние-действие" (state-action). Как обычно, мы рассмотрим, как этот алгоритм можно применить в советнике, собранном с помощью Мастера.
Нейросети в трейдинге: Вероятностное прогнозирование временных рядов (K2VAE) Нейросети в трейдинге: Вероятностное прогнозирование временных рядов (K2VAE)
Предлагаем ознакомиться с оригинальной реализацией фреймворка K²VAE — гибкой модели, способной линейно аппроксимировать сложную динамику в латентном пространстве. В статье показано, как реализовать ключевые компоненты на языке MQL5, включая параметризованные матрицы и их управление вне стандартных нейросетевых слоёв. Материал будет полезен тем, кто ищет практический подход к созданию интерпретируемых моделей временных рядов.
Индикатор сезонности по часам, дням недели и месяца Индикатор сезонности по часам, дням недели и месяца
Статья объясняет, как разработать инструмент для анализа повторяющихся ценовых закономерностей на финансовых рынках — по дням месяца (1-31), дням недели (понедельник-воскресенье) или часам дня (0-23). Индикатор анализирует исторические данные, вычисляет среднюю доходность для каждого периода и отображает результаты в виде гистограммы с прогнозом. Включает настраиваемые параметры: тип сезонности, количество анализируемых баров, отображение в процентах или абсолютных значениях, цвета графиков.