Автоматизация классических рыночных методов в MQL5 (Часть 1): Накопление и распределение по Вайкоффу
Введение
Одной из наиболее устойчивых концепций технического анализа является метод Вайкоффа. Ричард Вайкофф разработал его в 1930-х годах после десятилетий наблюдений за тем, как крупные институциональные участники накапливают и распределяют позиции на финансовых рынках.
Этот метод рассматривает движение рынка не как случайный шум, а как последовательность причинно-следственных циклов, обусловленных институциональным спросом и предложением. Такие циклы оставляют характерные структурные признаки — spring (ложный прокол поддержки), признак силы (sign of strength, SOS), upthrust (ложный вынос выше сопротивления) и признак слабости (sign of weakness, SOW), — которые опытный трейдер может распознать и отрабатывать в сделках.
Несмотря на широкое распространение, метод Вайкоффа редко автоматизируют в MQL5. Причина не в сложности обнаружения отдельных событий: например, определить факт закрытия ниже поддержки тривиально. Основная трудность состоит в том, что каждое событие Вайкоффа имеет смысл только в контексте предшествующих событий. Spring — это не просто снижение ниже поддержки. Это уход ниже поддержки внутри определённого диапазона после предшествующего нисходящего тренда, подтверждённый характерным объёмом. Само по себе такое событие ничего не значит; важно выявить его как часть подтверждённой последовательности.
В этой статье мы создадим полноценный советник, автоматизирующий последовательное обнаружение событий с помощью конечного автомата. На таймфрейме H4 советник выявляет структуры накопления, обнаруживает spring как финальный вытряхивающий манёвр, подтверждает спрос по признаку силы и открывает длинную позицию в последней точке поддержки (LPS). Для распределения применяется зеркальная логика: upthrust, признак слабости и открытие короткой позиции в последней точке предложения (LPSY). Рассмотрим следующие темы:
- Основы концепции Вайкоффа
- Почему тиковый объём служит косвенным показателем институциональной активности
- Архитектура конечного автомата
- Реализация в MQL5
- Тестирование на исторических данных
- Заключение
В результате вы получите полностью работоспособный советник 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.

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

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

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

Рис. 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Анализ CSV-данных (Часть 2): Построение конвейера экспорта и парсинга CSV промышленного уровня для количественного анализа стратегий
Кодирование свечных моделей (Часть 2): Моделирование движения цены в виде упорядоченной последовательности
Анализ CSV-данных (Часть 1): Механизм экспорта CSV при многоядерной оптимизации в MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования