Optimización móvil continua (Parte 6): La lógica del optimizador automático y su estructura

27 julio 2020, 14:55
Andrey Azatskiy
0
386

Introducción:

Continuamos describiendo la creación del optimizador automático encargado de implementar la optimización móvil continua. En el artículo anterior, analizamos la interfaz gráfica de la aplicación obtenida, sin embargo, no hablamos sobre su parte lógica ni su estructura interna. Precisamente de dichos puntos vamos a hablar en esta parte. Los artículos anteriores pueden leerse clicando en los enlaces mostrados abajo:

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

A la hora de describir la estructura interna de la aplicación, en ocasiones recurriremos a diagramas UML buscando detalles más concretos, para sí describir de forma visual su estructura, además de las llamadas realizadas por la aplicación durante su trabajo. Merece la pena observar que durante la creación de estos diagramas, no existía el objetivo de crear todos los objetos que participan en las comunicaciones, sino tan solo una representación esquemática de los objetos principales, así como la interacción entre ellos.

Estructura interna de la aplicación: descripción y generación de objetos clave.

Como ya dijimos en el anterior artículo, el patrón principal que utilizaremos en el programa obtenido, es el patrón MVVM. De acuerdo con el mismo, toda la lógica del programa se ha sacado a la clase del modelo de datos que se ensambla con el gráfico mediante la clase que implementa el papel del objeto ViewModel. La lógica del programa se fracciona con más detalle en una serie de clases que suponen entidades elementales. Las entidades elementales del programa encargadas de describir su lógica, así como la interacción de la lógica y la interfaz gráfica, se representan en el diagrama UML de clases mostrado más abajo.    


Antes de analizar el diagrama, vamos a asignar las indicaciones a color a los tipos de objetos con los que se corresponden. En azul, destacaremos la capa gráfica: podemos considerar que tras estos objetos se encuentra un marcado XAML con todos los mecanismos WPF ocultos tras él, invisibles tanto para el usuario principiante, como para el desarrollador. En violeta, mostraremos la capa que une la representación gráfica de la aplicación con su lógica, en otras palabras, la capa ViewModel del modelo MVVM utilizado. En rosa, designaremos las interfaces que suponen un representante abstracto de los datos ocultos bajo ellas.

La primera de ellas, oculta una implementación concreta del modelo de datos, y es que, siguiendo la idea principal de la plantilla MVVM, el modelo de los datos deberá ser máximo, independiente, y ViewModel, a su vez, no deberá depender de una implementación concreta del modelo. La segunda (IOptimiser), es la interfaz de la lógica de optimización; y es que, de acuerdo con una de las ideas de este programa, puede haber más de una lógica para realizar y seleccionar las optimizaciones, y si el usuario así lo desea, podrá sustituir esta seleccionando el optimizador correspondiente de la lista desplegable.

En marrón, destacaremos la capa que representa el modelo de datos en las interfaces gráficas. Como podemos ver, en el esquema hay dos modelos de datos: el primero está relacionado con el propio optimizador, mientras que el segundo está relacionado con la interfaz gráfica del optimizador automático. En amarillo, destacaremos el único gestor de optimizaciones que tenemos por el momento. No obstante, puede haber tantos gestores de optimización semejantes cuantos deseemos; si el lector quiere implementar su propia lógica de optimización, esto también será posible. El propio modo de implementación de este mecanismo lo veremos en los siguientes artículos. En verde, destacaremos los objetos auxiliares que actúan como fábricas e implementan la creación de los objetos en el momento necesario.

Bien, una vez titulados los tipos de los objetos analizados, vamos a analizar sus interacciones mutuas y el proceso de generación al iniciar la aplicación. Sin embargo, primero deberíamos analizar la capa principal de interfaz y sus componentes:

  • AutoOptimiser (ventana principal),
  • AutoOptimiserVM (view model),
  • IMainModel (interfaz del modelo),
  • MainModel (Model),
  • MainModelCreator (fábrica estática para crear el modelo de datos).


Estos son los primeros 5 objetos representados en este diagrama. La clase AutoOptimiser se instancia con el primer inicio de la aplicación, creando la interfaz gráfica. Como podemos ver en el artículo anterior, el marcado XAML de la interfaz gráfica contiene una indicación al objeto AutoOptimiserVM, que ejecuta el papel de ViewModel. Por consiguiente, al crear una capa gráfica, también crearemos la clase AutoOptimiserVM,; en este caso, además, la capa gráfica abarca a aquella por completo, por lo que el objeto existirá hasta que no sea eliminada la interfaz gráfica. Por ello, está relacionada con la clase AutoOptimiser (nuestra ventana) mediante el vínculo "Composición", que presupone la posesión y control plenos del objeto.  

La clase ViewModel debe tener acceso a la clase Model, pero la clase del modelo de datos debe mantener su independiencia respecto a ViewModel. En otras palabras, no debe saber qué clase precisamente representa el modelo de datos. En lugar de esta información, la clase ViewModel conoce la interfaz del modelo que contiene el conjunto de métodos públicos, eventos y propiedades que puede utilizar nuestro intermediario. Por eso, esta clase no está relacionada directamente con la clase MainModel, sino con su interfaz, mediante el vínculo "Agregación", que presupone (al igual que la composición) la pertenencia de la clase investigada respecto a la clase que la utiliza.

No obstante, una de las diferencias de esta conexiones consiste en que la clase analizada puede perteneceer a más de un objeto al mismo tiempo, y su proceso de actividad no es controlado por ninguno de los objetos de los contenedores. Esta afirmación es totalmente justa para la clase MainModel, ya que esta se crea en su propio constructor estático (la clase MainModelCreator) y se guarda tanto en él, como en la clase AutoOptimiserVM simultáneamente. El objeto se elimina cuando la aplicación finaliza su funcionamiento: esto se hace así para introducirlo primero en la propiedad estática, que se limpia solo al finalizar el funcionamiento de la aplicación.   

Siguiendo esta lógica, hemos analizado la interrelación entre los tres objetos clave: Model — View — ViewModel. La parte posterior del diagrama se dedica a la lógica de negocios principal de nuestra aplicación. En otras palabras, analizamos la relación de los objetos encargados del proceso de optimización con el objeto del modelo de datos. Los objetos encargados del proceso de gestión de la optimización son un cierto controlador que inicia los procesos buscados y delega su ejecución en objetos de programa más aislados. Uno de estos objetos es el optimizador. El optimizador, a su vez, también es una especie de gestor y, delega la ejecución de las tareas planteadas en objetos más orientados a tareas, por ejemplo, al inicio del terminal o la composición del archivo de configuración solicitado para iniciar el terminal. 


Durante la instanciación de la clase MainModel, nosotros, usando el mecanismo ya conocido de los constructores estáticos, instanciamos la clase del optimizador. Como podemos ver por este esquema, la clase del optimizador debe implementar la interfaz de IOptimiser, y también tener una clase-instructor heredada de la clase OptimiserCreator, que creará un ejemplar concreto del optimizador. Esto es necesario para implementar la sustitución dinámica de los optimizadores en el modo de ejecución del programa.

Cada uno de los optimizadores puede contener su propia y única lógica de optimización. En los próximos artículos, hablaremos con más detalle sobre la lógica del optimizador actual, así como de la implementación de los optimizadores. Ahora, vamos a volver a la arquitectura. La clase del modelo de datos está relacionada con la clase básica de todos los constructores de modelos por medio de una conexión de asociación; esto significa que la clase del modelo de datos usa los constructores de los optimizadores convertidos a su clase básica para generar un ejemplar concreto del optimizador.

El optimizador creado, a su vez, es convertido a su tipo de interfaz, guardándose después en el campo correspondiente de la clase MainModel. Con ello, usando la abstracción tanto en el proceso de generacuón del objeto (constructores de objetos), como en sus ejemplares (optimizadores), logramos la posibilidad de sustituir los optimizadores de forma dinámica durante la ejecución del programa. El enfoque que utilizamos en la instanciación de los optimizadores tiene el nombre de "Fábrica abstracta". Su sentido reside en que, tanto el producto (la clase que implementa la lógica de optimización), como sus fábricas (las clases que crean el producto) tienen su propia abstracción. La clase de usuario, a su vez, no debe estar al tanto de nada relacionado con la implementación concreta de la lógica de ambos componentes; no obstante, debe tener la posibilidad de utilizar sus diferentes implementaciones.

Un ejemplo en la vida real podría ser el agua con gas, o el té, o el café, etcétera, y por otra parte, las fábricas que los producen. La gente no tiene por qué conocer la metodología de fabricación concreta de cada una de las bebidas, para poder consumirlas. De la misma forma, tampoco tienen por qué conocer la estructura interna concreta de las fábricas que las producen o de las tiendas que se las venden, para comprarlas. En el presente ejemplo:

  • La persona es el usuario,
  • Las tiendas o las factorías que venden las bebidas son las fábricas,
  • Y las bebidas, el producto. 
En nuestro programa, el usuario es la clase MainModel.


Si echamos un vistazo a la implementación del optimizador por defecto, podremos ver que también tiene una interfaz gráfica con ajustes (la misma que se llama clicando en el botón "GUI" junto a ComboBox, donde se enumeran todos los optimizadores). En el diagrama de las clases (y en el código), la parte gráfica de los ajustes del optimizador se llama "SimpleOptimiserSettings", mientras que ViewModel y View se llaman "SimpleOptimiserVM" y "SimpleOptimiserM", respectivamente. Como podemos ver en el diagrama de clases, el ViewModel de los ajustes del optimizador pertenece de forma indivisible a la parte gráfica, y por eso se relaciona con ella con una conexión de "Composición". La propia parte de View pertenece de forma indivisible al optimizador, y por eso se relaciona con la clase "Manager" mediante una conexión de "Composición". La parte del modelo de datos de los ajustes del optimizador pertenece tanto al optimizador, como a ViewModel, y por eso tiene una conexión de "Agregación" tanto con el primero, como con el segundo. Esto se ha hecho así para que el optimizador tenga acceso a los ajustes guardados en el modelo de datos del gráfico de ajustes del optimizador.      

Tras analizar los diagramas de las clases, así como los procesos de interrelación entre estas, finalizaremos el presente capítulo con un diagrama de secuencias que muestra el proceso de instanciación de los objetos analizados.


Este diagrama se lee de arriba hacia abajo: el punto de partida del proceso representado será el punto Instance, que muestra el momento de inicio de la aplicación y la instanciación de la capa gráfica de la ventana principal del optimizador. En el momemnto de la instanciación, la interfaz gráfica instancia la clase "SimpleOptimiserVM", dado que esta se declara como DataContext de la ventana principal. Al instanciar la clase "SimpleOptimiserVM", llama a la propiedad estática "MainModelCreator.Model", que a su vez genera el objeto "MainModel" y lo convierte en el tipo de interfaz IMainModel.

Al instanciar la clase MainModel, se crea una lista con los constructores de los optimizadores. Precisamente esta lista vemos en la ComboBox de selección del optimizador necesario. Tras instanciar el modelo de datos, se llama al constructor de la clase SimpleOptimiserVM, que llama al método "ChangeOptimiser" del modelo de datos presentado con el tipo de interfaz IMainModel. El método "ChangeOptimiser" llama al método Create() en el constructor elegido de los optimizadores. Dado que ya hemos analizado el inicio de la aplicación, el constructor seleccionado del optimizador será el primero de la lista indicada. Llamando al método Create en el constructor que nos interesa del optimizador, delegaremos en el constructor la creación del tipo de optimizador concreto que hemos seleccionado, lo cual precisamente hace, retornando el objeto de optimizador convertido al tipo de interfaz y transmitiéndolo al modelo de datos, donde se guarda en la propiedad correspondiente. Después de ello, finaliza el funcionamiento del método ChangeOptimiser, y regresamos al constructor de la clase SimpleOptimiserVM.

La clase Model y la parte lógica del programa.

Una vez analizada en el capítulo anterior la estructura general de la aplicación resultante, así como el proceso de generación de los objetos principales en el momento de inicio de la aplicación, merece la pena pasar a los detalles de implementación de su lógica. Todos los objetos que describen la lógica de la aplicación creada se ubican en el directorio "Model". En la carpeta raíz del directorio, se encuentra el archivo "MainModel.cs", en el que se ubica la clase del modelo de datos que constituye el punto de inicio para lanzar nuestra propia lógica comercial en la aplicación. Su lógica cuenta con más de 1000 líneas de código, por lo que, claro está, nuestras explicaciones tocarán la implementación de métodos selectos, no la clase al completo. No obstante, dado que esta se hereda de la interfaz IMainModel, para mostrar su estructura, representaremos solo el código de la interfaz mencionada.

/// <summary>
/// Interfaz del modelo de datos de la ventana principal del optimizador
/// </summary>    
interface IMainModel : INotifyPropertyChanged
{
    #region Getters
    /// <summary>
    /// Optimizador seleccionado
    /// </summary>
    IOptimiser Optimiser { get; }
    /// <summary>
    /// Lista de nombres de los terminales instalados en la computadora
    /// </summary>
    IEnumerable<string> TerminalNames { get; }
    /// <summary>
    /// Lista de nombres de los optimizadores disponibles para su uso
    /// </summary>
    IEnumerable<string> OptimisatorNames { get; }
    /// <summary>
    /// Lista de nombres de los directorios con las optimizaciones guardadas (Data/Reperts/*)
    /// </summary>
    IEnumerable<string> SavedOptimisations { get; }
    /// <summary>
    /// Estructura con todas las pasadas de los resultados de optimización
    /// </summary>
    ReportData AllOptimisationResults { get; }
    /// <summary>
    /// Test forward
    /// </summary>
    List<OptimisationResult> ForwardOptimisations { get; }
    /// <summary>
    /// Tests históricos
    /// </summary>
    List<OptimisationResult> HistoryOptimisations { get; }
    #endregion

    #region Events
    /// <summary>
    /// Evento de valor atípico del error del modelo de datos
    /// </summary>
    event Action<string> ThrowException;
    /// <summary>
    /// Evento de pausa de la optimización
    /// </summary>
    event Action OptimisationStoped;
    /// <summary>
    /// Evento de actualización de la barra de progreso del modelo de datos
    /// </summary>
    event Action<string, double> PBUpdate;
    #endregion

    #region Methods
    /// <summary>
    /// Método que carga los resultados de optimización anteriormente guardados
    /// </summary>
    /// <param name="optimisationName">Nombre del informe necesario</param>
    void LoadSavedOptimisation(string optimisationName);
    /// <summary>
    /// Método que cambia el terminal anteriormente seleccionado
    /// </summary>
    /// <param name="terminalName">ID del terminal solicitado</param>
    /// <returns></returns>
    bool ChangeTerminal(string terminalName);
    /// <summary>
    /// Método de cambio de optimizador
    /// </summary>
    /// <param name="optimiserName">Nombre del optimizador</param>
    /// <param name="terminalName">Nombre del terminal</param>
    /// <returns></returns>
    bool ChangeOptimiser(string optimiserName, string terminalName = null);
    /// <summary>
    /// Inicio de la optimización
    /// </summary>
    /// <param name="optimiserInputData">Datos de entrada para el inicio de las optimizaciones</param>
    /// <param name="IsAppend">Signo que indica si hay que completar las descargas existentes (si existen estas) o sobrescribirlas</param>
    /// <param name="dirPrefix">Prefijo del directorio con las optimizaciones</param>
    void StartOptimisation(OptimiserInputData optimiserInputData, bool IsAppend, string dirPrefix);
    /// <summary>
    /// Pausa de la optimización desde el exterior (por parte del usuario)
    /// </summary>
    void StopOptimisation();
    /// <summary>
    /// Obtener los parámetros del robot
    /// </summary>
    /// <param name="botName">Nombre del experto</param>
    /// <param name="isUpdate">Signo que indica si hay que actualizar el archivo con los parámetros antes de su lectura</param>
    /// <returns>Lista de parámetros</returns>
    IEnumerable<ParamsItem> GetBotParams(string botName, bool isUpdate);
    /// <summary>
    /// Guardando en un archivo (*.csv) las optimizaciones seleccionadas
    /// </summary>
    /// <param name="pathToSavingFile">Ruta al archivo guardado</param>
    void SaveToCSVSelectedOptimisations(string pathToSavingFile);
    /// <summary>
    /// Guardando en un archivo (*.csv) las optimizaciones en la fecha transmitida
    /// </summary>
    /// <param name="dateBorders">Límites de las fechas</param>
    /// <param name="pathToSavingFile">Ruta al archivo guardado</param>
    void SaveToCSVOptimisations(DateBorders dateBorders, string pathToSavingFile);
    /// <summary>
    /// Iniciando el proceso de simulación
    /// </summary>
    /// <param name="optimiserInputData">Lista con los parámetros de ajuste del simulador</param>
    void StartTest(OptimiserInputData optimiserInputData);
    /// <summary>
    /// Iniciando el proceso de clasificación de los resultados
    /// </summary>
    /// <param name="borders">Límites de las fechas</param>
    /// <param name="sortingFlags">Matriz con los nombres de los parámetros para la clasificación</param>
    void SortResults(DateBorders borders, IEnumerable<SortBy> sortingFlags);
    /// <summary>
    /// Filtrando los resultados de la optimización
    /// </summary>
    /// <param name="borders">Límites de las fechas</param>
    /// <param name="compareData">Banderas de filtrado de datos</param>
    void FilterResults(DateBorders borders, IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData);
    #endregion
}

Al estudiar la interfaz mostrada, podemos percibir en ella los límites de sus componenetes usando la directiva #region. Estos sirven para dividir en componentes de tipo cada uno de los miembros de esta interfaz. Como podemos ver, posee una serie de propiedades que muestran información diversa sobre los datos de los campos regulados por el modelo en la interfaz gráfica. No obstante, son solo getters que limitan el acceso a los datos a su lectura, sin posibilidad de sobrescribir el propio objeto leído. Se ha hecho así para no perjudicar casualmente desde ViewModel la lógica de funcionamiento del modelo de datos. Lo primero interesante de las propiedades de esta interfaz son las listas con los resultados de las optimizaciones:

  • AllOptimisationResults
  • ForwardOptimisations
  • HistoryOptimisations

Precisamente en estos campos se encuentra la lista de optimizaciones que vemos en los recuadros en la pestaña Results de nuestra interfaz gráfica. Debemos notar que la lista con todas las pasadas de optimización se encuentra en la estructura "ReportData", creada especialmente para ello:

/// <summary>
/// Estructura que describe los resultados de optimización
/// </summary>
struct ReportData
{
    /// <summary>
    /// Registro con las pasadas de optimización
    /// key - intervalo de fechas
    /// value - lista con las pasadas de optimización en dicho intervalo
    /// </summary>
    public Dictionary<DateBorders, List<OptimisationResult>> AllOptimisationResults;
    /// <summary>
    /// Experto y divisa
    /// </summary>
    public string Expert, Currency;
    /// <summary>
    /// Depósito
    /// </summary>
    public double Deposit;
    /// <summary>
    /// Apalancamiento crediticio
    /// </summary>
    public int Laverage;
}

Esta estructura aparte de los propios datos de optimización, describe los ajustes principales del optimizador, lo cual resulta necesario, en primer lugar, para iniciar los tests (clicando dos veces sobre la pasada optimización elegida), y en segundo lugar, para comprobar los resultados de la optimización cuando se selecciona el modo de adición de nuevos datos a los ya optimizados anteriormente.

Asimismo, el modelo de datos contiene la lista de terminales instalados en la computadora, los nombres de las optimizadores disponibles para su elección (se crea desde los constructures de estos mismos optimizadores) y la lista de optimizaciones anteriormente guardadas (los nombres de los directorios se ubican en la ruta "Data/Reports"). Asimismo, se ofrece acceso al propio optimizador.

Intercambio inverso de información (del modelo al modelo View) se realiza con la ayuda de los eventos a los que se suscribe ViewModel después de instanciarse el modelo de datos. Hay 4 eventos así, 3 de los cuales son de usuario, y uno heredado de la interfaz INotifyPropertyChanged. La herencia de la interfaz INotifyPropertyChanged sobra en el modelo de datos. No obstante, nos pareció cómodo, y por eso existe en la presente implementación de este programa.

Entre los eventos mencionados, merece la pena hablar del evento ThrowException. Inicialmente, se creó solo para transmitir mensajes sobre el error a la parte gráfica de la aplicación y su posterior representación, y es que resulta mejor no controlar directamente el gráfico desde el modelo de datos. No obstante, como consecuencia, ni siquiera lo hemos comenzado a usar para transmitir una serie de mensajes de texto al gráfico desde el modelo de datos. No se trata de mensajes, sino más bien de ciertas alertas de texto. Por eso, pedimos al lector que no se sienta extrañado de encontrar en lo sucesivo alusiones en el código a la transmisión de mensajes que no son errores utilizando este evento. 

Para analizar los métodos del modelo de datos, pasaremos a la propia clase que implementa la parte analizada del programa. 

Lo primero que hace el optimizador al seleccionar un nuevo robot es cargar sus parámetros. El encargado de esta acción es el método "GetBotParams": este método ejecuta dos lógicas posibles. Puede, o bien actualizar el archivo de configuración de los parámetros del robot, o bien simplemente leerlo. Asimismo, puede ser recursivo. 

/// <summary>
/// Obtenener los parámetros para el experto seleccionado
/// </summary>
/// <param name="botName">Nombre del experto</param>
/// <param name="terminalName">Nombre del terminal</param>
/// <returns>Parámetros del experto</returns>
public IEnumerable<ParamsItem> GetBotParams(string botName, bool isUpdate)
{
    if (botName == null)
        return null;

    FileInfo setFile = new FileInfo(Path.Combine(Optimiser
                                   .TerminalManager
                                   .TerminalChangeableDirectory
                                   .GetDirectory("MQL5")
                                   .GetDirectory("Profiles")
                                   .GetDirectory("Tester")
                                   .FullName, $"{Path.GetFileNameWithoutExtension(botName)}.set"));


    try
    {
        if (isUpdate)
        {
            if (Optimiser.TerminalManager.IsActive)
            {
                ThrowException("Wating for closing terminal");
                Optimiser.TerminalManager.WaitForStop();
            }
            if (setFile.Exists)
                setFile.Delete();

            FileInfo iniFile = terminalDirectory.Terminals
                                                .First(x => x.Name == Optimiser.TerminalManager.TerminalID)
                                                .GetDirectory("config")
                                                .GetFiles("common.ini").First();

            Config config = new Config(iniFile.FullName);

            config = config.DublicateFile(Path.Combine(workingDirectory.WDRoot.FullName, $"{Optimiser.TerminalManager.TerminalID}.ini"));

            config.Tester.Expert = botName;
            config.Tester.FromDate = DateTime.Now;
            config.Tester.ToDate = config.Tester.FromDate.Value.AddDays(-1);
            config.Tester.Optimization = ENUM_OptimisationMode.Disabled;
            config.Tester.Model = ENUM_Model.OHLC_1_minute;
            config.Tester.Period = ENUM_Timeframes.D1;
            config.Tester.ShutdownTerminal = true;
            config.Tester.UseCloud = false;
            config.Tester.Visual = false;

            Optimiser.TerminalManager.WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized;
            Optimiser.TerminalManager.Config = config;

            if (Optimiser.TerminalManager.Run())
                Optimiser.TerminalManager.WaitForStop();

            if (!File.Exists(setFile.FullName))
                return null;

            SetFileManager setFileManager = new SetFileManager(setFile.FullName, false);
            return setFileManager.Params;
        }
        else
        {
            if (!setFile.Exists)
                return GetBotParams(botName, true);

            SetFileManager setFileManager = new SetFileManager(setFile.FullName, false);
            if (setFileManager.Params.Count == 0)
                return GetBotParams(botName, true);

            return setFileManager.Params;
        }
    }
    catch (Exception e)
    {
        ThrowException(e.Message);
        return null;
    }
}

Al inicio del método, creamos un representación orientada a objetos del archivo analizado con los parámetros del robot, utilizando para ello la clase FileInfo incluida en la biblioteca estándar del lenguaje C#. De acuerdo con los ajustes estándar del terminal, este archivo se guarda en el directorio MQL5/Profiles/Tester/{nombre del robot seleccionado}.set. Precisamente esta ruta se establece al crear la representación orientada a objetos del archivo. Las acciones posteriores se convierten en una construcción de try-catch, debido a que existe el riesgo de que surjan valores atípicos durante las operaciones con el archivo. Ahora, dependiendo del parámetro isUpdate transmitido, de ejecuta una de las ramas lógicas posibles. Si isUpdate = true, significará que debemos actualizar el archivo con los ajustes, reseteando con ello sus valores hasta los valores por defecto y leyendo sus parámetros. Precisamente esta rama lógica se ejecuta cuando pulsamos el botón "Update (*.set) file" en la parte gráfica de nuestra aplicación. La forma más cómoda de actualizar el archivo con los ajustes del experto consiste en generarlo de nuevo.

La generación del archivo se realiza con el simulador de estrategias, si el archivo no se encuentra en el simulador al seleccionar el robot. Por consiguiente, lo único que necesitamos es iniciar el simulador, eliminando previamente este archivo, esperar la generación del archivo, cerrar el simulador, leer el archivo y retornar su valor. Primero, comprobamos si se ha iniciado el terminal: si se ha iniciado, mostramos el mensaje correspondiente y esperamos a que se finalice, y después comprobamos si existe o no un archivo con los parámetros; si existe, lo eliminamos.

Acto seguido, rellenamos el archivo de configuración para iniciar el terminal, usando la ya conocida clase Config, que tantas veces hemos mencionado y analizado en artículos anteriores. Merece la pena hacer hincapié en las fechas registradas en el archivo de configuración. Iniciamos el test en el temrinal, pero indicamos la fecha de inicio de la prueba como 1 día antes de la finalización de la misma. Gracias a esto, el simulador se inicia y genera el archivo con los ajustes que necesitamos; a continuación, no sabiendo iniciar el test, se cierra de inmediato y pasamos a la lectura del archivo. Después de crear y preparar el archivo de configuración, usando la clase TerminalManager, se inicia el proceso de generación del archivo de ajustes, que ya hemos analizado anteriormente. Después de finalizar la generación, nosotros, usando la clase SetFileManager (que también hemos visto con anterioridad), leemos el achivo con ajustes y retornamos su contenido.

Si necesitamos otra rama lógica que no presuponga una generación explícita del archivo de ajustes, recurriremos a la segunda parte de la condición. Aquí, este método, o bien lee directamente el archivo con los ajustes del experto y retorna su contenido, o bien se da el inicio recursivo de este método, pero con el parámetro isUpdate = true y, como consecuencia, se ejecuta la parte de la lógica que ya hemos analizado.

El siguiente método que nos interesa es "StartOptimisation":

/// <summary>
/// Iniciando optimizaciones
/// </summary>
/// <param name="optimiserInputData">Datos de entrada para el optimizador</param>
/// <param name="isAppend">Bandera - ¿hay que completar el archivo ?</param>
/// <param name="dirPrefix">Prefijo del directorio</param>
public async void StartOptimisation(OptimiserInputData optimiserInputData, bool isAppend, string dirPrefix)
{
    if (string.IsNullOrEmpty(optimiserInputData.Symb) ||
        string.IsNullOrWhiteSpace(optimiserInputData.Symb) ||
        (optimiserInputData.HistoryBorders.Count == 0 && optimiserInputData.ForwardBorders.Count == 0))
    {
        ThrowException("Fill in asset name and date borders");
        OnPropertyChanged("ResumeEnablingTogle");
        return;
    }

    if (Optimiser.TerminalManager.IsActive)
    {
        ThrowException("Terminal already running");
        return;
    }

    if (optimiserInputData.OptimisationMode == ENUM_OptimisationMode.Disabled)
    {
        StartTest(optimiserInputData);
        return;
    }

    if (!isAppend)
    {
        var dir = workingDirectory.GetOptimisationDirectory(optimiserInputData.Symb,
                                                  Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot),
                                                  dirPrefix, Optimiser.Name);
        List<FileInfo> data = dir.GetFiles().ToList();
        data.ForEach(x => x.Delete());
        List<DirectoryInfo> dirData = dir.GetDirectories().ToList();
        dirData.ForEach(x => x.Delete());
    }

    await Task.Run(() =>
    {
        try
        {
            DirectoryInfo cachDir = Optimiser.TerminalManager.TerminalChangeableDirectory
                                                     .GetDirectory("Tester")
                                                     .GetDirectory("cache", true);
            DirectoryInfo cacheCopy = workingDirectory.Tester.GetDirectory("cache", true);
            cacheCopy.GetFiles().ToList().ForEach(x => x.Delete());
            cachDir.GetFiles().ToList()
                   .ForEach(x => x.MoveTo(Path.Combine(cacheCopy.FullName, x.Name)));

            Optimiser.ClearOptimiser();
            Optimiser.Start(optimiserInputData,
                Path.Combine(terminalDirectory.Common.FullName,
                $"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}_Report.xml"), dirPrefix);
        }
        catch (Exception e)
        {
            Optimiser.Stop();
            ThrowException(e.Message);
        }
    });
}

Este método es asincrónico, y ha sido escrito usando la tecnología async await, que permite declarar los métodos asincrónicos de una forma menos aparatosa. En primer lugar, comprobamos el nombre transmitido del símbolo y los límites de las fechas de las optimizaciones. Si no existieran estas, desbloqueamos la interfaz gráfica bloqueada (al iniciar la optimización, se bloquea una serie de botones de la interfaz gráfica) y mostramos un mensaje marcando el error; a continuación, se finaliza la ejecución de la función. Justo de la misma forma reaccionaríamos si el terminal ya hubiera sido iniciado. Si se ha elegido el modo de prueba, y no el de optimización, redirigimos la ejecución del proceso hacia el método que inicia el test.

Si se ha seleccionado el modo de adición de datos (Append), eliminamos todos los archivos en el directorio con las optimizaciones, así como todos los directorios incluidos en él; después, procedemos a iniciar la optimizaición. El proceso de optimización se inicia de forma asincrónica, para no bloquear la interfaz gráfica durante la ejecución de esta tarea, y también se convierte en una construcción de try-catch, por si surgen errores. Antes de comenzar el proceso, como ya hemos mencionado antes, tomamos todos los archivos con la caché de las optimizaciones realizadas anteriormente y lo copiamos en el directorio temporal creado en el directorio de trabajo Data del optimizador automático. Esto es necesario para que las optimizaciones se inicien incluso si se han realizado antes. A continuación, limpiamos el optimizador de todos los datos anteriormente registrados en las variables locales del optimizador e iniciamos el proceso de optimización. Uno de los parámetros de inicio de las optimizaciones es la ruta al archivo con el informe, formado el robot. Como ya mencionamos en el artículo № 3, el informe se forma con un nombre {nombre del robot}_Report.xml, en el optimizador automático, este nombre se establece con la siguiente línea:

$"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}_Report.xml")

Esto se realiza mediante una concatenación de líneas, donde el nombre del robot se forma a partir de la ruta hasta el robot indicada como uno de los parámetros del archivo de optimización. El proceso de detención de la optimización al completo se transfiere a la clase de optimizador, mientras que el método que lo implementa, simplemente llama al método "StopOptimisation" en el ejemplar de la clase del optimizador.

/// <summary>
/// Finalizamos la optimización desde el exterior del optimizador
/// </summary>
public void StopOptimisation()
{
    Optimiser.Stop();
}

El inicio de las pruebas que hemos mencionado anteriormente se realiza con el método implementado en la clase del modelo de datos, y no en el optimizador.

/// <summary>
/// Iniciando pruebas
/// </summary>
/// <param name="optimiserInputData">Datos de entrada para el simulador</param>
public async void StartTest(OptimiserInputData optimiserInputData)
{
    // Comprobando si el terminal está iniciado
    if (Optimiser.TerminalManager.IsActive)
    {
        ThrowException("Terminal already running");
        return;
    }

    // Estableciendo el intervalo de fechas
    #region From/Forward/To
    DateTime Forward = new DateTime();
    DateTime ToDate = Forward;
    DateTime FromDate = Forward;

    // Comprobando la cantidad de fechas transmitidas. Como máximo, una histórica y una forward
    if (optimiserInputData.HistoryBorders.Count > 1 ||
        optimiserInputData.ForwardBorders.Count > 1)
    {
        ThrowException("For test there mast be from 1 to 2 date borders");
        OnPropertyChanged("ResumeEnablingTogle");
        return;
    }

    // Si se han transmitido tanto una fecha histórica como una forward
    if (optimiserInputData.HistoryBorders.Count == 1 &&
        optimiserInputData.ForwardBorders.Count == 1)
    {
        // Ponemos a prueba si el intervalo establecido es correcto
        DateBorders _Forward = optimiserInputData.ForwardBorders[0];
        DateBorders _History = optimiserInputData.HistoryBorders[0];

        if (_History > _Forward)
        {
            ThrowException("History optimisation mast be less than Forward");
            OnPropertyChanged("ResumeEnablingTogle");
            return;
        }

        // Registrando las fechas
        Forward = _Forward.From;
        FromDate = _History.From;
        ToDate = (_History.Till < _Forward.Till ? _Forward.Till : _History.Till);
    }
    else // Si se ha transmitido solo la fecha forward o la fecha histórica
    {
        // Las guardamos y consideramos que era una fecha forward (incluso si se ha transmitido una en tiempo real)
        if (optimiserInputData.HistoryBorders.Count > 0)
        {
            FromDate = optimiserInputData.HistoryBorders[0].From;
            ToDate = optimiserInputData.HistoryBorders[0].Till;
        }
        else
        {
            FromDate = optimiserInputData.ForwardBorders[0].From;
            ToDate = optimiserInputData.ForwardBorders[0].Till;
        }
    }
    #endregion

    PBUpdate("Start test", 100);

    // Iniciando la prueba en el flujo secundario
    await Task.Run(() =>
    {
        try
        {
            // Creando un archivo con los ajustes del experto
            #region Create (*.set) file
            FileInfo file = new FileInfo(Path.Combine(Optimiser
                                             .TerminalManager
                                             .TerminalChangeableDirectory
                                             .GetDirectory("MQL5")
                                             .GetDirectory("Profiles")
                                             .GetDirectory("Tester")
                                             .FullName, $"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}.set"));

            List<ParamsItem> botParams = new List<ParamsItem>(GetBotParams(optimiserInputData.RelativePathToBot, false));

            // Rellenando los ajustes del experto con aquellos que han sido introducidos en la interfaz gráfica
            for (int i = 0; i < optimiserInputData.BotParams.Count; i++)
            {
                var item = optimiserInputData.BotParams[i];

                int ind = botParams.FindIndex(x => x.Variable == item.Variable);
                if (ind != -1)
                {
                    var param = botParams[ind];
                    param.Value = item.Value;
                    botParams[ind] = param;
                }
            }

            // Guardando los ajsutes en un archivo
            SetFileManager setFile = new SetFileManager(file.FullName, false)
            {
                Params = botParams
            };
            setFile.SaveParams();
            #endregion

            // Creando el archivo de configuración del terminal
            #region Create config file
            Config config = new Config(Optimiser.TerminalManager
                                                .TerminalChangeableDirectory
                                                .GetDirectory("config")
                                                .GetFiles("common.ini")
                                                .First().FullName);
            config = config.DublicateFile(Path.Combine(workingDirectory.WDRoot.FullName, $"{Optimiser.TerminalManager.TerminalID}.ini"));

            config.Tester.Currency = optimiserInputData.Currency;
            config.Tester.Deposit = optimiserInputData.Balance;
            config.Tester.ExecutionMode = optimiserInputData.ExecutionDelay;
            config.Tester.Expert = optimiserInputData.RelativePathToBot;
            config.Tester.ExpertParameters = setFile.FileInfo.Name;
            config.Tester.ForwardMode = (Forward == new DateTime() ? ENUM_ForvardMode.Disabled : ENUM_ForvardMode.Custom);
            if (config.Tester.ForwardMode == ENUM_ForvardMode.Custom)
                config.Tester.ForwardDate = Forward;OnPropertyChanged("StopTest");
            else
                config.DeleteKey(ENUM_SectionType.Tester, "ForwardDate");
            config.Tester.FromDate = FromDate;
            config.Tester.ToDate = ToDate;
            config.Tester.Leverage = $"1:{optimiserInputData.Laverage}";
            config.Tester.Model = optimiserInputData.Model;
            config.Tester.Optimization = ENUM_OptimisationMode.Disabled;
            config.Tester.Period = optimiserInputData.TF;
            config.Tester.ShutdownTerminal = false;
            config.Tester.Symbol = optimiserInputData.Symb;
            config.Tester.Visual = false;
            #endregion

            // Configurando e iniciando el terminal
            Optimiser.TerminalManager.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
            Optimiser.TerminalManager.Config = config;
            Optimiser.TerminalManager.Run();

            // Esperando el cierre del terminal
            Optimiser.TerminalManager.WaitForStop();
        }
        catch (Exception e)
        {
            ThrowException(e.Message);
        }

        OnPropertyChanged("StopTest");
    });
}

Después de comprobar de la forma acostumbrada si el terminal está o no iniciado, procedemos a establecer las fechas para los periodos histórico y forward para las pruebas. Para estas, solo puede establecerse un intervalo histórico, o bien histórico y forward. Si en los ajustes hemos establecido solo un interavalo forward, este se igualará a uno histórico. En primer lugar, se actualizan las variables que guardan las fechas de la prueba (la fecha forward, la última, la fecha inicial de la prueba). A continuación, comprobamos el método: si se ha transmitido más de un límite de pruebas históricas o más de un límite de pruebas forward, mostraremos un mensaje sobre el error. A continuación, establecemos los límites: el sentido de esta condición reside en establecer las 4 fechas transmitidas (o 2, si solo se ha establecido el periodo histórico) entre las tres variables declaradas.

  • FromDate — es igual a la menor de las fechas transmitidas,
  • ToDate — es igual a la mayor de las fechas transmitidas,
  • Forward — es igual a la menor de las fechas forward.

El inicio de la prueba es igualmente convertido en una construcción de try-catch. En primer lugar, generamos un archivo con los ajustes del robot y lo rellenamos con los ajustes transmitidos del robot. Esto se realiza con la ayuda del objeto SetFileManager, que ya hemos analizado anteriormente. A continuación, creamos el archivo de configuración de acuerdo con las instrucciones e iniciamos el proceso de prueba. Después, esperamos la apertura del terminal. Al finalizar el funcionamiento del método, comunicamos al gráfico que la prueba ha terminado: es necesario hacer esto a través de un evento, dado que este método es asincrónico, y por eso, después de llamarlo, el programa sigue ejecutándose sin esperar la finalización del método llamado.

Volviendo al proceso de optimización, merece la pena destacar que el optimizador notifica de la misma manera al modelo de datos sobre la interrupción del proceso de optimización: a través del evento de finalización del proceso de optimización. Analizaremos esto con más detalle en el artículo final.

Conclusión

En los artículos anteriores, hemos descrito con detalle tanto el poroceso de ensamblaje de los algoritmos con el optimizador automático creado, como varias de sus partes. Hemos analizado la lógica de trabajo con los informes de optimización, y después hemos mostrado su aplicación en los algortimos comerciales. Asimismo, en el artículo precedente, hemos estudiado la interfaz gráfica (parte View del programa) y la estructura de los archivos del proyecto.

El presente artículo está dedicado a la estructura interna del proyecto, a las interrelaciones de las clases entre sí y al proceso de inicio de las optimizaciones desde el punto de vista del programa. Dado que el programa da soporte a la posibilidad de escribir multitud de lógicas para los optimizadores, no hemos descrito aquí el proceso detallado de implementación de la lógica, puesto que será mejor describirlo en un artículo aparte como ejemplo de la implementación del optimizador; y es que, a buen seguro, todos los lectores tienen sus propias ideas sobre cómo implementar este proceso de una forma más clara y bonita. Nos esperan dos artículos más, donde analizaremos el ensamblaje de la parte lógica analizada en el presente artículo con la parte gráfica. Asimismo, describiremos el algoritmo de implementación de los optimizadores y daremos un ejemplo de su implementación.

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

Para aquellos lectores que aún no han trabajado con Visual Studio, vamos a describir el proceso de compilación. Podrán compilar el proyecto después de abrirlo en VisualStudio con una amplia gama de métodos, aquí tienen 3 de ellos:

  1. El más sencillo consiste en pulsar la combinación de teclas CTRL+SHIFT+B,
  2. Otro más visual consiste en pulsar la flecha verde en el editor: se iniciará la aplicación en el modo de depuración del código, pero también tendrá lugar la compilación (funcionará sin problemas solo si está seleccionado el modo de compilación Debug),
  3. Otra opción sería desde el punto Build del menú desplegable.

Después, en la carpeta MetaTrader Auto Optimiser/bin/Debug (o MetaTrader Auto Optimiser/bin/Release, depende del tipo de ensamblaje), aparecerá el programa compilado.

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

Archivos adjuntos |
Auto_Optimiser.zip (125.7 KB)
El lenguaje MQL como medio de marcado de la interfaz gráfica de programas MQL (Parte 3). Diseñador de formas El lenguaje MQL como medio de marcado de la interfaz gráfica de programas MQL (Parte 3). Diseñador de formas

En este artículo, finalizaremos la descripción del nuevo concepto para la construcción de la interfaz de ventana de los programas MQL con la ayuda de las construcciones del lenguaje MQL. El editor gráfico especial permitirá ajustar de forma interactiva una disposición formada por las clases básicas de elementos de GUI, y después exportarla a una descripción MQL para usarla en nuestro proyecto MQL. Asimismo, presentamos la construcción interna del editor y las instrucciones para el usuario. Los códigos fuente se adjuntan al final del artículo.

Trabajando con las series temporales en la biblioteca DoEasy (Parte 39): Indicadores basados en la biblioteca - Preparación de datos y eventos de la series temporales Trabajando con las series temporales en la biblioteca DoEasy (Parte 39): Indicadores basados en la biblioteca - Preparación de datos y eventos de la series temporales

En el presente artículo, analizaremos la aplicación de la biblioteca DoEasy para crear indicadores de periodo y símbolo múltiples. Hoy, vamos a preparar las clases de la biblioteca para trabajar con indicadores y poner a prueba la correcta creación de series temporales para su posterior uso como fuentes de datos en los indicadores. Asimismo, organizaremos la creación y el envío de los eventos de series temporales.

Creando un EA gradador multiplataforma: simulación del asesor multidivisa Creando un EA gradador multiplataforma: simulación del asesor multidivisa

En un solo mes, los mercados han caído más de un 30%. ¿Acaso no se trata del mejor momento para simular asesores basados en cuadrículas y martingale? Este artículo es una continuación de la serie de artículos "Creando un EA gradador multiplataforma" cuya publicación, en principio, no estaba planeada. Pero, si el propio mercado nos ofrece la posibilidad de organizar un test de estrés para el asesor gradador, ¿por qué no aprovechar la oportunidad? Pongámonos manos a la obra.

Trabajando con las series temporales en la biblioteca DoEasy (Parte 40): Indicadores basados en la biblioteca - actualización de datos en tiempo real Trabajando con las series temporales en la biblioteca DoEasy (Parte 40): Indicadores basados en la biblioteca - actualización de datos en tiempo real

En el artículo, vamos a analizar la creación de un indicador multiperiodo basado en la biblioteca DoEasy. Asimismo, vamos a mejorar las clases de las series temporales para obtener los datos de cualquier marco temporal y representarlos en el periodo actual del gráfico.