English
preview
Создание пользовательских индикаторов в MQL5 (Часть 11): Улучшение футпринт-графика с помощью слоёв рыночной структуры и ордер-флоу

Создание пользовательских индикаторов в MQL5 (Часть 11): Улучшение футпринт-графика с помощью слоёв рыночной структуры и ордер-флоу

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

Введение

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

В нашей предыдущей статье (Часть 10) мы улучшили MQL5-индикатор футпринт-графика, добавив информационный блок с оценкой настроений по объёму для каждого бара с цветовым кодированием интенсивности дельты, скруглёнными углами с суперсэмплингом и альфа-композитингом. В Части 11 мы вводим полосы объёмного профиля, подсветку точки контроля и зоны стоимости, выявление многоуровневых дисбалансов, классификацию зон поглощения, маркеры single print, панель кумулятивной дельты объёма и гистограмму дельты. В статье будут рассмотрены следующие темы:

  1. Слои рынка — как структура и ордер-флоу сочетаются внутри каждого бара
  2. Реализация в MQL5
  3. Тестирование
  4. Заключение

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


Слои рынка — как структура и ордер-флоу сочетаются внутри каждого бара

Распределение объёма по ценовым уровням показывает, где участники были наиболее активны. Уровень с наибольшим суммарным объёмом является точкой контроля (Point of Control, POC). Он часто выступает центром активности бара и вероятным уровнем реакции. Зона стоимости (value area) — это ценовой диапазон вокруг точки контроля, который содержит настраиваемую долю объёма бара, обычно семьдесят процентов. Она обозначает область принятия цены — цены за её пределами с большей вероятностью будут отвергнуты. Single prints (одиночные принты) — это уровни, на которых цена задерживалась лишь кратко и с минимальным объёмом; они отражают импульсное прохождение цены через область низкого интереса. Unfinished business (незавершённая аукционная отработка экстремума) отмечает экстремальные уровни, где торговалась только одна сторона, указывая, что цена ушла без завершения аукциона и может вернуться.

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

В реальной торговле используйте точку контроля как ориентир для входов по стратегии возврата к среднему: возврат цены к точке контроля предыдущего бара после отклонения часто даёт область с высокой вероятностью реакции. Следите за перекрытием зон стоимости между последовательными барами, чтобы находить зоны принятия цены, которые участники рынка, вероятно, будут защищать. Используйте многоуровневые ask-дисбалансы как индикатор подпитки направленного движения, открывая лонги, когда цена удерживается выше зоны многоуровневого ask-дисбаланса. Отмечайте бары поглощения на ключевых уровнях как потенциальных кандидатов на разворот, особенно когда кумулятивная дельта объёма расходится с направлением цены. Используйте single prints как зоны ускорения, где цена может быстро пройти при повторном тестировании, а уровни unfinished business — как магниты для цены, к которым рынок склонен возвращаться для завершения аукциона.

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

СХЕМА СЛОЁВ РЫНОЧНОЙ СТРУКТУРЫ И ОРДЕРФЛОУ ДЛЯ ФУТПРИНТА


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

Начнём реализацию с расширения и реорганизации групп входных параметров для поддержки всех новых аналитических слоёв.

Расширение и реорганизация групп входных параметров для новых аналитических слоёв

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

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "Core Settings"
input FootprintDisplayMode displayMode             = DELTA; // Display Mode
input int                  ticksPerPriceLevel      = 5;     // Ticks Per Price Level
input int                  maxBarsToRender         = 50;    // Max Bars to Display
input int                  priceLevelFontSize      = 11;    // Price Level Font Size (base)
input double               minPriceLevelSpacing    = 1.1;   // Min Spacing Between Price Levels
input bool                 useStrictPricePositions = true;  // Strict Price Positions

input group "Volume Profile"
input bool   showVolumeProfileBars    = true;  // Show Volume Profile Bars (behind levels)
input double volBarMaxWidthRatio      = 0.85;  // Max Bar Width as Fraction of Bar Width
input int    volBarOpacityPercent     = 30;    // Volume Bar Opacity (0-100)
input bool   showPOC                  = true;  // Show Point of Control Line
input bool   showValueArea            = true;  // Show Value Area (70% volume zone)
input int    valueAreaPercent         = 70;    // Value Area % (default 70)
input int    valueAreaOpacityPercent  = 12;    // Value Area Opacity

input group "Imbalance Detection"
input bool   showStackedImbalance     = true;  // Show Stacked Imbalance Highlight
input int    stackedImbalanceLevels   = 3;     // Min Consecutive Levels for Stack
input double imbalanceRatioThreshold  = 3.0;   // Imbalance Ratio Threshold (ask/bid or bid/ask)
input int    imbalanceOpacityPercent  = 18;    // Imbalance Row Highlight Opacity
input bool   showImbalanceIcon        = true;  // Show Icon on Stacked Imbalance
input bool   showAbsorptionZone       = true;  // Show Absorption Zone (high vol, low delta)
input double absorptionDeltaRatio     = 0.15;  // Absorption: delta/total < this = absorption
input bool   showSinglePrints         = true;  // Show Single Print Levels (dashed border)
input bool   showUnfinishedBusiness   = true;  // Show Unfinished Business (zero vol at edge)

input group "Cumulative Delta"
input bool   showCVDLine              = true;      // Show Cumulative Volume Delta Line
input int    cvdPanelHeightPixels     = 60;        // CVD Panel Height (pixels, below candles)
input color  cvdPositiveColor         = 0x0077ff;  // CVD Line Color (positive)
input color  cvdNegativeColor         = 0xfe3300;  // CVD Line Color (negative)
input bool   showDeltaHistogram       = true;      // Show Delta Histogram (below candle)
input int    histogramHeightPixels    = 30;        // Delta Histogram Height (pixels)

input group "Colors"
input color upColor1       = 0x8d8d8d;
input color upColor2       = 0x6b8bb6;
input color upColor3       = 0x5289d3;
input color upColor4       = 0x2e7de6;
input color upColor5       = 0x0077ff;
input color downColor1     = 0x8d8d8d;
input color downColor2     = 0xb87b6c;
input color downColor3     = 0xd46d53;
input color downColor4     = 0xec5732;
input color downColor5     = 0xfe3300;
input color volumeColor1   = 0x636363;
input color volumeColor2   = 0x858585;
input color volumeColor3   = 0xa3a3a3;
input color volumeColor4   = 0xc9c9c9;
input color volumeColor5   = 0x000000;
input color candleWickColor = 0x666666;
input color pocLineColor    = 0xEF9F27; // POC Line Color
input color valueAreaColor  = 0x185FA5; // Value Area Fill Color
input color stackedAskColor = 0x0077ff; // Stacked Ask Imbalance Color
input color stackedBidColor = 0xfe3300; // Stacked Bid Imbalance Color
input color absorptionColor = 0x9932CC; // Absorption Zone Color
input color singlePrintColor= 0xFFD700; // Single Print Border Color

input group "Info Box"
input bool showInfoBox                   = true;
input int  infoBoxPaddingWidthPixels     = 4;
input int  infoBoxPaddingHeightPixels    = 2;
input int  infoBoxBaseFontSize           = 6;
input int  backgroundTransparencyPercent = 15;
input int  borderTransparencyPercent     = 20;
input int  infoBoxGapPixels              = 10;
input bool showMiniDeltaBar              = true; // Show buy/sell % bar in info box

input group "Info Box Corners"
input bool enableRoundedCorners  = true;
input int  cornerRadiusPixels    = 6;
input int  borderThicknessPixels = 1;
input int  supersamplingFactor   = 4;

input group "Filters"
input bool   dynamicFontSize      = true; // Dynamic Font Size (scales with zoom)
input double minVolumeToShowLevel = 0.0;  // Min Volume to Show Level (0 = show all)

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

Группа выявления дисбалансов добавляет переключатели и пороги для подсветки строк с многоуровневыми дисбалансами, минимальное число последовательных уровней, необходимое для формирования серии, пороговое соотношение ask/bid для выявления, прозрачность подсветки, направленные треугольные значки, подсветку зон поглощения, порог отношения дельты к суммарному объёму для классификации поглощения, пунктирные границы single print и точечные маркеры unfinished business на экстремальных односторонних уровнях.

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

Расширение структур данных и глобальных переменных для метаданных рыночной структуры

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

//+------------------------------------------------------------------+
//| Structures                                                       |
//+------------------------------------------------------------------+
// Store per-level bid/ask volume and print metadata
struct FootprintPriceLevel
  {
   double price;        // Price of this level
   double upVolume;     // Ask (buy) volume at level
   double downVolume;   // Bid (sell) volume at level
   bool   isSinglePrint; // Flag: single print level
   bool   isUnfinished;  // Flag: unfinished business level
  };

// Store all footprint data aggregated per bar
struct BarFootprintData
  {
   datetime            time;               // Bar open time
   FootprintPriceLevel priceLevels[];      // Array of price levels for bar
   double              totalUpVolume;      // Total ask volume for bar
   double              totalDownVolume;    // Total bid volume for bar
   double              maxDeltaValue;      // Max absolute delta across levels
   double              maxTotalVolumeValue; // Max total volume across levels
   double              maxAskValue;        // Max ask volume across levels
   double              maxBidValue;        // Max bid volume across levels
   color               boxColor;           // Info box background color
   color               textColor;          // Info box text color
   double              delta;              // Net delta (ask - bid) for bar
   double              upPercentage;       // Ask volume as % of total
   double              downPercentage;     // Bid volume as % of total
   int                 pocLevelIndex;      // Index of POC level in priceLevels
   int                 valueAreaLow;       // Lower bound index of value area
   int                 valueAreaHigh;      // Upper bound index of value area
   double              cumulativeDelta;    // Running cumulative delta up to bar
   bool                hasStackedAskImbalance; // Flag: stacked ask imbalance present
   bool                hasStackedBidImbalance; // Flag: stacked bid imbalance present
   int                 stackedAskStart;    // Start index of stacked ask imbalance
   int                 stackedBidStart;    // Start index of stacked bid imbalance
   bool                isAbsorptionBar;    // Flag: bar classified as absorption
  };

double           runningCVD           = 0.0;        // Cumulative delta running total

Мы расширяем структуру "FootprintPriceLevel" двумя булевыми флагами наряду с существующими полями цены и объёма. Флаг "isSinglePrint" отмечает уровни, где суммарный объём настолько мал относительно максимума бара, что цена проходит через них без заметного участия участников рынка. Флаг "isUnfinished" отмечает экстремальные уровни на максимуме или минимуме бара, где торговалась только одна сторона рынка, указывая на unfinished business — экстремум, к которому цена может вернуться для завершения аукционной отработки.

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

Мы также объявляем глобальную переменную "runningCVD" с начальным значением ноль, чтобы поддерживать накопленную сумму кумулятивной дельты по всем обработанным барам. Эта сумма записывается в поле cumulativeDelta каждого бара на каждом тике. После обновления структур и глобальных переменных перейдём к новым вспомогательным функциям, которые вычисляют дополнительные аналитические значения.

Добавление утилиты смешивания цветов

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

//+------------------------------------------------------------------+
//| Blend two colors by interpolation factor t (0=a, 1=b)            |
//+------------------------------------------------------------------+
color BlendColors(color a, color b, double t)
  {
   //--- Unpack channels of color a
   uchar ar = uchar(a & 0xFF), ag = uchar((a >> 8) & 0xFF), ab_ = uchar((a >> 16) & 0xFF);
   //--- Unpack channels of color b
   uchar br = uchar(b & 0xFF), bg = uchar((b >> 8) & 0xFF), bb_ = uchar((b >> 16) & 0xFF);
   //--- Interpolate red channel
   uchar r  = uchar(ar + t * (br - ar));
   //--- Interpolate green channel
   uchar g  = uchar(ag + t * (bg - ag));
   //--- Interpolate blue channel
   uchar bl = uchar(ab_ + t * (bb_ - ab_));
   //--- Repack blended channels into BGR color
   return (color)((bl << 16) | (g << 8) | r);
  }

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

Добавление утилиты рисования прямоугольников и вычисление точки контроля и зоны стоимости

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

//+------------------------------------------------------------------+
//| Draw filled rectangle on canvas with ARGB color                  |
//+------------------------------------------------------------------+
void DrawFilledRect(CCanvas &cv, int x1, int y1, int x2, int y2, uint argb)
  {
   //--- Normalize coordinates so x1 < x2
   if(x1 > x2) { int t = x1; x1 = x2; x2 = t; }
   //--- Normalize coordinates so y1 < y2
   if(y1 > y2) { int t = y1; y1 = y2; y2 = t; }
   //--- Fill normalized rectangle
   cv.FillRectangle(x1, y1, x2, y2, argb);
  }

//+------------------------------------------------------------------+
//| Compute POC and Value Area indices for footprint bar             |
//+------------------------------------------------------------------+
void ComputePOCAndValueArea(BarFootprintData &fp)
  {
   //--- Get level count; early-exit on empty bar
   int sz = ArraySize(fp.priceLevels);
   if(sz == 0)
     {
      fp.pocLevelIndex = -1;
      fp.valueAreaLow  = -1;
      fp.valueAreaHigh = -1;
      return;
     }

   //--- Find level with highest total volume as POC
   int    pocIdx = 0;
   double pocVol = 0;
   for(int i = 0; i < sz; i++)
     {
      double tv = fp.priceLevels[i].upVolume + fp.priceLevels[i].downVolume;
      if(tv > pocVol) { pocVol = tv; pocIdx = i; }
     }
   //--- Assign POC level index
   fp.pocLevelIndex = pocIdx;

   //--- Skip value area expansion when disabled
   if(!showValueArea)
     {
      fp.valueAreaLow  = pocIdx;
      fp.valueAreaHigh = pocIdx;
      return;
     }

   //--- Calculate target volume coverage for value area
   double totalVol   = fp.totalUpVolume + fp.totalDownVolume;
   double targetVol  = totalVol * valueAreaPercent / 100.0;
   double coveredVol = pocVol;

   //--- Initialize value area bounds at POC
   int vaHigh = pocIdx;
   int vaLow  = pocIdx;

   //--- Expand value area upward and downward until target is covered
   while(coveredVol < targetVol)
     {
      //--- Sample volume one level above current upper bound
      double aboveVol = 0;
      if(vaHigh > 0)
         aboveVol = fp.priceLevels[vaHigh-1].upVolume + fp.priceLevels[vaHigh-1].downVolume;

      //--- Sample volume one level below current lower bound
      double belowVol = 0;
      if(vaLow < sz - 1)
         belowVol = fp.priceLevels[vaLow+1].upVolume + fp.priceLevels[vaLow+1].downVolume;

      //--- Exit loop if no volume available on either side
      if(aboveVol == 0 && belowVol == 0) break;

      //--- Expand toward whichever side has more volume
      if(aboveVol >= belowVol && vaHigh > 0)
        { coveredVol += aboveVol; vaHigh--; }
      else if(vaLow < sz - 1)
        { coveredVol += belowVol; vaLow++; }
      else if(vaHigh > 0)
        { coveredVol += aboveVol; vaHigh--; }
      else break;
     }

   //--- Store final value area boundaries
   fp.valueAreaHigh = vaHigh;
   fp.valueAreaLow  = vaLow;
  }

Сначала мы определяем функцию "DrawFilledRect" как тонкую обёртку вокруг "FillRectangle", которая нормализует пары координат перед рисованием, меняя x1 с x2 или y1 с y2, если первое значение больше второго. Это гарантирует, что все вызовы заливки прямоугольников в конвейере отрисовки будут давать корректный результат независимо от направления, в котором были вычислены координаты.

Затем для поиска точки контроля и расширения зоны стоимости мы определяем функцию "ComputePOCAndValueArea". Если в баре нет ценовых уровней, все три индекса устанавливаются в -1, и функция сразу завершает работу. В противном случае мы сканируем каждый уровень и отслеживаем уровень с наибольшим суммарным объёмом, сохраняя его индекс как точку контроля. Если переключатель зоны стоимости отключён, обе границы устанавливаются в индекс точки контроля, и функция завершает работу.

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

Выявление многоуровневых дисбалансов, зон поглощения и single prints

Перед отрисовкой дополнительных слоёв мы запускаем одну функцию для каждого бара, чтобы классифицировать многоуровневые дисбалансы на стороне ask и bid, поглощение, single prints и unfinished business. Результаты сохраняются в структуре бара, поэтому модуль отрисовки читает кэшированные флаги, а не повторяет анализ при каждой перерисовке.

//+------------------------------------------------------------------+
//| Detect stacked imbalances and absorption zones for bar           |
//+------------------------------------------------------------------+
void DetectImbalancesAndAbsorption(BarFootprintData &fp)
  {
   //--- Reset all imbalance and absorption flags
   fp.hasStackedAskImbalance = false;
   fp.hasStackedBidImbalance = false;
   fp.stackedAskStart        = -1;
   fp.stackedBidStart        = -1;
   fp.isAbsorptionBar        = false;

   //--- Skip detection if not enough levels
   int sz = ArraySize(fp.priceLevels);
   if(sz < stackedImbalanceLevels) return;

   //--- Initialize consecutive counters for stacking
   int consecutiveAsk = 0;
   int consecutiveBid = 0;

   for(int i = 0; i < sz - 1; i++)
     {
      //--- Check diagonal ask imbalance: ask at level i vs bid at level i+1
      double ask      = fp.priceLevels[i].upVolume;
      double bidBelow = fp.priceLevels[i+1].downVolume;

      if(bidBelow > 0 && ask / bidBelow >= imbalanceRatioThreshold)
        {
         //--- Increment consecutive ask imbalance streak
         consecutiveAsk++;
         //--- Record first occurrence of stacked ask imbalance
         if(consecutiveAsk >= stackedImbalanceLevels && !fp.hasStackedAskImbalance)
           {
            fp.hasStackedAskImbalance = true;
            fp.stackedAskStart        = i - stackedImbalanceLevels + 1;
           }
        }
      else consecutiveAsk = 0;

      //--- Check diagonal bid imbalance: bid at level i+1 vs ask at level i
      double bid      = fp.priceLevels[i+1].downVolume;
      double askAbove = fp.priceLevels[i].upVolume;

      if(askAbove > 0 && bid / askAbove >= imbalanceRatioThreshold)
        {
         //--- Increment consecutive bid imbalance streak
         consecutiveBid++;
         //--- Record first occurrence of stacked bid imbalance
         if(consecutiveBid >= stackedImbalanceLevels && !fp.hasStackedBidImbalance)
           {
            fp.hasStackedBidImbalance = true;
            fp.stackedBidStart        = i - stackedImbalanceLevels + 1;
           }
        }
      else consecutiveBid = 0;
     }

   //--- Check for absorption: high volume with very low net delta
   double total = fp.totalUpVolume + fp.totalDownVolume;
   if(total > 0)
     {
      double absDelta = MathAbs(fp.delta);
      //--- Mark bar as absorption if delta-to-total ratio is below threshold
      if(absDelta / total <= absorptionDeltaRatio)
         fp.isAbsorptionBar = true;
     }

   //--- Classify single prints and unfinished business at bar extremes
   double barHigh = fp.priceLevels[0].price;
   double barLow  = fp.priceLevels[sz-1].price;
   for(int i = 0; i < sz; i++)
     {
      double tv = fp.priceLevels[i].upVolume + fp.priceLevels[i].downVolume;
      //--- Flag level as single print if volume is very small relative to max
      fp.priceLevels[i].isSinglePrint = (tv > 0 && tv <= fp.maxTotalVolumeValue * 0.05);
      //--- Check if level sits at bar extreme
      bool atExtreme = (MathAbs(fp.priceLevels[i].price - barHigh) < _Point / 2 ||
                        MathAbs(fp.priceLevels[i].price - barLow)  < _Point / 2);
      //--- Flag unfinished business: extreme level with one-sided volume
      fp.priceLevels[i].isUnfinished = atExtreme &&
                                       (fp.priceLevels[i].upVolume   == 0 ||
                                        fp.priceLevels[i].downVolume == 0);
     }
  }

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

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

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

Классификация single print и unfinished business выполняется на финальном проходе по всем уровням. Уровень помечается как single print, если его суммарный объём больше нуля, но не превышает пяти процентов от максимального объёма уровня в баре, что указывает на области минимального интереса, в которых цена находилась лишь кратковременно. Затем мы проверяем, находится ли уровень на максимуме или минимуме бара, используя допуск в половину пункта, и помечаем его как unfinished, если он находится на экстремуме и при этом ask- или bid-объём равен нулю. Это сигнализирует об одностороннем аукционе, который цена оставила незавершённым. Когда все структурные флаги заполнены, определим вспомогательную функцию для рисования пунктирной линии.

Рисование горизонтальных пунктирных линий для точки контроля и маркеров single print

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

//+------------------------------------------------------------------+
//| Draw a horizontal dashed line on canvas                          |
//+------------------------------------------------------------------+
void DrawDashedHLine(CCanvas &cv, int x1, int x2, int y, uint col, int dashLen = 4, int gapLen = 3)
  {
   //--- Skip lines outside vertical canvas bounds
   if(y < 0 || y >= cv.Height()) return;
   //--- Ensure left-to-right ordering
   if(x1 > x2) { int t = x1; x1 = x2; x2 = t; }
   //--- Initialize dash state
   bool drawing = true;
   int  count   = 0;
   //--- Iterate pixels and alternate between dash and gap
   for(int x = x1; x <= x2; x++)
     {
      if(drawing) cv.PixelSet(x, y, col);
      count++;
      //--- Switch from dash to gap after dashLen pixels
      if(drawing  && count >= dashLen) { drawing = false; count = 0; }
      //--- Switch from gap to dash after gapLen pixels
      if(!drawing && count >= gapLen)  { drawing = true;  count = 0; }
     }
  }

Мы определяем функцию "DrawDashedHLine", которая рисует горизонтальную пунктирную линию между двумя x-координатами на заданной координате y. Сначала мы сразу выходим из функции, если y-координата находится за пределами высоты холста, затем нормализуем x-координаты, чтобы обход шёл слева направо. Мы инициализируем флаг состояния рисования значением true и счётчик нулём, после чего проходим каждый пиксель горизонтального отрезка. Когда флаг рисования активен, мы устанавливаем пиксель с помощью PixelSet, увеличиваем счётчик и переключаемся в режим пропуска, как только счётчик достигает заданной длины штриха.

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

Построение полного послойного конвейера перерисовки холста

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

//+------------------------------------------------------------------+
//| Redraw entire main canvas with all footprint visual layers       |
//+------------------------------------------------------------------+
void RedrawCanvas(int ratesTotal)
  {
   //--- Skip if canvas dimensions are not yet valid
   if(currentChartWidth <= 0 || currentChartHeight <= 0) return;

   //--- Allocate buffers for price data copy
   double highs[], lows[];
   datetime times[];
   //--- Copy high prices for all bars
   if(CopyHigh(_Symbol, _Period, 0, ratesTotal, highs)  != ratesTotal) return;
   //--- Copy low prices for all bars
   if(CopyLow(_Symbol,  _Period, 0, ratesTotal, lows)   != ratesTotal) return;
   //--- Copy bar timestamps for all bars
   if(CopyTime(_Symbol, _Period, 0, ratesTotal, times)   != ratesTotal) return;

   //--- Clear canvas to fully transparent before redraw
   mainCanvas.Erase(0);

   //--- Determine bar width in pixels at current scale
   int barPx = GetBarWidth(currentChartScale);
   //--- Compute effective font size, clamping to readable range
   int fSize = priceLevelFontSize;
   if(dynamicFontSize)
     {
      fSize = (int)(barPx / 8.5);
      fSize = MathMax(7, MathMin(16, fSize));
     }

   //--- Convert spacing multiplier to chart price units
   double minSpacing = minPriceLevelSpacing * fSize * _Point;

   //--- Prepare CVD data arrays if panel is enabled
   double cvdValues[];
   int    cvdBarIndices[];
   int    cvdCount = 0;
   if(showCVDLine)
     {
      ArrayResize(cvdValues,     visibleBarsCount);
      ArrayResize(cvdBarIndices, visibleBarsCount);
     }

   //--- Iterate all visible bars from right to left
   for(int i = 0; i < visibleBarsCount; i++)
     {
      //--- Compute absolute bar index from visible offset
      int barIndex = firstVisibleBarIndex - i;
      //--- Skip bars outside valid data range
      if(barIndex < 0 || barIndex >= ratesTotal) continue;

      //--- Map bar index to rates buffer index
      int      bufIdx  = ratesTotal - 1 - barIndex;
      datetime barTime = times[bufIdx];
      //--- Look up footprint data for this bar time
      int fpIdx = GetBarFootprintIndex(barTime);
      if(fpIdx < 0) continue;

      //--- Skip bars with no price levels
      int sz = ArraySize(barFootprints[fpIdx].priceLevels);
      if(sz == 0) continue;

      //--- Compute bar center and side coordinates in pixels
      int cx        = BarToXCoordinate(barIndex);
      int bw        = GetBarWidth(currentChartScale);
      int halfBW    = bw / 2;
      int gap       = MathMax(1, bw / 4);
      int xLeft     = cx - gap;
      int xRight    = cx + gap;
      int xBarLeft  = cx - halfBW;
      int xBarRight = cx + halfBW;

      //--- Build display price array, optionally applying minimum spacing
      double displayPrices[];
      ArrayResize(displayPrices, sz);
      for(int j = 0; j < sz; j++) displayPrices[j] = barFootprints[fpIdx].priceLevels[j].price;
      //--- Apply minimum spacing between levels when strict mode is off
      if(!useStrictPricePositions)
         for(int j = 1; j < sz; j++)
           {
            double diff = displayPrices[j-1] - displayPrices[j];
            if(diff < minSpacing && diff >= 0) displayPrices[j] = displayPrices[j-1] - minSpacing;
           }

      // --- Layer 1: Value Area background fill
      if(showValueArea && barFootprints[fpIdx].valueAreaHigh >= 0 &&
         barFootprints[fpIdx].valueAreaLow  >= 0)
        {
         //--- Get value area bound indices
         int vaHigh = barFootprints[fpIdx].valueAreaHigh;
         int vaLow  = barFootprints[fpIdx].valueAreaLow;
         //--- Convert VA price bounds to pixel Y coordinates
         double priceTop    = barFootprints[fpIdx].priceLevels[vaHigh].price + priceLevelStep;
         double priceBottom = barFootprints[fpIdx].priceLevels[vaLow].price  - priceLevelStep;
         int    yTop        = PriceToYCoordinate(priceTop);
         int    yBottom     = PriceToYCoordinate(priceBottom);
         //--- Draw semi-transparent value area background
         uint vaArgb = ColorToArgbWithOpacity(valueAreaColor, valueAreaOpacityPercent);
         DrawFilledRect(mainCanvas, xBarLeft, yTop, xBarRight, yBottom, vaArgb);
        }

      // --- Layer 2: Volume Profile bars behind price levels
      if(showVolumeProfileBars && barFootprints[fpIdx].maxTotalVolumeValue > 0)
        {
         //--- Compute maximum profile bar width in pixels
         int maxBarW = (int)(bw * volBarMaxWidthRatio);
         for(int j = 0; j < sz; j++)
           {
            //--- Calculate combined level volume and bar-relative width
            double tv    = barFootprints[fpIdx].priceLevels[j].upVolume +
                           barFootprints[fpIdx].priceLevels[j].downVolume;
            double ratio = tv / barFootprints[fpIdx].maxTotalVolumeValue;
            int    bwPx  = (int)(maxBarW * ratio);
            if(bwPx < 1) bwPx = 1;

            //--- Get level Y pixel span
            int yL = PriceToYCoordinate(displayPrices[j] + priceLevelStep * 0.45);
            int yH = PriceToYCoordinate(displayPrices[j] - priceLevelStep * 0.45);
            if(yH <= yL) yH = yL + 2;

            //--- Split bar width between ask and bid proportionally
            double upV  = barFootprints[fpIdx].priceLevels[j].upVolume;
            double downV= barFootprints[fpIdx].priceLevels[j].downVolume;
            int upPx    = (tv > 0) ? (int)(bwPx * upV   / tv) : 0;
            int dnPx    = (tv > 0) ? (int)(bwPx * downV / tv) : 0;

            //--- Draw ask (right) and bid (left) volume bars with opacity
            uint argbAsk = ColorToArgbWithOpacity(upColor4,   volBarOpacityPercent);
            uint argbBid = ColorToArgbWithOpacity(downColor4, volBarOpacityPercent);
            DrawFilledRect(mainCanvas, cx,        yL, cx + upPx, yH, argbAsk);
            DrawFilledRect(mainCanvas, cx - dnPx, yL, cx,        yH, argbBid);
           }
        }

      // --- Layer 3: Stacked imbalance row highlights
      if(showStackedImbalance)
        {
         //--- Prepare ask and bid imbalance highlight colors
         uint argbAsk = ColorToArgbWithOpacity(stackedAskColor, imbalanceOpacityPercent);
         uint argbBid = ColorToArgbWithOpacity(stackedBidColor, imbalanceOpacityPercent);

         //--- Highlight stacked ask imbalance rows if detected
         if(barFootprints[fpIdx].hasStackedAskImbalance)
           {
            int s = barFootprints[fpIdx].stackedAskStart;
            for(int j = s; j < MathMin(sz, s + stackedImbalanceLevels + 2); j++)
              {
               int yT = PriceToYCoordinate(displayPrices[j] + priceLevelStep * 0.5);
               int yB = PriceToYCoordinate(displayPrices[j] - priceLevelStep * 0.5);
               DrawFilledRect(mainCanvas, xBarLeft, yT, xBarRight, yB, argbAsk);
              }
           }
         //--- Highlight stacked bid imbalance rows if detected
         if(barFootprints[fpIdx].hasStackedBidImbalance)
           {
            int s = barFootprints[fpIdx].stackedBidStart;
            for(int j = s; j < MathMin(sz, s + stackedImbalanceLevels + 2); j++)
              {
               int yT = PriceToYCoordinate(displayPrices[j] + priceLevelStep * 0.5);
               int yB = PriceToYCoordinate(displayPrices[j] - priceLevelStep * 0.5);
               DrawFilledRect(mainCanvas, xBarLeft, yT, xBarRight, yB, argbBid);
              }
           }
        }

      // --- Layer 4: Absorption zone highlight with border lines
      if(showAbsorptionZone && barFootprints[fpIdx].isAbsorptionBar)
        {
         //--- Compute full bar vertical extent
         double priceTop    = barFootprints[fpIdx].priceLevels[0].price + priceLevelStep;
         double priceBottom = barFootprints[fpIdx].priceLevels[sz-1].price - priceLevelStep;
         int    yT = PriceToYCoordinate(priceTop);
         int    yB = PriceToYCoordinate(priceBottom);
         //--- Fill absorption zone with low-opacity tint
         uint ac = ColorToArgbWithOpacity(absorptionColor, 8);
         DrawFilledRect(mainCanvas, xBarLeft, yT, xBarRight, yB, ac);
         //--- Draw top and bottom border lines at higher opacity
         uint aBorder = ColorToArgbWithOpacity(absorptionColor, 40);
         mainCanvas.LineAA(xBarLeft, yT, xBarRight, yT, aBorder);
         mainCanvas.LineAA(xBarLeft, yB, xBarRight, yB, aBorder);
        }

      // --- Layer 5: POC dashed horizontal line
      if(showPOC && barFootprints[fpIdx].pocLevelIndex >= 0)
        {
         //--- Get POC level pixel position
         int pocIdx  = barFootprints[fpIdx].pocLevelIndex;
         int yPOC    = PriceToYCoordinate(displayPrices[pocIdx]);
         //--- Draw dashed POC line with high opacity
         uint pocArgb = ColorToArgbWithOpacity(pocLineColor, 90);
         DrawDashedHLine(mainCanvas, xBarLeft, xBarRight, yPOC, pocArgb, 3, 2);
        }

      // --- Layer 6: Build label text and color arrays for all levels
      string leftTexts[], rightTexts[];
      color  leftColors[], rightColors[];
      ArrayResize(leftTexts,  sz);
      ArrayResize(leftColors, sz);
      ArrayResize(rightTexts, sz);
      ArrayResize(rightColors, sz);

      for(int j = 0; j < sz; j++)
        {
         double uv = barFootprints[fpIdx].priceLevels[j].upVolume;
         double dv = barFootprints[fpIdx].priceLevels[j].downVolume;

         //--- Format labels as delta and total volume for delta mode
         if(displayMode == DELTA)
           {
            double dval  = uv - dv;
            double total = uv + dv;
            //--- Compute delta level color from max delta ratio
            color dc = downColor1;
            if(barFootprints[fpIdx].maxDeltaValue > 0 && total > 0)
               dc = GetVolumeColor(dval >= 0, MathAbs(dval) / barFootprints[fpIdx].maxDeltaValue);
            //--- Compute total volume color from max volume ratio
            color tc = volumeColor1;
            if(barFootprints[fpIdx].maxTotalVolumeValue > 0)
               tc = GetTotalVolumeColor(total / barFootprints[fpIdx].maxTotalVolumeValue);
            //--- Format delta with sign and total without sign
            leftTexts[j]   = StringFormat("%+.0f", dval);
            leftColors[j]  = dc;
            rightTexts[j]  = StringFormat("%.0f", total);
            rightColors[j] = tc;
           }
         else
           {
            //--- Format labels as bid (left) and ask (right) for BID_VS_ASK mode
            leftTexts[j]   = StringFormat("%.0f", dv);
            leftColors[j]  = downColor1;
            rightTexts[j]  = StringFormat("%.0f", uv);
            rightColors[j] = upColor1;
           }
        }

      //--- Apply diagonal imbalance coloring in BID_VS_ASK mode
      if(displayMode == BID_VS_ASK)
         for(int j = 0; j < sz - 1; j++)
           {
            double higherAsk = barFootprints[fpIdx].priceLevels[j].upVolume;
            double lowerBid  = barFootprints[fpIdx].priceLevels[j+1].downVolume;
            //--- Color ask side of higher level if significant relative to bar max
            if(barFootprints[fpIdx].maxAskValue > 0 &&
               higherAsk / barFootprints[fpIdx].maxAskValue >= 0.3)
               rightColors[j] = GetDiagonalVolumeColor(true, higherAsk / (lowerBid + 0.0001));
            //--- Color bid side of lower level if significant relative to bar max
            if(barFootprints[fpIdx].maxBidValue > 0 &&
               lowerBid / barFootprints[fpIdx].maxBidValue >= 0.3)
               leftColors[j+1] = GetDiagonalVolumeColor(false, lowerBid / (higherAsk + 0.0001));
           }

      // --- Layer 7: Draw price level text labels on canvas
      mainCanvas.FontSet("Consolas", fSize, FW_NORMAL);
      for(int j = 0; j < sz; j++)
        {
         double tv = barFootprints[fpIdx].priceLevels[j].upVolume +
                     barFootprints[fpIdx].priceLevels[j].downVolume;
         //--- Skip level if below minimum volume threshold
         if(minVolumeToShowLevel > 0 && tv < minVolumeToShowLevel) continue;

         //--- Get Y pixel for this level's price
         int yPos = PriceToYCoordinate(displayPrices[j]);

         //--- Draw dashed border around single print levels
         if(showSinglePrints && barFootprints[fpIdx].priceLevels[j].isSinglePrint)
           {
            int  yT     = PriceToYCoordinate(displayPrices[j] + priceLevelStep * 0.4);
            int  yB     = PriceToYCoordinate(displayPrices[j] - priceLevelStep * 0.4);
            uint spArgb = ColorToArgbWithOpacity(singlePrintColor, 60);
            DrawDashedHLine(mainCanvas, xBarLeft, xBarRight, yT, spArgb, 3, 2);
            DrawDashedHLine(mainCanvas, xBarLeft, xBarRight, yB, spArgb, 3, 2);
           }

         //--- Draw dotted line for unfinished business levels at bar extremes
         if(showUnfinishedBusiness && barFootprints[fpIdx].priceLevels[j].isUnfinished)
           {
            uint ubArgb = ColorToArgbWithOpacity(singlePrintColor, 80);
            int  yU     = PriceToYCoordinate(displayPrices[j]);
            //--- Place dots at every fourth pixel across bar width
            for(int px = xBarLeft; px <= xBarRight; px += 4)
               mainCanvas.PixelSet(px, yU, ubArgb);
           }

         //--- Render left label (delta or bid) right-aligned at gap position
         mainCanvas.TextOut(xLeft,  yPos, leftTexts[j],  ColorToARGB(leftColors[j],  255), TA_RIGHT | TA_VCENTER);
         //--- Render right label (total or ask) left-aligned at gap position
         mainCanvas.TextOut(xRight, yPos, rightTexts[j], ColorToARGB(rightColors[j], 255), TA_LEFT  | TA_VCENTER);
        }

      // --- Layer 8: Stacked imbalance direction icon overlay
      if(showImbalanceIcon)
        {
         //--- Draw upward triangle icon at ask imbalance start level
         if(barFootprints[fpIdx].hasStackedAskImbalance && barFootprints[fpIdx].stackedAskStart >= 0)
           {
            int s  = barFootprints[fpIdx].stackedAskStart;
            int yI = PriceToYCoordinate(displayPrices[s]);
            mainCanvas.FontSet("Arial", 9, FW_BOLD);
            mainCanvas.TextOut(xBarRight + 2, yI, "▲", ColorToARGB(stackedAskColor, 220), TA_LEFT | TA_VCENTER);
           }
         //--- Draw downward triangle icon at bid imbalance start level
         if(barFootprints[fpIdx].hasStackedBidImbalance && barFootprints[fpIdx].stackedBidStart >= 0)
           {
            int s  = barFootprints[fpIdx].stackedBidStart;
            int yI = PriceToYCoordinate(displayPrices[s]);
            mainCanvas.FontSet("Arial", 9, FW_BOLD);
            mainCanvas.TextOut(xBarRight + 2, yI, "▼", ColorToARGB(stackedBidColor, 220), TA_LEFT | TA_VCENTER);
           }
        }

      // --- Layer 9: Collect CVD data point for panel rendering
      if(showCVDLine && cvdCount < visibleBarsCount)
        {
         cvdValues[cvdCount]     = barFootprints[fpIdx].cumulativeDelta;
         cvdBarIndices[cvdCount] = barIndex;
         cvdCount++;
        }

      // --- Layer 10: Info Box rendering above bar high
      if(!showInfoBox) continue;
      //--- Fetch delta and volume summary for info box
      double delta    = barFootprints[fpIdx].delta;
      double totalVol = barFootprints[fpIdx].totalUpVolume + barFootprints[fpIdx].totalDownVolume;
      //--- Skip info box on zero-volume bars
      if(totalVol == 0) continue;

      //--- Get display values for info box content
      double upPct   = barFootprints[fpIdx].upPercentage;
      double downPct = barFootprints[fpIdx].downPercentage;
      color  boxClr  = barFootprints[fpIdx].boxColor;
      color  txtClr  = barFootprints[fpIdx].textColor;

      //--- Calculate info box font size scaled to chart zoom
      int iFontSize = (int)(infoBoxBaseFontSize + currentChartScale * 1.5);
      iFontSize     = MathMax(8, MathMin(18, iFontSize));

      //--- Build info box text lines
      string line1 = StringFormat("Δ %+.0f", delta);
      string line2 = StringFormat("V %.0f",  totalVol);
      string line3 = StringFormat("↓%.0f%% ↑%.0f%%", downPct, upPct);
      //--- Add absorption label if applicable
      string line4 = barFootprints[fpIdx].isAbsorptionBar ? "◈ ABSORB" : "";

      //--- Measure text dimensions for each line
      mainCanvas.FontSet("Arial Bold", (uint)iFontSize, FW_BOLD);
      int tw1 = mainCanvas.TextWidth(line1), th1 = mainCanvas.TextHeight(line1);
      mainCanvas.FontSet("Arial", (uint)iFontSize, FW_NORMAL);
      int tw2 = mainCanvas.TextWidth(line2), th2 = mainCanvas.TextHeight(line2);
      int tw3 = mainCanvas.TextWidth(line3), th3 = mainCanvas.TextHeight(line3);
      int tw4 = 0, th4 = 0;
      if(line4 != "")
        {
         mainCanvas.FontSet("Arial", (uint)(iFontSize - 1), FW_NORMAL);
         tw4 = mainCanvas.TextWidth(line4); th4 = mainCanvas.TextHeight(line4);
        }

      //--- Compute mini delta bar dimensions
      int miniBarW   = showMiniDeltaBar ? MathMax(tw1, MathMax(tw2, tw3)) : 0;
      int miniBarH   = showMiniDeltaBar ? 5 : 0;
      int miniBarGap = showMiniDeltaBar ? 4 : 0;

      //--- Compute info box total dimensions
      int maxTW  = MathMax(miniBarW, MathMax(tw1, MathMax(tw2, MathMax(tw3, tw4))));
      int totalH = th1 + 2 + th2 + 2 + th3 + (line4 != "" ? th4 + 2 : 0) + miniBarH + miniBarGap;
      int rW     = maxTW + infoBoxPaddingWidthPixels  * 2;
      int rH     = totalH + infoBoxPaddingHeightPixels * 2 + 4;

      //--- Position info box centered above bar high
      int xRect = BarToXCoordinate(barIndex) - rW / 2;
      int yRect = PriceToYCoordinate(highs[bufIdx]) - rH - infoBoxGapPixels;

      //--- Build ARGB values for fill and border
      uint  argbFill   = ColorToArgbWithOpacity(boxClr, backgroundTransparencyPercent);
      color borderClr  = DarkenColor(boxClr, 0.7);
      uint  argbBorder = ColorToArgbWithOpacity(borderClr, borderTransparencyPercent);
      uint  argbText   = ColorToARGB(txtClr, 255);

      //--- Compute supersampling parameters
      int ssFactor     = enableRoundedCorners ? MathMax(1, supersamplingFactor) : 1;
      int scaledW      = rW * ssFactor, scaledH  = rH * ssFactor;
      int scaledR      = cornerRadiusPixels * ssFactor;
      int scaledBorder = borderThicknessPixels * ssFactor;

      //--- Create high-res and low-res temp canvases for supersampled rendering
      CCanvas tempHi, tempLo;
      if(ssFactor > 1 && !tempHi.Create("thi_" + IntegerToString(i), scaledW, scaledH,
                                         COLOR_FORMAT_ARGB_NORMALIZE)) continue;
      if(!tempLo.Create("tlo_" + IntegerToString(i), rW, rH, COLOR_FORMAT_ARGB_NORMALIZE)) continue;
      //--- Clear both canvases
      if(ssFactor > 1) tempHi.Erase(0);
      tempLo.Erase(0);

      //--- Select target canvas based on supersampling mode
      CCanvas *dc = (ssFactor > 1) ? &tempHi : &tempLo;
      int dW      = (ssFactor > 1) ? scaledW : rW;
      int dH      = (ssFactor > 1) ? scaledH : rH;
      int dR      = (ssFactor > 1) ? scaledR : cornerRadiusPixels;
      int dB      = (ssFactor > 1) ? scaledBorder : borderThicknessPixels;

      //--- Draw box shape with rounded or straight corners
      if(enableRoundedCorners)
        {
         RenderRoundedRectangleFill(*dc, 0, 0, dW, dH, dR, argbFill);
         if(borderThicknessPixels > 0)
            RenderRoundedRectangleBorder(*dc, 0, 0, dW, dH, dR, dB, argbBorder);
        }
      else
        {
         //--- Fill and optionally border with straight edges
         dc.FillRectangle(0, 0, dW - 1, dH - 1, argbFill);
         if(borderThicknessPixels > 0)
           {
            dc.LineAA(0,  0,  dW, 0,  argbBorder);
            dc.LineAA(dW, 0,  dW, dH, argbBorder);
            dc.LineAA(dW, dH, 0,  dH, argbBorder);
            dc.LineAA(0,  dH, 0,  0,  argbBorder);
           }
        }

      //--- Downsample high-res canvas to final size when supersampling is active
      if(ssFactor > 1) DownsampleBicubic(tempLo, tempHi, ssFactor);

      //--- Draw text content into the low-res canvas
      int tX = rW / 2;
      int tY = infoBoxPaddingHeightPixels + 2;
      //--- Render delta line in bold
      tempLo.FontSet("Arial Bold", (uint)iFontSize, FW_BOLD);
      tempLo.TextOut(tX, tY, line1, argbText, TA_CENTER | TA_TOP); tY += th1 + 2;
      //--- Render volume and percentage lines in normal weight
      tempLo.FontSet("Arial", (uint)iFontSize, FW_NORMAL);
      tempLo.TextOut(tX, tY, line2, argbText, TA_CENTER | TA_TOP); tY += th2 + 2;
      tempLo.TextOut(tX, tY, line3, argbText, TA_CENTER | TA_TOP); tY += th3 + miniBarGap;

      //--- Render mini delta bar if enabled
      if(showMiniDeltaBar && miniBarW > 0)
        {
         int  barX  = (rW - miniBarW) / 2;
         //--- Draw background track of mini bar
         uint bgBar = ColorToArgbWithOpacity(downColor1, 40);
         uint fgBar = ColorToArgbWithOpacity(upColor4,   80);
         tempLo.FillRectangle(barX, tY, barX + miniBarW, tY + miniBarH, bgBar);
         //--- Fill ask percentage portion of mini bar
         int fillW = (int)(miniBarW * upPct / 100.0);
         if(fillW > 0) tempLo.FillRectangle(barX, tY, barX + fillW, tY + miniBarH, fgBar);
         tY += miniBarH + 2;
        }

      //--- Render absorption label if present
      if(line4 != "")
        {
         tempLo.FontSet("Arial", (uint)(iFontSize - 1), FW_NORMAL);
         uint absArgb = ColorToArgbWithOpacity(absorptionColor, 230);
         tempLo.TextOut(tX, tY, line4, absArgb, TA_CENTER | TA_TOP);
        }

      //--- Alpha-blend the info box temp canvas onto the main canvas at bar position
      for(int dy = 0; dy < rH; dy++)
         for(int dx = 0; dx < rW; dx++)
            BlendPixelSet(mainCanvas, xRect + dx, yRect + dy, tempLo.PixelGet(dx, dy));

      //--- Release supersampling canvas memory
      if(ssFactor > 1) tempHi.Destroy();
      tempLo.Destroy();
     }

   // --- Layer 11: CVD Panel rendering below candles
   if(showCVDLine && cvdCount >= 2)
     {
      //--- Position CVD panel below visible area, above histogram if enabled
      int panelY = currentChartHeight - cvdPanelHeightPixels -
                   (showDeltaHistogram ? histogramHeightPixels + 4 : 0);
      int panelH = cvdPanelHeightPixels;

      //--- Draw separator line at panel top
      uint sepCol = ColorToArgbWithOpacity(0x444444, 50);
      for(int px = 0; px < currentChartWidth; px++) mainCanvas.PixelSet(px, panelY, sepCol);

      //--- Find CVD min and max for vertical scaling
      double cvdMin = cvdValues[0], cvdMax = cvdValues[0];
      for(int k = 0; k < cvdCount; k++)
        {
         cvdMin = MathMin(cvdMin, cvdValues[k]);
         cvdMax = MathMax(cvdMax, cvdValues[k]);
        }
      //--- Guard against flat CVD range
      double cvdRange = cvdMax - cvdMin;
      if(cvdRange == 0) cvdRange = 1;

      //--- Draw zero reference line in panel
      int zeroY = panelY + panelH - (int)((-cvdMin / cvdRange) * (panelH - 4)) - 2;
      uint zeroCol = ColorToArgbWithOpacity(0x888888, 40);
      for(int px = 0; px < currentChartWidth; px++)
         mainCanvas.PixelSet(px, MathMin(panelY + panelH - 1, MathMax(panelY, zeroY)), zeroCol);

      //--- Draw CVD label in panel
      mainCanvas.FontSet("Arial", 8, FW_NORMAL);
      mainCanvas.TextOut(4, panelY + 3, "CVD", ColorToARGB(cvdPositiveColor, 180), TA_LEFT | TA_TOP);

      //--- Connect CVD data points with color-coded line segments
      for(int k = 0; k < cvdCount - 1; k++)
        {
         double v1 = cvdValues[k],     v2 = cvdValues[k + 1];
         int    b1 = cvdBarIndices[k], b2 = cvdBarIndices[k + 1];
         //--- Convert bar indices to pixel X coordinates
         int x1 = BarToXCoordinate(b1);
         int x2 = BarToXCoordinate(b2);
         //--- Convert CVD values to pixel Y coordinates within panel
         int y1 = panelY + panelH - (int)(((v1 - cvdMin) / cvdRange) * (panelH - 4)) - 2;
         int y2 = panelY + panelH - (int)(((v2 - cvdMin) / cvdRange) * (panelH - 4)) - 2;
         //--- Clamp Y coordinates to panel bounds
         y1 = MathMin(panelY + panelH - 1, MathMax(panelY + 1, y1));
         y2 = MathMin(panelY + panelH - 1, MathMax(panelY + 1, y2));
         //--- Choose line color based on rising or falling CVD
         color lineCol = v2 >= cvdValues[MathMax(0, k - 1)] ? cvdPositiveColor : cvdNegativeColor;
         uint  lArgb   = ColorToArgbWithOpacity(lineCol, 90);
         mainCanvas.LineAA(x1, y1, x2, y2, lArgb);
        }
     }

   // --- Layer 12: Delta Histogram rendering at bottom of chart
   if(showDeltaHistogram)
     {
      //--- Position histogram at bottom of canvas
      int histY = currentChartHeight - histogramHeightPixels;
      //--- Draw separator line above histogram
      uint sepCol = ColorToArgbWithOpacity(0x444444, 40);
      for(int px = 0; px < currentChartWidth; px++) mainCanvas.PixelSet(px, histY, sepCol);

      //--- Find maximum absolute delta across all visible bars
      double maxAbsDelta = 0;
      for(int i = 0; i < visibleBarsCount; i++)
        {
         int barIndex = firstVisibleBarIndex - i;
         if(barIndex < 0 || barIndex >= ratesTotal) continue;
         int bufIdx = ratesTotal - 1 - barIndex;
         int fpIdx  = GetBarFootprintIndex(times[bufIdx]);
         if(fpIdx >= 0) maxAbsDelta = MathMax(maxAbsDelta, MathAbs(barFootprints[fpIdx].delta));
        }
      //--- Guard against all-zero deltas
      if(maxAbsDelta == 0) maxAbsDelta = 1;

      //--- Draw histogram label
      mainCanvas.FontSet("Arial", 8, FW_NORMAL);
      mainCanvas.TextOut(4, histY + 3, "Δ", ColorToARGB(0x888888, 180), TA_LEFT | TA_TOP);

      //--- Draw delta histogram bar for each visible bar
      for(int i = 0; i < visibleBarsCount; i++)
        {
         int barIndex = firstVisibleBarIndex - i;
         if(barIndex < 0 || barIndex >= ratesTotal) continue;
         int bufIdx = ratesTotal - 1 - barIndex;
         int fpIdx  = GetBarFootprintIndex(times[bufIdx]);
         if(fpIdx < 0) continue;

         //--- Get bar delta and compute bar dimensions
         double d    = barFootprints[fpIdx].delta;
         int    bw   = GetBarWidth(currentChartScale);
         int    cx   = BarToXCoordinate(barIndex);
         //--- Scale bar height by delta proportion of maximum
         int    barH = (int)(MathAbs(d) / maxAbsDelta * (histogramHeightPixels - 4));
         if(barH < 1) barH = 1;
         int    barW = MathMax(1, bw - 2);

         //--- Choose color based on positive or negative delta
         color bc  = d >= 0 ? upColor4 : downColor4;
         uint  ba  = ColorToArgbWithOpacity(bc, 70);
         int   yTop = currentChartHeight - barH;
         //--- Draw histogram bar from bottom up
         DrawFilledRect(mainCanvas, cx - barW / 2, yTop, cx + barW / 2, currentChartHeight - 1, ba);
        }
     }

   //--- Flush canvas to screen
   mainCanvas.Update();
  }

Мы начинаем функцию "RedrawCanvas" с защиты от недопустимых размеров холста, затем копируем массивы high, low и time для всех баров с помощью CopyHigh, "CopyLow" и CopyTime, прерывая выполнение, если какая-либо операция копирования возвращает неполные данные. Мы очищаем холст до полной прозрачности, вычисляем активную ширину бара в пикселях и определяем эффективный размер шрифта — фиксированный или динамически масштабируемый от ширины бара с помощью MathMax и "MathMin", чтобы ограничить его диапазоном от 7 до 16 пикселей. Мы также переводим множитель минимального расстояния в ценовые единицы и, если включена панель кумулятивной дельты объёма, изменяем размер массивов сбора данных до числа видимых баров.

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

Слой 1 рисует фоновую заливку зоны стоимости, если она включена и существуют корректные границы, преобразуя верхнюю и нижнюю границы зоны стоимости в пиксельные координаты и заполняя диапазон полупрозрачным прямоугольником с помощью "DrawFilledRect". Слой 2 рисует полосы объёмного профиля позади ценовых уровней. Для каждого уровня мы вычисляем суммарный объём, определяем ширину бара как долю от максимального объёма уровня, масштабированную заданным максимальным коэффициентом ширины, затем делим ширину между сторонами ask и bid пропорционально и рисуем два залитых прямоугольника: ask вправо от центра, bid влево, каждый со своей прозрачностью.

Слой 3 рисует подсветку строк многоуровневого дисбаланса при его обнаружении. Для сторон ask и bid мы проходим от начального индекса дисбаланса через заданное число уровней с небольшим расширением и заполняем каждую строку полупрозрачным прямоугольником соответствующего цвета дисбаланса. Слой 4 рисует наложение зоны поглощения, когда бар классифицирован как поглощение. Мы заполняем весь вертикальный диапазон бара оттенком с очень низкой непрозрачностью, затем рисуем верхнюю и нижнюю граничные линии с более высокой непрозрачностью с помощью LineAA чтобы обрамить зону, не перекрывая уровни под ней. Слой 5 рисует пунктирную линию точки контроля на пиксельной позиции уровня с наибольшим объёмом с помощью "DrawDashedHLine" с высокой непрозрачностью.

Слой 6 строит массивы текста и цветов для левых и правых подписей всех уровней, форматируя их для режима дельты или режима Bid vs Ask точно так же, как в предыдущей версии, затем применяет переопределение цветов для диагональных дисбалансов в режиме Bid vs Ask. Слой 7 рисует текстовые подписи ценовых уровней, но сначала проверяет минимальный порог объёма и пропускает любой уровень ниже него. Для уровней single print он рисует пунктирные верхние и нижние границы с помощью "DrawDashedHLine". Для уровней unfinished business он ставит точки через каждые четыре пикселя по ширине бара с помощью метода PixelSet. Затем левые и правые подписи рисуются с помощью TextOut в соответствующих опорных позициях. Слой 8 рисует направленные треугольные значки рядом с барами, где обнаружены многоуровневые дисбалансы: восходящий треугольник для ask-серий и нисходящий треугольник для bid-серий с помощью жирного "TextOut" у правой границы бара.

Слой 9 собирает значение кумулятивной дельты и индекс бара для каждого видимого бара в массивы панели для последующей отрисовки. Слой 10 отвечает за информационный блок, который теперь получает два дополнения по сравнению с предыдущей версией. Четвёртая строка текста с меткой поглощения создаётся, когда бар классифицирован как поглощение, а в нижней части информационного блока рисуется мини-бар дельты — горизонтальная полоса фиксированной высоты, где фоновая дорожка занимает всю ширину блока, а часть процента ask заполняется пропорционально слева. Размеры блока, конвейер суперсэмплинга, скруглённые углы, даунсэмплинг, отрисовка текста и альфа-композитинг следуют той же структуре, что и раньше.

Слой 11 рисует панель кумулятивной дельты объёма под свечами, когда собрано не менее двух точек данных. Мы размещаем панель выше гистограммы, если она тоже включена, рисуем разделительную строку пикселей, находим минимальные и максимальные значения кумулятивной дельты для вертикального масштабирования с помощью MathMin и "MathMax", рисуем нулевую опорную линию, добавляем подпись "CVD" и соединяем последовательные точки данных сглаженными отрезками, цвет которых зависит от того, растёт или падает кумулятивная дельта. Слой 12 рисует гистограмму дельты в самом низу холста. Мы рисуем разделитель, сканируем все видимые бары, чтобы найти максимальную абсолютную дельту для масштабирования, подписываем панель, затем для каждого бара рисуем залитый прямоугольник, масштабированный по доле его дельты от максимума и окрашенный по направлению знака. После завершения всех слоёв мы вызываем Update на холсте, чтобы вывести готовый кадр на экран.

После этого остаётся лишь вызвать эти функции в необходимых местах. В обработчике события OnCalculate необходимо инициализировать новые поля и вызвать новые аналитические функции после обновления объёма — и на этом реализация завершена.

Обновление обработчика расчёта для новых аналитических функций и отслеживания кумулятивной дельты

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

//+------------------------------------------------------------------+
//| Process incoming ticks and trigger canvas redraw                 |
//+------------------------------------------------------------------+
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 before processing
   if(rates_total < 2) return 0;

   //--- Get current bar time and detect new bar formation
   datetime curBarTime = time[rates_total - 1];
   bool     isNewBar   = (curBarTime != lastBarTime);

   //--- Initialize new bar footprint entry on bar change
   if(isNewBar)
     {
      //--- Append slot to footprints array
      int ns = ArraySize(barFootprints);
      ArrayResize(barFootprints, ns + 1);
      currentBarIndex = ns;

      //--- Initialize new bar entry with default values
      barFootprints[currentBarIndex].time              = curBarTime;
      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;
      barFootprints[currentBarIndex].pocLevelIndex     = -1;
      barFootprints[currentBarIndex].valueAreaLow      = -1;
      barFootprints[currentBarIndex].valueAreaHigh     = -1;
      barFootprints[currentBarIndex].cumulativeDelta   = runningCVD;
      barFootprints[currentBarIndex].hasStackedAskImbalance = false;
      barFootprints[currentBarIndex].hasStackedBidImbalance = false;
      barFootprints[currentBarIndex].stackedAskStart   = -1;
      barFootprints[currentBarIndex].stackedBidStart   = -1;
      barFootprints[currentBarIndex].isAbsorptionBar   = false;
      ArrayResize(barFootprints[currentBarIndex].priceLevels, 0);

      //--- Purge oldest bars when array exceeds double the render limit
      if(ns > maxBarsToRender * 2)
        {
         int toRemove = ns - maxBarsToRender;
         //--- Delete candle objects for purged bars
         for(int i = 0; i < toRemove; i++)
           {
            string ts = TimeToString(barFootprints[i].time);
            ObjectDelete(0, objectPrefix + "Body_"      + ts);
            ObjectDelete(0, objectPrefix + "UpperWick_" + ts);
            ObjectDelete(0, objectPrefix + "LowerWick_" + ts);
           }
         //--- Remove purged entries from footprints array
         ArrayRemove(barFootprints, 0, toRemove);
         currentBarIndex -= toRemove;
        }

      //--- Update bar tracking state variables
      lastBarTime    = curBarTime;
      lastClosePrice = open[rates_total - 1];
      lastTickVolume = 0;
     }

   //--- Guard against invalid current bar index
   if(currentBarIndex < 0 || currentBarIndex >= ArraySize(barFootprints)) return rates_total;

   //--- Calculate volume change since last tick
   double volDiff = (double)(tick_volume[rates_total - 1] - lastTickVolume);
   lastTickVolume = tick_volume[rates_total - 1];

   bool dataChanged = false;

   //--- Process volume increment if non-zero
   if(volDiff > 0)
     {
      dataChanged = true;
      double curClose = close[rates_total - 1];
      double upDiff = 0, downDiff = 0;

      //--- Classify tick direction by close price movement
      if(curClose > lastClosePrice)      { upDiff   = volDiff; lastTradeAtAsk = true; }
      else if(curClose < lastClosePrice) { downDiff = volDiff; lastTradeAtAsk = false; }
      //--- Maintain previous direction on unchanged close
      else { if(lastTradeAtAsk) upDiff = volDiff; else downDiff = volDiff; }

      lastClosePrice = curClose;

      //--- Quantize close price to footprint level grid
      double qp = QuantizePriceToLevel(curClose);
      //--- Update level volumes and bar totals
      UpdatePriceLevel(barFootprints[currentBarIndex], qp, upDiff, downDiff);
      barFootprints[currentBarIndex].totalUpVolume   += upDiff;
      barFootprints[currentBarIndex].totalDownVolume += downDiff;

      //--- Recompute derived statistics for current bar
      ComputeMaxValues(barFootprints[currentBarIndex]);
      SortPriceLevelsDescending(barFootprints[currentBarIndex]);
      CalculateBarColorsAndPercentages(barFootprints[currentBarIndex]);
      ComputePOCAndValueArea(barFootprints[currentBarIndex]);
      DetectImbalancesAndAbsorption(barFootprints[currentBarIndex]);

      //--- Update running cumulative delta with new bar delta
      runningCVD = barFootprints[currentBarIndex].cumulativeDelta +
                   barFootprints[currentBarIndex].delta;
      barFootprints[currentBarIndex].cumulativeDelta = runningCVD;
     }

   //--- Render candle objects for bars within render window
   int firstBar = MathMax(0, rates_total - maxBarsToRender);
   for(int i = firstBar; i < rates_total; i++)
     {
      int fpIdx = GetBarFootprintIndex(time[i]);
      if(fpIdx >= 0)
         RenderFootprint(fpIdx, rates_total - 1 - i, time[i], rates_total);
     }

   //--- Check for chart dimension or viewport changes
   bool changed = false;
   int  nW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int  nH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   int  nS = (int)ChartGetInteger(0, CHART_SCALE);
   int  nF = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);
   int  nV = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   double nMin = ChartGetDouble(0, CHART_PRICE_MIN, 0);
   double nMax = ChartGetDouble(0, CHART_PRICE_MAX, 0);

   //--- Resize canvas if chart pixel dimensions changed
   if(nW != currentChartWidth || nH != currentChartHeight)
     {
      mainCanvas.Resize(nW, nH);
      currentChartWidth  = nW;
      currentChartHeight = nH;
      changed = true;
     }

   //--- Update viewport state if scale, scroll, or price range changed
   if(nS != currentChartScale || nF != firstVisibleBarIndex || nV != visibleBarsCount ||
      nMin != minVisiblePrice  || nMax != maxVisiblePrice)
     {
      currentChartScale    = nS;
      firstVisibleBarIndex = nF;
      visibleBarsCount     = nV;
      minVisiblePrice      = nMin;
      maxVisiblePrice      = nMax;
      changed = true;
     }

   //--- Trigger full canvas redraw on any state change or new data
   if(changed || rates_total > prev_calculated || dataChanged)
      RedrawCanvas(rates_total);

   //--- Force chart to repaint after all updates
   ChartRedraw(0);
   return rates_total;
  }

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

В блоке обработки объёма мы сохраняем классификацию направления тика, квантование цены, обновление уровня, накопление суммарных объёмов, вызов "ComputeMaxValues", вызов "SortPriceLevelsDescending" и вызов "CalculateBarColorsAndPercentages" точно как раньше. Мы добавляем два новых вызова: "ComputePOCAndValueArea" для обновления индекса точки контроля и границ зоны стоимости, а также "DetectImbalancesAndAbsorption" для обновления всех структурных флагов после последнего прироста объёма. После этих вызовов мы обновляем накопленную кумулятивную дельту, прибавляя чистую дельту текущего бара к его начальному значению и записывая результат обратно в структуру. Это гарантирует, что кумулятивная дельта, переносимая в следующий бар, отражает весь обработанный к этому моменту объём.

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


Тестирование

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

GIF-анимация бэктеста

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


Заключение

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

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Levison Da Silva Barbosa
Levison Da Silva Barbosa | 15 мая 2026 в 14:04
Спасибо, учитель.
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Кодирование свечных моделей (Часть 1): Алфавитная система для выявления сигналов Кодирование свечных моделей (Часть 1): Алфавитная система для выявления сигналов
Мы представляем основанный на правилах алфавит для анализа ценовых движений на основе свечных графиков, который сопоставляет измеримые формы и направления с буквенными кодами (A/a, H/h, E/e, G/g, D). В статье показана реализация на MQL5: классификация свечей, построение двухбарных последовательностей с помощью перестановок и сканирование графиков с помощью индикатора и оповещений. Читатели получают практический шаблон для объективного обнаружения паттернов и систематического тестирования.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Создание пользовательских индикаторов в MQL5 (Часть 10): Дополнение футпринт-графика информационным блоком настроений по объёму по каждому бару Создание пользовательских индикаторов в MQL5 (Часть 10): Дополнение футпринт-графика информационным блоком настроений по объёму по каждому бару
В статье показано, как улучшить MQL5-индикатор футпринта: над каждой свечой добавляется компактный информационный блок, отражающий настроения по объёму и в котором отображаются чистая дельта, общий объём и процентное соотношение покупок и продаж. Мы реализуем сглаженную отрисовку с суперсэмплингом, скруглённые углы с помощью растеризации дуг и четырёхугольных примитивов, а также попиксельное альфа-смешивание. Вспомогательные инструменты включают преобразование цветов в ARGB, сканлайновую растеризацию и понижающую дискретизацию бокс-фильтром. Блок позволяет быстро оценивать настроения по объёму и остаётся разборчивым при разных уровнях масштаба.