Доработка тестера стратегий для оптимизации индикаторов на примерах тренда и флета

Carl Schreiber | 13 июня, 2016

Проблематика

Одна из самых распространенных проблем при оптимизации торговых роботов — слишком большое количество используемых параметров.

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

Представьте себе идею для эксперта, по которой краткосрочная торговля при флэте происходит по свингам (к средней скользящей), а при наличии тренда — в направлении от средней скользящей.  Для определения состояния рынка эксперт должен руководствоваться поведением ADX на старшем таймфрейме — в данном случае это будут часовые бары. Помимо ADX, эксперт может включать в себя еще 5 индикаторов, которые используются для краткосрочного управления торговлей. Каждый из них, в свою очередь, имеет 4 настраиваемых параметра, и, поскольку шаг небольшой, то каждый параметр может принимать 2000 значений. В общей сложности это составляет 2000*5*4 = 40 000. Теперь добавим в эту картину ADX — и в результате получим очень громоздкую задачу: для каждой комбинации параметров ADX теоретически необходимо провести еще 40 000 расчетов.

В данном примере мы установим период ADX' (PER), его цену (PRC) и лимит (LIM). Если индикатор ADX (MODE_MAIN) поднимается выше уровня LIM — значит, начинается тренд. Если MODE_MAIN опускается ниже LIM — будем считать, что рынок входит во флет. Для периода PER можно выбрать 2,..,90 (Step1=> 89 разных значений), для цены — 0,..,6 (=Close,..,Weighted, Step 1=> 7), а для LIM — 4,..,90 (Step1 => 87). Итого мы имеем 89*7*87 = 54 201 комбинацию для тестирования. Ниже вы найдете настройки для тестера стратегий:

Рис. 01 Параметры установки тестера

Рис. 02 Установка тестера - функции эксперта

Рис. 03 Функции установки тестера

Рис. 04 Оптимизация установки тестера

При каждом новом запуске оптимизации не забудьте очистить кэш в \tester\cache\. Иначе результаты будут записаны не в csv-файл, а только в "Результаты оптимизации" и "График оптимизации" Тестера Стратегий, поскольку OnTester() в этом случае не выполняется.

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

Из-за того, что наш псевдоэксперт не торгует (а значит, нет необходимости использовать все тики), и в наличии имеется только один индикатор с 54 201 комбинациями, которые необходимо протестировать, мы можем отключить генетический режим и позволить Тестеру самому рассчитать все комбинации. Если бы мы не могли провести предварительную оптимизацию ADX или уменьшить число комбинаций, то потребовалось бы умножить 40 000 комбинаций других переменных эксперта на 54 201 комбинацию ADX. Согласитесь, 2 168 040 000 комбинаций для оптимизации — это очень много. Именно поэтому в таком случае потребовалась бы генетическая оптимизация.

Наконец, цель нашей работы — не только резко снизить диапазон параметров ADX. Мы получим также более полное представление об этом индикаторе, поскольку увидим, что ADX в состоянии отличить трендовое состояние рынка от флетового — и это даже при том, что определение флета чуть сильнее задерживается по сравнению с трендом (здесь остается пространство для дальнейшей доработки!). Мы получим представление о том, как определить стоп-лоссы и цели эксперта в зависимости от найденных диапазонов движения цены в состоянии флета и тренда.

ADX будет протестирован только по следующим показателям: период PER 11..20 (Step 1=> 10), цена PRC 0,6 (Step 6=>2) и лимит LIM: 17,..,23 (Step 1=> 7), что составляет всего 140 комбинаций. Это означает, что вместо 2 168 040 000 у нас остается лишь 4 680 000 комбинаций для тестирования — а это в 460 раз ускорит тестирование в негенетическом режиме или в 460 раз улучшит результаты в генетическом режиме. Тестер в генетическом режиме осуществит порядка 10.000 проходов, но сейчас мы можем протестировать в нем существенно большее количество значений других параметров эксперта!

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


Идея

Итак, мы создаем псевдоэксперта, в задачи которого не входят торговые операции. В нем только три важные функции. OnTick() используется для проверки индикатора и определения состояния рынка, OnTester() записывает окончательный результат в наш csv-файл, и в calcOptVal() рассчитывается значение OptVal, которое OnTester() возвращает в Тестер Стратегий для ранжирования и запуска генетического алгоритма. Функция OnTester() вызывается в конце каждого тестового прогона, возвращает определенное значение и добавляет новую строку к csv-файлу для анализа после полного завершения процесса оптимизации. 


Псевдоэксперт, предварительная версия

Теперь нам необходимо определить критерии для расчета возвращаемого значения: OptVal. Мы выбираем диапазон ценовых значений флетового и трендового рынка: это будет разница между максимальным High и минимальным Low рынка. Делим TrndRange на FlatRange, и оптимизатор находит максимальное значение этого отношения:
double   TrndRangHL,       // разность максимального High и минимального Low трендовых рынков
         TrndNum,          // число трендовых рынков
         FlatRangHL,       // разность максимального High и минимального Low флетовых рынков
         FlatNum,          // число флетовых рынков
         RangesRaw,        // Отношение диапазонов цены трендовых и флетовых рынков (чем оно больше, тем лучше)
         // ...            смотрите ниже

double calcOptVal() // первое приближение!!
   {
      FlatRange    = FlatRangHL / FlatNum;
      TrndRange    = TrndRangHL / TrndNum;
      RangesRaw    = FlatRange>0 ? TrndRange/FlatRange : 0.0; 
      return(RangesRaw);
   }
...
double OnTester() 
   {
      OptVal = calcOptVal();
      return( OptVal );
   }

Мы запускаем оптимизацию при вышеуказанных настройках и OptVal = RangesRaw. Результат в "Графике оптимизации" будет выглядеть следующим образом:

Рис. 05 График оптимизации для Raw

Теперь ознакомимся с лучшими значениями в "Результатах оптимизации", упорядоченными по "Результатам OnTester" сверху вниз. Картина следующая:

Рис. 06 Лучшие значения оптимизации для Raw

Мы видим, что соотношения абсурдно высоки. Сsv-файл показывает, что средняя длина флетовых рынков составляет 1 бар, и смен рынка (число флетовых рынков + число трендовых рынков) слишком мало для эффективного использования. (Нам не нужно беспокоиться о выбивающихся цифрах вроде PRC=1994719249 вместо 0,..,6 поскольку правильные значения цены индикатора ADX записаны в csv-файле).

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


Доработка псевдоэксперта: выравнивание критериев

Вначале зададим минимальную длину (минимальное число баров) флета или тренда:

      FlatBarsAvg  = FlatBars/FlatNum; // сложить все 'флетовые бары'  / число флетовых рынков
      TrndBarsAvg  = TrndBars/TrndNum; // сложить все 'трендовые бары' / число трендовых рынков
      BrRaw        = fmin(FlatBarsAvg,TrndBarsAvg);

Затем укажем минимальное количество переключений между флетом и трендом:

      SwitchesRaw  = TrndNum+FlatNum; // число трендовых и флетовых рынков

Теперь перед нами стоит другая проблема. RangesRaw колеблется от 0 до 100 000.0, BrRaw — от 0 до 0.5, а SwitchesRaw — от 0 до ~8000 (=Bars()). Теоретически, это происходит каждый раз, когда новый бар переключается на другой тип рынка.

Три этих критерия необходимо выровнять. Для всех них нам потребуется одна и та же функция: Arc tangens, или atan(..) в mq4. В отличие, например, от sqrt() или log(), при ее использовании не возникают проблемы с нулем или отрицательными значениями. Также atan() никогда не превышает предела. Таким образом, например в случае RangesRaw, разница между atan(100 000) и atan(20) близка к нулю, и взвешены они почти одинаково. Это увеличивает влияние других факторов. Кроме того, atan() обеспечивает плавный подъем к пределу, в то время как жесткий предел вроде if(x>limit) взвешивает все значения, превышающие предел, поровну, и снова находит лучшие ближайшие к пределу значения. Почему для наших целей не подходит жесткий предел, будет рассказано ниже. 

Давайте посмотрим как работает atan(). Для построения графика я использовал этот инструмент:

Рис. 07 Функция Atan

Синия линия проходит через начало координат и ограничена по оси ординат между +1 и -1.
Красная линия (и ее функция) демонстрирует сдвиг по оси абсцисс от x=0 к x=4.
Зеленая линия демонстрирует, как мы можем изменять крутизну. Мы можем контролировать, как быстро atan() будет приближаться к пределам и как быстро будут уменьшаться различия.

В нашем случае не требуется изменять пределы, к которым стремится функция. Однако для информации отмечу: если сменить 1*atan(..) на 2*atan(..), то пределы переместятся к значениям +2 и -2, и т.д.

Смена верхнего предела на нижний путем изменения 1*atan() на -1*atan() нам тоже не понадобится. Но если это сделать, то функция будет стремиться к -1 при увеличении х.

Теперь у нас есть все необходимое для окончания работы над псевдоэкспертом.


Окончательная версия псевдоэксперта

Напомним, что программа, которую мы разрабатываем, не может торговать. Псевдоэксперт вызывает iADX(..) только при открытии нового бара. Это означает, что нам не требуются режимы "Все тики" или "Контрольные точки".  При оценке состояния рынка мы можем использовать самую быструю модель "Только цены открытия" по предыдущим 2 барам индикатора ADX:

extern int                 PER   = 22;             // период ADX
extern ENUM_APPLIED_PRICE  PRC   = PRICE_TYPICAL;  // цена ADX
extern double              LIM   = 14.0;           // лимит для основной линии ADX
extern string            fName   = "";             // название файла в \tester\files, "" => Не csv-файла!


//+------------------------------------------------------------------+ 
//| Определение глобальной переменной                                |
//+------------------------------------------------------------------+ 
double   OptVal,           // OnTester() возвращает это значение, оно может быть найдено в колонке "OnTester Result" 
         TrndHi,           // максимальный High текущего трендового рынка
         TrndLo,           // минимальный Low текущего трендового рынка
         TrndBeg,          // цена в начале трендового рынка
         TrndRangHL,       // максимальный High - минимальный Low трендового рынка
         TrndRangCl,       // последняя цена закрытия - первая цена закрытия трендового рынка (осталась, но не использована)
         TrndNum,          // число трендовых рынков
         TrndBars=0.0,     // число баров на трендовых рынках
         TrndBarsAvg=0.0,  // среднее число баров на трендовых рынках
         FlatBarsAvg=0.0,  // среднее число баров на флетовых рынках
         FlatHi,           // максимальный High текущего флетового рынка
         FlatLo,           // минимальный Low текущего флетового рынка
         FlatBeg,          // цена в начале флетового рынка
         FlatRangHL,       // максимальный High - минимальный Low флетового рынка
         FlatRangCl,       // последняя цена закрытия - первая цена закрытия флетового рынка (осталась, но не использована)
         FlatNum,          // число флетовых рынков
         FlatBars=0.0,     // число баров флетовых рынков
         FlatRange,        // tmp FlatRangHL / FlatNum
         TrndRange,        // tmp TrndRangHL / TrndNum
         SwitchesRaw,      // количество смен рынка с флета на тренд или наоборот
         SwitchesAtan,     // Atan (арктангенс) количества смен рынка
         BrRaw,            // Минимум часов флетового или трендового рынков (чем больше, тем лучше)
         BrAtan,           // арктангенс BrRaw
         RangesRaw,        // Отношение диапазона цены трендовых рынков и диапазона цены флетовых рынков (чем больше, тем лучше)
         RangesAtan;       // арктангенс отношения (TrndRange/FlatRange)

enum __Mkt // 3 состояния рынков 
 {
   UNDEF,  
   FLAT,
   TREND
 };
__Mkt MARKET = UNDEF;      // начальное состояние рынка
string iName;              // имя индикатора
double main1,main2;        // значения основной линии Adx main


//+------------------------------------------------------------------+ 
//| OnTick расчет индикатора, определение состояния рынка             |
//+------------------------------------------------------------------+ 
void OnTick() 
 {
 //---
   static datetime tNewBar=0;
   if ( tNewBar < Time[0] ) 
    {
      tNewBar = Time[0];
      main1 = iADX(_Symbol,_Period,PER,PRC,  MODE_MAIN, 1); // ADX
      main2 = iADX(_Symbol,_Period,PER,PRC,  MODE_MAIN, 2); // ADX)
      iName = "ADX";

      // установить переменную для соответствующего состояния рынка 
      if ( MARKET == UNDEF ) 
       { 
         if      ( main1 < LIM ) main2 = LIM+10.0*_Point; // на РЫНКЕ присутствует ФЛЕТ
         else if( main1 > LIM ) main2 = LIM-10.0*_Point; // на РЫНКЕ присутствует ТРЕНД
         FlatHi  = High[0];
         FlatLo  = Low[0];
         FlatBeg = Close[2];//
         TrndHi  = High[0];
         TrndLo  = Low[0];
         TrndBeg = Close[2];//
       }
      
      // сейчас флетовый рынок?
      if ( MARKET != FLAT && main2>LIM && main1<LIM)  // ADX
       {
         //завершается трендовый рынок
         TrndRangCl += fabs(Close[2] - TrndBeg)/_Point;
         TrndRangHL += fabs(TrndHi - TrndLo)/_Point;
                  
         // обновить соответствующие значения
         OptVal = calcOptVal();

         //устанавливается новый флет
         MARKET  = FLAT;
         FlatHi  = High[0];
         FlatLo  = Low[0];
         FlatBeg = Close[1];//
         ++FlatNum;
         if ( IsVisualMode() )
          {
            if (!drawArrow("Flat "+TimeToStr(Time[0]), Time[0], Open[0]-(High[1]-Low[1]), 243, clrDarkBlue) ) // 39:свеча, рынок спит
               Print("Error drawError ",__LINE__," ",_LastError);
          }
       } 
      else if ( MARKET == TREND )   // обновление текущего трендового рынка
       {
         TrndHi = fmax(TrndHi,High[0]); 
         TrndLo = fmin(TrndLo,Low[0]); 
         TrndBars++;
       }
      
      // мы входим в трендовый рынок?
      if ( MARKET != TREND && main2<LIM && main1>LIM) 
       { 
         // завершается флет 
         FlatRangCl += fabs(Close[2] - FlatBeg)/_Point;
         FlatRangHL += fabs(FlatHi - FlatLo)/_Point;
         
         // обновить соответствующие значения
         OptVal = calcOptVal();

         // устанавливается новый трендовый рынок
         MARKET  = TREND;
         TrndHi  = High[0];
         TrndLo  = Low[0];
         TrndBeg = Close[1];//
         ++TrndNum;
         TrndBars++;
         if ( IsVisualMode() )
          {
            if(!drawArrow("Trend "+TimeToStr(Time[0]), Time[0], Open[0]-(High[1]-Low[1]), 244, clrRed)) // 119:kl Diamond
               Print("Error drawError ",__LINE__," ",_LastError);
          }
       } 
      else if ( MARKET == FLAT  ) // обновление текущего флета
       {
         FlatHi = fmax(FlatHi,High[0]);
         FlatLo = fmin(FlatLo,Low[0]); 
         FlatBars++; 
       }
      
    }
   if ( IsVisualMode() )  // отображение текущей ситуации в визуальном режиме
    {
      string lne = StringFormat("%s  PER: %i    PRC: %s    LIM: %.2f\nMarket  #   BarsAvg  RangeAvg"+
                                "\nFlat:    %03.f    %06.2f         %.1f\nTrend: %03.f    %06.2f         %.1f   =>  %.2f",
                                 iName,PER,EnumToString(PRC),LIM,FlatNum,FlatBarsAvg,FlatRange,
                                 TrndNum,TrndBarsAvg,TrndRange,(FlatRange>Point?TrndRange/FlatRange:0.0)
      );
      Comment(TimeToString(tNewBar),"  ",EnumToString(MARKET),"  Adx: ",DoubleToString(main1,3),
              "  Adx-Lim:",DoubleToString(main1-LIM,3),"\n",lne);
    }
 }

Если ADX пересекает LIM , значит, предыдущее состояние рынка завершается, и мы готовимся к новому. Псевдоэксперт рассчитывает все различия котировок в пунктах.

Давайте разберемся, чего мы хотим достичь, и определим, что для этого потребуется. Нам нужно число, возвращаемое функцией OnTester(). Оптимизатор Тестера Стратегий рассчитывает его по принципу "чем больше, тем лучше". Значение, возвращенное с помощью OnTester() (OptVal), должно увеличиться, если разница между флетом и трендом становится лучше для наших целей.

Мы определили три переменных для расчета OptVal. Для двух из них мы можем установить обоснованный минимум:

  1. RangesRaw = TrndRage/FlatRange должно быть больше 1. Трендовый рынок по сравнению с флетом должен иметь больший средний диапазон цены. TrndRage и FlatRange определены как разница между максимальным High и минимальным Low текущего рынка. Установим пересечение оси x при x=1.
  2. BrRaw должно быть больше чем 3 бара (= 3 часа). BrRaw = fmin(FlatBarsAvg,TrndBarsAvg).  FlatBarsAvg и TrndBarsAvg — среднее число баров для соответствующего типа рынка. Это ограничение необходимо нам, чтобы предотвратить появление абсурдно запредельных значений. Установим для него пересечение с осью x на x=3.
  3. SwitchesRaw. Мы проводим оптимизацию на более чем 8000 барах. На такой дистанции результат количества переключений, равный, например, 20 (10 флетовых и 10 трендовых рынков) не имеет смысла. В этом случае продолжительность каждого тренда или флета составила бы в среднем 400 часов или 16 дней.

Проблема заключается в том, чтобы найти приемлемый предел для SwitchesRaw, поскольку он сильно зависит от таймфрейма и от общего числа баров. В отличие от 1) и 2), где мы могли бы установить ограничения из соображений достоверности, здесь мы сначала должны еще раз рассмотреть первые результаты (вкладка Opti ADX ВСЕ в прикрепленном csv-файле), чтобы выставить подходящий предел:

Рис. 08 Opti ADX график всех переключений

Вместо того, чтобы по отдельности обрабатывать ~2500 различных переключений, мы используем лишь sqrt(2500) = 50 классов, с которыми намного удобнее работать. Для каждого класса мы рассчитаем и построим среднюю. Мы видим наличие локального минимума на значении 172. Давайте попробуем установить нижний предел на 100, чтобы увидеть как наш псевдоэксперт справится с этой границей. Установим небольшой коэффициент 0.01, чтобы обеспечить медленное движение к выбранному пределу от 100. Оговорюсь еще раз, обычно мы бы использовали более высокий предел, например, 200, но чтобы окончательно разобраться — так сказать, в педагогических целях — поступим иначе.

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

Рис. 09 Atan для переключений (синий)

Разберем две другие функции оценки.
Красная линия —  функция BrRaw: приемлемый минимум длительности рынка составляет 3 бара, а коэффициент в размере 0.5 гарантирует, что будут заметны изменения ситуации даже на 8 барах (часах).
Зеленая линия — функция для RangesRaw: приемлемый минимум составляет 1, и поскольку  чудес не бывает, то результат более 8 не может учитываться как серьезный.

Рис. 10 Бары Atan (красный) Диапазон (зеленый) и Переключения (синий)

Теперь мы можем создать функцию для расчета OptVal, которое будет возвращаться с помощью OnTester().

  1. Так как это распространяется на все три переменные, мы можем их перемножить.
  2.  atan(..) может принять отрицательное значение для всех трех наших функций, поэтому вычисляем так: fmax(0.0,atan(..)). Иначе, к примеру, два отрицательных результата нашей функции atan() приведут к ложному положительному значению для OptVal .
//+------------------------------------------------------------------+ 
//| calcOptVal рассчитывает OptVal для возвращения  Тестеру Стратегий|
//| и коэффициенты для оценки                                    |
//+------------------------------------------------------------------+ 
// Коэфф. для SwitchesAtan, число переключений:
double SwHigh = 1.0, SwCoeff=0.01, SwMin = 100;
// Коэфф. для BrAtan, число баров:
double BrHigh = 1.0, BrCoeff=0.5,  BrMin = 3.0;
// Коэфф. для RangesAtan, TrendRange/FlatRange:
double RgHigh = 1.0, RgCoeff=0.7,  RgMin = 1.0;

double calcOptVal() {
   if ( FlatNum*TrndNum>0 ) {
      SwitchesRaw  = TrndNum+FlatNum;
      SwitchesAtan = SwHigh*atan( SwCoeff*(SwitchesRaw-SwMin))/M_PI_2;

      FlatBarsAvg  = FlatBars/FlatNum;
      TrndBarsAvg  = TrndBars/TrndNum;
      BrRaw        = fmin(FlatBarsAvg,TrndBarsAvg);
      BrAtan       = BrHigh*atan( BrCoeff*(BrRaw-BrMin))/M_PI_2;

      FlatRange    = FlatRangHL / FlatNum;
      TrndRange    = TrndRangHL / TrndNum;
      RangesRaw    = FlatRange>0 ? TrndRange/FlatRange : 0.0; 
      RangesAtan   = FlatRange>0 ? RgHigh*atan( RgCoeff*(RangesRaw-RgMin))/M_PI_2 : 0.0;
      return(fmax(0.0,SwitchesAtan) * fmax(0.0,BrAtan) * fmax(0.0,RangesAtan));  
   }
   return(0.0);
}



Вторая важная функция псевдоэксперта — OnInit(). Она используется для записи заголовков столбцов csv-файла:

//+------------------------------------------------------------------+ 
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+ 
int OnInit() 
  {
//---
   // записать строку заголовка листа с расчетами
   if ( StringLen(fName)>0 ) {
      if ( StringFind(fName,".csv", StringLen(fName)-5) < 0 ) fName = fName+".csv";    //  проверить название файла
      if ( !FileIsExist(fName) ) {                                                     // записать заголовки столбца нового файла
         int fH = FileOpen(fName,FILE_WRITE);
         if ( fH == INVALID_HANDLE ) Print("ERROR open ",fName,": ",_LastError); 
         string hdr = StringFormat("Name;OptVal;RangesRaw;PER;PRC;LIM;FlatNum;FlatBars;FlatBarsAvg;FlatRgHL;FlatRgCls;FlatRange;"+
                      "TrendNum;TrendBars;TrendBarsAvg;TrendRgHL;TrendRgCl;TrendRange;"+
                      "SwitchesRaw;SwitchesAtan;BrRaw;BrAtan;RangesRaw;RangesAtan;FlatHoursAvg;TrendHoursAvg;Bars;"+
                      "Switches: %.1f %.1f %.f, Hours: %.1f %.1f %.1f, Range: %.1f %.1f %.1f\n",
                      SwHigh,SwCoeff,SwMin,BrHigh,BrCoeff,BrMin,RgHigh,RgCoeff,RgMin);
         FileWriteString(fH, hdr, StringLen(hdr));
         FileClose(fH);
      }   
   }
//---
   return(INIT_SUCCEEDED);
  }

Функция OnTester() завершает определение текущего состояния рынка и записывает результат оптимизации в конец csv-файла:

double OnTester() 
 {
   // проверить предел: по крайней мере одно переключение
   if ( FlatNum*TrndNum<=1 ) return(0.0);  // один из них равен 0 => пропустить незначимые результаты
   
   // теперь завершим последний рынок: флет
   if ( MARKET == FLAT ) 
    {
      TrndRangCl += fabs(Close[2] - TrndBeg)/_Point;
      TrndRangHL += fabs(TrndHi - TrndLo)/_Point;

      // обновить соответствующие значения
      OptVal = calcOptVal();

    } 
   else if ( MARKET == TREND ) // .. и тренд
    {
      FlatRangCl += fabs(Close[2] - FlatBeg)/_Point;
      FlatRangHL += fabs(FlatHi - FlatLo)/_Point;

      // обновить OptVal
      OptVal = calcOptVal();
    }
   
   // записать значения в csv файле
   if ( StringLen(fName)>0 ) 
    {
      string row = StringFormat("%s;%.5f;%.3f;%i;%i;%.2f;%.0f;%.0f;%.1f;%.0f;%.0f;%.2f;%.2f;%.0f;%.0f;%.1f;%.0f;%.0f;%.2f;%.2f;%.0f;%.5f;%.6f;%.5f;%.6f;%.5f;%.2f;%.2f;%.0f\n",
                  iName,OptVal,RangesRaw,PER,PRC,LIM,
                  FlatNum,FlatBars,FlatBarsAvg,FlatRangHL,FlatRangCl,FlatRange,
                  TrndNum,TrndBars,TrndBarsAvg,TrndRangHL,TrndRangCl,TrndRange,
                  SwitchesRaw,SwitchesAtan,BrRaw,BrAtan,RangesRaw,RangesAtan,
                  FlatBarsAvg*_Period/60.0,TrndBarsAvg*_Period/60.0,
                  (FlatBars+TrndBars)
             );
             
      int fH = FileOpen(fName,FILE_READ|FILE_WRITE);
      if ( fH == INVALID_HANDLE ) Print("ERROR open ",fName,": ",_LastError);
      FileSeek(fH,0,SEEK_END); 
      FileWriteString(fH, row, StringLen(row) );
      FileClose(fH);
    }
   // вернуть 0.0 вместо отрицательных значений. В нашем случае, они приводят к беспорядку в графике оптимизации.
   return( fmax(0.0,OptVal) );
 }


Наш псевдоэксперт готов. Теперь подготовим тестер для оптимизации.

  1. Отключаем генетический алгоритм, чтобы проверить каждую комбинацию.
  2. Меняем "Оптимизированные параметры" на Custom. Это дает нам более интересные изображения в "Графике оптимизации".
  3. Необходимо убедиться, что кэш в ..\tester\caches удален
  4. Для csv-файла требуется, чтобы fName не был пуст, и существующий csv файл с таким именем был удален из \tester\files
  5. Если оставить csv-файлу такое название, оптимизатор будет добавлять строку за строкой, до тех пор, пока не начнутся проблемы с его размером.
  6. Выберем символ EURUSD.
  7. Период установлен на H1 (здесь берется период с 2015. 08. 13 по 2015.11.20).
  8. Выбран режим "Только цены открытия".
  9. Не забудьте активировать режим "Оптимизация".

На моем ноутбуке тестер провел оптимизацию данных с 2007 года в течение всего 25 минут. Окончательный csv-файл находится в ..\tester\files\.

На графике оптимизации мы можем увидеть, к примеру, такую картину (по оси ординат — LIM, по оси абсцисс — PER):

Рис. 11 TesterGraph SwLim 100

Это выглядит намного лучше нашей первоначальной оптимизации. Мы видим более высокую плотность: 34>PER>10 и 25>LIM>13, что намного лучше чем 2,..,90 и 4,..,90.

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

SwMin = 50:

Рис. 11 TesterGraph SwLim 050

SwMin = 150

Рис. 13 TesterGraph SwLim 150

SwMin = 200:

Рис. 14 TesterGraph SwLim 200

Для всех проходов оптимизации действительны следующие пределы: 34>PER>10 и 25>LIM>13, что является хорошим признаком надежности этого подхода.

Краткое резюме:
  • Использование функции atan(..), делает OptVal одинаково чувствительным для трех наших переменных.
  • При использовании atan(..) необходимо правильно подобрать коэффициенты. Я потратил много времени, пока не достиг приемлемых результатов. Возможно, у вас получится лучше.
  • Я вносил изменения до тех пор, пока не получил нужный результат.
  • Этот псевдоэксперт не предусмотрен для поиска единственного лучшего решения. Цель его использования — в том, чтобы найти приемлемые пределы уменьшения количества необходимых данных по каждому параметру. В конечном итоге, эффективность работы подобного вспомогательного псевдоэксперта определяется только результатами работы основной программы — торгового советника.


Анализ результатов в EXCEL, проверка на достоверность

Каждый проход оптимизации добавляет новую строку к csv-файлу с более подробной информацией, по сравнению с информацией, предлагаемой Тестером Стратегий. При этом он пропускает ненужные нам категории, вроде прибыли (Profit), количества cделок (trades), факторов прибыли (profit factor)... Этот файл мы загрузим в Excel (или, как в моем случае, в LibreOffice).

Нам необходимо все отсортировать, во-первых, по OptVal и, во-вторых, по RangesRaw. После этого получаем результат в виде таблицы (Таблица: "Optimizing ADX SwLim 100 raw"):

Рис. 15 Optimizing ADX SwLim 100 raw

Рассмотрим лучшие 50 результатов по OptVal. Различные параметры PER, PRC и LIM отмечены разными цветами для удобства распознавания.

  1. RangesRaw колеблется от 2.9 до 4.5. Это означает, что ценовой диапазон трендового рынка в 3-4.5 раз больше, чем у флетового рынка.
  2. Флетовый рынок держится на протяжении 6 — 9 баров (часов).
  3. Диапазон цены флетового рынка ограничен 357 — 220 пунктами, этого достаточно для "торговли внутри диапазона".
  4. Тренд длится от 30 до 53 часов.
  5. Диапазон цены на трендовом рынке колеблется от 1 250 до 882 пунктов.
  6. Если взглянуть на лучшие 200, а не 50, то диапазоны RangesRaw почти одинаковые: от 2.5 до 5.4. Флет ограничен диапазоном 221 — 372 пунктов, тренд — 1,276 — 783 пунктов.
  7. PER лучших 200: от 14 до 20; LIM: от 14 до 20.  Однако эти параметры надо рассматривать более детально.
  8. Если посмотреть на ту часть, где OptVal становится 0.0, мы видим очень высокие значения RangesRaw, однако другие значения свидетельствуют о том, что они недостаточно хороши для торговли (вкладка: "skipped OptVal=0"):

Рис. 16 skipped OptVal 0

Значения RangesRaw невероятно большие, но FlatBarsAvg — слишком короткие для торговли, и/или значения TrndBarsAvg слишком велики (более 1000 часов).

Теперь проверим RangeRaw в той части, где OptVal>0, и упорядочим по RangesRaw (вкладка: "OptVal>0 sort RangesRaw"):

Рис. 17 OptVal gt 0 sort RangesRaw

50 наибольших значений RangesRaw выстраиваются в диапазоне от 20 до 11. Однако обратите внимание на TrendBarsAvg: среднее значение около 100, это больше 4 дней.

В целом, можно сказать, что OptVal достаточно хорошо исключил из рассмотрения все результаты ADX, которые затруднили бы торговлю. С другой стороны, наиболее высокое значение RangesRaw в топе лучших 200 (5.4) или лучших 500 (7.1) выглядит очень многообещающе.



Проверка параметров

После необходимой проверки на достоверность рассмотрим параметры PER и PRC индикатора ADX и его предел LIM.

Данных очень много: 29 106 строк, но нас интересуют лишь строки, где значение OptVal превышает 0. В таблице необработанных значений это первые 4085 строк (если они отсортированы согласно OptVal!). Мы сохраним их в новую таблицу. К PER мы добавим еще три столбца, как показано на рисунке. Все формулы доступны в прикрепленном файле.

С 5 строки столбцов D,E,F введем: AVERAGE(D$2:D5), STDEV(D$2:D5), SKEW(D$2:D5). Ячейки во втором ряду только показывают значения последнего ряда, которые являются статистическим результатом целого столбца RangesRaw. Почему? Так как таблица упорядочена от лучшего значения к худшему, в строке n указаны среднее значение, стандартное отклонение и асимметрия лучшего n. Сравнение лучших значений n со всеми результатами сможет дать нам информацию о том, где найти то, что мы ищем (вкладка: "OptVal>0 Check PER, PRC, LIM"):

Рис. 18 OptVal gt 0 Check PER, PRC, LIM

Какую информацию можно из этого почерпнуть? Во второй строке(под last) мы видим, что среднее значение (Avg) для PER составляет 33.55, стандартное отклонение — (StdDev) 21.60. Если PER распределен по Гауссу, 68% всех значений PER будут находиться в пределах среднего значения +/- StdDev, а 95% — в пределах +/-2*StdDev. Здесь оно находится между 33.55 - 21.60 = 11.95 и 33.55 + 21.60 = 55,15.

Теперь обратим внимание на строки лучших значений. Среднее начинается в пятой строке с 19 и медленно увеличивается до 20. StdDev изменяется с 2.0 до 2.6. 68% всех значений охватываются диапазоном с 18 до 23.

И наконец, асимметрия. Она составляет 0.61 во 2-ой строке для всех значений PER. Это означает, что левая сторона имеет больше значений, чем правая, хотя она до сих пор подчиняется распределению Гаусса. Только когда асимметрия превышает +/- 1.96, распределение перестает быть гауссианой, и поэтому должны быть очень осторожны при использовании среднего значения и стандартного отклонения. Нам мешает то, что одна сторона чрезмерно 'заполнена', а другая — более или менее 'пустая'. Асимметрия больше 0 означает, что правая сторона (>среднего значения) имеет меньше значений, чем правая. Поэтому для PER используется распределение Гаусса, и применяется среднее значение вместе с StdDev. Если сравнить развитие лучших результатов (согласно OptVal), мы увидим, что среднее значение медленно увеличивается от 19 до 20 (строка 487!). StdDev, между тем, увеличивается от ~2.0 до 5.36 (строка 487). Асимметрия никогда не превышает 0.4, в случае пропуска первых 10 результатов, и в основном она положительная. Это означает, что нам необходимо добавить одно (или два) значения слева от среднего.

Результаты PRC нуждаются в иной интерпретации. В отличие от PER и LIM, значения PRC определяются по номинальной шкале, любые вычисления с ними бесполезны. Поэтому мы просто посчитаем, сколько раз они появились, а также посчитаем их среднее значение RangesRaw для каждого PRC 0,..,6. Как мы помним, в наши планы входила проверка даже неподходящих на первый взгляд настроек. Обычно мы не используем PRC=Open (1), PRC=High (2) или PRC=Low (3). Но в данном случае мы должны признать, что Open является наиболее часто встречающимся значением среди пятидесяти лучших. Скорее всего, это вызвано тем фактом, что мы используем только целые бары, а ADX использует High и Low баров, в связи с чем параметры high, low, close 'known to the open' имеют некое парадоксальное преимущество.

Успех High и Low объяснить непросто. Сам факт того, что цена EURUSD падает с 1.33 в августе 2014 до 1.08 в декабре 2015, объясняет успех Low, но не High. Возможно, это результат более сильной рыночной динамики. В любом случае, мы их ослабим. При сравнении PER = Close, Typical, Median и Weighted, мы осознаем, что между ними нет особой разницы, если смотреть на столбцы Q, R, и S. В топ-100 PRC=Typical(4) был бы лучшим вариантом, который обошел бы даже PRC=High(2). Но среди лучших 500 побеждает PRC=Close.

Для LIM мы используем те же формулы, что и для PER. Интересно заметить, что 'последняя' асимметрия (из всех) намного выше +1.96, но не для лучших 100 (=0.38) или 500 (=0.46). Поэтому мы будем использовать только 500 лучших. Среднее значение лучших 500 составляет 16.65, и StdDev — 3.03.  Конечно, LIM в основном зависит от PER: чем меньше PER , тем больше LIM , и наоборот. Именно поэтому диапазон LIM соответствует диапазону PER.

Поэтому мы выберем 500 лучших результатов для диапазонов наших трех переменных: PER, PRC, и LIM :

  • PER Avg=20.18 +/- StdDev=5.51 Skew=0.35 (-2) => (20.18-5.41-2=) 14,..,(20.18+5.52=) 26 (Step 1 => 13).
  • PRC согласно 500 ряду мы можем выбрать только для close (Step 0 => 1).
  • LIM Avg=16.64 +/- StdDev=3.03 Skew=0.46 (-2) => (16.64-3.03-2=) 12,..,(16.64+3.03=) 20 (Step 1 => 9)

В целом у нас сейчас только 13*1*9 = 117 комбинаций для оптимизации эксперта.

Рассмотрим результаты подробнее (вкладка: "OPT Top 500 Best PER's Average"):

Рис. 19 OPT Top 500 Best PER's Average

Мы видим, что значение PER=18 чаще всего встречается среди лучших 500, и у PER=22 имеется самое большое среднее значение. Оба они, в том числе их LIM, охватываются нашей выборкой.


Визуальный режим

Наконец, проверим PER с наилучшим средним значением из топа 500: PER=22. Отменяя выбор PRC=Open,Low,High, мы находим эти настройки с отношением диапазона 4.48 в 38 ряду (желтый фон на предыдущей таблице).

Мы запускаем псевдоэксперт в визуальном режиме с этими параметрами, и используем ADX с теми же параметрами.

В визуальном режиме наш псевдоэксперт размещает синюю стрелку направо/налево на следующий бар, где был обнаружен флет, и красную стрелку вверх/вниз в случае трендового бара (тут от: ср. 2015.07.30 05:00 до вт. 2015.08.04 12:00):

Рис. 20 VisualMode Per 22

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

  1. ΑDX запаздывает, в особенности при обнаружении флета, если значения индикатора ADX подскочили после крупных движений. Для того, чтобы 'успокоиться', потребуется достаточно много времени. Было бы лучше, если бы флет был обнаружен 2015.08.03 в 00:00, а не 2015.08.3 в 09:00.
  2. Если ADX находится близко к LIM , мы держим в уме возможность получения ложных сигналов. Например, было бы лучше, если бы была возможность не обнаружить тренд 2015.08.03 в 14:00.
  3. Если диапазон баров high-low становится меньше, даже пара 'небольших' баров в одном и том же направлении распознается как новый тренд. Вместо нового тренда 2015.08.03 в 20.00 было бы лучше если бы тренд был обнаружен позже, ориентировочно около 2015.08.04 в 07:00.
  4. Представленный псевдоэксперт не видит отличий между восходящим и нисходящим трендами. От вас зависит, будете ли вы использовать, например, DI+ и DI-индикатора ADX, или другие индикаторы.
  5. Возможно, средняя длина трендовых рынков (46.76, а это четыре дня) может оказаться слишком длинной. В этом случае, если SwMin больше (вместо 100) или SwCoeff меньше (вместо 0.01), или в обоих случаях, Вы получите результаты, которые больше соответствуют вашим пожеланиям.
Эти пять пунктов могут послужить вам отправными точками, чтобы найти или написать код для собственного индикатора с целью лучшего определения флетового или трендового рынка. Прикрепленный псевдоэксперт можно легко подправить, если у вас есть собственное представление о диагностике флета или тренда на рынке.



Заключение

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

  1. ADX способен различать, флетовый или трендовый рынок сложился к настоящему времени. 
  2. Мы смогли уменьшить количество значимых комбинаций с 54,201 всего лишь до 117 (= 13 (PER) * 1 (PRC) * 9 (LIM)).
  3. Диапазон флета колеблется между 372 и 220 пунктами (топ-100).
  4. Диапазон тренда находится между 1,277 и 782 пунктами.

Следовательно, мы можем уменьшить первоначальные комбинации эксперта в размере 2,168,040,000 до (117*40,000=) 4,680,000. Это составляет лишь 0.21%, что на 99.7% быстрее или намного лучше в случае генетической оптимизации, потому что большее число вариантов других параметров, не имеющих отношения к ADX, будут проверены. Сокращенные настройки нашего псевдоэксперта:

Рис. 21 StratTester-Setup EA Options reduced

Рис. 22 StratTester-Setup Optimizations reduced

Кроме того, мы получаем ценную информацию о критериях для начала торговли ее завершения и установки/сдвига стопов и целей.

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

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

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

  1. Убедиться, что файл cache удален из ..\tester\caches.
  2. Если вам необходим csv-файл в ..\tester\files\, то введите имя файла для fName и удалите существующий csv-файл с этим именем.
  3. Если вам не нужен csv-файл, то оставьте fName в параметрах псевдо-эксперта пустым.
  4. Если оставить название файла для csv-файла, то оптимизатор будет добавлять строку за строкой, до тех пор, пока не начнутся проблемы с его размером.
  5. Смените "Оптимизированные параметры" на "Custom"" во вкладке "Тестер".
  6. Самый простой способ повлиять на OptVal и результаты генетического алгоритма заключается в изменении минимума трех коэффициентов: SwMin, BrMin, RgMin.
  7. Установите "Модель" на "Только цены открытия", так будет быстрее.
  8. Если вы используете различные даты ("Use Date" : From..To), необходимо будет настроить коэффициенты прямо над функцией calcOptVal() внутри самого псевдоэксперта.
  9. После завершения оптимизации выберите настройки из вкладки "Результат оптимизации" и начните снова в "Визуальном режиме" , чтобы убедиться, что оптимизация реализовала ваши идеи.
  10. Синяя стрелка направо/налево обозначает флет, красная стрелка вверх-вниз — тренд.
  11. Если вы хотите создать лучшую альтернативу индикатору ADX, csv-файл может не понадобиться: просто оптимизируйте, отслеживайте лучшие результаты в "визуальном режиме", вносите изменения, снова оптимизируйте — и так далее, пока не добьетесь необходимого результата.
  12. По вопросам к рынку, не относящимся ко флету и тренду, необходимо использовать csv-файлы, и, возможно, другие способы для расчета OptVal.

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