Рыночные секреты Ларри Уильямса (Часть 6): Оценка пробоев волатильности по свингам рынка
Введение
Волатильность — основа торговли на пробоях, однако ее часто рассматривают как одномерное понятие. В предыдущей статье этой серии мы изучили, как Ларри Уильямс измерял волатильность с помощью диапазона последнего торгового периода, и показали, как эту идею можно преобразовать в полностью автоматизированный советник на MQL5. Несмотря на эффективность, такой подход представляет лишь один способ оценивать расширение волатильности.
В этой статье мы рассмотрим волатильность с другой точки зрения — через свинги (ценовые колебания). Ларри Уильямс утверждает, что свинги, а не диапазоны отдельных периодов, могут показать, как покупатели и продавцы позиционируются под поверхностью видимого рыночного движения. Изучая, какое расстояние прошла цена между ключевыми точками свинга за последние дни, мы получаем представление о возможном расширении волатильности еще до того, как оно действительно проявится.
Цель этой статьи — не оптимизация, не подгонка под исторические данные и не фильтрация сделок. Вместо этого мы сосредоточимся на понимании и автоматизации исходной концепции в описанном виде, сохраняя реализацию гибкой и не привязанной к конкретному инструменту. Мы пошагово разберем расчет волатильности на основе свингов, переведем его в точные торговые правила и реализуем в виде универсального, хорошо структурированного советника MQL5.
Понимание измерения волатильности на основе свингов
Ларри Уильямс предлагает альтернативный способ оценки краткосрочной волатильности: измерять недавние свинги, а не полагаться на такие индикаторы, как ATR или стандартное отклонение. Основная идея проста: недавнее направленное движение цены дает практическую оценку того, как далеко рынок может пройти в следующей сессии.
Вместо измерения одного свинга рассчитываются два разных диапазона свинга по ценовым данным предыдущих завершенных баров. Эти свинги оцениваются в момент открытия нового бара, до принятия каких-либо торговых решений.
Ларри задает два конкретных способа измерения свинга. Первый свинг измеряет расстояние от максимума, зафиксированного три торговых дня назад, до минимума последнего завершенного дня.

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

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

Пороги покупки и продажи рассчитываются путем прибавления или вычитания настраиваемых процентов этого диапазона свинга из цены открытия. Сделки открываются только тогда, когда цена выходит за пределы этих расчетных уровней, подтверждая волатильность, а не прогнозируя ее.
Перевод концепции в торговые правила
При открытии каждого нового бара на выбранном таймфрейме советник рассчитывает диапазон свинга с помощью методики измерения свингов Ларри Уильямса, объясненной выше. Этот диапазон свинга становится основой для всех решений в текущем торговом периоде. На его основе советник рассчитывает два ключевых ценовых уровня. Уровень входа в покупку рассчитывается путем прибавления заданного пользователем процента диапазона свинга к цене открытия текущего торгового периода; уровень входа в продажу — путем вычитания того же процента из этой цены открытия. Эти расчетные уровни сохраняются в памяти и остаются неизменными до открытия нового бара.
В течение активного торгового периода ценовое движение отслеживается тик за тиком. Если цена поднимается выше уровня входа в покупку, советник открывает позицию по рынку на покупку. Если цена опускается ниже уровня входа в продажу, советник открывает позицию по рынку на продажу. Обратите внимание: отложенные ордера не используются. Все сделки исполняются по рынку при возникновении действительного пробоя.
Управление риском напрямую связано с измеренным диапазоном свинга. Стоп-лосс для каждой сделки рассчитывается как заданный пользователем процент того же диапазона свинга, который используется для входов. Тейк-профит затем определяется на основе заданного соотношения риск/доходность. Расстояние между ценой входа и стоп-лоссом определяет риск. Это расстояние риска умножается на настраиваемый множитель прибыли, чтобы рассчитать уровень тейк-профита. Это гарантирует, что каждая сделка имеет единообразную и контролируемую структуру риска.
В любой момент времени допускается только одна позиция. После открытия позиции дополнительные позиции не могут быть открыты, пока текущая позиция не будет закрыта. Если цена за весь торговый период не достигает ни уровня покупки, ни уровня продажи, позиция не открывается. Когда открывается новый бар, все ранее рассчитанные уровни отбрасываются, и для следующего периода вычисляются новые.
Советник также позволяет трейдеру управлять разрешенным направлением торговли. Пользователь может ограничить торговлю режимом только длинные позиции, только короткие позиции или оба направления. Эта функция удобна для трейдеров, которые применяют дискреционный анализ тренда и предпочитают торговать только в направлении доминирующего движения рынка.
Размер позиции гибко задается через два режима расчета лота. В ручном режиме, советник использует фиксированный размер лота, заданный пользователем. В автоматическом режиме, размер позиции рассчитывается на основе фиксированного процента от баланса счета. Автоматический режим динамически корректирует размер лота, чтобы поддерживать заранее заданный процент риска в сделках независимо от волатильности цены или роста счета.
Пошаговое построение советника
Этот раздел знаменует начало сборки советника. Далее фокус смещается с теории на реализацию. Чтобы уверенно следовать материалу, читателю желательно уже иметь базовый практический опыт работы с MQL5. Это включает использование платформы MetaTrader 5, прикрепление советников к графикам и запуск тестов в Тестере стратегий. Читателю также следует быть знакомым с MetaEditor 5 и уметь писать код, компилировать его, проверять ошибки и при необходимости выполнять отладку. Программированию учатся на практике, а не пассивным чтением, поэтому этот раздел рассчитан на активное выполнение шагов.
По этой причине полный и окончательный исходный файл, разработанный в этой статье, приложен как lwVolatilitySwingBreakoutExpert.mq5. Если у вас возникнут проблемы при пошаговом создании советника, вы всегда можете сравнить свою работу с приложенным файлом, чтобы не отклоняться от исходной версии. Настоятельно рекомендуется скачать его перед продолжением.
Начните с открытия MetaEditor 5 и создания нового файла советника. Вы можете дать ему любое имя. После создания файла вставьте в него приведенный ниже каркас кода.
//+------------------------------------------------------------------+ //| lwVolatilitySwingBreakoutExpert.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_LOT_SIZE_INPUT_MODE { MODE_MANUAL, MODE_AUTO }; //+------------------------------------------------------------------+ //| Входные параметры пользователя | //+------------------------------------------------------------------+ input group "Информация" input ulong magicNumber = 254700680002; input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT; input group "Параметры пробоя волатильности" input double inpBuyRangeMultiplier = 0.50; input double inpSellRangeMultiplier = 0.50; input double inpStopRangeMultiplier = 0.50; input double inpRewardValue = 4.0; input group "Торговля и управление риском" input ENUM_TRADE_DIRECTION direction = ONLY_LONG; 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; //+------------------------------------------------------------------+ //| Функция инициализации советника | //+------------------------------------------------------------------+ 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); } //+------------------------------------------------------------------+ //| Функция TradeTransaction | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { } //--- СЛУЖЕБНЫЕ ФУНКЦИИ //+------------------------------------------------------------------+
Этот начальный код задает структуру, на которой мы будем строить дальнейшую реализацию.
Понимание структуры шаблона
Шаблонный код разделен на четко определенные секции, каждая из которых выполняет определенную задачу. Раздел заголовка задает информацию о владельце и версии. Это не влияет на торговую логику, но помогает идентифицировать файл и его автора.
//+------------------------------------------------------------------+ //| lwVolatilitySwingBreakoutExpert.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"
Подключение стандартной библиотеки добавляет классCTrade. Этот класс упрощает исполнение ордеров и управление сделками и будет использоваться позже при размещении сделок.
//+------------------------------------------------------------------+ //| Стандартные библиотеки | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh>
Пользовательские перечисленияопределяются далее.
//--- ПОЛЬЗОВАТЕЛЬСКИЕ ПЕРЕЧИСЛЕНИЯ enum ENUM_TRADE_DIRECTION { ONLY_LONG, ONLY_SHORT, TRADE_BOTH }; 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 double inpBuyRangeMultiplier = 0.50; input double inpSellRangeMultiplier = 0.50; input double inpStopRangeMultiplier = 0.50; input double inpRewardValue = 4.0; input group "Торговля и управление риском" input ENUM_TRADE_DIRECTION direction = ONLY_LONG; 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;
Здесь мы создаем объект CTrade для исполнения сделок и определяем переменные для хранения текущих цен bid и ask. Эти цены обновляются на каждом тике и используются для точных торговых расчетов.
ФункцияOnInitзапускается один раз при старте советника.
//+------------------------------------------------------------------+ //| Функция инициализации советника | //+------------------------------------------------------------------+ int OnInit(){ //--- Назначаем уникальный magic number для идентификации сделок этого советника Trade.SetExpertMagicNumber(magicNumber); return(INIT_SUCCEEDED); }
Ее роль здесь важна и проста. Она назначает уникальный magic number объекту CTrade так, чтобы все сделки, открытые этим советником, можно было надежно идентифицировать. В дальнейшем мы будем использовать эту функцию для инициализации глобальных переменных, которые должны начинаться с известных значений.
ФункцияOnDeinitзапускается, когда советник удаляется или останавливается.
//+------------------------------------------------------------------+ //| Функция деинициализации советника | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- Сообщаем причину остановки программы Print("Программа завершена! Код причины: ", reason); }
Она просто сообщает причину завершения и не влияет на торговую логику.
ФункцияOnTickвызывается на каждом рыночном тике.
//+------------------------------------------------------------------+ //| Тиковая функция советника | //+------------------------------------------------------------------+ void OnTick(){ //--- Получаем текущие рыночные цены для исполнения сделок askPrice = SymbolInfoDouble (_Symbol, SYMBOL_ASK); bidPrice = SymbolInfoDouble (_Symbol, SYMBOL_BID); }
На этом этапе она только обновляет цены bid и ask. Позже эта функция станет центральной точкой управления для выполнения логики нашей стратегии.
Раздел служебных функций сначала намеренно оставлен пустым.
//--- СЛУЖЕБНЫЕ ФУНКЦИИ //+------------------------------------------------------------------+
Здесь мы разместим все пользовательские вспомогательные функции, поддерживающие основную торговую логику.
Определение нового бара
Наша стратегия требует пересчитывать уровни только один раз за торговый период. Для этого необходимо определить момент открытия нового бара на выбранном таймфрейме.
Для этой цели в раздел служебных функций добавляется пользовательская функция.
//--- СЛУЖЕБНЫЕ ФУНКЦИИ //+------------------------------------------------------------------+ //| Функция проверки нового бара на заданном таймфрейме графика | //+------------------------------------------------------------------+ bool IsNewBar(string symbol, ENUM_TIMEFRAMES tf, datetime &lastTm){ datetime currentTm = iTime(symbol, tf, 0); if(currentTm != lastTm){ lastTm = currentTm; return true; } return false; }
Функция сравнивает время открытия текущего бара с ранее записанным временем бара. Если значения отличаются, значит сформировался новый бар.
Функция принимает три параметра. symbol и timeframe указывают, какой график мы отслеживаем. Третий параметр передается по ссылке и хранит время открытия последнего обработанного бара. При обнаружении нового бара это значение автоматически обновляется.
Для поддержки этой логики объявляется глобальная переменная типа datetime.
//--- Для отслеживания открытия нового бара datetime lastBarOpenTime;
Эта переменная отслеживает время открытия последнего обработанного бара. Внутри функции OnInit эта переменная инициализируется значением ноль для обеспечения чистого начального состояния.
//+------------------------------------------------------------------+ //| Функция инициализации советника | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Инициализируем глобальные переменные lastBarOpenTime = 0; return(INIT_SUCCEEDED); }
Хранение дневных уровней волатильности
Для каждого торгового периода требуется фиксированный набор ценовых уровней, остающихся действительными до открытия следующего бара. Сюда входят диапазон свинга, цены входа, уровни стоп-лосса и тейк-профита для обоих направлений торговли.
Чтобы аккуратно хранить эти значения, в глобальной области определяется пользовательская структура.
//--- Хранит все ценовые уровни, полученные из расчетов пробоя волатильности Ларри Уильямса struct MqlLwVolatilityLevels { double dominantSwingRange; double buyEntryPrice; double sellEntryPrice; double bullishStopLoss; double bearishStopLoss; double bullishTakeProfit; double bearishTakeProfit; }; MqlLwVolatilityLevels lwVolatilityLevels;
Эта структура объединяет все связанные ценовые уровни в одну логическую единицу. Экземпляр этой структуры создается сразу после ее определения.
Внутри функции OnInit экземпляр структуры сбрасывается с помощью функцииZeroMemory.
//+------------------------------------------------------------------+ //| Функция инициализации советника | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Сбрасываем уровни волатильности Ларри Уильямса ZeroMemory(lwVolatilityLevels); return(INIT_SUCCEEDED); }
Это гарантирует, что все поля начинают работу с известных значений, и предотвращает непреднамеренное поведение, вызванное неинициализированными данными.
Расчет диапазона волатильности на основе свингов
Первая пользовательская расчетная функция определяет диапазон волатильности на основе свингов, описанный Ларри Уильямсом.
//+------------------------------------------------------------------+ //| Рассчитывает диапазон волатильности по свингам Ларри Уильямса | //+------------------------------------------------------------------+ 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)); }
Эта функция получает конкретные максимумы и минимумы из исторических баров на выбранном таймфрейме. Два расстояния свинга вычисляются с использованием абсолютных значений, чтобы обеспечить корректность независимо от порядка цен. Большее из двух расстояний свинга выбирается как доминирующий диапазон свинга.
Это значение представляет самое значимое недавнее расширение цены на рынке и служит прокси-показателем волатильности для всех последующих расчетов. Перед возвратом результат нормализуется в соответствии с точностью цены символа.
Расчет цен входа
Цена входа в покупку рассчитывается путем прибавления заданного пользователем процента диапазона свинга к сегодняшней цене открытия. Это проецирует бычий уровень пробоя выше рынка.
//+--------------------------------------------------------------------------------+ //| Рассчитывает цену входа для бычьего пробоя по открытию дня и диапазону свинга | //+--------------------------------------------------------------------------------+ double CalculateBuyEntryPrice(double todayOpen, double swingRange, double buyMultiplier){ return todayOpen + (swingRange * buyMultiplier); }
Цена входа в продажу рассчитывается путем вычитания заданного пользователем процента диапазона свинга из сегодняшней цены открытия. Это проецирует медвежий уровень пробоя ниже рынка.
//+--------------------------------------------------------------------------------+ //| Рассчитывает цену входа для медвежьего пробоя по открытию дня и диапазону свинга | //+--------------------------------------------------------------------------------+ double CalculateSellEntryPrice(double todayOpen, double swingRange, double sellMultiplier){ return todayOpen - (swingRange * sellMultiplier); }
Обе функции намеренно просты. Они преобразуют волатильность в рабочие ценовые уровни без лишнего усложнения.
Расчет уровней стоп-лосса
Уровни стоп-лосса выводятся непосредственно из цен входа.
//+--------------------------------------------------------------------------------------------+ //| Рассчитывает цену стоп-лосса для бычьей позиции по цене входа и диапазону свинга | //+--------------------------------------------------------------------------------------------+ double CalculateBullishStopLoss(double entryPrice, double swingRange, double stopMultiplier){ return entryPrice - (swingRange * stopMultiplier); } //+--------------------------------------------------------------------------------------------+ //| Рассчитывает цену стоп-лосса для медвежьей позиции по цене входа и диапазону свинга | //+--------------------------------------------------------------------------------------------+ double CalculateBearishStopLoss(double entryPrice, double swingRange, double stopMultiplier){ return entryPrice + (swingRange * 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()); }
Для бычьих сделок риск определяется расстоянием между ценой входа и стоп-лоссом. Это расстояние умножается на коэффициент прибыли, чтобы рассчитать тейк-профит выше цены входа.
Для медвежьих сделок та же логика применяется в противоположном направлении. Перед возвратом рассчитанные уровни тейк-профита нормализуются по точности символа.
Объединение всего в OnTick
Когда все вспомогательные функции готовы, функция OnTick связывает все вместе.
//+------------------------------------------------------------------+ //| Тиковая функция советника | //+------------------------------------------------------------------+ void OnTick(){ ... //--- Выполняем этот блок только при обнаружении нового бара на выбранном таймфрейме if(IsNewBar(_Symbol, timeframe, lastBarOpenTime)){ lwVolatilityLevels.dominantSwingRange = CalculateLwSwingVolatilityRange(_Symbol, timeframe); lwVolatilityLevels.buyEntryPrice = CalculateBuyEntryPrice (askPrice, lwVolatilityLevels.dominantSwingRange, inpBuyRangeMultiplier ); lwVolatilityLevels.sellEntryPrice = CalculateSellEntryPrice(bidPrice, lwVolatilityLevels.dominantSwingRange, inpSellRangeMultiplier); lwVolatilityLevels.bullishStopLoss = CalculateBullishStopLoss(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.dominantSwingRange, inpStopRangeMultiplier); lwVolatilityLevels.bearishStopLoss = CalculateBearishStopLoss(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.dominantSwingRange, inpStopRangeMultiplier); lwVolatilityLevels.bullishTakeProfit = CalculateBullishTakeProfit(lwVolatilityLevels.buyEntryPrice, lwVolatilityLevels.bullishStopLoss, inpRewardValue); lwVolatilityLevels.bearishTakeProfit = CalculateBearishTakeProfit(lwVolatilityLevels.sellEntryPrice, lwVolatilityLevels.bearishStopLoss, inpRewardValue); } }
На каждом тике советник сначала обновляет цены bid и ask. Затем он проверяет, сформировался ли новый бар на выбранном таймфрейме. Если новый бар не обнаружен, больше ничего не происходит.
При обнаружении нового бара все уровни на основе волатильности пересчитываются. Диапазон свинга вычисляется первым. Цены входа, стоп-лоссы и уровни тейк-профита затем последовательно выводятся с использованием ранее рассчитанного диапазона свинга.
Эти значения сохраняются в глобальной структуре и остаются неизменными до обнаружения следующего нового бара. Такой подход гарантирует, что торговые решения в течение периода основаны на фиксированных, заранее рассчитанных уровнях, а не на постоянно меняющихся значениях.
На этом этапе у советника есть полноценный каркас для расчета и хранения всех ценовых уровней, необходимых для исполнения сделок. В следующем разделе эти уровни будут использоваться для запуска сделок и управления позициями согласно ранее определенным правилам.
Завершение торговой логики и исполнение сделок
На этом этапе у нас уже есть все необходимое для принятия торговых решений. Дневные уровни волатильности рассчитаны и сохранены в памяти, и они остаются действительными до формирования нового бара и расчета свежих уровней. Осталось определить, как и когда открываются сделки, как предотвратить дублирование позиций и как исполнять ордера контролируемо и последовательно.
Основная идея проста. Когда цена пересекает один из заранее определенных уровней входа, мы открываем позицию в этом направлении. Если первым пересекается уровень покупки, открываем длинную сделку. Если первым пересекается уровень продажи, открываем короткую сделку. При этом всегда соблюдается строгое правило: в любой момент времени может быть открыта только одна позиция.
Чтобы реализовать это аккуратно, мы разбиваем логику на небольшие служебные функции.
Определение пересечений цены
Первая задача — определить, когда цена пересекает конкретный уровень. Нас не интересует простое касание уровня. Нам нужно подтверждение, что цена перешла с одной стороны уровня на другую. Для этого мы определяем две служебные функции. IsCrossOver обнаруживает, когда цена пересекает уровень снизу вверх.
//+------------------------------------------------------------------+ //| Для определения пересечения заданного ценового уровня снизу вверх | //+------------------------------------------------------------------+ bool IsCrossOver(const double price, const double &closePriceMinsData[]){ if(closePriceMinsData[1] <= price && closePriceMinsData[0] > price){ return true; } return false; }
Эта функция сравнивает две последовательные цены закрытия минутных баров с целевым уровнем. Значение с индексом один представляет предыдущий завершенный минутный бар. Значение с индексом ноль представляет самый последний бар. Пересечение вверх происходит, когда предыдущее закрытие было на уровне или ниже него, а текущее закрытие оказалось выше уровня. Такое простое сравнение дает четкий и надежный сигнал о том, что цена переместилась выше уровня.
Функция IsCrossUnder выполняет противоположную проверку. Она обнаруживает, когда цена пересекает уровень сверху вниз.
//+------------------------------------------------------------------+ //| Для определения пересечения заданного ценового уровня сверху вниз | //+------------------------------------------------------------------+ bool IsCrossUnder(const double price, const double &closePriceMinsData[]){ if(closePriceMinsData[1] >= price && closePriceMinsData[0] < price){ return true; } return false; }
Здесь логика обратная. Мы подтверждаем, что предыдущее закрытие было на уровне или выше него, а самое последнее закрытие переместилось ниже. Это говорит о том, что цена пересекла уровень вниз. Вместе эти две функции образуют основу нашей логики входа.
Хранение минутных ценовых данных
Обе функции пересечения используют цены закрытия на 1-минутном таймфрейме. Для этого мы определяем глобальный массив, в котором будут храниться эти данные.
//--- Для хранения минутных данных double closePriceMinutesData [];
Этот массив должен обрабатываться как временной ряд. В MQL5 массивы по умолчанию не ведут себя как временные ряды. Если явно не изменить направление индексации, индекс ноль не будет представлять самый последний бар. Это нарушит логику пересечений. Поэтому мы настраиваем массив во время инициализации.
//+------------------------------------------------------------------+ //| Функция инициализации советника | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Обрабатываем следующие массивы как таймсерии (индекс 0 — самый последний бар) ArraySetAsSeries(closePriceMinutesData, true); }
Эта инструкция меняет порядок индексации так, что индекс ноль всегда относится к самому последнему бару. Без этого шага проверки пересечений использовали бы неправильные ценовые значения, и советник вел бы себя непредсказуемо.
Обновление минутных данных на каждом тике
Внутри функции OnTick мы обновляем массив минутных данных при поступлении новых ценовых данных.
//+------------------------------------------------------------------+ //| Тиковая функция советника | //+------------------------------------------------------------------+ void OnTick(){ ... //--- Получаем минутные данные if(CopyClose(_Symbol, PERIOD_M1, 0, 7, closePriceMinutesData) == -1){ Print("Ошибка при копировании минутных данных ", GetLastError()); return; } }
Мы копируем семь последних минутных цен закрытия. Хотя для определения пересечения нужны только два последних значения, копирование нескольких дополнительных баров дает небольшой запас надежности и не влияет на производительность.
Если копирование данных не удалось, мы прекращаем выполнение на этом тике. Действия на основе неполных или отсутствующих ценовых данных привели бы к ненадежным торговым решениям.
Предотвращение нескольких активных позиций
Цена может пересекать один и тот же уровень несколько раз, особенно в условиях высокой волатильности. Без надлежащих защитных проверок это могло бы привести к открытию нескольких сделок подряд.
Чтобы предотвратить это, мы определяем две функции, которые проверяют, есть ли у этого советника уже активная позиция. Для проверки наличия длинной позиции мы определяем следующую пользовательскую функцию:
//+------------------------------------------------------------------+ //| Проверяет, есть ли у советника активная позиция на покупку. | | //+------------------------------------------------------------------+ 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; }
Эта функция перебирает все открытые позиции в терминале. Для каждой позиции она проверяет два условия. Во-первых, magic number должен совпадать с magic number советника. Это гарантирует, что мы проверяем только сделки, открытые этим советником. Во-вторых, тип позиции должен быть покупкой. Если оба условия выполнены, функция сразу возвращает true. Если подходящая позиция не найдена, функция возвращает 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; }
Эта функция имеет ту же структуру, что и предыдущая, но явно проверяет короткие позиции. Вместе эти две проверки гарантируют, что мы никогда не открываем более одной сделки одновременно.
Автоматический расчет размера позиции по риску
Для поддержки автоматического расчета лота мы определяем функцию, которая рассчитывает размер позиции на основе фиксированного процента от баланса счета.
//+----------------------------------------------------------------------------------+ //| Рассчитывает размер позиции по фиксированному проценту риска от баланса счета | //+----------------------------------------------------------------------------------+ 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 entryPrice, double stopLoss, double takeProfit, double lotSize){ if(lotSizeMode == MODE_AUTO){ lotSize = CalculatePositionSizeByRisk(lwVolatilityLevels.buyEntryPrice - lwVolatilityLevels.bullishStopLoss); } if(!Trade.Buy(lotSize, _Symbol, entryPrice, stopLoss, takeProfit)){ Print("Ошибка при исполнении рыночного ордера на покупку: ", GetLastError()); Print(Trade.ResultRetcode()); Print(Trade.ResultComment()); return false; } return true; }
Если включен автоматический расчет лота, функция вычисляет размер позиции на основе расстояния между ценой входа и стоп-лоссом. Затем сделка исполняется с помощью класса CTrade. Если ордер не выполнен, мы записываем подробную информацию об ошибке. Если ордер выполнен успешно, функция возвращает true.
Функция продажи имеет ту же структуру.
//+------------------------------------------------------------------+ //| Функция открытия рыночной позиции на продажу | //+------------------------------------------------------------------+ bool OpenSel(double entryPrice, double stopLoss, double takeProfit, double lotSize){ if(lotSizeMode == MODE_AUTO){ lotSize = CalculatePositionSizeByRisk(lwVolatilityLevels.bearishStopLoss - lwVolatilityLevels.sellEntryPrice); } if(!Trade.Sell(lotSize, _Symbol, entryPrice, stopLoss, takeProfit)){ Print("Ошибка при исполнении рыночного ордера на продажу: ", GetLastError()); Print(Trade.ResultRetcode()); Print(Trade.ResultComment()); return false; } return true; }
Единственное отличие — направление расстояния до стопа и использование ордера на продажу. Структура остается последовательной, что облегчает сопровождение кода.
Объединение всего в OnTick
Когда все строительные блоки готовы, мы связываем все вместе внутри функции OnTick.
//+------------------------------------------------------------------+ //| Тиковая функция советника | //+------------------------------------------------------------------+ void OnTick(){ ... //--- Логика длинной позиции if(direction == TRADE_BOTH || direction == ONLY_LONG){ if(IsCrossOver(lwVolatilityLevels.buyEntryPrice, closePriceMinutesData)){ if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){ OpenBuy(askPrice, lwVolatilityLevels.bullishStopLoss, lwVolatilityLevels.bullishTakeProfit, positionSize); } } } //--- Логика короткой позиции if(direction == TRADE_BOTH || direction == ONLY_SHORT){ if(IsCrossUnder(lwVolatilityLevels.sellEntryPrice, closePriceMinutesData)){ if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){ OpenSel(bidPrice, lwVolatilityLevels.bearishStopLoss, lwVolatilityLevels.bearishTakeProfit, positionSize); } } } }
Этот блок проверяет, разрешены ли длинные сделки. Если разрешены, мы проверяем пересечение вверх над уровнем входа в покупку. Если пересечение обнаружено и активных позиций нет, мы открываем сделку на покупку с использованием заранее рассчитанных уровней. Логика для коротких сделок устроена аналогично. Такая структура обеспечивает ясность, контроль и строгое соблюдение правил стратегии.
Настройка внешнего вида графика
Перед тестированием улучшим читаемость графика, настроив визуальные параметры. Определим следующую пользовательскую служебную функцию:
//+------------------------------------------------------------------+ //| Эта функция настраивает внешний вид графика. | //+------------------------------------------------------------------+ 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; } //+------------------------------------------------------------------+
Эта функция задает чистый белый фон, убирает сетку, включает режим японских свечей и применяет понятные цвета для бычьих и медвежьих свечей. Если какая-либо настройка завершается ошибкой, функция сообщает об ошибке и прекращает выполнение. Мы вызываем эту функцию во время инициализации.
//+------------------------------------------------------------------+ //| Функция инициализации советника | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Для настройки внешнего вида графика if(!ConfigureChartAppearance()){ Print("Ошибка при настройке внешнего вида графика", GetLastError()); return INIT_FAILED; } }
Это гарантирует, что график подготовлен до начала любой торговой активности.
На этом этапе советник полностью реализован. Теперь можно скомпилировать исходный код. Если все добавлено правильно, код должен компилироваться без ошибок. Если возникнут проблемы, приложенный исходный файл lwVolatilitySwingBreakoutExpert.mq5, можно использовать как справочник.
Тестирование советника
Тестирование позволяет убедиться, что торговая логика ведет себя ожидаемым образом, и оценить работу стратегии на исторических рыночных данных. В этом разделе мы сосредоточимся на бэктестинге советника с помощью Тестера стратегий MetaTrader 5.
Среда и настройка бэктеста
Бэктест был проведен на золоте (символ XAUUSD) с использованием дневного таймфрейма. Период тестирования охватывает время с 1 января 2025 года по 31 декабря 2025 года. Это дает полную годовую выборку, достаточную для наблюдения за реакцией стратегии в разных рыночных условиях.
Советник был настроен на работу в режиме ONLY_LONG. Это означает, что алгоритму разрешалось открывать только длинные позиции. Короткие сделки были полностью отключены. В каждой сделке рисковался ровно один процент от баланса счета с использованием ранее реализованной логики автоматического расчета размера позиции.
Чтобы результаты можно было воспроизвести, к этой статье приложены два важных файла. Первый файл —configurations.ini. Он содержит все настройки среды Тестера стратегий, такие как символ, таймфрейм и диапазон тестирования. Второй файл —parameters.set. Он содержит точные входные параметры, использованные во время теста, включая настройки риска и множители волатильности. Загрузив оба файла в Тестер стратегий, можно точно воспроизвести похожие условия тестирования.
Обзор результатов бэктеста
Бэктест начался с начального баланса счета 10 000 долларов. К концу тестового периода система получила общую чистую прибыль 4 450,23 доллара. Это соответствует доходности немного выше сорока четырех процентов за один год.

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

Резких падений или внезапных провалов капитала нет. Такое поведение указывает на контролируемые просадки и последовательное исполнение торговых правил.
Важно понимать, что этот тест представляет только одну конфигурацию и один рынок. Стратегия намеренно разработана гибкой. Входные параметры, такие как процент риска, множители волатильности и направление торговли, можно менять. Это позволяет трейдерам тестировать разные варианты и адаптировать логику к собственным идеям.
Основная цель этой статьи — не представить готовую или оптимизированную торговую систему. Цель — показать, как методологию Ларри Уильямса можно перевести в рабочий алгоритм, который трейдеры смогут изучать, изменять и расширять. Читателям рекомендуется проводить собственные тесты. Разные символы, таймфреймы и комбинации параметров могут приводить к разным результатам. Эксперименты помогают находить варианты, лучше соответствующие индивидуальным торговым предпочтениям и терпимости к риску.
Приглашаем всех делиться своими наблюдениями, результатами тестов и выводами в разделе комментариев. Совместные эксперименты и обсуждение часто выявляют идеи, которые не сразу очевидны по одному бэктесту.
Заключение
В этой статье мы успешно перевели концепцию пробоя волатильности на основе свингов Ларри Уильямса в полностью функциональный советник для MetaTrader 5. Начиная с исходных объяснений из его книги, мы внимательно интерпретировали измерения свинга, уточнили их практические последствия и превратили их в точные, основанные на правилах расчеты, пригодные для автоматизации.
Мы спроектировали и реализовали полноценную торговую систему в пошаговом процессе. Это включало определение новых торговых периодов, измерение доминирующего диапазона свинга, расчет уровней входа на пробой, задание целей стоп-лосса и тейк-профита, а также соблюдение строгих правил управления сделками. Каждый компонент был разработан ясно и целенаправленно, что привело к созданию советника со структурированным, читаемым и легко расширяемым кодом.
Помимо логики стратегии, статья показала надежные инженерные практики в MQL5. Мы рассмотрели управление состоянием с помощью структур, безопасную инициализацию глобальных переменных, надежное определение пересечений, фильтрацию позиций по magic number, а также ручной и основанный на риске расчет размера позиции. Это важные строительные блоки для любой серьезной алгоритмической торговой системы.
Раздел бэктестинга показал, что стратегия может работать последовательно при дисциплинированном применении и правильном контроле риска. Что еще важнее, система намеренно создана гибкой. Вынося ключевые параметры в настройки, советник позволяет трейдерам тестировать разные идеи, адаптировать логику к различным рынкам и искать собственное преимущество вместо опоры на фиксированные предположения.
Эта статья не претендует на представление идеальной или оптимизированной стратегии. Вместо этого она показывает, как преобразовать дискреционную торговую идею в структурированный, тестируемый и повторяемый алгоритм. Читатели, выполнявшие шаги, теперь имеют рабочий советник для торговли пробоями волатильности, более глубокое понимание методологии Ларри Уильямса и прочный каркас для дальнейших исследований.
В следующей таблице перечислены все дополнительные файлы, приложенные к этой статье, вместе с кратким описанием назначения каждого файла. Эти файлы предоставлены, чтобы помочь читателям воспроизвести обсуждаемые результаты и точно следовать реализации.
| Имя файла | Описание | |
|---|---|---|
| 1 | lwVolatilitySwingBreakoutExpert.mq5 | Полный исходный код советника разработан и объяснен в этой статье. |
| 2 | configurations.ini | Конфигурация среды Тестера стратегий, использованная для бэктеста. |
| 3 | parameters.set | Набор входных параметров, примененный во время бэктеста и показанный в статье. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20862
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Самообучающийся SuperTrend: адаптивный индикатор тренда на машинном обучении
Нейросети в трейдинге: Когнитивная инерция в анализе финансовых рынков (CogDriver)
Сеточный советник на клеточном автомате с онлайн-обучением в MQL5 (Часть II): Новый уровень онлайн-адаптации
От начального до среднего уровня: FileSave и FileLoad
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования