Универсальный индикатор RSI для работы одновременно в двух направлениях

Anatoli Kazharski | 12 июня, 2018

Содержание

Введение

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

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

На схеме ниже показано, как к главному файлу индикатора (Indicator.mq5) подключены два класса индикаторов (CIndicator1 и CIndicator2). CIndicator2 — вспомогательный индикатор, результаты его расчетов нужны для CIndicator1. Применим здесь метод для определения истинного бара, о котором мы уже говорили в статье о создании мультивалютных индикаторов. Для этой статьи написан отдельный класс CFirstTrueBar. Он будет подключаться ко всем индикаторам, чтобы исключить расчёт на тех барах, которые не относятся к текущему таймфрейму.

 Рис. 1 – Одна из возможных схем при создании индикатора методом ООП.

Рис. 1. Одна из возможных схем при создании индикатора методом ООП.


Выбор индикатора

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

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

Для примера в этой серии статей возьмём индикатор RSI (Relative Strength Index). Ниже показан результат вычислений этого индикатора с периодом 8 на графике AUDUSD, H1

 Рис. 2. Индикатор Relative Strength Index.

Рис. 2. Индикатор Relative Strength Index.

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

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

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

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

 Рис. 3. Сигналы по индикатору RSI.

Рис. 3. Сигналы по индикатору RSI.


Доработка индикатора RSI

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

Первая версия. Добавляем сигнальные буферы

Стандартная версия RSI находится в директории терминала \MQL5\Indicators\Examples. Сделаем его копию и начнем вносить в нее изменения. В списке фиксированных параметров добавим ещё два индикаторных буфера. Общее их количество теперь будет равно 5, а отображающихся на графике будет 3. Два буфера останутся зарезервированными для вспомогательных расчётов. Метки на покупку отобразим зелёным цветом (clrMediumSeaGreen), а метки на продажу — красным (clrRed). 

//--- Свойства
#property indicator_separate_window
#property indicator_minimum 0
#property indicator_maximum 100
#property indicator_buffers 5
#property indicator_plots   3
#property indicator_color1  clrSteelBlue
#property indicator_color2  clrMediumSeaGreen
#property indicator_color3  clrRed

Определим коды для меток сигналов. Если нужно, чтобы отображались точки, то это код 159. Если сигналы нужно отображать в виде стрелок, то это коды 233 и 234, соответственно.

//--- Стрелки для сигналов: 159 - точки; 233/234 - стрелки
#define ARROW_BUY  159
#define ARROW_SELL 159

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

Ниже все эти режимы будут показаны на графике.

//--- Перечисление режимов пробоя границ канала
enum ENUM_BREAK_INOUT
  {
   BREAK_IN          =0, // Break in
   BREAK_IN_REVERSE  =1, // Break in reverse
   BREAK_OUT         =2, // Break out
   BREAK_OUT_REVERSE =3  // Break out reverse
  };

Всего в индикаторе будут три внешних параметра:

//--- Входные параметры
input  int              PeriodRSI   =8;         // RSI Period
input  double           SignalLevel =30;        // Signal Level
input  ENUM_BREAK_INOUT BreakMode   =BREAK_OUT; // Mode Break

Свойства индикатора устанавливаем в функции SetPropertiesIndicator(). Вспомогательные массивы устанавливаем в последнюю очередь. Все индикаторные массивы инициализируются нулевыми значениями в функции ZeroIndicatorBuffers(). Затем  укажем, что нулевые значения не нужно отображать на графике, то есть такие значения будут пустыми.

//+------------------------------------------------------------------+
//| Устанавливает свойства индикатора                                |
//+------------------------------------------------------------------+
void SetPropertiesIndicator(void)
  {
//--- Короткое имя
   ::IndicatorSetString(INDICATOR_SHORTNAME,"RSI_PLUS1");
//--- Знаков после запятой
   ::IndicatorSetInteger(INDICATOR_DIGITS,2);
//--- Массивы индикатора
   ::SetIndexBuffer(0,rsi_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(1,buy_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(2,sell_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(3,pos_buffer,INDICATOR_CALCULATIONS);
   ::SetIndexBuffer(4,neg_buffer,INDICATOR_CALCULATIONS);
//--- Инициализация массивов
   ZeroIndicatorBuffers();
//--- Установим текстовые метки
   string plot_label[]={"RSI","buy","sell"};
   for(int i=0; i<indicator_plots; i++)
      ::PlotIndexSetString(i,PLOT_LABEL,plot_label[i]);
//--- Установим толщину для индикаторных массивов
   for(int i=0; i<indicator_plots; i++)
      ::PlotIndexSetInteger(i,PLOT_LINE_WIDTH,1);
//--- Установим тип для индикаторных массивов
   ENUM_DRAW_TYPE draw_type[]={DRAW_LINE,DRAW_ARROW,DRAW_ARROW};
   for(int i=0; i<indicator_plots; i++)
      ::PlotIndexSetInteger(i,PLOT_DRAW_TYPE,draw_type[i]);
//--- Коды меток
   ::PlotIndexSetInteger(1,PLOT_ARROW,ARROW_BUY);
   ::PlotIndexSetInteger(2,PLOT_ARROW,ARROW_SELL);
//--- Индекс элемента, от которого начинается расчёт
   for(int i=0; i<indicator_plots; i++)
      ::PlotIndexSetInteger(i,PLOT_DRAW_BEGIN,period_rsi);
//--- Количество горизонтальных уровней индикатора
   ::IndicatorSetInteger(INDICATOR_LEVELS,2);
//--- Значения горизонтальных уровней индикатора
   up_level   =100-SignalLevel;
   down_level =SignalLevel;
   ::IndicatorSetDouble(INDICATOR_LEVELVALUE,0,down_level);
   ::IndicatorSetDouble(INDICATOR_LEVELVALUE,1,up_level);
//--- Стиль линии
   ::IndicatorSetInteger(INDICATOR_LEVELSTYLE,0,STYLE_DOT);
   ::IndicatorSetInteger(INDICATOR_LEVELSTYLE,1,STYLE_DOT);
//--- Пустое значение для построения, для которого нет отрисовки
   for(int i=0; i<indicator_buffers; i++)
      ::PlotIndexSetDouble(i,PLOT_EMPTY_VALUE,0);
//--- Сдвиг по оси Y
   if(BreakMode==BREAK_IN_REVERSE || BreakMode==BREAK_OUT_REVERSE)
     {
      ::PlotIndexSetInteger(0,PLOT_ARROW_SHIFT,arrow_shift);
      ::PlotIndexSetInteger(1,PLOT_ARROW_SHIFT,-arrow_shift);
     }
   else
     {
      ::PlotIndexSetInteger(0,PLOT_ARROW_SHIFT,-arrow_shift);
      ::PlotIndexSetInteger(1,PLOT_ARROW_SHIFT,arrow_shift);
     }
  }

Предварительные и основные расчёты значений индикатора RSI вынесены для удобства в отдельные функции PreliminaryCalculations() и CalculateRSI(). Их содержание такое же, как в индикаторе RSI из стандартной поставки. Рассмотрим только функцию для определения сигналов индикатора — CalculateSignals(). Здесь сначала проверяются условия, в зависимости от установленного во внешних параметрах режима. Затем, если условия исполняются, в соответствующий индикаторный массив сохраняем значение индикатора RSI. Если же условие не выполняется, то будем сохранять нулевые значения, которые не будут отображаться на графике.

//+------------------------------------------------------------------+
//| Рассчитывает сигналы индикатора                                  |
//+------------------------------------------------------------------+
void CalculateSignals(const int i)
  {
   bool condition1 =false;
   bool condition2 =false;
//--- Пробой внутрь канала
   if(BreakMode==BREAK_IN || BreakMode==BREAK_IN_REVERSE)
     {
      condition1 =rsi_buffer[i-1]<down_level && rsi_buffer[i]>down_level;
      condition2 =rsi_buffer[i-1]>up_level && rsi_buffer[i]<up_level;
     }
   else
     {
      condition1 =rsi_buffer[i-1]<up_level && rsi_buffer[i]>up_level;
      condition2 =rsi_buffer[i-1]>down_level && rsi_buffer[i]<down_level;
     }
//--- Отображаем сигналы, если условия исполнились
   if(BreakMode==BREAK_IN || BreakMode==BREAK_OUT)
     {
      buy_buffer[i]  =(condition1)? rsi_buffer[i] : 0;
      sell_buffer[i] =(condition2)? rsi_buffer[i] : 0;
     }
   else
     {
      buy_buffer[i]  =(condition2)? rsi_buffer[i] : 0;
      sell_buffer[i] =(condition1)? rsi_buffer[i] : 0;
     }
  }

В итоге код основных функций индикатора, таких как OnInit() и OnCalculate(), будет выглядеть аккуратным и читаемым:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
void OnInit(void)
  {
//--- Проверка значения внешнего параметра
   if(PeriodRSI<1)
     {
      period_rsi=2;
      Print("Incorrect value for input variable PeriodRSI =",PeriodRSI,
            "Indicator will use value =",period_rsi,"for calculations.");
     }
   else
      period_rsi=PeriodRSI;
//--- Установим свойства индикатора
   SetPropertiesIndicator();
  }
//+------------------------------------------------------------------+
//| Relative Strength Index                                          |
//+------------------------------------------------------------------+
int OnCalculate(const int      rates_total,
                const int      prev_calculated,
                const datetime &time[],
                const double   &open[],
                const double   &high[],
                const double   &low[],
                const double   &close[],
                const long     &tick_volume[],
                const long     &volume[],
                const int      &spread[])
  {
//--- Выйти, если данных недостаточно
   if(rates_total<=period_rsi)
      return(0);
//--- Предварительные расчеты
   PreliminaryCalculations(prev_calculated,close);
//--- Основной цикл для расчётов
   for(int i=start_pos; i<rates_total && !::IsStopped(); i++)
     {
      //--- Рассчитывает индикатор RSI
      CalculateRSI(i,close);
      //--- Рассчитывает сигналы
      CalculateSignals(i);
     }
//--- Вернуть последнее рассчитанное количество элементов
   return(rates_total);
  }

Загрузим этот индикатор на график, чтобы посмотреть, что получилось. Ниже показаны результаты во всех четырёх режимах работы (внешний параметр Break Mode).

 Рис. 4. Демонстрация работы модифицированного индикатора RSI.

Рис. 4. Демонстрация работы модифицированного индикатора RSI.

После такой модификации индикатора RSI уже лучше видно, сколько ложных сигналов он даёт. Рассматривая графики, приходим к выводу, что по этому индикатору иногда лучше торговать во флэте, а иногда — по тренду. 

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

Вторая версия. Добавляем буферы сигнальных счётчиков

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

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

//--- Свойства
...
#property indicator_buffers 7
#property indicator_plots   5
...
#property indicator_color4  clrMediumSeaGreen
#property indicator_color5  clrRed

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

//--- Индикаторные массивы
...
double buy_counter_buffer[];
double sell_counter_buffer[];
//--- Счётчики непрерывных последовательностей сигналов
int buy_counter  =0;
int sell_counter =0;

Остальные изменения касаются только функции для определения сигналов. Здесь, если условия исполняются (появляется очередной сигнал на покупку или продажу), срабатывает и соответствующий счётчик, а счётчик для противоположной серии сигналов обнуляется. Чтобы исключить увеличение счётчика на текущем баре, нужно пропускать его срабатывание на последнем несформировавшемся баре.

//+------------------------------------------------------------------+
//| Рассчитывает сигналы индикатора                                  |
//+------------------------------------------------------------------+
void CalculateSignals(const int i,const int rates_total)
  {
   int last_index=rates_total-1;
//---
   bool condition1 =false;
   bool condition2 =false;
//--- Пробой внутрь канала
   if(BreakMode==BREAK_IN || BreakMode==BREAK_IN_REVERSE)
     {
      condition1 =rsi_buffer[i-1]<down_level && rsi_buffer[i]>down_level;
      condition2 =rsi_buffer[i-1]>up_level && rsi_buffer[i]<up_level;
     }
   else
     {
      condition1 =rsi_buffer[i-1]<up_level && rsi_buffer[i]>up_level;
      condition2 =rsi_buffer[i-1]>down_level && rsi_buffer[i]<down_level;
     }
//--- Отображаем сигналы, если условия исполнились
   if(BreakMode==BREAK_IN || BreakMode==BREAK_OUT)
     {
      buy_buffer[i]  =(condition1)? rsi_buffer[i] : 0;
      sell_buffer[i] =(condition2)? rsi_buffer[i] : 0;
      //--- Счётчик только по сформировавшимся барам
      if(i<last_index)
        {
         if(condition1)
           {
            buy_counter++;
            sell_counter=0;
           }
         else if(condition2)
           {
            sell_counter++;

            buy_counter=0;
           }
        }
     }
   else
     {
      buy_buffer[i]  =(condition2)? rsi_buffer[i] : 0;
      sell_buffer[i] =(condition1)? rsi_buffer[i] : 0;
      //--- Счётчик только по сформировавшимся барам
      if(i<last_index)
        {
         if(condition2)
           {
            buy_counter++;
            sell_counter=0;
           }
         else if(condition1)
           {
            sell_counter++;
            buy_counter=0;
           }
        }
     }
//--- Корректировка последнего значения (равно предпоследнему)
   if(i<last_index)
     {
      buy_counter_buffer[i]  =buy_counter;
      sell_counter_buffer[i] =sell_counter;
     }
   else
     {
      buy_counter_buffer[i]  =buy_counter_buffer[i-1];
      sell_counter_buffer[i] =sell_counter_buffer[i-1];
     }
  }

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

 Рис. 5. Модифицированный индикатор RSI со счётчиками непрерывных однонаправленных сигналов.

Рис. 5. Модифицированный индикатор RSI со счётчиками непрерывных однонаправленных сигналов.

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

 Рис. 6. Проверка работы модифицированного индикатора RSI в тестере стратегий.

Рис. 6. Проверка работы модифицированного индикатора RSI в тестере стратегий.

На следующем скриншоте смотрим, что показывает индикатор во всех режимах параметра Break Mode.

 Рис. 7. Индикатор во всех режимах параметра Break Mode.

Рис. 7. Индикатор во всех режимах параметра Break Mode.

На графике можно увидеть ситуации, когда между двумя однонаправленными сигналами цена может пройти довольно большое расстояние (см. рис. 8). Такое случается очень часто и на любых таймфреймах. При этом можно сколько угодно изменять внешний параметр Signal Level, настраивая границы диапазона — проблемы это не решает. Такая неопределённость может помешать в создании чёткой торговой логики или усложнить её необходимостью писать дополнительные условия. 

 Рис. 8. Пропуски сигналов при значительном прохождении цены.

Рис. 8. Пропуски сигналов при значительном прохождении цены.

В следующей версии устраним подобные пропуски, сделав индикатор ещё более информативным.  

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

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

//--- Значения горизонтальных уровней индикатора и их количество
double up_level          =0;
double down_level        =0;
int    up_levels_total   =0;
int    down_levels_total =0;
//--- Массивы горизонтальных уровней 
double up_levels[];
double down_levels[];

Чтобы не было пропусков сигналов, о которых шла речь в предыдущем разделе, будем устанавливать уровни через каждые 5 пунктов. То есть, если во внешнем параметре Signal Level установлено значение 30, то для верхних уровней будут рассчитаны такие значения: 70, 75, 80, 85, 90, 95. 

Для расчёта уровней индикатора предназначена функция GetLevelsIndicator(). В двух отдельных циклах рассчитываются значения уровней, которые помещаются в массивы. Функция возвращает общее количество уровней.

//+------------------------------------------------------------------+
//| Возвращает уровни индикатора                                     |
//+------------------------------------------------------------------+
int GetLevelsIndicator(void)
  {
   int levels_counter=0;
   double level=down_level;
//--- Нижние уровни до нижнего предела
   while(level>0 && !::IsStopped())
     {
      int size=::ArraySize(down_levels);
      ::ArrayResize(down_levels,size+1);
      down_levels[size]=level;
      level-=5;
      levels_counter++;
     }
   level=up_level;
//--- Верхние уровни до верхнего предела
   while(level<100 && !::IsStopped())
     {
      int size=::ArraySize(up_levels);
      ::ArrayResize(up_levels,size+1);
      up_levels[size]=level;
      level+=5;
      levels_counter++;
     }
//---
   return(levels_counter);
  }

Уровни устанавливаются в функции SetPropertiesIndicator(). Ниже показана ее сокращённая версия. Здесь сначала рассчитываются начальные уровни для верхнего и нижнего диапазонов и обнуляются массивы уровней. Затем устанавливаем общее количество уровней индикатора вызовом функции GetLevelsIndicator(). После этого устанавливаем из массивов уже рассчитанные уровни нижнего и верхнего диапазонов.

//+------------------------------------------------------------------+
//| Устанавливает свойства индикатора                                |
//+------------------------------------------------------------------+
void SetPropertiesIndicator(void)
  {
...
//--- Расчёт первых уровней
   up_level   =100-SignalLevel;
   down_level =SignalLevel;
//--- Обнуление массивов уровней
   ::ArrayFree(up_levels);
   ::ArrayFree(down_levels);
//--- Количество горизонтальных уровней индикатора
   ::IndicatorSetInteger(INDICATOR_LEVELS,GetLevelsIndicator());
//--- Значения горизонтальных уровней индикатора нижнего уровня
   down_levels_total=::ArraySize(down_levels);
   for(int i=0; i<down_levels_total; i++)
      ::IndicatorSetDouble(INDICATOR_LEVELVALUE,i,down_levels[i]);
//--- Значения горизонтальных уровней индикатора верхнего уровня
   up_levels_total=::ArraySize(up_levels);
   int total=up_levels_total+down_levels_total;
   for(int i=down_levels_total,k=0; i<total; i++,k++)
      ::IndicatorSetDouble(INDICATOR_LEVELVALUE,i,up_levels[k]);
...
  }

Соответственно, нужно внести изменения в функцию CalculateSignals(). Здесь тоже показана только измененная часть функции. Для проверки исполнения условий в циклах смотрим, пересекается ли хоть один из уровней в массивах. 

//+------------------------------------------------------------------+
//| Рассчитывает сигналы индикатора                                  |
//+------------------------------------------------------------------+
void CalculateSignals(const int i,const int rates_total)
  {
   int last_index=rates_total-1;
//---
   bool condition1 =false;
   bool condition2 =false;
//--- Пробой внутрь канала
   if(BreakMode==BREAK_IN || BreakMode==BREAK_IN_REVERSE)
     {
      if(rsi_buffer[i]<50)
        {
         for(int j=0; j<down_levels_total; j++)
           {
            condition1=rsi_buffer[i-1]<down_levels[j] && rsi_buffer[i]>down_levels[j];
            if(condition1)
               break;
           }
        }
      //---
      if(rsi_buffer[i]>50)
        {
         for(int j=0; j<up_levels_total; j++)
           {
            condition2=rsi_buffer[i-1]>up_levels[j] && rsi_buffer[i]<up_levels[j];
            if(condition2)
               break;
           }
        }
     }
   else
     {
      for(int j=0; j<up_levels_total; j++)
        {
         condition1=rsi_buffer[i-1]<up_levels[j] && rsi_buffer[i]>up_levels[j];
         if(condition1)
            break;
        }
      //---
      for(int j=0; j<down_levels_total; j++)
        {
         condition2=rsi_buffer[i-1]>down_levels[j] && rsi_buffer[i]<down_levels[j];
         if(condition2)
            break;
        }
     }
//--- Отображаем сигналы, если условия исполнились
...
//--- Корректировка последнего значения (равно предпоследнему)
...
  }

На рис. 9 показано, как это выглядит. 

 Рис. 9. Формирование сигналов при пересечении нескольких уровней.

Рис. 9. Формирование сигналов при пересечении нескольких уровней.

Одна проблема решена, но появились ещё две. Первая заключается в необходимости исключить те сигналы, на которых цена оказывается выше предыдущей покупки или ниже предыдущей продажи. На рис. 10 показана такая ситуация для серии сигналов на покупку: цена на последнем сигнале серии выше цены предыдущего сигнала. Это актуально для режимов Break in и Break out reverse, когда нужно придерживаться концепции покупать дешевле и продавать дороже.

 Рис. 10. Ситуация, когда цена сигнала выше цены предыдущего сигнала.

Рис. 10. Ситуация, когда цена сигнала выше цены предыдущего сигнала.

Вторая проблема — слишком частые сигналы, порой даже на нескольких барах подряд. При этом цена между сигналами проходит на незначительное расстояние (см. рис. 11).

 Рис. 11. Скопления частых сигналов с незначительным движением цены.

Рис. 11. Скопления частых сигналов с незначительным движением цены.

Решением этих проблем займёмся в следующей версии.

Четвёртая версия. Переносим индикатор в главное окно графика

Эту версию индикатора перенесём на главный график. Здесь лучше видно, как работает индикатор: сигналы мы увидим сразу на цене. Чтобы контролировать расстояние между сигналами (в пунктах), можно было бы просто установить фиксированное значение во внешнем параметре. Но мы попробуем сделать динамический вариант и привяжем эту величину к индикатору волатильности (ATR). Для удобства оформим расчёты для индикаторов в отдельных классах: CATR и CRsiPlus. Используя такой метод, можно комбинировать любое количество индикаторов, совмещая результаты их вычислений в одной программе. 

Определение истинного бара

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

Определения членов и методов класса CFirstTrueBar показаны ниже. Кратко рассмотрим их. 

//+------------------------------------------------------------------+
//| Класс для определения истинного бара                             |
//+------------------------------------------------------------------+
class CFirstTrueBar
  {
private:
   //--- Время истинного бара
   datetime          m_limit_time;
   //--- Номер истинного бара
   int               m_limit_bar;
   //---
public:
                     CFirstTrueBar(void);
                    ~CFirstTrueBar(void);
   //--- Возвращает (1) время и (2) номер истинного бара
   datetime          LimitTime(void) const { return(m_limit_time); }
   int               LimitBar(void)  const { return(m_limit_bar);  }
   //--- Определяет первый истинный бар
   bool              DetermineFirstTrueBar(void);
   //---
private:
   //--- Ищет первый истинный бар текущего периода
   void              GetFirstTrueBarTime(const datetime &time[]);
  };

Для поиска истинного бара используется приватный метод CFirstTrueBar::GetFirstTrueBarTime(). В него нужно передать массив времени баров истории для поиска первого истинного бара. Проходим от начала массива и как только найден бар, соответствующий текущему таймфрейму, запоминаем время и индекс этого бара. Когда истинный бар определён, то получить его время и индекс можно с помощью методов CFirstTrueBar::LimitTime() и CFirstTrueBar::LimitBar().

//+------------------------------------------------------------------+
//| Ищет первый истинный бар текущего периода                        |
//+------------------------------------------------------------------+
void CFirstTrueBar::GetFirstTrueBarTime(const datetime &time[])
  {
//--- Получим размер массива
   int array_size=::ArraySize(time);
   ::ArraySetAsSeries(time,false);
//--- Поочередно проверяем каждый бар
   for(int i=1; i<array_size; i++)
     {
      //--- Если бар соответствует текущему таймфрейму
      if(time[i]-time[i-1]==::PeriodSeconds())
        {
         //--- Запомним и остановим цикл
         m_limit_time =time[i-1];
         m_limit_bar  =i-1;
         break;
        }
     }
  }

Метод CFirstTrueBar::GetFirstTrueBarTime() вызывается в методе CFirstTrueBar::DetermineFirstTrueBar(). Именно здесь получаем массив времени баров, по которому затем ищем первый истинный бар.

//+------------------------------------------------------------------+
//| Определяет первый истинный бар                                   |
//+------------------------------------------------------------------+
bool CFirstTrueBar::DetermineFirstTrueBar(void)
  {
//--- Массив времени баров
   datetime time[];
//--- Получим общее количество баров символа
   int available_bars=::Bars(_Symbol,_Period);
//--- Скопируем массив времени баров. Если не получилось, попробуем еще раз.
   if(::CopyTime(_Symbol,_Period,0,available_bars,time)<available_bars)
      return(false);
//--- Получим время первого истинного бара, который соответствует текущему таймфрейму
   GetFirstTrueBarTime(time);
   return(true);
  }

Добавляем индикатор ATR

Индикатора ATR будет рассчитываться так же, как и в стандартной поставке. Берем код отсюда: \MQL5\Indicators\Examples. Ниже показаны объявления членов и методов класса CATR. Отличие от стандартной версии — только в том, что здесь мы будем определять первый истинный бар, от которого начнутся расчеты.

Подключаем файл с классом CFirstTrueBar и объявляем его экземпляр в теле класса CATR. Обратите внимание, что индикаторные массивы здесь объявлены с публичным доступом. Это нужно, чтобы оставалась возможность их установить в качестве индикаторных буферов в главном файле индикатора.

//+------------------------------------------------------------------+
//|                                                          ATR.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "FirstTrueBar.mqh"
//+------------------------------------------------------------------+
//| Индикатор ATR                                                    |
//+------------------------------------------------------------------+
class CATR
  {
private:
   //--- Для определения первого истинного бара
   CFirstTrueBar     m_first_true_bar;
   //--- Период индикатора
   int               m_period;
   //--- Ограничитель в расчётах значений индикатора
   int               m_limit;
   //---
public:
   //--- Индикаторные буферы
   double            m_tr_buffer[];
   double            m_atr_buffer[];
   //---
public:
                     CATR(const int period);
                    ~CATR(void);
   //--- Период индикатора
   void              PeriodATR(const int period) { m_period=period; }
   //--- Рассчитывает индикатор ATR 
   bool              CalculateIndicatorATR(const int rates_total,const int prev_calculated,const datetime &time[],const double &close[],const double &high[],const double &low[]);
   //--- Обнуление индикаторных буферов
   void              ZeroIndicatorBuffers(void);
   //---
private:
   //--- Предварительные расчёты
   bool              PreliminaryCalc(const int rates_total,const int prev_calculated,const double &close[],const double &high[],const double &low[]);
   //--- Рассчитывает ATR 
   void              CalculateATR(const int i,const datetime &time[],const double &close[],const double &high[],const double &low[]);
  };

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

//+------------------------------------------------------------------+
//| Предварительные расчёты                                          |
//+------------------------------------------------------------------+
bool CATR::PreliminaryCalc(const int rates_total,const int prev_calculated,const double &close[],const double &high[],const double &low[])
  {
//--- Если расчёт в первый раз или были изменения
   if(prev_calculated==0)
     {
      //--- Определим номер истинного бара
      m_first_true_bar.DetermineFirstTrueBar();
      //--- Выйти, если истинный бар не определён
      if(m_first_true_bar.LimitBar()<0)
         return(false);
      //---
      m_tr_buffer[0]  =0.0;
      m_atr_buffer[0] =0.0;
      //--- Бар, от которого будет расчёт
      m_limit=(::Period()<PERIOD_D1)? m_first_true_bar.LimitBar()+m_period : m_period;
      //--- Выйти, если выходим из диапазона (недостаточно баров)
      if(m_limit>=rates_total)
         return(false);
      //--- Рассчитаем значения истинного диапазона
      int start_pos=(m_first_true_bar.LimitBar()<1)? 1 : m_first_true_bar.LimitBar();
      for(int i=start_pos; i<m_limit && !::IsStopped(); i++)
         m_tr_buffer[i]=::fmax(high[i],close[i-1])-::fmin(low[i],close[i-1]);
      //--- Первые значения ATR не рассчитываются
      double first_value=0.0;
      for(int i=m_first_true_bar.LimitBar(); i<m_limit; i++)
        {
         m_atr_buffer[i]=0.0;
         first_value+=m_tr_buffer[i];
        }
      //--- Расчёт первого значения
      first_value/=m_period;
      m_atr_buffer[m_limit-1]=first_value;
     }
   else
      m_limit=prev_calculated-1;
//---
   return(true);
  }

Фильтрация сигналов в индикаторе RSI

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

Для оптимизации кода некоторые его блоки теперь реализованы как отдельные методы. Это проверка условий, работа со счётчиками и т.д. Единственный из них новый метод, который ранее не рассматривался, — CRsiPlus::DirectionControl(). Именно в нём контролируется направление движения и отсеиваются лишние сигналы по текущей волатильности. Кроме этого, есть вспомогательные методы для удаления лишних сигналов CRsiPlus::DeleteBuySignal() и CRsiPlus::DeleteSellSignal().

//+------------------------------------------------------------------+
//|                                                          RSI.mqh |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "FirstTrueBar.mqh"
#include "ATR.mqh"
//--- Перечисление режимов пробоя каналов
enum ENUM_BREAK_INOUT
  {
   BREAK_IN          =0, // Break in
   BREAK_IN_REVERSE  =1, // Break in reverse
   BREAK_OUT         =2, // Break out
   BREAK_OUT_REVERSE =3  // Break out reverse
  };
//+------------------------------------------------------------------+
//| Индикатор RSI с фильтром по волатильности                        |
//+------------------------------------------------------------------+
class CRsiPlus
  {
private:
   //--- Для определения первого истинного бара
   CFirstTrueBar     m_first_true_bar;
   //--- Указатель на ATR
   CATR             *m_atr;
   
   //--- Период индикатора
   int               m_period;
   //--- Уровень RSI
   double            m_signal_level;
   //--- Режим для формирования сигналов
   ENUM_BREAK_INOUT  m_break_mode;
      
   //--- Счётчики однонаправленных сигналов
   int               m_buy_counter;
   int               m_sell_counter;
   //--- Индикаторные уровни
   double            m_up_level;
   double            m_down_level;
   double            m_up_levels[];
   double            m_down_levels[];
   int               m_up_levels_total;
   int               m_down_levels_total;
   
   //--- Ограничитель в расчётах значений индикатора
   int               m_limit;
   //--- Для определения последнего бара
   bool              m_is_last_index;
   //---
public:
   //--- Индикаторные буферы
   double            m_rsi_buffer[];
   double            m_pos_buffer[];
   double            m_neg_buffer[];
   //---
   double            m_buy_buffer[];
   double            m_sell_buffer[];
   double            m_buy_level_buffer[];
   double            m_sell_level_buffer[];
   double            m_buy_counter_buffer[];
   double            m_sell_counter_buffer[];
   //---
public:
                     CRsiPlus(const int period,const double signal_level,const ENUM_BREAK_INOUT break_mode);
                    ~CRsiPlus(void) {}
   //--- Указатель на ATR
   void              AtrPointer(CATR &object) { m_atr=::GetPointer(object);  }
   CATR             *AtrPointer(void)         { return(::GetPointer(m_atr)); }
   //--- Рассчитывает индикатор RSI
   bool              CalculateIndicatorRSI(const int rates_total,const int prev_calculated,const double &close[],const int &spread[]);
   //--- Инициализация всех индикаторных буферов
   void              ZeroIndicatorBuffers(void);
   //---
private:
   //--- Получает уровни индикатора
   int               GetLevelsIndicator(void);
   //--- Предварительные расчёты
   bool              PreliminaryCalc(const int rates_total,const int prev_calculated,const double &close[]);
   //--- Рассчитывает серию RSI
   void              CalculateRSI(const int i,const double &price[]);
   //--- Рассчитывает сигналы индикатора
   void              CalculateSignals(const int i,const int rates_total,const double &close[],const int &spread[]);

   //--- Проверка условий
   void              CheckConditions(const int i,bool &condition1,bool &condition2);
   //--- Проверка счётчиков
   void              CheckCounters(bool &condition1,bool &condition2);
   //--- Увеличивает buy- и sell-счётчики
   void              IncreaseBuyCounter(const bool condition);
   void              IncreaseSellCounter(const bool condition);

   //--- Контроль направления движения
   void              DirectionControl(const int i,bool &condition1,bool &condition2);
   //--- Удаляют лишние buy- и sell-сигналы
   void              DeleteBuySignal(const int i);
   void              DeleteSellSignal(const int i);
   //--- Обнуление указанного элемента индикаторных буферов
   void              ZeroIndexBuffers(const int index);
  };

В методе CRsiPlus::DirectionControl() проверяются следующие условия, по которым определяется, является ли сигнал лишним:

Если условия исполняются, то сигнал стирается.

//+------------------------------------------------------------------+
//| Контроль направления движения                                    |
//+------------------------------------------------------------------+
void CRsiPlus::DirectionControl(const int i,bool &condition1,bool &condition2)
  {
   double atr_coeff     =0.0;
   double impulse_size  =0.0;
   bool   atr_condition =false;
//---
   bool buy_condition  =false;
   bool sell_condition =false;
//--- Если реверс отключен
   if(m_break_mode==BREAK_IN || m_break_mode==BREAK_OUT)
     {
      buy_condition =condition1 && m_buy_counter>1;
      impulse_size  =::fabs(m_buy_buffer[i]-m_buy_level_buffer[i-1]);
      atr_condition =impulse_size<m_atr.m_atr_buffer[i];
      //---
      if((m_buy_counter>1 && atr_condition) || 
         (m_break_mode==BREAK_IN && buy_condition && m_buy_buffer[i]>m_buy_level_buffer[i-1]) ||
         (m_break_mode==BREAK_OUT && buy_condition && m_buy_buffer[i]<m_buy_level_buffer[i-1]))
        {
         DeleteBuySignal(i);
        }
      //---
      sell_condition =condition2 && m_sell_counter>1;
      impulse_size   =::fabs(m_sell_buffer[i]-m_sell_level_buffer[i-1]);
      atr_condition  =impulse_size<m_atr.m_atr_buffer[i];
      //---
      if((m_sell_counter>1 && atr_condition) || 
         (m_break_mode==BREAK_IN && sell_condition && m_sell_buffer[i]<m_sell_level_buffer[i-1]) ||
         (m_break_mode==BREAK_OUT && sell_condition && m_sell_buffer[i]>m_sell_level_buffer[i-1]))
        {
         DeleteSellSignal(i);
        }
     }
//--- Реверс включен     
   else
     {
      buy_condition =condition2 && m_buy_counter>1;
      impulse_size  =::fabs(m_buy_buffer[i]-m_buy_level_buffer[i-1]);
      atr_condition =impulse_size<m_atr.m_atr_buffer[i];
      //---
      if((m_buy_counter>1 && atr_condition) || 
         (m_break_mode==BREAK_IN_REVERSE && buy_condition && m_buy_buffer[i]<m_buy_level_buffer[i-1]) ||
         (m_break_mode==BREAK_OUT_REVERSE && buy_condition && m_buy_buffer[i]>m_buy_level_buffer[i-1]))
        {
         DeleteBuySignal(i);
        }
      //---
      sell_condition =condition1 && m_sell_counter>1;
      impulse_size   =::fabs(m_sell_buffer[i]-m_sell_level_buffer[i-1]);
      atr_condition  =impulse_size<m_atr.m_atr_buffer[i];
      //---      
      if((m_sell_counter>1 && atr_condition) || 
         (m_break_mode==BREAK_IN_REVERSE && sell_condition && m_sell_buffer[i]>m_sell_level_buffer[i-1]) ||
         (m_break_mode==BREAK_OUT_REVERSE && sell_condition && m_sell_buffer[i]<m_sell_level_buffer[i-1]))
        {
         DeleteSellSignal(i);
        }
     }
  }

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

//--- Свойства
#property indicator_chart_window
#property indicator_buffers 11
#property indicator_plots   7
#property indicator_color1  clrMediumSeaGreen
#property indicator_color2  clrRed
#property indicator_color5  clrMediumSeaGreen
#property indicator_color6  clrRed

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

 Рис. 12. Директория индикатора.

Рис. 12. Директория индикатора.

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

//--- Подключаем классы индикаторов
#include "Includes\ATR.mqh"
#include "Includes\RsiPlus.mqh"

К внешним параметрам добавляем ещё один для периода индикатора ATR.

//--- Входные параметры
input int              PeriodRSI   =8;         // RSI period
input double           SignalLevel =30;        // Signal level
input ENUM_BREAK_INOUT BreakMode   =BREAK_OUT; // Break mode
input int              PeriodATR   =200;       // ATR period

Индикаторы объявляются с передачей параметров в конструктор.

//--- Экземпляры индикаторов для работы
CATR     atr(PeriodATR);
CRsiPlus rsi(PeriodRSI,SignalLevel,BreakMode);

В функции инициализации OnInit() не забываем передать в индикатор RSI указатель на ATR.

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
void OnInit(void)
  {
//--- Инициализация индикаторов
   rsi.AtrPointer(atr);
//--- Установим свойства индикатора
   SetPropertiesIndicator();
  }

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

//+------------------------------------------------------------------+
//| Устанавливает свойства индикатора                                |
//+------------------------------------------------------------------+
void SetPropertiesIndicator(void)
  {
//--- Короткое имя
   ::IndicatorSetString(INDICATOR_SHORTNAME,"RSI_PLUS_CHART");
//--- Знаков после запятой
   ::IndicatorSetInteger(INDICATOR_DIGITS,::Digits());
//--- Буферы индикатора
   ::SetIndexBuffer(0,rsi.m_buy_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(1,rsi.m_sell_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(2,rsi.m_buy_counter_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(3,rsi.m_sell_counter_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(4,rsi.m_buy_level_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(5,rsi.m_sell_level_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(6,atr.m_atr_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(7,rsi.m_rsi_buffer,INDICATOR_CALCULATIONS);
   ::SetIndexBuffer(8,rsi.m_pos_buffer,INDICATOR_CALCULATIONS);
   ::SetIndexBuffer(9,rsi.m_neg_buffer,INDICATOR_CALCULATIONS);
   ::SetIndexBuffer(10,atr.m_tr_buffer,INDICATOR_CALCULATIONS);
//--- Инициализация массивов
   atr.ZeroIndicatorBuffers();
   rsi.ZeroIndicatorBuffers();
...
  }

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

//+------------------------------------------------------------------+
//| Устанавливает свойства индикатора                                |
//+------------------------------------------------------------------+
void SetPropertiesIndicator(void)
  {
...
//--- Установим тип для индикаторных буферов
   ENUM_DRAW_TYPE draw_type[]={DRAW_ARROW,DRAW_ARROW,DRAW_NONE,DRAW_NONE,DRAW_LINE,DRAW_LINE,DRAW_NONE};
   for(int i=0; i<indicator_plots; i++)
      ::PlotIndexSetInteger(i,PLOT_DRAW_TYPE,draw_type[i]);
...
  }

Содержание функции OnCalculate() сводится только к тому, чтобы вызвать два метода для расчёта индикатора ATR и модифицированного RSI

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int      rates_total,
                const int      prev_calculated,
                const datetime &time[],
                const double   &open[],
                const double   &high[],
                const double   &low[],
                const double   &close[],
                const long     &tick_volume[],
                const long     &volume[],
                const int      &spread[])
  {
//--- Рассчитать индикатор ATR
   if(!atr.CalculateIndicatorATR(rates_total,prev_calculated,time,close,high,low))
      return(0);
//--- Рассчитать индикатор RSI
   if(!rsi.CalculateIndicatorRSI(rates_total,prev_calculated,close,spread))
      return(0);
//--- Вернуть последнее рассчитанное количество элементов
   return(rates_total);
  }

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

 Рис. 13. Результат работы модифицированного индикатора RSI на главном графике.

Рис. 13. Результат работы модифицированного индикатора RSI на главном графике.

При разработке торговой системы может понадобиться визуально получать точные значения того или иного индикаторного буфера. Особенно это касается случаев, когда на графике не видно серий некоторых буферов, значения которых выпадают из зоны видимости. Их значения можно посмотреть в Окне данных. Для удобства можно включить перекрестье средней кнопкой мыши. На рис. 14 на график добавлен индикатор ATR из стандартной поставки, чтобы можно было сравнить с тем, что рассчитывается в модифицированном RSI.

 Рис. 14. Просмотр значений в окне данных индикатора.

Рис. 14. Просмотр значений в окне данных индикатора.

Пятая версия. Универсальный индикатор RSI для работы в двух направлениях одновременно

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

Кратко рассмотрим, как создать такой индикатор. Всё необходимое у нас уже есть, и изменения будут только в главном файле. В этой версии будет всего 20 буферов, из них 15 используем для отрисовки.

#property indicator_buffers 20
#property indicator_plots   15

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

//--- Стрелки для сигналов: 159 - точки; 233/234 - стрелки;
#define ARROW_BUY_IN   233
#define ARROW_SELL_IN  234
#define ARROW_BUY_OUT  159
#define ARROW_SELL_OUT 159

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

//--- Подключаем классы индикаторов
#include "Includes\ATR.mqh"
#include "Includes\RsiPlus.mqh"

Во внешних параметрах этой версии индикатора не нужно указывать тип сигналов. Но сделаем так, чтобы можно было указать уровни для сигналов по тренду (Signal level In) и флэту отдельно (Signal level Out)

//--- Входные параметры
input int    PeriodRSI      =8;   // RSI period
input double SignalLevelIn  =35;  // Signal level In
input double SignalLevelOut =30;  // Signal level Out
input int    PeriodATR      =100; // ATR period

Далее нужно объявить два экземпляра класса CRsiPlus: один — для сигналов по тренду, а второй — для сигналов во флэте. В обоих случаях используются реверсивные типы сигналов (BREAK_IN_REVERSE и BREAK_OUT_REVERSE). То есть для флэта сигналом будет импульс из канала, а для тренда вход будет по направлению тренда после отката.

//--- Экземпляры индикаторов для работы
CATR atr(PeriodATR);
CRsiPlus rsi_in(PeriodRSI,SignalLevelIn,BREAK_IN_REVERSE);
CRsiPlus rsi_out(PeriodRSI,SignalLevelOut,BREAK_OUT_REVERSE);

Указатель на индикатор ATR нужно передать в оба экземпляра RSI:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
void OnInit(void)
  {
//--- Передача указателя на ATR  
   rsi_in.AtrPointer(atr);
   rsi_out.AtrPointer(atr);
//--- Установим свойства индикатора
   SetPropertiesIndicator();
  }

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

//+------------------------------------------------------------------+
//| Устанавливает свойства индикатора                                |
//+------------------------------------------------------------------+
void SetPropertiesIndicator(void)
  {
//--- Короткое имя
   ::IndicatorSetString(INDICATOR_SHORTNAME,"RSI_PLUS2_CHART");
//--- Знаков после запятой
   ::IndicatorSetInteger(INDICATOR_DIGITS,::Digits());
//--- Буферы индикатора
   ::SetIndexBuffer(0,rsi_in.m_buy_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(1,rsi_in.m_sell_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(2,rsi_in.m_buy_counter_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(3,rsi_in.m_sell_counter_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(4,rsi_in.m_buy_level_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(5,rsi_in.m_sell_level_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(6,rsi_in.m_rsi_buffer,INDICATOR_CALCULATIONS);
   ::SetIndexBuffer(7,rsi_in.m_pos_buffer,INDICATOR_CALCULATIONS);
   ::SetIndexBuffer(8,rsi_in.m_neg_buffer,INDICATOR_CALCULATIONS);
//---
   ::SetIndexBuffer(9,rsi_out.m_buy_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(10,rsi_out.m_sell_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(11,rsi_out.m_buy_counter_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(12,rsi_out.m_sell_counter_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(13,rsi_out.m_buy_level_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(14,rsi_out.m_sell_level_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(15,rsi_out.m_rsi_buffer,INDICATOR_CALCULATIONS);
   ::SetIndexBuffer(16,rsi_out.m_pos_buffer,INDICATOR_CALCULATIONS);
   ::SetIndexBuffer(17,rsi_out.m_neg_buffer,INDICATOR_CALCULATIONS);
//---
   ::SetIndexBuffer(18,atr.m_atr_buffer,INDICATOR_DATA);
   ::SetIndexBuffer(19,atr.m_tr_buffer,INDICATOR_CALCULATIONS);
//--- Инициализация массивов
   atr.ZeroIndicatorBuffers();
   rsi_in.ZeroIndicatorBuffers();
   rsi_out.ZeroIndicatorBuffers();
...
  }

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

//+------------------------------------------------------------------+
//| Устанавливает свойства индикатора                                |
//+------------------------------------------------------------------+
void SetPropertiesIndicator(void)
  {
...
//--- Установим стиль линиям указанных индикаторных буферов
   ::PlotIndexSetInteger(13,PLOT_LINE_STYLE,STYLE_DOT);
   ::PlotIndexSetInteger(14,PLOT_LINE_STYLE,STYLE_DOT);
...
  }

Код функции OnCalculate() ограничивается только вызовом методов для каждого экземпляра индикатора.

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int      rates_total,
                const int      prev_calculated,
                const datetime &time[],
                const double   &open[],
                const double   &high[],
                const double   &low[],
                const double   &close[],
                const long     &tick_volume[],
                const long     &volume[],
                const int      &spread[])
  {
//--- Рассчитать индикатор ATR
   if(!atr.CalculateIndicatorATR(rates_total,prev_calculated,time,close,high,low))
      return(0);
//--- Рассчитать индикатор RSI
   if(!rsi_in.CalculateIndicatorRSI(rates_total,prev_calculated,close,spread))
      return(0);
   if(!rsi_out.CalculateIndicatorRSI(rates_total,prev_calculated,close,spread))
      return(0);
//--- Вернуть последнее рассчитанное количество элементов
   return(rates_total);
  }

На рис. 15 показано, как выглядит работа индикатора на графике. Всё это — только результат запуска RSI.

 Рис. 15. Универсальный индикатор RSI.

Рис. 15. Универсальный индикатор RSI.

 Рис. 16. Универсальный индикатор RSI в тестере стратегий.

Рис. 16. Универсальный индикатор RSI в тестере стратегий.

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

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

Заключение

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

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

Наименование файла Комментарий
MQL5\Indicators\RSI\RSI_Plus1.mq5 Первая версия модифицированного индикатора RSI
MQL5\Indicators\RSI\RSI_Plus2.mq5 Вторая версия модифицированного индикатора RSI
MQL5\Indicators\RSI\RSI_Plus3.mq5 Третья версия модифицированного индикатора RSI
MQL5\Indicators\RSI\ChartRSI_Plus1\ChartRSI_Plus1.mq5 Четвёртая версия модифицированного индикатора RSI
MQL5\Indicators\RSI\ChartRSI_Plus2\ChartRSI_Plus2.mq5 Пятая версия модифицированного индикатора RSI