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

Anatoli Kazharski | 5 июля, 2013

Введение

Схема мультивалютного эксперта, которую мы рассматривали в предыдущей статье Рецепты MQL5 - Мультивалютный эксперт: пример простой, точной и быстрой схемы, вполне удобна, если используется небольшое количество символов, и количество параметров для торговой стратегии тоже невелико. Однако, в MQL5 существует ограничение на количество входных параметров эксперта: их не должно быть больше 1024.

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

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

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

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

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


Процесс разработки эксперта

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

//--- Входные параметры эксперта
sinput long                      MagicNumber          =777;      // Магический номер
sinput int                       Deviation            =10;       // Проскальзывание
sinput string                    delimeter_00=""; // --------------------------------
sinput int                       SymbolNumber         =1;        // Номер тестируемого символа
sinput bool                      RewriteParameters    =false;    // Перезапись параметров
sinput ENUM_INPUTS_READING_MODE  ParametersReadingMode=FILE;     // Режим чтения параметров
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;       // Шаг для приращения объема

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

Код перечисления ENUM_INPUTS_READING_MODE:

//+------------------------------------------------------------------+
//| Режимы чтения входных параметров                                 |
//+------------------------------------------------------------------+
enum ENUM_INPUTS_READING_MODE
  {
   FILE             = 0, // Файл
   INPUT_PARAMETERS = 1  // Входные параметры
  };

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

//--- Количество торгуемых символов. Рассчитывается и зависит от режима тестирования и количества символов в файле
int SYMBOLS_COUNT=0;

В начале кода нашего советника объявим еще одну константу TESTED_PARAMETERS_COUNT, которая определяет размеры массивов и количество итераций в циклах при переборе входных параметров:

//--- Количество тестируемых/оптимизируемых параметров
#define TESTED_PARAMETERS_COUNT 8

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

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

//--- Массивы входных параметров
string InputSymbols[];            // Названия символов
//---
int    InputIndicatorPeriod[];    // Периоды индикатора
double InputTakeProfit[];         // Значения Тейк Профит
double InputStopLoss[];           // Значения Стоп Лосс
double InputTrailingStop[];       // Значения Трейлинг Стоп
bool   InputReverse[];            // Значения флагов разворота позиции
double InputLot[];                // Значения лотов
double InputVolumeIncrease[];     // Приращения объемов позиций
double InputVolumeIncreaseStep[]; // Шаги для приращения объемов
//--- Массив хэндлов для индикаторов-агентов
int spy_indicator_handles[];
//--- Массив хэндлов сигнальных индикаторов
int signal_indicator_handles[];
//--- Массивы данных для проверки торговых условий
struct PriceData
  {
   double            value[];
  };
PriceData open[];      // Цена открытия бара
PriceData high[];      // Цена максимума бара
PriceData low[];       // Цена минимума бара
PriceData close[];     // Цена закрытия бара
PriceData indicator[]; // Массив значений индикаторов
//--- Массивы для получения времени открытия текущего бара
struct Datetime
  {
   datetime          time[];
  };
Datetime lastbar_time[];
//--- Массив для проверки нового бара на каждом символе
datetime new_bar[];
//--- Массив названий входных параметров для записи в файл
string input_parameters[TESTED_PARAMETERS_COUNT]=
  {
   "IndicatorPeriod",   // Период индикатора
   "TakeProfit",        // Тейк Профит
   "StopLoss",          // Стоп Лосс
   "TrailingStop",      // Трейлинг Стоп
   "Reverse",           // Разворот позиции
   "Lot",               // Лот
   "VolumeIncrease",    // Приращение объема позиции
   "VolumeIncreaseStep" // Шаг для приращения объема
  };
//--- Массив для непроверенных символов
string temporary_symbols[];
//--- Массив, в котором сохраняются входные параметры из файла символа, выбранного для тестирования или торговли
double tested_parameters_from_file[];
//--- Массив значений входных параметров для записи в файл
double tested_parameters_values[TESTED_PARAMETERS_COUNT];

Далее нужно определить размеры массивов и инициализировать их значениями.

В файле InitializeArrays.mqh создадим функцию InitializeTestedParametersValues(), в которой будет инициализироваться созданный выше массив значений входных параметров. Этот массив будет использоваться при записи значений входных параметров в файл.

//+------------------------------------------------------------------+
//| Инициализирует массив тестируемых входных параметров             |
//+------------------------------------------------------------------+
void InitializeTestedParametersValues()
  {
   tested_parameters_values[0]=IndicatorPeriod;
   tested_parameters_values[1]=TakeProfit;
   tested_parameters_values[2]=StopLoss;
   tested_parameters_values[3]=TrailingStop;
   tested_parameters_values[4]=Reverse;
   tested_parameters_values[5]=Lot;
   tested_parameters_values[6]=VolumeIncrease;
   tested_parameters_values[7]=VolumeIncreaseStep;
  }

Теперь мы вплотную приблизились к работе с файлами. Для начала в папке UnlimitedParametersEA\Include нашего советника создайте еще одну библиотеку функций FileFunctions.mqh, в которой будем создавать функции, связанные с чтением и записью данных в файл. Подключите ее к основному файлу эксперта и другим файлам проекта.

//--- Подключаем свои библиотеки
#include "Include\Enums.mqh"
#include "Include\InitializeArrays.mqh"
#include "Include\Errors.mqh"
#include "Include\FileFunctions.mqh"
#include "Include\TradeSignals.mqh"
#include "Include\TradeFunctions.mqh"
#include "Include\ToString.mqh"
#include "Include\Auxiliary.mqh"

Первой мы создадим функцию чтения списка символов из текстового файла ReadSymbolsFromFile(). Этот файл (назовем его TestedSymbols.txt) следует поместить во вложенную папку \Files общей папки клиентских терминалов MetaTrader 5. В моем случае это C:\ProgramData\MetaQuotes\Terminal\Common, но вам нужно обязательно уточнить этот путь с помощью стандартной константы TERMINAL_COMMONDATA_PATH:

TerminalInfoString(TERMINAL_COMMONDATA_PATH)
Все пользовательские файлы должны находиться во вложенной папке \Files общей папки клиентских терминалов, либо в каталоге <папка данных терминала>\MQL5\Files\. Файловые функции имеют доступ только к этим папкам.

В созданном текстовом файле для проверки работоспособности добавьте несколько символов, разделяя каждый переводом строки ("\r\n"):

Рис. 1. Список символов в файле из общей папки терминалов

Рис. 1. Список символов в файле из общей папки терминалов.

Далее разберем код функции ReadSymbolsFromFile():

//+------------------------------------------------------------------+
//| Возвращает количество строк (символов) в файле и                 |
//| заполняет временный массив символов temporary_symbols[]          |
//+------------------------------------------------------------------+
//--- При подготовке файла символы в списке следует разделять переводом строки
int ReadSymbolsFromFile(string file_name)
  {
   int strings_count=0; // Счетчик строк
//--- Откроем файл для чтения из общей папки терминала
   int file_handle=FileOpen(file_name,FILE_READ|FILE_ANSI|FILE_COMMON);
//--- Если хэндл файла получен
   if(file_handle!=INVALID_HANDLE)
     {
      ulong  offset =0; // Смещение для определения положения файлового указателя
      string text ="";  // В эту переменную будем записывать прочитанную строку
      //--- Читать, пока текущее положение файлового указателя не окажется в конце файла или программа не будет удалена
      while(!FileIsEnding(file_handle) || !IsStopped())
        {
         //--- Читать до конца строки или пока программа не будет удалена
         while(!FileIsLineEnding(file_handle) || !IsStopped())
           {
            //--- Прочитаем всю строку
            text=FileReadString(file_handle);
            //--- Получим положение указателя
            offset=FileTell(file_handle);
            //--- Переход на другую строку, если это не конец файла
            //    Для этого увеличим смещение указателя файла
            if(!FileIsEnding(file_handle))
               offset++;
            //--- Переведем его на следующую строку
            FileSeek(file_handle,offset,SEEK_SET);
            //--- Если строка не пустая
            if(text!="")
              {
               //--- Увеличим счетчик строк
               strings_count++;
               //--- Увеличим размер массива строк,
               ArrayResize(temporary_symbols,strings_count);
               //--- Запишем прочитанную строку в текущий индекс
               temporary_symbols[strings_count-1]=text;
              }
            //--- Выйдем из вложенного цикла
            break;
           }
         //--- Если это конец файла прервем основной цикл
         if(FileIsEnding(file_handle))
            break;
        }
      //--- Закроем файл
      FileClose(file_handle);
     }
//--- Вернем количество строк в файле
   return(strings_count);
  }

Здесь происходит чтение текстового файла последовательно по одной строке. Название каждого прочитанного финансового инструмента записывается во временный массив temporary_symbols[], созданный нами ранее (реальная доступность символа на торговом сервере будет проверяться позже). Кроме того функция в конце возвращает количество прочитанных строк, т.е. количество символов, на которых мы будем тестировать наш советник.

Обязательно обращайтесь к Справочнику MQL5 для получения подробной информации о тех функциях и идентификаторах, которые видите впервые. Комментарии в коде упростят процесс изучения.

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

//+------------------------------------------------------------------+
//| Заполняет массив символов InputSymbol[]                          |
//+------------------------------------------------------------------+
void InitializeInputSymbols()
  {
   int    strings_count=0;    // Количество строк в файле символов
   string checked_symbol="";  // Для проверки доступности символа на торговом сервере
//--- Получим количество символов из файла "TestedSymbols.txt"
   strings_count=ReadSymbolsFromFile("TestedSymbols.txt");
//--- Если сейчас режим оптимизации или один из двух режимов (тестирование или визуализация), а также УКАЗАН НОМЕР символа
   if(IsOptimization() || ((IsTester() || IsVisualMode()) && SymbolNumber>0))
     {
      //--- Определим символ, который будет участвовать в оптимизации параметров
      for(int s=0; s<strings_count; s++)
        {
         //--- Если указанный в параметрах номер и текущий индекс цикла совпали
         if(s==SymbolNumber-1)
           {
            //--- Проверим, есть ли символ на торговом сервере
            if((checked_symbol=GetSymbolByName(temporary_symbols[s]))!="")
              {
               //--- Установим количество символов
               SYMBOLS_COUNT=1;
               //--- Установим размер массива символов
               ArrayResize(InputSymbols,SYMBOLS_COUNT);
               //--- Запишем название символа
               InputSymbols[0]=checked_symbol;
              }
            //--- Выходим
            return;
           }
        }
     }
//--- Если сейчас режим тестирования или визуализации и нужно протестировать ВСЕ символы из списка в файле
   if((IsTester() || IsVisualMode()) && SymbolNumber==0)
     {
      //--- Режим чтения параметров: из файла
      if(ParametersReadingMode==FILE)
        {
         //--- Пройдем по всем символам в файле
         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;
              }
           }
         //--- Выходим
         return;
        }
      //--- Режим чтения параметров: из входных параметров советника
      if(ParametersReadingMode==INPUT_PARAMETERS)
        {
         //--- Установим количество символов
         SYMBOLS_COUNT=1;
         //--- Установим размер массива символов
         ArrayResize(InputSymbols,SYMBOLS_COUNT);
         //--- Запишем название текущего символа
         InputSymbols[0]=Symbol();
         //--- Выходим
         return;
        }
     }
//--- Если сейчас штатный режим работы эксперта, будем использовать текущий символ графика
   if(IsRealtime())
     {
      //--- Установим количество символов
      SYMBOLS_COUNT=1;
      //--- Установим размер массива символов
      ArrayResize(InputSymbols,SYMBOLS_COUNT);
      //--- Запишем название символа
      InputSymbols[0]=Symbol();
     }
//---
  }

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

//+------------------------------------------------------------------+
//| Устанавливает новый размер для массивов входных параметров       |
//+------------------------------------------------------------------+
void ResizeInputParametersArrays()
  {
   ArrayResize(InputIndicatorPeriod,SYMBOLS_COUNT);
   ArrayResize(InputTakeProfit,SYMBOLS_COUNT);
   ArrayResize(InputStopLoss,SYMBOLS_COUNT);
   ArrayResize(InputTrailingStop,SYMBOLS_COUNT);
   ArrayResize(InputReverse,SYMBOLS_COUNT);
   ArrayResize(InputLot,SYMBOLS_COUNT);
   ArrayResize(InputVolumeIncrease,SYMBOLS_COUNT);
   ArrayResize(InputVolumeIncreaseStep,SYMBOLS_COUNT);
  }

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

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

//+------------------------------------------------------------------+
//| Читает параметры из файла и помещает в переданный массив         |
//| Формат текстового файла: ключ=значение
//+------------------------------------------------------------------+
bool ReadInputParametersValuesFromFile(int handle,double &array[])
  {
   int    delimiter_position  =0;  // Номер позиции символа "=" в строке
   int    strings_count       =0;  // Счетчик строк
   string read_string         =""; // Прочитанная строка
   ulong  offset              =0;  // Положение указателя (смещение в байтах)
//--- Переместим файловый указатель в начало файла
   FileSeek(handle,0,SEEK_SET);
//--- Читаем, пока текущее положение файлового указателя не окажется в конце файла
   while(!FileIsEnding(handle))
     {
      //--- Если пользователь удалил программу
      if(IsStopped())
         return(false);
      //--- Читаем до конца строки
      while(!FileIsLineEnding(handle))
        {
         //--- Если пользователь удалил программу
         if(IsStopped())
            return(false);
         //--- Прочитаем строку
         read_string=FileReadString(handle);
         //--- Получим индекс символа разделителя ("=") в прочитанной строке
         delimiter_position=StringFind(read_string,"=",0);
         //--- Заберем все, что после разделителя ("=") до конца строки
         read_string=StringSubstr(read_string,delimiter_position+1);
         //--- Поместим полученное значение, сконвертировав его в тип double, в выходной массив
         array[strings_count]=StringToDouble(read_string);
         //--- Получим текущее положение файлового указателя
         offset=FileTell(handle);
         //--- Если это конец строки
         if(FileIsLineEnding(handle))
           {
            //--- Перейдем на другую строку, если это не конец файла
            if(!FileIsEnding(handle))
               //--- Увеличим смещение файлового указателя на 1 для перехода на следующую строку
               offset++;
            //--- Переместим указатель относительно начала файла
            FileSeek(handle,offset,SEEK_SET);
            //--- Увеличим счетчик строк
            strings_count++;
            //--- Выйдем из вложенного цикла чтения строки
            break;
           }
        }
      //--- Если это конец файла, то выйдем из главного цикла
      if(FileIsEnding(handle))
         break;
     }
//--- Вернем факт успешного завершения
   return(true);
  }

Какие же файловые хэндлы и массивы будем передавать в эту функцию? Это уже зависит от того, с какими символами мы будем работать, прочитав их из файла "TestedSymbols.txt". Каждому символу будет соответствовать свой текстовый файл со значениями входных параметров. Здесь возможны два варианта развития событий:

  1. Файл существует, и мы читаем из него значения входных параметров с помощью функции ReadInputParametersValuesFromFile(), описанной выше.
  2. Файла нет, либо необходимо перезаписать существующие значения входных параметров.

Формат текстового файла (с расширением .ini, хотя вы можете выбрать любое другое расширение на ваше усмотрение) со значениями входных параметров будет простым:

название_входного_параметра1=значение
название_входного_параметра2=значение
....
название_входного_параметраN=значение

Объединим эту логику в одну функцию ReadWriteInputParameters(), код которой приведен ниже:

//+------------------------------------------------------------------+
//| Чтение/запись входных параметров из/в файл(а) для символа        |
//+------------------------------------------------------------------+
void ReadWriteInputParameters(int symbol_number,string path)
  {
   string file_name=path+InputSymbols[symbol_number]+".ini"; // Имя файла
//---
   Print("Ищем файл '"+file_name+"' ...");
//--- Откроем файл с входными параметрами символа
   int file_handle_read=FileOpen(file_name,FILE_READ|FILE_ANSI|FILE_COMMON);
   
//--- Вариант #1: файл существует и значения параметров перезаписывать не нужно
   if(file_handle_read!=INVALID_HANDLE && !RewriteParameters)
     {
      Print("Файл '"+InputSymbols[symbol_number]+".ini' существует, читаем...");
      //--- Установим размер массива
      ArrayResize(tested_parameters_from_file,TESTED_PARAMETERS_COUNT);
      //--- Заполним массив значениями из файла
      ReadInputParametersValuesFromFile(file_handle_read,tested_parameters_from_file);
      //--- Если размер массива корректен
      if(ArraySize(tested_parameters_from_file)==TESTED_PARAMETERS_COUNT)
        {
         //--- Запишем значения параметров в массивы
         InputIndicatorPeriod[symbol_number]    =(int)tested_parameters_from_file[0];
         Print("InputIndicatorPeriod[symbol_number] = "+(string)InputIndicatorPeriod[symbol_number]);
         InputTakeProfit[symbol_number]         =tested_parameters_from_file[1];
         InputStopLoss[symbol_number]           =tested_parameters_from_file[2];
         InputTrailingStop[symbol_number]       =tested_parameters_from_file[3];
         InputReverse[symbol_number]            =(bool)tested_parameters_from_file[4];
         InputLot[symbol_number]                =tested_parameters_from_file[5];
         InputVolumeIncrease[symbol_number]     =tested_parameters_from_file[6];
         InputVolumeIncreaseStep[symbol_number] =tested_parameters_from_file[7];
        }
      //--- Закроем файл и выйдем
      FileClose(file_handle_read);
      return;
     }
//--- Вариант #2: Если файла нет или нужно перезаписать параметры
   if(file_handle_read==INVALID_HANDLE || RewriteParameters)
     {
      //--- Закроем хэндл файла для чтения
      FileClose(file_handle_read);
      //--- Получим хэндл файла для записи
      int file_handle_write=FileOpen(file_name,FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON,"");
      //--- Если хэндл получен
      if(file_handle_write!=INVALID_HANDLE)
        {
         string delimiter="="; // Разделитель
         //--- Запишем параметры
         for(int i=0; i<TESTED_PARAMETERS_COUNT; i++)
           {
            FileWrite(file_handle_write,input_parameters_names[i],delimiter,tested_parameters_values[i]);
            Print(input_parameters_names[i],delimiter,tested_parameters_values[i]);
           }
         //--- Запишем значения параметров в массивы
         InputIndicatorPeriod[symbol_number]    =(int)tested_parameters_values[0];
         InputTakeProfit[symbol_number]         =tested_parameters_values[1];
         InputStopLoss[symbol_number]           =tested_parameters_values[2];
         InputTrailingStop[symbol_number]       =tested_parameters_values[3];
         InputReverse[symbol_number]            =(bool)tested_parameters_values[4];
         InputLot[symbol_number]                =tested_parameters_values[5];
         InputVolumeIncrease[symbol_number]     =tested_parameters_values[6];
         InputVolumeIncreaseStep[symbol_number] =tested_parameters_values[7];
         //--- В зависимости от указания выведем соответствующее сообщение
         if(RewriteParameters)
            Print("Перезаписан файл '"+InputSymbols[symbol_number]+".ini' с параметрами эксперта '"+EXPERT_NAME+".ex5'");
         else
            Print("Создан файл '"+InputSymbols[symbol_number]+".ini' с параметрами эксперта '"+EXPERT_NAME+".ex5'");
        }
      //--- Закроем хэндл файла для записи
      FileClose(file_handle_write);
     }
  }

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

//+------------------------------------------------------------------+
//| Создает папку для файлов входных параметров, если ее нет,        |
//| и возвращает путь, если все прошло успешно                       |
//+------------------------------------------------------------------+
string CreateInputParametersFolder()
  {
   long   search_handle       =INVALID_HANDLE;   // Хэндл поиска папки/файла
   string EA_root_folder      =EXPERT_NAME+"\\"; // Корневая папка эксперта
   string returned_filename   ="";               // Имя найденного объекта (файла/папки)
   string search_path         ="";               // Путь для поиска
   string folder_filter       ="*";              // Фильтр поиска (* - проверить все файлы/папки)
   bool   is_root_folder      =false;            // Флаг существования/отсутствия корневой папки эксперта

//--- Ищем корневую папку эксперта
   search_path=folder_filter;
//--- Установим хэндл поиска в общей папке терминала
   search_handle=FileFindFirst(search_path,returned_filename,FILE_COMMON);
//--- Если первая папка корневая, ставим флаг
   if(returned_filename==EA_root_folder)
      is_root_folder=true;
//--- Если хэндл поиска получен
   if(search_handle!=INVALID_HANDLE)
     {
      //--- Если первая папка была не корневой
      if(!is_root_folder)
        {
         //--- Перебираем все файлы с целью поиска корневой папки
         while(FileFindNext(search_handle,returned_filename))
           {
            //--- Выполнение прервано пользователем
            if(IsStopped())
               return("");
            //--- Если находим, то ставим флаг
            if(returned_filename==EA_root_folder)
              {
               is_root_folder=true;
               break;
              }
           }
        }
      //--- Закроем хэндл поиска корневой папки
      FileFindClose(search_handle);
      //search_handle=INVALID_HANDLE;
     }
//--- Иначе выведем сообщение об ошибке
   else
      Print("Ошибка при получении хэндла поиска, либо "
            "папка '"+TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"' пуста: ",ErrorDescription(GetLastError()));

//--- По результатам проверки создадим нужную директорию
   search_path=EXPERT_NAME+"\\";
//--- Если нет корневой папки эксперта
   if(!is_root_folder)
     {
      //--- Создадим ее. 
      if(FolderCreate(EXPERT_NAME,FILE_COMMON))
        {
         //--- Если папка создана, установим флаг
         is_root_folder=true;
         Print("Создана корневая папка эксперта '..\\"+EXPERT_NAME+"\\'");
        }
      else
        {
         Print("Ошибка при создании "
               "корневой папки эксперта: ",ErrorDescription(GetLastError()));
         return("");
        }
     }
//--- Если нужная папка существует
   if(is_root_folder)
      //--- Вернем путь, в котором будет создан файл для записи параметров эксперта
      return(search_path+"\\");
//--- Если были ошибки, вернем пустую строку
   return("");
  }

Теперь объединим вызовы описанных выше функций в одну функцию InitializeInputParametersArrays(). В этой функции рассмотрены 4 варианта инициализации входных параметров при работе с экспертом:

  1. Штатный режим работы (или оптимизация параметров на выбранном символе) с использованием текущих значений входных параметров
  2. Перезапись параметров в файлах при тестировании или оптимизации
  3. Тестирование выбранного символа
  4. Тестирование всех символов из списка в файле

Все подробно расписано в комментариях к коду:

//+-------------------------------------------------------------------+
//| Инициализирует массивы входных параметров в зависимости от режима |
//+-------------------------------------------------------------------+
void InitializeInputParametersArrays()
  {
   string path=""; // Для определения папки с файлами входных параметров
//--- Режим #1 :
//    - штатный режим работы советника ИЛИ
//    - режим оптимизации ИЛИ
//    - чтение из входных параметров советника без перезаписи файла
   if(IsRealtime() || IsOptimization() || (ParametersReadingMode==INPUT_PARAMETERS && !RewriteParameters))
     {
      //--- Инициализируем массивы параметров текущими значениями
      InitializeWithCurrentValues();
      return;
     }
//--- Режим #2 :
//    - перезапись параметров в файле для указанного символа
   if(RewriteParameters)
     {
      //--- Инициализируем массивы параметров текущими значениями
      InitializeWithCurrentValues();
      //--- Если папка эксперта существует или при ее создании не возникло ошибок
      if((path=CreateInputParametersFolder())!="")
         //--- Запишем/прочитаем файл параметров символа
         ReadWriteInputParameters(0,path);
      //---
      return;
     }
//--- Режим #3 :
//    - режим тестирования (возможно, в визуальном режиме, без оптимизации) советника НА ВЫБРАННОМ символе
   if((IsTester() || IsVisualMode()) && !IsOptimization() && SymbolNumber>0)
     {
      //--- Если папка эксперта существует или при ее создании не возникло ошибок
      if((path=CreateInputParametersFolder())!="")
        {
         //--- Пройдемся по всем символам (в данном случае количество символов = 1)
         for(int s=0; s<SYMBOLS_COUNT; s++)
            //--- Запишем или прочитаем файл параметров символа
            ReadWriteInputParameters(s,path);
        }
      return;
     }
//--- Режим #4 :
//    - режим тестирования (возможно, в визуальном режиме, без оптимизации) советника НА ВСЕХ символах
   if((IsTester() || IsVisualMode()) && !IsOptimization() && SymbolNumber==0)
     {
      //--- Если директория эксперта существует и
      //    при ее создании не возникло ошибок
      if((path=CreateInputParametersFolder())!="")
        {
         //--- Пройдемся по всем символам
         for(int s=0; s<SYMBOLS_COUNT; s++)
            //--- Запишем или прочитаем файл параметров символа
            ReadWriteInputParameters(s,path);
        }
      return;
     }
  }

Функция InitializeWithCurrentValues() используется в режимах #1 и #2. В ней производится инициализация нулевого (единственного) индекса текущими значениями входных параметров. Другими словами, эта функция используется тогда, когда нужен только один символ:

//+------------------------------------------------------------------+
//| Инициализирует массивы входных параметров текущими значениями    |
//+------------------------------------------------------------------+
void InitializeWithCurrentValues()
  {
   InputIndicatorPeriod[0]=IndicatorPeriod;
   InputTakeProfit[0]=TakeProfit;
   InputStopLoss[0]=StopLoss;
   InputTrailingStop[0]=TrailingStop;
   InputReverse[0]=Reverse;
   InputLot[0]=Lot;
   InputVolumeIncrease[0]=VolumeIncrease;
   InputVolumeIncreaseStep[0]=VolumeIncreaseStep;
  }

Теперь самое главное и одновременно самое простое - организовать последовательный вызов вышеописанных функций из точки входа - функции OnInit():

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
void OnInit()
  {
//--- Инициализируем массив тестируемых входных параметров для записи в файл
   InitializeTestedParametersValues();
//--- Заполним массив с названиями символов
   InitializeInputSymbols();
//--- Установим размер массивов входных параметров
   ResizeInputParametersArrays();
//--- Инициализируем массивы хэндлов индикаторов
   InitializeIndicatorHandlesArrays();
//--- Инициализируем массивы входных параметров в зависимости от режима работы советника
   InitializeInputParametersArrays();
//--- Получим хэндлы агентов из индикатора "EventsSpy.ex5"
   GetSpyHandles();
//--- Получим хэндлы индикаторов
   GetIndicatorHandles();
//--- Инициализируем новый бар
   InitializeNewBarArray();
//--- Инициализируем массивы цен и индикаторных буферов
   ResizeDataArrays();
  }

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


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

Как уже было рассказано выше, в общей папке клиентских терминалов должен быть файл TestedSymbols.txt со списком символов. Для примера/теста составим список из трех символов: AUDUSD, EURUSD, NZDUSD. Теперь последовательно оптимизируем входные параметры для каждого символа отдельно. Настройки тестера установим так, как показано на рисунке ниже:

Рис. 2. Настройки тестера

Рис. 2. Настройки тестера.

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

Рис. 3. Входные параметры эксперта

Рис. 3. Входные параметры эксперта.

На рисунке выше видно, что для параметра SymbolNumber ("Номер тестируемого символа") установлено значение 1. Это значит, что при запуске оптимизации эксперт будет использовать первый символ из списка в файле TestedSymbols.txt. В данном случае AUDUSD.

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

В одной из следующих статей мы постараемся обойти это ограничение.

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

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

Рис. 4. Список входных параметров в файле символа

Рис. 4. Список входных параметров в файле символа.

После того, как нужное сочетание параметров подобрано, чтобы их сохранить, нужно в параметре RewriteParameters ("Перезапись параметров") установить значение true и снова запустить тест. Файл с параметрами будет обновлен. Затем, если это нужно, можно снова установить значение false и посмотреть другие результаты проходов оптимизации. Еще удобно сравнивать результаты по значениям, которые записаны в файл, и теми, которые установлены во входных параметрах, просто переключая варианты параметра Режим чтения параметров.

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

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

Рис. 5. Совокупный результат мультивалютного советника

Рис. 5. Совокупный результат мультивалютного советника.


Заключение

Получилась довольно удобная схема для мультивалютных экспертов, которую можно при желании развить дальше. В приложении вы можете загрузить архив с файлами эксперта для изучения. После распаковки архива поместите папку UnlimitedParametersEA в папку <папка терминала MetaTrader 5>\MQL5\Experts. Индикатор EventsSpy.mq5 нужно поместить в директорию <папка терминала MetaTrader 5>\MQL5\Indicators. Кроме того, не забудьте создать текстовый файл TestedSymbols.txt в общей папке клиентских терминалов.