Разработка инструментария для анализа Price Action (Часть 38): VWAP на основе тикового буфера и модуль расчета дисбаланса на коротком окне
Содержание
Введение
Большинство трейдеров быстро понимают, что ценовые графики не показывают всей картины. То, что действительно движет рынком, скрыто чуть глубже – в стакане заявок, где биды и аски показывают, кто ждет покупки, кто готов продавать и где сосредоточена ликвидность. Профессиональные трейдеры ежедневно используют эту информацию, чтобы точно определять зоны спроса и предложения, выявлять дисбалансы и с высокой точностью оценивать краткосрочные ценовые сдвиги.
Однако розничные трейдеры редко видят эту картину. Многие платформы вообще не дают прямого доступа к книге ордеров, а другие предоставляют лишь данные с задержкой или в неполном объеме, нередко по отдельной платной подписке. На распространенных розничных платформах, таких как MetaTrader 5, прозрачность полностью зависит от брокера, и большинство трейдеров вынуждены ориентироваться на рынке без тех микроструктурных сигналов, которые для профессионалов давно стали нормой.
Инструмент Slippage Tool был создан, чтобы закрыть этот пробел. Хотя он не пытается воспроизвести полноценную книгу ордеров или необработанную ленту котировок, он восстанавливает наиболее полезные сигналы, которые трейдеры обычно извлекают из глубины рынка, при этом используя только тиковые данные, доступные у любого брокера. Он рассчитывает VWAP (средневзвешенную по объему цену), чтобы показать, где концентрируется активность, измеряет краткосрочные дисбалансы ордеров для оценки направленного давления и агрегирует тиковые объемы, чтобы яснее показать недавнюю активность участников. Затем эти показатели рассматриваются вместе со спредом и контекстом ATR, чтобы помочь трейдерам понять, когда рыночные условия поддерживают их направленное смещение.
Отображаясь прямо на графике вместе с алертами и маркерами сделок, этот инструмент дает розничным трейдерам практичный способ приближенной оценки потока ордеров – позволяет наблюдать за смещениями ликвидности, выявлять дисбалансы и точнее выбирать моменты входа даже без доступа к профессиональной книге ордеров.
Схема ниже показывает это на практике. Обратите внимание на медвежье движение от точки E до точки F. Это не прямое падение: рынок колеблется, формирует временные максимумы и минимумы и одновременно снимает ликвидность. Трейдер, входящий в продажу в точке C, может верно определить направление, но при плохом управлении рисками или слишком большом лоте откат к точке D может выбить его из сделки. Slippage Tool показывает здесь другой подход: вход в покупку в точке C и закрытие в точке D, что превращает потенциальный убыток в контролируемую возможность. Аналогичным образом, когда цена поднимается от A, инструмент выделяет B как благоприятную зону для продажи, помогая трейдерам торговать по свингам, а не вылетать из позиции из-за них.

Торговля всегда будет связана с волатильностью, разворотами и ловушками ликвидности. Ключевое различие в том, выбивают ли эти движения трейдеров из рынка или, наоборот, дают возможность более эффективно управлять риском и исполнением сделок. Slippage Tool не устраняет неопределенность, но добавляет на график розничного трейдера структурную ясность, сокращая разрыв между розничными платформами и профессиональной торговой инфраструктурой.
В следующих разделах подробно разбираются принципы работы инструмента, значение каждого ключевого показателя и то, как эти функции можно использовать для улучшения торговых решений. Будь вы дискреционным трейдером, которому нужны более точные входы, или разработчиком алгоритмов, ищущим новые источники сигналов, Slippage Tool предлагает легкое и практичное решение одного из самых устойчивых ограничений розничной торговли – ограниченного доступа к надежным данным о глубине рынка.
Подробный обзор ключевых функций инструмента
Slippage Tool – это система для MetaTrader 5, работающая прямо на графике: она восстанавливает сигналы потока ордеров из потока тиков в реальном времени (VWAP, дисбаланс тиков/объема, спред и контекст ATR), выводит торговые возможности через алерты и маркеры и при необходимости учитывает ожидаемое проскальзывание при расчете риска и размера лота, выступая практичной заменой информации из книги ордеров, когда биржевой стакан недоступен.
Ниже приведены математические расчеты и подробные объяснения ключевых функций этой системы.
1. VWAP (средневзвешенная по объему цена)
В предыдущей статье я подробно разобрал VWAP (Volume Weighted Average Price). VWAP – это ориентир, который объединяет данные о цене и объеме, чтобы рассчитать средневзвешенную цену за заданный период времени. В отличие от простого арифметического среднего, которое одинаково учитывает каждую ценовую точку, VWAP придает больший вес тем ценовым уровням, на которых объем торгов был выше. Этот подход гарантирует, что VWAP точно отражает те уровни цен, на которые пришлась основная торговая активность, и тем самым отражает фактический рыночный консенсус за рассматриваемый период.
Включая объем в расчет, VWAP дает более содержательную оценку "справедливой стоимости" рынка, поскольку учитывает интенсивность торгов на разных ценовых уровнях. Трейдеры и институциональные участники часто используют VWAP для оценки рыночных условий, поиска потенциальных точек входа и выхода, а также для определения общего направления рынка относительно этого ориентира. Благодаря своей динамической природе VWAP служит надежным ориентиром для оценки внутридневных движений цены и состояния ликвидности, что делает его важным инструментом и в стратегиях исполнения, и в более широком анализе рынка.

Где:
𝑃(i) = цена i-го тика𝑉(i) = тиковый объем i-го тика
Когда цена торгуется выше VWAP, это означает, что рынок находится выше своего среднего уровня, то есть демонстрирует бычье настроение. И напротив, если цена торгуется ниже VWAP, это указывает на дисконт и отражает медвежьи тенденции. Трейдеры часто используют VWAP как динамический уровень поддержки или сопротивления, привязывая к этому ориентиру свои решения.

Поскольку не все брокеры предоставляют полные данные книги ордеров, этот инструмент строит линию VWAP исключительно по входящим тиковым данным. Этот подход дает трейдерам ориентиры, сопоставимые с теми, которые дают "зоны справедливой стоимости" в книге ордеров, выступает ценным индикатором рыночного равновесия и помогает принимать более обоснованные решения.
2. Дисбаланс
Индикатор дисбаланса (Imbalance) показывает, какая сторона преобладает в торговой активности – агрессивные покупатели или агрессивные продавцы. В традиционной книге ордеров это обычно оценивают, сравнивая объем размещенных ордеров на покупку и продажу. Однако, когда прямые данные книги заявок недоступны, инструмент косвенно оценивает рыночный дисбаланс по поведению тиков, давая альтернативный способ измерять направленное давление и рыночные настроения.

Где:
BuyVolume = сумма тиковых объемов для тиков, в которых цена двигалась вверх
SellVolume = сумма тиковых объемов для тиков, в которых цена двигалась вниз
Положительный дисбаланс указывает на давление покупателей, то есть на преобладание более агрессивных покупателей, тогда как отрицательный дисбаланс означает давление продавцов, то есть преобладание более агрессивных продавцов.

Инструмент отслеживает движение цены по каждому тику. Каждое движение цены вверх считается покупательской активностью, тогда как каждое движение вниз относится к продажам. Этот непрерывный расчет в реальном времени постоянно показывает, какая сторона сейчас сильнее влияет на рынок, и дает ценное представление о краткосрочной направленной динамике рынка.
3. Спред
Спред – одна из самых базовых и важных характеристик рыночной микроструктуры. Он отражает цену немедленного исполнения и показывает, сколько трейдерам приходится "платить" за исполнение рыночного ордера без задержки. Этот показатель дает важную информацию о состоянии ликвидности и общей эффективности рынка, отражая транзакционные издержки, связанные с быстрым исполнением.
![]()
Узкий спред обычно указывает на ликвидные и эффективные рыночные условия, способствуя более плавной и менее затратной торговле. Напротив, широкий спред указывает на сниженную ликвидность, более высокие транзакционные издержки и иногда может сигнализировать о рыночном стрессе или неопределенности.

Инструмент непрерывно отслеживает спред в режиме реального времени. Его сигналы можно фильтровать по заданным порогам спреда, помогая трейдерам избегать входов в периоды, когда транзакционные издержки необычно высоки. Этот подход имитирует поведение тех, кто следит за книгой ордеров и воздерживается от торговли, когда глубина рынка недостаточна, тем самым способствуя более благоприятным условиям исполнения.
4. Поток
Индикатор потока (Flow) работает как краткосрочный индикатор настроений, показывая, преобладают ли в заданном скользящем окне бычьи или медвежьи тики. Он отражает текущее давление в потоке ордеров и дает трейдерам мгновенное представление о преобладающем рыночном моментуме.

- Где:

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

В совокупности эти четыре функции (VWAP, дисбаланс, спред и поток) воссоздают ключевые сигналы книги ордеров. Они достигают этого, извлекая ключевую динамику рынка исключительно из необработанных тиковых данных и тем самым предоставляя ценную микроструктурную информацию даже у брокеров, которые не предоставляют полноценную поддержку книги ордеров.
Реализация на MQL5
В этом разделе я пошагово покажу, как создать инструмент Slippage Tool на языке MQL5.
Заголовок файла, директивы include и предварительные объявления.
В начале файла код задает метаданные (#property) и подключает Trade.mqh, а затем объявляет сигнатуры VWAP и потока, чтобы их можно было использовать до определения самих функций. Заголовок выполняет три практические функции: документирует авторство и версию для дальнейшей поддержки, включает строгий режим компилятора с более безопасной современной семантикой MQL5 и объявляет зависимости, чтобы советник мог вызывать торговые процедуры, если в будущем будет добавлена логика автоматических входов. Предварительные объявления – это небольшое стилистическое удобство: они позволяют размещать высокоуровневые функции в любом месте файла, не думая о текстовом порядке. С точки зрения сопровождения этот верхний блок служит удобной картой структуры файла: он показывает, какой внешний функционал (хэндлы индикаторов, торговые помощники) доступен, и заранее подводит читателя к основным метрикам, реализованным далее.
// File metadata and includes #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/ru/users/lynnchris" #property version "1.0" #property strict #include <Trade\Trade.mqh> // CTrade utility for possible auto-trade usage
Перечисления и входные параметры (пространство конфигурации).
Определения перечислений (enum) и длинный список входных переменных образуют публичный API инструмента: это те самые параметры настройки, с помощью которых трейдер меняет поведение инструмента без правки кода. Перечисления дают читаемые имена для UI-опций (углы привязки панели, режимы алертов). Во входных параметрах вынесены критически важные параметры сигнала – окно VWAP в минутах, окно дисбаланса в секундах, доля спреда к ATR для обнаружения "дешевого" спреда, пороговое значение потока и гистерезис для алертов, процент риска и множитель ATR для стопов, а также множество параметров UI/внешнего вида. То, что эти значения вынесены во входные параметры, – важное проектное решение: оно делает советник универсальным для разных инструментов и стилей торговли.
В статье такого уровня стоит объяснить рекомендуемый диапазон каждого входного параметра и связанные с ним компромиссы: например, более длинные окна VWAP дают стабильность, но снижают краткосрочную отзывчивость; большее значение InpImbWindowSec сглаживает поток, но притупляет микроструктурные сигналы. Также стоит подчеркнуть, что разумные значения по умолчанию уже заданы, но следует рекомендовать пользователям подстраивать их под скорость появления тиков и рыночную микроструктуру каждого инструмента.
// Enums (UI choices) and user-exposed inputs (tuning knobs) enum eCorner { COR_LT=0, COR_LB=1, COR_RT=2, COR_RB=3 }; enum eAlertMode { AM_SINGLE=0, AM_ROLLING_CLEAR=1, AM_POOL_REUSE=2 }; input int InpVWAPminutes = 1440; // VWAP window in minutes (rolling) input int InpImbWindowSec = 30; // Imbalance/flow window in seconds input double InpCheapSpreadFrac = 0.50; // Spread < ATR * this fraction => "cheap" input double InpFlowTh = 0.30; // flow threshold for alerts input double InpFlowHystFactor = 0.80; // hysteresis factor for flow reset input double InpRiskPct = 1.0; // risk per trade as % of balance input double InpStopATRmult = 1.2; // stop = ATR * this multiplier input uint InpRingSize = 20000; // tick ring buffer size input uint InpTimerSec = 2; // UI refresh / aggregation cadence
Глобальные переменные и правила именования.
Глобальные переменные инициализируют рабочее состояние: настройки символа, рыночные параметры (g_point, g_tickVal), хэндл индикатора ATR, объект CTrade и набор массивов, реализующих кольцевой буфер тиков. Код использует префикс g_ для имен объектов на графике; такая схема именования крайне важна для безопасной очистки объектов – она гарантирует, что советник будет работать только со своими метками и не удалит случайно объекты других индикаторов.
Массивы кольцевого буфера (g_time, g_bid, g_ask, g_last, g_vol) плюс индексы g_head, g_used, g_size составляют основу всех агрегированных вычислений; локальное хранение тиков предотвращает повторные вызовы API платформы, повышает детерминированность и позволяет выполнять агрегацию по временному окну. Глобальные переменные также хранят предыдущие значения для отслеживания изменений, чтобы сократить лишние перерисовки интерфейса. В статье стоит подчеркнуть, что в MQL5 глобальным состоянием нужно управлять особенно аккуратно, потому что терминал работает в одном потоке, а утечки памяти из-за хэндлов индикаторов или неудаленных объектов – типичная скрытая проблема.
// Globals: symbol settings, ring-buffer arrays and indices string g_sym=""; double g_point=0.0, g_tickVal=0.0, g_tickSize=0.0; double g_volMin=0, g_volMax=0, g_volStep=0; int g_atrHandle = INVALID_HANDLE; MqlTick g_latestTick; datetime g_time[]; // ring buffer timestamps double g_bid[], g_ask[], g_last[], g_vol[]; // ring buffer data int g_head = -1; // index of most recent slot int g_used = 0; // number of filled slots int g_size = 0; // capacity (InpRingSize)
Небольшие вспомогательные функции (SafeDiv, ARGB, RR).
Несколько небольших вспомогательных функций улучшают корректность и читаемость кода. SafeDiv – это защитная обертка, которая предотвращает ошибки деления на ноль; это важный паттерн при работе с данными в реальном времени, где могут встречаться нули или неинициализированные значения. ARGB формирует целочисленное представление цвета с альфа-смешиванием для фона панели; вынесение этой логики в отдельную функцию централизует вычисления цвета и делает код UI более лаконичным. RR вычисляет отношение риск/прибыль одним выражением и одновременно защищает от нулевого знаменателя. Эти вспомогательные функции короткие, но важные: они позволяют основной логике сосредоточиться на торговых решениях, а не на повторяющейся служебной рутине, и защищают от ошибок времени выполнения, которые трудно отлаживать при реальной торговле.// Defensive helpers used across the codebase double SafeDiv(double a, double b) { return (b == 0.0 ? 0.0 : a/b); } uint ARGB(color c, int a) // produce ARGB color integer { if(a<0) a=0; if(a>255) a=255; return ((uint)a<<24) | (c & 0x00FFFFFF); } double RR(double entry,double stop,double tp) // simple R/R guard { return (entry - stop != 0.0 ? (tp - entry) / (entry - stop) : 0.0); }
Инициализация кольцевого буфера и добавление тиков (BufInit / BufAdd).
Конструкция кольцевого буфера выбрана осознанно: BufInit обеспечивает минимальную емкость и меняет размер массивов, а BufAdd записывает последний тик в следующую ячейку и при необходимости зацикливает индекс по модулю. Это дает время вставки O(1) и компактный фиксированный расход памяти. На практике такой подход выбран вместо неограниченного расширения массивов (например, вызова ArrayResize на каждом тике), чтобы избежать лишней нагрузки на память на высокочастотных инструментах. BufAdd также нормализует объем, используя volume_real, если он доступен, а в противном случае берет volume или 1.0, потому что многие реальные потоки данных либо не дают реалистичных объемов, либо представляют их в ином виде. Большее значение InpRingSize сохраняет больше истории для длинных окон VWAP, но требует больше памяти. Этот компромисс влияет и на отзывчивость, и на расход оперативной памяти.// Initialize ring buffer with minimum capacity void BufInit(int n) { if(n < 128) n = 128; g_size = n; ArrayResize(g_time, n); ArrayResize(g_bid, n); ArrayResize(g_ask, n); ArrayResize(g_last, n); ArrayResize(g_vol, n); g_head = -1; g_used = 0; } // Append a tick into the circular buffer (O(1) insertion) void BufAdd(const MqlTick &q) { if(g_size <= 0) return; g_head = (g_head + 1) % g_size; g_time[g_head] = q.time; g_bid[g_head] = q.bid; g_ask[g_head] = q.ask; g_last[g_head] = q.last; double vol = (q.volume_real > 0 ? q.volume_real : q.volume); g_vol[g_head] = (vol > 0 ? vol : 1.0); // safe fallback if(g_used < g_size) g_used++; }
Функция доступа к цене (Mid).
Функция Mid возвращает лучшую доступную цену для сохраненного тика: если доступно значение последней цены, используется оно (last trade), а в противном случае берется середина между bid и ask. Этот многоуровневый подход прагматичен: цены сделок дают самое прямое представление о фактической цене исполнения, но некоторые обновления тиков содержат только котировки, поэтому срединная точка приблизительно отражает справедливую стоимость. Возврат 0 для непригодных ячеек сигнализирует, что такие записи нужно пропускать. Важно понимать, что выбор между последней ценой и срединной точкой влияет на чувствительность: VWAP или индикатор потока ориентируются на последнюю цену и реагируют только при совершении сделок, тогда как ориентация на срединную точку повышает плотность данных, но может вносить шум.// Prefer last trade price; fallback to midpoint if last absent double Mid(int idx) { if(idx < 0 || idx >= g_size) return 0.0; if(g_last[idx] > 0.0) return g_last[idx]; if(g_bid[idx] > 0.0 && g_ask[idx] > 0.0) return 0.5 * (g_bid[idx] + g_ask[idx]); return 0.0; }
Функции доступа к спреду и ATR.
SpreadPips и ATR описывают две независимые характеристики: непосредственные издержки сделки и рыночную волатильность. SpreadPips вызывает SymbolInfoTick и делит разницу ask - bid на g_point, чтобы выразить спред в пипсах (или пунктах), что упрощает сравнение с ATR. ATR() считывает последнее значение ATR через CopyBuffer из хэндла iATR, а ATR_Avg вычисляет короткое скользящее среднее. Это разделение важно: ATR здесь рассматривается как базовый показатель волатильности, который используется для расчета стопа и в проверке "узкого спреда" (спред < ATR * коэффициент). Важно учитывать вычислительные затраты: CopyBuffer выполняет ввод-вывод, поэтому использовать его стоит экономно; также нужно уточнить, что ATR рассчитывается на таймфрейме, заданном через InpTF, поэтому выбор InpTF влияет на ATR, а значит – и на уровни стопов, и на решения о "дешевизне" спреда.// Spread in pips (points normalized by g_point) double SpreadPips() { MqlTick t; if(!SymbolInfoTick(g_sym, t) || g_point == 0.0) return 0.0; return (t.ask - t.bid) / g_point; } // Latest ATR (indicator handle must be created beforehand) double ATR() { if(g_atrHandle == INVALID_HANDLE) return 0.0; double buf[]; if(CopyBuffer(g_atrHandle, 0, 0, 1, buf) == 1) return buf[0]; return 0.0; } // Average ATR across 'bars' (caps at 200 to limit work) double ATR_Avg(int bars) { if(g_atrHandle == INVALID_HANDLE || bars <= 0) return 0.0; int cap = MathMin(bars, 200); double buf[]; int copied = CopyBuffer(g_atrHandle, 0, 0, cap, buf); if(copied <= 0) return 0.0; double s = 0.0; for(int i=0; i<copied; i++) s += buf[i]; return s / copied; }
Агрегатор (Acc) – основной сборщик данных.
Acc – это единая центральная процедура агрегации, которую вызывают функции VWAP и Flow. Получив метку времени since, функция идет назад по кольцевому буферу от самого нового тика и накапливает price*volume (для числителя VWAP), суммарный объем (для знаменателя VWAP) и – опционально, когда flowNeeded равно true, – количество тиков вверх и тиков вниз. Цикл останавливается, как только встречает тик старше since, то есть функция учитывает временное окно. Важны тонкости реализации: prev инициализируется значением DBL_MAX, чтобы пропустить первое сравнение; тики с недопустимыми ценами пропускаются; индекс зацикливается, когда доходит до начала буфера.
Функция намеренно считает up и dn как события (число тиков), а не суммирует объемы со знаком – это проектное решение, продиктованное простотой и устойчивостью при работе с потоками, в которых нельзя полагаться на объем. Важно четко отметить: Acc обеспечивает и взвешенную по объему VWAP, и поток на основе тиков, но выбранная здесь метрика потока подсчитывает события, а не объемы – и это различие практически влияет на поведение сигналов.
// Accumulate price*volume and volume; optionally compute up/dn tick counts void Acc(datetime since, double &pxVol, double &vol, int &up, int &dn, bool flowNeeded) { pxVol = 0.0; vol = 0.0; up = 0; dn = 0; if(g_used == 0) return; int idx = g_head; double prev = DBL_MAX; for(int i = 0; i < g_used; ++i) { if(g_time[idx] < since) break; double p = Mid(idx); if(p <= 0.0) { idx--; if(idx < 0) idx = g_size - 1; continue; } double w = (g_vol[idx] > 0.0 ? g_vol[idx] : 1.0); pxVol += p * w; // VWAP numerator vol += w; // VWAP denominator if(flowNeeded && prev != DBL_MAX) { if(p > prev) up++; else if(p < prev) dn++; } prev = p; idx--; if(idx < 0) idx = g_size - 1; } }
Реализация VWAP (функция VWAP).
Функция VWAP превращает универсальный Acc в привычную формулу расчета. Она вычисляет since как TimeCurrent() минус заданное число минут, передает это значение в Acc для получения px и v, а затем возвращает px / v. Если объем отсутствует, функция возвращает ноль, предотвращая появление NaN. С концептуальной точки зрения VWAP в этом инструменте – это скользящий VWAP с временным окном, а не сессионный VWAP по умолчанию, поэтому управлять отзывчивостью можно через InpVWAPminutes. Обычно VWAP используют двумя способами: длинное окно – как якорь справедливой стоимости, короткие окна – для внутридневной микроструктуры; также стоит показать, как изменить since, чтобы при необходимости рассчитывать сессионный VWAP.// Rolling VWAP over last 'minutes' minutes double VWAP(int minutes) { if(minutes <= 0) return 0.0; datetime since = TimeCurrent() - (datetime)minutes * 60; double px = 0.0, v = 0.0; int u, d; Acc(since, px, v, u, d, false); return (v > 0.0 ? px / v : 0.0); }
Реализация потока (функция Flow).
Функция потока Flow использует Acc с флагом flowNeeded=true, чтобы получить количество тиков вверх и тиков вниз, и возвращает (up - dn) / (up + dn) – нормализованный дисбаланс числа тиков в диапазоне [-1,1]. Это простое отношение сразу дает представление о направленной активности: значения, близкие к +1, показывают, что почти все тики в окне были восходящими, то есть указывают на преобладание покупок в потоке тиков, тогда как -1 указывает на смещение в сторону продаж.
Явный выбор подсчета тиков вместо объемно-взвешенного потока со знаком заслуживает особого внимания: подсчет тиков дает равный вес всем ценовым движениям и устойчив к аномально крупным отдельным сделкам. Взвешенный по объему поток, напротив, отражает объем, стоящий за движениями, и может быть информативнее, когда истинные размеры сделок надежны. Оба подхода допустимы; в примере ниже показана реализация, которая используется в нашем инструменте.
// Tick-count based flow (implemented in base tool) double Flow(int sec) { if(sec <= 0) return 0.0; datetime since = TimeCurrent() - sec; double px = 0.0, v = 0.0; int up = 0, dn = 0; Acc(since, px, v, up, dn, true); int tot = up + dn; return (tot ? double(up - dn) / tot : 0.0); // normalized [-1,1] } // Alternative: volume-weighted flow (replace Acc's tick-count mode) // *Requires modifying Acc to accumulate signedVol and totalVol* double VolumeWeightedFlow(int sec) { if(sec <= 0) return 0.0; datetime since = TimeCurrent() - sec; int idx = g_head; double signedVol = 0.0, totalVol = 0.0; double prev = DBL_MAX; for(int i = 0; i < g_used; ++i) { if(g_time[idx] < since) break; double p = Mid(idx); if(p <= 0.0) { idx--; if(idx < 0) idx = g_size - 1; continue; } double w = (g_vol[idx] > 0.0 ? g_vol[idx] : 1.0); if(prev != DBL_MAX) { if(p > prev) signedVol += w; else if(p < prev) signedVol -= w; } totalVol += w; prev = p; idx--; if(idx < 0) idx = g_size - 1; } return (totalVol ? signedVol / totalVol : 0.0); // normalized to [-1,1] }
Вспомогательные функции для объектов, панели и меток (EnsureObj, SetLabelIfChanged, SetRectIfChanged).
Вспомогательные функции UI позволяют сократить объем повторяющегося кода по созданию объектов и обновлению их свойств, а также избежать лишних вызовов ObjectSet* благодаря сравнению текущих значений перед их изменением. Это уменьшает мерцание и нагрузку на процессор, поскольку вызовы API терминала для создания объектов и изменения их свойств не бесплатны с точки зрения ресурсов. EnsureObj централизует логику создания объектов, а SetLabelIfChanged и SetRectIfChanged стандартизируют обновление свойств меток и фоновых прямоугольников. С точки зрения разработки ПО это хорошая практика – изолировать особенности платформы и сохранять компактность основной логики. В документации стоит отметить, что эти помощники также последовательно делают объекты невыделяемыми, предотвращая случайные манипуляции с графиком и превращая панель скорее в дашборд, чем в набор интерактивных графических объектов.// Create object if missing void EnsureObj(string name, ENUM_OBJECT type) { if(ObjectFind(0, name) == -1) ObjectCreate(0, name, type, 0, 0, 0); } // Update label text & position only if changed (reduces redraws) void SetLabelIfChanged(string name, int corner, int xdist, int ydist, string text, int fontsize, color col, string font) { EnsureObj(name, OBJ_LABEL); if(ObjectGetInteger(0,name,OBJPROP_CORNER) != corner) ObjectSetInteger(0,name,OBJPROP_CORNER,corner); if(ObjectGetInteger(0,name,OBJPROP_XDISTANCE) != xdist) ObjectSetInteger(0,name,OBJPROP_XDISTANCE,xdist); if(ObjectGetInteger(0,name,OBJPROP_YDISTANCE) != ydist) ObjectSetInteger(0,name,OBJPROP_YDISTANCE,ydist); if(ObjectGetString(0,name,OBJPROP_TEXT) != text) ObjectSetString(0,name,OBJPROP_TEXT,text); if(ObjectGetInteger(0,name,OBJPROP_FONTSIZE) != fontsize) ObjectSetInteger(0,name,OBJPROP_FONTSIZE,fontsize); if(ObjectGetInteger(0,name,OBJPROP_COLOR) != (int)col) ObjectSetInteger(0,name,OBJPROP_COLOR,col); if(ObjectGetString(0,name,OBJPROP_FONT) != font) ObjectSetString(0,name,OBJPROP_FONT,font); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); } // Update rectangle only when a property changed void SetRectIfChanged(string name,int corner,int xdist,int ydist,int xsize,int ysize,uint bgARGB) { EnsureObj(name, OBJ_RECTANGLE_LABEL); if(ObjectGetInteger(0,name,OBJPROP_CORNER) != corner) ObjectSetInteger(0,name,OBJPROP_CORNER,corner); if(ObjectGetInteger(0,name,OBJPROP_XDISTANCE) != xdist) ObjectSetInteger(0,name,OBJPROP_XDISTANCE,xdist); if(ObjectGetInteger(0,name,OBJPROP_YDISTANCE) != ydist) ObjectSetInteger(0,name,OBJPROP_YDISTANCE,ydist); if(ObjectGetInteger(0,name,OBJPROP_XSIZE) != xsize) ObjectSetInteger(0,name,OBJPROP_XSIZE,xsize); if(ObjectGetInteger(0,name,OBJPROP_YSIZE) != ysize) ObjectSetInteger(0,name,OBJPROP_YSIZE,ysize); if((uint)ObjectGetInteger(0,name,OBJPROP_BGCOLOR) != bgARGB) ObjectSetInteger(0,name,OBJPROP_BGCOLOR,bgARGB); if((uint)ObjectGetInteger(0,name,OBJPROP_COLOR) != bgARGB) ObjectSetInteger(0,name,OBJPROP_COLOR,bgARGB); ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false); }
Создание панели и компоновка элементов (CreatePanelObjects и UpdatePanelObjects).
CreatePanelObjects создает набор меток и прямоугольников, из которых состоит информационная панель, а UpdatePanelObjects выступает движком компоновки: вычисляет ширину текста, размеры полос и позиции, а затем обновляет свойства, чтобы визуально отразить последние метрики. Он использует константу ширины символа для оценки ширины меток, вычисляет доступное пространство для полос и рисует на переднем плане пропорциональные полосы для спреда, ATR и потока.
Код выбирает компактный визуальный язык – заголовок, строки сводки, три горизонтальные полосы и временную метку внизу, чтобы трейдер видел ключевые сигналы с первого взгляда. Компоновка панели, во-первых, связывает каждый элемент UI с его базовой переменной (spPips – полоса спреда, flow – полоса потока), а во-вторых, объясняет компромиссы этого дизайна. А именно: предварительное вычисление приблизительных значений ширины текста позволяет избежать сложных вызовов метрик шрифта, но в редких шрифтах могут слегка нарушать выравнивание; выбранное альфа-смешивание поддерживает читаемость интерфейса без загромождения графика.
// Minimal set of dashboard objects used by UpdatePanelObjects void CreatePanelObjects() { EnsureObj("ESGP_bg", OBJ_RECTANGLE_LABEL); EnsureObj("ESGP_hdr", OBJ_LABEL); EnsureObj("ESGP_lbl", OBJ_LABEL); EnsureObj("ESGP_vwap", OBJ_LABEL); EnsureObj("ESGP_spbar", OBJ_RECTANGLE_LABEL); EnsureObj("ESGP_spbar_fg", OBJ_RECTANGLE_LABEL); EnsureObj("ESGP_flow_lbl", OBJ_LABEL); // Make them non-selectable by default string objs[] = {"ESGP_bg","ESGP_hdr","ESGP_lbl","ESGP_vwap","ESGP_spbar","ESGP_spbar_fg","ESGP_flow_lbl"}; for(int i=0;i<ArraySize(objs);i++) ObjectSetInteger(0,objs[i],OBJPROP_SELECTABLE,false); }
Управление алертами и визуальными маркерами.
Подсистема алертов поддерживает три режима: одиночную замену, скользящую очистку (уникальные имена на основе временных меток) и кольцевой пул маркеров. Алерты при необходимости рисуются только тогда, когда выполняется проверка на "дешевый" спред и поток пересекает заданные пороги; для предотвращения мерцания используется гистерезис. Код сохраняет временную метку в OBJPROP_TOOLTIP для каждого маркера, а AutoClearOldAlerts удаляет маркеры старше InpAlertMaxAgeSec. Это элегантный способ вести журнал прямо на графике с минимумом зависимостей: он не требует внешнего хранилища и при этом оставляет трейдерам наглядную и проверяемую историю событий сигнала. Эти проектные решения повышают практическую пригодность инструмента: постоянные маркеры упрощают обзор и тестирование на исторических данных через визуальное сравнение, а гистерезис снижает шум и количество ложных повторных срабатываний.// Draw a simple buy/sell alert label in the panel corner void DrawAlertMarker(bool isBuy) { string nm = (isBuy ? "ESGP_alert_buy" : "ESGP_alert_sell"); if(ObjectFind(0, nm) != -1) ObjectDelete(0, nm); ObjectCreate(0, nm, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(0, nm, OBJPROP_CORNER, COR_LT); ObjectSetInteger(0, nm, OBJPROP_XDISTANCE, 8); ObjectSetInteger(0, nm, OBJPROP_YDISTANCE, isBuy ? 60 : 80); ObjectSetString(0, nm, OBJPROP_TEXT, isBuy ? "▲ BUY" : "▼ SELL"); ObjectSetInteger(0, nm, OBJPROP_FONTSIZE, 12); ObjectSetInteger(0, nm, OBJPROP_COLOR, isBuy ? clrLime : clrRed); ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false); ObjectSetString(0, nm, OBJPROP_TOOLTIP, IntegerToString((int)TimeCurrent())); // store timestamp }
OnInit и OnDeinit: управление жизненным циклом.
OnInit выполняет все необходимые задачи при запуске: выбор символа, при необходимости изменение таймфрейма графика, получение параметров символа (SYMBOL_POINT, SYMBOL_VOLUME_STEP и т.д.), создание индикатора ATR через iATR, инициализацию кольцевого буфера, создание объектов панели и настройку таймера через EventSetTimer. На каждом шаге проверяется наличие ошибок (например, недопустимый хэндл ATR) и возвращается INIT_FAILED, если критически важная инициализация завершается неудачей, – это важный паттерн безопасности.int OnInit() { g_sym = (_Symbol); if(!SymbolSelect(g_sym, true)) return INIT_FAILED; g_point = SymbolInfoDouble(g_sym, SYMBOL_POINT); g_tickVal = SymbolInfoDouble(g_sym, SYMBOL_TRADE_TICK_VALUE); g_tickSize= SymbolInfoDouble(g_sym, SYMBOL_TRADE_TICK_SIZE); g_volMin = SymbolInfoDouble(g_sym, SYMBOL_VOLUME_MIN); g_volMax = SymbolInfoDouble(g_sym, SYMBOL_VOLUME_MAX); g_volStep = SymbolInfoDouble(g_sym, SYMBOL_VOLUME_STEP); // create ATR handle for InpTF and InpATRperiod (example uses default TF & period) g_atrHandle = iATR(g_sym, PERIOD_M1, 14); if(g_atrHandle == INVALID_HANDLE) return INIT_FAILED; BufInit((int)InpRingSize); CreatePanelObjects(); EventSetTimer((int)InpTimerSec); return INIT_SUCCEEDED; }
OnDeinit выполняет очистку: останавливает таймер, удаляет объекты панели и алертов, а также освобождает хэндлы индикаторов. Если не освобождать хэндлы индикаторов и не отключать таймеры, в терминале могут возникать скрытые утечки памяти и хэндлов, а при повторном подключении советника – неожиданное поведение.
void OnDeinit(const int reason) { EventKillTimer(); // remove panel objects (prefix "ESGP_") int total = ObjectsTotal(0); for(int i=total-1; i>=0; --i) { string nm = ObjectName(0, i); if(StringFind(nm, "ESGP_", 0) == 0) ObjectDelete(0, nm); } if(g_atrHandle != INVALID_HANDLE) { IndicatorRelease(g_atrHandle); g_atrHandle = INVALID_HANDLE; } }
Стратегия приема тиков в OnTick.
OnTick намеренно сделан легковесным: он считывает последний тик через SymbolInfoTick и добавляет его в кольцевой буфер через BufAdd. Тяжелая обработка перенесена в OnTimer. Это важное архитектурное решение: некоторые инструменты генерируют сотни или тысячи тиков в секунду, и выполнение затратных обновлений интерфейса или чтений индикатора внутри OnTick может перегрузить главный поток и замедлить терминал. Разделяя прием данных и обработку, советник остается отзывчивым и детерминированным. Эта архитектура также упрощает тестирование: OnTimer работает с контролируемой частотой, поэтому наблюдаемое поведение легче воспроизвести.void OnTick() { MqlTick q; if(SymbolInfoTick(g_sym, q)) { g_latestTick = q; // store latest tick BufAdd(q); // append into ring buffer (fast) } }
Основной цикл OnTimer – вычисления, принятие решений, интерфейс и алерты.
OnTimer – это оркестратор: он поддерживает актуальность буфера, выдерживает заданную периодичность обновления, вычисляет спред и ATR в пипсах, применяет проверку "дешевого" спреда, вызывает функции VWAP и Flow, рассчитывает размер позиции и параметры риска, решает, нужно ли обновлять панель (по порогам изменений), и оценивает условия алерта. Процедура расчета размера позиции с учетом риска вычисляет стоп на основе ATR и InpStopATRmult, переводит расстояние до стопа в тики и денежный риск на лот, а затем определяет такой размер лота, который ограничивает риск по счету в соответствии с InpRiskPct, округляя его к шагу объема g_volStep и ограничивая диапазоном от минимально до максимально допустимого объема. Для срабатывания алерта нужны оба условия: "дешевый" спред и поток выше порога; гистерезис через InpFlowHystFactor предотвращает быстрое переключение состояния. Этот абзац в статье можно подать как объяснение "движка принятия решений" – он показывает, как фильтрация издержек (спред против ATR), подтверждение направления (индикатор потока) и управление капиталом (расчет риска и размера лота) объединяются в практический торговый сигнал.
Важно прямо указать, что индикатор потока Flow в текущей реализации основан на подсчете тиков: он измеряет частоту тиков вверх и тиков вниз, а не суммарный объем, стоящий за этими движениями. Этот выбор делает метрику устойчивой на потоках, где нет надежного объема по каждому тику, но при этом она может искажаться множеством мелких тиков по сравнению с одной крупной сделкой. VWAP здесь реализован как скользящий VWAP с временным окном в минутах, а не как сессионный VWAP, и это влияет на интерпретацию: скользящий VWAP быстрее реагирует на изменения, тогда как сессионный VWAP дает внутридневной ориентир.
Спред нормализуется в пипсы с помощью g_point, а ATR считывается в точках и затем переводится в пипсы, чтобы сравнение было корректным. Здесь уместно рассмотреть и реализованные формулы, и альтернативные варианты. При желании можно перейти и к объемно-взвешенному потоку: для этого нужно изменить Acc так, чтобы функция накапливала знаковые объемы и делила их на общий объем.
void OnTimer() { // 1) ensure buffer has latest tick (sometimes needed) MqlTick t; if(SymbolInfoTick(g_sym, t)) { if(g_used == 0 || g_time[g_head] != t.time) BufAdd(t); } // 2) compute metrics double spPips = SpreadPips(); double atrPts = ATR(); // ATR in price units (points) double atrPips = (atrPts > 0 && g_point > 0) ? atrPts / g_point : 0.0; bool cheap = (atrPts > 0 && spPips < atrPips * InpCheapSpreadFrac); double vwap = VWAP(InpVWAPminutes); double flow = Flow(InpImbWindowSec); // tick-count flow // 3) Example risk sizing (conservative) double bal = AccountInfoDouble(ACCOUNT_BALANCE); double bid = SymbolInfoDouble(g_sym, SYMBOL_BID); double stop = bid - InpStopATRmult * atrPts; double riskPx = bid - stop; double ticks = (g_tickSize > 0 ? riskPx / g_tickSize : 0.0); double cashPerLot = ticks * g_tickVal; double maxLoss = bal * InpRiskPct / 100.0; double rawLot = (cashPerLot > 0.0 ? maxLoss / cashPerLot : 0.0); double lot = (g_volStep > 0.0 ? MathFloor(rawLot / g_volStep) * g_volStep : 0.0); // 4) Alerting with hysteresis double resetThresh = InpFlowTh * InpFlowHystFactor; static bool buyF=false, sellF=false; if(cheap) { if(flow >= InpFlowTh && !buyF) { Alert("BUY edge: cheap spread + buy flow"); buyF = true; sellF = false; DrawAlertMarker(true); } else if(flow < resetThresh) buyF = false; if(flow <= -InpFlowTh && !sellF) { Alert("SELL edge: cheap spread + sell flow"); sellF = true; buyF = false; DrawAlertMarker(false); } else if(flow > -resetThresh) sellF = false; } }
Наконец, о стресс-тестировании: детализация тиков в тестере стратегий может не отражать поведение реального потока, поэтому для проверки логики на тиках нужны либо режим воспроизведения тиков, либо исторические тиковые данные. Логирование должно быть умеренным: на активных инструментах не стоит засыпать журнал вызовами Print; при небольшом InpTimerSec также желательно профилировать нагрузку на процессор. Рекомендуемые стартовые значения параметров: InpVWAPminutes – 60-240 для внутридневной работы; InpImbWindowSec – 10-60 для выявления микроструктуры; InpCheapSpreadFrac – 0.3-0.7 в зависимости от инструмента. Рекомендуется поэтапная настройка: начните с консервативных порогов, сначала проверьте сигналы по визуальным маркерам и только после строгого тестирования на реальных данных включайте автоторговлю. Можно предложить и дальнейшие улучшения: добавить режим сессионного VWAP, сделать поток объемно-взвешенным, ввести EMA-сглаживание для индикатора потока, чтобы уменьшить число ложных рывков, а также предусмотреть запись истории тиков для воспроизводимого офлайн-анализа.
Результаты
После разработки советника следующим шагом стало развертывание для тестирования, которое можно проводить либо на демо-счете, либо в режиме тестирования на исторических данных. Ниже я приведу серию изображений, сделанных во время тестирования в терминале MetaTrader 5, чтобы показать работу системы и ее поведение.
На иллюстрации ниже панель показывает ранее выданный алерт BUY (зеленый заголовок / маркер BUY). Однако текущий поток на коротком окне слегка отрицательный: значение дисбаланса составляет примерно -10,3%, а значение потока – -0,10.
Спред в 71.0 пипс относительно мал по сравнению с текущей волатильностью, поэтому система считает его "дешевым" (условие выполнено). Поскольку ATR здесь довольно велик и составляет 469.4 пипса, защитные уровни на его основе получаются широкими: примерно 563.3 пипса для стопа и 1126.6 пипса для TP2R, что дает соотношение риск-прибыль 2.00.
В целом стоимостный фильтр советника срабатывает: условие "дешевого" спреда выполнено. Однако на этот момент подтверждение направления через индикатор потока слегка медвежье, поэтому система не формирует новый сигнал на покупку, несмотря на маркер BUY, оставшийся от более раннего сигнала.
После первоначального сигнала на покупку рынок со временем пошел в направлении сделки. Это положительное движение показано на следующей иллюстрации. Сейчас система уже показывает новый сигнал на продажу, отражающий изменившиеся рыночные условия.
Впоследствии рынок развернулся вниз, подтвердив состоятельность сигнала. Это движение показано на иллюстрации ниже.
Каждый сигнал сопровождается алертом и записывается во вкладке "Эксперты" в MetaTrader 5, что упрощает отслеживание и разбор событий.
Заключение
Инструмент Slippage Tool разработан как решение практической и распространенной проблемы розничных трейдеров: доступ к надежным данным книги ордеров часто ограничен, хотя сигналы потока ордеров все равно нужны для поиска точек входа с более высокой вероятностью. Вместо того чтобы пытаться заменить полноценную лестницу глубины рынка, система извлекает из доступного потока тиков наиболее полезные микроструктурные сигналы. Система использует скользящую VWAP как якорь справедливой стоимости, метрики дисбаланса и потока на коротком окне для выявления направленного давления, а также связку спреда и ATR, чтобы сигналы оставались и торгово пригодными, и осмысленными с точки зрения риска. В сочетании с визуализацией на графике, алертами и автоматическим расчетом размера позиции инструмент превращает сырые тиковые данные в информацию, готовую к принятию решений, в реальном времени.
Slippage Tool отличает именно прагматичный акцент на надежности и удобстве для пользователя. Такие решения, как кольцевой буфер и легковесная обработка данных в OnTick, помогают сохранять производительность терминала на инструментах с высокой скоростью появления тиков; защитные вспомогательные функции и отслеживание изменений предотвращают лишние перерисовки; алерты же используют гистерезис и настраиваемые режимы маркеров, чтобы уменьшить шум и число ложных сигналов. Встроенная логика расчета размера позиции связывает генерацию сигнала с реальным управлением капиталом: каждая потенциальная сделка оценивается через стопы на базе ATR и риск по счету, что поддерживает дисциплинированный размер позиции вместо безрассудного набора объема.
При этом важно ясно понимать ограничения инструмента. Он является лишь приближенным представлением информации из книги ордеров, не предоставляя поток данных из стакана напрямую: значение потока выводится из количества тиков (или, опционально, рассчитывается с учетом объема), а VWAP восстанавливается по принтам и срединным ценам. Поэтому сигналы нужно проверять в контексте графика, нескольких таймфреймов и воспроизведения тиковых данных. Такие параметры, как окно VWAP, окно дисбаланса, доля спреда и пороговые значения потока, нужно регулировать в соответствии со скоростью появления тиков и волатильностью конкретного инструмента; универсальной конфигурации здесь нет.
Тем, кто хочет внедрять систему уверенно, стоит придерживаться поэтапного подхода: (1) визуально проверить сигналы по панели и маркерам в режиме воспроизведения или на демо-счете; (2) консервативно настроить пороги и проверить работу в тесте на реальных данных; (3) только после этого рассматривать автоматизацию входов с надежными правилами для стопов и таймаута, а также с полноценным логированием. В будущем можно добавить выбор режима сессионного VWAP, опциональный объемно-взвешенный расчет потока, EMA-сглаживание для снижения числа ложных рывков и экспортируемый журнал событий для воспроизводимого анализа.
Читайте другие мои статьи здесь.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/19290
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Алгоритм Цветовой Гармонии — Color Harmony Algorithm (CHA)
Как обучить MLP на признаках марковской цепи в MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Почему нет панели?
сэр,
Почему нет панели?