Рецепты MQL5 - Разработка схемы для торговой системы типа "Три экрана Элдера"

Anatoli Kazharski | 24 мая, 2013

Введение

Многие трейдеры при поиске или разработке торговых систем слышали о схеме "Три экрана", которую предложил когда-то Александр Элдер. Найдется множество людей, высказывающих свое мнение в интернете, что эта система не работает. И найдется также множество мнений, что из этого можно извлечь прибыль. Необязательно верить ни тем, ни другим. Все всегда нужно проверять самому. А если вы изучаете программирование, то все тем более в ваших руках, так как можно проверить работоспособность схемы на истории.

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

В эксперте из предыдущей статьи уже есть: возможность включить/отключить уровни Stop Loss/Take Profit, Trailing Stop, наращивание объема позиции, переворот позиции по противоположному сигналу. Все необходимы функции уже на своих местах. В итоге вся работа сводится к тому, что нужно всего лишь изменить список внешних параметров, добавив в него дополнительные опции, и модифицировать некоторые уже имеющиеся функции.

В этой схеме в качестве примера сделаем так, чтобы сигналы на трех таймфреймах формировались по индикатору Moving Average. После, для продолжения эксперимента с этой схемой, вы можете вызывать любые другие индикаторы, внеся небольшие изменения в код. Также сделаем так, чтобы для каждого "экрана" можно было устанавливать таймфреймы. Если параметр, который отвечает за период индикатора, имеет нулевое значение, то это будет означать, что этот "экран" не используется. То есть, можно настроить систему на один или два таймфрейма.

Перед тем, как начать, сделайте копию папки с экспертом из предыдущей статьи и переименуйте ее.

 

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

Начнем с внешних параметров. Ниже представлен код обновленного списка. Выделены новые строки. Таймфреймы объявлены с типом перечисления ENUM_TIMEFRAMES. Из выпадающего списка можно будет выбрать любой таймфрейм.

//--- Внешние параметры эксперта
sinput   long              MagicNumber=777;        // Магический номер
sinput   int               Deviation=10;           // Проскальзывание
//---
input    ENUM_TIMEFRAMES   Screen01TimeFrame=PERIOD_W1;  // Таймфрейм первого экрана
input    int               Screen01IndicatorPeriod=14;   // Период индикатора первого экрана
//---
input    ENUM_TIMEFRAMES   Screen02TimeFrame=PERIOD_D1;  // Таймфрейм второго экрана
input    int               Screen02IndicatorPeriod=24;   // Период индикатора второго экрана
//---
input    ENUM_TIMEFRAMES   Screen03TimeFrame=PERIOD_H4;  // Таймфрейм третьего экрана
input    int               Screen03IndicatorPeriod=44;   // Период индикатора третьего экрана
//---
input    double            Lot=0.1;                      // Лот
input    double            VolumeIncrease=0.1;           // Приращение объема позиции
input    double            VolumeIncreaseStep=10;        // Шаг для приращения объема
input    double            StopLoss=50;                  // Стоп Лосс
input    double            TakeProfit=100;               // Тейк Профит
input    double            TrailingStop=10;              // Трейлинг Стоп
input    bool              Reverse=true;                 // Разворот позиции
sinput   bool              ShowInfoPanel=true;           // Показ информационной панели

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

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

//--- Хэндлы индикаторов
int                  Screen01IndicatorHandle=INVALID_HANDLE;   // Хэндл индикатора на первом экране
int                  Screen02IndicatorHandle=INVALID_HANDLE;   // Хэндл индикатора на втором экране
int                  Screen03IndicatorHandle=INVALID_HANDLE;   // Хэндл индикатора на третьем экране

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

Так как эксперт может быть настроен на работу не только по трем таймфреймам, но и на одном или двух, то нужно учесть все варианты при определении минимального таймфрейма. Ниже представлен код функции GetMinimumTimeframe():

//+------------------------------------------------------------------+
//| Определяет минимальный таймфрейм для проверки нового бара        |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES GetMinimumTimeframe(ENUM_TIMEFRAMES timeframe1,int period1,
                                    ENUM_TIMEFRAMES timeframe2,int period2,
                                    ENUM_TIMEFRAMES timeframe3,int period3)
  {
//--- Значение минимального таймфрейма по-умолчанию
   ENUM_TIMEFRAMES timeframe_min=PERIOD_CURRENT;

//--- Конвертируем значения таймфреймов в секунды для расчетов
   int t1= PeriodSeconds(timeframe1);
   int t2= PeriodSeconds(timeframe2);
   int t3= PeriodSeconds(timeframe3);

//--- Проверка на некорректные значения периодов
   if(period1<=0 && period2<=0 && period3<=0)
      return(timeframe_min);

//--- Условия для одного таймфрейма
   if(period1>0 && period2<=0 && period3<=0)
      return(timeframe1);
   if(period2>0 && period1<=0 && period3<=0)
      return(timeframe2);
   if(period3>0 && period1<=0 && period2<=0)
      return(timeframe3);

//--- Условия для двух таймфреймов
   if(period1>0 && period2>0 && period3<=0)
     {
      timeframe_min=(MathMin(t1,t2)==t1) ? timeframe1 : timeframe2;
      return(timeframe_min);
     }
   if(period1>0 && period3>0 && period2<=0)
     {
      timeframe_min=(MathMin(t1,t3)==t1) ? timeframe1 : timeframe3;
      return(timeframe_min);
     }
   if(period2>0 && period3>0 && period1<=0)
     {
      timeframe_min=(MathMin(t2,t3)==t2) ? timeframe2 : timeframe3;
      return(timeframe_min);
     }

//--- Условия для трех таймфреймов
   if(period1>0 && period2>0 && period3>0)
     {
      timeframe_min=(int)MathMin(t1,t2)==t1 ? timeframe1 : timeframe2;
      int t_min=PeriodSeconds(timeframe_min);
      timeframe_min=(int)MathMin(t_min,t3)==t_min ? timeframe_min : timeframe3;
      return(timeframe_min);
     }
   return(WRONG_VALUE);
  }

Для сохранения минимального значения таймфрейма создадим еще одну переменную на глобальном уровне:

//--- Переменная для определения минимального таймфрейма
ENUM_TIMEFRAMES  MinimumTimeframe=WRONG_VALUE;

Функцию GetMinimumTimeframe() нужно будет вызывать при инициализации эксперта в функции OnInit().

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Определим минимальный таймфрейм для проверки нового бара
   MinimumTimeframe=GetMinimumTimeframe(Screen01TimeFrame,Screen01IndicatorPeriod,
                                        Screen02TimeFrame,Screen02IndicatorPeriod,
                                        Screen03TimeFrame,Screen03IndicatorPeriod);
//--- Получим хэндлы индикаторов
   GetIndicatorHandles();
//--- Инициализируем новый бар
   CheckNewBar();
//--- Получим свойства
   GetPositionProperties(P_ALL);
//--- Установим информационную панель
   SetInfoPanel();
//---
   return(0);
  }

Значение переменной MinimumTimeframe затем используется в функциях CheckNewBar() и GetBarsData().

Функция GetIndicatorHandle() теперь выглядит так, как показано ниже. Для каждого индикатора указываются свой период и таймфрейм.

//+------------------------------------------------------------------+
//| Получает хэндлы индикаторов                                      |
//+------------------------------------------------------------------+
void GetIndicatorHandles()
  {
//--- Получим хэндлы индикаторов, которые указаны в параметрах
   if(Screen01IndicatorPeriod>0)
     Screen01IndicatorHandle=iMA(_Symbol,Screen01TimeFrame,Screen01IndicatorPeriod,0,MODE_SMA,PRICE_CLOSE);
   if(Screen02IndicatorPeriod>0)
     Screen02IndicatorHandle=iMA(_Symbol,Screen02TimeFrame,Screen02IndicatorPeriod,0,MODE_SMA,PRICE_CLOSE);
   if(Screen03IndicatorPeriod>0)
     Screen03IndicatorHandle=iMA(_Symbol,Screen03TimeFrame,Screen03IndicatorPeriod,0,MODE_SMA,PRICE_CLOSE);
     
//--- Если не удалось получить хэндл индикатора для первого таймфрейма
   if(Screen01IndicatorHandle==INVALID_HANDLE)
     Print("Не удалось получить хэндл индикатора для Экрана 1!");
//--- Если не удалось получить хэндл индикатора для второго таймфрейма
   if(Screen01IndicatorHandle==INVALID_HANDLE)
     Print("Не удалось получить хэндл индикатора для Экрана 2!");
//--- Если не удалось получить хэндл индикатора для третьего таймфрейма
   if(Screen01IndicatorHandle==INVALID_HANDLE)
     Print("Не удалось получить хэндл индикатора для Экрана 3!");
  }

Еще нужно добавить массивы для получения значений индикаторов (для каждого таймфрейма отдельно):

//--- Массивы для значений индикаторов
double               indicator_buffer1[];
double               indicator_buffer2[];
double               indicator_buffer3[];

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

//+------------------------------------------------------------------+
//| Получает значения индикаторов                                    |
//+------------------------------------------------------------------+
bool GetIndicatorsData()
  {
//--- Количество значений индикаторного буфера для определения торгового сигнала   
   int NumberOfValues=3;
//--- Если хэндлы индикаторов не получены
   if((Screen01IndicatorPeriod>0 && Screen01IndicatorHandle==INVALID_HANDLE) ||
      (Screen02IndicatorPeriod>0 && Screen02IndicatorHandle==INVALID_HANDLE) ||
      (Screen03IndicatorPeriod>0 && Screen03IndicatorHandle==INVALID_HANDLE))
      //--- попробуем получить их еще раз
      GetIndicatorHandles();

//--- Если используется таймфрейм первого экрана и хэндл индикатора был получен
   if(Screen01TimeFrame>0 && Screen01IndicatorHandle!=INVALID_HANDLE)
     {
      //--- Установим обратный порядок индексации (... 3 2 1 0)
      ArraySetAsSeries(indicator_buffer1,true);
      //--- Получим значения индикатора
      if(CopyBuffer(Screen01IndicatorHandle,0,0,NumberOfValues,indicator_buffer1)<NumberOfValues)
        {
         Print("Не удалось скопировать значения ("+
                  _Symbol+"; "+TimeframeToString(Period())+") в массив indicator_buffer1! Ошибка ("+
                  IntegerToString(GetLastError())+"): "+ErrorDescription(GetLastError()));
         //---
         return(false);
        }
     }
//--- Если используется таймфрейм второго экрана и хэндл индикатора был получен
   if(Screen02TimeFrame>0 && Screen02IndicatorHandle!=INVALID_HANDLE)
     {
      //--- Установим обратный порядок индексации (... 3 2 1 0)
      ArraySetAsSeries(indicator_buffer2,true);
      //--- Получим значения индикатора
      if(CopyBuffer(Screen02IndicatorHandle,0,0,NumberOfValues,indicator_buffer2)<NumberOfValues)
        {
         Print("Не удалось скопировать значения ("+
                  _Symbol+"; "+TimeframeToString(Period())+") в массив indicator_buffer2! Ошибка ("+
                  IntegerToString(GetLastError())+"): "+ErrorDescription(GetLastError()));
         //---
         return(false);
        }
     }
//--- Если используется таймфрейм третьего экрана и хэндл индикатора был получен
   if(Screen03TimeFrame>0 && Screen03IndicatorHandle!=INVALID_HANDLE)
     {
      //--- Установим обратный порядок индексации (... 3 2 1 0)
      ArraySetAsSeries(indicator_buffer3,true);
      //--- Получим значения индикатора
      if(CopyBuffer(Screen03IndicatorHandle,0,0,NumberOfValues,indicator_buffer3)<NumberOfValues)
        {
         Print("Не удалось скопировать значения ("+
                  _Symbol+"; "+TimeframeToString(Period())+") в массив indicator_buffer3! Ошибка ("+
                  IntegerToString(GetLastError())+"): "+ErrorDescription(GetLastError()));
         //---
         return(false);
        }
     }
//---
   return(true);
  }

Функции GetTradingSignal() и GetSignal() нужно изменить в соответствии с текущей задачей. Ниже можно ознакомиться с кодом этих функций.

//+------------------------------------------------------------------+
//| Определяет торговые сигналы                                      |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE GetTradingSignal()
  {
//--- Если позиции нет
   if(!pos.exists)
     {
      //--- Сигнал на продажу
      if(GetSignal()==ORDER_TYPE_SELL)
         return(ORDER_TYPE_SELL);
      //--- Сигнал на покупку
      if(GetSignal()==ORDER_TYPE_BUY)
         return(ORDER_TYPE_BUY);
     }
//--- Если позиция есть
   if(pos.exists)
     {
      //--- Получим тип позиции
      GetPositionProperties(P_TYPE);
      //--- Получим цену последней сделки
      GetPositionProperties(P_PRICE_LAST_DEAL);

      //--- Сигнал на продажу
      if(pos.type==POSITION_TYPE_BUY && 
         GetSignal()==ORDER_TYPE_SELL)
         return(ORDER_TYPE_SELL);
      if(pos.type==POSITION_TYPE_SELL && 
         GetSignal()==ORDER_TYPE_SELL && 
         close_price[1]<pos.last_deal_price-CorrectValueBySymbolDigits(VolumeIncreaseStep*_Point))
         return(ORDER_TYPE_SELL);

      //--- Сигнал на покупку
      if(pos.type==POSITION_TYPE_SELL && 
         GetSignal()==ORDER_TYPE_BUY)
         return(ORDER_TYPE_BUY);
      if(pos.type==POSITION_TYPE_BUY && 
         GetSignal()==ORDER_TYPE_BUY && 
         close_price[1]>pos.last_deal_price+CorrectValueBySymbolDigits(VolumeIncreaseStep*_Point))
         return(ORDER_TYPE_BUY);

     }
//--- Отсутствие сигнала
   return(WRONG_VALUE);
  }

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

//+------------------------------------------------------------------+
//| Проверяет условие и возвращает сигнал                            |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE GetSignal()
  {
//--- СИГНАЛ НА ПРОДАЖУ: текущее значение индикаторов на сформировавшихся барах ниже, чем на предыдущих
//--- Условия для одного таймфрейма
   if(Screen01IndicatorPeriod>0 && Screen02IndicatorPeriod<=0 && Screen03IndicatorPeriod<=0)
     {
      if(indicator_buffer1[1]<indicator_buffer1[2])
         return(ORDER_TYPE_SELL);
     }
   if(Screen01IndicatorPeriod<=0 && Screen02IndicatorPeriod>0 && Screen03IndicatorPeriod<=0)
     {
      if(indicator_buffer2[1]<indicator_buffer2[2])
         return(ORDER_TYPE_SELL);
     }
//---
   if(Screen01IndicatorPeriod<=0 && Screen02IndicatorPeriod<=0 && Screen03IndicatorPeriod>0)
     {
      if(indicator_buffer3[1]<indicator_buffer3[2])
         return(ORDER_TYPE_SELL);
     }

//--- Условия для двух таймфреймов
   if(Screen01IndicatorPeriod>0 && Screen02IndicatorPeriod>0 && Screen03IndicatorPeriod<=0)
     {
      if(indicator_buffer1[1]<indicator_buffer1[2] && 
         indicator_buffer2[1]<indicator_buffer2[2])
         return(ORDER_TYPE_SELL);
     }
   if(Screen01IndicatorPeriod<=0 && Screen02IndicatorPeriod>0 && Screen03IndicatorPeriod>0)
     {
      if(indicator_buffer2[1]<indicator_buffer2[2] && 
         indicator_buffer3[1]<indicator_buffer3[2])
         return(ORDER_TYPE_SELL);
     }
   if(Screen01IndicatorPeriod>0 && Screen02IndicatorPeriod<=0 && Screen03IndicatorPeriod>0)
     {
      if(indicator_buffer1[1]<indicator_buffer1[2] && 
         indicator_buffer3[1]<indicator_buffer3[2])
         return(ORDER_TYPE_SELL);
     }

//--- Условия для трех таймфреймов
   if(Screen01IndicatorPeriod>0 && Screen02IndicatorPeriod>0 && Screen03IndicatorPeriod>0)
     {
      if(indicator_buffer1[1]<indicator_buffer1[2] && 
         indicator_buffer2[1]<indicator_buffer2[2] && 
         indicator_buffer3[1]<indicator_buffer3[2]
         )
         return(ORDER_TYPE_SELL);
     }

//--- СИГНАЛ НА ПОКУПКУ: текущее значение индикаторов на сформировавшихся барах выше, чем на предыдущих
//--- Условия для одного таймфрейма
   if(Screen01IndicatorPeriod>0 && Screen02IndicatorPeriod<=0 && Screen03IndicatorPeriod<=0)
     {
      if(indicator_buffer1[1]>indicator_buffer1[2])
         return(ORDER_TYPE_BUY);
     }
   if(Screen01IndicatorPeriod<=0 && Screen02IndicatorPeriod>0 && Screen03IndicatorPeriod<=0)
     {
      if(indicator_buffer2[1]>indicator_buffer2[2])
         return(ORDER_TYPE_BUY);
     }
   if(Screen01IndicatorPeriod<=0 && Screen02IndicatorPeriod<=0 && Screen03IndicatorPeriod>0)
     {
      if(indicator_buffer3[1]>indicator_buffer3[2])
         return(ORDER_TYPE_BUY);
     }
     
//--- Условия для двух таймфреймов
   if(Screen01IndicatorPeriod>0 && Screen02IndicatorPeriod>0 && Screen03IndicatorPeriod<=0)
     {
      if(indicator_buffer1[1]>indicator_buffer1[2] && 
         indicator_buffer2[1]>indicator_buffer2[2])
         return(ORDER_TYPE_BUY);
     }
   if(Screen01IndicatorPeriod<=0 && Screen02IndicatorPeriod>0 && Screen03IndicatorPeriod>0)
     {
      if(indicator_buffer2[1]>indicator_buffer2[2] && 
         indicator_buffer3[1]>indicator_buffer3[2])
         return(ORDER_TYPE_BUY);
     }
   if(Screen01IndicatorPeriod>0 && Screen02IndicatorPeriod<=0 && Screen03IndicatorPeriod>0)
     {
      if(indicator_buffer1[1]>indicator_buffer1[2] && 
         indicator_buffer3[1]>indicator_buffer3[2])
         return(ORDER_TYPE_BUY);
     }

//--- Условия для трёх таймфреймов
   if(Screen01IndicatorPeriod>0 && Screen02IndicatorPeriod>0 && Screen03IndicatorPeriod>0)
     {
      if(indicator_buffer1[1]>indicator_buffer1[2] && 
         indicator_buffer2[1]>indicator_buffer2[2] && 
         indicator_buffer3[1]>indicator_buffer3[2]
         )
         return(ORDER_TYPE_BUY);
     }
     
//--- Отсутствие сигнала
   return(WRONG_VALUE);
  }

Осталось только сделать небольшие изменения в функциях OnInit() и OnDeinit(). Эти изменения выделены в коде ниже:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Определим минимальный таймфрейм для проверки нового бара
   MinimumTimeframe=GetMinimumTimeframe(Screen01TimeFrame,Screen01IndicatorPeriod,
                                        Screen02TimeFrame,Screen02IndicatorPeriod,
                                        Screen03TimeFrame,Screen03IndicatorPeriod);
//--- Получим хэндлы индикаторов
   GetIndicatorHandles();
//--- Инициализируем новый бар
   CheckNewBar();
//--- Получим свойства
   GetPositionProperties(P_ALL);
//--- Установим информационную панель
   SetInfoPanel();
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Выведем в журнал причину деинициализации
   Print(GetDeinitReasonText(reason));
//--- При удалении с графика
   if(reason==REASON_REMOVE)
     {
      //--- Удалим все объекты с графика, которые относятся к информационной панели
      DeleteInfoPanel();
      //--- Удалим хэндлы индикаторов
      IndicatorRelease(Screen01IndicatorHandle);
      IndicatorRelease(Screen02IndicatorHandle);
      IndicatorRelease(Screen03IndicatorHandle);
     }
  } 

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

 

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

Далее оптимизируем параметры и посмотрим на результаты. Настройки тестера установим, как на картинке ниже (обязательно укажите младший из трех таймфреймов):

Рис. 1. Настройки тестера

Рис. 1. Настройки тестера.

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

Рис. 2. Настройки эксперта

Рис. 2. Настройки эксперта.

Оптимизация длилась приблизительно 30 минут на двухъядерном компьютере. Ниже представлен График оптимизации:

Рис. 3. График оптимизации

Рис. 3. График оптимизации.

Результат по максимальному балансу имеет меньше просадку, чем по максимальному фактору восстановления. Поэтому для демонстрации используем этот результат:

Рис. 4. Результат по максимальному балансу

Рис. 4. Результат по максимальному балансу.

Рис. 5. График результата теста по максимальному балансу

Рис. 5. График результата теста по максимальному балансу.

 

Заключение

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