Индикатор ElliotWaveByBollinger

Советник ElliotWaveByBollinger_Expert

Развернутые результаты тестирования эксперта

 

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

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

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

    Таким образом, задача сводится к идентификации трех точек: т. 1 - начало первой волны, т. 2 - конец первой волны и начало второй, т. 3 - конец второй волны и предположительно начало третьей. Как только определена т. 3, мы должны совершить сделку в направлении первой волны. Остается лишь решить, каким образом мы будем выделять первую и вторую волны. Наблюдая на графике индикатор Bollinger Bands, можно придти к выводу, что окончанием волн будет пересечение ценой верхней и нижней полос индикатора.

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

Рис. 1. - Идентификация первой и второй волн по индикатору Bollinger Bands. 

В результате фиксации т. 3, мы должны открыть сделку. На рис. 1 это сделка Sell. Ее целью будет являться нижняя полоса Боллинджера. Как видим, в этом случае все прошло удачно. И, что самое интересное, определенная нами третья волна впоследствии оказывается первой волной следующей трехволновой структуры. Но это можно объяснить тем, что мы сознательно упростили теорию Эллиотта, взяв от нее только само понятие "волна". А ведь на самом деле приведенная на рис. 1 ситуация классически описывается именно пятью волнами.

    Но вернемся ко второму участку, где нами определена вторая волновая структура. Здесь нас поджидает сюрприз - перед тем как уйти в нужном нам направлении, цена делает небольшое забегание за т. 1 (на четыре пункта!), сбивая таким образом стоп. Но и к таким ситуациям мы должны быть готовы. Для этого на вооружении у трейдеров имеется старое правило: "для стопа всегда необходим запас", то есть не стоит ставить стоп непосредственно на определенный уровень, нужно отодвинуть его хотя бы на десять пунктов, а лучше на 20-30 дальше.

    Когда цена достигает нижней границы канала Боллинджера после т. 3, запоминаем значение полосы. Оно нам еще послужит в дальнейшем для сравнения с верхней границей, когда цена пойдет вверх. Такое сравнение понадобится для определения импульсной волны, чтобы не спутать ее ненароком с первой. На рис. 1 четко видно, что после ухода цены вниз верхняя полоса будет достигнута на уровне, который находится намного ниже зафиксированного нами значения нижней полосы. Именно такое сравнение будет свидетельствовать об окончании третьей волны и даст возможность приступить к новому поиску первой волны.

    Еще одним ограничением при идентификации первой и второй волн должно стать условие достижения второй волной хотя бы половины высоты волны 1 и при этом волна 2 ни в коем случае не должна пробить вершину волны 1. Описанный принцип идентификации волн реализован в индикаторе ElliotWaveByBollinger, ссылку на который можно найти в начале статьи. А раз есть индикатор, то на его основе можно сделать торгового робота, чем и займемся. Как всегда, сначала пишем сигнальную часть, то есть функцию GetSignal:

 
//+-------------------------------------------------------------------------------------+
//| Расчет сигнала по волнам 1 и 2                                                      |
//+-------------------------------------------------------------------------------------+
void GetSignal()
{
  Signal = 0;
  ArrayInitialize(EllBB, 0);
  ArrayInitialize(EllNum, 0);
  ArrayInitialize(EllType, -1);

// - 1 - ================ Начало цикла и расчет значений полос Боллинджера ==============
   int Cnt = 0;
   for (int i = iBarShift(Symbol(), 0, LastThirdWave) - 1; i > 0; i--)
     {
      double BBUp = iBands(Symbol(), 0, BandsPeriod, BandsDeviation, BandsShift, 
                           BandsPrice, MODE_UPPER, i);
      double BBDn = iBands(Symbol(), 0, BandsPeriod, BandsDeviation, BandsShift, 
                           BandsPrice, MODE_LOWER, i);
// - 1 - ================ Окончание блока ===============================================

// - 2 - ================ Пробита верхняя полоса Боллинджера ============================
      // 
      if (High[i] > BBUp && Low[i] >= BBDn)
        {
         if (EllType[Cnt] == 0)           // Если предыдущая точка - минимум, то отмечаем
           Cnt++;                                                      // следующую точку
         // Если предыдущая точка максимум, но ее значение меньше или предыдущая точка 
         // не указана или была минимумом, то записываем все данные
         if ((EllType[Cnt] == 1 && High[EllNum[Cnt]] < High[i]) || EllType[Cnt] < 1)
           {
            EllType[Cnt] = 1; // текущая точка - максимум
            EllNum[Cnt] = i;  // номер бара точки - i
            if (EllBB[Cnt] == 0)
              EllBB[Cnt] = BBUp; // значение верхней полосы Боллинджера
           }           
         // Если предыдущий минимум Боллинджера больше текущего максимума, то начинаем
         // искать волны заново
         if (Cnt > 0 && EllBB[Cnt-1] > EllBB[Cnt] && EllType[Cnt] == 1)
           {
            ArrayInitialize(EllBB, 0);
            ArrayInitialize(EllNum, 0);
            ArrayInitialize(EllType, -1);
            Cnt = 0;
            EllType[Cnt] = 1; // текущая точка - максимум
            EllNum[Cnt] = i;  // номер бара точки - i
            EllBB[Cnt] = BBUp;        // значение верхней полосы Боллинджера, не меняется
                                                               // последующими пробитиями
           }  
        }
// - 2 - ================ Окончание блока ===============================================
        
// - 3 - ================ Пробита нижняя полоса Боллинджера =============================
      if (Low[i] < BBDn && High[i] <= BBUp)
        {
         if (EllType[Cnt] == 1)          // Если предыдущая точка - максимум, то отмечаем
           Cnt++;                                                      // следующую точку
         // Если предыдущая точка минимум, но ее значение больше или предыдущая точка 
         // не указана или была максимумом, то записываем все данные
         if ((EllType[Cnt] == 0 && Low[EllNum[Cnt]] > Low[i]) || EllType[Cnt] < 0 ||
              EllType[Cnt] == 1)
           {
            EllType[Cnt] = 0; // текущая точка - минимум
            EllNum[Cnt] = i;  // номер бара точки - i
            if (EllBB[Cnt] == 0)
              EllBB[Cnt] = BBDn; // значение верхней полосы Боллинджера
           }           
         // Если предыдущий максимум Боллинджера меньше текущего минимума, то начинаем
         // искать волны заново
         if (Cnt > 0 && EllBB[Cnt-1] < EllBB[Cnt] && EllType[Cnt] == 0)
           {
            ArrayInitialize(EllBB, 0);
            ArrayInitialize(EllNum, 0);
            ArrayInitialize(EllType, -1);
            Cnt = 0;
            EllType[Cnt] = 0; // текущая точка - минимум
            EllNum[Cnt] = i;  // номер бара точки - i
            EllBB[Cnt] = BBDn;        // значение верхней полосы Боллинджера, не меняется
                                                                 // следуюшими пробитиями
           }  
        }
// - 3 - ================ Окончание блока ===============================================

// - 4 - ====== Если найдена третья точка, то заново ищем волны =========================
      if (Cnt == 2)
        {
         // Сигнал Buy
         if (EllType[0] == 0 && EllType[1] == 1 && EllType[2] == 0 &&
             Low[EllNum[0]] < Low[EllNum[2]] && 
             0.5*(High[EllNum[1]]-Low[EllNum[0]]) < (High[EllNum[1]]-Low[EllNum[2]]))
           {
            Signal = 1;
            WTime1 = Time[EllNum[0]];
            WTime2 = Time[EllNum[1]];
            WTime3 = Time[EllNum[2]];
            WPrice1 = Low[EllNum[0]];
            WPrice2 = High[EllNum[1]];
            WPrice3 = Low[EllNum[2]];
           }
         
         // Сигнал Sell
         if (EllType[0] == 1 && EllType[1] == 0 && EllType[2] == 1 &&
             High[EllNum[0]] > High[EllNum[2]] &&
             0.5*(High[EllNum[0]]-Low[EllNum[1]]) < (High[EllNum[2]]-Low[EllNum[1]]))
           {
            Signal = 2;
            WTime1 = Time[EllNum[0]];
            WTime2 = Time[EllNum[1]];
            WTime3 = Time[EllNum[2]];
            WPrice1 = High[EllNum[0]];
            WPrice2 = Low[EllNum[1]];
            WPrice3 = High[EllNum[2]];
           }
           
         LastThirdWave = WTime3;
         ArrayInitialize(EllBB, 0);
         ArrayInitialize(EllNum, 0);
         ArrayInitialize(EllType, -1);
         Cnt = 0;
        }
// - 4 - ================ Окончание блока ===============================================
     }  

   BandUp = iBands(Symbol(), 0, BandsPeriod, BandsDeviation, BandsShift, BandsPrice, 
                   MODE_UPPER, 0);
   BandDn = iBands(Symbol(), 0, BandsPeriod, BandsDeviation, BandsShift, BandsPrice,
                   MODE_LOWER, 0);
}

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

    Первый блок зауряден - начало цикла и получение значений верхней и нижней полос Боллинджера.

    А вот начиная со второго блока, потребуется серьезное напряжение мозговых извилин. В нем отслеживается пересечение ценой верхней полосы и при этом текущая свеча не должна пересекать нижнюю полосу (бывает и такое). Если пересечение верхней полосы определено, то дальше нужно "вспомнить", какое пересечение полосы было последним. За память эксперта в данном случае отвечает небольшой массив EllType, состоящий из трех элементов. Значений каждый элемент может принимать всего два - 0 или 1. Нулю соответствует пересечение нижней полосы, а единице - пересечение верхней. Таким образом, дальнейшие действия эксперта во втором блоке сводятся к определению последней точки. Если значение EllType равно 0, значит, последним был минимум, который нужно зафиксировать и перейти к следующему элементу массива EllType (увеличение счетчика Cnt). Если же значение EllType другое (-1 или 1), то текущий элемент перезаписывается (при предыдущем 1 только в случае более высокого максимума свечи). В этом же случае перезаписывается значение элемента массива EllNum, отвечающего за номер бара, на котором зафиксирован максимум. Массив EllBB заполняется только один раз - при первом пересечении верхней линии индикатора, что позволяет в дальнейшем сравнивать цены верхней и нижней линий для выявления третьей волны. Такое сравнение производится в последнем условном операторе первого блока.

    Третий блок по логике повторяет второй, только все сравнивается с нижней полосой Боллинджера. Минимум свечи должен быть ниже нижней полосы, а максимум не выше верхней полосы. Если выполняется это условие, то проверяем значение текущего (по значению счетчика Cnt) элемента массива EllType. При равенстве его единице (перед этим было пересечение верхней полосы), приращиваем Cnt и все дальнейшие записи производим в другой элемент массивов. Дальнейшая фиксация значений будет в том случае, если в новый элемент еще ничего не было записано (значение было меньше нуля) или уже содержится ноль и текущий минимум свечи меньше предыдущего записанного. Далее точно также последний условный оператор проверяет, не стало ли новое значение нижней полосы Боллинджера выше предыдущей зафиксированной верхней полосы, что означало бы окончание третьей импульсной волны.

    Четвертый блок исполняется, когда найдены все три точки (Cnt равен двум). В этом случае определяется, какая волновая структура найдена (восходящая или нисходящая), проверяется соответствие волн (высота второй не меньше 50% высоты первой волны) и не выходит ли третья точка за пределы первой точки. Если все эти условия выполняются, то координаты каждой точки записываются в глобальные переменные эксперта (пары WTime1, WPrice1 и т. д.) для использования в других функциях при открытии и сопровождении позиций и ордеров. Вне зависимости от выполнения условий для принятия первой и второй волн все данные о волновой структуре обнуляются, и производится поиск следующих структур. А переменная LastThirdWave принимает значение даты/времени последней найденной точки, чтобы при следующем вызове функции GetSignal не производить поиск волн по всей истории заново.

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

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

    Чтобы реализовать задуманное, возьмем функцию AddPending из эксперта RollBackInTrend_Expert (описан в статье "Откат в тренде") и немного изменим ее:

 
//+-------------------------------------------------------------------------------------+
//| Поиск первичной позиции и установка ей отложенника                                  |
//+-------------------------------------------------------------------------------------+
bool AddPending()
{
// - 1 - == Поиск первичной позиции =====================================================
 for (int i = OrdersTotal()-1; i >= 0; i--)
   if (OrderSelect(i, SELECT_BY_POS))
     if (OrderSymbol() == Symbol() && MathMod(OrderMagicNumber(), 10) == 0 
        && OrderType() < 2)     // поиск первичной позиции , которая открылась по сигналу
        {
         bool Res = False;     
         int Type = OrderType();                              // запоминаем ее параметры
         double OpenP = OrderStopLoss();
         double TP = MathAbs(OrderOpenPrice()-OrderStopLoss());
// - 1 - == Окончание блока =============================================================

// - 2 - == Поиск отложенника, который должен быть установлен на стопе позиции ==========
         for (int j = OrdersTotal()-1; j >= 0; j--)
            if (OrderSelect(j, SELECT_BY_POS))
              if (OrderSymbol() == Symbol() && MathMod(OrderMagicNumber(), 10) == 1 
                && OrderType() > 2)       
             {   
              Res = True;               // отложенник найден, нет нужды его устанавливать
              break;
             } 
// - 2 - == Окончание блока =============================================================

// - 3 - == Установка отложенного ордера ================================================
         if (!Res)                          // если отложенник не найден, то усановим его
           {
            if (Type == OP_BUY)                     // если первичная позиция длинная, то
              {
               Type = OP_SELLSTOP;                             // устанавливаем SELL STOP
               TP = OpenP - TP - Delta*Tick;//и цель отстоит от цены открытия на величину
                                                                                 // стопа
               double SL = NP(2*WPrice2 - WPrice1 + Spread);   // стоп - на ширине канала
              } 
             else
              {                                    // если первичная позиция короткая, то
               Type = OP_BUYSTOP;                               // устанавливаем BUY STOP
               TP = OpenP + TP + Delta*Tick;//и цель отстоит от цены открытия на величину
                                                                                 // стопа
               SL = NP(2*WPrice2 - WPrice1);                   // стоп - на ширине канала
              } 
            if (!OpenOrderCorrect(Type, OpenP, SL, TP, 1))  // если установить не удалось
              return(False);                                          // возвращаем False
           }
// - 3 - == Окончание блока =============================================================
         break;
        }
 return(True);
}

    Изменения коснулись лишь расчета уровня профита и стоп-приказа. Профит теперь увеличивается на значение внешнего параметра эксперта Delta, в котором также задается отступ от цены точки 1 (используется в качестве уровня стопа, если кто забыл). Это и позволяет в случае успешного закрытия первичной и разворотной сделок получить небольшой, но все же плюс. А уровень стоп-приказа теперь рассчитывается совсем по-другому. За основу берется значение цены точки 2 (окончание первой волны и начало второй). Затем к ней прибавляется (в случае установки Sell Stop, или вычитается в случае установки Buy Stop) высота волны 1 (разница между точками 1 и 2).

    Наравне с функцией AddPending заимствуем у эксперта RollBackIntrand_Expert код функций DeletePending (удаление несработавшего отложенного ордера при закрытии первичной позиции по профиту) и самой функции start.

    Но все же одну уникальную для этого эксперта  функцию придется добавить. Речь идет о постоянной проверке (хотя бы раз за свечу) уровня профита первичной позиции. Как упоминалось выше, открытая позиция должна закрываться при достижении противоположной границы канала Боллинджера. Но всвязи с тем, что эта граница постоянно меняет свое положение, требуется  постоянно менять уровень закрытия. Закрытие же позиции по рынку отбросим как не очень удобное в данной ситуации. Более изящным решением здесь видится именно корректировка уровня профита позиции при открытии новой свечи, опираясь на значения BandUp и BandDn, которые мы заботливо рассчитали в конце кода функции GetSignal.

    Функцию, отвечающую за правильное значение профита позиции, назовем просто - CheckProfit:

 
//+-------------------------------------------------------------------------------------+
//| Проверка профита на равенство одной из полос Боллинджера                            |
//+-------------------------------------------------------------------------------------+
void CheckProfit()
{
 for (int i = OrdersTotal()-1; i >= 0; i--)
   if (OrderSelect(i, SELECT_BY_POS))
     if (OrderSymbol() == Symbol() && MathMod(OrderMagicNumber(), 10) == 0 
        && OrderType() < 2)                                     // поиск основной позиции
       {
        if (OrderType() == OP_BUY)
          double Price = NP(BandUp);
         else
          Price = NP(BandDn+Spread);
        if (MathAbs(OrderTakeProfit() - Price) >= Tick)
          if (WaitForTradeContext())
            if ((ND(Ask - StopLevel - Price) > 0 && OrderType() == OP_SELL) ||
                (ND(Price - StopLevel - Bid) > 0 && OrderType() == OP_BUY))
              OrderModify(OrderTicket(), 0, OrderStopLoss(), Price, 0);      
       }
}

   Ничего особо сложного в ней нет.  Перебирается весь список ордеров и позиций, среди которых находится своя позиция и именно первичная (ее MagicNumber заканчивается нулем). Затем для длинной позиции берется верхняя полоса Боллинджера, а для короткой - нижняя. А дальше - дело техники. Требуется сравнить эталонное значение с текущим значением профита позиции и, в случае несовпадения, изменить уровень профита.

    Вот такой код эксперта обеспечивает функционирование торгового робота в соответствии с разработанной нами стратегией. Остается лишь проверить его на исторических котировках. Исходя из того, что сделки по данной стратегии совершаются довольно редко (за 2009 год не более 40 на таймфрейме Н1), стоит подумать об их увеличении. Этого можно достигнуть двумя способами - уменьшением таймфрейма и увеличением тестового периода. Уменьшение таймфрейма автоматически приведет к уменьшению абсолютной величины сделок, чего бы не очень хотелось. А вот увеличение тестового периода это, по крайней мере, интересно, так как позволит проверить стратегию на очень разных, по сути, рынках.

   В конечном итоге остановимся на периоде 01.01.2003 - 03.10.2009, таймфрейм Н1, так как только на нем был достигнут минимальный предел для оценки стратегии в 200 сделок. Результаты приведены на рис. 2 - 5.

Рис. 2. - График кривой баланса, получаемый при тестировании советника на валютной паре EURUSD.

 

Рис. 3. - График кривой баланса, получаемый при тестировании советника на валютной паре USDCHF.

Рис. 4. - График кривой баланса, получаемый при тестировании советника на валютной паре GBPUSD.

Рис. 5. - График кривой баланса, получаемый при тестировании советника на валютной паре USDJPY.

    Огромных прибылей мы так и не увидели как, впрочем, и больших максимальных просадок. Это говорит об устойчивости стратегии, которая в самой худшей ипостаси предстала перед нами на валютной паре EURUSD, показав максимальную просадку 2463 доллара (напоминаю, это за шесть лет!). Правда, и в прибыли эта пара толком не была. Детально рассмотрим две валютные пары - USDCHF и USDJPY, которые показали итоговую чистую прибыль

   USDJPY. Во-первых, здесь плохо то, что очень мало сделок - 191. Во-вторых, нестабильная кривая баланса, неожиданно меняющая свое направление. В-третьих, чистая прибыль 811.43 при максимальной просадке 1254.95 долларов, что дает фактор восстановления ниже единицы. Единственное, что утешает, это позитивная динамика тридцати последних сделок.

    USDCHF. Нельзя сказать, что показатели феерические, но на фоне остальных приемлемые. Чистая прибыль 1184.90 против максимальной просадки 692.08 (ФВ = 1.71). То есть про чистую прибыль можно воскликнуть: "Так ведь этот мизер заработан за шесть лет!", а про просадку: "Мы всего-то чуток просели за целых шесть лет!". Но больше всего в результатах радует вид кривой баланса, который лишь немного подпорчен в начале теста. За то дальше кривая держалась очень уверенно, позволив себе расслабиться лишь ближе к концу тестирования, да и то ради финишного спурта.

    Выводы по стратегии  напрашиваются такие:

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

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

  •     Торговля очень вялая. Иногда следующую сделку приходится ждать неделями

      

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

Игорь Герасько

Октябрь 2009