Советник AMKA_SelfTest_V2

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

 

        В последней статье, Советник с самотестом. Часть 1, была сделана попытка подстроить работу советника под "пульс рынка". Делалось это, исходя из средних значений прибыльных и убыточных сделок подряд. В итоге особенного улучшения работы советника получено не было, а в ряде случаев стабильность системы серьезно пошатнулась.

        Перед тем как попытаться исправить ситуацию, необходимо понять причины, из-за которых правильная, на первый взгляд, идея самоанализа советника привела к плачевным результатам. Для этого рассмотрим две одинаковые рыночные ситуации для оригинального советника AMKA_Expert (см. рис. 1) и для самотестирующегося советника AMKA_SelfTest_Expert (см. рис 2)

 

 Рис. 1. - Совершение сделок советником оригинальным советником AMKA_Expert.

 

 Рис. 2. - Совершение сделок самотестирующимся советником AMKA_SelfTest_Expert.

    Значимые точки времени на обоих рисунках выделены красными вертикальными линиями. Как видим, оригинальный эксперт произвел за этот отрезок времени две положительных сделки - BUY и SELL, четко следуя сигналам. Самотестирующийся же советник точно также открыл 13.11.2008 в 11:00 длинную сделку, а закрыть ее не смог, так как для него обратным сигналом (а значит и сигналом закрытия текущей позиции) являются стандартные условия открытия сделки плюс хотя бы одна убыточная (среднее значение серий убыточных сделок на тот момент). Поэтому советник с самотестом не закрывал сделку, до тех пор, пока оригинальный эксперт не совершил убыточную сделку, которая была открыта в 12:00 21.11.2008 и закрыта в 15:00 21.11.2008.

     В результате, пока самотестирующийся советник ждал образования убыточной виртуальной сделки (сделки от AMKA_Expert), в оригинальной версии была закрыта текущая прибыльная сделка (около 100 пунктов), открыта вторая, которая также принесла прибыль (еще около 150 пунктов), а затем и третья (убыточная, но убыток небольшой - порядка 20 пунктов). И все это против одной убыточной сделки эксперта с самотестом.

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

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

4 4 3 4 1 1 3 3 1 1 2 3 4 2 1 1 1 3 1 2

            Рис. 3. - Реальная последовательность серий сделок (зеленые - прибыльные, синие - убыточные)

     Если высчитать среднее для каждого типа сделки, то получим 2.1 для прибыльной серии и 2.4 для убыточной. Так как оперировать приходится целыми числами (невозможно отследить 2.2 сделки), то при округлении получаем для всех серий среднее 2. И эксперт начинает отрабатывать такой пульс сделок - после двух прибылей подряд сделки перестают совершаться и возобновляются только после получения двух виртуальных убытков подряд. Но, как видно на рис. 3, паритет серий 2 к 2, бывает очень редко, что и приводит к частым осечкам самотестирующегося советника по сравнению с оригинальным экспертом.

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

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

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

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

    Что необходимо изменить в эксперте AMKA_SelfTest для принятия нововведений? Изменится код только двух функций: DoTransaction (совершение виртуальных сделок) и start. Функция DoTransaction станет немного больше:

 
//+-------------------------------------------------------------------------------------+
//| Совершение виртуальных сделок                                                       |
//+-------------------------------------------------------------------------------------+
void DoTransaction(int Bar)
{
// - 1 - == Проверка срабатывания стопа или профита на предыдущей свече =================
    if (Trade != -1)                                      // если имеется открытая сделка
      if (Trade == 0)
        {  // Длинная позиция
         if (ND(Low[Bar+1]) <= SLPrice)                             // Срабатывание стопа
           {
            Trade = -1;                                   // закрываем виртуальную сделку
            LossCount();
           }
          else 
           if (ND(High[Bar+1]) >= TPPrice)                        // срабатывание профита
             {
              Trade = -1;                                 // закрываем виртуальную сделку
              ProfitCount();
             }
         CurMaxProfit[0] = MathMax(CurMaxProfit[0], (High[Bar+1] - OpenPrice)/Point);    
         CurMaxLoss = MathMax(CurMaxLoss, (OpenPrice - Low[Bar+1])/Point);    
        }
       else
        { // Короткая позиция
         if (ND(High[Bar+1]+Spread) >= SLPrice)                     // Срабатывание стопа
           {
            Trade = -1;                                   // закрываем виртуальную сделку
            LossCount();
           }
          else 
           if (ND(Low[Bar+1]+Spread) <= TPPrice)                  // срабатывание профита
             {
              Trade = -1;                                 // закрываем виртуальную сделку
              ProfitCount();
             }
         CurMaxProfit[0] = MathMax(CurMaxProfit[0], (OpenPrice - Low[Bar+1] - Spread)/Point);    
         CurMaxLoss = MathMax(CurMaxLoss, (High[Bar+1] + Spread - OpenPrice)/Point);    
        }    
// - 1 -  == Окончание блока ============================================================

// - 2 - == Имитация открытия длинной и закрытия короткой сделки ========================
    if (Signal == 1 && Trade != 0)
     {
      double Price = ND(Open[Bar]+Spread);
      if (Trade == 1)                                 // закрываем короткую, если имеется
        {
         if (OpenPrice-Price >= 0)
           ProfitCount();
          else
           LossCount();
         MaxLoss = MathMax(CurMaxLoss, MaxLoss); 
        }
      Trade = 0;                                              // Открытие длинной позиции
      TimeOrder = Time[Bar];
      OpenPrice = Price;                                          // Цена открытия сделки
      TPPrice = ND(Price + TakeProfit*Tick);                             // Профит сделки
      SLPrice = ND(Price - StopLoss*Tick);                                 // Стоп сделки
      ThrowOld();
      CurMaxProfit[0] = -(Spread/Point);// Текущая максимальная прибыль равна минус спрэд
      CurMaxLoss = Spread/Point;              // Текущий максимальный убыток равен спрэду
     }
// - 2 -  == Окончание блока ============================================================
     
// - 3 - == Имитация открытия короткой и закрытия длинной сделки ========================
    if (Signal == 2 && Trade != 1)
     {
      Price = ND(Open[Bar]);
      if (Trade == 0)                                  // закрываем длинную, если имеется
        {
         if (Price-OpenPrice-Spread >= 0)
           ProfitCount();
          else
           LossCount();
         MaxLoss = MathMax(CurMaxLoss, MaxLoss);  
        }
      Trade = 1;                                             // Открытие короткой позиции
      TimeOrder = Time[Bar];
      OpenPrice = Price;                                          // Цена открытия сделки
      TPPrice = ND(Price - TakeProfit*Tick);                             // Профит сделки
      SLPrice = ND(Price + StopLoss*Tick);                                 // Стоп сделки
      ThrowOld();
      CurMaxProfit[0] = -(Spread/Point);// Текущая максимальная прибыль равна минус спрэд
      CurMaxLoss = Spread/Point;              // Текущий максимальный убыток равен спрэду
     }
// - 3 -  == Окончание блока ============================================================
}

    Добавлено четыре хранилища данных: массив CurMaxprofit, переменная CurMaxLoss, переменная MaxLoss и переменная TimeOrder.

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

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

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

 
//+-------------------------------------------------------------------------------------+
//| Отбрасываем самое старое значение массива и освобождаем нулевой элемент             |
//+-------------------------------------------------------------------------------------+
void ThrowOld()
{
 for (int i = ResultsPeriod; i > 0; i--)
   CurMaxProfit[i] = CurMaxProfit[i-1];
}

    Функция ThrowOld вызывается при каждом открытии виртуальной сделки и позволяет массиву CurMaxProfit всегда содержать данные по последним ResultsPeriod сделкам. В свою очередь, параметр ResultsPeriod вынесен во внешние параметры эксперта для задания значения пользователем. Его значение по умолчанию 11.

    Изменения в функции start коснулись лишь шестого и седьмого блоков:

 
// - 6 - == Открытие длинной позиции ====================================================
   if (Signal == 1 && TimeOrder == Time[0])
     {
      int Res = CheckOrdersReal(OP_SELL);
      RefreshRates();
      if (Res == 0)                                          // если нет открытых позиций
        {                                               
         Price = ND(Ask);
         if (LossSeries > 0)
           {
            int NewTP = MathRound(TPKoef*AverageProfit());
            int NewSL = SLKoef*MaxLoss;
           } 
          else
           {
            NewTP = ArrayMinimum(CurMaxProfit, ResultsPeriod, 1);
            NewSL = NewTP+2*(Spread/Point);
           } 
         double TP = ND(IF(NewTP > StopLevel/Point, Price+NewTP*Tick,
                           Price + StopLevel));
         double SL = ND(IF(NewSL > (StopLevel+Spread)/Point, Price-NewSL*Tick,
                           Price - StopLevel-Spread));
         Type = OP_BUY;
        }                    
      if (Res == 1) return(0);                      // не удалось закрыть противоположную
     }
// - 6 -  == Окончание блока ============================================================
     
// - 7 - == Установка ордера SellStop ===================================================
   if (Signal == 2 && TimeOrder == Time[0])
     {
      Res = CheckOrdersReal(OP_BUY);
      if (Res == 0)                                          // если нет открытых позиций
        {               
         RefreshRates();
         Price = ND(Bid);
         if (LossSeries > 0)
           {
            NewTP = MathRound(TPKoef*AverageProfit());
            NewSL = SLKoef*MaxLoss;
           } 
          else
           {
            NewTP = ArrayMinimum(CurMaxProfit, ResultsPeriod, 1);
            NewSL = NewTP+2*(Spread/Point);
           } 
         TP = ND(IF(NewTP > StopLevel/Point, Price-NewTP*Tick, 
                    Price - StopLevel));
         SL = ND(IF(NewSL > (StopLevel+Spread)/Point, Price+NewSL*Tick,
                    Price + StopLevel + Spread));
         Type = OP_SELL;
        }                    
      if (Res == 1) return(0);                      // не удалось закрыть противоположную
     }
// - 7 -  == Окончание блока ============================================================

    Теперь сделки открываются, если соответствующий сигнал (переменная Signal) является сигналом открытия виртуальной сделки (переменная TimeOrder). В противном случае сигнал пропускается.

    Если сигнал принимается, то для новой сделки рассчитываются уровни стопа и профита. Если текущая серия виртуальных сделок убыточная (LossSeries > 0), то стоп ставится на максимальное значение потенциального убытка и при этом еще умножается на коэффициент SLKoef, а профит - на среднее значение потенциальной прибыли, также умноженное на коэффициент TPKoef. При помощи TPKoef и SLKoef пользователь может задавать увеличение (больше единицы) или уменьшение (меньше единицы) полученных расчетных значений профита и стопа.

    Проверим же, насколько изменились результаты работы эксперта (это будет вторая версия - AMKA_SelTest_V2) по сравнению с той же оригинальной версией. Исторический диапазон по сравнению с первой частью статьи немного увеличим - 01.01.2008-14.08.2009. Все входные параметры экспертов были взяты по умолчанию. Таймфрейм Н1. Для более удобного визуального сравнения графики изменения кривых баланса приведены попарно. Вверху - от AMKA_Expert, внизу - от AMKA_SelfTest_V2 (см. рис. 4-7).

 

 

Рис. 4. - Графики изменения кривых баланса по результатам тестирования экспертов AMKA_Expert (вверху) и AMKA_SelfTest_V2 на валютной паре EURUSD.

 

Рис. 5. - Графики изменения кривых баланса по результатам тестирования экспертов AMKA_Expert (вверху) и AMKA_SelfTest_V2 на валютной паре USDCHF.

 

Рис. 6. - Графики изменения кривых баланса по результатам тестирования экспертов AMKA_Expert (вверху) и AMKA_SelfTest_V2 на валютной паре GBPUSD.

 

Рис. 7. - Графики изменения кривых баланса по результатам тестирования экспертов AMKA_Expert (вверху) и AMKA_SelfTest_V2 на валютной паре USDJPY.

 

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

 

AMKA_Expert

AMKA_SelfTest_V2

  Чистая прибыль Максимальная просадка Фактор восстановления Чистая прибыль Максимальная просадка Фактор восстановления
EURUSD -224.18 2669.72 -0.84 1268.40 1183.11 1.072
USDCHF 729.41 1603.08 0.455 787.49 636.38 1.237
GBPUSD 3260.01 1755.99 1.857 2312.23 1058.17 2.815
USDJPY 4105.62 1457.85 2.816 1675.67 663.68 2.525

    Отличительной особенностью результатов от AMKA_SelfTest_V2 по сравнению с AMKA_Expert является намного меньшее значение максимальной просадки. Да, в случае с парами GBPUSD и USDJPY чистая прибыль уменьшилась, но это с лихвой компенсируется уменьшением максимальной просадки, что в итоге и увеличивает значение фактора восстановления. И заметьте - мало того, что везде показана чистая прибыль, так еще и фактор восстановления по всем валютным парам не опускается ниже единицы.

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

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

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

Август 2009