English Deutsch 日本語
preview
Рыночные секреты Ларри Уильямса (Часть 2): Автоматизация торговой системы на основе рыночной структуры

Рыночные секреты Ларри Уильямса (Часть 2): Автоматизация торговой системы на основе рыночной структуры

MetaTrader 5Трейдинг |
39 3
Chacha Ian Maroa
Chacha Ian Maroa

Введение

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

В первой статье этой серии мы частично решили эту проблему, создав пользовательский индикатор рыночной структуры на MQL5 на основе концепций, представленных в книге Ларри Уильямса Долгосрочные секреты краткосрочной торговли. Этот индикатор определяет краткосрочные и среднесрочные свинговые точки непосредственно на графике, давая трейдерам ясное и структурированное представление о поведении цены. Во второй статье мы делаем следующий логичный шаг: переходим от визуального анализа к полной автоматизации. С помощью MQL5 мы проектируем советник, который считывает данные рыночной структуры из индикатора и превращает их в конкретные торговые решения. Цель — показать, как дискреционную торговую идею можно выразить в виде четких правил и исполнять автоматически, исключая эмоциональное вмешательство.

Эта статья является частью серии «Рыночные секреты Ларри Уильямса», в которой каждый выпуск посвящен практической и проверяемой реализации одной концепции из работ Ларри Уильямса. В этой части мы сосредоточимся на краткосрочных и среднесрочных свинговых точках и покажем, как их можно использовать для открытия сделок сразу после подтверждения структуры. К концу статьи у читателя будет рабочая торговая система, которая соединяет теорию рыночной структуры с практической автоматизацией в MQL5.


Кто такой Ларри Уильямс?

Ларри Уильямс — одно из самых уважаемых имен в трейдинге. Он торгует акциями и товарными фьючерсами и имеет многолетний успешный опыт. Он также автор многих книг о трейдинге. Одна из его самых известных книг — Долгосрочные секреты краткосрочной торговли. Многие трейдеры изучают эту книгу благодаря ее практическому подходу к рыночной структуре и свинговому анализу, которые служат основой этой статьи.

Ларри Уильямс получил широкое признание после победы в World Cup Championship of Futures Trading в 1987 году. В этом соревновании он за двенадцать месяцев превратил десять тысяч долларов ($10,000) в более чем один миллион долларов ($1,000,000). Никто до сих пор не побил этот рекорд. Десять лет спустя его дочь Мишель Уильямс приняла участие в том же соревновании и тоже победила. Это показало, что его подход можно изучить и успешно применять другим трейдерам.


Обзор стратегии

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

Ниже показан скриншот графика, на котором индикатор рыночной структуры, разработанный в части 1, применен на реальном рыночном графике.

Индикатор рыночной структуры Ларри Уильямса

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

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

краткосрочный минимум

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

краткосрочный максимум

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

Рынки не ограничиваются одним уровнем структуры. По Ларри Уильямсу краткосрочные свинговые точки объединяются и формируют среднесрочные свинговые точки. Среднесрочный минимум — это краткосрочный минимум, который ниже краткосрочных минимумов с обеих сторон.

среднесрочный минимум

Среднесрочный максимум — это краткосрочный максимум, который выше краткосрочных максимумов с обеих сторон.

среднесрочный максимум

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

Одно из важнейших наблюдений, которое Ларри Уильямс делает в книге Долгосрочные секреты краткосрочной торговли, заключается в том, что эти свинговые точки не просто описывают рынок. Их можно использовать в торговле. Он объясняет, что стабильно получал прибыль, применяя формирование и пробой этих свинговых точек как уровни входа, выхода и стоп-лосса. По его словам, эти точки представляют собой наиболее значимые уровни поддержки и сопротивления на рынке. Когда они удерживаются, это подтверждает продолжение тренда. Когда они пробиваются, это предупреждает об изменении тренда.

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

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

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

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

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

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

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

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

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

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

Целевые уровни прибыли задаются через настраиваемое соотношение риска и прибыли. Это позволяет стратегии сохранять единообразие на разных рынках и таймфреймах.

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

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



Логика генерации сигналов

Для начала откройте MetaEditor 5, создайте новый файл советника и назовите его larryWilliamsMarketStructureExpert.mq5. После создания файла удалите стандартный шаблонный код и замените его базовой заготовкой, показанной ниже.

//+------------------------------------------------------------------+
//|                           larryWilliamsMarketStructureExpert.mq5 |
//|          Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian |
//|                          https://www.mql5.com/en/users/chachaian |
//+------------------------------------------------------------------+

#property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian"
#property link      "https://www.mql5.com/en/users/chachaian"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Standard Libraries                                               |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| User input variables                                             |
//+------------------------------------------------------------------+
input group "Information"
input ulong           magicNumber = 254700680002;                 
input ENUM_TIMEFRAMES timeframe   = PERIOD_CURRENT;

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
//--- Create a CTrade object to handle trading operations
CTrade Trade;

//--- Bid and Ask
double   askPrice;
double   bidPrice;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   //---  Assign a unique magic number to identify trades opened by this EA
   Trade.SetExpertMagicNumber(magicNumber);

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
   
   //--- Notify why the program stopped running
   Print("Program terminated! Reason code: ", reason);
   
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   //--- Scope variables
   askPrice      = SymbolInfoDouble (_Symbol, SYMBOL_ASK);
   bidPrice      = SymbolInfoDouble (_Symbol, SYMBOL_BID);

}

//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
{
}

//--- UTILITY FUNCTIONS

//+------------------------------------------------------------------+

Этот начальный код дает нам чистую и надежную структуру, которую мы будем постепенно расширять.

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

Далее мы подключаем стандартную торговую библиотеку. Файл Trade.mqh предоставляет класс CTrade , который мы позже будем использовать для безопасного и структурированного открытия, управления и закрытия позиций.

После этого мы определяем входные переменные пользователя. Эти параметры позволяют трейдеру управлять важными настройками при установке советника на график. Пока мы задаем только магический номер (magic number) для уникальной идентификации сделок, открытых этим советником, и параметр таймфрейма timeframe, который обеспечивает гибкость при работе с разными графиками.

Затем идет раздел глобальных переменных. Здесь мы создаем объект CTrade, который будет обрабатывать все торговые операции. Также мы объявляем переменные для хранения текущих цен bid и ask , которые обновляются на каждом тике и повторно используются во всем советнике.

Функция OnInit выполняется один раз при установке советника на график. На этом этапе мы только назначаем магический номер (magic number) объекту CTrade. Это гарантирует, что все сделки, открытые этим советником, можно отслеживать, идентифицировать и управлять ими независимо от других советников.

Функция OnDeinit вызывается, когда советник удаляется или останавливается. Пока она просто выводит сообщение с объяснением причины завершения программы. Это полезно при тестировании и отладке.

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

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

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

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

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

Есть два простых способа сделать это. Первый вариант — скачать приложенный исходный файл, открыть MetaEditor 5, создать новый пустой файл индикатора с именем larryWilliamsMarketStructureIndicator.mq5, вставить в него исходный код и скомпилировать. Второй вариант еще проще. После скачивания файла скопируйте его напрямую в папку Indicators внутри каталога данных MQL5. После перезапуска терминала индикатор станет доступен в терминале, и при необходимости его можно будет отредактировать или скомпилировать.

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

#resource "\\Indicators\\larryWilliamsMarketStructureIndicator.ex5"

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

Далее нам нужен способ связать советник с индикатором. В MQL5 такая связь выполняется через хэндл индикатора. В разделе глобальных переменных объявите следующую переменную.

//--- The Larry Williams Market Structure Indicator handle
int larryWilliamsMarketStructureIndicatorHandle;

Хэндл индикатора — это просто ссылка. Он представляет активное соединение между советником и экземпляром индикатора, работающим в фоне. Без этого хэндла советник не сможет запрашивать данные из буферов индикатора.

После этого мы объявляем четыре массива в глобальной области, чтобы хранить значения рыночной структуры, считанные из индикатора.

//--- Arrays to track market structure data
double shortTermLows [];
double shortTermHighs[];
double intermediateTermLows [];
double intermediateTermHighs[];

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

Внутри функции OnInit мы теперь указываем MQL5, что эти массивы должны трактоваться как временные ряды.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   ...
   
   //--- Treat the following arrays as timeseries (index 0 becomes the most recent bar)
   ArraySetAsSeries(shortTermLows,  true);
   ArraySetAsSeries(shortTermHighs, true);
   ArraySetAsSeries(intermediateTermLows,  true);
   ArraySetAsSeries(intermediateTermHighs, true);
   ArraySetAsSeries(closePriceMinutesData, true);

   return(INIT_SUCCEEDED);
}

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

После объявления хэндла можно инициализировать сам индикатор. Это также делается внутри функции OnInit function.

int OnInit(){

   ...

   //--- Initialize larryWilliamsMarketStructureIndicator
   larryWilliamsMarketStructureIndicatorHandle    = iCustom(_Symbol, timeframe, "::Indicators\\larryWilliamsMarketStructureIndicator.ex5");
   if(larryWilliamsMarketStructureIndicatorHandle == INVALID_HANDLE){
      Print("Error while initializing Larry Williams' Market Structure Indicator: ", GetLastError());
      return(INIT_FAILED);
   }

   return(INIT_SUCCEEDED);
}

Здесь iCustom загружает индикатор и при успешном выполнении возвращает хэндл. Параметры symbol и timeframe гарантируют, что индикатор работает в том же контексте символа и таймфрейма, что и советник. Если хэндл недействителен, советник сразу останавливается. Это не позволяет стратегии работать без надежных данных.

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

//--- UTILITY FUNCTIONS
//+-------------------------------------------------------------------------------+
//| Copies the latest swing high and low data from the market structure indicator |
//+-------------------------------------------------------------------------------+
void RefreshMarketStructureBuffers(){

   //--- Get the last 200 short-term swing low points
   int copiedShortTermSwingLows = CopyBuffer(larryWilliamsMarketStructureIndicatorHandle, 0, 0, 200, shortTermLows);
   if(copiedShortTermSwingLows == -1){
      Print("Error while copying short-term swing lows: ", GetLastError());
      return;
   }
   
   //--- Get the last 200 short-term swing high points
   int copiedShortTermSwingHighs = CopyBuffer(larryWilliamsMarketStructureIndicatorHandle, 1, 0, 200, shortTermHighs);
   if(copiedShortTermSwingHighs == -1){
      Print("Error while copying short-term swing highs: ", GetLastError());
      return;
   }
   
   //--- Get the last 200 intermediate swing low points
   int copiedIntermediateSwingLows = CopyBuffer(larryWilliamsMarketStructureIndicatorHandle, 2, 0, 200, intermediateTermLows);
   if(copiedIntermediateSwingLows == -1){
      Print("Error while copying intermediate swing lows: ", GetLastError());
      return;
   }
   
   //--- Get the last 200 intermediate swing high points
   int copiedIntermediateSwingHighs = CopyBuffer(larryWilliamsMarketStructureIndicatorHandle, 3, 0, 200, intermediateTermHighs);
   if(copiedIntermediateSwingHighs == -1){
      Print("Error while copying intermediate swing highs: ", GetLastError());
      return;
   }
   
   //--- Treat the following arrays as timeseries (index 0 becomes the most recent bar)
   ArraySetAsSeries(shortTermLows,  true);
   ArraySetAsSeries(shortTermHighs, true);
   ArraySetAsSeries(intermediateTermLows,  true);
   ArraySetAsSeries(intermediateTermHighs, true);
      
}

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

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

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

Эта функция делает логику советника чище. Вместо копирования данных индикатора в нескольких местах мы при необходимости обновляем все одним контролируемым шагом.

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

//+------------------------------------------------------------------+
//| Function to check if there's a new bar on a given chart timeframe|
//+------------------------------------------------------------------+
bool IsNewBar(string symbol, ENUM_TIMEFRAMES tf, datetime &lastTm)
{

   datetime currentTm = iTime(symbol, tf, 0);
   if(currentTm != lastTm){
      lastTm       = currentTm;
      return true;
   }  
   return false;
   
}

Эта функция сравнивает время открытия текущего бара с сохраненным значением. Если время изменилось, значит сформировался новый бар. Затем функция обновляет сохраненное значение и возвращает true.

Для поддержки этой логики мы объявляем глобальную переменную.

//--- To help track new bar open
datetime lastBarOpenTime;

Эта переменная хранит время открытия последнего обработанного бара. Внутри функции OnInit она инициализируется нулем, поэтому первый бар всегда определяется корректно.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   ...
   
   //--- Initialize global variables
   lastBarOpenTime = 0;

   return(INIT_SUCCEEDED);
}

Когда свежие данные индикатора доступны, мы можем определить сигнальную логику. Первая функция проверяет сигнал на покупку.

//+---------------------------------------------------------------------------+
//| Checks whether current market structure conditions generate a buy signal  |
//+---------------------------------------------------------------------------+
bool IsBuySignal(){

   if(shortTermLows[2] == EMPTY_VALUE){
      return false;
   }
   
   int commonIndex = -1;
   for(int i = 3; i < ArraySize(shortTermLows); i++){
      if(shortTermLows[i] != EMPTY_VALUE){
         commonIndex = i;
         break;
      }
   }
   
   if(commonIndex == -1){
      return false;
   }
   
   if(intermediateTermLows[commonIndex] != EMPTY_VALUE){
      return true;
   }
   
   return false;
   
}

Функция начинает с проверки того, что только что сформировался краткосрочный минимум. Для этого проверяется индекс недавнего бара. Если краткосрочного минимума нет, функция сразу завершается.

bool IsBuySignal(){

   if(shortTermLows[2] == EMPTY_VALUE){
      return false;
   }
   
   ...
   
}

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

bool IsBuySignal(){

   ...
   
   int commonIndex = -1;
   for(int i = 3; i < ArraySize(shortTermLows); i++){
      if(shortTermLows[i] != EMPTY_VALUE){
         commonIndex = i;
         break;
      }
   }
   
   if(commonIndex == -1){
      return false;
   }
   
   ...
   
}

Если в этой позиции есть среднесрочный минимум, это означает, что краткосрочная структура подтвердила среднесрочный минимум. В этот момент функция возвращает true. В противном случае она возвращает false.

bool IsBuySignal(){

   ...
   
   if(intermediateTermLows[commonIndex] != EMPTY_VALUE){
      return true;
   }
   
   return false;
   
}

Эта логика отражает идею Ларри Уильямса о вложенных свингах. Сигнал свинга более высокого уровня становится торговым только тогда, когда его подтверждает структура более низкого уровня.

Функция сигнала на продажу имеет ту же структуру, но использует обратную логику. Вместо минимумов она работает с максимумами.

//+---------------------------------------------------------------------------+
//| Checks whether current market structure conditions generate a sell signal |
//+---------------------------------------------------------------------------+
bool IsSelSignal(){

   if(shortTermHighs[2] == EMPTY_VALUE){
      return false;
   }
   
   int commonIndex = -1;
   for(int i = 3; i < ArraySize(shortTermHighs); i++){
      if(shortTermHighs[i] != EMPTY_VALUE){
         commonIndex = i;
         break;
      }
   }
   
   if(commonIndex == -1){
      return false;
   }
   
   if(intermediateTermHighs[commonIndex] != EMPTY_VALUE){
      return true;
   }
   
   return false;
   
}

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

Поскольку логика симметрична, понимание функции сигнала на покупку сразу проясняет и сигнал на продажу.

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   ...

   //--- Execute logic only when a new bar opens
   if(IsNewBar(_Symbol, timeframe, lastBarOpenTime)){
   
      //--- Get updated market structure data
      RefreshMarketStructureBuffers();
      
      //--- Handle Buy signals
      if(IsBuySignal()){
         Print("Intermediate low confirmed!");
      }
      
      //--- Handle Sell signals
      if(IsSelSignal()){
         Print("Intermediate high confirmed!");
      }      
   }      
}

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

Когда это поведение проверено, можно переходить к логике исполнения сделок в следующем разделе.



От сигналов к сделкам

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

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

Управление направлением торговли

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

//+------------------------------------------------------------------+
//| Custom Enumerations                                              |
//+------------------------------------------------------------------+
enum ENUM_TRADE_DIRECTION  
{ 
   ONLY_LONG, 
   ONLY_SHORT, 
   TRADE_BOTH 
};

ONLY_LONG разрешает только длинные позиции. ONLY_SHORT разрешает только короткие позиции. TRADE_BOTH разрешает как длинные, так и короткие позиции.

Затем мы предоставляем этот выбор пользователю в виде входного параметра.

...

input group "Trade and Risk Management"
input ENUM_TRADE_DIRECTION            direction  = TRADE_BOTH;

По умолчанию советнику разрешено торговать в обоих направлениях. При установке советника на график пользователь может изменить это поведение в зависимости от своего рыночного взгляда. Это дает гибкость без изменения исходного кода.

Режимы расчета размера лота

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

enum ENUM_LOT_SIZE_INPUT_MODE 
{ 
   MODE_MANUAL, 
   MODE_AUTO 
};

MODE_MANUAL использует фиксированный размер лота. MODE_AUTO рассчитывает размер лота на основе риска по счету.

Когда выбран автоматический режим, советник использует заданный пользователем процент риска от баланса счета. Этот процент представляет максимальный убыток, который трейдер готов принять при срабатывании стоп-лосса. Когда выбран ручной режим, советник использует фиксированный размер лота, заданный пользователем.

input ENUM_LOT_SIZE_INPUT_MODE      lotSizeMode  = MODE_AUTO;
input double                 riskPerTradePercent = 1.0;
input double                             lotSize = 5.0;

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

Размещение стоп-лосса на основе рыночной структуры

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

enum ENUM_STOP_LOSS_STRUCTURE{
   SL_AT_SHORT_TERM_SWING,
   SL_AT_INTERMEDIATE_SWING
};

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

input ENUM_STOP_LOSS_STRUCTURE stopLossStructure = SL_AT_INTERMEDIATE_SWING;

Допустимый диапазон расстояния стопа

Поскольку рыночная структура динамична, расстояния до стопа иногда могут быть слишком короткими или слишком длинными. Оба варианта могут быть нежелательными.

Чтобы контролировать это, пользователь задает минимальное и максимальное допустимое расстояние стопа в пунктах. Любая сделка, выходящая за пределы этого диапазона, игнорируется. Это защищает советник от входа в сделки с плохими характеристиками риска.

input int              minimumStopDistancePoints = 100;
input int              maximumStopDistancePoints = 600;

Настройка соотношения риска и прибыли

Мы также позволяем пользователю выбрать заранее заданное соотношение риска и прибыли.

enum ENUM_RISK_REWARD_RATIO   
{ 
   ONE_TO_ONE, 
   ONE_TO_ONEandHALF, 
   ONE_TO_TWO, 
   ONE_TO_THREE, 
   ONE_TO_FOUR, 
   ONE_TO_FIVE, 
   ONE_TO_SIX 
};

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

input ENUM_RISK_REWARD_RATIO     riskRewardRatio = ONE_TO_TWO;

Отслеживание информации об открытой сделке

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

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

//+------------------------------------------------------------------+
//| Data Structures                                                  |
//+------------------------------------------------------------------+
struct MqlTradeInfo
{
   ulong orderTicket;                 
   ENUM_ORDER_TYPE type;
   ENUM_POSITION_TYPE posType;
   double entryPrice;
   double takeProfitLevel;
   double stopLossLevel;
   datetime openTime;
   double lotSize;   
};

//--- Instantiate the trade information data structure
MqlTradeInfo tradeInfo

Инициализация значения пункта

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

//--- The size of a point for this financial security
double pointValue;

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   ...
   
   pointValue      = SymbolInfoDouble(_Symbol, SYMBOL_POINT);

   return(INIT_SUCCEEDED);
}

Открытие длинной позиции

Функция OpenBuy открывает рыночный ордер на покупку. Она выполняет несколько важных шагов в структурированном виде.

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){

   ENUM_ORDER_TYPE action          = ORDER_TYPE_BUY;
   ENUM_POSITION_TYPE positionType = POSITION_TYPE_BUY;
   datetime currentTime            = TimeCurrent();
   double contractSize             = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
   double accountBalance           = AccountInfoDouble(ACCOUNT_BALANCE);
   double rewardValue              = 1.0;
   
   switch(riskRewardRatio){
      case ONE_TO_ONE: 
         rewardValue = 1.0;
         break;
      case ONE_TO_ONEandHALF:
         rewardValue = 1.5;
         break;
      case ONE_TO_TWO: 
         rewardValue = 2.0;
         break;
      case ONE_TO_THREE: 
         rewardValue = 3.0;
         break;
      case ONE_TO_FOUR: 
         rewardValue = 4.0;
         break;
      case ONE_TO_FIVE: 
         rewardValue = 5.0;
         break;
      case ONE_TO_SIX: 
         rewardValue = 6.0;
         break;
      default:
         rewardValue = 1.0;
         break;
   }
   
   double stopLevel = 0;
   
   if(stopLossStructure == SL_AT_SHORT_TERM_SWING  ){
      stopLevel = NormalizeDouble(shortTermLows[2], Digits());
   }
   
   if(stopLossStructure == SL_AT_INTERMEDIATE_SWING){
   
      for(int i = 0; i < ArraySize(intermediateTermLows); i++){
         if(intermediateTermLows[i] != EMPTY_VALUE){
            stopLevel = NormalizeDouble(intermediateTermLows[i], Digits());
            break;
         }
      }      
   }
   
   double stopDistance = NormalizeDouble(askPr - stopLevel, Digits());
   if(stopDistance > (maximumStopDistancePoints * pointValue) || stopDistance < (minimumStopDistancePoints * pointValue)){
      Print("The Stop Distance falls outside desired distance range");
      return false;
   }
   
   double targetLevel  = NormalizeDouble(askPr + (rewardValue * stopDistance), Digits());
   
   double volume       = NormalizeDouble(lotSize, 2);
   if(lotSizeMode == MODE_AUTO){
      double amountAtRisk = (riskPerTradePercent / 100.0) *  accountBalance;
      volume              = amountAtRisk / (contractSize * stopDistance);
      volume              = NormalizeDouble(volume, 2);
   }
   
   if(!Trade.Buy(volume, _Symbol, askPr, stopLevel, targetLevel)){
      Print("Error while opening a long position, ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }else{
      MqlTradeResult result = {};
      Trade.Result(result);
      tradeInfo.orderTicket                 = result.order;
      tradeInfo.type                        = action;
      tradeInfo.posType                     = positionType;
      tradeInfo.entryPrice                  = result.price;
      tradeInfo.takeProfitLevel             = targetLevel;
      tradeInfo.stopLossLevel               = stopLevel;
      tradeInfo.openTime                    = currentTime;
      tradeInfo.lotSize                     = result.volume;
      
      return true;
   }
   
   return false;
}

Сначала она определяет выбранное соотношение риска и прибыли и преобразует его в числовое значение прибыли. Это значение позже используется для расчета уровня тейк-профита.

Затем функция определяет уровень стоп-лосса. В зависимости от выбора пользователя стоп размещается либо на последнем краткосрочном минимуме, либо на последнем среднесрочном минимуме.

Когда уровень стопа известен, рассчитывается расстояние до стопа. Затем это расстояние проверяется на соответствие заданному пользователем минимальному и максимальному диапазону. Если расстояние неприемлемо, сделка пропускается.

Уровень тейк-профита рассчитывается с использованием значения прибыли и расстояния до стопа. Это гарантирует, что каждая сделка соответствует выбранному профилю риска и прибыли.

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

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

Открытие короткой позиции

Функция OpenSel следует той же логике, что и функция покупки, но в противоположном направлении. Уровни стоп-лосса берутся от свинговых максимумов вместо свинговых минимумов, а ценовые расчеты соответственно инвертируются.

//+------------------------------------------------------------------+
//| Function used to open a market sell order.                       |   
//+------------------------------------------------------------------+
bool OpenSel( const double bidPr){

   ENUM_ORDER_TYPE action          = ORDER_TYPE_SELL;
   ENUM_POSITION_TYPE positionType = POSITION_TYPE_SELL;
   datetime currentTime            = TimeCurrent();   
   double contractSize             = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
   double accountBalance           = AccountInfoDouble(ACCOUNT_BALANCE);
   double rewardValue              = 1.0;
   
   switch(riskRewardRatio){
      case ONE_TO_ONE: 
         rewardValue = 1.0;
         break;
      case ONE_TO_ONEandHALF:
         rewardValue = 1.5;
         break;
      case ONE_TO_TWO: 
         rewardValue = 2.0;
         break;
      case ONE_TO_THREE: 
         rewardValue = 3.0;
         break;
      case ONE_TO_FOUR: 
         rewardValue = 4.0;
         break;
      case ONE_TO_FIVE: 
         rewardValue = 5.0;
         break;
      case ONE_TO_SIX: 
         rewardValue = 6.0;
         break;
      default:
         rewardValue = 1.0;
         break;
   }
   
   double stopLevel = 0;
   
   if(stopLossStructure == SL_AT_SHORT_TERM_SWING  ){
      stopLevel = NormalizeDouble(shortTermHighs[2], Digits());
   }
   
   if(stopLossStructure == SL_AT_INTERMEDIATE_SWING){
   
      for(int i = 0; i < ArraySize(intermediateTermHighs); i++){
         if(intermediateTermHighs[i] != EMPTY_VALUE){
            stopLevel = NormalizeDouble(intermediateTermHighs[i], Digits());
            break;
         }
      }
      
   }
   
   double stopDistance = NormalizeDouble(stopLevel - bidPr, Digits());
   if(stopDistance > (maximumStopDistancePoints * pointValue) || stopDistance < (minimumStopDistancePoints * pointValue)){
      Print("The Stop Distance falls outside desired distance range");
      return false;
   }
   
   double targetLevel  = NormalizeDouble(bidPr - (rewardValue * stopDistance), Digits());
   double volume       = NormalizeDouble(lotSize, 2);
   if(lotSizeMode == MODE_AUTO){
      double amountAtRisk = (riskPerTradePercent / 100.0) *  accountBalance;
      volume              = amountAtRisk / (contractSize * stopDistance);
      volume              = NormalizeDouble(volume, 2);
   }
   
   if(!Trade.Sell(volume, _Symbol, bidPr, stopLevel, targetLevel)){
      Print("Error while opening a short position, ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }else{ 
      MqlTradeResult result = {};
      Trade.Result(result);
      tradeInfo.orderTicket                 = result.order;
      tradeInfo.type                        = action;
      tradeInfo.posType                     = positionType;
      tradeInfo.entryPrice                  = result.price;
      tradeInfo.takeProfitLevel             = targetLevel;
      tradeInfo.stopLossLevel               = stopLevel;
      tradeInfo.openTime                    = currentTime;
      tradeInfo.lotSize                     = result.volume;
      
      return true;
   }
   
   return false;   
}

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

Проверка существующих позиций

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

//+------------------------------------------------------------------+
//| To verify whether this EA currently has an active buy position.  |                                 |
//+------------------------------------------------------------------+
bool IsThereAnActiveBuyPosition(ulong magic){
   
   for(int i = PositionsTotal() - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0){
         Print("Error while fetching position ticket ", _LastError);
         continue;
      }else{
         if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
            return true;
         }
      }
   }
   
   return false;
}

//+------------------------------------------------------------------+
//| To verify whether this EA currently has an active sell position. |                                 |
//+------------------------------------------------------------------+
bool IsThereAnActiveSellPosition(ulong magic){
   
   for(int i = PositionsTotal() - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0){
         Print("Error while fetching position ticket ", _LastError);
         continue;
      }else{
         if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
            return true;
         }
      }
   }
   
   return false;
}

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

Это гарантирует, что советник открывает только одну позицию за раз и избегает конфликтующих сделок.

Исполнение сделок внутри функции OnTick

Когда все компоненты готовы, последний шаг — подключить торговую логику к основному циклу исполнения.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   //--- Scope variables
   askPrice      = SymbolInfoDouble (_Symbol, SYMBOL_ASK);
   bidPrice      = SymbolInfoDouble (_Symbol, SYMBOL_BID);

   //--- Execute logic only when a new bar opens
   if(IsNewBar(_Symbol, timeframe, lastBarOpenTime)){
   
      //--- Get updated market structure data
      RefreshMarketStructureBuffers();
      
      //--- Handle Buy signals
      if(IsBuySignal()){
      
         //--- Open a long position if there is no active position
         if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
            OpenBuy(askPrice);
         }
      }
      
      //--- Handle Sell signals
      if(IsSelSignal()){
         
         //--- Open a short position if there is no active position
         if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
            OpenSel(bidPrice);
         }
      }
            
   }
   
}

Внутри функции OnTick сначала мы обновляем текущие цены bid и ask . Затем проверяем, открылся ли новый бар. Торговая логика выполняется только на новых барах, чтобы избежать повторяющихся сигналов.

Когда обнаружен сигнал на покупку, советник проверяет отсутствие активной позиции. Если условия выполнены, открывается ордер на покупку.

Та же логика применяется к сигналам на продажу. На этом завершается полный цикл от определения рыночной структуры до исполнения сделки.

На этом этапе советник полностью работоспособен и способен торговать сигналы рыночной структуры контролируемым и настраиваемым образом.



Добавление динамического ступенчатого трейлинг-стопа

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

Включение и отключение трейлинг-стопа

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

input bool                    enableTrailingStop = false;

Когда значение равно true, советник активно управляет трейлинг-стопами. Когда оно равно false, сделки без вмешательства доходят либо до стоп-лосса, либо до тейк-профита. Это решение полностью остается за пользователем.

Определение структуры ступенчатого трейлинг-стопа

Трейлинг-стоп реализован как ступенчатая система. Вместо непрерывного перемещения стоп-лосса он продвигается по заранее заданным этапам по мере движения цены к цели.

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

//--- Instantiate the trade information data structure
MqlTradeInfo tradeInfo;

struct MqlTrailingStop
{
   double level1;
   double level2;
   double level3;
   double level4;
   double level5;
   
   double stopLevel1;
   double stopLevel2;
   double stopLevel3;
   double stopLevel4;
   double stopLevel5;
   
   bool isLevel1Active;
   bool isLevel2Active;
   bool isLevel3Active;
   bool isLevel4Active;
   bool isLevel5Active;
};

//--- Instantiate the trailing stop structure
MqlTrailingStop trailingStop;

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

Определение пересечений цены

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

//+------------------------------------------------------------------+
//| To detect a crossover at a given price level                     |                               
//+------------------------------------------------------------------+
bool IsCrossOver(const double price, const double &closePriceMinsData[]){
   if(closePriceMinsData[1] <= price && closePriceMinsData[0] > price){
      return true;
   }
   return false;
}


//+------------------------------------------------------------------+
//| To detect a crossunder at a given price level                    |                               
//+------------------------------------------------------------------+
bool IsCrossUnder(const double price, const double &closePriceMinsData[]){
   if(closePriceMinsData[1] >= price && closePriceMinsData[0] < price){
      return true;
   }
   return false;
}

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

Хранение минутных ценовых данных

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

//--- To store minutes data
double closePriceMinutesData [];

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

Подготовка уровней трейлинг-стопа при открытии сделки

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

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){

   ...
   
   if(!Trade.Buy(volume, _Symbol, askPr, stopLevel, targetLevel)){
   
   ...
   
   }else{

      ...
      
      //--- Refill the trailing Stop struct
      double targetDistance       = targetLevel - askPr;
      double trailingStep         = NormalizeDouble(targetDistance / 6,   Digits());
      trailingStop.level1         = NormalizeDouble(askPr + trailingStep, Digits());
      trailingStop.level2         = NormalizeDouble(trailingStop.level1 + trailingStep, Digits());      
      trailingStop.level3         = NormalizeDouble(trailingStop.level2 + trailingStep, Digits());
      trailingStop.level4         = NormalizeDouble(trailingStop.level3 + trailingStep, Digits());      
      trailingStop.level5         = NormalizeDouble(trailingStop.level4 + trailingStep, Digits());
      
      trailingStop.stopLevel1     = NormalizeDouble(stopLevel + trailingStep, Digits());
      trailingStop.stopLevel2     = NormalizeDouble(trailingStop.stopLevel1 + trailingStep, Digits());
      trailingStop.stopLevel3     = NormalizeDouble(trailingStop.stopLevel2 + trailingStep, Digits());
      trailingStop.stopLevel4     = NormalizeDouble(trailingStop.stopLevel3 + trailingStep, Digits());
      trailingStop.stopLevel5     = NormalizeDouble(trailingStop.stopLevel4 + trailingStep, Digits());
      
      trailingStop.isLevel1Active = false;
      trailingStop.isLevel2Active = false;
      trailingStop.isLevel3Active = false;
      trailingStop.isLevel4Active = false;
      trailingStop.isLevel5Active = false;
      
      return true;
   }
   
   return false;
}

//+------------------------------------------------------------------+
//| Function used to open a market sell order.                       |   
//+------------------------------------------------------------------+
bool OpenSel( const double bidPr){

   ...
   
   if(!Trade.Sell(volume, _Symbol, bidPr, stopLevel, targetLevel)){
      
      ...
      
      return false;
   }else{ 

      ...
      
      //--- Refill the trailing Stop struct
      double targetDistance       = bidPr - targetLevel;
      double trailingStep         = NormalizeDouble(targetDistance / 6,   Digits());
      trailingStop.level1         = NormalizeDouble(bidPr - trailingStep, Digits());
      trailingStop.level2         = NormalizeDouble(trailingStop.level1 - trailingStep, Digits());
      trailingStop.level3         = NormalizeDouble(trailingStop.level2 - trailingStep, Digits());
      trailingStop.level4         = NormalizeDouble(trailingStop.level3 - trailingStep, Digits());
      trailingStop.level5         = NormalizeDouble(trailingStop.level4 - trailingStep, Digits());
      
      trailingStop.stopLevel1     = NormalizeDouble(stopLevel - trailingStep, Digits());
      trailingStop.stopLevel2     = NormalizeDouble(trailingStop.stopLevel1 - trailingStep, Digits());
      trailingStop.stopLevel3     = NormalizeDouble(trailingStop.stopLevel2 - trailingStep, Digits());
      trailingStop.stopLevel2     = NormalizeDouble(trailingStop.stopLevel3 - trailingStep, Digits());
      trailingStop.stopLevel3     = NormalizeDouble(trailingStop.stopLevel4 - trailingStep, Digits());
      
      trailingStop.isLevel1Active = false;
      trailingStop.isLevel2Active = false;
      trailingStop.isLevel3Active = false;
      trailingStop.isLevel4Active = false;
      trailingStop.isLevel5Active = false;
      return true;
   }
   
   return false;  
}

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

Управление трейлинг-стопом в реальном времени

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

//+------------------------------------------------------------------+
//| To track price action and updates the trailing stop              |   
//+------------------------------------------------------------------+
void ManageTrailingStop(){

   int totalPositions = PositionsTotal();
   //--- Loop through all open positions
   for(int i = totalPositions - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket != 0){
         // Get some useful position properties
         ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);  
         string symbol                   = PositionGetString (POSITION_SYMBOL);
         ulong magic                     = PositionGetInteger(POSITION_MAGIC);
         double targetLevel              = PositionGetDouble(POSITION_TP);
         if(positionType == POSITION_TYPE_BUY ){
            if(symbol == _Symbol && magic == magicNumber){
            
               if(IsCrossOver(trailingStop.level1, closePriceMinutesData) && !trailingStop.isLevel1Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel1, targetLevel)){
                     Print("Error while trailing SL at level 1: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel1Active = true;
                  }
               }
               
               if(IsCrossOver(trailingStop.level2, closePriceMinutesData) && !trailingStop.isLevel2Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel2, targetLevel)){
                     Print("Error while trailing SL at level 2: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel2Active = true;
                  }
               }
               
               if(IsCrossOver(trailingStop.level3, closePriceMinutesData) && !trailingStop.isLevel3Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel3, targetLevel)){
                     Print("Error while trailing SL at level 3: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel3Active = true;
                  }
               }
               
               if(IsCrossOver(trailingStop.level4, closePriceMinutesData) && !trailingStop.isLevel4Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel4, targetLevel)){
                     Print("Error while trailing SL at level 4: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel4Active = true;
                  }
               }
               
               if(IsCrossOver(trailingStop.level5, closePriceMinutesData) && !trailingStop.isLevel5Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel5, targetLevel)){
                     Print("Error while trailing SL at level 5: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel5Active = true;
                  }
               }
            }
         }
         
               
         if(positionType == POSITION_TYPE_SELL){
            if(symbol == _Symbol && magic == magicNumber){
            
               if(IsCrossUnder(trailingStop.level1, closePriceMinutesData) && !trailingStop.isLevel1Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel1, targetLevel)){
                     Print("Error while trailing SL at level 1: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel1Active = true;
                  }
               }
               
               if(IsCrossUnder(trailingStop.level2, closePriceMinutesData) && !trailingStop.isLevel2Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel2, targetLevel)){
                     Print("Error while trailing SL at level 2: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel2Active = true;
                  }
               }
               
               if(IsCrossUnder(trailingStop.level3, closePriceMinutesData) && !trailingStop.isLevel3Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel3, targetLevel)){
                     Print("Error while trailing SL at level 3: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel3Active = true;
                  }
               }
               
               if(IsCrossUnder(trailingStop.level4, closePriceMinutesData) && !trailingStop.isLevel4Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel4, targetLevel)){
                     Print("Error while trailing SL at level 4: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel4Active = true;
                  }
               }
               
               if(IsCrossUnder(trailingStop.level5, closePriceMinutesData) && !trailingStop.isLevel5Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel5, targetLevel)){
                     Print("Error while trailing SL at level 5: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel5Active = true;
                  }
               }      
            }
         }
      }
   }  
}

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

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

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

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

Если какая-либо модификация стопа не удалась, выводится сообщение об ошибке, чтобы помочь при отладке.

Интеграция трейлинг-стопа в советник

Теперь функцию трейлинг-стопа можно вызвать внутри OnTick . Она должна выполняться только тогда, когда пользователь включил функцию трейлинг-стопа.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   ...
   
   //--- Manage trailing stop
   if(enableTrailingStop){
      ManageTrailingStop();
   }   
}

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

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



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

Когда вся торговая логика готова, следующий шаг — проверить работу советника в реальных рыночных условиях. Для этого советник был протестирован с помощью MetaTrader 5 Strategy Tester на исторических ценовых данных. Бэктест проводился на золоте с использованием H1 таймфрейма. Период тестирования длился с 1 января 2025 года по 30 ноября 2025 года. Начальный баланс счета был установлен на уровне 10000 USD, а все сделки во время теста выполнялись советником автоматически, без ручного вмешательства.

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

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

Помимо прибыльности, важно посмотреть, как вела себя кривая капитала на протяжении теста. Кривая капитала показывает плавное и стабильное развитие, а не резкие всплески или нестабильные колебания.

Кривая капитала

отчет о тестировании

Это указывает на то, что прибыль последовательно накапливалась со временем, а просадки удерживались под контролем правилами стратегии.

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


Заключение

В этой статье мы взяли полноценную торговую идею и превратили ее в полностью рабочий советник на MQL5. Отталкиваясь от объективного определения рыночной структуры у Ларри Уильямса, мы перенесли краткосрочные и среднесрочные свинговые точки в код и использовали их для генерации четких, повторяемых торговых сигналов. Это устраняет элемент догадок из рыночного анализа и заменяет его правилами, которые можно тестировать, проверять и улучшать.

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

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

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

Наконец, эта статья укрепляет важную идею, лежащую в основе работ Ларри Уильямса. Рынки не являются полностью случайными. Цена склонна двигаться структурированными свингами, которые можно объективно определить. Закодировав эти концепции в советнике, мы можем системно изучать поведение рынка и устранять эмоциональную предвзятость из исполнения.

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

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

Имя файла Описание
larryWilliamsMarketStructureIndicator.mq5 Пользовательский индикатор рыночной структуры, который определяет и отображает краткосрочные и среднесрочные свинговые точки на основе методологии Ларри Уильямса.
larryWilliamsMarketStructureExpert.mq5 Советник, который считывает сигналы из индикатора рыночной структуры и автоматически выполняет сделки с управлением риском и логикой ступенчатого трейлинг-стопа.
setFile.set Файл конфигурации, содержащий точные входные параметры, использованные при тестировании и демонстрационных запусках советника.

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

Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
William Tosolini
William Tosolini | 12 янв. 2026 в 10:00
Доброе утро, интересный советник! Хотел бы спросить, можно ли получить полный код для MetaTrader 4, пожалуйста.
Кроме того, поскольку я не знаю, как пользоваться и программировать на MQL, хотелось бы спросить, не могли бы вы прислать мне готовый файл советника для MetaTrader 4, чтобы я мог просто вставить его в платформу.
Дайте знать, возможно ли это, и ещё раз спасибо, а также поздравляю с проделанной работой.
Chacha Ian Maroa
Chacha Ian Maroa | 12 янв. 2026 в 14:28
William Tosolini советник, я хотел бы спросить, можно ли получить полный код для MetaTrader 4, пожалуйста.
Кроме того, поскольку я не знаю, как пользоваться и программировать на MQL, хотелось бы спросить, не могли бы вы прислать мне готовый файл советника для MetaTrader 4, чтобы я мог просто вставить его в платформу.
Дайте знать, возможно ли это, и ещё раз спасибо, а также поздравляю с проделанной работой.

Уважаемый Уильям,

Спасибо за добрые слова и за то, что следите за моей работой.

Что касается вашего запроса, я специализируюсь исключительно на разработке на языке MQL5 для платформы MetaTrader 5. Поскольку архитектура MT4 и MT5 значительно различается, для переноса кода потребуется его полная переработка.

К сожалению, в настоящее время я не могу выполнить заказы на индивидуальную разработку или конвертацию из-за графика моих текущих проектов. Благодарю за понимание и надеюсь, что версия статьи для MT5 окажется вам полезной.

William Tosolini
William Tosolini | 12 янв. 2026 в 20:40
Chacha Ian Maroa #:

Дорогой Уильям,

Спасибо за ваши добрые слова и за то, что следите за моей работой.

Что касается Вашей просьбы, я специализируюсь исключительно на разработке MQL5 для платформы MetaTrader 5. Поскольку архитектура MT4 и MT5 значительно различается, для переноса кода потребуется его полная переработка.

К сожалению, в настоящее время я не могу выполнить заказы на индивидуальную разработку или конвертацию кода из-за загруженности текущими проектами. Благодарю за понимание и надеюсь, что версия статьи для MT5 окажется вам полезной.

Спасибо за ответ, но, к сожалению, я использую только MetaTrader 4, так как считаю его лучше, чем MetaTrader 5.
Ничего страшного, жаль, мне бы очень хотелось получить советник для MT4, но всё равно спасибо. Вы были так любезны, что ответили и объяснили свои причины.
Создание профессиональной торговой системы на базе Heikin Ashi (Часть 2): Разработка советника Создание профессиональной торговой системы на базе Heikin Ashi (Часть 2): Разработка советника
В этой статье объясняется, как разработать профессиональный советник (EA) на MQL5 на основе Heikin Ashi. Вы узнаете, как настроить входные параметры, перечисления, индикаторы, глобальные переменные и реализовать основную торговую логику. Вы также сможете выполнить бэктест на золоте, чтобы проверить свою работу.
Разработка инструментария для анализа Price Action (Часть 65): Создание системы для мониторинга и анализа построенных вручную уровней Фибоначчи Разработка инструментария для анализа Price Action (Часть 65): Создание системы для мониторинга и анализа построенных вручную уровней Фибоначчи
Инструмент коррекции Фибоначчи – важный элемент анализа Price Action, указывающий ключевые уровни возможной рыночной реакции. Однако его эффективность часто ограничена необходимостью постоянного ручного наблюдения, из-за чего часть сетапов может быть пропущена. В этой части серии представлен инструмент, который с помощью MQL5 синхронизирует и активно отслеживает вручную построенные уровни Фибоначчи, сочетая дискреционный подход с автоматизированным контролем.
Нейросети в трейдинге: От рыночного шума к устойчивому торговому плану (Окончание) Нейросети в трейдинге: От рыночного шума к устойчивому торговому плану (Окончание)
Продолжается адаптация MomAD к алгоритмическому трейдингу: собран класс CNeuronMomAD, объединяющий UncAD с модулями согласования и уточнения сценариев (TTM, MPI). Разобраны этапы последовательного обучения модели и тестирование на EURUSD H1 за январь–апрель 2026 года. Статья фокусируется на интеграции в общий вычислительный контур и практических выводах по управлению риском при положительном результате.
Алгоритм оптимизации койотов — Coyote Optimization Algorithm (COA) Алгоритм оптимизации койотов — Coyote Optimization Algorithm (COA)
Представляем MQL5-реализацию Coyote Optimization Algorithm: стаи с локальными альфами, медианная тенденция и встроенный кроссовер обеспечивают параллельное исследование областей пространства и контроль преждевременной сходимости. Алгоритм встроен в C_AO и проверен на стандартном стенде и композитном античит-тесте. В статье — код, псевдокод и разбор операторов, позволяющие применить COA для оптимизации параметров торговой системы.