preview
Создание индикатора ZigZag со статистикой волн в MQL5

Создание индикатора ZigZag со статистикой волн в MQL5

MetaTrader 5Торговые системы |
129 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Индикатор количественного анализа волн ZigZag: статистика вместо интуиции

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

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

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


Почему один зигзаг — это мало

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

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


Почему медиана, а не только среднее

Среднее арифметическое чувствительно к выбросам. Если рынок один раз дал аномально длинный импульс (например, на новостях), среднее по выборке будет заметно искажено. Эффект сохранится надолго, пока новые обычные волны не снизят вклад выброса.

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

№ волны Длина, пункты
1 120
2 145
3 98
4 160
5 132
6 1240
7 110
8 155

Среднее по этой выборке — 270 пунктов, что выглядит как типичная волна почти в два раза длиннее любой реально произошедшей, кроме одного новостного выброса на волне №6. Медиана по той же выборке — 138,5 пункта, что гораздо честнее отражает то, с чем трейдер реально сталкивается в девяти случаях из десяти. Именно поэтому индикатор считает обе величины параллельно и не подменяет одну другой.


Архитектура: буферы данных и буфер отрисовки

Ключевое архитектурное решение — три буфера при одном отображаемом графическом объекте:

//+------------------------------------------------------------------+
//| Буферы индикатора: один отображаемый + два служебных             |
//+------------------------------------------------------------------+
#property indicator_buffers 3
#property indicator_plots   1

double ZigZagBuffer[];     // финальные значения ZigZag (отображаемая линия)
double HighMapBuffer[];    // обнаруженные хаи (служебный буфер)
double LowMapBuffer[];     // обнаруженные лои (служебный буфер)

HighMapBuffer и LowMapBuffer объявлены как INDICATOR_CALCULATIONS — они участвуют в расчёте, но не рисуются и не засоряют DataWindow. Сначала кандидаты на экстремумы записываются в служебные буферы, затем конечный автомат выбирает из них финальную альтернирующую последовательность хай-лоу-хай-лоу. Такое разделение — не украшательство, а прямое следствие того, что промежуточные кандидаты и финальный зигзаг живут в разных системах координат: кандидат может быть отменён backstep-механизмом на несколько баров позже своего появления, а финальная линия должна оставаться неизменной после того, как она отрисована.

Инициализация индикатора связывает буферы и настраивает поведение линии на пустых значениях:

//+------------------------------------------------------------------+
//| Инициализация                                                    |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- привязка буферов: один рисуемый, два для внутренних расчётов
   SetIndexBuffer(0, ZigZagBuffer,  INDICATOR_DATA);
   SetIndexBuffer(1, HighMapBuffer, INDICATOR_CALCULATIONS);
   SetIndexBuffer(2, LowMapBuffer,  INDICATOR_CALCULATIONS);

   //--- нули не должны отображаться как точки линии
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0);
   PlotIndexSetInteger(0, PLOT_SHIFT, 0);

   //--- определяем тестер/оптимизацию: там поканальная отрисовка объектов
   //--- и комментарий не нужны и замедляют валидатор Маркета
   g_in_tester = (bool)MQLInfoInteger(MQL_TESTER) ||
                 (bool)MQLInfoInteger(MQL_OPTIMIZATION) ||
                 (bool)MQLInfoInteger(MQL_VISUAL_MODE);

   IndicatorSetString(INDICATOR_SHORTNAME, "Quantitative Analysis Indicator");
   lastBars = 0;                                  // принудительный полный проход отрисовки
   return(INIT_SUCCEEDED);
  }

Обратная сторона инициализации — деинициализация, которая аккуратно убирает за индикатором графический мусор при его удалении с графика или смене таймфрейма:

//+------------------------------------------------------------------+
//| Деинициализация: очистка графика                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, OBJ_PREFIX);              // удаляем все свои объекты
   Comment("");                                  // очищаем комментарий
   ChartRedraw();
  }


Поиск экстремума в скользящем окне

Прежде чем разбирать конечный автомат, стоит остановиться на двух вспомогательных функциях, которые находят индекс максимума и минимума в окне заданной глубины назад от текущего бара. Это не готовые функции терминала, а собственная реализация под нужды индикатора — без использования индексации таймсерий, что важно при работе с массивами, переданными в OnCalculate в порядке non-series (не как таймсерия):

//+------------------------------------------------------------------+
//| Позиция наибольшего значения в окне назад (не таймсерия)         |
//+------------------------------------------------------------------+
int ZZHighest(const double &array[], int depth, int start)
  {
   if(start < 0)
      return(0);

   double max   = array[start];
   int    index = start;
   for(int i = start - 1; i > start - depth && i >= 0; i--)
     {
      if(array[i] > max)                          // найден новый локальный максимум
        {
         index = i;
         max   = array[i];
        }
     }
   return(index);
  }

//+------------------------------------------------------------------+
//| Позиция наименьшего значения в окне назад (не таймсерия)         |
//+------------------------------------------------------------------+
int ZZLowest(const double &array[], int depth, int start)
  {
   if(start < 0)
      return(0);

   double min   = array[start];
   int    index = start;
   for(int i = start - 1; i > start - depth && i >= 0; i--)
     {
      if(array[i] < min)                          // найден новый локальный минимум
        {
         index = i;
         min   = array[i];
        }
     }
   return(index);
  }

Обе функции линейны по глубине окна InpDepth и вызываются на каждом баре внутри первого проходного цикла, то есть сложность полного пересчёта — O(rates_total × InpDepth). Для типичной глубины 12 и графика в несколько десятков тысяч баров это остаётся быстрой операцией, но при экстремально большом InpDepth на очень длинной истории стоит иметь это в виду.


Конечный автомат поиска экстремумов

Логика поиска пивотов реализована как явный конечный автомат с тремя состояниями:

//+------------------------------------------------------------------+
//| Состояния конечного автомата поиска экстремумов ZigZag           |
//+------------------------------------------------------------------+
enum EnSearchMode
  {
   Extremum = 0,           // ищем первый экстремум
   Bottom   = -1,          // ищем следующий низ
   Peak     = 1             // ищем следующий верх
  };

Первый проход по барам детектирует кандидатов на хай и лоу через ZZHighest/ZZLowest — поиск экстремума в скользящем окне InpDepth баров назад. Здесь же работает backstep: если в последних InpBackstep барах уже был зафиксирован менее выгодный экстремум того же типа, он аннулируется — так убираются дребезжащие локальные пики внутри одной волны, которые иначе создавали бы несколько соседних зубцов ZigZag там, где по смыслу должна быть одна волна.

//--- кандидат на низ
val = low[ZZLowest(low, InpDepth, shift)];
if(val == last_low)
   val = 0.0;
else
  {
   last_low = val;
   if((low[shift] - val) > InpDeviation * _Point)
      val = 0.0;
   else
     {
      //--- backstep: сбрасываем более высокие лои рядом
      for(back = 1; back <= InpBackstep; back++)
        {
         res = LowMapBuffer[shift - back];
         if((res != 0) && (res > val))
            LowMapBuffer[shift - back] = 0.0;
        }
     }
  }
if(low[shift] == val)
   LowMapBuffer[shift] = val;
else
   LowMapBuffer[shift] = 0.0;

Симметричный блок работает для кандидатов на хай, с той же логикой отклонения (InpDeviation) и того же backstep-подавления. Второй проход превращает облако кандидатов в чистую альтернирующую последовательность:

//--- состояние Peak: ищем следующий верх после низа
case Peak:
   //--- более глубокий лоу заменяет текущий
   if(LowMapBuffer[shift] != 0.0 && LowMapBuffer[shift] < last_low && HighMapBuffer[shift] == 0.0)
     {
      ZigZagBuffer[last_low_pos] = 0.0;
      last_low_pos = shift;
      last_low     = LowMapBuffer[shift];
      ZigZagBuffer[shift] = last_low;
      res = 1;
     }
   //--- хай закрывает волну, переключаемся на поиск низа
   if(HighMapBuffer[shift] != 0.0 && LowMapBuffer[shift] == 0.0)
     {
      last_high      = HighMapBuffer[shift];
      last_high_pos  = shift;
      ZigZagBuffer[shift] = last_high;
      extreme_search = Bottom;
      res = 1;
     }
   break;

Пока автомат в состоянии Peak (ищем вершину после низа), новый более глубокий лоу заменяет предыдущий — он честнее отражает истинное дно текущей коррекции, — а появление хая переключает состояние на Bottom. Симметрично работает состояние Bottom. Именно это гарантирует главное свойство любого корректного зигзага: хай и лоу строго чередуются, без двух хаёв подряд.


Инкрементальный пересчёт без полной перерисовки

Важный практический момент, который часто упускают в самописных зигзагах — поведение при поступлении нового тика без полной перерисовки истории. Индикатор откатывается назад ровно на ExtRecalc = 3 последних зафиксированных экстремума и восстанавливает состояние автомата из содержимого LowMapBuffer/HighMapBuffer на этой точке:

//+------------------------------------------------------------------+
//| Восстановление состояния автомата при инкрементальном пересчёте  |
//+------------------------------------------------------------------+
if(prev_calculated > 0)
  {
   i = rates_total - 1;
   while(extreme_counter < ExtRecalc && i > rates_total - 100)
     {
      res = ZigZagBuffer[i];
      if(res != 0)
         extreme_counter++;
      i--;
     }
   i++;
   start = i;

   //--- восстанавливаем, какой тип экстремума искали последним
   if(LowMapBuffer[i] != 0)
     {
      cur_low = LowMapBuffer[i];
      extreme_search = Peak;
     }
   else
     {
      cur_high = HighMapBuffer[i];
      extreme_search = Bottom;
     }

   //--- очищаем хвост, который будет пересчитан заново
   for(i = start + 1; i < rates_total && !IsStopped(); i++)
     {
      ZigZagBuffer[i]  = 0.0;
      LowMapBuffer[i]  = 0.0;
      HighMapBuffer[i] = 0.0;
     }
  }

Это стандартный для MetaQuotes паттерн инкрементального расчёта в OnCalculate, который не гоняет весь массив на каждом тике — критично для нагруженных графиков и мультивалютных панелей. Полный пересчёт (prev_calculated == 0) происходит только при первом запуске индикатора, смене таймфрейма или подгрузке дополнительной истории, а во всех остальных случаях обрабатывается только хвост из последних нескольких баров.


Структура и накопление статистики волн

Статистика по каждому направлению хранится в отдельной структуре, которая сбрасывается перед каждым полным пересчётом:

//+------------------------------------------------------------------+
//| Структура, накапливающая статистику по одному направлению волн   |
//+------------------------------------------------------------------+
struct WaveStats
  {
   double currentLength;   // длина последней завершённой волны, пункты
   double maxLength;       // максимальная длина волны
   double minLength;       // минимальная длина волны
   double avgLength;       // средняя длина волны
   double medianLength;    // медианная длина волны
   int    count;           // количество волн
  };

WaveStats bullishWaves;    // статистика бычьих волн (движения вверх)
WaveStats bearishWaves;    // статистика медвежьих волн (движения вниз)

Для среднего используется формула инкрементального пересчёта без хранения всех значений — классическая скользящая формула, требующая O(1) памяти на одну волну:

//+------------------------------------------------------------------+
//| Обновление статистики одной новой волной                        |
//+------------------------------------------------------------------+
void UpdateWaveStats(WaveStats &stats, double length)
  {
   stats.currentLength = length;                                 // последняя волна
   stats.maxLength     = (stats.count == 0) ? length : MathMax(stats.maxLength, length);
   stats.minLength     = (stats.count == 0) ? length : MathMin(stats.minLength, length);
   stats.avgLength     = (stats.avgLength * stats.count + length) / (stats.count + 1);
   stats.count++;
  }

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

//--- идём по парам соседних пивотов (val[0] старше val[n-1])
for(int i = 1; i < n; i++)
  {
   double waveLength = MathAbs(val[i] - val[i - 1]) / _Point; // длина в пунктах

   if(val[i] > val[i - 1])                    // пивот выше предыдущего -> движение вверх
     {
      UpdateWaveStats(bullishWaves, waveLength);
      int sz = ArraySize(bullLen);
      ArrayResize(bullLen, sz + 1);
      bullLen[sz] = waveLength;
     }
   else                                       // пивот ниже -> движение вниз
     {
      UpdateWaveStats(bearishWaves, waveLength);
      int sz = ArraySize(bearLen);
      ArrayResize(bearLen, sz + 1);
      bearLen[sz] = waveLength;
     }
  }

Для медианы такой инкрементальный трюк невозможен — медиану нельзя обновить без знания всего упорядоченного набора, поэтому массивы bullLen[]/bearLen[] накапливаются полностью и сортируются через ArraySort только один раз на пересчёт статистики, а не на каждый бар:

//+------------------------------------------------------------------+
//| Медиана массива длин волн                                        |
//+------------------------------------------------------------------+
double CalculateMedian(double &array[])
  {
   int size = ArraySize(array);
   if(size == 0)
      return(0);

   ArraySort(array);                             //--- сортировка по возрастанию

   if(size % 2 == 0)
      return((array[size / 2 - 1] + array[size / 2]) / 2.0);
   return(array[size / 2]);
  }

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

//--- статистика и отрисовка: только на новом баре, чтобы не грузить терминал
if(rates_total == lastBars && prev_calculated != 0)
   return(rates_total);
lastBars = rates_total;


Вывод статистики в комментарий графика

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

//+------------------------------------------------------------------+
//| Печать статистики в комментарий графика                          |
//+------------------------------------------------------------------+
void DisplayStats()
  {
   string info = "=== Количественный анализ волн ZigZag (пункты) ===\n";
   info += "Бычьи волны (" + IntegerToString(bullishWaves.count) + "):\n";
   info += StringFormat("  Текущая:  %.1f\n", bullishWaves.currentLength);
   info += StringFormat("  Максимум: %.1f\n", bullishWaves.maxLength);
   info += StringFormat("  Минимум:  %.1f\n", bullishWaves.minLength);
   info += StringFormat("  Среднее:  %.1f\n", bullishWaves.avgLength);
   info += StringFormat("  Медиана:  %.1f\n\n", bullishWaves.medianLength);

   info += "Медвежьи волны (" + IntegerToString(bearishWaves.count) + "):\n";
   info += StringFormat("  Текущая:  %.1f\n", bearishWaves.currentLength);
   info += StringFormat("  Максимум: %.1f\n", bearishWaves.maxLength);
   info += StringFormat("  Минимум:  %.1f\n", bearishWaves.minLength);
   info += StringFormat("  Среднее:  %.1f\n", bearishWaves.avgLength);
   info += StringFormat("  Медиана:  %.1f", bearishWaves.medianLength);

   Comment(info);
  }

Такой формат вывода удобен для быстрой визуальной проверки при разработке и отладке, но у него есть очевидное ограничение — значения недоступны программно другим индикаторам и советникам через iCustom. Этот момент разберу отдельно в разделе про ограничения.


Защита производительности в тестере и оптимизации

//--- определяем режим работы: тестер, оптимизация или визуальный режим
g_in_tester = (bool)MQLInfoInteger(MQL_TESTER) ||
              (bool)MQLInfoInteger(MQL_OPTIMIZATION) ||
              (bool)MQLInfoInteger(MQL_VISUAL_MODE);
...
//--- в тестере нужна только линия ZigZag: пропускаем статистику, комментарий и объекты
if(g_in_tester)
   return(rates_total);

При оптимизации в составе советника отрисовка объектов и вызов Comment() на каждом баре резко замедляют прогон. Это легко превращает 10 минут расчёта в часы. Индикатор явно отключает весь визуальный слой при работе в тестере, оставляя только расчёт самого буфера ZigZag, который может понадобиться советнику через iCustom. Это ровно та деталь, которую валидатор Маркета проверяет одной из первых у любого индикатора с графическими объектами.


Визуализация: подписанные пивоты

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

//+------------------------------------------------------------------+
//| Отрисовка точек пивотов и подписей длины волн                       |
//+------------------------------------------------------------------+
void DrawPivots(const datetime &time[], const int &idx[], const double &val[], int n)
  {
   int total = ArraySize(time);

   for(int i = 0; i < n; i++)
     {
      int barPos = idx[i];
      if(barPos < 0 || barPos >= total)
         continue;

      datetime t = time[barPos];

      //--- цвет пивота: лоу (после него движение вверх) зелёный, хай красный
      bool isLow;
      if(i == 0)
        isLow = (n > 1 && val[1] > val[0]);      // первый пивот — по следующему
      else
        isLow = (val[i] < val[i - 1]);           // ниже предыдущего -> лоу
      color dotColor = isLow ? InpBullColor : InpBearColor;

      //--- точка пивота
      string dotName = OBJ_PREFIX + "dot_" + IntegerToString(idx[i]);
      ObjectCreate(0, dotName, OBJ_ARROW, 0, t, val[i]);
      ObjectSetInteger(0, dotName, OBJPROP_ARROWCODE, 159); // крупная точка
      ObjectSetInteger(0, dotName, OBJPROP_COLOR, dotColor);
      ObjectSetInteger(0, dotName, OBJPROP_WIDTH, InpDotSize);
      ObjectSetInteger(0, dotName, OBJPROP_ANCHOR, ANCHOR_CENTER);
      ObjectSetInteger(0, dotName, OBJPROP_BACK, false);

      //--- подпись с длиной завершённой волны на пивоте
      if(InpShowLabels && i > 0)
        {
         double waveLength = MathAbs(val[i] - val[i - 1]) / _Point;
         bool isBull = (val[i] > val[i - 1]);
         string txtName = OBJ_PREFIX + "txt_" + IntegerToString(idx[i]);
         ObjectCreate(0, txtName, OBJ_TEXT, 0, t, val[i]);
         ObjectSetString(0, txtName, OBJPROP_TEXT, StringFormat("%.0f", waveLength));
         ObjectSetInteger(0, txtName, OBJPROP_COLOR, isBull ? InpBullColor : InpBearColor);
         ObjectSetInteger(0, txtName, OBJPROP_FONTSIZE, 8);
         //--- хай подписывается сверху, лоу снизу
         ObjectSetInteger(0, txtName, OBJPROP_ANCHOR, isBull ? ANCHOR_LOWER : ANCHOR_UPPER);
        }
     }
  }

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

Вот так выглядит график. А в Print мы видим статистику:


Практическое применение статистики

Ситуация Что показывает статистика Практический вывод
Медиана 180 пт, среднее 340 пт На инструменте есть редкие крупные импульсы Стоп ставить у медианы, тейк частично держать под хвостовые движения
Среднее растёт, медиана стабильна Появились редкие выбросы, а не общий рост волатильности Не пересматривать базовые параметры риска по всей истории
Медиана медвежьих волн больше медианы бычьих Снижения на инструменте в среднем резче роста Асимметричные стопы/тейки для длинных и коротких позиций
Текущая волна заметно длиннее медианы Движение уже вышло за типичный диапазон Повышенная осторожность против входа по тренду на истощении импульса
Минимум и медиана близки, максимум сильно выше Распределение волн сильно скошено вправо Управление риском по процентилям, а не по среднему отклонению


Входные параметры

Параметр Роль
InpDepth Глубина окна поиска экстремума в барах. Больше — более крупные волны, меньше шума от локальных зубцов
InpDeviation Минимальное отклонение в пунктах, чтобы кандидат считался значимым экстремумом, а не шумом внутри окна
InpBackstep Число баров, в пределах которых менее выгодный экстремум того же типа аннулируется
InpShowLabels Включает подписи длины волны на каждом пивоте
InpDotSize Размер точки пивота на графике
InpBullColor / InpBearColor Цвет пивотов и подписей для бычьих (лоу) и медвежьих (хай) точек соответственно


Честно про ограничения

  1. Статистика считается по всей истории на графике целиком, без разбивки по временным окнам. Режим волатильности год назад и текущий режим смешиваются в одном среднем и одной медиане, что может маскировать реальные изменения поведения инструмента.
  2. Значения статистики доступны только через Comment(), то есть недоступны программно другим индикаторам и советникам. Для использования в автоматической торговле их нужно выводить через дополнительные скрытые буферы и читать через iCustom.
  3. Backstep и Deviation — эвристики, а не параметры, выведенные из статистики самого инструмента. Их разумные значения могут заметно отличаться между инструментами с разной типичной волатильностью, и подбор без отдельной валидации на out-of-sample-периоде рискует переобучением на конкретном участке истории.
  4. Отрисовка полностью очищает и заново создаёт все объекты на каждом новом баре (ObjectsDeleteAll перед DrawPivots), что на очень длинной истории с большим количеством пивотов может создавать заметную, хоть и разовую, нагрузку на каждый новый бар.
  5. Индикатор не разделяет статистику по фазам рынка (тренд/флэт), хотя типичная длина волны в трендовой и флэтовой фазе у большинства инструментов заметно различается — это прямое направление для доработки, а не признак ошибки в текущей реализации.


Куда расширять

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

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

Разделение по фазам рынка. Добавить фильтр по ADX или собственному классификатору тренд/флэт, чтобы считать отдельную статистику волн для каждого режима — это существенно точнее отражает то, чего ждать от следующей волны в текущих условиях.

Процентили вместо только медианы. Поскольку распределение длин волн часто скошено (см. раздел 2), добавление 25-го и 75-го процентилей дало бы более полную картину разброса, чем пара медиана/среднее — особенно полезно для калибровки диапазонов стопов и тейков.


Заключение

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

Статистику нужно учитывать с поправкой на ограничения: она агрегирует всю историю без разделения по режимам и сейчас доступна только через комментарий графика. Параметры (depth, deviation, backstep) следует проверять на конкретном инструменте, а не переносить значения по умолчанию без вопросов.

Название файла Описание файла
Quantitative Analysis Indicator.mq5 Индикатор ZigZag с расчётом статистики длины волн (среднее, медиана, максимум, минимум) отдельно для бычьих и медвежьих движений
Прикрепленные файлы |
Встраивание торговой дисциплины в код (Часть 6): Построение единого фреймворка дисциплины на MQL5 Встраивание торговой дисциплины в код (Часть 6): Построение единого фреймворка дисциплины на MQL5
В статье представлен унифицированный фреймворк обеспечения дисциплины на MQL5, которая объединяет модули белого списка символов, торговых часов и новостных фильтров, а также ежедневных лимитов сделок в рамках файла CDisciplineEngine.mqh. Объясняется централизованная проверка сделок и синхронизация состояний, выполняемые совместно дашбордом графиков и советником-принудителем. Читатели узнают, как авторизовывать ордера через единый шлюз, отслеживать разрешения в реальном времени и автоматически обеспечивать соблюдение правил на уровне всего терминала.
Создание пользовательских индикаторов в MQL5 (Часть 11): Улучшение футпринт-графика с помощью слоёв рыночной структуры и ордер-флоу Создание пользовательских индикаторов в MQL5 (Часть 11): Улучшение футпринт-графика с помощью слоёв рыночной структуры и ордер-флоу
В статье MQL5-индикатор футпринт-графика расширяется слоями рыночной структуры и ордер-флоу: барами объёмного профиля, точкой контроля (Point of Control, POC), подсветкой зоны стоимости (value area), выявлением последовательных дисбалансов, зонами поглощения, а также маркерами single print (одиночных принтов) и unfinished business (незавершённой аукционной отработки экстремума). Мы расширим структуры данных бара, добавим функции для POC/зоны стоимости, дисбаланса и поглощения, а также построим конвейер отрисовки с фиксированным порядком слоёв. Вы получите готовые входные параметры, метаданные и утилиты отрисовки для интеграции и настройки этих слоёв в индикаторе.
Автоматизация торговых стратегий в MQL5 (Часть 32): Создание системы распознавания гармонического паттерна "5 Drives" с использованием Price Action Автоматизация торговых стратегий в MQL5 (Часть 32): Создание системы распознавания гармонического паттерна "5 Drives" с использованием Price Action
В этой статье мы разрабатываем систему распознавания паттернов "5 Drives" на языке MQL5, которая определяет бычьи и медвежьи гармонические паттерны "5 Drives" с использованием точек разворота и коэффициентов Фибоначчи, совершая сделки с настраиваемыми уровнями входа, стоп-лосса и тейк-профита на основе выбранных пользователем параметров. Мы улучшим работу трейдеров с помощью визуальной обратной связи, используя графические объекты, такие как треугольники, линии тренда и подписи, для наглядного отображения структуры паттерна A-B-C-D-E-F.
Кодирование свечных моделей (Часть 1): Алфавитная система для выявления сигналов Кодирование свечных моделей (Часть 1): Алфавитная система для выявления сигналов
Мы представляем основанный на правилах алфавит для анализа ценовых движений на основе свечных графиков, который сопоставляет измеримые формы и направления с буквенными кодами (A/a, H/h, E/e, G/g, D). В статье показана реализация на MQL5: классификация свечей, построение двухбарных последовательностей с помощью перестановок и сканирование графиков с помощью индикатора и оповещений. Читатели получают практический шаблон для объективного обнаружения паттернов и систематического тестирования.