English Deutsch 日本語
preview
Рыночные секреты Ларри Уильямса (Часть 8): Объединение волатильности, структуры и временных фильтров

Рыночные секреты Ларри Уильямса (Часть 8): Объединение волатильности, структуры и временных фильтров

MetaTrader 5Торговые системы |
62 0
Chacha Ian Maroa
Chacha Ian Maroa

Введение

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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


Пошаговое построение советника

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

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

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

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

Создание основы советника

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

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

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

//+------------------------------------------------------------------+
//| Стандартные библиотеки                                               |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Пользовательские перечисления                                              |
//+------------------------------------------------------------------+
enum ENUM_TRADE_DIRECTION  
{ 
   ONLY_LONG, 
   ONLY_SHORT, 
   TRADE_BOTH 
};

enum ENUM_VOLATILITY_ENTRY_MODE
{
   VOL_SIMPLE_PREVIOUS_RANGE,
   VOL_SWING_BASED
};

enum ENUM_STOP_LOSS_MODE
{
   SL_BY_RANGE_PERCENT,
   SL_AT_SWING_EXTREME
};

enum ENUM_TAKE_PROFIT_MODE
{
   TP_FIRST_PROFITABLE_OPEN,
   TP_AFTER_N_CANDLES,
   TP_BY_RISK_REWARD 
};

enum ENUM_TDW_MODE
{
   TDW_ALL_DAYS,     
   TDW_SELECTED_DAYS
};

enum ENUM_LOT_SIZE_INPUT_MODE 
{ 
   MODE_MANUAL, 
   MODE_AUTO 
};

//+------------------------------------------------------------------+
//| Входные параметры пользователя                                             |
//+------------------------------------------------------------------+
input group "Информация"
input ulong magicNumber         = 254700680002;                 
input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT;

input group "Параметры пробоя волатильности"
input ENUM_VOLATILITY_ENTRY_MODE volatilityEntryMode = VOL_SIMPLE_PREVIOUS_RANGE;
input double inpBuyRangeMultiplier                   = 0.50;   
input double inpSellRangeMultiplier                  = 0.50;   
input double inpStopRangeMultiplier                  = 0.50;

input group "Фильтр TDW"
input ENUM_TDW_MODE tradeDayMode = TDW_SELECTED_DAYS;
input bool tradeSunday           = false;
input bool tradeMonday           = true;
input bool tradeTuesday          = false;
input bool tradeWednesday        = false;
input bool tradeThursday         = false;
input bool tradeFriday           = false;
input bool tradeSaturday         = false;

input group "Фильтр времени суток"
input bool useTimeFilter         = false; 
input double startTime           = 9.30; 
input double endTime             = 16.00;

input group "Торговля и управление риском"
input ENUM_TRADE_DIRECTION direction       = ONLY_LONG;
input ENUM_STOP_LOSS_MODE stopLossMode     = SL_BY_RANGE_PERCENT;
input ENUM_TAKE_PROFIT_MODE takeProfitMode = TP_BY_RISK_REWARD;
input double riskRewardRatio               = 3.0;
input int exitAfterCandles                 = 3;
input ENUM_LOT_SIZE_INPUT_MODE lotSizeMode = MODE_AUTO;
input double riskPerTradePercent           = 1.0;
input double positionSize                  = 0.1;

//+------------------------------------------------------------------+
//| Глобальные переменные                                                 |
//+------------------------------------------------------------------+
//--- Создаем объект CTrade для обработки торговых операций
CTrade Trade;

//--- Bid и Ask
double   askPrice;
double   bidPrice;
datetime currentTime;

//+------------------------------------------------------------------+
//| Функция инициализации советника                                   |
//+------------------------------------------------------------------+
int OnInit(){

   //---  Назначаем уникальный magic number для сделок, открытых этим советником
   Trade.SetExpertMagicNumber(magicNumber);

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Функция деинициализации советника                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){

   //--- Сообщаем причину остановки программы
   Print("Советник остановлен. Код причины: ", reason);

}

//+------------------------------------------------------------------+
//| Тиковая функция советника                                             |
//+------------------------------------------------------------------+
void OnTick(){

   //--- Получаем текущие рыночные цены для исполнения сделок
   askPrice      = SymbolInfoDouble (_Symbol, SYMBOL_ASK);
   bidPrice      = SymbolInfoDouble (_Symbol, SYMBOL_BID);
   currentTime   = TimeCurrent();

}

//--- СЛУЖЕБНЫЕ ФУНКЦИИ

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

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

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

Определение открытия нового бара

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

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

//+------------------------------------------------------------------+
//| Проверяет, сформировался ли новый бар на заданном таймфрейме графика |
//+------------------------------------------------------------------+
bool IsNewBar(string symbol, ENUM_TIMEFRAMES tf, datetime &lastTm){

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

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

//--- Для отслеживания открытия нового бара
datetime lastBarOpenTime;

Эта переменная хранит последнее известное время открытия бара и инициализируется нулем при инициализации советника.

//+------------------------------------------------------------------+
//| Функция инициализации советника                                   |
//+------------------------------------------------------------------+
int OnInit(){

   ...
   
   //--- Инициализируем глобальные переменные
   lastBarOpenTime       = 0;

   return(INIT_SUCCEEDED);
}

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

Создание контейнера для уровней волатильности

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

//--- Хранит все ценовые уровни, полученные из расчетов пробоя волатильности по Ларри Уильямсу
struct MqlLwVolatilityLevels
{
   double range;      
   double buyEntryPrice;       
   double sellEntryPrice;   
   double bullishStopLoss;   
   double bearishStopLoss;    
   double bullishTakeProfit;
   double bearishTakeProfit;
   double bullishStopDistance;
   double bearishStopDistance;
};
Каждое поле представляет отдельный компонент логики волатильности Ларри Уильямса.
  • Поле range хранит рабочий диапазон волатильности, используемый для расчетов.
  • Поля buyEntryPrice и sellEntryPrice хранят уровни пробоя.
  • Поля стоп-лосса хранят расчетные защитные уровни.
  • Поля тейк-профита хранят расчетные цели, когда активен режим риск/доходность.
  • Поля расстояния до стопа хранят расстояние риска, используемое для динамического расчета размера позиции.

Затем мы создаем один экземпляр этой структуры сразу под ее определением.

MqlLwVolatilityLevels lwVolatilityLevels;

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

Во время инициализации советника мы обнуляем все поля.

//+------------------------------------------------------------------+
//| Функция инициализации советника                                   |
//+------------------------------------------------------------------+
int OnInit(){

   ...
   
   //--- Сбрасываем уровни волатильности Ларри Уильямса 
   ZeroMemory(lwVolatilityLevels);

   return(INIT_SUCCEEDED);
}
Это гарантирует, что от предыдущих запусков не останется устаревших значений.

Определение краткосрочных точек свинга Ларри Уильямса

Логика стратегии начинается со структуры. Бычий сигнал действителен только при формировании краткосрочного свинг-минимума. Медвежий сигнал действителен только при формировании краткосрочного свинг-максимума.

Для определения краткосрочного свинг-максимума мы задаем:

//+------------------------------------------------------------------+
//| Определяет краткосрочный максимум Ларри Уильямса на последних трех барах |
//| Бар с индексом 2 должен быть свинг-максимумом с более низкими максимумами по обе стороны |
//| Бар 2 не должен быть внешним баром |
//| Бар 1 не должен быть внутренним баром |
//+------------------------------------------------------------------+
bool IsLarryWilliamsShortTermHigh(string symbol, ENUM_TIMEFRAMES tf){

   //--- Ценовые данные трех баров
   double high1 = iHigh(symbol, tf, 1);
   double low1  = iLow (symbol, tf, 1);

   double high2 = iHigh(symbol, tf, 2);
   double low2  = iLow (symbol, tf, 2);

   double high3 = iHigh(symbol, tf, 3);
   double low3  = iLow (symbol, tf, 3);

   //--- Условие 1: бар 2 должен быть свинг-максимумом
   bool isSwingHigh =
      (high2 > high1) &&
      (high2 > high3);

   if(!isSwingHigh){
      return false;
   }      

   //--- Условие 2: бар 2 не должен быть внешним баром относительно бара 3
   bool isOutsideBar =
      (high2 > high3) &&
      (low2  < low3);

   if(isOutsideBar){
      return false;
   }

   //--- Условие 3: бар 1 не должен быть внутренним баром относительно бара 2
   bool isInsideBar =
      (high1 < high2) &&
      (low1  > low2);

   if(isInsideBar){
      return false;
   }

   return true;
}

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

Логика краткосрочного свинг-минимума зеркально повторяет эту структуру.

//+------------------------------------------------------------------+
//| Определяет краткосрочный минимум Ларри Уильямса на последних трех барах |
//| Бар с индексом 2 должен быть свинг-минимумом с более высокими минимумами по обе стороны |
//| Бар 2 не должен быть внешним баром |
//| Бар 1 не должен быть внутренним баром |
//+------------------------------------------------------------------+
bool IsLarryWilliamsShortTermLow(string symbol, ENUM_TIMEFRAMES tf){

   //--- Ценовые данные трех баров
   double high1 = iHigh(symbol, tf, 1);
   double low1  = iLow (symbol, tf, 1);

   double high2 = iHigh(symbol, tf, 2);
   double low2  = iLow (symbol, tf, 2);

   double high3 = iHigh(symbol, tf, 3);
   double low3  = iLow (symbol, tf, 3);

   //--- Условие 1: бар 2 должен быть свинг-минимумом
   bool isSwingLow =
      (low2 < low1) &&
      (low2 < low3);

   if(!isSwingLow){
      return false;
   }
      
   //--- Условие 2: бар 2 не должен быть внешним баром относительно бара 3
   bool isOutsideBar =
      (high2 > high3) &&
      (low2  < low3);

   if(isOutsideBar){
      return false;
   }

   //--- Условие 3: бар 1 не должен быть внутренним баром относительно бара 2
   bool isInsideBar =
      (high1 < high2) &&
      (low1  > low2);

   if(isInsideBar){
      return false;
   }

   return true;
}

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

Измерение волатильности с помощью двух моделей Ларри Уильямса

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

//+------------------------------------------------------------------+
//| Рассчитывает диапазон волатильности по свингам Ларри Уильямса |
//+------------------------------------------------------------------+
double CalculateLwSwingVolatilityRange(const string symbol, ENUM_TIMEFRAMES tf){
   
   //--- Получаем необходимые максимумы и минимумы
   double high_3_days_ago = iHigh(symbol, tf, 4);
   double low_yesterday   = iLow (symbol, tf, 1);

   double high_1_day_ago  = iHigh(symbol, tf, 2);
   double low_3_days_ago  = iLow (symbol, tf, 4);

   //--- Проверяем данные
   if(high_3_days_ago == 0.0 || low_yesterday == 0.0 ||
      high_1_day_ago  == 0.0 || low_3_days_ago == 0.0)
   {
      return 0.0;
   }

   //--- Рассчитываем расстояния свинга с использованием абсолютных значений
   double swingRangeA = MathAbs(high_3_days_ago - low_yesterday);
   double swingRangeB = MathAbs(high_1_day_ago  - low_3_days_ago);

   //--- Выбираем доминирующий свинг
   double usableRange = MathMax(swingRangeA, swingRangeB);

   //--- Нормализуем по точности символа
   return NormalizeDouble(usableRange, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS));
}

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

//+------------------------------------------------------------------+
//| Возвращает ценовой диапазон бара (high - low) по заданному индексу |
//+------------------------------------------------------------------+
double GetBarRange(const string symbol, ENUM_TIMEFRAMES tf, int index){

   double high = iHigh(symbol, tf, index);
   double low  = iLow (symbol, tf, index);

   if(high == 0.0 || low == 0.0){
      return 0.0;
   }

   return NormalizeDouble(high - low, Digits());
}

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

Расчет уровней входа

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

//+--------------------------------------------------------------------------------+
//| Рассчитывает цену входа на бычий пробой по открытию текущего дня и диапазону свинга |
//+--------------------------------------------------------------------------------+
double CalculateBuyEntryPrice(double todayOpen, double range, double buyMultiplier){

   return todayOpen + (range * buyMultiplier);
}
Мы прибавляем долю диапазона к сегодняшней цене открытия. Так формируется порог пробоя, который цена должна пересечь, чтобы подтвердить восходящий импульс.

Для медвежьих пробоев:

//+--------------------------------------------------------------------------------+
//| Рассчитывает цену входа на медвежий пробой по открытию текущего дня и диапазону свинга |
//+--------------------------------------------------------------------------------+
double CalculateSellEntryPrice(double todayOpen, double range, double sellMultiplier){

   return todayOpen - (range * sellMultiplier);
}

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

Расчет уровней стоп-лосса и тейк-профита

Стоп-лосс можно размещать с использованием двух моделей.

Модель на основе диапазона:

//+--------------------------------------------------------------------------------------------------+
//| Рассчитывает цену стоп-лосса для бычьей позиции на основе цены входа и вчерашнего диапазона |
//+--------------------------------------------------------------------------------------------------+
double CalculateBullishStopLoss(double entryPrice, double range, double stopMultiplier){

   return entryPrice - (range * stopMultiplier);
}
 
//+--------------------------------------------------------------------------------------------------+
//| Рассчитывает цену стоп-лосса для медвежьей позиции на основе цены входа и вчерашнего диапазона |
//+--------------------------------------------------------------------------------------------------+
double CalculateBearishStopLoss(double entryPrice, double range, double stopMultiplier){

   return entryPrice + (range * stopMultiplier);
}

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

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

//+--------------------------------------------------------------------------+
//| Рассчитывает уровень тейк-профита для бычьей сделки по логике риск/доходность |
//+--------------------------------------------------------------------------+
double CalculateBullishTakeProfit(double entryPrice, double stopLossPrice, double rewardValue){

   double stopDistance   = entryPrice - stopLossPrice;
   double rewardDistance = stopDistance * rewardValue;
   return NormalizeDouble(entryPrice + rewardDistance, Digits());
}

//+--------------------------------------------------------------------------+
//| Рассчитывает уровень тейк-профита для медвежьей сделки по логике риск/доходность |
//+--------------------------------------------------------------------------+
double CalculateBearishTakeProfit(double entryPrice, double stopLossPrice, double rewardValue){

   double stopDistance   = stopLossPrice - entryPrice;
   double rewardDistance = stopDistance * rewardValue;
   return NormalizeDouble(entryPrice - rewardDistance, Digits());
}

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

Централизация расчетов волатильности

Вся логика волатильности объединена в одной функции.

//+--------------------------------------------------------------------------------------------------------------+
//| Рассчитывает и обновляет все уровни входа, стопа и тейк-профита на основе выбранных моделей волатильности |
//+--------------------------------------------------------------------------------------------------------------+
void UpdateVolatilityEntryLevels(){

   if(volatilityEntryMode == VOL_SWING_BASED){
      lwVolatilityLevels.range              = CalculateLwSwingVolatilityRange(_Symbol, timeframe);
      lwVolatilityLevels.buyEntryPrice      = CalculateBuyEntryPrice (askPrice, lwVolatilityLevels.range, inpBuyRangeMultiplier );
      lwVolatilityLevels.sellEntryPrice     = CalculateSellEntryPrice(bidPrice, lwVolatilityLevels.range, inpSellRangeMultiplier);
      if(stopLossMode == SL_BY_RANGE_PERCENT){
         lwVolatilityLevels.bullishStopLoss = CalculateBullishStopLoss(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.range,  inpStopRangeMultiplier);
         lwVolatilityLevels.bearishStopLoss = CalculateBearishStopLoss(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.range, inpStopRangeMultiplier);                   
      }
      if(stopLossMode == SL_AT_SWING_EXTREME){
         lwVolatilityLevels.bullishStopLoss = iLow(_Symbol,  timeframe, 2);
         lwVolatilityLevels.bearishStopLoss = iHigh(_Symbol, timeframe, 2);                  
      }
      lwVolatilityLevels.bullishTakeProfit   = CalculateBullishTakeProfit(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.bullishStopLoss,  riskRewardRatio);
      lwVolatilityLevels.bearishTakeProfit   = CalculateBearishTakeProfit(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.bearishStopLoss, riskRewardRatio);
      lwVolatilityLevels.bullishStopDistance = lwVolatilityLevels.buyEntryPrice - lwVolatilityLevels.bullishStopLoss;
      lwVolatilityLevels.bearishStopDistance = lwVolatilityLevels.bearishStopLoss - lwVolatilityLevels.sellEntryPrice;
   }
   
   if(volatilityEntryMode == VOL_SIMPLE_PREVIOUS_RANGE){
      lwVolatilityLevels.range              = GetBarRange(_Symbol, timeframe, 1);
      lwVolatilityLevels.buyEntryPrice      = CalculateBuyEntryPrice (askPrice, lwVolatilityLevels.range, inpBuyRangeMultiplier );
      lwVolatilityLevels.sellEntryPrice     = CalculateSellEntryPrice(bidPrice, lwVolatilityLevels.range, inpSellRangeMultiplier);
      if(stopLossMode == SL_BY_RANGE_PERCENT){
         lwVolatilityLevels.bullishStopLoss = CalculateBullishStopLoss(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.range,  inpStopRangeMultiplier);
         lwVolatilityLevels.bearishStopLoss = CalculateBearishStopLoss(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.range, inpStopRangeMultiplier);                   
      }
      if(stopLossMode == SL_AT_SWING_EXTREME){
         lwVolatilityLevels.bullishStopLoss = iLow(_Symbol,  timeframe, 2);
         lwVolatilityLevels.bearishStopLoss = iHigh(_Symbol, timeframe, 2);                  
      }
      lwVolatilityLevels.bullishTakeProfit   = CalculateBullishTakeProfit(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.bullishStopLoss,  riskRewardRatio);
      lwVolatilityLevels.bearishTakeProfit   = CalculateBearishTakeProfit(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.bearishStopLoss, riskRewardRatio);
      lwVolatilityLevels.bullishStopDistance = lwVolatilityLevels.buyEntryPrice - lwVolatilityLevels.bullishStopLoss;
      lwVolatilityLevels.bearishStopDistance = lwVolatilityLevels.bearishStopLoss - lwVolatilityLevels.sellEntryPrice;
   }
}

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

Эта функция вызывается один раз на новый бар внутри функции OnTick.

//+------------------------------------------------------------------+
//| Тиковая функция советника                                             |
//+------------------------------------------------------------------+
void OnTick(){

   ...
   
   //--- Выполняем этот блок только при обнаружении нового бара на выбранном таймфрейме
   if(IsNewBar(_Symbol, timeframe, lastBarOpenTime)){
      
      //--- Пересчитываем уровни входа по волатильности на новом баре с учетом выбранных моделей входа и стопа
      UpdateVolatilityEntryLevels();
   }   
}

Отслеживание внутридневных пробоев по минутным данным

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

//+------------------------------------------------------------------+
//| Определяет пересечение заданного ценового уровня снизу вверх |
//+------------------------------------------------------------------+
bool IsCrossOver(const double price, const double &closePriceMinsData[]){
   if(closePriceMinsData[1] <= price && closePriceMinsData[0] > price){
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Определяет пересечение заданного ценового уровня сверху вниз |
//+------------------------------------------------------------------+
bool IsCrossUnder(const double price, const double &closePriceMinsData[]){
   if(closePriceMinsData[1] >= price && closePriceMinsData[0] < price){
      return true;
   }
   return false;
}

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

//--- Для хранения минутных данных
double closePriceMinutesData [];

Мы обрабатываем его как временной ряд и обновляем на каждом тике с помощью CopyClose.

Фильтрация по торговому дню и времени суток

Для реализации фильтра торгового дня недели мы определяем:

//+------------------------------------------------------------------------------------+
//| Возвращает день недели (0 = воскресенье, 6 = суббота) для заданного datetime-значения |
//+------------------------------------------------------------------------------------+
int TimeDayOfWeek(datetime time){
   MqlDateTime timeStruct = {};
   if(!TimeToStruct(time, timeStruct)){
      Print("TimeDayOfWeek: сбой TimeToStruct");
      return -1;
   }      
   return timeStruct.day_of_week;
}

//+-----------------------------------------------------------------------------------------------------+
//| Определяет, разрешена ли торговля для заданного времени с учетом выбранного режима торгового дня |
//+-----------------------------------------------------------------------------------------------------+
bool IsTradingDayAllowed(datetime time)
{
   // Базовый режим: без фильтрации
   if(tradeDayMode == TDW_ALL_DAYS){
      return true;
   }

   int day = TimeDayOfWeek(time);

   switch(day)
   {
      case 0: return tradeSunday;
      case 1: return tradeMonday;
      case 2: return tradeTuesday;
      case 3: return tradeWednesday;
      case 4: return tradeThursday;
      case 5: return tradeFriday;
      case 6: return tradeSaturday;
   }

   return false;
}

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

Для реализации фильтра времени суток мы определяем:

//+------------------------------------------------------------------+
//| Разбирает значение времени |
//+------------------------------------------------------------------+
bool ParseTime(double hhmm, int &hours, int &minutes){
    
    // Проверяем диапазон входного значения (от 0.00 до 23.59)
    if(hhmm < 0.0 || hhmm >= 24.00){
      return false;
    }
    
    hours = (int)hhmm;
    double fractional = hhmm - hours;
    
    // Учитываем погрешность чисел с плавающей точкой с помощью округления
    minutes = (int)MathRound(fractional * 100);
    
    // Проверяем минуты (0-59)
    if(minutes < 0 || minutes > 59){
      return false;
    } 
        
    // Обрабатываем случаи вроде 12.60, превращая их в 13:00
    if(minutes >= 60){
        hours += minutes / 60;
        minutes %= 60;
    }
    
    // Финальная проверка (часы могли увеличиться)
    if(hours < 0 || hours > 23){
      return false;
    }
    
    return true;
}

//+------------------------------------------------------------------+
//| Возвращает true, если текущее время находится внутри разрешенных торговых часов |
//+------------------------------------------------------------------+
bool IsTimeWithinTradingHours(){

   datetime currentTm = currentTime;
   MqlDateTime currentTimeStruct;
   if(!TimeToStruct(currentTm, currentTimeStruct)){
      Print("Ошибка при преобразовании datetime в структуру MqlDateTime: ", GetLastError());
      return false;
   }
   
   int startHour;
   int startMins;
   ParseTime(startTime, startHour, startMins);
   int endHour;
   int endMins;
   ParseTime(endTime, endHour, endMins);
   
   MqlDateTime startTimeStruct = currentTimeStruct;
   startTimeStruct.hour        = startHour;
   startTimeStruct.min         = startMins;
   startTimeStruct.sec         = 0;
   MqlDateTime endTimeStruct   = currentTimeStruct;
   endTimeStruct.hour          = endHour;
   endTimeStruct.min           = endMins;
   endTimeStruct.sec           = 0;
   
   datetime startTm          = StructToTime(startTimeStruct);
   datetime endTm            = StructToTime(endTimeStruct);    

   if(currentTm >= startTm && currentTm <= endTm){
      return true;
   }
      
   return false;
}

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

Ограничение одной открытой позицией

Чтобы в любой момент существовала только одна сделка, мы определяем:

//+------------------------------------------------------------------+
//| Проверяет, есть ли у этого советника активная позиция на покупку.  |                                 |
//+------------------------------------------------------------------+
bool IsThereAnActiveBuyPosition(ulong magic){
   
   for(int i = PositionsTotal() - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0){
         Print("Ошибка при получении тикета позиции ", _LastError);
         continue;
      }else{
         if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
            return true;
         }
      }
   }
   
   return false;
}

//+------------------------------------------------------------------+
//| Проверяет, есть ли у этого советника активная позиция на продажу. |                                 |
//+------------------------------------------------------------------+
bool IsThereAnActiveSellPosition(ulong magic){
   
   for(int i = PositionsTotal() - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0){
         Print("Ошибка при получении тикета позиции ", _LastError);
         continue;
      }else{
         if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
            return true;
         }
      }
   }
   
   return false;
}

Эти функции просматривают все открытые позиции и возвращают true, если позиция с magic number этого советника уже существует.

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

//+------------------------------------------------------------------+
//| Закрывает все позиции с заданным magic number |
//+------------------------------------------------------------------+
void ClosePositionsByMagic(ulong magic) {
    
    for (int i = PositionsTotal() - 1; i >= 0; i--) {
        ulong ticket = PositionGetTicket(i);
        if (PositionSelectByTicket(ticket)) {
            if (PositionGetInteger(POSITION_MAGIC) == magic) {
                ulong positionType = PositionGetInteger(POSITION_TYPE);
                double volume = PositionGetDouble(POSITION_VOLUME);
                if (positionType == POSITION_TYPE_BUY) {
                    Trade.PositionClose(ticket);
                } else if (positionType == POSITION_TYPE_SELL) {
                    Trade.PositionClose(ticket);
                }
            }
        }
    }    
}

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

Открытие сделок и расчет размера позиции

Динамический расчет размера позиции реализуется с помощью:

//+----------------------------------------------------------------------------------+
//| Рассчитывает размер позиции на основе фиксированного процента риска от баланса счета |
//+----------------------------------------------------------------------------------+
double CalculatePositionSizeByRisk(double stopDistance){
   double amountAtRisk = (riskPerTradePercent / 100.0) * AccountInfoDouble(ACCOUNT_BALANCE);
   double contractSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
   double volume       = amountAtRisk / (contractSize * stopDistance);
   return NormalizeDouble(volume, 2);
}

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

Исполнение сделок выполняется через:

//+------------------------------------------------------------------+
//| Функция для открытия рыночной позиции на покупку |
//+------------------------------------------------------------------+
bool OpenBuy(double stopLoss, double takeProfit, double lotSize){
   
   if(lotSizeMode == MODE_AUTO){
      lotSize = CalculatePositionSizeByRisk(lwVolatilityLevels.bullishStopDistance);
   }
   
   if(takeProfitMode == TP_BY_RISK_REWARD){
      if(!Trade.Buy(lotSize, _Symbol, askPrice, lwVolatilityLevels.bullishStopLoss, lwVolatilityLevels.bullishTakeProfit)){
         Print("Ошибка при исполнении рыночной заявки на покупку: ", GetLastError());
         Print(Trade.ResultRetcode());
         Print(Trade.ResultComment());
         return false;
      }
      return true;
   }
   
   if(!Trade.Buy(lotSize, _Symbol, askPrice, lwVolatilityLevels.bullishStopLoss)){
      Print("Ошибка при исполнении рыночной заявки на покупку: ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }
   
   return true;
}

//+------------------------------------------------------------------+
//| Функция для открытия рыночной позиции на продажу |
//+------------------------------------------------------------------+
bool OpenSel(double stopLoss, double takeProfit, double lotSize){
   
   if(lotSizeMode == MODE_AUTO){
      lotSize = CalculatePositionSizeByRisk(lwVolatilityLevels.bearishStopDistance);
   }
   
   if(takeProfitMode == TP_BY_RISK_REWARD){
      if(!Trade.Sell(lotSize, _Symbol, bidPrice, lwVolatilityLevels.bearishStopLoss, lwVolatilityLevels.bearishTakeProfit)){
         Print("Ошибка при исполнении рыночной заявки на продажу: ", GetLastError());
         Print(Trade.ResultRetcode());
         Print(Trade.ResultComment());
         return false;
      }
      return true;
   }
   
   if(!Trade.Sell(lotSize, _Symbol, bidPrice, lwVolatilityLevels.bearishStopLoss)){
      Print("Ошибка при исполнении рыночной заявки на продажу: ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }
   
   return true;
}
Эти функции поддерживают как фиксированный, так и динамический размер лота и применяют логику стоп-лосса и тейк-профита в зависимости от выбранного режима прибыли.

Управление сигналами входа

Вся логика входа объединена в функции:

//+---------------------------------------------------------------------------------------------------------+
//| Оценивает сигналы входа по свингам, применяет фильтры и открывает сделки при выполнении условий |
//+---------------------------------------------------------------------------------------------------------+
void EvaluateAndExecuteEntrySignals(){

   bool timeAllowed = true;
   
   if(useTimeFilter){
      timeAllowed = IsTimeWithinTradingHours();
   }

   //--- Обрабатываем бычьи сигналы входа
   if(IsLarryWilliamsShortTermLow(_Symbol, timeframe)){
      if(IsCrossOver(lwVolatilityLevels.buyEntryPrice,   closePriceMinutesData)){
         if(timeAllowed){
            if(tradeDayMode == TDW_SELECTED_DAYS){
               if(IsTradingDayAllowed(currentTime)){
                  if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
                     if(direction == TRADE_BOTH || direction == ONLY_LONG){
                        OpenBuy(lwVolatilityLevels.bullishStopLoss, lwVolatilityLevels.bullishTakeProfit, positionSize);
                        if(takeProfitMode  == TP_AFTER_N_CANDLES){
                           barsSinceEntry = 1;
                        }
                     }
                  }
               }
            }else{
               if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
                  if(direction == TRADE_BOTH || direction == ONLY_LONG){
                     OpenBuy(lwVolatilityLevels.bullishStopLoss, lwVolatilityLevels.bullishTakeProfit, positionSize);
                     if(takeProfitMode  == TP_AFTER_N_CANDLES){
                        barsSinceEntry = 1;
                     }
                  }
               }
            }
         }
      }
   }
   
   //--- Обрабатываем медвежьи сигналы входа
   if(IsLarryWilliamsShortTermHigh(_Symbol, timeframe)){
      if(IsCrossUnder(lwVolatilityLevels.sellEntryPrice, closePriceMinutesData)){
         if(timeAllowed){
            if(tradeDayMode == TDW_SELECTED_DAYS){
               if(IsTradingDayAllowed(currentTime)){
                  if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
                     if(direction == TRADE_BOTH || direction == ONLY_SHORT){
                        OpenSel(lwVolatilityLevels.bearishStopLoss, lwVolatilityLevels.bearishTakeProfit, positionSize);
                        if(takeProfitMode  == TP_AFTER_N_CANDLES){
                           barsSinceEntry = 1;
                        }
                     }
                  }
               }
            }else{
               if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){
                  if(direction == TRADE_BOTH || direction == ONLY_SHORT){
                     OpenSel(lwVolatilityLevels.bearishStopLoss, lwVolatilityLevels.bearishTakeProfit, positionSize);
                     if(takeProfitMode  == TP_AFTER_N_CANDLES){
                        barsSinceEntry = 1;
                     }
                  }
               }
            }
         }
      }
   }
}

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

Управление логикой выхода

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

//+-------------------------------------------------------------------------------------------+
//| Управляет логикой выхода для текущей открытой позиции с учетом выбранного режима тейк-профита |
//+-------------------------------------------------------------------------------------------+
void ManageOpenPositionExits(){

   if(takeProfitMode == TP_FIRST_PROFITABLE_OPEN){
      for(int i = PositionsTotal() - 1; i >= 0; i--){
         ulong ticket = PositionGetTicket(i);
         if(ticket == 0){
            Print("Ошибка при получении тикета позиции ", GetLastError());
            continue;
         }else{
            if(PositionGetDouble(POSITION_PROFIT) > 0 ){
               ClosePositionsByMagic(magicNumber);
            }
         }
      }
   }
   
   if(takeProfitMode == TP_AFTER_N_CANDLES){
      if(barsSinceEntry > exitAfterCandles){
         ClosePositionsByMagic(magicNumber);
         barsSinceEntry = 0;
      }
   }   
}

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

Для поддержки выходов по времени мы определяем в глобальной области:

//--- Отслеживает количество завершенных баров с момента открытия текущей сделки
int barsSinceEntry;

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

Сведение всего вместе

Наконец, мы завершаем функцию OnTick.

//+------------------------------------------------------------------+
//| Тиковая функция советника                                             |
//+------------------------------------------------------------------+
void OnTick(){

   //--- Получаем текущие рыночные цены для исполнения сделок
   askPrice      = SymbolInfoDouble (_Symbol, SYMBOL_ASK);
   bidPrice      = SymbolInfoDouble (_Symbol, SYMBOL_BID);
   currentTime   = TimeCurrent();
   
   //--- Получаем минутные данные
   if(CopyClose(_Symbol, PERIOD_M1, 0, 5, closePriceMinutesData) == -1){
      Print("Ошибка при копировании минутных данных ", GetLastError());
      return;
   }
   
   //--- Выполняем этот блок только при обнаружении нового бара на выбранном таймфрейме
   if(IsNewBar(_Symbol, timeframe, lastBarOpenTime)){
      
      //--- Пересчитываем уровни входа по волатильности на новом баре с учетом выбранных моделей входа и стопа
      UpdateVolatilityEntryLevels();     
      
      //--- Увеличиваем счетчик завершенных баров с момента открытия позиции
      if(barsSinceEntry > 0){
         barsSinceEntry = barsSinceEntry + 1;
      }
      
      //--- Обрабатываем условия выхода для текущей активной позиции с учетом выбранного режима тейк-профита
      if(takeProfitMode == TP_FIRST_PROFITABLE_OPEN){
         ManageOpenPositionExits();
      }
      
   }
   
   //--- Проверяем валидные сигналы входа и открываем сделки, если все правила выполнены
   EvaluateAndExecuteEntrySignals();
   
   //--- Обрабатываем условия выхода для текущей активной позиции с учетом выбранного режима тейк-профита
   if(takeProfitMode == TP_AFTER_N_CANDLES){
      ManageOpenPositionExits();
   }
}

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

Готовый исходный код приложен в файле lwVolatilityStructureTimeFilterExpert.mq5. Вместо того чтобы приводить здесь полный код, будем считать этот файл итоговым результатом сборки.

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

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

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

//+------------------------------------------------------------------+
//| Эта функция настраивает внешний вид графика |
//+------------------------------------------------------------------+
bool ConfigureChartAppearance()
{
   if(!ChartSetInteger(0, CHART_COLOR_BACKGROUND, clrWhite)){
      Print("Ошибка при настройке фона графика, ", GetLastError());
      return false;
   }
   
   if(!ChartSetInteger(0, CHART_SHOW_GRID, false)){
      Print("Ошибка при настройке сетки графика, ", GetLastError());
      return false;
   }
   
   if(!ChartSetInteger(0, CHART_MODE, CHART_CANDLES)){
      Print("Ошибка при настройке режима графика, ", GetLastError());
      return false;
   }

   if(!ChartSetInteger(0, CHART_COLOR_FOREGROUND, clrBlack)){
      Print("Ошибка при настройке переднего плана графика, ", GetLastError());
      return false;
   }

   if(!ChartSetInteger(0, CHART_COLOR_CANDLE_BULL, clrSeaGreen)){
      Print("Ошибка при настройке цвета бычьих свечей, ", GetLastError());
      return false;
   }
      
   if(!ChartSetInteger(0, CHART_COLOR_CANDLE_BEAR, clrBlack)){
      Print("Ошибка при настройке цвета медвежьих свечей, ", GetLastError());
      return false;
   }
   
   if(!ChartSetInteger(0, CHART_COLOR_CHART_UP, clrSeaGreen)){
      Print("Ошибка при настройке цвета медвежьих свечей, ", GetLastError());
      return false;
   }
   
   if(!ChartSetInteger(0, CHART_COLOR_CHART_DOWN, clrBlack)){
      Print("Ошибка при настройке цвета медвежьих свечей, ", GetLastError());
      return false;
   }
   
   return true;
}

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

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

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

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

После определения функция вызывается из функции инициализации советника. Это гарантирует, что внешний вид графика настраивается один раз — сразу при инициализации советника.

//+------------------------------------------------------------------+
//| Функция инициализации советника                                   |
//+------------------------------------------------------------------+
int OnInit(){

   ...
   
   //--- Настройка внешнего вида графика
   if(!ConfigureChartAppearance()){
      Print("Ошибка при настройке внешнего вида графика", GetLastError());
      return INIT_FAILED;
   }

   return(INIT_SUCCEEDED);
}

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

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


Бэктест стратегии на золоте

Когда основная логика завершена, следующий шаг — проверить поведение модели в реальных рыночных условиях. Чтобы тест был предметным и легко воспроизводимым, были выбраны один рынок и фиксированное временное окно. Используемый инструмент — XAUUSD (золото) на дневном таймфрейме. Период тестирования охватывает время с 1 января 2025 года по 30 декабря 2025 года, что на момент написания представляет полный год рыночных данных.

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

Чтобы результаты были полностью воспроизводимыми, к статье приложены два вспомогательных файла. Первый файл, configurations.ini, содержит настройки среды, использованные Тестером стратегий. Второй файл, parameters.set, хранит полный набор входных параметров, использованных в этом тесте. Вместе эти файлы позволяют восстановить те же условия с минимальными усилиями.

Бэктест начался с начального баланса счета $10 000. К концу периода тестирования система получила общую чистую прибыль $3 710,55.

Test Results

Это соответствует доходности немного выше 35% за год. Зафиксированный процент прибыльных сделок составил 54,55%, что не является исключительно высоким показателем. Однако он соответствует природе систем пробоя волатильности, которые опираются на асимметричное соотношение доходности к риску, а не на частые выигрыши.

Один из самых обнадеживающих аспектов этого прогона — форма кривой капитала. Приложенный снимок показывает плавное увеличение счета без резких просадок или внезапных обвалов.

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

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

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

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


Заключение

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

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

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

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

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

Следующая таблица перечисляет все дополнительные файлы, приложенные к статье, вместе с кратким описанием назначения каждого файла. Эти файлы предоставлены, чтобы помочь читателям воспроизвести обсуждаемые результаты и точно следовать реализации.
Название файла Описание
lwVolatilityStructureTimeFilterExpert.mq5
Основной исходный файл советника, реализующий полную торговую логику, включая модели пробоя волатильности Ларри Уильямса, определение свинг-структуры, временные и TDW-фильтры, а также правила торговли и управления риском.
configurations.ini
Конфигурация среды Тестера стратегий, использованная для бэктеста.
parameters.set
Файл настроек Тестера стратегий MetaTrader 5, содержащий фиксированный набор входных параметров, использованных для воспроизведения бэктестов и результатов, обсуждаемых в статье.

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

Прикрепленные файлы |
configurations.ini (2.79 KB)
parameters.set (2.66 KB)
Преодоление проблем доступности в торговых инструментах на MQL5 (Часть III): Двунаправленное голосовое взаимодействие между трейдером и советником Преодоление проблем доступности в торговых инструментах на MQL5 (Часть III): Двунаправленное голосовое взаимодействие между трейдером и советником
Создадим локальный двунаправленный голосовой интерфейс для MetaTrader 5 с помощью WebRequest в MQL5 и двух сервисов Python. В статье реализовано автономное распознавание речи с помощью Vosk, обнаружение фразы активации, HTTP‑endpoint для получения команд и сервер преобразования текста в речь на локальном хосте. Вы подключите советника, который будет получать команды, открывать сделки и возвращать голосовые подтверждения для возможности работать без помощи рук.
Адаптивная архитектура Smart Money (ASMA): Интеграция логики SMC с анализом сентимента для динамического переключения стратегий Адаптивная архитектура Smart Money (ASMA): Интеграция логики SMC с анализом сентимента для динамического переключения стратегий
В этой теме рассматривается, как построить адаптивную архитектуру Smart Money (ASMA) — интеллектуального советника, который объединяет концепции Smart Money Concepts (Order Blocks, Break of Structure, Fair Value Gaps) с рыночными настроениями в реальном времени, чтобы автоматически выбирать наиболее подходящую торговую стратегию исходя из текущего рыночного режима.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Моделирование рынка: Position View (IV) Моделирование рынка: Position View (IV)
Здесь мы начнем объединять различные компоненты или приложения, которые ранее были полностью изолированы друг от друга. Chart Trade, индикатор мыши и советник уже были связаны между собой, однако всё ещё отсутствовал способ прямой визуализации на графике открытых на торговом сервере позиций, которые зачастую обрабатывались через систему встречных ордеров. С этого момента это становится возможным, открывая путь для различных идей и будущих реализаций. Хотя мы только начинаем внедрять эти компоненты в работу, у нас уже появится направление для дальнейшего развития.