Раскладываем входы по индикаторам

Метатрейдер 5 как работать с ним

Введение

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

1. Постановка задачи

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

2. Первое тестирование анализируемого советника

Для репрезентативности результатов увеличим период тестирования в восемь раз. Безо всякой оптимизации установим период для построения авторегрессионной функции в 3120 баров (около 3 месяцев) и запустим тест.

Test1_1__1Test1_2__1

По результатам тестирования мы получили явно убыточный график баланса, где после 1 — 2 прибыльных сделок идет ряд убыточных. В целом, доля прибыльных сделок составила чуть меньше 34%. Хотя и средний размер прибыли превышает средний размер убытка на 45%, этого недостаточно для получения прибыли на протяжении всего периода тестирования.

Test1_3

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

EURUSDH1__1

Прежде всего, надо сохранить отчет тестирования для последующей обработки. Но тут есть один нюанс: из соображений безопасности в языке MQL5 строго контролируется работа с файлами. Файлы, с которыми мы проводим операции средствами языка MQL5, должны находиться внутри файловой “песочницы”. Поэтому в нее и нужно сохранить отчет. Но поскольку мы будем запускать программу в Тестере стратегий, надо учесть еще и то, что каждый агент работает в своей “песочнице”. Следовательно, чтобы при тестировании на любом агенте программа могла получить доступ к отчету, мы сохраним его в общей папке терминалов.

Чтобы узнать путь к общей папке клиентских терминалов, откроем в MetaEditor меню “File” и выберем подменю “Open Common Data Folder”.

Path_1__1

В открывшемся окне перейдем в папку “Files”.

Path_2__1

Затем полностью скопируем строку пути в буфер обмена нажатием клавиш “Ctrl+C”.

Path_3

Путь в “песочницу” мы знаем, и теперь можем сохранить наш отчет тестирования. Для этого в “Strategy Tester” выбираем вкладку “Result” и в любом ее месте нажимаем на правую клавишу мыши. В появившемся меню выбираем “Report” -> “HTML (Internet Explorer)”.

Save_1

После выполнения этих операций откроется системное окно для сохранения файла. Сначала в поле ввода имени файла вставляем путь в нашу “песочницу” и нажимаем “Сохранить”. Эта операция изменит папку для сохранения файла.

Save_2__1

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

Save_3__1

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

3. Создаем массив сделок

3.1. Общее представление о парсинге

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

Deals

По существу, html-файл представляет собой текст, разделенный тэгами, описывающими его форматирование и дизайн. Открыв отчет в текстовом редакторе, вы легко можете найти в нем 2 тега “<table>”, что говорит нам о разделении всех данных в отчете на 2 таблицы данных. Информация по сделкам находится во 2-ой таблице. В ее начале содержится информация об ордерах, а затем — информация о сделках.

Report_HTML

Строки таблицы выделяются тэгами “<tr>…</tr>”. Внутри строк информация разделяется на ячейки тэгами “<td>…</td>”.

3.2. Класс для сохранения информации о сделке

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

  • время открытия позиции;
  • объем открытия позиции;
  • направление сделки;
  • объем закрытия позиции;
  • сумма комиссии;
  • сумма свопа;
  • сумма прибыли.

Мы определились с основными аспектами этого этапа работы. Начнем писать код. Сначала создадим класс сделки CDeal.

class CDeal       :  public CObject
  {
private:
   datetime          OpenTime;         // Time of open position
   double            OpenedVolume;     // Volume of opened position
   ENUM_POSITION_TYPE Direct;          // Direct of opened position
   double            ClosedVolume;     // Closed volume
   double            Comission;        // Comission to position
   double            Swap;             // Swap of position
   double            Profit;           // Profit of position
   
public:
                     CDeal();
                    ~CDeal();
  };

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

CDeal::CDeal(ENUM_POSITION_TYPE type,datetime time,double volume,double comission=0.0)  : ClosedVolume(0),
                                                                                          Swap(0),
                                                                                          Profit(0)
  {
   OpenTime      = time;
   OpenedVolume  = volume;
   Direct        = type;
   Comission     = comission;
  }

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

bool CDeal::IsClosed(double &opened_volume)
  {
   opened_volume=OpenedVolume-ClosedVolume;
   return (opened_volume<=0);
  }

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

bool CDeal::IsClosed(void)
  {
   double opened_volume;
   return IsClosed(opened_volume);
  }

Чтобы правильно закрыть сделку, нам нужно знать ее тип. Создадим функцию GetType, которая будет возвращать значение private переменной Direct. Функция достаточно короткая, поэтому ее можно прописать в теле класса.

ENUM_POSITION_TYPE Type(void) {  return Direct; }

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

bool CDeal::Close(double volume,double profit,double comission=0.0,double swap=0.0)
  {
   if((OpenedVolume-ClosedVolume)<volume)
      return false;
   ClosedVolume   += volume;
   Profit         += profit;
   Comission      += comission;
   Swap           += swap;
   return true;
  }

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

double CDeal::GetProfit(void)
  {
   return (Comission+Swap+Profit);
  }

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

datetime          GetTime(void)  {  return OpenTime;  }

3.3. Класс парсинга отчета

После создания класса для хранения информации о каждой сделке приступим непосредственно к парсингу отчета. Для этого создадим класс CParsing. В классе мы объявим:

  • объект класса CArrayObj — для хранения массива сделок;
  • объект класса CFileTxt — для работы с файлом отчета;
  • переменную типа string — для хранения наименования инструмента.

Помимо функций инициализации и деинициализации, в классе будут еще две функции:

  • ReadFile — непосредственно для парсинга;
  • GetSymbol — для возврата наименования инструмента по требованию.
class CParsing
  {
private:
   CArrayObj        *car_Deals;     //Array of deals
   CFileTxt         *c_File;        //File to parsing
   
   string            s_Symbol;      //Symbol of deals
   
public:
                     CParsing(CArrayObj *&array);
                    ~CParsing();
                    
   bool              ReadFile(string file_name);
   string            GetSymbol(void)   {  return s_Symbol;  }
  };

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

CParsing::CParsing(CArrayObj *&array)  :  s_Symbol(NULL)
  {
   if(CheckPointer(array)==POINTER_INVALID)
     {
      array=new CArrayObj();
     }
   car_Deals=array;
  }

В функцию деинициализации запишем удаление объекта класса CFileTxt. Закрытие файла прописано в функции деинициализации родительского класса CFile, не будем его здесь приводить.

CParsing::~CParsing()
  {
   if(CheckPointer(c_File)!=POINTER_INVALID)
      delete c_File;
  }

Перейдем непосредственно к парсингу. При вызове функции парсинга ReadFile в параметрах передаем имя файла отчета. Первое, что мы делаем в функции — проверяем, не является ли пустым переданный параметр. Также проверяем наличие массива для сохранения информации о сделках. В случае неудовлетворения хотя бы одного из условий прекращаем выполнение функции и возвращаем false.

bool CParsing::ReadFile(string file_name)
  {
   //---
   if(file_name==NULL || file_name=="" || CheckPointer(car_Deals)==POINTER_INVALID)
      return false;

Затем инициализируем объект класса CFileTxt и пытаемся открыть файл, переданный в параметре функции. При возникновении ошибки выходим из функции с результатом false.

   if(CheckPointer(c_File)==POINTER_INVALID)
     {
      c_File=new CFileTxt();
      if(CheckPointer(c_File)==POINTER_INVALID)
         return false;
     }
   //---
   if(c_File.Open(file_name,FILE_READ|FILE_COMMON)<=0)
      return false;

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

   string html_report=NULL;
   while(!c_File.IsEnding())
      html_report+=c_File.ReadString();
   c_File.Close();
   if(html_report==NULL || html_report=="")
      return false;

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

   string delimiter  =  NULL;
   ushort separate   =  0;
   for(uchar tr=1;tr<255;tr++)
     {
      string temp =  CharToString(tr);
      if(StringFind(html_report,temp,0)>0)
         continue;
      delimiter   =  temp;
      separate    =  tr;
      break;
     }
   if(delimiter==NULL)
      return false;

Как уже говорилось выше, в структуре html-файла таблицы закрываются тегом “</table>”. Заменим этот тег нашим разделителем и разделим весь отчет на строки по нему. Так мы выделим требуемую таблицу в отдельную строку.

   if(StringReplace(html_report,"</table>",delimiter)<=0)
      return false;
   //---
   s_Symbol=NULL;
   car_Deals.Clear();
   //---
   string html_tables[];
   int size=StringSplit(html_report,separate,html_tables);
   if(size<=1)
      return false;

Повторив эту процедуру с тегом “</tr>”, мы разобьем таблицу на строки.

   if(StringReplace(html_tables[size-2],"</tr>",delimiter)<=0)
      return false;
   size=StringSplit(html_tables[size-2],separate,html_tables);
   if(size<=1)
      return false;

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

   bool found_start=false;
   double opened_volume=0;
   for(int i=0;i<size;i++)
     {
      //---
      if(!found_start)
        {
         if(StringFind(html_tables[i],"Deals",0)>=0)
            found_start=true;
         continue;
        }

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

      string columns[];
      int temp=StringFind(html_tables[i],"<td>",0);
      if(temp<0)
         continue;
      if(temp>0)
         html_tables[i]=StringSubstr(html_tables[i],temp);
      StringReplace(html_tables[i],"<td>","");
      StringReplace(html_tables[i],"</td>",delimiter);
      temp=StringSplit(html_tables[i],separate,columns);
      if(temp<13)
         continue;
      //---
      ENUM_POSITION_TYPE   e_direction =  (ENUM_POSITION_TYPE)(columns[3]=="buy" ? POSITION_TYPE_BUY : columns[3]=="sell" ?
 POSITION_TYPE_SELL : -1);
      if(e_direction==-1)
         continue;
      //---
      datetime             dt_time     =  StringToTime(columns[0]);
      StringReplace(columns[5]," ","");
      double               d_volume    =  StringToDouble(columns[5]);
      StringReplace(columns[8]," ","");
      double               d_comission =  StringToDouble(columns[8]);
      StringReplace(columns[9]," ","");
      double               d_swap      =  StringToDouble(columns[9]);
      StringReplace(columns[10]," ","");
      double               d_profit    =  StringToDouble(columns[10]);
      if(s_Symbol==NULL || s_Symbol=="")
        {
         s_Symbol=columns[2];
         StringTrimLeft(s_Symbol);
         StringTrimRight(s_Symbol);
        }

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

      if(opened_volume>0 && StringFind(columns[4],"out",0)>=0)
        {
         int total=car_Deals.Total();
         double total_volume=MathMin(opened_volume,d_volume);
         for(int d=0;(d<total && e_direction!=(-1) && total_volume>0);d++)
           {
            CDeal *deal=car_Deals.At(d);
            if(CheckPointer(deal)==POINTER_INVALID)
               continue;
            //---
            if(deal.Type()==e_direction)
               continue;
            //---
            double deal_unclosed=0;
            if(deal.IsClosed(deal_unclosed))
               continue;
            double close_volume     =  MathMin(deal_unclosed,total_volume);
            double close_comission  =  d_comission/d_volume*close_volume;
            double close_swap       =  d_swap/total_volume*close_volume;
            double close_profit     =  d_profit/total_volume*close_volume;
            if(deal.Close(close_volume,close_profit,close_comission,close_swap))
              {
               opened_volume  -= close_volume;
               d_volume       -= close_volume;
               total_volume   -= close_volume;
               d_comission    -= close_comission;
               d_swap         -= close_swap;
               d_profit       -= close_profit;
              }
           }
        }

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

      if(d_volume>0 && StringFind(columns[4],"in",0)>=0)
        {
         CDeal *deal = new CDeal(e_direction,dt_time,d_volume,d_comission);
         if(CheckPointer(deal)==POINTER_INVALID)
            return false;
         if(!car_Deals.Add(deal))
            return false;
         opened_volume  += d_volume;
        }
     }

Если была сохранена хотя бы одна сделка, функция вернет в конце true, в противном случае — false.

   return (car_Deals.Total()>0);
  }

Переходим к следующему этапу работы.

4. Подготовка классов для работы с индикаторами

Как мы уже говорили ранее, одна из наших задач состоит в том, чтобы отсеять убыточные сделки при отсутствии ярко выраженного тренда. Вопрос определения тренда поднимается регулярно, в том числе и на этом сайте (например, статьи [3] и [4]). Я не претендую на открытие каких-то неординарных методов определения тренда. Хочу лишь предложить технологию сопоставления совершенных сделок и показаний индикаторов для последующего анализа и осознанной оптимизации торговых систем. Поэтому рассмотрим здесь самые распространенные индикаторы, которые уже есть в стандартной поставке терминала.

4.1. Класс для подключения индикатора ATR

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

Рассматриваемый индикатор — типичный для своего класса индикатор с одним буфером. Поэтому и нам имеет смысл сделать единый класс для работы с подобными индикаторами.

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

Первый класс назовем CValue. Он будет содержать 3 private переменные для хранения информации о значении индикатора (Value), отношение двух последних значений индикатора (Dinamic) и номер тикета ордера, для которого сохранялись значения (Deal_Ticket). Номер тикета нам потребуется для последующего сопоставления значений индикаторов с ордерами при анализе. Все требуемые для сохранения значения в экземпляр класса будут передаваться при его инициализации. А для извлечения требуемой информации создадим функции GetTicket, GetValue и GetDinamic, которые будут возвращать значения соответствующих переменных. Дополнительно создадим функцию GetValues, которая будет одновременно возвращать значение индикатора и его динамику.

class CValue       : public CObject
  {
private:
   double            Value;            //Indicator's value
   double            Dinamic;          //Dinamics value of indicator
   long              Deal_Ticket;      //Ticket of deal 
   
public:
                     CValue(double value, double dinamic, long ticket);
                    ~CValue(void);
   //---
   long              GetTicket(void)   {  return Deal_Ticket;  }
   double            GetValue(void)    {  return Value;        }
   double            GetDinamic(void)  {  return Dinamic;      }
   void              GetValues(double &value, double &dinamic);
  };

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

class COneBufferArray   :  CObject
  {
private:
   CArrayObj        *IndicatorValues;     //Array of indicator's values
   
   int               i_handle;            //Handle of indicator
   string            s_Name;
   string            GetIndicatorName(int handle);
   
public:
                     COneBufferArray(int handle);
                    ~COneBufferArray();
   //---
   bool              SaveNewValues(long ticket);
   //---
   double            GetValue(long ticket);
   double            GetDinamic(long ticket);
   bool              GetValues(long ticket, double &value, double &dinamic);
   int               GetIndyHandle(void)  {  return i_handle;     }
   string            GetName(void)        {  return (s_Name!= NULL ? s_Name : "...");       }
  };

Для сохранения данных по внешнему запросу создадим функцию SaveNewValues, которая будет содержать только один параметр — тикет ордера. В начале функции мы проверим состояние массивов для хранения данных и хэндла индикатора. В случае ошибки функция вернет значение false.

bool COneBufferArray::SaveNewValues(long ticket)
  {
   if(CheckPointer(IndicatorValues)==POINTER_INVALID)
      return false;
   if(i_handle==INVALID_HANDLE)
      return false;

После этого получим данные индикатора. Если не удастся загрузить значения индикатора, функция вернет false.

   double ind_buffer[];
   if(CopyBuffer(i_handle,0,1,2,ind_buffer)<2)
      return false;

На следующем шаге создадим экземпляр класса CValue и передадим в него требуемые значения. При ошибке создания экземпляра класса функция вернет false.

   CValue *object=new CValue(ind_buffer[1], (ind_buffer[0]!=0 ? ind_buffer[1]/ind_buffer[0] : 1), ticket);
   if(CheckPointer(object)==POINTER_INVALID)
      return false;

В случае, если класс еще не знает наименование индикатора, получим его с графика, вызвав функцию GetIndicatorName (код функции приведен во вложении).

   if(s_Name==NULL)
      s_Name=GetIndicatorName(i_handle);

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

   return IndicatorValues.Add(object);
  }

Для возврата данных из массива по запросу создадим функции GetValue, GetDinamic и GetValues, которые будут возвращать требуемые значения по номеру тикета ордера.

С полным кодом классов можно ознакомиться в приложении.

Этот класс я применил для сбора данных по индикаторам CCI, Объемов, Силы, осциллятора Чайкина и стандартного отклонения.

4.2. Класс для подключения индикатора MACD

Добавим в нашу коллекцию еще один стандартный индикатор — MACD. Как известно, он используется для определения силы и направления тренда.

В отличие от ранее рассмотренных индикаторов, MACD имеет 2 индикаторных буфера (Main и Signal). Следовательно, и сохранять мы будем информацию о двух линиях. Используя алгоритм, приведенный для индикаторов выше, код класса для хранения данных примет следующий вид:

class CMACDValue      : public CObject
  {
private:
   double            Main_Value;        //Main line value
   double            Main_Dinamic;      //Dinamics value of main lime
   double            Signal_Value;      //Signal line value
   double            Signal_Dinamic;    //Dinamics value of signal lime
   long              Deal_Ticket;       //Ticket of deal 
   
public:
                     CMACDValue(double main_value, double main_dinamic, double signal_value, double signal_dinamic, long ticket);
                    ~CMACDValue(void);
   //---
   long              GetTicket(void)         {  return Deal_Ticket;     }
   double            GetMainValue(void)      {  return Main_Value;      }
   double            GetMainDinamic(void)    {  return Main_Dinamic;    }
   double            GetSignalValue(void)    {  return Signal_Value;    }
   double            GetSignalDinamic(void)  {  return Signal_Dinamic;  }
   void              GetValues(double &main_value, double &main_dinamic, double &signal_value, double &signal_dinamic);
  };

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

class CMACD
  {
private:
   CArrayObj        *IndicatorValues;     //Array of indicator's values
   
   int               i_handle;            //Handle of indicator
   
public:
                     CMACD(string symbol, ENUM_TIMEFRAMES timeframe, uint fast_ema, uint slow_ema, uint signal, ENUM_APPLIED_PRICE applied_price);
                    ~CMACD();
   //---
   bool              SaveNewValues(long ticket);
   //---
   double            GetMainValue(long ticket);
   double            GetMainDinamic(long ticket);
   double            GetSignalValue(long ticket);
   double            GetSignalDinamic(long ticket);
   bool              GetValues(long ticket, double &main_value, double &main_dinamic, double &signal_value, double &signal_dinamic);                
  };

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

bool CMACD::SaveNewValues(long ticket)
  {
   if(CheckPointer(IndicatorValues)==POINTER_INVALID)
      return false;
   if(i_handle==INVALID_HANDLE)
      return false;
   double main[], signal[];
   if(!CopyBuffer(i_handle,0,1,2,main)<2 || !CopyBuffer(i_handle,1,1,2,signal)<2)
      return false;
   CMACDValue *object=new CMACDValue(main[1], (main[0]!=0 ? main[1]/main[0] : 1), signal[1], (signal[0]!=0 ? signal[1]/signal[0] : 1), ticket);
   if(CheckPointer(object)==POINTER_INVALID)
      return false;
   return IndicatorValues.Add(object);
  }

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

Для закрепления материала, если можно так сказать, приведу еще пример сохранения данных индикатора с 3 буферами данных.

4.3. Класс для подключения индикатора ADX

Индикатор ADX широко используется для определения силы и направления тренда. Он соответствует нашей задаче и заслуженно добавляется в нашу “копилку”.

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

class CADXValue      : public CObject
  {
private:
   double            ADX_Value;        //ADX value
   double            ADX_Dinamic;      //Dinamics value of ADX
   double            PDI_Value;        //+DI value
   double            PDI_Dinamic;      //Dinamics value of +DI
   double            NDI_Value;        //-DIvalue
   double            NDI_Dinamic;      //Dinamics value of -DI
   long              Deal_Ticket;      //Ticket of deal 
   
public:
                     CADXValue(double adx_value, double adx_dinamic, double pdi_value, double pdi_dinamic, double ndi_value, double ndi_dinamic, long ticket);
                    ~CADXValue(void);
   //---
   long              GetTicket(void)         {  return Deal_Ticket;     }
   double            GetADXValue(void)       {  return ADX_Value;       }
   double            GetADXDinamic(void)     {  return ADX_Dinamic;     }
   double            GetPDIValue(void)       {  return PDI_Value;       }
   double            GetPDIDinamic(void)     {  return PDI_Dinamic;     }
   double            GetNDIValue(void)       {  return NDI_Value;       }
   double            GetNDIDinamic(void)     {  return NDI_Dinamic;     }
   void              GetValues(double &adx_value, double &adx_dinamic, double &pdi_value, double &pdi_dinamic, double &ndi_value, double &ndi_dinamic);
  };

Увеличение хранимых данных повлечет и изменения в классе работы с массивом.

class CADX
  {
private:
   CArrayObj        *IndicatorValues;     //Array of indicator's values
   
   int               i_handle;            //Handle of indicator
   
public:
                     CADX(string symbol, ENUM_TIMEFRAMES timeframe, uint period);
                    ~CADX();
   //---
   bool              SaveNewValues(long ticket);
   //---
   double            GetADXValue(long ticket);
   double            GetADXDinamic(long ticket);
   double            GetPDIValue(long ticket);
   double            GetPDIDinamic(long ticket);
   double            GetNDIValue(long ticket);
   double            GetNDIDinamic(long ticket);
   bool              GetValues(long ticket,double &adx_value,double &adx_dinamic,double &pdi_value,double &pdi_dinamic,double &ndi_value,double &ndi_dinamic);
  };
bool CADX::SaveNewValues(long ticket)
  {
   if(CheckPointer(IndicatorValues)==POINTER_INVALID)
      return false;
   if(i_handle==INVALID_HANDLE)
      return false;
   double adx[], pdi[], ndi[];
   if(!CopyBuffer(i_handle,0,1,2,adx)<2 || !CopyBuffer(i_handle,1,1,2,pdi)<2 || !CopyBuffer(i_handle,1,1,2,ndi)<2)
      return false;
   CADXValue *object=new CADXValue(adx[1], (adx[0]!=0 ? adx[1]/adx[0] : 1), pdi[1], (pdi[0]!=0 ? pdi[1]/pdi[0] : 1), ndi[1], (ndi[0]!=0 ? ndi[1]/ndi[0] : 1), ticket);
   if(CheckPointer(object)==POINTER_INVALID)
      return false;
   return IndicatorValues.Add(object);
  }

Думаю, теперь всем понятен принцип построения классов для работы с индикаторами. Поэтому не будем расписывать код для следующих индикаторов, чтобы сэкономить объем статьи. Подобным образом в “копилку” для анализа я добавил индикаторы BW MFI и Alligator. Все желающие могут ознакомиться с полным кодом классов во вложении.

5. Подготавливаем формы отчетов для вывода результатов

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

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

Сначала создадим классы, которые будут подготавливать информацию по каждому индикатору.

5.1. Универсальный класс однобуферных индикаторов

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

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

Для отрисовки графиков создадим класс CStaticOneBuffer. Этот класс будет содержать ссылку на массив сохраненных данных DataArray, массив значений индикатора Value с заданным шагом d_Step, а также два массива суммарной прибыли отдельно для длинных и коротких позиций. Обратите внимание: массивы для подсчета суммарной прибыли будут двумерными. Размер первого измерения будет соответствовать размеру массива Value. Второе измерение будет содержать три элемента: первый — для падающей динамики индикатора, второй — для горизонтального движения индикатора и третий — для возрастающего движения.

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

class CStaticOneBuffer  :  CObject
  {
private:
   COneBufferArray  *DataArray;
   
   double            d_Step;                    //Step in values Array
   double            Value[];                   //Array of values
   double            Long_Profit[][3];          //Array of long trades profit, direct -> DOWN-0, EQUAL-1, UP-2
   double            Short_Profit[][3];         //Array of short trades profit, direct -> DOWN-0, EQUAL-1, UP-2
   
   bool              AdValues(double value, double dinamic, double profit, ENUM_POSITION_TYPE type);
   int               GetIndex(double value);
   bool              Sort(void);
   
public:
                     CStaticOneBuffer(COneBufferArray *data, double step);
                    ~CStaticOneBuffer();
   bool              Ad(long ticket, double profit, ENUM_POSITION_TYPE type);
   string            HTML_header(void);
   string            HTML_body(void);
  };

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

CStaticOneBuffer::CStaticOneBuffer(COneBufferArray *data,double step)
  {
   DataArray   =  data;
   d_Step      =  step;
   ArrayFree(Value);
   ArrayFree(Long_Profit);
   ArrayFree(Short_Profit);
  }

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

bool CStaticOneBuffer::Ad(long ticket,double profit,ENUM_POSITION_TYPE type)
  {
   if(CheckPointer(DataArray)==POINTER_INVALID)
      return false;

   double value, dinamic;
   if(!DataArray.GetValues(ticket,value,dinamic))
      return false;
   value = NormalizeDouble(value/d_Step,0)*d_Step;
   return AdValues(value,dinamic,profit,type);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CStaticOneBuffer::AdValues(double value,double dinamic,double profit,ENUM_POSITION_TYPE type)
  {
   int index=GetIndex(value);
   if(index<0)
      return false;
   
   switch(type)
     {
      case POSITION_TYPE_BUY:
        if(dinamic<1)
           Long_Profit[index,0]+=profit;
        else
           if(dinamic==1)
              Long_Profit[index,1]+=profit;
           else
              Long_Profit[index,2]+=profit;
        break;
      case POSITION_TYPE_SELL:
        if(dinamic<1)
           Short_Profit[index,0]+=profit;
        else
           if(dinamic==1)
              Short_Profit[index,1]+=profit;
           else
              Short_Profit[index,2]+=profit;
        break;
     }
   return true;
  }

Для визуализации графиков создадим функции HTML_header и HTML_body, в которых будут генерироваться куски кода заголовка и тела HTML-страницы. Принципы построения кода HTML-страницы подробно описаны в статье [2], не станем на этом останавливаться. Полный код функций приведен во вложении.

5.2. Класс для отображения данных индикатора Bill Williams MFI

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

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

class CStaticBWMFI  :  CObject
  {
private:
   CBWMFI           *DataArray;
   
   double            d_Step;                       //Step in values Array
   double            Value[];                      //Array of values
   double            Long_Profit[][3][4];          //Array of long trades profit, direct -> DOWN-0, EQUAL-1, UP-2
   double            Short_Profit[][3][4];         //Array of short trades profit, direct -> DOWN-0, EQUAL-1, UP-2
   
   bool              AdValues(double value, double _color, double dinamic, double profit, ENUM_POSITION_TYPE type);
   int               GetIndex(double value);
   bool              Sort(void);
   
public:
                     CStaticBWMFI(CBWMFI *data, double step);
                    ~CStaticBWMFI();
   bool              Ad(long ticket, double profit, ENUM_POSITION_TYPE type);
   string            HTML_header(void);
   string            HTML_body(void);
  };

С полным кодом класса можно ознакомиться во вложении.

5.3. Класс для отображения данных индикатора MACD

Далее рассмотрим индикатор MACD. Как известно, у него два буфера: гистограмма и сигнальная линия. По правилам трактовки сигналов этого индикатора, важно значение и направление движения гистограммы, а также положение сигнальной линии (выше или ниже гистограммы). Для всестороннего анализа нам потребуется создать целый ряд графиков.

  • Зависимость прибыльности сделок от значений гистограммы и ее направления (отдельно и комплексно).
  • Зависимость прибыльности сделок от значений сигнальной линии и ее направления.
  • Зависимость прибыли от положения сигнальной линии относительно гистограммы.
  • Зависимость прибыли от совместного влияния значений гистограммы, ее направления и положения сигнальной линии относительно гистограммы.
Для анализа данных создадим класс CStaticMACD. В построении класса будут применены те же принципы, что и при построении предыдущих статистических классов. Он будет иметь трехмерный массив статистики прибыли по значениям гистограммы, но, в отличие от предыдущего класса, третье измерение будет содержать 3 элемента по положению сигнальной линии относительно гистограммы (ниже, на уровне и выше). Также добавим еще один двухмерный массив для подсчета прибыли по значениям сигнальной линии.
class CStaticMACD  :  CObject
  {
private:
   CMACD            *DataArray;
   
   double            d_Step;                       //Step in values Array
   double            Value[];                      //Array of values
   double            SignalValue[];                //Array of values
   double            Long_Profit[][3][3];          //Array of long trades profit, direct -> DOWN-0, EQUAL-1, UP-2
   double            Short_Profit[][3][3];         //Array of short trades profit, direct -> DOWN-0, EQUAL-1, UP-2
   double            Signal_Long_Profit[][3];      //Array of long trades profit, direct -> DOWN-0, EQUAL-1, UP-2
   double            Signal_Short_Profit[][3];     //Array of short trades profit, direct -> DOWN-0, EQUAL-1, UP-2
   
   bool              AdValues(double main_value, double main_dinamic, double signal_value, double signal_dinamic, double profit, ENUM_POSITION_TYPE type);
   int               GetIndex(double value);
   int               GetSignalIndex(double value);
   bool              Sort(void);
   
public:
                     CStaticMACD(CMACD *data, double step);
                    ~CStaticMACD();
   bool              Ad(long ticket, double profit, ENUM_POSITION_TYPE type);
   string            HTML_header(void);
   string            HTML_body(void);
  };

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

5.4. Класс для отображения данных индикатора ADX

Следующим мы рассмотрим класс CStaticADX. Он будет собирать статистику по значениям индикатора ADX. Правила трактовки сигналов индикатора: линия +DI показывает силу положительного движения, -DI — силу отрицательного движения, а ADX — среднюю силу движения. Исходя из этих правил, мы и будем строить графики зависимостей:

  • зависимость прибыли от величины +DI, ее направления и положения относительно ADX;
  • зависимость прибыли от величины -DI, ее направления и положения относительно ADX.

Создавая класс для сбора статистики, я решил собрать немного больше данных. В результате мне потребовалось сохранять информацию о:

  • значении индикатора;
  • направлении линий;
  • положении относительно линии противоположного движения;
  • направлении лини противоположного движения;
  • положении относительно линии ADX;
  • направлении линии ADX.
При таком дроблении информации и подходе, используемом в предыдущих классах, мне понадобились шестимерные массивы. Но массивы такого размера не поддерживаются в MQL. Для решения этой задачи был создан вспомогательный класс CProfitData, в который будет сохраняться вся необходимая информация.
class CProfitData
  {
   public:
   double         Value;
   double         LongProfit[3]/*UppositePosition*/[3]/*Upposite Direct*/[3]/*ADX position*/[3]/*ADX direct*/;
   double         ShortProfit[3]/*UppositePosition*/[3]/*Upposite Direct*/[3]/*ADX position*/[3]/*ADX direct*/;
   
                  CProfitData(void) 
                  {  ArrayInitialize(LongProfit,0); ArrayInitialize(ShortProfit,0);  }
                 ~CProfitData(void) {};
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CStaticADX  :  CObject
  {
private:
   CADX             *DataArray;
   
   double            d_Step;           //Step in values Array
   CProfitData      *PDI[][3];         //Array of values +DI
   CProfitData      *NDI[][3];         //Array of values -DI
   
   bool              AdValues(double adx_value, double adx_dinamic, double pdi_value, double pdi_dinamic, double ndi_value, double ndi_dinamic, double profit, ENUM_POSITION_TYPE type);
   int               GetPDIIndex(double value);
   int               GetNDIIndex(double value);
   bool              Sort(void);
   
public:
                     CStaticADX(CADX *data, double step);
                    ~CStaticADX();
   bool              Ad(long ticket, double profit, ENUM_POSITION_TYPE type);
   string            HTML_header(void);
   string            HTML_body(void);
  };

В остальном были сохранены подходы и принципы построения из предыдущих классов. С полным кодом класса можно ознакомиться во вложении.

5.5. Класс для отображения данных индикатора Alligator

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

Чтобы конкретизировать сигналы индикатора, введем определение тренда по положению линий. Если линия LIPS выше TEETH, а TEETH выше JAW — считаем BUY трендом. Если LIPS ниже TEETH, а TEETH ниже JAW — считаем SELL трендом. В случае отсутствия строгого порядка линий считаем тренд неопределенным или FLAT.

Соответственно, и графики зависимости будут строиться от сигналов направления тренда и динамики линий индикатора.

Следуя вышеприведенным вводным, создадим класс CStaticAlligator. Принципы построения класса унаследованы от предыдущих классов.

class CStaticAlligator  :  CObject
  {
private:
   CAlligator             *DataArray;
   
   double            Long_Profit[3]/*Signal*/[3]/*JAW direct*/[3]/*TEETH direct*/[3]/*LIPS direct*/;  //Array of long deals profit
   double            Short_Profit[3]/*Signal*/[3]/*JAW direct*/[3]/*TEETH direct*/[3]/*LIPS direct*/; //Array of short feals profit
   
   bool              AdValues(double jaw_value, double jaw_dinamic, double teeth_value, double teeth_dinamic, double lips_value, double lips_dinamic, double profit, ENUM_POSITION_TYPE type);
   
public:
                     CStaticAlligator(CAlligator *data);
                    ~CStaticAlligator();
   bool              Ad(long ticket, double profit, ENUM_POSITION_TYPE type);
   string            HTML_header(void);
   string            HTML_body(void);
  };

С полным кодом класса можно ознакомится во вложении.

6. Строим советник для сбора и анализа информации

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

input string            FileName          =  "Kalman_test.html"   ;
input ENUM_TIMEFRAMES   Timefarame        =  PERIOD_CURRENT       ;
input string            s1                =  "ADX"                ;  //---
input uint              ADX_Period        =  14                   ;
input string            s2                =  "Alligator"          ;  //---
input uint              JAW_Period        =  13                   ;
input uint              JAW_Shift         =  8                    ;
input uint              TEETH_Period      =  8                    ;
input uint              TEETH_Shift       =  5                    ;
input uint              LIPS_Period       =  5                    ;
input uint              LIPS_Shift        =  3                    ;
input ENUM_MA_METHOD    Alligator_Method  =  MODE_SMMA            ;
input ENUM_APPLIED_PRICE Alligator_Price  =  PRICE_MEDIAN         ;
input string            s3                =  "ATR"                ;  //---
input uint              ATR_Period        =  14                   ;
input string            s4                =  "BW MFI"             ;  //---
input ENUM_APPLIED_VOLUME BWMFI_Volume    =  VOLUME_TICK          ;
input string            s5                =  "CCI"                ;  //---
input uint              CCI_Period        =  14                   ;
input ENUM_APPLIED_PRICE CCI_Price        =  PRICE_TYPICAL        ;
input string            s6                =  "Chaikin"            ;  //---
input uint              Ch_Fast_Period    =  3                    ;
input uint              Ch_Slow_Period    =  14                   ;
input ENUM_MA_METHOD    Ch_Method         =  MODE_EMA             ;
input ENUM_APPLIED_VOLUME Ch_Volume       =  VOLUME_TICK          ;
input string            s7                =  "Force Index"        ;  //---
input uint              Force_Period      =  14                   ;
input ENUM_MA_METHOD    Force_Method      =  MODE_SMA             ;
input ENUM_APPLIED_VOLUME Force_Volume    =  VOLUME_TICK          ;
input string            s8                =  "MACD"               ;  //---
input uint              MACD_Fast         =  12                   ;
input uint              MACD_Slow         =  26                   ;
input uint              MACD_Signal       =  9                    ;
input ENUM_APPLIED_PRICE MACD_Price       =  PRICE_CLOSE          ;
input string            s9                =  "Standart Deviation" ;  //---
input uint              StdDev_Period     =  14                   ;
input uint              StdDev_Shift      =  0                    ;
input ENUM_MA_METHOD    StdDev_Method     =  MODE_SMA             ;
input ENUM_APPLIED_PRICE StdDev_Price     =  PRICE_CLOSE          ;
input string            s10               =  "Volumes"            ;  //---
input ENUM_APPLIED_VOLUME Applied_Volume  =  VOLUME_TICK          ;

Затем объявим экземпляры всех описанных выше классов.

CArrayObj         *Deals;
CADX              *ADX;
CAlligator        *Alligator;
COneBufferArray   *ATR;
CBWMFI            *BWMFI;
COneBufferArray   *CCI;
COneBufferArray   *Chaikin;
COneBufferArray   *Force;
CMACD             *MACD;
COneBufferArray   *StdDev;
COneBufferArray   *Volume;
CStaticOneBuffer  *IndicatorsStatic[];
CStaticBWMFI      *BWMFI_Stat;
CStaticMACD       *MACD_Stat;
CStaticADX        *ADX_Stat;
CStaticAlligator  *Alligator_Stat;

6.1. Функция инициализации советника

 

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

int OnInit()
  {
//---
   if(!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION))
      return INIT_FAILED;

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

   CParsing *Parsing =  new CParsing(Deals);
   if(CheckPointer(Parsing)==POINTER_INVALID)
      return INIT_FAILED;
   if(!Parsing.ReadFile(FileName) || CheckPointer(Deals)==POINTER_INVALID || Deals.Total()<=0)
     {
      delete Parsing;
      return INIT_FAILED;
     }
   delete Parsing;

После этого проведем инициализацию индикаторных классов.

//---
   ADX =  new CADX(_Symbol,Timefarame,ADX_Period);
   if(CheckPointer(ADX)==POINTER_INVALID)
      return INIT_FAILED;
//---
   Alligator =  new CAlligator(_Symbol,Timefarame,JAW_Period,JAW_Shift,TEETH_Period,TEETH_Shift,LIPS_Period,LIPS_Shift,Alligator_Method,Alligator_Price);
   if(CheckPointer(Alligator)==POINTER_INVALID)
      return INIT_FAILED;
//---
   int handle=iATR(_Symbol,Timefarame,ATR_Period);
   if(handle>0)
     {
      ATR      =  new COneBufferArray(handle);
      if(CheckPointer(ATR)==POINTER_INVALID)
         return INIT_FAILED;
     }
//---
   BWMFI    =  new CBWMFI(_Symbol,Timefarame,BWMFI_Volume);
   if(CheckPointer(BWMFI)==POINTER_INVALID)
      return INIT_FAILED;
//---
   handle=iCCI(_Symbol,Timefarame,CCI_Period,CCI_Price);
   if(handle>0)
     {
      CCI      =  new COneBufferArray(handle);
      if(CheckPointer(CCI)==POINTER_INVALID)
         return INIT_FAILED;
     }
//---
   handle=iChaikin(_Symbol,Timefarame,Ch_Fast_Period,Ch_Slow_Period,Ch_Method,Ch_Volume);
   if(handle>0)
     {
      Chaikin  =  new COneBufferArray(handle);
      if(CheckPointer(Chaikin)==POINTER_INVALID)
         return INIT_FAILED;
     }
//---
   handle=iForce(_Symbol,Timefarame,Force_Period,Force_Method,Force_Volume);
   if(handle>0)
     {
      Force    =  new COneBufferArray(handle);
      if(CheckPointer(Force)==POINTER_INVALID)
         return INIT_FAILED;
     }
//---
   MACD     =  new CMACD(_Symbol,Timefarame,MACD_Fast,MACD_Slow,MACD_Signal,MACD_Price);
   if(CheckPointer(MACD)==POINTER_INVALID)
      return INIT_FAILED;
//---
   handle=iStdDev(_Symbol,Timefarame,StdDev_Period,StdDev_Shift,StdDev_Method,StdDev_Price);
   if(handle>0)
     {
      StdDev   =  new COneBufferArray(handle);
      if(CheckPointer(StdDev)==POINTER_INVALID)
         return INIT_FAILED;
     }
//---
   handle=iVolumes(_Symbol,Timefarame,Applied_Volume);
   if(handle>0)
     {
      Volume   =  new COneBufferArray(handle);
      if(CheckPointer(Volume)==POINTER_INVALID)
         return INIT_FAILED;
     }

В заключении функции OnInit установим счетчик ордеров на 0 и выйдем из функции.

   cur_ticket   =  0;
//---
   return(INIT_SUCCEEDED);
  }

6.2. Сбор статистических данных

 

Сбор данных о состоянии индикаторов будем проводить в функции OnTick. В начале функции проверим, по всем ли ордерам собрана информация. Если да, то выходим из функции.

void OnTick()
  {
   if(cur_ticket>=Deals.Total())
      return;

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

   CDeal *object  =  Deals.At(cur_ticket);
   if(object.GetTime()>TimeCurrent())
      return;

Если же мы прошли предыдущие проверки, проводим проверку состояния экземпляров индикаторных классов и сохраняем нужную информацию, вызывая функцию SaveNewValues для каждого индикаторного класса.

   if(CheckPointer(ADX)!=POINTER_INVALID)
      ADX.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(Alligator)!=POINTER_INVALID)
      Alligator.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(ATR)!=POINTER_INVALID)
      ATR.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(BWMFI)!=POINTER_INVALID)
      BWMFI.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(CCI)!=POINTER_INVALID)
      CCI.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(Chaikin)!=POINTER_INVALID)
      Chaikin.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(Force)!=POINTER_INVALID)
      Force.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(MACD)!=POINTER_INVALID)
      MACD.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(StdDev)!=POINTER_INVALID)
      StdDev.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(Volume)!=POINTER_INVALID)
      Volume.SaveNewValues(cur_ticket);

В заключении функции увеличиваем счетчик обработанных ордеров и выходим из функции.

   cur_ticket++;
   return;
  }

6.3. Вывод графиков для анализа

 

Анализ данных и вывод отчета проведем в функции OnTester. При запуске функции проверим количество сделок для анализа.

double OnTester()
  {
   double ret=0.0;
   int total=Deals.Total();

Если есть необходимость проведения анализа, то проводим инициализацию статистических классов.

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

   int total_indy=0;
   if(total>0)
     {
      if(CheckPointer(ADX)!=POINTER_INVALID)
         ADX_Stat=new CStaticADX(ADX,1);
      //---
      if(CheckPointer(Alligator)!=POINTER_INVALID)
         Alligator_Stat=new CStaticAlligator(Alligator);
      //---
      if(CheckPointer(ATR)!=POINTER_INVALID)
        {
         CStaticOneBuffer *indy=new CStaticOneBuffer(ATR,_Point*10);
         if(CheckPointer(indy)!=POINTER_INVALID)
           {
            if(ArrayResize(IndicatorsStatic,total_indy+1)>0)
              {
               IndicatorsStatic[total_indy]=indy;
               total_indy++;
              }
           }
        }
      //---
      if(CheckPointer(BWMFI)!=POINTER_INVALID)
         BWMFI_Stat=new CStaticBWMFI(BWMFI,_Point*100);
      //---
      if(CheckPointer(CCI)!=POINTER_INVALID)
        {
         CStaticOneBuffer *indy=new CStaticOneBuffer(CCI,10);
         if(CheckPointer(indy)!=POINTER_INVALID)
            if(ArrayResize(IndicatorsStatic,total_indy+1)>0)
              {
               IndicatorsStatic[total_indy]=indy;
               total_indy++;
              }
        }
      //---
      if(CheckPointer(Chaikin)!=POINTER_INVALID)
        {
         CStaticOneBuffer *indy=new CStaticOneBuffer(Chaikin,100);
         if(CheckPointer(indy)!=POINTER_INVALID)
            if(ArrayResize(IndicatorsStatic,total_indy+1)>0)
              {
               IndicatorsStatic[total_indy]=indy;
               total_indy++;
              }
        }
      //---
      if(CheckPointer(Force)!=POINTER_INVALID)
        {
         CStaticOneBuffer *indy=new CStaticOneBuffer(Force,0.1);
         if(CheckPointer(indy)!=POINTER_INVALID)
            if(ArrayResize(IndicatorsStatic,total_indy+1)>0)
              {
               IndicatorsStatic[total_indy]=indy;
               total_indy++;
              }
        }
      //---
      if(CheckPointer(MACD)!=POINTER_INVALID)
         MACD_Stat=new CStaticMACD(MACD,_Point*10);
      //---
      if(CheckPointer(StdDev)!=POINTER_INVALID)
        {
         CStaticOneBuffer *indy=new CStaticOneBuffer(StdDev,_Point*10);
         if(CheckPointer(indy)!=POINTER_INVALID)
            if(ArrayResize(IndicatorsStatic,total_indy+1)>0)
              {
               IndicatorsStatic[total_indy]=indy;
               total_indy++;
              }
        }
      //---
      if(CheckPointer(Volume)!=POINTER_INVALID)
        {
         CStaticOneBuffer *indy=new CStaticOneBuffer(Volume,100);
         if(CheckPointer(indy)!=POINTER_INVALID)
            if(ArrayResize(IndicatorsStatic,total_indy+1)>0)
              {
               IndicatorsStatic[total_indy]=indy;
               total_indy++;
              }
        }
     }

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

   for(int i=0;i<total;i++)
     {
      CDeal               *deal     =  Deals.At(i);
      ENUM_POSITION_TYPE   type     =  deal.Type();
      double               d_profit =  deal.GetProfit();
      
      for(int ind=0;ind<total_indy;ind++)
         IndicatorsStatic[ind].Ad(i,d_profit,type);
      if(CheckPointer(BWMFI_Stat)!=POINTER_INVALID)
         BWMFI_Stat.Ad(i,d_profit,type);
      if(CheckPointer(MACD_Stat)!=POINTER_INVALID)
         MACD_Stat.Ad(i,d_profit,type);
      if(CheckPointer(ADX_Stat)!=POINTER_INVALID)
         ADX_Stat.Ad(i,d_profit,type);
      if(CheckPointer(Alligator_Stat)!=POINTER_INVALID)
         Alligator_Stat.Ad(i,d_profit,type);
     }

После группировки данных создадим файл отчета Report.html и сохраним его в общей папке терминалов.

   if(total_indy>0 || CheckPointer(BWMFI_Stat)!=POINTER_INVALID || CheckPointer(MACD_Stat)!=POINTER_INVALID
      || CheckPointer(ADX_Stat)!=POINTER_INVALID || CheckPointer(Alligator_Stat)!=POINTER_INVALID )
     {
      int handle=FileOpen("Report.html",FILE_WRITE|FILE_TXT|FILE_COMMON);
      if(handle<0)
         return ret;

В начале файла запишем заголовок нашего html-отчета.

      FileWrite(handle,"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">");
      FileWrite(handle,"<html> <head> <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">");
      FileWrite(handle,"<title>Deals to Indicators</title> <!-- - -->");
      FileWrite(handle,"<script src=\"http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.js\" type=\"text/javascript\"></script>");
      FileWrite(handle,"<script src=\"https://code.highcharts.com/highcharts.js\" type=\"text/javascript\"></script>");
      FileWrite(handle,"<!-- - --> <script type=\"text/javascript\">$(document).ready(function(){");

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

      for(int ind=0;ind<total_indy;ind++)
         FileWrite(handle,IndicatorsStatic[ind].HTML_header());
      if(CheckPointer(BWMFI_Stat)!=POINTER_INVALID)
         FileWrite(handle,BWMFI_Stat.HTML_header());
      if(CheckPointer(MACD_Stat)!=POINTER_INVALID)
         FileWrite(handle,MACD_Stat.HTML_header());
      if(CheckPointer(ADX_Stat)!=POINTER_INVALID)
         FileWrite(handle,ADX_Stat.HTML_header());
      if(CheckPointer(Alligator_Stat)!=POINTER_INVALID)
         FileWrite(handle,Alligator_Stat.HTML_header());

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

      FileWrite(handle,"});</script> <!-- - --> </head> <body>");
      for(int ind=0;ind<total_indy;ind++)
        {
         FileWrite(handle,IndicatorsStatic[ind].HTML_body());
         delete IndicatorsStatic[ind];
        }
      if(CheckPointer(BWMFI_Stat)!=POINTER_INVALID)
        {
         FileWrite(handle,BWMFI_Stat.HTML_body());
         delete BWMFI_Stat;
        }
      if(CheckPointer(MACD_Stat)!=POINTER_INVALID)
        {
         FileWrite(handle,MACD_Stat.HTML_body());
         delete MACD_Stat;
        }
      if(CheckPointer(ADX_Stat)!=POINTER_INVALID)
        {
         FileWrite(handle,ADX_Stat.HTML_body());
         delete ADX_Stat;
        }
      if(CheckPointer(Alligator_Stat)!=POINTER_INVALID)
        {
         FileWrite(handle,Alligator_Stat.HTML_body());
         delete Alligator_Stat;
        }

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

      FileWrite(handle,"</body> </html>");
      FileFlush(handle);
      FileClose(handle);
     }
//---
   ArrayFree(IndicatorsStatic);
//---
   return(ret);
  }

Не забываем удалить оставшиеся классы в функции OnDeinit.

7. Анализ информации

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

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

7.1. ATR

 

ATR

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

7.2. CCI.

 

CCI

Графики зависимости прибыли от индикатора CCI позволяют извлечь некую прибыль по BUY сделкам при значении индикатора выше 200 и растущей линии индикатора. Но по SELL-сделкам прибыльные области отсутствуют.

 

7.3. Chaikin

Chaikin

Осциллятор Чайкина, как и ATR, не показал взаимосвязи между показаниями индикатора и прибылью от сделок.

 

7.4. Индикатор силы

Force

Аналитические графики индикатора силы также не выявили никаких зависимостей.

7.5. Стандартное отклонение

StdDev

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

7.6. Индикатор объемов

Volumes

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

7.7. Bill Williams MFI

BW_MFI

Индикатор BW MFI позволяет получить прибыль при фильтрации сделок на покупку, если они открыты только при цвете 0. Но не удается выявить какие-либо зависимости для сделок на продажу.

7.8. MACD.

MACD1MACD2

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

7.9. ADX

Анализ сигналов индикатора ADX не дает возможности фильтровать сделки.

7.10. Alligator

Alligator1Alligator2

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

  • положение линий индикатора показывает тренд на продажу и линия LIPS или JAW разворачивается вверх;
  • положение линий индикатора показывает тренд на покупку и линии LIPS и TEETH направлены вверх;
  • тренд неопределенный и линии TEETH и JAW направлены вниз.

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

8. Корректируем исходный советник

Мы провели очень обширную работу по анализу сделок нашего советника. Теперь посмотрим, как это повлияет на результаты работы нашей стратегии. Для этого в модуль торговых сигналов из статьи [1] добавим индикаторы с правилами фильтрации согласно проведенному выше анализу. Я предлагаю добавить в наш модуль MACD и Alligator.

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

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

//| Parameter=JAW_Period,uint,13,JAW Period                                   |
//| Parameter=JAW_Shift,uint,8,JAW Shift                                      |
//| Parameter=TEETH_Period,uint,8,TEETH Period                                |
//| Parameter=TEETH_Shift,uint,5,TEETH Shift                                  |
//| Parameter=LIPS_Period,uint,5,LIPS Period                                  |
//| Parameter=LIPS_Shift,uint,3,LIPS_Shift                                    |
//| Parameter=Alligator_Method,ENUM_MA_METHOD,MODE_SMMA,Method                |
//| Parameter=Alligator_Price,ENUM_APPLIED_PRICE,PRICE_MEDIAN,Alligator Price |
//| Parameter=MACD_Fast,uint,12,MACD Fast                                     |
//| Parameter=MACD_Slow,uint,26,MACD Slow                                     |
//| Parameter=MACD_Signal,uint,9,MACD Signal                                  |
//| Parameter=MACD_Price,ENUM_APPLIED_PRICE,PRICE_CLOSE,MACD Price            |

Соответственно, в блок private добавляем переменные для хранения параметров, а в блок public — функции для их записи.

   uint              ci_MACD_Fast;
   uint              ci_MACD_Slow;
   uint              ci_MACD_Signal;
   ENUM_APPLIED_PRICE ce_MACD_Price;
   uint              ci_JAW_Period;
   uint              ci_JAW_Shift;
   uint              ci_TEETH_Period;
   uint              ci_TEETH_Shift;
   uint              ci_LIPS_Period;
   uint              ci_LIPS_Shift;
   ENUM_MA_METHOD    ce_Alligator_Method;
   ENUM_APPLIED_PRICE ce_Alligator_Price;
   void              JAW_Period(uint value)                 {  ci_JAW_Period  =  value;   }
   void              JAW_Shift(uint value)                  {  ci_JAW_Shift   =  value;   }
   void              TEETH_Period(uint value)               {  ci_TEETH_Period=  value;   }
   void              TEETH_Shift(uint value)                {  ci_TEETH_Shift =  value;   }
   void              LIPS_Period(uint value)                {  ci_LIPS_Period =  value;   }
   void              LIPS_Shift(uint value)                 {  ci_LIPS_Shift  =  value;   }
   void              Alligator_Method(ENUM_MA_METHOD value) {  ce_Alligator_Method  =  value;   }
   void              Alligator_Price(ENUM_APPLIED_PRICE value) {  ce_Alligator_Price=  value;   }
   void              MACD_Fast(uint value)                  {  ci_MACD_Fast   =  value;   }
   void              MACD_Slow(uint value)                  {  ci_MACD_Slow   =  value;   }
   void              MACD_Signal(uint value)                {  ci_MACD_Signal =  value;   }
   void              MACD_Price(ENUM_APPLIED_PRICE value)   {  ce_MACD_Price  =  value;   }

Также нужно добавить классы для работы с индикаторами и функции инициализации получения необходимых данных. Для работы с MACD я использовал стандартный класс. А поскольку для Alligator не существует стандартного класса, я заменил его тремя классами скользящих средних, присвоив им имена в соответствии с наименованиями линий индикатора.

protected:
   CiMACD            m_MACD;           // object-oscillator
   CiMA              m_JAW;
   CiMA              m_TEETH;
   CiMA              m_LIPS;
     
   //--- method of initialization of the indicators
   bool              InitMACD(CIndicators *indicators);
   bool              InitAlligator(CIndicators *indicators);
   //--- methods of getting data
   double            Main(int ind)                     { return(m_MACD.Main(ind));      }
   double            Signal(int ind)                   { return(m_MACD.Signal(ind));    }
   double            DiffMain(int ind)                 { return(Main(ind+1)!=0 ? Main(ind)-Main(ind+1) : 0); }
   int               AlligatorTrend(int ind);
   double            DiffJaw(int ind)                  { return(m_JAW.Main(ind+1)!=0 ? m_JAW.Main(ind)/m_JAW.Main(ind+1) : 1); }
   double            DiffTeeth(int ind)                { return(m_TEETH.Main(ind+1)!=0 ? m_TEETH.Main(ind)/m_TEETH.Main(ind+1) : 1); }
   double            DiffLips(int ind)                 { return(m_LIPS.Main(ind+1)!=0 ? m_LIPS.Main(ind)/m_LIPS.Main(ind+1) : 1); }

Следующим шагом внесем изменения в функцию InitIndicators, чтобы добавить наши индикаторы в библиотеку советника.

bool CSignalKalman::InitIndicators(CIndicators *indicators)
  {
//--- initialization of indicators and timeseries of additional filters
   if(!CExpertSignal::InitIndicators(indicators))
      return(false);
//--- initialize close serias
   if(CheckPointer(m_close)==POINTER_INVALID)
     {
      if(!InitClose(indicators))
         return false;
     }
//--- create and initialize MACD oscilator
   if(!InitMACD(indicators))
      return(false);
//--- create and initialize Alligator
   if(!InitAlligator(indicators))
      return(false);
//--- create and initialize Kalman Filter
   if(CheckPointer(Kalman)==POINTER_INVALID)
      Kalman=new CKalman(ci_HistoryBars,ci_ShiftPeriod,m_symbol.Name(),ce_Timeframe);
   
//--- ok
   return(true);
  }

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

int CSignalKalman::LongCondition(void)
  {
   if(!CalculateIndicators())
      return 0;
   int result=0;
   //--- 
   if(cd_correction>cd_forecast)
     {
      if(Signal(1)>Main(1))
         result=80;
      else
        {
         switch(AlligatorTrend(1))
           {
            case 1:
              if(DiffLips(1)>1 && DiffTeeth(1)>1 && DiffJaw(1)<=1)
                 result=80;
              break;
            case -1:
              if(DiffLips(1)>1 || DiffJaw(1)>1)
                 result=80;
              break;
            case 0:
              if(DiffJaw(1)<1)
                {
                 if(DiffLips(1)>1)
                    result=80;
                 else
                    if(DiffTeeth(1)<1)
                       result=80;
                }
              break;
           }
        }
     }
   return result;
  }

Аналогичные изменения вносим в функцию ShortCondition. С полным кодом модуля торговых решений можно ознакомиться во вложении.

9. Тестирование советника после внесения изменений

После внесения изменений в модуль торговых решений создадим новый советник (подробное описание создания советника с использованием модуля торговых сигналов описано в статье [5]). Проведем тестирование вновь созданного советника с параметрами, аналогичными первичному тестированию в разделе 2 данной статьи.

Как показали результаты тестирования, без изменения параметров советника использование фильтров позволило увеличить профит-фактор с 0.75 до 1.12. Т.е. нам удалось при убыточных параметрах исходного советника получить прибыль. Напомню, что вначале я намеренно взял неоптимизированные параметры исходного советника.

Test2_1Test2_2Test2_3

Заключение

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

Comments ()