Скачать MetaTrader 5

LifeHack для трейдера: один бэк-тест хорошо, а четыре – лучше

8 августа 2016, 17:22
Vladimir Karputov
9
4 067

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

Оглавление


Введение

Основная цель данной статьи — показать, как из одного терминала (назовём его Мастер-терминал) запустить одиночное тестирование (не оптимизацию, а именно тестирование!) советника сразу в четырёх терминалах (назовём их Подчинённые терминалы и присвоим им обозначения #1, #2, #3 и #4). При этом тестеры стратегий в Подчиненных терминалах будут запущены в разных режимах генерации тиков:

  • в терминале #1 — "Каждый тик на основе реальных тиков";
  • в терминале #2 —  "Все тики";
  • в терминале #3 — "OHLC на M1";
  • в терминале #4 — "Только цены открытия".

Важные ограничения:

  1. Мастер-терминал должен запускаться без ключа /portable.
  2. Нужно иметь как минимум пять установленных терминалов MetaTrader 5.
  3. Торговый счёт в Мастер-терминале — назовём его Мастер-счёт — должен быть хотя бы один раз запущен в каждом из Подчинённых терминалов. Это нужно, потому что эксперт из данной статьи не передаёт пароль от торгового счёта через ini-файлы в Подчинённые терминалы. Вместо пароля передаётся только номер торгового счёта, на котором нужно запустить тестер стратегий, и этот номер всегда совпадает с номером Мастер-счёта.
    Такое поведение видится логичным, поскольку тестирование советника в разных режимах генерации тиков нужно проводить на одном и том же торговом счёте.
  4. Перед запуском эксперта максимально разгрузите центральный процессор: выключите онлайн-игры, медиаплеер и другие ресурсоемкие программы. В противном случае, одно из ядер процессора может быть заблокировано, и на этом ядре не запустится тестирование.


1. Общие принципы

Що занадто, то не здраво (польская пословица). 

Я всегда предпочитаю использование стандартных возможностей программного обеспечения. Применительно к торговым терминалам MetaTrader 5 это правило звучит так: "никогда не запускать терминал с ключом /portable, никогда не отключать в операционной системе контроль учётных записей пользователей (UAC)". Исходя из этого, работу с файлами описываемый советник будет проводить в папке AppData.

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

Рассматриваемый советник, наряду с возможностями MQL5, широко применяет dll:

dll

Рис. 1. Зависимости 

В частности, использованы вызовы таких Windows API функций:


2. Входные параметры

inputs

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

Пути folder of the MetaTrader#Х installation — это пути к папкам установки Подчинённых терминалов. При задании пути в mq5 коде нужно прописывать двойной слэш. Также очень важно ставить двойной обратный слэш в конце пути:

//--- input parameters                                 
input string   ExtInstallationPathTerminal_1="C:\\Program Files\\MetaTrader 5 1\\";    // folder of the MetaTrader#1 installation
input string   ExtInstallationPathTerminal_2="D:\\MetaTrader 5 2\\";                   // folder of the MetaTrader#2 installation
input string   ExtInstallationPathTerminal_3="D:\\MetaTrader 5 3\\";                   // folder of the MetaTrader#3 installation
input string   ExtInstallationPathTerminal_4="D:\\MetaTrader 5 4\\";                   // folder of the MetaTrader#4 installation
input string   ExtTerminalName="terminal64.exe";                                       // correct name of the file of the terminal

Название терминала "terminal64.exe" приведено для 64-битной операционной системы.

 

Связь между папкой установки и каталогом данных в папке AppData 

При запуске терминала обычным способом или с ключом /portable терминал будет выдавать разные пути для переменной TERMINAL_DATA_PATH. Рассмотрим эту ситуацию на примере Мастер-терминала, который установлен в папку "C:\Program Files\MetaTrader 5 1".

Когда Мастер-терминал запущен с ключом /portable, из этого терминала MQL будет выдавать такие результаты:

TERMINAL_PATH = C:\Program Files\MetaTrader 5
TERMINAL_DATA_PATH = C:\Program Files\MetaTrader 5
TERMINAL_COMMONDATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common

А вот ответ этого же терминала без ключа /portable:

TERMINAL_PATH = C:\Program Files\MetaTrader 5
TERMINAL_DATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962
TERMINAL_COMMONDATA_PATH = C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common

Но это нам пригодится, только если мы будем получать параметры из текущего терминала. А как поступать в случае с Подчинёнными терминалами, в которых мы будем запускать тестирование советника? Как связать пути установки Подчинённых терминалов с путями их каталогов данных?

Здесь нужно пояснить, почему так важно знать путь к каталогам данных в папке AppData (цитата из справки):

Начиная с MS Windows Vista, по умолчанию программам, установленным в каталог Program Files, запрещено сохранять данные в каталоге установки. Все данные должны храниться в отдельном каталоге данных пользователя Windows.

Другими словами, наш эксперт может свободно создавать и изменять файлы в папке, вроде этой: C:\Users\имя пользователя\AppData\Roaming\MetaQuotes\Terminal\идентификатор терминала\MQL5\Files. Здесь "идентификатор терминала" — это идентификатор Мастер-терминала.


3. Сопоставляем папку установки и папку AppData Подчинённых терминалов

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

...
[Tester]
Expert=test
...

Как видите, путь не указывается, а это означает, что тестируемый советник может находиться исключительно в "песочнице" MQL5. На примере Подчинённого терминала 1 это могут быть два пути:

  1. C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Experts
  2. или C:\Program Files\MetaTrader 5 1\MQL5\Experts

Вариант №2 отбрасываем, так как, согласно политике безопасности, начиная с Windows Vista, писать в папку "Program Files" нам запрещено. Остаётся вариант №1 — а это означает, что для всех Подчинённых терминалов мы должны провести сопоставление папок установки и папок в AppData. 

3.1. Секрет №1

В каждом каталоге данных есть файл "origin.txt". На примере Подчинённого терминала 1:

origin.txt 

Рис. 4. Файл origin.txt 

и содержание файла origin.txt:

C:\Program Files\MetaTrader 5 1

Данная запись в файле указывает, что папку "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962" создал терминал, установленный в "C:\Program Files\MetaTrader 5 1".

3.2. FindFirstFileW, FindNextFileW

FindFirstFileW — осуществляет поиск директории для файла или подкаталога с именем, которое совпадает с определенным именем (или частью имени, если используются специальные символы).

HANDLE  FindFirstFileW(
   string           lpFileName,         //
   WIN32_FIND_DATA  &lpFindFileData     //
   ); 

Параметры

lpFileName

[in]  Директория или путь и имя файла, который может включать в себя символы подстановки, например, звездочка (*) или знак вопроса (?).

lpFindFileData

[in][out]  Указатель на структуру WIN32_FIND_DATA, которая получает информацию о найденном файле или директории. 

Возвращаемое значение

Если функция завершается успешно, возвращаемым значением будет хэндл поиска, используемый в последующем вызове FindNextFile or FindClose, а lpFindFileData параметр содержит информацию о первом файле или найденной папке.

Если функция терпит неудачу или не может найти файлы из строки поиска в параметре lpFileName, возвращается значение INVALID_HANDLE_VALUE, и содержимое lpFindFileData будет неопределенным. Чтобы получить дополнительную информацию об ошибке, вызовите функцию  GetLastError.

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


FindNextFileW — продолжает поиск файла из предыдущего вызова функции FindFirstFileFindFirstFileEx, или FindFirstFileTransacted.

bool  FindNextFileW(
   HANDLE           FindFile,           //
   WIN32_FIND_DATA  &lpFindFileData     //
   );

Параметры

FindFile

[in] Хэндл поиска, возвращённый предыдущим вызовом функции FindFirstFile или FindFirstFileEx.

lpFindFileData

[in][out]  Указатель на структуру WIN32_FIND_DATA, которая получает информацию о найденном файле или директории. 

Возвращаемое значение

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

Если функция завершается ошибкой, возвращаемое значение равно нулю, а содержание lpFindFileData будет неопределённым. Чтобы получить дополнительную информацию об ошибке, вызовите функцию  GetLastError.

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

Пример объявления Win API функций FindFirstFileW и FindNextFileW (код взят из включаемого файла ListingFilesDirectory.mqh):

#define MAX_PATH                 0x00000104  //
#define FILE_ATTRIBUTE_DIRECTORY 0x00000010  //
#define ERROR_NO_MORE_FILES      0x00000012  //there are no more files
#define ERROR_FILE_NOT_FOUND     0x00000002  //the system cannot find the file specified
//+------------------------------------------------------------------+
//| FILETIME structure                                               |
//+------------------------------------------------------------------+
struct FILETIME
  {
   uint              dwLowDateTime;
   uint              dwHighDateTime;
  };
//+------------------------------------------------------------------+
//| WIN32_FIND_DATA structure                                        |
//+------------------------------------------------------------------+
struct WIN32_FIND_DATA
  {
   uint              dwFileAttributes;
   FILETIME          ftCreationTime;
   FILETIME          ftLastAccessTime;
   FILETIME          ftLastWriteTime;
   uint              nFileSizeHigh;
   uint              nFileSizeLow;
   uint              dwReserved0;
   uint              dwReserved1;
   ushort            cFileName[MAX_PATH];
   ushort            cAlternateFileName[14];
  };

#import "kernel32.dll"
int      GetLastError();
long     FindFirstFileW(string lpFileName,WIN32_FIND_DATA  &lpFindFileData);
int      FindNextFileW(long FindFile,WIN32_FIND_DATA &lpFindFileData);
int      FindClose(long hFindFile);
int      FindNextFileW(int FindFile,WIN32_FIND_DATA &lpFindFileData);
int      FindClose(int hFindFile);
int      CopyFileW(string lpExistingFileName,string lpNewFileName,bool bFailIfExists);
#import

bool WinAPI_FindClose(long hFindFile)
  {
   bool res;
   if(_IsX64)
      res=FindClose(hFindFile)!=0;      
   else
      res=FindClose((int)hFindFile)!=0;      
//---
   return(res);
  }
  
bool WinAPI_FindNextFile(long hFindFile,WIN32_FIND_DATA &lpFindFileData)
  {
   bool res;
   if(_IsX64)
      res=FindNextFileW(hFindFile,lpFindFileData)!=0;      
   else
      res=FindNextFileW((int)hFindFile,lpFindFileData)!=0;      
//---
   return(res);
  }

3.3. Пример использования FindFirstFileW, FindNextFileW

Скрипт "ListingFilesDirectory.mq5" — одновременно и пример, и практически полная копия рабочего кода советника. Иными словами, этот код максимально приближен к реальности.

Задача: получить имена всех папок для пути TERMINAL_COMMONDATA_PATH — "Common". 

На примере моего компьютера путь TERMINAL_COMMONDATA_PATH возвращает значение "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common". Значит, если отсечь от этого пути "Common", то получим искомый путь "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\":

Find First

Рис. 5. Find First 

Обычно для поиска всех файлов применяется маска поиска "*.*". Значит, нам нужно провести две операции со следующими строками: отсечь слово "Common", а затем добавить маску "*.*":

   string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH);
   int pos=StringFind(common_data_path,"Common",0);
   if(pos!=-1)
     {
      common_data_path=StringSubstr(common_data_path,0,pos-1);
     }
   else
      return;

   string path_addition="\\*.*";
   string mask_path=common_data_path+path_addition;
   printf("mask_path=%s",mask_path);

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

debug 

Рис. 6. Отладка 

Получим:

mask_path=C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*

Пока всё верно: подготовлена маска поиска ВСЕХ файлов и папок в директории "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\".

Идём дальше: инициализируем поисковый хендл "hFind" (в моем случае это вызвано скорее привычкой) и вызываем Win API функцию FindFirstFileW:

   printf("mask_path=%s",mask_path);
   hFind=-100;
   hFind=FindFirstFileW(mask_path,ffd);
   if(hFind==INVALID_HANDLE)
     {
      PrintFormat("Failed FindFirstFile (hFind) with error: %x",kernel32::GetLastError());
      return;
     }

// List all the files in the directory with some info about them

Если вызов FindFirstFileW закончится неудачей, то поисковый хендл "hFind" будет равен "INVALID_HANDLE" и выполнение скрипта закончится.

В случае удачного вызова FindFirstFileW организуем цикл do while, в котором будем получать имя файла или папки, а в конце цикла будет вызвана Win API функция FindNextFileW:

// List all the files in the directory with some info about them
   PrintFormat("hFind=%d",hFind);
   bool rezult=0;
   do
     {
      string name="";
      for(int i=0;i<MAX_PATH;i++)
        {
         name+=ShortToString(ffd.cFileName[i]);
        }
      
      Print("\"",name,"\", File Attribute Constants (dec): ",ffd.dwFileAttributes);
      //---
      ArrayInitialize(ffd.cFileName,0);
      ArrayInitialize(ffd.cAlternateFileName,0);
      ffd.dwFileAttributes=-100;
      ResetLastError();
      rezult=WinAPI_FindNextFile(hFind,ffd);
     }
   while(rezult!=0);
   if(kernel32::GetLastError()!=ERROR_NO_MORE_FILES)
      PrintFormat("Failed FindNextFileW (hFind) with error: %x",kernel32::GetLastError());
   WinAPI_FindClose(hFind);

Цикл do while будет продолжаться, пока вызов Win API функции FindNextFileW возвращает ненулевое значение. Если вызов Win API функции FindNextFileW возвращает ноль и ошибка не равна "ERROR_NO_MORE_FILES" — значит, произошла критическая ошибка.

В конце работы скрипта закрываем поисковый хендл

Результат работы скрипта "ListingFilesDirectory.mq5":

mask_path=C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*
hFind=-847293552
".", File Attribute Constants (dec): 16
"..", File Attribute Constants (dec): 16
"038C9E8FAFF9EA373522ECC6D5159962", File Attribute Constants (dec): 16
"0C46DDCEB43080B0EC647E0C66170465", File Attribute Constants (dec): 16
"2A6A33B25AA0984C6AB9D7F28665B88E", File Attribute Constants (dec): 16
"50CA3DFB510CC5A8F28B48D1BF2A5702", File Attribute Constants (dec): 16
"BC11041F9347CD71C5F8926F53AA908A", File Attribute Constants (dec): 16
"Common", File Attribute Constants (dec): 16
"Community", File Attribute Constants (dec): 16
"D0E8209F77C8CF37AD8BF550E51FF075", File Attribute Constants (dec): 16
"D3852169A6E781B7F35488A051432620", File Attribute Constants (dec): 16
"EE57F715BA53F2E183D6731C9376293D", File Attribute Constants (dec): 16
"Help", File Attribute Constants (dec): 16

3.4. Заглядываем внутрь папок терминалов

Вышеописанный пример продемонстрировал нам работу на верхнем уровне — в папке "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\". Но мы с вами помним о разделе 3.1. Секрет №1, согласно которому, нам нужно заглянуть во все вложенные папки.

Для этого организуем двухуровневый поиск, причем для поиска во вложенных папках нужно использовать такую маску первичного поиска для Win API функции FindFirstFileW:

"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\" + имя найденной папки верхнего уровня + "origin.txt".

Таким образом, первичный поиск FindFirstFileW во вложенной папке будет искать только один файл — "origin.txt".

Привожу полный листинг функции FindDataPath():

//+------------------------------------------------------------------+
//| Find and read the origin.txt                                     |
//+------------------------------------------------------------------+
void FindDataPath(string &array[][2])
  {
//---
   WIN32_FIND_DATA ffd;
   long            hFirstFind_0,hFirstFind_1;

   ArrayInitialize(ffd.cFileName,0);
   ArrayInitialize(ffd.cAlternateFileName,0);
//+------------------------------------------------------------------+
//| Get common path for all of the terminals installed on a computer.|
//| The common path on my computer:                                  |
//| C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common          |
//+------------------------------------------------------------------+
   string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH);
   int pos=StringFind(common_data_path,"Common",0);
   if(pos!=-1)
     {
      //+------------------------------------------------------------------+
      //| Cuts "Common" ... and we get:                                    |
      //| C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal                 |
      //+------------------------------------------------------------------+
      common_data_path=StringSubstr(common_data_path,0,pos-1);
     }
   else
      return;

//--- stage Search №0. 
   string filter_0=common_data_path+"\\*.*"; // filter_0==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*

   hFirstFind_0=FindFirstFileW(filter_0,ffd);
//---
   string str_handle="";
   if(hFirstFind_0==INVALID_HANDLE)
      str_handle="INVALID_HANDLE";
   else
      str_handle=IntegerToString(hFirstFind_0);
   Print("filter_0: \"",filter_0,"\", handle hFirstFind_0: ",str_handle);
//---
   if(hFirstFind_0==INVALID_HANDLE)
     {
      PrintFormat("Failed FindFirstFile (hFirstFind_0) with error: %x",kernel32::GetLastError());
      return;
     }

//--- list all the files in the directory with some info about them
   bool rezult=0;
   do
     {
      if((ffd.dwFileAttributes  &FILE_ATTRIBUTE_DIRECTORY)==FILE_ATTRIBUTE_DIRECTORY)
        {
         string name_0="";
         for(int i=0;i<MAX_PATH;i++)
           {
            name_0+=ShortToString(ffd.cFileName[i]);
           }
         if(name_0!="." && name_0!="..")
           {
            ArrayInitialize(ffd.cFileName,0);
            ArrayInitialize(ffd.cAlternateFileName,0);
            //--- stage Search №1. search origin.txt file in the folder
            string filter_1=common_data_path+"\\"+name_0+"\\origin.txt";
            ResetLastError();
            hFirstFind_1=FindFirstFileW(filter_1,ffd);
            //---
            if(hFirstFind_1==INVALID_HANDLE)
               str_handle="INVALID_HANDLE";
            else
               str_handle=IntegerToString(hFirstFind_1);
            Print("   filter_1: \"",filter_1,"\", handle hFirstFind_1: ",str_handle);
            //---
            if(hFirstFind_1==INVALID_HANDLE)
              {
               if(kernel32::GetLastError()!=ERROR_FILE_NOT_FOUND)
                 {
                  PrintFormat("Failed FindFirstFile (hFirstFind_1) with error: %x",kernel32::GetLastError());
                  break;
                 }
               WinAPI_FindClose(hFirstFind_1);
               ArrayInitialize(ffd.cFileName,0);
               ArrayInitialize(ffd.cAlternateFileName,0);
               ResetLastError();
               rezult=WinAPI_FindNextFile(hFirstFind_0,ffd);
               continue;
              }
            //--- origin.txt file in this folder is found
            bool rezultTwo=0;
            string name_1="";
            for(int i=0;i<MAX_PATH;i++)
              {
               name_1+=ShortToString(ffd.cFileName[i]);
              }
            string origin=CopiedAndReadFile(filter_1); //--- receiving a string of the file found origin.txt
            if(origin!=NULL)
              {
               //--- write a string into an array
               int size=ArrayRange(array,0);
               ArrayResize(array,size+1,0);
               array[size][0]=common_data_path+"\\"+name_0;
               //value array[][0]==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962
               array[size][1]=origin;
               //value array[][1]==C:\Program Files\MetaTrader 5 1\
              }
            WinAPI_FindClose(hFirstFind_1);
           }
        }
      ArrayInitialize(ffd.cFileName,0);
      ArrayInitialize(ffd.cAlternateFileName,0);
      ResetLastError();
      rezult=WinAPI_FindNextFile(hFirstFind_0,ffd);
     }
   while(rezult!=0); //if(hFirstFind_1==INVALID_HANDLE), we appear here
   if(kernel32::GetLastError()!=ERROR_NO_MORE_FILES)
      PrintFormat("Failed FindNextFileW (hFirstFind_0) with error: %x",kernel32::GetLastError());
   else
      Print("filter_0: \"",filter_0,"\", handle hFirstFind_0: ",hFirstFind_0,", NO_MORE_FILES");
   WinAPI_FindClose(hFirstFind_0);
  }

Функция FindDataPath() распечатывает примерно такую информацию:

filter_0: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*", handle hFirstFind_0: 1901014212592
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt", handle hFirstFind_1: 1901014213744
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\0C46DDCEB43080B0EC647E0C66170465\origin.txt", handle hFirstFind_1: 1901014213840
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\2A6A33B25AA0984C6AB9D7F28665B88E\origin.txt", handle hFirstFind_1: INVALID_HANDLE
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\50CA3DFB510CC5A8F28B48D1BF2A5702\origin.txt", handle hFirstFind_1: 1901014218448
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\BC11041F9347CD71C5F8926F53AA908A\origin.txt", handle hFirstFind_1: 1901014213936
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common\origin.txt", handle hFirstFind_1: INVALID_HANDLE
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Community\origin.txt", handle hFirstFind_1: INVALID_HANDLE
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\origin.txt", handle hFirstFind_1: 1901014216720
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D3852169A6E781B7F35488A051432620\origin.txt", handle hFirstFind_1: 1901014217104
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\EE57F715BA53F2E183D6731C9376293D\origin.txt", handle hFirstFind_1: 1901014218640
   filter_1: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Help\origin.txt", handle hFirstFind_1: INVALID_HANDLE
filter_0: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*", handle hFirstFind_0: 1901014212592, NO_MORE_FILES 

Пояснения к первым строкам распечатки: сначала создаётся фильтр "filter_0" первичного поиска (фильтр равен "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*") и получаем хэндл первичного поиска "hFirstFind_0", равный 1901014212592. Раз значение "hFirstFind_0" не равно "INVALID_HANDLE" — значит, фильтр "filter_0" первичного поиска, переданный в Win API функцию FindFirstFileW(filter_0,ffd), правильный. После удачного вызова FindFirstFileW(filter_0,ffd) получим имя первой попавшейся папки: это папка "038C9E8FAFF9EA373522ECC6D5159962". 

Далее нужно провести поиск файла origin.txt" внутри папки 038C9E8FAFF9EA373522ECC6D5159962. Для этого формируем маску-фильтр. Например, для папки 038C9E8FAFF9EA373522ECC6D5159962, эта маска будет такой: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt". Если хэндл "hFirstFind_1" при этом не будет равен "INVALID_HANDLE" — значит, в указанной папке (038C9E8FAFF9EA373522ECC6D5159962) есть искомый файл (origin.txt). 

В распечатке хорошо видно, что первичный поиск во вложенных папках иногда возвращает "INVALID_HANDLE". Это означает, что в указанных папкках нет файла "origin.txt". 

Остановимся подробнее на том, что мы будем делать, когда файл "origin.txt" будет найден во вложенной папке.

3.5. CopyFileW

CopyFileW — копирует существующий файл в новый файл.

bool  CopyFileW(
   string lpExistingFileName,     //
   string lpNewFileName,          //
   bool bFailIfExists             //
   );

Параметры

lpExistingFileName

[in] Имя существующего файла.

Здесь принудительно принято ограничение по длине имени — MAX_PATH символов, этого всегда хватит для нашего примера.

Если файл с именем lpExistingFileName не существует, то функция терпит неудачу, и GetLastError возвращает ERROR_FILE_NOT_FOUND.

lpNewFileName

[in]  Имя нового файла. 

Здесь принудительно принято ограничение по длине имени — MAX_PATH символов, этого всегда хватит для нашего примера.

bFailIfExists
[in] 
Если этот параметр TRUE и новый файл, указанный в lpNewFileName существует, функция терпит неудачу. Если этот параметр FALSE и новый файл существует, функция перепишет существующий файл и успешно завершит работу.

Возвращаемое значение

Если функция завершается успешно, то возвращаемое значение не равно нулю.

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

Пример объявления Win API функции CopyFileW (код взят из включаемого файла ListingFilesDirectory.mqh):

#import "kernel32.dll"
int      GetLastError();
bool     CopyFileW(string lpExistingFileName,string lpNewFileName,bool bFailIfExists);
#import

3.6. Работаем с файлом "origin.txt"

Описание работы функции ListingFilesDirectory.mqh::CopiedAndReadFile(string full_file_name).

Во входном параметре функция получает полное имя файла "origin.txt", который был найден в одной из вложенных папок. Это может быть примерно такой путь: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt". Открывать файл "origin.txt" и читать его содержимое будем при помощи MQL5, а это означает, что файл должен быть в "песочнице". Значит, нам нужно скопировать файл "origin.txt" из вложенной папки в песочницу (в данном случае в "песочницу" в общих файлах всех терминалов). Такое копирование выполним при помощи вызова Win API функции CopyFileW.

Запишем в переменную "new_path" путь к файлу "origin.txt" в песочнице:

//+------------------------------------------------------------------+
//| Copying to the Common Data Folder                                |
//| for all client terminals ***\Terminal\Common\Files               |
//+------------------------------------------------------------------+
string CopiedAndReadFile(string full_file_name)
  {
   string new_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\Files\\origin.txt";
// => new_path==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common\Files\origin.txt
//--- Win API

и вызовем Win API функцию CopyFileW с третьим параметром, равным false — разрешим перезапись файла "origin.txt" в песочнице:

//--- Win API
   if(!CopyFileW(full_file_name,new_path,false))
     {
      Print("Error CopyFile ",full_file_name," to ",new_path);
      return(NULL);
     }
//--- open the file using MQL5

Средствами MQL5 открываем файл "origin.txt" для чтения, при этом не забываем указать флаг FILE_COMMON, ведь файл находится в папке общих файлов:

//--- open the file using MQL5
   string str;
   ResetLastError();
   int file_handle=FileOpen("origin.txt",FILE_READ|FILE_TXT|FILE_COMMON);
   if(file_handle!=INVALID_HANDLE)
     {
      //--- read a string using the MQL5 
      str=FileReadString(file_handle,-1)+"\\";
      //--- close the file using the MQL5
      FileClose(file_handle);
     }
   else
     {
      PrintFormat("File %s open failed , MQL5 error=%d","origin.txt",GetLastError());
      return(NULL);
     }
   return(str);
  }

Читаем только один раз — одну строку, дописываем к ней в конце "\\" и возвращаем полученный результат.

3.7. Последний штрих

Во входных параметрах советника задаются пути к папкам установки для четырёх терминалов:

//--- input parameters                                 
input string   ExtInstallationPathTerminal_1="C:\\Program Files\\MetaTrader 5 1\\";    // folder of the MetaTrader#1 installation
input string   ExtInstallationPathTerminal_2="D:\\MetaTrader 5 2\\";                   // folder of the MetaTrader#2 installation
input string   ExtInstallationPathTerminal_3="D:\\MetaTrader 5 3\\";                   // folder of the MetaTrader#3 installation
input string   ExtInstallationPathTerminal_4="D:\\MetaTrader 5 4\\";                   // folder of the MetaTrader#4 installation

Эти пути прописываются жёстко, и они должны корректно указывать на папки установки терминалов.

Также ниже, на глобальном уровне, объявлены ещё четыре строковых переменных и один массив:

string         slaveTerminalDataPath1=NULL;                                // the path to the Data Folder of the terminal #1
string         slaveTerminalDataPath2=NULL;                                // the path to the Data Folder of the terminal #2
string         slaveTerminalDataPath3=NULL;                                // the path to the Data Folder of the terminal #3
string         slaveTerminalDataPath4=NULL;                                // the path to the Data Folder of the terminal #4
//---
string         arr_path[][2];

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

GetStatsFromAccounts_EA.mq5::OnInit() >вызов> GetStatsFromAccounts_EA.mq5::FindDataFolders(arr_path) 
>вызов> ListingFilesDirectory.mqh::FindDataPath(string &array[][2]) >вызов> CopiedAndReadFile(string full_file_name) 

            string origin=CopiedAndReadFile(filter_1); //--- receiving a string of the file found origin.txt
            if(origin!=NULL)
              {
               //--- write a string into an array
               int size=ArrayRange(array,0);
               ArrayResize(array,size+1,0);
               array[size][0]=common_data_path+"\\"+name_0;
               //value array[][0]==C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962
               array[size][1]=origin;
               //value array[][1]==C:\Program Files\MetaTrader 5 1\
              }
            FindClose(hFirstFind_1);

В функции ListingFilesDirectory.mqh::FindDataPath(string &array[][2]), когда во вложенных папках терминалов обнаруживается файл "origin.txt", вызывается функция CopiedAndReadFile(string full_file_name), и после её вызова проводится запись в двумерный массив. В измерение массива "0" пишется путь к папке терминала в AppData, а в измерение массива "1" пишется путь к папке установки (этот путь, напомню, получаем из найденного файла "origin.txt").

>возвращаем управление в> GetStatsFromAccounts_EA.mq5::FindDataFolders(arr_path): 

здесь, путём простого обхода по двумерному массиву, заполняются переменные slaveTerminalDataPath1, slaveTerminalDataPath2, slaveTerminalDataPath3 и slaveTerminalDataPath4:

   FindDataPath(array);
   for(int i=0;i<ArrayRange(array,0);i++)
     {
      //Print("array[",i,"][0]: ",array[i][0]);
      //Print("array[",i,"][1]: ",array[i][1]);
      if(StringCompare(ExtInstallationPathTerminal_1,array[i][1],true)==0)
         slaveTerminalDataPath1=array[i][0];
      if(StringCompare(ExtInstallationPathTerminal_2,array[i][1],true)==0)
         slaveTerminalDataPath2=array[i][0];
      if(StringCompare(ExtInstallationPathTerminal_3,array[i][1],true)==0)
         slaveTerminalDataPath3=array[i][0];
      if(StringCompare(ExtInstallationPathTerminal_4,array[i][1],true)==0)
         slaveTerminalDataPath4=array[i][0];
     }
   if(slaveTerminalDataPath1==NULL || slaveTerminalDataPath2==NULL ||
      slaveTerminalDataPath3==NULL || slaveTerminalDataPath4==NULL)
     {
      Print("slaveTerminalDataPath1 ",slaveTerminalDataPath1,", slaveTerminalDataPath2 ",slaveTerminalDataPath2);
      Print("slaveTerminalDataPath3 ",slaveTerminalDataPath3,", slaveTerminalDataPath4 ",slaveTerminalDataPath4);
      return(false);
     }

Если мы дошли до этого этапа, значит, советник сопоставил пути установки терминалов и пути их папок в AppData. В случае, если хотя бы один путь к папке терминала в AppData не найден (то есть он будет равен NULL), то все пути распечатываются в последних строках и советник завершит работу ошибкой.


4. Выбор советника для тестирования

Перед запуском четырёх Подчинённых терминалов сначала нужно выбрать файл тестируемого эксперта. Этот эксперт должен быть предварительно скомпилирован и расположен в каталоге данных Мастер-терминала.

4.1. GetOpenFileName

GetOpenFileName — создаёт диалоговое окно "Открыть", позволяющее пользователю указать диск, папку и имя файла или набора файлов, которые будут открыты. Объявление и реализация диалогового окна "Открыть" полностью представлено во включаемом файле GetOpenFileNameW.mqh.

4.2. Выбираем советник при помощи системного диалога "Открыть файл"

Системное диалоговое окно "Открыть" вызывается из OnInit() советника:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   ArrayFree(arr_path);
   if(!FindDataFolders(arr_path))
      return(INIT_SUCCEEDED);
//---
   if(MessageBox("Ready?",NULL,MB_YESNO)==IDYES)
     {
      expert_name=OpenFileName();
      if(expert_name==NULL)
         return(INIT_FAILED);
      //--- editing and copying of the ini-file in the folder of the terminals

где происходит вызов GetOpenFileNameW.mqh::OpenFileName(void)

//+------------------------------------------------------------------+
//| Creates an Open dialog box                                       |
//+------------------------------------------------------------------+
string OpenFileName(void)
  {
   string path=NULL;
   string filter=NULL;
   if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
      filter="Компилированный код";
   else
      filter="Compiled code";
   if(GetOpenFileName(path,filter+"\0*.ex5\0",TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\","Select source file"))
      return(path);
   else
     {
      PrintFormat("Failed with error: %x",kernel32::GetLastError());
      return(NULL);
     }
  }

Переменная "path", при удачном вызове Win API функции GetOpenFileName, будет содержать полное имя выбранного файла, наподобие этого: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Experts\Examples\MACD\MACD Sample.ex5".

Переменная "filter" отвечает за текст ① рис. 2. Строка "\0*.ex5\0" отвечает за фильтр по типам файлов (② рис. 2). Строка "TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\"" задаёт путь к папке, которая будет открыта в системном диалоге "Открыть".

4.3. Конфигурационный ini-файл

Для запуска терминала на тестирование советника из командной строки (или при помощи Win API), нужно иметь конфигурационный ini-файл в котором обязательно должен присутствовать раздел [Tester] и необходимые указания:

[Tester]
Expert=test             //имя файла эксперта, который должен быть запущен на тестирование
Symbol=EURUSD           //название инструмента, который будет использоваться в качестве основного символа тестирования
Period=H1               //период графика тестирования
Deposit=10000           //сумма начального депозита для тестирования
Model=4                 //режим генерации тиков
Optimization=0          //включение/отключение оптимизации и указание ее вида
FromDate=2016.01.22     //начальная дата тестирования
ToDate=2016.06.06       //конечная дата тестирования
Report=TesterReport     //имя файла, в который будет сохранен отчет о результатах тестирования
ReplaceReport=1         //разрешить/запретить перезапись файла отчета 
UseLocal=1              //включение/отключения возможности использования локальных агентов для тестирования
Port=3000               //порт агента тестирования
Visual=0                //включить или выключить тестирование в визуальном режиме
ShutdownTerminal=0      //разрешить/запретить выключение торговой платформы по завершении тестирования 

Забегая наперёд, скажу, что этот раздел [Tester] мы будем самостоятельно добавлять в файл.

За основу решено было взять ini-файл Мастер-терминала. Этот файл (common.ini) располагается в каталоге данных терминала, в папке "config ". Для моего терминала путь к нему выглядит так: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\config\common.ini".

Схема работы с ini-файлом такая:

  1. Получить полный путь к "common.ini" Мастер-терминала. Полный путь — это строка вида 
    "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\config\common.ini". (MQL5)
  2. Получить новый путь к ini-файлу в песочнице "\Files". Новый путь — это строка вида:
    "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\myconfiguration.ini" Мастер-терминала. (MQL5)
  3. Копирование файла "common.ini" в "myconfiguration.ini". (WIn API функция CopyFileW).
  4. Редактирование файла "myconfiguration.ini". (MQL5).
  5. Получить новый путь к ini-файлу в песочнице Подчинённого терминала. Это строка вида (на примере моего Подчинённого терминала №1)
    "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Files\myconfiguration.ini". (MQL5)
  6. Копировать отредактированный ini-файл "myconfiguration.ini" из песочницы Мастер-терминала в песочницу Подчинённого терминала. (WIn API функция CopyFileW).
  7. Удаление файла "myconfiguration.ini" из песочницы Мастер-терминала. (MQL5)

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

К редактированию конфигурационных ini-файлов приступаем после того, как уже выбран советник для тестирования, GetStatsFromAccounts_EA.mq5::OnInit():

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   ArrayFree(arr_path);
   if(!FindDataFolders(arr_path))
      return(INIT_SUCCEEDED);
//---
   if(MessageBox("Ready?",NULL,MB_YESNO)==IDYES)
     {
      expert_name=OpenFileName();
      if(expert_name==NULL)
         return(INIT_FAILED);
      //--- editing and copying of the ini-file in the folder of the terminals
      if(!CopyCommonIni())
         return(INIT_FAILED);
      if(!CopyTerminalIni())
         return(INIT_FAILED);
      //--- сopying an expert in the terminal folders

Схема работы с ini-файлом, на примере Подчинённого терминала №1, GetStatsFromAccounts_EA.mq5::CopyCommonIni():

//+------------------------------------------------------------------+
//| Copying common.ini - file in a shared folder of client           |
//| terminals. Edit the ini-file and copy obtained                   |
//| ini-files into folders                                           |
//| ...\AppData\Roaming\MetaQuotes\Terminal\"id terminal"\MQL5\Files |
//+------------------------------------------------------------------+
bool CopyCommonIni()
  {
//0 — "Evey tick", "1 — 1 minute OHLC", 2 — "Open price only"
//3 — "Math calculations", 4 — "Every tick based on real ticks" 
//--- path to Data Folder
   string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH);
//--- path to Commomm Data Folder
   string common_data_path=TerminalInfoString(TERMINAL_COMMONDATA_PATH);
//---
   string existing_file_name=terminal_data_path+"\\config\\common.ini"; // full path to the ini-file                                                        
   string temp_name_ini=terminal_data_path+"\\MQL5\\Files\\"+common_file_name;
   string test=NULL;
//--- terminal #1
   if(!CopyFileW(existing_file_name,temp_name_ini,false))
     {
      PrintFormat("Failed with error: %x",kernel32::GetLastError());
      return(false);
     }
   EditCommonIniFile(common_file_name,3000,4);
   test=slaveTerminalDataPath1+"\\MQL5\\Files\\"+common_file_name;
   if(!CopyFileW(temp_name_ini,test,false))
     {
      PrintFormat("Failed with error: %x",kernel32::GetLastError());
      return(false);
     }
   ResetLastError();
   if(!FileDelete(common_file_name,0))
      Print("#1 file ",common_file_name," not deleted, an error ",GetLastError());
//--- terminal #2 

В вызов функции EditCommonIniFile(common_file_name,3000,4) передаётся:

common_file_name — имя ini-файла, который нужно редактировать;

3000 — номер порта агента тестирования. Каждый терминал должен запускаться на своём агенте тестирования. Нумерация агентов производится начиная с 3000. Увидеть номера портов агентов тестирования можно так: в терминале MetaTrader 5 перейти в тестер стратегий и выполнить правый клик мышки во вкладке "Журнал" тестера стратегий. При этом в выпадающем меню можно будет увидеть нумерацию портов агентов тестирования:

агенты тестирования 

Рис. 7. Агенты тестирования 

4 - тип тестирования: 

  • 0 — "Все тики",
  • 1 — "OHLC на M1",
  • 2 — "Только цены открытия",
  • 3 — "Математические вычисления",
  • 4 — "Каждый тик на основе реальных тиков"

Редактирование конфигурационного commom.ini файла выполняется в функции GetStatsFromAccounts_EA.mq5::EditCommonIniFile(string name,const int port,const int model) — операции открытия файла, чтения из файла и записи в файл выполняются средствами MQL5:

//+------------------------------------------------------------------+
//| Editing common.ini file                                          |
//+------------------------------------------------------------------+
bool EditCommonIniFile(string name,const int port,const int model)
  {
   bool tester=false;      // if false - means the section [Tester] not found
   int  count_tester=0;    // counter discoveries section [Tester]
//--- откроем файл 
   ResetLastError();
   int file_handle=FileOpen(name,FILE_READ|FILE_WRITE|FILE_TXT);
   if(file_handle!=INVALID_HANDLE)
     {
      //--- auxiliary variable
      string str;
      //--- read data
      while(!FileIsEnding(file_handle))
        {
         //--- read line 
         str=FileReadString(file_handle,-1);
         //--- find [Tester]
         if(StringFind(str,"[Tester]",0)!=-1)
           {
            tester=true;
            count_tester++;
           }
        }
      if(!tester)
        {
         FileWriteString(file_handle,"[Tester]\n",-1);
         FileWriteString(file_handle,"Expert=test\n",-1);
         FileWriteString(file_handle,"Symbol=EURUSD\n",-1);
         FileWriteString(file_handle,"Period=H1\n",-1);
         FileWriteString(file_handle,"Deposit=10000\n",-1);
         //0 — "Evey tick", "1 — 1 minute OHLC", 2 — "Open price only"
         //3 — "Math calculations", 4 — "Every tick based on real ticks" 
         FileWriteString(file_handle,"Model="+IntegerToString(model)+"\n",-1);
         FileWriteString(file_handle,"Optimization=0\n",-1);
         FileWriteString(file_handle,"FromDate=2016.01.22\n",-1);
         FileWriteString(file_handle,"ToDate=2016.06.06\n",-1);
         FileWriteString(file_handle,"Report=TesterReport\n",-1);
         FileWriteString(file_handle,"ReplaceReport=1\n",-1);
         FileWriteString(file_handle,"UseLocal=1\n",-1);
         FileWriteString(file_handle,"Port="+IntegerToString(port)+"\n",-1);
         FileWriteString(file_handle,"Visual=0\n",-1);
         FileWriteString(file_handle,"ShutdownTerminal=0\n",-1);
        }
      //--- close file
      FileClose(file_handle);
     }
   else
     {
      PrintFormat("Unable to open file %s, error = %d",name,GetLastError());
      return(false);
     }
   return(true);
  }

4.4. Секрет №2

Перед завершением работы терминал MetaTrader 5 сохраняет расположение окон и панелей, а также их размеры в файл "terminal.ini". Сам файл находится в каталоге данных терминала, в подпапке "config". Например, для моего Подчинённого терминала №1 полный путь к "terminal.ini" будет таким:

"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\config\terminal.ini".

В самом файле "terminal.ini" нас будет интересовать только блок "[Window]". Свернём в окно терминал MetaTrader 5. Терминал примет примерно такие размеры:

mini terminal

Рис. 8. Терминал свёрнутый в окно

Если этот терминал закрыть, то в файле terminal.ini блок [Window] будет иметь такой вид:

Arrange=1
[Window]
Fullscreen=0
Type=1
Left=412
Top=65
Right=1212
Bottom=665
LSave=412

То есть, блок [Window] хранит координаты терминала и его состояние. 


4.5. Задаём размер терминала (ширину, высоту). Вставка строк в середину файла 

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

 terminals

Рис. 9. Расположение терминалов

Как было сказано выше, нужно редактировать файл "terminal.imi" для каждого Подчинённого терминала. Здесь нужно обратить внимание на то, что строки придется вставлять не в конец, а в середину файла "terminal.ini". Ниже показаны особенности этой процедуры.

Поясню на таком примере: есть файл "test.txt" расположенный в "песочнице" терминала. Содержимое файла "test.txt":

s=0
df=12
asf=3
g=3
n=0
param_f=123

Нужно изменить информацию во второй и третьей строке, чтобы получилось так:

s=0
df=1256
asf=5
g=3
n=0
param_f=123

На первый взгляд, следует поступить так:

  • открыть файл для чтения и записи, прочитать первую строку (эта операция переместит файловый указатель на начало второй строки);
  • записать во вторую строку новое значение "df=1256";
  • записать в третью строку новое значение "asf=5";
  • закрыть файл.
Смотрим на примере кода скрипта "InsertRowsMistakenly.mq5":

//+------------------------------------------------------------------+
//|                                         InsertRowsMistakenly.mq5 |
//|                              Copyright © 2016, Vladimir Karputov |
//|                                           http://wmua.ru/slesar/ |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2016, Vladimir Karputov"
#property link      "http://wmua.ru/slesar/"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- open file
   ResetLastError();
   string name="test.txt";
   int file_handle=FileOpen(name,FILE_READ|FILE_WRITE|FILE_TXT);
   if(file_handle!=INVALID_HANDLE)
     {
      FileReadString(file_handle,-1);
      FileWriteString(file_handle,"df=1256"+"\r\n",-1);
      FileWriteString(file_handle,"asf=5"+"\r\n",-1);
      //--- close file
      FileClose(file_handle);
     }
   else
     {
      PrintFormat("Unable to open file %s, error = %d",name,GetLastError());
      return;
     }
  }
//+------------------------------------------------------------------+

Получаем неожиданный результат — в четвёртой строке пропали символы "g=":

Было Стало 
s=0
df=12
asf=3
g=3
n=0
param_f=123
s=0
df=1256
asf=5
3
n=0
param_f=123

Почему так произошло? Представьте себе, что файл состоит из множества ячеек, которые идут друг за другом. В каждой ячейке помещается один символ. Поэтому когда мы пишем в файл, начиная с его середины, то, по сути, мы просто перезаписываем ячейки. Если добавить больше символов, чем было на этом месте изначально (как в примере выше: было "df=12", а мы записали на два символа больше - "df=1256"), то лишние символы просто повредят дальнейший код. Вот как это выглядит:

write string

Рис. 10. Повреждение информации.

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

  • Копируем из Подчинённого терминала файл "terminal.ini" в песочницу Мастер-терминала в файл с именем "terminal_ext.ini" (Win API CopyFileW).
  • Создаём в песочнице Мастер-терминала файл "terminal.ini", открываем его на запись (MQL5).
  • Открываем в песочнице Мастер-терминала файл "terminal_ext.ini" на чтение (MQL5).
  • В песочнице Мастер терминала: считываем строки из "terminal_ext.ini" и записываем их в файл "terminal.ini" (MQL5).
  • Как только считанная строка равна "[Window]" - записываем в файл "terminal.ini" новые координаты (это шесть строк), а в файле "terminal_ext.ini" перемещаем файловый указатель тоже на шесть строк (MQL5).
  • В песочнице Мастер терминала: считываем строки из "terminal_ext.ini" и записываем их в файл "terminal.ini", пока не обнаружим конец файла "terminal_ext.ini" (MQL5).
  • В песочнице Мастер терминала: закрываем файлы "terminal.ini" и "terminal_ext.ini" (MQL5).
  • Копируем из песочницы Мастер-терминала файл "terminal.ini" в Подчинённый терминал, в файл "terminal.ini" (Win API CopyFileW).
  • В песочнице Мастер терминала: удаляем файлы "terminal.ini" и "terminal_ext.ini" (MQL5).

Порядок вызовов функций:

GetStatsFromAccounts_EA.mq5::OnInit() >вызов> GetStatsFromAccounts_EA.mq5::CopyTerminalIni()

//+------------------------------------------------------------------+
//| Editing Files "terminal.ini"                                     |
//+------------------------------------------------------------------+
bool CopyTerminalIni()
  {
//--- path to the terminal data folder 
   string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH);
//---
   string existing_file_name=NULL;
   string ext_ini=terminal_data_path+"\\MQL5\\Files\\terminal_ext.ini";
   string ini=terminal_data_path+"\\MQL5\\Files\\terminal.ini";
   int left=0;
   int top=0;
   int right=0;
   int bottom=0;
//---
   for(int i=1;i<5;i++)
     {
      switch(i)
        {
         case 1:
            existing_file_name=slaveTerminalDataPath1+"\\config\\terminal.ini";
            left=0; top=0; right=682; bottom=420;
            break;
         case 2:
            existing_file_name=slaveTerminalDataPath2+"\\config\\terminal.ini";
            left=682; top=0; right=1366; bottom=420;
            break;
         case 3:
            existing_file_name=slaveTerminalDataPath3+"\\config\\terminal.ini";
            left=0; top=738-413; right=682; bottom=738;
            break;
         case 4:
            existing_file_name=slaveTerminalDataPath4+"\\config\\terminal.ini";
            left=682; top=738-413; right=1366; bottom=738;
            break;
        }
      //---
      if(!CopyFileW(existing_file_name,ext_ini,false))
        {
         PrintFormat("Failed with error: %x",kernel32::GetLastError());
         return(false);
        }
      if(!EditTerminalIniFile("terminal_ext.ini",left,top,right,bottom))
         return(false);
      if(!CopyFileW(ini,existing_file_name,false))
        {
         PrintFormat("Failed with error: %x",kernel32::GetLastError());
         return(false);
        }
      ResetLastError();
      if(!FileDelete("terminal.ini",0))
         Print("#",i," file terminal.ini not deleted, an error ",GetLastError());
      ResetLastError();
      if(!FileDelete("terminal_ext.ini",0))
         Print("#",i," file terminal_ext.ini not deleted, an error ",GetLastError());
     }
//---
   return(true);
  }

 >вызов> GetStatsFromAccounts_EA.mq5::EditTerminalIniFile

//+------------------------------------------------------------------+
//| Editing terminal.ini file                                        |
//+------------------------------------------------------------------+
bool EditTerminalIniFile(string ext_name,const int Left=0,const int Top=0,const int Right=1366,const int Bottom=738)
  {
//--- creates and opens files
   string name="terminal.ini";
   ResetLastError();
   int terminal_ini_handle=FileOpen(name,FILE_WRITE|FILE_TXT);
   int terminal_ext_ini__handle=FileOpen(ext_name,FILE_READ|FILE_TXT);
   if(terminal_ini_handle==INVALID_HANDLE)
     {
      PrintFormat("Unable to open file %s, error = %d",name,GetLastError());
     }
   if(terminal_ext_ini__handle==INVALID_HANDLE)
     {
      PrintFormat("Unable to open file %s, error = %d",ext_name,GetLastError());
     }
   if(terminal_ini_handle==INVALID_HANDLE && terminal_ext_ini__handle==INVALID_HANDLE)
     {
      FileClose(terminal_ext_ini__handle);
      FileClose(terminal_ini_handle);
      return(false);
     }

//--- auxiliary variable
   string str=NULL;
//--- read data
   while(!FileIsEnding(terminal_ext_ini__handle))
     {
      //--- read line
      str=FileReadString(terminal_ext_ini__handle,-1);
      FileWriteString(terminal_ini_handle,str+"\r\n",-1);
      //--- find [Window]
      if(StringFind(str,"[Window]",0)!=-1)
        {
         FileReadString(terminal_ext_ini__handle,-1);
         FileWriteString(terminal_ini_handle,"Fullscreen=0\r\n",-1);

         FileReadString(terminal_ext_ini__handle,-1);
         FileWriteString(terminal_ini_handle,"Type=1\r\n",-1);

         FileReadString(terminal_ext_ini__handle,-1);
         FileWriteString(terminal_ini_handle,"Left="+IntegerToString(Left)+"\r\n",-1);

         FileReadString(terminal_ext_ini__handle,-1);
         FileWriteString(terminal_ini_handle,"Top="+IntegerToString(Top)+"\r\n",-1);

         FileReadString(terminal_ext_ini__handle,-1);
         FileWriteString(terminal_ini_handle,"Right="+IntegerToString(Right)+"\r\n",-1);

         FileReadString(terminal_ext_ini__handle,-1);
         FileWriteString(terminal_ini_handle,"Bottom="+IntegerToString(Bottom)+"\r\n",-1);
        }
     }
//--- close files
   FileClose(terminal_ext_ini__handle);
   FileClose(terminal_ini_handle);
   return(true);
  }

Таким образом, редактируются файлы "terminal.ini" в Подчинённых терминалах, что позволяет запустить их так, как на Рис. 9. При этом можно наблюдать за графиками тестирования и сравнивать точность тестирования в разных режимах. 


5. Запуск Подчинённых терминалов на тестирование

К настоящему моменту у нас всё готово для запуска Подчинённых терминалов в режиме тестирования советника:

  • мы подготовили конфигурационные файлы "myconfiguration.ini" для всех Подчинённых терминалов;
  • мы отредактировали файлы "terminal.ini" всех Подчинённых терминалов;
  • мы знаем имя советника, который будет проходить тестирование.
Остаётся две задачи: скопировать выбранный советник в песочницы Подчинённых терминалов и запустить эти терминалы.

5.1. Копирование советника в папки Подчинённых терминалов

Копирование выбранного ранее советника (его название сохранено в переменной "expert_name") производится в OnInit():

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   ArrayFree(arr_path);
   if(!FindDataFolders(arr_path))
      return(INIT_SUCCEEDED);
//---
   if(MessageBox("Ready?",NULL,MB_YESNO)==IDYES)
     {
      expert_name=OpenFileName();
      if(expert_name==NULL)
         return(INIT_FAILED);
      //--- editing and copying of the ini-file in the folder of the terminals
      if(!CopyCommonIni())
         return(INIT_FAILED);
      if(!CopyTerminalIni())
         return(INIT_FAILED);
      //--- сopying an expert in the terminal folders
      ResetLastError();
      if(!CopyFileW(expert_name,slaveTerminalDataPath1+"\\MQL5\\Experts\\test.ex5",false))
        {
         PrintFormat("Failed CopyFileW #1 with error: %x",kernel32::GetLastError());
         return(INIT_FAILED);
        }

      if(!CopyFileW(expert_name,slaveTerminalDataPath2+"\\MQL5\\Experts\\test.ex5",false))
        {
         PrintFormat("Failed CopyFileW #2 with error: %x",kernel32::GetLastError());
         return(INIT_FAILED);
        }

      if(!CopyFileW(expert_name,slaveTerminalDataPath3+"\\MQL5\\Experts\\test.ex5",false))
        {
         PrintFormat("Failed CopyFileW #3 with error: %x",kernel32::GetLastError());
         return(INIT_FAILED);
        }

      if(!CopyFileW(expert_name,slaveTerminalDataPath4+"\\MQL5\\Experts\\test.ex5",false))
        {
         PrintFormat("Failed CopyFileW #4 with error: %x",kernel32::GetLastError());
         return(INIT_FAILED);
        }
      //---
      Sleep(sleeping);

5.2. ShellExecuteW

ShellExecuteW — выполняет операцию на указанном файле.

//--- x64
long ShellExecuteW(
   long hwnd,               //
   string lpOperation,      //
   string lpFile,           //
   string lpParameters,     //
   string lpDirectory,      //
   int nShowCmd             //
   );
//--- x32
int ShellExecuteW(
   int hwnd,                //
   string lpOperation,      //
   string lpFile,           //
   string lpParameters,     //
   string lpDirectory,      //
   int nShowCmd             //
   );

Параметры

hwnd

[in] Хэндл родительского окна, используемого для отображения пользовательского интерфейса и сообщения об ошибках. Это значение должно быть NULL, если операция не связана с окнами.

lpOperation

[in] Строка с именем команды, определяющей действие, которое будет выполняться. Набор доступных команд зависит от конкретного файла или папки. Как правило, это действия, доступные из контекстного меню объекта. Обычно используются следующие команды:

"edit"

Запускает редактор и открывает документ для редактирования. Если lpFile не является файлом документа, то функция не будет выполнена.

"explore"

Открывает папку, которая указана в lpFile.

"find"

Инициирует поиск, начинающийся в каталоге, указанном в lpDirectory.

"open"

Открывает элемент, заданный параметром lpFile. Этим элементом может быть файл или папка.

"print"

Печатает файл, указанный lpFile. Если lpFile не является файлом документа, функция завершается ошибкой.

"NULL"

Используется имя команды по умолчанию, если такая имеется. Если такой команды нет, то будет использовано команда "open". Если не используется ни одна команда, то система использует первую команду, которая указана в реестре.

lpFile 

[in] Строка, задающая файл или объект, на котором можно исполнить команду. Передаётся полное имя (включающее не только имя файла, но и путь к нему). Обратите внимание, что объект может поддерживать не все команды. Например, не все документы поддерживают команду "print". Если относительный путь используется для lpDirectory параметра, то не используйте относительный путь для lpFile.

lpParameters

[in] Если lpFile указывает на исполняемый файл, этот параметр - строка, которая определяет параметры, которые будут переданы приложению. Формат этой строки определяется именем команды, которая должна быть выполнена. Если lpFile указывает на файл документа, lpParameters должен быть NULL.

lpDirectory

[in] Строка, которая определяет рабочий каталог. Если это значение NULL, используется текущий рабочий каталог. Если относительный путь задан в lpFile, тогда не используйте относительный путь для lpDirectory.

nShowCmd

[in] Флаги, которые определяют, как приложение должно отображаться при его открытии. Если lpFile определяет файл документа, флаг просто передается соответствующему приложению. Используемые флаги:

//+------------------------------------------------------------------+
//| Enumeration command to start the application                     |
//+------------------------------------------------------------------+
enum EnSWParam
  {
   //+------------------------------------------------------------------+
   //| Displays the window as a minimized window. This value is similar |
   //| to SW_SHOWMINIMIZED, except the window is not activated.         |
   //+------------------------------------------------------------------+
   SW_SHOWMINNOACTIVE=7,
   //+------------------------------------------------------------------+
   //| Activates and displays a window. If the window is minimized or   |
   //| maximized, the system restores it to its original size and       |
   //| position. An application should specify this flag when           |
   //| displaying the window for the first time.                        |
   //+------------------------------------------------------------------+
   SW_SHOWNORMAL=1,
   //+------------------------------------------------------------------+
   //| Activates the window and displays it as a minimized window.      |
   //+------------------------------------------------------------------+
   SW_SHOWMINIMIZED=2,
   //+------------------------------------------------------------------+
   //| Activates the window and displays it as a maximized window.      |
   //+------------------------------------------------------------------+
   SW_SHOWMAXIMIZED=3,
   //+------------------------------------------------------------------+
   //| Hides the window and activates another window.                   |
   //+------------------------------------------------------------------+
   SW_HIDE=0,
   //+------------------------------------------------------------------+
   //| Activates the window and displays it in its current size         |
   //| and position.                                                    |
   //+------------------------------------------------------------------+
   SW_SHOW=5,
  };

Возвращаемое значение

Если функция завершается успешно, она возвращает значение большее, чем 32.

Пример объявления Win API функции ShellExecuteW:

#import  "shell32.dll"
int  GetLastError();
//+------------------------------------------------------------------+
//| ShellExecute function                                            |
//| https://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx
//| Performs an operation on a specified file                        |
//+------------------------------------------------------------------+
//--- x64
long ShellExecuteW(long hwnd,string lpOperation,string lpFile,string lpParameters,string lpDirectory,int nShowCmd);
//--- x32
int ShellExecuteW(int hwnd,string lpOperation,string lpFile,string lpParameters,string lpDirectory,int nShowCmd);
#import
#import "kernel32.dll"

//+------------------------------------------------------------------+
//| Enumeration command to start the application                     |
//+------------------------------------------------------------------+
enum EnSWParam
  {
   //+------------------------------------------------------------------+
   //| Displays the window as a minimized window. This value is similar |
   //| to SW_SHOWMINIMIZED, except the window is not activated.         |
   //+------------------------------------------------------------------+
   SW_SHOWMINNOACTIVE=7,
   //+------------------------------------------------------------------+
   //| Activates and displays a window. If the window is minimized or   |
   //| maximized, the system restores it to its original size and       |
   //| position. An application should specify this flag when           |
   //| displaying the window for the first time.                        |
   //+------------------------------------------------------------------+
   SW_SHOWNORMAL=1,
   //+------------------------------------------------------------------+
   //| Activates the window and displays it as a minimized window.      |
   //+------------------------------------------------------------------+
   SW_SHOWMINIMIZED=2,
   //+------------------------------------------------------------------+
   //| Activates the window and displays it as a maximized window.      |
   //+------------------------------------------------------------------+
   SW_SHOWMAXIMIZED=3,
   //+------------------------------------------------------------------+
   //| Hides the window and activates another window.                   |
   //+------------------------------------------------------------------+
   SW_HIDE=0,
   //+------------------------------------------------------------------+
   //| Activates the window and displays it in its current size         |
   //| and position.                                                    |
   //+------------------------------------------------------------------+
   SW_SHOW=5,
  };

5.3. Запуск терминалов

Запускаются подчинённые терминалы из OnInit():

      //---
      Sleep(sleeping);
      LaunchSlaveTerminal(ExtInstallationPathTerminal_1,slaveTerminalDataPath1+"\\MQL5\\Files\\"+common_file_name);
      Sleep(sleeping);
      LaunchSlaveTerminal(ExtInstallationPathTerminal_2,slaveTerminalDataPath2+"\\MQL5\\Files\\"+common_file_name);
      Sleep(sleeping);
      LaunchSlaveTerminal(ExtInstallationPathTerminal_3,slaveTerminalDataPath3+"\\MQL5\\Files\\"+common_file_name);
      Sleep(sleeping);
      LaunchSlaveTerminal(ExtInstallationPathTerminal_4,slaveTerminalDataPath4+"\\MQL5\\Files\\"+common_file_name);
     }
//---
   return(INIT_SUCCEEDED);
  }

при этом между запусками советник ожидает "sleeping" миллисекунд. По умолчанию параметр "sleeping" равен 9000 (то есть, 9 секунд). Если происходят ошибки авторизации агентов в Подчинённых терминалах, увеличьте этот параметр. 

Параметры, передаваемые в Win API функцию (на примере моего Подчинённого терминала №1) выглядят так:

LaunchSlaveTerminal("C:\Program Files\MetaTrader 5 1\",
"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Files\myconfiguration.ini");


6. Возможные ошибки

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

В тестере, во вкладке "Журнал" будет примерно такая запись:

2016.07.15 15:10:48.327 Tester  EURUSD: history data begins from 2014.01.14 00:00
2016.07.15 15:10:49.212 Core 1  agent process started
2016.07.15 15:10:49.717 Core 1  connecting to 127.0.0.1:3002
2016.07.15 15:11:00.771 Core 1  tester agent authorization error
2016.07.15 15:11:01.417 Core 1  connection closed

В логах агента записи такие:

2016.07.15 16:08:45.416 Startup MetaTester 5 x64 build 1368 (13 Jul 2016)
2016.07.15 16:08:45.612 Server  MetaTester 5 started on 127.0.0.1:3000
2016.07.15 16:08:45.612 Startup initialization finished
2016.07.15 16:09:36.811 Server  MetaTester 5 stopped
2016.07.15 16:09:38.422 Tester  shutdown tester machine

В таких случаях рекомендуется увеличить паузу между запусками терминалов (переменная "sleeping"), а также выгрузить все ресурсоёмкие приложения, которые могут захватить в своё пользование ядра процессора.


Заключение

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

Также в статье было показано, как вызывать такие Win API функции:

  • CopyFileW — копирует файлы в "песочницу" и из "песочницы" MQL5.
  • FindClose — закрывает хэндлы поиска.
  • FindFirstFileW — ищет каталог файла или подкаталог, название которого соответствует указанному имени файла.
  • FindNextFileW — продолжает поиск файла из предыдущего вызова функции FindFirstFile.
  • GetOpenFileNameW — вызывает системный диалог открытия файла
  • ShellExecuteW — запуск приложения

 

Прикрепленные файлы |
Alexey Volchanskiy
Alexey Volchanskiy | 14 сен 2016 в 21:42
Karputov Vladimir:

UAC всегда должен быть включён.

Всё сказанное выше и ниже относится к Windows 10 - предыдущие операционные системы даже не обсуждаются и не рассматриваются.

Из-за конфликтов с безопасностью операционной системы пользователь (или программа запущенная из под него) не имеет прав писать в Program Files, а вот в AppData - запретов нет. А что происходит когда терминал устанавливается без ключа \Portable и когда включён UAC? Правильно, идёт СТАНДАРТНАЯ установка и тогда с записью файлов нет проблем.

В общем нужно по максимуму использовать стандартные программы и установки - намного меньше проблем и конфликтов. 

У меня Win 10 x64, UAC включен. Я написал, что в Program Files терминалы не ставлю, все находится в c:\forex и его подпапках. Туда можно спокойно писать.

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

elibrarius
elibrarius | 19 сен 2016 в 13:08

Отличная статья.

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

А можно где-то посмотреть все варианты переменных и их значений, которые можно применять в конфигурационном ini-файле?

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

Vladimir Karputov
Vladimir Karputov | 19 сен 2016 в 13:29
elibrarius:

Отличная статья.

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

А можно где-то посмотреть все варианты переменных и их значений, которые можно применять в конфигурационном ini-файле?

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

Запуск с собственным конфигурационным файлом - здесь описание файла common.ini

Rashid Umarov
Rashid Umarov | 19 сен 2016 в 14:03
elibrarius:

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

Конечно, Владимир уже дал ссылку на справку
elibrarius
elibrarius | 19 сен 2016 в 14:04
Karputov Vladimir:

Запуск с собственным конфигурационным файлом - здесь описание файла common.ini

Спасибо)
Графические интерфейсы X: Обновления для библиотеки Easy And Fast (build 2) Графические интерфейсы X: Обновления для библиотеки Easy And Fast (build 2)

С момента предыдущей публикации статьи этой серии, библиотека Easy And Fast пополнилась новыми возможностями. Проведена частичная оптимизация схемы и кода библиотеки, что немного сократило потребление ресурсов CPU. Некоторые повторяющиеся методы во многих классах элементов были перенесены в базовый класс CElement.

Тестирование торговых стратегий на реальных тиках Тестирование торговых стратегий на реальных тиках

В данной статье мы покажем результаты тестирования простой торговой стратегии в 3-х режимах: "OHLC на M1", "Все тики" и "Каждый тик на основе реальных тиков" с использованием записанных тиков из истории.

Кроссплатформенный торговый советник: Введение Кроссплатформенный торговый советник: Введение

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

Как в MetaTrader 5 быстро разработать и отладить торговую стратегию Как в MetaTrader 5 быстро разработать и отладить торговую стратегию

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