preview
Как добавить Trailing Stop по индикатору Parabolic SAR

Как добавить Trailing Stop по индикатору Parabolic SAR

MetaTrader 5Примеры | 29 апреля 2024, 16:42
657 3
Artyom Trishkin
Artyom Trishkin

Содержание



Введение

Трейлинг-стоп — функция, хорошо знакомая большинству трейдеров. Эта функция интегрирована в торговый терминал MetaTrader 5 и автоматически регулирует уровень StopLoss, поддерживая его на определённом расстоянии от текущей цены:

Включение стандартного Trailing Stop в MetaTrader 5


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

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

Есть функция-программа, которой передаётся требуемая цена для установки уровня StopLoss. Программа проверяет некоторые запрещающие факторы, такие как уровень StopLevel — дистанция, ближе которой стопы ставить нельзя, уровень FreezeLevel — дистанция заморозки, в пределах которой нельзя модифицировать позицию или отложенный ордер. Т.е. если цена подошла к стоп-уровню позиции на дистанцию ближе, чем уровень FreezeLevel, то ожидается срабатывание стоп-приказа, и модификация запрещена. У тралов есть ещё некоторые индивидуальные настройки параметров, которые тоже проверяются перед переносом уровня стоплосс на указанную цену, например символ и магик позиции. Все эти критерии проверяются непосредственно перед смещением StopLoss позиции на указанный уровень.

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

И вот таким "указателем" требуемых для StopLoss уровней как нельзя лучше подойдёт индикатор Parabolic SAR.



Индикатор Parabolic SAR (Stop and Reverse) — это популярный инструмент в техническом анализе, который используется для определения моментов возможного окончания и разворота текущего тренда. Этот индикатор разработан Уэллсом Уайлдером и часто используется для автоматического трейлинга стоп-лоссов. Вот основные причины, почему индикатор Parabolic SAR привлекателен для подтягивания защитного стопа:

  1. Легкость интерпретации: Parabolic SAR легко интерпретировать, так как он представлен на графике в виде точек, которые размещаются выше или ниже цены. Когда точки находятся ниже цен, это сигнал к покупке; когда точки выше – это сигнал к продаже.

  2. Автоматическое следование за ценой: Основное преимущество Parabolic SAR заключается в его способности автоматически адаптироваться к изменениям цены и перемещаться с течением времени. Это делает его идеальным инструментом для установки трейлинг стопов, так как он обеспечивает защиту прибыли, подтягивая стоп-лосс ближе к текущей цене по мере движения тренда.

  3. Защита прибыли: По мере того как цена движется в сторону прибыли открытой позиции, Parabolic SAR подтягивает уровень стоп-лосса, что помогает защитить часть накопленной прибыли от возможного разворота тренда.

  4. Сигналы для выхода: Кроме функции трейлинг стопа, Parabolic SAR также может служить сигналом для закрытия позиции, когда точки индикатора пересекают цену. Это может предотвратить дальнейшие потери при быстром изменении тренда.

  5. Простота настройки: Параметры Parabolic SAR (шаг и максимум) могут быть легко настроены для адаптации к конкретной волатильности рынка или торговой стратегии. Это делает его универсальным инструментом для различных торговых условий.

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

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


Трал по Parabolic SAR

Давайте рассмотрим структурную схему любого трала.

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

  1. блок расчёта требуемого уровня StopLoss; полученное значение передаётся в блок Trailing StopLoss.
  2. блок Trailing StopLoss, включает в себя
    1. блок фильтров
    2. блок установки StopLoss на полученное значение из блока расчёта требуемого уровня StopLoss, включает в себя
      1. блок фильтров на соблюдение условий сервера по уровням для символа и на соблюдение условий для смещения StopLoss.
      2. блок модификации значения StopLoss.

Блок расчёта требуемого уровня StopLoss — это в данном конкретном случае индикатор Parabolic SAR. Его значение, обычно с бара 1, на каждом тике отправляется в блок Trailing StopLoss, где в цикле по списку открытых позиций каждая выбранная позиция, её свойства, проходят через блок фильтров — обычно по символу и магику. Далее, если фильтры по символу/магику пройдены, то необходимый уровень StopLoss подвергается дополнительной фильтрации на соблюдение условий по уровню StopLevel сервера, на шаг трала, значению требуемого StopLoss относительно прошлого его положения и критерию запуска трала по прибыли позиции в пунктах. Если и эти фильтры пройдены, то StopLoss позиции модифицируется для установки на новый уровень.

Таким образом, блок расчёта уровня стоплосс у нас уже готов — это индикатор Parabolic SAR. Значит, необходимо сделать только блок смещения уровней StopLoss позиций, выбранных по текущему символу и идентификатору эксперта (Magic Number). Если значение магика будет задано как -1, то тралиться будут любые позиции, открытые по символу графика. Если же будет указан магик, то в этом случае тралиться будут только позиции с соответствующим магиком. Запускаться функция трала будет только при открытии нового бара, либо при открытии новой позиции. Пример оформим как советник

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

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

В папке терминала \MQL5\Experts\ создадим новый файл советника с наименованием TrailingBySAR_01.mq5.

На втором шаге работы мастера создания нового файла советника в появившемся окне поставим галочку на обработчике OnTradeTransaction():


Обработчик OnTradeTransaction() потребуется для запуска трала в момент открытия новой позиции.

Обработчик вызывается при наступлении события TradeTransaction где есть и открытие новой позиции.

Подробнее про торговые транзакции и события можно почитать в статье "С чего начать при создании торгового робота для Московской биржи MOEX".

В созданный файл советника добавим такие входные параметры и глобальные переменные:

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

#define   SAR_DATA_INDEX   1  // бар, с которого получаем данные Parabolic SAR

//--- input 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

//--- global variables
int      ExtHandleSAR =INVALID_HANDLE// хендл Parabolic SAR
double   ExtStepSAR   =0;                 // шаг Parabolic SAR
double   ExtMaximumSAR=0;                 // максимум Parabolic SAR

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 

  }
//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {

  }


В обработчике OnInit() советника установим корректные значения для введённых в настройках параметров индикатора, создадим индикатор и получим его хендл:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- устанавливаем параметры Parabolic SAR в допустимых пределах
   ExtStepSAR   =(InpStepSAR<0.0001 ? 0.0001 : InpStepSAR);
   ExtMaximumSAR=(InpMaximumSAR<0.0001 ? 0.0001 : InpMaximumSAR);

//--- при ошибке создания индикатора выводим сообщение в журнал и выходим с ошибкой из OnInit
   ExtHandleSAR =iSAR(Symbol(), InpTimeframeSAR, ExtStepSAR, ExtMaximumSAR);
   if(ExtHandleSAR==INVALID_HANDLE)
     {
      PrintFormat("Failed to create iSAR(%s, %s, %.3f, %.2f) handle. Error %d",
                  Symbol(), TimeframeDescription(InpTimeframeSAR), ExtStepSAR, ExtMaximumSAR, GetLastError());
      return(INIT_FAILED);
     }
//--- успешно
   return(INIT_SUCCEEDED);
  }

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

//+------------------------------------------------------------------+
//| Возвращает описание таймфрейма                                   |
//+------------------------------------------------------------------+
string TimeframeDescription(const ENUM_TIMEFRAMES timeframe)
  {
   return(StringSubstr(EnumToString(timeframe==PERIOD_CURRENT ? Period() : timeframe), 7));
  }

Получаем часть текста перечисления из константы таймфрейма и в итоге получаем строку, содержащую только название таймфрейма.
Например, из константы PERIOD_H1 получаем строку "PERIOD_H1", а из неё берём и возвращаем только "H1".

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

//+------------------------------------------------------------------+
//| Возвращает время открытия бара по индексу таймсерии              |
//+------------------------------------------------------------------+
datetime TimeOpenBar(const int index)
  {
   datetime array[1];
   ResetLastError();
   if(CopyTime(NULL, PERIOD_CURRENT, index, 1, array)!=1)
     {
      PrintFormat("%s: CopyTime() failed. Error %d", __FUNCTION__, GetLastError());
      return 0;
     }
   return array[0];
  }

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

Теперь, используя эту функцию, напишем функцию, возвращающую флаг открытия нового бара:

//+------------------------------------------------------------------+
//| Возвращает флаг открытия нового бара таймсерии                   |
//+------------------------------------------------------------------+
bool IsNewBar(void)
  {
   static datetime time_prev=0;
   datetime        bar_open_time=TimeOpenBar(0);
   if(bar_open_time==0)
      return false;
   if(bar_open_time!=time_prev)
     {
      time_prev=bar_open_time;
      return true;
     }
   return false;
  }

Сравниваем прошлое время открытия нулевого бара с текущим, полученным из функции TimeOpenBar(). Если сравниваемые значения не равны, то запоминаем новое время для следующей проверки и возвращаем флаг true открытия нового бара. При ошибке, или при равенстве сравниваемых значений, возвращаем false — нет нового бара.

Для получения данных от Parabolic SAR и отправке значений в функцию трейлинга, напишем функцию получения данных по хендлу индикатора:

//+------------------------------------------------------------------+
//| Возвращает данные Parabolic SAR с указанного индекса таймсерии   |
//+------------------------------------------------------------------+
double GetSARData(const int index)
  {
   double array[1];
   ResetLastError();
   if(CopyBuffer(ExtHandleSAR, 0, index, 1, array)!=1)
     {
      PrintFormat("%s: CopyBuffer() failed. Error %d", __FUNCTION__, GetLastError());
      return EMPTY_VALUE;
     }
   return array[0];
  }

Здесь всё точно так же, как и в функции получения времени открытия бара: получаем значение в массив с размерностью 1 по переданному в функцию индексу и при успешном получении возвращаем значение из массива. При ошибке — возвращаем "пустое" значение EMPTY_VALUE.

Для установки StopLoss позиции нужно проверить, что дистанция стопа от цены не находится в пределах, установленных уровнем StopLevel символа. Если цена StopLoss окажется ближе к цене, чем это разрешено дистанцией StopLevel, то стоп позиции установить не выйдет — будет выдана ошибка "неправильные стопы". Во избежание получения таких ошибок, нужно проверять эту дистанцию перед установкой стопа позиции. Есть ещё один уровень — уровень заморозки (FreezeLevel), который указывает дистанцию от цены до стопа позиции (StopLoss или TakeProfit), внутри которой нельзя изменять уровни стопов, так как вероятно их срабатывание. Но в подавляющем большинстве случаев эти уровни уже не используются, и здесь мы не будем делать их проверку.

Что же касается уровней StopLevel, то существует нюанс: если уровень задан как 0, то это не означает его отсутствие. Это означает плавающие значения этого уровня, и они чаще всего равны двум значениям спреда. Иногда трём. Тут нужно подбирать значения, так как они зависят от настроек сервера. Для этого в функции получения значения StopLevel сделаем настраиваемый параметр. В функцию будет передаваться множитель, на который нужно умножить спред по символу для получения уровня StopLevel в том случае, если StopLevel задан нулевым. Если же значение StopLevel установлено не нулевым, то просто возвращается это значение:

//+------------------------------------------------------------------+
//| Возвращает размер StopLevel текущего символа в пунктах           |
//+------------------------------------------------------------------+
int StopLevel(const int spread_multiplier)
  {
   int spread    =(int)SymbolInfoInteger(Symbol(), SYMBOL_SPREAD);
   int stop_level=(int)SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL);
   return(stop_level==0 ? spread * spread_multiplier : stop_level);
  }


Напишем основную функцию трала:

//+------------------------------------------------------------------+
//| Функция трейлинга стопа по значению цены StopLoss                |
//+------------------------------------------------------------------+
void TrailingStopByValue(const double value_sl, const long magic=-1, const int trailing_step_pt=0, const int trailing_start_pt=0)
  {
//--- структура цен
   MqlTick tick={};
//--- в цикле по общему количеству открытых позиций
   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((magic!=-1 && pos_magic!=magic) || pos_symbol!=Symbol())
         continue;
         
      //--- если цены получить не удалось - идём далее
      if(!SymbolInfoTick(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 подходят - модифицируем стоп позиции
      if(CheckCriterion(pos_type, pos_open, pos_sl, value_sl, trailing_step_pt, trailing_start_pt, tick))
         ModifySL(pos_ticket, value_sl);
     }
  }

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

Функция для проверки критериев модификации стопов:

//+------------------------------------------------------------------+
//|Проверяет критерии модификации StopLoss позициии и возвращает флаг|
//+------------------------------------------------------------------+
bool CheckCriterion(ENUM_POSITION_TYPE pos_type, double pos_open, double pos_sl, double value_sl, 
                    int trailing_step_pt, int trailing_start_pt, MqlTick &tick)
  {
//--- если стоп позиции и уровень стопа для модификации равны - возвращаем false
   if(NormalizeDouble(pos_sl-value_sl, Digits())==0)
      return false;

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

Условия просты:

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

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

Напишем функцию для модификации цены StopLoss позиции по её тикету:

//+------------------------------------------------------------------+
//| Модифицирует StopLoss позиции по тикету                          |
//+------------------------------------------------------------------+
bool ModifySL(const ulong ticket, const double stop_loss)
  {
//--- если позицию не удалось выбрать по тикету - сообщаем об этом в журнал и возвращаем 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,(int)SymbolInfoInteger(Symbol(),SYMBOL_DIGITS));
   
//--- если торговую операцию отправить не удалось - сообщаем об этом в журнал и возвращаем false
   if(!OrderSend(request, result))
     {
      PrintFormat("%s: OrderSend() failed to modify position #%I64u. Error %d",__FUNCTION__, ticket, GetLastError());
      return false;
     }
     
//--- успешно отправлен запрос на изменение StopLoss позиции
   return true;
  }

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

В итоге напишем функцию трала стопа позиции по значениям индикатора Parabolic SAR:

//+------------------------------------------------------------------+
//| Функция трейлинга StopLoss по индикатору Parabolic SAR           |
//+------------------------------------------------------------------+
void TrailingStopBySAR(const long magic=-1, const int trailing_step_pt=0, const int trailing_start_pt=0)
  {
//--- получаем значение Parabolic SAR с первого бара таймсерии
   double sar=GetSARData(SAR_DATA_INDEX);
   
//--- если данные получить не удалось - уходим
   if(sar==EMPTY_VALUE)
      return;
      
//--- вызываем функцию трала с указанием цены StopLoss, полученной от Parabolic SAR 
   TrailingStopByValue(sar, magic, trailing_step_pt, trailing_start_pt);
  }

Сначала получаем значение индикатора с бара 1, если значение не получено — уходим. Если же значение от Parabolic SAR успешно получили — отправляем его в функцию трала стопа по значению.

Теперь пропишем созданный трал по Parabolic SAR в обработчиках советника.

В OnTick():

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- если не новый бар - уходим из обработчика
   if(!IsNewBar())
      return;
      
//--- тралим стопы позиций по Parabolic SAR
   TrailingStopBySAR();
  }

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

В OnTradeTransaction():

//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {
   if(trans.type==TRADE_TRANSACTION_DEAL_ADD)
      TrailingStopBySAR();
  }

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

Мы создали базовую функцию трейлинг-стопа и на её основе сделали советник-трал по индикатору Parabolic SAR. Можно скомпилировать советник и, запустив его на графике, открыть позицию и контролировать работу трала по значению Parabolic SAR. Файл советника прикреплён в конце статьи для самостоятельного изучения.


Используем CTrade Стандартной Библиотеки

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

Воспользуемся такой возможностью и заменим функцию модификации StopLoss позиции методом PositionModify()  класса CTrade.

Посмотрим содержание метода модификации позиции:

//+------------------------------------------------------------------+
//| Modify specified opened position                                 |
//+------------------------------------------------------------------+
bool CTrade::PositionModify(const ulong ticket,const double sl,const double tp)
  {
//--- check stopped
   if(IsStopped(__FUNCTION__))
      return(false);
//--- check position existence
   if(!PositionSelectByTicket(ticket))
      return(false);
//--- clean
   ClearStructures();
//--- setting request
   m_request.action  =TRADE_ACTION_SLTP;
   m_request.position=ticket;
   m_request.symbol  =PositionGetString(POSITION_SYMBOL);
   m_request.magic   =m_magic;
   m_request.sl      =sl;
   m_request.tp      =tp;
//--- action and return the result
   return(OrderSend(m_request,m_result));
  }

и сравним с функцией модификации стопов позиции, написанной в советнике выше:

//+------------------------------------------------------------------+
//| Модифицирует StopLoss позиции по тикету                          |
//+------------------------------------------------------------------+
bool ModifySL(const ulong ticket, const double stop_loss)
  {
//--- если позицию не удалось выбрать по тикету - сообщаем об этом в журнал и возвращаем 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,(int)SymbolInfoInteger(Symbol(),SYMBOL_DIGITS));
   
//--- если торговую операцию отправить не удалось - сообщаем об этом в журнал и возвращаем false
   if(!OrderSend(request, result))
     {
      PrintFormat("%s: OrderSend() failed to modify position #%I64u. Error %d",__FUNCTION__, ticket, GetLastError());
      return false;
     }
     
//--- успешно отправлен запрос на изменение StopLoss позиции
   return true;
  }

Разницы особой нет. В методе торгового класса, в самом начале есть проверка на флаг снятия советника с графика. В функции — нет.
При этом, используется не стандартная функция IsStopped(), а одноимённый метод торгового класса, но с формальным параметром:

//+------------------------------------------------------------------+
//| Checks forced shutdown of MQL5-program                           |
//+------------------------------------------------------------------+
bool CTrade::IsStopped(const string function)
  {
   if(!::IsStopped())
      return(false);
//--- MQL5 program is stopped
   PrintFormat("%s: MQL5 program is stopped. Trading is disabled",function);
   m_result.retcode=TRADE_RETCODE_CLIENT_DISABLES_AT;
   return(true);
  }

Здесь, если функция IsStopped() вернула true, то сначала выводится сообщение в журнал о том, что эксперт снимается с графика и далее возвращается флаг остановки советника.

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

Сделаем изменения в уже написанном советнике, сохранив его под новым именем TrailingBySAR_02.mq5. В советнике сделаем возможность тестирования трала в тестере стратегий, открывая позиции по значениям индикатора Parabolic SAR, при этом перемещая уровни StopLoss открываемых позиций по значениям этого же индикатора.

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

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

#define   SAR_DATA_INDEX   1  // бар, с которого получаем данные Parabolic SAR

#include <Trade\Trade.mqh>    // заменим торговые функции методами Стандартной Библиотеки

//--- input 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 ulong             InpMagic          =  123;              // Magic Number

//--- global variables
int      ExtHandleSAR=INVALID_HANDLE;  // хендл Parabolic SAR
double   ExtStepSAR=0;                 // шаг Parabolic SAR
double   ExtMaximumSAR=0;              // максимум Parabolic SAR
CTrade   ExtTrade;                     // экземпляр класса торговых операций

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- устанавливаем в объект торгового класса магический номер
   ExtTrade.SetExpertMagicNumber(InpMagic);

//--- устанавливаем параметры Parabolic SAR в допустимых пределах
   ExtStepSAR   =(InpStepSAR<0.0001 ? 0.0001 : InpStepSAR);
   ExtMaximumSAR=(InpMaximumSAR<0.0001 ? 0.0001 : InpMaximumSAR);
   
//--- при ошибке создания индикатора выводим сообщение в журнал и выходим с ошибкой из OnInit
   ExtHandleSAR =iSAR(Symbol(), InpTimeframeSAR, ExtStepSAR, ExtMaximumSAR);
   if(ExtHandleSAR==INVALID_HANDLE)
     {
      PrintFormat("Failed to create iSAR(%s, %s, %.3f, %.2f) handle. Error %d",
                  Symbol(), TimeframeDescription(InpTimeframeSAR), ExtStepSAR, ExtMaximumSAR, GetLastError());
      return(INIT_FAILED);
     }   
//--- успешно
   return(INIT_SUCCEEDED);
  }


В обработчик OnTick() советника добавим код для открытия позиций по Parabolic SAR в тестере стратегий, а в функцию трейлинга добавим передачу магического номера советника:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- если не новый бар - уходим из обработчика
   if(!IsNewBar())
      return;
      
   if(MQLInfoInteger(MQL_TESTER))
     {
      //--- получаем данные Parabolic SAR с баров 1 и 2
      double sar1=GetSARData(SAR_DATA_INDEX);
      double sar2=GetSARData(SAR_DATA_INDEX+1);
      
      //--- если структура цен заполнена и данные от Parabolic SAR получены
      MqlTick tick={};
      if(SymbolInfoTick(Symbol(), tick) && sar1!=EMPTY_VALUE && sar2!=EMPTY_VALUE)
        {
         //--- если Parabolic SAR на баре 1 ниже цены Bid, а на баре 2 выше - открываем длинную позицию
         if(sar1<tick.bid && sar2>tick.bid)
            ExtTrade.Buy(0.1);
         //--- если Parabolic SAR на баре 1 выше цены Ask, а на баре 2 ниже - открываем короткую позицию
         if(sar1>tick.ask && sar2<tick.ask)
            ExtTrade.Sell(0.1);
        }
     }
      
//--- тралим стопы позиций по Parabolic SAR
   TrailingStopBySAR(InpMagic);
  }


В обработчике OnTradeTransaction() также допишем передачу магика в функцию трала:

//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {
   if(trans.type==TRADE_TRANSACTION_DEAL_ADD)
      TrailingStopBySAR(InpMagic);
  }

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

В функции универсального трейлинга вызов функции модификации заменим на вызов метода торгового класса:

//+------------------------------------------------------------------+
//| Универсальная функция трейлинга стопа по значению цены StopLoss  |
//+------------------------------------------------------------------+
void TrailingStopByValue(const double value_sl, const long magic=-1, const int trailing_step_pt=0, const int trailing_start_pt=0)
  {
//--- структура цен
   MqlTick tick={};
//--- в цикле по общему количеству открытых позиций
   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((magic!=-1 && pos_magic!=magic) || pos_symbol!=Symbol())
         continue;
         
      //--- если цены получить не удалось - идём далее
      if(!SymbolInfoTick(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 подходят - модифицируем стоп позиции
      if(CheckCriterion(pos_type, pos_open, pos_sl, value_sl, trailing_step_pt, trailing_start_pt, tick))
         ExtTrade.PositionModify(pos_ticket, value_sl, PositionGetDouble(POSITION_TP));
     }
  }

Теперь здесь для модификации стопа позиции вызывается не ранее написанная функция, а метод PositionModify() торгового класса CTrade. Разница между этими вызовами в одном параметре. Если функция была написана для модификации только цены StopLoss позиции, то и при указании параметров необходимо было передать в функцию значение тикета модифицируемой позиции и новый уровень её стопа. Теперь же в метод торгового класса нужно передавать три параметра — кроме тикета позиции и значения уровня StopLoss, необходимо указать ещё и уровень TakeProfit. Так как позиция уже выбрана, а значение её TakeProfit изменять не нужно, то передаём в метод значение TakeProfit без изменений прямо из свойств выбранной позиции.

Ранее написанная функция ModifySL() теперь удалена из кода советника.

Скомпилируем советник и запустим его в тестере стратегий на любом символе и любом таймфрейме графика с моделированием "Все тики":


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

Файл советника можно посмотреть в конце статьи для самостоятельного изучения.


Готовый трейлинг в советнике "за пару строк"

И всё же, при всей лёгкости создания трала и его использования в советнике, хотелось бы не прописывать каждый раз все функции, требуемые для его работы в каждом новом советнике, а просто сделать всё в стиле "Plug & Play".
И это возможно в MQL5. Для этого нужно лишь единожды написать подключаемый файл, и далее просто подключать его к нужному советнику и прописывать где нужно вызов функций трейлинга.

Перенесём созданные функции всех трейлингов в один новый файл. Для этого создадим в папке с советником новый подключаемый файл с именем TrailingsFunc.mqh. Вообще, все такие файлы желательно хранить в общей папке для всех подключаемых файлов \MQL5\Include\, либо в подпапке внутри этого каталога. Но для данного теста достаточно создать файл прямо в папке с советником и подключать его оттуда.

Нажмём в редакторе Ctrl+N и выберем новый подключаемый файл:


В следующем окне мастера введём имя подключаемого файла TrailingFunc.
Важно отметить, что по умолчанию в строке для ввода имени файла уже вписан корневой каталог для подключаемых файлов — Include. Т.е. файл будет создан в этой папке.
Далее его можно просто переместить в нужную папку с советником, либо самостоятельно прописать путь в строке имени файла к нужной папке (вместо Include\ вписать Experts\ и далее путь к папке с тестовыми советниками, если таковая используется, а затем уже имя создаваемого файла TrailingFunc):

Нажмём "Готово" и создадим пустой файл:

//+------------------------------------------------------------------+
//|                                                TrailingsFunc.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+
// #define MacrosHello   "Hello, world!"
// #define MacrosYear    2010
//+------------------------------------------------------------------+
//| DLL imports                                                      |
//+------------------------------------------------------------------+
// #import "user32.dll"
//   int      SendMessageA(int hWnd,int Msg,int wParam,int lParam);
// #import "my_expert.dll"
//   int      ExpertRecalculate(int wParam,int lParam);
// #import
//+------------------------------------------------------------------+
//| EX5 imports                                                      |
//+------------------------------------------------------------------+
// #import "stdlib.ex5"
//   string ErrorDescription(int error_code);
// #import
//+------------------------------------------------------------------+


Теперь сюда нужно перенести все ранее созданные функции из тестовых советников:

//+------------------------------------------------------------------+
//|                                                TrailingsFunc.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
//+------------------------------------------------------------------+
//| Простой трал по значению                                         |
//+------------------------------------------------------------------+
void SimpleTrailingByValue(const double value_sl, const long magic=-1, 
                           const int trailing_step_pt=0, const int trailing_start_pt=0, const int trailing_offset_pt=0)
  {
//--- структура цен
   MqlTick tick={};
   
//--- в цикле по общему количеству открытых позиций
   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((magic!=-1 && pos_magic!=magic) || pos_symbol!=Symbol())
         continue;
         
      //--- если цены получить не удалось - идём далее
      if(!SymbolInfoTick(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 подходят - модифицируем стоп позиции
      if(CheckCriterion(pos_type, pos_open, pos_sl, value_sl, trailing_step_pt, trailing_start_pt, tick))
         ModifySL(pos_ticket, value_sl);
     }
  }
//+------------------------------------------------------------------+
//|Проверяет критерии модификации StopLoss позициии и возвращает флаг|
//+------------------------------------------------------------------+
bool CheckCriterion(ENUM_POSITION_TYPE pos_type, double pos_open, double pos_sl, double value_sl, 
                    int trailing_step_pt, int trailing_start_pt, MqlTick &tick)
  {
//--- если стоп позиции и уровень стопа для модификации равны - возвращаем false
   if(NormalizeDouble(pos_sl-value_sl, Digits())==0)
      return false;

   double trailing_step = trailing_step_pt * Point(); // переводим шаг трала в цену
   double stop_level    = StopLevel(2) * Point();     // переводим уровень StopLevel символа в цену
   int    pos_profit_pt = 0;                          // прибыль позиции в пунктах
   
//--- в зависимости от типа позиции проверяем условия для модицикации StopLoss
   switch(pos_type)
     {
      //--- длинная позиция
      case POSITION_TYPE_BUY :
        pos_profit_pt=int((tick.bid - pos_open) / Point());             // рассчитываем прибыль позиции в пунктах
        if(tick.bid - stop_level > value_sl                             // если цена и отложенный от неё уровень StopLevel выше уровня StopLoss (соблюдена дистанция по StopLevel)
           && pos_sl + trailing_step < value_sl                         // если уровень StopLoss выше, чем шаг трала, отложенный от текущего StopLoss позиции
           && (trailing_start_pt==0 || pos_profit_pt>trailing_start_pt) // если тралим при любой прибыли или прибыль позиции в пунктах больше значения начала трейлинга - возвращаем true
          )
           return true;
        break;
        
      //--- короткая позиция
      case POSITION_TYPE_SELL :
        pos_profit_pt=int((pos_open - tick.ask) / Point());             // рассчитываем прибыль позиции в пунктах
        if(tick.ask + stop_level < value_sl                             // если цена и отложенный от неё уровень StopLevel ниже уровня StopLoss (соблюдена дистанция по StopLevel)
           && (pos_sl - trailing_step > value_sl || pos_sl==0)          // если уровень StopLoss ниже, чем шаг трала, отложенный от текущего StopLoss позиции или у позиции ещё не установлен StopLoss
           && (trailing_start_pt==0 || pos_profit_pt>trailing_start_pt) // если тралим при любой прибыли или прибыль позиции в пунктах больше значения начала трейлинга - возвращаем true
          )
           return true;
        break;
        
      //--- по умолчанию вернём false
      default: break;
     }
//--- нет соответствующих критериев
   return false;
  }
//+------------------------------------------------------------------+
//| Модифицирует StopLoss позиции по тикету                          |
//+------------------------------------------------------------------+
bool ModifySL(const ulong ticket, const double stop_loss)
  {
//--- если позицию не удалось выбрать по тикету - сообщаем об этом в журнал и возвращаем 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,(int)SymbolInfoInteger(request.symbol,SYMBOL_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 в пунктах                            |
//+------------------------------------------------------------------+
int StopLevel(const int spread_multiplier)
  {
   int spread    =(int)SymbolInfoInteger(Symbol(), SYMBOL_SPREAD);
   int stop_level=(int)SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL);
   return(stop_level==0 ? spread * spread_multiplier : stop_level);
  }
//+------------------------------------------------------------------+
//| Возвращает описание таймфрейма                                   |
//+------------------------------------------------------------------+
string TimeframeDescription(const ENUM_TIMEFRAMES timeframe)
  {
   return(StringSubstr(EnumToString(timeframe==PERIOD_CURRENT ? Period() : timeframe), 7));
  }
//+------------------------------------------------------------------+
//| Возвращает флаг открытия нового бара таймсерии                   |
//+------------------------------------------------------------------+
bool IsNewBar(void)
  {
   static datetime time_prev=0;
   datetime        bar_open_time=TimeOpenBar(0);
   if(bar_open_time==0)
      return false;
   if(bar_open_time!=time_prev)
     {
      time_prev=bar_open_time;
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//| Возвращает время открытия бара по индексу таймсерии              |
//+------------------------------------------------------------------+
datetime TimeOpenBar(const int index)
  {
   datetime array[1];
   ResetLastError();
   if(CopyTime(NULL, PERIOD_CURRENT, index, 1, array)!=1)
     {
      PrintFormat("%s: CopyTime() failed. Error %d", __FUNCTION__, GetLastError());
      return 0;
     }
   return array[0];
  }

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

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

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

Соответственно, теперь есть функция трала по данным индикатора, получаемым от вышенаписанной функции:

//+------------------------------------------------------------------+
//| Трал по данным индикатора, указанного по хендлу                  |
//+------------------------------------------------------------------+
void TrailingByDataInd(const int handle_ind, const int index=1, const long magic=-1, 
                       const int trailing_step_pt=0, const int trailing_start_pt=0, const int trailing_offset_pt=0)
  {
//--- получаем значение Parabolic SAR с указанного индекса таймсерии
   double data=GetIndData(handle_ind, index);
   
//--- если данные получить не удалось - уходим
   if(data==EMPTY_VALUE)
      return;
      
//--- вызываем функцию простого трала с указанием цены для StopLoss, полученной от Parabolic SAR 
   SimpleTrailingByValue(data, magic, trailing_step_pt, trailing_start_pt, trailing_offset_pt);
  }

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

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

Чтобы не вписывать в код советника создание индикатора Parabolic SAR, разместим в файле такую функцию:

//+------------------------------------------------------------------+
//| Создаёт и возвращает хендл Parabolic SAR                         |
//+------------------------------------------------------------------+
int CreateSAR(const string symbol_name, const ENUM_TIMEFRAMES timeframe, const double step_sar=0.02, const double max_sar=0.2)
  {
//--- устанавливаем параметры индикатора в допустимых пределах
   double step=(step_sar<0.0001 ? 0.0001 : step_sar);
   double max =(max_sar <0.0001 ? 0.0001 : max_sar);

//--- корректируем значения символа и таймфрейма
   ENUM_TIMEFRAMES period=(timeframe==PERIOD_CURRENT ? Period() : timeframe);
   string          symbol=(symbol_name==NULL || symbol_name=="" ? Symbol() : symbol_name);
 
//--- создаём хендл индикатора
   ResetLastError();
   int handle=iSAR(symbol, period, step, max);
   
//--- при ошибке создания индикатора выводим сообщение об ошибке в журнал
   if(handle==INVALID_HANDLE)
     {
      PrintFormat("Failed to create iSAR(%s, %s, %.3f, %.2f) handle. Error %d",
                  symbol, TimeframeDescription(period), step, max, GetLastError());
     } 
//--- возвращаем результат создания хендла индикатора
   return handle;
  }

По сути, функция CreateSAR() — это код, перенесённый из обработчика OnInit() тестового советника TrailingBySAR_01.mq5. Такой подход позволит просто вызывать эту функцию, не прибегая к написанию в советнике строк коррекции входных переменных для индикатора и создание его хендла.

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

//+------------------------------------------------------------------+
//| Создаёт и возвращает хендл Adaptive Moving Average               |
//+------------------------------------------------------------------+
int CreateAMA(const string symbol_name, const ENUM_TIMEFRAMES timeframe,
              const int ama_period=9, const int fast_ema_period=2, const int slow_ema_period=30, const int shift=0, const ENUM_APPLIED_PRICE price=PRICE_CLOSE)
  {
//--- устанавливаем параметры индикатора в допустимых пределах
   int ma_period=(ama_period<1 ? 9 : ama_period);
   int fast_ema=(fast_ema_period<1 ? 2 : fast_ema_period);
   int slow_ema=(slow_ema_period<1 ? 30 : slow_ema_period);

//--- корректируем значения символа и таймфрейма
   ENUM_TIMEFRAMES period=(timeframe==PERIOD_CURRENT ? Period() : timeframe);
   string          symbol=(symbol_name==NULL || symbol_name=="" ? Symbol() : symbol_name);
 
//--- создаём хендл индикатора
   ::ResetLastError();
   int handle=::iAMA(symbol, period, ma_period, fast_ema, slow_ema, shift, price);
   
//--- при ошибке создания индикатора выводим сообщение об ошибке в журнал
   if(handle==INVALID_HANDLE)
     {
      ::PrintFormat("Failed to create iAMA(%s, %s, %d, %d, %d, %s) handle. Error %d",
                    symbol, TimeframeDescription(period), ma_period, fast_ema, slow_ema,
                    ::StringSubstr(::EnumToString(price),6), ::GetLastError());
     }
//--- возвращаем результат создания хендла индикатора
   return handle;
  }

Все остальные функции подобны представленной выше, и рассматривать их здесь нет необходимости — их можно будет посмотреть в прикреплённом к статье файлу TrailingsFunc.mqh.

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


Для тестирования написанных функций создадим новый тестовый советник с именем TrailingBySAR_03.mq5 и подключим к нему только что созданный подключаемый файл TrailingsFunc.mqh.
В глобальной области объявим переменную для хранения хендла создаваемого индикатора, а в обработчике OnInit() присвоим этой переменной результат создания индикатора ParabolicSAR:

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

#define   SAR_DATA_INDEX   1  // бар, с которого получаем данные Parabolic SAR

#include "TrailingsFunc.mqh"

//--- input 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 long              InpMagic          =  123;              // Expert Magic Number

//--- global variables
int   ExtHandleSAR=INVALID_HANDLE;  // хендл Parabolic SAR

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- создаём хендл Parabolic SAR
   ExtHandleSAR=CreateSAR(Symbol(), InpTimeframeSAR, InpStepSAR, InpMaximumSAR);
   
//--- при ошибке создания индикатора выходим с ошибкой из OnInit
   if(ExtHandleSAR==INVALID_HANDLE)
      return(INIT_FAILED);

//--- успешно
   return(INIT_SUCCEEDED);
  }


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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- если не новый бар - уходим из обработчика
   if(!IsNewBar())
      return;
      
//--- тралим стопы позиций по Parabolic SAR
   TrailingByDataInd(ExtHandleSAR, SAR_DATA_INDEX, InpMagic);
  }
//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {
   if(trans.type==TRADE_TRANSACTION_DEAL_ADD)
      TrailingByDataInd(ExtHandleSAR, SAR_DATA_INDEX, InpMagic);
  }

Созданный советник представляет собой трейлинг-стоп по данным индикатора Parabolic SAR. Он будет тралить стопы позиций, открытых на символе, на котором запущен этот советник, и у которых магик совпадает с установленным в настройках советника.


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

В завершение давайте подключим трейлинг по Parabolic SAR к советнику ExpertMACD стандартной поставки, находящемуся в расположении \MQL5\Experts\Advisors\ExpertMACD.mq5.

Сохраним его под новым именем ExpertMACDPSAR.mq5 и внесём в него изменения для подключения трала.

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

//+------------------------------------------------------------------+
//|                                                   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 "TrailingsFunc.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;

//--- inputs for trail
input group  " - PSAR Trailing Parameters -"
input bool   InpUseTrail       =  true;      // Trailing is Enabled
input double InpSARStep        =  0.02;      // Trailing SAR Step
input double InpSARMaximum     =  0.2;       // Trailing SAR Maximum
input int    InpTrailingStart  =  0;         // Trailing start
input int    InpTrailingStep   =  0;         // Trailing step in points
input int    InpTrailingOffset =  0;         // Trailing offset in points
//+------------------------------------------------------------------+
//| Global expert object                                             |
//+------------------------------------------------------------------+
CExpert ExtExpert;
int     ExtHandleSAR;
//+------------------------------------------------------------------+
//| Initialization function of the expert                            |
//+------------------------------------------------------------------+


В обработчике OnInit() создадим индикатор и запишем его хендл в переменную:

//+------------------------------------------------------------------+
//| Initialization function of the expert                            |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- Initializing trail
   if(InpUseTrail)
     {
      ExtHandleSAR=CreateSAR(NULL,PERIOD_CURRENT,InpSARStep,InpSARMaximum);
      if(ExtHandleSAR==INVALID_HANDLE)
         return(INIT_FAILED);
     }
   
//--- Initializing expert
//...
//...


В обработчике OnDeinit() удалим хендл и освободим расчётную часть индикатора:

//+------------------------------------------------------------------+
//| Deinitialization function of the expert                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ExtExpert.Deinit();
   IndicatorRelease(ExtHandleSAR);
  }


В обработчиках OnTick() и OnTrade() запустим трал по индикатору Parabolic SAR:

//+------------------------------------------------------------------+
//| Function-event handler "tick"                                    |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   ExtExpert.OnTick();
   TrailingByDataInd(ExtHandleSAR, 1, Expert_MagicNumber, InpTrailingStep, InpTrailingStart, InpTrailingOffset);
  }
//+------------------------------------------------------------------+
//| Function-event handler "trade"                                   |
//+------------------------------------------------------------------+
void OnTrade(void)
  {
   ExtExpert.OnTrade();
   TrailingByDataInd(ExtHandleSAR, 1, Expert_MagicNumber, InpTrailingStep, InpTrailingStart, InpTrailingOffset);
  }

Это всё, что нужно дописать в файл советника для того, чтобы добавить к нему полноценный трал по Parabolic SAR.

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

Важно помнить: представленные в файле TrailingFunc.mqh функции позволяют создавать

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

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

Запустим созданного эксперта на одиночный проход в тестере с установленными параметрами.

Интервал и настройки тестирования выберем такими:

  • Символ: EURUSD,
  • Таймфрейм: M15,
  • Тестируем последний год по всем тикам без задержек исполнения.

Настройки входных параметров:


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



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


Видно, что после работы трала график стал немного ровнее. Предлагаю читателям самостоятельно попробовать различные решения для тестирования тралов.

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


Заключение

Итак, мы научились быстро создавать и подключать к советникам трейлинг-стоп. Всё, что нужно сделать, чтобы подключить трейлинг-стоп к любому советнику, это:

  1. разместить подключаемый файл TrailingsFunc.mqh в папке с советником,
  2. подключить этот файл к файлу советника командой #include "TrailingsFunc.mqh",
  3. вписать создание индикатора ParabolicSAR в OnInit() советника: ExtHandleSAR=CreateSAR(NULL,PERIOD_CURRENT);
  4. вписать в обработчики советника OnTick() и (при необходимости) в OnTrade() или OnTradeTransaction() вызов трала: TrailingByDataInd(ExtHandleSAR);
  5. в OnDeinit() советника освободить расчётную часть индикатора ParabolicSAR при помощи IndicatorRelease(ExtHandleSAR).


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

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



Прикрепленные файлы |
TrailingsFunc.mqh (37.8 KB)
ExpertMACDPSAR.mq5 (7.02 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
Roman Shiredchenko
Roman Shiredchenko | 2 мая 2024 в 06:26
Спс за интересную статью. Бегло прочел. Основное усвоил. С телефона. Дома с компа подробнее ознакомлюсь и по аналогии буду использовать в своих проектах включаемые файлы - позже.
Artyom Trishkin
Artyom Trishkin | 2 мая 2024 в 09:21
Roman Shiredchenko #:
Спс за интересную статью. Бегло прочел. Основное усвоил. С телефона. Дома с компа подробнее ознакомлюсь и по аналогии буду использовать в своих проектах включаемые файлы - позже.

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

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

amrali
amrali | 14 мая 2024 в 07:59

Thanks for the article,

But I wish you modify this to avoid truncation errors with fp numbers:

        pos_profit_pt= int ((tick.bid - pos_open) / Point ());              // calculate the profit of the position in points 

To:

        pos_profit_pt= (int) MathRound((tick.bid - pos_open) / Point ());              // calculate the profit of the position in points 
Как разработать агент обучения с подкреплением на MQL5 с интеграцией RestAPI (Часть 3): Создание автоматических ходов и тестовых скриптов на MQL5 Как разработать агент обучения с подкреплением на MQL5 с интеграцией RestAPI (Часть 3): Создание автоматических ходов и тестовых скриптов на MQL5
В этой статье рассматривается реализация автоматических ходов в игре "Крестики-нолики" на языке Python, интегрированная с функциями MQL5 и модульными тестами. Цель - улучшить интерактивность игры и обеспечить надежность системы с помощью тестирования на MQL5. Изложение охватывает разработку игровой логики, интеграцию и практическое тестирование, а завершается созданием динамической игровой среды и надежной интегрированной системы.
Разметка данных в анализе временных рядов (Часть 5):Применение и тестирование советника с помощью Socket Разметка данных в анализе временных рядов (Часть 5):Применение и тестирование советника с помощью Socket
В этой серии статей представлены несколько методов разметки временных рядов, которые могут создавать данные, соответствующие большинству моделей искусственного интеллекта (ИИ). Целевая разметка данных может сделать обученную модель ИИ более соответствующей пользовательским целям и задачам, повысить точность модели и даже помочь модели совершить качественный скачок!
Нейросети — это просто (Часть 88): Полносвязный Энкодер временных рядов (TiDE) Нейросети — это просто (Часть 88): Полносвязный Энкодер временных рядов (TiDE)
Желание получить наиболее точные прогнозы толкает исследователей к усложнению моделей прогнозирование. Что в свою очередь ведет к увеличению затрат на обучение и обслуживание модели. Но всегда ли это оправдано? В данной статье я предлагаю Вам познакомиться с алгоритмом, который использует простоту и скорость линейных моделей и демонстрирует результаты на уровне лучших с более сложной архитектурой.
Разработка системы репликации (Часть 38): Прокладываем путь (II) Разработка системы репликации (Часть 38): Прокладываем путь (II)
Многие люди, которые считают себя программистами на MQL5, не обладают базовыми знаниями, которые мы изложим в этой статье. Многие считают MQL5 ограниченным инструментом, однако всё дело в недостатке знаний. Так что если вы чего-то не знаете, не стыдитесь этого. Лучше пусть вам будет стыдно за то, что вы не спросили. Простое принуждение MetaTrader 5 к запрету дублирования индикатора никоим образом не обеспечивает двустороннюю связь между индикатором и советником. Мы еще очень далеки от этого, но тот факт, что индикатор не дублируется на графике, дает нам некоторое утешение.