English 中文 Español Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 10): Разработка стратегии Trend Flat Momentum

Автоматизация торговых стратегий на MQL5 (Часть 10): Разработка стратегии Trend Flat Momentum

MetaTrader 5Трейдинг |
761 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В предыдущей статье (Часть 9) мы разработали советник для автоматизации стратегии прорыва азиатской сессии с использованием ключевых уровней сессии и динамического управления рисками на MetaQuotes Language 5 (MQL5). Сейчас, в Части 10, мы переместим внимание на стратегию Trend Flat Momentum. Данный метод сочетает в пересечение двух скользящих средних с фильтрами таких импульсов, как Индекс относительной силы (RSI) и Индекс товарного канала (CCI) для эффективного использования трендовых движений с точностью. Статья структурирована по следующим темам:

  1. План стратегии
  2. Реализация средствами MQL5
  3. Тестирование на истории и оптимизация
  4. Заключение

В итоге у нас будет полностью функциональный советник, автоматизирующий стратегию Trend Flat Momentum. Давайте погрузимся в процесс!


План стратегии

Стратегия Trend Flat Momentum разработана таким образом, чтобы улавливать рыночные тенденции, сочетая простую систему пересечения скользящих средних с надежной фильтрацией импульса. Основная идея заключается в том, чтобы генерировать сигналы на покупку, когда быстрая скользящая средняя пересекает более медленную скользящую среднюю, что указывает на бычий тренд, и одновременно подтверждает сигнал индикаторами импульса, которые представляют собой RSI и два разных CCI значения. И наоборот, сигнал о короткой сделке подается, когда медленная скользящая средняя превышает быструю скользящую среднюю, а индикаторы импульса подтверждают медвежьи условия. Настройки индикатора:

  • Индекс товарного канала (CCI) (36 периодов, закрытие)
  • Индекс товарного канала (CCI) (55 периодов, закрытие)
  • Медленный индекс относительной силы (RSI) (27 периодов, закрытие)
  • Быстрая скользящая средняя (11 периодов, сглаженная, медианная цена)
  • Быстрая скользящая средняя (25 периодов, сглаженная, медианная цена)

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

STRATEGY PLAN


Реализация средствами MQL5

Чтобы создать программу на MQL5, откройте  MetaEditor, перейдите в Навигатор, найдите папку «Индикаторы» (Indicators), перейдите на вкладку "Создать" (New) и следуйте инструкциям по созданию файла. Как только это будет сделано, в среде программирования нам нужно будет объявить некоторые  глобальные переменные, которые будем использовать во всей программе.

//+------------------------------------------------------------------+
//|                        Copyright 2025, Forex Algo-Trader, Allan. |
//|                                 "https://t.me/Forex_Algo_Trader" |
//+------------------------------------------------------------------+
#property copyright "Forex Algo-Trader, Allan"
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property description "This EA trades based on Trend Flat Momentum Strategy"
#property strict

#include <Trade\Trade.mqh> //--- Include the Trade library for order management.
CTrade obj_Trade; //--- Create an instance of the CTrade class to handle trading operations.

// Input parameters
input int    InpCCI36Period      = 36;              //--- CCI period 1
input int    InpCCI55Period      = 55;              //--- CCI period 2
input int    InpRSIPeriod        = 27;              //--- RSI period
input int    InpMAFastPeriod     = 11;              //--- Fast MA period
input int    InpMASlowPeriod     = 25;              //--- Slow MA period
input double InpRSIThreshold     = 58.0;            //--- RSI threshold for Buy signal (Sell uses 100 - Threshold)
input int    InpTakeProfitPoints = 300;             //--- Take profit in points
input double InpLotSize          = 0.1;             //--- Trade lot size

// Pivot parameters for detecting swing highs/lows
input int    PivotLeft  = 2;  //--- Number of bars to the left for pivot detection
input int    PivotRight = 2;  //--- Number of bars to the right for pivot detection

// Global indicator handles
int handleCCI36;  //--- Handle for the CCI indicator with period InpCCI36Period
int handleCCI55;  //--- Handle for the CCI indicator with period InpCCI55Period
int handleRSI;    //--- Handle for the RSI indicator with period InpRSIPeriod
int handleMA11;   //--- Handle for the fast moving average (MA) with period InpMAFastPeriod
int handleMA25;   //--- Handle for the slow moving average (MA) with period InpMASlowPeriod

// Global dynamic storage buffers
double ma11_buffer[];  //--- Dynamic array to store fast MA values
double ma25_buffer[];  //--- Dynamic array to store slow MA values
double rsi_buffer[];   //--- Dynamic array to store RSI values
double cci36_buffer[]; //--- Dynamic array to store CCI values (period 36)
double cci55_buffer[]; //--- Dynamic array to store CCI values (period 55)

// To detect a new bar
datetime lastBarTime = 0;  //--- Variable to store the time of the last processed bar

Начинаем с добавления файла "Trade\Trade.mqh", используя #include для доступа к встроенным торговым функциям, и создаем "obj_Trade", экземпляр класса "CTrade", для исполнения ордеров на покупку и продажу. Определяем ключевые входные параметры для настройки стратегии, включая "InpCCI36Period" и "InpCCI55Period" для индикаторов "CCI", "InpRSIPeriod" для "RSI" и "InpMAFastPeriod" и "InpMASlowPeriod" для двух скользящих средних. "InpRSIThreshold" устанавливает условие для фильтрации сделок, в то время как "InpTakeProfitPoints" определяет фиксированный уровень тейк-профита, а "InpLotSize" управляет размером позиции.

Чтобы улучшить исполнение сделок, вводим "PivotLeft" и "PivotRight", которые определяют количество баров, используемых для определения максимумов и минимумов колебаний для размещения стоп-лосса. Глобальные хэндлы индикаторов, такие как "handleCCI36", "handleCCI55", "handleRSI", "handleMA11" и "handleMA25", позволяют нам эффективно извлекать значения индикаторов. Динамические массивы хранят эти значения в "ma11_buffer", "ma25_buffer", "rsi_buffer", "cci36_buffer" и "cci55_buffer", обеспечивая бесперебойную обработку данных. Наконец, "lastBarTime" отслеживает последний обработанный бар, чтобы предотвратить несколько сделок на одной свече и обеспечить точное исполнение сделки. Затем мы можем инициализировать индикаторы в обработчике событий OnInit .

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   //--- Create CCI handle for period InpCCI36Period using the close price.
   handleCCI36 = iCCI(_Symbol, _Period, InpCCI36Period, PRICE_CLOSE); //--- Create the CCI36 indicator handle.
   if (handleCCI36 == INVALID_HANDLE) { //--- Check if the CCI36 handle is valid.
      Print("Error creating CCI36 handle"); //--- Print an error message if invalid.
      return (INIT_FAILED); //--- Return failure if handle creation failed.
   }
   
   //--- Create CCI handle for period InpCCI55Period using the close price.
   handleCCI55 = iCCI(_Symbol, _Period, InpCCI55Period, PRICE_CLOSE); //--- Create the CCI55 indicator handle.
   if (handleCCI55 == INVALID_HANDLE) { //--- Check if the CCI55 handle is valid.
      Print("Error creating CCI55 handle"); //--- Print an error message if invalid.
      return (INIT_FAILED); //--- Return failure if handle creation failed.
   }
   
   //--- Create RSI handle for period InpRSIPeriod using the close price.
   handleRSI = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE); //--- Create the RSI indicator handle.
   if (handleRSI == INVALID_HANDLE) { //--- Check if the RSI handle is valid.
      Print("Error creating RSI handle"); //--- Print an error message if invalid.
      return (INIT_FAILED); //--- Return failure if handle creation failed.
   }
   
   //--- Create fast MA handle using MODE_SMMA on the median price with period InpMAFastPeriod.
   handleMA11 = iMA(_Symbol, _Period, InpMAFastPeriod, 0, MODE_SMMA, PRICE_MEDIAN); //--- Create the fast MA handle.
   if (handleMA11 == INVALID_HANDLE) { //--- Check if the fast MA handle is valid.
      Print("Error creating MA11 handle"); //--- Print an error message if invalid.
      return (INIT_FAILED); //--- Return failure if handle creation failed.
   }
   
   //--- Create slow MA handle using MODE_SMMA on the median price with period InpMASlowPeriod.
   handleMA25 = iMA(_Symbol, _Period, InpMASlowPeriod, 0, MODE_SMMA, PRICE_MEDIAN); //--- Create the slow MA handle.
   if (handleMA25 == INVALID_HANDLE) { //--- Check if the slow MA handle is valid.
      Print("Error creating MA25 handle"); //--- Print an error message if invalid.
      return (INIT_FAILED); //--- Return failure if handle creation failed.
   }
   
   //--- Set the dynamic arrays as time series (index 0 = most recent closed bar).
   ArraySetAsSeries(ma11_buffer, true); //--- Set ma11_buffer as a time series.
   ArraySetAsSeries(ma25_buffer, true); //--- Set ma25_buffer as a time series.
   ArraySetAsSeries(rsi_buffer, true);  //--- Set rsi_buffer as a time series.
   ArraySetAsSeries(cci36_buffer, true); //--- Set cci36_buffer as a time series.
   ArraySetAsSeries(cci55_buffer, true); //--- Set cci55_buffer as a time series.
   
   return (INIT_SUCCEEDED); //--- Return success after initialization.
}

В обработчике событий OnInit инициализируем советник, создавая и проверяя хэндлы индикаторов. Используем функцию iCCI для создания хэндлов CCI с периодами "InpCCI36Period" и "InpCCI55Period," функцию iRSI для хэндла RSI и функцию iMA для хэндлов быстрых и медленных SMMA с периодами "InpMAFastPeriod" и "InpMASlowPeriod." Если какой-либо хэндл неверен (INVALID_HANDLE), выводим сообщение об ошибке и возвращаем ошибку (INIT_FAILED). Наконец, используем функцию ArraySetAsSeries для форматирования буферов в виде временных рядов и возврата сообщения «успешно» после успешной инициализации. Для экономии ресурсов необходимо освободить созданные хэндлы при удалении программы следующим образом.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if (handleCCI36 != INVALID_HANDLE) { //--- If the CCI36 handle is valid,
      IndicatorRelease(handleCCI36); //--- release the CCI36 indicator.
   }
   if (handleCCI55 != INVALID_HANDLE) { //--- If the CCI55 handle is valid,
      IndicatorRelease(handleCCI55); //--- release the CCI55 indicator.
   }
   if (handleRSI != INVALID_HANDLE) { //--- If the RSI handle is valid,
      IndicatorRelease(handleRSI); //--- release the RSI indicator.
   }
   if (handleMA11 != INVALID_HANDLE) { //--- If the fast MA handle is valid,
      IndicatorRelease(handleMA11); //--- release the fast MA indicator.
   }
   if (handleMA25 != INVALID_HANDLE) { //--- If the slow MA handle is valid,
      IndicatorRelease(handleMA25); //--- release the slow MA indicator.
   }
}

Здесь мы выполняем деинициализацию советника, высвобождая ресурсы индикатора в OnDeinit. Проверяем, действительны ли значения каждого хэндла индикатора — "CCI36", "CCI55", "RSI", быстрой скользящей средней и медленной скользящей средней. Если это так, используем функцию IndicatorRelease для освобождения выделенных ресурсов, обеспечивая эффективное управление памятью. Теперь можем перейти к обработчику событий OnTick где будет происходить обработка всех данных и принятие решений.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   //--- Get the time of the current bar.
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Retrieve the current bar's time.
   if (currentBarTime != lastBarTime) { //--- If a new bar has formed,
      lastBarTime = currentBarTime; //--- update lastBarTime with the current bar's time.
      OnNewBar(); //--- Process the new bar.
   }
}

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

//+------------------------------------------------------------------+
//| Function called on every new bar (bar close)                     |
//+------------------------------------------------------------------+
void OnNewBar() {
   
   //--- Resize the dynamic arrays to hold 2 values (last closed bar and the one before).
   ArrayResize(ma11_buffer, 2); //--- Resize ma11_buffer to 2 elements.
   ArrayResize(ma25_buffer, 2); //--- Resize ma25_buffer to 2 elements.
   ArrayResize(rsi_buffer, 2);  //--- Resize rsi_buffer to 2 elements.
   ArrayResize(cci36_buffer, 2); //--- Resize cci36_buffer to 2 elements.
   ArrayResize(cci55_buffer, 2); //--- Resize cci55_buffer to 2 elements.
   
   //--- Copy indicator values into the dynamic arrays.
   if (CopyBuffer(handleMA11, 0, 1, 2, ma11_buffer) != 2) { //--- Copy 2 values from the fast MA indicator.
      return; //--- Exit the function if copying fails.
   }
   if (CopyBuffer(handleMA25, 0, 1, 2, ma25_buffer) != 2) { //--- Copy 2 values from the slow MA indicator.
      return; //--- Exit the function if copying fails.
   }
   if (CopyBuffer(handleRSI, 0, 1, 2, rsi_buffer) != 2) { //--- Copy 2 values from the RSI indicator.
      return; //--- Exit the function if copying fails.
   }
   if (CopyBuffer(handleCCI36, 0, 1, 2, cci36_buffer) != 2) { //--- Copy 2 values from the CCI36 indicator.
      return; //--- Exit the function if copying fails.
   }
   if (CopyBuffer(handleCCI55, 0, 1, 2, cci55_buffer) != 2) { //--- Copy 2 values from the CCI55 indicator.
      return; //--- Exit the function if copying fails.
   }
   
   //--- For clarity, assign the values from the arrays.
   //--- Index 0: last closed bar, Index 1: bar before.
   double ma11_current   = ma11_buffer[0]; //--- Fast MA value for the last closed bar.
   double ma11_previous  = ma11_buffer[1]; //--- Fast MA value for the previous bar.
   double ma25_current   = ma25_buffer[0]; //--- Slow MA value for the last closed bar.
   double ma25_previous  = ma25_buffer[1]; //--- Slow MA value for the previous bar.
   double rsi_current    = rsi_buffer[0];  //--- RSI value for the last closed bar.
   double cci36_current  = cci36_buffer[0]; //--- CCI36 value for the last closed bar.
   double cci55_current  = cci55_buffer[0]; //--- CCI55 value for the last closed bar.
}

В создаваемой нами функции типа void "OnNewBar" сначала проверяем, чтобы размеры динамических массивов, содержащих значения индикаторов, были изменены с помощью функции ArrayResize для сохранения двух последних закрытых баров. Затем извлекаем значения индикаторов, используя функцию CopyBuffer для индикаторов быстрая скользящая средняя, медленная скользящая средняя, RSI и двух индикаторов CCI. Если какая-либо из этих операций завершается неудачей, функция завершает работу для предотвращения ошибок. Как только значения успешно скопированы, присваиваем их переменным для удобства справки, различая последний закрытый бар и бар перед ним. Такая настройка гарантирует, что мы всегда будем располагать самыми свежими рыночными данными для принятия торговых решений. В случае успешного получения данных мы сможем приступить к принятию торговых решений. Начинаем с логики покупки.

//--- Check for Buy Conditions:
bool maCrossoverBuy    = (ma11_previous < ma25_previous) && (ma11_current > ma25_current); //--- True if fast MA crosses above slow MA.
bool rsiConditionBuy   = (rsi_current > InpRSIThreshold); //--- True if RSI is above the Buy threshold.
bool cci36ConditionBuy = (cci36_current > 0); //--- True if CCI36 is positive.
bool cci55ConditionBuy = (cci55_current > 0); //--- True if CCI55 is positive.

if (maCrossoverBuy) { //--- If crossover for MA Buy is true...
   bool conditionsOk = true; //--- Initialize a flag to track if all conditions are met.
   
   //--- Check RSI condition for Buy.
   if (!rsiConditionBuy) { //--- If the RSI condition is not met...
      Print("Buy signal rejected: RSI condition not met. RSI=", rsi_current, " Threshold=", InpRSIThreshold); //--- Notify the user.
      conditionsOk = false; //--- Mark the conditions as not met.
   }
   //--- Check CCI36 condition for Buy.
   if (!cci36ConditionBuy) { //--- If the CCI36 condition is not met...
      Print("Buy signal rejected: CCI36 condition not met. CCI36=", cci36_current); //--- Notify the user.
      conditionsOk = false; //--- Mark the conditions as not met.
   }
   //--- Check CCI55 condition for Buy.
   if (!cci55ConditionBuy) { //--- If the CCI55 condition is not met...
      Print("Buy signal rejected: CCI55 condition not met. CCI55=", cci55_current); //--- Notify the user.
      conditionsOk = false; //--- Mark the conditions as not met.
   }

Здесь мы оцениваем условия для входа в сделку на покупку. Переменная "maCrossoverBuy" проверяет, пересеклась ли быстрая скользящая средняя ("ma11") выше медленной скользящей средней ("ma25"), указывая на потенциальный сигнал к покупке. "rsiConditionBuy" обеспечивает, что значение RSI находится выше определенного "InpRSIThreshold", подтверждая сильный бычий импульс. "cci36ConditionBuy" и "cci55ConditionBuy" проверяют, являются ли оба индикатора CCI положительными, что указывает на то, что рынок находится в благоприятном тренде. Если условие "maCrossoverBuy" выполняется, переходим к проверке остальных условий. Если какое-либо условие не выполняется, выводим сообщение с указанием причины отклонения сигнала на покупку и устанавливаем флаг "conditionsOk" в значение false, чтобы предотвратить дальнейшее совершение сделки. Эта всесторонняя проверка гарантирует, что будут совершены только сделки с сильным бычьим подтверждением. Если мы затем обнаружим таковое, то сможем приступить к открытию позиции.

if (conditionsOk) { //--- If all Buy conditions are met...
   //--- Get stop loss from previous swing low.
   double stopLoss = GetPivotLow(PivotLeft, PivotRight); //--- Use pivot low as the stop loss.
   if (stopLoss <= 0) { //--- If no valid pivot low is found...
      stopLoss = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Fallback to current bid price.
   }
   
   double entryPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Determine entry price as current ask price.
   double tp         = entryPrice + InpTakeProfitPoints * _Point; //--- Calculate take profit based on fixed points.
   
   //--- Print the swing point (pivot low) used as stop loss.
   Print("Buy signal: Swing Low used as Stop Loss = ", stopLoss); //--- Notify the user of the pivot low used.
   
   if (obj_Trade.Buy(InpLotSize, NULL, entryPrice, stopLoss, tp, "Buy Order")) { //--- Attempt to open a Buy order.
      Print("Buy order opened at ", entryPrice); //--- Notify the user if the order is opened successfully.
   }
   else {
      Print("Buy order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Notify the user if the order fails.
   }
   
   return; //--- Exit after processing a valid Buy signal.
}
else {
   Print("Buy signal not executed due to failed condition(s)."); //--- Notify the user if Buy conditions failed.
}

После подтверждения того, что все условия покупки выполнены, переходим к определению стоп-лосса для сделки. Мы используем функцию "GetPivotLow", чтобы найти предыдущий минимум колебаний, который установлен в качестве стоп-лосса. Если действительный пивот-минимум не найден (т.е. стоп-лосс меньше или равен 0), в качестве запасного варианта используется текущая цена bid. Цена входа берется из текущей цены ask с помощью функции SymbolInfoDouble с параметром SYMBOL_ASK.  Рассчитываем тейк-профит ("tp"), добавляя указанные "InpTakeProfitPoints" к цене входа, скорректированной на рыночное значение пункта (_Point).

Как только цена входа, стоп-лосс и тейк-профит определены, выставляется ордер на покупку с использованием метода "obj_Trade.Buy". Если ордер на покупку успешно открыт, выводим сообщение с подтверждением. Если ордер не выполнен, предоставляем сообщение о сбое и описание ошибки с помощью "obj_Trade.ResultRetcodeDescription". Если условия покупки не выполняются, выводится сообщение о том, что сигнал на покупку отклонен и сделка не открывается. Нами использована пользовательская функция "GetPivotLow». Её реализация приведена ниже.

//+------------------------------------------------------------------+
//| Function to find the most recent swing low (pivot low)           |
//+------------------------------------------------------------------+
double GetPivotLow(int left, int right) {
   MqlRates rates[]; //--- Declare an array to store rate data.
   int copied = CopyRates(_Symbol, _Period, 0, 100, rates); //--- Copy the last 100 bars into the rates array.
   if (copied <= (left + right)) { //--- Check if sufficient data was copied.
      return (0); //--- Return 0 if there are not enough bars.
   }
   
   //--- Loop through the bars to find a pivot low.
   for (int i = left; i <= copied - right - 1; i++) {
      bool isPivot = true; //--- Assume the current bar is a pivot low.
      double currentLow = rates[i].low; //--- Get the low value of the current bar.
      for (int j = i - left; j <= i + right; j++) { //--- Loop through neighboring bars.
         if (j == i) { //--- Skip the current bar.
            continue;
         }
         if (rates[j].low <= currentLow) { //--- If any neighbor's low is lower or equal,
            isPivot = false; //--- then the current bar is not a pivot low.
            break;
         }
      }
      if (isPivot) { //--- If a pivot low is confirmed,
         return (currentLow); //--- return the low value of the pivot.
      }
   }
   return (0); //--- Return 0 if no pivot low is found.
}

В этой функции мы стремимся определить самый последний минимум колебаний (пивот-минимум), сканируя последние 100 баров рыночных данных с помощью функции CopyRates.  Массив "rates" структуры MqlRates содержит данные о ценах, и мы проверяем, достаточно ли баров для выполнения расчета, проверяя, являются ли скопированные данные больше или они равны сумме параметров "left" и "right". Затем функция выполняет циклический просмотр баров, проверяя наличие пивот-минимума, сравнивая значение минимума текущего бара с соседними барами в пределах указанного диапазона "left" и "right". Если минимум любого соседнего бара ниже или равен минимуму текущего бара, текущий бар не является пивот-минимумом. Если найден пивот-минимум, возвращается его минимальное значение. Если после проверки всех баров пивот-минимум не найден, функция возвращает 0.

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

//+------------------------------------------------------------------+
//| Function to find the most recent swing high (pivot high)         |
//+------------------------------------------------------------------+
double GetPivotHigh(int left, int right) {
   MqlRates rates[]; //--- Declare an array to store rate data.
   int copied = CopyRates(_Symbol, _Period, 0, 100, rates); //--- Copy the last 100 bars into the rates array.
   if (copied <= (left + right)) { //--- Check if sufficient data was copied.
      return (0); //--- Return 0 if there are not enough bars.
   }
   
   //--- Loop through the bars to find a pivot high.
   for (int i = left; i <= copied - right - 1; i++) {
      bool isPivot = true; //--- Assume the current bar is a pivot high.
      double currentHigh = rates[i].high; //--- Get the high value of the current bar.
      for (int j = i - left; j <= i + right; j++) { //--- Loop through neighboring bars.
         if (j == i) { //--- Skip the current bar.
            continue;
         }
         if (rates[j].high >= currentHigh) { //--- If any neighbor's high is higher or equal,
            isPivot = false; //--- then the current bar is not a pivot high.
            break;
         }
      }
      if (isPivot) { //--- If a pivot high is confirmed,
         return (currentHigh); //--- return the high value of the pivot.
      }
   }
   return (0); //--- Return 0 if no pivot high is found.
}

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

//--- Check for Sell Conditions:
bool maCrossoverSell    = (ma11_previous > ma25_previous) && (ma11_current < ma25_current); //--- True if fast MA crosses below slow MA.
bool rsiConditionSell   = (rsi_current < (100.0 - InpRSIThreshold)); //--- True if RSI is below the Sell threshold.
bool cci36ConditionSell = (cci36_current < 0); //--- True if CCI36 is negative.
bool cci55ConditionSell = (cci55_current < 0); //--- True if CCI55 is negative.

if (maCrossoverSell) { //--- If crossover for MA Sell is true...
   bool conditionsOk = true; //--- Initialize a flag to track if all conditions are met.
   
   //--- Check RSI condition for Sell.
   if (!rsiConditionSell) { //--- If the RSI condition is not met...
      Print("Sell signal rejected: RSI condition not met. RSI=", rsi_current, " Required below=", (100.0 - InpRSIThreshold)); //--- Notify the user.
      conditionsOk = false; //--- Mark the conditions as not met.
   }
   //--- Check CCI36 condition for Sell.
   if (!cci36ConditionSell) { //--- If the CCI36 condition is not met...
      Print("Sell signal rejected: CCI36 condition not met. CCI36=", cci36_current); //--- Notify the user.
      conditionsOk = false; //--- Mark the conditions as not met.
   }
   //--- Check CCI55 condition for Sell.
   if (!cci55ConditionSell) { //--- If the CCI55 condition is not met...
      Print("Sell signal rejected: CCI55 condition not met. CCI55=", cci55_current); //--- Notify the user.
      conditionsOk = false; //--- Mark the conditions as not met.
   }
   
   if (conditionsOk) { //--- If all Sell conditions are met...
      //--- Get stop loss from previous swing high.
      double stopLoss = GetPivotHigh(PivotLeft, PivotRight); //--- Use pivot high as the stop loss.
      if (stopLoss <= 0) { //--- If no valid pivot high is found...
         stopLoss = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Fallback to current ask price.
      }
      
      double entryPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Determine entry price as current bid price.
      double tp         = entryPrice - InpTakeProfitPoints * _Point; //--- Calculate take profit based on fixed points.
      
      //--- Print the swing point (pivot high) used as stop loss.
      Print("Sell signal: Swing High used as Stop Loss = ", stopLoss); //--- Notify the user of the pivot high used.
      
      if (obj_Trade.Sell(InpLotSize, NULL, entryPrice, stopLoss, tp, "Sell Order")) { //--- Attempt to open a Sell order.
         Print("Sell order opened at ", entryPrice); //--- Notify the user if the order is opened successfully.
      }
      else {
         Print("Sell order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Notify the user if the order fails.
      }
      
      return; //--- Exit after processing a valid Sell signal.
   }
   else {
      Print("Sell signal not executed due to failed condition(s)."); //--- Notify the user if Sell conditions failed.
   }
}

Здесь мы проверяем условия для подачи сигнала на продажу, изменяя логику, используемую для подачи сигналов на покупку. Начинаем с проверки того, пересекается ли быстрая скользящая средняя ниже медленной скользящей средней, которая фиксируется условием "maCrossoverSell". Далее проверяем, находится ли RSI ниже порога Sell (продажа), и проверяем, что оба значения CCI36 и CCI55 отрицательны.

Если все условия выполнены, рассчитываем стоп-лосс, используя функцию "GetPivotHigh", чтобы найти самый последний максимум колебаний и определить тейк-профит на основе фиксированного расстояния до точки. Затем пытаемся открыть ордер на продажу, используя метод "obj_Trade.Sell". Если ордер выполнен успешно, выводим сообщение с подтверждением; в противном случае отображаем сообщение об ошибке. Если какое-либо условие не выполняется, уведомляем пользователя о том, что сигнал на продажу отклонен. После компиляции и запуска программы получаем следующий результат.

BUY TRADE CONFIRMED.

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


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

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

INVALID STOPS ERROR

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

//+------------------------------------------------------------------+
//| Function to find the most recent swing low (pivot low)           |
//+------------------------------------------------------------------+
double GetPivotLow(int left, int right) {
   MqlRates rates[]; //--- Declare an array to store rate data.
   ArraySetAsSeries(rates, true);

//---



}

После дальнейшего тестирования для подтверждения мы получили следующий результат.

CORRECTED SWING POINT

Из изображения видно, что мы корректно получаем фактические последние точки колебания, следовательно, избавляемся от ошибки "invalid stops" (недопустимые остановки). Таким образом, мы не будем заблокированы в сделках, и после тщательного тестирования, с 2023 по 2024 год, мы получили следующие результаты.

График тестирования на истории:

График

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

REPORT


Заключение

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

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

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

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

Прикрепленные файлы |
Алгоритм голубых обезьян — Blue Monkey (BM) Algorithm Алгоритм голубых обезьян — Blue Monkey (BM) Algorithm
В статье представлена реализация метаэвристического алгоритма Blue Monkey, основанного на моделировании социального поведения голубых мартышек. Рассматриваются ключевые механизмы алгоритма - групповая структура популяции, следование за локальными лидерами и обновление поколений через замену худших взрослых особей лучшими детёнышами, а также анализируются результаты тестирования.
От начального до среднего уровня: Индикатор (I) От начального до среднего уровня: Индикатор (I)
В этой статье мы создадим наш первый индикатор, который будет полностью практичным и функциональным. Цель не в том, чтобы показать, как создать приложение, а в том, чтобы помочь вам понять, как можно развивать собственные идеи и дать вам возможность применить их на практике безопасным, простым и практичным способом.
Генеративно-состязательные сети (GAN) для синтетических данных в сфере финансового моделирования (Часть 2): Создание синтетического символа для тестирования Генеративно-состязательные сети (GAN) для синтетических данных в сфере финансового моделирования (Часть 2): Создание синтетического символа для тестирования
В этой статье мы создаем синтетический символ с использованием генеративно-состязательной сети (GAN), которая включает в себя генерацию реалистичных финансовых данных, имитирующих поведение реальных рыночных инструментов, таких как EURUSD. Модель GAN изучает закономерности и волатильность на основе исторических рыночных данных и создает синтетические ценовые данные с аналогичными характеристиками.
Нейросети в трейдинге: Обучение глубоких спайкинговых моделей (Интеграция спайков) Нейросети в трейдинге: Обучение глубоких спайкинговых моделей (Интеграция спайков)
В статье представлена практическая реализация ключевых компонентов фреймворка SEW-ResNet средствами MQL5. Использование динамических массивов и спайковых механизмов позволяет гибко строить архитектуру модели и эффективно обрабатывать финансовые временные ряды. Предложенные решения показывают, как SEW-ResNet может оптимизировать вычисления и улучшить выделение значимых признаков.