Рецепты MQL5 - Уменьшаем эффект подгонки и решаем проблему недостаточного количества котировок

Anatoli Kazharski | 2 сентября, 2013

Введение

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

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

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

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

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


Пример в NeuroShell DayTrader Professional

Перед тем, как перейти к программированию в MetaTrader 5, давайте рассмотрим пример в программе NeuroShell DayTrader Professional. Она обладает хорошими возможностями оптимизации параметров торговой системы (собранной в конструкторе) на множестве символах. При этом можно указать в настройках торгового модуля, оптимизировать параметры для каждого символа отдельно или найти один набор оптимальных параметров сразу для всех символов. Эта опция находится на вкладке Optimization:

Вкладка Optimization в торговом модуле NeuroShell DayTrader Professional

Рис. 1. Вкладка Optimization в торговом модуле NeuroShell DayTrader Professional

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

Как собирать торговые стратегии в NeuroShell DayTrader Professional вы можете посмотреть в других статьях моего блога (воспользуйтесь поиском или облаком меток). Также рекомендую почитать статью Как подготовить котировки MetaTrader 5 для других программ, в которой рассказывается и показывается, как с помощью скрипта можно скачать котировки из MetaTrader 5 в формате, совместимом с NeuroShell DayTrader Professional.

Для этого теста были подготовлены данные дневных баров по восьми символам от 2000 года до текущего дня (январь 2013 года):

Список символов для теста в NeuroShell DayTrader Professional

Рис. 2. Список символов для теста в NeuroShell DayTrader Professional

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

Сравнение результатов двух режимов оптимизации параметров

Рис. 3. Сравнение результатов двух режимов оптимизации параметров

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

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


Пример в MetaTrader 5

Теперь давайте посмотрим, какие режимы оптимизации параметров предлагает MetaTrader 5. На рисунке ниже стрелкой указан вариант в выпадающем списке режимов оптимизации: Все символы, выбранные в окне 'Обзор Рынка'.

Рис. 4. Режимы оптимизации параметров в тестере MetaTrader 5

Рис. 4. Режимы оптимизации параметров в тестере MetaTrader 5

Но этот режим позволяет только протестировать эксперта с текущими параметрами поочередно на каждом символе, которые в текущий момент выбраны в окне "Обзор Рынка". То есть, оптимизация параметров при этом не производится. Но зато MetaTrader 5 и MQL5 дает возможность реализовать эту идею самостоятельно.

Теперь нужно решить, как реализовать схему эксперта. Список символов будем составлять в текстовом файле (*.txt). Реализуем возможность хранения нескольких наборов списков символов. Каждый набор будет в отдельной секции, у которой будет свой заголовок с номером секции. Номера нужны просто для удобного визуального контроля.

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

Ниже показан пример заполненного файла SymbolsList.txt, в котором есть три набора символов для тестов. Именно этот образец будем потом использовать при тестировании метода.

Рис. 5. Несколько наборов символов для тестов в текстовом файле

Рис. 5. Несколько наборов символов для тестов в текстовом файле

Чтобы указывать эксперту, какой набор символов использовать в текущем тесте, во внешних параметрах добавим параметр SectionOfSymbolList. В этом параметре нужно просто ввести номер (от нуля и выше), который определяет набор символов. Если выйти за предел количества наборов, то эксперт сообщит об этом в журнал, и тест будет произведен только на текущем символе.

Файл SymbolsList.txt должен находиться в локальной папке терминала в директории Metatrader 5\MQL5\Files. Его можно расположить и в общей папке, но тогда он не будет доступен для оптимизации параметров в сети распределенных вычислений MQL5 Cloud Network. Также, для того чтобы файл и используемые пользовательские индикаторы были доступны для теста, нужно в начале файла прописать вот такие строки:

//--- Делаем доступными внешний файл и индикатор для оптимизации в облаке
#property tester_file      "SymbolsList.txt"
#property tester_indicator "EventsSpy.ex5"

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

Внешние параметры эксперта нужно сделать так, как показано ниже:

//--- Внешние параметры эксперта
sinput int    SectionOfSymbolList = 1;     // Номер раздел в списках символов
sinput bool   UpdateReport        = false; // Обновление отчёта
sinput string delimeter_00="";   // --------------------------------
sinput long   MagicNumber         = 777;   // Магический номер
sinput int    Deviation           = 10;    // Проскальзывание
sinput string delimeter_01="";   // --------------------------------
input  int    IndicatorPeriod     = 5;     // Период индикатора
input  double TakeProfit          = 100;   // Тейк Профит
input  double StopLoss            = 50;    // Стоп Лосс
input  double TrailingStop        = 10;    // Трейлинг Стоп
input  bool   Reverse             = true;  // Разворот позиции
input  double Lot                 = 0.1;   // Лот
input  double VolumeIncrease      = 0.1;   // Приращение объема позиции
input  double VolumeIncreaseStep  = 10;    // Шаг для приращения объема

Все массивы, которые относятся к внешним параметрам, нужно удалить, теперь они не понадобятся. Вместо них везде нужно подставить внешние переменные. Оставить нужно только динамический массив символов InputSymbols[], размер которого будет зависеть от количества используемых символов одного из наборов в файле SymbolsList.txt. Если эксперт используется вне тестера, то размер этого массива будет равен 1, так как в реальном времени эксперт будет работать на одном символе.

Соответственные изменения нужно произвести и в файле инициализации массивов InitializeArrays.mqh. То есть, все функции, в которых производилась инициализация массивов внешних переменных, нужно удалить. Функция InitializeArraySymbols() теперь выглядит так, как показано в коде ниже:

//+------------------------------------------------------------------+
//| Заполняет массив символов                                        |
//+------------------------------------------------------------------+
void InitializeArraySymbols()
  {
   int    strings_count  =0;   // Количество строк в файле символов
   string checked_symbol ="";  // Для проверки доступности символа на торговом сервере
//--- Сообщение для теста
   string message_01="<--- Все имена символов в файле <- SymbolsList.txt -> некорректные ... --->\n"
                     "<--- ... или в параметре \"Section of List Symbols\" указано большее значение, "
                     "чем количество разделов в файле! --->\n"
                     "<--- Поэтому будем тестировать только текущий символ. --->";
//--- Сообщение для реал-тайма
   string message_02="<--- В реал-тайме работаем только на текущем символе. --->";
//--- Если реал-тайм
   if(!IsRealtime())
     {
      //--- Получим количество строк из указанного набора символов в файле и заполним временный массив символов
      strings_count=ReadSymbolsFromFile("SymbolsList.txt");
      //--- Пройдем по всем символам из указанного набора
      for(int s=0; s<strings_count; s++)
        {
         //--- Если после проверки символа возвращена корректная строка
         if((checked_symbol=GetSymbolByName(temporary_symbols[s]))!="")
           {
            //--- увеличим счетчик
            SYMBOLS_COUNT++;
            //--- установим/увеличим размер массива
            ArrayResize(InputSymbols,SYMBOLS_COUNT);
            //--- проиндексируем именем символа
            InputSymbols[SYMBOLS_COUNT-1]=checked_symbol;
           }
        }
     }
//--- Если все имена символов были некорректно введены или сейчас реал-тайм
   if(SYMBOLS_COUNT==0)
     {
      //--- Сообщение для реал-тайма
      if(IsRealtime())
         Print(message_02);
      //--- Сообщение для теста
      if(!IsRealtime())
         Print(message_01);
      //--- Будем работать только с текущим символом
      SYMBOLS_COUNT=1;
      //--- установим размер массива и
      ArrayResize(InputSymbols,SYMBOLS_COUNT);
      //--- проиндексируем именем текущего символа
      InputSymbols[0]=_Symbol;
     }
  }

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

//+------------------------------------------------------------------+
//| Возвращает количество строк (символов) в файле и                 |
//| из указанного набора и заполняет временный массив символов       |
//+------------------------------------------------------------------+
//--- При подготовке файла символы в списке следует разделять переводом строки
int ReadSymbolsFromFile(string file_name)
  {
   ulong  offset         =0;   // Смещение для определения положения файлового указателя
   string delimeter      ="#"; // Признак начала раздела
   string read_line      ="";  // Для проверки прочитанной строки
   int    limit_count    =0;   // Счетчик ограничитель кол-ва возможных открытых графиков
   int    strings_count  =0;   // Счетчик строк
   int    sections_count =-1;  // Счетчик разделов
   
//--- Сообщение 01
   string message_01="<--- Файл <- "+file_name+" -> составлен некорректно! --->\n"
                     "<--- В первой строке нет признака номера раздела ("+delimeter+")! --->";
//--- Сообщение 02
   string message_02="<--- Файл <- "+file_name+" -> составлен некорректно! --->\n"
                     "<--- Нет признака перевода строки в последней строке, --->\n"
                     "<--- поэтому в тесте будет участвовать только текущий символ. --->";
//--- Сообщение 03
   string message_03="<--- Файл <- "+file_name+" -> не найден! --->"
                     "<--- В тесте будет участвовать только текущий символ. --->";
                     
//--- Откроем файл (получим хэндл) для чтения в локальной папке терминала
   int file_handle=FileOpen(file_name,FILE_READ|FILE_ANSI,'\n');
//--- Если хэндл файла получен
   if(file_handle!=INVALID_HANDLE)
     {
      //--- Читать пока текущее положение файлового указателя
      //    не окажется в конце файла или программа не будет удалена
      while(!FileIsEnding(file_handle) || !IsStopped())
        {
         //--- Читать до конца строки или пока программа не будет удалена
         while(!FileIsLineEnding(file_handle) || !IsStopped())
           {
            //--- Прочитаем всю строку
            read_line=FileReadString(file_handle);
            //--- Если нашли признак номера раздела
            if(StringFind(read_line,delimeter,0)>-1)
               //--- Увеличим счетчик раздела
               sections_count++;
            //--- Если раздел прочитан, то выходим из функции
            if(sections_count>SectionOfSymbolList)
              {
               FileClose(file_handle); // Закроем файл
               return(strings_count);  // Вернем количество строк в файле
              }
            //--- Если это первая итерация и в первой строке не было признака номера раздела
            if(limit_count==0 && sections_count==-1)
              {
               PrepareArrayForOneSymbol(strings_count,message_01);
               //--- Закроем файл
               FileClose(file_handle);
               //--- Вернем количество строк в файле
               return(strings_count);
              }
            //--- Увеличим счетчик ограничитель кол-ва возможных открытых графиков
            limit_count++;
            //--- Если дошли до предела
            if(limit_count>=CHARTS_MAX)
              {
               PrepareArrayForOneSymbol(strings_count,message_02);
               //--- Закроем файл
               FileClose(file_handle);
               //--- Вернем количество строк в файле
               return(strings_count);
              }
            //--- Получим положение указателя
            offset=FileTell(file_handle);
            //--- Если это конец строки
            if(FileIsLineEnding(file_handle))
              {
               //--- Переход на другую строку, если это не конец файла
               //    Для этого увеличим счетчик указателя файла
               if(!FileIsEnding(file_handle))
                  offset++;
               //--- переведем его на следующую строку
               FileSeek(file_handle,offset,SEEK_SET);
               //--- Если мы сейчас не в указанном разделе файла, выйдем из цикла
               if(sections_count!=SectionOfSymbolList)
                  break;
               //--- В противном случае,
               else
                 {
                  //--- если строка не пустая
                  if(read_line!="")
                    {
                     //--- увеличим счетчик строк
                     strings_count++;
                     //--- увеличим размер массива строк,
                     ArrayResize(temporary_symbols,strings_count);
                     //--- занесем в текущий индекс строку
                     temporary_symbols[strings_count-1]=read_line;
                    }
                 }
               //--- Выйдем из этого цикла
               break;
              }
           }
         //--- Если это конец файла прервем весь цикл
         if(FileIsEnding(file_handle))
            break;
        }
      //--- Закроем файл
      FileClose(file_handle);
     }
   else
      PrepareArrayForOneSymbol(strings_count,message_03);
//--- Вернем количество строк в файле
   return(strings_count);
  }

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

//+------------------------------------------------------------------+
//| Подготовка массива для одного символа                            |
//+------------------------------------------------------------------+
void PrepareArrayForOneSymbol(int &strings_count,string message)
  {
//--- Выведем сообщение в журнал
   Print(message);
//--- Размер массива
   strings_count=1;
//--- Установим размер массива символов
   ArrayResize(temporary_symbols,strings_count);
//--- Занесем в текущий индекс строку с именем текущего символа
   temporary_symbols[0]=_Symbol;
  }

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

Ниже представлен код функции CreateSymbolBalanceReport():

//+------------------------------------------------------------------+
//| Создает отчет тестирования по сделкам в формате CSV              |
//+------------------------------------------------------------------+
void CreateSymbolBalanceReport()
  {
   int    file_handle =INVALID_HANDLE; // Хэндл файла
   string path        ="";             // Путь к файлу

//--- Если ошибка при создании/получении директории, выходим
   if((path=CreateInputParametersFolder())=="")
      return;
//--- Создадим файл для записи данных в общей папке терминала
   file_handle=FileOpen(path+"\\LastTest.csv",FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON);
//--- Если хэндл валиден (файл создался/открылся)
   if(file_handle>0)
     {
      int           digits           =0;   // Количество знаков в цене после запятой
      int           deals_total      =0;   // Количество сделок выбранной истории
      ulong         ticket           =0;   // Тикет сделки
      double        drawdown_max     =0.0; // Просадка
      double        balance          =0.0; // Баланс
      string        delimeter        =","; // Разделитель
      string        string_to_write  ="";  // Для формирования строки для записи
      static double percent_drawdown =0.0; // Просадка в процентах
      static double money_drawdown   =0.0; // Просадка в деньгах

      //--- Сформируем строку заголовков
      string headers="TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME,"
                     "PRICE,SWAP($),PROFIT($),DRAWDOWN(%),DRAWDOWN($),BALANCE";
      //--- Если участвует больше одного символа, то дополним строку заголовков
      if(SYMBOLS_COUNT>1)
        {
         for(int s=0; s<SYMBOLS_COUNT; s++)
            StringAdd(headers,","+InputSymbols[s]);
        }
      //--- Запишем заголовки отчета
      FileWrite(file_handle,headers);
      //--- Получим всю историю
      HistorySelect(0,TimeCurrent());
      //--- Узнаем количество сделок
      deals_total=HistoryDealsTotal();
      //--- Установим размер массива балансов по кол-ву символов
      ArrayResize(symbol_balance,SYMBOLS_COUNT);
      //--- Установим размер массивов сделок для каждого символа
      for(int s=0; s<SYMBOLS_COUNT; s++)
         ArrayResize(symbol_balance[s].balance,deals_total);
      //--- Пройдемся в цикле и запишем данные
      for(int i=0; i<deals_total; i++)
        {
         //--- Получим тикет сделки
         ticket=HistoryDealGetTicket(i);
         //--- Получим все свойства сделки
         GetHistoryDealProperties(ticket,D_ALL);
         //--- Узнаем кол-во знаков в цене
         digits=(int)SymbolInfoInteger(deal.symbol,SYMBOL_DIGITS);
         //--- Посчитаем общий баланс
         balance+=deal.profit+deal.swap+deal.commission;
         //--- Посчитаем максимальную просадку от локального максимума
         TesterDrawdownMaximum(i,balance,percent_drawdown,money_drawdown);
         //--- Сформируем строку для записи путем конкатенации
         StringConcatenate(string_to_write,
                           deal.time,delimeter,
                           DealSymbolToString(deal.symbol),delimeter,
                           DealTypeToString(deal.type),delimeter,
                           DealEntryToString(deal.entry),delimeter,
                           DealVolumeToString(deal.volume),delimeter,
                           DealPriceToString(deal.price,digits),delimeter,
                           DealSwapToString(deal.swap),delimeter,
                           DealProfitToString(deal.symbol,deal.profit),delimeter,
                           DrawdownToString(percent_drawdown),delimeter,
                           DrawdownToString(money_drawdown),delimeter,
                           DoubleToString(balance,2));
         //--- Если участвует больше одного символа, то запишем их значения баланса
         if(SYMBOLS_COUNT>1)
           {
            //--- Пройдемся по всем символам
            for(int s=0; s<SYMBOLS_COUNT; s++)
              {
               //--- Если символы равны и результат сделки ненулевой
               if(deal.symbol==InputSymbols[s] && deal.profit!=0)
                 {
                  //--- Отразим сделку в балансе с этим символом
                  //    Учтем своп и комиссию
                  symbol_balance[s].balance[i]=symbol_balance[s].balance[i-1]+
                                               deal.profit+
                                               deal.swap+
                                               deal.commission;
                  //--- Добавим к строке
                  StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2));
                 }
               //--- Иначе запишем предыдущее значение
               else
                 {
                  //--- Если тип сделки "Начисление баланса" (первая сделка)
                  if(deal.type==DEAL_TYPE_BALANCE)
                    {
                     //--- для всех символов баланс одинаковый
                     symbol_balance[s].balance[i]=balance;
                     StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2));
                    }
                  //--- Иначе запишем предыдущее значение в текущий индекс
                  else
                    {
                     symbol_balance[s].balance[i]=symbol_balance[s].balance[i-1];
                     StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2));
                    }
                 }
              }
           }
         //--- Запишем сформированную строку
         FileWrite(file_handle,string_to_write);
         //--- Обязательное обнуление переменной для следующей строки
         string_to_write="";
        }
      //--- Закроем файл
      FileClose(file_handle);
     }
//--- Если файл не создался/открылся, выведем сообщение
   else
      Print("Ошибка при создании файла! Ошибка: "+IntegerToString(GetLastError())+"");
  }

До этого просадки считались в функции DrawdownMaximumToString(). Теперь вместо нее это делает функция TesterDrawdownMaximum(), а в строку значение просадки преобразует простая функция DrawdownToString().

Код функции TesterDrawdownMaximum():

//+------------------------------------------------------------------+
//| Возвращает максимальную просадку от локального максимума         |
//+------------------------------------------------------------------+
void TesterDrawdownMaximum(int deal_number,
                           double balance,
                           double &percent_drawdown,
                           double &money_drawdown)
  {
   ulong         ticket =0;   // Тикет сделки
   string        str    ="";  // Строка для отображения в отчете
//--- Для расчета локального максимума и просадки
   static double max    =0.0;
   static double min    =0.0;
   
//--- Если первая сделка
   if(deal_number==0)
     {
      //--- Просадки нет
      percent_drawdown =0.0;
      money_drawdown   =0.0;
      //--- Зададим начальную точку, как локальный максимум
      max=balance;
      min=balance;
     }
   else
     {
      //--- Если текущий баланс больше, чем в памяти,...
      if(balance>max)
        {
         //--- Посчитаем просадку по предыдущим значениям:
         //    в деньгах
         money_drawdown=max-min;
         //    в процентах
         percent_drawdown=100-((min/max)*100);
         //--- Обновим локальный максимум
         max=balance;
         min=balance;
        }
      //--- В противном случае
      else
        {
         //--- Возвратим нулевое значение просадки
         money_drawdown=0.0;
         percent_drawdown=0.0;
         //--- Обновим минимум
         min=fmin(min,balance);
         //--- Если тикет сделки по ее позиции в списке получен, то...
         if((ticket=HistoryDealGetTicket(deal_number))>0)
           {
            //--- ...получим комментарий сделки
            GetHistoryDealProperties(ticket,D_COMMENT);
            //--- Флаг последней сделки
            static bool last_deal=false;
            //--- Последнюю сделку в тесте можно определить по комментарию "end of test"
            if(deal.comment=="end of test" && !last_deal)
              {
               //--- Установим флаг
               last_deal=true;
               //--- Обновим значения просадки:
               //    в деньгах
               money_drawdown=max-min;
               //    в процентах
               percent_drawdown+=100-((min/max)*100);
              }
           }
        }
     }
  }

Код функции DrawdownToString():

//+------------------------------------------------------------------+
//| Преобразует просадку в строку                                    |
//+------------------------------------------------------------------+
string DrawdownToString(double drawdown)
  {
   return((drawdown<=0) ? "" : DoubleToString(drawdown,2));
  }

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


Оптимизация параметров и тестирование эксперта

Настройки тестера установим так, как показано на скриншоте ниже:

Рис. 6. Настройки тестера для оптимизации

Рис. 6. Настройки тестера для оптимизации

Настройки эксперта для оптимизации параметров показаны ниже:

Рис. 7. Настройки эксперта для оптимизации параметров

Рис. 7. Настройки эксперта для оптимизации параметров

Так как в оптимизации участвует три символа и на каждом производится наращивание объема позиции, то лот для открытия позиции и наращивания объема позиции установим минимальными. В данном случае это 0.01.

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

Рис. 8. Результат теста в MetaTrader 5

Рис. 8. Результат теста в MetaTrader 5

На скриншоте ниже показано, как выглядит результат в Excel 2010:

Результат теста на трех символах в Excel 2010

Рис. 9. Результат теста на трех символах в Excel 2010

Просадка в денежном выражении отображается на нижнем графике на второй (вспомогательной) шкале в виде зеленых маркеров.

Следует также знать об ограничениях диаграмм в Excel 2010 (с полным списком вы можете ознакомиться на сайте Microsoft на странице Технические характеристики и ограничения Microsoft Excel).

Технические характеристики и ограничения диаграмм в Excel 2010

Рис. 10. Технические характеристики и ограничения диаграмм в Excel 2010

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

Теперь проведем тест на семи символах из третьего набора с текущими параметрами и посмотрим на результат:

Результат теста на семи символах в Excel 2010

Рис. 11. Результат теста на семи символах в Excel 2010

На семи символах получилось 6901 сделка, и Excel 2010 довольно быстро их обновляет на диаграмме.


Заключение

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

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

После распаковки архива поместите папку ReduceOverfittingEA в директорию Metatrader 5\MQL5\Experts. Индикатор EventsSpy.mq5 нужно поместить в директорию Metatrader 5\MQL5\Indicators. Текстовый файл SymbolsList.txt должен находиться в директории Metatrader 5\MQL5\Files.