Gestión de la optimización (Parte 2): Creando los objetos clave y la lógica de la aplicación

16 septiembre 2019, 08:26
Andrey Azatskiy
0
273

Índice

Introducción

En el presente artículo, continuamos el proceso de la creación de una interfaz gráfica conveniente para gestionar las optimizaciones en varios terminales simultáneamente. En el artículo anterior, consideramos el método que nos permitía iniciar el terminal desde la consola, así como, analizamos la estructura del archivo de configuración. En este artículo, vamos a hablar de la creación del envoltorio para el terminal en el lenguaje C# que nos permitirá controlarlo como un proceso ajeno. La interfaz considerada anteriormente no tenía lógica y no era capaz de hacer nada, a menos que reaccionar a la pulsación de las teclas de introducción del texto de la tecla pulsada en la consola (de la que la invocamos). Aquí, le añadiremos la lógica que va a procesar los eventos de la interfaz gráfica y ejecutar la lógica integrada. Además, crearemos una serie de objetos que trabajan con los archivos, gracias a los cuales podremos implementar la parte lógica del funcionamiento del programa, trabajando con estos objetos, en vez de trabajar con los archivos directamente: eso permitirá realizar el trabajo de una manera más fácil y el código será más informativo. En realidad, en este artículo, la extensión descrita por fin obtendrá una forma que se muestra en el vídeo.



Gestor para controlar el terminal desde fuera (ITerminalManager y Config)

Antes, hemos analizado el proceso de la creación de la capa gráfica de nuestra extensión. En esta parte, vamos a considerar la manera para crear la parte lógica. Usando las ventajas de la POO, la parte lógica ha sido dividida en varias clases, cada una de las cuales será responsable de su propia área. Consideramos esta parte a partir de las clases que realizan determinadas acciones con los archivos y el terminal, luego, pasaremos a considerar la clase resultante ExtentionGUI_M donde se describe la lógica central. Durante su implementación, se usaban las clases con las cuales iniciamos nuestro análisis. 

Este artículo está dedicado al trabajo con los terminales que se compone de los siguientes componentes:

  1. Trabajo con los archivos de configuración
  2. Trabajo con el terminal como con un proceso ajeno

Vale la pena empezar con el análisis del trabajo con los archivos de configuración. Usando la instrucción acerca del trabajo con el terminal como con un proceso ajeno, podemos descubrir el componente exacto. Lo primero que hay que hacer es crear todas las enumeraciones usadas que vamos a aplicar en el archivo de configuración: se puede ver los valores numéricos de estas enumeraciones en el terminal (lo que yo hice), y su implementación se puede encontrar en el archivo Config.cs. La implementación de una manera conveniente de transferir la dirección del servidor es mucho más interesante, es que tiene que transferirse en un determinado formado, y además de la dirección, hay que indicar el puerto. Este problema fue resuelto mediante la creación de la clase que almacena la dirección del servidor recibida a través del constructor y verifica si es correcta antes de ser instanciada.

/// <summary>
/// dirección IPv4 del servidor y puerto
/// </summary>
class ServerAddressKeeper
{
    public ServerAddressKeeper(IPv4Adress ip, uint port)
    {
        IP = ip;
        Port = port;
    }
    public ServerAddressKeeper(string adress)
    {
        if (string.IsNullOrEmpty(adress) || string.IsNullOrWhiteSpace(adress))
            throw new ArgumentException("adress is incorrect");

        string[] data = adress.Split(':');

        if (data.Length != 2)
            throw new ArgumentException("adress is incorrect");

        IP = new IPv4Adress(data[0]);
        Port = Convert.ToUInt32(data[1]);
    }

    public IPv4Adress IP { get; }
    public uint Port { get; }

    public string Address => $"{IP.ToString()}:{Port}";
}

/// <summary>
/// dirección IPv4 del servidor
/// </summary>
struct IPv4Adress
{
    public IPv4Adress(string adress)
    {
        string[] ip = adress.Split('.');
        if (ip.Length != 4)
            throw new ArgumentException("ip is incorrect");

        part_1 = (char)Convert.ToInt32(ip[0]);
        part_2 = (char)Convert.ToInt32(ip[1]);
        part_3 = (char)Convert.ToInt32(ip[2]);
        part_4 = (char)Convert.ToInt32(ip[3]);
    }

    public char part_1;
    public char part_2;
    public char part_3;
    public char part_4;

    public new string ToString()
    {
        return $"{(int)part_1}.{(int)part_2}.{(int)part_3}.{(int)part_4}";
    }
}

Merece la pena empezar a analizar esta clase con la estructura IPv4Adress que almacena la dirección IP del servidor. Cuando recopilaba los datos antes de escribir este artículo, nunca encontré la dirección del servidor diferente del formato IPv4. Por eso, esta estructura implementa este formato de la dirección. En su constructor , ella recibe una cadena con la dirección, luego la analiza y la guarda en los campos correspondientes. Si el número de los dígitos en la dirección es inferior a 4, se genera un error. El constructor de la clase principal tiene dos sobrecargas, una de las cuales asume una representación de string de la dirección del servidor, la otra, la dirección IP formada y el número del puerto. La estructura IPv4Adress también tiene un método sobrecargado  ToString que ha sido heredado de la clase base Object, de la cual se heredan implícitamente todos los objetos de C#. La clase ServerAddressKeeper tiene la propiedad Address que realiza el mismo trabajo. Como resultado, hemos obtenido una clase-envoltorio que almacena la dirección del servidor en un formato conveniente y es capaz de montarla en un formato necesario para los archivos de configuración.  

Ahora, vale la pena de pensar en los recursos para trabajar con los propios archivos de configuración en formato (*.ini). Como ha sido mencionado anteriormente, ahora este formato de los archivos está obsoleto y prácticamente no se usa. C# no tiene interfaces internas para trabajar con ellas (por ejemplo, para trabajar con el marcado XML que ya hemos conocido y que vamos a usar más detalladamente en los siguientes capítulos de este artículo). No obstante, WinApi por ahora soporta las funciones   WritePrivateProfileString y GetPrivateProfileString para trabajar con los archivos de este formato. Además, como Microsoft escribe:

Note:  This function is provided only for compatibility with 16-bit Windows-based applications. Applications should store initialization information in the registry.

Estos métodos se almacenan en WinApi sólo para la compatibilidad versiones anteriores de las aplicaciones de 16 bits para Windows. Las aplicaciones tienen que almacenar la información de la inicialización en el registro. No obstante, podemos usarlas para no inventar la propia bicicleta. Para eso, tenemos que importar los datos de las funciones C en nuestro código C# (en realidad, es una combinación de dos lenguajes de programación). En C#, eso se realiza aproximadamente como en MQL5:

[DllImport("kernel32.dll", SetLastError = true)]
private extern static int GetPrivateProfileString(string AppName, string KeyName, string Default, StringBuilder ReturnedString, int Size, string FileName);

[DllImport("kernel32.dll", SetLastError = true)]
private extern static int WritePrivateProfileString(string AppName, string KeyName, string Str, string FileName);

Pero a diferencia del comando #import, tenemos que especificar el atributo DLLImport, pasándole el atributo de dll de la que importamos la función y otros parámetros no obligatorios. En particular, al hacer esta importación, he especificado el parámetro   SetLastErro =true, lo que nos da la posibilidad de obtener los errores del código de C++ a través de GetLastError() en nuestro código C#, y por tanto procesar la ejecución correcta de estos métodos. Como la manera de trabajar con las cadenas (string) en C# y en C es diferente, usaremos los métodos-envoltorios que permiten trabajar con funciones importadas de manera conveniente y procesan posibles errores. Los he implementado de la siguiente manera:  

/// <summary>
/// Envoltorio conveniente para la función WinAPI GetPrivateProfileString
/// </summary>
/// <param name=»section»>nombre de la sección</param>
/// <param name=»key»>clave</param>
/// <returns>parámetro solicitado o null si la clave no ha sido encontrada</returns>
protected virtual string GetParam(string section, string key)
{
    //Para obtener el valor
    StringBuilder buffer = new StringBuilder(SIZE);
 
   //Obtener valor en buffer
    if (GetPrivateProfileString(section, key, null, buffer, SIZE, Path) == 0)
        ThrowCErrorMeneger("GetPrivateProfileStrin", Marshal.GetLastWin32Error());

    //Devolver el valor obtenido
    return buffer.Length == 0 ? null : buffer.ToString();
}

/// <summary>
/// Envoltorio conveniente para WinAPI WritePrivateProfileString
/// </summary>
/// <param name=»section»>Sección</param>
/// <param name=»key»>Clave</param>
/// <param name=»value»>Valor</param>
protected virtual void WriteParam(string section, string key, string value)
{
    //Registrar el valor en el archivo INI
    if (WritePrivateProfileString(section, key, value, Path) == 0)
        ThrowCErrorMeneger("WritePrivateProfileString", Marshal.GetLastWin32Error());
}

/// <summary>
/// Mostrar error
/// </summary>
/// <param name=»methodName»>Nombre del método</param>
/// <param name=»er»>Código del error</param>
private void ThrowCErrorMeneger(string methodName, int er)
{
    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");
        }
    }
}

Al trabajar con estos métodos, se descubrió una interesante particularidad. Después de buscar la información, verifiqué que era no sólo mi problema. La particularidad de trabajar con estas funciones consiste en el hecho de que el método GetPrivateProfileString devuelve el error  ERROR_FILE_NOT_FOUND (código del error = 2) no sólo cuando el archivo no ha sido encontrado, sino también en las siguientes condiciones:

  1. La sección no existe en el archivo legible
  2. La clave solicitada no existe

Precisamente debido a esta particularidad, en el método ThrowCErrorMeneger , comprobamos la existencia de un archivo legible si este error ha surgido. Para obtener el último error (método GetLastError), en C# existe un método estático de la clase Marshal ( Marshal.GetLastWin32Error()) que vamos a usar para extraer el error después de cada llamada a los métodos de lectura o escritura en el archivo. Por motivos de conveniencia, importábamos los métodos que leen o escriben sólo las cadenas, ya que cualquier tipo de datos puede ser convertido en una cadena de caracteres. 

El siguiente aspecto interesante del trabajo con estas funciones es la manera eliminar los datos del archivo. Por ejemplo, para eliminar la sección entera, hay que pasar el nombre de la clave igual a null al método   WriteParam.  Usando esta posibilidad, creé el método correspondiente, sacando previamente todos los nombres de las secciones en enum ENUM_SectionType:

/// <summary>
/// Eliminación de la sección
/// </summary>
/// <param name=»section»>sección seleccionada para la eliminación</param>
public void DeleteSection(ENUM_SectionType section)
{
    WriteParam(section.ToString(), null, null);
}

Además, existe una manera de eliminar una determinada clave, para eso, es necesario especificar el nombre de esta clave, pero su valor tiene que ser null. En la implementación de este método, he dejado el nombre de la clave transmitida como un campo string, porque las claves de cada sección en su mayoría son únicas.

/// <summary>
/// Eliminando la clave
/// </summary>
/// <param name=»section»>sección de la que va a eliminarse la clave</param>
/// <param name=»key»>Clave eliminada</param>
public void DeleteKey(ENUM_SectionType section, string key)
{
    if (string.IsNullOrEmpty(key) || string.IsNullOrWhiteSpace(key))
        throw new ArgumentException("Key is not vailed");

    WriteParam(section.ToString(), key, null);
}

Para facilitar el acceso a las secciones, he decidido implimentarlas por medio de las propiedades, para que se pueda acceder a cualquier sección en la instancia de la clase Config a través del operador punto (.), y luego, a cualquier clave de esta sección, conforme se muestra en el ejemplo de abajo:

Config myConfig = new Config("Path");

myConfig.Tester.Expert = MyExpert;
string MyExpert = myConfig.Tester.Expert; 

Obviamente, para implementar esta empresa, necesitamos crear una clase para cada una de las secciones, y en la clase de cada sección especificada, escribir las propiedades que escriben y leen esta cadena especificada en el archivo. Puesto que las secciones representan esencialmente un componente de este archivo de inicialización, y la clase Config en realidad es una representación de este archivo orientada a objetos, sería lógico hacer las clases que describen estas secciones como clases anidadas en la clase Config, y luego en la clase Config, crear las propiedades disponibles sólo para la lectura, tipificadas por las clases concretas. En el ejemplo de abajo, recorté todo los sobrante del código, dejando sólo la parte que ilustra la idea descrita en esta parte del texto:  

class Config
{
    public Config(string path)
    {
        Path = path;
        CreateFileIfNotExists();

        Common = new CommonSection(this);
        Charts = new ChartsSection(this);
        Experts = new ExpertsSection(this);
        Objects = new ObjectsSection(this);
        Email = new EmailSection(this);
        StartUp = new StartUpSection(this);
        Tester = new TesterSection(this);
    }

    protected virtual void CreateFileIfNotExists()
    {
        if (!File.Exists(Path))
        {
            File.Create(Path).Close();
        }
    }

    public readonly string Path; // ruta hacia el archivo

    public virtual Config DublicateFile(string path)
    {
        File.Copy(Path, path, true);
        return new Config(path);
    }

    #region Section managers
    internal class CommonSection
    {
    }
    internal class ChartsSection
    {
    }
    internal class ExpertsSection
    {
    }
    internal class ObjectsSection
    {
    }
    internal class EmailSection
    {
    }
    internal class StartUpSection
    {
    }
    internal class TesterSection
    {
    }
    #endregion

    public CommonSection Common { get; }
    public ChartsSection Charts { get; }
    public ExpertsSection Experts { get; }
    public ObjectsSection Objects { get; }
    public EmailSection Email { get; }
    public StartUpSection StartUp { get; }
    public TesterSection Tester { get; }
}

La implementación de cada una de las clases anidadas, que describen una sección, es del mismo tipo. Vamos a considerarla con el ejemplo de la clase Config.ChartsSection.

internal class ChartsSection
{
    private readonly Converter converter;
    public ChartsSection(Config parent)
    {
        converter = new Converter(parent, "Charts");
    }

    public string ProfileLast
    {
        get => converter.String("ProfileLast");
        set => converter.String("ProfileLast", value);
    }
    public int? MaxBars
    {
        get => converter.Int("MaxBars");
        set => converter.Int("MaxBars", value);
    }
    public bool? PrintColor
    {
         get => converter.Bool("PrintColor");
         set => converter.Bool("PrintColor", value);
    }
    public bool? SaveDeleted
    {
         get => converter.Bool("SaveDeleted");
         set => converter.Bool("SaveDeleted", value);
    }
 }

Como podemos ver, la clase que describe la sección incluye la sección Nullable, que utiliza otra clase intermediaria para la lectura y la escritura en el archivo. Vamos a considerar la implementación de esta clase más tarde. Pero ahora, vamos a centrarnos en los datos devueltos, para ser más exactos, en el hecho de que si esta clave no ha sido guardada en el archivo, nuestra clase-envorltorio va a devolver null, en vez del valor de la clase. Si pasamos el valor null a la propiedad de alguna clave, adelantándose se puede decir que este valor simplemente será ignorado. Si queremos eliminar un campo, hay que usar el método DeleteKey discutido más arriba.

Ahora, vamos a considerar la propia clase Converter, que escribe y lee los datos del archivo. pues ella también es una clase anidada, y por eso, puede usar los métodos WriteParam y GetParam de la clase principal, a pesar de que ellos estén marcados con el modificador de acceso protected. Esta clase tiene las sobrecargas de los métodos de lectura y escritura para los siguientes tipos:

  • Bool
  • Int
  • Double
  • String
  • DateTime

Todos los demás tipos se convierten a uno de estos tipos más apropiado en las clases que describen las secciones. La implementación de esta clase tiene el siguiente formato.

private class Converter
{
    private readonly Config parent;
    private readonly string section;
    public Converter(Config parent, string section)
    {
        this.parent = parent;
        this.section = section;
    }

    public bool? Bool(string key)
    {
        string s = parent.GetParam(section, key);
        if (s == null)
            return null;

        int n = Convert.ToInt32(s);
        if (n < 0 || n > 1)
            throw new ArgumentException("string mast be 0 or 1");
        return n == 1;
    }
    public void Bool(string key, bool? val)
    {
        if (val.HasValue)
            parent.WriteParam(section, key, val.Value ? "1" : "0");
    }

    public int? Int(string key)
    {
        string s = parent.GetParam(section, key);
        return s == null ? null : (int?)Convert.ToInt32(s);
    }
    public void Int(string key, int? val)
    {
        if (val.HasValue)
            parent.WriteParam(section, key, val.Value.ToString());
    }

    public double? Double(string key)
    {
        string s = parent.GetParam(section, key);
        return s == null ? null : (double?)Convert.ToDouble(s);
    }
    public void Double(string key, double? val)
    {
        if (val.HasValue)
            parent.WriteParam(section, key, val.Value.ToString());
    }

    public string String(string key) => parent.GetParam(section, key);
    public void String(string key, string value)
    {
        if (value != null)
            parent.WriteParam(section, key, value);
    }

    public DateTime? DT(string key)
    {
        string s = parent.GetParam(section, key);
        return s == null ? null : (DateTime?)DateTime.ParseExact(s, "yyyy.MM.dd", null);
    }
    public void DT(string key, DateTime? val)
    {
        if (val.HasValue)
            parent.WriteParam(section, key, val.Value.ToString("yyyy.MM.dd"));
    }
}

Esta clase convierte los valores transferidos en el formato esperado en el archivo y los escribe en él. Al leer el archivo, también convierte las cadenas en el formato devuelto de datos y pasa el resultado a la clase, que describe una determinada sección, que a su vez convierte el valor en el formato que deseamos. Nótese que, con cada llamada a alguna de las propiedades, esta clase escribe o lee los datos directamente en el archivo. Este comportamiento siempre ofrece una información más reciente cuando se trabaja con el archivo, pero puede ser más duradero que cuando se trabaja con la memoria. Pero si tomamos en cuenta que para la escritura y la lectura se gastan microsegundos, eso no se nota en absoluta durante el trabajo del programa.  

En la siguiente parte, vamos a considerar el gestor para trabajar con el terminal. La tarea de esta clase es el inicio y la parada del terminal, la posibilidad de obtener los datos sobre si está en ejecución o no, así como, la colocación de las banderas del inicio y del archivo de configuración. En otras palabras, esta clase debe entender todos los modos del inicio del terminal descritos en la instrucción y ofrecer la posibilidad de gestionar el proceso del trabajo del terminal. De acuerdo con todos estos requerimientos, primero, fue escrita la siguiente interfaz que describía las signaturas de las propiedades y métodos deseados. Luego, en el programa, el trabajo con el terminal va a realizarse a través de la interfaz de abajo.

interface ITerminalManager
{
    uint? Login { get; set; }
    string Profile { get; set; }
    Config Config { get; set; }
    bool Portable { get; set; }
    System.Diagnostics.ProcessWindowStyle WindowStyle { get; set; }
    DirectoryInfo TerminalInstallationDirectory { get; }
    DirectoryInfo TerminalChangeableDirectory { get; }
    DirectoryInfo MQL5Directory { get; }
    List<string> Experts { get; }
    List<string> Indicators { get; }
    List<string> Scripts { get; }
    string TerminalID { get; }
    bool IsActive { get; }

    bool Run();
    void Close();
    void WaitForStop();
    bool WaitForStop(int miliseconds);

    event Action<ITerminalManager> TerminalClosed;
}

Como se puede ver en la interfaz presentada, las primeras cuatro propiedades reciben los valores de las banderas presentadas en la instrucción y discutidas antes en la parte donde ha sido descrito el proceso de la creación de la interfaz gráfica. La quinta bandera define el tamaño de la ventana del terminal cuando se inicia. Ella puede minimizar el terminal en el área de notificación (tray), ejecutar la ventana en el modo de Pantalla completa o minimizada. Sin embargo, al definir su valor en el parámetro Hidden (que debe ocultar la ventana), el comportamiento esperado no se ejecuta. Para ocultar la ventana del terminal, hay que editar otro archivo de inicialización, pero como este comportamiento no es crítico no he querido complicar el código aún más y programar el trabajo con otro archivo de inicialización.

La propia clase que hereda esta interfaz, tiene dos sobrecargas del constructor que se muestran a continuación.

public TerminalManager(DirectoryInfo TerminalChangeableDirectory) :
    this(TerminalChangeableDirectory, new DirectoryInfo(File.ReadAllText(TerminalChangeableDirectory.GetFiles().First(x => x.Name == "origin.txt").FullName)), false)
{
}

public TerminalManager(DirectoryInfo TerminalChangeableDirectory, DirectoryInfo TerminalInstallationDirectory, bool isPortable)
{
    this.TerminalInstallationDirectory = TerminalInstallationDirectory;
    this.TerminalChangeableDirectory = TerminalChangeableDirectory;

    TerminalID = TerminalChangeableDirectory.Name;

    CheckDirectories();

    Process.Exited += Process_Exited;

    Portable = isPortable;
}

Conforme lo descrito en el artículo de Vladimir Karputov, el directorio variable del terminal contiene el archivo origin.txt que almacena la ruta hacia el directorio de instalación que usamos en la primera sobrecarga del constructor. Esta sobrecarga  busca el archivo origin.txt, lo lee por completo y crea la clase DirectiryInfo que describe este directorio, pasándole la información leída desde el archivo en su constructor. También es importante mencionar que, en realidad, toda la preparación de la clase se realiza por el segundo constructor, que recibe 3 parámetros:

  • Ruta hacia el directorio alterado (el que se encuentra en AppData).
  • Ruta hacia el directorio de instalación.
  • Bandera del inicio del terminal en modo Portable.  

El último parámetro de este constructor ha sido añadido para facilitar la configuración y su asignación debe realizarse exactamente al final del constructor intencionalmente. Lo que pasa es que cuando el terminal se inicia en modo Portable, su directorio MQL5 —donde se inician todos los EAs e indicadores— se crea (si no ha sido creado antes) en el directorio de instalación del terminal. Inicialmente, si el terminal nunca ha sido iniciado en modo Portable, este directorio estará ausente. Por eso, al colocar esta bandera, es necesario verificar la existencia de este directorio. La propiedad que define y lee esta bandera se describe de la siguiente manera.

/// <summary>
/// Bandera del inicio del terminal en 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(100);
                Close();
            }
	    WaitForStop();
            WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
        }
    }
} 

Como se puede observar en el setter presentado, cuando se asigna el valor transferido (si es true), se comprueba si el directorio MQL5 existe. Si este directorio no existe en el directorio de instalación, iniciamos el terminal y mantenemos el flujo hasta que el terminal no sea inicido. En el momento del inicio del terminal, como al principio hemos definido la bandera de la inicialización del terminal, la inicialización en sí va a realizarse en modo Portable, mientras que el directorio deseado será creado durante el primer inicio del terminal en modo Portable (que ejecutamos con esta inicialización). Una vez iniciado el terminal, lo cerramos con el comando Close desde nuestro envoltorio para el trabajo con el terminal y esperamos su cierre. Después de finalizar este procedimiento, y si no hay problemas con los derechos de acceso, el directorio MQL5 será creado. Nuestra propiedad que devuelve la ruta hacia el directorio MQL5 del terminal funciona a través de una construcción condicional, devolviendo la ruta hacia el directorio deseado desde el directorio de instalación, o desde el directorio con archivos alterados (dependiendo de la bandera descrita arriba).

/// <summary>
/// Ruta hacia la carpeta MQL5
/// </summary>
public DirectoryInfo MQL5Directory => (Portable ? TerminalInstallationDirectory : TerminalChangeableDirectory).GetDirectory("MQL5");

Además, hay que prestar atención en un detalle, respecto al uso de la segunda sobrecarga del constructor. Si Usted pasa, en vez del directorio editable, la ruta hacia el directorio de instalación, esta clase en principio debe funcionar correctamente si ocurre por lo menos una inicialización en modo Portable (o si ha sido definida la bandera isPortable = true). No obstante, ella va a ver sólo el directorio de instalación del terminal, y en este caso, TerminalID será igual al nombre de la carpeta en la que se encuentra el terminal (es decir, el nombre del directorio de instalación), en vez de ser un conjunto de números y símbolos latinos indicado en el directorio de alteración del terminal.  
Las propiedades que facilitan la información sobre los robots, indicadores y scripts que están en el terminal representan el siguiente detalle de la implementación de esta clase. La implementación de estas propiedades se realiza por el método private GetEX5FilesR.

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

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

Antes de examinar este método en detalle, es necesario hacer hincapié en el hecho de que en estas propiedades no intentamos obtener posibles rutas hacia los archivos de los EAs. En vez de eso, obtenemos las rutas relativas hacia los EAs respecto a la carpeta Experts, hacia los indicadores, respecto a la carpeta Indicators, hacia los scripts, respecto a la carpeta Scripts. Además, durante la selección, nuestra clase se guía sólo por la extensión del archivo (busca sólo los archivos EX5 en los directorios transferidos).

El método que devuelve la lista de los archivos EX5 encontrados utiliza la recursión. Vamos a analizarla más detalladamente. Primero,  verifica el valor del segundo parámetro que es opcional: si no está definido, se le asigna el nombre del directorio transferido actual. Es así como entendemos respecto al cuál directorio es necesario emitir las rutas de los archivos. Luego, hay otra construcción del lenguaje C#, es decir, funciones anidadas. Estas funciones existen solamente dentro del método actual. Hemos recurrido al uso de esta construcción porque no vamos a necesitar esta función en ningún otro lugar de la clase, y su cuerpo no es demasiado grande, lo que permite meterla dentro del método en cuestión. Esta función recibe la ruta hacia un archivo EX5 y la divide usando el símbolo "\\" como divisor. Como resultado, obtenemos un array de los nombres de los directorios, y al final de este array se encuentra el nombre del archivo EX5. El siguiente paso es asignar a la variable i el índice del directorio respecto al cual buscamos la ruta hacia el archivo, y aumentamos su valor en 1, moviendo así el puntera al siguiente directorio o inmediatamente al archivo. La variable ans va a almacenar la dirección encontrada. Para eso, le asignamos el valor del directorio especificado, luego, añadimos cíclicamente un directorio nuevo o archivo. Y así, hasta que no salgamos del ciclo (es decir, hasta añadir el nombre del archivo deseado). El método GetEX5FilesR funciona según el siguiente esquema:

  1. Obtenemos las rutas hacia todos los subdirectorios.
  2. Buscamos los archivos EX5 en el directorio actual y guardamos sus rutas relativas.
  3. En el ciclo, iniciamos una recursión para cada subdirectorio, pasando el nombre del directorio (respecto al necesitamos obtener la ruta hacia el EA), valor devuelto. Añadimos a la lista de las rutas relativas hacia los archivos EX5 
  4. Devolvemos las rutas encontradas de los archivos.

De esta manera, este método ejecuta la búsqueda completa de los archivos y devuelve todos los EAs encontrados y los demás archivos de ejecución escritos en el lenguaje MQL5.

Ahora, vamos a considerar el modo de iniciar las aplicaciones ajenas en C#. Este lenguaje tiene una funcionalidad muy conveniente para iniciar y para trabajar con otras aplicaciones, a saber, la clase Process que representa un envoltorio para cualquier proceso inicializado externo. Por ejemplo, para iniciar el bloc de notas desde C#, hay que escribir sólo 3 líneas del código:

System.Diagnostics.Process Process = new System.Diagnostics.Process();
Process.StartInfo.FileName = "Notepad.exe";
Process.Start();

Precisamente con la ayuda de esta clase, implementamos el proceso del control de los terminales ajenos desde de nuestra extensión. Es el método que inicia el terminal:

public bool Run()
{
    if (IsActive)
        return false;
    // Definimos la ruta hacia el terminal
    Process.StartInfo.FileName = Path.Combine(TerminalInstallationDirectory.FullName, "terminal64.exe");
    Process.StartInfo.WindowStyle = WindowStyle;
    // Definimos los datos para iniciar el terminal (si ellos han sido instalados)
    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";

    // Avisamos al proceso sobre la necesidad de llamar al evento Exit tras el cierre del terminal
    Process.EnableRaisingEvents = true;

    //Iniciamos el proceso y guardamos el estatus de la inicialización en la variable IsActive
    return (IsActive = Process.Start());
}

Cuando configuramos el terminal antes de iniciarlo, primero (como en el caso con el bloc de notas), hay que hacer lo siguiente:

  1. Especificar la ruta hacia el archivo de ejecución a inicar.
  2. Definir el tipo de la ventana del proceso a iniciar.
  3. Definir las claves (en el ejemplo con la aplicación de consola, eran todos los valores especificados separados con un espacio después del nombre del archivo a iniciar).
  4. Establecer la bandera Process.EnableRaisingEvents= true. Si eso no ha sido hecho, el evento de la conclusión del proceso al que hemos suscrito en el constructor, no va a activarse.
  5. Iniciar el proceso y guardar el estatus de la inicialización en la variable IsActive.

La propiedad IsActive vuelve a ser igual a false en el callback, que se acciona tras el cierre del terminal. En este callback, también llamamos a nuestro evento TerminalClosed.

/// <summary>
/// Evento del cierre del terminal
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Process_Exited(object sender, EventArgs e)
{
    IsActive = false;
    TerminalClosed?.Invoke(this);
}

Los demás métodos que controlan el terminal (espera de la parada y cierre del terminal) son un envoltorio de los métodos estándar de la clase Process.

public void WaitForStop()
{
    if (IsActive)
        Process.WaitForExit();
}
/// <summary>
/// Suspensión del proceso
/// </summary>
public void Close()
{
    if (IsActive && !Process.HasExited)
        Process.Kill();
}
/// <summary>
/// Esperar a que el terminal concluya su funcionamiento durante un cierto tiempo
/// </summary>
public bool WaitForStop(int miliseconds)
{
    if (IsActive)
        return Process.WaitForExit(miliseconds);
   return true;
}

De esta manera, usando la clase estándar Process, hemos creado un envoltorio conveniente que funciona especialmente con MetaTrader 5 y, posteriormente, en todo el programa, podremos trabajar con el terminal con más comodidad de que si asáramos la clase Process directamente.

Objetos que representan la estructura de los directorios

Puesto que ya hemos abordado el tema del trabajo con los directorios en la parte anterior de este artículo, vale la pena considerar las maneras de trabajar con el sistema de archivos, utilizadas en esta extensión. Lo primero que me gustaría mencionar es el método que se usa para crear las rutas hacia los archivos y directorios. Para eso, el lenguaje C# cuenta con la clase Path que es muy conveniente. Gracias a esta clase, obtenemos la posibilidad de crear las rutas hacia los archivos y directorios con seguridad, evitando posibles errores en este sentido. Para representar el directorio, se usa la clase  DirectoryInfo. Gracias a ella, podemos obtener rápidamente la información sobre los subdirectorios, el directorio padre, el nombre y la ruta completa hacia este directorio, entre muchas otras propiedades útiles. Por ejemplo, esta clase permite obtener todos los archivos ubicados en este directorio mediante la llamada a un solo método. Para la representación orientada a objetos de cualquiera de los archivos, se usa la clase  FileInfo, que por su funcionalidad es un análogo de la clase DirectoryInfo. Como resultado, todo el trabajo con los archivos y directorios, básicamente, se resume al trabajo con las clases mencionadas. Durante el proceso del desarrollo, eso permite centrarnos en la propia tarea, casi sin crear funciones y métodos intermedios.

Además, cabe mencionar que en la clase TerminalManager descrita antes, el método GetDirectory se usaba con frecuencia en la instancia de la clase DirectoryInfo. Este método no forma parte de la composición estándar de la clase DirectoryInfo, y ha sido añadido por conveniencia. En C# existe una manera de extender la funcionalidad de las clases estándar y personalizadas adicionando los métodos-extensiones a ellas. Vamos a usar esta funcionalidad del lenguaje C# para añadir el método de extensión GetDirectory. Esa es su implementación:

static class DirectoryInfoExtention
{
    public static DirectoryInfo GetDirectory(this DirectoryInfo directory, string Name, bool createIfNotExists = false)
    {
        DirectoryInfo ans = new DirectoryInfo(Path.Combine(directory.FullName, Name));
        if (!ans.Exists)
        {
            if (!createIfNotExists)
                return null;
            ans.Create();
        }
        return ans;
    }
}

Como se puede observar, para crear un método de la extensión (son reglas generales), es necesario crear una clase estática en la que se crea un método estático público, cuyo primer parámetro tiene que ser tipificado con el tipo para el que se crea la extensión, y antes de él debe ir la palabra clave this.  Este parámetro se indica automáticamente cuando se invoca el método de la extensión (no es necesario pasarlo a la función explícitamente, él es una instancia determinada de la clase para la cual ha sido escrita la extensión, en la que ha sido llamado el método de la extensión). No es necesario crear una instancia de la clase que almacena los métodos de la extensión, pues, todos los métodos de la extensión se incluyen automáticamente en el conjunto de los métodos de la clase para la cual han sido escritos. El método en cuestión opera de acuerdo con el siguiente algoritmo:

  1. Si el parámetro createIfNotExists=false (o no está especificado), devuelve una subcarpeta con el nombre transferido, que ha sido convertida al tipo DirectoryInfo (si ella existe), o null.
  2. Si el parámetro createIfNotExists=true, si la carpeta no está creada, será creada, y como respuesta, esta carpeta será devuelta como convertida al tipo DirectoryInfo. 

Además de eso, para facilitar el manejo de las carpetas de los directorios alterables de los terminales, ha sido creada una clase que es una representación orientada a objetos del directorio 

~\AppData\Roaming\MetaQuotes\Terminal

Esta clase está implementada de la siguiente manera:

class TerminalDirectory
{
    public TerminalDirectory() :
        this(Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), "MetaQuotes", "Terminal"))
    {
    }

    public TerminalDirectory(string path)
    {
        pathToTerminal = path;
    }

    private readonly string pathToTerminal;

    public List<DirectoryInfo> Terminals
    {
        get
        {
            List<DirectoryInfo> ans = new List<DirectoryInfo>();
            string[] dir_array = Directory.GetDirectories(pathToTerminal);
            foreach (var item in dir_array)
            {
                string pathToOrigin = Path.Combine(pathToTerminal, item, "origin.txt");
                if (!File.Exists(pathToOrigin))
                    continue;
                if (!File.Exists(Path.Combine(File.ReadAllText(pathToOrigin), "terminal64.exe")))
                    continue;
                ans.Add(new DirectoryInfo(Path.Combine(pathToTerminal, item)));
            }

            return ans;
        }
    }
    public DirectoryInfo Common => new DirectoryInfo(Path.Combine(pathToTerminal, "Common"));
    public DirectoryInfo Community => new DirectoryInfo(Path.Combine(pathToTerminal, "Community"));
}

Como podemos ver, contiene tres campos:

  1. Terminals
  2. Common
  3. Community

Estos campos corresponden a los nombres de los subdirectorios en el directorio en cuestión. La propiedad de mayor interés es la propiedad Terminals, que devuelve la lista de los directorios que pertenecen al sistema alterable de archivos de los terminales. Puesto que, después de eliminar un terminal, me enfrentaba más de una vez con el problema cuando su carpeta en este directorio permanecía intacta, decidí verificar la relevancia de estos directorios. Los criterios de verificación fueron:

  1. La presencia del archivo "origin.txt" en la raíz del directorio en cuestión, pues, gracias a este archivo, obtenemos la ruta hacia el directorio con el terminal.
  2. La presencia del archivo de ejecución del terminal en el directorio correspondiente. 

También es importante mencionar que esta extensión está diseñado para trabajar con la versión de 64 bits del terminal. Para trabajar con la versión de 32 bits, en todo el programa es necesario (en la clase TerminalManager y en la clase en cuestión) renombrar  "terminal64.exe" por "terminal.exe". Así, se ignoran los directorios para los cuales no se puede encontrar los archivos de ejecución del terminal.

La siguiente particularidad de esta clase que merece la pena considerar es el primer constructor. Dicho constructor permite automáticamente formar la ruta hacia el directorio de los archivos alterables de los terminales de la siguiente manera:

System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData)

Como puede ver, la clase Enviroment permite obtener la ruta hacia el directorio "AppData" en este ordenador automáticamente. Por ese motivo, no necesitamos indicar el nombre del usuario, y precisamente gracias a esta línea en nuestros ordenadores, la extensión en cuestión podrá encontrar la lista de todos los terminales instalados de modo estándar.

Además de la clase que describe la carpeta con los directorios alterables de los terminales, nuestra extensión tiene su propio directorio donde almacena los archivos temporales y los informes de la optimización. La clase que describe este directorio se presenta de la siguiente manera.

class OptimisationExtentionWorkingDirectory
{
    public OptimisationExtentionWorkingDirectory(string DirectoryName)
    {
        DirectoryRoot = CreateIfNotExists(DirectoryName);
        Configs = CreateIfNotExists(Path.Combine(DirectoryName, "Configs"));
        Reports = CreateIfNotExists(Path.Combine(DirectoryName, "Reports"));
    }

    public DirectoryInfo DirectoryRoot { get; }

    public DirectoryInfo Configs { get; }

    public DirectoryInfo Reports { get; }

    protected DirectoryInfo CreateIfNotExists(string path)
    {
        DirectoryInfo ans = new DirectoryInfo(path);
        if (!ans.Exists)
            ans.Create();
        return ans;
    }
}

Como se ve en el constructor de esta clase, durante su creación, comprobamos la existencia del directorio raíz y de los directorios anidados. Si no existen, serán creados.

  • "DirectoryRoot" es el directorio principal, y nuestra extensión almacena sus propios archivos y directorios dentro del mismo. 
  • "Configs" es el directorio al que vamos a copiar los archivos de configuración, modificarlos y después, definirlos como parámetros de entrada al iniciar el terminal.
  • "Reports" es el directorio que va a contener una estructura compuesta de los archivos y carpetas con los informes y ajustes de optimización cargados después de cada una de las pruebas.

Adelantándose, cabe mencionar que la estructura interna del directorio Reports se crea en la clase "OptimisationManager" y se forma para cada optimización una vez finalizada la misma. Incluye los siguientes puntos:

  1. Directorio cuyo nombre es ID del terminal.
  2. Directorio según el nombre del robot que contiene lo siguiente:
    • Settings.xml — archivo con ajustes de la optimización (se forma dentro del programa)
    • History.xml — archivo de la optimización histórica copiado (se forma por el terminal)
    • Forward.xml — archivo de la optimización forward copiado (se forma por el terminal)

De esta manera, hemos creado dos clases que son puntos de partida para trabajar con el sistema de archivos. Luego, en el código, el trabajo con el sistema de archivos se realiza usando las clases estándar de C#, lo que permite evitar errores en las rutas hacia los archivos y acelera considerablemente el desarrollo.

Objetos que trabajan con los archivos de los informes y ajustes del robot (OptimisatorSettingsManager, ReportReader, SetFileManager)

En este capítulo, vamos a considerar el trabajo con los archivos. Durante este proceso, nuestra extensión tiene que trabajar con los siguientes archivos:

  • Archivo de configuración de los parámetros del robot
  • Archivo con el informe de la negociación
  • Archivo de los ajustes de la optimización que se guarda junto con los informes en el directorio "Reports" de nuestra extensión.

Empezamos nuestra discusión con el archivo que contiene los parámetros del robot para la optimización. Los archivos de los ajustes de los parámetros del robot tienen la extensión (*.set). No obstante, existen varios archivos de configuración, o sea, los archivos de los ajustes del robot cuando se inicia en el gráfico y los archivos de los ajustes del robot cuando se inicia en el Simulador de estrategias. Nos interesa el segundo formato de los archivos, ellos se almacenan en el directorio alterable del terminal según la ruta 

~\MQL5\Profiles\Tester

Es importante notar que, a veces, al realizar una instalación limpia del terminal, este directorio está ausente. Por eso, antes de empezar a trabajar, es necesario verificar si existe, y crearlo manualmente en caso de necesidad. Si este directorio no esta presente, el terminal no podrá guardar los ajustes de la optimización. Precisamente por esta razón, a veces surge el problema cuando después de una instalación limpia del terminal, después de cada nuevo repaso de la prueba u optimización, después de ir a la pestaña de los ajustes de la optimización, ellos se redefinen como por defecto. La estructura de los archivos descritos es un poco parecida a los archivos INI y tiene la siguiente apariencia:

Variable_name=Value||Start||Step||Stop||(Y/N)

En otras palabras, en estos archivos, la clave es el nombre del parámetro del robot, y el valor de la clave puede asumir la lista de sus valores cuyos nombres son idénticos a las columnas en el Simulador de estrategias en el ejemplo mostrado. El último valor de la variable puede adquirir uno de dos valores (Y/N) y significa la activación/desactivación de la optimización de este parámetro del robot. Una excepción de esta regla es el modo de escribir los parámetros string, ellos tienen el formato como en un archivo INI:

Variable_name=Value

Como los archivos de inicialización, los archivos SET tienen los comentarios. La línea del comentario siempre comienza con ";" (punto con coma).  El ejemplo más simple de tal archivo puede ser el siguiente:

; saved automatically on 2019.05.19 09:04:18

; this file contains last used input parameters for testing/optimizing 2MA_Martin expert advisor

;

Fast=12||12||1||120||N

Slow=50||50||1||500||N

maxLot=1||1||0.100000||10.000000||N

pathToDB=C:\Users\Administrator\Desktop\test_2MA_8

Para trabajar con estos archivos, necesitamos crear una clase-envoltorio que nos permita leer estos archivos, así como, una clase que almacene los valores de cada cadena leída. Esta clase ya ha sido considerada durante la parte View de este artículo, por eso, no vamos a discutirla aquí. Vamos a considerar la clase principal para leer los archivo y guarda dentro los parámetros especificados de la interfaz gráfica (SetFileManager). La implementación de esta clase es la siguiente:

class CWndContainer
{
    public SetFileManager(string filePath, bool createIfNotExists)
    {
        if ((FileInfo = new FileInfo(filePath)).Extension.CompareTo(".set") != 0)
            throw new ArgumentException("File mast have '.set' extention!");
        if (createIfNotExists)
            File.Create(filePath).Close();
        if (!File.Exists(filePath))
            throw new ArgumentException("File doesn`t exists");

    }

    public FileInfo FileInfo { get; }

    #region File data
        
    private List<ParamsItem> _params = new List<ParamsItem>();
    public List<ParamsItem> Params
    {
        get
        {
            if (_params.Count == 0)
                UpdateParams();
            return _params;
        }
        set
        {
            if (value != null && value.Count != 0)
                _params = value;
        }
    }
    #endregion

    public virtual void SaveParams()
    {
        if (_params.Count == 0)
            return;

        using (var file = new StreamWriter(FileInfo.FullName, false))
        {
            file.WriteLine(@"; saved by OptimisationManagerExtention program");
            file.WriteLine(";");
            foreach (var item in _params)
            {
                file.WriteLine($"{item.Variable}={item.Value}||{item.Start}||{item.Step}||{item.Stop}||{(item.IsOptimize ? "Y" : "N")}");
            }
        }
    }

    public virtual SetFileManager DublicateFile(string pathToFile)
    {
        if (new FileInfo(pathToFile).Extension.CompareTo(".set") != 0)
            throw new ArgumentException("File mast have '.set' extention!");

        File.Copy(FileInfo.FullName, pathToFile, true);
        return new SetFileManager(pathToFile, false);
    }
        
    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(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);
                }
            }
        }
    }
}

Primero, es necesario prestar atención en la comprobación del formato del archivo definida en el constructor de esta clase. Si el formato del archivo es diferente del formato del archivo SET, esta clase va a generar un error, ya que estamos intentando trabajar con un archivo que el terminal probablemente no entienda. El archivo en sí se almacena en la propiedad pública sólo para lectura (readonly)  FileInfo. La lectura del archivo se realiza en el método  UpdateParams, que lee el archivo en la construcción using de la primera línea hasta la última ignorando las líneas de los comentarios. También es necesario observar cómo se definen los parámetros del archivo leído. Primero, la línea leída se divide en dos, usando el signo de igualdad ("=") como separador, así, separamos el nombre de la variable de sus valores. El siguiente paso es separar los valores de la variable en un array compuesto de 4 elementos [Value, Start, Step, Stop, IsOptimise]. En caso de las líneas, este array no será dividido en estos elementos, porque no serán encontrados los símbolos separadores ("||"). Se recomienda no usar este símbolo en ellas para evitar los errores con las líneas. Si para cada elemento nuevo de nuestra línea faltan datos en el array, se le asigna el valor null, de lo contrario, se usa el valor del array.

Los valores se guardan en el método SaveParams. Hay que prestar atención en la escritura de los datos en el archivo que se realiza por esta cadena del código:

file.WriteLine($"{item.Variable}={item.Value}||{item.Start}||{item.Step}||{item.Stop}||{(item.IsOptimize ? "Y" : "N")}");

Aquí se puede observar que sean cadenas de caracteres u otros tipos de datos, las registramos todas en un formato de escritura de datos que NO son cadena. El terminal entiende si se trata de una cadena o no, por eso, ha sido elegido este tipo de la escritura de datos. Entre las desventajas de esta clase cabe destacar la imposibilidad de averiguar el tipo de datos. No podemos saber su formato con certeza, ya que la estructura del archivo no facilita esta información. El propio terminal recibe esta información directamente del EA, pero en la documentación no está descrito cómo se hace eso.  

El acceso y la definición de los parámetros del archivo leído se realiza a través de la propiedad Params. Como, de hecho, todo el trabajo con los datos del archivo se realiza a través de la propiedad descrita, por conveniencia, verificamos en el getter si el archivo había sido leído antes o no. Si el archivo no fue leído, se invoca el método  UpdateParams ya considerado por nosotros. En general, el procedimiento de trabajo con esta clase es el siguiente:

  1. Instanciamos, obteniendo así la representación POO del archivo
  2. Leemos, llamando al método Params (o UpdateParams, en caso de necesidad, por ejemplo, si de repente el archivo fue alterado desde fuera)
  3. Establecemos nuestros propios valores a través de Setter, o simplemente los modificamos, trabajando con el array con el array recibido a través de Getter.
  4. Guardamos los cambios a través del método SaveParams 

Como se puede observar, la principal desventaja, en comparación con los archivos INI, es que entre la lectura y escritura de los datos se encuentran en la memoria del programa. Sin embargo, si excluimos este archivo accidental o intencionadamente, el archivo puede ser alterado desde fuera —esta suposición es bastante realista, además de eso, la mayoría de los programas para trabajar con los archivos actúa según un esquema semejante. Además, en el arsenal de esta clase hay método DublicateFile cuya tarea es copiar el archivo según la ruta transferida (el copiado se ejecuta con sustitución si hay un archivo con el mismo nombre en la ruta propuesta).

El siguiente clase a examinar será RepirtReader, que lee los informes de optimización formados por el terminal, los analiza para poder mostrar la información en una tabla. El archivo con el historial de la optimización está representado en el formato XML creado para MS Excel. Su nodo de raíz (la primera etiqueta) es <Workbook/> y describe el libro. El siguiente nodo  <DocumentProperties/> describe los parámetros con los cuales se realizaba la optimización. Este nodo contiene la siguiente información útil:

  1. El título que se forma del nombre del robot, nombre del activo, timeframe y período de la optimización.
  2. Fecha de creación
  3. Nombre del servidor en el que se realizaba la optimización.
  4. Depósito divisa del depósito
  5. Apalancamiento

El siguiente nodo <Styles/> no es útil para nosotros (está creada principalmente para Excel, es seguido por el nodo  <Worksheet />, que describe la hoja con la descarga de los repasos de la optimización. Este nodo incluye el nodo <Table/> que almacena la información que buscamos, es decir, una lista de los resultados de la optimización divididos por las columnas, como después de la conclusión de la iteración de los parámetros en el Simulador de Estrategias. Merece la pena mencionar que la primera fila de la tabla incluye los encabezados de la tabla seguidos por los valores. Cada nodo <Row/>  contiene una lista de valores de esta tabla incluidos en la etiqueta <Cell/>. Además, cada etiqueta <Cell/> contiene el atributo Type, que indica en el tipo del valor en esta celda. Puesto que este archivo, incluso en el formato recortado, es bastante voluminoso, no voy a mostrar su ejemplo, pero Usted puede estudiarlo optimizando algún robot y abriendo los resultados de su optimización en la carpeta Reports de nuestra extensión. Ahora, vamos a analizar la clase descrita. Empezamos esta discusión con la visión general de las propiedades que describen el archivo de la optimización.

#region DocumentProperties and column names
/// <summary>
/// Nombres de las columnas del documento
/// </summary>
protected List<string> columns = new List<string>();
/// <summary>
/// Acceso externo a la colección de las columnas del documento, se devuelve una copia de la colección 
/// para proteger la colección original contra la modificación 
/// </summary>
public List<string> ColumnNames => new List<string>(columns);
/// <summary>
/// Encabezado del documento
/// </summary>
public string Title { get; protected set; }
/// <summary>
/// Autor del docuemento
/// </summary>
public string Author { get; protected set; }
/// <summary>
/// Fecha de la creación del documento
/// </summary>
public DateTime Created { get; protected set; }
/// <summary>
/// Servidor en el que ha sido realizado la optimización
/// </summary>
public string Server { get; protected set; }
/// <summary>
/// Depósito inicial 
/// </summary>
public Deposit InitialDeposit { get; protected set; }
/// <summary>
/// Apalancamiento
/// </summary>
public int Leverage { get; protected set; }
#endregion

Como podemos ver, estas propiedades se obtienen del nodo <DocumentProperties/>. Estas propiedades se rellenan en el siguiente método:

protected virtual void GetDocumentProperties(string path)
{
    document.Load(path);

    Title = document["Workbook"]["DocumentProperties"]["Title"].InnerText;
    Author = document["Workbook"]["DocumentProperties"]["Author"].InnerText;
    string DT = document["Workbook"]["DocumentProperties"]["Created"].InnerText;
    Created = Convert.ToDateTime(DT.Replace("Z", ""));
    Server = document["Workbook"]["DocumentProperties"]["Server"].InnerText;
    string[] deposit = document["Workbook"]["DocumentProperties"]["Deposit"].InnerText.Split(' ');
    Deposit = new Deposit(Convert.ToDouble(deposit[0]), deposit[1]);
    Leverage = Convert.ToInt32(document["Workbook"]["DocumentProperties"]["Leverage"].InnerText);

    enumerator = document["Workbook"]["Worksheet"]["Table"].ChildNodes.GetEnumerator();
    enumerator.MoveNext();

    foreach (XmlElement item in (XmlElement)enumerator.Current)
    {
        columns.Add(item["Data"].InnerText);
    }
}

Como se puede ver, usando los recursos del lenguaje C#, el trabajo con los archivos (*.xml) se realiza de manera casi tan fácil como el trabajo con los arrays. El objeto document es la instancia de la clase XmlDocument, almacena el archivo leído y ofrece un trabajo cómodo con él. El campo  enumerator, al que se le asigna un valor, lo vamos a necesitar en el método Read, que lee el documento línea por línea, volveremos a analizarlo más tarde. Ahora, merece la pena decir algunas palabras sobre la interfaz IDisposable que se utiliza al declarar la clase: 

class ReportReader : IDisposable

Esta interfaz contiene sólo un método Dispose() y es necesaria para poder usar esta clase en la construcción using. La construcción using, con la que nos hemos encontrado al grabar en el archivo en la última clase considerada, asegura un trabajo correcto, lo que significa que, en cada lectura desde el archivo, no necesitamos cerrar el archivo por cuenta propia. En vez de eso, el cierre se realiza en el método Dispise(), que se invoca automáticamente después de salir del bloque de las llaves en el cual trabajamos con el archivo. En este determinado caso, usaremos el método Dispise para limpiar el campo document con el fin de no almacenar un montón de la información dentro de la memoria cuando ya no la necesitamos. La implementación de este método es la siguiente:

public void Dispose()
{
    document.RemoveAll();
}

Ahora, echemos un vistazo a la interfaz IEnumerator mencionada arriba, que representa una interfaz estándar del lenguaje C#. Es la siguiente:

//
// Summary:
//     Supports a simple iteration over a non-generic collection.
[ComVisible(true)]
[Guid("496B0ABF-CDEE-11d3-88E8-00902754C43A")]
public interface IEnumerator
{
    object Current { get; }

    bool MoveNext();
    
    void Reset();
}

Como se puede ver, se compone de dos métodos y una propiedad. Esta interfaz sirve de una especie del envoltorio para la colección iterada por un valor cada una vez. El método MoveNext sirve del método que mueve el cursor a un valor hacia adelante hasta que no se termine la colección. Si intentamos llamar a este método después de recorrer toda la colección, devolverá false, lo que significará el fin de la iteración. El método Reset es necesario para reiniciar la iteración, o sea, mueve el cursor hacia el índice cero de la colección. La propiedad Current contiene el elemento actual seleccionado de la colección para el índice obtenido mediante el desplazamiento a través de MoveNext. Esta interfaz se usa ampliamente en C#, en particular, sirve de base para los ciclos foreach, sin embargo, la necesitamos para implementar el método Read. 

/// <summary>
/// Comando para leer las filas de la tabla de optimización
/// </summary>
/// <param name="row">
/// Fila leída key - título de la columna; value - valor de la celda</param>
/// <returns>
/// true - si la fila ha sido leída
/// false - si la fila no ha sido leída
/// </returns>
public virtual bool Read(out List<KeyValuePair<string, object>> row)
{
    row = new List<KeyValuePair<string, object>>();

    if (enumerator == null)
        return false;

    bool ans = enumerator.MoveNext();
    if (ans)
    {
        XmlNodeList nodes = ((XmlElement)enumerator.Current).ChildNodes;

        for (int i = 0; i < columns.Count; i++)
        {
            string value = nodes[i]["Data"].InnerText;
            string type = nodes[i]["Data"].Attributes["ss:Type"].Value;
            KeyValuePair<string, object> item = new KeyValuePair<string, object>(columns[i], ConvertToType(value, type));
            row.Add(item);
        }
    }
    return ans;
}

Como se puede observar, la tarea del método Read es muy parecida a la tarea del método MoveNext(). No obstante, este método también devuelve el resultado de su trabajo a través del parámetro que le ha sido pasado. Puesto que él debe devolver sólo las filas con valores, cuando definimos los valores de la variable enumerator, nosotros llamamos el método MoveNext una vez, desplazando así el cursor de la posición cero (encabezado de la tabla) al índice 1 (la primera fila con valores). Al leer los datos, también usamos el método ConvertToType, que convierte los valores leídos del formato string en el formato definido por el atributo Type. Precisamente por eso, en la lista devuelta, se especifica el tipo object como tipo del valor, así, tenemos la posibilidad de convertir cualquiera de los tipos en el tipo devuelto. Abajo se muestra la implementación del método   ConvertToType. .

private object ConvertToType(string value, string type)
{
    object ans;
    switch (type)
    {
        case "Number":
            {
                System.Globalization.NumberFormatInfo provider = new System.Globalization.NumberFormatInfo()
                {
                    NumberDecimalSeparator = ","
                };

                ans = Convert.ToDouble(value.Replace('.', ','), provider);
            }
            break;
        case "DateTime": ans = Convert.ToDateTime(value); break;
        case "Boolean":
            {
                try
                {
                    ans = Convert.ToBoolean(value.ToLower());
                }
                catch (Exception)
                {
                    ans = Convert.ToInt32(value) == 1;
                }
            }
            break;
        default: ans = value; break; // String
    }

    return ans;
}

De los objetos de la implementación de este método, merece la pena destacar la conversión de la cadena en formato numérico. Puesto que para diferentes configuraciones de los datos y de la hora (de acuerdo con los principios de su visualización en diferentes países) y, por tanto, para el separador de dígitos decimales existen varias opciones, entonces, después de realizar las pruebas con el formato de datos en ruso e inglés, llegué a la necesidad de indicar explícitamente el separador decimal utilizado.

Para reiniciar el lector, ha sido creado el método ResetReader que es un envoltorio para el método IEnumerator.Reset y está implementado de la siguiente manera:

public void ResetReader()
{
    if (enumerator != null)
    {
        enumerator.Reset(); // Reseteamos
        enumerator.MoveNext(); // Ignoramos los encabezados
    }
}

De esta manera, usando un envoltorio conveniente para el análisis de los archivos XML proporcionado por el lenguaje C#, hemos conseguido escribir una clase-envoltorio para analizar los archivos de los informes sin dificultad alguna, leyendo y recibiendo información adicional.

Siguiendo la lista propuesta, en este capítulo, nos queda por considerar otra clase que, a diferencia de dos clases discutidas arriba, trabaja con los archivos de configuraciones del optimizador generadas por la extensión en cuestión, y no por el terminal directamente. Puesto que una de las funcionalidades intituladas es la inicialización del robot en el Simulador de estrategias cuando se hace doble clic en el parámetro seleccionado de la optimización, entonces, surge la pregunta, ¿de dónde podemos obtener las configuraciones para el Simulador (intervalo de datos, nombre del activo, etc.)? Es que el informe del optimizador almacena sólo una parte de estos datos pero no todos. Obviamente, será necesario guardar estos ajustes en un archivo para resolver este problema. El marcado XML ha sido elegido como el formato de almacenamiento de datos, como el más conveniente. En la clase considerada arriba, ya hemos ejemplificado la lectura de los archivos XML. En ella, aparte de la lectura, también vamos a escribir en el archivo. Primero, es necesario aclararse con la información que va a almacenarse en el archivo de configuraciones.

El primer de los objetos guardado es la estructura que almacena los datos de las configuraciones del optimizador (presentadas en la pestaña Settings del área inferior de la pestaña principal Settings). Esta estructura está implementada de la siguiente manera:

struct OptimisationInputData
{
    public void Copy(OptimisationInputData data)
    {
        Login = data.Login;
        ForvardDate = data.ForvardDate;
        IsVisual = data.IsVisual;
        Deposit = data.Deposit;
        Laverage = data.Laverage;
        Currency = data.Currency;
        DepositIndex = data.DepositIndex;
        ExecutionDelayIndex = data.ExecutionDelayIndex;
        ModelIndex = data.ModelIndex;
        CurrencyIndex = data.CurrencyIndex;
        LaverageIndex = data.LaverageIndex;
        OptimisationCriteriaIndex = data.OptimisationCriteriaIndex;
    }
        
    public uint? Login;
    public DateTime ForvardDate;
    public bool IsVisual;
    public int Deposit;
    public string Laverage;
    public string Currency;
    public int DepositIndex, ExecutionDelayIndex, ModelIndex,
               CurrencyIndex, LaverageIndex, OptimisationCriteriaIndex;
    public ENUM_Model Model => GetEnum<ENUM_Model>(ModelIndex);
    public ENUM_OptimisationCriteria GetOptimisationCriteria => GetEnum<ENUM_OptimisationCriteria>(OptimisationCriteriaIndex);
    public ENUM_ExecutionDelay ExecutionDelay => GetEnum<ENUM_ExecutionDelay>(ExecutionDelayIndex);
    private T GetEnum<T>(int ind)
    {
        Type type = typeof(T);
        string[] arr = Enum.GetNames(type);
        return (T)Enum.Parse(type, arr[ind]);
    }
}

Inicialmente, esta estructura se creaba como un contenedor para transferir los datos de View en Model, y por tanto, ella contiene los índicespara ComboBox, además de los propios datos. Para facilitar el trabajo con esta estructura en el modelo, así como en otras clases, han sido escritos los  métodos de la conversión de valores de las enumeraciones (enum), que se almacenan en la estructura por su número del índice, en los tipos de enumeraciones solicitadas. Esta conversión funciona de la siguiente manera: en el Modelo de datos, para mostrar los valores de estas listas en ComboBox, ellos se almacenan en una forma string legible; el método  GetEnum<T> se usa para la conversión inversa. Este método es Generic, que es un análogo de las plantillas en C++. En este método, para obtener Enum deseado, primero, necesitamos averiguar un determinado valor del tipo transferido, para eso, se usa el clase Type que almacena el valor del tipo. Luego, primero, descomponemos de este tipo de la enumeración en una lista de cadenas. Después, usamos la transformación inversa de la cadena en enum, para obtener el valor de una determinada enumeración no en forma de una cadena, sino en forma de una enumeración deseada.

El siguiente objeto que contiene los datos almacenados es ConfigCreator_inputData. Esta estructura contiene los datos de la table con el terminal seleccionado, y se usa solamente en la clase OptimisationManager considerada a continuación para crear el archivo de configuración (de aquí proviene su nombre). Esta estructura es la siguiente:

struct ConfigCreator_inputData
{
    public ENUM_Timeframes TF;
    public uint? Login;
    public string TerminalID, pathToBot, setFileName,
           Pass, CertPass, Server, Symbol, ReportName;
    public DateTime From, Till;
    public ENUM_OptimisationMode OptimisationMode;
}

El tercero y el último de los datos almacenados es la lista de los parámetros del robot tipificada por la lista con ParamItem (List<ParamsItem>). Ahora, cuando ya tenemos analizados los datos a guardar, es necesario hablar del archivo que se compone durante el trabajo de la clase en cuestión:

<?xml version="1.0"?>
<Settings>
        <OptimisationInputData>
                <Item Name="Login" />
                <Item Name="ForvardDate">2019.04.01</Item>
                <Item Name="IsVisual">False</Item>
                <Item Name="Deposit">10000</Item>
                <Item Name="Laverage">1:1</Item>
                <Item Name="Currency">USD</Item>
                <Item Name="DepositIndex">2</Item>
                <Item Name="ExecutionDelayIndex">0</Item>
                <Item Name="ModelIndex">1</Item>
                <Item Name="CurrencyIndex">1</Item>
                <Item Name="LaverageIndex">0</Item>
                <Item Name="OptimisationCriteriaIndex">0</Item>
        </OptimisationInputData>
        <ConfigCreator_inputData>
                <Item Name="TF">16386</Item>
                <Item Name="Login">18420888</Item>
                <Item Name="TerminalID">0CFEFA8410765D70FC53545BFEFB44F4</Item>
                <Item Name="pathToBot">Examples\MACD\MACD Sample.ex5</Item>
                <Item Name="setFileName">MACD Sample.set</Item>
                <Item Name="Pass" />
                <Item Name="CertPass" />
                <Item Name="Server" />
                <Item Name="Symbol">EURUSD</Item>
                <Item Name="ReportName">MACD Sample</Item>
                <Item Name="From">2019.01.01</Item>
                <Item Name="Till">2019.06.18</Item>
                <Item Name="OptimisationMode">2</Item>
        </ConfigCreator_inputData>
        <SetFileParams>
                <Variable Name="InpLots">
                        <Value>0.1</Value>
                        <Start>0.1</Start>
                        <Step>0.010000</Step>
                        <Stop>1.000000</Stop>
                        <IsOptimize>False</IsOptimize>
                </Variable>
                <Variable Name="InpTakeProfit">
                        <Value>50</Value>
                        <Start>50</Start>
                        <Step>1</Step>
                        <Stop>500</Stop>
                        <IsOptimize>False</IsOptimize>
                </Variable>
                <Variable Name="InpTrailingStop">
                        <Value>30</Value>
                        <Start>30</Start>
                        <Step>1</Step>
                        <Stop>300</Stop>
                        <IsOptimize>False</IsOptimize>
                </Variable>
                <Variable Name="InpMACDOpenLevel">
                        <Value>3</Value>
                        <Start>3</Start>
                        <Step>1</Step>
                        <Stop>30</Stop>
                        <IsOptimize>True</IsOptimize>
                </Variable>
                <Variable Name="InpMACDCloseLevel">
                        <Value>2</Value>
                        <Start>2</Start>
                        <Step>1</Step>
                        <Stop>20</Stop>
                        <IsOptimize>True</IsOptimize>
                </Variable>
                <Variable Name="InpMATrendPeriod">
                        <Value>26</Value>
                        <Start>26</Start>
                        <Step>1</Step>
                        <Stop>260</Stop>
                        <IsOptimize>False</IsOptimize>
                </Variable>
        </SetFileParams>
</Settings>

El archivo presentado fue creado durante el trabajo con el vídeo de ejemplo para uno de los robots. Como se puede ver en su estructura, el nodo de raíz del archivo es <Settings/>, que incluye otros tres nodos:    <OptimisationInputData/><ConfigCreator_inputData/><SetFileParams/>. Los tipos de datos en cada uno de los nodos corresponden a sus nombres. En los nodos que almacenan la información sobre las configuraciones del Simulador, el elemento final es la etiqueta Item, que contiene el atributo Name, a través del cual definimos el nombre del parámetro guardado. Para la lista de los parámetros del robot existe la etiqueta  <Variable/>, que también posee el atributo Name que almacena el nombre del parámetro, mientras que el valor correspondiente de los parámetros de la optimización se guarda en las etiquetas anidadas. Para crear este archivo, la clase OptimisatorSettingsManager se hereda de nuevo de la interfaz IDisposable, y en el método Dispose, los valores definidos se guardan en el archivo. Los getters de las propiedades ccorrespondientes se usan para leer los datos del archivo.

#region OptimisationInputData
/// <summary>
/// Estructura OptimisationInputData insertada para el almacenamiento
/// </summary>
private OptimisationInputData? _optimisationInputData = null;
/// <summary>
/// Obtener y guardar la estructura OptimisationInputData
/// </summary>
public virtual OptimisationInputData OptimisationInputData
{
    get
    {
        return new OptimisationInputData
        {
            Login = StrToUintNullable(GetItem(NodeType.OptimisationInputData, "Login")),
            ForvardDate = DateTime.ParseExact(GetItem(NodeType.OptimisationInputData, "ForvardDate"), DTFormat, null),
            IsVisual = Convert.ToBoolean(GetItem(NodeType.OptimisationInputData, "IsVisual")),
            Deposit = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "Deposit")),
            Laverage = GetItem(NodeType.OptimisationInputData, "Laverage"),
            Currency = GetItem(NodeType.OptimisationInputData, "Currency"),
            DepositIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "DepositIndex")),
            ExecutionDelayIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "ExecutionDelayIndex")),
            ModelIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "ModelIndex")),
            CurrencyIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "CurrencyIndex")),
            LaverageIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "LaverageIndex")),
            OptimisationCriteriaIndex = Convert.ToInt32(GetItem(NodeType.OptimisationInputData, "OptimisationCriteriaIndex"))
        };
    }
    set => _optimisationInputData = value;
}
#endregion

En este ejemplo, el getter obtiene la estructura OptimisationInputData. Los valores para esta estructura se retiran del archivo presentado arriba. Como se puede observar, el método GetItem se aplica en cualquier lugar del getter para obtener los datos del archivo. Este método usa 2 parámetros:

  1. Tipo del nodo del que se obtienen los datos. 
  2. Nombre del parámetro especificado en el atributo Name. 

La implementación de este método es la siguiente:

/// <summary>
/// Obtener el elemento del archivo de configuración
/// </summary>
/// <param name=»NodeName»>Tipo de estructura</param>
/// <param name=»Name»>Nombre del campo</param>
/// <returns>
/// Valor del campo
/// </returns>
public string GetItem(NodeType NodeName, string Name)
{
    if (!document.HasChildNodes)
        document.Load(Path.Combine(PathToReportDataDirectory, SettingsFileName));

    return document.SelectSingleNode($"/Settings/{NodeName.ToString()}/Item[@Name='{Name}']").InnerText;
}

Este modo de obtener los datos es remarcable debido al uso del lenguaje Xpath que parece a SQL, pero para el formato XML. Para obtener los datos del nodo que nos interesa a base del valor del atributo especificado, indicamos la  ruta completa hacia este nodo, y luego en el nodo final Item, indicamos la condición de que el atributo  Name tiene que ser igual al nombre pasado. De esta manera, se realiza la lectura desde el archivo para todas las estructuras, mientras que para la lista de parámetros, se usa un método diferente porque la estructura de este nodo es más compleja.

#region SetFileParams
/// <summary>
/// Lista de parámetros para guardar
/// </summary>
private List<ParamsItem> _setFileParams = new List<ParamsItem>();
/// <summary>
/// Obtenemos y establecemos los parámetros (.set) del archivo para almacenamiento
/// </summary>
public List<ParamsItem> SetFileParams
{
    get
    {
        if (!document.HasChildNodes)
            document.Load(Path.Combine(PathToReportDataDirectory, SettingsFileName));
        var data = document["Settings"]["SetFileParams"];

        List<ParamsItem> ans = new List<ParamsItem>();
        foreach (XmlNode item in data.ChildNodes)
        {
            ans.Add(new ParamsItem(item.Attributes["Name"].Value)
            {
                Value = item["Value"].InnerText,
                Start = item["Start"].InnerText,
                Step = item["Step"].InnerText,
                Stop = item["Stop"].InnerText,
                IsOptimize = Convert.ToBoolean(item["IsOptimize"].InnerText)
            });
        }

        return ans;
    }
    set { if (value.Count > 0) _setFileParams = value; }
}
#endregion

En este caso, repasamos cíclicamente todos los nodos <Variable/>, obtenemos el valor del atributo Name de cada uno de ellos, y rellenamos la clase ParamItem con los datos ubicados en este nodo ParamsItem determinado.

El método final Dispose(), en el que hemos convenido guardar los datos en el archivo, está representado por la siguiente implementación:

public virtual void Dispose()
{
    // Método anidado que ayuda a escribir los elementos de las estructuras
    void WriteItem(XmlTextWriter writer, string Name, string Value)
    {
        writer.WriteStartElement("Item");

        writer.WriteStartAttribute("Name");
        writer.WriteString(Name);
        writer.WriteEndAttribute();

        writer.WriteString(Value);

        writer.WriteEndElement();
    }
    void WriteElement(XmlTextWriter writer, string Node, string Value)
    {
        writer.WriteStartElement(Node);
        writer.WriteString(Value);
        writer.WriteEndElement();
    }

    // primero, limpiamos la clase que guarda el marcado xml del archivo de configuraciones
    if (document != null)
        document.RemoveAll();

    // luego, comprobamos si se puede guardar los resultados
    if (!_configInputData.HasValue ||
        !_optimisationInputData.HasValue ||
        _setFileParams.Count == 0)
    {
        return;
    }

    using (var xmlWriter = new XmlTextWriter(Path.Combine(PathToReportDataDirectory, SettingsFileName), null))
    {
        xmlWriter.Formatting = Formatting.Indented;
        xmlWriter.IndentChar = '\t';
        xmlWriter.Indentation = 1;

        xmlWriter.WriteStartDocument();

        xmlWriter.WriteStartElement("Settings");

        xmlWriter.WriteStartElement("OptimisationInputData");
        WriteItem(xmlWriter, "Login", _optimisationInputData.Value.Login.ToString());
        WriteItem(xmlWriter, "ForvardDate", _optimisationInputData.Value.ForvardDate.ToString(DTFormat));
        WriteItem(xmlWriter, "IsVisual", _optimisationInputData.Value.IsVisual.ToString());
        WriteItem(xmlWriter, "Deposit", _optimisationInputData.Value.Deposit.ToString());
        WriteItem(xmlWriter, "Laverage", _optimisationInputData.Value.Laverage);
        WriteItem(xmlWriter, "Currency", _optimisationInputData.Value.Currency);
        WriteItem(xmlWriter, "DepositIndex", _optimisationInputData.Value.DepositIndex.ToString());
        WriteItem(xmlWriter, "ExecutionDelayIndex", _optimisationInputData.Value.ExecutionDelayIndex.ToString());
        WriteItem(xmlWriter, "ModelIndex", _optimisationInputData.Value.ModelIndex.ToString());
        WriteItem(xmlWriter, "CurrencyIndex", _optimisationInputData.Value.CurrencyIndex.ToString());
        WriteItem(xmlWriter, "LaverageIndex", _optimisationInputData.Value.LaverageIndex.ToString());
        WriteItem(xmlWriter, "OptimisationCriteriaIndex", _optimisationInputData.Value.OptimisationCriteriaIndex.ToString());
        xmlWriter.WriteEndElement();

        xmlWriter.WriteStartElement("ConfigCreator_inputData");
        WriteItem(xmlWriter, "TF", ((int)_configInputData.Value.TF).ToString());
        WriteItem(xmlWriter, "Login", _configInputData.Value.Login.ToString());
        WriteItem(xmlWriter, "TerminalID", _configInputData.Value.TerminalID.ToString());
        WriteItem(xmlWriter, "pathToBot", _configInputData.Value.pathToBot);
        WriteItem(xmlWriter, "setFileName", _configInputData.Value.setFileName);
        WriteItem(xmlWriter, "Pass", _configInputData.Value.Pass);
        WriteItem(xmlWriter, "CertPass", _configInputData.Value.CertPass);
        WriteItem(xmlWriter, "Server", _configInputData.Value.Server);
        WriteItem(xmlWriter, "Symbol", _configInputData.Value.Symbol);
        WriteItem(xmlWriter, "ReportName", _configInputData.Value.ReportName);
        WriteItem(xmlWriter, "From", _configInputData.Value.From.ToString(DTFormat));
        WriteItem(xmlWriter, "Till", _configInputData.Value.Till.ToString(DTFormat));
        WriteItem(xmlWriter, "OptimisationMode", ((int)_configInputData.Value.OptimisationMode).ToString());
        xmlWriter.WriteEndElement();

        xmlWriter.WriteStartElement("SetFileParams");
        foreach (var item in _setFileParams)
        {
            xmlWriter.WriteStartElement("Variable");

            xmlWriter.WriteStartAttribute("Name");
            xmlWriter.WriteString(item.Variable);
            xmlWriter.WriteEndAttribute();

            WriteElement(xmlWriter, "Value", item.Value);
            WriteElement(xmlWriter, "Start", item.Start);
            WriteElement(xmlWriter, "Step", item.Step);
            WriteElement(xmlWriter, "Stop", item.Stop);
            WriteElement(xmlWriter, "IsOptimize", item.IsOptimize.ToString());

            xmlWriter.WriteEndElement();
        }
        xmlWriter.WriteEndElement();

        xmlWriter.WriteEndElement();
        xmlWriter.WriteEndDocument();
        xmlWriter.Close();
    }
}

Al principio de este método, han sido creadas dos funciones anidadas. La función WriteItem está diseñado para sacar el bloque del código repetitivo que sirve para la escritura de los elementos de las estructuras. La función  WriteElement se usa para escribir los valores de los parámetros de la optimización, como Start, Step, Stop, IsOptimize. Como necesitamos la presencia de las tres etiquetas en el archivo de configuración, antes de la escritura tenemos un  bloque de verificación, cuya tarea consiste en impedir la escritura del archivo si no han sido pasados todos los parámetros necesarios. Luego, en la construcción using, cuyo significado ya ha sido considerado anteriormente, se realiza la escritura de los datos en el archivo. Gracias a las funciones incorporadas, la parte del método considerado, responsable de la escritura de los datos en el archivo, ha sido reducida en más de tres veces.  

Testeando los objetos clave

Concluyendo, me gustaría decir algunas palabras sobre el testeo de la aplicación. Puesto que el complemento creado va a desarrollarse y modificarse, he decidido escribir las pruebas de los principales objetos para tener en el futuro la posibilidad de verificar rápidamente su capacidad funcional (si surge la necesidad de su modificación). Actualmente, las simulaciones cubren parcialmente las siguientes clases:

  • Config
  • ReportReader
  • OptimisationSettingsManager
  • SetFileManager
  • TerminalManager

En el próximo artículo, tengo planeado modificar las clases que serán descritas en las siguientes capítulos, y estas modificaciones afectarán la lógica y el resultado del funcionamiento de algunos de los métodos, por tanto, estás clases no se cubren por las pruebas Unit. Las pruebas para estás clases serán implementadas en el siguiente artículo. Además, cabe mencionar que a pesar de que las pruebas estén escritas como pruebas Unit, en realidad, todas ellas son actualmente de integración, ya que interactúan con objetos externos (terminal/sistema de archivos, etc.). Los siguientes objetos planeados se plantea testear sin dependencias de los objetos descritos arriba, es decir, como pruebas Unit. Precisamente por esta razón, antes de cada uno de los objetos descritos anteriormente, existen fábricas para su creación. Un ejemplo es la fábrica para crear la clase ReportReader:

#region ReportReaderFabric
abstract class ReportReaderCreator
{
    public abstract ReportReader Create(string path);
}

class MainReportReaderCreator : ReportReaderCreator
{
    public override ReportReader Create(string path)
    {
        return new ReportReader(path);
    }
}
#endregion

Su código es simple. En sustancia, envolvemos la creación del objeto tipo ReportReader en la clase MainReportReaderCreator que se hereda de la clase ReportReaderFabric. Este enfoque ofrece la posibilidad de transferir en los objetos principales (se discuten en los capítulos de abajo) un objeto tipificado como ReportReaderFabric, y la implementación de una determinada fábrica ya puede ser diferente. De esta manera, en las pruebas Unit para los objetos clave, podemos sustituir las clases que trabajan con los archivos y con el terminal, así como, reduciremos las dependencias de las clases unas de otras. Esta técnica de la formación de los objetos se llama el «Método Fábrica» (en inglés, Factory Method).

La implementación de las futuras pruebas será considerada más detalladamente en el próximo artículo. Un ejemplo del uso del método de fábrica para crear los objetos será considerado en los capítulos siguientes. En el capítulo actual, analizaremos una prueba escrita para la clase que trabaja con los archivos de configuración. Al principio, hay que decir que todas las pruebas del proyecto actual tienen que realizarse en un proyecto separado: "Unit Test Project".


Vamos a llamarlo "OptimisationManagerExtentionTests", porque las pruebas serán escritas para el proyecto "OptimisationManagerExtention", el siguiente paso será la adición de las referencias al proyecto "OptimisationManagerExtention", es decir, a nuestra DLL con la interfaz gráfica y la lógica. Como vamos a testear los objetos que están marcados por el modificador de acceso public, hay dos opciones de hacerlos disponibles en nuestro proyecto de prueba:

  1. Hacerlos públicos (lo que no es correcto , ya que los usamos sólo dentro del proyecto)
  2. Añadir la posibilidad de ver las clases internas en un determinado proyecto (lo que es más preferible en comparación con el primer método)

Para resolver este problema, acudí al segundo método, mientras que para la clase del testeo de la gráfica añadí el siguiente atributo al código del proyecto principal:

[assembly: InternalsVisibleTo("OptimisationManagerExtentionTests")]

El siguiente paso es escribir las pruebas de las clases seleccionadas. Como nuestro proyecto de testeo es un proyecto auxiliar, no vamos a considerar cada clase de prueba. En vez de eso, mostraré sólo una clase, y usando su ejemplo, intentaré describir las matices del trabajo con las pruebas. Por conveniencia, mostraremos la clase completa que testea la clase Config. Como podemos ver, la primera condición para que esta clase sea de prueba es la necesidad de de que tenga el atributo  [TestClass]. Además, la clase de testeo tiene que ser pública y sus métodos de testeo deben tener el atributo   [TestMethod] —pues, el proceso de testeo va a realizarse en ellos. El método marcado con el atributo  [TestInitialize] se inicializa siempre antes del inicio de la prueba siguiente. Además, existe el atributo semejante [ClassInitialize], que no se usa en esta prueba, pero se usa en otras, y a diferencia del método marcado con el atributo   [TestInitialize], se ejecuta sólo una vez antes del inicio de la primera prueba. Cada uno de los métodos, al final de la prueba, contiene la llamada  a uno de los métodos de la clase Assert, que comparan el valor de testeo con el necesario, por tanto, la prueba se confirma o se rechaza.        

[TestClass]
public class ConfigTests
{
    private string ConfigName = $"{Environment.CurrentDirectory}\\MyTestConfig.ini";
    private string first_excention = "first getters call mast be null becouse file doesn`t contains this key";
    Config config;

    [TestInitialize]
    public void TestInitialize()
    {
        if (File.Exists(ConfigName))
            File.Delete(ConfigName);
        config = new Config(ConfigName);
    }
    [TestMethod]
    public void StringConverter_GetSetTest()
    {
        string expected = null;
 
        // first get
        string s = config.Common.Password;
        Assert.AreEqual(expected, s, first_excention);

        // set
        expected = "MyTestPassward";
        config.Common.Password = expected;
        s = config.Common.Password;
        Assert.AreEqual(expected, s, "Login mast be equal to MyTestLogin");

        // set null
        config.Common.Login = null;
        s = config.Common.Password;
        Assert.AreEqual(expected, s, "Login mast be equal to MyTestLogin");
    }
    [TestMethod]
    public void ServerConverter_GetSetTest()
    {
        ServerAddressKeeper expected = null;

        // first get;
        ServerAddressKeeper server = config.Common.Server;
        Assert.AreEqual(expected, server);

        // set
        expected = new ServerAddressKeeper("193.219.127.76:4443"); // Open broker demo server
        config.Common.Server = expected;
        server = config.Common.Server;
        Assert.AreEqual(server.Address, expected.Address, $"Adress mast be {expected.Address}");
    }
    [TestMethod]
    public void BoolConverter_GetSetTest()
    {
        bool? expected = null;

        // first get
        bool? b = config.Common.ProxyEnable;
        Assert.AreEqual(expected, b, first_excention);

        // set
        Random gen = new Random();
        int prob = gen.Next(100);
        expected = prob <= 50;
        config.Common.ProxyEnable = expected;
        b = config.Common.ProxyEnable;
        Assert.AreEqual(expected.Value, b.Value, "ProxyEnables mast be equal to true");

        // set null
        config.Common.ProxyEnable = null;
        b = config.Common.ProxyEnable;
        Assert.AreEqual(expected.Value, b.Value, "ProxyEnables mast be equal to true");

    }
    [TestMethod]
    public void ENUMConverter_GetSetTest()
    {
        ENUM_ProxyType? expected = null;

        // first get
        ENUM_ProxyType? p = config.Common.ProxyType;
        Assert.AreEqual(expected, p, first_excention);

        // set
        Random gen = new Random();
        int prob = gen.Next(300);
        int n = prob <= 100 ? 0 : (prob > 100 && prob <= 200 ? 1 : 2);
        expected = (ENUM_ProxyType)n;

        config.Common.ProxyType = expected;
        p = config.Common.ProxyType;
        Assert.AreEqual(expected.Value, p.Value, $"ProxyType mast be equal to {expected.Value}");

        // set null
        config.Common.ProxyEnable = null;
        p = config.Common.ProxyType;
        Assert.AreEqual(expected.Value, p.Value, $"ProxyType mast be equal to {expected.Value}");
    }
    [TestMethod]
    public void DTConverter_GetSetTest()
    {
        DateTime? expected = null;

        // first get
        DateTime? p = config.Tester.FromDate;
        Assert.AreEqual(expected, p, first_excention);

        // set
        expected = DateTime.Now;

        config.Tester.FromDate = expected;
        p = config.Tester.FromDate;
        Assert.AreEqual(expected.Value.Date, p.Value.Date, $"ProxyType mast be equal to {expected.Value}");

        // set null
        config.Common.ProxyEnable = null;
        p = config.Tester.FromDate;
        Assert.AreEqual(expected.Value.Date, p.Value.Date, $"ProxyType mast be equal to {expected.Value}");
    }
    [TestMethod]
    public void DoubleConverter_GetSetTest()
    {
        double? expected = null;

        // first get
        double? p = config.Tester.Deposit;
        Assert.AreEqual(expected, p, first_excention);

        // set
        Random rnd = new Random();
        expected = rnd.NextDouble();

        config.Tester.Deposit = expected;
        p = config.Tester.Deposit;
        Assert.AreEqual(Math.Round(expected.Value, 6), Math.Round(p.Value, 6), $"Deposit mast be equal to {expected.Value}");

        // set null
        config.Common.ProxyEnable = null;
        p = config.Tester.Deposit;
        Assert.AreEqual(Math.Round(expected.Value, 6), Math.Round(p.Value, 6), $"Deposit mast be equal to {expected.Value}");
    }
    [TestMethod]
    public void DeleteKeyTest()
    {
        config.Common.Login = 12345;
        config.DeleteKey(ENUM_SectionType.Common, "Login");

        Assert.AreEqual(null, config.Common.Login, "Key mast be deleted");
    }
    [TestMethod]
    public void DeleteSectionTest()
    {
        config.Common.Login = 12345;
        config.DeleteSection(ENUM_SectionType.Common);

        Assert.AreEqual(null, config.Common.Login, "Key mast be deleted");
    }
}

Si consideramos esta determinada clase de prueba, cabe mencionar que ella no cubre todos los métodos necesarios. En vez de eso, testea la clase Config.Converter que, de hecho, ejecuta toda la lógica de trabajo con el archivo de configuración. No obstante, como es una clase private, tenemos que escribir las pruebas para las propiedades que utilizan esta clase, y no directamente para ella. Por ejemplo, la prueba   DoubleConverter_GetSetTest() verifica si string ha sido convertido en Double correctamente a través de la propiedad   config.Tester.Deposit.  Si consideramos esta determinada prueba en detalle, podremos ver que se compone de 3 partes:

  1. Solicitud del parámetro tipo double en el campo que no ha sido creado —se devuelve el valor null.
  2. Escritura el valor aleatorio en el archivo y su lectura
  3. Escritura de null, que debe ser ignorado

Si ha sido detectado un error en cualquier etapa, será fácil captar y corregirlo, así que las pruebas tienen que ser bastante útiles en el desarrollo de las aplicaciones. Después de crear todas las pruebas, podemos ejecutarlas directamente desde VisualStudio, el inicio se ejecuta por la ruta Test => Run => AllTests


También pueden ser útiles para los lectores cuyos estándares regionales del ordenador se diferencian del mío— al ejecutar estas pruebas, Usted puede detectar posibles errores (por ejemplo, el separador de la parte fraccionada de un número decimal) y corregirlos personalmente.

Gestor de la optimización (OptimissationManager)

Como ha sido mencionado antes, al escribir esta aplicación, uno de los criterios era la extensibilidad. Tomando en cuenta que el proceso de la optimización será alterado en el siguiente artículo, y la principal interfaz de la extensión no exigirá alteraciones significativas, he decidido remover el proceso de la optimización de la clase del modelo en forma de una clase abstracta cuya implementación puede depender del método de la optimización solicitada. Esta clase está escrita de acuerdo con la plantilla de una fábrica abstarcta, empezamos a considerarla con la clase de la propia fábrica:

/// <summary>
/// Fábrica para crear las clases que controlan el proceso de la optimización
/// </summary>
abstract class OptimisationManagerFabric
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name=»ManagerName»>Nombre del gestor de la optimización creado</param>
    public OptimisationManagerFabric(string ManagerName)
    {
        this.ManagerName = ManagerName;
    }
    /// <summary>
    /// Nombre que refleja el tipo del gestor de la optimización creado (su particularidad)
    /// </summary>
    public string ManagerName { get; }
    /// <summary>
    /// Método que crea el gestor de la optimización
    /// </summary>
    /// <returns>Gestor de la optimización</returns>
    public abstract OptimisationManager Create(Dictionary<string, BotParamKeeper> botParamsKeeper,
                                               List<ViewModel.TerminalAndBotItem> selectedTerminals);
}

Como se puede ver en la fábrica abstracta, ella contiene el nombre de la clase implementada que vamos a necesitar en los siguientes artículos, así como, el método que crea el gestor de la optimización. Se supone que el gestor de la optimización se crea antes de cada optimización. Luego, se le delega todo el trabajo con el terminal, por tanto, en  el método que genera el objeto pasamos los parámetros como el diccionario con la lista de los robots y la lista de los terminales (o sea, los parámetros que se varían de una optimización a otra). Todos los demás parámetros necesarios se planea transferir a la clase de una determinada fábrica desde el constructor. Ahora, vamos a considerar la clase OptimisationManager. Esta clase ha sido proyectada para controlar la optimización, no obstante, a parte de la optimización, también se encarga de la ejecución de las pruebas. Puesto que la ejecución de las pruebas casi siempre se realiza de acuerdo con el mismo algoritmo, esta funcionalidad está implementada directamente en la clase abstracta en cuestión, hablaremos sobre su implementación más tarde. En cuanto al inicio y el fin de la optimización, esta funcionalidad ha sido transformada en dos métodos abstractos que requieren la implementación en la clase derivada. El constructor de esta clase recibe el volumen excesivo de las fábricas, pudiendo así operar con todos los objetos considerados antes.

public OptimisationManager(TerminalDirectory terminalDirectory,
                                   TerminalCreator terminalCreator,
                                   ConfigCreator configCreator,
                                   ReportReaderCreator reportReaderCreator,
                                   SetFileManagerCreator setFileManagerCreator,
                                   OptimisationExtentionWorkingDirectory currentWorkingDirectory,
                                   Dictionary<string, BotParamKeeper> botParamsKeeper,
                                   Action<double, string, bool> pbUpdate,
                                   List<ViewModel.TerminalAndBotItem> selectedTerminals,
                                   OptimisatorSettingsManagerCreator optimisatorSettingsManagerCreator)

Para avisar la clase del método sobre la conclusión del proceso de la optimización, contiene el evento AllOptimisationsFinished. Para descubrir exactamente qué terminales y robots se encuentran en este gestor de la optimización usando la clase del modelo, ha sido creado la siguiente propiedad.

/// <summary>
/// Diccionario donde:
/// key - ID del terminal
/// value - ruta completa hacia el robot
/// </summary>
public virtual Dictionary<string, string> TerminalAndBotPairs
{
    get
    {
        Dictionary<string, string> ans = new Dictionary<string, string>();
        foreach (var item in botParamsKeeper)
        {
            ans.Add(item.Key, item.Value.BotName);
        }
        return ans;
    }
}

Esta propiedad está implementada en una clase abstracta, no obstante, puede ser reescrita, porque está marcada con la palabra clave virtual. Para avisar a la clase del modelo sobre si el proceso de la optimización/prueba está en ejecución, ha sido creada una propiedad correspondiente cuyos valores se definen a partir de los métodos que inician el proceso de la optimización/prueba.

public bool IsOptimisationOrTestInProcess { get; private set; } = false;

Por conveniencia, un largo método que será esencialmente inalterable en la mayoría de los casos en las clases con la optimización y para ejecutar el testeo, también está implementado directamente en la clase abstracta. Se trata del método para formar el archivo de configuración, vamos a considerarlo más detalladamente. 

protected virtual Config CreateConfig(ConfigCreator_inputData data,
                                      OptimisationInputData optData)
{
    DirectoryInfo termonalChangableFolder = terminalDirectory.Terminals.Find(x => x.Name == data.TerminalID);

    Config config = configCreator.Create(Path.Combine(termonalChangableFolder.GetDirectory("config").FullName, "common.ini"))
                                         .DublicateFile(Path.Combine(currentWorkingDirectory.Configs.FullName, $"{data.TerminalID}.ini"));

    // Llenamos el archivo de configuración
    config.Common.Login = data.Login;
    config.Common.Password = data.Pass;
    config.Common.CertPassword = data.CertPass;
    if (!string.IsNullOrEmpty(data.Server) || !string.IsNullOrWhiteSpace(data.Server))
    {
        try
        {
            config.Common.Server = new ServerAddressKeeper(data.Server);
        }
        catch (Exception e)
        {
            System.Windows.MessageBox.Show($"Server adress was incorrect. Your adress is '{data.Server}' but mast have following type 'IPv4:Port'" +
                                           $"\nError message:\n{e.Message}\n\nStack Trace is {e.StackTrace}");
            return null;
        }
    }

    bool IsOptimisation = (data.OptimisationMode == ENUM_OptimisationMode.Fast_genetic_based_algorithm ||
                           data.OptimisationMode == ENUM_OptimisationMode.Slow_complete_algorithm);

    config.Tester.Expert = data.pathToBot;
    config.Tester.ExpertParameters = data.setFileName;
    сonfig.Tester.Symbol = data.Symbol;
    config.Tester.Period = data.TF;
    config.Tester.Login = optData.Login;
    config.Tester.Model = optData.Model;
    config.Tester.ExecutionMode = optData.ExecutionDelay;
    config.Tester.Optimization = data.OptimisationMode;
    с data.From;
    config.Tester.ToDate = data.Till;
    config.Tester.ForwardMode = ENUM_ForvardMode.Custom;
    config.Tester.ForwardDate = optData.ForvardDate;
    config.Tester.ShutdownTerminal = IsOptimisation;
    config.Tester.Deposit = optData.Deposit;
    config.Tester.Currency = optData.Currency;
    config.Tester.Leverage = optData.Laverage;
    config.Tester.OptimizationCriterion = optData.GetOptimisationCriteria;
    config.Tester.Visual = optData.IsVisual;

    if (IsOptimisation)
    {
        config.Tester.Report = data.ReportName;
        config.Tester.ReplaceReport = true;
    }

    return config;
}

Primero, usando la clase que describe el directorio alterable de los terminales, así como, la fábrica para para crear los objetos tipo Config, creamos el objeto del archivo de configuración y lo copiamos al directorio correspondiente de nuestra extensión, asignándole como nombre ID del terminal al que pertenecía el archivo de configuración original. Luego, ocurre el relleno de la sección [Tester] del archivo de configuración copiado. Todos los datos para rellenar esta sección se obtienen directamente desde las estructuras transmitidas que se forman en el código (en caso de la optimización), o se obtienen desde el archivo (en caso de la ejecución de la prueba). Si el servidor ha sido transmitido incorrectamente, se muestra un mensaje correspondiente en forma de MessageBox, y se devuelve null, en vez del archivo de configuración. Con el mismo propósito —para sacar el código repetido— en la clase abstracta, ha sido implementado el método que crea el gestor de los terminales. Vamos a analizarlo a continuación:

protected virtual ITerminalManager GetTerminal(Config config, string TerminalID)
{
    DirectoryInfo TerminalChangebleFolder = terminalDirectory.Terminals.Find(x => x.Name == TerminalID);

    ITerminalManager terminal = terminalCreator.Create(TerminalChangebleFolder);
    terminal.Config = config;

    if (MQL5Connector.MainTerminalID == terminal.TerminalID)
        terminal.Portable = true;

     return terminal;
}

Entre las particularidades de su implementación, cabe destacar el hecho de que si ID del terminal requerido coincide con ID del terminal del que ha sido iniciada nuestra extensión, el terminal estará configurado para la ejecución en modo Portable. No obstante, actualmente, la aplicación en sí va a trabajar correctamente sólo con la inicialización del terminal en modo estándar. Por tanto, como veremos en la siguiente clase que describe el modelo, aquí hay un filtro que ignora el terminal actual y no lo coloca en la lista de los disponibles.

El método que inicia la prueba en el terminal seleccionado por el evento de doble clic también se coloca en la clase abstracta y está implementado de la siguiente manera:

/// <summary>
/// Método del inicio por el evento de doble clic 
/// </summary>
/// <param name="TerminalID">ID seleccionado del terminal</param>
/// <param name=»pathToBot»>Ruta hacia el robot respecto a la carpeta de los EAs</param>
/// <param name=»row»>Fila de la tabla de optimización</param>
public virtual void StartTest(ConfigCreator_inputData data,
                              OptimisationInputData optData)
{
    pbUpdate(0, "Start Test", true);

    double pb_step = 100.0 / 3;

    IsOptimisationOrTestInProcess = true;

    pbUpdate(pb_step, "Create Config File", false);
    Config config = CreateConfig(data, optData);
    config.Tester.Optimization = ENUM_OptimisationMode.Disabled;
    config.Tester.ShutdownTerminal = false;
    config.DeleteKey(ENUM_SectionType.Tester, "ReplaceReport");
    config.DeleteKey(ENUM_SectionType.Tester, "Report");

    pbUpdate(pb_step, "Create TerminalManager", false);
    ITerminalManager terminal = GetTerminal(config, data.TerminalID);

    pbUpdate(pb_step, "Testing", false);
    terminal.Run();
    terminal.WaitForStop();
    IsOptimisationOrTestInProcess = false;
    pbUpdate(0, null, true);
}

En sus parámetros de entrada, él acepta los datos, que en la clase que describe el modelo, se obtienen del archivo con configuraciones guardadas. Dentro del método, se definen los valores de la barra de progreso (Progress Bar) y del estatus de la operación a través del delegado transmitido. El archivo de configuración formado se ajusta para la inicialización del Simulador de estrategias, a saber, se eliminan las claves que describen el informe del optimizador (por innecesario), así como, se deshabilita la desactivación automática del terminal cuando el Simulador termina su trabajo. Después de la ejecución del terminal, el flujo que lo ha inicializado se queda en suspenso esperando la conclusión de su trabajo, así, se realiza la notificación del formulario sobre la conclusión del testeo. Adelantándose, cabe mencionar que para que el propio formulario no quede colgado durante el inicio de la optimización/prueba, estos procesos se inician en el contexto del flujo secundario. En cuanto a la optimización, como ya ha sido mencionado antes, su proceso ha sido pasado al metodo abstracto protected. Sin embargo, también hay un método público implementado en la clase abstracta, que es necesario para un funcionamiento correcto de la propia clase y no puede ser reescrita.

/// <summary>
/// Inicio del proceso de la optimización/prueba de todos los terminales pleneados
/// </summary>
/// <param name=»BotParamsKeeper»>Lista de los terminales, robots y parámetros de robots</param>
/// <param name=»PBUpdate»>Delegado que edita los valores de la barra de la carga y del estatus</param>
/// <param name=»sturtup_status»>Respuesta de la función - se usa si no se ha podido iniciar la optimización/prueba, 
/// en esta línea se escribe la razón de la inicialización fallada</param>
/// <returns>true - en caso del éxito</returns>
public void StartOptimisation()
{
    pbUpdate(0, "Start Optimisation", true);
    IsOptimisationOrTestInProcess = true;
 
    DoOptimisation();
    OnAllOptimisationsFinished();
    IsOptimisationOrTestInProcess = false;
    pbUpdate(0, null, true);
}
protected abstract void DoOptimisation();

/// <summary>
/// Método que interrumpe la optimización
/// </summary>
public abstract void BreakOptimisation();

Básicamente, este método, regula el orden en que se acciona el proceso de la optimización en relación a la actualización de la barra de progreso, a la colocación de las banderas sobre el inicio y la conclusión de la optimización, así como, la llamada al evento de la conclusión de los recorridos de optimización.

El último método implementado en la clase abstracta es el método del desplazamiento del informe al directorio de trabajo de nuestra extensión. Como el archivo con los ajustes de la optimización tiene que crearse juntamente con el desplazamiento del informe, todas estas acciones han sido incluidas en el mismo método.

protected virtual void MoveReportToWorkingDirectery(ITerminalManager terminalManager,
                                                    string FileName,
                                                    ConfigCreator_inputData ConfigCreator_inputData,
                                                    OptimisationInputData OptimisationInputData)
{
    FileInfo pathToFile_history = new FileInfo(Path.Combine(terminalManager.TerminalChangeableDirectory.FullName, $"{FileName}.xml"));
    FileInfo pathToFile_forward = new FileInfo(Path.Combine(terminalManager.TerminalChangeableDirectory.FullName, $"{FileName}.forward.xml"));
    int _i = 0;
    while (_i <= 100 && (!pathToFile_history.Exists && !pathToFile_forward.Exists))
    {
        _i++;
        System.Threading.Thread.Sleep(500);
    }

    string botName = new FileInfo(terminalManager.Config.Tester.Expert).Name.Split('.')[0];
    DirectoryInfo terminalReportDirectory = currentWorkingDirectory.Reports.GetDirectory(terminalManager.TerminalID, true);
    if (terminalReportDirectory == null)
        throw new Exception("Can`t create directory");
    DirectoryInfo botReportDir = terminalReportDirectory.GetDirectory(botName, true);
    if (botReportDir == null)
        throw new Exception("Can`t create directory");

    FileInfo _history = new FileInfo(Path.Combine(botReportDir.FullName, "History.xml"));
    FileInfo _forward = new FileInfo(Path.Combine(botReportDir.FullName, "Forward.xml"));

    if (_history.Exists)
        _history.Delete();
    if (_forward.Exists)
        _forward.Delete();

    if (pathToFile_history.Exists)
    {
        pathToFile_history.CopyTo(_history.FullName, true);
        pathToFile_history.Delete();
    }
    if (pathToFile_forward.Exists)
    {
        pathToFile_forward.CopyTo(_forward.FullName, true);
        pathToFile_forward.Delete();
    }

    string pathToSetFile = Path.Combine(terminalManager.TerminalChangeableDirectory
                                        .GetDirectory("MQL5")
                                        .GetDirectory("Profiles")
                                        .GetDirectory("Tester").FullName,
                                        ConfigCreator_inputData.setFileName);

    using (OptimisatorSettingsManager manager =
           optimisatorSettingsManagerCreator.Create(botReportDir.FullName))
    {
        manager.OptimisationInputData = OptimisationInputData;
        manager.ConfigCreator_inputData = ConfigCreator_inputData;
        manager.SetFileParams = setFileManagerCreator.Create(pathToSetFile, false).Params;
    }
}

Primero, en este método, obtenemos las rutas hacia los archivos con informes. Después, esperamos en el ciclo la creación de uno de los archivos buscados (sólo uno, ya que es posible que no se formen dos archivos, por ejemplo, se inicia sólo la optimización histórica sin un período «forward»). Luego, formamos la ruta hacia el directorio donde va a almacenarse el archivo con informe. Básicamente, este fragmento del código contiene el layout de las subcarpetas del directorio Reports. Después, se crean las rutas hacia futuros archivos y se eliminan los archivos antiguos (si existen). Luego, los informes se copian al directorio de nuestra extensión. Al final, creamos un archivo (*.xml) con las configuraciones que se usan en el proceso de la optimización. Puesto que este proceso debe realizarse por etapas y es poco probable que sea alterado, lo hemos movido en una clase abstracta, y para iniciarlo, ahora es suficiente llamar a este método de la clase derrivada.

Después de analizar la clase abstracta, vamos a considerar el proceso de la optimización. Actualmente, se trata de una inicialización habitual del terminal con parámetros de la optimización seleccionados, como en el Simulador estándar. Los aspectos más interesantes de su implementación son el proceso de la inicialización y el manejador del evento de la conclusión de la optimización. Vamos a analizarlos paso a paso, partiendo de la inicialización del proceso.

private readonly List<ITerminalManager> terminals = new List<ITerminalManager>();
/// <summary>
/// Método que interrumpe el proceso de la optimización y cierra el terminal forzosamente
/// </summary>
public override void BreakOptimisation()
{
    foreach (var item in terminals)
    {
        if (item.IsActive)
            item.Close();
    }
}
private void UnsubscribeTerminals()
{
    if (terminals.Count > 0)
    {
        foreach (var item in terminals)
        {
            item.TerminalClosed -= Terminal_TerminalClosed;
        }
        terminals.Clear();
    }
}

protected override void DoOptimisation()
{
    UnsubscribeTerminals();

    double pb_step = 100.0 / (botParamsKeeper.Count + 1);

    foreach (var item in botParamsKeeper)
    {
        pbUpdate(pb_step, item.Key, false);

        ConfigCreator_inputData configInputData = GetConfigCreator_inputData(item.Key);
        OptimisationInputData optData = item.Value.OptimisationData;

        Config config = CreateConfig(configInputData, optData);

        ITerminalManager terminal = GetTerminal(config, item.Key);
        terminal.TerminalClosed += Terminal_TerminalClosed;
        terminal.Run();

        terminals.Add(terminal);
    }

    pbUpdate(pb_step, "Waiting for Results", false);

    foreach (var item in terminals)
    {
        if (item.IsActive)
            item.WaitForStop();
    }
}

Como se puede observar, la lista de los gestores de los terminales ha sido colocada en el campo para poder acceder a ella desde diferentes métodos. Gracias a eso, podemos implementar el método BreakOptimisations. Después de crear el terminal, suscribimos al evento de su cierre en el método del inicio del proceso de la optimización. Eso nos permite observar la conclusión del proceso de la optimización. Después de iniciar la optimización en el ciclo, mantenemos el flujo hasta que no se cierren todos los terminales en ejecución. El método UnsubscribeTerminals es necesario para dar de baja todos los eventos asignados anteriormente cuando se reinicia la optimización. Este método también se invoca en el destructor de esta clase. El manejador del evento de la parad de la optimización está implementado de la siguiente manera:

protected virtual void Terminal_TerminalClosed(ITerminalManager terminalManager)
{
    string FileName = new FileInfo(terminalManager.Config.Tester.Expert).Name.Split('.')[0];

    ConfigCreator_inputData ConfigCreator_inputDat = GetConfigCreator_inputData(terminalManager.TerminalID);
    OptimisationInputData optData = botParamsKeeper[terminalManager.TerminalID].OptimisationData;

    MoveReportToWorkingDirectery(terminalManager, FileName, ConfigCreator_inputDat, optData);
}

private ConfigCreator_inputData GetConfigCreator_inputData(string TerminalID)
{
    ViewModel.TerminalAndBotItem settingsData = selectedTerminals.Find(x => x.TerminalID == TerminalID);
    BotParamKeeper ParamKeeper = botParamsKeeper[TerminalID];

    ConfigCreator_inputData ConfigCreator_inputDat = new ConfigCreator_inputData
    {
        TerminalID = TerminalID,
        pathToBot = ParamKeeper.BotName,
        CertPass = settingsData.CertPass,
        From = settingsData.From,
        Till = settingsData.Till,
        Login = settingsData.Login,
        OptimisationMode = settingsData.GetOptimisationMode,
        Pass = settingsData.Pass,
        Server = settingsData.Server,
        setFileName = botParamsKeeper[TerminalID].BotParams.FileInfo.Name,
        Symbol = settingsData.AssetName,
        TF = settingsData.GetTF,
        ReportName = new FileInfo(ParamKeeper.BotName).Name.Split('.')[0]
    };

    return ConfigCreator_inputDat;
}

Como podemos ver, su principal tarea es desplazar los archivos con informe de la optimización en el directorio correspondiente. De esta manera, se implementa la lógica de la inicialización de la optimización y el Simulador de estrategias. Una de las operaciones a ejecutar en el siguiente artículo es la implementación de los métodos adicionales de la optimización según la muestra ya descrita. Hemos analizado casi toda la aplicación creada. Ahora, nos queda considerar la principal clase resultante que describe el modelo, a la que nos referimos desde ViewModel.

Clase resultante del modelo (IExtentionGUI_M y su implementación)

Como hemos mencionado antes, esta parte del proyecto implementa la interfaz IExtentionGUI_M, siendo un punto de partida para implementar la lógica del formulario descrito. La parte gráfica y ViewModel, como ha sido dicho antes, se refiere a esta clase para recibir los datos y delegar la ejecución de diferentes comandos. Consideramos esta parte comenzando con la descripción de su interfaz, que está implementado como sigue.

/// <summary>
/// Interfaz del modelo
/// </summary>
interface IExtentionGUI_M : INotifyPropertyChanged
{
    #region Properties

    bool IsTerminalsLVEnabled { get; }
    List<FileReaders.ParamsItem> BotParams { get; }
    VarKeeper<string> Status { get; }
    VarKeeper<double> PB_Value { get; }
    ObservableCollection<string> TerminalsID { get; }
    DataTable HistoryOptimisationResults { get; }
    DataTable ForvardOptimisationResults { get; }
    ObservableCollection<ViewExtention.ColumnDescriptor> OptimisationResultsColumnHeadders { get; }
    ObservableCollection<string> TerminalsAfterOptimisation { get; }
    VarKeeper<int> TerminalsAfterOptimisation_Selected { get; set; }
    ObservableCollection<string> BotsAfterOptimisation { get; }
    VarKeeper<int> BotsAfterOptimisation_Selected { get; set; }

    #endregion

    void LoadOptimisations();
    void LoadBotParams(string fullExpertName,
        string TerminalID,
        out OptimisationInputData? optimisationInputData);
    List<string> GetBotNamesList(int terminalIndex);
    uint? GetCurrentLogin(int terminalIndex);
    void StartOptimisationOrTest(List<ViewModel.TerminalAndBotItem> SelectedTerminals);
    void StartTest(ENUM_TableType TableType, int rowIndex);
    bool RemoveBotParams(string TerminalID);
    bool IsEnableToAddNewTerminal();
    void SelectNewBotsAfterOptimisation_forNewTerminal();
    void UpdateTerminalOptimisationsParams(OptimisationInputData optimisationInputData);
}

#region Accessory objects 

/// <summary>
/// Enum que caracteriza el tipo de las tablas con el resultado de las optimizaciones
/// </summary>
enum ENUM_TableType
{
    History,
    Forvard
}

VewModel trabaja precisamente con esta interfaz. Si es necesario, su implementación (que vamos a considerar a continuación) puede ser sustituida por otra. Además de eso, sin alterar la parte gráfica del programa, o se puede alterar la gráfica del programa y no afectar su lógica. Dado que esta interfaz se hereda de la interfaz  INotifyPropertyChanged, hemos obtenido la posibilidad de notificar a ViewModel (y por consiguiente, a View de nuestra aplicación) sobre una modificación de alguna propiedad implementada en nuestro modelo de datos. Para facilitar la escritura del código en el modelo, he añadido una clase-envoltorio universal VarKeeper que, además de almacenar un valor de cualquier tipo, también es capaz de convertirse implícitamente al tipo almacenado y, cuando un valor almacenado se altera, notificar a ViewModel sobre la modificación realizada. La implementación de esta clase es la siguiente:

/// <summary>
/// La clase que almacena la variable _Var tipo T_keeper.
/// Puede convertirse implícitamente en el tipo T_keeper así como, modificar el valor de la variable guardada
/// En el momento de la modificación del valor, notifica sobre  a todos los suscriptores sobre eso
/// </summary>
/// <typeparam name=»T_keeper»>Tipo de la variable almacenada</typeparam>
class VarKeeper<T_keeper>
{
    /// <summary>
    /// Constructor que establece el nombre de identificación de la variable
    /// </summary>
    /// <param name="propertyName">nombre de identificación de la variable</param>
    public VarKeeper(string propertyName)
    {
        this.propertyName = propertyName;
    }
    /// <summary>
    /// Constructor que establece el nombre de identificación de la variable 
    /// y el valor inicial de la variable
    /// </summary>
    /// <param name="PropertyName">nombre de identificación de la variable</param>
    /// <param name=»Var»>valor inicial de la variable</param>
    public VarKeeper(string PropertyName, T_keeper Var) : this(PropertyName)
    {
        _Var = Var;
    }
    /// <summary>
    /// Sobrecarga del operador de la conversión implícita de tipos.
    /// Convierte este tipo en T_keeper
    /// </summary>
    /// <param name="obj"></param>
    public static implicit operator T_keeper(VarKeeper<T_keeper> obj)
    {
        return obj._Var;
    }
    /// <summary>
    /// variable almacenada 
    /// </summary>
    protected T_keeper _Var;
    /// <summary>
    /// Nombre de identificación de la variable
    /// </summary>
    public readonly string propertyName;
    #region Event 
    /// <summary>
    /// Evento que notifica sobre la modificación de la variable almacenada
    /// </summary>
    public event Action<string> PropertyChanged;
    /// <summary>
    /// Método que llama al evento que notifica sobre la modificación de la variable almacenada
    /// </summary>
    protected void OnPropertyChanged()
    {
        PropertyChanged?.Invoke(propertyName);
    }
    #endregion
    /// <summary>
    /// Método que define el valor de la variable con el valor value
    /// </summary>
    /// <param name=»value»>nuevo valor de la variable</param>
    public void SetVar(T_keeper value)
    {
        SetVarSilently(value);
        OnPropertyChanged();
    }
    public void SetVarSilently(T_keeper value)
    {
        _Var = value;
    }
}

En el constructor de esta clase, ocurre la transferencia del valor inicial para la variable almacenada, así como, del nombre de la variable que será usado cuando se notifica sobre una modificación de su valor. La  variable en sí se almacena en el campo protected de esta clase. El nombre de la variable usado para notificar sobre la modificación de su valor, se almacena en el campo público disponible sólo para la lectura,  ropertyName. Los métodos para definir el valor de una variable se dividen en el método que define sus valores y que llama al evento de notificación de todos los suscriptores sobre la alteración hecha, y en el  método que solamente establece el valor de la variable. Para poder convertir implícitamente esta clase en el tipo del valor almacenado de ella, se usa la  sobrecarga del operador de la conversión de tipos. Gracias a esta clase, hemos obtenido la posibilidad de almacenar los valores de las variables, leerlos sin usar la conversión explícita de tipos, así como, notificar al ambiente sobre la alteración del valor de esta variable. En el constructor de la clase que implementa la interfaz IExtentionGUI_M, establecemos los valores de las propiedades tipificadas por el tipo que examinamos, y también suscribimos al evento de la notificación cuando estas propiedades se actualizan. En el destructor de esta clase, por lo contrario, damos de baja de los eventos de estas propiedades.

public ExtentionGUI_M(TerminalCreator TerminalCreator,
                      ConfigCreator ConfigCreator,
                      ReportReaderCreator ReportReaderCreator,
                      SetFileManagerCreator SetFileManagerCreator,
                      OptimisationExtentionWorkingDirectory CurrentWorkingDirectory,
                      OptimisatorSettingsManagerCreator SettingsManagerCreator,
                      TerminalDirectory terminalDirectory)
{
    // Asignamos el directorio actual de trabajo
    this.CurrentWorkingDirectory = CurrentWorkingDirectory;
    this.terminalDirectory = terminalDirectory;
    //Creamos las fábricas
    this.TerminalCreator = TerminalCreator;
    this.ReportReaderCreator = ReportReaderCreator;
    this.ConfigCreator = ConfigCreator;
    this.SetFileManagerCreator = SetFileManagerCreator;
    this.SettingsManagerCreator = SettingsManagerCreator;
    CreateOptimisationManagerFabrics();

    // suscribimos al evento de la actualización de la colección de columnas en la tabla con optimizaciones históricas
    HistoryOptimisationResults.Columns.CollectionChanged += Columns_CollectionChanged;

    // Atribuimos el estatus inicial
    Status = new VarKeeper<string>("Status", "Wait for the operation");
    Status.PropertyChanged += OnPropertyChanged;
    // Asignamos valores iniciales para la barra de progreso
    PB_Value = new VarKeeper<double>("PB_Value", 0);
    PB_Value.PropertyChanged += OnPropertyChanged;
    // Creamos una variable que almacena el índice del terminal seleccionado de la lista de terminales disponibles que han participado en la optimización
    TerminalsAfterOptimisation_Selected = new VarKeeper<int>("TerminalsAfterOptimisation_Selected", 0);
    TerminalsAfterOptimisation_Selected.PropertyChanged += OnPropertyChanged;
    // Creamos una variable que almacena el índice del robot seleccionado de la lista de robots disponibles que han participado en la optimización
    BotsAfterOptimisation_Selected = new VarKeeper<int>("BotsAfterOptimisation_Selected", -1);
    BotsAfterOptimisation_Selected.PropertyChanged += OnPropertyChanged;

    _isTerminalsEnabled = new VarKeeper<bool>("IsTerminalsLVEnabled", true);
    _isTerminalsEnabled.PropertyChanged += OnPropertyChanged;

    // Cargamos los datos sobre los terminales instalados en el ordenador
    FillInTerminalsID();
    FillInTerminalsAfterOptimisation();
    LoadOptimisations();
}

Obsérvese que nosotros llamamos a una serie de los métodos en el constructor:

  • CreateOptimisationManagerFabrics — proporciona las fábricas que crean los gestores de las optimizaciones: se insertan en el array, luego, vamos a seleccionar el gestor de la optimización necesario de acuerdo con determinadas condiciones.
  • FillInTerminalsID — rellena la lista de los IDs de terminales que vemos en el menú desplegable al seleccionar un terminal para la optimización. Se registran todos los terminales, a excepción del actual, en el que ha sido iniciada nuestra extensión.
  • FillInTerminalsAfterOptimisation — rellena la lista de los terminales en los cuales ya ha sido realizada alguna optimización y hay datos para cargar en la tabla de la optimización.
  • LoadOptimiations — se rellena la tabla con optimizaciones conforme al terminal y robot seleccionados (de momento, ambos parámetros tienen el índice cero).

Por tanto, realizamos la principal tarea del constructor, es decir, preparamos el programa para la incialización, llenando todas las tablas y variables con valores iniciales. Si seguimos los pasos del trabajo con la interfaz gráfica en nuestro artículo, el siguiente paso será el trabajo con las tablas de los terminales seleccionados para la optimización. Los terminales seleccionados se almacenan en el diccionario, en uno de los campos de la clase.

/// <summary>
/// Representación de la tabla de terminales seleccionados de la pestaña inicial de esta extensión
/// key - Termonal ID
/// value - bot params
/// </summary>
private readonly Dictionary<string, BotParamKeeper> BotParamsKeeper = new Dictionary<string, BotParamKeeper>();
/// <summary>
/// Terminal seleccionado en este momento
/// </summary>
private string selectedTerminalID = null;
/// <summary>
/// Lista de los parámetros del robot para la edición
/// </summary>
List<ParamsItem> IExtentionGUI_M.BotParams
{
    get
    {
        return (BotParamsKeeper.Count > 0 && selectedTerminalID != null) ?
               BotParamsKeeper[selectedTerminalID].BotParams.Params :
               new List<ParamsItem>();
    }
}

La propiedad BotParams recibe en su getter una lista de parámetros del robot precisamente desde este diccionario y, al cambiar el robot seleccionado (el mecanismo será descrito más tarde), simplemente accedemos a una clave nueva en este diccionario. La adición y la modificación del contenido de este diccionario se controla por el método LoadBotParam, que se invoca inmediatamente después de la pulsación en el botón de adición de un terminal nuevo seleccionado de la lista desplegable en la primera pestaña de nuestra extensión. Este método está implementado de la siguiente manera:

void IExtentionGUI_M.LoadBotParams(string fullExpertName,
            string TerminalID,
            out OptimisationInputData? optimisationInputData)
{
    PBUpdate(0, "Loading params", true);
    optimisationInputData = null;

    if (!IsTerminalsLVEnabled)
        return;

    _isTerminalsEnabled.SetVar(false);

    if (!BotParamsKeeper.Keys.Contains(TerminalID))
    {
        PBUpdate(100, "Add New Terminal", false);
        AddNewTerminalIntoBotParamsKeeper(fullExpertName, TerminalID);
    }
    else
    {
        if (selectedTerminalID != null)
            BotParamsKeeper[selectedTerminalID].BotParams.SaveParams();
        else
        {
            foreach (var item in BotParamsKeeper)
            {
                item.Value.BotParams.SaveParams();
            }
        }
    }

    selectedTerminalID = TerminalID;
    optimisationInputData = BotParamsKeeper[selectedTerminalID].OptimisationData;

    if (BotParamsKeeper[selectedTerminalID].BotName != fullExpertName)
    {
        PBUpdate(100, "Load new params", false);
        BotParamKeeper param = BotParamsKeeper[selectedTerminalID];
        param.BotName = fullExpertName;
        param.BotParams = GetSetFile(fullExpertName, TerminalID);
        BotParamsKeeper[selectedTerminalID] = param;
    }
    PBUpdate(0, null, true);
    _isTerminalsEnabled.SetVar(true);
}

Como podemos ver en el código, además de bloquear la interfaz gráfica de usuario en el momento de la optimización y el testeo (como se ve en el vídeo), el código también contiene una  comprobación de que si podemos actualizar la lista de los parámetros del robot (y tal vez de los terminales), o no. Si podemos realizar la actualización de los parámetros del robot o terminal, entonces,  bloqueamos la interfaz gráfica. Luego, tiene lugar la adición de un robot nuevo, o el guardado de los parámetros introducidos anteriormente desde la interfaz gráfica. Luego, se guarda el ID seleccionado del terminal (clave en nuestro diccionario) y los parámetros del robot recién seleccionado se pasan de vuelta a ViewModel. Si hemos cambiado el robot seleccionado en comparación con el seleccionado anteriormente, cargamos los parámetros para él usando el método  GetSetFile. El método que añade un nuevo terminal es bastante simple y repite prácticamente por completo la última construcción condicional del método en cuestión. El trabajo principal en él se ejecuta por el método GetSetFile.

private SetFileManager GetSetFile(string fullExpertName, string TerminalID)
{
    DirectoryInfo terminalChangableFolder = terminalDirectory.Terminals.Find(x => x.Name == TerminalID);

    // Creamos el gestor que trabaja con el terminal
    ITerminalManager terminalManager = TerminalCreator.Create(terminalChangableFolder);

    // Creamos la ruta hacia la carpeta Tester (que se guarda en el directorio ~/MQL5/Profiles) 
    // Si no existe (a veces, MetaTrader no la crea en el momento de la primera instalación), la creamos personalmente
    // Ella almacena los archivos con ajustes de los parámetros de optimización
    DirectoryInfo pathToMqlTesterFolder = terminalManager.MQL5Directory.GetDirectory("Profiles").GetDirectory("Tester", true);
    if (pathToMqlTesterFolder == null)
        throw new Exception("Can`t find (or create) ~/MQL5/Profiles/Tester directory");

    // Creamos un archivo de configuración y lo copiamos inmediatamente a la carpeta Configs del directorio de trabajo actual de esta extensión
    Config config = ConfigCreator.Create(Path.Combine(terminalChangableFolder.GetDirectory("config").FullName, "common.ini"))
                                 .DublicateFile(Path.Combine(CurrentWorkingDirectory.Configs.FullName, $"{TerminalID}.ini"));
    // Configuramos el terminal para que inicie el testeo del robot seleccionado y se desactive inmediatamente
    // Eso es necesario para que el terminal cree el archivo .set con ajuste de este EA.
    // Para que se desactive inmediatamente, especificamos la fecha final de la prueba un día más tarde de la fecha del inicio.
    config.Tester.Expert = fullExpertName;
    config.Tester.Model = ENUM_Model.OHLC_1_minute;
    config.Tester.Optimization = ENUM_OptimisationMode.Disabled;
    config.Tester.Period = ENUM_Timeframes.D1;
    config.Tester.ShutdownTerminal = true;
    config.Tester.FromDate = DateTime.Now.Date;
    config.Tester.ToDate = config.Tester.FromDate.Value.AddDays(-1);

    // Definimos un archivo de configuración del gestor del terminal, lo ejecutamos y esperamos a que el terminal se cierre
    // Para que el terminal se cierre automáticamente después de la conclusión del testeo, 
    // asignamos el valor  true al campo config.Tester.ShutdownTerminal
    terminalManager.Config = config;
    terminalManager.WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized;
    string fileName = $"{new FileInfo(fullExpertName).Name.Split('.')[0]}.set";

    while (!terminalManager.Run())
    {
        System.Windows.MessageBoxResult mb_ans =
            System.Windows.MessageBox.Show(@"Can`t start terminal 
Close by hands all Metatrader terminals that running now (except main terminal)",
"Can`t start terminal", System.Windows.MessageBoxButton.OKCancel);
        if (mb_ans == System.Windows.MessageBoxResult.Cancel)
            break;
    }
    terminalManager.WaitForStop();

    bool isSetFileWasCreated = pathToMqlTesterFolder.GetFiles().Any(x => x.Name == fileName);

    return SetFileManagerCreator.Create(Path.Combine(pathToMqlTesterFolder.FullName, fileName), !isSetFileWasCreated);
}

Este método está bastante bien comentado por sí sólo, no obstante, merece la pena aclarar su principal objetivo. El método recibe los parámetros del robot seleccionado, y por tanto, su archivo SET. Este archivo se crea por el terminal cuando el robot se inicia en el Simulador, por tanto, la única manera de formarlo es ejecutar el algoritmo seleccionado en el Simulador de estrategias. Para que eso sea menos notable, el terminal con el Simulador activado se inicia en el modo minimizado del tay, y para que el Simulador termine rápidamente su trabajo y se desactive,  definimos la fecha final de la prueba un día antes de la fecha de su inicio. Si el terminal que se planea abrir ya está inicializado,  tratamos de inicializarlo en el ciclo mostrando el aviso correspondiente. Al final de la ejecución, devolvemos la representación orientada a objetos del archivo SET.

El siguiente momento interesante en esta clase es el proceso de la activación de las optimizaciones realizado por el método asincrónico StartOptimisationOrTest.

async void IExtentionGUI_M.StartOptimisationOrTest(List<ViewModel.TerminalAndBotItem> SelectedTerminals)
{
    if (BotParamsKeeper.Count == 0)
       return;
    foreach (var item in BotParamsKeeper)
    {
        item.Value.BotParams.SaveParams();
    }

    SetOptimisationManager(SelectedTerminals);

    // Iniciamos la optimización y esperamos su conclusión
    _isTerminalsEnabled.SetVar(false);
    await System.Threading.Tasks.Task.Run(() => selectedOptimisationManager.StartOptimisation());
    _isTerminalsEnabled.SetVar(true);
}

private void SetOptimisationManager(List<ViewModel.TerminalAndBotItem> SelectedTerminals)
{
    // Seleccionamos una fábrica para crear un gestor de la optimización de la lista
    OptimisationManagerFabric OMFabric = optimisationManagerFabrics[0];
    // Nos damos de baja del gestor de optimización usado antes
    if (selectedOptimisationManager != null)
    {
        // Verificamos si está en ejecución la optimización en este momento
        if (selectedOptimisationManager.IsOptimisationOrTestInProcess)
            return;

        selectedOptimisationManager.AllOptimisationsFinished -= SelectedOptimisationManager_AllOptimisationsFinished;
    }

    // Creamos un gestor de optimizaciones y lo suscribimos al evento de la conclusión de la optimización
    selectedOptimisationManager = OMFabric.Create(BotParamsKeeper, SelectedTerminals);
    selectedOptimisationManager.AllOptimisationsFinished += SelectedOptimisationManager_AllOptimisationsFinished;
}

Su implementación es interesante porque demuestra cómo usamos el gestor de la optimización.  Antes de cada inicio de la optimización, siempre volvemos a crearlo . En esta implementación, su creación se realiza  sólo para el primer gestor de los insertados en el array correspondiente, este proceso será más complicado en el próximo artículo. La inicialización de la prueba es algo parecida a la inicialización de la optimización. Sin embargo, en ella ocurre la sustitución de los parámetros del robot por los que han sido seleccionados a través de doble clic. 

async void IExtentionGUI_M.StartTest(ENUM_TableType TableType, int rowIndex)
{
    if (!IsTerminalsLVEnabled)
        return;

    string TerminalID = TerminalsAfterOptimisation[TerminalsAfterOptimisation_Selected];
    string pathToBot = BotsAfterOptimisation[BotsAfterOptimisation_Selected];
    DirectoryInfo terminalChangableFolder = terminalDirectory.Terminals.Find(x => x.Name == TerminalID);

    DataRow row = (TableType == ENUM_TableType.History ? HistoryOptimisationResults : ForvardOptimisationResults).Rows[rowIndex];

    ConfigCreator_inputData configInputData;
    OptimisationInputData OptimisatorSettings;

    DirectoryInfo BotReportDirectory = CurrentWorkingDirectory.Reports.GetDirectory(TerminalID).GetDirectory(pathToBot);
    using (OptimisatorSettingsManager settingsManager = SettingsManagerCreator.Create(BotReportDirectory.FullName))
    {
        configInputData = settingsManager.ConfigCreator_inputData;
        OptimisatorSettings = settingsManager.OptimisationInputData;

        string setFilePath = Path.Combine(terminalChangableFolder
                                          .GetDirectory("MQL5")
                                          .GetDirectory("Profiles")
                                          .GetDirectory("Tester", true).FullName,
                                           configInputData.setFileName);

        SetFileManager setFile = SetFileManagerCreator.Create(setFilePath, true);
        setFile.Params = settingsManager.SetFileParams;

        foreach (var item in setFile.Params)
        {
            if (row.Table.Columns.Contains(item.Variable))
                item.Value = row[item.Variable].ToString();
        }
        setFile.SaveParams();
    }

    _isTerminalsEnabled.SetVar(false);
    if (selectedOptimisationManager == null)
        SetOptimisationManager(new List<ViewModel.TerminalAndBotItem>());

    await System.Threading.Tasks.Task.Run(() =>
    {
        selectedOptimisationManager.StartTest(configInputData, OptimisatorSettings);
    });
    _isTerminalsEnabled.SetVar(true);
}

Este método también es asincrónico, y durante su proceso, también se crea un gestor de optimizaciones, pero sólo en el caso si ha sido creado antes. Para obtener los parámetros de entrada para ejecutar la prueba,  recurrimos al archivo de configuraciones ubicado al lado del informe de la optimización para el robot seleccionado. Después de crear el archivo de configuraciones del robot, buscamos entre sus parámetros aquéllos que han sido indicados en el informe de la optimización, y  definimos el valor de la fila de optimización seleccionada para el valor Value. Después de guardar los parámetros, pasamos a la ejecución de la prueba

Para cargar los resultados de la optimización en la tabla correspondiente, se utiliza el siguiente método que contiene un método anidado.

public void LoadOptimisations()
{
    // Método interno que rellena la tabla de datos que recibe
    void SetData(bool isForvard, DataTable tb)
    {
        // Limpiamos la tabla de los datos llenados anteriormente
        tb.Clear();
        tb.Columns.Clear();

        // Obtenemos los datos
        string TerminalID = TerminalsAfterOptimisation[TerminalsAfterOptimisation_Selected];
        string botName = BotsAfterOptimisation[BotsAfterOptimisation_Selected];
        string path = Path.Combine(CurrentWorkingDirectory.Reports
                                                          .GetDirectory(TerminalID)
                                                          .GetDirectory(botName)
                                                          .FullName,
                                                          $"{(isForvard ? "Forward" : "History")}.xml");
        if (!File.Exists(path))
            return;

        using (ReportReader reader = ReportReaderCreator.Create(path))
        {
            if (reader.ColumnNames.Count == 0)
                return;

            // Rellenamos las columnas
            foreach (var item in reader.ColumnNames)
            {
                tb.Columns.Add(item);
            }

            // Rellenamos las filas
            while (reader.Read(out List<KeyValuePair<string, object>> data))
            {
                DataRow row = tb.NewRow();
                foreach (var item in data)
                {
                    row[item.Key] = item.Value;
                }
                tb.Rows.Add(row);
            }
        }
    }

    if (TerminalsAfterOptimisation.Count == 0 && BotsAfterOptimisation.Count == 0)
    {
        return;
    }

    // Primero, rellenamos las optimizaciones históricas, y luego, las optimizaciones forward
    SetData(false, HistoryOptimisationResults);
    SetData(true, ForvardOptimisationResults);
}

Este método llama sólo dos veces a la función anidada donde se realiza todo el trabajo. En esta función ocurre lo siguiente:

  1. limpieza de la tabla transferida (y de sus columnas) 
  2. definición de la ruta hacia el archivo del informe cargado
  3. lectura del informe usando la clase ReportReader y la carga de sus datos en la tabla

Obsérvese que el constructor incluye la siguiente línea del código:

// suscribimos al evento de la actualización de la colección de columnas en la tabla con optimizaciones históricas
HistoryOptimisationResults.Columns.CollectionChanged += Columns_CollectionChanged;

Ella suscribe el método Columns_CollectionChanged al evento de la actualización de las columans de la tabla con optimizaciones históricas. Este evento ayuda a rastrear la adición de las columnas. En método suscrito, cuya implementación puede ser observada en el código (es muy grande y fácil de implementar), se añaden o se eliminan automáticamente los nombres de las columnas en la colección  OptimisationResultsColumnHeadders, de donde los datos pasan a ViewModel y a View, donde se añaden a ListView a través de la extensión para la carga automática descrita anteriormente. De esta manera, al editar la lista de las columnas en la tabla de optimizaciones históricas, las columnas en Viewe se editan automáticamente en ambas tablas.  

En este artículo, hemos considerado los detalles de la implementación de la lógica para iniciar la optimización, la carga del programa y la carga adicional de los archivos con recorridos históricos y forward, así como, el mecanismo para iniciar el testeo según el evento de doble clic. De hecho, ya tenemos preparada la aplicación que ha sido mostrada al principio del artículo en el vídeo. Nos queda sólo implementar su inicialización desde el terminal. Para eso, necesitamos escribir un envoltorio en forma del EA, que tiene el segundo aspecto.

//+------------------------------------------------------------------+
//|                                 OptimisationManagerExtention.mq5 |
//|                        Copyright 2019, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"

#import "OptimisationManagerExtention.dll"
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---   

   string data[];
   StringSplit(TerminalInfoString(TERMINAL_DATA_PATH),'\\',data);
   MQL5Connector::Instance(data[ArraySize(data)-1]);

   while(!MQL5Connector::IsWindowActive())
     {
      Sleep(500);
     }

   EventSetMillisecondTimer(500);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnTimer()
  {
   if(!MQL5Connector::IsWindowActive())
      ExpertRemove();
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }
//+------------------------------------------------------------------+

Después de compilar nuestro proyecto en C# (modo Release), necesitamos ubicarlo en el directorio correspondiente (~/Libraries) e incluir en nuestro robot. Para obtener ID del terminal actual, necesitamos obtener  la ruta hacia su directorio alterable, y luego, dividirla en las rutas compuestas, usando el método StringSplit. El último directorio va a contener el ID del terminal que buscamos. Después de  inicalizar la gráfica, se establece el retardo del flujo actual hasta que no se cargue la ventana. Luego, ejecutamos el temporizador. El temporizador es necesario para rastrear el evento del cierre de la ventana,  al cerrar la ventana, tenemos que eliminar el EA del gráfico. De esta manera tan simple, conseguimos el comportamiento descrito en el vídeo de ejemplo.

Conclusión y archivos adjuntos

Al comienzo del trabajo realizado, el objetivo era crear un complemento expansible al máximo para el terminal para gestionar los procesos de la optimización con una interfaz gráfica. Durante la implementación, ha sido elegido el lenguaje C#, porque posee una interfaz más conveniente para escribir las aplicaciones gráficas y una gran variedad de recursos adicionales, a veces mágicos, que simplifican considerablemente el proceso de la programación. En el presente artículo, hemos analizado el proceso entero de la creación de esta aplicación, empezando desde el principio de la inicialización de los programas a través de la consola y terminando con el desarrollo del envoltorio para iniciar MetaTrader desde otro terminal usando la tecnología de C#. El trabajo realizado era bastante emocionante, y espero que el lector no se quede decepcionado. Además, me gustaría decir que, a mi parecer, se puede mejorar las clases descritas en los últimos capítulos del presente artículo. Por tanto, en el siguiente artículo, probablemente, haré una refactorización del código colocado en estas clases, e intentaré estructurarlo mejor.

La aplicación contiene un archivo con dos carpetas:

  • MQL5 — está destinado para el terminal principal MetaTrader 5 en el que va a iniciarse la extensión descrita, y contiene un archivo que inicializa la extensión. 
  • Visual Studio — contiene tres proyectos descritos para Visual Studio, que deben ser compilados antes de usarlos. La biblioteca (*.dll), obtenida por medio de la compilación del proyecto OptimisationManagerExtention, tiene que ubicarse en el directorio Libraries del terminal en el que va a ejecutarse el proyecto descrito. 


Traducción del ruso hecha por MetaQuotes Software Corp.
Artículo original: https://www.mql5.com/ru/articles/7059

Archivos adjuntos |
Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XIII): Eventos del objeto "cuenta" Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XIII): Eventos del objeto "cuenta"

En este artículo, analizaremos los métodos de trabajo con los eventos de cuenta (de la cuenta comercial) que permiten monitorear los eventos importantes de cambio en las propiedades de una cuenta comercial y que influyen de una forma u otra en el comercio automático. Ya creamos cierta parte de la funcionalidad para el seguimiento de eventos de cuenta en el artículo anterior, al crear la colección de objetos de cuenta.

Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XII): Implementando la clase de objeto "cuenta" y la colección de objetos de cuenta Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XII): Implementando la clase de objeto "cuenta" y la colección de objetos de cuenta

En el artículo anterior, definimos los eventos de cierre de posiciones para MQL4 en la biblioteca y eliminamos las propiedades de las órdenes que nos resultaban innecesarias. En el presente artículo, analizaremos la creación del objeto "Cuenta", crearemos una colección de objetos de cuenta y prepararemos la funcionalidad de para monitorear los eventos de las cuentas.

Estudio de las figuras técnicas de Merrill Estudio de las figuras técnicas de Merrill

En el presente artículo, vamos a analizar el modelo de las figuras técnicas de Merrill, e intentaremos averiguar hasta qué punto estos patrones técnicos son útiles hoy en día. Para este propósito, crearemos una herramienta para testearlos y aplicaremos este modelo a diferentes tipos de datos, a saber: precio de cierre, sus máximos y mínimos, indicadores del tipo oscilatorio.

La batalla por la velocidad: QLUA vs MQL5 - ¿Por qué MQL5 es de 50 a 600 veces más rápido? La batalla por la velocidad: QLUA vs MQL5 - ¿Por qué MQL5 es de 50 a 600 veces más rápido?

Para comparar los lenguajes MQL5 y QLUA, hemos diseñado varias pruebas que miden la velocidad de ejecución de las operaciones básicas. En dichos tests, hemos utilizado una computadora con Windows 7 Professional 64 bit, MetaTrader 5 build 1340 y QUIK de versión 7.2.0.45.