English
preview
Создание индикатора профиля объёма "Спектр ликвидности" (Liquidity Spectrum) на языке MQL5

Создание индикатора профиля объёма "Спектр ликвидности" (Liquidity Spectrum) на языке MQL5

MetaTrader 5 — Индикаторы |
53 2
ALGOYIN LTD
Israel Pelumi Abioye

Содержание

  1. Введение
  2. Обзор проекта и план реализации
  3. Создание индикатора профиля объёма «Спектр ликвидности» на языке MQL5
  4. Заключение

Введение

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

  • получение согласованного набора данных за период анализа с помощью функций CopyHigh, CopyLow, CopyClose и CopyTickVolume (с использованием резервных вариантов);
  • распределение объёма по равным ценовым интервалам в пределах диапазона максимумов/минимумов на основе цен закрытия и нормализация этих интервалов;
  • управление объектами графика и их размещением во времени посредством создания, обновления и удаления объектов с заданным префиксом, а также преобразования смещений в барах в позиции datetime.

Цель — создать рабочий и воспроизводимый индикатор профиля объёма «Спектр ликвидности» для MT5, который устраняет неопределённость относительно того, где сосредоточена реальная рыночная активность.


Обзор проекта и план реализации

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

Что мы создаём

В рамках этого проекта мы разрабатываем индикатор профиля объёма «Спектр ликвидности» на языке MQL5. Основная цель данного индикатора — наглядно показать распределение объёма торгов по различным ценовым уровням за заданный период анализа. Вместо того чтобы отображать лишь динамику цены или одно значение объёма, индикатор разбивает рынок на несколько ценовых интервалов (бинов). Затем индикатор рассчитывает объём, приходящийся на каждый ценовой интервал, и наглядно отображает эту информацию на графике. Каждый бин отражает определённую ценовую зону, а его объём определяет, насколько широким и заметным он будет выглядеть. Зоны с высоким объёмом становятся шире и заметнее, тогда как зоны с более низким объёмом остаются меньшими. Кроме того, индикатор выделяет зоны с высоким объёмом, известные как точки контроля (POC, point of control), которые представляют собой наиболее активные ценовые уровни с наибольшей торговой активностью.

Эти уровни POC отрисовываются в виде горизонтальных линий, чтобы выделить ключевые зоны ликвидности, на которые рынок, скорее всего, отреагирует.

Рисунок 1. Профиль объёма «Спектр ликвидности»

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

План реализации

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

Извлечение данных о свечах с помощью функций копирования

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

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

Определение ценового диапазона в пределах периода анализа

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

Например, если период анализа установлен равным 18, мы будем анализировать последние 18 свечей на графике. Мы определяем наивысший максимум и наименьший минимум среди этих свечей. Ценовой диапазон для нашего анализа составляет от 1,1950 до 1,2050, если максимальная цена равна 1,2050, а минимальная — 1,1950.

Рисунок 2. Диапазон анализа

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

Разделение ценового диапазона на ценовые интервалы и расчёт распределения объёма

Следующим шагом после определения ценового диапазона является его разбиение на более мелкие интервалы и оценка распределения объёма в каждом из них. Бин — это узкий ценовой диапазон (интервал). Чтобы определить, где сосредоточена активность, мы разбиваем ценовой диапазон на несколько более мелких диапазонов, а не рассматриваем его как единый блок. Предположим, что мы разделим ценовой диапазон от 1,1950 до 1,2050 на девять ценовых интервалов. Каждый ценовой интервал будет представлять равную часть общего диапазона, в результате чего мы получим девять более мелких ценовых интервалов вместо одного большого диапазона.

Рисунок 3. Ценовые интервалы

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

Рисунок 4. Распределение объёмов

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

Отрисовка профиля объёма и линий POC

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

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

После этого приступаем к построению профиля объёма. Для каждого ценового интервала мы будем:

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

Рисунок 5. Профиль объёма

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

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

  • рассчитываем середину ценового диапазона
  • проводим горизонтальную линию, чтобы выделить этот уровень

Рисунок 6. Точки контроля (POC)


Создание индикатора профиля объёма «Спектр ликвидности» на языке MQL5

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

Извлечение данных о свечах с помощью функций копирования

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

Пример:
#property indicator_chart_window
#property indicator_plots 0
//--- INPUT PARAMETERS
input int   InpLookback      = 100;   // Number of bars used for calculation
input bool  InpVolumeProfile = true;  // Display volume profile boxes
input bool  InpLiqLevels     = true;  // Display POC (liquidity) lines

//+------------------------------------------------------------------+
//| Core calculation function                                        |
//| Builds and draws the volume profile                              |
//+------------------------------------------------------------------+
void RecalcVolumeProfile()
  {
//--- Exit early if both features are disabled
   if(!InpVolumeProfile && !InpLiqLevels)
      return;

   int totalBars = Bars(_Symbol, _Period);
   int lookback  = MathMin(InpLookback, totalBars - 1);
   if(lookback < 2)
      return;

//--- Prepare arrays for market data
   double   hi[], lo[], cl[];
   long     vol[];
   datetime tm[];
   ArraySetAsSeries(hi,true);
   ArraySetAsSeries(lo,true);
   ArraySetAsSeries(cl,true);
   ArraySetAsSeries(vol,true);
   ArraySetAsSeries(tm,true);

//--- Copy required data from terminal
   if(CopyHigh(_Symbol,_Period,0,lookback,hi)  < lookback)
      return;
   if(CopyLow(_Symbol,_Period,0,lookback,lo)  < lookback)
      return;
   if(CopyClose(_Symbol,_Period,0,lookback,cl)  < lookback)
      return;
   if(CopyTime(_Symbol,_Period,0,lookback,tm)  < lookback)
      return;
//--- Prefer tick volume, fallback to real volume
   if(CopyTickVolume(_Symbol,_Period,0,lookback,vol) < lookback)
      if(CopyRealVolume(_Symbol,_Period,0,lookback,vol) < lookback)
         return;
  }

Пояснение:

В этом разделе показано, как работает индикатор, и выполняется его подготовка к основному расчету. Все визуальные компоненты, такие как прямоугольники объёма и линии POC, будут отображаться непосредственно на ценовом графике, поскольку в начале индикатор настроен на отрисовку прямо в главном окне графика. Кроме того, указывается, что, поскольку вся отрисовка будет выполняться вручную с использованием объектов графика, встроенные индикаторные построения (plots) применяться не будут. Далее мы определяем входные параметры. Значение параметра Lookback определяет, сколько свечей будет анализировать индикатор. Два логических параметра позволяют пользователю выбрать, отображать ли прямоугольники профиля объёма и линии ликвидности точки контроля (POC). Эти параметры позволяют гибко включать или отключать отдельные элементы визуализации без изменения кода.

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

Определение ценового диапазона в пределах периода анализа

Следующим шагом является определение ценового диапазона в пределах периода анализа.

Пример:

#define OBJ_PREFIX      "VP_"  // Prefix for all chart objects
//--- INPUT PARAMETERS
input int   InpLookback      = 100;   // Number of bars used for calculation
input bool  InpVolumeProfile = true;  // Display volume profile boxes
input bool  InpLiqLevels     = true;  // Display POC (liquidity) lines

//--- CONSTANTS
#define N_BINS          100    // Number of price levels (bins)

//+------------------------------------------------------------------+
//| Delete all indicator objects                                     |
//+------------------------------------------------------------------+
void DeleteAllObjects()
  {
   for(int i = ObjectsTotal(0, 0, -1) - 1; i >= 0; i--)
     {
      string n = ObjectName(0, i, 0, -1);
      if(StringFind(n, OBJ_PREFIX) == 0)
         ObjectDelete(0, n);
     }
  }
//+------------------------------------------------------------------+
//| Core calculation function                                        |
//| Builds and draws the volume profile                              |
//+------------------------------------------------------------------+
void RecalcVolumeProfile()
  {

//--- Exit early if both features are disabled
   if(!InpVolumeProfile && !InpLiqLevels)
      return;
   int totalBars = Bars(_Symbol, _Period);
   int lookback  = MathMin(InpLookback, totalBars - 1);
   if(lookback < 2)
      return;

//--- Prepare arrays for market data
   double   hi[], lo[], cl[];
   long     vol[];
   datetime tm[];
   ArraySetAsSeries(hi,true);
   ArraySetAsSeries(lo,true);
   ArraySetAsSeries(cl,true);
   ArraySetAsSeries(vol,true);
   ArraySetAsSeries(tm,true);

//--- Copy required data from terminal
   if(CopyHigh(_Symbol,_Period,0,lookback,hi)  < lookback)
      return;
   if(CopyLow(_Symbol,_Period,0,lookback,lo)  < lookback)
      return;
   if(CopyClose(_Symbol,_Period,0,lookback,cl)  < lookback)
      return;
   if(CopyTime(_Symbol,_Period,0,lookback,tm)  < lookback)
      return;
//--- Prefer tick volume, fallback to real volume
   if(CopyTickVolume(_Symbol,_Period,0,lookback,vol) < lookback)
      if(CopyRealVolume(_Symbol,_Period,0,lookback,vol) < lookback)
         return;

//--- Determine price range
   double priceMax = hi[ArrayMaximum(hi, 0, lookback)];
   double priceMin = lo[ArrayMinimum(lo, 0, lookback)];
   if(priceMax <= priceMin)
      return;

   int idxHigh = ArrayMaximum(hi, 0, lookback);
   int idxLow  = ArrayMinimum(lo, 0, lookback);

   string lookback_line = OBJ_PREFIX + "_LL";
   string high_range = OBJ_PREFIX + "_HR";
   string low_range = OBJ_PREFIX + "_LR";
   ObjectCreate(0,lookback_line,OBJ_VLINE,0,tm[lookback - 1],0);
   ObjectCreate(0,high_range,OBJ_TREND,0,tm[lookback - 1],priceMax,tm[0],priceMax);
   ObjectCreate(0,low_range,OBJ_TREND,0,tm[lookback - 1],priceMin,tm[0],priceMin);
  }

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   RecalcVolumeProfile();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Deinitialization                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   DeleteAllObjects();
  }
//+------------------------------------------------------------------+


Вывод:

Рисунок 7. Ценовой диапазон

Пояснение:

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

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

При удалении индикатора с графика все созданные объекты удаляются с помощью функции DeleteAllObjects(). Это удаляет все объекты графика с префиксом «VP_».

Разделение ценового диапазона на ценовые интервалы и расчёт распределения объёмов

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

Пример:

//+------------------------------------------------------------------+
//| Core calculation function                                        |
//| Builds and draws the volume profile                              |
//+------------------------------------------------------------------+
void RecalcVolumeProfile()
  {
//--- Exit early if both features are disabled
   if(!InpVolumeProfile && !InpLiqLevels)
      return;

   int totalBars = Bars(_Symbol, _Period);
   int lookback  = MathMin(InpLookback, totalBars - 1);
   if(lookback < 2)
      return;

//--- Prepare arrays for market data
   double   hi[], lo[], cl[];
   long     vol[];
   datetime tm[];
   ArraySetAsSeries(hi,true);
   ArraySetAsSeries(lo,true);
   ArraySetAsSeries(cl,true);
   ArraySetAsSeries(vol,true);
   ArraySetAsSeries(tm,true);

//--- Copy required data from terminal
   if(CopyHigh(_Symbol,_Period,0,lookback,hi)  < lookback)
      return;
   if(CopyLow(_Symbol,_Period,0,lookback,lo)  < lookback)
      return;
   if(CopyClose(_Symbol,_Period,0,lookback,cl)  < lookback)
      return;
   if(CopyTime(_Symbol,_Period,0,lookback,tm)  < lookback)
      return;
//--- Prefer tick volume, fallback to real volume
   if(CopyTickVolume(_Symbol,_Period,0,lookback,vol) < lookback)
      if(CopyRealVolume(_Symbol,_Period,0,lookback,vol) < lookback)
         return;

//--- Determine price range
   double priceMax = hi[ArrayMaximum(hi, 0, lookback)];
   double priceMin = lo[ArrayMinimum(lo, 0, lookback)];
   if(priceMax <= priceMin)
      return;

   int idxHigh = ArrayMaximum(hi, 0, lookback);
   int idxLow  = ArrayMinimum(lo, 0, lookback);

   string lookback_line = OBJ_PREFIX + "_LL";
   string high_range = OBJ_PREFIX + "_HR";
   string low_range = OBJ_PREFIX + "_LR";
   ObjectCreate(0,lookback_line,OBJ_VLINE,0,tm[lookback - 1],0);
   ObjectCreate(0,high_range,OBJ_TREND,0,tm[lookback - 1],priceMax,tm[0],priceMax);
   ObjectCreate(0,low_range,OBJ_TREND,0,tm[lookback - 1],priceMin,tm[0],priceMin);

//--- Divide price range into bins
   double step = (priceMax - priceMin) / N_BINS;

//--- build bins
   double bins[];
   ArrayResize(bins, N_BINS);
   ArrayInitialize(bins, 0.0);

//--- Assign volume to bins
   for(int i = 0; i < N_BINS; i++)
     {
      double lower = priceMin + step * i;
      double upper = lower + step;
      string bin_box = OBJ_PREFIX + "BIN_" + IntegerToString(i);
      ObjectCreate(0,bin_box,OBJ_RECTANGLE,0,tm[0],lower,tm[lookback - 1],upper);
      ObjectSetInteger(0,bin_box,OBJPROP_COLOR,clrBlue);
      for(int j = 0; j < lookback; j++)
        {
         double c = cl[j];
         if(c >= lower - step && c <= upper + step)
           {
            bins[i] += (double)vol[j];
            string bin_volume = OBJ_PREFIX + "VOL_" + IntegerToString(i);
            ObjectCreate(0,bin_volume,OBJ_TEXT,0,tm[0],lower);
            ObjectSetString(0,bin_volume,OBJPROP_TEXT,DoubleToString(bins[i]));
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   RecalcVolumeProfile();
//---
   return(INIT_SUCCEEDED);
  }

Вывод:

Рисунок 8. Распределение объёмов по ценовым интервалам

Пояснение:

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

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

Ключевым условием здесь является проверка:
c >= lower - step && c <= upper + step

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

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

Построение профиля объёма и линий POC

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

Пример:

//--- CONSTANTS
#define N_BINS          100    // Number of price levels (bins)
#define MAX_BAR_WIDTH   50     // Maximum horizontal width of profile
//+------------------------------------------------------------------+
//| Core calculation function                                        |
//| Builds and draws the volume profile                              |
//+------------------------------------------------------------------+
void RecalcVolumeProfile()
  {
//--- Exit early if both features are disabled
   if(!InpVolumeProfile && !InpLiqLevels)
      return;

   int totalBars = Bars(_Symbol, _Period);
   int lookback  = MathMin(InpLookback, totalBars - 1);

   if(lookback < 2)
      return;

//--- Prepare arrays for market data
   double   hi[], lo[], cl[];
   long     vol[];
   datetime tm[];

   ArraySetAsSeries(hi,true);
   ArraySetAsSeries(lo,true);
   ArraySetAsSeries(cl,true);
   ArraySetAsSeries(vol,true);
   ArraySetAsSeries(tm,true);

//--- Copy required data from terminal
   if(CopyHigh(_Symbol,_Period,0,lookback,hi)  < lookback)
      return;
   if(CopyLow(_Symbol,_Period,0,lookback,lo)  < lookback)
      return;
   if(CopyClose(_Symbol,_Period,0,lookback,cl)  < lookback)
      return;
   if(CopyTime(_Symbol,_Period,0,lookback,tm)  < lookback)
      return;
//--- Prefer tick volume, fallback to real volume
   if(CopyTickVolume(_Symbol,_Period,0,lookback,vol) < lookback)
      if(CopyRealVolume(_Symbol,_Period,0,lookback,vol) < lookback)
         return;
         
//--- Determine price range
   double priceMax = hi[ArrayMaximum(hi, 0, lookback)];
   double priceMin = lo[ArrayMinimum(lo, 0, lookback)];
   if(priceMax <= priceMin)
      return;

   int idxHigh = ArrayMaximum(hi, 0, lookback);
   int idxLow  = ArrayMinimum(lo, 0, lookback);

//--- Divide price range into bins
   double step = (priceMax - priceMin) / N_BINS;

//--- build bins
   double bins[];
   ArrayResize(bins, N_BINS);
   ArrayInitialize(bins, 0.0);

//--- Assign volume to bins 
   for(int i = 0; i < N_BINS; i++)
     {
      double lower = priceMin + step * i;
      double upper = lower + step;
      for(int j = 0; j < lookback; j++)
        {
         double c = cl[j];
         if(c >= lower - step && c <= upper + step)
           {
            bins[i] += (double)vol[j];
           }
        }
     }

//--- Find maximum volume bin
   double maxBin = 0;
   for(int i = 0; i < N_BINS; i++)
      if(bins[i] > maxBin)
         maxBin = bins[i];
   if(maxBin == 0)
      return;

//--- Loop through bins and draw
   for(int i = 0; i < N_BINS; i++)
     {
      double lower = priceMin + step * i;
      double upper = lower + step;
      double mid   = (lower + upper) * 0.5;

      //--- Normalize bin width
      int val = (int)(bins[i] / maxBin * (double)MAX_BAR_WIDTH);
      if(val < 1)
         continue;
     }

Пояснение:

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

Мы снова просматриваем все ценовые интервалы, чтобы подготовить их к визуализации на графике. Для каждого ценового интервала мы сначала вычисляем его ценовые границы. Нижнее значение получается следующим образом: берётся минимальная цена, а затем выполняется смещение вверх с учётом индекса ценового интервала и размера шага. Верхнее значение находится на один шаг выше нижней границы, что определяет полный ценовой диапазон данного ценового интервала. Кроме того, мы вычисляем середину (mid) ценового интервала, усредняя нижнее и верхнее значения. Эта середина впоследствии пригодится при размещении линий POC (ликвидности) в центре зон с высоким объёмом.

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

bins[i] / maxBin * MAX_BAR_WIDTH

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

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

Пример:

//+------------------------------------------------------------------+
//| Convert "bars ago" into actual chart time                        |
//| This allows drawing objects into the past or future              |
//+------------------------------------------------------------------+
datetime BarsAgoToTime(const datetime &times[], int copiedBars,
                       int barsAgo, long barDur)
  {
//--- Case 1: within available history
   if(barsAgo >= 0 && barsAgo < copiedBars)
      return times[barsAgo];
//--- Case 2: future projection (negative index)
   if(barsAgo < 0)
      return (datetime)((long)times[0] - (long)barsAgo * barDur); // barsAgo is negative
//--- Case 3: beyond copied range
   return (datetime)((long)times[copiedBars-1] - (long)(barsAgo - copiedBars + 1) * barDur);
  }

//+------------------------------------------------------------------+
//| Create or update a rectangle (volume profile bar)                |
//| Returns true if a new object was created                         |
//+------------------------------------------------------------------+
bool DrawBox(const string name,
             datetime x1, double yTop,
             datetime x2, double yBot,
             color fillCol)
  {
   bool created = false;

//--- Create object only if it does not exist
   if(ObjectFind(0, name) < 0)
     {
      ObjectCreate(0, name, OBJ_RECTANGLE, 0, x1, yTop, x2, yBot);
      created = true;
     }

//--- Update object properties
   ObjectSetInteger(0, name, OBJPROP_COLOR, fillCol);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, fillCol);
   ObjectSetInteger(0, name, OBJPROP_FILL, true);
   ObjectSetInteger(0, name, OBJPROP_BACK, true);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);

//--- Update position
   ObjectMove(0, name, 0, x1, yTop);
   ObjectMove(0, name, 1, x2, yBot);
   return created;
  }

//+------------------------------------------------------------------+
//| Delete all indicator objects                                     |
//+------------------------------------------------------------------+
void DeleteAllObjects()
  {
   for(int i = ObjectsTotal(0, 0, -1) - 1; i >= 0; i--)
     {
      string n = ObjectName(0, i, 0, -1);
      if(StringFind(n, OBJ_PREFIX) == 0)
         ObjectDelete(0, n);
     }
  }

//+------------------------------------------------------------------+
//| Core calculation function                                        |
//| Builds and draws the volume profile                              |
//+------------------------------------------------------------------+
void RecalcVolumeProfile(bool &need_redraw)
  {

   DeleteAllObjects();
//--- Exit early if both features are disabled
   if(!InpVolumeProfile && !InpLiqLevels)
      return;
   int totalBars = Bars(_Symbol, _Period);
   int lookback  = MathMin(InpLookback, totalBars - 1);

   if(lookback < 2)
      return;

//--- Prepare arrays for market data
   double   hi[], lo[], cl[];
   long     vol[];
   datetime tm[];
   ArraySetAsSeries(hi,true);
   ArraySetAsSeries(lo,true);
   ArraySetAsSeries(cl,true);
   ArraySetAsSeries(vol,true);
   ArraySetAsSeries(tm,true);

//--- Copy required data from terminal
   if(CopyHigh(_Symbol,_Period,0,lookback,hi)  < lookback)
      return;
   if(CopyLow(_Symbol,_Period,0,lookback,lo)  < lookback)
      return;
   if(CopyClose(_Symbol,_Period,0,lookback,cl)  < lookback)
      return;
   if(CopyTime(_Symbol,_Period,0,lookback,tm)  < lookback)
      return;
//--- Prefer tick volume, fallback to real volume
   if(CopyTickVolume(_Symbol,_Period,0,lookback,vol) < lookback)
      if(CopyRealVolume(_Symbol,_Period,0,lookback,vol) < lookback)
         return;

//--- Determine price range
   double priceMax = hi[ArrayMaximum(hi, 0, lookback)];
   double priceMin = lo[ArrayMinimum(lo, 0, lookback)];
   if(priceMax <= priceMin)
      return;
   int idxHigh = ArrayMaximum(hi, 0, lookback);
   int idxLow  = ArrayMinimum(lo, 0, lookback);

//--- Divide price range into bins
   double step = (priceMax - priceMin) / N_BINS;

//--- build bins
   double bins[];
   ArrayResize(bins, N_BINS);
   ArrayInitialize(bins, 0.0);

//--- Assign volume to bins 
   for(int i = 0; i < N_BINS; i++)
     {
      double lower = priceMin + step * i;
      double upper = lower + step;
      for(int j = 0; j < lookback; j++)
        {
         double c = cl[j];
         if(c >= lower - step && c <= upper + step)
           {
            bins[i] += (double)vol[j];
           }
        }
     }

//--- Find maximum volume bin
   double maxBin = 0;
   for(int i = 0; i < N_BINS; i++)
      if(bins[i] > maxBin)
         maxBin = bins[i];
   if(maxBin == 0)
      return;

//--- Define drawing boundaries
   int profileLeftBarsAgo = lookback + MAX_BAR_WIDTH;
   long barDur = (long)PeriodSeconds(_Period);
   datetime profileLeftTime = BarsAgoToTime(tm, lookback, profileLeftBarsAgo, barDur);

//--- Loop through bins and draw
   for(int i = 0; i < N_BINS; i++)
     {
      double lower = priceMin + step * i;
      double upper = lower + step;
      double mid   = (lower + upper) * 0.5;

      //--- Normalize bin width
      int val = (int)(bins[i] / maxBin * (double)MAX_BAR_WIDTH);
      if(val < 1)
         continue;

      int profileRightBarsAgo = profileLeftBarsAgo - val;
      datetime profileRightTime  = BarsAgoToTime(tm, lookback, profileRightBarsAgo, barDur);
      color    box_line_clr   = (val >= 40) ? clrBlue : (val > 30) ? clrGreen : (val > 20) ? clrGray : (val > 10) ? clrOlive : clrAquamarine;
      //--- Draw volume box
      if(InpVolumeProfile)
        {
         string boxName = OBJ_PREFIX + "BOX_" + IntegerToString(i);
         if(DrawBox(boxName, profileRightTime, upper, profileLeftTime, lower, box_line_clr))
            need_redraw = true;
        }
     }
  }

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   bool redraw = false;
//--- Initial calculation so indicator appears immediately
   RecalcVolumeProfile(redraw);
   if(redraw)
      ChartRedraw(0);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Deinitialization                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   DeleteAllObjects();
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t 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 int32_t &spread[])
  {
//---
   bool need_redraw = false;

//--- Recalculate only when new data arrives
   if(prev_calculated == 0 || rates_total != prev_calculated)
      RecalcVolumeProfile(need_redraw);

//--- Redraw only if something changed
   if(need_redraw)
      ChartRedraw(0);

//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

Вывод:

Рисунок 9. Профиль объёма

Пояснение:

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

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

Переменная profileRightBarsAgo определяет, на какое расстояние правая сторона прямоугольника должна быть смещена относительно левой. Она вычитает нормализованную ширину (val) из начальной позиции (profileLeftBarsAgo), тем самым регулируя, насколько широким будет отображаться каждый прямоугольник объёма. Чем больше объём, тем больше значение val и, следовательно, тем шире становится прямоугольник. Затем profileRightTime использует функцию BarsAgoToTime для преобразования этого смещения в барах в фактическое время на графике. Это позволяет точно расположить прямоугольник на временной оси, даже если он выходит за пределы отображаемых данных свечей. Затем его цвет определяется на основе силы (величины объёма) соответствующего прямоугольника профиля объёма. Более насыщенные оттенки, такие как синий или зелёный, используются для ценовых интервалов с большим объёмом, тогда как более светлые оттенки — для ценовых интервалов с меньшим объёмом.

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

Далее мы переходим к отрисовке линий POC, которые выделяют ключевые уровни ликвидности в профиле объёма.

Пример:
//--- CONSTANTS
#define N_BINS          100    // Number of price levels (bins)
#define MAX_BAR_WIDTH   50     // Maximum horizontal width of profile
#define OBJ_PREFIX      "VP_"  // Prefix for all chart objects
#define POC_THRESHOLD   25     // Threshold for drawing POC lines
//+------------------------------------------------------------------+
//| Create or update a horizontal POC (liquidity) line               |
//| Returns true if a new object was created                         |
//+------------------------------------------------------------------+
bool DrawTrend(const string name,
               datetime x1, double y,
               datetime x2,
               color col, int w)
  {
   bool created = false;
//--- Create only if missing
   if(ObjectFind(0, name) < 0)
     {
      ObjectCreate(0, name, OBJ_TREND, 0, x1, y, x2, y);
      created = true;
     }

//--- Update properties
   ObjectSetInteger(0, name, OBJPROP_COLOR, col);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, w);
   ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false);
   ObjectSetInteger(0, name, OBJPROP_BACK, true);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);

//--- Update position
   ObjectMove(0, name, 0, x1, y);
   ObjectMove(0, name, 1, x2, y);

   return created;
  }
//--- Define drawing boundaries
int profileLeftBarsAgo = lookback + MAX_BAR_WIDTH;
long barDur = (long)PeriodSeconds(_Period);
datetime profileLeftTime = BarsAgoToTime(tm, lookback, profileLeftBarsAgo, barDur);

// POC right end = bar_index + 5 (5 bars into the future)
datetime pocRightTime = BarsAgoToTime(tm, lookback, -5, barDur);

//--- Loop through bins and draw
for(int i = 0; i < N_BINS; i++)
  {
   double lower = priceMin + step * i;
   double upper = lower + step;
   double mid   = (lower + upper) * 0.5;

//--- Normalize bin width
   int val = (int)(bins[i] / maxBin * (double)MAX_BAR_WIDTH);
   if(val < 1)
      continue;

   int profileRightBarsAgo = profileLeftBarsAgo - val;
   datetime profileRightTime  = BarsAgoToTime(tm, lookback, profileRightBarsAgo, barDur);
   color    box_line_clr   = (val >= 40) ? clrBlue : (val > 30) ? clrGreen : (val > 20) ? clrGray : (val > 10) ? clrOlive : clrAquamarine;

//--- Draw volume box
   if(InpVolumeProfile)
     {
      string boxName = OBJ_PREFIX + "BOX_" + IntegerToString(i);
      if(DrawBox(boxName, profileRightTime, upper, profileLeftTime, lower, box_line_clr))
         need_redraw = true;
     }

//--- Draw POC line for strong bins
// POC lines for high-volume bins
   if(val > POC_THRESHOLD && InpLiqLevels)
     {
      string lineName = OBJ_PREFIX + "POC_" + IntegerToString(i);
      int    lineW   = (val > 45) ? 3 : (val > 35) ? 2 : 1;
      if(DrawTrend(lineName, pocRightTime, mid, profileLeftTime, box_line_clr, lineW))
         need_redraw = true;
     }
  }

//--Border right = bar_index + 5 (same as POC)
datetime borderRightTime = pocRightTime;
datetime borderLeftTime = BarsAgoToTime(tm, lookback,
                                        profileLeftBarsAgo + MAX_BAR_WIDTH, barDur);
//--- outer border box
if(DrawBox(OBJ_PREFIX+"BORDER", borderRightTime, priceMax, borderLeftTime, priceMin, clrSnow))
   need_redraw = true;

Вывод:

Рисунок 10. POC

Пояснение:

POC_THRESHOLD — это фиксированное значение, используемое для определения того, какие ценовые интервалы имеют достаточно большой объём, чтобы считаться значимыми уровнями ликвидности. Линии POC будут отрисованы только для тех ценовых интервалов, нормализованный объём которых превышает этот порог, что помогает выделить зоны высокой торговой активности и отфильтровать более слабые зоны. Функция DrawTrend используется для отрисовки POC в виде горизонтальной линии: она создаёт линию, если её ещё нет, и обновляет её ширину, цвет и положение в соответствии с последним уровнем ликвидности, оставляя линию на заднем плане. Проецируя линию на пять баров в будущее, строка datetime pocRightTime = BarsAgoToTime(tm, lookback, -5, barDur); задаёт правую конечную точку линий POC. Функция использует длительность бара для преобразования отрицательного значения, указывающего, что мы выходим за пределы текущих свечей, в допустимую будущую временную метку.

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

 

Заключение

После внедрения описанных проектных и инженерных исправлений вы получаете полноценный практический индикатор профиля объёма «Спектр ликвидности» для MetaTrader 5, который можно тестировать и настраивать. Индикатор:

  • разделяет диапазон между максимумом и минимумом за период анализа на настраиваемые ценовые интервалы и распределяет объём по цене закрытия свечи (предпочтительно использовать тиковый объём, резервный вариант — реальный объём);
  • нормализует объёмы ценовых интервалов и масштабирует прямоугольники до настраиваемой максимальной ширины, чтобы выделить относительную силу;
  • отрисовывает профиль в виде заполненных прямоугольников и выделяет значимые ценовые интервалы горизонтальными линиями точки контроля (POC) с использованием настраиваемого порога и ширины;
  • использует преобразование barsAgo→datetime для правильного размещения графических объектов во времени (включая небольшую проекцию в будущее для наглядности);
  • безопасно управляет объектами графика с помощью уникального префикса (чтобы не удалить объекты других пользователей) и обновляет их только при поступлении новых данных;
  • предоставляет доступ к параметрам (период анализа, количество ценовых интервалов, максимальная ширина, порог POC, переключатели профиля/POC), что позволяет настраивать поведение без переписывания логики.

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

Проект поддерживается на платформе Algo Forge.

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Leon Sendrick
Leon Sendrick | 12 мая 2026 в 14:32
🥰🥰🥰
ALGOYIN LTD
Israel Pelumi Abioye | 12 мая 2026 в 17:08
Leon Sendrick #:
🥰🥰🥰
Спасибо.
Рыночная микроструктура в MQL5 (Часть 6): Поток ордеров Рыночная микроструктура в MQL5 (Часть 6): Поток ордеров
В этой статье в MicroStructure_Foundation.mqh добавляются шесть функций анализа потока ордеров и новая структура OrderFlowAnalysis: VPINOHLC, дисбаланс потока с учетом направления, интенсивность торговли относительно базового уровня за 20 сессий, индекс умных денег как разность поздней и ранней внутрисессионной доходности, моментум потока и функция-обертка, возвращающая вес достоверности. Достоверность сигнала потока фильтруется по шуму из пятой части и интенсивности скачков из четвертой. Реализация откалибрована на 602 нью-йоркских сессиях NQ на таймфрейме M1 и формирует готовые внутридневные сигналы потока с описанными пороговыми значениями.
За пределами GARCH (Часть VI): Фракционное броуновское движение и мультипликативный каскад в MQL5 За пределами GARCH (Часть VI): Фракционное броуновское движение и мультипликативный каскад в MQL5
В этой статье реализуется модуль моделирования MMAR: по параметрам, полученным при подгонке (H, распределение, коэффициенты, выборочная волатильность), он строит синтетические ценовые траектории. Модуль строит мультифрактальное торговое время с помощью мультипликативного каскада, генерирует фракционное броуновское движение методом Дэвиса — Харта или разложением Холецкого, масштабирует его до целевой волатильности и строит составной процесс посредством деформации времени. Читатели получат класс на языке MQL5, пригодный для многократного использования, рекомендации по выбору метода в зависимости от длины траектории и этапы валидации для сценарного тестирования и применения метода Монте-Карло в следующей части.
За пределами GARCH (Часть VII): Прогнозирование волатильности методом Монте-Карло в MQL5 За пределами GARCH (Часть VII): Прогнозирование волатильности методом Монте-Карло в MQL5
Мы реализуем модуль CMonteCarlo, который преобразует подогнанные параметры MMAR в прогноз волатильности методом Монте-Карло. Модуль выполняет N независимых симуляций на выбранном горизонте прогнозирования и возвращает среднее, медиану, стандартное отклонение и 95-процентный доверительный интервал на основе процентилей. При необходимости доступны значения каждого прогона. Адаптивная глубина каскада выбирает минимальное k, при котором b^k покрывает горизонт прогнозирования, обеспечивая быстродействие и согласованность симуляции.
Рыночная микроструктура в MQL5 (Часть 5): Микроструктурный шум Рыночная микроструктура в MQL5 (Часть 5): Микроструктурный шум
В статье файл MicroStructure_Foundation.mqh дополняется структурой MicrostructureAnalysis и пятью функциями для анализа изменений цены на таймфрейме M1: прокси для котируемого спреда, спреда по модели Ролла, коэффициента шума на основе OHLC, дисбаланса ордеров и компонента неблагоприятного отбора. Функция-обертка заполняет эти поля и связывает их с показателями волатильности из четвертой части. Эмпирические пороги получены по 602 нью-йоркским сессиям фьючерса NQ E-mini за период с января 2024 г. по июнь 2026 г. Они помогают фильтровать сигналы волатильности, выбирать величину риска и выявлять эффекты рыночного трения, обусловленные спредом.