Создание профессиональной торговой системы на базе Heikin Ashi (Часть 2): Разработка советника
Введение
Эта статья — вторая часть серии «Создание профессиональной торговой системы на базе Heikin Ashi». В первой части мы создали пользовательский индикатор Heikin Ashi на языке MetaQuotes Language 5 (MQL5), следуя лучшим практикам разработки пользовательских индикаторов. В этой части мы переходим на следующий этап и разрабатываем советник Zen Breakout, который использует наш пользовательский индикатор Heikin Ashi и стандартный индикатор MetaTrader 5 Fractals для генерации надежных сигналов пробоя.
Идея проста:
- Когда сильная бычья свеча Heikin Ashi закрывается выше недавнего локального максимума (определенного индикатором Fractals), советник открывает длинную позицию.
- Когда сильная медвежья свеча Heikin Ashi закрывается ниже недавнего локального минимума, советник открывает короткую позицию.
Каждая сделка, открытая нашим советником, имеет четко заданные уровни Stop Loss и Take Profit с настраиваемым соотношением риска к прибыли. После прочтения этой статьи вы поймете, как:
- Подключить к советнику пользовательский и встроенный индикатор.
- Реализовать логику входа на пробой с использованием Heikin Ashi и фракталов.
- Применять гибкий расчет размера позиции (вручную или на основе процента риска от счета).
- Упаковать советник вместе с его индикатором в один файл для удобного распространения.
Концепция стратегии
Стратегия Zen Breakout сочетает обнаружение импульса с подтверждением пробоя.
- Сценарий для покупки
Сценарий для покупки формируется, когда сильная бычья свеча Heikin Ashi закрывается выше последнего локального максимума.

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

- Размещение Stop Loss
Для длинных позиций Stop Loss размещается на минимуме пробойной свечи.

Для коротких позиций Stop Loss размещается на максимуме пробойной свечи.

- Размещение Take Profit
Take Profit определяется с помощью настраиваемого соотношения риска к прибыли. Например, если риск на сделку составляет 100 пунктов, а соотношение риска к прибыли равно 1:2, Take Profit будет находиться на расстоянии 200 пунктов от входа.
Подготовка советника
Для генерации торговых сигналов Zen Breakout будет считывать данные напрямую из двух индикаторов:
- Индикатор Fractals
Индикатор Fractals предустановлен в терминале MetaTrader 5 и широко используется для определения недавних локальных максимумов и минимумов рынка. В нашем советнике мы используем функцию MQL5 iFractals() для инициализации индикатора и получения его хэндла для дальнейшего использования в коде.
- Пользовательский индикатор Heikin Ashi
Мы будем использовать пользовательский индикатор Heikin Ashi, который создали в первой части серии «Создание профессиональной торговой системы на базе Heikin Ashi», для обнаружения пробоев с сильным импульсом. Чтобы обращаться к нему программно, мы используем функцию MQL5 iCustom() для инициализации и получения его хэндла. Кроме того, мы упакуем индикатор как ресурс внутри советника, чтобы его можно было распространять в виде одного самодостаточного файла.
Чтобы сделать советник Zen Breakout более гибким, мы добавим следующие настраиваемые входные параметры.
- magicNumber
Магический номер — это уникальный идентификатор, который советник назначает каждой открываемой им сделке. Он позволяет советнику отличать свои сделки от сделок, открытых вручную или другими советниками, гарантируя, что он изменяет или закрывает только собственные позиции.
- timeFrame
Этот параметр задает таймфрейм графика, на котором должен работать советник. Пользователи могут выбрать один из 21 доступного таймфрейма в MetaTrader 5 — от M1 (1 минута) до MN1 (месяц).
- lotSizeMode
Определяет, как советник рассчитывает размер лота для новых позиций:
- Manual — пользователь задает фиксированный размер лота в параметре "lotSize".
- Auto — советник динамически рассчитывает размер лота на основе баланса счета и параметра "riskPerTradePercent".
- riskPerTradePercent
Задает процент баланса счета, которым следует рисковать в одной сделке (используется только когда параметр "lotSizeMode" установлен в автоматический режим). Например, если баланс счета составляет $10000, а параметр равен 1.0, советник рассчитает размер позиции так, чтобы срабатывание Stop Loss приводило к убытку $100 (1% от $10000).
- lotSize
Задает фиксированный размер лота для всех новых сделок (используется только когда параметр "lotSizeMode" установлен в ручной режим). Например, если параметр "lotSize" равен 0.5, каждая новая позиция будет открываться объемом 0.5 лота.
- RRr (соотношение риска к прибыли)
Определяет соотношение риска к прибыли для каждой сделки. Пользователи могут выбрать один из семи предустановленных вариантов, чтобы потенциальная прибыль превышала потенциальный убыток при достижении уровня Take Profit.
Пошаговое руководство по написанию советника
В этой статье предполагается, что вы уже знакомы с базовыми концепциями программирования и имеете уверенный опыт работы с языком MQL5 в MetaTrader 5 и MetaEditor. Мы не будем рассматривать эти темы, поэтому сразу начнем писать советник. Подготовьте пустой исходный файл в MetaEditor. Мы готовы приступить к кодированию. Начнем с начального шаблонного кода. Мы будем использовать его как основу для дальнейшей разработки советника.
//+------------------------------------------------------------------+ //| zenBreakout.mq5 | //| Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian | //| https://www.mql5.com/en/users/chachaian | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/en/users/chachaian" #property version "1.10" //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ } //--- Utility functions //+------------------------------------------------------------------+
Следующий шаг — добавить пользовательские функции для нашего советника. Добавьте следующие функции сразу под функцией OnTick(). По мере разработки советника мы будем вызывать эти функции одну за другой из нашего исходного кода.
... //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ } //--- Utility functions //+------------------------------------------------------------------+ //| This function configures the chart's appearance. | | //+------------------------------------------------------------------+ bool ConfigureChartAppearance() { if(!ChartSetInteger(0, CHART_COLOR_BACKGROUND, clrWhite)){ Print("Error while setting chart background, ", GetLastError()); return false; } if(!ChartSetInteger(0, CHART_SHOW_GRID, false)){ Print("Error while setting chart grid, ", GetLastError()); return false; } if(!ChartSetInteger(0, CHART_MODE, CHART_LINE)){ Print("Error while setting chart mode, ", GetLastError()); return false; } if(!ChartSetInteger(0, CHART_COLOR_FOREGROUND, clrBlack)){ Print("Error while setting chart foreground, ", GetLastError()); return false; } return true; } //+-------------------------------------------------------------------------+ //| Function to generate a unique graphical object name with a given prefix | | //+-------------------------------------------------------------------------+ string GenerateUniqueName(string prefix){ int attempt = 0; string uniqueName; while(true) { uniqueName = prefix + IntegerToString(MathRand() + attempt); if(ObjectFind(0, uniqueName) < 0) break; attempt++; } return uniqueName; } //+-------------------------------------------------------------------------+ //| Returns true if Heikin Ashi candle is bullish and has no lower wick | | //+-------------------------------------------------------------------------+ bool IsBullishBreakoutCandle(int index) { if(index < 0 || index >= ArraySize(heikinAshiOpen)) return false; double open = heikinAshiOpen[index]; double close = heikinAshiClose[index]; double low = heikinAshiLow[index]; //--- Candle must be bullish and have no lower wick return (close > open && low >= MathMin(open, close)); } //+-------------------------------------------------------------------------+ //| Returns true if Heikin Ashi candle is bearish and has no upper wick | | //+-------------------------------------------------------------------------+ bool IsBearishBreakoutCandle(int index) { if(index < 0 || index >= ArraySize(heikinAshiOpen)) return false; double open = heikinAshiOpen[index]; double close = heikinAshiClose[index]; double high = heikinAshiHigh[index]; //--- Candle must be bearish and have no upper wick return (close < open && high <= MathMax(open, close)); } //+----------------------------------------------------------------------------------------------+ //| Returns the index of the most recent swing high before 'fromIndex'. Returns -1 if not found | | //+----------------------------------------------------------------------------------------------+ int FindMostRecentSwingHighIndex(int fromIndex) { if(fromIndex <= 0 || fromIndex >= ArraySize(swingHighs)) fromIndex = 1; for(int i = fromIndex; i < ArraySize(swingHighs); i++) { if(swingHighs[i] != EMPTY_VALUE) return i; } return -1; //--- No swing high found } //+----------------------------------------------------------------------------------------------+ //| Returns the index of the most recent swing low before 'fromIndex'. Returns -1 if not found | | //+----------------------------------------------------------------------------------------------+ int FindMostRecentSwingLowIndex(int fromIndex) { if(fromIndex <= 0 || fromIndex >= ArraySize(swingLows)) fromIndex = 1; for(int i = fromIndex; i < ArraySize(swingLows); i++) { if(swingLows[i] != EMPTY_VALUE) return i; } return -1; // No swing low found } //+------------------------------------------------------------------+ //| This function detects a bullish signal | //+------------------------------------------------------------------+ bool IsBullishSignal(datetime &timeStart, int &indexStart, datetime &timeEnd, int &indexEnd) { indexStart = FindMostRecentSwingHighIndex(1); double recentSwingHigh = iHigh(_Symbol, timeframe, indexStart); double previousHeikinAshiCandleClose = heikinAshiClose[1]; double previousHeikinAshiCandleOpen = heikinAshiOpen[1]; if(IsBullishBreakoutCandle(1)){ if(previousHeikinAshiCandleClose > recentSwingHigh && previousHeikinAshiCandleOpen < recentSwingHigh){ timeStart = iTime(_Symbol, timeframe, indexStart); indexEnd = 0; timeEnd = iTime(_Symbol, timeframe, indexEnd); return true; } } return false; } //+------------------------------------------------------------------+ //| This function detects a bearish signal | //+------------------------------------------------------------------+ bool IsBearishSignal(datetime &timeStart, int &indexStart, datetime &timeEnd, int &indexEnd) { indexStart = FindMostRecentSwingLowIndex(1); double recentSwingLow = iLow(_Symbol, timeframe, indexStart); double previousHeikinAshiCandleClose = heikinAshiClose[1]; double previousHeikinAshiCandleOpen = heikinAshiOpen[1]; if(IsBearishBreakoutCandle(1)){ if(previousHeikinAshiCandleClose < recentSwingLow && previousHeikinAshiCandleOpen > recentSwingLow){ timeStart = iTime(_Symbol, timeframe, indexStart); indexEnd = 0; timeEnd = iTime(_Symbol, timeframe, indexEnd); return true; } } return false; } //+-------------------------------------------------------------------+ //| Function to check if there's a new bar on a given chart timeframe | | //+-------------------------------------------------------------------+ bool IsNewBar(string symbol, ENUM_TIMEFRAMES tf, datetime &lastTm) { datetime currentTime = iTime(symbol, tf, 0); if(currentTime != lastTm){ lastTm = currentTime; return true; } return false; } //+------------------------------------------------------------------+ //| To check if there is an active buy position opened by this EA | | //+------------------------------------------------------------------+ bool IsThereAnActiveBuyPosition(ulong magicNm){ for(int i = PositionsTotal() - 1; i >= 0; i--){ ulong ticket = PositionGetTicket(i); if(ticket == 0){ Print("Error while fetching position ticket ", _LastError); continue; }else{ if(PositionGetInteger(POSITION_MAGIC) == magicNm && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){ return true; } } } return false; } //+------------------------------------------------------------------+ //| To check if there is an active sell position opened by this EA | | //+------------------------------------------------------------------+ bool IsThereAnActiveSellPosition(ulong mgcNumber){ for(int i = PositionsTotal() - 1; i >= 0; i--){ ulong ticket = PositionGetTicket(i); if(ticket == 0){ Print("Error while fetching position ticket ", _LastError); continue; }else{ if(PositionGetInteger(POSITION_MAGIC) == mgcNumber && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){ return true; } } } return false; } //+------------------------------------------------------------------+ //| To open a buy position | //+------------------------------------------------------------------+ bool OpenBuy(){ double rewardValue = 1.0; switch(RRr){ case ONE_TO_ONE: rewardValue = 1.0; break; case ONE_TO_ONEandHALF: rewardValue = 1.5; break; case ONE_TO_TWO: rewardValue = 2.0; break; case ONE_TO_THREE: rewardValue = 3.0; break; case ONE_TO_FOUR: rewardValue = 4.0; break; case ONE_TO_FIVE: rewardValue = 5.0; break; case ONE_TO_SIX: rewardValue = 6.0; break; default: rewardValue = 1.0; break; } ENUM_POSITION_TYPE positionType = POSITION_TYPE_BUY; ENUM_ORDER_TYPE action = ORDER_TYPE_BUY; double stopLevel = iLow(_Symbol, timeframe, 1); double askPrice = AppData.askPrice; double bidPrice = AppData.bidPrice; double stopDistance = askPrice - stopLevel; double targetLevel = askPrice + (stopDistance * rewardValue); double lotSz = AppData.amountAtRisk / (AppData.contractSize * stopDistance); if(lotSizeMode == MODE_AUTO){ lotSz = NormalizeDouble(lotSz, 2); }else{ lotSz = NormalizeDouble(lotSize, 2); } if(!Trade.Buy(lotSz, _Symbol, askPrice, stopLevel, targetLevel)){ Print("Error while opening a long position, ", GetLastError()); Print(Trade.ResultRetcode()); Print(Trade.ResultComment()); return false; }else{ MqlTradeResult result = {}; Trade.Result(result); AppData.tradeInfo.orderTicket = result.order; AppData.tradeInfo.type = action; AppData.tradeInfo.posType = positionType; AppData.tradeInfo.entryPrice = result.price; AppData.tradeInfo.takeProfitLevel = targetLevel; AppData.tradeInfo.stopLossLevel = stopLevel; AppData.tradeInfo.openTime = AppData.currentGmtTime; AppData.tradeInfo.lotSize = lotSz; return true; } return false; } //+------------------------------------------------------------------+ //| To open a sell position | //+------------------------------------------------------------------+ bool OpenSel(){ double rewardValue = 1.0; switch(RRr){ case ONE_TO_ONE: rewardValue = 1.0; break; case ONE_TO_ONEandHALF: rewardValue = 1.5; break; case ONE_TO_TWO: rewardValue = 2.0; break; case ONE_TO_THREE: rewardValue = 3.0; break; case ONE_TO_FOUR: rewardValue = 4.0; break; case ONE_TO_FIVE: rewardValue = 5.0; break; case ONE_TO_SIX: rewardValue = 6.0; break; default: rewardValue = 1.0; break; } ENUM_POSITION_TYPE positionType = POSITION_TYPE_SELL; ENUM_ORDER_TYPE action = ORDER_TYPE_SELL; double stopLevel = iHigh(_Symbol, timeframe, 1); double bidPrice = AppData.bidPrice; double askPrice = AppData.askPrice; double stopDistance = stopLevel - bidPrice; double targetLevel = bidPrice - (stopDistance * rewardValue); double lotSz = AppData.amountAtRisk / (AppData.contractSize * stopDistance); if(lotSizeMode == MODE_AUTO){ lotSz = NormalizeDouble(lotSz, 2); }else{ lotSz = NormalizeDouble(lotSize, 2); } if(!Trade.Sell(lotSz, _Symbol, bidPrice, stopLevel, targetLevel)){ Print("Error while opening a short position, ", GetLastError()); Print(Trade.ResultRetcode()); Print(Trade.ResultComment()); return false; }else{ MqlTradeResult result = {}; Trade.Result(result); AppData.tradeInfo.orderTicket = result.order; AppData.tradeInfo.type = action; AppData.tradeInfo.posType = positionType; AppData.tradeInfo.entryPrice = result.price; AppData.tradeInfo.takeProfitLevel = targetLevel; AppData.tradeInfo.stopLossLevel = stopLevel; AppData.tradeInfo.openTime = AppData.currentGmtTime; AppData.tradeInfo.lotSize = lotSz; return true; } return false; } //+------------------------------------------------------------------+
Если вы попробуете скомпилировать советник сейчас, то увидите ряд ошибок на этапе компиляции. Это происходит потому, что многие добавленные функции ссылаются на переменные, которые еще не определены. Позже мы добавим эти переменные в глобальную область видимости. А пока давайте разберем функциональность каждой функции и объясним, что она делает.
- ConfigureChartAppearance
Эта функция настраивает внешний вид графика перед запуском советника. Она обеспечивает чистый минималистичный вид: меняет фон графика на белый, скрывает сетку, чтобы избежать визуального шума, переводит график в линейный режим и задает черный цвет основных элементов графика для хорошей контрастности.
- GenerateUniqueName
Она используется для генерации уникального имени для каждого вновь создаваемого графического объекта на графике, где работает наш советник. Это гарантирует, что каждый объект, нарисованный советником, имеет уникальный идентификатор, предотвращая случайную перезапись ранее созданных объектов. Функция принимает строку на вход, применяет к ней алгоритм и генерирует уникальный идентификатор объекта.
- IsBullishBreakoutCandle
Эта функция проверяет, соответствует ли свеча Heikin Ashi, заданная своим индексом, критериям бычьего пробоя, проверяя следующие конкретные условия.
- Цена закрытия свечи должна быть больше цены ее открытия.
- У свечи не должно быть нижней тени.
Если оба условия выполнены, функция возвращает true, указывая, что свеча Heikin Ashi соответствует критериям бычьей пробойной свечи.
- IsBearishBreakoutCandle
Эта функция проверяет, соответствует ли свеча Heikin Ashi, заданная своим индексом, критериям медвежьего пробоя.
- FindMostRecentSwingHighIndex
Основная задача этой функции — найти индекс последнего локального максимума. Для этого она сканирует массив значений верхних фракталов, которые берутся напрямую из буфера номер ноль индикатора Fractals. Функция специально ищет последний локальный максимум, который появился до заданного индекса, переданного на вход.
- FindMostRecentSwingLowIndex
Назначение этой функции — найти индекс последнего локального минимума.
- IsBullishSignal
Эта функция проверяет, сформировался ли подтвержденный бычий сигнал пробоя. Сначала она находит последний локальный максимум и получает его цену. Затем получает значения открытия и закрытия предыдущей свечи Heikin Ashi. Если последняя свеча бычья, без нижней тени, а ее закрытие находится выше локального максимума, тогда как открытие — ниже него, функция записывает время начала и окончания для справки и возвращает true. В противном случае она возвращает false.
- IsBearishSignal
Эта функция проверяет, сформировался ли подтвержденный медвежий сигнал пробоя.
- IsNewBar
Эта функция проверяет, сформировался ли новый бар на указанном символе и таймфрейме. Она сравнивает время открытия текущего бара с ранее сохраненным временем. Если они различаются, функция обновляет сохраненное время и возвращает true; иначе возвращает false.
- IsThereAnActiveBuyPosition
Эта функция проверяет, есть ли активная длинная позиция, открытая именно этим советником. На вход она принимает магический номер — уникальный идентификатор, назначаемый сделкам советника. Функция перебирает все открытые позиции, и если находит позицию на покупку, магический номер которой совпадает с переданным, возвращает true; иначе возвращает false.
- IsThereAnActiveSellPosition
Эта функция проверяет, есть ли активная короткая позиция, открытая именно этим советником.
- OpenBuy
Эта функция отвечает за открытие позиции на покупку в соответствии с настройками соотношения риска к прибыли и правилами управления рисками советника. Она начинает с выбора множителя прибыли на основе заданного пользователем соотношения риска к прибыли (1:1, 1:1.5, 1:2 и т. д.). Затем рассчитывает уровень Stop Loss на минимуме предыдущей свечи и измеряет расстояние до стопа от текущей цены Ask. Используя это расстояние, она вычисляет уровень Take Profit, умножая расстояние на выбранное соотношение прибыли, чтобы сделка соответствовала заданному профилю риска к прибыли.
Далее функция определяет размер лота. Если режим размера лота установлен в автоматический, она рассчитывает размер лота на основе суммы риска, размера контракта и расстояния до стопа, а затем нормализует его до двух знаков после запятой. Если выбран ручной режим, вместо этого используется заданный пользователем размер лота.
Наконец, функция пытается отправить ордер на покупку с рассчитанными параметрами. Если сделка успешно размещена, она сохраняет подробную информацию об ордере (номер тикета, тип, цену входа, Stop Loss, Take Profit, размер лота и время) в структурированной переменной AppData.tradeInfo для последующего использования. Если отправка ордера не удалась, функция выводит подробные сообщения об ошибках для облегчения отладки и возвращает false.
Эта функция фактически объединяет управление рисками советника, расчет прибыли и исполнение сделки в единый хорошо структурированный процесс, что делает ее одним из ключевых компонентов советника Zen Breakout.
- OpenSel
Эта функция работает так же, как функция 'OpenBuy', только открывает короткую позицию вместо длинной.
Теперь, когда структура советника (EA) подготовлена, следующий шаг — определить его входные параметры. Это можно сделать, объявив их сразу под директивами #property в самой верхней части программного файла. Просто добавьте в это место следующий блок кода:
... #property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/en/users/chachaian" #property version "1.10" //--- Input parameters input group "Information" input ulong magicNumber = 254700680002; input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT; input group "Risk Management" input ENUM_LOT_SIZE_INPUT_MODE lotSizeMode = MODE_AUTO; input double riskPerTradePercent = 1.0; input double lotSize = 0.1; input ENUM_RISK_REWARD_RATIO RRr = ONE_TO_ONEandHALF; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ return(INIT_SUCCEEDED); } ...
Поскольку мы уже определили и объяснили эти параметры в разделе подготовки советника, не будем повторять их описания здесь. Далее мы создадим пользовательские перечисления для некоторых входных параметров. Перечисления предоставляют пользователям выпадающий список предопределенных вариантов при настройке советника. Мы определим пользовательские перечисления сразу под директивами #property и над входными параметрами.
... #property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/en/users/chachaian" #property version "1.10" //--- Custom enumerations enum ENUM_RISK_REWARD_RATIO { ONE_TO_ONE, ONE_TO_ONEandHALF, ONE_TO_TWO, ONE_TO_THREE, ONE_TO_FOUR, ONE_TO_FIVE, ONE_TO_SIX }; enum ENUM_LOT_SIZE_INPUT_MODE { MODE_MANUAL, MODE_AUTO }; //--- Input parameters input group "Information" input ulong magicNumber = 254700680002; input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT; ...
Наш код включает два пользовательских перечисления, которые делают настройку советника более интуитивной:
- ENUM_RISK_REWARD_RATIO
Это перечисление определяет семь предустановленных вариантов соотношения риска к прибыли (от 1:1 до 1:6). Оно позволяет трейдерам просто выбрать нужное соотношение из выпадающего списка, не вводя значения вручную.
- ENUM_LOT_SIZE_INPUT_MODE
Определяет, как советник рассчитывает размер лота. MODE_MANUAL позволяет пользователю задать фиксированный размер лота, а MODE_AUTO рассчитывает размер лота динамически на основе баланса счета и процента риска.
Далее мы определяем макрос с именем zenBreakout, который хранит имя советника в виде строки. Позже этот макрос используется в нашей пользовательской функции GenerateUniqueName() для создания уникальных имен новых графических объектов. Теперь разместим определение макроса сразу под существующими директивами #property.
... #property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/en/users/chachaian" #property version "1.10" //--- Macros #define zenBreakout "zenBreakout" //--- Custom enumerations enum ENUM_RISK_REWARD_RATIO { ONE_TO_ONE, ONE_TO_ONEandHALF, ONE_TO_TWO, ONE_TO_THREE, ONE_TO_FOUR, ONE_TO_FIVE, ONE_TO_SIX }; enum ENUM_LOT_SIZE_INPUT_MODE { MODE_MANUAL, MODE_AUTO }; ...
Далее мы подключаем необходимые библиотеки сразу под определением пользовательского перечисления.
... //--- Custom enumerations enum ENUM_RISK_REWARD_RATIO { ONE_TO_ONE, ONE_TO_ONEandHALF, ONE_TO_TWO, ONE_TO_THREE, ONE_TO_FOUR, ONE_TO_FIVE, ONE_TO_SIX }; enum ENUM_LOT_SIZE_INPUT_MODE { MODE_MANUAL, MODE_AUTO }; //--- Libraries #include <Trade\Trade.mqh> #include <ChartObjects\ChartObjectsLines.mqh> //--- Input parameters input group "Information" input ulong magicNumber = 254700680002; input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT; ...
- Trade.mqh
- ChartObjectsLines.mqh
Предоставляет доступ к классам для создания и управления линейными объектами на графике, которые мы позже используем для отображения подтвержденных пробоев выше и ниже недавних экстремумов.
Далее мы определяем две структуры данных сразу под входными параметрами, чтобы организовать и хранить ключевую информацию, используемую советником.
... //--- Input parameters input group "Information" input ulong magicNumber = 254700680002; input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT; input group "Risk Management" input ENUM_LOT_SIZE_INPUT_MODE lotSizeMode = MODE_AUTO; input double riskPerTradePercent = 1.0; input double lotSize = 0.1; input ENUM_RISK_REWARD_RATIO RRr = ONE_TO_ONEandHALF; //--- Data Structures struct MqlTradeInfo { ulong orderTicket; ENUM_ORDER_TYPE type; ENUM_POSITION_TYPE posType; double entryPrice; double takeProfitLevel; double stopLossLevel; datetime openTime; double lotSize; }; struct MqlAppData { double bidPrice; double askPrice; double currentBalance; double currentEquity; datetime currentGmtTime; datetime lastDailyCheckTime; datetime lastBarOpenTime; double contractSize; long digitValue; double amountAtRisk; MqlTradeInfo tradeInfo; }; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ return(INIT_SUCCEEDED); } ...
- MqlTradeInfo
Эта структура данных хранит все сведения об активной позиции, включая номер тикета, тип ордера, тип позиции, цену входа, Stop Loss, Take Profit, размер лота и время открытия.
- MqlAppData
Эта структура данных служит контейнером для рабочих данных советника, таких как цены Bid и Ask, баланс счета, эквити, время GMT, время открытия последнего бара, размер контракта, количество знаков после запятой для символа и сумма риска на сделку. Она также содержит экземпляр MqlTradeInfo, позволяя хранить информацию о счете и сделке вместе в одном месте.
Далее объявим глобальные переменные, которые будут доступны во всем советнике. Мы объявим их сразу под нашими структурами данных.
... //--- Data Structures struct MqlTradeInfo { ulong orderTicket; ENUM_ORDER_TYPE type; ENUM_POSITION_TYPE posType; double entryPrice; double takeProfitLevel; double stopLossLevel; datetime openTime; double lotSize; }; struct MqlAppData { double bidPrice; double askPrice; double currentBalance; double currentEquity; datetime currentGmtTime; datetime lastDailyCheckTime; datetime lastBarOpenTime; double contractSize; long digitValue; double amountAtRisk; MqlTradeInfo tradeInfo; }; //--- Global variables CTrade Trade; CChartObjectTrend TrendLine; MqlAppData AppData; int heikinAshiIndicatorHandle; double heikinAshiOpen []; double heikinAshiHigh []; double heikinAshiLow []; double heikinAshiClose []; int fractalsIndicatorHandle; double swingHighs []; double swingLows []; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ return(INIT_SUCCEEDED); } ...
- CTrade Trade
Экземпляр класса CTrade, используемый для обработки торговых операций, таких как открытие, закрытие и модификация ордеров и позиций.
- CChartObjectTrend Trendline
Представляет объект трендовой линии, который мы можем рисовать и изменять на графике
- MqlAppData AppData
Экземпляр нашей структуры MqlAppData, позволяющий хранить все общие рабочие данные советника и обращаться к ним из любой части кода.
Мы также объявляем хэндлы индикаторов и несколько массивов для хранения значений, считываемых из нашего пользовательского Heikin Ashi и встроенного индикатора Fractals. В них будут храниться значения индикаторов в реальном времени, чтобы советник мог анализировать движение цены и обнаруживать подтвержденные пробои.
Внутри функции OnTick() первое, что мы делаем, — обновляем глобальные переменные, чтобы при каждом тике они всегда отражали самые актуальные рыночные данные и данные счета. Добавим следующий блок кода в самое начало функции OnTick().
... //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- Scope variables AppData.bidPrice = SymbolInfoDouble (_Symbol, SYMBOL_BID); AppData.askPrice = SymbolInfoDouble (_Symbol, SYMBOL_ASK); AppData.currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); AppData.currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); AppData.amountAtRisk = (riskPerTradePercent/100.0) * AppData.currentBalance; } ...
- AppData.bidPrice
Получает и сохраняет текущую цену Bid для текущего торгового инструмента, на графике которого работает советник.
- AppData.askPrice
Получает и сохраняет текущую цену Ask для текущего торгового инструмента, на графике которого работает советник.
- AppData.currentBalance
Получает и записывает текущий баланс счета при каждом изменении цены.
- AppData.currentEquity
Получает и сохраняет эквити счета в реальном времени.
- AppData.amountAtRisk
Рассчитывает фактическую сумму риска для следующей сделки на основе параметра riskPerTradePercent и текущего баланса счета.
После подготовки структур данных и глобальных переменных следующий шаг — добавить индикаторы, на которых основана наша стратегия. Мы начнем с инициализации хэндлов пользовательского индикатора Heikin Ashi, созданного в первой части серии «Создание профессиональной торговой системы на базе Heikin Ashi», и стандартного индикатора Fractals, доступного в MetaTrader 5. После инициализации хэндлов мы будем считывать данные в реальном времени из их буферов, чтобы советник мог использовать их для обнаружения подтвержденных сигналов пробоя. Для эффективного использования памяти мы также будем освобождать хэндлы индикаторов, когда они больше не нужны. Кроме того, мы упакуем наш пользовательский индикатор Heikin Ashi как ресурс внутри файла советника, что позволит распространять его как один самодостаточный файл без необходимости вручную устанавливать индикатор отдельно.
Прежде чем мы сможем упаковать пользовательский индикатор Heikin Ashi как ресурс, сначала нам нужно создать его самостоятельно. Подготовьте новый пустой исходный файл индикатора в MetaEditor и назовите его 'heikinAshiIndicator.mq5'. Затем скопируйте и вставьте в этот файл приложенный исходный код индикатора и скомпилируйте его. После успешной компиляции MetaTrader создаст файл 'heikinAshiIndicator.ex5'. Затем мы упакуем этот скомпилированный файл как ресурс, чтобы он стал частью нашего советника.
... #property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/en/users/chachaian" #property version "1.10" #resource "\\Indicators\\heikinAshiIndicator.ex5" ...
Это сообщает компилятору, что нужно встроить скомпилированный файл индикатора (heikinAshiIndicator.ex5) в советник. Благодаря этому пользователям не придется вручную устанавливать индикатор в каталог Indicators. Советник всегда будет иметь к нему доступ, пока файл присутствует во время компиляции. Это значительно упрощает распространение и обеспечивает удобную установку для конечных пользователей.
Далее инициализируем хэндлы индикаторов внутри функции OnInit(), чтобы наш советник мог получать данные в реальном времени как из пользовательского индикатора Heikin Ashi, так и из встроенного индикатора Fractals.
... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Initialize global variables AppData.currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); AppData.currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); AppData.lastDailyCheckTime = iTime(_Symbol, PERIOD_D1, 0); AppData.lastBarOpenTime = 0; AppData.digitValue = SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); AppData.contractSize = SymbolInfoDouble (_Symbol, SYMBOL_TRADE_CONTRACT_SIZE); //--- Initialize the Heikin Ashi indicator heikinAshiIndicatorHandle = iCustom(_Symbol, timeframe, "::Indicators\\heikinAshiIndicator.ex5"); if(heikinAshiIndicatorHandle == INVALID_HANDLE){ Print("Error while initializing The Heikin Ashi Indicator: ", GetLastError()); return INIT_FAILED; } //--- Initialize the Fractals indicator fractalsIndicatorHandle = iFractals(_Symbol, timeframe); if(fractalsIndicatorHandle == INVALID_HANDLE){ Print("Error while initializing The Fractals Indicator: ", GetLastError()); return INIT_FAILED; } } ...
Мы только что добавили код, необходимый для инициализации двух индикаторов, которые будем использовать в нашем советнике.
- Индикатор Heikin Ashi
Мы используем iCustom() для загрузки упакованного индикатора Heikin Ashi. Если хэндл не удается успешно создать, советник выводит сообщение об ошибке в журнал экспертов и прекращает работу. Это полезно для отладки и также гарантирует, что советник не будет работать без основного источника сигналов.
- Индикатор Fractals
После инициализации индикаторов следующий шаг внутри функции OnTick() — считывание данных из их буферов. Теперь нужно добавить следующий блок кода внутри функции OnTick(), разместив его сразу под операторами присваивания глобальных переменных.
... //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- Scope variables AppData.bidPrice = SymbolInfoDouble (_Symbol, SYMBOL_BID); AppData.askPrice = SymbolInfoDouble (_Symbol, SYMBOL_ASK); AppData.currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); AppData.currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); AppData.amountAtRisk = (riskPerTradePercent/100.0) * AppData.currentBalance; //--- Get a few Heikin Ashi values int copiedHeikinAshiOpen = CopyBuffer(heikinAshiIndicatorHandle, 0, 0, 10, heikinAshiOpen); if(copiedHeikinAshiOpen == -1){ Print("Error while copying Heikin Ashi Open prices: ", GetLastError()); return; } int copiedHeikinAshiHigh = CopyBuffer(heikinAshiIndicatorHandle, 1, 0, 10, heikinAshiHigh); if(copiedHeikinAshiHigh == -1){ Print("Error while copying Heikin Ashi High prices: ", GetLastError()); return; } int copiedHeikinAshiLow = CopyBuffer(heikinAshiIndicatorHandle, 2, 0, 10, heikinAshiLow); if(copiedHeikinAshiLow == -1){ Print("Error while copying Heikin Ashi Low prices: ", GetLastError()); return; } int copiedHeikinAshiClose = CopyBuffer(heikinAshiIndicatorHandle, 3, 0, 10, heikinAshiClose); if(copiedHeikinAshiClose == -1){ Print("Error while copying Heikin Ashi Close prices: ", GetLastError()); return; } //--- Get the latest Fractals indicator values int copiedSwingHighs = CopyBuffer(fractalsIndicatorHandle, 0, 0, 200, swingHighs); if(copiedSwingHighs == -1){ Print("Error while copying fractal's indicator swing highs: ", GetLastError()); } int copiedSwingLows = CopyBuffer(fractalsIndicatorHandle, 1, 0, 200, swingLows); if(copiedSwingLows == -1){ Print("Error while copying fractal's indicator swing lows: ", GetLastError()); } } ...
Мы используем CopyBuffer() для получения последних 10 значений цен открытия, максимума, минимума и закрытия из индикатора Heikin Ashi. Аналогично мы копируем последние 200 значений локальных максимумов и минимумов из индикатора Fractals. Аналогично мы копируем последние 200 значений локальных максимумов и минимумов из индикатора Fractals. В этих двух случаях при ошибке копирования выводится сообщение в журнал, но выполнение программы не прерывается..
Следующий шаг — установить наши массивы данных как серии с помощью ArraySetAsSeries(). Эта функция меняет направление индексации массивов так, что элемент 0 соответствует точке данных на самом последнем баре, а большие индексы — более старым барам. Мы добавим следующий блок кода прямо под разделом, где инициализируем хэндлы индикаторов.
... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Initialize the Heikin Ashi indicator heikinAshiIndicatorHandle = iCustom(_Symbol, timeframe, "::Indicators\\heikinAshiIndicator.ex5"); if(heikinAshiIndicatorHandle == INVALID_HANDLE){ Print("Error while initializing The Heikin Ashi Indicator: ", GetLastError()); return INIT_FAILED; } //--- Initialize the Fractals indicator fractalsIndicatorHandle = iFractals(_Symbol, timeframe); if(fractalsIndicatorHandle == INVALID_HANDLE){ Print("Error while initializing The Fractals Indicator: ", GetLastError()); return INIT_FAILED; } //--- Set Arrays as series ArraySetAsSeries(heikinAshiOpen, true); ArraySetAsSeries(heikinAshiHigh, true); ArraySetAsSeries(heikinAshiLow, true); ArraySetAsSeries(heikinAshiClose, true); ArraySetAsSeries(swingHighs, true); ArraySetAsSeries(swingLows, true); return(INIT_SUCCEEDED); } ...
Это очень полезно, потому что позволяет советнику легко и единообразно обращаться к последним значениям.
Последний шаг при работе с индикаторами — убедиться, что их ресурсы корректно освобождаются после удаления советника с графика. Это делается внутри функции OnDeinit(), которая автоматически выполняется при удалении советника с графика. Вызывая IndicatorRelease() для наших хэндлов индикаторов, мы освобождаем занимаемую ими память.
... //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- Free up memory used by indicators if(heikinAshiIndicatorHandle != INVALID_HANDLE){ IndicatorRelease(heikinAshiIndicatorHandle); } if(fractalsIndicatorHandle != INVALID_HANDLE){ IndicatorRelease(fractalsIndicatorHandle); } } ...
Хорошей практикой является включать этот этап очистки в каждый советник, использующий пользовательские или встроенные индикаторы.
Перед освобождением хэндлов индикаторов полезно очистить график от любых графических объектов, которые мог создать наш советник. Это гарантирует, что после удаления советника с графика на нем не останутся лишние трендовые линии. Для этого мы вызовем ObjectsDeleteAll(0) внутри функции OnDeinit(). Теперь сделаем это непосредственно перед разделом освобождения памяти.
... //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- Delete all graphical objects ObjectsDeleteAll(0); //--- Free up memory used by indicators if(heikinAshiIndicatorHandle != INVALID_HANDLE){ IndicatorRelease(heikinAshiIndicatorHandle); } ... } ...
На этом этапе компиляция исходного кода больше не должна выдавать ошибок, поскольку все необходимые глобальные переменные были правильно определены и инициализированы.
Наконец, мы добрались до сердца нашего советника — основной торговой логики. Этот блок кода выполняется только при открытии нового бара, что гарантирует оценку сигналов один раз после закрытия свечи. Вставим основную торговую логику непосредственно под существующим кодом внутри функции OnTick().
... //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ ... //--- Get the latest Fractals indicator values int copiedSwingHighs = CopyBuffer(fractalsIndicatorHandle, 0, 0, 200, swingHighs); if(copiedSwingHighs == -1){ Print("Error while copying fractal's indicator swing highs: ", GetLastError()); } int copiedSwingLows = CopyBuffer(fractalsIndicatorHandle, 1, 0, 200, swingLows); if(copiedSwingLows == -1){ Print("Error while copying fractal's indicator swing lows: ", GetLastError()); } //--- Run this block on new bar open if(IsNewBar(_Symbol, timeframe, AppData.lastBarOpenTime)){ datetime timeStart = 0; int indexStart = 0; datetime timeEnd = 0; int indexEnd = 0; //--- Handle Bullish Signals if(IsBullishSignal(timeStart, indexStart, timeEnd, indexEnd)){ if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){ OpenBuy(); } double high = iHigh(_Symbol, timeframe, indexStart); TrendLine.Create(0, GenerateUniqueName(zenBreakout), 0, timeStart, high, timeEnd, high); } //--- Handle Bearish Signals if(IsBearishSignal(timeStart, indexStart, timeEnd, indexEnd)){ if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){ OpenSel(); } double low = iLow (_Symbol, timeframe, indexStart); TrendLine.Create(0, GenerateUniqueName(zenBreakout), 0, timeStart, low, timeEnd, low); } } } ...
В этом разделе мы сначала объявляем несколько вспомогательных переменных (timeStart, indexStart, timeEnd, indexEnd), которые будут заполняться при обнаружении подтвержденного сигнала.
Затем мы вызываем нашу пользовательскую функцию IsBullishSignal(). Если обнаружен бычий сценарий и нет активных позиций на покупку или продажу, советник вызывает OpenBuy() для открытия новой длинной позиции. Сразу после этого он рисует трендовую линию через соответствующий диапазон баров с помощью TrendLine.Create(), визуально отмечая место появления сигнала.
Та же логика применяется к медвежьим сигналам, но в этом случае советник вызывает OpenSel() для открытия короткой позиции и рисует трендовую линию на локальном минимуме, где был обнаружен медвежий сценарий.
Весь этот блок крайне важен, потому что он связывает ранее настроенные входные параметры, индикаторы и глобальные переменные, чтобы в итоге формировать торговые решения.
Поздравляю, вы добрались до этого этапа! На данный момент наш советник полностью разработан и должен компилироваться без ошибок. Рекомендую скачать приложенный исходный файл и сравнить его со своей реализацией. Это поможет найти пропущенные шаги или опечатки и убедиться, что ваш код соответствует тому, что мы построили вместе. Потратить время на проверку и сравнение работы — отличный способ укрепить понимание и уверенность перед переходом к тестированию советника на графике.
Тестирование
Я провел бэктест с использованием золота в качестве торгового инструмента за период с 1 января 2025 года по 31 августа 2025 года. Входные параметры были настроены следующим образом:
- magicNumber: 254700680002
- timeFrame: H1
- lotSizeMode: MODE_AUTO
- riskPerTradePercent: 1.0
- lotSize: 0.1
- RRr: ONE_TO_ONEandHALF
При начальном балансе счета $100000 бэктест показывает рост эквити чуть выше 12% за 8-месячный период.

Ниже приведена кривая роста эквити:

Кривая эквити показывает, что текущая стратегия находится примерно в безубытке, что является положительным признаком, поскольку демонстрирует, что подход не является заведомо убыточным. Я считаю, что есть потенциал для дальнейшего улучшения результатов за счет оптимизации параметров и добавления расширенных фильтров сигналов, например торговых сессий.
Заключение
Мы успешно завершили разработку нашего советника. Вместе мы прошли все шаги: от настройки входных параметров, перечислений и глобальных переменных до инициализации индикаторов, чтения данных из их буферов и реализации основной торговой логики.
Теперь у нас есть полностью функциональный советник, который автоматически торгует, используя нашу логику Heikin Ashi. Бэктест на золоте показал относительно стабильную кривую эквити, подтвердив, что логика работает как задумано и что советник компилируется без ошибок. Это важный этап.
Далее попробуйте оптимизировать параметры, чтобы проверить, можно ли повысить прибыльность, или добавьте дополнительные фильтры, например время торговых сессий или проверки волатильности. Проделанная нами работа закладывает прочную основу для создания более сложных автоматизированных систем, способных работать в реальных рыночных условиях.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18810
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Нейросети в трейдинге: От рыночного шума к устойчивому торговому плану (Окончание)
Рыночные секреты Ларри Уильямса (Часть 2): Автоматизация торговой системы на основе рыночной структуры
От начального до среднего уровня: Песочница и MetaTrader
Разработка инструментария для анализа Price Action (Часть 65): Создание системы для мониторинга и анализа построенных вручную уровней Фибоначчи
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования