LifeHack para tráders: un back test está bien, pero cuatro están mucho mejor

Vladimir Karputov | 8 agosto, 2016

A cualquier tráder le surge la misma pregunta antes de la primera simulación: "¿cuál de los cuatro modos debo utilizar?" Cada uno de los modos propuestos tiene sus ventajas y peculiaridades, por eso haremos la tarea más simple, ¡iniciaremos todos los modos a la vez con solo un botón! En el artículo se muestra cómo con la ayuda de Win API y un poco de magia se pueden ver los cuatro gráficos de simulación.

Capítulos


Introducción

El objetivo principal de este artículo es mostrar cómo desde un terminal (lo llamaremos terminal Maestro) se puede iniciar una única simulación (¡no una optimización, sino precisante una simulación!) de un asesor en cuatro terminales a la vez (los llamaremos terminales Subordinados #1, #2, #3 y #4). Además, los simuladores de estrategias en los terminales Subordinados se iniciarán en diferentes modos de generación ticks:

  • en el terminal #1 — "Cada tick basado en ticks reales";
  • en el terminal #2 —  "Todos los ticks";
  • en el terminal #3 — "1 minuto en OHLC";
  • en el terminal #4 — "Solo precios de apertura".

Limitaciones importantes:

  1. El terminal Maestro debe inciarse sin la clave /portable.
  2. Debemos tener como mínimo cinco terminales MetaTrader 5 instalados.
  3. La cuenta comercial en el terminal Maestro - la llamaremos cuenta Maestra - debe ser iniciada al menos una vez en cada uno de los terminales Subordinados. Esto es necesario porque el experto de este artículo no transmite la contraseña de la cuenta comercial a través de archivos ini a los terminales Subordinados. En lugar de la contraseña, se transmite el número de la cuenta comercial en la que hay que iniciar el simulador de estrategias, y este número siempre coincide con el número de la cuenta Maestra.
    Este comportamiento parece lógico, puesto que la simulación del asesor en los diferentes modos de generación de ticks se debe realizar en una misma cuenta comercial.
  4. Antes de iniciar el experto, aligere de trabajo todo lo posible el procesador central: desconecte los juegos online, el reproductor multimedia y otros programas de alto consumo. En caso contrario, alguno de los núcleos del procesador podría bloquearse y en este no se iniciaría la simulación.

1. Principios generales

Lo que está de más, no es bueno (refrán polaco). 

Siempre he preferido usar las posibilidades estándar del software. Aplicada a los terminales MetaTrader 5, esta norma suena así: "nunca inicies el terminal con la clave /portable, nunca desconectes en el sistema operativo el control de las cuentas de usuario (UAC)". Partiendo de esto, el trabajo con los archivos del asesor descrito se realizará en la carpeta AppData.

Todas las capturas de pantalla mostradas en el artículo demuestran el funcionamiento en Windows 10, puesto que se trata de un sistema nuevo y con licencia. Todo el código descrito en este trabajo se analizará aplicado precisamente a este.

El asesor analizado, junto con las posibilidades de MQL5, utiliza ampliamente dll:


Fig. 1. Dependencias 

En concreto, se usan las llamadas de estas funciones Windows API:


2. Parámetros de entrada


Fig. 3. Parámetros de entrada 

Las rutas folder of the MetaTrader#Х installation son las rutas a las carpetas de instalación de los terminales Subordinados. Al establecer una ruta en código mq5 hay que anotar una barra oblicua doble. Asimismo, es muy importante poner una barra oblicua doble inversa al final de la ruta:

//--- 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

El nombre del terminal "terminal64.exe" se muestra para un sistema operativo de 64 bits.

 

La conexión entre la carpeta de instalación y el catálogo de datos está en la carpeta AppData 

Al iniciar el terminal de forma convencional o con la clave /portable, el terminal proporcionará diferentes rutas para la variable TERMINAL_DATA_PATH. Veamos esta situación con el ejemplo del terminal Maestro, que ha sido instalado en la carpeta "C:\Program Files\MetaTrader 5 1".

Cuando el terminal Maestro ha sido iniciado con la clave /portable, desde este terminal MQL dará los resultados siguientes:

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

Y aquí tenemos la respuesta de este mismo terminal sin la clave /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

Pero esto solo lo necesitaremos si vamos a obtener los parámetros del terminal actual. ¿Y cómo debemos actuar en el caso de los terminales Subordinados en los que iniciaremos la simulación del asesor? ¿Cómo unir las rutas de instalación de los terminales Subordinados con las rutas de sus catálogos de datos?

Aquí hay que aclarar por qué es tan importante conocer la ruta a los catálogos de datos en la carpeta AppData (cita del manual):

Comenzando por MS Windows Vista, los programas instalados por defecto en el catálogo Program Files tienen prohibido guardar datos en el catálogo de instalación. Todos los datos deberán guardarse en un catálogo aparte del usuario de Windows.

En otras palabras, nuestro experto puede crear y cambiar libremente archivos en la carpeta, del tipo: C:\Users\nombre del usuario\AppData\Roaming\MetaQuotes\Terminal\identificador del terminal\MQL5\Files. Aquí el "identificador del terminal" se refiere al identificador del terminal Maestro.


3. Comparando la carpeta de instalación y la carpeta AppData de los terminales Subordinados

El experto ejecuta el inicio de los terminales Subordinados indicando el archivo de configuración. Al mismo tiempo, para cada terminal se usa su propio archivo de configuración. En cada archivo de configuración se indica que al iniciar el terminal se debe comenzar directamente la simulación del archivo establecido. Los comandos existentes se ubican en la sección [Tester] del archivo de configuración:

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

Como puede ver, la ruta no se indica, lo que significa que el asesor simulado puede encontrarse exclusivamente en el "sandbox" MQL5. En el ejemplo del terminal 1 se puede tratar de dos rutas:

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

La variante №2 la descartaremos, puesto que, de acuerdo con la política de seguridad, comenzando por Windows Vista, tenemos prohibido escribir en la carpeta "Program Files". Nos queda la variante №1, y esto significa que para todos los terminales Subordinados deberemos comparar las carpetas de instalación y las carpetas en AppData. 

3.1. Secreto №1

En cada catálogo de datos hay un archivo "origin.txt". Tomando el terminal Subordinado 1 como ejemplo:


 

Fig. 4. Archivo origin.txt 

y contenido del archivo origin.txt:

C:\Program Files\MetaTrader 5 1

Esta entrada en el archivo indica que la carpeta "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962" la ha creado el terminal instalado en "C:\Program Files\MetaTrader 5 1".

3.2. FindFirstFileW, FindNextFileW

FindFirstFileW realiza la búsqueda del directorio para el archivo o subcatálogo con el nombre que coincida con determinado nombre (o parte del nombre, si se usan símbolos especiales).

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

Parámetros

lpFileName

[in]  Directorio o ruta y nombre del archivo que puede incluir los símbolos de sustitución, por ejemplo el asterisco (*) o el signo de interrogación (?).

lpFindFileData

[in][out]  Puntero a la estructura WIN32_FIND_DATA, que recibe información sobre el archivo o directorio encontrado. 

Valor retornado

Si la función finaliza con éxito, el valor retornado será el manejador de búsqueda usado en la última llamada FindNextFile or FindClose, y el parámetro lpFindFileData  contendrá información sobre el primer archivo o carpeta encontrada.

Si la función no tiene éxito o no puede encontrar los archivos de la línea de búsqueda en el parámetro lpFileName, se retorna el valor INVALID_HANDLE_VALUE, y el contenido lpFindFileData será indeterminado. Para obtener información sobre el error, llame la función  GetLastError.

Si la función no se activa porque no se pueden encontrar los archivos correspondientes, la función GetLastError retorna ERROR_FILE_NOT_FOUND.


FindNextFileWcontinúa la búsqueda del archivo de la anterior llamada de la función FindFirstFileFindFirstFileEx, o FindFirstFileTransacted.

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

Parámetros

FindFile

[in] Manejador de la búsqueda retornado por la anterior llamada de la función FindFirstFile o FindFirstFileEx.

lpFindFileData

[in][out]  Puntero a la estructura WIN32_FIND_DATA, que recibe información sobre el archivo o directorio encontrado. 

Valor retornado

Si la función finaliza con éxito, entonces el valor retornado no será igual a cero, y el parámetro lpFindFileData contendrá información sobre el siguiente archivo o catálogo encontrado.

Si la función finaliza con error, el valor retornado será igual a cero, y el contenido lpFindFileData será indeterminado. Para obtener información adicional sobre el error, llame la función  GetLastError.

Si la función fracasa porque no puede encontrar más archivos, la función GetLastError retornará ERROR_NO_MORE_FILES.

Ejemplo de declaración de las funciones Win API FindFirstFileW y FindNextFileW (el código se ha tomado del archivo de inclusión 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. Ejemplo de uso de FindFirstFileW, FindNextFileW

El script "ListingFilesDirectory.mq5" es a la vez un ejemplo y prácticamente una copia absoluta del código de trabajo del asesor. En otras palabras, este código se aproxima al máximo a la realidad.

La tarea es obtener los nombres de todas las carpetas para la ruta TERMINAL_COMMONDATA_PATH — "Common". 

Tomando mi computadora como ejemplo, la ruta TERMINAL_COMMONDATA_PATH retorna el valor "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\Common". Significa que si cortamos de esta ruta "Common", entonces obtenedremos la ruta buscada "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\":


Fig. 5. Find First 

Normalmente para la búsqueda de todos los archivos se aplica la máscara de búsqueda "*.*". Esto significa que necesitamos realizar dos operaciones con las siguientes líneas: cortar la palabra "Common", y después añadir la máscara "*.*":

   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);

Vamos a comprobar qué ruta hemos obtenido al fin. Para ello, colocamos un punto de interrupción e iniciamos la depuración


 

Fig. 6. Depuración 

Obtenemos:

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

Por ahora, todo es correcto: la máscara de búsqueda de TODOS los archivos y carpetas en el directorio está lista "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\".

Continuamos: inicializamos el manejador de búsqueda "hFind" (en mi caso, esto lo hago por costumbre) y llamamos la función 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

Si la llamada de FindFirstFileW fracasa, entonces el manejador de búsqueda "hFind" será igual a "INVALID_HANDLE" y finalizará la ejecución del script.

En el caso de que se llame con éxito FindFirstFileW organizamos un ciclo do while, en el que obtenedremos el nombre del archivo o carpeta, y al final del ciclo se llamará la función 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);

El ciclo do while continuará hasta que la llamada de la función Win API FindNextFileW retorne un valor que no sea cero. Si la llamada de la función Win API FindNextFileW retorna cero y el error no es igual a "ERROR_NO_MORE_FILES", significa que ha habido un error crítico.

Al final del trabajo del script, cerramos el manejador de búsqueda

Resultado del funcionamiento del script "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. Echando un vistazo dentro de las carpetas de los terminales

El ejemplo descrito más arriba nos ha mostrado el funcionamiento en el nivel superior en la carpeta "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\". Pero nosotros recordamos el apartado 3.1. Secreto №1, de acuerdo al cual, tenemos que mirar en todas las carpetas incorporadas.

Para ello, organizamos una búsqueda de dos niveles, además, para la búsqueda en las carpetas incorporadas hay que usar esta máscara de búsqueda primera para la función Win API FindFirstFileW:

"C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\" + nombre de la carpeta encontrada del nivel superior + "origin.txt".

De esta forma, la búsqueda primera de FindFirstFileW en la carpeta incorporada buscará solo un archivo: "origin.txt".

Aquí mostramos el listado completo de la función 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);
  }

La función FindDataPath() imprime aproximadamente esta información:

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 

Aclaraciones de las primeras líneas de la impresión: en primer lugar se crea el flitro "filter_0" de la búsqueda primera (el filtro es igual a "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\*.*") y obtenemos el manejador de la búsqueda primera "hFirstFind_0", igual 1901014212592. Ya que el valor "hFirstFind_0" no es igual a "INVALID_HANDLE", siginifica que el filtro "filter_0" de la búsqueda primera transmitido a la función Win API FindFirstFileW(filter_0,ffd) es correcto. Después de llamar con éxito FindFirstFileW(filter_0,ffd), obtenemos el nombre de una primera carpeta cualquiera, se trata de la carpeta "038C9E8FAFF9EA373522ECC6D5159962". 

A continuación, debemos realizar la búsqueda del archivo origin.txt" dentro de la carpeta 038C9E8FAFF9EA373522ECC6D5159962. Para ello, formamos una máscara-filtro. Por ejemplo, para la carpeta 038C9E8FAFF9EA373522ECC6D5159962, esta máscara será así: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt". Si el manejador "hFirstFind_1", en este caso, no es igual a "INVALID_HANDLE", significa que en la carpeta indicada (038C9E8FAFF9EA373522ECC6D5159962) se encuentra el archivo buscado (origin.txt). 

En la impresión se ve bien que la búsqueda primera en las carpetas incorporadas a veces retorna "INVALID_HANDLE". Esto significa que en las carpetas indicadas no está el archivo "origin.txt". 

Vamos a ver con más detalle lo que hay que hacer cuando se encuentre en la carpeta incorporada el archivo "origin.txt".

3.5. CopyFileW

CopyFileW — copia un archivo existente en el nuevo archivo.

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

Parámetros

lpExistingFileName

[in] Nombre del archivo existente.

Aquí se adopta una limitación forzosa en la longitud del nombre MAX_PATH de los símbolos, esto siempre será suficiente para nuestro ejemplo.

Si el archivo con el nombre lpExistingFileName no existe, entonces la función fracasará y GetLastError retornará ERROR_FILE_NOT_FOUND.

lpNewFileName

[in]  Nombre del nuevo archivo. 

Aquí se adopta una limitación forzosa en la longitud del nombre MAX_PATH de los símbolos, esto siempre será suficiente para nuestro ejemplo.

bFailIfExists
[in] 
Si este parámetro es TRUE y el nuevo archivo indicado en lpNewFileName existe, la función fracasará. Si este parámetro es FALSE y el nuevo archivo existe, la función reescribirá el archivo existente y finalizará con éxito el trabajo.

Valor retornado

Si la función finaliza con éxito, entones el valor retornado no será igual a cero.

Si la función finaliza con error, el valor retornado será igual a cero. Para obtener información adicional sobre el error, llame la función  GetLastError.

Ejemplo de declaración de la función Win API CopyFileW (el código se ha tomado del archivo de inclusión ListingFilesDirectory.mqh):

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

3.6. Trabajando con el archivo "origin.txt"

Descripción del funcionamiento de la función ListingFilesDirectory.mqh::CopiedAndReadFile(string full_file_name).

En el parámetro de entrada la función recibe el nombre completo del archivo "origin.txt", que encontramos en una de las carpetas incorporadas. Puede ser una ruta aproximadamente así: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\origin.txt". Abriremos el archivo y leeremos el contenido del archivo "origin.txt" con la ayuda de MQL5, y esto significa que el archivo debe encontrarse en el "sandbox". Por eso debemos copiar el archivo "origin.txt" de la carpeta incorporada al sandbox (en este caso, el "sandbox" en los archivos generales de todos los terminales). Realizaremos este copiado con la ayuda de la función Win API CopyFileW.

Anotamos en la variable "new_path" la ruta al archivo "origin.txt" en el sandbox:

//+------------------------------------------------------------------+
//| 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

y con la llamada a la función Win API CopyFileW con el tercer parámetro igual a false, permitimos anotar de nuevo el archivo "origin.txt" en el sandbox:

//--- 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

Con los recursos MQL5 abrimos el archivo "origin.txt" para su lectura, no olvidando además indicar la bandera FILE_COMMON, ya que el archivo se encuentra en la carpeta de archivos generales:

//--- 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);
  }

Leemos solo una vez una línea, acabamos de escribir en ella al final "\\" y retornamos el resultado obtenido.

3.7. Últimos retoques

En los parámetros de entrada del asesor se establecen las rutas a las carpetas de instalación para los cuatro terminales:

//--- 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

Estas rutas se anotan rigurosamente, pues deberán indicar correctamente a las carpetas de instalación de los terminales.

Asimismo, más abajo, están declaradas a nivel global otras cuatro variables de línea y una matriz:

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];

En estas variables habrá que anotar las rutas a las carpetas de los terminales en AppData, y la matriz bidimensional nos ayudará a ello. Ahora podemos componer un esquema general de la forma de comparar las carpetas de instalación de los terminales Subordinados con sus carpetas en AppData:

GetStatsFromAccounts_EA.mq5::OnInit() >llamada> GetStatsFromAccounts_EA.mq5::FindDataFolders(arr_path) 
>llamada> ListingFilesDirectory.mqh::FindDataPath(string &array[][2]) >llamada> 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);

En la función ListingFilesDirectory.mqh::FindDataPath(string &array[][2]), cuando en las carpetas incorporadas de los terminales se detecta el archivo "origin.txt", se llama la función CopiedAndReadFile(string full_file_name), y después de su llamada se realiza la anotación en la matriz bidimensional. En la dimensión de la matriz "0" se escribe la ruta a la carpeta del terminal en AppData, y en la dimensión de la matriz "1" se escribe la ruta a la carpeta de instalación (esta ruta, recordemos, la obtenemos del archivo encontrado "origin.txt").

>retornamos el control a> GetStatsFromAccounts_EA.mq5::FindDataFolders(arr_path): 

aquí, mediante el rodeo sencillo con una matriz bidimensional, se rellenan las variables slaveTerminalDataPath1, slaveTerminalDataPath2, slaveTerminalDataPath3 y 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);
     }

Si hemos llegado hasta esta etapa, significa que el asesor ha comparado las rutas de instalación de los terminales y las rutas de sus carpetas en AppData. En el caso de que aunque sea una ruta a la carpeta del terminal en AppData no sea encontrada (es decir, que sea igual a NULL), entonces todas las rutas se imprimirán en las últimas líneas y el asesor finalizará el funcionamiento con error.


4. Eligiendo un asesor para la simulación

Antes de iniciar los cuatro terminales Subordinados, primero hay que elegir el archivo del experto a simular. Este experto debe estar compilado preliminarmente y ubicado en el catálogo de datos del terminal Maestro.

4.1. GetOpenFileName

GetOpenFileName crea el cuadro de diálogo "Abrir", que permite al usuario indicar el disco, carpeta y nombre del archivo o conjunto de archivos que serán abiertos. La declaración e implementación del cuadro de diálogo "Abrir" se muestra al completo en el archivo de inclusión GetOpenFileNameW.mqh.

4.2. Eligiendo un asesor con la ayuda del cuadro de diálogo de sistema "Abrir archivo"

El cuadro de diálogo de sistema "Abrir" se llama desde OnInit() del asesor:

//+------------------------------------------------------------------+
//| 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

donde tiene lugar la llamada de GetOpenFileNameW.mqh::OpenFileName(void)

//+------------------------------------------------------------------+
//| Creates an Open dialog box                                       |
//+------------------------------------------------------------------+
string OpenFileName(void)
  {
   string path=NULL;
   string filter=NULL;
   if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
      filter="Código compilado";
   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);
     }
  }

La variable "path", si se llama con éxito la función Win API GetOpenFileName, contendrá el nombre completo del archivo elegido, algo semejante a esto: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Experts\Examples\MACD\MACD Sample.ex5".

La variable "filter" es responsable del texto ① de la fig. 2. La línea "\0*.ex5\0" es responsable del filtro de los tipos de los archivos (② fig. 2). La línea "TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Experts\\"" establece la ruta que se abrirá en el cuadro de diálogo de sistema "Abrir".

4.3. Archivo ini de configuración

Para iniciar el terminal en la simulación del asesor desde una línea de comando (con la ayuda de Win API), hay que tener un archivo ini de configuración en el que debe estar obligatoriamente el apartado [Tester] y las indicaciones imprescindibles:

[Tester]
Expert=test             //nombre del experto que deber iniciarse para la simulación
Symbol=EURUSD           //nombre del instrumento que se usará como símbolo principal de simulación
Period=H1               //periodo del gráfico de simulación
Deposit=10000           //suma del depósito inicial para la simulación
Model=4                 //modo de generación de los ticks
Optimization=0          //activación/desactivación de la optimización e indicación de su tipo
FromDate=2016.01.22     //fecha inicial de la simulación
ToDate=2016.06.06       //fecha final de la simulación
Report=TesterReport     //nombre del archivo en el que se guardará el informe sobre los resultados de la simulación
ReplaceReport=1         //permitir/prohibir regrabar el archivo del informe 
UseLocal=1              //activar/desactivar la posibilidad de usar agentes locales para la simulación
Port=3000               //puerto del agente de simulación
Visual=0                //activar o desactivar la simulación en el modo visual
ShutdownTerminal=0      //permitir/prohibir desactivar la plataforma comercial al finalizar la simualción 

Adelantándonos un poco, diremos que este apartado [Tester] lo añadiremos al archivo independientemente.

Se ha decido tomar como base un archivo ini del terminal Maestro. Esta archivo (common.ini) se ubica en el catálogo de datos del terminal, en la carpeta "config ". Para mi terminal, la ruta a él tendrá el aspecto siguiente: "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\config\common.ini".

El esquema del trabajo con el archivo ini es así:

  1. Obtener la ruta completa a "common.ini" del terminal Maestro. La ruta completa es una línea del tipo 
    "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\config\common.ini". (MQL5)
  2. Obtener la nueva ruta al archivo ini en el sandbox "\Files". La nueva ruta es una línea del tipo:
    "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\myconfiguration.ini" del terminal Maestro. (MQL5)
  3. Copiar al archivo "common.ini" en "myconfiguration.ini". (La función WIn API CopyFileW).
  4. Editar el archivo "myconfiguration.ini". (MQL5).
  5. Obtener la nueva ruta al archivo ini en el sandbox del terminal Subordinado. Es una línea del tipo (usando como ejemplo mi terminal Subordinado №1)
    "C:\Users\KVN\AppData\Roaming\MetaQuotes\Terminal\038C9E8FAFF9EA373522ECC6D5159962\MQL5\Files\myconfiguration.ini". (MQL5)
  6. Copiar el archivo ini editado "myconfiguration.ini" del sandbox del terminal Maestro al sandbox del terminal Subordinado. (La función WIn API CopyFileW).
  7. Eliminar el archivo "myconfiguration.ini" del sandbox del terminal Maestro. (MQL5)

Para cada terminal Subordinado hay que repetir este esquema. Aunque aquí haya sitio para la optimización, no me he planteado el objetivo de describir este proceso en este artículo. 

Procederemos a la edición de los archivos ini de configuración después de haber elegido el asesor para la simulación, 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

Esquema de trabajo con el archivo ini, tomando como ejemplo el terminal Subordinado №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

En la llamada de la función EditCommonIniFile(common_file_name,3000,4) se transmite:

common_file_name — nombre del archivo ini que se debe editar;

3000 — número del puerto del agente de simulación. Cada terminal debe iniciarse en su agente de simulación. La numeración de los agentes comienza a partir del 3000. Es posible ver los números de los puertos de los agentes de simulación de la forma siguiente: en el terminal MetaTrader 5 hay que entrar en el simulador de estrategias y pulsar el botón derecho del ratón en la pestaña "Diario" del simulador de estrategias. Además, en la lista desplegable del menú se puede ver la numeración de los puertos de los agentes de simulación:


 

Fig. 7. Agentes de simulación 

4 - tipo de simulación: 

  • 0 — "Todos los ticks",
  • 1 — "1 minuto OHLC",
  • 2 — "Solo precios de apertura",
  • 3 — "Cálculos matemáticos",
  • 4 — "Cada tick basado en ticks reales"

La edición del archivo commom.ini de configuración se ejecuta en la función GetStatsFromAccounts_EA.mq5::EditCommonIniFile(string name,const int port,const int model). Las operaciones de apertura del archivo, lectura del archivo y grabado del archivo en el archivo se ejecutan con los recursos de 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]
//--- abrimos el archivo 
   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. Secreto №2

Antes de finalizar el funcionamiento, el terminal MetaTrader 5 guarda la ubicación de las ventanas y paneles, así como sus dimensiones en el archivo "terminal.ini". El propio archivo se encuentra en el catálogo de datos del terminal, en la subcarpeta "config". Por ejemplo, para mi termianl Subordinado №1, la ruta completa a "terminal.ini" será esta:

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

En el propio archivo "terminal.ini" nos interesará solo el bloque "[Window]". Minimizamos en una ventana el terminal MetaTrader 5. El terminal adoptará aproximadamente este tamaño:


Fig. 8. Terminal minimizado en una ventana

Si este terminal está cerrado, entonces en el archivo terminal.ini el bloque [Window] tendrá este aspecto:

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

Es decir, el bloque [Window] guarda las coordenadas del terminal y su estado. 


4.5. Establecemos el tamaño del terminal (anchura, altura). Pegamos las líneas en la parte media del archivo 

Es necesario cambiar las coordenadas en los archivos terminal.ini de los terminales Subordinados, para que los cuatro Terminales Subordinados se configuren al iniciarse de la siguiente forma:

 

Fig. 9. Ubicación de los terminales

Como ya hemos dicho más arriba, hay que editar el archivo "terminal.imi" para cada terminal Subordinado. Aquí hay que prestar atención a que las líneas se deben pegar no al final, sino en la parte media del archivo "terminal.ini". Más abajo se muestran las particularidades de este procedimiento.

Lo aclararé con este ejcmplo: tenemos el archivo "test.txt" ubicado en el "sandbox" del terminal. Contenido del archivo "test.txt":

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

Hay que cambiar la información en la segunda y tercera línea, para obtener lo siguiente:

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

A primera vista, hay que actuar así:

  • abrir el archivo para lectura y guardado, leer la primera línea (esta operación reubicará el puntero al archivo al inicio de la segunda línea);
  • anotar en la segunda línea el nuevo valor "df=1256";
  • anotar en la tercera línea el nuevo valor "asf=5";
  • cerrar el archivo.
Veámoslo usando como ejemplo el código del script "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;
     }
  }
//+------------------------------------------------------------------+

Obtenemos un resultado inesperado, en la cuarta línea han desaparecido los símbolos "g=":

Era Ahora es 
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

¿Por qué ha sucedido esto? Imagínese que el archivo consta de multitud de celdas que van unas detrás de otras. En cada celda cabe un símbolo. Por eso, cuando escribimos en un archivo comenzando desde su parte media, lo que estamos haciendo, en esencia, es anotar celdas de nuevo. Si añadimos más símbolos de los que había en este lugar inicialmente (como en el ejemplo de más arriba: había "df=12", y nosostros hemos añadido dos símbolos más - "df=1256"), entonces los símbolos sobrantes simplemente dañarán el código posterior. Este es el aspecto que tiene:

write string

Fig. 10. Daño a la información.

para evitar que la información sufra daños al insertar líneas en la parte media de un archivo, haremos lo siguiente.

  • Copiamos del terminal Subordinado el archivo "terminal.ini" en el sandbox del terminal Maestro, en el archivo con el nombre "terminal_ext.ini" (Win API CopyFileW).
  • Creamos en el sandbox del terminal Maestro el archivo "terminal.ini", lo abrimos en la anotación (MQL5).
  • Abrimos en el sandbox del terminal Maestro el archivo "terminal_ext.ini" para la lectura (MQL5).
  • En el sandbox del terminal Maestro: calculamos las líneas de "terminal_ext.ini" y las anotamos en el archivo "terminal.ini" (MQL5).
  • En cuanto la línea calculada sea igual a "[Window]" - anotamos en el archivo "terminal.ini" las nuevas coordenadas (son seis líneas), y en el archivo "terminal_ext.ini" reubicamos el puntero al archivo en seis líneas (MQL5).
  • En el sandbox del terminal Maestro: calculamos las líneas de "terminal_ext.ini" y las anotamos en el archivo "terminal.ini" hasta que no observemos el final del archivo (MQL5).
  • En el sandbox del terminal Maestro: cerramos los archivos "terminal.ini" y "terminal_ext.ini" (MQL5).
  • Copiamos del sandbox del terminal Maestro el archivo "terminal.ini" al terminal Subordinado, en el archivo "terminal.ini" (Win API CopyFileW).
  • En el sandbox del terminal Maestro: eliminamos los archivos "terminal.ini" y "terminal_ext.ini" (MQL5).

Orden de llamada de las funciones:

GetStatsFromAccounts_EA.mq5::OnInit() >llamada> 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);
  }

 >llamada> 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);
  }

De esta forma, se editan los archivos "terminal.ini" en los terminales Subordinados, lo que permite iniciarlos de la misma forma que en la fig. 9. Además, es posible observar los gráficos de la simulación y comparar la precisión de la simulación en los diferentes modos. 


5. Iniciando los terminales Subordinados para la simulación

En este momento ya tenemos todo listo para iniciar los terminales Subordinados en el modo de simulación del asesor:

  • hemos preparado los archivos de configuración "myconfiguration.ini" para todos los terminales Subordinados;
  • hemos editado los archivos "terminal.ini" de todos los terminales Subordinados;
  • sabemos el nombre del asesor que va a someterse a la simulación.
Solo quedan dos tareas: copiar el asesor elegido al sandbox de los terminales Subordinados e iniciar dichos terminales.

5.1. Copiando el asesor a las carpetas de los terminales Subordinados

El copiado del asesor elegido con anterioridad (su nombre está guardado en la variable "expert_name") se realiza en 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 — ejecuta una operación en el archivo indicado.

//--- 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             //
   );

Parámetros

hwnd

[in] Manejador de la ventana base, usada para representar la interfaz de usario y los mensajes sobre errores. Este valor deberá ser NULL, si la operación no está relacionada con las ventanas.

lpOperation

[in] Líneas con los nombres del comando que define la acción que va a ejecutarse. El conjunto de comandos accesibles depende de un archivo o carpeta concreto. Normalemente, son acciones accesibles desde el menú contextual del objeto. Habitualmente se usan los siguientes comandos:

"edit"

Inicia el editor y abre el documento a editar. Si lpFile no es un archivo del documento, entonces la función no se ejecutará.

"explore"

Abre la carpeta indicada en lpFile.

"find"

Inicializa la búsqueda comenzada en el catálogo indicado en lpDirectory.

"open"

Abre el elemento establecido por el parámetro lpFile. Este elemento puede ser un archivo o una carpeta.

"print"

Imprime el archivo indicado en lpFile. Si lpFile no es el archivo del documento, la función finaliza con error.

"NULL"

Se usa el nombre del comando por defecto, si este existe. Si no existe ese comando, entonces se usará el comando "open". Si no se usa ningún comando, entonces el sistema usará el primer comando indicado en el registro.

lpFile 

[in] Línea que establece el archivo u objeto en el que se puede ejecutar el comando. Se transmite el nombre completo (incluido no solo el nombre del archivo, sino la ruta hasta él). Preste atención a que el objeto puede no dar soporte a todos los comandos. Por ejemplo, no todos los documentos dan soporte al comando "print". Si la ruta relativa se usa para eln parámetro lpDirectory, entonces no use la ruta relativa para lpFile.

lpParameters

[in] Si lpFile indica un archivo ejecutable, este parámetro será una línea que define los parámetros transmitidos a la aplicación. El formato de esta línea es definido por el nombre del comando que deberá ser ejecutado. Si lpFile indica el archivo del documento, lpParameters deberá ser NULL.

lpDirectory

[in] Línea que define el catálogo de trabajo. Si este valor es NULL, se usa el catálogo de trabajo actual. Si la ruta relativa no ha sido establecida en lpFile, no use entonces la ruta relativa para lpDirectory.

nShowCmd

[in] Banderas que definen cómo se debe representar la aplicación al abrirse. Si lpFile define el archivo del documento, la bandera simplemente se transmite a la aplicación correspondiente. Banderas utilizadas:

//+------------------------------------------------------------------+
//| 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,
  };

Valor retornado

Si la función finaliza con éxito, retornará un valor superior a 32.

Ejemplo de declaración de una función Win API ShellExecuteW:

#import  "shell32.dll"
int  GetLastError();
//+------------------------------------------------------------------+
//| ShellExecute function                                            |
//| https://msdn.microsoft.com/es-es/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. Iniciando los terminales

Se inician los terminales subordinados desde 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);
  }

además, entre inicios, el asesor esperará "sleeping" milisegundos. Por defecto, el parámetro "sleeping" es igual a 9000 (es decir, 9 segundos). Si tiene lugar un error de autenticación de los agentes en los terminales Subordinados, aumente este parámetro. 

Los parámetros transmitidos a la función Win API (usando como ejemplo mi terminal Subordinado №1) tienen este aspecto:

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


6. Posibles errores

Puede darse la situación en la que uno de los terminales Subordinados se inicia, pero el simulador no puede conectarse al agente de simulación.

En el simulador, en la pestaña "Diario" encontraremos aproximadamente esta entrada:

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

En los logs del agente, las entradas serán las siguientes:

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

En estas situaciones, se recomienda aumentar la pausa entre inicios de los terminales (la variable "sleeping"), y también aligerar todas las aplicaciones que consumen muchos recursos, que pueden ocupar durante su uso los núcleos del procesador.


Conclusión

La tarea inicial de la simulación del asesor elegido en cuatro modos de simulación a la vez ha sido ejecutada con éxito. Después del inicio, se podrá observar de forma prácticamente simultánea cómo tiene lugar la simulación de los cuatro terminales al mismo tiempo.

Asimismo, hemos mostrado en el artículo cómo llamar tales funciones Win API como:

  • CopyFileW — copia los archivos en el "sandbox" y desde el "sandbox" MQL5.
  • FindClose — cierra los manejadores de búsqueda.
  • FindFirstFileW — busca el catálogo del archivo o el subcatálogo cuyo nombre corresponda al nombre indicado del archivo.
  • FindNextFileW — continúa la búsqueda del archivo de la anterior llamada de la función FindFirstFile.
  • GetOpenFileNameW — llama el cuadro de diálogo de sistema de apertura de archivos
  • ShellExecuteW — inicio de la aplicación