preview
Как сделать любой тип Trailing Stop и подключить к советнику

Как сделать любой тип Trailing Stop и подключить к советнику

MetaTrader 5Примеры | 16 мая 2024, 14:17
503 0
Artyom Trishkin
Artyom Trishkin

Содержание


Введение

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

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

  • старт трейлинга — количество пунктов прибыли позиции, при достижении которых запускается трейлинг-стоп;
  • шаг трала — количество пунктов, которые должна пройти цена в сторону прибыли позиции, для следующего смещения StopLoss позиции;
  • дистанция трала — расстояние от текущей цены, на котором удерживается StopLoss.

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

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

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

  • цена StopLoss не должна быть ближе к текущей цене на значение уровня StopLevel символа (SYMBOL_TRADE_STOPS_LEVEL — минимальный отступ в пунктах от текущей цены закрытия для установки Stop ордеров);
  • цена StopLoss не должна равняться уже установленной, и должна быть выше текущего уровня стоп-лосс для длинной позиции и ниже — для короткой;
  • если в алгоритме трала используются перечисленные выше параметры, то дополнительно нужно проверить условия, установленные этими параметрами.

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

Так может работать простой трал:

  1. в параметре "старт трейлинга" указывается прибыль в пунктах, при достижении которой нужно запустить трейлинг, либо ноль, если этот параметр не используется;
  2. в параметре "шаг трала" указывается, через сколько пунктов прибыли нужно подтягивать стоп за ценой, либо ноль, если этот параметр не используется;
  3. в параметре "дистанция трала" устанавливается расстояние от текущей цены до стопа позиции, на котором нужно держать стоп, либо ноль, если этот параметр не используется;
  4. дополнительно можно установить символ и магик позиций, которые нужно тралить, либо NULL и -1 соответственно — если тралить нужно позиции всех символов и с любым магиком.

При достижении прибыли позиции указанного количества пунктов, запускается трейлинг и устанавливается стоп позиции на указанное расстояние от текущей цены. Далее, после того как цена ещё прошла указанное количество пунктов в сторону прибыли позиции (шаг трала), стоп позиции опять переносится за ценой так, чтобы держать от неё заданную дистанцию. И так постоянно до тех пор, пока цена не пойдёт против позиции. В этом случае стоп остаётся на уже установленном уровне. Как только цена достигнет его, позиция будет закрыта в прибыли по StopLoss. Таким образом трейлинг-стоп позволяет сохранять прибыль, закрывая позицию при развороте цены, и при этом давая цене "гулять" в пределах заданного отступа стопа от цены, шаг за шагом подтягивая стоп за ценой, если та движется в направлении позиции.

Вместо параметра "дистанция трала" можно указать значение цены, получаемое, например, от индикатора. В этом случае мы получим трал по индикатору. Можно передать значение одной из предыдущих свечей (High, Low, например) — тогда получим трал по ценам баров, и т.д. Но базовый трал — его алгоритм, остаётся неизменным.

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

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

Если же использовать классы трейлингов, то мы можем создать множество экземпляров одного класса трейлинга с различными настройками, и тогда в одном советнике смогут работать одновременно все созданные таким образом тралы по определённому, заданному программистом, алгоритму. Т.е. для каждого набора параметров практически "за пару строк кода" будет создан собственный трал, который будет работать по своему алгоритму. Каждые созданные тралы можно запускать в советнике по неким условиям, создавая сложные алгоритмы трейлинга StopLoss позиций.

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

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

В прошлой статье этот простой базовый трал назывался TrailingStopByValue(). Здесь мы его назовём SimpleTrailing — "Простой Трейлинг". И его можно будет полноценно использовать в своих программах.

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


Базовый класс трейлинга

В папке терминала \MQL5\Include\ создадим новую подпапку Trailings\, а в ней новый файл класса с именем Trailings.mqh:



Имя класса должно быть CSimpleTrailing, имя создаваемого файла Trailings.mqh, а базовым классом должен быть класс базового объекта Стандартной Библиотеки CObject:


По завершении работы мастера MQL получаем такую заготовку класса в папке \MQL5\Include\Trailings\Trailings.mqh:

//+------------------------------------------------------------------+
//|                                                    Trailings.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
class CSimpleTrailing : public CObject
  {
private:

public:
                     CSimpleTrailing();
                    ~CSimpleTrailing();
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CSimpleTrailing::CSimpleTrailing()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CSimpleTrailing::~CSimpleTrailing()
  {
  }
//+------------------------------------------------------------------+


Подключим файл базового объекта Стандартной Библиотеки к созданному файлу:

//+------------------------------------------------------------------+
//|                                                    Trailings.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include <Object.mqh>

//+------------------------------------------------------------------+
//| Класс простого трала StopLoss позиций                            |
//+------------------------------------------------------------------+
class CSimpleTrailing : public CObject

Для чего необходимо наследовать все создаваемые классы тралов от базового объекта Стандартной Библиотеки?

Это позволит легко создавать из этих классов коллекции тралов — помещать их в списки, искать в списках нужные объекты, и т.д. То есть, если мы хотим просто использовать будущие объекты классов как экземпляры в файлах советников, то наследоваться от CObject не обязательно. Но если мы хотим сделать из этих классов полноценные коллекции и использовать все возможности Стандартной Библиотеки, то эти классы должны быть наследниками базового класса CObject:

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

В приватной секции класса объявим три метода:

class CSimpleTrailing : public CObject
  {
private:
//--- проверяет критерии модификации StopLoss позициии и возвращает флаг
   bool              CheckCriterion(ENUM_POSITION_TYPE pos_type, double pos_open, double pos_sl, double value_sl, MqlTick &tick);

//--- модифицирует StopLoss позиции по её тикету
   bool              ModifySL(const ulong ticket, const double stop_loss);

//--- возвращает размер StopLevel в пунктах
   int               StopLevel(void);

protected: 

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

Метод ModifySL() будет модифицировать стоп-лосс позиции по указанному значению.

Метод StopLevel() будет возвращать значение свойства StopLevel символа.

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

В защищённой секции класса объявим все необходимые переменные для работы класса:

protected: 
   string            m_symbol;                        // торговый символ
   long              m_magic;                         // идентификатор эксперта
   double            m_point;                         // Point символа
   int               m_digits;                        // Digits символа
   int               m_offset;                        // дистанция стопа от цены
   int               m_trail_start;                   // прибыль в пунктах для запуска трала
   uint              m_trail_step;                    // шаг трала
   uint              m_spread_mlt;                    // множитель спреда для возврата значения StopLevel
   bool              m_active;                        // флаг активности трала

//--- рассчитывает и возвращает уровень StopLoss выбранной позиции
   virtual double    GetStopLossValue(ENUM_POSITION_TYPE pos_type, MqlTick &tick);

public:

Из всех переменных, объявленных здесь, пояснений требует только "множитель спреда". Значение StopLevel символа — это дистанция до текущей цены, ближе которой нельзя поставить стоп-лосс. И, если значение StopLevel установлено на сервере нулевым, то это не означает отсутствия StopLevel. Это означает то, что уровень стопов плавающий и обычно равен двум спредам. Иногда трём. Всё зависит от настроек сервера. Поэтому здесь можно будет самостоятельно задать этот множитель. Если используется двойной спред для значения StopLevel, то значение в переменную m_spread_mlt устанавливается равное двум (по умолчанию). Если тройной спред — то трём, и т.д.

Виртуальный метод GetStopLossValue() рассчитывает и возвращает цену StopLoss для позиции. В разных типах трейлингов уровень стоп-лосс, на который необходимо поставить стоп позиции, рассчитывается по-разному. Именно по этой причине этот метод объявлен виртуальным, и должен переопределяться в каждом наследуемом классе трейлингов, если в них расчёт стоп-лосс отличается от того, что будет написан в этом методе этого класса.

В публичной секции класса напишем методы для установки и возврата значений переменным, объявленным в защищённой секции класса, а также конструкторы и деструктор класса:

public:
//--- установка параметров трала
   void              SetSymbol(const string symbol)
                       {
                        this.m_symbol = (symbol==NULL || symbol=="" ? ::Symbol() : symbol);
                        this.m_point  =::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT);
                        this.m_digits = (int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS);
                       }
   void              SetMagicNumber(const long magic)       { this.m_magic = magic;       }
   void              SetStopLossOffset(const int offset)    { this.m_offset = offset;     }
   void              SetTrailingStart(const int start)      { this.m_trail_start = start; }
   void              SetTrailingStep(const uint step)       { this.m_trail_step = step;   }
   void              SetSpreadMultiplier(const uint value)  { this.m_spread_mlt = value;  }
   void              SetActive(const bool flag)             { this.m_active = flag;       }

//--- возврат параметров трала
   string            Symbol(void)                     const { return this.m_symbol;       }
   long              MagicNumber(void)                const { return this.m_magic;        }
   int               StopLossOffset(void)             const { return this.m_offset;       }
   int               TrailingStart(void)              const { return this.m_trail_start;  }
   uint              TrailingStep(void)               const { return this.m_trail_step;   }
   uint              SpreadMultiplier(void)           const { return this.m_spread_mlt;   }
   bool              IsActive(void)                   const { return this.m_active;       }

//--- запуск трала с отступом StopLoss от цены
   bool              Run(void);

//--- конструкторы
                     CSimpleTrailing() : m_symbol(::Symbol()), m_point(::Point()), m_digits(::Digits()),
                                         m_magic(-1), m_trail_start(0), m_trail_step(0), m_offset(0), m_spread_mlt(2) {}
                     CSimpleTrailing(const string symbol, const long magic,
                                     const int trailing_start, const uint trailing_step, const int offset);
//--- деструктор
                    ~CSimpleTrailing() {}
  };


Класс будет иметь два конструктора:

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

Рассмотрим реализацию параметрического конструктора:

//+------------------------------------------------------------------+
//| Параметрический конструктор                                      |
//+------------------------------------------------------------------+
CSimpleTrailing::CSimpleTrailing(const string symbol, const long magic,
                                 const int trail_start, const uint trail_step, const int offset) : m_spread_mlt(2)
  {
   this.SetSymbol(symbol);
   this.m_magic      = magic;
   this.m_trail_start= trail_start;
   this.m_trail_step = trail_step;
   this.m_offset     = offset;
  }

В отличие от конструктора по умолчанию, где в строке инициализации всем переменным устанавливаются значения текущего символа и нулевые значения параметров трала, здесь в конструктор передаются значения, которые и устанавливаются в соответствующие переменные. Метод SetSymbol() устанавливает значения сразу нескольким переменным:

   void              SetSymbol(const string symbol)
                       {
                        this.m_symbol = (symbol==NULL || symbol=="" ? ::Symbol() : symbol);
                        this.m_point  =::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT);
                        this.m_digits = (int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS);
                       }

В обоих конструкторах множитель спреда устанавливается в значение 2. При необходимости, его можно будет изменить методом SetSpreadMultiplier().


Виртуальный метод, рассчитывающий и возвращающий уровень StopLoss выбранной позиции:

//+------------------------------------------------------------------+
//| Рассчитывает и возвращает уровень StopLoss выбранной позиции     |
//+------------------------------------------------------------------+
double CSimpleTrailing::GetStopLossValue(ENUM_POSITION_TYPE pos_type, MqlTick &tick)
  {
//--- в зависимости от типа позиции рассчитываем и возвращаем уровень StopLoss
   switch(pos_type)
     {
      case POSITION_TYPE_BUY  :  return(tick.bid - this.m_offset * this.m_point);
      case POSITION_TYPE_SELL :  return(tick.ask + this.m_offset * this.m_point);
      default                 :  return 0;
     }
  }

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

Рассчитанные значения StopLoss позиций возвращаются из метода и далее проверяются в методе CheckCriterion():

//+------------------------------------------------------------------+
//|Проверяет критерии модификации StopLoss позициии и возвращает флаг|
//+------------------------------------------------------------------+
bool CSimpleTrailing::CheckCriterion(ENUM_POSITION_TYPE pos_type, double pos_open, double pos_sl, double value_sl, MqlTick &tick)
  {
//--- если стоп позиции и уровень стопа для модификации равны, или передан нулевой StopLoss - возвращаем false
   if(::NormalizeDouble(pos_sl - value_sl, this.m_digits) == 0 || value_sl==0)
      return false;
      
//--- переменные трала
   double trailing_step = this.m_trail_step * this.m_point;                      // переводим шаг трала в цену
   double stop_level    = this.StopLevel() * this.m_point;                       // переводим StopLevel символа в цену
   int    pos_profit_pt = 0;                                                     // прибыль позиции в пунктах

//--- в зависимости от типа позиции проверяем условия для модицикации StopLoss
   switch(pos_type)
     {
      //--- длинная позиция
      case POSITION_TYPE_BUY :
         pos_profit_pt = int((tick.bid - pos_open) / this.m_point);              // рассчитываем прибыль позиции в пунктах
         if(tick.bid - stop_level > value_sl                                     // если уровень StopLoss ниже цены с отложенным вниз от неё уровнем StopLevel (соблюдена дистанция по StopLevel)
            && pos_sl + trailing_step < value_sl                                 // если уровень StopLoss выше, чем шаг трала, отложенный вверх от текущего StopLoss позиции
            && (this.m_trail_start == 0 || pos_profit_pt > this.m_trail_start)   // если тралим при любой прибыли, или прибыль позиции в пунктах больше значения начала трейлинга - возвращаем true
           )
            return true;
         break;
         
      //--- короткая позиция
      case POSITION_TYPE_SELL :
         pos_profit_pt = int((pos_open - tick.ask) / this.m_point);              // рассчитываем прибыль позиции в пунктах
         if(tick.ask + stop_level < value_sl                                     // если уровень StopLoss выше цены с отложенным вверх от неё уровнем StopLevel (соблюдена дистанция по StopLevel)
            && (pos_sl - trailing_step > value_sl || pos_sl == 0)                // если уровень StopLoss ниже, чем шаг трала, отложенный вниз от текущего StopLoss позиции, или у позиции ещё не установлен StopLoss
            && (this.m_trail_start == 0 || pos_profit_pt > this.m_trail_start)   // если тралим при любой прибыли, или прибыль позиции в пунктах больше значения начала трейлинга - возвращаем true
           )
            return true;
         break;
         
      //--- по умолчанию вернём false
      default:
         break;
     }
     
//--- условия не подошли - возвращаем false
   return false;
  }

Если переданное в метод значение StopLoss удовлетворяет всем критериям всех фильтров, то метод возвращает true, и стоп-лосс позиция модифицируется при помощи метода ModifySL():

//+------------------------------------------------------------------+
//| Модифицирует StopLoss позиции по тикету                          |
//+------------------------------------------------------------------+
bool CSimpleTrailing::ModifySL(const ulong ticket, const double stop_loss)
  {
//--- если взведён флаг остановки эксперта - сообщаем об этом в журнал и возвращаем false
   if(::IsStopped())
     {
      Print("The Expert Advisor is stopped, trading is disabled");
      return false;
     }
//--- если позицию не удалось выбрать по тикету - сообщаем об этом в журнал и возвращаем false
   ::ResetLastError();
   if(!::PositionSelectByTicket(ticket))
     {
      ::PrintFormat("%s: Failed to select position by ticket number %I64u. Error %d", __FUNCTION__, ticket, ::GetLastError());
      return false;
     }
     
//--- объявляем структуры торгового запроса и результата запроса
   MqlTradeRequest   request= {};
   MqlTradeResult    result = {};
   
//--- заполняем структуру запроса
   request.action    = TRADE_ACTION_SLTP;
   request.symbol    = ::PositionGetString(POSITION_SYMBOL);
   request.magic     = ::PositionGetInteger(POSITION_MAGIC);
   request.tp        = ::PositionGetDouble(POSITION_TP);
   request.position  = ticket;
   request.sl        = ::NormalizeDouble(stop_loss, this.m_digits);
   
//--- если торговую операцию отправить не удалось - сообщаем об этом в журнал и возвращаем false
   if(!::OrderSend(request, result))
     {
      PrintFormat("%s: OrderSend() failed to modify position #%I64u. Error %d",__FUNCTION__, ticket, ::GetLastError());
      return false;
     }
     
//--- успешно отправлен запрос на изменение StopLoss позиции
   return true;
  }


Для получения уровня стопов используем метод StopLevel():

//+------------------------------------------------------------------+
//| Возвращает размер StopLevel в пунктах                            |
//+------------------------------------------------------------------+
int CSimpleTrailing::StopLevel(void)
  {
   int spread     = (int)::SymbolInfoInteger(this.m_symbol, SYMBOL_SPREAD);
   int stop_level = (int)::SymbolInfoInteger(this.m_symbol, SYMBOL_TRADE_STOPS_LEVEL);
   return int(stop_level == 0 ? spread * this.m_spread_mlt : stop_level);
  }

Так как уровень стопов — величина не постоянная и может динамически изменяться, то каждый раз при обращении к методу запрашиваем нужные значения из свойств символа. Если StopLevel символа установлен равным нулю, то используем значение спреда символа, умноженного на 2. Это значение установлено по умолчанию.

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

Основным же методом трала является метод Run(), запускающий цикл выбора позиций и перемещение их стопов на рассчитанные значения:

//+------------------------------------------------------------------+
//| Запускает простой трейлинг с отступом StopLoss от цены           |
//+------------------------------------------------------------------+
bool CSimpleTrailing::Run(void)
  {
//--- если отключен - уходим
   if(!this.m_active)
      return false;
//--- переменные трала
   MqlTick tick = {};                           // структура цен
   bool    res  = true;                         // результат модификации всех позиций
//--- проверка корректности данных по символу
   if(this.m_point==0)
     {
      //--- попробуем ещё раз получить данные
      ::ResetLastError();
      this.SetSymbol(this.m_symbol);
      if(this.m_point==0)
        {
         ::PrintFormat("%s: Correct data was not received for the %s symbol. Error %d",__FUNCTION__, this.m_symbol, ::GetLastError());
         return false;
        }
     }
     
//--- в цикле по общему количеству открытых позиций
   int total =::PositionsTotal();
   for(int i = total - 1; i >= 0; i--)
     {
      //--- получаем тикет очередной позиции
      ulong  pos_ticket =::PositionGetTicket(i);
      if(pos_ticket == 0)
         continue;
         
      //--- получаем символ и магик позиции
      string pos_symbol = ::PositionGetString(POSITION_SYMBOL);
      long   pos_magic  = ::PositionGetInteger(POSITION_MAGIC);
      
      //--- если позиция не соответствует фильтру по символу и магику - уходим
      if((this.m_magic != -1 && pos_magic != this.m_magic) || (pos_symbol != this.m_symbol))
         continue;
      
      //--- если цены получить не удалось - идём дальше
      if(!::SymbolInfoTick(this.m_symbol, tick))
         continue;

      //--- получаем тип позиции, цену её открытия и уровень StopLoss
      ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE);
      double             pos_open =::PositionGetDouble(POSITION_PRICE_OPEN);
      double             pos_sl   =::PositionGetDouble(POSITION_SL);
      
      //--- получаем рассчитанный уровень StopLoss
      double value_sl = this.GetStopLossValue(pos_type, tick);
      
      //--- если условия для модификации StopLoss подходят - модифицируем стоп позиции и добавляем результат к переменной res
      if(this.CheckCriterion(pos_type, pos_open, pos_sl, value_sl, tick))
         res &=this.ModifySL(pos_ticket, value_sl);
     }
//--- по окончании цикла возвращаем результат модификации каждой подходящей по фильтру "символ/магик" позиции
   return res;
  }

Метод перебирает позиции в списке действующих позиций терминала, фильтрует их по символу и магику и перемещает стопы позиций на рассчитанную дистанцию от текущей цены. Результат модификации StopLoss каждой позиции добавляется к переменной res. По окончании цикла модификации стопов всех позиций, в переменной будет записан либо флаг true — только при успешной модификации стопов всех позиций, подходящих по фильтру символ/магик. Либо, если хоть одна позиция не была успешно модифицирована, флаг будет содержать значение false. Сделано это для дополнительного контроля модификации стопов извне.

Теперь подключим этот класс к советнику для проверки его работоспособности.

Для теста возьмём советник Moving Average.mq5 из папки \MQL5\Experts\Examples\ терминала и сохраним его под именем MovingAverageWithSimpleTrail.mq5.

Подключим к советнику файл класса трейлинга, впишем дополнительные параметры в настройки и создадим экземпляр класса простого трала:

//+------------------------------------------------------------------+
//|                                 MovingAverageWithSimpleTrail.mq5 |
//|                             Copyright 2000-2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2000-2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include <Trade\Trade.mqh>
#include <Trailings\Trailings.mqh>

input group  " - Moving Averages Expert Parameters -"
input double MaximumRisk        = 0.02;    // Maximum Risk in percentage
input double DecreaseFactor     = 3;       // Descrease factor
input int    MovingPeriod       = 12;      // Moving Average period
input int    MovingShift        = 6;       // Moving Average shift

input group  " - Simple Trailing Parameters -"
input int   InpTrailingStart  =  10;      // Trailing start
input int   InpTrailingStep   =  20;      // Trailing step in points
input int   InpTrailingOffset =  30;      // Trailing offset in points
input bool  InpUseTrailing    =  true;    // Use Trailing Stop

//---
int    ExtHandle=0;
bool   ExtHedging=false;
CTrade ExtTrade;

CSimpleTrailing ExtTrailing;              // экземпляр класса простого трала

#define MA_MAGIC 1234501
//+------------------------------------------------------------------+


В обработчике OnInit() установим параметры трала:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- prepare trade class to control positions if hedging mode is active
   ExtHedging=((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
   ExtTrade.SetExpertMagicNumber(MA_MAGIC);
   ExtTrade.SetMarginMode();
   ExtTrade.SetTypeFillingBySymbol(Symbol());
//--- Moving Average indicator
   ExtHandle=iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE);
   if(ExtHandle==INVALID_HANDLE)
     {
      printf("Error creating MA indicator");
      return(INIT_FAILED);
     }
     
//--- устанавливаем параметры трала
   ExtTrailing.SetActive(InpUseTrailing);
   ExtTrailing.SetSymbol(Symbol());
   ExtTrailing.SetMagicNumber(MA_MAGIC);
   ExtTrailing.SetTrailingStart(InpTrailingStart);
   ExtTrailing.SetTrailingStep(InpTrailingStep);
   ExtTrailing.SetStopLossOffset(InpTrailingOffset);
//--- ok
   return(INIT_SUCCEEDED);
  }


В обработчике OnTick() запустим трейлинг:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void)
  {
//---
   if(SelectPosition())
      CheckForClose();
   else
      CheckForOpen();
      
//--- запускаем трейлинг
   ExtTrailing.Run();
  }


Это всё, что нужно вписать дополнительно в файл советника. Скомпилируем его и запустим в тестере стратегий на графике EURUSD, M15, одиночный тест за последний год. Все тики без задержек исполнения.

Параметры установим такими (трал отключен):


Запустим тест с отключенным тралом и посмотрим результат:



Теперь включим использование трала:


и запустим точно такой же тест.

Результат тестирования получили такой:



Результат немного лучше. Видно, что трал немного улучшает статистику на этих настройках.

Теперь на основе созданного простого трала можно делать любые другие трейлинги.

Сделаем несколько классов тралов по индикаторам — по индикатору Parabolic SAR и по различным скользящим средним из списка трендовых индикаторов, представленных в клиентском терминале.


Классы тралов по индикаторам

Продолжим писать код в том же файле. Но прежде чем начать новый класс трала по индикатору, вспомним, что для тралов по значениям индикаторов необходимо создавать сам индикатор, с данных которого будем получать значения уровней стоп-лосс. Для индикаторов — для их создания, необходимо указывать символ и таймфрейм, на данных которых индикатор строит свою серию данных. И ещё нужно сохранить хендл созданного индикатора в переменной — чтобы по этому хендлу обращаться к индикатору за получением данных. Получение данных по хендлу не зависит от типа индикатора — данные получаются функцией CopyBuffer() с указанием индикатора по хендлу.

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

Продолжим писать код в файле Trailings.mqh. Напишем базовый класс тралов по индикаторам:

//+------------------------------------------------------------------+
//| Базовый класс тралов по индикаторам                              |
//+------------------------------------------------------------------+
class CTrailingByInd : public CSimpleTrailing
  {
protected:
   ENUM_TIMEFRAMES   m_timeframe;            // таймфрейм индикатора
   int               m_handle;               // хэндл индикатора
   uint              m_data_index;           // бар данных индикатора
   string            m_timeframe_descr;      // описание таймфрейма

//--- возвращает данные индикатора
   double            GetDataInd(void)              const { return this.GetDataInd(this.m_data_index); }
   
//--- рассчитывает и возвращает уровень StopLoss выбранной позиции
   virtual double    GetStopLossValue(ENUM_POSITION_TYPE pos_type, MqlTick &tick);

public:
//--- установка параметров
   void              SetTimeframe(const ENUM_TIMEFRAMES timeframe)
                       {
                        this.m_timeframe=(timeframe==PERIOD_CURRENT ? ::Period() : timeframe);
                        this.m_timeframe_descr=::StringSubstr(::EnumToString(this.m_timeframe), 7);
                       }
   void              SetDataIndex(const uint index)                  { this.m_data_index=index;       }
                       
//--- возврат параметров
   ENUM_TIMEFRAMES   Timeframe(void)                           const { return this.m_timeframe;       }
   uint              DataIndex(void)                           const { return this.m_data_index;      }
   string            TimeframeDescription(void)                const { return this.m_timeframe_descr; }
     
//--- возвращает данные индикатора с указанного индекса таймсерии
   double            GetDataInd(const int index) const;
                      
//--- конструкторы
                     CTrailingByInd(void) : CSimpleTrailing(::Symbol(), -1, 0, 0, 0), m_timeframe(::Period()), 
                                            m_handle(INVALID_HANDLE), m_data_index(1), m_timeframe_descr(::StringSubstr(::EnumToString(::Period()), 7)) {}
                     
                     CTrailingByInd(const string symbol, const ENUM_TIMEFRAMES timeframe, const long magic, const int trail_start, const uint trail_step, const int trail_offset) :
                                    CSimpleTrailing(symbol, magic, trail_start, trail_step, trail_offset), m_data_index(1), m_handle(INVALID_HANDLE)
                       {
                        this.SetTimeframe(timeframe);
                       }
//--- деструктор
                    ~CTrailingByInd(void)
                       {
                        if(this.m_handle!=INVALID_HANDLE)
                          {
                           ::IndicatorRelease(this.m_handle);
                           this.m_handle=INVALID_HANDLE;
                          }
                       }
  };

Класс унаследован от класса простого трала, поэтому в нём доступен весь функционал этого класса, плюс реализованы методы для доступа к данным индикатора по хендлу.

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

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

В конструкторе по умолчанию, в его строке инициализации, в родительский класс передаётся текущий символ, магик со значением -1 (все позиции) и нулевые параметры для трала. В качестве таймфрейма графика для расчёта индикатора устанавливается период текущего графика.

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


Метод, возвращающий данные индикатора с указанного индекса таймсерии:

//+------------------------------------------------------------------+
//| Возвращает данные индикатора с указанного индекса таймсерии      |
//+------------------------------------------------------------------+
double CTrailingByInd::GetDataInd(const int index) const
  {
//--- если хендл невалидный - сообщаем об этом и возвращаем "пустое значение"
   if(this.m_handle==INVALID_HANDLE)
     {
      ::PrintFormat("%s: Error. Invalid handle",__FUNCTION__);
      return EMPTY_VALUE;
     }
//--- получаем значение буфера индикатора по указанному индексу
   double array[1];
   ::ResetLastError();
   if(::CopyBuffer(this.m_handle, 0, index, 1, array)!=1)
     {
      ::PrintFormat("%s: CopyBuffer() failed. Error %d", __FUNCTION__, ::GetLastError());
      return EMPTY_VALUE;
     }
//--- возвращаем полученное значение
   return array[0];
  }

При помощи CopyBuffer() получаем значение одного бара индикатора по указанному индексу и возвращаем его при успешном получении. При ошибке — выводим сообщение в журнал и возвращаем пустое значение (EMPTY_VALUE).

Функция CopyBuffer() работает с данными любого индикатора — по его хендлу. Таким образом, этот метод является универсальным и используется без изменений в каждом классе трейлингов по индикаторам.


Виртуальный метод, рассчитывающий и возвращающий уровень StopLoss выбранной позиции:

//+------------------------------------------------------------------+
//| Рассчитывает и возвращает уровень StopLoss выбранной позиции     |
//+------------------------------------------------------------------+
double CTrailingByInd::GetStopLossValue(ENUM_POSITION_TYPE pos_type, MqlTick &tick)
  {
//--- получаем значение индикатора как уровень для StopLoss
   double data=this.GetDataInd();
       
//--- в зависимости от типа позиции рассчитываем и возвращаем уровень StopLoss
   switch(pos_type)
     {
      case POSITION_TYPE_BUY  :  return(data!=EMPTY_VALUE ? data - this.m_offset * this.m_point : tick.bid);
      case POSITION_TYPE_SELL :  return(data!=EMPTY_VALUE ? data + this.m_offset * this.m_point : tick.ask);
      default                 :  return 0;
     }
  }

В этом методе, в родительском классе, просто выполняется расчёт отступа уровня стоп-лосс от цены. В этом же классе в этом методе необходимо получить данные от индикатора и передать их в качестве уровня StopLoss. Если данные от индикатора не были получены, то метод возвратит текущую цену, что не даст выставить стоп-лосс позиции, так как выставить стоп на текущую цену не позволит уровень StopLevel.

В итоге в этом классе реализован доступ к данным индикатора по его хендлу и расчёт уровня для StopLoss позиции по значению индикатора, и все его методы будут доступны в дочерних классах.

Теперь можно на основе созданного класса создать классы трейлингов по индикаторам.


Класс трала по Parabolic SAR

Продолжим писать код в том же файле \MQL5\Include\Trailings\Trailings.mqh.

Класс трала по Parabolic SAR, как и все другие классы трейлингов по индикаторам, должен быть унаследован от базового класса тралов по индикаторам:

//+------------------------------------------------------------------+
//| Класс трала StopLoss позиций по Parabolic SAR                    |
//+------------------------------------------------------------------+
class CTrailingBySAR : public CTrailingByInd
  {
private:
   double            m_sar_step;             // параметр Step Parabolic SAR
   double            m_sar_max;              // параметр Maximum Parabolic SAR

public:
//--- установка параметров Parabolic SAR
   void              SetSARStep(const double step)                   { this.m_sar_step=(step<0.0001 ? 0.0001 : step);   }
   void              SetSARMaximum(const double max)                 { this.m_sar_max =(max <0.0001 ? 0.0001 : max);    }
   
//--- возврат параметров Parabolic SAR
   double            SARStep(void)                             const { return this.m_sar_step;                          }
   double            SARMaximum(void)                          const { return this.m_sar_max;                           }
   
//--- создаёт индикатор Parabolic SAR, возвращает результат
   bool              Initialize(const string symbol, const ENUM_TIMEFRAMES timeframe, const double sar_step, const double sar_maximum);

//--- конструкторы
                     CTrailingBySAR() : CTrailingByInd(::Symbol(), ::Period(), -1, 0, 0, 0)
                       {
                        this.Initialize(this.m_symbol, this.m_timeframe, 0.02, 0.2);
                       }
                     
                     CTrailingBySAR(const string symbol, const ENUM_TIMEFRAMES timeframe, const long magic,
                                    const double sar_step, const double sar_maximum,
                                    const int trail_start, const uint trail_step, const int trail_offset);
//--- деструктор
                    ~CTrailingBySAR(){}
  };

В приватной секции класса объявлены переменные для хранения значений параметров индикатора Parabolic SAR.

В публичной секции написаны методы для установки и возврата значений свойств индикатора, объявлен метод для создания индикатора и два конструктора — по умолчанию и параметрический.

В конструкторе по умолчанию создаётся индикатор на текущем символе и периоде графика, а тралу устанавливаются нулевые значения его параметров.

В параметрическом конструкторе все параметры передаются через входные переменные конструктора и далее создаётся индикатор на переданных в конструктор параметрах:

//+------------------------------------------------------------------+
//| Параметрический конструктор                                      |
//+------------------------------------------------------------------+
CTrailingBySAR::CTrailingBySAR(const string symbol, const ENUM_TIMEFRAMES timeframe, const long magic,
                               const double sar_step, const double sar_maximum,
                               const int trail_start, const uint trail_step, const int trail_offset) :
                               CTrailingByInd(symbol, timeframe, magic, trail_start, trail_step, trail_offset)
  {
   this.Initialize(symbol, timeframe, sar_step, sar_maximum);
  }


Метод Initialize() создаёт индикатор Parabolic SAR и возвращает результат его создания:

//+------------------------------------------------------------------+
//| создаёт индикатор Parabolic SAR, возвращает результат            |
//+------------------------------------------------------------------+
bool CTrailingBySAR::Initialize(const string symbol, const ENUM_TIMEFRAMES timeframe, const double sar_step, const double sar_maximum)
  {
   this.SetSymbol(symbol);
   this.SetTimeframe(timeframe);
   this.SetSARStep(sar_step);
   this.SetSARMaximum(sar_maximum);
   ::ResetLastError();
   this.m_handle=::iSAR(this.m_symbol, this.m_timeframe, this.m_sar_step, this.m_sar_max);
   if(this.m_handle==INVALID_HANDLE)
     {
      ::PrintFormat("Failed to create iSAR(%s, %s, %.3f, %.2f) handle. Error %d",
                    this.m_symbol, this.TimeframeDescription(), this.m_sar_step, this.m_sar_max, ::GetLastError());
     }
   return(this.m_handle!=INVALID_HANDLE);
  }


Протестируем созданный класс трала по Parabolic SAR.

Для теста возьмём советник из стандартной поставки \MQL5\Experts\Advisors\ExpertMACD.mq5, сохраним его под новым именем ExpertMACDWithTrailingBySAR.mq5 и добавим необходимые строки кода.

Подключим файл с классами тралов, добавим новые входные переменные и объявим экземпляр класса трейлинга:

//+------------------------------------------------------------------+
//|                                                   ExpertMACD.mq5 |
//|                             Copyright 2000-2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2000-2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Include                                                          |
//+------------------------------------------------------------------+
#include <Expert\Expert.mqh>
#include <Expert\Signal\SignalMACD.mqh>
#include <Expert\Trailing\TrailingNone.mqh>
#include <Expert\Money\MoneyNone.mqh>

#include <Trailings\Trailings.mqh>

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
//--- inputs for expert
input group  " - ExpertMACD Parameters -"
input string Inp_Expert_Title            ="ExpertMACD";
int          Expert_MagicNumber          =10981;
bool         Expert_EveryTick            =false;
//--- inputs for signal
input int    Inp_Signal_MACD_PeriodFast  =12;
input int    Inp_Signal_MACD_PeriodSlow  =24;
input int    Inp_Signal_MACD_PeriodSignal=9;
input int    Inp_Signal_MACD_TakeProfit  =50;
input int    Inp_Signal_MACD_StopLoss    =20;

input group  " - Trailing By SAR Parameters -"
input ENUM_TIMEFRAMES   InpTimeframeSAR   =  PERIOD_CURRENT;   // Parabolic SAR Timeframe
input double            InpStepSAR        =  0.02;             // Parabolic SAR Step
input double            InpMaximumSAR     =  0.2;              // Parabolic SAR Maximum
input int               InpTrailingStart  =  0;                // Trailing Start
input uint              InpTrailingStep   =  0;                // Trailing Step
input int               InpTrailingOffset =  0;                // Trailing Offset
input bool              InpUseTrailing    =  true;             // Use Trailing Stop

//+------------------------------------------------------------------+
//| Global expert object                                             |
//+------------------------------------------------------------------+
CExpert ExtExpert;
CTrailingBySAR ExtTrailing;


В обработчике OnInit() инициализируем трал по параболику:

//+------------------------------------------------------------------+
//| Initialization function of the expert                            |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- Initializing trailing
   if(ExtTrailing.Initialize(NULL,PERIOD_CURRENT,InpStepSAR,InpMaximumSAR))
     {
      ExtTrailing.SetMagicNumber(Expert_MagicNumber);
      ExtTrailing.SetTrailingStart(InpTrailingStart);
      ExtTrailing.SetTrailingStep(InpTrailingStep);
      ExtTrailing.SetStopLossOffset(InpTrailingOffset);
      ExtTrailing.SetActive(InpUseTrailing);
     }
   
//--- Initializing expert


В обработчике OnTick() запустим трал в работу:

//+------------------------------------------------------------------+
//| Function-event handler "tick"                                    |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   ExtExpert.OnTick();
   ExtTrailing.Run();
  }


Скомпилируем советник и запустим в тестере с параметрами по умолчанию:



Видим, как стопы позиций исправно тралятся по значениям первого бара Parabolic SAR.

Теперь создадим классы тралов по различным скользящим средним, представленным в клиентском терминале.


Классы тралов по скользящим средним

Эти классы отличаются от класса трейлинга по Parabolic SAR только набором входных параметров и тем, что в методе Initialize() создаётся соответствующий классу индикатор.

Исходя из этого, рассмотрим лишь один класс: класс трейлинга по адаптивной скользящей средней:

//+------------------------------------------------------------------+
//| Класс трала StopLoss позиций по Adaptive Moving Average          |
//+------------------------------------------------------------------+
class CTrailingByAMA : public CTrailingByInd
  {
private:
   int               m_period;               // параметр Period AMA
   int               m_fast_ema;             // параметр Fast EMA Period
   int               m_slow_ema;             // параметр Slow EMA Period
   int               m_shift;                // параметр Shift AMA
   ENUM_APPLIED_PRICE m_price;               // параметр Applied Price AMA

public:
//--- установка параметров AMA
   void              SetPeriod(const uint period)                    { this.m_period=int(period<1 ? 9 : period);     }
   void              SetFastEMAPeriod(const uint period)             { this.m_fast_ema=int(period<1 ? 2 : period);   }
   void              SetSlowEMAPeriod(const uint period)             { this.m_slow_ema=int(period<1 ? 30 : period);  }
   void              SetShift(const int shift)                       { this.m_shift = shift;                         }
   void              SetPrice(const ENUM_APPLIED_PRICE price)        { this.m_price = price;                         }
   
//--- возврат параметров AMA
   int               Period(void)                              const { return this.m_period;                         }
   int               FastEMAPeriod(void)                       const { return this.m_fast_ema;                       }
   int               SlowEMAPeriod(void)                       const { return this.m_slow_ema;                       }
   int               Shift(void)                               const { return this.m_shift;                          }
   ENUM_APPLIED_PRICE Price(void)                              const { return this.m_price;                          }
   
//--- создаёт индикатор AMA, возвращает результат
   bool              Initialize(const string symbol, const ENUM_TIMEFRAMES timeframe,
                                const int period, const int fast_ema, const int slow_ema, const int shift, const ENUM_APPLIED_PRICE price);
   
//--- конструкторы
                     CTrailingByAMA() : CTrailingByInd(::Symbol(), ::Period(), -1, 0, 0, 0)
                       {
                        this.Initialize(this.m_symbol, this.m_timeframe, 9, 2, 30, 0, PRICE_CLOSE);
                       }
                     
                     CTrailingByAMA(const string symbol, const ENUM_TIMEFRAMES timeframe, const long magic,
                                    const int period, const int fast_ema, const int slow_ema, const int shift, const ENUM_APPLIED_PRICE price,
                                    const int trail_start, const uint trail_step, const int trail_offset);
//--- деструктор
                    ~CTrailingByAMA(){}
  };
//+------------------------------------------------------------------+
//| Параметрический конструктор                                      |
//+------------------------------------------------------------------+
CTrailingByAMA::CTrailingByAMA(const string symbol, const ENUM_TIMEFRAMES timeframe, const long magic,
                               const int period, const int fast_ema, const int slow_ema, const int shift, const ENUM_APPLIED_PRICE price,
                               const int trail_start, const uint trail_step, const int trail_offset) :
                               CTrailingByInd(symbol, timeframe, magic, trail_start, trail_step, trail_offset)
  {
   this.Initialize(symbol, timeframe, period, fast_ema, slow_ema, shift, price);
  }
//+------------------------------------------------------------------+
//| создаёт индикатор AMA, возвращает результат                      |
//+------------------------------------------------------------------+
bool CTrailingByAMA::Initialize(const string symbol, const ENUM_TIMEFRAMES timeframe,
                                const int period, const int fast_ema, const int slow_ema, const int shift, const ENUM_APPLIED_PRICE price)
  {
   this.SetSymbol(symbol);
   this.SetTimeframe(timeframe);
   this.SetPeriod(period);
   this.SetFastEMAPeriod(fast_ema);
   this.SetSlowEMAPeriod(slow_ema);
   this.SetShift(shift);
   this.SetPrice(price);
   ::ResetLastError();
   this.m_handle=::iAMA(this.m_symbol, this.m_timeframe, this.m_period, this.m_fast_ema, this.m_slow_ema, this.m_shift, this.m_price);
   if(this.m_handle==INVALID_HANDLE)
     {
      ::PrintFormat("Failed to create iAMA(%s, %s, %d, %d, %d, %s) handle. Error %d",
                    this.m_symbol, this.TimeframeDescription(), this.m_period, this.m_fast_ema, this.m_slow_ema,
                    ::StringSubstr(::EnumToString(this.m_price),6), ::GetLastError());
     }
   return(this.m_handle!=INVALID_HANDLE);
  }

Класс абсолютно идентичен выше рассмотренному классу трала по Parabolic SAR. В каждом классе тралов по скользящим средним будет собственный набор переменных для хранения параметров индикатора и соответствующие этим переменным методы установки и возврата значений. Все классы тралов по скользящим средним идентичны друг другу, и изучить их можно, посмотрев коды в прикреплённом к статье файлу классов трейлингов Trailings.mqh. Протестировать работу класса трейлинга по простой скользящей средней можно, запустив тестовый советник из файла ExpertMACDWithTrailingByMA.mq5, также прикреплённый в конце статьи.

Мы создали классы простого трала и тралов по стандартным индикаторам, представленным в терминале, и подходящим для указания уровней для размещения StopLoss позиций.

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

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


Класс трала по указанным значениям уровней StopLoss

Всё, что необходимо сделать для создания трала по указанным значениям — это вместо параметров индикатора определить переменные для записи в них значений стоп-лосс для длинных и коротких позиций. А вместо получения данных от индикатора, нужно просто рассчитывать значения для StopLoss позиций от значений, указанных в этих переменных. Значения в переменные должны заноситься из управляющей программы, непосредственно при вызове метода Run() трала.

Напишем такой класс:

//+------------------------------------------------------------------+
//| Класс трала по указанному значению                               |
//+------------------------------------------------------------------+
class CTrailingByValue : public CSimpleTrailing
  {
protected:
   double            m_value_sl_long;     // значение уровня StopLoss длинных позиций
   double            m_value_sl_short;    // значение уровня StopLoss коротких позиций

//--- рассчитывает и возвращает уровень StopLoss выбранной позиции
   virtual double    GetStopLossValue(ENUM_POSITION_TYPE pos_type, MqlTick &tick);

public:
//--- возвращает уровень StopLoss (2) длинных, (2) коротких позиций
   double            StopLossValueLong(void)    const { return this.m_value_sl_long;   }
   double            StopLossValueShort(void)   const { return this.m_value_sl_short;  }

//--- запуск трала с указанным отступом StopLoss от цены
   bool              Run(const double value_sl_long, double value_sl_short);

//--- конструкторы
                     CTrailingByValue(void) : CSimpleTrailing(::Symbol(), -1, 0, 0, 0), m_value_sl_long(0), m_value_sl_short(0) {}
                     
                     CTrailingByValue(const string symbol, const long magic, const int trail_start, const uint trail_step, const int trail_offset) :
                                      CSimpleTrailing(symbol, magic, trail_start, trail_step, trail_offset), m_value_sl_long(0), m_value_sl_short(0) {}
//--- деструктор
                    ~CTrailingByValue(void){}
  };

Видно, что класс практически идентичен всем вышерассмотренным классам трейлингов по индикаторам. Но здесь нет метода Initialize(), так как никакой индикатор создавать не требуется.

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

//+------------------------------------------------------------------+
//| Рассчитывает и возвращает уровень StopLoss выбранной позиции     |
//+------------------------------------------------------------------+
double CTrailingByValue::GetStopLossValue(ENUM_POSITION_TYPE pos_type, MqlTick &tick)
  {
//--- в зависимости от типа позиции рассчитываем и возвращаем уровень StopLoss
   switch(pos_type)
     {
      case POSITION_TYPE_BUY  :  return(this.m_value_sl_long  - this.m_offset * this.m_point);
      case POSITION_TYPE_SELL :  return(this.m_value_sl_short + this.m_offset * this.m_point);
      default                 :  return 0;
     }
  }


В методе Run(), посредством его формальных параметров передаются в класс значения стоплосс для длинных и для коротких позиций:

//+------------------------------------------------------------------+
//| Запуск трала с указанным отступом StopLoss от цены               |
//+------------------------------------------------------------------+
bool CTrailingByValue::Run(const double value_sl_long, double value_sl_short)
  {
   this.m_value_sl_long =value_sl_long;
   this.m_value_sl_short=value_sl_short;
   return CSimpleTrailing::Run();
  }

Сначала в переменные-члены класса записываются переданные в формальных параметрах значения, а затем вызывается метод Run() родительского класса. А уже из родительского класса будет вызван виртуальный метод GetStopLossValue(), переопределённый в этом классе, и стопы позиций будут устанавливаться на рассчитанные в нём значения.


Подключаем трейлинг-стоп к советнику

Как подключить трал к советнику мы уже рассматривали выше, при тестировании трейлингов по индикаторам. Рассмотрим теперь, как подключить и запустить трал по передаваемым в него значениям, а именно — по High и Low свечей.

Трал подключим к советнику из стандартной поставки \MQL5\Experts\Advisors\ExpertMACD.mq5. Сохраним его под новым именем ExpertMACDWithTrailingByValue.mq5 и внесём необходимые доработки.

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

//+------------------------------------------------------------------+
//|                                                   ExpertMACD.mq5 |
//|                             Copyright 2000-2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2000-2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Include                                                          |
//+------------------------------------------------------------------+
#include <Expert\Expert.mqh>
#include <Expert\Signal\SignalMACD.mqh>
#include <Expert\Trailing\TrailingNone.mqh>
#include <Expert\Money\MoneyNone.mqh>

#include <Trailings\Trailings.mqh>

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
//--- inputs for expert
input group  " - ExpertMACD Parameters -"
input string Inp_Expert_Title            ="ExpertMACD";
int          Expert_MagicNumber          =10981;
bool         Expert_EveryTick            =false;
//--- inputs for signal
input int    Inp_Signal_MACD_PeriodFast  =12;
input int    Inp_Signal_MACD_PeriodSlow  =24;
input int    Inp_Signal_MACD_PeriodSignal=9;
input int    Inp_Signal_MACD_TakeProfit  =50;
input int    Inp_Signal_MACD_StopLoss    =20;

input group  " - Trailing By Value Parameters -"
input ENUM_TIMEFRAMES   InpTimeframe      =  PERIOD_CURRENT;   // Data Rates Timeframe
input uint              InpDataRatesIndex =  2;                // Data Rates Index for StopLoss
input uint              InpTrailingStep   =  0;                // Trailing Step
input int               InpTrailingStart  =  0;                // Trailing Start
input int               InpTrailingOffset =  0;                // Trailing Offset
input bool              InpUseTrailing    =  true;             // Use Trailing Stop

//+------------------------------------------------------------------+
//| Global expert object                                             |
//+------------------------------------------------------------------+
CExpert ExtExpert;
CTrailingByValue ExtTrailing;

Входная переменная Data Rates Timeframe — это период графика, с которого будут браться цены High для стопов коротких, и Low — для стопов длинных позиций.

Входная переменная Data Rates Index for StopLoss — индекс бара на графике с периодом Data Rates Timeframe, с которого будут браться цены High и Low для установки StopLoss позиций.


В обработчике OnInit() советника инициализируем параметры трала:

//+------------------------------------------------------------------+
//| Initialization function of the expert                            |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- Initializing trailing
   ExtTrailing.SetMagicNumber(Expert_MagicNumber);
   ExtTrailing.SetTrailingStart(InpTrailingStart);
   ExtTrailing.SetTrailingStep(InpTrailingStep);
   ExtTrailing.SetStopLossOffset(InpTrailingOffset);
   ExtTrailing.SetActive(InpUseTrailing);
   
//--- Initializing expert

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


В обработчике OnTick() советника получим данные бара с индексом, указанным во входной переменной InpDataRatesIndex, и запускаем трал с указанием цен StopLoss (High и Low бара) для длинных и коротких позиций:

//+------------------------------------------------------------------+
//| Function-event handler "tick"                                    |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   ExtExpert.OnTick();
   MqlRates rates[1]={};
   if(CopyRates(ExtTrailing.Symbol(),InpTimeframe,InpDataRatesIndex,1,rates))
      ExtTrailing.Run(rates[0].low,rates[0].high);
  }

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

Скомпилируем советник и запустим его в визуальном режиме тестера с настройками по умолчанию:

Видно, как стопы позиций выставляются на значения High и Low свечей, имеющих индекс в таймсерии 2.

Насколько такой трал может дать выигрыш в торговой системе — это уже предмет самостоятельного тестирования.

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


Заключение

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

В примерах был рассмотрен лишь один способ работы с объектами классов — создание экземпляра объекта класса трейлинга. Такой подход даёт возможность заранее в программе определить, какие типы тралов потребуются, и какие параметры тралов должны быть. Для каждого трала создаётся один объект в глобальной области. Такой подход имеет право на использование — он прост и понятен. Но чтобы сделать динамическое создание объектов тралов при помощи оператора создания объекта new прямо во время выполнения программы, лучше воспользоваться возможностями, предоставляемыми Стандартной Библиотекой для создания связанных списков объектов. Именно для этих целей все классы трейлингов являются наследниками базового объекта Стандартной Библиотеки CObject. Этот подход можно будет обсудить в комментариях к статье, так как уже выходит за рамки данной темы.

Все представленные в статье классы можно использовать "как есть" в своих собственных разработках, а можно доработать под свои нужды и задачи.


Разрабатываем мультивалютный советник (Часть 11): Начало автоматизации процесса оптимизации Разрабатываем мультивалютный советник (Часть 11): Начало автоматизации процесса оптимизации
Для получения хорошего советника нам надо подобрать для него множество хороших наборов параметров экземпляров торговых стратегий. Это можно делать вручную, запуская оптимизацию на разных символах, и затем отбирая лучшие результаты. Но лучше поручить эту работу программе и заняться более продуктивной деятельностью.
Алгоритм кометного следа (Comet Tail Algorithm, CTA) Алгоритм кометного следа (Comet Tail Algorithm, CTA)
В данной статье мы рассмотрим новый авторский алгоритм оптимизации CTA (Comet Tail Algorithm), который черпает вдохновение из уникальных космических объектов - комет и их впечатляющих хвостов, формирующихся при приближении к Солнцу. Данный алгоритм основан на концепции движения комет и их хвостов, и предназначен для поиска оптимальных решений в задачах оптимизации.
Разработка и тестирование торговых систем Aroon Разработка и тестирование торговых систем Aroon
В этой статье мы узнаем, как построить торговую систему Aroon, изучив основы индикаторов и необходимые шаги для создания торговой системы на основе индикатора Aroon. После создания этой торговой системы мы проверим, может ли она быть прибыльной или требует дополнительной оптимизации.
Нейросети — это просто (Часть 90): Частотная интерполяция временных рядов (FITS) Нейросети — это просто (Часть 90): Частотная интерполяция временных рядов (FITS)
При изучении метода FEDformer мы приоткрыли дверь в частотную область представления временного ряда. В новой статье мы продолжим начатую тему. И рассмотрим метод, позволяющий не только проводить анализ, но и прогнозировать последующие состояния в частной области.