English
preview
Создание пользовательских индикаторов в MQL5 (Часть 10): Дополнение футпринт-графика информационным блоком настроений по объёму по каждому бару

Создание пользовательских индикаторов в MQL5 (Часть 10): Дополнение футпринт-графика информационным блоком настроений по объёму по каждому бару

MetaTrader 5Торговые системы |
38 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

Футпринт-график показывает объём на каждом ценовом уровне внутри каждого бара. Без информационного блока настроений для каждого бара приходится вручную агрегировать уровни, чтобы понять, какая сторона доминировала, оценить степень перекоса участия и сравнить настроения по объёму между барами. Это замедляет как визуальный анализ, так и алгоритмическое принятие решений. Эта статья предназначена для MetaQuotes Language 5 MQL5-разработчиков и системных трейдеров, которые хотят расширить существующий индикатор футпринта информационным блоком настроений для каждого бара. Этот блок обновляется при поступлении каждого нового тика, остаётся лёгким при отрисовке за счёт кэширования метрик бара и рисуется со скруглёнными углами и альфа-смешиванием, чтобы детализация ценовых уровней под ним оставалась хорошо различимой.

В нашей предыдущей статье (Часть 9), мы создали индикатор футпринта ордер-флоу в MQL5, который отслеживал потиковый объём на дискретизированных ценовых уровнях, разделял покупательскую и продавательскую активность и отображал её в режимах Bid vs Ask и Delta, а также выводил окрашенные по объёму текстовые метки на холсте Canvas рядом со свечами, построенными с помощью объектов трендовых линий с обновлением в реальном времени. В Части 10 мы добавляем над каждой свечой информационный блок. Он показывает чистую дельту, общий объём и процентное соотношение покупок и продаж с цветовым кодированием интенсивности дельты, использует суперсэмплинг для сглаженного вывода, скруглённые углы с помощью растеризации дуг и четырёхугольников, а также альфа-смешивание, чтобы аккуратно накладывать блок на холст. Мы рассмотрим следующие темы:

  1. Чтение истории бара — что с первого взгляда показывает сводка объёмных настроений по бару
  2. Реализация в MQL5
  3. Бэктестирование
  4. Заключение

К концу статьи у вас будет улучшенный MQL5-индикатор футпринт-графика с информационным блоком настроений для каждого бара. Он кратко показывает дельту, объём и процентное соотношение покупок и продаж над каждой свечой и готов к дальнейшей настройке. Приступим!


Чтение истории бара — что с первого взгляда показывает сводка объёмных настроений по бару

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

В реальной торговле используйте чистую дельту в информационном блоке настроений по объёму как первый фильтр — бар, закрывшийся выше при сильно отрицательной дельте, предупреждает, что продавцы абсорбировали движение вверх, поэтому такой бар хуже подходит для длинной позиции, чем бар с соответствующей положительной дельтой. Следите за последовательными барами, где направление дельты расходится с направлением цены: устойчивая дивергенция дельты часто предшествует разворотам. Используйте значение общего объёма, чтобы отделять бары с уверенным участием от шума: большая дельта на низком общем объёме имеет меньший вес, чем такая же дельта при высоком участии. Процентное соотношение особенно полезно на ключевых уровнях: бар с 80% покупок, отвергающий уровень поддержки, подтверждает сильное поглощение, тогда как почти равное соотношение 50/50 на том же уровне говорит скорее о нерешительности, чем об уверенности. Используйте интенсивность цвета самого информационного блока как визуальную тепловую карту по графику: последовательность всё более насыщенных цветов в одном направлении сигнализирует о нарастающем импульсе, а внезапная смена цвета — о потенциальной смене рыночного режима.

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

PER-BAR VOLUME SENTIMENT INFORMATION BOX FRAMEWORK


Реализация в MQL5

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

//+------------------------------------------------------------------+
//|                                   OrderFlow FootPrint PART 2.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property indicator_chart_window

#property indicator_buffers 0
#property indicator_plots   0

#include <Canvas\Canvas.mqh>

//---

input double               minPriceLevelSpacing    = 1.1;   // Min Spacing Between Price Levels

input group "Info Box (Above Each Candle)"
input bool showInfoBox                  = true; // Show Info Box
input int  infoBoxPaddingWidthPixels    = 2;    // Info Box Padding Width (pixels)
input int  infoBoxPaddingHeightPixels   = 1;    // Info Box Padding Height (pixels)

input group "Info Box Fonts"
input int infoBoxBaseFontSize = 5; // Info Box Font Size (base for all labels)

input group "Info Box Style"
input int backgroundTransparencyPercent = 15;  // Background Transparency (0=opaque, 100=invisible)
input int borderTransparencyPercent     = 20;  // Border Transparency (0=opaque, 100=invisible)
input int infoBoxGapPixels              = 10;  // Gap Above Candle High (pixels)

input group "Info Box Rounded Corners"
input bool enableRoundedCorners    = true; // Enable Rounded Corners
input int  cornerRadiusPixels      = 4;    // Corner Radius (pixels)
input int  borderThicknessPixels   = 1;    // Border Thickness (pixels)
input int  supersamplingFactor     = 4;    // Supersampling Level (1=off, 2=2x, 4=4x)

struct BarFootprintData
  {
   datetime            time;                 // Bar open time
   FootprintPriceLevel priceLevels[];        // Array of price levels for this bar
   double              totalUpVolume;        // Cumulative buy volume for the bar
   double              totalDownVolume;      // Cumulative sell volume for the bar
   double              maxDeltaValue;        // Max absolute delta across levels
   double              maxTotalVolumeValue;  // Max combined volume across levels
   double              maxAskValue;          // Max ask volume across levels
   double              maxBidValue;          // Max bid volume across levels
   color               boxColor;             // Computed info box background color
   color               textColor;            // Computed info box text color
   double              delta;                // Net delta (up minus down) for the bar
   double              upPercentage;         // Buy volume as percentage of total
   double              downPercentage;       // Sell volume as percentage of total
  };

Сначала мы добавляем "minPriceLevelSpacing" в группу настроек, чтобы точнее управлять минимальным вертикальным расстоянием между соседними метками ценовых уровней и заменить прежний фиксированный множитель размера шрифта на настраиваемый коэффициент. Затем вводим четыре новые группы входных параметров для информационного блока. Первая группа добавляет переключатель "showInfoBox", а также "infoBoxPaddingWidthPixels" и "infoBoxPaddingHeightPixels" для управления внутренними отступами между краем блока и его текстовым содержимым. Группа шрифтов добавляет "infoBoxBaseFontSize" как базовый размер, масштабируемый вместе с графиком.

Группа стиля вводит "backgroundTransparencyPercent" и "borderTransparencyPercent" для независимого управления прозрачностью заливки и границы по шкале, где 0 означает полную непрозрачность, а 100 — невидимость, а "infoBoxGapPixels" задаёт вертикальный зазор между нижней границей блока и максимумом свечи (high). Группа скруглённых углов добавляет "enableRoundedCorners" как переключатель, "cornerRadiusPixels" для размера дуги в каждом углу, "borderThicknessPixels" для толщины линии и "supersamplingFactor" для задания множителя разрешения отрисовки, используемого при сглаженном рисовании блока.

В структуре "BarFootprintData" мы сохраняем все существующие поля из предыдущей части и добавляем пять новых полей для поддержки информационного блока. Мы добавляем "boxColor" и "textColor" как предварительно вычисленные значения цветов, чтобы цикл отрисовки мог применять их напрямую без пересчёта на каждом кадре. Мы добавляем "delta" для хранения чистой разницы между суммарным объёмом покупок и продаж по всему бару, а "upPercentage" и "downPercentage" — для хранения округлённых процентов участия покупателей и продавцов. Для наглядности они выделены. Кэширование этих значений в структуре означает, что основные вычисления выполняются при поступлении нового тика в отдельной функции, а не внутри цикла отрисовки, благодаря чему процесс перерисовки остаётся минимально затратным. Расширив структуру, перейдём к вспомогательным функциям, которые дадут больше контроля над рендерингом цветов.

Затемнение цветов и расчёт цветов и процентных значений для каждого бара

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

//+------------------------------------------------------------------+
//| Darken color by a given factor                                   |
//+------------------------------------------------------------------+
color DarkenColor(color baseColor, double darkenFactor = 0.5)
  {
   //--- Scale the red channel by the darkening factor
   uchar red   = uchar((baseColor & 0xFF) * darkenFactor);
   //--- Scale the green channel by the darkening factor
   uchar green = uchar(((baseColor >> 8)  & 0xFF) * darkenFactor);
   //--- Scale the blue channel by the darkening factor
   uchar blue  = uchar(((baseColor >> 16) & 0xFF) * darkenFactor);
   //--- Reassemble and return the darkened color in BGR format
   return (color)((blue << 16) | (green << 8) | red);
  }

//+------------------------------------------------------------------+
//| Calculate bar info box colors and volume percentages             |
//+------------------------------------------------------------------+
void CalculateBarColorsAndPercentages(BarFootprintData &footprint)
  {
   //--- Compute the net delta for the full bar
   footprint.delta = footprint.totalUpVolume - footprint.totalDownVolume;
   //--- Compute the combined volume for percentage calculations
   double totalVolume = footprint.totalUpVolume + footprint.totalDownVolume;

   if(totalVolume == 0)
     {
      //--- Default to neutral colors and zero percentages when no volume exists
      footprint.boxColor      = upColor1;
      footprint.textColor     = upColor1;
      footprint.upPercentage  = 0;
      footprint.downPercentage = 0;
      return;
     }

   //--- Round percentages to whole numbers for display
   footprint.upPercentage   = MathRound(100 * footprint.totalUpVolume   / totalVolume);
   footprint.downPercentage = MathRound(100 * footprint.totalDownVolume / totalVolume);

   if(footprint.delta > 0)
     {
      //--- Derive box color from bullish delta intensity
      double ratio = MathAbs(footprint.delta / totalVolume);
      footprint.boxColor = GetVolumeColor(true, ratio);
     }
   else if(footprint.delta < 0)
     {
      //--- Derive box color from bearish delta intensity
      double ratio = MathAbs(footprint.delta / totalVolume);
      footprint.boxColor = GetVolumeColor(false, ratio);
     }
   else
     {
      //--- Assign neutral color when delta is exactly zero
      footprint.boxColor = upColor1;
     }

   //--- Identify the dominant side percentage for text color derivation
   double winningPercentage = (footprint.delta > 0) ? footprint.upPercentage : footprint.downPercentage;
   //--- Compute how far the dominant side exceeds 50%
   double percentAbove50 = winningPercentage - 50;
   //--- Clamp to zero so values below 50% do not produce negative ratios
   if(percentAbove50 < 0) percentAbove50 = 0;
   //--- Scale the excess to a 0–1 ratio for color intensity
   double textRatio = MathCeil(percentAbove50) / 50.0;
   //--- Assign the text color matching the dominant direction and intensity
   footprint.textColor = GetVolumeColor(footprint.delta > 0, textRatio);
  }

Мы определяем функцию "DarkenColor" для создания более тёмного варианта любого цвета путём независимого масштабирования каждого из трёх цветовых каналов. Она извлекает красный, зелёный и синий компоненты входного цвета с помощью побитовых операций, умножает каждый из них на "darkenFactor", который по умолчанию равен 0.5 и даёт уменьшение яркости вдвое, а затем собирает компоненты обратно в значение цвета в правильном порядке байтов. Эта функция используется при вычислении цвета границы информационного блока: затемнённая версия цвета заливки создаёт естественно выглядящий контур без отдельного входного параметра для цвета границы.

Функция "CalculateBarColorsAndPercentages" является ядром расчёта настроений для каждого бара и выполняется при каждом обновлении тика после обновления объёма. Сначала она вычисляет чистую дельту бара, вычитая общий объём продаж из общего объёма покупок, и сохраняет результат в поле "delta". Затем она вычисляет общий объём и, если он равен нулю, устанавливает для всех полей нейтральные значения и завершает выполнение. Если объём есть, она округляет процентное соотношение покупок и продаж до целых чисел с помощью MathRound и сохраняет их в "upPercentage" и "downPercentage".

Для цвета фона блока функция вычисляет абсолютное отношение дельты к общему объёму и передаёт его в "GetVolumeColor" с флагом направления true для положительной дельты и false для отрицательной. В результате получается цвет, интенсивность которого отражает, насколько одна сторона доминировала относительно общей активности. Нулевая дельта напрямую назначает базовый нейтральный оттенок.

Цвет текста рассчитывается по другой логике. Вместо отношения дельты к общему объёму берётся процент доминирующей стороны, из него вычитается 50, чтобы определить превышение над равным распределением, отрицательный результат ограничивается нулём, а затем превышение делится на 50, чтобы преобразовать его в коэффициент от нуля до единицы с помощью функции MathCeil. Это означает, что бар с идеально равным соотношением 50/50 получает самый слабый цвет текста, а бар с выраженным преобладанием одной стороны — самый сильный. Таким образом, сам текст несёт визуальный сигнал о степени дисбаланса. Когда цвета и проценты кэшированы в структуре, можно строить геометрические утилиты, от которых зависит отрисовка скруглённых углов.

Преобразование цветов в ARGB, понижающая дискретизация холстов высокого разрешения и нормализация углов

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

//+------------------------------------------------------------------+
//| Convert color to ARGB with opacity percent                       |
//+------------------------------------------------------------------+
uint ColorToArgbWithOpacity(color clr, int opacityPercent)
  {
   //--- Extract the individual RGB channels from the color value
   uchar redComponent   = (uchar)((clr >> 0)  & 0xFF);
   uchar greenComponent = (uchar)((clr >> 8)  & 0xFF);
   uchar blueComponent  = (uchar)((clr >> 16) & 0xFF);
   //--- Map the 0–100 opacity percent to a 0–255 alpha value
   uchar alphaComponent = (uchar)((opacityPercent * 255) / 100);
   //--- Assemble and return the ARGB value with the computed alpha
   return ((uint)alphaComponent << 24) | ((uint)redComponent << 16) |
          ((uint)greenComponent << 8)  | (uint)blueComponent;
  }

//+------------------------------------------------------------------+
//| Downsample high-res canvas to target using box filter            |
//+------------------------------------------------------------------+
void DownsampleBicubic(CCanvas &targetCanvas, CCanvas &highResCanvas, int downsampleFactor)
  {
   //--- Get target canvas dimensions for iteration bounds
   int targetWidth  = targetCanvas.Width();
   int targetHeight = targetCanvas.Height();

   //--- Iterate over every pixel in the target (low-res) canvas
   for(int pixelY = 0; pixelY < targetHeight; pixelY++)
     {
      for(int pixelX = 0; pixelX < targetWidth; pixelX++)
        {
         //--- Map the target pixel back to its source region in the high-res canvas
         double sourceX = pixelY * downsampleFactor;
         double sourceY = pixelX * downsampleFactor;

         //--- Accumulate weighted channel sums across the source region
         double sumAlpha = 0, sumRed = 0, sumGreen = 0, sumBlue = 0;
         double weightSum = 0;

         //--- Loop over all source pixels that map to this target pixel
         for(int deltaY = 0; deltaY < downsampleFactor; deltaY++)
           {
            for(int deltaX = 0; deltaX < downsampleFactor; deltaX++)
              {
               int sourcePixelX = (int)(sourceX + deltaX);
               int sourcePixelY = (int)(sourceY + deltaY);

               //--- Skip source pixels that fall outside the high-res canvas bounds
               if(sourcePixelX >= 0 && sourcePixelX < highResCanvas.Width() &&
                  sourcePixelY >= 0 && sourcePixelY < highResCanvas.Height())
                 {
                  //--- Read the source pixel ARGB value
                  uint pixelValue = highResCanvas.PixelGet(sourcePixelX, sourcePixelY);

                  //--- Unpack individual channels from the ARGB pixel
                  uchar alpha = (uchar)((pixelValue >> 24) & 0xFF);
                  uchar red   = (uchar)((pixelValue >> 16) & 0xFF);
                  uchar green = (uchar)((pixelValue >> 8)  & 0xFF);
                  uchar blue  = (uchar)(pixelValue & 0xFF);

                  //--- Use uniform weight of 1.0 for simple box-filter averaging
                  double weight = 1.0;
                  sumAlpha += alpha * weight;
                  sumRed   += red   * weight;
                  sumGreen += green * weight;
                  sumBlue  += blue  * weight;
                  weightSum += weight;
                 }
              }
           }

         if(weightSum > 0)
           {
            //--- Average the accumulated channel values across the sample region
            uchar finalAlpha = (uchar)(sumAlpha / weightSum);
            uchar finalRed   = (uchar)(sumRed   / weightSum);
            uchar finalGreen = (uchar)(sumGreen / weightSum);
            uchar finalBlue  = (uchar)(sumBlue  / weightSum);

            //--- Reassemble the averaged ARGB value and write it to the target canvas
            uint finalColor = ((uint)finalAlpha << 24) | ((uint)finalRed << 16) |
                              ((uint)finalGreen << 8)  | (uint)finalBlue;
            targetCanvas.PixelSet(pixelX, pixelY, finalColor);
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Normalize angle to 0–2π range                                    |
//+------------------------------------------------------------------+
double NormalizeAngle(double angle)
  {
   //--- Define the full-circle constant
   double twoPi = 2.0 * M_PI;
   //--- Wrap the angle into the 0–2π range using modulo
   angle = MathMod(angle, twoPi);
   //--- Shift negative results back into the positive range
   if(angle < 0) angle += twoPi;
   return angle;
  }

//+------------------------------------------------------------------+
//| Check if angle falls between start and end angles                |
//+------------------------------------------------------------------+
bool IsAngleBetween(double angle, double startAngle, double endAngle)
  {
   //--- Normalize all three angles to the 0–2π range
   angle      = NormalizeAngle(angle);
   startAngle = NormalizeAngle(startAngle);
   endAngle   = NormalizeAngle(endAngle);

   //--- Compute the arc span from start to end going clockwise
   double span          = NormalizeAngle(endAngle   - startAngle);
   //--- Compute how far the test angle is from the start going clockwise
   double relativeAngle = NormalizeAngle(angle - startAngle);

   //--- Return true if the test angle falls within the span
   return relativeAngle <= span;
  }

Мы определяем функцию "ColorToArgbWithOpacity", чтобы сформировать полноценное ARGB-значение из цвета и значения непрозрачности в процентах. Она извлекает красный, зелёный и синий каналы цвета с помощью побитовых операций, преобразует процент из диапазона от 0 до 100 в альфа-байт от нуля до 255, умножая на 255 и деля на 100, а затем собирает все четыре канала в одно беззнаковое целое в порядке ARGB. Функция используется при подготовке цветов заливки и границы информационного блока.

Чтобы получить гладкие сглаженные края блока, мы реализуем функцию "DownsampleBicubic", которая уменьшает холст высокого разрешения до экранного разрешения с помощью равномерного box-фильтра. Важно отметить, что, несмотря на название функции, фильтр присваивает одинаковый вес каждому исходному пикселю внутри области покрытия целевого пикселя, поэтому это обычное усреднение бокс-фильтром, а не бикубическая интерполяция. Название сохранено из исходной реализации, но фактическое поведение соответствует стандартному усреднению бокс-фильтром.

Для каждого пикселя целевого холста функция сопоставляет ему соответствующий блок исходных пикселей на холсте высокого разрешения: умножает X-координату целевого пикселя на коэффициент понижения, чтобы получить исходную X-координату, и Y-координату целевого пикселя на тот же коэффициент, чтобы получить исходную Y-координату. Затем она накапливает значения альфа-, красного, зелёного и синего каналов с одинаковыми весами по всем исходным пикселям блока с помощью PixelGet, делит сумму на общий вес и записывает усреднённый результат обратно в целевой холст методом PixelSet. Именно это усреднение создаёт гладкие края: резкие пиксельные переходы на холсте высокого разрешения превращаются в плавные смешивания в уменьшенном изображении, благодаря чему скруглённые углы и штрихи границы выглядят аккуратно при любом масштабе.

Для поддержки отрисовки дуг скруглённых углов мы определяем функцию "NormalizeAngle", которая приводит любое значение угла к диапазону от 0 до 2π. Она применяет MathMod, чтобы поместить угол в пределы окружности, а затем добавляет 2π, если результат отрицательный, чтобы результат всегда был неотрицательным. На этой основе функция "IsAngleBetween" определяет, находится ли заданный угол внутри дуги, описанной начальным и конечным углами. Она нормализует все три значения, вычисляет полный диапазон дуги как нормализованную разницу между концом и началом, таким же образом вычисляет относительное положение проверяемого угла от начала и возвращает true, если относительный угол попадает в диапазон. Это корректно обрабатывает дуги, пересекающие нулевую границу, без специальных условий, что важно для угловых дуг информационного блока, где разные углы соответствуют различным квадрантам окружности. Подготовив эти утилиты, можно построить функции заливки четырёхугольников и отрисовки скруглённых прямоугольников.

Построение конвейера геометрической отрисовки — заливка четырёхугольников, скруглённые прямоугольники, обводка дуг и альфа-смешивание

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

//+------------------------------------------------------------------+
//| Fill convex quadrilateral using scanline rasterization           |
//+------------------------------------------------------------------+
void FillConvexQuadrilateral(CCanvas &canvas, double &verticesX[], double &verticesY[], uint fillColor)
  {
   //--- Find the vertical extents of the quadrilateral
   double minY = verticesY[0], maxY = verticesY[0];
   for(int i = 1; i < 4; i++)
     {
      if(verticesY[i] < minY) minY = verticesY[i];
      if(verticesY[i] > maxY) maxY = verticesY[i];
     }

   //--- Determine the inclusive scanline row range
   int yStart = (int)MathCeil(minY);
   int yEnd   = (int)MathFloor(maxY);

   //--- Process each horizontal scanline within the bounding box
   for(int y = yStart; y <= yEnd; y++)
     {
      //--- Sample the scanline at the pixel centre to avoid boundary artifacts
      double scanlineY        = (double)y + 0.5;
      double xIntersections[8];
      int    intersectionCount = 0;

      //--- Compute edge–scanline intersections for all four edges
      for(int i = 0; i < 4; i++)
        {
         int    nextIndex = (i + 1) % 4;
         double x0 = verticesX[i],       y0 = verticesY[i];
         double x1 = verticesX[nextIndex], y1 = verticesY[nextIndex];

         //--- Determine the edge's vertical span
         double edgeMinY = (y0 < y1) ? y0 : y1;
         double edgeMaxY = (y0 > y1) ? y0 : y1;

         //--- Skip edges that do not cross this scanline
         if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue;
         //--- Skip degenerate horizontal edges
         if(MathAbs(y1 - y0) < 1e-12) continue;

         //--- Linearly interpolate to find the X crossing
         double interpolationFactor = (scanlineY - y0) / (y1 - y0);
         if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue;

         //--- Record the intersection X coordinate
         xIntersections[intersectionCount++] = x0 + interpolationFactor * (x1 - x0);
        }

      //--- Sort intersections left to right using bubble sort
      for(int a = 0; a < intersectionCount - 1; a++)
         for(int b = a + 1; b < intersectionCount; b++)
            if(xIntersections[a] > xIntersections[b])
              {
               double temp          = xIntersections[a];
               xIntersections[a]    = xIntersections[b];
               xIntersections[b]    = temp;
              }

      //--- Fill horizontal spans between each pair of intersections
      for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2)
        {
         int xLeft  = (int)MathCeil(xIntersections[pairIndex]);
         int xRight = (int)MathFloor(xIntersections[pairIndex + 1]);
         //--- Paint every pixel in this horizontal span
         for(int x = xLeft; x <= xRight; x++)
            canvas.PixelSet(x, y, fillColor);
        }
     }
  }

//+------------------------------------------------------------------+
//| Render rounded rectangle fill region                             |
//+------------------------------------------------------------------+
void RenderRoundedRectangleFill(CCanvas &canvas, int positionX, int positionY, int width, int height, int radius, uint fillColor)
  {
   //--- Fill the horizontal center strip spanning the full width minus corners
   canvas.FillRectangle(positionX + radius, positionY,
                        positionX + width - radius, positionY + height, fillColor);
   //--- Fill the left vertical strip between the top and bottom corner arcs
   canvas.FillRectangle(positionX, positionY + radius,
                        positionX + radius, positionY + height - radius, fillColor);
   //--- Fill the right vertical strip between the top and bottom corner arcs
   canvas.FillRectangle(positionX + width - radius, positionY + radius,
                        positionX + width, positionY + height - radius, fillColor);

   //--- Fill the four corner quadrants with the matching circle segment
   FillCircleQuadrant(canvas, positionX + radius,         positionY + radius,          radius, fillColor, 2); // TL
   FillCircleQuadrant(canvas, positionX + width - radius, positionY + radius,          radius, fillColor, 1); // TR
   FillCircleQuadrant(canvas, positionX + radius,         positionY + height - radius, radius, fillColor, 3); // BL
   FillCircleQuadrant(canvas, positionX + width - radius, positionY + height - radius, radius, fillColor, 4); // BR
  }

//+------------------------------------------------------------------+
//| Fill one quadrant of a circle at given center                    |
//+------------------------------------------------------------------+
void FillCircleQuadrant(CCanvas &canvas, int centerX, int centerY, int radius, uint fillColor, int quadrant)
  {
   //--- Cast radius to double for distance comparisons
   double radiusDouble = (double)radius;

   //--- Iterate over the bounding box of the quadrant including a 1-pixel border
   for(int deltaY = -radius - 1; deltaY <= radius + 1; deltaY++)
     {
      for(int deltaX = -radius - 1; deltaX <= radius + 1; deltaX++)
        {
         //--- Determine whether this pixel lies in the requested quadrant
         bool inQuadrant = false;
         if(quadrant == 1 && deltaX >= 0 && deltaY <= 0) inQuadrant = true; // TR
         else if(quadrant == 2 && deltaX <= 0 && deltaY <= 0) inQuadrant = true; // TL
         else if(quadrant == 3 && deltaX <= 0 && deltaY >= 0) inQuadrant = true; // BL
         else if(quadrant == 4 && deltaX >= 0 && deltaY >= 0) inQuadrant = true; // BR

         //--- Skip pixels outside the requested quadrant
         if(!inQuadrant) continue;

         //--- Paint the pixel only if it lies within the circle radius
         double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY);
         if(distance <= radiusDouble)
            canvas.PixelSet(centerX + deltaX, centerY + deltaY, fillColor);
        }
     }
  }

//+------------------------------------------------------------------+
//| Render rounded rectangle border strokes                          |
//+------------------------------------------------------------------+
void RenderRoundedRectangleBorder(CCanvas &canvas, int positionX, int positionY, int width, int height, int radius, int thickness, uint borderColorArgb)
  {
   //--- Draw the four straight edges connecting the corner arcs
   RenderRectangleStraightEdge(canvas, positionX + radius,         positionY,
                               positionX + width - radius,         positionY,             thickness, borderColorArgb); // Top
   RenderRectangleStraightEdge(canvas, positionX + width - radius, positionY + height - 1,
                               positionX + radius,                 positionY + height - 1, thickness, borderColorArgb); // Bottom
   RenderRectangleStraightEdge(canvas, positionX,                  positionY + height - radius,
                               positionX,                          positionY + radius,     thickness, borderColorArgb); // Left
   RenderRectangleStraightEdge(canvas, positionX + width - 1,      positionY + radius,
                               positionX + width - 1,              positionY + height - radius, thickness, borderColorArgb); // Right

   //--- Draw the four corner arcs connecting the straight edges
   RenderRectangleCornerArcPrecise(canvas, positionX + radius,         positionY + radius,
                                   radius, thickness, borderColorArgb, M_PI,       M_PI * 1.5);  // TL
   RenderRectangleCornerArcPrecise(canvas, positionX + width - radius, positionY + radius,
                                   radius, thickness, borderColorArgb, M_PI * 1.5, M_PI * 2.0);  // TR
   RenderRectangleCornerArcPrecise(canvas, positionX + radius,         positionY + height - radius,
                                   radius, thickness, borderColorArgb, M_PI * 0.5, M_PI);         // BL
   RenderRectangleCornerArcPrecise(canvas, positionX + width - radius, positionY + height - radius,
                                   radius, thickness, borderColorArgb, 0.0,        M_PI * 0.5);   // BR
  }

//+------------------------------------------------------------------+
//| Render a thick straight edge segment as a filled quadrilateral   |
//+------------------------------------------------------------------+
void RenderRectangleStraightEdge(CCanvas &canvas, double startX, double startY, double endX, double endY, int thickness, uint borderColor)
  {
   //--- Compute the direction vector of the edge
   double deltaX    = endX - startX;
   double deltaY    = endY - startY;
   double edgeLength = MathSqrt(deltaX * deltaX + deltaY * deltaY);
   //--- Skip degenerate zero-length edges
   if(edgeLength < 1e-6) return;

   //--- Compute the unit perpendicular vector for thickness expansion
   double perpendicularX = -deltaY / edgeLength;
   double perpendicularY =  deltaX / edgeLength;

   //--- Compute the unit direction vector along the edge
   double edgeDirectionX = deltaX / edgeLength;
   double edgeDirectionY = deltaY / edgeLength;

   //--- Use half-thickness for symmetric expansion on both sides
   double halfThickness = (double)thickness / 2.0;

   //--- Extend endpoints slightly to avoid gaps at arc–edge junctions
   double extensionLength  = 1.5;
   double extendedStartX   = startX - edgeDirectionX * extensionLength;
   double extendedStartY   = startY - edgeDirectionY * extensionLength;
   double extendedEndX     = endX   + edgeDirectionX * extensionLength;
   double extendedEndY     = endY   + edgeDirectionY * extensionLength;

   //--- Build the four corners of the thick edge rectangle
   double verticesX[4], verticesY[4];
   verticesX[0] = extendedStartX - perpendicularX * halfThickness;
   verticesY[0] = extendedStartY - perpendicularY * halfThickness;
   verticesX[1] = extendedStartX + perpendicularX * halfThickness;
   verticesY[1] = extendedStartY + perpendicularY * halfThickness;
   verticesX[2] = extendedEndX   + perpendicularX * halfThickness;
   verticesY[2] = extendedEndY   + perpendicularY * halfThickness;
   verticesX[3] = extendedEndX   - perpendicularX * halfThickness;
   verticesY[3] = extendedEndY   - perpendicularY * halfThickness;

   //--- Rasterize the quadrilateral with the border color
   FillConvexQuadrilateral(canvas, verticesX, verticesY, borderColor);
  }

//+------------------------------------------------------------------+
//| Render a precise anti-aliased corner arc segment                 |
//+------------------------------------------------------------------+
void RenderRectangleCornerArcPrecise(CCanvas &canvas, int centerX, int centerY, int radius, int thickness, uint borderColor,
                                     double startAngle, double endAngle)
  {
   //--- Compute the half-thickness offset for the arc band
   int    halfThickness = thickness / 2;
   double outerRadius   = (double)radius + halfThickness;
   double innerRadius   = (double)radius - halfThickness;
   //--- Clamp inner radius to zero for very thin radii
   if(innerRadius < 0) innerRadius = 0;

   //--- Determine the pixel scan range from the outer radius
   int pixelRange = (int)(outerRadius + 2);

   //--- Test each pixel in the bounding box for arc membership
   for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++)
     {
      for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++)
        {
         //--- Skip pixels outside the annular band
         double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY);
         if(distance < innerRadius || distance > outerRadius) continue;

         //--- Compute the pixel's angle from the arc center
         double angle = MathArctan2((double)deltaY, (double)deltaX);

         //--- Paint the pixel only if its angle falls within the arc span
         if(IsAngleBetween(angle, startAngle, endAngle))
            canvas.PixelSet(centerX + deltaX, centerY + deltaY, borderColor);
        }
     }
  }

//+------------------------------------------------------------------+
//| Blend source pixel over existing canvas pixel (alpha composite)  |
//+------------------------------------------------------------------+
void BlendPixelSet(CCanvas &canvas, int x, int y, uint src)
  {
   //--- Reject pixels outside the canvas boundaries
   if(x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return;

   //--- Read the destination pixel currently on the canvas
   uint dst = canvas.PixelGet(x, y);

   //--- Unpack source ARGB channels and normalise to 0–1 range
   double sa = ((src >> 24) & 0xFF) / 255.0;
   double sr = ((src >> 16) & 0xFF) / 255.0;
   double sg = ((src >> 8)  & 0xFF) / 255.0;
   double sb = (src & 0xFF)          / 255.0;

   //--- Unpack destination ARGB channels and normalise to 0–1 range
   double da = ((dst >> 24) & 0xFF) / 255.0;
   double dr = ((dst >> 16) & 0xFF) / 255.0;
   double dg = ((dst >> 8)  & 0xFF) / 255.0;
   double db = (dst & 0xFF)          / 255.0;

   //--- Compute the composited alpha using the Porter–Duff over formula
   double out_a = sa + da * (1 - sa);
   if(out_a == 0)
     {
      //--- Write full transparency when both source and destination are clear
      canvas.PixelSet(x, y, 0);
      return;
     }

   //--- Composite each colour channel weighted by the respective alphas
   double out_r = (sr * sa + dr * da * (1 - sa)) / out_a;
   double out_g = (sg * sa + dg * da * (1 - sa)) / out_a;
   double out_b = (sb * sa + db * da * (1 - sa)) / out_a;

   //--- Convert composited channels back to 0–255 byte range with rounding
   uchar oa  = (uchar)(out_a * 255 + 0.5);
   uchar or_ = (uchar)(out_r * 255 + 0.5);
   uchar og  = (uchar)(out_g * 255 + 0.5);
   uchar ob  = (uchar)(out_b * 255 + 0.5);

   //--- Reassemble and write the final composited pixel
   uint out_col = ((uint)oa << 24) | ((uint)or_ << 16) | ((uint)og << 8) | (uint)ob;
   canvas.PixelSet(x, y, out_col);
  }

Здесь мы определяем функцию "FillConvexQuadrilateral" для растеризации и заливки любого выпуклого четырёхугольника построчным методом. Сначала она находит вертикальные границы четырёхугольника, просматривая Y-координаты всех четырёх вершин и выбирая минимум и максимум, затем проходит по каждой целочисленной строке сканирования в этом диапазоне. Для каждой строки она берёт выборку по центру пикселя, добавляя 0.5 к индексу строки, чтобы избежать артефактов на границах, а затем проверяет все четыре ребра на пересечения с этой строкой.

Каждое ребро проверяется относительно собственного вертикального диапазона, вырожденные горизонтальные рёбра пропускаются, а X-координата пересечения вычисляется линейной интерполяцией по относительному положению строки сканирования между Y-значениями двух конечных точек ребра. Пересечения сортируются слева направо пузырьковой сортировкой, затем соседние пары задают горизонтальные интервалы заливки, а каждый пиксель между ними закрашивается с помощью метода PixelSet. Эта функция является основой для отрисовки толстых прямых рёбер, где сегменты границы выражаются как тонкие прямоугольники и заполняются этим растеризатором.

Чтобы заполнить фон скруглённого прямоугольника информационного блока, мы реализуем функцию "RenderRoundedRectangleFill", которая разлагает форму на пять прямоугольных областей и четыре круговых квадранта. Три вызова FillRectangle покрывают горизонтальную центральную полосу на всю ширину, а также левую и правую вертикальные полосы между центрами угловых дуг. Затем четыре угла заполняются вызовом "FillCircleQuadrant" в каждом центре угла с соответствующим номером квадранта. Функция "FillCircleQuadrant" проходит по ограничивающему прямоугольнику квадранта, определяет, попадает ли каждый пиксель в нужный направленный квадрант с помощью проверки знаков дельта-координат, а затем закрашивает его только в том случае, если евклидово расстояние от центра угла не превышает радиус, используя MathSqrt, что создаёт чистую круговую заливку без зазоров и наложений в переходах от прямоугольника к дуге.

Для обводки границы мы определяем функцию "RenderRoundedRectangleBorder", которая рисует четыре прямых ребра и четыре угловые дуги, обрамляющие залитую форму. Прямые рёбра обрабатывает "RenderRectangleStraightEdge": она вычисляет направление ребра и перпендикулярный единичный вектор, симметрично расширяет ребро на половину заданной толщины с каждой стороны, слегка продлевает конечные точки на 1.5 пикселя, чтобы предотвратить зазоры в местах стыка рёбер с дугами, собирает получившиеся четыре угловые точки в массивы вершин и вызывает "FillConvexQuadrilateral", чтобы закрасить толстое ребро как заполненный прямоугольник.

Угловые дуги обрабатывает функция "RenderRectangleCornerArcPrecise". Она задаёт кольцевую полосу между внутренним и внешним радиусами, рассчитанными из радиуса угла и половины толщины границы, просматривает каждый пиксель в ограничивающем прямоугольнике этой полосы, вычисляет расстояние и угол каждого пикселя с помощью "MathSqrt" и MathArctan2, а затем закрашивает только те пиксели, которые попадают одновременно в кольцевую полосу и в угловой диапазон дуги, определённый "IsAngleBetween". Каждому из четырёх углов назначаются правильные начальный и конечный углы в радианах, чтобы покрыть ровно свой квадрант окружности и получить бесшовные соединения с соседними прямыми рёбрами.

Наконец, мы определяем функцию "BlendPixelSet" для альфа-смешивания исходного пикселя поверх существующего содержимого в заданной позиции Canvas. Сначала она проверяет координаты на выход за границы, затем считывает целевой пиксель с помощью PixelGet и распаковывает все четыре канала исходного и целевого пикселей в нормализованные значения с плавающей точкой. Она применяет формулу Porter-Duff over, вычисляя выходную alpha как альфа-канал источника плюс альфа-канал назначения, умноженная на единицу минус alpha источника, а затем смешивает каждый цветовой канал как взвешенную сумму вкладов источника и назначения, делённую на выходную alpha. Результат снова преобразуется в байтовый диапазон с округлением и записывается методом PixelSet. Такой подход к композитингу позволяет полупрозрачному информационному блоку настроений по объёму аккуратно располагаться поверх меток футпринта и графических объектов свечей без жёстких краёв и без стирания нижележащего содержимого независимо от фоновых пикселей.

Если вам интересно, композитинг Porter–Duff, предложенный Томасом Портером и Томом Даффом, — это метод компьютерной графики для объединения нового изображения (источника, source) с существующим изображением (назначением, destination) на основе их значений альфа-канала, то есть прозрачности. Он задаёт набор операторов — например, распространённый оператор «source over» («источник поверх»), — которые определяют, какую часть каждого пикселя сохранить. Это позволяет реализовывать слои, маски и прозрачность за счёт математического смешивания цветов в зависимости от альфа-канала, то есть степени прозрачности каждого пикселя. Ниже приведён визуальный пример.

PORTER-DUFF BLEND COMPOSITION

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

Упрощение функции RenderFootprint и построение полной перерисовки холста Canvas с информационным блоком настроений по объёму

В этой версии функция "RenderFootprint" упрощена и отвечает только за отрисовку свечей, а вся логика рисования меток ценовых уровней и информационного блока перенесена в "RedrawCanvas" для централизованного управления. Теперь функция перерисовки обрабатывает полный конвейер по каждому бару — метки ценовых уровней, окраску диагонального дисбаланса и новый информационный блок с суперсэмплингом — в одном цикле по видимым барам.

//--- Simplified and most logic moved to redraw canvas function

//+------------------------------------------------------------------+
//| Render footprint candle for a single bar                         |
//+------------------------------------------------------------------+
void RenderFootprint(int footprintIndex, int barIndex, datetime barTime, int ratesTotal)
  {
   //--- Validate the footprint index before accessing the array
   if(footprintIndex < 0 || footprintIndex >= ArraySize(barFootprints)) return;

   //--- Get the total price levels for this footprint
   int size = ArraySize(barFootprints[footprintIndex].priceLevels);
   //--- Skip rendering if no levels have been collected yet
   if(size == 0) return;

   //--- Pre-compute the minimum pixel spacing between adjacent price labels
   double minSpacing = minPriceLevelSpacing * priceLevelFontSize * _Point;

   //--- Draw the candle body and wicks behind the footprint labels
   RenderCandleWithTrendLines(barIndex, barTime);
  }

//+------------------------------------------------------------------+
//| Redraw entire canvas for all visible bars                        |
//+------------------------------------------------------------------+
void RedrawCanvas(int ratesTotal)
  {
   //--- Skip rendering if canvas dimensions are not yet valid
   if(currentChartWidth <= 0 || currentChartHeight <= 0) return;

   //--- Declare buffers for high, low, and time data needed during rendering
   double   highs[], lows[];
   datetime times[];

   //--- Abort if bar data cannot be fully retrieved
   if(CopyHigh(_Symbol, _Period, 0, ratesTotal, highs) != ratesTotal) return;
   if(CopyLow(_Symbol,  _Period, 0, ratesTotal, lows)  != ratesTotal) return;
   if(CopyTime(_Symbol, _Period, 0, ratesTotal, times)  != ratesTotal) return;

   //--- Clear the canvas to fully transparent before redrawing
   uint defaultColor = 0;
   mainCanvas.Erase(defaultColor);

   //--- Pre-compute the minimum label spacing for the current settings
   double minSpacing = minPriceLevelSpacing * priceLevelFontSize * _Point;

   //--- Iterate over every visible bar and render its footprint and info box
   for(int i = 0; i < visibleBarsCount; i++)
     {
      //--- Map the visible slot index back to an absolute bar index
      int barIndex = firstVisibleBarIndex - i;
      if(barIndex < 0 || barIndex >= ratesTotal) continue;

      //--- Convert bar index to the chronological buffer position
      int      bufferIndex = ratesTotal - 1 - barIndex;
      datetime barTime     = times[bufferIndex];

      //--- Look up the stored footprint data for this bar
      int footprintIndex = GetBarFootprintIndex(barTime);
      if(footprintIndex < 0) continue;

      int size = ArraySize(barFootprints[footprintIndex].priceLevels);
      if(size > 0)
        {
         //--- Prepare a display price array mirroring the sorted levels
         double displayPrices[];
         ArrayResize(displayPrices, size);

         //--- Copy raw level prices into the display array
         for(int j = 0; j < size; j++)
           {
            displayPrices[j] = barFootprints[footprintIndex].priceLevels[j].price;
           }

         if(!useStrictPricePositions)
           {
            //--- Spread overlapping price labels when strict positioning is disabled
            for(int j = 1; j < size; j++)
              {
               //--- Calculate vertical distance between adjacent labels
               double priceDiff = displayPrices[j - 1] - displayPrices[j];
               //--- Push the label down if it would overlap the one above
               if(priceDiff < minSpacing && priceDiff >= 0)
                 {
                  displayPrices[j] = displayPrices[j - 1] - minSpacing;
                 }
              }
           }

         //--- Declare text and color arrays for left and right column labels
         string leftTexts[];
         color  leftColors[];
         string rightTexts[];
         color  rightColors[];
         ArrayResize(leftTexts,   size);
         ArrayResize(leftColors,  size);
         ArrayResize(rightTexts,  size);
         ArrayResize(rightColors, size);

         //--- Populate label text and color for each price level
         for(int j = 0; j < size; j++)
           {
            double upVolume   = barFootprints[footprintIndex].priceLevels[j].upVolume;
            double downVolume = barFootprints[footprintIndex].priceLevels[j].downVolume;

            if(displayMode == DELTA)
              {
               //--- Compute signed delta and total volume for delta display mode
               double deltaValue = upVolume - downVolume;
               double total      = upVolume + downVolume;

               //--- Default delta color to the weakest down shade
               color deltaColor = downColor1;
               if(barFootprints[footprintIndex].maxDeltaValue > 0 && total > 0)
                 {
                  //--- Derive delta color from signed delta relative to bar maximum
                  deltaColor = GetVolumeColor(
                                 deltaValue >= 0,
                                 MathAbs(deltaValue) / barFootprints[footprintIndex].maxDeltaValue);
                 }

               //--- Default total volume color to the weakest shade
               color totalColor = volumeColor1;
               if(barFootprints[footprintIndex].maxTotalVolumeValue > 0)
                 {
                  //--- Derive total volume color from ratio to bar maximum
                  totalColor = GetTotalVolumeColor(total / barFootprints[footprintIndex].maxTotalVolumeValue);
                 }

               //--- Format delta with explicit sign and total as a plain integer
               leftTexts[j]   = StringFormat("%+.0f", deltaValue);
               leftColors[j]  = deltaColor;
               rightTexts[j]  = StringFormat("%.0f", total);
               rightColors[j] = totalColor;
              }
            else
              {
               //--- Display raw bid volume on the left and ask volume on the right
               leftTexts[j]   = StringFormat("%.0f", downVolume);
               leftColors[j]  = downColor1;
               rightTexts[j]  = StringFormat("%.0f", upVolume);
               rightColors[j] = upColor1;
              }
           }

         if(displayMode == BID_VS_ASK)
           {
            //--- Apply diagonal imbalance coloring across adjacent levels
            for(int j = 0; j < size - 1; j++)
              {
               //--- Get ask volume at the current level and bid at the level below
               double higherAsk = barFootprints[footprintIndex].priceLevels[j].upVolume;
               double lowerBid  = barFootprints[footprintIndex].priceLevels[j + 1].downVolume;

               //--- Highlight ask if it meets the significance threshold
               if(barFootprints[footprintIndex].maxAskValue > 0 &&
                  higherAsk / barFootprints[footprintIndex].maxAskValue >= 0.3)
                 {
                  //--- Compute ask-to-bid ratio and color accordingly
                  double ratio   = higherAsk / (lowerBid + 0.0001);
                  rightColors[j] = GetDiagonalVolumeColor(true, ratio);
                 }

               //--- Highlight bid if it meets the significance threshold
               if(barFootprints[footprintIndex].maxBidValue > 0 &&
                  lowerBid / barFootprints[footprintIndex].maxBidValue >= 0.3)
                 {
                  //--- Compute bid-to-ask ratio and color accordingly
                  double ratio        = lowerBid / (higherAsk + 0.0001);
                  leftColors[j + 1]   = GetDiagonalVolumeColor(false, ratio);
                 }
              }
           }

         //--- Configure the canvas font before drawing level labels
         mainCanvas.FontSet("Consolas", priceLevelFontSize, FW_NORMAL);

         //--- Compute horizontal layout anchors for this bar
         int centerX  = BarToXCoordinate(barIndex);
         int barWidth = GetBarWidth(currentChartScale);
         int gap      = MathMax(1, barWidth / 4);
         int xLeft    = centerX - gap;
         int xRight   = centerX + gap;

         //--- Draw left and right labels for every price level
         for(int j = 0; j < size; j++)
           {
            double displayPrice = displayPrices[j];
            //--- Convert the display price to a vertical canvas pixel position
            int yPosition = PriceToYCoordinate(displayPrice);

            //--- Draw the left column label right-aligned at the gap boundary
            mainCanvas.TextOut(xLeft,  yPosition, leftTexts[j],
                               ColorToARGB(leftColors[j],  255), TA_RIGHT | TA_VCENTER);
            //--- Draw the right column label left-aligned at the gap boundary
            mainCanvas.TextOut(xRight, yPosition, rightTexts[j],
                               ColorToARGB(rightColors[j], 255), TA_LEFT  | TA_VCENTER);
           }
        }

      if(showInfoBox)
        {
         //--- Retrieve the net delta and total volume for the info box content
         double delta       = barFootprints[footprintIndex].delta;
         double totalVolume = barFootprints[footprintIndex].totalUpVolume +
                              barFootprints[footprintIndex].totalDownVolume;

         //--- Skip info box rendering for bars with no volume
         if(totalVolume == 0) continue;

         //--- Retrieve pre-computed percentages and colors for this bar
         double upPercentage   = barFootprints[footprintIndex].upPercentage;
         double downPercentage = barFootprints[footprintIndex].downPercentage;
         color  boxColor       = barFootprints[footprintIndex].boxColor;
         color  textColor      = barFootprints[footprintIndex].textColor;

         //--- Format the three info box label strings
         string line1 = StringFormat("Δ: %.0f",             delta);
         string line2 = StringFormat("V: %.0f",             totalVolume);
         string line3 = StringFormat("↓%.0f%% ↑%.0f%%", downPercentage, upPercentage);

         //--- Determine horizontal and vertical anchor for the info box
         int    x      = BarToXCoordinate(barIndex);
         double highPrice = highs[bufferIndex];
         int    yHigh  = PriceToYCoordinate(highPrice);

         //--- Compute a scale-aware font size clamped to a readable range
         int fontSize = (int)(infoBoxBaseFontSize + currentChartScale * 1.5);
         fontSize     = MathMax(8, MathMin(18, fontSize));

         //--- Measure the bold delta label dimensions
         mainCanvas.FontSet("Arial Bold", (uint)fontSize, FW_BOLD);
         int textWidth1  = mainCanvas.TextWidth(line1);
         int textHeight1 = mainCanvas.TextHeight(line1);

         //--- Measure the normal volume and percentage label dimensions
         mainCanvas.FontSet("Arial", (uint)fontSize, FW_NORMAL);
         int textWidth2  = mainCanvas.TextWidth(line2);
         int textHeight2 = mainCanvas.TextHeight(line2);
         int textWidth3  = mainCanvas.TextWidth(line3);
         int textHeight3 = mainCanvas.TextHeight(line3);

         //--- Determine the box dimensions from the widest and tallest content
         int maxTextWidth    = MathMax(textWidth1, MathMax(textWidth2, textWidth3));
         int totalTextHeight = textHeight1 + textHeight2 + textHeight3 + 4;
         int rectWidth       = maxTextWidth    + infoBoxPaddingWidthPixels  * 2;
         int rectHeight      = totalTextHeight + infoBoxPaddingHeightPixels * 2;

         //--- Centre the box horizontally above the bar's high
         int xRect = x     - rectWidth / 2;
         int yRect = yHigh - rectHeight - infoBoxGapPixels;

         //--- Compute ARGB fill, border, and text colors with configured transparency
         uint  argbFill   = ColorToArgbWithOpacity(boxColor, backgroundTransparencyPercent);
         color borderColor = DarkenColor(boxColor, 0.7);
         uint  argbBorder = ColorToArgbWithOpacity(borderColor, borderTransparencyPercent);
         uint  argbText   = ColorToARGB(textColor, 255);

         //--- Determine the supersampling factor; disable when rounded corners are off
         int ssFactor      = enableRoundedCorners ? MathMax(1, supersamplingFactor) : 1;
         int scaledWidth   = rectWidth   * ssFactor;
         int scaledHeight  = rectHeight  * ssFactor;
         int scaledRadius  = cornerRadiusPixels      * ssFactor;
         int scaledThickness = borderThicknessPixels * ssFactor;

         //--- Declare temporary canvases for supersampled rendering
         CCanvas tempHighRes, tempLowRes;

         if(ssFactor > 1)
           {
            //--- Create the high-resolution canvas for supersampled drawing
            if(!tempHighRes.Create("temp_hi_" + IntegerToString(i),
                                   scaledWidth, scaledHeight, COLOR_FORMAT_ARGB_NORMALIZE)) continue;
            tempHighRes.Erase(0);
           }

         //--- Create the final low-resolution canvas that maps to screen pixels
         if(!tempLowRes.Create("temp_lo_" + IntegerToString(i),
                               rectWidth, rectHeight, COLOR_FORMAT_ARGB_NORMALIZE)) continue;
         tempLowRes.Erase(0);

         //--- Point the draw target to the appropriate canvas resolution
         CCanvas *drawCanvas   = (ssFactor > 1) ? &tempHighRes : &tempLowRes;
         int      drawWidth    = (ssFactor > 1) ? scaledWidth    : rectWidth;
         int      drawHeight   = (ssFactor > 1) ? scaledHeight   : rectHeight;
         int      drawRadius   = (ssFactor > 1) ? scaledRadius   : cornerRadiusPixels;
         int      drawThickness = (ssFactor > 1) ? scaledThickness : borderThicknessPixels;

         if(enableRoundedCorners)
           {
            //--- Draw the filled rounded rectangle background
            RenderRoundedRectangleFill(*drawCanvas, 0, 0, drawWidth, drawHeight, drawRadius, argbFill);
            //--- Overlay the rounded border stroke if thickness is non-zero
            if(borderThicknessPixels > 0)
               RenderRoundedRectangleBorder(*drawCanvas, 0, 0, drawWidth, drawHeight,
                                            drawRadius, drawThickness, argbBorder);
           }
         else
           {
            //--- Draw a plain filled rectangle background for the sharp-corner style
            drawCanvas.FillRectangle(0, 0, drawWidth - 1, drawHeight - 1, argbFill);
            if(borderThicknessPixels > 0)
              {
               //--- Draw the four border edges as anti-aliased lines
               drawCanvas.LineAA(0,          0,           drawWidth,  0,           argbBorder);
               drawCanvas.LineAA(drawWidth,  0,           drawWidth,  drawHeight,  argbBorder);
               drawCanvas.LineAA(drawWidth,  drawHeight,  0,          drawHeight,  argbBorder);
               drawCanvas.LineAA(0,          drawHeight,  0,          0,           argbBorder);
              }
           }

         if(ssFactor > 1)
           {
            //--- Downsample the high-resolution canvas to the screen-resolution canvas
            DownsampleBicubic(tempLowRes, tempHighRes, ssFactor);
           }

         //--- Set the horizontal text anchor to the centre of the box
         int textX = rectWidth / 2;
         //--- Start the vertical text cursor at the top padding offset
         int textY = infoBoxPaddingHeightPixels;

         //--- Draw the bold delta label centred at the top of the box
         tempLowRes.FontSet("Arial Bold", (uint)fontSize, FW_BOLD);
         tempLowRes.TextOut(textX, textY, line1, argbText, TA_CENTER | TA_TOP);
         textY += textHeight1 + 2;

         //--- Draw the normal volume label below the delta label
         tempLowRes.FontSet("Arial", (uint)fontSize, FW_NORMAL);
         tempLowRes.TextOut(textX, textY, line2, argbText, TA_CENTER | TA_TOP);
         textY += textHeight2 + 2;

         //--- Draw the percentage label at the bottom of the text block
         tempLowRes.TextOut(textX, textY, line3, argbText, TA_CENTER | TA_TOP);

         //--- Alpha-composite the completed box onto the main canvas pixel by pixel
         for(int dy = 0; dy < rectHeight; dy++)
           {
            for(int dx = 0; dx < rectWidth; dx++)
              {
               //--- Read the composited box pixel
               uint col = tempLowRes.PixelGet(dx, dy);
               //--- Blend it over the existing main canvas content
               BlendPixelSet(mainCanvas, xRect + dx, yRect + dy, col);
              }
           }

         //--- Release the high-resolution temporary canvas when supersampling was used
         if(ssFactor > 1) tempHighRes.Destroy();
         //--- Release the low-resolution composite canvas
         tempLowRes.Destroy();
        }
     }

   //--- Push the completed frame to the screen
   mainCanvas.Update();
  }

Здесь мы возвращаем функцию "RenderFootprint" к её основной ответственности: проверке индекса футпринта, подтверждению, что существует хотя бы один ценовой уровень, и вызову "RenderCandleWithTrendLines" для отрисовки тела свечи и фитилей. Логика подготовки и рисования меток, которая раньше находилась здесь, перенесена в "RedrawCanvas", чтобы информационный блок и метки ценовых уровней использовали один и тот же цикл по барам. Это устраняет лишние поиски футпринтов и обеспечивает согласованную последовательность отрисовки.

В "RedrawCanvas" мы сохраняем ту же внешнюю структуру из предыдущей части — получаем массивы high, low и time, очищаем холст Canvas и проходим по видимым барам, — но теперь обрабатываем два основных блока для каждого бара. В первом блоке мы строим массив "displayPrices", при необходимости раздвигаем перекрывающиеся метки с помощью нового множителя "minPriceLevelSpacing", заполняем массивы левого и правого текста и цветов по каждому уровню на основе активного режима отображения, применяем окраску диагонального дисбаланса в режиме Bid vs Ask, задаём шрифт с помощью FontSet, вычисляем горизонтальные опорные позиции и рисуем каждую пару меток с помощью TextOut, как и в предыдущей версии.

Второй блок выполняется, когда "showInfoBox" равен true и бар имеет ненулевой объём. Мы считываем заранее вычисленные поля "delta", "upPercentage", "downPercentage", "boxColor" и "textColor" напрямую из структуры футпринта, затем форматируем три строки меток: дельту со знаком и префиксом в виде греческого символа Δ, общий объём с префиксом V, а также проценты продаж и покупок со стрелками направления. Мы вычисляем размер шрифта с учётом масштаба, добавляя масштабированное кратное "currentChartScale" к "infoBoxBaseFontSize", и ограничиваем результат диапазоном от 8 до 18 пикселей, чтобы метки оставались разборчивыми при любом масштабе.

Затем мы измеряем все три метки с помощью TextWidth и TextHeight с настройками жирного и обычного шрифта, чтобы определить максимальную ширину и высоту содержимого, определяем размеры блока в пикселях, добавляя настроенные отступы с каждой стороны, и размещаем блок горизонтально по центру над максимумом бара с зазором "infoBoxGapPixels". Мы собираем ARGB-значения заливки и границы, вызывая "ColorToArgbWithOpacity" со значениями прозрачности из входных параметров, а цвет границы выводим, передавая цвет заливки блока в "DarkenColor".

Когда "enableRoundedCorners" равен true, мы применяем "supersamplingFactor": создаём временный холст высокого разрешения с размерами блока, умноженными на этот коэффициент, рисуем всю геометрию в этом масштабированном разрешении с помощью "RenderRoundedRectangleFill" и "RenderRoundedRectangleBorder" с пропорционально масштабированными значениями радиуса и толщины, затем уменьшаем результат до экранного разрешения, вызывая "DownsampleBicubic" во второй временный холст низкого разрешения. Когда скруглённые углы отключены, мы полностью пропускаем этап суперсэмплинга и рисуем блок напрямую в экранном разрешении с помощью FillRectangle и LineAA для границ.

Затем мы рисуем три текстовые метки на холсте низкого разрешения с помощью TextOut — жирным начертанием для строки дельты и обычным начертанием для объёма и процентов, центрируя каждую строку по горизонтали и располагая их вертикально с двухпиксельными промежутками. Наконец, мы накладываем готовый блок на основной холст пиксель за пикселем с помощью "BlendPixelSet", освобождаем оба временных холста через Destroy, а когда все бары обработаны, обновляем изображение на экране методом Update. Остаётся обновить обработчик события расчёта, чтобы изменения начали применяться. Вот и всё.

Обновление обработчика события расчёта для инициализации и обновления полей информационного блока

Обработчик события OnCalculate переносит всю логику из предыдущей части с двумя точечными дополнениями: новые поля информационного блока в "BarFootprintData" инициализируются при открытии нового бара, а "CalculateBarColorsAndPercentages" вызывается после каждого обновления объёма, чтобы кэшированные показатели настроений по объёму оставались актуальными перед следующей перерисовкой.

//+------------------------------------------------------------------+
//| Calculate custom indicator values on each tick                   |
//+------------------------------------------------------------------+
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[])
  {
   //--- Require at least two bars for meaningful processing
   if(rates_total < 2) return 0;

   //--- Retrieve the open time of the forming bar
   datetime currentBarTime = time[rates_total - 1];
   //--- Detect a bar boundary by comparing against the last known bar time
   bool isNewBar = (currentBarTime != lastBarTime);

   if(isNewBar)
     {
      //--- Append a fresh footprint record for the newly opened bar
      int newSize = ArraySize(barFootprints);
      ArrayResize(barFootprints, newSize + 1);
      currentBarIndex = newSize;

      //--- Initialize all scalar fields of the new footprint entry
      barFootprints[currentBarIndex].time              = currentBarTime;
      barFootprints[currentBarIndex].totalUpVolume     = 0;
      barFootprints[currentBarIndex].totalDownVolume   = 0;
      barFootprints[currentBarIndex].boxColor          = upColor1;
      barFootprints[currentBarIndex].textColor         = upColor1;
      barFootprints[currentBarIndex].delta             = 0;
      barFootprints[currentBarIndex].upPercentage      = 0;
      barFootprints[currentBarIndex].downPercentage    = 0;
      //--- Clear the price levels array for the new bar
      ArrayResize(barFootprints[currentBarIndex].priceLevels, 0);

      if(newSize > maxBarsToRender * 2)
        {
         //--- Trim oldest entries when the array exceeds the render budget
         int toRemove = newSize - maxBarsToRender;
         for(int i = 0; i < toRemove; i++)
           {
            //--- Build the time string to locate related chart objects for deletion
            string timeString = TimeToString(barFootprints[i].time);
            ObjectsDeleteAll(0, objectPrefix + "Body_"      + timeString);
            ObjectsDeleteAll(0, objectPrefix + "UpperWick_" + timeString);
            ObjectsDeleteAll(0, objectPrefix + "LowerWick_" + timeString);
           }
         //--- Remove the oldest footprint records from the front of the array
         ArrayRemove(barFootprints, 0, toRemove);
         //--- Shift the current bar index to reflect the removed entries
         currentBarIndex -= toRemove;
        }

      //--- Commit the new bar time and seed the close price from the open
      lastBarTime    = currentBarTime;
      lastClosePrice = open[rates_total - 1];
      lastTickVolume = 0;
     }

   //--- Abort if the current bar index is out of bounds
   if(currentBarIndex < 0 || currentBarIndex >= ArraySize(barFootprints)) return rates_total;

   //--- Compute the volume increment since the last tick
   double volumeDiff = (double)(tick_volume[rates_total - 1] - lastTickVolume);
   //--- Advance the stored tick volume to the current tick
   lastTickVolume = tick_volume[rates_total - 1];

   //--- Flag whether data changed this tick, triggering a redraw
   bool dataChanged = false;

   if(volumeDiff > 0)
     {
      //--- Mark data as dirty so the canvas will be refreshed
      dataChanged = true;

      double currentClose = close[rates_total - 1];
      double upDiff       = 0.0;
      double downDiff     = 0.0;

      if(currentClose > lastClosePrice)
        {
         //--- Price moved up — attribute the full increment to the ask side
         upDiff         = volumeDiff;
         lastTradeAtAsk = true;
        }
      else if(currentClose < lastClosePrice)
        {
         //--- Price moved down — attribute the full increment to the bid side
         downDiff       = volumeDiff;
         lastTradeAtAsk = false;
        }
      else
        {
         //--- Price unchanged — carry volume to whichever side traded last
         if(lastTradeAtAsk) upDiff   = volumeDiff;
         else               downDiff = volumeDiff;
        }

      //--- Persist the close price for comparison on the next tick
      lastClosePrice = currentClose;

      //--- Snap the current close to its discrete price level
      double quantizedPrice = QuantizePriceToLevel(currentClose);
      //--- Accumulate the volume split into the matching price level entry
      UpdatePriceLevel(barFootprints[currentBarIndex], quantizedPrice, upDiff, downDiff);

      //--- Update the bar's cumulative up and down volume totals
      barFootprints[currentBarIndex].totalUpVolume   += upDiff;
      barFootprints[currentBarIndex].totalDownVolume += downDiff;

      //--- Recompute normalisation maximums after the volume update
      ComputeMaxValues(barFootprints[currentBarIndex]);
      //--- Re-sort levels so the highest prices render at the top
      SortPriceLevelsDescending(barFootprints[currentBarIndex]);
      //--- Refresh the info box colors and percentage fields
      CalculateBarColorsAndPercentages(barFootprints[currentBarIndex]);
     }

   //--- Render footprints for the most recent bars within the render budget
   int firstBar = MathMax(0, rates_total - maxBarsToRender);
   for(int i = firstBar; i < rates_total; i++)
     {
      datetime barTime       = time[i];
      int      footprintIndex = GetBarFootprintIndex(barTime);
      if(footprintIndex >= 0)
        {
         //--- Convert chronological index to reverse bar index for chart coordinates
         int barIndex = rates_total - 1 - i;
         RenderFootprint(footprintIndex, barIndex, barTime, rates_total);
        }
     }

   //--- Track whether chart geometry changed this tick
   bool hasChartChanged = false;

   //--- Sample the current chart geometry for comparison
   int    newChartWidth      = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int    newChartHeight     = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   int    newChartScale      = (int)ChartGetInteger(0, CHART_SCALE);
   int    newFirstVisibleBar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);
   int    newVisibleBars     = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   double newMinPrice        = ChartGetDouble(0, CHART_PRICE_MIN, 0);
   double newMaxPrice        = ChartGetDouble(0, CHART_PRICE_MAX, 0);

   if(newChartWidth != currentChartWidth || newChartHeight != currentChartHeight)
     {
      //--- Resize the canvas bitmap to match the new window dimensions
      mainCanvas.Resize(newChartWidth, newChartHeight);
      currentChartWidth  = newChartWidth;
      currentChartHeight = newChartHeight;
      hasChartChanged    = true;
     }

   if(newChartScale      != currentChartScale     ||
      newFirstVisibleBar != firstVisibleBarIndex   ||
      newVisibleBars     != visibleBarsCount       ||
      newMinPrice        != minVisiblePrice        ||
      newMaxPrice        != maxVisiblePrice)
     {
      //--- Sync all viewport state variables with the current chart values
      currentChartScale    = newChartScale;
      firstVisibleBarIndex = newFirstVisibleBar;
      visibleBarsCount     = newVisibleBars;
      minVisiblePrice      = newMinPrice;
      maxVisiblePrice      = newMaxPrice;
      hasChartChanged      = true;
     }

   //--- Redraw the canvas whenever geometry, new bars, or volume data changed
   datetime currentTime = TimeCurrent();
   if(hasChartChanged || rates_total > prev_calculated || dataChanged)
     {
      RedrawCanvas(rates_total);
      //--- Record the redraw timestamp for potential throttling use
      lastRedrawTime = currentTime;
     }

   //--- Trigger a chart refresh to display the updated canvas overlay
   ChartRedraw(0);

   return rates_total;
  }

Когда обнаружен новый бар, мы инициализируем пять новых полей, добавленных в расширенную структуру, вместе с уже существующими: задаём "boxColor" и "textColor" как базовый нейтральный оттенок и обнуляем "delta", "upPercentage" и "downPercentage". Благодаря этому информационный блок всегда имеет корректные начальные значения и никогда не показывает устаревшие данные предыдущего бара на первом тике нового. Логика обрезки старых баров, начальная установка времени и цены закрытия, а также проверка границ "currentBarIndex" остаются без изменений по сравнению с предыдущей частью.

В блоке обработки объёма логика направления тика, квантование цены, обновление уровня, накопление суммарных объёмов, вызов "ComputeMaxValues" и вызов "SortPriceLevelsDescending" остаются точно такими же, как раньше. Единственное добавление — вызов "CalculateBarColorsAndPercentages" после сортировки. Он пересчитывает и кэширует дельту на уровне бара, процентное соотношение, цвет фона блока и цвет текста в структуре, чтобы эти значения были готовы для прохода отрисовки информационного блока в "RedrawCanvas" без пересчёта во время рисования.

Мы также добавляем цикл отрисовки футпринта, который проходит по последним барам в пределах бюджета рендеринга, вычисляет индекс бара в обратном порядке из хронологической позиции и вызывает "RenderFootprint" для каждого бара, для которого сохранены данные футпринта. Это обеспечивает актуальность свечных объектов, построенных на трендовых линиях, после каждого тика. Обнаружение изменения геометрии, изменение размера холста Canvas, синхронизация состояния области просмотра, условный вызов "RedrawCanvas" и финальный ChartRedraw остаются без изменений, завершая обработчик событий минимальными добавлениями, которые полностью поддерживают новый информационный блок без изменения существующего потока обработки тиков. Осталось протестировать программу, и этим мы займёмся в следующем разделе.


Бэктестирование

Мы выполнили тестирование, и ниже показана итоговая визуализация, собранная в одном GIF-изображении (Graphics Interchange Format).

BACKTEST GIF

Во время тестирования информационный блок корректно обновлялся при поступлении каждого нового тика: значения дельты, объёма и процентов отражали фактическое накопление объёма по bid и ask на каждом ценовом уровне, цвет блока менялся от менее интенсивных к более интенсивным оттенкам по мере того, как одна сторона постепенно доминировала на последовательных барах, а скруглённые углы и полупрозрачный фон аккуратно отображались над фитилями свечей, не перекрывая нижележащие метки ценовых уровней.


Заключение

В заключение мы расширили индикатор футпринта от инструмента, ориентированного только на ценовые уровни, к комбинированному решению для анализа и уровней, и баров, добавив кэшируемые поля бара — "boxColor", "textColor", "delta", "upPercentage" и "downPercentage" — в структуру "BarFootprintData" и вычисляя их один раз на каждом тике через "CalculateBarColorsAndPercentages", чтобы перерисовка требовала минимальных вычислений. Мы построили полный цикл отрисовки, включающий цветовое кодирование интенсивности дельты, интенсивность текста на основе процентов, геометрию скруглённого прямоугольника с построчной заливкой четырёхугольников и точной обводкой дуг, опциональный этап суперсэмплинга и понижающей дискретизации бокс-фильтром, а также попиксельное альфа-смешивание по Porter–Duff, чтобы информационный блок накладывался на футпринт, не скрывая детализацию ценовых уровней. Вся отрисовка централизована в "RedrawCanvas", а обработчик события "OnCalculate" отвечает только за потиковые обновления и поддержание кэшированных данных в актуальном состоянии. После прочтения статьи вы сможете:

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

Удачной торговли!

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21829

Прикрепленные файлы |
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Ассоциативные правила и теория вероятностей на Форекс: Фильтр сигналов без нейросетей и без магии Ассоциативные правила и теория вероятностей на Форекс: Фильтр сигналов без нейросетей и без магии
Рассматриваем альтернативу нейросетевым фильтрам сигналов: ассоциативные правила и базовую теорию вероятностей в MQL5, без Python и внешних библиотек. Показано, как дискретизировать рыночные условия (RSI, EMA, ATR, импульс, сессия), собрать статистику по support, confidence, lift и edge с сглаживанием Лапласа и применять результат как прозрачный входной фильтр перед сделкой.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Нейросети в трейдинге: От единственного прогноза к пространству рыночных сценариев (кодовая книга сценариев) Нейросети в трейдинге: От единственного прогноза к пространству рыночных сценариев (кодовая книга сценариев)
Статья описывает переход к многосценарному прогнозу на базе ORION: создаётся кодовая книга рыночных прототипов с EMA-памятью и слой, объединяющий генератор, роутер и оценку неопределённости. Такой модуль формирует траектории, их априорные вероятности и допустимый разброс, что позволяет учитывать альтернативные продолжения рынка при проектировании торговых решений.