Создание пользовательских индикаторов в MQL5 (Часть 11): Улучшение футпринт-графика с помощью слоёв рыночной структуры и ордер-флоу
Введение
График футпринта отображает объём на каждом ценовом уровне, однако без контекста рыночной структуры трудно определить, на каком уровне внутри бара прошёл наибольший объём, где на нескольких соседних уровнях проявлялось агрессивное давление одной стороны и поглотил ли бар значительный объём без заметного движения цены. Эта статья предназначена для MQL5-разработчиков и алгоритмических трейдеров, которые хотят расширить футпринт-график аналитическими слоями рыночной структуры и ордер-флоу.
В нашей предыдущей статье (Часть 10) мы улучшили MQL5-индикатор футпринт-графика, добавив информационный блок с оценкой настроений по объёму для каждого бара с цветовым кодированием интенсивности дельты, скруглёнными углами с суперсэмплингом и альфа-композитингом. В Части 11 мы вводим полосы объёмного профиля, подсветку точки контроля и зоны стоимости, выявление многоуровневых дисбалансов, классификацию зон поглощения, маркеры single print, панель кумулятивной дельты объёма и гистограмму дельты. В статье будут рассмотрены следующие темы:
- Слои рынка — как структура и ордер-флоу сочетаются внутри каждого бара
- Реализация в MQL5
- Тестирование
- Заключение
К концу статьи у вас будет улучшенный 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)-изображении.

Во время тестирования зона стоимости и точка контроля обновлялись на каждом тике по мере накопления объёма на ценовых уровнях. Подсветка многоуровневых дисбалансов и направленные значки появлялись на соответствующих уровнях, когда выполнялись пороги соотношения и глубины. Панель кумулятивной дельты объёма отслеживала накопленную чистую дельту через границы торговых сессий, а гистограмма дельты в нижней части графика отражала направленное доминирование в каждом баре.
Заключение
Мы улучшили индикатор футпринт-графика в MQL5, добавив двенадцать визуальных и аналитических слоёв, включая объёмный профиль позади ценовых уровней каждого бара, подсветку точки контроля и зоны стоимости, выявление многоуровневых дисбалансов с направленными значками, классификацию зон поглощения, маркеры single print и unfinished business, панель кумулятивной дельты объёма и побарную гистограмму дельты. В реализации были рассмотрены расширенные структуры бара и ценового уровня, отдельные вычислительные функции для точки контроля, расширения зоны стоимости, выявления серий дисбалансов и классификации поглощения, послойный конвейер перерисовки холста, который рисует каждое наложение в заданной последовательности, а также обновлённый обработчик расчёта, вызывающий все новые аналитические функции и поддерживающий накопленную кумулятивную дельту на каждом тике. После прочтения статьи вы сможете:
- Использовать наложение точки контроля и зоны стоимости для определения зоны принятия цены внутри бара, рассматривать возвраты к точке контроля после отклонения как ориентиры для входов по стратегии возврата к среднему и использовать границы зоны стоимости как уровни поддержки и сопротивления для следующего бара
- Определять зоны многоуровневого дисбаланса, отмеченные направленными значками, и использовать их как индикатор подпитки направленного движения — входить в направлении зоны многоуровневого ask-дисбаланса, когда цена удерживается выше неё, или зоны многоуровневого bid-дисбаланса, когда цена удерживается ниже неё, а также рассматривать бары поглощения на ключевых уровнях как потенциальных кандидатов на разворот, когда кумулятивная дельта объёма расходится с направлением цены
- Отслеживать устойчивое расхождение панели кумулятивной дельты объёма с направлением цены как раннее предупреждение об истощении и использовать гистограмму дельты для сравнения направленного доминирования по барам в серии перед открытием позиции
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21984
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Кодирование свечных моделей (Часть 1): Алфавитная система для выявления сигналов
Создание пользовательских индикаторов в MQL5 (Часть 10): Дополнение футпринт-графика информационным блоком настроений по объёму по каждому бару
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования