Рецепты MQL5 - Как не получить ошибку при установке/изменении торговых уровней?

Anatoli Kazharski | 2 апреля, 2013

Введение

Продолжая работу над экспертом из предыдущей статьи Рецепты MQL5 - Изучение свойств позиции в тестере MetaTrader 5, на этот раз внедрим в него еще целый ряд полезных функций, а также усовершенствуем и оптимизируем уже имеющиеся.

Очень часто на форуме-(ах) по программированию на MQL можно увидеть вопросы от новичков, касающиеся ошибок при установке/модификации торговых уровней (Stop Loss, Take Profit, отложенные ордера). Думаю, уже многие знакомы с сообщением в журнале, в котором в конце строки содержится [Invalid stops]. В этой статье мы создадим функции, в которых нормализуются и проверяются на корректность значения торговых уровней перед открытием/модификацией позиции.

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

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


Процесс разработки эксперта

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

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

//--- Перечисление свойств позиции
enum ENUM_POSITION_PROPERTIES
  {
   P_SYMBOL        = 0,
   P_MAGIC         = 1,
   P_COMMENT       = 2,
   P_SWAP          = 3,
   P_COMMISSION    = 4,
   P_PRICE_OPEN    = 5,
   P_PRICE_CURRENT = 6,
   P_PROFIT        = 7,
   P_VOLUME        = 8,
   P_SL            = 9,
   P_TP            = 10,
   P_TIME          = 11,
   P_ID            = 12,
   P_TYPE          = 13,
   P_ALL           = 14
  };
//--- Перечисление свойств символа
enum ENUM_SYMBOL_PROPERTIES
  {
   S_DIGITS       = 0,
   S_SPREAD       = 1,
   S_STOPSLEVEL   = 2,
   S_POINT        = 3,
   S_ASK          = 4,
   S_BID          = 5,
   S_VOLUME_MIN   = 6,
   S_VOLUME_MAX   = 7,
   S_VOLUME_LIMIT = 8,
   S_VOLUME_STEP  = 9,
   S_FILTER       = 10,
   S_UP_LEVEL     = 11,
   S_DOWN_LEVEL   = 12,
   S_ALL          = 13
  };

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

Далее идут внешние параметры эксперта:

//--- Внешние параметры эксперта
input int            NumberOfBars=2;     // Кол-во Бычьих/Медвежьих баров для покупки/продажи
input double         Lot         =0.1;   // Лот
input double         StopLoss    =50;    // Стоп Лосс
input double         TakeProfit  =100;   // Тейк Профит
input double         TrailingStop=10;    // Трейлинг Стоп
input bool           Reverse     =true;  // Разворот позиции

Рассмотрим подробнее внешние параметры:

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

//--- Для проверки значения внешнего параметра NumberOfBars
int                  AllowedNumberOfBars=0;

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

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

//--- Свойства символа
int                  sym_digits=0;           // Количество знаков в цене после запятой
int                  sym_spread=0;           // Размер спреда в пунктах
int                  sym_stops_level=0;      // Ограничитель установки Stop ордеров
double               sym_point=0.0;          // Значение одного пункта
double               sym_ask=0.0;            // Цена ask
double               sym_bid=0.0;            // Цена bid
double               sym_volume_min=0.0;     // Минимальный объем для заключения сделки
double               sym_volume_max=0.0;     // Максимальный объем для заключения сделки
double               sym_volume_limit=0.0;   // Максимально допустимый объем для позиции и ордеров в одном направлении
double               sym_volume_step=0.0;    // Минимальный шаг изменения объема для заключения сделки
double               sym_offset=0.0;         // Отступ от максимально возможной цены для операции
double               sym_up_level=0.0;       // Цена верхнего уровня stop level
double               sym_down_level=0.0;     // Цена нижнего уровня stop level

Так как трейлинг должен рассчитываться от максимума и минимума баров, нам понадобятся массивы для этих данных баров:

//--- Массивы ценовых данных
double               close_price[]; // Close (цены закрытия бара)
double               open_price[];  // Open (цены открытия бара)
double               high_price[];  // High (цены максимума бара)
double               low_price[];   // Low (цены минимума бара)

Теперь приступим к модификации и созданию функций. У нас уже есть функция GetBarsData, в которой в ценовые массивы копируются цены открытия и закрытия баров. Теперь нам нужны еще максимумы и минимумы. Кроме того, нужно проводить корректировку значения из параметра NumberOfBars. Вот как выглядит эта функция после модификации:

//+------------------------------------------------------------------+
//| Получает значения баров                                          |
//+------------------------------------------------------------------+
void GetBarsData()
  {
//--- Скорректируем значение количества баров для условия открытия позиции
   if(NumberOfBars<=1)
      AllowedNumberOfBars=2;              // Нужно не менее двух баров
   if(NumberOfBars>=5)
      AllowedNumberOfBars=5;              // и не более 5
   else
      AllowedNumberOfBars=NumberOfBars+1; // и всегда на один больше
//--- Установим обратный порядок индексации (... 3 2 1 0)
   ArraySetAsSeries(close_price,true);
   ArraySetAsSeries(open_price,true);
   ArraySetAsSeries(high_price,true);
   ArraySetAsSeries(low_price,true);
//--- Получим цену закрытия бара
//    Если полученных значений меньше, чем запрошено, вывести сообщение об этом
   if(CopyClose(_Symbol,Period(),0,AllowedNumberOfBars,close_price)<AllowedNumberOfBars)
     {
      Print("Не удалось скопировать значения ("
            +_Symbol+", "+TimeframeToString(Period())+") в массив цен Close! "
            "Ошибка "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError()));
     }
//--- Получим цену открытия бара
//    Если полученных значений меньше, чем запрошено, вывести сообщение об этом
   if(CopyOpen(_Symbol,Period(),0,AllowedNumberOfBars,open_price)<AllowedNumberOfBars)
     {
      Print("Не удалось скопировать значения ("
            +_Symbol+", "+TimeframeToString(Period())+") в массив цен Open! "
            "Ошибка "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError()));
     }
//--- Получим цену максимума бара
//    Если полученных значений меньше, чем запрошено, вывести сообщение об этом
   if(CopyHigh(_Symbol,Period(),0,AllowedNumberOfBars,high_price)<AllowedNumberOfBars)
     {
      Print("Не удалось скопировать значения ("
            +_Symbol+", "+TimeframeToString(Period())+") в массив цен High! "
            "Ошибка "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError()));
     }
//--- Получим цену минимума бара
//    Если полученных значений меньше, чем запрошено, вывести сообщение об этом
   if(CopyLow(_Symbol,Period(),0,AllowedNumberOfBars,low_price)<AllowedNumberOfBars)
     {
      Print("Не удалось скопировать значения ("
            +_Symbol+", "+TimeframeToString(Period())+") в массив цен Low! "
            "Ошибка "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError()));
     }
  }

Не менее двух баров и всегда на один больше нужно потому, что ориентироваться будем только на сформировавшиеся бары, которые начинаются с индекса [1]. Корректировку в этом случае на самом деле можно и не делать, ведь данные баров можно начать копировать с указанного индекса в третьем параметре функций CopyOpen, CopyClose, CopyHigh и CopyLow. Ограничение в пять сформировавшихся баров тоже можно изменить (больше/меньше) на ваше усмотрение.

Функция GetTradingSignal теперь немного усложнилась, так как в зависимости от того, сколько баров указано в параметре NumberOfBars, условие будет формироваться иначе. Кроме того, теперь используется более правильный тип возвращаемого значения - тип ордера:

//+------------------------------------------------------------------+
//| Определяет торговые сигналы                                      |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE GetTradingSignal()
  {
//--- Сигнал на покупку (ORDER_TYPE_BUY) :
   if(AllowedNumberOfBars==2 && 
      close_price[1]>open_price[1])
      return(ORDER_TYPE_BUY);
   if(AllowedNumberOfBars==3 && 
      close_price[1]>open_price[1] && 
      close_price[2]>open_price[2])
      return(ORDER_TYPE_BUY);
   if(AllowedNumberOfBars==4 && 
      close_price[1]>open_price[1] && 
      close_price[2]>open_price[2] && 
      close_price[3]>open_price[3])
      return(ORDER_TYPE_BUY);
   if(AllowedNumberOfBars==5 && 
      close_price[1]>open_price[1] && 
      close_price[2]>open_price[2] && 
      close_price[3]>open_price[3] && 
      close_price[4]>open_price[4])
      return(ORDER_TYPE_BUY);
   if(AllowedNumberOfBars>=6 && 
      close_price[1]>open_price[1] && 
      close_price[2]>open_price[2] && 
      close_price[3]>open_price[3] && 
      close_price[4]>open_price[4] && 
      close_price[5]>open_price[5])
      return(ORDER_TYPE_BUY);
//--- Сигнал на продажу (ORDER_TYPE_SELL) :
   if(AllowedNumberOfBars==2 && 
      close_price[1]<open_price[1])
      return(ORDER_TYPE_SELL);
   if(AllowedNumberOfBars==3 && 
      close_price[1]<open_price[1] && 
      close_price[2]<open_price[2])
      return(ORDER_TYPE_SELL);
   if(AllowedNumberOfBars==4 && 
      close_price[1]<open_price[1] && 
      close_price[2]<open_price[2] && 
      close_price[3]<open_price[3])
      return(ORDER_TYPE_SELL);
   if(AllowedNumberOfBars==5 && 
      close_price[1]<open_price[1] && 
      close_price[2]<open_price[2] && 
      close_price[3]<open_price[3] && 
      close_price[4]<open_price[4])
      return(ORDER_TYPE_SELL);
   if(AllowedNumberOfBars>=6 && 
      close_price[1]<open_price[1] && 
      close_price[2]<open_price[2] && 
      close_price[3]<open_price[3] && 
      close_price[4]<open_price[4] && 
      close_price[5]<open_price[5])
      return(ORDER_TYPE_SELL);
//--- Отсутствие сигнала (WRONG_VALUE):
   return(WRONG_VALUE);
  }

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

//+------------------------------------------------------------------+
//| Получает свойства позиции                                        |
//+------------------------------------------------------------------+
void GetPositionProperties(ENUM_POSITION_PROPERTIES position_property)
  {
//--- Узнаем, есть ли позиция
   pos_open=PositionSelect(_Symbol);
//--- Если позиция есть, получим её свойства
   if(pos_open)
     {
      switch(position_property)
        {
         case P_SYMBOL        : pos_symbol=PositionGetString(POSITION_SYMBOL);                  break;
         case P_MAGIC         : pos_magic=PositionGetInteger(POSITION_MAGIC);                   break;
         case P_COMMENT       : pos_comment=PositionGetString(POSITION_COMMENT);                break;
         case P_SWAP          : pos_swap=PositionGetDouble(POSITION_SWAP);                      break;
         case P_COMMISSION    : pos_commission=PositionGetDouble(POSITION_COMMISSION);          break;
         case P_PRICE_OPEN    : pos_price=PositionGetDouble(POSITION_PRICE_OPEN);               break;
         case P_PRICE_CURRENT : pos_cprice=PositionGetDouble(POSITION_PRICE_CURRENT);           break;
         case P_PROFIT        : pos_profit=PositionGetDouble(POSITION_PROFIT);                  break;
         case P_VOLUME        : pos_volume=PositionGetDouble(POSITION_VOLUME);                  break;
         case P_SL            : pos_sl=PositionGetDouble(POSITION_SL);                          break;
         case P_TP            : pos_tp=PositionGetDouble(POSITION_TP);                          break;
         case P_TIME          : pos_time=(datetime)PositionGetInteger(POSITION_TIME);           break;
         case P_ID            : pos_id=PositionGetInteger(POSITION_IDENTIFIER);                 break;
         case P_TYPE          : pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); break;
         case P_ALL           :
            pos_symbol=PositionGetString(POSITION_SYMBOL);
            pos_magic=PositionGetInteger(POSITION_MAGIC);
            pos_comment=PositionGetString(POSITION_COMMENT);
            pos_swap=PositionGetDouble(POSITION_SWAP);
            pos_commission=PositionGetDouble(POSITION_COMMISSION);
            pos_price=PositionGetDouble(POSITION_PRICE_OPEN);
            pos_cprice=PositionGetDouble(POSITION_PRICE_CURRENT);
            pos_profit=PositionGetDouble(POSITION_PROFIT);
            pos_volume=PositionGetDouble(POSITION_VOLUME);
            pos_sl=PositionGetDouble(POSITION_SL);
            pos_tp=PositionGetDouble(POSITION_TP);
            pos_time=(datetime)PositionGetInteger(POSITION_TIME);
            pos_id=PositionGetInteger(POSITION_IDENTIFIER);
            pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);                     break;
         default: Print("Переданное свойство позиции не учтено в перечислении!");               return;
        }
     }
//--- Если позиции нет, обнулим переменные свойств позиции
   else
      ZeroPositionProperties();
  }

Аналогичным образом реализуем функцию GetSymbolProperties для получения свойств символа:

//+------------------------------------------------------------------+
//| Получает свойства символа                                        |
//+------------------------------------------------------------------+
void GetSymbolProperties(ENUM_SYMBOL_PROPERTIES symbol_property)
  {
   int lot_offset=1; // Количество пунктов для отступа от уровней stops level
//---
   switch(symbol_property)
     {
      case S_DIGITS        : sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);                   break;
      case S_SPREAD        : sym_spread=(int)SymbolInfoInteger(_Symbol,SYMBOL_SPREAD);                   break;
      case S_STOPSLEVEL    : sym_stops_level=(int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);   break;
      case S_POINT         : sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);                           break;
      //---
      case S_ASK           :
         sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
         sym_ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),sym_digits);                       break;
      case S_BID           :
         sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
         sym_bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),sym_digits);                       break;
         //---
      case S_VOLUME_MIN    : sym_volume_min=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);                 break;
      case S_VOLUME_MAX    : sym_volume_max=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);                 break;
      case S_VOLUME_LIMIT  : sym_volume_limit=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_LIMIT);             break;
      case S_VOLUME_STEP   : sym_volume_step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);               break;
      //---
      case S_FILTER        :
         sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
         sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
         sym_offset=NormalizeDouble(CorrectValueBySymbolDigits(lot_offset*sym_point),sym_digits);        break;
         //---
      case S_UP_LEVEL      :
         sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
         sym_stops_level=(int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
         sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
         sym_ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),sym_digits);
         sym_up_level=NormalizeDouble(sym_ask+sym_stops_level*sym_point,sym_digits);                     break;
         //---
      case S_DOWN_LEVEL    :
         sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
         sym_stops_level=(int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
         sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
         sym_bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),sym_digits);
         sym_down_level=NormalizeDouble(sym_bid-sym_stops_level*sym_point,sym_digits);                   break;
         //---
      case S_ALL           :
         sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
         sym_spread=(int)SymbolInfoInteger(_Symbol,SYMBOL_SPREAD);
         sym_stops_level=(int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
         sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
         sym_ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),sym_digits);
         sym_bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),sym_digits);
         sym_volume_min=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
         sym_volume_max=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);
         sym_volume_limit=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_LIMIT);
         sym_volume_step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
         sym_offset=NormalizeDouble(CorrectValueBySymbolDigits(lot_offset*sym_point),sym_digits);
         sym_up_level=NormalizeDouble(sym_ask+sym_stops_level*sym_point,sym_digits);
         sym_down_level=NormalizeDouble(sym_bid-sym_stops_level*sym_point,sym_digits);                   break;
         //---
      default: Print("Переданное свойство символа не учтено в перечислении!"); return;
     }
  }

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

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

//+------------------------------------------------------------------+
//| Коррекция значения по количеству знаков в цене (int)             |
//+------------------------------------------------------------------+
int CorrectValueBySymbolDigits(int value)
  {
   return (sym_digits==3 || sym_digits==5) ? value*=10 : value;
  }
//+------------------------------------------------------------------+
//| Коррекция значения по количеству знаков в цене (double)          |
//+------------------------------------------------------------------+
double CorrectValueBySymbolDigits(double value)
  {
   return (sym_digits==3 || sym_digits==5) ? value*=10 : value;
  }

В нашем эксперте будет внешний параметр для указания объема (Lot) открываемой позиции. Сделаем функцию CalculateLot, которая будет корректировать объем в соответствии со спецификацией символа:

//+------------------------------------------------------------------+
//| Рассчитывает объем для позиции                                   |
//+------------------------------------------------------------------+
double CalculateLot(double lot)
  {
//--- Для корректировки с учетом шага
   double corrected_lot=0.0;
//---
   GetSymbolProperties(S_VOLUME_MIN);  // Получим минимально возможный лот
   GetSymbolProperties(S_VOLUME_MAX);  // Получим максимально возможный лот
   GetSymbolProperties(S_VOLUME_STEP); // Получим шаг увеличения/уменьшения лота
//--- Скорректируем с учетом шага лота
   corrected_lot=MathRound(lot/sym_volume_step)*sym_volume_step;
//--- Если меньше минимального, вернем минимальный
   if(corrected_lot<sym_volume_min)
      return(NormalizeDouble(sym_volume_min,2));
//--- Если больше максимального, вернем максимальный
   if(corrected_lot>sym_volume_max)
      return(NormalizeDouble(sym_volume_max,2));
//---
   return(NormalizeDouble(corrected_lot,2));
  }

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

Функция CalculateTakeProfit предназначена для расчета значения уровня фиксации прибыли:

//+------------------------------------------------------------------+
//| Рассчитывает уровень Take Profit                                 |
//+------------------------------------------------------------------+
double CalculateTakeProfit(ENUM_ORDER_TYPE order_type)
  {
//--- Если Take Profit нужен
   if(TakeProfit>0)
     {
      //--- Для рассчитанного значения Take Profit
      double tp=0.0;
      //--- Если нужно рассчитать значение для позиции SELL
      if(order_type==ORDER_TYPE_SELL)
        {
         //--- Рассчитаем уровень
         tp=NormalizeDouble(sym_bid-CorrectValueBySymbolDigits(TakeProfit*sym_point),sym_digits);
         //--- Вернем рассчитанное значение, если оно ниже нижней границы stops level
         //    Если значение выше или равно, вернем скорректированное значение
         return(tp<sym_down_level ? tp : sym_down_level-sym_offset);
        }
      //--- Если нужно рассчитать значение для позиции BUY
      if(order_type==ORDER_TYPE_BUY)
        {
         //--- Рассчитаем уровень
         tp=NormalizeDouble(sym_ask+CorrectValueBySymbolDigits(TakeProfit*sym_point),sym_digits);
         //--- Вернем рассчитанное значение, если оно выше верхней границы stops level
         //    Если значение ниже или равно, вернем скорректированное значение
         return(tp>sym_up_level ? tp : sym_up_level+sym_offset);
        }
     }
//---
   return(0.0);
  }

Функция CalculateStopLoss предназначена для расчета значения защитного уровня:

//+------------------------------------------------------------------+
//| Рассчитывает уровень Stop Loss                                   |
//+------------------------------------------------------------------+
double CalculateStopLoss(ENUM_ORDER_TYPE order_type)
  {
//--- Если Stop Loss нужен
   if(StopLoss>0)
     {
      //--- Для рассчитанного значения Stop Loss
      double sl=0.0;
      //--- Если нужно рассчитать значение для позиции BUY
      if(order_type==ORDER_TYPE_BUY)
        {
         // Рассчитаем уровень
         sl=NormalizeDouble(sym_ask-CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
         //--- Вернем рассчитанное значение, если оно ниже нижней границы stops level
         //    Если значение выше или равно, вернем скорректированное значение
         return(sl<sym_down_level ? sl : sym_down_level-sym_offset);
        }
      //--- Если нужно рассчитать значение для позиции SELL
      if(order_type==ORDER_TYPE_SELL)
        {
         //--- Рассчитаем уровень
         sl=NormalizeDouble(sym_bid+CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
         //--- Вернем рассчитанное значение, если оно выше верхней границы stops level
         //    Если значение ниже или равно, вернем скорректированное значение
         return(sl>sym_up_level ? sl : sym_up_level+sym_offset);
        }
     }
//---
   return(0.0);
  }

Функция CalculateTrailingStop предназначена для расчета значения трейлинга защитного уровня:

//+------------------------------------------------------------------+
//| Рассчитывает уровень Trailing Stop                               |
//+------------------------------------------------------------------+
double CalculateTrailingStop(ENUM_POSITION_TYPE position_type)
  {
//--- Переменные для расчётов
   double            level       =0.0;
   double            buy_point   =low_price[1];    // Значение Low для Buy
   double            sell_point  =high_price[1];   // Значение High для Sell
//--- Рассчитаем уровень для позиции BUY
   if(position_type==POSITION_TYPE_BUY)
     {
      //--- Минимум бара минус указанное количество пунктов
      level=NormalizeDouble(buy_point-CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
      //--- Если рассчитанный уровень ниже, чем нижний уровень ограничения (stops level), 
      //    то расчет закончен, вернем текущее значение уровня
      if(level<sym_down_level)
         return(level);
      //--- Если же не ниже, то попробуем рассчитать от цены bid
      else
        {
         level=NormalizeDouble(sym_bid-CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
         //--- Если рассчитанный уровень ниже ограничителя, вернем текущее значение уровня
         //    Иначе установим максимально возможный близкий
         return(level<sym_down_level ? level : sym_down_level-sym_offset);
        }
     }
//--- Рассчитаем уровень для позиции SELL
   if(position_type==POSITION_TYPE_SELL)
     {
      // Максимум бара плюс указанное кол-во пунктов
      level=NormalizeDouble(sell_point+CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
      //--- Если рассчитанный уровень выше, чем верхний уровень ограничения (stops level), 
      //    то расчёт закончен, вернем текущее значение уровня
      if(level>sym_up_level)
         return(level);
      //--- Если же не выше, то попробуем рассчитать от цены ask
      else
        {
         level=NormalizeDouble(sym_ask+CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
         //--- Если рассчитанный уровень выше ограничителя, вернем текущее значение уровня
         //    Иначе установим максимально возможный близкий
         return(level>sym_up_level ? level : sym_up_level+sym_offset);
        }
     }
//---
   return(0.0);
  }

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

Обратите внимание, как используются все те функции, которые были созданы/модифицированы выше. С помощью переключателя switch в зависимости от того, какой тип у текущей позиции, определяется соответствующее условие, результат которого сохраняется в переменной condition. Для модификации позиции используется метод PositionModify из торгового класса CTrade стандартной библиотеки.

//+------------------------------------------------------------------+
//| Изменяет уровень Trailing Stop                                   |
//+------------------------------------------------------------------+
void ModifyTrailingStop()
  {
//--- Если включен трейлинг и StopLoss
   if(TrailingStop>0 && StopLoss>0)
     {
      double         new_sl=0.0;       // Для расчета нового уровня Stop loss
      bool           condition=false;  // Для проверки условия на модификацию
      //--- Получим флаг наличия/отсутствия позиции
      pos_open=PositionSelect(_Symbol);
      //--- Если есть позиция
      if(pos_open)
        {
         //--- Получим свойства символа
         GetSymbolProperties(S_ALL);
         //--- Получим свойства позиции
         GetPositionProperties(P_ALL);
         //--- Получим уровень для Stop Loss
         new_sl=CalculateTrailingStop(pos_type);
         //--- В зависимости от типа позиции проверим соответствующее условие на модификацию Trailing Stop
         switch(pos_type)
           {
            case POSITION_TYPE_BUY  :
               //--- Если новое значение для Stop Loss выше,
               //    чем текущее значение плюс установленный шаг
               condition=new_sl>pos_sl+CorrectValueBySymbolDigits(TrailingStop*sym_point);
               break;
            case POSITION_TYPE_SELL :
               //--- Если новое значение для Stop Loss ниже,
               //    чем текущее значение минус установленный шаг
               condition=new_sl<pos_sl-CorrectValueBySymbolDigits(TrailingStop*sym_point);
               break;
           }
         //--- Если Stop Loss есть, то сравним значения перед модификацией
         if(pos_sl>0)
           {
            //--- Если выполняется условие на модификацию ордера, т.е. новое значение ниже/выше, 
            //    чем текущее, модифицируем защитный уровень позиции
            if(condition)
              {
               if(!trade.PositionModify(_Symbol,new_sl,pos_tp))
                  Print("Ошибка при модификации позиции: ",GetLastError()," - ",ErrorDescription(GetLastError()));
              }
           }
         //--- Если Stop Loss нет, то просто установим его
         if(pos_sl==0)
           {
            if(!trade.PositionModify(_Symbol,new_sl,pos_tp))
               Print("Ошибка при модификации позиции: ",GetLastError()," - ",ErrorDescription(GetLastError()));
           }
        }
     }
  }

Теперь приведем в порядок функцию TradingBlock в соответствии со всеми изменениями выше. Так же, как и в функции ModifyTrailingStop, будем определять все значения переменных для торгового приказа с помощью переключателя switch. Это значительно сокращает объем кода и упрощает последующие модификации, так как вместо одной ветки для двух типов позиции остается одна.

//+------------------------------------------------------------------+
//| Торговый блок                                                    |
//+------------------------------------------------------------------+
void TradingBlock()
  {
   ENUM_ORDER_TYPE      signal=WRONG_VALUE;                 // Переменная для приема сигнала
   string               comment="hello :)";                 // Комментарий для позиции
   double               tp=0.0;                             // Take Profit
   double               sl=0.0;                             // Stop Loss
   double               lot=0.0;                            // Объем для расчета позиции в случае переворота позиции
   double               position_open_price=0.0;            // Цена открытия позиции
   ENUM_ORDER_TYPE      order_type=WRONG_VALUE;             // Тип ордера для открытия позиции
   ENUM_POSITION_TYPE   opposite_position_type=WRONG_VALUE; // Противоположный тип позиции
//--- Получим сигнал
   signal=GetTradingSignal();
//--- Если сигнала нет, выходим
   if(signal==WRONG_VALUE)
      return;
//--- Узнаем, есть ли позиция
   pos_open=PositionSelect(_Symbol);
//--- Получим все свойства символа
   GetSymbolProperties(S_ALL);
//--- Определим значения торговым переменным
   switch(signal)
     {
      //--- Присвоим переменным значения для BUY
      case ORDER_TYPE_BUY  :
         position_open_price=sym_ask;
         order_type=ORDER_TYPE_BUY;
         opposite_position_type=POSITION_TYPE_SELL;
         break;
         //--- Присвоим переменным значения для SELL
      case ORDER_TYPE_SELL :
         position_open_price=sym_bid;
         order_type=ORDER_TYPE_SELL;
         opposite_position_type=POSITION_TYPE_BUY;
         break;
     }
//--- Рассчитаем уровни Take Profit и Stop Loss
   sl=CalculateStopLoss(order_type);
   tp=CalculateTakeProfit(order_type);
//--- Если позиции нет
   if(!pos_open)
     {
      //--- Скорректируем объём
      lot=CalculateLot(Lot);
      //--- Откроем позицию
      //    Если позиция не открылась, вывести сообщение об этом
      if(!trade.PositionOpen(_Symbol,order_type,lot,position_open_price,sl,tp,comment))
        {
         Print("Ошибка при открытии позиции: ",GetLastError()," - ",ErrorDescription(GetLastError()));
        }
     }
//--- Если позиция есть
   else
     {
      //--- Получим тип позиции
      GetPositionProperties(P_TYPE);
      //--- Если позиция противоположна сигналу и включен переворот позиции
      if(pos_type==opposite_position_type && Reverse)
        {
         //--- Получим объём позиции
         GetPositionProperties(P_VOLUME);
         //--- Скорректируем объём
         lot=pos_volume+CalculateLot(Lot);
         //--- Откроем позицию. Если позиция не открылась, вывести сообщение об этом
         if(!trade.PositionOpen(_Symbol,order_type,lot,position_open_price,sl,tp,comment))
           {
            Print("Ошибка при открытии позиции: ",GetLastError()," - ",ErrorDescription(GetLastError()));
           }
        }
     }
//---
   return;
  }

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

//+------------------------------------------------------------------+
//| Возвращает флаг тестирования                                     |
//+------------------------------------------------------------------+
bool IsTester()
  {
   return(MQL5InfoInteger(MQL5_TESTER));
  }
//+------------------------------------------------------------------+
//| Возвращает флаг оптимизации                                      |
//+------------------------------------------------------------------+
bool IsOptimization()
  {
   return(MQL5InfoInteger(MQL5_OPTIMIZATION));
  }
//+------------------------------------------------------------------+
//| Возвращает флаг визуального режима тестирования                  |
//+------------------------------------------------------------------+
bool IsVisualMode()
  {
   return(MQL5InfoInteger(MQL5_VISUAL_MODE));
  }
//+------------------------------------------------------------------+
//| Возвращает флаг режима реального времени вне тестера,            |
//| если все условия выполняются                                     |
//+------------------------------------------------------------------+
bool IsRealtime()
  {
   if(!IsTester() && !IsOptimization() && !IsVisualMode())
      return(true);
   else
      return(false); 
  }

В функции SetInfoPanel теперь нужно всего лишь добавить условие, которое будет говорить программе, что информационную панель нужно выводить только в режимах визуализации и реального времени. Если этого не сделать, то время тестирования будет возрастет примерно в 4-5 раз. Это особенно актуально, когда проводится оптимизация параметров.

//+------------------------------------------------------------------+
//| Устанавливает информационную панель                              |
//|------------------------------------------------------------------+
void SetInfoPanel()
  {
//--- Режим визуализации или реального времени
   if(IsVisualMode() || IsRealtime())
     {
     // Остальной код функции SetInfoPanel()
     // ...
     }
  }

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Инициализируем новый бар
   CheckNewBar();
//--- Получим свойства и установим панель
   GetPositionProperties(P_ALL);
//--- Установим информационную панель
   SetInfoPanel();
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Если бар не новый, выходим
   if(!CheckNewBar())
      return;
//--- Если есть новый бар
   else
     {
      GetBarsData();          // Получим данные баров
      TradingBlock();         // Проверим условия и торгуем
      ModifyTrailingStop();   // Изменим уровень Trailing Stop
     }
//--- Получим свойства и обновим значения на панели
   GetPositionProperties(P_ALL);
//--- Обновим информационную панель
   SetInfoPanel();
  }
//+------------------------------------------------------------------+
//| Торговое событие                                                 |
//+------------------------------------------------------------------+
void OnTrade()
  {
//--- Получить свойства позиции и обновить значения на панели
   GetPositionProperties(P_ALL);
//--- Обновим информационную панель
   SetInfoPanel();
  }


Оптимизация параметров и тестирование эксперта

Теперь произведем оптимизацию параметров. Настройки тестера установим, например так, как на рисунке ниже:

Рис. 1. Настройки тестера для оптимизации параметров.

Рис. 1. Настройки тестера для оптимизации параметров.

Настройки эксперта установим с широкими диапазонами параметров:

Рис. 2. Настройки эксперта для оптимизации параметров.

Рис. 2. Настройки эксперта для оптимизации параметров.

На двухъядерном процессоре (Intel Core2 Duo P7350 @ 2.00GHz) время оптимизации заняло около 7 минут. Результат по максимальному фактору восстановления получился вот таким:

Рис. 3. Результат теста по максимальному фактору восстановления.

Рис. 3. Результат теста по максимальному фактору восстановления.


Заключение

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