Português
preview
Постфактумный анализ торговли: подбираем TrailingStop и новые стопы в тестере стратегий

Постфактумный анализ торговли: подбираем TrailingStop и новые стопы в тестере стратегий

MetaTrader 5Примеры |
790 4
Artyom Trishkin
Artyom Trishkin

Содержание


Введение

В прошлой статье мы создали советник, торгующий в тестере стратегий клиентского терминала по результатам торговли на реальном счёте. Мы добавили возможность установки новых размеров StopLoss и TakeProfit для тестирования своей торговли в тестере с иными размерами стоп-приказов. Результат оказался неожиданным: вместо убытка, получили прибыль, соразмерную с полученным в реале убытком. Значит, если бы мы в своей торговле на счёте использовали уровни StopLoss и TakeProfit такими, при которых в тестере получили прибыль, то торговля и на реальном счёте была бы прибыльной. И это всего лишь простое изменение размеров стопов. Интересно, а что, если добавить трейлинг StopLoss? Как при этом изменится торговля?

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


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

Попробуем подключить трал по индикатору Parabolic SAR и по скользящей средней. Индикатор Parabolic SAR, судя по его описанию, да и по названию (SAR = Stop And Reverse), идеально должен подходить для перемещения линии стопа по его значениям:

Parabolic SAR

Технический индикатор Параболическая Система SAR (Parabolic SAR) был разработан для анализа трендовых рынков. Индикатор строится на ценовом графике. По своему смыслу данный индикатор аналогичен скользящей средней, с той лишь разницей, что Parabolic SAR движется с большим ускорением и может менять положение относительно цены. На "бычьем тренде" (Up Trend) индикатор располагается ниже цен, на "медвежьем тренде" (Down Trend) — выше.

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

Parabolic SAR превосходно определяет точки выхода из рынка. Длинные позиции следует закрывать, когда цена опускается ниже линии технического индикатора, а короткие — когда цена поднимается выше линии Parabolic SAR. То есть, необходимо отслеживать направление движения Parabolic SAR и держать открытыми на рынке позиции только в направлении этого движения. Часто данный индикатор используют в качестве линии скользящего стопа (trailing stop).

Если открыта длинная позиция (то есть цена выше линии Parabolic SAR), то линия индикатора будет перемещаться вверх, независимо от того, в каком направлении движутся цены. Величина перемещения линии Parabolic SAR зависит от величины ценового движения.

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

Ранее мною уже была написана статья о подключении любых тралов к советникам: "Как сделать любой тип Trailing Stop и подключить к советнику". В ней предлагается создать классы трейлингов и просто подключать их к советнику.

Воспользуемся этой возможностью. Единственное, что не очень нам подходит — там каждый символ или магик "крутит" собственный цикл перебора открытых позиций. Значит, придётся доработать класс из статьи. Благо, это не сложно.

Загрузим из прилагаемых к статье файлов единственный нужный нам файл Trailings.mqh. В прошлой статье мы создали папку, где были расположены файлы классов и советников для торговли по истории сделок: \MQL5\Experts\TradingByHistoryDeals. Сохраним в эту папку загруженный файл Trailings.mqh и откроем его в редакторе кода MetaEditor.

Основной и главный метод, вызываемый из программы — метод 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;
  }

Если для каждого торгового объекта символа (см. первую статью) вызывать этот метод, то у нас получится столько циклов перебора открытых позиций, сколько символов использовалось в торговле. Это не оптимально. Сделаем так, что в самой программе будет цикл перебора всех открытых позиций, а эти методы будем вызывать уже из этого цикла. Но в методе тоже встроен цикл... Значит, нам в классе трейлингов нужно сделать ещё один метод Run(), но с указанием тикета позиции, стоп которой нужно перемещать, и из основного цикла вызывать уже его.

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

//+------------------------------------------------------------------+
//| Класс простого трала StopLoss позиций                            |
//+------------------------------------------------------------------+
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: 
   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:
//--- установка параметров трала
   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);
   bool              Run(const ulong pos_ticket);

//--- конструкторы
                     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() {}
  };

За пределами тела класса напишем его реализацию:

//+------------------------------------------------------------------+
//| Запускает простой трейлинг с отступом StopLoss от цены           |
//+------------------------------------------------------------------+
bool CSimpleTrailing::Run(const ulong pos_ticket)
  {
//--- если трейлинг отключен, или невалидный тикет - уходим
   if(!this.m_active || pos_ticket==0)
      return false;
      
//--- переменные трала
   MqlTick tick = {};   // структура цен
   
//--- проверка корректности данных по символу
   ::ResetLastError();
   if(this.m_point==0)
     {
      //--- попробуем ещё раз получить данные
      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;
        }
     }
//--- выбираем позицию по тикету
   if(!::PositionSelectByTicket(pos_ticket))
     {
      ::PrintFormat("%s: PositionSelectByTicket(%I64u) failed. Error %d",__FUNCTION__, pos_ticket, ::GetLastError());
      return false;
     }
     
//--- если цены получить не удалось - возвращаем false
   if(!::SymbolInfoTick(this.m_symbol, tick))
      return false;

//--- получаем тип позиции, цену её открытия и уровень 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 подходят - возвращаем результат модифицикации стопа позиции
   if(this.CheckCriterion(pos_type, pos_open, pos_sl, value_sl, tick))
      return(this.ModifySL(pos_ticket, value_sl));
//--- условия для модификации не подходят
   return false;
  }

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

Доработаем метод Run() без формальных параметров. Удалим из цикла блок кода:

   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);
     }

Вместо него впишем вызов нового метода:

//+------------------------------------------------------------------+
//| Запускает простой трейлинг с отступом StopLoss от цены           |
//+------------------------------------------------------------------+
bool CSimpleTrailing::Run(void)
  {
//--- если отключен - уходим
   if(!this.m_active)
      return false;
//--- переменные трала
   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;
      
      res &=this.Run(pos_ticket);
     }
//--- по окончании цикла возвращаем результат модификации каждой подходящей по фильтру "символ/магик" позиции
   return res;
  }

В классе трейлинга по указанному значению точно так же объявим новый метод 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);
   bool              Run(const ulong pos_ticket, 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){}
  };

И за пределами тела класса напишем его реализацию:

//+------------------------------------------------------------------+
//| Запуск трала с указанным отступом StopLoss от цены               |
//+------------------------------------------------------------------+
bool CTrailingByValue::Run(const ulong pos_ticket,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(pos_ticket);
  }

Здесь вызывается добавленный в класс простого трала метод Run() с указанием тикета позиции.

С классами тралов желательно ознакомиться в статье, из которой мы взяли файл с трейлингами.

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

В каталоге терминала \MQL5\Experts\TradingByHistoryDeals\ создадим новый файл SymbolTradeExt.mqh класса CSymbolTradeExt. К файлу должны быть подключены файл классов трейлингов и файл класса CSymbolTrade, от которого и должен быть унаследован создаваемый класс:

//+------------------------------------------------------------------+
//|                                               SymbolTradeExt.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 "Trailings.mqh"
#include "SymbolTrade.mqh"

class CSymbolTradeExt : public CSymbolTrade
  {
  }

При использовании взятого из статьи класса трейлингов, нам предоставляются в пользование тралы по параболику и по всем типам стандартных скользящих средних. Также в классе есть трал по указанным значениям, но его мы использовать здесь не будем — он требует собственного расчёта уровней стопов в своей программе и передаче их в параметрах вызываемого метода Run() класса трейлинга. Это может быть, например, расчёт уровней по индикатору ATR и передача рассчитанных значений в класс трейлинга.

Напишем перечисление режимов трейлинга:

//+------------------------------------------------------------------+
//|                                               SymbolTradeExt.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 "Trailings.mqh"
#include "SymbolTrade.mqh"

enum ENUM_TRAILING_MODE    // Перечисление режимов трейлинга
  {
   TRAILING_MODE_SIMPLE=2, // Простой трал
   TRAILING_MODE_SAR,      // Трал по Parabolic SAR
   TRAILING_MODE_AMA,      // Трал по адартивной скользящей средней
   TRAILING_MODE_DEMA,     // Трал по двойной экспоненциальной скользящей средней
   TRAILING_MODE_FRAMA,    // Трал по фрактальной адартивной скользящей средней
   TRAILING_MODE_MA,       // Трал по простой скользящей средней
   TRAILING_MODE_TEMA,     // Трал по тройной экспоненциальной скользящей средней
   TRAILING_MODE_VIDYA,    // Трал по скользящей средней с динамическим периодом усреднения
  };

class CSymbolTradeExt : public CSymbolTrade
  {
  }

Почему значения констант перечисления начинаются не с нуля, а со значения 2? В советнике, на основе которого будем делать новый, тоже есть перечисление режимов тестирования:

enum ENUM_TESTING_MODE
  {
   TESTING_MODE_ORIGIN,    /* Original trading                          */ // Оригинальная торговля
   TESTING_MODE_SLTP,      /* Specified StopLoss and TakeProfit values  */ // Указанные значения StopLoss и TakeProfit
  };

Здесь есть две константы: оригинальная торговля (0) и торговля с указанными значениями стоп-приказов (1). Сюда позже добавим новые константы, соответствующие константам перечисления трейлингов. Именно по этой причине значения констант перечисления трейлингов начинаются со значения 2.

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

class CSymbolTradeExt : public CSymbolTrade
  {
private:
   CSimpleTrailing  *m_trailing;    // Объект класса трейлингов
   ENUM_TIMEFRAMES   m_timeframe;   // Таймфрейм расчёта индикатора для трала
public:
//--- Устанавливает трал и его параметры
   bool              SetTrailing(const ENUM_TRAILING_MODE trailing_mode, const int data_index, const long magic, const int start, const int step, const int offset, const MqlParam &param[]);
//--- Запускает трал указанной по тикету позиции
   void              Trailing(const ulong pos_ticket);

//--- Конструктор/деструктор
                     CSymbolTradeExt() : m_trailing(NULL), m_timeframe(::Period()) { this.SetSymbol(::Symbol()); }
                     CSymbolTradeExt(const string symbol, const ENUM_TIMEFRAMES timeframe);
                    ~CSymbolTradeExt();
  };

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

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CSymbolTradeExt::CSymbolTradeExt(const string symbol, const ENUM_TIMEFRAMES timeframe) : CSymbolTrade(symbol)
  {
   this.m_trailing=NULL;
   this.m_timeframe=timeframe;
  }

В деструкторе класса, если объект трейлинга был создан, он удаляется:

//+------------------------------------------------------------------+
//| Деструктор                                                       |
//+------------------------------------------------------------------+
CSymbolTradeExt::~CSymbolTradeExt()
  {
//--- удаляем созданный объект трала
   if(this.m_trailing!=NULL)
      delete this.m_trailing;
  }

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

//+------------------------------------------------------------------+
//| Устанавливает параметры трала                                    |
//+------------------------------------------------------------------+
bool CSymbolTradeExt::SetTrailing(const ENUM_TRAILING_MODE trailing_mode, const int data_index, const long magic, const int start, const int step, const int offset, const MqlParam &param[])
  {
//--- Устанавливаем параметры трала (для каждого типа индикаторов используются только свои, нужные индикатору, поля структуры)
   int                ma_period  = (int)param[0].integer_value;
   int                ma_shift   = (int)param[1].integer_value;
   ENUM_APPLIED_PRICE ma_price   = (ENUM_APPLIED_PRICE)param[2].integer_value;
   ENUM_MA_METHOD     ma_method  = (ENUM_MA_METHOD)param[3].integer_value;
   int                fast_ema   = (int)param[4].integer_value;
   int                slow_ema   = (int)param[5].integer_value;
   int                period_cmo = (int)param[6].integer_value;
   double             sar_step   = param[0].double_value;
   double             sar_max    = param[1].double_value;
   
//--- в зависимости от типа трейлинга создаём объект трала
//---если в качестве периода расчёта передано значение, меньше допустимого, то для каждого индикатора устанавливается его собственное значение по умолчанию
   switch(trailing_mode)
     {
      case TRAILING_MODE_SIMPLE  :
        this.m_trailing=new CSimpleTrailing(this.Symbol(), magic, start, step, offset);
        break;
      case TRAILING_MODE_AMA     :
        this.m_trailing=new CTrailingByAMA(this.Symbol(), this.m_timeframe, magic,
                                           (ma_period<1 ?  9 : ma_period), (fast_ema<1  ?  2 : fast_ema), (slow_ema<1  ? 30 : slow_ema), ma_shift, ma_price, start, step, offset);
        break;
      case TRAILING_MODE_DEMA    :
        this.m_trailing=new CTrailingByDEMA(this.Symbol(), this.m_timeframe, magic, (ma_period==0 ? 14 : ma_period), ma_shift, ma_price, start, step, offset);
        break;
      case TRAILING_MODE_FRAMA   :
        this.m_trailing=new CTrailingByFRAMA(this.Symbol(), this.m_timeframe, magic, (ma_period==0 ? 14 : ma_period), ma_shift, ma_price, start, step, offset);
        break;
      case TRAILING_MODE_MA      :
        this.m_trailing=new CTrailingByMA(this.Symbol(), this.m_timeframe, magic, ma_period, (ma_period==0 ? 10 : ma_period), ma_method, ma_price, start, step, offset);
        break;
      case TRAILING_MODE_TEMA    :
        this.m_trailing=new CTrailingByTEMA(this.Symbol(), this.m_timeframe, magic, (ma_period==0 ? 14 : ma_period), ma_shift, ma_price, start, step, offset);
        break;
      case TRAILING_MODE_VIDYA   :
        this.m_trailing=new CTrailingByVIDYA(this.Symbol(), this.m_timeframe, magic, (period_cmo<1 ? 9 : period_cmo), (ma_period==0 ? 12 : ma_period), ma_shift, ma_price, start, step, offset);
        break;
      case TRAILING_MODE_SAR     :
        this.m_trailing=new CTrailingBySAR(this.Symbol(), this.m_timeframe, magic, (sar_step<0.0001 ? 0.02 : sar_step), (sar_max<0.02 ? 0.2 : sar_max), start, step, offset);
        break;
      default :
        break;
     }
//--- что-то пошло не так - возвращаем false
   if(this.m_trailing==NULL)
      return false;
      
//--- всё успешно - делаем трал активным и возвращаем true
   this.m_trailing.SetActive(true);
   return true;
  }

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

Метод, запускающий трал указанной по тикету позиции:

//+------------------------------------------------------------------+
//| Запускает трал указанной по тикету позиции                       |
//+------------------------------------------------------------------+
void CSymbolTradeExt::Trailing(const ulong pos_ticket)
  {
    if(this.m_trailing!=NULL)
      this.m_trailing.Run(pos_ticket);
  }

Из советника, из его цикла по открытым позициям, в зависимости от символа позиции, будем получать из списка торговый объект символа, а из него — вызывать данный метод для трала стопа позиции, тикет которой будем передавать в метод. Если объект трала существует, то вызывается его новый метод Run() с указанием тикета позиции.


Тестируем различные типы TrailingStop

Для тестирования возьмём файл советника TradingByHistoryDeals_SLTP.mq5из прошлой статьи и сохраним его в той же папке \MQL5\Experts\TradingByHistoryDeals\под новым именем TradingByHistoryDeals_Ext.mq5.

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

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

#include "SymbolTradeExt.mqh"
#include "Trailings.mqh"

enum ENUM_TESTING_MODE
  {
   TESTING_MODE_ORIGIN,       /* Original trading                          */ // Оригинальная торговля
   TESTING_MODE_SLTP,         /* Specified StopLoss and TakeProfit values  */ // Указанные значения StopLoss и TakeProfit
   TESTING_MODE_TRAIL_SIMPLE, /* Simple Trailing                           */ // Простой трал
   TESTING_MODE_TRAIL_SAR,    /* Trailing by Parabolic SAR indicator       */ // Трал по Parabolic SAR
   TESTING_MODE_TRAIL_AMA,    /* Trailing by AMA indicator                 */ // Трал по адаптивной скользящей средней
   TESTING_MODE_TRAIL_DEMA,   /* Trailing by DEMA indicator                */ // Трал по двойной экспоненциальной скользящей средней
   TESTING_MODE_TRAIL_FRAMA,  /* Trailing by FRAMA indicator               */ // Трал по фрактальной адаптивной скользящей средней
   TESTING_MODE_TRAIL_MA,     /* Trailing by MA indicator                  */ // Трал по простой скользящей средней
   TESTING_MODE_TRAIL_TEMA,   /* Trailing by TEMA indicator                */ // Трал по тройной экспоненциальной скользящей средней
   TESTING_MODE_TRAIL_VIDYA,  /* Trailing by VIDYA indicator               */ // Трал по скользящей средней с динамическим периодом усреднения
  };

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+
//--- input parameters
input    group                " - Strategy parameters - "
input    string               InpTestedSymbol   =  "";                  /* The symbol being tested in the tester        */ // Тестируемый символ
input    long                 InpTestedMagic    =  -1;                  /* The magic number being tested in the tester  */ // Тестируемый магик
sinput   bool                 InpShowDataInLog  =  false;               /* Show collected data in the log               */ // Показать собранные данные сделок в журнале

input    group                " - Stops parameters - "
input    ENUM_TESTING_MODE    InpTestingMode    =  TESTING_MODE_ORIGIN; /* Testing Mode                                 */ // Режим тестирования
input    int                  InpStopLoss       =  300;                 /* StopLoss in points                           */ // Отступ StopLoss в пунктах
input    int                  InpTakeProfit     =  500;                 /* TakeProfit in points                         */ // Отступ TakeProfit в пунктах

input    group                " - Trailing Parameters -"
input    bool                 InpSetStopLoss    =  true;                /* Set Initial StopLoss                         */ // Устанавливать StopLoss
input    bool                 InpSetTakeProfit  =  true;                /* Set Initial TakeProfit                       */ // Устанавливать TakeProfit
input    int                  InpTrailingStart  =  150;                 /* Trailing start                               */ // Прибыль в пунктах для старта трейлинга
input    int                  InpTrailingStep   =  50;                  /* Trailing step in points                      */ // Шаг трала в пунктах цены
input    int                  InpTrailingOffset =  0;                   /* Trailing offset in points                    */ // Отступ трала от цены в пунктах

input    group                " - Indicator Parameters -"
input    ENUM_TIMEFRAMES      InpIndTimeframe   =  PERIOD_CURRENT;      /* Indicator's timeframe                        */ // Таймфрейм индикатора, используемого в расчёте трала
input    int                  InpMAPeriod       =  0;                   /* MA Period                                    */ // Период расчёта скользящей средней
input    int                  InpMAShift        =  0;                   /* MA Shift                                     */ // Горизонтальный сдвиг скользящей средней
input    int                  InpFastEMAPeriod  =  2;                   /* AMA Fast EMA Period                          */ // Период расчёта быстрой EMA адаптивной скользящей средней
input    int                  InpSlowEMAPeriod  =  30;                  /* AMA Slow EMA Period                          */ // Период расчёта медленной EMA адаптивной скользящей средней
input    int                  InpCMOPeriod      =  9;                   /* VIDYA CMO Period                             */ // Период CMO скользящей средней с динамическим периодом усреднения
input    double               InpSARStep        =  0.02;                /* Parabolic SAR Step                           */ // Шаг Parabolic SAR
input    double               InpSARMax         =  0.2;                 /* Parabolic SAR Max                            */ // Максимум Parabolic SAR
input    ENUM_APPLIED_PRICE   InpAppliedPrice   =  PRICE_CLOSE;         /* MA Applied Price                             */ // Цена для расчёта скользящей средней
input    ENUM_MA_METHOD       InpMAMethod       =  MODE_SMA;            /* MA Smoothing Method                          */ // Тип сглаживания скользящей средней
input    int                  InpDataIndex      =  1;                   /* Indicator data index                         */ // Бар данных, получаемых от индикатора

InpMAPeriod по умолчанию здесь установлен в ноль. Причиной тому послужило то, что у каждого типа скользящей средней значение периода по умолчанию — своё. А так, как при нулевом значении этого параметра в класс трейлинга передаётся значение по умолчанию, установленное для индикатора, то все данные будут корректными, если требуются простые значения по умолчанию для используемого в трале индикатора.

Во всём коде заменим все вхождения строки "CSymbolTrade" на "CSymbolTradeExt". Теперь у нас новый класс торгового объекта символа CSymbolTradeExt унаследован от прошлого класса CSymbolTrade, написанном в прошлой статье. И в этом классе объявлен объект класса трейлингов. Поэтому мы заменили тип прошлого класса на новый. По сути, везде это делать не обязательно, а только там, где потребуется трейлинг. Но не будем здесь вдаваться в подробности наследования классов, а просто заменим старый класс на новый.

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

//+------------------------------------------------------------------+
//| Возвращает указатель на торговый объект символа по имени         |
//+------------------------------------------------------------------+
CSymbolTrade *GetSymbolTrade(const string symbol, CArrayObj *list)
  {
   SymbTradeTmp.SetSymbol(symbol);
   list.Sort();
   int index=list.Search(&SymbTradeTmp);
   return list.At(index);
  }

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

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

//+------------------------------------------------------------------+
//| Создаёт массив используемых символов                             |
//+------------------------------------------------------------------+
bool CreateListSymbolTrades(SDeal &array_deals[], CArrayObj *list_symbols)
  {
   bool res=true;                      // результат
   MqlParam param[7]={};               // параметры тралов
   int total=(int)array_deals.Size();  // общее количество сделок в массиве
   
//--- если массив сделок пустой - возвращаем false
   if(total==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return false;
     }
   
//--- в цикле по массиву сделок
   CSymbolTradeExt *SymbolTrade=NULL;
   for(int i=0; i<total; i++)
     {
      //--- получаем очередную сделку и, если это не покупка и не продажа - идём к следующей
      SDeal deal_str=array_deals[i];
      if(deal_str.type!=DEAL_TYPE_BUY && deal_str.type!=DEAL_TYPE_SELL)
         continue;
      
      //--- найдём торговый объект в списке, у которого символ равен символу сделки
      string symbol=deal_str.Symbol();
      SymbTradeTmp.SetSymbol(symbol);
      list_symbols.Sort();
      int index=list_symbols.Search(&SymbTradeTmp);
      
      //--- если индекс искомого объекта в списке равен -1 - такого объекта в списке нет
      if(index==WRONG_VALUE)
        {
         //--- создаём новый торговый объект символа и, если создать не получилось -
         //--- добавляем к результату значение false и идём к следующей сделке
         SymbolTrade=new CSymbolTradeExt(symbol, InpIndTimeframe);
         if(SymbolTrade==NULL)
           {
            res &=false;
            continue;
           }
         //--- если торговый объект символа не удалось добавить в список -
         //--- удаляем вновь созданный объект, добавляем к результату значение false
         //--- и идём к следующей сделке
         if(!list_symbols.Add(SymbolTrade))
           {
            delete SymbolTrade;
            res &=false;
            continue;
           }
         //--- инициализируем в торговом объекте заданный в настройках трейлинг
         ENUM_TRAILING_MODE mode=(ENUM_TRAILING_MODE)InpTestingMode;
         SetTrailingParams(mode, param);
         SymbolTrade.SetTrailing(mode, InpDataIndex, InpTestedMagic, InpTrailingStart, InpTrailingStep, InpTrailingOffset, param);
         
        }
      //--- иначе, если торговый объект уже существует в списке - получаем его по индексу
      else
        {
         SymbolTrade=list_symbols.At(index);
         if(SymbolTrade==NULL)
            continue;
        }
         
      //--- если текущей сделки ещё нет в списке сделок торгового объекта символа
      if(SymbolTrade.GetDealByTime(deal_str.time)==NULL)
        {
         //--- создаём объект сделки по её образцу-структуре
         CDeal *deal=CreateDeal(deal_str);
         if(deal==NULL)
           {
            res &=false;
            continue;
           }
         //--- к значению результата добавляем результат добавления объекта сделки в список сделок торгового объекта символа
         res &=SymbolTrade.AddDeal(deal);
        }
     }
//--- возвращаем итоговый результат создания торговых объектов и добавления сделок в их списки
   return res;
  }

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

Для установки параметров выбранного в настройках трала напишем специальную функцию:

//+------------------------------------------------------------------+
//| Устанавливает параметры трала по выбранному его типу             |
//+------------------------------------------------------------------+
void SetTrailingParams(const ENUM_TRAILING_MODE mode, MqlParam &param[])
  {
//--- обнуляем все параметры
   ZeroMemory(param);

//--- в зависимости от выбранного типа трейлинга устанавливаем параметры индикатора
   switch(mode)
     {
      case TRAILING_MODE_SAR     :
        param[0].type=TYPE_DOUBLE;
        param[0].double_value=InpSARStep;
        
        param[1].type=TYPE_DOUBLE;
        param[1].double_value=InpSARMax;
        break;
      
      case TRAILING_MODE_AMA     :
        param[0].type=TYPE_INT;
        param[0].integer_value=InpMAPeriod;
        
        param[1].type=TYPE_INT;
        param[1].integer_value=InpMAShift;

        param[2].type=TYPE_INT;
        param[2].integer_value=InpAppliedPrice;

        param[4].type=TYPE_INT;
        param[4].integer_value=InpFastEMAPeriod;

        param[5].type=TYPE_INT;
        param[5].integer_value=InpSlowEMAPeriod;
        break;
      
      case TRAILING_MODE_DEMA    :
        param[0].type=TYPE_INT;
        param[0].integer_value=InpMAPeriod;
        
        param[1].type=TYPE_INT;
        param[1].integer_value=InpMAShift;

        param[2].type=TYPE_INT;
        param[2].integer_value=InpAppliedPrice;
        break;
      
      case TRAILING_MODE_FRAMA   :
        param[0].type=TYPE_INT;
        param[0].integer_value=InpMAPeriod;
        
        param[1].type=TYPE_INT;
        param[1].integer_value=InpMAShift;

        param[2].type=TYPE_INT;
        param[2].integer_value=InpAppliedPrice;
        break;
      
      case TRAILING_MODE_MA      :
        param[0].type=TYPE_INT;
        param[0].integer_value=InpMAPeriod;
        
        param[1].type=TYPE_INT;
        param[1].integer_value=InpMAShift;

        param[2].type=TYPE_INT;
        param[2].integer_value=InpAppliedPrice;

        param[3].type=TYPE_INT;
        param[3].integer_value=InpMAMethod;
        break;
      
      case TRAILING_MODE_TEMA    :
        param[0].type=TYPE_INT;
        param[0].integer_value=InpMAPeriod;
        
        param[1].type=TYPE_INT;
        param[1].integer_value=InpMAShift;

        param[2].type=TYPE_INT;
        param[2].integer_value=InpAppliedPrice;
        break;
      
      case TRAILING_MODE_VIDYA   :
        param[0].type=TYPE_INT;
        param[0].integer_value=InpMAPeriod;
        
        param[1].type=TYPE_INT;
        param[1].integer_value=InpMAShift;

        param[2].type=TYPE_INT;
        param[2].integer_value=InpAppliedPrice;

        param[6].type=TYPE_INT;
        param[6].integer_value=InpCMOPeriod;
        break;
         
      case TRAILING_MODE_SIMPLE  :
        break;
      
      default:
        break;
     }
  }

Здесь для каждого из типов тралов, для используемых в трале индикаторов устанавливаются в структуру значения, заданные во входных параметрах советника. Присвоенные полям структуры величины проверяются на правильность и корректируются в новом классе CSymbolTradeExt торгового объекта символа, рассмотренном нами выше.

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

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

input    bool                 InpSetStopLoss    =  true;                /* Set Initial StopLoss                         */ // Устанавливать начальный StopLoss
input    bool                 InpSetTakeProfit  =  true;                /* Set Initial TakeProfit                       */ // Устанавливать начальный TakeProfit

При их использовании тоже нужно получить корректные размеры стопов и установить их для открываемой позиции:

//+------------------------------------------------------------------+
//| Торговля по истории                                              |
//+------------------------------------------------------------------+
void TradeByHistory(const string symbol="", const long magic=-1)
  {
   datetime time=0;
   int total=ExtListSymbols.Total();   // количество торговых объектов в списке
   
//--- в цикле по всем торговым объектам символов
   for(int i=0; i<total; i++)
     {
      //--- получаем очередной торговый объект
      CSymbolTradeExt *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      
      //--- получаем текущую сделку, на которую указывает индекс списка сделок
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         continue;
      
      //--- фильтруем сделку по магику и символу
      if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         continue;
      
      //--- фильтруем сделку по типу (только сделки покупки/продажи)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         continue;
      
      //--- если это уже обработанная в тестере сделка - идём к следующей
      if(deal.TicketTester()>0)
         continue;
      
      //--- если время сделки ещё не настало - идём к следующему торговому объекту следующего символа
      if(!obj.CheckTime(deal.Time()))
         continue;

      //--- если сделка входа в рынок
      ENUM_DEAL_ENTRY entry=deal.Entry();
      if(entry==DEAL_ENTRY_IN)
        {
         //--- устанавливаем размеры стоп-приказов в зависимости от метода установки стопов
         //--- изначально стоп-приказы не используются (для оригинальной торговли)
         ENUM_ORDER_TYPE order_type=(deal.TypeDeal()==DEAL_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
         double sl=0;
         double tp=0;
         //--- если режим установки указанных значений стоп-приказов
         if(InpTestingMode==TESTING_MODE_SLTP)
           {
            //--- получаем корректные значения для StopLoss и TakeProfit
            sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss);
            tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit);
           }
         //--- иначе, если тестирование с трейлингами
         else
           {
            if(InpTestingMode!=TESTING_MODE_ORIGIN)
              {
               //--- если разрешено в настройках, выставляем корректные стоп-приказы открываемым позициям
               if(InpSetStopLoss)
                  sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss);
               if(InpSetTakeProfit)
                  tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit);
              }
           }
         
         //--- открываем позицию по типу сделки
         ulong ticket=(type==DEAL_TYPE_BUY  ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : 
                       type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0);
         
         //--- если позиция открыта (получили её тикет)
         if(ticket>0)
           {
            //--- увеличиваем количество обработанных тестером сделок и записываем тикет сделки в тестере в свойства объекта сделки
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket);
            //--- получаем идентификатор позиции в тестере и записываем его в свойства объекта сделки
            long pos_id_tester=0;
            if(HistoryDealSelect(ticket))
              {
               pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
               deal.SetPosIDTester(pos_id_tester);
              }
           }
        }
      
      //--- если сделка выхода из рынка
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
        {
         //--- получаем сделку, на основании которой была открыта позиция
         CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
         if(deal_in==NULL)
            continue;

         //--- получаем тикет позиции в тестере из свойств открывающей сделки
         //--- если тикет равен нулю, значит скорее всего позиция в тестере уже закрыта
         ulong ticket_tester=deal_in.TicketTester();
         if(ticket_tester==0)
           {
            PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester());
            obj.SetNextDealIndex();
            continue;
           }
         //--- если воспроизводим в тестере оригинальную торговую историю,
         if(InpTestingMode==TESTING_MODE_ORIGIN)
           {
            //--- если позиция закрыта по тикету
            if(obj.ClosePos(ticket_tester))
              {
               //--- увеличиваем количество обработанных тестером сделок и записываем тикет сделки в тестере в свойства объекта сделки
               obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
               deal.SetTicketTester(ticket_tester);
              }
           }
         //--- иначе - в тестере работаем со стоп-приказами, выставляемыми по различным алгоритмам, и сделки закрытия пропускаются;
         //--- соответственно, для закрывающей сделки просто увеличиваем количество обработанных тестером сделок и
         //--- записываем тикет сделки в тестере в свойства объекта сделки
         else
           {
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket_tester);
           }
        }
      //--- если теперь в объекте сделки записан тикет - значит сделка успешно обработана -
      //--- устанавливаем индекс сделки в списке на следующую сделку
      if(deal.TicketTester()>0)
        {
         obj.SetNextDealIndex();
        }
     }
  }

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

//+------------------------------------------------------------------+
//| Трейлинг позиций                                                 |
//+------------------------------------------------------------------+
void Trailing(void)
  {
//--- переменные для получения свойств позиции
   long   magic=-1;
   string symbol="";
   
//--- в цикле по всем позициям
   int total=PositionsTotal();
   for(int i=total-1; i>=0; i--)
     {
      //--- получаем тикет очередной позиции
      ulong ticket=PositionGetTicket(i);
      if(ticket==0)
         continue;

      //--- получаем магик и символ позиции
      ResetLastError();
      if(!PositionGetInteger(POSITION_MAGIC, magic))
        {
         Print("PositionGetInteger() failed. Error ", GetLastError());
         continue;
        }
      if(!PositionGetString(POSITION_SYMBOL, symbol))
        {
         Print("PositionGetString() failed. Error ", GetLastError());
         continue;
        }
      
      //--- если позиция не проходит по заданным условиям магика и символа - идём к следующей
      if((InpTestedMagic>-1 && magic!=InpTestedMagic) || (InpTestedSymbol!="" && symbol!=InpTestedSymbol))
         continue;
      
      //--- получаем торговый объект по имени символа и вызываем его метод трейлинга позиции по тикету
      CSymbolTradeExt *obj=GetSymbolTrade(symbol, &ExtListSymbols);
      if(obj!=NULL)
         obj.Trailing(ticket);
     }
  }

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

Впишем вызов этой функции в обработчик OnTick() советника:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- работаем только в тестере стратегий
   if(!MQLInfoInteger(MQL_TESTER))
      return;
      
//--- Тралим открытые позиции
   Trailing();
   
//---  Обрабатываем список сделок из файла
   TradeByHistory(InpTestedSymbol, InpTestedMagic);
  }

Всё. Советник готов. С полным кодом советника можно ознакомиться в прилагаемых к статье файлах.

Давайте протестируем различные виды тралов и сравним результаты оригинальной торговли и торговли с применением трейлинга позиций по разным алгоритмам.

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

В журнал выводит используемые в торговле символы и количество совершённых на каждом символе сделок:

Alert: Now you can run testing
Interval: 2024.09.13 - current date
Initial deposit: 3000.00, leverage 1:500
Symbols used in trading:
  1. AUDUSD trade object. Total deals: 222
  2. EURJPY trade object. Total deals: 120
  3. EURUSD trade object. Total deals: 526
  4. GBPUSD trade object. Total deals: 352
  5. NZDUSD trade object. Total deals: 182
  6. USDCAD trade object. Total deals: 22
  7. USDCHF trade object. Total deals: 250
  8. USDJPY trade object. Total deals: 150
  9. XAUUSD trade object. Total deals: 118

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

Оригинальная торговля:

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

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

Простой трейлинг:

Получили убыток в 746.1 долларов. Даже больше, чем в оригинальной торговле.

Проверим тралы по индикаторам.

Трал по Parabolic SAR:

Профит - 541.8 доллара.

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

Трал по АМА:

Профит - 806.5 доллара.

Трал по DEMA:

Профит - 1397.1 долларов.

Трал по FRAMA:

Профит - 1291.6 долларов.

Трал по MA:

Профит - 563.1 доллара.

Трал по TEMA:

Профит - 1355.1 долларов.

Трал по VIDYA:

Профит - 283.3 долларов.

Итак, в итоговой таблице расположены результаты при использовании различных типов тралов StopLoss относительно оиригинальной торговли:

#
Тип трала
Размер StopLoss
Размер TakeProfit
 Итоговая прибыль
1
Оригинальная торговля
100
500
 - 658.0
2
Простой TrailingStop
Изначально 100, далее стоп на 100 пунктов от цены
0
 - 746.1
3
TrailingStop по Parabolic SAR
Изначально 100, далее стоп по значению индикатора
0
 + 541.8
4
TrailingStop по VIDYA
Изначально 100, далее стоп по значению индикатора
0
 -283.3
5
TrailingStop по MA
Изначально 100, далее стоп по значению индикатора
0
 + 563.1
6
TrailingStop по АМА
Изначально 100, далее стоп по значению индикатора
0
 + 806.5
7
TrailingStop по FRAMA
Изначально 100, далее стоп по значению индикатора
0
 + 1291.6
8
TrailingStop по TEMA
Изначально 100, далее стоп по значению индикатора
0
 + 1355.1
9
TrailingStop по DEMA
Изначально 100, далее стоп по значению индикатора
0
 + 1397.1

В итоге, при убыточной оригинальной торговле, обычный простой трал, примерный аналог трала в клиентском терминале, даёт ещё больший убыток. Ладно, смотрим трал на основе Parabolic SAR, позиционируемый как индикатор, отображающий развороты и уровни стопов (Stop And Reverse). Да, при следовании уровня StopLoss за линией индикатора на первом баре мы получили прибыль в 540 долларов.

Посмотрим теперь на результаты перемещения стопов позиций по значениям различных типов скользящих средних на первом баре. Трейлинг по индикатору VIDYA дал убыток в 280 долларов, но все остальные показали прибыль. Причём, следование стопа за простой скользящей средней дало прибыль большую, чем трал по Parabolic SAR. А абсолютным "чемпионом" по улучшению результатов торговли стала двойная экспоненциальная скользящая средняя — +1397 долларов прибыли.


Заключение

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

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

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

Программы, используемые в статье:

#
Имя
 Тип  Описание
1
Trailings.mqh
Библиотека класса
Библиотека классов трейлингов
2
SymbolTrade.mqh
Библиотека класса
Библиотека структуры и класса сделки, торговый класс символа
3
SymbolTradeExt.mqh
Библиотека класса
Торговый класс символа и трейлинга
4
TradingByHistoryDeals_Ext.mq5
Советник
Советник для просмотра и изменения при помощи StopLoss, TakeProfit и трейлингов в тестере стратегий сделок и трейдов, проторгованных на счёте
5
MQL5.zip
Архив
Архив файлов, представленных выше, для распаковки в каталог MQL5 клиентского терминала
Прикрепленные файлы |
Trailings.mqh (101.32 KB)
SymbolTrade.mqh (54.77 KB)
SymbolTradeExt.mqh (13.02 KB)
MQL5.zip (28.27 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
Roman Shiredchenko
Roman Shiredchenko | 30 янв. 2025 в 15:38
Спасибо большое за статью. Читается на одном дыхании и сразу всё понятно.
Буду использовать и фрагменты кода и ф -- ии у себя в роботах на торгах. Простой трал также.
Одни в одних роботах другой тралл в других.
Yevgeniy Koshtenko
Yevgeniy Koshtenko | 30 янв. 2025 в 23:21
Статья ТОП!
den2008224
den2008224 | 31 янв. 2025 в 05:16

Трал по VIDYA:

Профит - 283.3 долларов.

Ошибка:  Убыток- 283.3 долларов.

Alexey Viktorov
Alexey Viktorov | 31 янв. 2025 в 06:46
den2008224 #:

Ошибка:  Убыток- 283.3 долларов.

В статье, написано отрицательное значение профита.

Правда пробел после минуса закрался случайно.
Нейросети в трейдинге: Иерархический двухбашенный трансформер (Hidformer) Нейросети в трейдинге: Иерархический двухбашенный трансформер (Hidformer)
Предлагаем познакомиться с фреймворком иерархического двухбашенного трансформера (Hidformer), который был разработан для прогнозирования временных рядов и анализа данных. Авторы фреймворка предложили несколько улучшений к архитектуре Transformer, что позволило повысить точность прогнозов и снизить потребление вычислительных ресурсов.
Как интегрировать в советник концепции Smart Money (BOS) в сочетании с индикатором RSI Как интегрировать в советник концепции Smart Money (BOS) в сочетании с индикатором RSI
Концепция Smart Money (Break of Structure) в сочетании с индикатором RSI для принятия обоснованных решений в автоматической торговле на основе структуры рынка.
Разработка системы репликации (Часть 62): Нажатие кнопки воспроизведения в сервисе (III) Разработка системы репликации (Часть 62): Нажатие кнопки воспроизведения в сервисе (III)
В данной статье мы начнем решать проблему переизбытка тиков, которые могут влиять на работу приложения при использовании реальных данных. Данный переизбыток часто мешает правильному отсчету времени, необходимому для построения минутного бара в соответствующем окне.
От начального до среднего уровня: Оператор IF ELSE От начального до среднего уровня: Оператор IF ELSE
В этой статье мы проанализируем, как работать с оператором IF и ее спутником ELSE, Данный оператор - самый важный и значимый из существующих в любом языке программирования. Однако, несмотря на простоту использования, он иногда приводит в замешательство, если у нас нет опыта его применения и связанных с ней понятий. Представленные здесь материалы предназначены только для обучения. Ни в коем случае не рассматривайте его как окончательное приложение, целью которого не является изучение представленных понятий.