Разработка инструментария для анализа Price Action (Часть 74): Создание советника MQL5 на основе буферов индикатора
Содержание
- Введение
- Интеграция советника с индикатором
- Реализация советника на MQL5
- Тестирование на исторических данных и проверка
- Заключение
Введение
В предыдущей статье разработан индикатор торговли по гэпам выходного дня, формирующий сигналы на покупку и продажу вместе с соответствующими уровнями тейк-профита и стоп-лосса. Однако индикатор сам по себе не может совершать сделки. Если его сигналы не исполняются вручную или автоматизированной системой, они остаются визуальными указаниями на графике, пусть даже и эффективными.
В этой статье создается советник, который считывает сигналы индикатора, автоматически совершает сделки и использует значения стоп-лосса и тейк-профита, уже определенные индикатором.
Далее рассмотрим реализацию советника.
Интеграция советника с индикатором
Чтобы советник мог подключаться к индикатору, индикатор должен предоставлять буферы, которые советник сможет считывать через CopyBuffer(). В MQL5 буферы – это выходные массивы индикатора, хранящие для каждого бара такие значения, как сигнальные стрелки, уровни стоп-лосса и тейк-профита. Они служат каналом передачи данных между индикатором и советником.
Буферы индикатора обеспечивают связь между индикатором и советником. Индикатор записывает вычисленные значения в буферы, а советник считывает их по мере необходимости. Буферы сами по себе не выполняют действий.

Пользовательский индикатор, разработанный в предыдущей статье, предоставляет шесть буферов, содержащих всю информацию, необходимую советнику. Эти буферы образуют слой обмена данными между индикатором и советником, что позволяет принимать торговые решения без повторного расчета логики торговли по гэпам выходного дня.
Буферы предоставляют следующую информацию:
| Индекс буфера | Имя буфера | Назначение |
|---|---|---|
| 0 | BufferBuyArrow | Хранит точки появления сигналов на покупку. |
| 1 | BufferSellArrow | Хранит точки появления сигналов на продажу. |
| 2 | BufferBuyTP | Хранит уровни тейк-профита для сигналов на покупку. |
| 3 | BufferBuySL | Хранит уровни стоп-лосса для сигналов на покупку. |
| 4 | BufferSellTP | Хранит уровни тейк-профита для сигналов на продажу. |
| 5 | BufferSellSL | Хранит уровни стоп-лосса для сигналов на продажу. |
Эти буферы объявляются в индикаторе и заполняются всякий раз, когда обнаруживается валидная торговая возможность по гэпу выходного дня.
//+------------------------------------------------------------------+ //| Indicator buffers | //+------------------------------------------------------------------+ double BufferBuyArrow[]; // Buy arrow prices double BufferSellArrow[]; // Sell arrow prices double BufferBuyTP[]; // Buy take profit levels double BufferBuySL[]; // Buy stop loss levels double BufferSellTP[]; // Sell take profit levels double BufferSellSL[]; // Sell stop loss levels
Чтобы сделать эти значения доступными вне индикатора, каждый буфер регистрируется с помощью SetIndexBuffer() при инициализации.
//--- set indicator buffers SetIndexBuffer(0, BufferBuyArrow, INDICATOR_DATA); SetIndexBuffer(1, BufferSellArrow, INDICATOR_DATA); SetIndexBuffer(2, BufferBuyTP, INDICATOR_CALCULATIONS); SetIndexBuffer(3, BufferBuySL, INDICATOR_CALCULATIONS); SetIndexBuffer(4, BufferSellTP, INDICATOR_CALCULATIONS); SetIndexBuffer(5, BufferSellSL, INDICATOR_CALCULATIONS);
Буферы стрелок на покупку и продажу служат буферами графического отображения и поэтому используют тип INDICATOR_DATA. Оставшиеся четыре буфера являются расчетными буферами для хранения информации о стоп-лоссе и тейк-профите, которую позднее использует советник.
Когда формируется сигнал, индикатор записывает соответствующие значения в эти буферы с помощью функции RenderSignalBuffers().
if(sig.signalIsBuy) { BufferBuyArrow[sh] = sig.signalPrice; BufferBuyTP[sh] = NormalizeDouble(sig.signalTP, _Digits); BufferBuySL[sh] = NormalizeDouble(sig.signalSL, _Digits); } else { BufferSellArrow[sh] = sig.signalPrice; BufferSellTP[sh] = NormalizeDouble(sig.signalTP, _Digits); BufferSellSL[sh] = NormalizeDouble(sig.signalSL, _Digits); }
Такая архитектура позволяет индикатору предоставлять больше, чем простые сигналы направления. Помимо выявления возможностей для покупки и продажи, он предоставляет полную структуру сделки, включая уровни стоп-лосса и тейк-профита. В результате советник, разработанный в этой статье, может полностью сосредоточиться на исполнении сделок и управлении позициями, опираясь на индикатор при формировании сигналов и получении параметров риска.
После того как установлено, что индикатор должен предоставлять буферы, советник подключается к нему через функцию iCustom(), создающую хэндл, который позднее используется функцией CopyBuffer() для считывания значений сигнала.
Реализация советника на MQL5
В этом разделе создается советник, который считывает значения, формируемые предыдущим индикатором, и совершает сделки. Индикатор уже выполняет анализ, поэтому советник не повторяет логику обнаружения гэпов. Вместо этого он подключается к индикатору, считывает буферы сигналов, проверяет каждый сетап и открывает сделки только при выполнении всех правил исполнения.
Реализация выполняется в следующей последовательности:
- Сначала рассматриваются входные параметры и конфигурация.
- Затем определяются глобальные объекты и буферы для хранения данных.
- Далее добавляются вспомогательные функции и выполняется подключение к индикатору.
- Затем проверяется торговый сетап.
- Торговые сигналы обрабатываются в основном цикле исполнения.

Входные параметры и настройка
Реализация начинается с определения входных параметров. Эти параметры задают настраиваемые свойства советника и позволяют трейдеру адаптировать логику исполнения без изменения исходного кода.
Первая группа входных параметров управляет подключением к индикатору и основными настройками исполнения сделок. К ним относятся имя индикатора, размер позиции, допустимое проскальзывание и магическое число, используемое для идентификации позиций, открытых этим советником.
//+------------------------------------------------------------------+ //| Gap_Trading_EA.mq5 | //| Copyright 2026, Christian Benjamin. | //| https://www.mql5.com/ru/users/lynchris | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Christian Benjamin." #property link "https://www.mql5.com/ru/users/lynchris" #property version "1.0" #property strict #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input string InpIndicatorName = "WeekendGapMultiSignal"; input double InpLotSize = 1.0; input int InpSlippage = 30; input long InpMagicNumber = 20260609; input bool InpUseIndicatorSLTP = true; input bool InpRequireClosedBar = true; input bool InpAllowMultipleSameSignal = false; input bool InpCloseOppositePosition = false; //+------------------------------------------------------------------+ //| Midpoint stop-loss management | //+------------------------------------------------------------------+ input bool InpEnableMidpointSLMove = true; input int InpMidpointSLBufferPoints = 5;
Дополнительные входные параметры определяют, как должны обрабатываться сигналы. Трейдер может выбрать, использовать ли уровни стоп-лосса и тейк-профита, предоставленные индикатором, должны ли сигналы формироваться по закрытым свечам, разрешено ли несколько сигналов на одном баре и следует ли закрывать существующие позиции при появлении сигнала противоположного направления.
Последняя группа параметров предназначена для управления стоп-лоссом по срединному уровню. Эти настройки определяют, следует ли перемещать стоп-лоссы после достижения ценой срединного уровня между точкой входа и тейк-профитом, а также какой буфер применять при переносе стоп-лосса.
Размещение этих параметров в начале кода отделяет конфигурацию от логики и упрощает сопровождение и оптимизацию.
Глобальные объекты и управление состоянием
После определения входных параметров создаются глобальные объекты и переменные, используемые во всем советнике.
Самый важный объект – экземпляр класса CTrade, предоставляющий доступ к торговым функциям MetaTrader. Этот объект отвечает за отправку ордеров, модификацию позиций и закрытие сделок.
Затем определяется хэндл индикатора, который будет создан позднее с помощью iCustom(). Этот хэндл служит связующим звеном между советником и индикатором.
//+------------------------------------------------------------------+ //| Global objects and variables | //+------------------------------------------------------------------+ CTrade trade; int indHandle = INVALID_HANDLE; double buyArrowBuffer[]; double sellArrowBuffer[]; double buyTPBuffer[]; double buySLBuffer[]; double sellTPBuffer[]; double sellSLBuffer[]; datetime lastProcessedSignalBar = 0; datetime lastBarTime = 0; datetime readyBarTime = 0; bool indicatorReady = false;
Также объявляются несколько массивов для хранения значений буферов индикатора. В эти массивы записываются сформированные индикатором стрелки сигналов на покупку и продажу, а также уровни тейк-профита и стоп-лосса.
Раздел завершается небольшим набором переменных для отслеживания состояния. Эти переменные хранят информацию о последнем обработанном баре сигнала, последней завершенной свече, времени, когда индикатор стал доступен, и признаке завершения его инициализации.
Хотя эти переменные кажутся простыми, они играют критически важную роль на всех этапах реализации. Функции IsDuplicateSignalBar(), MarkSignalBar() и OnTick() используют их, чтобы предотвратить дублирование сделок и обеспечить обработку каждого сигнала только один раз.
Вспомогательные функции
Перед реализацией основной торговой логики создается набор вспомогательных функций, выполняющих повторяющиеся задачи по всему советнику. Эти функции инкапсулируют общие операции: считывание данных индикатора, проверку сигналов, отслеживание обработанных баров и управление существующими позициями. Выделение этих задач в отдельные вспомогательные функции делает основной поток исполнения понятнее и удобнее для сопровождения.
1. Обнаружение пустых значений буфера
Первая вспомогательная функция, IsEmptyBufferValue(), определяет, является ли значение, скопированное из буфера индикатора, действительным сигналом или пустым служебным значением. Поскольку буферы индикатора могут содержать специальные значения, указывающие на отсутствие данных, этот этап проверки обеспечивает обработку советником только значимых сигналов.
//+------------------------------------------------------------------+ //| Check whether a buffer value is empty | //+------------------------------------------------------------------+ bool IsEmptyBufferValue(const double value) { return (value == EMPTY_VALUE || value == 0.0 || value == DBL_MAX || value == -DBL_MAX); }
2. Копирование буферов индикатора
Далее CopyAllBuffers() выполняет обмен данными с пользовательским индикатором. Вместо многократного вызова CopyBuffer() во всех разделах советника весь процесс централизуется в одной вспомогательной функции. Это упорядочивает логику доступа к индикатору и обеспечивает последовательное копирование всех шести буферов.
//+------------------------------------------------------------------+ //| Copy indicator buffers | //+------------------------------------------------------------------+ bool CopyAllBuffers(const int shift,const int count) { if(CopyBuffer(indHandle,0,shift,count,buyArrowBuffer) != count) return false; if(CopyBuffer(indHandle,1,shift,count,sellArrowBuffer) != count) return false; if(CopyBuffer(indHandle,2,shift,count,buyTPBuffer) != count) return false; if(CopyBuffer(indHandle,3,shift,count,buySLBuffer) != count) return false; if(CopyBuffer(indHandle,4,shift,count,sellTPBuffer) != count) return false; if(CopyBuffer(indHandle,5,shift,count,sellSLBuffer) != count) return false; return true; }
3. Отслеживание позиций и сигналов
Советнику также требуется механизм отслеживания существующих позиций и предотвращения повторных входов. Функция HasOpenPositionByMagicSymbol() проверяет, существует ли уже сделка для комбинации текущего символа и магического числа. Это позволяет советнику применять правила управления позициями перед попыткой нового входа.
//+------------------------------------------------------------------+ //| Check for an existing position by symbol and magic number | //+------------------------------------------------------------------+ bool HasOpenPositionByMagicSymbol() { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(!PositionSelectByTicket(ticket)) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if((long)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue; return true; } return false; } //+------------------------------------------------------------------+ //| Check whether the signal bar has already been processed | //+------------------------------------------------------------------+ bool IsDuplicateSignalBar(const datetime signalBarTime) { return (signalBarTime == lastProcessedSignalBar); } //+------------------------------------------------------------------+ //| Store the most recently processed signal bar | //+------------------------------------------------------------------+ void MarkSignalBar(const datetime signalBarTime) { lastProcessedSignalBar = signalBarTime; }
Чтобы предотвратить многократное исполнение одного сигнала, реализуются функции IsDuplicateSignalBar() и MarkSignalBar(). Эти функции совместно отмечают последний обработанный бар сигнала и гарантируют, что каждый сигнал обрабатывается только один раз.
4. Управление позициями противоположного направления
Наконец, CloseOppositeIfNeeded() реализует опциональную логику разворота позиции. При включении через входные параметры функция перебирает все позиции советника и закрывает сделки, конфликтующие с направлением вновь сформированного сигнала.
//+------------------------------------------------------------------+ //| Close opposite positions when enabled | //+------------------------------------------------------------------+ void CloseOppositeIfNeeded(const bool isBuy) { if(!InpCloseOppositePosition) return; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(!PositionSelectByTicket(ticket)) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if((long)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue; ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); if((isBuy && type == POSITION_TYPE_SELL) || (!isBuy && type == POSITION_TYPE_BUY)) trade.PositionClose(ticket); } }
Это позволяет системе плавно переходить от длинной позиции к короткой и наоборот, не удерживая одновременно позиции противоположных направлений.
Проверка сделки и рисков
Прежде чем сигнал будет преобразован в рыночный ордер, советник должен проверить корректность параметров сделки.
Эту проверку выполняет функция StopsAreValidForTrade().
Когда индикатор предоставляет уровни стоп-лосса и тейк-профита, эта функция проверяет, корректно ли они расположены относительно предполагаемой цены входа. Например, позиция на покупку всегда должна иметь стоп-лосс ниже цены входа и тейк-профит выше нее.
//+------------------------------------------------------------------+ //| Validate and adjust stop-loss and take-profit levels | //+------------------------------------------------------------------+ bool StopsAreValidForTrade(const bool isBuy,const double entry,double &sl,double &tp) { if(!InpUseIndicatorSLTP) return true; if(sl <= 0.0 || tp <= 0.0) return false; double point = _Point; int stopsLevelPoints = (int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL); double minDistance = (double)stopsLevelPoints * point; if(isBuy) { if(tp <= entry) return false; if(sl >= entry) return false; if(minDistance > 0.0) { if((tp - entry) < minDistance) tp = entry + minDistance; if((entry - sl) < minDistance) sl = entry - minDistance; } } else { if(tp >= entry) return false; if(sl <= entry) return false; if(minDistance > 0.0) { if((entry - tp) < minDistance) tp = entry - minDistance; if((sl - entry) < minDistance) sl = entry + minDistance; } } return true; }
Функция также проверяет установленное брокером минимально допустимое расстояние до стопа. Если заданные уровни находятся слишком близко к текущей рыночной цене, они автоматически корректируются в соответствии с торговыми правилами брокера.
Выполнение этих проверок до размещения ордера позволяет советнику избежать ненужного отклонения ордеров и обеспечивает соответствие каждой сделки требованиям стратегии и торгового сервера.
Управление стоп-лоссом по срединному уровню
Теперь, когда мы рассмотрели механизм совершения сделок, перейдем к реализации компонента советника по управлению позициями.
Эта логика реализована в функции MoveSLToBreakevenOnMidpoint().
Цель этой процедуры – снизить риск после того, как сделка начинает двигаться в нужном направлении. Вместо ожидания достижения тейк-профита советник постоянно отслеживает открытые позиции и вычисляет середину расстояния между ценой входа и целевой ценой.
//+------------------------------------------------------------------+ //| Midpoint stop-loss management | //+------------------------------------------------------------------+ void MoveSLToBreakevenOnMidpoint() { if(!InpEnableMidpointSLMove) return; double point = _Point; double bufferPrice = InpMidpointSLBufferPoints * point; double epsilon = point / 2.0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(!PositionSelectByTicket(ticket)) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if((long)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue; ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); double entry = PositionGetDouble(POSITION_PRICE_OPEN); double tp = PositionGetDouble(POSITION_TP); double currentSL = PositionGetDouble(POSITION_SL); if(tp == 0.0) continue; double midpoint = 0.0; double currentPrice; double newSL = 0.0; if(type == POSITION_TYPE_BUY) { if(tp <= entry) continue; midpoint = entry + (tp - entry) / 2.0; currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(currentPrice < midpoint) continue; //--- Calculate new stop-loss above the entry price newSL = entry + bufferPrice; //--- Ensure broker minimum stop distance requirements are met double minStop = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * point; if((newSL - entry) < minStop) newSL = entry + minStop; //--- Prevent stop-loss from crossing the take-profit level if(newSL >= tp) newSL = tp - point; } else if(type == POSITION_TYPE_SELL) { if(entry <= tp) continue; midpoint = entry - (entry - tp) / 2.0; currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK); if(currentPrice > midpoint) continue; //--- Calculate new stop-loss below the entry price newSL = entry - bufferPrice; //--- Ensure broker minimum stop distance requirements are met double minStop = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * point; if((entry - newSL) < minStop) newSL = entry - minStop; //--- Prevent stop-loss from crossing the take-profit level if(newSL <= tp) newSL = tp + point; } else continue; //--- Skip if stop-loss already matches target level if(MathAbs(currentSL - newSL) <= epsilon) continue; if(trade.PositionModify(ticket, newSL, tp)) { Print("Midpoint reached for ", (type == POSITION_TYPE_BUY ? "BUY" : "SELL"), " #", ticket, " - SL moved from ", DoubleToString(currentSL, _Digits), " to ", DoubleToString(newSL, _Digits)); } else { Print("Failed to modify SL for #", ticket, ", error: ", GetLastError()); } } }
Когда цена достигает этого срединного уровня, функция автоматически переносит стоп-лосс за уровень безубытка на заданное буферное расстояние.
Для позиций на покупку стоп-лосс перемещается выше цены входа. Для позиций на продажу он перемещается ниже цены входа. Дополнительная проверка обеспечивает соответствие измененного стоп-лосса ограничениям брокера и не допускает его пересечения с уровнем тейк-профита.
Этот подход позволяет раньше зафиксировать прибыль, оставляя сделке пространство для продолжения движения к цели.
Инициализация советника
После подготовки всех вспомогательных компонентов начинается этап инициализации, реализованный в OnInit().
Первая задача OnInit() – настройка торговой среды. Советник задает для объекта CTrade выбранное магическое число и настройки проскальзывания.
Затем функция загружает пользовательский индикатор с помощью iCustom(). Если индикатор не найден или не загружен, инициализация немедленно завершается с ошибкой, поскольку советник полностью зависит от индикатора при формировании сигналов.
//+------------------------------------------------------------------+ //| Expert initialization | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(InpMagicNumber); trade.SetDeviationInPoints(InpSlippage); indHandle = iCustom(_Symbol, _Period, InpIndicatorName); if(indHandle == INVALID_HANDLE) { Print("Failed to load indicator '", InpIndicatorName, "'. Error: ", GetLastError()); return INIT_FAILED; } ArraySetAsSeries(buyArrowBuffer, true); ArraySetAsSeries(sellArrowBuffer, true); ArraySetAsSeries(buyTPBuffer, true); ArraySetAsSeries(buySLBuffer, true); ArraySetAsSeries(sellTPBuffer, true); ArraySetAsSeries(sellSLBuffer, true); Print("EA initialized. Waiting for indicator signals..."); return INIT_SUCCEEDED; }
После создания хэндла индикатора массивы буферов настраиваются как таймсерии. Это обеспечивает соответствие индексации буферов соглашению, используемому в ценовых сериях MetaTrader, где индекс 0 всегда соответствует самому новому бару.
Наконец, в журнал записывается подтверждающее сообщение о готовности советника к отслеживанию сигналов.
Этот этап устанавливает все внешние зависимости и подготавливает инфраструктуру для остальной части реализации.
Деинициализация советника
Очистка ресурсов выполняется в OnDeinit().
При удалении советника с графика или закрытии терминала хэндл индикатора, созданный при инициализации, освобождается с помощью IndicatorRelease().
//+------------------------------------------------------------------+ //| Expert deinitialization | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(indHandle != INVALID_HANDLE) IndicatorRelease(indHandle); Comment(""); }
Область комментариев на графике также очищается, чтобы удалить ранее отображенную советником информацию.
Хотя этот этап сравнительно невелик по сравнению с остальной реализацией, корректная очистка предотвращает утечки ресурсов и обеспечивает стабильность платформы при многократной установке и удалении советника с графика.
Основная обработка тиков
Основной механизм исполнения советника реализован в OnTick().
Каждый рыночный тик, получаемый терминалом, вызывает выполнение этой функции, которая координирует всю торговую активность.
OnTick() сначала проверяет готовность индикатора по выходным данным буферов. Когда данные становятся доступны, советник сохраняет время их получения и переходит в нормальный режим работы.
Затем функция определяет новый бар с помощью переменной отслеживания lastBarTime. Такая архитектура обеспечивает оценку сигналов советником только один раз на каждой закрытой свече, а не на каждом тике.
//+------------------------------------------------------------------+ //| Main tick | //+------------------------------------------------------------------+ void OnTick() { if(indHandle == INVALID_HANDLE) return; if(!indicatorReady) { double testBuffer[1]; if(CopyBuffer(indHandle, 0, 0, 1, testBuffer) == 1) { indicatorReady = true; readyBarTime = iTime(_Symbol, _Period, 1); Print("Indicator ready. Monitoring WeekendGapMultiSignal..."); } else return; } datetime currentBarTime = iTime(_Symbol, _Period, 0); if(currentBarTime == 0) return; if(currentBarTime == lastBarTime) return; lastBarTime = currentBarTime; const int signalShift = (InpRequireClosedBar ? 1 : 0); datetime signalBarTime = iTime(_Symbol, _Period, signalShift); if(signalBarTime == 0) return; if(signalBarTime <= readyBarTime) return; if(!CopyAllBuffers(signalShift, 1)) return; bool buySignal = !IsEmptyBufferValue(buyArrowBuffer[0]); bool sellSignal = !IsEmptyBufferValue(sellArrowBuffer[0]); if(!buySignal && !sellSignal) return; if(!InpAllowMultipleSameSignal && IsDuplicateSignalBar(signalBarTime)) return; if(HasOpenPositionByMagicSymbol()) return; bool isBuy = buySignal && !sellSignal; if(buySignal && sellSignal) { Print("Both buy and sell signals detected on the same bar. Skipping to avoid conflict."); return; } CloseOppositeIfNeeded(isBuy); double entryPrice = isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = 0.0; double tp = 0.0; if(InpUseIndicatorSLTP) { if(isBuy) { tp = buyTPBuffer[0]; sl = buySLBuffer[0]; } else { tp = sellTPBuffer[0]; sl = sellSLBuffer[0]; } if(IsEmptyBufferValue(tp)) tp = 0.0; if(IsEmptyBufferValue(sl)) sl = 0.0; } if(!StopsAreValidForTrade(isBuy, entryPrice, sl, tp)) { Print("Signal ignored because SL/TP are invalid for the current entry price. ", "Entry=", DoubleToString(entryPrice, _Digits), " SL=", DoubleToString(sl, _Digits), " TP=", DoubleToString(tp, _Digits), " SignalBar=", TimeToString(signalBarTime, TIME_DATE | TIME_MINUTES)); MarkSignalBar(signalBarTime); return; } string comment = isBuy ? "Weekend Gap Buy" : "Weekend Gap Sell"; bool success = false; if(isBuy) success = trade.Buy(InpLotSize, _Symbol, entryPrice, sl, tp, comment); else success = trade.Sell(InpLotSize, _Symbol, entryPrice, sl, tp, comment); if(success) { MarkSignalBar(signalBarTime); Print((isBuy ? "BUY" : "SELL"), " | Entry: ", DoubleToString(entryPrice, _Digits), " | SL: ", DoubleToString(sl, _Digits), " | TP: ", DoubleToString(tp, _Digits), " | Signal bar: ", TimeToString(signalBarTime, TIME_DATE | TIME_MINUTES)); } else { Print("Order failed. Error: ", GetLastError()); } //--- Midpoint stop-loss management MoveSLToBreakevenOnMidpoint(); }
После подтверждения формирования нового бара советник определяет свечу для оценки на основе настроек пользователя для подтверждения сигнала. Когда включено подтверждение закрытым баром, советник оценивает последний завершенный бар вместо формирующегося.
Затем выбранный бар сигнала сопоставляется со временем готовности индикатора и данными об уже обработанных сигналах, чтобы учитывались только новые сигналы.
На этом этапе OnTick() вызывает CopyAllBuffers(), чтобы получить последние выходные данные индикатора и определить наличие сигнала на покупку или продажу.
Этот этап фактически служит центром управления всего советника, координируя переход от рыночных данных к исполнению ордеров.
Обработка сигналов и исполнение сделок
После обнаружения в OnTick() действительного сигнала советник переходит к этапу исполнения.
Первый шаг – определение направления сигнала. Советник анализирует предоставленные индикатором буферы стрелок покупки и продажи и определяет, соответствует ли текущий сетап возможности открыть сделку на покупку или продажу.
Затем применяются дополнительные фильтры. Дублирующие сигналы отклоняются с помощью IsDuplicateSignalBar(), существующие позиции проверяются через HasOpenPositionByMagicSymbol(), а при необходимости позиции противоположного направления обрабатываются с помощью CloseOppositeIfNeeded().
bool buySignal = !IsEmptyBufferValue(buyArrowBuffer[0]); bool sellSignal = !IsEmptyBufferValue(sellArrowBuffer[0]); if(!buySignal && !sellSignal) return; if(!InpAllowMultipleSameSignal && IsDuplicateSignalBar(signalBarTime)) return; if(HasOpenPositionByMagicSymbol()) return; bool isBuy = buySignal && !sellSignal; if(buySignal && sellSignal) { Print("Both buy and sell signals detected on the same bar. Skipping to avoid conflict."); return; } CloseOppositeIfNeeded(isBuy); double entryPrice = isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = 0.0; double tp = 0.0; if(InpUseIndicatorSLTP) { if(isBuy) { tp = buyTPBuffer[0]; sl = buySLBuffer[0]; } else { tp = sellTPBuffer[0]; sl = sellSLBuffer[0]; } if(IsEmptyBufferValue(tp)) tp = 0.0; if(IsEmptyBufferValue(sl)) sl = 0.0; } if(!StopsAreValidForTrade(isBuy, entryPrice, sl, tp)) { Print("Signal ignored because SL/TP are invalid for the current entry price. ", "Entry=", DoubleToString(entryPrice, _Digits), " SL=", DoubleToString(sl, _Digits), " TP=", DoubleToString(tp, _Digits), " SignalBar=", TimeToString(signalBarTime, TIME_DATE | TIME_MINUTES)); MarkSignalBar(signalBarTime); return; } string comment = isBuy ? "Weekend Gap Buy" : "Weekend Gap Sell"; bool success = false; if(isBuy) success = trade.Buy(InpLotSize, _Symbol, entryPrice, sl, tp, comment); else success = trade.Sell(InpLotSize, _Symbol, entryPrice, sl, tp, comment); if(success) { MarkSignalBar(signalBarTime); Print((isBuy ? "BUY" : "SELL"), " | Entry: ", DoubleToString(entryPrice, _Digits), " | SL: ", DoubleToString(sl, _Digits), " | TP: ", DoubleToString(tp, _Digits), " | Signal bar: ", TimeToString(signalBarTime, TIME_DATE | TIME_MINUTES)); } else { Print("Order failed. Error: ", GetLastError()); }
Затем советник определяет цену входа в рынок и получает значения стоп-лосса и тейк-профита из буферов индикатора при включенном управлении рисками на основе индикатора.
Перед размещением сделки предлагаемые уровни передаются функции StopsAreValidForTrade(). Советник приступает к размещению ордера только после успешного прохождения всех проверок.
В зависимости от направления сигнала советник исполняет ордер на покупку через trade.Buy() либо ордер на продажу через trade.Sell().
После успешного исполнения ордера бар сигнала фиксируется с помощью MarkSignalBar(), чтобы предотвратить повторную обработку, а подробная информация о сделке записывается в журнал терминала.
На этом этапе выходные данные индикатора окончательно преобразуются в открытую рыночную позицию.
Управление позициями во время работы
Реализация завершается непрерывным управлением позициями.
В конце каждого цикла исполнения OnTick() вызывает MoveSLToBreakevenOnMidpoint().
Благодаря этому каждая открытая позиция непрерывно проверяется на соответствие ранее определенным правилам защиты по срединному уровню. По мере изменения рыночных цен советник автоматически корректирует уровни стоп-лосса при выполнении необходимых условий.
Интеграция этого этапа управления в основной цикл выполнения позволяет советнику активно контролировать риски еще долго после открытия сделки.
Тестирование на исторических данных и проверка
После интеграции советника с пользовательским индикатором система может считывать буферы индикатора, обнаруживать сигналы на покупку и продажу, получать соответствующие уровни стоп-лосса и тейк-профита и автоматически исполнять сделки. Это подтверждает, что связь между индикатором и советником работает корректно, а торговые решения могут преобразовываться в рыночные ордера без ручного вмешательства.

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

Анализ результатов тестирования
Тестирование на исторических данных дало чистую прибыль 166,16 при начальном депозите 10 000 долларов. Максимальная просадка средств счета оставалась сравнительно низкой – 0,79%, что указывает на контролируемый уровень риска в течение всего периода тестирования.
Всего было исполнено 23 сделки: 13 прибыльных и 10 убыточных. Доля прибыльных сделок составила 56,52%. Хотя доля прибыльных сделок лишь немного превышала 50%, стратегия оставалась прибыльной, поскольку средняя прибыль по прибыльной сделке значительно превышала средний убыток по убыточной сделке.
Профит-фактор 2,45 указывает, что валовая прибыль по прибыльным позициям более чем в два раза превышала валовой убыток по убыточным позициям. На практике это указывает на то, что стратегия оказалась способной эффективно компенсировать убытки, сохраняя общую прибыльность.
Коэффициент восстановления 2,09 дополнительно демонстрирует способность системы генерировать доходность относительно просадки, достигнутой в период тестирования. В сочетании с низкой просадкой это указывает на то, что правила управления рисками, реализованные в советнике, включая уровни стоп-лосса, получаемые от индикатора, и механизм корректировки стоп-лосса по срединному уровню, способствовали сохранению капитала.
В целом тестирование на исторических данных подтверждает, что советник успешно обрабатывает сигналы индикатора, применяет переданные уровни стоп-лосса и тейк-профита и управляет открытыми позициями в соответствии с разработанной торговой логикой. Предварительные результаты указывают на то, что взаимодействие индикатора и советника работает согласно замыслу.
Заключение
В этой статье индикатор гэпов выходного дня преобразован в советник, способный считывать выходные данные из буферов, автоматически исполнять ордера и применять сформированные индикатором уровни стоп-лосса и тейк-профита.
Также добавлены фильтрация сигналов, контроль позиций и управление стоп-лоссом по срединному уровню, чтобы советник мог вести сделки более последовательно и дисциплинированно. Тестирование на исторических данных подтвердило, что взаимодействие индикатора и советника корректно работает на всех этапах: от обнаружения сигнала до исполнения ордеров и управления сделками.
В совокупности эти компоненты показывают, как пользовательский индикатор можно преобразовать в полноценную автоматизированную торговую систему на MQL5, отделив анализ от исполнения и сохранив связь между ними через буферы индикатора.
Приложения
| Файл | Описание |
|---|---|
| WeekendGapMultiSignal.mq5 | Пользовательский индикатор, который обнаруживает гэпы выходного дня, формирует торговые сигналы на покупку и продажу и предоставляет соответствующие уровни стоп-лосса и тейк-профита через буферы индикатора. |
| Gap_Trading_EA.mq5 | Советник, который считывает сигналы и параметры риска из индикатора WeekendGapMultiSignal, автоматически совершает сделки и управляет открытыми позициями с помощью защиты стоп-лоссом по срединному уровню. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23015
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Пайплайны Codex: от Python до MQL5 для выбора индикаторов — анализ ETF XLF по нескольким кварталам с применением машинного обучения
Возможности Мастера MQL5, которые вам нужно знать (Часть 97): Использование выпуклой оболочки и миниатюрной GRU-сети в пользовательском классе трейлинг-стопа
От начального до среднего уровня: Перегрузка операторов (IV)
Разработка инструментария для анализа Price Action (Часть 73): Построение системы торговых сигналов для гэпов выходного дня в MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования