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

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

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

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

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

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

Думаю задача понятна. Начнём...

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

Ниже представлен список внешних параметров эксперта:

//+------------------------------------------------------------------+
//| ВНЕШНИЕ ПАРАМЕТРЫ ЭКСПЕРТА                                       |
//+------------------------------------------------------------------+
sinput long                  MagicNumber  = 777;      // Magic Number
sinput int                   Deviation    = 10;       // Deviation (p)
//---
sinput string dlm_00=""; // * * * * * * * * * * * * * * * * * * * * * * * * * *
//---
sinput int                   NumberSymbol = 1;        // Number Symbol
sinput bool                  ReWrtParams  = false;    // ReWrite Parameters
sinput ENUM_MODE_READ_PARAMS ModeReadPrms = MRP_FILE; // Mode Read Parameters
//---
sinput string dlm_01=""; // * * * * * * * * * * * * * * * * * * * * * * * * * *
//---
input  int                   PeriodInd    = 5;        // Period Indicator
input  double                TakeProfit   = 100;      // Take Profit (p)
input  double                StopLoss     = 50;       // Stop Loss (p)
input  double                TrailingSL   = 10;       // Step Trailing Stop (p)
input  bool                  Reverse      = true;     // On/Off Reverse
input  double                Lot          = 0.1;      // Lot
input  double                Increase     = 0.1;      // Increase Volume
input  double                StepIncrease = 10;       // Step Increase (p)
//---

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

  • - Number Symbol - в этом параметре указывается номер символа из файла, в котором содержится список символов. Если установлено нулевое значение, то будут тестироваться все символы из списка. Если выйти за пределы списка, то тест проводиться не будет.
  • - ReWrite Parameters - если значение этого параметра установлено в true, то файл с параметрами указанного символа (номер в параметре Number Symbol) будет перезаписан текущими значениями внешних параметров. Если же установлено значение false, то параметры будут читаться из файла.
  • - Mode Read Parameters - этот параметр связан с перечислением ENUM_MODE_READ_PARAMS (код ниже). В этом перечислении есть два варианта: читать из файла и использовать текущие параметры.

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

//+------------------------------------------------------------------+
//| ПЕРЕЧИСЛЕНИЕ РЕЖИМОВ ЧТЕНИЯ ВНЕШНИХ ПАРАМЕТРОВ                   |
//+------------------------------------------------------------------+
enum ENUM_MODE_READ_PARAMS
  {
   MRP_FILE = 0, // FILE
   MRP_INPP = 1  // INPUT PARAMETERS
  };
//---

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

//+------------------------------------------------------------------+
//| ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ                                            |
//+------------------------------------------------------------------+
// Кол-во торгуемых символов указанных в настройках (рассчитывается)
// Зависит от режима тестирования и кол-ва символов в списке файла
int CNTS=0;
//---

В начале файла в качестве констант объявим ещё две:

//---
#define SZTP 8 // Количество тестируемых/оптимизируемых параметров
#define TRM_CDP TerminalInfoString(TERMINAL_COMMONDATA_PATH) // Общая папка всех клиентских терминалов
//---

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

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

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

//+------------------------------------------------------------------+
//| МАССИВЫ                                                          |
//+------------------------------------------------------------------+
//| МАССИВЫ ВНЕШНИХ ПАРАМЕТРОВ                                       |
//+------------------------------------------------------------------+
string aSymbol[];       // Symbol
int    aPeriodInd[];    // Period Indicator
double aTakeProfit[];   // Take Profit (p)
double aStopLoss[];     // Stop Loss (p)
double aTrailingSL[];   // Step Trailing Stop (p)
bool   aReverse[];      // On/Off Reverse
double aLot[];          // Lot
double aIncrease[];     // Increase Volume
double aStepIncrease[]; // Step Increase (p)
//---
//+------------------------------------------------------------------+
//| МАССИВЫ ХЭНДЛОВ ИНДИКАТОРОВ                                      |
//+------------------------------------------------------------------+
int
hdlSpyInd[], // Массив хэндлов для индикаторов-агентов
hdlSgnInd[]; // Массив хэндлов сигнальных индикаторов
//---
//+------------------------------------------------------------------+
//| МАССИВЫ ЦЕН И ЗНАЧЕНИЙ ИНДИКАТОРОВ                               |
//+------------------------------------------------------------------+
struct Buffer { double dBuf[]; };
//---
Buffer prc_c[]; // Close (цена закрытия бара)
Buffer prc_o[]; // Open (цена открытия бара)
Buffer prc_h[]; // High (цена максимума бара)
Buffer prc_l[]; // Low (цена минимума бара)
//---
Buffer bfr_ind[]; // Массив для значений индикаторов
//---
//+------------------------------------------------------------------+
//| МАССИВЫ ДАТЫ И ВРЕМЕНИ                                           |
//+------------------------------------------------------------------+
struct Buffer2 { datetime dtBuf[]; };
//---
Buffer2 lastbar_time[]; // Массив для получения времени открытия текущего бара
//----
datetime aNewBar[]; // Массив для проверки нового бара
//---
//+------------------------------------------------------------------+
//| МАССИВЫ ДЛЯ РАБОТЫ С ФАЙЛАМИ                                     |
//+------------------------------------------------------------------+
string arrTestParams[SZTP]=
  {
   "PeriodInd",   // Period Indicator
   "TakeProfit",  // Take Profit (p)
   "StopLoss",    // Stop Loss (p)
   "TrailingSL",  // Step Trailing Stop (p)
   "Reverse",     // On/Off Reverse
   "Lot",         // Lot
   "Increase",    // Increase Volume
   "StepIncrease" // Step Increase (p)
  };
//---
string aTmpSmb[]; // Массив для непроверенных символов
double arrTParams[]; // Массив, в котором сохраняются параметры из файла
//---
double arrTestVParams[SZTP]; // Массив значений внешних параметров
//---

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

//+------------------------------------------------------------------+
//| СВОИ БИБЛИОТЕКИ                                                  |
//+------------------------------------------------------------------+
#include "Include/ENUMS.mqh"
#include "Include/ARRAYS.mqh"
#include "Include/INIT_ARRAYS.mqh"
#include "Include/ERRORS.mqh"
#include "Include/FILE_OPERATIONS.mqh"
#include "Include/TRADE_SIGNALS.mqh"
#include "Include/TRADE_FUNCTIONS.mqh"
#include "Include/GET_STRING.mqh"
#include "Include/ADD_FUNCTIONS.mqh"
//---

Теперь в файле INIT_ARRAYS.mqh создадим функцию InitATestVParams(), в которой будет инициализироваться массив внешних параметров. Он будет использоваться для записи значений параметров в файл. В функции OnInit() она должна быть первой.

//+------------------------------------------------------------------+
//| ИНИЦИАЛИЗАЦИЯ МАССИВА ВНЕШНИХ ПАРАМЕТРОВ                         |
//+------------------------------------------------------------------+
void InitATestVParams()
  {
   arrTestVParams[0]=PeriodInd;
   arrTestVParams[1]=TakeProfit;
   arrTestVParams[2]=StopLoss;
   arrTestVParams[3]=TrailingSL;
   arrTestVParams[4]=Reverse;
   arrTestVParams[5]=Lot;
   arrTestVParams[6]=Increase;
   arrTestVParams[7]=StepIncrease;
  }
//---

Далее следует функция InitASymbols(). Она может быть довольно сложная для начинающих, поэтому я очень подробно прокомментировал её код.

//+------------------------------------------------------------------+
//| ЗАПОЛНЯЕТ МАССИВ СИМВОЛОВ                                        |
//+------------------------------------------------------------------+
void InitASymbols()
  {
   int nmbS=0; // Для корректировки параметра NumberSymbol
   int cntTmpS=0; // Кол-во строк в файле символов
   string check_smb=""; // Для проверки символов
//---
// Получим количество строк из списка символов в файле и
// заполним временный массив символов
   cntTmpS=GetTSymbolsFromFile();
//---
// Если сейчас режим оптимизации или
// один из двух режимов (тестирование или визуализация) и указан один символ
   if(Optimization() || ((Testing() || VisualMode()) && NumberSymbol>0))
     {
      // Скорректируем значение номера символа,
      // если сейчас режим оптимизации
      if(Optimization())
        {// То есть, если ноль, то используем первый символ в списке
         if(NumberSymbol==0) { nmbS=1; } else { nmbS=NumberSymbol; }
        }
      else { nmbS=NumberSymbol; } // Иначе используем указанный номер
      //---
      // Пройдёмся в цикле и определим символ,
      // который будет участвовать в оптимизации параметров
      for(int s=0; s<cntTmpS; s++)
        {
         // Если указанный в параметрах номер и
         // текущий индекс цикла совпали
         if(s==nmbS-1)
           {
            // Если после проверки символа возвращена корректная строка, то...
            if((check_smb=CheckGetSymbol(aTmpSmb[s]))!="")
              {
               CNTS++; // ...увеличим счётчик,
               ArrayResize(aSymbol,CNTS); // установим размер массива и
               aSymbol[CNTS-1]=check_smb; // проиндексируем именем символа
              }
            //---
            return; // Выход из функции
           }
        }
     }
//---
// Если сейчас режим тестирования или визуализации и
// нужно протестировать все символы из списка в файле 
   if((Testing() || VisualMode()) && NumberSymbol==0)
     {
      // Пройдём по всем символам в файле
      for(int s=0; s<cntTmpS; s++)
        {
         // Если после проверки символа возвращена корректная строка, то...
         if((check_smb=CheckGetSymbol(aTmpSmb[s]))!="")
           {
            CNTS++; // ...увеличим счётчик,
            ArrayResize(aSymbol,CNTS); // установим/увеличим размер массива и
            aSymbol[CNTS-1]=check_smb; // проиндексируем именем символа
           }
        }
     }
  }
//---

Также в функции InitASymbols() используются ещё две пользовательские функции: GetTSymbolsFromFile() и CheckGetSymbol(). Они выделены в коде выше. Их код представлен ниже, также с подробными комментариями для лёгкого изучения.

В функции  GetTSymbolsFromFile() открывается текстовый файл TestSymbols.txt, который должен находиться в общей папке терминала в директории C:\ProgramData\MetaQuotes\Terminal\Common\Files. Уточняйте путь к общей папке с помощью функции и идентификатора, которые приводились выше.

Для теста, который проведём после разбора всего кода схемы, в файле TestSymbols.txt составим такой список символов, как показано на рисунке ниже:

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

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

//+------------------------------------------------------------------+
//| СЧИТАЕТ КОЛИЧЕСТВО СТРОК (СИМВОЛОВ) В ФАЙЛЕ И                    |
//| ЗАПОЛНЯЕТ ВРЕМЕННЫЙ МАССИВ СИМВОЛОВ                              |
//+------------------------------------------------------------------+
// При подготовке файла после ввода каждой строки
// нужно обязательно нажимать Enter для перехода на следующую строку
int GetTSymbolsFromFile()
  {
   int hFl=-1; // Хэндл файла
   int cntStr=0; // Счётчик строк
   string nm_file="TestSymbols.txt"; // Имя файла, в котором нужно посчитать строки
//---
// Откроем файл nm_file (получим хэндл) для чтения в общей папке терминала
   hFl=FileOpen(nm_file,FILE_READ|FILE_ANSI|FILE_COMMON);
//---
// Если хэндл файла получен
   if(hFl!=INVALID_HANDLE)
     {
      ulong tseek=0; // Для определения положения указателя
      string txt_string=""; // Для проверки прочитанной строки
      //---
      // Читать пока текущее положение файлового указателя
      // не окажется в конце файла или программа не будет удалена
      while(!FileIsEnding(hFl) || !IsStopped())
        {
         // Читать до конца строки или пока программа не будет удалена
         while(!FileIsLineEnding(hFl) || !IsStopped())
           {
            txt_string=FileReadString(hFl); // Прочитаем всю строку
            tseek=FileTell(hFl); // Получим положение указателя
            //---
            if(FileIsLineEnding(hFl)) // Если это конец строки
              {
               // Переход на другую строку, если это не конец файла
               // Для этого увеличим счётчик указателя файла и...
               if(!FileIsEnding(hFl)) { tseek++; }
               //---
               // ...переведём его на следующую строку
               FileSeek(hFl,tseek,SEEK_SET);
               //---
               if(txt_string!="") // Если строка не пустая...
                 {
                  cntStr++; // ...увеличим счётчик строк,
                  ArrayResize(aTmpSmb,cntStr); // увеличим размер массива строк,
                  aTmpSmb[cntStr-1]=txt_string; // занесём в текущий индекс строку
                 }
               //---
               break; // Выйдем из этого цикла
              }
           }
         //---
         // Если это конец файла прервём весь цикл
         if(FileIsEnding(hFl)) { break; }
        }
      //---
      FileClose(hFl); // Закроем файл
     }
//---
   return(cntStr); // Вернём количество строк в файле
  }
//---

Далее можно посмотреть код функции CheckGetSymbol(). В ней производится проверка на корректность переданного имени символа и факт существования этого символа в списке символов сервера.

//+------------------------------------------------------------------+
//| ДОБАВЛЯЕТ УКАЗАННЫЙ СИМВОЛ В ОКНО ОБЗОР РЫНКА                    |
//+------------------------------------------------------------------+
string CheckGetSymbol(string symbol)
  {
   int symbol_total=0; // Количество символов
   string nm_symbol=""; // Имя символа
//---
// Если передали пустую строку, то
// ...вернуть пустую строку
   if(symbol=="") { return(""); }
//---
   symbol_total=SymbolsTotal(false); // Всего символов на сервере
//---
// Пройтись по всему списку символов
   for(int s=symbol_total-1; s>=0; s--)
     {
      nm_symbol=SymbolName(s,false); // Имя символа на сервере
      //---
      // Если есть такой символ, то...
      if(nm_symbol==symbol)
        {
         // ...установим его в окно Обзор Рынка и...
         SymbolSelect(nm_symbol,true);
         return(symbol); // ...вернём его имя
        }
     }
//---
// Если такого символа нет, то вернём пустую строку
   return("");
  }
//---

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

//+------------------------------------------------------------------+
//| УСТАНАВЛИВАЕТ НОВЫЙ РАЗМЕР ДЛЯ МАССИВОВ ВНЕШНИХ ПАРАМЕТРОВ       |
//+------------------------------------------------------------------+
void ResizeAInputParams()
  {
   ArrayResize(aPeriodInd,CNTS);
   ArrayResize(aTakeProfit,CNTS);
   ArrayResize(aStopLoss,CNTS);
   ArrayResize(aTrailingSL,CNTS);
   ArrayResize(aReverse,CNTS);
   ArrayResize(aLot,CNTS);
   ArrayResize(aIncrease,CNTS);
   ArrayResize(aStepIncrease,CNTS);
  }
//---

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

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

//+------------------------------------------------------------------+
//| ИНИЦИАЛИЗАЦИЯ МАССИВА ПАРАМЕТРОВ ПО ТЕКУЩЕМУ РЕЖИМУ              |
//+------------------------------------------------------------------+
void InitAParamsByMode()
  {
   string lpath=""; // Для определения директории
//---
// 01
// Если сейчас реал-тайм (вне тестера) или
// режим оптимизации или
// указан режим чтения из внешних параметров и не нужно перезаписывать файл
   if(NotTest() || Optimization() || (ModeReadPrms==MRP_INPP && !ReWrtParams))
     {
      // ...проиндексируем массивы текущими параметрами
      InitACurrParams();
      //---
      return;
     }
//---
// 02
// Если нужно перезаписать параметры для указанного символа
   if(ReWrtParams)
     {
      // ...проиндексируем массив текущими параметрами
      InitACurrParams();
      //---
      // Если директория эксперта существует и
      // при её создании не возникло ошибок
      if((lpath=CheckCreateGetTestPath())!="")
        {
         // Запишем или прочитаем файл параметров символа
         WriteReadTestInpParams(0,lpath);
        }
      //---
      return;
     }
//---
// 03
// Если сейчас режим тестирования или визуализации и
// не оптимизация и 
// указано протестировать один символ
   if((Testing() || VisualMode()) && !Optimization() && NumberSymbol>0)
     {
      // Если директория эксперта существует и
      // при её создании не возникло ошибок
      if((lpath=CheckCreateGetTestPath())!="")
        {
         // Пройдёмся по всем символам
         for(int s=0; s<CNTS; s++)
           {
            // Запишем или прочитаем файл параметров символа
            WriteReadTestInpParams(s,lpath);
           }
        }
      //---
      return;
     }
//---
// 04
// Если сейчас режим тестирования или визуализации и
// не оптимизация и
// нужно протестировать все символы
   if((Testing() || VisualMode()) && !Optimization() && NumberSymbol==0)
     {
      // Если директория эксперта существует и
      // при её создании не возникло ошибок
      if((lpath=CheckCreateGetTestPath())!="")
        {
         // Пройдёмся по всем символам
         for(int s=0; s<CNTS; s++)
           {
            // Запишем или прочитаем файл параметров символа
            WriteReadTestInpParams(s,lpath);
           }
        }
      //---
      return;
     }
  }
//---

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

Функция InitACurrParams() используется в блоках кода выше под номерами 01 и 02. В ней производится инициализация нулевого (единственного) индекса текущими внешними параметрами. Другими словами, эта функция используется тогда, когда нужен только один символ. Ниже можно ознакомиться с её кодом:

//+------------------------------------------------------------------+
//| ИНИЦИАЛИЗАЦИЯ МАССИВОВ ВНЕШНИХ ПАРАМЕТРОВ ТЕКУЩИМИ ЗНАЧЕНИЯМИ    |
//+------------------------------------------------------------------+
void InitACurrParams()
  {
   aPeriodInd[0]=PeriodInd;
   aTakeProfit[0]=TakeProfit;
   aStopLoss[0]=StopLoss;
   aTrailingSL[0]=TrailingSL;
   aReverse[0]=Reverse;
   aLot[0]=Lot;
   aIncrease[0]=Increase;
   aStepIncrease[0]=StepIncrease;
  }
//---

В блоках 02, 03 и 04 используются функции CheckCreateGetTestPath() и WriteReadTestInpParams(). В первой производится проверка на существование папки эксперта в общей папке терминала. Ниже представлен её код:

//+------------------------------------------------------------------+
//| ПРОВЕРЯЕТ ДИРЕКТОРИЮ ДЛЯ ВНЕШНИХ ПАРАМЕТРОВ, СОЗДАЁТ ЕСЛИ ЕЁ НЕТ |
//+------------------------------------------------------------------+
string CheckCreateGetTestPath()
  {
   long hdlSearch=-1; // Хэндл поиска папки/файла
   string root=NAME_EXPERT+"\\"; // Корневая папка эксперта
   string
   ffname="",// Имя найденного файла/папки
   lpath="", // Путь для поиска
   folder="*"; // Фильтр поиска (* - проверить все файлы/папки)
   bool
   flgROOT=false; // Флаг существования/отсутствия корневой папки эксперта
   string delUser="------\nПользователь удалил эксперта!";
//---
// ИЩЕМ КОРНЕВУЮ ПАПКУ ЭКСПЕРТА
   lpath=folder;
//---
// Установим хэндл поиска в общей папке терминала
   hdlSearch=FileFindFirst(lpath,ffname,FILE_COMMON);
//---
// Если первая папка корневая, ставим флаг
   if(ffname==root) { flgROOT=true; }
//---
   if(hdlSearch!=INVALID_HANDLE) // Если хэндл поиска получен
     {
      if(!flgROOT) // Если первая папка была не корневой
        {
         // Перебираем все файлы с целью поиска корневой папки
         while(FileFindNext(hdlSearch,ffname))
           {
            // Выполнение прервано пользователем
            if(IsStopped()) { Print(delUser); return(""); }
            //---
            // Если находим, то ставим флаг
            if(ffname==root) { flgROOT=true; break; }
           }
        }
      //---
      // Закроем хэндл поиска корневой папки
      FileFindClose(hdlSearch);
      hdlSearch=INVALID_HANDLE;
     }
   else
     {
      Print("Ошибка при получении хэндла поиска либо "
            "директория "+TRM_CDP+" пуста: ",ErrorDesc(GetLastError()));
     }
//---
// ПО РЕЗУЛЬТАТАМ ПРОВЕРКИ СОЗДАДИМ НУЖНУЮ ДИРЕКТОРИЮ
   lpath=NAME_EXPERT+"\\";
//---
// Если нет корневой папки эксперта
   if(!flgROOT)
     {
      // ...создадим её. Если папка создана...
      if(FolderCreate(NAME_EXPERT,FILE_COMMON))
        {
         flgROOT=true; // ...установим флаг
         Print("Создана корневая папка эксперта ..""\\"+NAME_EXPERT+"""\\");
        }
      else
        {
         Print("Ошибка при создании "
               "корневой папки эксперта: ",ErrorDesc(GetLastError()));
         //---
         return("");
        }
     }
//---
// Если есть нужная директория, то...
   if(flgROOT)
     {
      // ...вернём путь, в котором будет 
      // создан файл для записи параметров эксперта
      return(lpath+"\\");
     }
//---
   return("");
  }
//---

Если папка эксперта есть или создана без ошибок, то функция CheckCreateGetTestPath() вернёт путь, где нужно создать/прочитать/переписать (зависит от режима/условий) файл с параметрами эксперта. И далее программа перейдёт к функции WriteReadTestInpParams(), в которой производятся вышеперечисленные действия.

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

//+------------------------------------------------------------------+
//| ЗАПИСЬ/ЧТЕНИЕ ПАРАМЕТРОВ В/ИЗ ФАЙЛ-(А)                           |
//+------------------------------------------------------------------+
void WriteReadTestInpParams(int s,string lpath)
  {
   int hFl=-1; // Хэндл файла
   string nm_fl=lpath+aSymbol[s]+".ini"; // Имя файла
//---
   Print("Ищем файл < "+nm_fl+" > ...");
//---
// Откроем файл с параметрами символа
   hFl=FileOpen(nm_fl,FILE_READ|FILE_ANSI|FILE_COMMON);
//---
// Если файл есть/открылся и не нужно перезаписывать параметры
   if(hFl!=INVALID_HANDLE && !ReWrtParams)
     {
      Print("Файл < "+aSymbol[s]+".ini > существует, читаем...");
      //---
      // Получим параметры из файла
      // Установим размер массива
      ArrayResize(arrTParams,SZTP);
      //---
      // Заполним массив значениями из файла
      GetValuesParamsFromFile(hFl,arrTParams);
      //---
      // Если размер массива корректен, то ...
      if(ArraySize(arrTParams)==SZTP)
        {
         // ...установим параметры в переменные
         //---
         aPeriodInd[s]=(int)arrTParams[0];
         aTakeProfit[s]=arrTParams[1];
         aStopLoss[s]=arrTParams[2];
         aTrailingSL[s]=arrTParams[3];
         aReverse[s]=arrTParams[4];
         aLot[s]=arrTParams[5];
         aIncrease[s]=arrTParams[6];
         aStepIncrease[s]=arrTParams[7];
        }
      //---
      FileClose(hFl); return; // Закроем файл и выйдем
     }
   else
     {
      // Сбросим хэндл файла, если он валиден
      if(hFl!=INVALID_HANDLE) { FileClose(hFl); }
     }
//---
// Если файла нет или нужно перезаписать параметры
   if(hFl==INVALID_HANDLE || ReWrtParams)
     {
      // При создании файла, запишем текущие параметры эксперта
      //---
      // Получим хэндл файла для записи
      int hFl2=FileOpen(nm_fl,FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON,"");
      //---
      if(hFl2!=INVALID_HANDLE) // Если хэндл получен
        {
         string sep="="; // Разделитель
         //---
         // Названия параметров и их значения берутся из массивов в файле ARRAYS.mqh
         for(int i=0; i<SZTP; i++)
           {
            FileWrite(hFl2,arrTestParams[i],sep,arrTestVParams[i]);
            Print(arrTestParams[i],sep,arrTestVParams[i]);
           }
         //---
         if(ReWrtParams)
           { Print("Перезаписан файл < "+aSymbol[s]+".ini > с параметрами эксперта < "+NAME_EXPERT+".ex5 >"); }
         else
           { Print("Создан файл < "+aSymbol[s]+".ini > с параметрами эксперта < "+NAME_EXPERT+".ex5 >"); }
        }
      //---
      FileClose(hFl2); // Закроем файл
     }
  }
//---

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

//+------------------------------------------------------------------+
//| ЧИТАЕТ ПАРАМЕТРЫ ИЗ ФАЙЛА И ПОМЕЩАЕТ В ПЕРЕДАННЫЙ МАССИВ         |
//+------------------------------------------------------------------+
bool GetValuesParamsFromFile(int hdl,double &array[])
  {
   int
   sgn=0,// Номер знака "="
   cnts=0; // Счётчик строк
   string str=""; // Прочитанная строка
   ulong tell_seek=0,tseek=0; // Положение указателя
   string delUser="------\nПользователь удалил программу с графика!";
//---
   FileSeek(hdl,0,SEEK_SET); // Переместим указатель в начало
//---
// Читать пока текущее положение файлового указателя не окажется в конце файла
   while(!FileIsEnding(hdl))
     {
      if(IsStopped()) // Если пользователь удалил программу
        { Print(delUser); return(false); }
      //---
      while(!FileIsLineEnding(hdl)) // Читаем пока не конец файла
        {
         if(IsStopped()) // Если пользователь удалил программу
           { Print(delUser); return(false); }
         //---
         str=FileReadString(hdl); // Прочитаем строку
         Print(str);
         //---
         sgn=StringFind(str,"=",0); // Получим номер разделителя
         str=StringSubstr(str,sgn+1); // Заберём всё, что после разделителя
         array[cnts]=SD(str); // Поместим значение в массив сконвертировав его в тип double
         //---
         tell_seek=FileTell(hdl); // Получим положение указателя
         //---
         if(FileIsLineEnding(hdl)) // Если это конец строки
           {
            // Переход на другую строку, если это не конец файла
            if(!FileIsEnding(hdl))
              { tseek=tell_seek+1; } // Увеличим счётчик для указателя
            //---
            // Переместим указатель
            FileSeek(hdl,tseek,SEEK_SET); cnts++; break;
           }
        }
      //---
      // Если это конец файла, то выйдем из цикла
      if(FileIsEnding(hdl)) { break; }
     }
//---
// Вернём факт успешного завершения
   return(true);
  }
//---

Весь процесс, который описывался выше производится в функции OnInit():

//+------------------------------------------------------------------+
//| ИНИЦИАЛИЗАЦИЯ                                                    |
//+------------------------------------------------------------------+
void OnInit()
  {
   InitATestVParams(); // Инициализация массива значений внешних параметров
   InitASymbols(); // Инициализация массива символов
   ResizeAInputParams(); // Установим размер массивов внешних параметров
//---
   InitAParamsByMode(); // Инициализация параметров по текущему режиму
//---
   InitAHandles(); // Инициализация массивов хэндлов индикаторов
   GetHandlesSpy(); // Получаем хэндлы агентов
   GetHandlesIndicators(); // Получаем хэндлы индикаторов
   InitANewBar(); // Инициализируем новый бар
   InitAPrcBufInd(); // Инициализируем массивы цен и буферов
  }
//---

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

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

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

Настройки тестера

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

Внешние параметры эксперта

На рисунке выше видно, что в параметре Number Symbol установлено значение 1. Это значит, что при запуске оптимизации эксперт  будет использовать первый символ из списка в файле символов. В данном случае AUDUSD.

После того, как оптимизация закончится, можно как и обычно запускать тесты изучая результаты разных проходов оптимизации. Только, если Вам нужно, чтобы эксперт читал параметры из файла в параметре Mode Read Parameters нужно выбрать вариант FILE из выпадающего списка. Если же нужно, чтобы параметры брались из внешних параметров эксперта, то нужно выбрать вариант INPUT PARAMETERS. При просмотре результатов оптимизации конечно же нужен вариант INPUT PARAMETERS. При первом запуске теста эксперт создаст папку со своим именем в общей папке терминала и в ней файл с текущими параметрами тестируемого символа. В данном случае это будет файл AUDUSD.ini. На рисунке ниже показано содержание этого файла:

Список внешних параметров в файле символа

После того, как нужное сочетание параметров подобрано, для того, чтобы их сохранить, нужно в параметре ReWrite Parameters установить значение true и снова запустить тест. Файл с параметрами будет обновлён. Затем, если это нужно, можно снова установить значение false и посмотреть другие результаты проходов оптимизации.

Ещё удобно сравнивать результаты по значениям, которые записаны в файл и теми, которые установлены во внешних параметрах, просто переключая варианты параметра Mode Read Parameters.

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

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

Ниже показан совокупный результат по всем символам:

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

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

Успехов!




Скачать архив с файлами эксперта expMltSmbMktWnd.zip
Скачать индикатор-агент EventsSpy.mq5


Комментариев нет :

Отправить комментарий