Optimización móvil continua (Parte 7): Encajando la parte lógica del optimizador automático con la parte gráfica y el control de la misma desde el programa

5 agosto 2020, 10:29
Andrey Azatskiy
0
1 693

En este artículo, el penúltimo ya de la serie, analizaremos la conexión lógica del programa con su representación gráfica. Asimismo, profundizaremos en el proceso de inicio de las optimizaciones desde su comienzo, y seguiremos paso a paso todas las etapas hasta la clase del optimizador automático. Además, analizaremos el mecanismo encargado de encajar la lógica del programa y su representación, mostrando igualmente los modos de gestión de la parte gráfica desde el código de la aplicación. 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
  6. Optimización móvil continua (Parte 6): La lógica del optimizador automático y su estructura 

La clase ViewModel y su interacción con la capa gráfica

Como hemos dicho más de una vez, ViewModel es la capa encargada de encajar la parte gráfica de la aplicación y la implementación programática de la lógica. En esencia, es una representación programática de la parte gráfica, donde se implementan la invoación de la lógica y la reacción de la parte gráfica a las llamadas de retorno por parte de la lógica de la aplicación. Por consiguiente, a cada campo modificado de la parte gráfica de la aplicación le corresponde una propiedad pública de una parte de ViewModel. Estas propiedades pueden ser tanto getters (lo cual excluye la posibilidad de modificar estas desde el gráfico), como setters (lo cual permite registrar nuevamente el objeto que se oculta tras esta propiedad). Ya hemos analizado detalladamente en los anteriores artículos la tecnología encargada de conectar los datos, por ello, aquí solo mostraremos un par de ejemplos. 

La conexión de los campos de texto se realiza con la ayuda de las propiedades con acceso, tanto para el registro, como para la lectura de datos. Analizaremos como ejemplo el campo en el que se indica el nombre del activo con el que se realizará la optimización. El marcado XAML de este campo es muy sencillo.

<TextBox Width="100"          IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"          Text="{Binding AssetName}"/>

Aparte de la indicación de la anchura de la ventana, podemos ver los campos IsEnabled y Text. El primero de ellos se encarga de la accesibilidad del campo para la edición. Si este se ha establecido en true, el campo estará disponible para la edición, si ha establecido en false, estará bloqueado. La propiedad Text contiene el propio texto introducido en este campo. A continuación, frente a cada uno de ellos, vemos una construcción entre llaves. Todo lo que se encuentra en ella, implementa la conexión del objeto con una propiedad pública determinada de la clase ViewModel, indicada tras el parámetro "Binding".

A continuación, podrían seguir un par más de parámetros, por ejemplo, el parámetro UpdateSourceTrigger indica al modo de actualización de la parte gráfica de esta aplicación, y puede ser igual a una serie de valores. Concretamente, el valor indicado en nuestro ejemplo (PropertyChanged), indica que la parte gráfica se actualizará solo al activarse el evento OnPropertyChanged de la clase ViewModel con el nombre transmitido e indicado tras el parámetro Binding (en nuestro ejemplo, se trata de "EnableMainTogles").

Asimismo, merece la pena notar que si el parámetro Text no está relacionado con una línea, sino, por ejemplo, con el parámetro double, solo podremos introducir cifras. Si conectamos este parámetro con el tipo int, no podremos introducir otra cosa que números enteros. En otras palabras, esta tecnología también ayuda a establecer límites sobre el tipo de valor introducido.

Si analizamos la parte de ViewModel, los campos que nos interesan se mostrarán de la forma que sigue:

Parámetro IsEnabled:

/// <summary> /// Si se trata de un conmutador = false, los campos más importantes no estarán disponibles /// </summary> public bool EnableMainTogles { get; private set; } = true;

y el parámetro Text:

/// <summary> /// Nombre del activo seleccionado para la simulación / optimización /// </summary> public string AssetName { get; set; }

Como podemos ver, ambos tienen acceso tanto al registro, como a la lectura de datos. La única diferencia es que la propiedad EnableMainTogles ofrece acceso al registro solo desde la clase AutoOptimiserVM (es decir, desde sí misma), por eso, no podremos cambiarla desde el exterior de ninguna forma.

Si analizamos alguna colección de datos, por ejemplo, la lista de resultados de las optimizaciones forward, le corresponderá la propiedad que contiene la lista de valores. Para ser más concretos, vamos a analizar un recuadro con los resultados de las pasadas forward:

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

Como podemos ver por el marcado, el recuadro del tipo ListView supone una indicación de la propia clase del recuadro. Después va la creación de la cuadrícula en la que se guardarán los datos, y finalmente, las columnas con los datos. Cuando hemos dicho que se crea la indicación de la clase, nos referíamos precisamente a la clase ListView. Y es que este marcado XAML, bastante sencillo a primera vista, oculta un mecanismo bastante meditado y complejo que permite tanto describir las clases como operar con los objetos de estas clases, usando el lenguaje de marcado. Todos los campos que conectamos con la clase AutoOptimiserVM, no son otra cosa en realidad que propiedades de estas clases. Incluso en el ejemplo mostrado con el recuadro, estamos usando 3 clases:

  • ListView — System.Windows.Controls.ListView.
  • GridView — System.Windows.Controls.GridView, que es heredera de la clase System.Windows.Controls.ViewBase, lo que permite usarla como clase que inicia la propiedad View de la clase ListView.
  • GridViewColumn — System.Windows.Controls.GridViewColumn.

La propiedad ItemsSource de la clase ListView indica la colección de elementos de la consta el recuadro. Después de vincular esta propiedad con la colección de ViewModel, tendremos una especie de DataContext para la clase Window, pero en el marco del recuadro analizado. Como estamos analizando el recuadro, la colección que lo forma deberá componerse de clases que tengan sus propiedades públicas para cada una de las columnas. Ahora, tras vincular la propiedad ItemsSource a la propiedad de ViewModel que forma el recuadro con los datos, podremos vincular cada una de las columnas con el valor buscado de la columna de este recuadro. Asimismo, el recuadro tiene una conexión entre la propiedad SelectedIndex y la propiedad SelectedForwardItem de ViewModel. Esto es necesario para que ViewModel sepa qué línea precisamente ha seleccionado el usuario en el recuadro presentado.

Si echamos un vistazo a la parte de ViewModel, la propiedad con la que conectamos el recuadro presentado se ha implementado de la forma siguiente:

/// <summary> /// Simulaciones forward seleccionadas /// </summary> public ObservableCollection<ReportItem> ForwardOptimisations { get; } = new ObservableCollection<ReportItem>();

Como hemos mencionado anteriormente, la clase ObservableCollection de la biblioteca estándar del lenguaje C# es un objeto que comunica por sí mismo al gráfico las modificaciones sucedidas. Esto sucede así porque esta clase ya posee el evento mecionado, y llama a este cada vez que se actualiza la lista con sus elementos. En lo demás, se trata de una colección de datos bastante estándar.

En lo que respecta a la propiedad SelectedForwardItem, esta cumple varios papeles: guarda los datos sobre la línea seleccionada en el recuadro y ejerce de llamada de retorno de la selección de línea.

/// <summary> /// Pasada forward seleccionada /// </summary> private int _selectedForwardItem; public int SelectedForwardItem {     get => _selectedForwardItem;     set     {         _selectedForwardItem = value;         if (value > -1)         {             FillInBotParams(model.ForwardOptimisations[value]);             FillInDailyPL(model.ForwardOptimisations[value]);             FillInMaxPLDD(model.ForwardOptimisations[value]);         }     } } 

Dado que esta propiedad se usa como llamada de retorno, lo cual supone que tenemos que indicar la reacción al establecimiento del valor (en nuestro ejemplo), el setter deberá contener la implementación de esta reacción y actuar asimismo como función. Debido a esta peculiaridad, guardaremos el valor de la propiedad en el private de la variable. Para obtener de ella los valores, la invocaremos directamente desde el getter. Para establecer los valores, le asignamos en el setter un valor guardado con el nombre value. La variable value no ha sido titulada en ninguna parte, se trata de un cierto alias para el valor establecido, previsto por el lenguaje C#. Si value es superior a -1, en la pestaña Results rellenaremos el resto de recuadros relacionados que hayan sido actualizados según la línea seleccionada. Se trata de los recuadros con los parámetros del robot, el beneficio medio, las pérdidas en el día de la semana y los valores máximos/mínimos de PL. La comprobación en la condición if se realiza por el siguiente moitivo: si el índice del elemento seleccionado es igual a -1, significará que el recuadro está vacío y, por consiguiente, no convendrá rellenar los recuadros relacionados. La implementación de los métodos es bastante trivial, y por eso no vamos a analizarla; no obstante, el lector podrá estudiarla en cualquier momento en el código de la clase AutoOptimiserVM.

Asimismo, mostramos la implementación de la clase que describe la línea del resultado de la optimización.

/// <summary> /// Clase - envoltorio del elemento del informe (para la interfaz gráfica) /// </summary> class ReportItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="item">Elemento</param>     public ReportItem(OptimisationResult item)     {         result = item;     }     /// <summary>     /// Elemento del informe     /// </summary>     private readonly OptimisationResult result;     public DateTime From => result.report.DateBorders.From;     public DateTime Till => result.report.DateBorders.Till;     public double SortBy => result.SortBy;     public double Payoff => result.report.OptimisationCoefficients.Payoff;     public double ProfitFactor => result.report.OptimisationCoefficients.ProfitFactor;     public double AverageProfitFactor => result.report.OptimisationCoefficients.AverageProfitFactor;     public double RecoveryFactor => result.report.OptimisationCoefficients.RecoveryFactor;     public double AverageRecoveryFactor => result.report.OptimisationCoefficients.AverageRecoveryFactor;     public double PL => result.report.OptimisationCoefficients.PL;     public double DD => result.report.OptimisationCoefficients.DD;     public double AltmanZScore => result.report.OptimisationCoefficients.AltmanZScore;     public int TotalTrades => result.report.OptimisationCoefficients.TotalTrades;     public double VaR90 => result.report.OptimisationCoefficients.VaR.Q_90;     public double VaR95 => result.report.OptimisationCoefficients.VaR.Q_95;     public double VaR99 => result.report.OptimisationCoefficients.VaR.Q_99;     public double Mx => result.report.OptimisationCoefficients.VaR.Mx;     public double Std => result.report.OptimisationCoefficients.VaR.Std; }

Esta clase se ofrece solo para mostrar qué aspecto tiene la representación de línea de una pasada de optimización en el código. Cada columna del recuadro analizado está relacionada con la propiedad correspondiente de una instancia concreta de esta clase, mientras que la propia clase no es otra cosa que un envoltorio para la estructura "OptimisationResult", ya analizada en el primer artículo del presente ciclo.

Todas las columnas o eventos del ciclo de alguna línea del recuadro están relacionadas con la propiedad Command de ViewModel, cuyo tipo básico es el tipo ICommand. Ya hemos analizado esta tecnología en los artículos sobre la creación de una interfaz gráfica para la gestión de la optimzición. 

La clase ViewModel y su interacción con el modelo de datos.

Sin apartarnos demasiado del capítulo anterior, comenzaremos a analizar esta parte del artículo por las llamadas de retorno y las pausas de la optimización, combinadas en un solo botón "StartStop". 


La pulsación del botón "StartStop" llama al método _StartStopOptimisation de la clase AutoOptimiserVM. A continuación, tenemos dos alternativas: detener la optimización e iniciar la optimización. En el diagrama podemos ver que, cuando la propiedad IsOptimisationInProcess de la clase del optimizador retorna true, ejecutamos la primera parte de la lógica y solicitamos el método StopOptimisation de la clase del modelo de datos, que, como ya hemos visto antes, redirecciona esta llamada al propio optimizador. Si la optimización no ha sido iniciada, llamamos al método StartOptimisation de la clase del modelo de datos. Este método es asincrónico, por consiguiente, el método Start llamado en la clase del optimizador prosigue su funcionamiento después de que el método _StartStopOptimisation finalice el suyo. 

Ahora que hemos analizado la cadena completa de las llamadas realizadas al iniciar este método, vamos a ver el bloque de código que describe el encaje de las llamadas de estos métodos con la parte gráfica y Model de ViewModel. El marcado gráfico XAML, en sí, es una parte bastante trivial de la aplicación, por lo que no vamos a mostrarla. En lo que respecta a ViewModel, las propiedades y el método encargado de iniciar la optimización son los siguientes:

private void _StartStopOptimisation(object o) {     if (model.Optimiser.IsOptimisationInProcess)     {         model.StopOptimisation();     }     else     {         EnableMainTogles = false;         OnPropertyChanged("EnableMainTogles");         Model.OptimisationManagers.OptimiserInputData optimiserInputData = new Model.OptimisationManagers.OptimiserInputData         {             Balance = Convert.ToDouble(OptimiserSettings.Find(x => x.Name == "Deposit").SelectedParam),             BotParams = BotParams?.Select(x => x.Param).ToList(),             CompareData = FilterItems.ToDictionary(x => x.Sorter, x => new KeyValuePair<CompareType, double>(x.CompareType, x.Border)),             Currency = OptimiserSettings.Find(x => x.Name == "Currency").SelectedParam,             ExecutionDelay = GetEnum<ENUM_ExecutionDelay>(OptimiserSettings.Find(x => x.Name == "Execution Mode").SelectedParam),             Laverage = Convert.ToInt32(OptimiserSettings.Find(x => x.Name == "Laverage").SelectedParam),             Model = GetEnum<ENUM_Model>(OptimiserSettings.Find(x => x.Name == "Optimisation model").SelectedParam),             OptimisationMode = GetEnum<ENUM_OptimisationMode>(OptimiserSettings.Find(x => x.Name == "Optimisation mode").SelectedParam),             RelativePathToBot = OptimiserSettings.Find(x => x.Name == "Available experts").SelectedParam,             Symb = AssetName,             TF = GetEnum<ENUM_Timeframes>(OptimiserSettings.Find(x => x.Name == "TF").SelectedParam),             HistoryBorders = (DateBorders.Any(x => x.BorderType == OptimisationType.History) ?                             DateBorders.Where(x => x.BorderType == OptimisationType.History)                             .Select(x => x.DateBorders).ToList() :                             new List<DateBorders>()),             ForwardBorders = (DateBorders.Any(x => x.BorderType == OptimisationType.Forward) ?                             DateBorders.Where(x => x.BorderType == OptimisationType.Forward)                             .Select(x => x.DateBorders).ToList() :                             new List<DateBorders>()),             SortingFlags = SorterItems.Select(x => x.Sorter)         };         model.StartOptimisation(optimiserInputData, FileWritingMode == "Append", DirPrefix);     } } /// <summary> /// Llamada de retorno de la interfaz gráfica para iniciar la optimización / simulación /// </summary> public ICommand StartStopOptimisation { get; }

Como podemos ver en el código y en el diagrama, este método ha sido dividido en dos ramas de la condición If Else. La primera de ellas detiene el proceso de optimización, con la condición de que el proceso esté iniciado, mientras que la segunda inicia el proceso, en el caso de que se dé la condición opuesta.

Em el momento que se inicia la optimización, bloqueamos los principales campos de la interfaz gráfica estableciendo la condición EnableMainTogles = false, que ya conocemos; después, procedemos a formar los parámetros de entrada. Para iniciar la optimización, necesitaremos crear la estructura OptimistionInputData, que precisamente rellenamos desde las colecciones OptimiserSettings, BotParams, FilterItems, SorterItems y DateBorders. Los valores en las mencionadas estructuras llegan directamente desde la interfaz gráfica a través del mecanismo de conexión de datos que ya hemos analizado. Al finalizar la formación de esta estructura, iniciamos el método StartOptimisation (que ya analizamos) en la instancia de la clase del modelo de datos. La propiedad StartStopOptimisation se establece en el constructor.

// Llamada de retorno del botón de inicio / interrupción del proceso de optimización StartStopOptimisation = new RelayCommand(_StartStopOptimisation);

Dicha propiedad se instancia utilizando una instancia de la clase RelayCommand que implementa la interfaz ICommand, necesaria para conectar los comandos de ViewModel con la propiedad Command de la parte gráfica de la aplicación.

Una vez superadas todas las optimizaciones y formados todos los recuadros en la pestaña con resultados (o bien cuando hayan sido cargados utilizando el botón Load y seleccionando una optimización de la lista), podremos iniciar la simulación de la pasada de optimización seleccionada en cualquiera de los intervalos temporales necesarios, clicando para ello dos veces sobre la pasada de optimización que nos interese. 

private void _StartTest(List<OptimisationResult> results, int ind) {     try     {         Model.OptimisationManagers.OptimiserInputData optimiserInputData = new Model.OptimisationManagers.OptimiserInputData         {             Balance = Convert.ToDouble(OptimiserSettingsForResults_fixed.First(x => x.Key == "Deposit").Value),             Currency = OptimiserSettingsForResults_fixed.First(x => x.Key == "Currency").Value,             ExecutionDelay = GetEnum<ENUM_ExecutionDelay>(OptimiserSettingsForResults_changing.First(x => x.Name == "Execution Mode").SelectedParam),             Laverage = Convert.ToInt32(OptimiserSettingsForResults_fixed.First(x => x.Key == "Laverage").Value),             Model = GetEnum<ENUM_Model>(OptimiserSettingsForResults_changing.First(x => x.Name == "Optimisation model").SelectedParam),             OptimisationMode = ENUM_OptimisationMode.Disabled,             RelativePathToBot = OptimiserSettingsForResults_fixed.First(x => x.Key == "Expert").Value,             ForwardBorders = new List<DateBorders>(),             HistoryBorders = new List<DateBorders> { new DateBorders(TestFrom, TestTill) },             Symb = OptimiserSettingsForResults_fixed.First(x => x.Key == "Symbol").Value,             TF = (ENUM_Timeframes)Enum.Parse(typeof(ENUM_Timeframes), OptimiserSettingsForResults_fixed.First(x => x.Key == "TF").Value),             SortingFlags = null,             CompareData = null,             BotParams = results[ind].report.BotParams.Select(x => new ParamsItem { Variable = x.Key, Value = x.Value }).ToList()         };         model.StartTest(optimiserInputData);     }     catch (Exception e)     {         System.Windows.MessageBox.Show(e.Message);     } }

Exactamente de la misma forma, creamos la estructura con los parámetros de entrada, y después iniciamos la simulación. Si ha sucedido algún error durante la ejecución de este método, mostramos un mensaje sobre el mismo en forma de MessageBox. La implementación del método ya ha sido descrita, no obstante, merece la pena prestar atención a la instanciación de las propiedades que contienen esta llamada de retorno. Tenemos 3 recuadros distintos:

  • Simulaciones forward,
  • Simulaciones históricas,
  • Lista de optimizaciones en el intervalo de fechas seleccionado.

Por consiguiente, hemos creado 3 llamadas de retorno. Esto ha sido necesario para procesar correctamente la información de cada uno de los recuadros. 

/// <summary>
/// Iniciando la simulación desde el recuadro con simulaciones forward
/// </summary>
public ICommand StartTestForward { get; }
/// <summary>
/// Iniciando la simulación desde el recuadro con simulaciones históricas
/// </summary>
public ICommand StartTestHistory { get; }
/// <summary>
/// Iniciando la simulación desde el recuadro con los resultados de la optimización
/// </summary>
public ICommand StartTestReport { get; }

Y su implementación se muestra como establecimiento de funciones lambda:

StartTestReport = new RelayCommand((object o) => {     _StartTest(model.AllOptimisationResults.AllOptimisationResults[ReportDateBorders[SelectedReportDateBorder]], SelecterReportItem); }); // Llamada de retorno del inicio de la simulación según el evento de doble clic sobre el recuadro con las simulaciones históricas StartTestHistory = new RelayCommand((object o) => {     _StartTest(model.HistoryOptimisations, SelectedHistoryItem); }); // Llamada de retorno del inicio de la simulación según el evento de doble clic sobre el recuadro con las simulaciones forward StartTestForward = new RelayCommand((object o) => {     _StartTest(model.ForwardOptimisations, SelectedForwardItem); });

Este enfoque es necesario para que podamos establecer la lista deseada con los resultados de optimización, que usaremos para obtener los parámetros del robot, los mismos que el algoritmo transmite al archivo (podrá leer más información en el artículo № 3). 

Una vez finalizado todo el proceso de optimización y la posterior selección de los mejores resultados, así como la simulación de estos con datos históricos y forward, guardamos la lista con todas las pasadas de optimización. Esto se hace para que el usuario pueda o bien comprobar la lógica de funcionamiento del optimizador seleccionado, o bien seleccionar manualmente otras pasadas modificando los factores de filtrado y clasificación. Por consiguiente, hemos previsto la posibilidad de utilizar el mecanismo incorporado para el filtrado de los resultados de optimización y su clasificación según varios criterios simultáneos. Este mecanismo se implementa en el modelo de datos, pero los parámetros de entrada para él se forman en la clase ViewModel.

/// <summary> /// Clasificación de informes /// </summary> /// <param name="o"></param> private void _SortResults(object o) {     if (ReportDateBorders.Count == 0)         return;     IEnumerable<SortBy> sortFlags = SorterItems.Select(x => x.Sorter);     if (sortFlags.Count() == 0)         return;     if (AllOptimisations.Count == 0)         return;     model.SortResults(ReportDateBorders[SelectedReportDateBorder], sortFlags); } public ICommand SortResults { get; } /// <summary> /// Filtrado de informes /// </summary> /// <param name="o"></param> private void _FilterResults(object o) {     if (ReportDateBorders.Count == 0)         return;     IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData =         FilterItems.ToDictionary(x => x.Sorter, x => new KeyValuePair<CompareType, double>(x.CompareType, x.Border));     if (compareData.Count() == 0)         return;     if (AllOptimisations.Count == 0)         return;     model.FilterResults(ReportDateBorders[SelectedReportDateBorder], compareData); } public ICommand FilterResults { get; }

Ambos métodos tienen una implementación similar. Estos comprueban la presencia de los parámetros de los filtros de datos para ver si el recuadro está vacío, y después redireccionan su ejecución a la clase del modelo de datos. Ambos métodos de la clase del modelo de datos redireccionan la ejecución al propio método de ampliación descrito en el primer artículo.

El método de clasificación tiene la siguiente estructura:

public static IEnumerable<OptimisationResult> SortOptimisations(this IEnumerable<OptimisationResult> results,                                                                         OrderBy order, IEnumerable<SortBy> sortingFlags,                                                                         Func<SortBy, SortMethod> sortMethod = null)

Y el método de filtrado, en consecuencia:

public static IEnumerable<OptimisationResult> FiltreOptimisations(this IEnumerable<OptimisationResult> results,                                                                   IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData)

Todo ello sucede en el modo asincrónico, para no bloquear la parte gráfica durante la clasificación, que puede ocupar más de un segundo, dependiendo de la cantidad de datos.

Y ya que hablamos de la clasificación de datos, debemos recordar el modo de implementación de la relación entre los dos recuadros de clasificación de datos y filtrado de datos. Si usted abre el optimizador automático, tanto en la pestaña con los resultados como en la pestaña con ajustes (principal), existe una zona con un recuadro con las clasificaciones y filtrados de los datos: a ese precisamente nos referimos.

   

En la captura de pantalla, hemos destacado la zona que nos interesa en la pestaña con los resultados de optimización. La cosa es que si añadimos algún parámetro de clasificación a esta zona, y después pasamos a otra pestaña (en nuestro ejemplo, a la pestaña de ajustes), en esa misma zona veremos exactamente el mismo valor añadido. Ahora, si eliminamos este valor de la zona que nos interesa en la pestaña con ajustes, y después pasamos de nuevo a la pestaña con los resultados de las optimizaciones, veremos que este valor también ha sido eliminado de la pestaña con los resultados de las optimizaciones. Esto se logra gracias a la conexión existente entre dos recuadros con una misma propiedad.

Los recuadros de las clasificaciones están conectados con la siguiente propiedad:

/// <summary> /// Parámetros de clasificación seleccionados /// </summary> public ObservableCollection<SorterItem> SorterItems { get; } = new ObservableCollection<SorterItem>();

Los recuadros de los filtros, con los siguientes:   

/// <summary> ///Filtros seleccionados /// </summary> public ObservableCollection<FilterItem> FilterItems { get; } = new ObservableCollection<FilterItem>();

Las clases que describen las líneas en estos recuadros contienen algunos campos que se repiten y están encabezados en el mismo archivo que ViewModel.

/// <summary> /// Clase de envoltorio para enum SortBy (para la interfaz gráfica) /// </summary> class SorterItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="sorter">Parámetro de clasificación</param>     /// <param name="deleteItem">Llamada de retorno para la eliminación de la lista</param>     public SorterItem(SortBy sorter, Action<object> deleteItem)     {         Sorter = sorter;         Delete = new RelayCommand((object o) => deleteItem(this));      }      /// <summary>      /// Elemento de la clasificación      /// </summary>      public SortBy Sorter { get; }      /// <summary>      /// Llamada de retorno para eliminar un elemento      /// </summary>      public ICommand Delete { get; } } /// <summary> /// Clase de envoltorio para enum SortBy y las banderas CompareType (para la interfaz gráfica) /// </summary> class FilterItem : SorterItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="sorter">Elemento de clasificación</param>     /// <param name="deleteItem">Llamada de retorno de eliminación</param>     /// <param name="compareType">Método de comparación</param>     /// <param name="border">Magnitud comparada</param>     public FilterItem(SortBy sorter, Action<object> deleteItem,                       CompareType compareType, double border) : base(sorter, deleteItem)     {         CompareType = compareType;         Border = border;     }     /// <summary>     /// Tipo de comparación     /// </summary>     public CompareType CompareType { get; }     /// <summary>     /// Valor comparado     /// </summary>     public double Border { get; } }

La clase SorterItem es un objeto que muestra las líneas en el recuadro de parámetros seleccionados de clasificación de valores. Dicha clase, aparte del parámetro de clasificación, contiene una propiedad que indica la llamada de retorno de eliminación de este parámetro de clasificación concreto de la lista. Debemos notar que esta llamada de retorno se establece desde fuera con la ayuda de un delegado. La clase de filtros de datos se hereda de la clase de clasificaciones: no es necesario escribir dos veces los campos ya implementados en ella, podemos simplemente heredarlos desde la clase básica. Al conjunto de parámetros ya analizado se ha añadido el tipo de comparación de datos con el valor límite y el propio valor límite.

La presencia de los métodos de eliminación en la clase que muestra la línea se ha implementado para que podamos colocar un botón "Delete" frente a cada una de las líneas, como se hace en la implementación actual. Esto resulta cómodo para los usuarios y tiene un aspecto bastante interesante en su implementación. Los propios métodos de eliminación han sido sacados de las clases, y se establecen como delegados, motivo por el cual se requiere acceso a las colecciones de datos que se encuentran en la clase que representa ViewModel. Su implementación es bastante sencilla, y por eso no la vamos a mostrar aquí. Todo lo que hacen estos métodos es llamar al método Delete en la instancia de la colección de datos necesaria.

Una vez finalizados los eventos del modelo de datos que requieren una reacción por parte de la capa gráfica, se llama al evento OnPropertyChanged. La llamada de retorno del evento en la clase que representa ViewModel se ha implementado de la forma que sigue:

private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e) {     // La simulación ha finalizado o resulta necesario actualizar la accesibilidad de los botones bloqueados al iniciar la optimización o la simulación     if (e.PropertyName == "StopTest" ||         e.PropertyName == "ResumeEnablingTogle")     {         // conmutador de accesibilidad de los botones = true         EnableMainTogles = true;         // Reseteamos el estado y el progreso         Status = "";         Progress = 0;         // Notificamos a la parte gráfica sobre los cambios sucedidos         dispatcher.Invoke(() =>         {             OnPropertyChanged("EnableMainTogles");             OnPropertyChanged("Status");             OnPropertyChanged("Progress");         });     }     // La lista de pasadas de optimización realizadas ha cambiado     if (e.PropertyName == "AllOptimisationResults")     {         dispatcher.Invoke(() =>         {             // Limpiamos las pasadas de optimización anteriormente guardadas y añadimos las nuevas             ReportDateBorders.Clear();             foreach (var item in model.AllOptimisationResults.AllOptimisationResults.Keys)             {                 ReportDateBorders.Add(item);             }             // Seleccionamos la primera fecha             SelectedReportDateBorder = 0;             // Rellenamos los ajustes fijos del simulador de acuerdo con los ajustes de los resultados descargados             ReplaceBotFixedParam("Expert", model.AllOptimisationResults.Expert);             ReplaceBotFixedParam("Deposit", model.AllOptimisationResults.Deposit.ToString());             ReplaceBotFixedParam("Currency", model.AllOptimisationResults.Currency);             ReplaceBotFixedParam("Laverage", model.AllOptimisationResults.Laverage.ToString());             OnPropertyChanged("OptimiserSettingsForResults_fixed");         });         // Notificamos la finalización de la carga de datos         System.Windows.MessageBox.Show("Report params where updated");     }     // O bien el filtrado o bien la clasificación de las pasadas de optimización     if (e.PropertyName == "SortedResults" ||         e.PropertyName == "FilteredResults")     {         dispatcher.Invoke(() =>         {             SelectedReportDateBorder = SelectedReportDateBorder;         });     }     // Se han actualizado los datos sobre las optimizaciones forward     if (e.PropertyName == "ForwardOptimisations")     {         dispatcher.Invoke(() =>         {             ForwardOptimisations.Clear();             foreach (var item in model.ForwardOptimisations)             {                 ForwardOptimisations.Add(new ReportItem(item));             }         });     }     // Se han actualizado los datos sobre las optimizaciones históricas     if (e.PropertyName == "HistoryOptimisations")     {         dispatcher.Invoke(() =>         {             HistoryOptimisations.Clear();             foreach (var item in model.HistoryOptimisations)             {                 HistoryOptimisations.Add(new ReportItem(item));             }         });     }     // Se ha guardado un archivo (*.csv) con los resultados de optimización / simulación     if (e.PropertyName == "CSV")     {         System.Windows.MessageBox.Show("(*.csv) File saved");     } }

Todas las condiciones en esta llamada de retorno comprueban la propiedad PropertyName del parámetro de entrada "e". La primera condición se ejecuta en caso de que finalice la simulación y se solicite el modelo de datos para desbloquear la interfaz gráfica. En esencia, al activarse esta condición, desbloqueamos la interfaz gráfica, y también devolvemos el estado de la barra de progreso y la propia barra de progreso a sus valores iniciales. Debemos notar que este evento puede ser llamado en el contexto del flujo secundario, mientras que la notificación de la parte gráfica (llamada del evento OnPropertyChanged) deberá siempre realizarse en el contexto del flujo primario, es decir, en un mismo flujo con la interfaz gráfica. Por eso, para evtiar errores, llamaremos este evento desde la clase dispatcher. Dispatcher permite recurrir a la parte gráfica desde el contexto del flujo de esta ventana.

La siguiente condición se llama en el momento en que el modelo de datos actualiza todas las optimizaciones realizadas. Para tener la posibilidad de seleccionar las listas de optimización a través de un cuadro combinado, deberemos rellenarlo con las fechas de estas optimizaciones. Precisamente de ello se encarga esta parte del código. Asimismo, rellena los parámetros fijos de los ajustes del simulador:

  • Nombre del experto,
  • Depósito,
  • Divisa del depósito,
  • Apalancamiento crediticio.

Al final, muestra un MessageBox con un texto que notifica la finalización de la actualización de los parámetros y los recuadros del informe de las pasadas de optimización.

Al terminar el filtrado o la clasificación, se activa la condición correspondiente; no obstante, para comprender su implementación, deberemos analizar la implementación de la propiedad SelectedReportDateBorder.

#region Selected optimisation date border index keeper private int _selectedReportDateBorder; public int SelectedReportDateBorder {     get => _selectedReportDateBorder;     set     {         AllOptimisations.Clear();         if (value == -1)         {             _selectedReportDateBorder = 0;             return;         }         _selectedReportDateBorder = value;         if (ReportDateBorders.Count == 0)             return;         List<OptimisationResult> collection = model.AllOptimisationResults.AllOptimisationResults[ReportDateBorders[value]];         foreach (var item in collection)         {             AllOptimisations.Add(new ReportItem(item));         }     } } #endregion

La parte que nos interesa del setter actualiza la colección AllOptimisations en la clase ViewModel, y gracias a ello, el código en la condición analizada, incomprensible inicialmente, cobra para nosotros sentido. Dicho de otra forma: asignando al parámetro SelectedReportDateBorder el valor de sí mismo, simplemente evitamos la duplicación de este ciclo. 

Las condiciones de actualización de los datos de los recuadros forward e históricos juegan el mismo papel que la anterior condición, en concreto, la sincronización de datos entre ViewModel y Model. Esta sincronización es necesaria para que podamos hacer referencia directamente a las estructuras con las que opera el modelo de datos, ya que para describir las líneas de los recuadros, se requieren las clases correspondientes, donde cada botón se representa con una propiedad. Estas clases han sido creadas como envoltorio para las estructuras utilizadas en el modelo de datos. Concretamente, para los recuadros con los informes se utiliza la clase "ReportItem", cuya implementación hemos analizado en el capítulo anterior.

Conclusión

Este artículo es el penúltimo sobre la optimización móvil y el optimizador automático, que implementa este proceso. En estos momentos, ya hemos analizado la estructura de las partes más significativas de la aplicación creada. La primera parte, describía la parte de la aplicación encargada de trabajar con informes y de guardar estos en el formato xml. Las partes segunda y tercera describían la forma en que se genera el informe para el optimizador automático, así como el mecanismo de ensamblaje de la interfaz programática para la descarga de informes, descrita en el primer artículo. La cuarta parte contenía las instrucciones para usar este programa, dado que, en aquel momento, ya habían sido descritas todas las acciones necesarias para conectar cualquier robot cuyo código fuente tenga el usuario al optimizador automático.

En las partes de la cinco a la siete, se analiza el propio programa del optimizador automático, encargado de gestionar el proceso. Hemos comenzado su análisis por la parte gráfica (quinta parte), después, hemos analizado su lógica de funcionamiento (sexta parte), y en la parte actual, hemos descrito cómo encajan entre sí. En los comentarios a la quinta parte, los lectores expresaron una serie de peticiones sobre la mejora de la interfaz del programa; entre ellas, seleccionamos las más interesantes.

La parte actual no contiene por ahora las correciones indicadas, dado que, en primer lugar, debemos describir la variante principal del trabajo realizado. La última (siguiente) parte del artículo ya contendrá las correcciones indicadas, con las pertienentes descripciones sobre los cambios realizados, así como la descripción del proceso de creación del propio optimizador. Entendemos por optimizador la lógica de ejecución del inicio de las optimizaciones. La lógica del optimizador actual ya la hemos visto más de una vez en los anteriores artículos, sobre todo en el cuarto. Por eso, en el artículo final del presente ciclo, analizaremos una especie de instrucciones para escribir una lógica semejante. En ellas, tomaremos como base la lógica de optimización disponible, pero la mejoraremos un tanto, mostrando por etapas el proceso completo de escritura de un optimizador propio.

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

Archivos adjuntos |
Auto_Optimiser.zip (125.7 KB)
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.

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.

Sobre los métodos de búsqueda de las zonas de sobrecompra/sobreventa. Parte I Sobre los métodos de búsqueda de las zonas de sobrecompra/sobreventa. Parte I

Las zonas de sobrecompra/sobreventa caracterizan un determinado estado del mercado que se distingue por el debilitamiento de la dinámica de los precios de los instrumentos financieros. En este caso, además, dicha dinámica negativa se manifiesta en mayor medida en el estadio final del desarrollo de una tendencia de cualquier escala. Y dado que la magnitud del beneficio en el trading depende directamente de la posibilidad de abarcar la máxima amplitud en la tendencia, la precisión a la hora de detectar estas zonas supone una tarea de capital importancia al comerciar con cualquier instrumento financiero.

Trabajando con las series temporales en la biblioteca DoEasy (Parte 41): Ejemplo de indicador de símbolo y periodo múltiples Trabajando con las series temporales en la biblioteca DoEasy (Parte 41): Ejemplo de indicador de símbolo y periodo múltiples

En el artículo, analizaremos un ejemplo de creación de un indicador de símbolo y periodo múltiples usando las clases de las series temporales de la biblioteca DoEasy. Dicho indicador representará en la subventana el gráfico de la pareja de divisas seleccionada con el marco temporal seleccionado en forma de velas japonesas. Asimismo, mejoraremos las clases de la biblioteca y crearemos un archivo aparte para guardar las enumeraciones para los parámetros de entrada de los programas y la selección del lenguaje de compilación.