Optimización móvil continua (Parte 5): Panorámica del proyecto del optimizador automático, creación de la interfaz gráfica

Andrey Azatskiy | 16 junio, 2020

Introducción

En los artículos anteriores, ya vimos cómo una parte del proyecto se relaciona directamente con el terminal, al igual que aquella que describe el uso de todo el proyecto en acción. El artículo anterior se adelantaba un poco respecto al ciclo completo de artículos, y se pensó de esa forma por dos motivos. En primer lugar, suponía una especie de instrucciones de uso de la aplicación obtenida. En segundo lugar, el artículo ilustra la idea de creación de la aplicación y su lógica de uso, cuyo conocimiento nos permitirá orientarnos en el código con mayor facilidad.

El lector podrá leer los artículos mencionados clicando sobre los siguientes enlaces:

  1. Optimización móvil continua (Parte 1): Mecanismo de trabajo con los informes de optimización
  2. Optimización móvil continua (Parte 2): Mecanismo de creación de informes de optimización para cualquier robot
  3. Optimización móvil continua (Parte 3): Método de adaptación del robot al optimizador automático
  4. Optimización móvil continua (Parte 4): Programa de control de la optimización (optimizador automático)

Los artículos anteriores de los se han tomado prestados algunos desarrollos, se pueden encontrar en los siguientes enlaces:

  1. Gestión de la optimización (Parte I): Creando una interfaz gráfica
  2. Gestión de la optimización (Parte 2): Creando los objetos clave y la lógica de la aplicación

El presente artículo tratará sobre la estructura del proyecto en IDE Visual Studio y sus componentes. En esta ocasión, describiremos la parte gráfica de la aplicación creada, la estructura del directorio controlado por ella -donde se encuentran los resultados de la optimización-, y también los cambios en las clases que tomamos prestadas del anterior proyecto, encargadas de controlar el proceso de optimización.


Panorámica de la estructura del proyecto

Como la presente parte del artículo está de nuevo dedicada al lenguaje C#, merece la pena analizar la estructura de sus archivos, para así orientarnos mejor en el proyecto:

Solution, que podemos encontrar en los archivos adjuntos, contiene dos proyectos. Uno de ellos ya ha sido analizado en el primer artículoal otro proyecto se le dedicarán los artículos finales del presente ciclo. Este proyecto supone el optimizador automático propiamente dicho.


Como el proyecto tiene una interfaz gráfica, hemos adoptado un enfoque semejante al del proyecto anterior, con un MVVM nombrado (ModelViewViewModel). Después de las partes de la plantilla utilizada, el proyecto se divide en los apartados correspondientes. Dado que toda la lógica del proyecto deberá encontrarse en la parte del modelo, las clases no relacionadas con la parte gráfica del proyecto están ubicadas en el directorio Model incorporado y divididas en directorios propios.

Vamos a comenzar analizando los objetos que han sufrido algunos cambios respecto al anterior ciclo de artículos. Además, merece la pena describirlos para aquellos lectores que no estén familiarizados con la parte anterior


Creando la parte gráfica de la aplicación

Vamos a analizar la interfaz gráfica. Antes, mostramos un método para crear una adición a la plataforma MetaTrader 5 en el lenguaje C# y encajar su funcionalidad con el experto con la ayuda de una biblioteca DLL y la llamada de retorno OnTimer; en la presente implementación, hemos tomado la decisión de sacar el optimizador automático del terminal. Ahora, supone (a su manera) un gestor de optimización externo que imita el trabajo del tráder en cuanto al inicio de los procesos de optimización y el procesamiento de los resultados. En este caso, además, renunciando a la optimización simultánea en varios terminales en la misma computadora y sacando el optimizador automático a una aplicación aparte, tenemos a nuestra disposición absolutamente todos los terminales instalados en la computadora, e incluso la propia computadora en la que ha sido iniciado el optimizador. En el proyecto anterior, no podíamos implicar el funcionamiento de esta.

Por eso, el proyecto actual no ha sido escrito al completo en una biblioteca DLL, como lo hicimos antes, sino dividido entre una biblioteca DLL y el archivo ejecutable del proyecto del optimizador automático del que hablamos en el presente artículo.


Como podemos ver por la captura de pantalla mostrada, la ventana del proyecto consta de un Encabezado y un Pie de página, además de un TabControl con dos pestañas: Settings y Reports. La parte del encabezado (al igual que sucede con el pie de página) no cambia sea cual sea la pestaña que seleccionemos en la parte central de la ventana, lo que permite acceder en cualquier momento a todos los elementos de control ubicados en ambos.

El encabezado de la ventana se crea con la siguiente etiqueta XALM:

<Grid>
        <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <WrapPanel Margin="2">
            <Label Content="Optimisation:"/>
            <ComboBox Width="200"
                      ItemsSource="{Binding SelectedOptimisationNames,UpdateSourceTrigger=PropertyChanged}"
                      SelectedItem="{Binding SelectedOptimisation}"
                      SelectedIndex="0"
                      IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/>
            <Button Content="Load" 
                    Margin="2,0,0,0"
                    Width="34"
                    Command="{Binding LoadResults}"
                    IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/>
        </WrapPanel>

        <WrapPanel HorizontalAlignment="Right" 
                   Margin="2"
                   Grid.Column="1">
            <Label Content="Terminal:"/>
            <ComboBox Width="200"
                      SelectedIndex="{Binding SelectedTerminalIndex}"
                      ItemsSource="{Binding Terminals,UpdateSourceTrigger=LostFocus}"
                      IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/>
        </WrapPanel>
</Grid>


El contenedor Grid en el que se ubican los elementos de control mostrados en la zona visualizada, se divide en 2 columnas. En la primera columna se añaden: la denominación del parámetro (Optimisation) y una lista desplegable con la lista de optimizaciones disponibles, así como el botón de carga de la optimización. La segunda columna contiene el nombre del parámetro y una lista desplegable con la Id de los terminales. 

El contenedor Grid, que muestra el pie de página de la ventana gráfica (con ProgressBar), tiene una estructura semejante:

<Grid Grid.Row="2">
        <Grid.ColumnDefinitions>
                <ColumnDefinition Width="150"/>
                <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <Label Content="{Binding Status, UpdateSourceTrigger=PropertyChanged}"/>
        <ProgressBar Grid.Column="1"
                     Value="{Binding Progress, UpdateSourceTrigger=PropertyChanged}"
                     Minimum="0"
                     Maximum="100"/>
</Grid>

Este simplemente divide su zona en 2 partes, limitando el tamaño de la primera de ellas. Como resultado, cedemos la mayor parte de este contenedor a ProgressBar; además, la anchura de ProgressBar cambiará al variar la anchura de la ventana en general. Los 3 componentes de la ventana mencionados, de acuerdo con las reglas de la etiqueta XALM, se ubican en un contendor <Window/>.

<Window x:Class="Metatrader_Auto_Optimiser.AutoOptimiser"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:vm="clr-namespace:Metatrader_Auto_Optimiser.View_Model"
        xmlns:v="clr-namespace:Metatrader_Auto_Optimiser.View"
        mc:Ignorable="d"
        Title="Auto Optimiser" Height="500" Width="1200"
        MinHeight="500" MinWidth="1200">

    <Window.DataContext>
        <vm:AutoOptimiserVM/>
    </Window.DataContext>


    ...


</Window>

 En este contenedor se definen los enlaces a los espacios de nombres:

Asimsimo, se han establecido las dimensiones mínimas de la ventana y aquellas que tendrá al abrirse con el inicio del programa. A continuación, usando el alias mencionado arriba para el espacio de nombres, que contiene ViewModel, establecemos DataContext para la interfaz gráfica. 

La parte central del panel consta del elemento TabControl, que contiene 2 pestañas. En esencia, se trata de la parte principal, el "Cuerpo" de nuestra interfaz gráfica. La estructura de la pestaña "Settings" es la siguiente:


Esta pestaña también se divide en 3 partes. En la parte superior de esta pestaña se encuentra el panel con el ajuste de los parámetros del informe guardado del optimizador automático, así como la selección del nombre del activo y la tecla de actualización (*set) del archivo. La parte media de la pestaña "Settings" contiene los ajustes del optimizador automático, así como la selección de los indicadores para el filtrado y la clasificación de los datos durante la optimización automática. La parte final contiene el ajuste de los parámetros del experto y la selección de las fechas de optimización y las pruebas en tiempo real. Para que el rellenado de los ajustes sea más cómodo, entre las dos partes principales de la pestaña anteriormente mencionada se ubica el elemento GridSplitter, que permite modificar fácilmente las dimensiones de las dos pestañas mencionadas tirando del mismo. Esto resulta especialmente cómodo cuando necesitamos rellenar los parámetros de optimización para trabajar con una gran lista de datos de entrada.

Vamos a analizar con mayor detalle el código de marcado de la primera de las tres pestañas que componen "Settings":

<Grid>
        <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>

        <WrapPanel HorizontalAlignment="Left"
                VerticalAlignment="Bottom">
        <Label Content="Select Optimiser:"/>
        <ComboBox Width="150"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"
                ItemsSource="{Binding Optimisers}"
                SelectedIndex="{Binding SelectedOptimiserIndex}"/>
        <Button Content="GUI"
                Command="{Binding ShowOptimiserGUI}"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/>
        <Label Content="Directory prefix:"/>
        <TextBox Width="150"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"
                Text="{Binding DirPrefix}"/>
        <ComboBox Width="100" 
                Margin="2,0,0,0"
                SelectedIndex="0"
                ItemsSource="{Binding FileFillingType}"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"
                SelectedItem="{Binding FileWritingMode}"/>
        <Label Content="Asset name:"/>
        <TextBox Width="100"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"
                Text="{Binding AssetName}"/>
        <Button Content="Update (*.set) file"
                Margin="2,0,0,0"
                IsEnabled="{Binding EnableMainTogles}"
                Command="{Binding UpdateSetFile}"/>
        </WrapPanel>
        <Button Content="Start/Stop"
                Grid.Column="2"
                Margin="2"
                Command="{Binding StartStopOptimisation}"/>

</Grid>

La parte descrita se divide en dos columnas: la primera de ellas cambia su anchura dinámicamente, mientras que la otra permanece fija en 100 píxeles. En la primera columna se encuentran todos los elementos de control ubicados en el panel analizado, todos ellos introducidos en el envoltorio WrapPanel, que permite colocar los elementos uno tras otro. En primer lugar van los elementos de control encargados de seleccionar el optimizador automático y sus ajustes. A continuación, se ubican los parámetros para denominar la carpeta con el informe de optimización y el método de creación de este informe (Rewrite, Append). Finalmente, se establece el nombre del activo en el que se realiza la optimización y el botón de actualización (*set) del archivo con los parámetros del robot. La columna con anchura fija se dedica por completo al botón "Start/Stop", y precisamente ella sirve como comienzo del inicio de la optimización, además de como elemento externo de control para deternerla. 

La segunda parte de la pestaña "Settings" se divide en 2 partes.


La primera de ellas contiene un ListView con la lista de parámetros de ajuste del optimizador. En este caso, además, las denominaciones y los valores de los parámetros son análogos a los campos de ajuste del optimizador en el terminal. En la segunda parte se establecen los coeficientes de filtrado y clasificación de datos. Los datos de la columna también tienen el elemento GridSplitter, que divide entre sí las zonas descritas. La marca que crea los elementos mencionados es trivial, por eso no la vamos a mostrar. Si el lector lo desea, podrá estudiar en el código adjunto la variante completa de marcado de estos elementos. La parte inferior de la pestaña es completamente idéntica a la superior, con la única salvedad de la parte más a la derecha, que contiene las fechas de las optimizaciones, y está también dividida en dos secciones. En la primera de ellas, se encuentran los elementos de control para añadir los datos a la lista, mientras que la segunda está dedicada a la representación de la lista creada.

El elemento final de la interfaz gráfica es la pestaña "Results", que está pensada para visualizar los resultados de optimización, así como los tests en tiempo real e históricos.  


Como podemos ver por la imagen adjunta, esta pestaña tiene una estructura más interesante que la anterior. Se divide en dos partes y está separada por el elemento GridSplitter, que permite reducir la parte inferior o superior para estudiar con mayor detalle los resultados de optimización. La parte superior contiene dos elementos del grupo TabItem, uno incorporado en el otro. La pestaña "Selected pass", en la que se encuentran los tests en tiempo real e históricos, no es tan interesante como la pestaña "Optimisations", pero volveremos a ellas más tarde.

La parte inferior de la pestaña descrita contiene dos campos divididos por un GridSplitter vertical. El primero sirve para indicar las fechas y los modos de simulación en uno de los recuadros de la parte superior, mientras que el otro sirve para representar una serie de indicadores reunidos en un recuadro, para que estos resulten más sencillos de leer. Asimismo, aquí podemos ver la lista de parámetros de la pasada de optimización seleccionada (pestaña "Bot Params").

La comparación de elementos de la marca con sus resultados en la pestaña "Optimisations" tiene la estructura siguiente:


Esta pestaña, al igual que la "Selected pass" colindante, dispone de la tecla "Save to (*csv)", que guarda en un archivo los resultados de todas las optimizaciones realizadas en la fecha seleccionada. No obstante, a los botones mencionados se le añaden nuevos botones de clasificación y filtrado de datos, que han sido introducidos en un recuadro que representa los resultados de todas las optimizaciones. En cuanto a su estructura, el recuadro con los resultados de optimización es análogo a los mismos recuadros ubicados en las pestañas "Selected pass.History" y "Selected pass.Forward". El propio fragmento de marcado que crea los datos del recuadro tiene el aspecto siguiente:

<ListView ItemsSource="{Binding AllOptimisations}"
          SelectedIndex="{Binding SelecterReportItem}"
          v:ListViewExtention.DoubleClickCommand="{Binding StartTestReport}">
        <ListView.View>
                <GridView>
                        <GridViewColumn Header="Date From" DisplayMemberBinding="{Binding From}"/>
                        <GridViewColumn Header="Date Till" DisplayMemberBinding="{Binding Till}"/>
                        <GridViewColumn Header="Sort by" DisplayMemberBinding="{Binding SortBy}"/>
                        <GridViewColumn Header="Payoff" DisplayMemberBinding="{Binding Payoff}"/>
                        <GridViewColumn Header="Profit pactor" DisplayMemberBinding="{Binding ProfitFactor}"/>
                        <GridViewColumn Header="Average Profit Factor" DisplayMemberBinding="{Binding AverageProfitFactor}"/>
                        <GridViewColumn Header="Recovery factor" DisplayMemberBinding="{Binding RecoveryFactor}"/>
                        <GridViewColumn Header="Average Recovery Factor" DisplayMemberBinding="{Binding AverageRecoveryFactor}"/>
                        <GridViewColumn Header="PL" DisplayMemberBinding="{Binding PL}"/>
                        <GridViewColumn Header="DD" DisplayMemberBinding="{Binding DD}"/>
                        <GridViewColumn Header="Altman Z score" DisplayMemberBinding="{Binding AltmanZScore}"/>
                        <GridViewColumn Header="Total trades" DisplayMemberBinding="{Binding TotalTrades}"/>
                        <GridViewColumn Header="VaR 90" DisplayMemberBinding="{Binding VaR90}"/>
                        <GridViewColumn Header="VaR 95" DisplayMemberBinding="{Binding VaR95}"/>
                        <GridViewColumn Header="VaR 99" DisplayMemberBinding="{Binding VaR99}"/>
                        <GridViewColumn Header="Mx" DisplayMemberBinding="{Binding Mx}"/>
                        <GridViewColumn Header="Std" DisplayMemberBinding="{Binding Std}"/>
                </GridView>
        </ListView.View>
</ListView>

TabItem, donde se encuentran los filtros y parámetros de clasificación de los resultados de optimización, es completamente idéntico a su análogo de la pestaña "Settings". Sin embargo, a pesar de que están separados en el marcado, ViewModel está construido de tal forma que todos los cambios de uno se registran instantáneamente en el otro. El propio mecanismo de registro de dichos cambios se analizará en el próximo artículo.  

Como podemos ver en este párrafo, el propio marcado de la interfaz gráfica es bastante sencillo. El programa no se ha implementado haciendo hincapié en un aspecto visual atractivo, sino en la funcionalidad. Si el lector quiere hacer la aplicación más atractiva, deberá editar el archivo App.xaml, el repositorio centralizado de los temas del proyecto. 


Las clases tomadas del anterior ciclo de artículos "Gestión de la optimización", y sus modificaciones

Como podemos notar echando un vistazo a este capítulo, al escribir el proyecto, hemos implicado los objetos anteriormente creados para el proyecto descrito en el ciclo de artículos "Gestión de la optimización". No vamos a presentar una descripción completa de cada uno de estos objetos, dado que ya fueron descritos en los artículos mencionados, no obstante, sí que nos detendremos con más detalle en alguno de ellos, especialmente en aquellos que han sufrido cambios en el presente proyecto. La lista completa de los objetos que hemos tomado del ciclo anterior es la siguiente:

Los cuatro últimos objetos de la lista mostrada se pueden valorar como una API de escritura propia para trabajar con el terminal de código C#. Debemos decir que los cambios descritos en esta parte del artículo, aunque también se relacionan con varios de los objetos enumerados, fueron solo internos. En otras palabras: la interfaz externa de trabajo con estas clases (los métodos públicos y sus propiedades) ha permanecido inalterada en cuanto a su signatura. Por ello, incluso si el lector cambia las anteriores implementaciones de estos objetos en el pasado proyecto por las nuevas, seguirá compilándose y funcionando. 

El primer objeto cuya estructura se ha visto modificada es la clase Config. Esta clase supone un recuadro que se describe en el apartado correspondiente de la documentación del terminal. Contiene todos los campos de este recuadro en sus propiedades, y al modificarse esta o aquella propiedad, el usuario cambia el valor para una clave concreta de una sección concreta del archivo de inicialización del terminal.  Los propios archivos de inicialización (*.ini) son un formato largamente utilizado, y para trabajar con ellos en el núcleo de OS Windows existen varias funciones, dos de las cuales hemos exportado a nuestro código C#. En la anterior implementación de esta clase, los métodos usados se importaban directamente a la clase Config. En la actual implementación, estos métodos se han introducido en la clase aparte "IniFileManager".

class IniFileManager
{
    private const int SIZE = 1024; //Tamaño máximo (para la lectura del valor desde el archivo)
        
    public static string GetParam(string section, string key, string path)
    {
        //Para obtener el valor
        StringBuilder buffer = new StringBuilder(SIZE);

        //Obtener el valor en buffer
        if (GetPrivateProfileString(section, key, null, buffer, SIZE, path) == 0)
            ThrowCErrorMeneger("GetPrivateProfileStrin", Marshal.GetLastWin32Error(), path);

        //Retornar el valor obtenido
        return buffer.Length == 0 ? null : buffer.ToString();
    }
    /// <summary>
    /// Valor atípico del error
    /// </summary>
    /// <param name="methodName">Nombre del método</param>
    /// <param name="er">Código de error</param>
    private static void ThrowCErrorMeneger(string methodName, int er, string path)
    {
        if (er > 0)
        {
            if (er == 2)
            {
                if (!File.Exists(path))
                    throw new Exception($"{path} - File doesn1t exist");
            }
            else
            {
                throw new Exception($"{methodName} error {er} " +
                    $"See System Error Codes (https://docs.microsoft.com/ru-ru/windows/desktop/Debug/system-error-codes) for detales");
            }
        }
    }

    public static void WriteParam(string section, string key, string value, string path)
    {
        //Anotar el valor en un archivo INI
        if (WritePrivateProfileString(section, key, value, path) == 0)
            ThrowCErrorMeneger("WritePrivateProfileString", Marshal.GetLastWin32Error(), path);
    }
}

De esta forma, en el archivo Config se quedan solo aquellos campos que se contienen en el archivo de configuración. No merece la pena dar una descripción completa del objeto: ya lo hicimos en artículos anteriores sobre la gestión de la optimización, y el volumen fue considerable.  

La siguiente clase que ha sufrido algunas modificaciones es la clase "TerminalManager". El propio rellenado de la clase no ha experimentado cambios en su mayor parte. No vamos a analizar su método de funcionamiento y sus componentes, ya que se trata de una clase prestada. No obstante, dado su papel en la aplicación (ya que él inicia y detiene el funcionamiento del terminal), consideramos necesario mostrar el código completo de su implementación.   

class TerminalManager
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="TerminalChangeableDirectory">
    /// Ruta al directorio con los archivos modificados (la que se encuentra en AppData)
    /// </param>
    public TerminalManager(DirectoryInfo TerminalChangeableDirectory) :
        this(TerminalChangeableDirectory, new DirectoryInfo(File.ReadAllText(TerminalChangeableDirectory.GetFiles().First(x => x.Name == "origin.txt").FullName)), false)
    {
    }
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="TerminalChangeableDirectory">
    /// Ruta al directorio con los archivos modificados
    /// </param>
    /// <param name="TerminalInstallationDirectory">
    /// Ruta a la carpeta con el terminal
    /// </param>
    public TerminalManager(DirectoryInfo TerminalChangeableDirectory, DirectoryInfo TerminalInstallationDirectory, bool isPortable)
    {
        this.TerminalInstallationDirectory = TerminalInstallationDirectory;
        this.TerminalChangeableDirectory = TerminalChangeableDirectory;

        TerminalID = TerminalChangeableDirectory.Name;

        CheckDirectories();

        Process.Exited += Process_Exited;

        Portable = isPortable;
    }
    /// <summary>
    /// Destructor
    /// </summary>
    ~TerminalManager()
    {
        Close();
        Process.Exited -= Process_Exited;
    }
    /// <summary>
    /// Proceso de inicio del terminal
    /// </summary>
    private readonly System.Diagnostics.Process Process = new System.Diagnostics.Process();
    /// <summary>
    /// Evento de finalización del proceso iniciado
    /// </summary>
    public event Action<TerminalManager> TerminalClosed;

    #region Terminal start Arguments
    /// <summary>
    /// Login para el comienzo - bandera /Login
    /// </summary>
    public uint? Login { get; set; } = null;
    /// <summary>
    /// inicio de la plataforma con un perfil determinado. 
    /// El perfil debe haberse creado de antemano y encontrarse en la carpeta /profiles/charts/ de la plataforma comercial
    /// </summary>
    public string Profile { get; set; } = null;
    /// <summary>
    /// Archivo de configuración en forma de objeto /Config
    /// </summary>
    public Config Config { get; set; } = null;
    /// <summary>
    /// Bandera de inicio del terminal en el modo /portable
    /// </summary>
    private bool _portable;
    public bool Portable
    {
        get => _portable;
        set
        {
            _portable = value;
            if (value && !TerminalInstallationDirectory.GetDirectories().Any(x => x.Name == "MQL5"))
            {
                WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized;

                if (Run())
                {
                    System.Threading.Thread.Sleep(1000);
                    Close();
                }
                WaitForStop();
                WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
            }
        }
    }
    /// <summary>
    /// estilo de la ventana del proceso iniciado
    /// </summary>
    public System.Diagnostics.ProcessWindowStyle WindowStyle { get; set; } = System.Diagnostics.ProcessWindowStyle.Normal;
    #endregion

    #region Terminal directories
    /// <summary>
    /// Ruta a la carpeta donde está instalado el terminal
    /// </summary>
    public DirectoryInfo TerminalInstallationDirectory { get; }
    /// <summary>
    /// Ruta a la carpeta del terminal con los archivos modificados
    /// </summary>
    public DirectoryInfo TerminalChangeableDirectory { get; }
    /// <summary>
    /// Ruta a la carpeta MQL5
    /// </summary>
    public DirectoryInfo MQL5Directory => (Portable ? TerminalInstallationDirectory : TerminalChangeableDirectory).GetDirectory("MQL5");
    #endregion

    /// <summary>
    /// ID del terminal - nombre de la carpeta en el directorio AppData
    /// </summary>
    public string TerminalID { get; }
    /// <summary>
    /// Señal que indica si el terminal está iniciado en este momento o no
    /// </summary>
    public bool IsActive => Process.StartInfo.FileName != "" && !Process.HasExited;

    #region .ex5 files relative paths
    /// <summary>
    /// Lista de nombres completos de los expertos
    /// </summary>
    public List<string> Experts => GetEX5FilesR(MQL5Directory.GetDirectory("Experts"));
    /// <summary>
    /// Lista de nombres completos de los indicadores
    /// </summary>
    public List<string> Indicators => GetEX5FilesR(MQL5Directory.GetDirectory("Indicators"));
    /// <summary>
    /// Lista de nombres completos de los scripts
    /// </summary>
    public List<string> Scripts => GetEX5FilesR(MQL5Directory.GetDirectory("Scripts"));
    #endregion

    /// <summary>
    /// Inicio del terminal
    /// </summary>
    public bool Run()
    {
        if (IsActive)
            return false;
        // Indicamos la ruta hacia el terminal
        Process.StartInfo.FileName = Path.Combine(TerminalInstallationDirectory.FullName, "terminal64.exe");
        Process.StartInfo.WindowStyle = WindowStyle;
        // Indicamos los datos para el inicio del terminal (si estos han sido establecidos)
        if (Config != null && File.Exists(Config.Path))
            Process.StartInfo.Arguments = $"/config:{Config.Path} ";
        if (Login.HasValue)
            Process.StartInfo.Arguments += $"/login:{Login.Value} ";
        if (Profile != null)
            Process.StartInfo.Arguments += $"/profile:{Profile} ";
        if (Portable)
            Process.StartInfo.Arguments += "/portable";

        // Notificamos el proceso sobre la necesidad de llamar al evento Exit después de cerrar el terminal
        Process.EnableRaisingEvents = true;

        //Iniciamos el proceso y guardamos el estado del inicio en la variable IsActive
        return Process.Start();
    }
    /// <summary>
    /// Esperando a que finalice el funcionamiento del terminal
    /// </summary>
    public void WaitForStop()
    {
        if (IsActive)
            Process.WaitForExit();
    }
    /// <summary>
    /// Deteniendo el proceso
    /// </summary>
    public void Close()
    {
        if (IsActive)
            Process.Kill();
    }
    /// <summary>
    /// Esperando a que finalice el funcionamiento del terminal un tiempo determinado
    /// </summary>
    public bool WaitForStop(int miliseconds)
    {
        if (IsActive)
            return Process.WaitForExit(miliseconds);
        return true;
    }
    /// <summary>
    /// Buscando archivos con la extensión Ex5 
    /// La búsqueda se realiza de forma recursiva, es decir,  los archivos se buscan en la carpeta indicada y en todas las carpetas incorporadas
    /// </summary>
    /// <param name="path">Ruta a la carpeta desde la que comienza la búsqueda</param>
    /// <param name="RelativeDirectory">Indicando la carpeta respecto a la cual se retorna la ruta</param>
    /// <returns>Lista de rutas a los archivos encontrados</returns>
    private List<string> GetEX5FilesR(DirectoryInfo path, string RelativeDirectory = null)
    {
        if (RelativeDirectory == null)
            RelativeDirectory = path.Name;
        string GetRelevantPath(string pathToFile)
        {
            string[] path_parts = pathToFile.Split('\\');
            int i = path_parts.ToList().IndexOf(RelativeDirectory) + 1;
            string ans = path_parts[i];

            for (i++; i < path_parts.Length; i++)
            {
                ans = Path.Combine(ans, path_parts[i]);
            }

            return ans;
        }
    
        List<string> files = new List<string>();
        IEnumerable<DirectoryInfo> directories = path.GetDirectories();

        files.AddRange(path.GetFiles("*.ex5").Select(x => GetRelevantPath(x.FullName)));

        foreach (var item in directories)
            files.AddRange(GetEX5FilesR(item, RelativeDirectory));

        return files;
    }
    /// <summary>
    /// Evento de cierre del terminal
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Process_Exited(object sender, EventArgs e)
    {
       TerminalClosed?.Invoke(this);
    }
    /// <summary>
    /// Comprobando si la ruta transmitida al terminal es correcta
    /// </summary>
    private void CheckDirectories()
    {
        if (!TerminalInstallationDirectory.Exists)
            throw new ArgumentException("PathToTerminalInstallationDirectory doesn`t exists");
        if (!TerminalChangeableDirectory.Exists)
            throw new ArgumentException("PathToTerminalChangeableDirectory doesn`t exists");
        if (!TerminalInstallationDirectory.GetFiles().Any(x => x.Name == "terminal64.exe"))
            throw new ArgumentException($"Can`t find terminal (terminal64.exe) in the instalation folder {TerminalInstallationDirectory.FullName}");
    }
}

Esta clase ya no implementa la interfaz ITeminalManager, como sucedió la vez anterior. El tema es que, renunciando a usar tests Unit a la hora de implementar la aplicaición descrita para acelerar el proceso de desarrollo y minimizar el número de proyectos, ya tampoco resulta necesario usar interfaces para este objeto.

El siguiente cambio es otro método para determinar la señal de inicio del terminal. En la anterior variante, esta propiedad obtenía su valor de los métodos Run (donde se asignaba el valor false) y la llamada de retorno de finalización de la optimización, no obstante, no se trataba de una solución muy bonita, y a veces podía no activarse. Por eso, hemos reescrito el getter de la propiedad IsActive como una apelación directa a la propiedad HasExited del objeto Process. Sin embargo, si recurrimos antes del primer inicio a la propiedad buscada, obtendremos un mensaje de error. Tras analizar las peculiaridades del funcionamiento de la clase Process, hemos notado que al iniciar el proceso a través del objeto descrito, su propiedad StartInfo.FileName se rellena bien hasta el archivo ejecutable, convirtiéndose antes del comienzo en un valor vacío (""). Precisamente por eso, el getter IsActive tiene un aspecto tan extraño. Primero comprueba la presencia del nombre antes del objeto iniciado, y ya después la propiedad Process.HasExited. Dicho de otra forma: suponemos por defecto que el terminal está cerrado y solo se puede iniciar a través de nuestra clase TerminalManager, por consiguiente, si StartInfo.FileName == "", retornaremos false (decimos que el terminal no está iniciado). A continuación, si el terminal ha sido iniciado aunque sea una sola vez, simplemnete comprobaremos el valor de la propiedad HasExited. Esta propiedad cambia su valor cada vez que se inicia el terminal, si el inicio se ha realizado desde el objeto descrito, y cuando el funcionamiento de este haya finalizado. Debido precisamente a esta peculiaridad, deberemos mantener el terminal cerrado al usar el optimizador automático. 

Esta descripción finaliza con el último objeto sometido a cambios en su estructura interna. Se trata de la clase SetFileManager y su método UpdateParams.

/// <summary>
/// Limpiando todos los datos anotados en Params y cargando los datos desde el archivo necesario
/// </summary>
public virtual void UpdateParams()
{
    _params.Clear();

    using (var file = FileInfo.OpenText())
    {
        string line;
        while ((line = file.ReadLine()) != null)
        {
            if (line[0].CompareTo(';') != 0 && line[0].CompareTo('#') != 0)
            {
                string[] key_value = line.Replace(" ", "").Split('=');
                string[] value_data = key_value[1].Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);

                ParamsItem item = new ParamsItem
                {
                    Variable = key_value[0],
                    Value = (value_data.Length > 0 ? value_data[0] : null),
                    Start = (value_data.Length > 1 ? value_data[1] : null),
                    Step = (value_data.Length > 2 ? value_data[2] : null),
                    Stop = (value_data.Length > 3 ? value_data[3] : null),
                    IsOptimize = (value_data.Length > 4 ? value_data[4].CompareTo("Y") == 0 : false)
                };

                _params.Add(item);
            }
        }
    }
}

En esta clase, los cambios se relacionan solo con un método, por eso no hay necesidad de mostrar el código completo de la clase. El tema es que, como notamos durante la simulación de la aplicación, el terminal a veces puede generar un archivo (*.set) con los parámetros del robot para el optimizador que resulte a medias vacío para alguno de los parámetros. Por ejemplo, que rellene el campo Value, pero no rellene el campo del valor inicial para la optimización, o bien su valor final. Esto depende del tipo de parámetro, por ejemplo, los parámetros de tipo string rellenan solo el campo Value. Precisamente para evitar este problema, se han añadido los cambios que podemos observar en el fragmento de código presentado.


Analizamos la estructura del directorio Data

Ya hemos mencionado en otros artículos el directorio local "Data", donde se guardan los informes de optimización y otros archivos de trabajo creados por el optimizador. Ahora, ha llegado el momento de conocerlo con más detalle. Al iniciar el terminal, como mostraremos en el próximo artículo, se crea el directorio "Data" junto con el archivo ejecutable. Dicho directorio se crea solo en el caso de que este no exista en el momento de iniciarse el optimizador automático; en caso contrario, solo guardaremos la ruta a él en la propiedad correspondiente de la clase que mostraremos más tarde. Este directorio sirve de carpeta de trabajo y repositorio al mismo tiempo. Si en la posterior implementación de sus propios algoritmos de optimización, el lector necesita operar con archivos y guardar estos en algún lugar, le recomendamos que lo haga en el directorio descrito. Este se crea y gestiona con el objeto siguiente:

/// <summary>
/// Objeto que describe el directorio Data controlado con los archivos modificados del optimizador automático.
/// </summary>
class WorkingDirectory
{
    /// <summary>
    /// Constructor por defecto
    /// </summary>
    public WorkingDirectory()
    {
        // Creando el directorio raíz con los archivos modificados
        WDRoot = new DirectoryInfo("Data");
        if (!WDRoot.Exists)
            WDRoot.Create();
        // Creando un directorio incorporado con los informes de optimización
        Reports = WDRoot.GetDirectory("Reports", true);
    }
    /// <summary>
    /// Directorio incorporado con los informes de optimización
    /// </summary>
    public DirectoryInfo Reports { get; }
    /// <summary>
    /// Directorio raíz con los archivos y carpetas modificados
    /// </summary>
    public DirectoryInfo WDRoot { get; }

    /// <summary>
    /// Obteniendo o creando (si no se ha creado antes) el directorio incorporado en el directorio Reports.
    /// El directorio creado guarda los resultados de una pasada de optimización concreta.
    /// </summary>
    /// <param name="Symbol">Símbolo en el que se ha realizado la optimización</param>
    /// <param name="ExpertName">Nombre del robot</param>
    /// <param name="DirectoryPrefix">Prefijo añadido al nombre del directorio</param>
    /// <param name="OptimiserName">Denominación del optimizador utilizado</param>
    /// <returns>
    /// Ruta al directorio  con los resultados de optimización.
    /// El nombre del directorio se construye de la forma siguiente:public DirectoryInfo WDRoot { get; }
    /// {DirectoryPrefix} {OptimiserName} {ExpertName} {Symbol}
    /// </returns>
    public DirectoryInfo GetOptimisationDirectory(string Symbol, string ExpertName,
                                                  string DirectoryPrefix, string OptimiserName)
    {
        return Reports.GetDirectory($"{DirectoryPrefix} {OptimiserName} {ExpertName} {Symbol}", true);
    }

    /// <summary>
    /// Ruta al directorio Data/Tester 
    /// Es necesaria para desplazar temporalmente los archivos desde el directorio homónimo del terminal
    /// </summary>
    public DirectoryInfo Tester => WDRoot.GetDirectory("Tester", true);

}

Esta clase sirve como controlador del directorio descrito. Resulta cómodo porque, sea donde sea que se encuentre el archivo ejecutable de nuestro optimizador automático, siempre podremos obtener la ruta correcta al directorio buscado recurriendo a la propiedad WDRoot del objeto presentado. En el caso de que no exista, creamos el directorio "Data" en el constructor, de lo contrario, simplemente guardamos su dirección en la propiedad mencionada. De la misma forma, guardamos la ruta al directorio "Reports"incorporado. El parámetro true transmitido indica que deberemos crear el directorio incorporado solicitado, en el caso de que este no exista. 


Finalmente, justo después del primer inicio, se crea el directorio Data con el único directorio incorporado (aún vacío) "Reports". Al realizar el primer inicio de la optimización o realizar una simulación, se creará el directorio incorporado Tester, recurriendo a la propiedad correspondiente del objeto descrito, mientras que el archivo de configuración llamado {Terminal ID}.ini se creará copiando el archivo de configuración del terminal que el lector haya seleccionado por defecto. Esto se hace así para no reescribir el archivo de configuración original. El directorio Tester se crea para copiar temporalmente la caché de las optimizaciones anteriormente realizadas, y repite parcialmente el directorio Tester correspondiente, que se encuentra entre los directorios modificados del terminal.

Dentro contiene solo la carpeta "cache", donde, antes de comenzar el proceso de optimización, se reubican todos los archivos del directorio correspondiente del terminal seleccionado. Una vez finalizado el proceso de optimización, estos archivos se devuelven a su antiguo lugar. Esta operación se realiza para que el proceso de optimización tenga lugar con garantías. De acuerdo con la lógica del optimizador, si en este directorio del terminal existen archivos que describan un proceso de optimización iniciado, en lugar de iniciar un nuevo proceso, se cargará la optimización anteriormente reproducida. Se trata de una solución fiable y magnífica que ahorra mucho tiempo, sin embargo, resulta inadecuada para nuestro objetivo. Dado que guardamos una copia propia del informe de optimización adaptada para nuestro optimizador automático (artículos №3 y №1 del presente ciclo de artículos), deberemos generar el informe buscado, y para ello necesitaremos precisamente el inicio del proceso de optimización. Por eso, vamos a emitir la ausencia de estos archivos, reubicándolos temporalmente en nuestro directorio local. Si el proceso de optimización finaliza con éxito, crearemos un directorio incorporado en el directorio Reports con la ayuda del método GetOptimisationDirectory.

 

En la captura de pantalla, destacamos a color el prefijo del directorio establecido en los ajustes del optimizador automático antes del inicio de la optimización. Es necesario para que podamos diferenciar dos o más optimizaciones distintas de un mismo experto. Dentro de cada directorio se guardan 3 archivos, en los que precisamente se almacenan los resultados de las optimizaciones realizadas:

La estructura de cada uno de estos archivos tiene el mismo tipo, y ya ha sido descrita anteriormente en el primer artículo del presente ciclo. A continuación, al pulsar el botón Load de la interfaz gráfica, el optimizador automático carga los 3 archivos del directorio seleccionado con las optimizaciones en el recuadro correspondiente. Si no se ha encontrado uno de los tres archivos, o bien ninguno está presente, se emitirá el mensaje con la información correspondiente. Los recuadros que se corresponden con los archivos ausentes se representan vacíos. 

Si el lector necesita trasladar los resultados de la optimización desde el programa del optimizador automático de una computadora al programa del optimizador automático de otra, solo tendrá que compilar el directorio Reports y recolocarlo en el lugar correspondiente en su segunda computadora. Después de la inicialización, el propio optimizador automático captará los directorios buscados con los resultados, y estos estarán disponibles para su carga y posterior análisis.

Conclusión

En los primeros artículos del presente ciclo, analizamos la creación y la descarga de los informes de optimización. Tras pasar al propio proyecto del optimizador automático, hemos podido analizar en primer lugar el proyecto finalizado en el artículo anterior, dedicado tanto a familiarizar a los lectores con el objetivo final del presente trabajo, como a proporcionar unas instrucciones de uso del optimizador automático preparado. En el este artículo, abordaremos los aspectos técnicos de su implementación. Antes de estudiar la propia parte teórica del proyecto, hemos analizado la interfaz gráfica y los cambios en los archivos que hemos tomado prestados del anterior ciclo de artículos. Podrá encontrar los enlaces al anterior ciclo de artículos en los anexos a este. Avanzando de lo simple hacia lo complejo, vamos aproximándonos al análisis de la implementación de la parte lógica del funcionamiento del programa descrito, a la que precisamente se dedicará el próximo artículo.

En los anexos se encuentra el nuevo proyecto del optimizador automático, con el robot de prueba analizado en el artículo №4. Lo único que el lector deberá hacer para usarlo es compilar los archivos del proyecto del optimizador automático y el robot de prueba. A continuación, deberá compilar ReportManager.dll (la implementación descrita en el primer artículo) en el directorio MQL5/Libraries, con lo que ya podrá proceder a probar la combinación obtenida. En los artículos 3 y 4 de esta serie, ya hemos hablado sobre cómo incluir la optimización automática de sus expertos.