English
preview
Автоматизация классических рыночных методов в MQL5 (Часть 1): Накопление и распределение по Вайкоффу

Автоматизация классических рыночных методов в MQL5 (Часть 1): Накопление и распределение по Вайкоффу

MetaTrader 5Трейдинг |
45 0
Tola Moses Hector
Tola Moses Hector

Введение

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

Этот метод рассматривает движение рынка не как случайный шум, а как последовательность причинно-следственных циклов, обусловленных институциональным спросом и предложением. Такие циклы оставляют характерные структурные признаки — spring (ложный прокол поддержки), признак силы (sign of strength, SOS), upthrust (ложный вынос выше сопротивления) и признак слабости (sign of weakness, SOW), — которые опытный трейдер может распознать и отрабатывать в сделках.

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

В этой статье мы создадим полноценный советник, автоматизирующий последовательное обнаружение событий с помощью конечного автомата. На таймфрейме H4 советник выявляет структуры накопления, обнаруживает spring как финальный вытряхивающий манёвр, подтверждает спрос по признаку силы и открывает длинную позицию в последней точке поддержки (LPS). Для распределения применяется зеркальная логика: upthrust, признак слабости и открытие короткой позиции в последней точке предложения (LPSY). Рассмотрим следующие темы:

  1. Основы концепции Вайкоффа
  2. Почему тиковый объём служит косвенным показателем институциональной активности
  3. Архитектура конечного автомата
  4. Реализация в MQL5
  5. Тестирование на исторических данных
  6. Заключение

В результате вы получите полностью работоспособный советник MQL5, который автоматически обнаруживает структуры Вайкоффа и торгует по ним.


Основы концепции Вайкоффа

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

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

Последовательность накопления

Накопление проходит пять фаз. В фазе A кульминация продаж на повышенном объёме останавливает нисходящий тренд. Затем следует автоматический отскок (automatic rally), формирующий верхнюю границу диапазона. Вторичный тест на пониженном объёме подтверждает исчерпание давления продавцов. Во второй фазе институциональные участники постепенно накапливают позиции внутри диапазона, пока цена колеблется между поддержкой и сопротивлением. В третьей фазе возникает spring — преднамеренный ложный пробой нижней границы диапазона на объёме ниже среднего, предназначенный для вынуждения слабых держателей выйти из позиции и сбора ликвидности перед началом роста. Четвёртая фаза подтверждает институциональный спрос признаком силы: закрытием бара выше сопротивления диапазона на высоком объёме. Последняя точка поддержки представляет собой откат после признака силы — последнюю возможность входа с низким риском перед началом фазы E и выходом цены из диапазона.

Рис. 1. График со spring, признаком силы и входом в точке LPS.

Зеркальная последовательность распределения

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

Рис. 2. График с upthrust, признаком слабости и входом в точке LPSY.

Что обнаруживает советник

Советник не пытается классифицировать все события Вайкоффа — от предварительной поддержки до фазы E. Полное обнаружение фаз на реальных данных требует субъективной оценки, которую трудно надёжно формализовать. Вместо этого советник сосредоточен на событиях с наиболее чёткими и объективными определениями: формировании диапазона после направленного движения, spring или upthrust как финальном вытряхивающем манёвре, признаке силы (SOS) или признаке слабости (SOW) как подтверждении и последней точке поддержки (LPS) либо последней точке предложения (LPSY) как сигнале для входа.


Почему тиковый объём служит косвенным показателем институциональной активности

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

Тиковый объём может использоваться как практический заменитель реального объёма и косвенный индикатор торговой активности. Многочисленные исследования показывают его высокую корреляцию — обычно выше 0,85 — с реальным объёмом торгов по основным валютным парам. Когда институциональные участники активны, цена изменяется часто; когда рынок спокоен, тики поступают редко. Описанные Вайкоффом относительные модели объёма — кульминационный объём на разворотах, низкий объём при тестировании уровней и расширение объёма на пробоях — хорошо проявляются в данных тикового объёма.

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


Архитектура конечного автомата

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

Рассмотрим упрощённый альтернативный вариант — независимую проверку в OnTick:

if(iLow(_Symbol, PERIOD_CURRENT, 1) < supportLevel)
   OpenLong();  // WRONG: fires on any dip, regardless of context

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

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

Полная последовательность состояний для накопления выглядит так: "STATE_IDLE" → "STATE_RANGE_FORMING" → "STATE_SPRING_DETECTED" → "STATE_SOS_CONFIRMED" → "STATE_IN_TRADE". Для распределения используется следующая последовательность: "STATE_IDLE" → "STATE_RANGE_FORMING" → "STATE_UPTHRUST_DETECTED" → "STATE_SOW_CONFIRMED" → "STATE_IN_TRADE". При аннулировании структуры любое состояние может быть сброшено в "STATE_IDLE". Например, если после обнаружения spring цена закроется ниже его минимума, сценарий накопления отбрасывается, и советник начинает искать новую конфигурацию.


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

Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в папку Experts, нажмите «New/Создать» и следуйте шагам мастера создания файла. Мы будем создавать советник по частям, последовательно объясняя каждый раздел.

Подключаемые библиотеки и входные параметры

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

//+------------------------------------------------------------------+
//|                                                    WyckoffEA.mq5 |
//|                                Copyright 2026, Tola Moses Hector |
//|                                          https://t.me/tolahector |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Tola Moses Hector"
#property link      "https://t.me/tolahector"
#property version   "1.00"
#property description "Wyckoff Accumulation and Distribution EA"
#property description "Detects Spring + SOS for long entries (LPS)"
#property description "Detects Upthrust + SOW for short entries (LPSY)"
#property description "H4 timeframe recommended"

#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| EA State Machine                                                 |
//+------------------------------------------------------------------+
enum ENUM_WYCKOFF_STATE
  {
   STATE_IDLE,               // No structure active
   STATE_RANGE_FORMING,      // Valid range identified — locked, watching for Spring/UT
   STATE_SPRING_DETECTED,    // Spring confirmed, watching for SOS
   STATE_SOS_CONFIRMED,      // SOS confirmed, watching for LPS entry
   STATE_UPTHRUST_DETECTED,  // Upthrust confirmed, watching for SOW
   STATE_SOW_CONFIRMED,      // SOW confirmed, watching for LPSY entry
   STATE_IN_TRADE            // Position open
  };

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input group "=== Range Detection ==="
input int    InpTrendBars       = 15;    // Bars of prior trend required
input int    InpMinRangeBars    = 10;    // Minimum bars to form range
input int    InpMaxRangeBars    = 60;    // Maximum bars to scan for range
input double InpMinRangePips    = 20.0;  // Minimum range height (pips)
input double InpMaxRangePips    = 400.0; // Maximum range height (pips)
input double InpSpringTolerance = 10.0;  // Pips below support for Spring
input int    InpRangeWatchBars  = 30;    // Max bars to watch range before reset

input group "=== Volume Settings ==="
input double InpHighVolMult     = 1.2;   // High-volume multiplier (SOS/SOW)
input double InpLowVolMult      = 1.2;   // Low-volume multiplier (Spring/Upthrust)

input group "=== Entry and Risk ==="
input double InpRiskPercent     = 1.0;   // Risk per trade (% of balance)
input double InpRR              = 2.0;   // Risk-reward ratio
input int    InpATRPeriod       = 14;    // ATR period for stop distance
input double InpATRMult         = 1.5;   // ATR multiplier for stop distance
input int    InpLPSBars         = 8;     // Bars to wait for LPS/LPSY pullback

input group "=== General ==="
input int    InpMagicNumber     = 777001; // Magic number
input int    InpSlippage        = 10;     // Slippage in points
input bool   InpShowLabels      = true;   // Draw event labels on chart

Сначала подключается файл "Trade\Trade.mqh", предоставляющий класс "CTrade", необходимый для выполнения ордеров и управления позициями. Затем определяется перечисление "ENUM_WYCKOFF_STATE" из семи состояний, охватывающих все возможные этапы последовательности обнаружения событий Вайкоффа. Это перечисление составляет основу всего советника: каждое решение об обнаружении зависит от текущего состояния. Входные параметры объединены в четыре группы с помощью директивы "input group". Группа "Range Detection" управляет поиском подходящего торгового диапазона и предшествующего тренда. В группе "Volume Settings" задаются пороговые значения относительного объёма для событий Вайкоффа. Группа "Entry and Risk" управляет расчётом объёма позиции и размещением стоп-лосса. Все параметры можно изменять в окне свойств без редактирования кода.

Глобальные переменные и структура диапазона

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

//+------------------------------------------------------------------+
//| Wyckoff Range Data Structure                                     |
//+------------------------------------------------------------------+
struct SWyckoffRange
  {
   double            support;        // Range support level
   double            resistance;     // Range resistance level
   int               start_bar;      // Bars back where range started
   double            avg_volume;     // Average tick volume within range
   bool              bullish_bias;   // true = accumulation, false = distribution
   double            spring_low;     // Low of the Spring bar
   double            sos_high;       // High of the SOS bar
   double            upthrust_high;  // High of the Upthrust bar
   double            sow_low;        // Low of the SOW bar
  };

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
ENUM_WYCKOFF_STATE g_state          = STATE_IDLE;         // Current EA state
SWyckoffRange      g_range;                               // Current range data
CTrade             g_trade;                               // Trade execution object
int                g_atr_handle     = INVALID_HANDLE;     // ATR indicator handle
datetime           g_last_bar       = 0;                  // Last processed bar time
int                g_lps_count      = 0;                  // Bars waited after SOS
int                g_lpsy_count     = 0;                  // Bars waited after SOW
int                g_range_watch    = 0;                  // Bars watched in RANGE_FORMING

Структура "SWyckoffRange" служит памятью советника о текущей рыночной структуре. В ней хранятся границы диапазона, базовый уровень объёма, рассчитанный по барам внутри диапазона, направленность, определяемая предшествующим нисходящим или восходящим трендом, и ценовые уровни каждого обнаруженного события. Хранение всех данных структуры в одном месте повышает читаемость кода и устраняет необходимость в разрозненных глобальных переменных. В глобальном разделе создаётся объект "g_trade" класса "CTrade" для выполнения торговых операций и дескриптор "g_atr_handle" индикатора ATR, используемого при расчёте расстояния до стоп-лосса.

Вспомогательные функции

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

//+------------------------------------------------------------------+
//| Returns pip size for the current symbol                          |
//+------------------------------------------------------------------+
double PipSize()
  {
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); // Get symbol digits
   return (digits == 3 || digits == 5) ? _Point * 10.0 : _Point; // Return pip size
  }

//+------------------------------------------------------------------+
//| Calculates lot size from risk percent and stop distance          |
//+------------------------------------------------------------------+
double CalcLots(double sl_pips)
  {
   double balance    = AccountInfoDouble(ACCOUNT_BALANCE);                    // Get balance
   double risk_money = balance * InpRiskPercent / 100.0;                      // Monetary risk
   double tick_val   = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);    // Tick value
   double tick_size  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);     // Tick size
   double pip_size   = PipSize();                                             // Get pip size
   if(tick_size <= 0 || tick_val <= 0 || sl_pips <= 0)
      return 0;                                                               // Validate inputs
   double pip_value  = (pip_size / tick_size) * tick_val;                     // Pip monetary value
   double lots       = risk_money / (sl_pips * pip_value);                    // Raw lot size
   double step       = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);         // Volume step
   double min_lot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);          // Minimum lot
   double max_lot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);          // Maximum lot
   lots = MathFloor(lots / step) * step;                                      // Normalize to step
   return MathMax(min_lot, MathMin(max_lot, lots));                           // Clamp to limits
  }

//+------------------------------------------------------------------+
//| Checks if EA has an open position on this symbol                 |
//+------------------------------------------------------------------+
bool PositionOpen()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)                             // Iterate positions
     {
      ulong ticket = PositionGetTicket(i);                                    // Get ticket
      if(!PositionSelectByTicket(ticket))
         continue;                                                            // Select position
      if(PositionGetString(POSITION_SYMBOL)  != _Symbol)
         continue;                                                            // Check symbol
      if(PositionGetInteger(POSITION_MAGIC)  != InpMagicNumber)
         continue;                                                            // Check magic
      return true;                                                            // Position found
     }
   return false;                                                              // No position
  }

Функция "PipSize()" определяет, содержит ли котировка символа три или пять знаков после запятой, и возвращает корректный размер пункта в ценовых единицах. Благодаря этому дальнейшие расчёты расстояний не зависят от инструмента. "CalcLots()" рассчитывает объём позиции на основе заданного денежного риска с использованием предоставляемых брокером значений "SYMBOL_TRADE_TICK_VALUE" и "SYMBOL_TRADE_TICK_SIZE", которые позволяют корректно переводить ценовое расстояние в валюту счёта для любых инструментов, включая золото и индексы. Такой способ точнее простого умножения на стоимость пункта, которое не работает для нестандартных инструментов. "PositionOpen()" просматривает все открытые позиции и возвращает true, если среди них есть позиция данного советника по текущему символу. Для этого проверяются имя символа и магический номер, что исключает смешение с ручными сделками.

Функции отрисовки на графике

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

//+------------------------------------------------------------------+
//| Draws a horizontal line at the specified price level             |
//+------------------------------------------------------------------+
void DrawHLine(string name, double price, color clr, ENUM_LINE_STYLE style)
  {
   if(!InpShowLabels)
      return;                                                                // Check flag
   string obj = "WYK_" + name;                                               // Build object name
   ObjectDelete(0, obj);                                                     // Remove existing
   ObjectCreate(0, obj, OBJ_HLINE, 0, 0, price);                             // Create hline
   ObjectSetInteger(0, obj, OBJPROP_COLOR, clr);                             // Set color
   ObjectSetInteger(0, obj, OBJPROP_STYLE, style);                           // Set style
   ObjectSetInteger(0, obj, OBJPROP_WIDTH, 1);                               // Set width
   ChartRedraw(0);                                                           // Redraw chart
  }

//+------------------------------------------------------------------+
//| Places a text label at the specified time and price              |
//+------------------------------------------------------------------+
void DrawLabel(string name, datetime time, double price, string text, color clr)
  {
   if(!InpShowLabels)
      return;                                                                // Check flag
   string obj = "WYK_" + name;                                               // Build object name
   ObjectDelete(0, obj);                                                     // Remove existing
   ObjectCreate(0, obj, OBJ_TEXT, 0, time, price);                           // Create text object
   ObjectSetString(0, obj, OBJPROP_TEXT, text);                              // Set text content
   ObjectSetInteger(0, obj, OBJPROP_COLOR, clr);                             // Set color
   ObjectSetInteger(0, obj, OBJPROP_FONTSIZE, 9);                            // Set font size
   ChartRedraw(0);                                                           // Redraw chart
  }

//+------------------------------------------------------------------+
//| Removes all chart objects created by this EA                     |
//+------------------------------------------------------------------+
void ClearLabels()
  {
   int total = ObjectsTotal(0);                                              // Get object count
   for(int i = total - 1; i >= 0; i--)                                       // Iterate backwards
     {
      string name = ObjectName(0, i);                                        // Get object name
      if(StringFind(name, "WYK_") == 0)
         ObjectDelete(0, name);                                              // Delete if EA's
     }
   ChartRedraw(0);                                                           // Redraw chart
  }

Функция "DrawHLine()" создаёт или обновляет горизонтальные линии на уровнях поддержки и сопротивления диапазона. Префикс "WYK_" во всех именах объектов позволяет функции "ClearLabels()" надёжно удалять только объекты этого советника, не затрагивая вручную добавленные линии и индикаторы. "DrawLabel()" размещает на графике обозначения событий — "SPRING", "SOS", "UPTHRUST", "SOW", "LPS" и "LPSY" — в точках, соответствующих цене и времени каждого обнаруженного события.

Определение предшествующего тренда

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

//+------------------------------------------------------------------+
//| Returns true if a downtrend preceded the specified bar           |
//+------------------------------------------------------------------+
bool HasPriorDowntrend(int from_bar)
  {
   double high_buf[], low_buf[];                                             // Price buffers
   ArraySetAsSeries(high_buf, true);                                         // Set as series
   ArraySetAsSeries(low_buf,  true);                                         // Set as series
   if(CopyHigh(_Symbol, PERIOD_CURRENT, from_bar, InpTrendBars, high_buf) < InpTrendBars)
      return false;                                                          // Copy highs
   if(CopyLow(_Symbol, PERIOD_CURRENT, from_bar, InpTrendBars, low_buf)  < InpTrendBars)
      return false;                                                          // Copy lows
   double first_high = 0, second_low = DBL_MAX;                              // Init comparators
   int half = InpTrendBars / 2;                                              // Split midpoint
   for(int i = 0; i < half; i++)
      first_high = MathMax(first_high, high_buf[i + half]);                  // Find early high
   for(int i = 0; i < half; i++)
      second_low = MathMin(second_low, low_buf[i]);                          // Find recent low
   return first_high > second_low + PipSize() * InpMinRangePips * 0.3;       // Confirm descent
  }

//+------------------------------------------------------------------+
//| Returns true if an uptrend preceded the specified bar            |
//+------------------------------------------------------------------+
bool HasPriorUptrend(int from_bar)
  {
   double high_buf[], low_buf[];                                              // Price buffers
   ArraySetAsSeries(high_buf, true);                                          // Set as series
   ArraySetAsSeries(low_buf,  true);                                          // Set as series
   if(CopyHigh(_Symbol, PERIOD_CURRENT, from_bar, InpTrendBars, high_buf) < InpTrendBars)
      return false;                                                          // Copy highs
   if(CopyLow(_Symbol, PERIOD_CURRENT, from_bar, InpTrendBars, low_buf)  < InpTrendBars)
      return false;                                                          // Copy lows
   double first_low = DBL_MAX, second_high = 0;                              // Init comparators
   int half = InpTrendBars / 2;                                              // Split midpoint
   for(int i = 0; i < half; i++)
      first_low   = MathMin(first_low,   low_buf[i + half]);                 // Find early low
   for(int i = 0; i < half; i++)
      second_high = MathMax(second_high, high_buf[i]);                       // Find recent high
   return second_high > first_low + PipSize() * InpMinRangePips * 0.3;       // Confirm ascent
  }

Обе функции делят период ретроспективного анализа "InpTrendBars" пополам и сравнивают ценовую структуру более ранней половины с последней. В функции "HasPriorDowntrend()" наибольший максимум должен находиться в раннем периоде, а наименьший минимум — в последнем, что подтверждает снижение цены на рассматриваемом интервале. "HasPriorUptrend()" использует зеркальную логику. Минимальный порог расстояния не позволяет ошибочно классифицировать плоский рынок без выраженного направления как тренд.

Определение диапазона

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

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

//+------------------------------------------------------------------+
//| Scans recent bars once to identify a valid Wyckoff range         |
//+------------------------------------------------------------------+
bool DetectRange()
  {
   double high_buf[], low_buf[];                                             // Price buffers
   long   vol_buf[];                                                         // Volume buffer
   ArraySetAsSeries(high_buf, true);                                         // Set as series
   ArraySetAsSeries(low_buf,  true);                                         // Set as series
   ArraySetAsSeries(vol_buf,  true);                                         // Set as series
   int bars = InpMaxRangeBars + InpTrendBars + 5;                            // Total bars needed
   if(CopyHigh(_Symbol, PERIOD_CURRENT, 1, bars, high_buf)     < bars)
      return false;                                                          // Copy highs
   if(CopyLow(_Symbol, PERIOD_CURRENT, 1, bars, low_buf)      < bars)
      return false;                                                          // Copy lows
   if(CopyTickVolume(_Symbol, PERIOD_CURRENT, 1, bars, vol_buf) < bars)
      return false;                                                          // Copy volumes
   double pip   = PipSize();                                                 // Get pip size
   double min_h = InpMinRangePips * pip;                                     // Min height in price
   double max_h = InpMaxRangePips * pip;                                     // Max height in price
//--- Try each possible range length from minimum to maximum
   for(int range_bars = InpMinRangeBars; range_bars <= InpMaxRangeBars; range_bars++)   // Try lengths
     {
      double rh = 0, rl = DBL_MAX;                                           // Init bounds
      for(int i = 0; i < range_bars; i++)                                    // Scan range bars
        {
         rh = MathMax(rh, high_buf[i]);                                      // Update range high
         rl = MathMin(rl, low_buf[i]);                                       // Update range low
        }
      double height = rh - rl;                                               // Compute height
      if(height < min_h)
         continue;                                                           // Too narrow, try longer
      if(height > max_h)
         break;                                                              // Too wide, stop
      //--- Check that all bars stay within 25% tolerance of range height
      double tol  = height * 0.25;                                           // Tolerance band
      bool   fits = true;                                                    // Fit flag
      for(int i = 0; i < range_bars; i++)                                    // Check each bar
        {
         if(high_buf[i] > rh + tol || low_buf[i] < rl - tol)                 // Bar outside range
           {
            fits = false;                                                    // Mark as not fitting
            break;                                                           // Stop checking
           }
        }
      if(!fits)
         continue;                                                           // Skip this length
      //--- Confirm a prior trend before the range
      bool down_before = HasPriorDowntrend(range_bars + 1);                  // Check downtrend
      bool up_before   = HasPriorUptrend(range_bars + 1);                    // Check uptrend
      if(!down_before && !up_before)
         continue;                                                           // No prior trend
      //--- Compute average volume within the range
      double avg_vol = 0;                                                    // Volume sum
      for(int i = 0; i < range_bars; i++)
         avg_vol += (double)vol_buf[i];                                      // Accumulate
      avg_vol /= range_bars;                                                 // Compute average
      //--- Populate the range struct
      g_range.support       = rl;                                            // Store support
      g_range.resistance    = rh;                                            // Store resistance
      g_range.start_bar     = range_bars;                                    // Store bar count
      g_range.avg_volume    = avg_vol;                                       // Store average vol
      //--- When both trends detected, pick the more dominant one
      if(down_before && up_before)
        {
         //--- Measure which directional move was larger immediately before range
         double high_buf[], low_buf[];
         ArraySetAsSeries(high_buf, true);
         ArraySetAsSeries(low_buf,  true);
         int trend_bars = InpTrendBars;
         CopyHigh(_Symbol, PERIOD_CURRENT, range_bars + 1, trend_bars, high_buf);
         CopyLow(_Symbol, PERIOD_CURRENT, range_bars + 1, trend_bars, low_buf);
         double trend_high = 0, trend_low = DBL_MAX;
         for(int k = 0; k < trend_bars; k++)
           {
            trend_high = MathMax(trend_high, high_buf[k]);
            trend_low  = MathMin(trend_low,  low_buf[k]);
           }
         double first_close  = iClose(_Symbol, PERIOD_CURRENT, range_bars + trend_bars);
         double last_close   = iClose(_Symbol, PERIOD_CURRENT, range_bars + 1);
         //--- If price fell into the range = downtrend before = accumulation
         //--- If price rose into the range = uptrend before = distribution
         g_range.bullish_bias = (last_close < first_close);
        }
      else
         g_range.bullish_bias = down_before;                                 // Set Bias
      g_range.spring_low    = 0;                                             // Clear spring low
      g_range.sos_high      = 0;                                             // Clear SOS high
      g_range.upthrust_high = 0;                                             // Clear upthrust
      g_range.sow_low       = 0;                                             // Clear SOW low
      DrawHLine("SUPPORT",    g_range.support,    clrGreen, STYLE_DASH);     // Draw support
      DrawHLine("RESISTANCE", g_range.resistance, clrRed,   STYLE_DASH);     // Draw resistance
      Print(StringFormat("WyckoffEA: Range locked | Bias: %s | S: %.5f | R: %.5f | Bars: %d | AvgVol: %.0f",
                         g_range.bullish_bias ? "ACCUMULATION" : "DISTRIBUTION",
                         g_range.support, g_range.resistance, range_bars, avg_vol));     // Log result
      return true;                                                           // Range valid
     }
   return false;                                                             // No range found
  }

Функция "DetectRange()" формирует каждый диапазон-кандидат, начиная с минимального количества баров, и постепенно расширяет его. Допуск 25% учитывает естественную изменчивость реальных диапазонов Вайкоффа, не принимая при этом чрезмерно расширенные зоны. После обнаружения диапазона вызываются функции "HasPriorDowntrend()" и "HasPriorUptrend()", которые проверяют наличие предшествующего направленного движения, после чего соответствующим образом задаётся значение "g_range.bullish_bias". Затем по барам диапазона рассчитывается средний объём — он становится базовым уровнем для всех последующих сравнений. Обратите внимание: копирование начинается с бара 1, а не с бара 0, чтобы исключить работу с незавершённым баром.

Обнаружение spring (ложный прокол поддержки) и upthrust (ложный вынос выше сопротивления)

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

//+------------------------------------------------------------------+
//| Checks the last closed bar for a valid Spring                    |
//+------------------------------------------------------------------+
bool CheckSpring()
  {
   double low1   = iLow(_Symbol, PERIOD_CURRENT, 1);                         // Last bar low
   double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);                       // Last bar close
   double pip    = PipSize();                                                 // Get pip size
   double thresh = g_range.support - InpSpringTolerance * pip;               // Spring threshold
   if(low1 < thresh && close1 > g_range.support)                             // Penetrate and recover
     {
      long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1);                   // Last bar volume
      if((double)vol1 < g_range.avg_volume * InpLowVolMult)                  // Below volume threshold
        {
         g_range.spring_low = low1;                                          // Store Spring low
         datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1);                    // Get bar time
         DrawLabel("SPRING", t1, low1 - pip * 5, "SPRING", clrLime);         // Draw label
         Print(StringFormat("WyckoffEA: SPRING | Low: %.5f | Close: %.5f | Vol: %I64d | AvgVol: %.0f",
                            low1, close1, vol1, g_range.avg_volume));                     // Log event
         return true;                                                        // Spring confirmed
        }
     }
   return false;                                                              // Not a Spring
  }

//+------------------------------------------------------------------+
//| Checks the last closed bar for a valid Upthrust                  |
//+------------------------------------------------------------------+
bool CheckUpthrust()
  {
   double high1  = iHigh(_Symbol, PERIOD_CURRENT, 1);                        // Last bar high
   double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);                       // Last bar close
   double pip    = PipSize();                                                // Get pip size
   double thresh = g_range.resistance + InpSpringTolerance * pip;            // Upthrust threshold
   if(high1 > thresh && close1 < g_range.resistance)                         // Penetrate and reverse
     {
      long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1);                   // Last bar volume
      if((double)vol1 < g_range.avg_volume * InpLowVolMult)                  // Below volume threshold
        {
         g_range.upthrust_high = high1;                                      // Store Upthrust high
         datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1);                    // Get bar time
         DrawLabel("UT", t1, high1 + pip * 5, "UPTHRUST", clrOrangeRed);     // Draw label
         Print(StringFormat("WyckoffEA: UPTHRUST | High: %.5f | Close: %.5f | Vol: %I64d | AvgVol: %.0f",
                            high1, close1, vol1, g_range.avg_volume));                    // Log event
         return true;                                                         // Upthrust confirmed
        }
     }
   return false;                                                              // Not an Upthrust
  }

Функция "CheckSpring()" одновременно проверяет три условия на последнем закрытом баре. Минимум должен опуститься ниже порога spring, то есть ниже уровня поддержки с учётом заданного допуска. В пределах того же бара цена закрытия должна вернуться выше поддержки. Тиковый объём должен быть ниже среднего объёма диапазона, умноженного на коэффициент низкого объёма, что подтверждает поглощение предложения, а не агрессивные продажи. Для подтверждения spring должны выполняться все три условия. Сохранённое значение "g_range.spring_low" используется как уровень аннулирования: если последующий бар закроется ниже него, сценарий накопления отменяется. "CheckUpthrust()" применяет зеркальную логику к сопротивлению диапазона.

Признак силы и признак слабости

Spring и upthrust необходимы, но сами по себе недостаточны. Требуется подтверждение того, что институциональный спрос или предложение действительно перехватили контроль над рынком.

//+------------------------------------------------------------------+
//| Checks the last closed bar for a Sign of Strength                |
//+------------------------------------------------------------------+
bool CheckSOS()
  {
   double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);                       // Last bar close
   double high1  = iHigh(_Symbol, PERIOD_CURRENT, 1);                        // Last bar high
   if(close1 > g_range.resistance)                                           // Close above resistance
     {
      long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1);                   // Last bar volume
      if((double)vol1 > g_range.avg_volume * InpHighVolMult)                 // Above volume threshold
        {
         g_range.sos_high = high1;                                           // Store SOS high
         datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1);                    // Get bar time
         DrawLabel("SOS", t1, high1 + PipSize() * 5, "SOS", clrDodgerBlue); // Draw label
         Print(StringFormat("WyckoffEA: SOS | Close: %.5f | Vol: %I64d | AvgVol: %.0f",
                            close1, vol1, g_range.avg_volume));              // Log event
         return true;                                                        // SOS confirmed
        }
     }
   return false;                                                              // Not a SOS
  }

//+------------------------------------------------------------------+
//| Checks the last closed bar for a Sign of Weakness                |
//+------------------------------------------------------------------+
bool CheckSOW()
  {
   double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);                       // Last bar close
   double low1   = iLow(_Symbol, PERIOD_CURRENT, 1);                         // Last bar low
   if(close1 < g_range.support)                                              // Close below support
     {
      long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1);                   // Last bar volume
      if((double)vol1 > g_range.avg_volume * InpHighVolMult)                 // Above volume threshold
        {
         g_range.sow_low = low1;                                             // Store SOW low
         datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1);                    // Get bar time
         DrawLabel("SOW", t1, low1 - PipSize() * 5, "SOW", clrCrimson);      // Draw label
         Print(StringFormat("WyckoffEA: SOW | Close: %.5f | Vol: %I64d | AvgVol: %.0f",
                            close1, vol1, g_range.avg_volume));              // Log event
         return true;                                                        // SOW confirmed
        }
     }
   return false;                                                              // Not a SOW
  }

Логика объёма для признака силы и признака слабости прямо противоположна логике spring (ложный прокол поддержки) и upthrust (ложный вынос выше сопротивления). Если для подтверждения поглощения при spring требовался низкий объём, то признак силы требует повышенного объёма, подтверждающего выход цены из диапазона под воздействием институционального спроса. Пробой сопротивления на слабом объёме неубедителен: институциональные участники не поддерживают такое движение. Пробой на объёме, как минимум в 1,2 раза превышающем средний объём диапазона, имеет необходимое структурное подтверждение. "CheckSOW()" применяет такое же требование к пробою вниз при распределении.

Логика входа: LPS и LPSY

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

//+------------------------------------------------------------------+
//| Waits for LPS pullback and opens long position                   |
//+------------------------------------------------------------------+
void CheckLPSEntry()
  {
   g_lps_count++;                                                            // Increment wait counter
   if(g_lps_count > InpLPSBars)                                              // Wait window expired
     {
      Print("WyckoffEA: LPS window expired — resetting.");                   // Log expiry
      g_state = STATE_IDLE;                                                  // Reset state
      ClearLabels();                                                         // Clear chart
      return;                                                                // Exit function
     }
   double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);                       // Last bar close
   long   vol1   = iTickVolume(_Symbol, PERIOD_CURRENT, 1);                  // Last bar volume
   bool pulled_back = (close1 < g_range.sos_high && close1 > g_range.support); // Pullback check
   bool low_vol     = ((double)vol1 < g_range.avg_volume * InpHighVolMult);     // Volume check
   Print(StringFormat("WyckoffEA: LPS check %d/%d | Close: %.5f | PulledBack: %s | LowVol: %s",
                      g_lps_count, InpLPSBars, close1,
                      pulled_back ? "YES" : "NO", low_vol ? "YES" : "NO"));  // Log check
   if(pulled_back && low_vol)                                                // LPS conditions met
     {
      double atr_buf[];                                                      // ATR buffer
      ArraySetAsSeries(atr_buf, true);                                       // Set as series
      if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1)
         return;                                                             // Copy ATR value
      double atr    = atr_buf[0];                                            // ATR value
      double ask    = SymbolInfoDouble(_Symbol, SYMBOL_ASK);                 // Current ask
      double sl     = ask - atr * InpATRMult;                                // Stop loss price
      double sl_pip = (ask - sl) / PipSize();                                // Stop in pips
      double tp     = ask + sl_pip * InpRR * PipSize();                      // Take profit price
      double lots   = CalcLots(sl_pip);                                      // Calculate lot size
      if(lots <= 0)
         return;                                                             // Invalid lot size
      long   stop_lv  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); // Broker stop level
      double min_dist = stop_lv * _Point;                                    // Minimum distance
      if(ask - sl < min_dist)
         sl = ask - min_dist - _Point;                                       // Adjust SL if needed
      if(tp - ask < min_dist)
         tp = ask + min_dist + _Point;                                       // Adjust TP if needed
      if(g_trade.Buy(lots, _Symbol, ask, sl, tp, "Wyckoff LPS Long"))        // Open long
        {
         datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1);                    // Get bar time
         DrawLabel("LPS", t1, iLow(_Symbol, PERIOD_CURRENT, 1) - PipSize() * 3, "LPS", clrGold); // Draw label
         Print(StringFormat("WyckoffEA: LONG opened | Lots: %.2f | Ask: %.5f | SL: %.5f | TP: %.5f",
                            lots, ask, sl, tp));                                          // Log trade
         g_state = STATE_IN_TRADE;                                           // Update state
        }
     }
  }

//+------------------------------------------------------------------+
//| Waits for LPSY rally and opens short position                    |
//+------------------------------------------------------------------+
void CheckLPSYEntry()
  {
   g_lpsy_count++;                                                           // Increment wait counter
   if(g_lpsy_count > InpLPSBars)                                             // Wait window expired
     {
      Print("WyckoffEA: LPSY window expired — resetting.");                  // Log expiry
      g_state = STATE_IDLE;                                                  // Reset state
      ClearLabels();                                                         // Clear chart
      return;                                                                // Exit function
     }
   double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);                       // Last bar close
   long   vol1   = iTickVolume(_Symbol, PERIOD_CURRENT, 1);                  // Last bar volume
   bool rallied = (close1 > g_range.sow_low && close1 < g_range.resistance); // Rally check
   bool low_vol = ((double)vol1 < g_range.avg_volume * InpHighVolMult);      // Volume check
   Print(StringFormat("WyckoffEA: LPSY check %d/%d | Close: %.5f | Rallied: %s | LowVol: %s",
                      g_lpsy_count, InpLPSBars, close1,
                      rallied ? "YES" : "NO", low_vol ? "YES" : "NO"));      // Log check
   if(rallied && low_vol)                                                    // LPSY conditions met
     {
      double atr_buf[];                                                      // ATR buffer
      ArraySetAsSeries(atr_buf, true);                                       // Set as series
      if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1)
         return;                                                             // Copy ATR value
      double atr    = atr_buf[0];                                            // ATR value
      double bid    = SymbolInfoDouble(_Symbol, SYMBOL_BID);                 // Current bid
      double sl     = bid + atr * InpATRMult;                                // Stop loss price
      double sl_pip = (sl - bid) / PipSize();                                // Stop in pips
      double tp     = bid - sl_pip * InpRR * PipSize();                      // Take profit price
      double lots   = CalcLots(sl_pip);                                      // Calculate lot size
      if(lots <= 0)
         return;                                                             // Invalid lot size
      long   stop_lv  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); // Broker stop level
      double min_dist = stop_lv * _Point;                                    // Minimum distance
      if(sl - bid < min_dist)
         sl = bid + min_dist + _Point;                                       // Adjust SL if needed
      if(bid - tp < min_dist)
         tp = bid - min_dist - _Point;                                       // Adjust TP if needed
      if(g_trade.Sell(lots, _Symbol, bid, sl, tp, "Wyckoff LPSY Short"))     // Open short
        {
         datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1);                    // Get bar time
         DrawLabel("LPSY", t1, iHigh(_Symbol, PERIOD_CURRENT, 1) + PipSize() * 3, "LPSY", clrGold); // Draw label
         Print(StringFormat("WyckoffEA: SHORT opened | Lots: %.2f | Bid: %.5f | SL: %.5f | TP: %.5f",
                            lots, bid, sl, tp));                                          // Log trade
         g_state = STATE_IN_TRADE;                                           // Update state
        }
     }
  }

При каждом вызове обе функции входа увеличивают счётчик баров. Если откат не происходит в течение "InpLPSBars" баров, конечный автомат возвращается в состояние "STATE_IDLE", а структура отменяется: советник никогда не догоняет движение. Для длинных позиций стоп-лосс размещается на расстоянии 1,5 ATR ниже точки входа, а для коротких — выше неё. Поэтому расстояние соответствует текущей волатильности рынка, а не фиксированному количеству пунктов. Тейк-профит задаётся в соответствии с установленным соотношением риска и прибыли. Перед выполнением сделки проверяется брокерское ограничение "SYMBOL_TRADE_STOPS_LEVEL"; если стоп-лосс или тейк-профит находятся ближе минимально допустимого расстояния, их уровни корректируются.

OnInit, OnDeinit и OnTick

После определения всех функций обнаружения и входа обработчики событий приводят советник в действие.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   g_atr_handle = iATR(_Symbol, PERIOD_CURRENT, InpATRPeriod);               // Create ATR handle
   if(g_atr_handle == INVALID_HANDLE)                                        // Check handle
     {
      Print("WyckoffEA: ATR indicator handle creation failed.");             // Log error
      return INIT_FAILED;                                                    // Return failure
     }
   g_trade.SetExpertMagicNumber(InpMagicNumber);                             // Set magic number
   g_trade.SetDeviationInPoints(InpSlippage);                                // Set slippage
   g_state       = STATE_IDLE;                                               // Initialize state
   g_lps_count   = 0;                                                        // Initialize LPS counter
   g_lpsy_count  = 0;                                                        // Initialize LPSY counter
   g_range_watch = 0;                                                        // Initialize watch counter
   g_last_bar    = 0;                                                        // Initialize bar time
   Print("WyckoffEA initialized | Symbol: ", _Symbol,
         " | TF: ", EnumToString(Period()),
         " | Magic: ", InpMagicNumber);                                      // Log initialization
   return INIT_SUCCEEDED;                                                    // Return success
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(g_atr_handle);                                            // Release ATR handle
   ClearLabels();                                                             // Remove chart objects
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   datetime current_bar = iTime(_Symbol, PERIOD_CURRENT, 0);                 // Current bar open time
   if(current_bar == g_last_bar)
      return;                                                                // Skip if same bar
   g_last_bar = current_bar;                                                 // Update last bar time
   if(g_state == STATE_IN_TRADE)                                             // In trade state
     {
      if(!PositionOpen())                                                    // Position has closed
        {
         Print("WyckoffEA: Trade closed — returning to IDLE.");              // Log closure
         g_state       = STATE_IDLE;                                         // Reset state
         g_lps_count   = 0;                                                  // Reset LPS counter
         g_lpsy_count  = 0;                                                  // Reset LPSY counter
         g_range_watch = 0;                                                  // Reset watch counter
         ClearLabels();                                                      // Clear chart
        }
      return;                                                                // Exit tick
     }
   switch(g_state)                                                           // State machine switch
     {
      case STATE_IDLE:                                                       // Idle state
         if(DetectRange())                                                   // Range found
           {
            g_state       = STATE_RANGE_FORMING;                             // Advance state
            g_range_watch = 0;                                               // Reset watch counter
           }
         break;                                                              // End case
      case STATE_RANGE_FORMING:                                              // Range locked — watch only
         g_range_watch++;                                                    // Increment watch counter
         if(g_range_watch > InpRangeWatchBars)                               // Range stale
           {
            Print("WyckoffEA: Range watch expired — returning to IDLE.");    // Log expiry
            g_state = STATE_IDLE;                                            // Reset state
            ClearLabels();                                                   // Clear chart
            break;                                                           // End case
           }
         if(g_range.bullish_bias)                                            // Accumulation bias
           {
            if(CheckSpring())
               g_state = STATE_SPRING_DETECTED;                              // Spring found
           }
         else                                                                // Distribution bias
           {
            if(CheckUpthrust())
               g_state = STATE_UPTHRUST_DETECTED;                            // Upthrust found
           }
         break;                                                              // End case
      case STATE_SPRING_DETECTED:                                            // Spring detected
         if(CheckSOS())                                                      // SOS found
           {
            g_state     = STATE_SOS_CONFIRMED;                               // Advance state
            g_lps_count = 0;                                                 // Reset LPS counter
           }
         else
            if(iClose(_Symbol, PERIOD_CURRENT, 1) < g_range.spring_low)      // Spring failed
              {
               Print("WyckoffEA: Spring invalidated — returning to IDLE.");  // Log failure
               g_state = STATE_IDLE;                                         // Reset state
               ClearLabels();                                                // Clear chart
              }
         break;                                                              // End case
      case STATE_SOS_CONFIRMED:                                              // SOS confirmed
         CheckLPSEntry();                                                    // Check LPS entry
         break;                                                              // End case
      case STATE_UPTHRUST_DETECTED:                                          // Upthrust detected
         if(CheckSOW())                                                      // SOW found
           {
            g_state      = STATE_SOW_CONFIRMED;                              // Advance state
            g_lpsy_count = 0;                                                // Reset LPSY counter
           }
         else
            if(iClose(_Symbol, PERIOD_CURRENT, 1) > g_range.upthrust_high)    // Upthrust failed
              {
               Print("WyckoffEA: Upthrust invalidated — returning to IDLE.");  // Log failure
               g_state = STATE_IDLE;                                           // Reset state
               ClearLabels();                                                  // Clear chart
              }
         break;                                                                // End case
      case STATE_SOW_CONFIRMED:                                                // SOW confirmed
         CheckLPSYEntry();                                                     // Check LPSY entry
         break;                                                                // End case
     }
  }

Функция "OnInit()" создаёт дескриптор индикатора ATR и немедленно возвращает "INIT_FAILED" в случае ошибки, не позволяя советнику работать без средства расчёта расстояния до стоп-лосса. "OnDeinit()" освобождает дескриптор индикатора и удаляет все графические объекты. "OnTick()" использует фильтр нового бара: время открытия текущего бара сравнивается с сохранённым значением "g_last_bar", поэтому вся логика обнаружения выполняется ровно один раз для каждого закрытого бара, а не на каждом тике. Оператор switch делает конечный автомат наглядным: каждый вариант отвечает за одну задачу, каждый переход имеет один триггер, а код не может вызвать функцию обнаружения, не пройдя через все предшествующие состояния.


Тестирование на исторических данных

Для тестирования советника откройте Тестер стратегий MetaTrader 5 и задайте следующие параметры: символ — EURUSD; таймфрейм — H4; режим моделирования — «Все тики на основе реальных тиков»; начальный депозит — 10 000 долларов США; период — 01.01.2022–31.12.2024. Используйте следующие входные параметры: "InpTrendBars" = 15; "InpMinRangeBars" = 15; "InpMaxRangeBars" = 60; "InpMinRangePips" = 20; "InpMaxRangePips" = 400; "InpSpringTolerance" = 10; "InpHighVolMult" = 1.2; "InpLowVolMult" = 1.2; "InpRiskPercent" = 1.0; "InpRR" = 2.0; "InpATRPeriod" = 14; "InpATRMult" = 1.5; "InpLPSBars" = 8.

Ожидаемые результаты

Структуры Вайкоффа возникают нечасто. На EURUSD H4 можно ожидать от четырёх до десяти подходящих моделей в год. Такое поведение является нормальным: советник не генерирует сигналы постоянно, а ожидает формирования полной последовательности подтверждений. При открытии позиции имеются все необходимые структурные основания: подтверждённый диапазон, подтверждённый spring или upthrust, подтверждённый признак силы или признак слабости и вход на откате.

Длительность сделок будет иметь бимодальное распределение: многие непродолжительные сделки в несостоявшихся структурах закроются по первоначальному стоп-лоссу, тогда как более редкие, но значительно более продолжительные прибыльные сделки охватят фазу роста или снижения после завершённого накопления либо распределения. Средняя прибыльная сделка должна быть существенно больше средней убыточной. Если коэффициент прибыльности (profit factor) стабильно ниже 1,0, увеличьте "InpHighVolMult", чтобы требовать более сильного подтверждения признака силы или признака слабости.

Результаты тестирования

Демонстрация на EURUSD H4.

Demonstration on a chart

Рис. 3. Наглядная демонстрация обнаружения сигналов и открытия позиции.

graph

Рис. 4. Результаты тестирования: кривые средств и баланса.

test results

Рис. 5. Результаты тестирования.

entries

Рис. 6. Результаты тестирования и точки входа.


Известные ограничения

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

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

Советник отслеживает только одну структуру одновременно. Если spring аннулируется из-за закрытия цены ниже его минимума, конечный автомат возвращается в состояние "STATE_IDLE" и начинает поиск нового диапазона. Это соответствует логике Вайкоффа: неудачный spring меняет интерпретацию структуры.

Окно входа для LPS и LPSY имеет фиксированную длину. После подтверждения признака силы или признака слабости советник ожидает откат в течение не более "InpLPSBars" баров. При сильном росте или снижении цена может не выполнить откат за этот период. В таком случае советник сбрасывает состояние, а не пытается догнать движение. Такое консервативное поведение заложено в логику советника намеренно.

Код представляет собой демонстрацию концепции Вайкоффа на MQL5. Перед запуском в реальной торговле настройте коэффициенты объёма с учётом типичного поведения тикового объёма выбранного символа и проведите тестирование как минимум за 24 месяца на демо-счёте.


Заключение

Вайкофф десятилетиями изучал то же поведение рынка, которое современные трейдеры называют концепциями Smart Money («умных денег»), следами институциональных участников и инженерией ликвидности. Его метод по-прежнему актуален, поскольку описывает структуру рынка через причины и следствия, а не через графические модели и индикаторы. Причинно-следственные связи не устаревают.

Сложность автоматизации метода Вайкоффа заключается не в обнаружении отдельных событий. Любой советник способен определить закрытие ниже поддержки. Основная задача — обеспечить, чтобы каждое событие имело смысл только в контексте предшествующих событий. Именно этот контекст обеспечивает конечный автомат, представленный в статье. Spring — не просто ложный пробой. Это ложный пробой внутри определённого диапазона после предшествующего нисходящего тренда и на объёме ниже среднего. Если убрать хотя бы одно из этих условий, сигнал перестанет быть spring по Вайкоффу и станет обычным снижением. Конечный автомат не позволяет обойти эту последовательность. Код не может вызвать "CheckSpring()", не пройдя сначала через "DetectRange()", и не может вызвать "CheckLPSEntry()", не выполнив предварительно "CheckSpring()" и "CheckSOS()".

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

Советник открывает сделки нечасто, однако каждый вход подкреплён полностью подтверждённой последовательностью Вайкоффа.

Весь код был скомпилирован и протестирован в MetaTrader 5. Полный исходный файл WyckoffEA.mq5 приложен к статье. Скопируйте WyckoffEA.mq5 в папку MQL5\\Experts\\ и скомпилируйте его в MetaEditor — дополнительные зависимости не требуются. Рекомендуемый инструмент и таймфрейм: EURUSD H4. Перед запуском в реальной торговле обязательно протестируйте советник на демо-счёте.

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

Прикрепленные файлы |
WyckoffEA.mq5 (41.36 KB)
Анализ CSV-данных (Часть 2): Построение конвейера экспорта и парсинга CSV промышленного уровня для количественного анализа стратегий Анализ CSV-данных (Часть 2): Построение конвейера экспорта и парсинга CSV промышленного уровня для количественного анализа стратегий
Файловая система MQL5 работает в строгой песочнице. Понимание флагов доступа и правил разрешения путей лежит в основе любого надёжного пайплайна экспорта. В статье создаётся класс CCSVExporter, который отвечает за создание файлов, безопасное дописывание и механизмы восстановления при ошибках. Также рассматриваются разбор CSV-файлов, токенизация полей, конфликты параллельного доступа и стратегии буферизации записи при интенсивных сериях оптимизации.
Кодирование свечных моделей (Часть 2): Моделирование движения цены в виде упорядоченной последовательности Кодирование свечных моделей (Часть 2): Моделирование движения цены в виде упорядоченной последовательности
Разработка инструментов на основе перестановок в MQL5 обеспечивает систематический способ анализа комбинаций свечных паттернов для торговых стратегий. В этой статье представлен калькулятор и генератор перестановок, предназначенные для вычисления и перечисления всех возможных упорядоченных последовательностей свечей из бычьих и медвежьих наборов, с повторениями или без них. Генерируя исчерпывающие комбинации паттернов, трейдеры могут проводить анализ на основе данных для выявления высоковероятных рыночных паттернов и улучшения принятия решений в автоматизированных торговых системах.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Анализ CSV-данных (Часть 1): Механизм экспорта CSV при многоядерной оптимизации в MQL5 Анализ CSV-данных (Часть 1): Механизм экспорта CSV при многоядерной оптимизации в MQL5
При многоядерной оптимизации в MetaTrader 5 результаты могут незаметно теряться, когда параллельные агенты одновременно обращаются к одному CSV-файлу. Повторно используемый механизм экспорта MQL5 применяет итерационную спин-блокировку, чтобы надёжно открыть файл и добавлять строки без потерь. Он сохраняет пользовательские метрики, включая коэффициент Сортино, среднюю продолжительность сделки и показатели качества сигналов — запаздывание и частота ложных разворотов (whipsaws) — в сводном CSV-файле для последующей аналитической обработки.