Автоматизация классических рыночных методов в MQL5 (Часть 2): Закон причины и следствия Вайкоффа — ценовые цели по графику "крестики-нолики"
Введение
В Части 1 мы создали советник, который распознаёт накопление и распределение по Вайкоффу с помощью конечного автомата. Для длинных позиций он подтверждает последовательность «спринг → SOS», а для коротких — «аптраст → SOW», после чего открывает позицию в области последней точки поддержки или последней точки предложения.
Тот советник отвечал на вопрос, где входить в позицию. Эта статья отвечает на связанный с ним вопрос, который Вайкофф считал не менее важным: где из неё выходить. Ответ даёт его второй закон — закон причины и следствия — и горизонтальный подсчёт по графику «крестики-нолики», позволяющий измерить эту связь. Советник, описанный в этой статье, полностью автономен: ниже приведены все необходимые для его работы функции, а код из Части 1 не требуется.
Ричард Вайкофф учил, что рынок проходит две основные фазы: накопление, в ходе которого крупные операторы незаметно поглощают предложение по низким ценам, и распределение, когда они сбрасывают эти позиции по высоким ценам. Переход от накопления к росту или от распределения к снижению не случаен. Он представляет собой пропорциональное следствие измеримой причины. Вайкофф называл это своим вторым законом и предложил трейдерам точный инструмент измерения — подсчёт по графику «крестики-нолики».
Далее мы реализуем полноценный советник на MQL5, автоматизирующий торговый цикл Вайкоффа. Он распознаёт структуры с помощью конечного автомата, подтверждает ключевые события, открывает позицию в структурно корректной точке входа и рассчитывает тейк-профит по модели диапазона на основе графика «крестики-нолики».
Внешние библиотеки не требуются: достаточно стандартного include-файла "Trade\Trade.mqh".
В статье рассмотрены следующие темы:
- Накопление и распределение по Вайкоффу
- Закон причины и следствия: теория и формула
- Архитектура решения
- Реализация на MQL5
- Ключевые решения при реализации
- Тестирование
- Известные ограничения
- Заключение
Накопление и распределение по Вайкоффу
Вайкофф выделил две структурные модели, предшествующие крупным ценовым движениям: накопление перед фазой роста и распределение перед фазой снижения. Обе модели развиваются в узнаваемой последовательности событий. Чтобы понять логику решений советника, нужно понимать эту последовательность.
Последовательность накопления
Накопление формируется после нисходящего тренда. Крупные участники рынка — то, что Вайкофф называл Composite Man, совокупным крупным оператором, — начинают поглощать предложение частных продавцов. Структура проходит через пять фаз, однако советник сосредоточен на конкретных событиях, подтверждающих её формирование.
Первое ключевое событие — спринг. Цена ненадолго пробивает поддержку, опускаясь ниже минимума диапазона, а затем на том же баре возвращается внутрь диапазона. Это движение выбивает слабых держателей длинных позиций и проверяет, осталось ли на рынке реальное предложение. Спринг при относительно низком тиковом объёме указывает на исчерпание давления продавцов.
За спрингом следует признак силы. Цена закрывается выше сопротивления на высоком тиковом объёме, показывая, что инициатива перешла к покупателям и происходит выход из диапазона вверх.
Последняя точка поддержки служит сигналом для входа. После признака силы цена на низком объёме откатывается к уровню сопротивления, который теперь стал поддержкой. Этот откат является последним ретестом перед началом фазы роста. Советник открывает длинную позицию в последней точке поддержки.
Зеркало накопления: распределение
Распределение является зеркальным отражением накопления. Оно формируется после восходящего тренда, когда крупные участники рынка передают свои позиции частным покупателям.
Аптраст — ключевое событие, соответствующее спрингу. Цена ненадолго пробивает сопротивление, а затем на том же баре возвращается внутрь диапазона при относительно низком тиковом объёме. После аптраста появляется признак слабости: цена закрывается ниже поддержки на высоком тиковом объёме.
Последняя точка предложения служит сигналом для входа. После признака слабости цена на низком объёме поднимается к уровню поддержки, который теперь выступает сопротивлением. Советник открывает короткую позицию в последней точке предложения.
Почему важна последовательность распознавания
Каждое из этих событий имеет значение только в соответствующем контексте. Закрытие ниже поддержки не является признаком слабости, если до этого не был подтверждён аптраст. Откат после пробоя не является последней точкой поддержки, если ему последовательно не предшествовали спринг и признак силы. Изолированная проверка любого отдельного события без учёта предшествующей структуры даст множество ложных сигналов. Конечный автомат обеспечивает правильную последовательность: каждое событие должно следовать за предыдущим. Если последовательность нарушается — например, цена закрывается ниже минимума спринга до подтверждения признака силы, — автомат возвращается в состояние ожидания и начинает поиск заново.
Закон причины и следствия: теория и формула

Рисунок 1. График со спрингом, SOS, LPS, точкой входа и целью по закону причины и следствия.
Формула ценовой цели по графику «крестики-нолики» имеет два варианта: для накопления и для распределения.
Для цели длинной позиции из диапазона накопления:
Target = (Column Count × Box Size × Reversal Factor) + Count Line Price
Для цели короткой позиции из диапазона распределения:
Target = Count Line Price − (Column Count × Box Size × Reversal Factor)
Каждый элемент формулы имеет точное определение.
Линия подсчёта — это ценовой уровень, на котором выполняется горизонтальный подсчёт. Для накопления Вайкофф предписывал проводить её на уровне последней точки поддержки — то есть последнего минимума перед признаком силы. На этом уровне проявился наиболее сильный институциональный спрос, поэтому он является самым консервативным уровнем для проецирования цели. Для распределения линия подсчёта проводится на уровне последней точки предложения — то есть последнего максимума перед признаком слабости.
Количество столбцов — это число столбцов графика «крестики-нолики» внутри торгового диапазона на уровне линии подсчёта. Столбец учитывается, если график фиксирует разворот на этом ценовом уровне или проходит через него. Чем шире диапазон накопления, тем больше в нём столбцов и тем выше расчётная цель.
Размер клетки — это шаг цены, представляемый каждым крестиком X или ноликом O. При слишком малом размере незначительные колебания создают шум, а при слишком большом скрывается структура. В этой статье размер клетки рассчитывается по ATR диапазона (примерно 0,25 ATR), что обеспечивает адаптацию к волатильности различных инструментов.
Коэффициент разворота — это количество клеток, необходимое для формирования нового столбца. Разворот на одну клетку наиболее подробно отражает движение: он даёт наибольшее число столбцов и, следовательно, наиболее оптимистичную цель. Для внутридневной и свинг-торговли валютными парами обычно используют разворот на одну клетку. Разворот на три клетки чаще применяется в долгосрочном анализе и даёт более консервативные цели, поскольку при том же движении цены формируется меньше столбцов.
В статье используется разворот на одну клетку. Он наиболее полно отражает колебания внутри торгового диапазона и тем самым обеспечивает наиболее точный подсчёт столбцов.
Рассмотрим пример с теми же параметрами EURUSD, что использовались в Части 1:
Советник определил диапазон от 1,0680 (поддержка) до 1,0820 (сопротивление). ATR в период формирования диапазона составляет 22 пипса. Размер клетки: 0,25 × 22 = 5,5 пипса; для удобства расчёта округлим его до 5 пипсов. Линия подсчёта, проведённая через последнюю точку поддержки, находится на уровне 1,0700. Представление диапазона в виде графика «крестики-нолики» даёт 18 столбцов на уровне 1,0700. Коэффициент разворота равен 1.
Цель длинной позиции = (18 × 5 пипсов × 1) + 1,0700 = 1,0700 + 90 пипсов = 1,0790
Согласно расчёту, фаза роста должна достичь уровня 1,0790. Если фактический рост выводит цену выше 1,0820 — верхней границы диапазона, — цель, вероятно, была консервативной, что полностью соответствует подходу Вайкоффа. Он рекомендовал сначала использовать самый консервативный подсчёт.
Архитектура решения
Перед написанием кода полезно понять, как связаны его части. Советник работает как конвейер: каждый этап передаёт результат следующему.
Бары OHLC и тиковый объём ↓ DetectRange() Определяет поддержку, сопротивление, контекст накопления/распределения, средний объём и ATR диапазона ↓ CheckSpring() или CheckUpthrust() Подтверждает структурное событие входа на низком объёме ↓ CheckSOS() или CheckSOW() Подтверждает направленный выход из диапазона на высоком объёме ↓ CheckLPSEntry() или CheckLPSYEntry() Ожидает отката и устанавливает линию подсчёта ↓ CalcPFTarget() Последовательно вызывает CalcRangeATR, BuildPFChart и CountColumnsAtLevel Возвращает расчётную цену или 0 для использования резервной цели ↓ CTrade.Buy() или CTrade.Sell() Открывает позицию со структурным тейк-профитом или с резервной целью 2R
Конечный автомат определяет, какой этап активен на текущем баре. Переход к следующему состоянию происходит только после подтверждения условий текущего этапа; при выполнении условия отмены автомат возвращается в состояние ожидания.
Состояния конечного автомата
Конечный автомат проходит через семь состояний. "STATE_IDLE" означает, что активной структуры нет и советник ищет диапазон. "STATE_RANGE_FORMING" означает, что валидный диапазон определён и советник ожидает спринг или аптраст. "STATE_SPRING_DETECTED" означает, что спринг подтверждён и советник ожидает признак силы. "STATE_SOS_CONFIRMED" означает, что признак силы подтверждён и советник ожидает откат к последней точке поддержки. "STATE_UPTHRUST_DETECTED" означает, что аптраст подтверждён и советник ожидает признак слабости. "STATE_SOW_CONFIRMED" означает, что признак слабости подтверждён и советник ожидает подъём к последней точке предложения. "STATE_IN_TRADE" означает, что позиция открыта и советник отслеживает её закрытие.
Карта функций
"DetectRange()" определяет корректный торговый диапазон Вайкоффа по барам OHLC. "HasPriorDowntrend()" подтверждает, что диапазону предшествовал нисходящий тренд, создающий контекст накопления. "HasPriorUptrend()" подтверждает, что диапазону предшествовал восходящий тренд, создающий контекст распределения. "CheckSpring()" распознаёт спринг на последнем закрытом баре. "CheckSOS()" распознаёт признак силы на последнем закрытом баре. "CheckUpthrust()" распознаёт аптраст на последнем закрытом баре. "CheckSOW()" распознаёт признак слабости на последнем закрытом баре. "CheckLPSEntry()" ожидает откат к последней точке поддержки и открывает длинную позицию. "CheckLPSYEntry()" ожидает подъём к последней точке предложения и открывает короткую позицию. "CalcRangeATR()" рассчитывает среднее значение "ATR" по барам диапазона. "BuildPFChart()" преобразует данные баров OHLC в последовательность столбцов "P&F" . "CountColumnsAtLevel()" подсчитывает количество столбцов, проходящих через линию подсчёта. "CalcPFTarget()" координирует полный расчёт цели по "P&F" . "CalcLots()" рассчитывает размер позиции с учётом денежного риска и расстояния до стоп-лосса. "PipSize()" возвращает размер пункта для текущего символа.
Реализация на MQL5
В каждом следующем разделе рассматривается отдельная логическая часть советника: назначение кода, сам фрагмент и пояснение ключевых решений.
Заголовок, подключаемые файлы и перечисления
Советнику требуется только стандартная торговая библиотека. Перечисление конечного автомата задаёт семь возможных состояний конвейера распознавания структуры Вайкоффа.
//+------------------------------------------------------------------+ //| WyckoffCauseEffectEA.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 "Automating Classic Market Methods in MQL5 Part 2" #property description "Wyckoff Cause and Effect — P&F Price Targets" #property description "Extends Part 1 with structural take-profit calculation" #property description "H4 timeframe recommended" #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| State Machine | //+------------------------------------------------------------------+ enum ENUM_WYCKOFF_STATE { STATE_IDLE, STATE_RANGE_FORMING, STATE_SPRING_DETECTED, STATE_SOS_CONFIRMED, STATE_UPTHRUST_DETECTED, STATE_SOW_CONFIRMED, STATE_IN_TRADE };
Входные параметры
Входные параметры разделены на четыре группы. Группа "Range Detection" («Определение диапазона») определяет, как советник выявляет корректную консолидацию. Группа "Volume Settings" («Настройки объёма») задаёт пороги тикового объёма, позволяющие отличать значимые события от шума. Группа "P&F Target Settings" («Настройки целей P&F») управляет расчётом тейк-профита. Группа "Entry and Risk" («Вход и риск») управляет размещением стоп-лосса и расчётом размера позиции.
//+------------------------------------------------------------------+ //| 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 in pips input double InpMaxRangePips = 400.0; // Maximum range height in 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 for SOS and SOW input double InpLowVolMult = 1.2; // Low-volume multiplier for Spring and Upthrust input group "=== P&F Target Settings ===" input int InpPFReversalBoxes = 1; // P&F reversal factor (1 = most conservative) input double InpMinTargetPips = 30.0; // Minimum valid target distance in pips input double InpMaxTargetPips = 500.0; // Maximum valid target distance in pips input group "=== Entry and Risk ===" input double InpRiskPercent = 1.0; // Risk per trade as percent of balance input int InpATRPeriod = 14; // ATR period for stop and box size input double InpATRMult = 1.5; // ATR multiplier for stop distance input int InpLPSBars = 8; // Bars to wait for LPS or LPSY pullback input group "=== General ===" input int InpMagicNumber = 888001; // Magic number input int InpSlippage = 10; // Slippage in points input bool InpShowLabels = true; // Draw event labels on chart
"InpPFReversalBoxes" задаёт количество клеток, которое цена должна пройти в противоположном направлении для регистрации нового столбца. При значении 1 движение на одну клетку в обратную сторону начинает новый столбец, обеспечивая наиболее полное представление колебаний внутри диапазона. "InpMinTargetPips" и "InpMaxTargetPips" защищают от бессмысленных проекций: если рассчитанная цель выходит за заданные пределы, советник использует резервный тейк-профит 2R и записывает причину в журнал.
Структуры данных
Рабочее состояние советника хранится в двух структурах. "SPFColumn" представляет один столбец графика «крестики-нолики». "SWyckoffRange" содержит все данные о текущем обнаруженном торговом диапазоне, включая два поля, добавленные для расчёта "P&F" : "range_atr" и "count_line."
//+------------------------------------------------------------------+ //| P&F Column Structure | //+------------------------------------------------------------------+ struct SPFColumn { bool is_up; // true = X column (rising), false = O column (falling) double extreme; // Furthest price reached in this column double base; // Price where this column began };Трёх полей достаточно, чтобы определить, пересекает ли столбец заданный ценовой уровень. Для проверки линии подсчёта нужны только "base" и "extreme." Флаг "is_up" определяет, какой ценовой экстремум отслеживается при построении.
//+------------------------------------------------------------------+ //| Wyckoff Range 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 double count_line; // P&F count line price (LPS or LPSY level) double range_atr; // ATR averaged over range bars };
Поле "count_line" инициализируется нулём в "DetectRange()" и получает фактическую цену закрытия последней точки поддержки или предложения только после подтверждения условий входа. Благодаря этому подсчёт всегда выполняется на реальном уровне отката, а не на заранее оценённом уровне. Поле "range_atr" заполняется вместе с другими параметрами диапазона и используется функцией "CalcPFTarget()" как основа для размера клетки.
Глобальные переменные и вспомогательные функции
//+------------------------------------------------------------------+ //| 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 stateФункция "PipSize()" возвращает корректный размер пункта для символов с 4 или 2 знаками, а также с 5 или 3 знаками. "CalcLots()" рассчитывает размер позиции по стоимости и размеру тика, указанным в спецификации символа, поэтому корректно работает не только с обычными валютными парами, но и с металлами и CFD.
//+------------------------------------------------------------------+ //| 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 in pips | //+------------------------------------------------------------------+ 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 }
Вместо фиксированной стоимости пункта денежный риск рассчитывается как размер пункта, делённый на размер тика и умноженный на стоимость тика и объём позиции. Такой подход корректно работает для пар с JPY, золота, индексов и других инструментов, где упрощённое умножение пунктов на лоты даёт неверный результат.
За визуальное отображение отвечают три вспомогательные функции графика:
//+------------------------------------------------------------------+ //| Returns true 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 } //+------------------------------------------------------------------+ //| 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 = "WYK2_" + 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 line 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 = "WYK2_" + 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, "WYK2_") == 0) ObjectDelete(0, name); // Delete if EA's } ChartRedraw(0); // Redraw chart }
Имена всех объектов советника на графике начинаются с префикса "WYK2_", поэтому их можно удалить выборочно, не затрагивая другие объекты.
Расчёт ATR диапазона
Назначение: рассчитать среднее значение ATR по барам обнаруженного диапазона и использовать его как основу для размера клетки графика «крестики-нолики». Вход: количество баров диапазона. Выход: среднее значение ATR в ценовых единицах либо 0 при недостатке данных.
//+------------------------------------------------------------------+ //| Computes average ATR over the range period | //+------------------------------------------------------------------+ double CalcRangeATR(int range_bars) { double atr_buf[]; // ATR buffer ArraySetAsSeries(atr_buf, true); // Set as series if(CopyBuffer(g_atr_handle, 0, 1, range_bars, atr_buf) < range_bars) return 0; // Copy ATR values double total = 0; // Accumulator for(int i = 0; i < range_bars; i++) total += atr_buf[i]; // Sum ATR values return total / range_bars; // Return average }
Использование среднего ATR за период диапазона вместо текущего ATR позволяет учитывать волатильность во время формирования причины, а не в момент входа. Диапазон, сформированный при низкой волатильности, даст меньший размер клетки и, следовательно, больше столбцов, чем диапазон такой же ширины, сформированный при высокой волатильности. Это корректно отражает более частые колебания цены в доступном ценовом пространстве.
Построение столбцов «крестики-нолики» по барам OHLC
Назначение: преобразовать данные OHLC торгового диапазона в последовательность столбцов графика «крестики-нолики». Вход: поддержка и сопротивление диапазона, количество баров, размер клетки, коэффициент разворота и выходной массив. Выход: общее количество столбцов; массив "columns[]" заполняется непосредственно.
В MQL5 нет встроенного типа данных для графиков «крестики-нолики». Функция "BuildPFChart()" строит такой график, последовательно просматривая бары диапазона от старых к новым, отслеживая направление и экстремум текущего столбца и создавая новый столбец, когда цена проходит в противоположном направлении расстояние, достаточное для разворота.
//+------------------------------------------------------------------+ //| Builds P&F chart from bar data — returns column count | //+------------------------------------------------------------------+ int BuildPFChart(double range_support, double range_resistance, int range_bars, double box_size, int reversal_boxes, SPFColumn &columns[]) { ArrayResize(columns, 0); // Clear array 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, 1, range_bars, high_buf) < range_bars) return 0; // Copy highs if(CopyLow(_Symbol, PERIOD_CURRENT, 1, range_bars, low_buf) < range_bars) return 0; // Copy lows double reversal_dist = box_size * reversal_boxes; // Reversal distance bool col_is_up = (high_buf[range_bars - 1] > range_support + box_size); // First direction double col_extreme = col_is_up ? high_buf[range_bars - 1] : low_buf[range_bars - 1]; // First extreme SPFColumn first_col; // First column first_col.is_up = col_is_up; // Set direction first_col.extreme = col_extreme; // Set extreme first_col.base = col_extreme; // Set base ArrayResize(columns, 1); // Add first column columns[0] = first_col; // Store column int col_count = 1; // Column counter for(int i = range_bars - 2; i >= 0; i--) // Scan bars oldest to newest { double bar_high = high_buf[i]; // Bar high double bar_low = low_buf[i]; // Bar low if(col_is_up) // In up column { if(bar_high > col_extreme + box_size) // Extends column { col_extreme = MathFloor((bar_high - range_support) / box_size) * box_size + range_support; // Snap to grid columns[col_count - 1].extreme = col_extreme; // Update extreme } else if(bar_low <= col_extreme - reversal_dist) // Reversal { col_is_up = false; // Switch direction double new_base = col_extreme; // New base col_extreme = MathCeil((bar_low - range_support) / box_size) * box_size + range_support; // Snap low SPFColumn nc; // New column nc.is_up = false; // Down column nc.extreme = col_extreme; // Set extreme nc.base = new_base; // Set base ArrayResize(columns, col_count + 1); // Expand array columns[col_count] = nc; // Store column col_count++; // Increment count } } else // In down column { if(bar_low < col_extreme - box_size) // Extends column { col_extreme = MathCeil((bar_low - range_support) / box_size) * box_size + range_support; // Snap to grid columns[col_count - 1].extreme = col_extreme; // Update extreme } else if(bar_high >= col_extreme + reversal_dist) // Reversal { col_is_up = true; // Switch direction double new_base = col_extreme; // New base col_extreme = MathFloor((bar_high - range_support) / box_size) * box_size + range_support; // Snap high SPFColumn nc; // New column nc.is_up = true; // Up column nc.extreme = col_extreme; // Set extreme nc.base = new_base; // Set base ArrayResize(columns, col_count + 1); // Expand array columns[col_count] = nc; // Store column col_count++; // Increment count } } } return col_count; // Return count }
Логика привязки к сетке — "MathFloor", умноженный на размер клетки и дополненный уровнем поддержки диапазона для максимумов, и "MathCeil" с теми же операциями для минимумов — выравнивает экстремумы столбцов по ближайшим границам клеток относительно поддержки диапазона. Это необходимо для сопоставимости столбцов при подсчёте на заданном ценовом уровне. Без привязки к сетке вычисления с плавающей точкой могут привести к непоследовательному расположению экстремумов относительно уровня.
Примечание по реализации: график «крестики-нолики» восстанавливается по данным OHLC баров, а не по внутридневным тикам. На очень малых таймфреймах один бар может содержать несколько движений, достаточных для разворота, которые не видны на уровне баров. Для этого подхода рекомендуется использовать таймфрейм H4 или выше.
Подсчёт горизонтальной причины
Назначение: определить, сколько столбцов графика «крестики-нолики» проходит через уровень линии подсчёта. Вход: сформированный массив столбцов, их количество и цена линии подсчёта. Выход: количество столбцов, диапазон которых включает линию подсчёта.
//+------------------------------------------------------------------+ //| Counts columns passing through the count line level | //+------------------------------------------------------------------+ int CountColumnsAtLevel(const SPFColumn &columns[], int col_count, double count_line) { int count = 0; // Initialize counter for(int i = 0; i < col_count; i++) // Iterate columns { double col_top = MathMax(columns[i].base, columns[i].extreme); // Column top double col_bot = MathMin(columns[i].base, columns[i].extreme); // Column bottom if(count_line >= col_bot && count_line <= col_top) // Passes through level count++; // Increment counter } return count; // Return count }Столбец проходит через линию подсчёта, если её цена находится между значениями "base" и "extreme" включительно. Направление столбца для этой проверки не имеет значения: восходящие и нисходящие столбцы учитываются одинаково, если охватывают данный уровень. Так измеряется горизонтальная ширина причины на линии подсчёта.
Расчёт цели по графику «крестики-нолики»
Назначение: выполнить полный расчёт цели по графику «крестики-нолики» и вернуть проверенную ценовую цель. Вход: флаг направления, где true означает длинную позицию, а false — короткую. Остальные данные считываются из "g_range". Выход: расчётная ценовая цель либо 0, если результат не прошёл проверку.
//+------------------------------------------------------------------+ //| Calculates P&F price target from the confirmed range | //+------------------------------------------------------------------+ double CalcPFTarget(bool is_long) { double box_size = g_range.range_atr * 0.25; // Box size = 0.25 ATR double pip = PipSize(); // Get pip size if(box_size < pip * 3) box_size = pip * 3; // Minimum box size if(box_size <= 0) return 0; // Invalid box size SPFColumn columns[]; // Column array int col_count = BuildPFChart(g_range.support, g_range.resistance, // Build P&F chart g_range.start_bar, box_size, InpPFReversalBoxes, columns); if(col_count < 2) { Print("WyckoffP&F: Insufficient columns (", col_count, ") — skipping P&F target."); // Log return 0; // Not enough columns } int h_count = CountColumnsAtLevel(columns, col_count, g_range.count_line); // Count at level if(h_count < 2) { Print("WyckoffP&F: Horizontal count too small (", h_count, ") — skipping."); // Log return 0; // Count too small } double projected_move = h_count * box_size * InpPFReversalBoxes; // Project move double target = is_long // Calculate target ? g_range.count_line + projected_move : g_range.count_line - projected_move; double target_pips = MathAbs(target - g_range.count_line) / pip; // Target in pips if(target_pips < InpMinTargetPips || target_pips > InpMaxTargetPips) // Validate range { Print(StringFormat("WyckoffP&F: Target %.1f pips outside valid range [%.0f, %.0f] — skipping.", target_pips, InpMinTargetPips, InpMaxTargetPips)); // Log validation return 0; // Invalid target } Print(StringFormat("WyckoffP&F: Box:%.5f | Cols:%d | Count:%d | Move:%.5f | Target:%.5f", box_size, col_count, h_count, projected_move, target)); // Log result return target; // Return target }
Функция записывает в журнал все параметры расчёта: размер клетки, общее количество столбцов, горизонтальный подсчёт на линии подсчёта, расчётное движение и итоговую цель. При тестировании эти записи позволяют проверить корректность расчёта для каждой сделки до анализа результатов. Если "CalcPFTarget()" возвращает 0, функции входа используют резервный тейк-профит 2R и фиксируют источник цели в журнале, что позволяет легко разделить две группы сделок для анализа.
Минимальный размер клетки в 3 пункта не позволяет ей стать слишком малой на инструментах с очень низкой волатильностью, что иначе привело бы к нереалистично большому числу столбцов. Верхнего ограничения размера клетки в коде нет, однако проверка "InpMaxTargetPips" не допускает передачи чрезмерно большой цели в торговый приказ.
Распознавание диапазона
Функция "DetectRange()" просматривает последние бары в поиске консолидации, соответствующей требованиям к ширине, высоте и предшествующему тренду. Внутри неё вызываются вспомогательные функции "HasPriorDowntrend()" и "HasPriorUptrend()", которые проверяют направленный контекст перед диапазоном.
//+------------------------------------------------------------------+ //| 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" пополам. Первая половина охватывает более раннюю часть периода, а вторая — более позднюю. Нисходящий тренд подтверждается, когда максимум ранней половины заметно выше минимума поздней половины. Для восходящего тренда действует обратное условие.
//+------------------------------------------------------------------+ //| Detects a valid Wyckoff trading 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 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 high rl = MathMin(rl, low_buf[i]); // Update low } double height = rh - rl; // Compute height if(height < min_h) continue; // Too narrow if(height > max_h) break; // Too wide double tol = height * 0.25; // Tolerance band bool fits = true; // Fit flag for(int i = 0; i < range_bars; i++) // Verify bars fit { if(high_buf[i] > rh + tol || low_buf[i] < rl - tol) // Outside tolerance { fits = false; // Mark invalid break; } } if(!fits) continue; // Skip this length 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 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 double range_atr = CalcRangeATR(range_bars); // Compute range ATR 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 g_range.count_line = 0; // Clear count line g_range.range_atr = range_atr; // Store range ATR DrawHLine("SUPPORT", g_range.support, clrGreen, STYLE_DASH); // Draw support DrawHLine("RESISTANCE", g_range.resistance, clrRed, STYLE_DASH); // Draw resistance Print(StringFormat("WyckoffEA: Range | Bias:%s | S:%.5f | R:%.5f | Bars:%d | ATR:%.5f", g_range.bullish_bias ? "ACCUMULATION" : "DISTRIBUTION", g_range.support, g_range.resistance, range_bars, range_atr)); // Log return true; // Range valid } return false; // No range found }
DetectRange() проверяет диапазоны с количеством баров от InpMinRangeBars до InpMaxRangeBars и останавливается на самом коротком корректном варианте. Допуск в 25% позволяет учитывать небольшие выходы цены за границы без отмены диапазона. Здесь же заполняется поле range_atr, после чего оно доступно функции CalcPFTarget().
Распознавание событий Вайкоффа
Каждая функция распознавания анализирует только последний закрытый бар с индексом 1. Все функции работают по одной схеме: сначала проверяется ценовое условие, затем выполняется подтверждение объёмом.
Распознавание спринга — события входа при накоплении:
//+------------------------------------------------------------------+ //| Checks for a valid Spring on the last closed bar | //+------------------------------------------------------------------+ 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 threshold { g_range.spring_low = low1; // Store Spring low g_range.count_line = low1 + (g_range.support - low1) * 0.5; // Count line near LPS level 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 | Vol:%I64d | AvgVol:%.0f", low1, vol1, g_range.avg_volume)); // Log event return true; // Spring confirmed } } return false; // Not a Spring }
Спринг считается корректным, если минимум опускается ниже поддержки на величину "InpSpringTolerance", а закрытие возвращается внутрь диапазона при тиковом объёме ниже среднего. Объём ниже среднего указывает на отсутствие реального предложения: снижение было выбиванием слабых участников, а не институциональными продажами.
Распознавание признака силы — события продолжения накопления:
//+------------------------------------------------------------------+ //| Checks for a Sign of Strength on the last closed bar | //+------------------------------------------------------------------+ 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) // High volume confirms { 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 for a valid Upthrust on the last closed bar | //+------------------------------------------------------------------+ 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 threshold { g_range.upthrust_high = high1; // Store Upthrust high g_range.count_line = high1 - (high1 - g_range.resistance) * 0.5; // Count line near LPSY level 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 | Vol:%I64d | AvgVol:%.0f", high1, vol1, g_range.avg_volume)); // Log event return true; // Upthrust confirmed } } return false; // Not an Upthrust }Распознавание признака слабости — события продолжения распределения:
//+------------------------------------------------------------------+ //| Checks for a Sign of Weakness on the last closed bar | //+------------------------------------------------------------------+ 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) // High volume confirms { 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 }Аптраст и признак слабости являются структурными зеркалами спринга и признака силы. Аптраст выбивает слабых держателей коротких позиций выше сопротивления на низком объёме, а признак слабости подтверждает контроль продавцов закрытием ниже поддержки на высоком объёме.
Функции входа
После подтверждения признака силы или слабости функции "CheckLPSEntry()" и "CheckLPSYEntry()" вызываются на каждом новом баре. Они ожидают до "InpLPSBars" баров, пока откат не выполнит условия, после чего задают линию подсчёта, рассчитывают цель по графику «крестики-нолики» и открывают позицию.
Вход в длинную позицию в последней точке поддержки:
//+------------------------------------------------------------------+ //| Waits for LPS pullback, calculates P&F target, opens long | //+------------------------------------------------------------------+ 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; } 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 { g_range.count_line = close1; // Update count line to LPS close double pf_target = CalcPFTarget(true); // Calculate P&F target 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 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 = (pf_target > 0) ? pf_target : ask + sl_pip * 2.0 * PipSize(); // P&F target or 2R fallback 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 { string tp_label = (pf_target > 0) ? "P&F" : "2R"; // Target label DrawHLine("TARGET", tp, clrGold, STYLE_DOT); // Draw target line datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1); // Get bar time DrawLabel("LPS", t1, iLow(_Symbol, PERIOD_CURRENT, 1) - PipSize() * 3, "LPS", clrGold); // Draw LPS label Print(StringFormat("WyckoffEA: LONG | Lots:%.2f | SL:%.5f | TP:%.5f (%s)", lots, sl, tp, tp_label)); // Log trade g_state = STATE_IN_TRADE; // Update state } } }
Линия подсчёта устанавливается по цене закрытия последней точки поддержки непосредственно в момент подтверждения отката, а не заранее. Это важно: линия должна отражать фактический уровень начала фазы роста, который неизвестен до закрытия бара отката. Присваивание "close1" полю "g_range.count_line" непосредственно перед вызовом "CalcPFTarget()" гарантирует, что измерение всегда выполняется по правильной цене.
Выбор тейк-профита выполняется одним условием: если цель по графику «крестики-нолики» больше нуля, используется эта проекция; иначе применяется фиксированное соотношение 2R. Метки "(P&F)" и "(2R)" в журнале позволяют легко разделить две группы при анализе.
Вход в короткую позицию в последней точке предложения:
//+------------------------------------------------------------------+ //| Waits for LPSY rally, calculates P&F target, opens short | //+------------------------------------------------------------------+ 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; } 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 { g_range.count_line = close1; // Update count line to LPSY close double pf_target = CalcPFTarget(false); // Calculate P&F target 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 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 = (pf_target > 0) ? pf_target : bid - sl_pip * 2.0 * PipSize(); // P&F target or 2R fallback 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 { string tp_label = (pf_target > 0) ? "P&F" : "2R"; // Target label DrawHLine("TARGET", tp, clrGold, STYLE_DOT); // Draw target line datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1); // Get bar time DrawLabel("LPSY", t1, iHigh(_Symbol, PERIOD_CURRENT, 1) + PipSize() * 3, "LPSY", clrGold); // Draw LPSY label Print(StringFormat("WyckoffEA: SHORT | Lots:%.2f | SL:%.5f | TP:%.5f (%s)", lots, sl, tp, tp_label)); // Log trade g_state = STATE_IN_TRADE; // Update state } } }
Вход в последней точке предложения зеркален входу в последней точке поддержки, но используется для короткой позиции. Вместо "CalcPFTarget(true)" вызывается "CalcPFTarget(false)", а цена Bid используется вместо Ask. Стоп-лосс размещается выше точки входа на расстоянии ATR, умноженного на "InpATRMult" и прибавленного к Bid. Остальная логика идентична.
Обработчики событий
Функция "OnInit()" создаёт хэндл индикатора ATR, настраивает торговый объект и инициализирует все переменные состояния. "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("WyckoffCauseEffectEA: ATR 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("WyckoffCauseEffectEA 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 }Один хэндл ATR используется для двух задач: расчёта расстояния до стоп-лосса и усреднения ATR диапазона. Дополнительные хэндлы индикаторов не требуются.
//+------------------------------------------------------------------+ //| 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("WyckoffCauseEffectEA: 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; case STATE_RANGE_FORMING: // Range locked g_range_watch++; // Increment watch counter if(g_range_watch > InpRangeWatchBars) // Range stale { Print("WyckoffCauseEffectEA: Range watch expired — resetting."); // Log expiry g_state = STATE_IDLE; // Reset state ClearLabels(); // Clear chart break; } 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; 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 invalidated { Print("WyckoffCauseEffectEA: Spring invalidated — resetting."); // Log failure g_state = STATE_IDLE; // Reset state ClearLabels(); // Clear chart } break; case STATE_SOS_CONFIRMED: // SOS confirmed CheckLPSEntry(); // Check LPS entry break; 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 invalidated { Print("WyckoffCauseEffectEA: Upthrust invalidated — resetting."); // Log failure g_state = STATE_IDLE; // Reset state ClearLabels(); // Clear chart } break; case STATE_SOW_CONFIRMED: // SOW confirmed CheckLPSYEntry(); // Check LPSY entry break; } } //+------------------------------------------------------------------+
"OnTick()" выполняется только один раз на новом баре: проверка "g_last_bar" в начале функции отфильтровывает внутрибаровые тики. Это правильно для распознавания сигналов, поскольку должны анализироваться только завершённые бары. Однако управление позицией — например, корректировка стоп-лосса, трейлинг или перевод в безубыток — при добавлении в будущей версии потребуется вынести за пределы этой проверки. Конечный автомат обрабатывает ровно одно состояние на бар и сразу завершает выполнение. Условия отмены проверяются до перехода, поэтому автомат не может пропустить состояние.
Ключевые решения при реализации
Размер клетки равен 0,25 ATR диапазона, поэтому он адаптируется к волатильности и применим к разным инструментам без привязки к их типу. Минимальный размер в 3 пункта предотвращает чрезмерное количество столбцов на низковолатильных символах. Коэффициент разворота фиксирован на уровне одной клетки, что позволяет учитывать максимальное количество внутренних колебаний диапазона. Линия подсчёта проводится по цене закрытия последней точки поддержки или предложения, что даёт объективную и воспроизводимую замену субъективной визуальной оценке. Резервный тейк-профит 2R применяется только тогда, когда структура «крестики-нолики» слишком узка для корректной проекции. Стоп-лосс размещается на расстоянии 1,5 ATR от входа. Риск на сделку фиксирован на уровне 1% баланса, а размер позиции рассчитывается с учётом стоимости тика. Допуск диапазона составляет 25% его высоты и позволяет учитывать небольшие выходы цены за границы. Порог объёма равен 1,2 среднего значения и применяется как к событиям с высоким, так и с низким объёмом.
Тестирование
Для тестирования советника откройте Тестер стратегий MetaTrader 5 и задайте следующие параметры. Символ: EURUSD. Таймфрейм: H4. Режим моделирования: каждый тик на основе реальных тиков. Начальный депозит: 10 000 долларов. Период тестирования: с 01.04.2025 по 01.04.2026. Используйте те же параметры диапазона и объёма, что и в Части 1. Для настроек «крестики-нолики» начните со значений "InpPFReversalBoxes" = 1, "InpMinTargetPips" = 30 и "InpMaxTargetPips" = 500.
Чего ожидать
В журнале подробно фиксируется каждый расчёт по графику «крестики-нолики»: размер клетки на основе ATR, общее число столбцов, горизонтальный подсчёт на линии подсчёта, расчётное движение и итоговая цель. Эти данные позволяют убедиться в корректности расчёта до того, как делать выводы по результатам.
Для сделок с корректной целью по графику «крестики-нолики» в журнале отображается "TP:%.5f (P&F)". Если структура оказалась слишком узкой, отображается "TP:%.5f (2R)", что указывает на использование резервной цели. Отслеживайте эти группы отдельно, чтобы сравнить результаты сделок со структурной целью и резервным тейк-профитом.
Если цель по графику «крестики-нолики» систематически оказывается ниже фактического движения цены, размер клетки может быть слишком большим — увеличьте множитель ATR с 0,25 в направлении 0,35. Если цель постоянно завышена, уменьшите множитель в направлении 0,20.
Результаты тестирования

Рисунок 2. Демонстрация тестирования.

Рисунок 3. Кривые баланса и средств на счёте.

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

Рисунок 5. Входы в ходе тестирования.
Известные ограничения
Размер клетки графика «крестики-нолики» рассчитывается по ATR, а не по абсолютному уровню цены. Такой подход не зависит от инструмента, но размер клетки изменяется вместе с волатильностью. Диапазон, сформированный при низкой волатильности, будет иметь меньшую клетку и больше столбцов, чем диапазон той же ширины при высокой волатильности. Поэтому итоговые цели будут различаться. Математически это корректно: подсчёт чувствителен к масштабу измерения. Однако цели нельзя напрямую сравнивать между различными рыночными условиями.
Линия подсчёта устанавливается по цене закрытия LPS или LPSY, а не по вручную определённому структурному уровню последней точки поддержки или последней точки предложения. В исходной методике Вайкоффа линия проводится по конкретному структурному уровню, выбор которого требует визуальной интерпретации. Советник приближённо реализует это, используя в качестве линии подсчёта цену закрытия бара отката. Такой подход обеспечивает консервативное и объективное измерение, однако выбранный уровень может отличаться от решения опытного аналитика по Вайкоффу.
Советник во всех расчётах использует разворот на одну клетку. Для долгосрочных подсчётов специалисты по Вайкоффу иногда применяют разворот на три клетки. Выбор коэффициента разворота существенно влияет на количество столбцов и, следовательно, на расчётную цель. В будущем можно добавить оба варианта подсчёта и использовать их среднее значение как целевую зону.
При построении графика «крестики-нолики» анализируются данные OHLC на уровне баров, а не внутридневные тики. На очень малых таймфреймах отдельные бары могут содержать несколько движений, достаточных для разворота, но невидимых на уровне баров. Для этого подхода рекомендуется использовать H4 и выше. На H1 результаты следует тщательно сверять с визуальным анализом графика «крестики-нолики».
Заключение
Закон причины и следствия Вайкоффа — одна из самых изящных идей технического анализа: величина тренда пропорциональна размеру предшествующей консолидации. Подсчёт по графику «крестики-нолики» превращает эту идею в конкретное число.
В Части 1 этой серии была создана архитектура распознавания и входа. В данной статье добавлен измерительный уровень, благодаря которому выход определяется структурой, а не произвольным коэффициентом. Входя в сделку в последней точке поддержки, советник теперь знает ширину диапазона накопления, количество сформированных столбцов «крестики-нолики» и расчётную протяжённость последующей фазы роста. Тейк-профит больше не является простым множителем расстояния до стоп-лосса — он представляет собой измерение причины.
Вместе Часть 1, посвящённая входу, и Часть 2, посвящённая выходу, образуют полноценную систему на основе метода Вайкоффа. Советник входит в точке с минимальным риском перед фазой роста и выходит на уровне расчётного следствия накопленной причины. Оба решения основаны на структуре цены, а не на внешних индикаторах.
Весь код скомпилирован и протестирован в MetaTrader 5. Полный исходный файл "WyckoffCauseEffectEA.mq5" приложен к статье. Скопируйте его в "MQL5\Experts\WyckoffCauseEffectEA.mq5" и скомпилируйте в MetaEditor; дополнительные зависимости не требуются. Рекомендуемые условия — EURUSD, H4. Перед использованием на реальном счёте обязательно протестируйте советник на демо-счёте.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22472
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Анализ CSV-данных (Часть 5): Потоковая передача CSV-данных в реальном времени из активных сессий MetaTrader 5
Встраивание торговой дисциплины в код (Часть 7): Автоматизация защиты эквити с помощью логики управления
Создание торговых систем с искусственным интеллектом на MQL5 (Часть 3): Переход к прокручиваемому интерфейсу с единым окном чата
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Ознакомьтесь с новой статьёй: «Автоматизация классических методов анализа рынка в MQL5 (часть 2): Причина и следствие по Уикоффу — целевые уровни цен по методу «точка и фигура»».
Автор: Тола Мозес Гектор