English Русский 中文 Deutsch 日本語 Português
preview
Optimización móvil continua (Parte 1): Mecanismo de trabajo con los informes de optimización

Optimización móvil continua (Parte 1): Mecanismo de trabajo con los informes de optimización

MetaTrader 5Probador | 17 enero 2020, 13:25
1 737 0
Andrey Azatskiy
Andrey Azatskiy

Introducción

En artículos anteriores (Gestión de la optimización (Parte I) y Gestión de la optimización (Parte 2)) ya analizamos el mecanismo de inicio de la optimización en el terminal a través de un proceso ajeno. Esto nos permite crear un cierto gestor de la optimización que ejecute este proceso como el algoritmo comercial ejecuta el suyo, es decir, de forma automática y sin intervención del usuario. Este tema está dedicado a la creación de un algoritmo que gestiona el proceso de la optimización móvil, cuando los segmentos en tiempo real e históricos se desplazan paulatinamente en el intervalo dado, solapándose unos sobre otros.

Este enfoque sobre la optimización de algoritmos se podría llamar test estabilidad de la estrategia antes que optimización, sin embargo, en esencia, cumple estos dos papeles. Como resultado, podremos responder a la pregunta "¿es el sistema comercial estable?" y conseguir una combinación de indicadores ideal para lanzarse a la batalla. Dado que el proceso descrito puede conjugarse con diferentes métodos de filtrado de coeficientes del robot y la selección de las combinaciones óptimas, y además todo ello debe hacerse en cada uno de los intervalos temporales (cuyo número puede ser muy elevado), realizar esto manualmente resultará extremadamente duro. Además, todo ello se une al riesgo de pasar algo por alto y equivocarse al trasladar los datos, o bien de cometer otros errores relacionados con el factor humano. Por eso, vamos a necesitar ciertas herramientas que gestionen el proceso de optimización sin nuestra intervención. El programa creado como resultado responde a las tareas establecidas. No obstante, para dotar de una estructura más definida a la exposición del material, hemos dividido el proceso de creación en una serie de artículos, cada uno de las cuales habla de una de las vertientes de los trabajos realizados para su creación.

En concreto, esta parte del artículo está dedicada a la creación de una herramienta para trabajar con los informes de optimización y su importación desde el terminal, así como a los procesos de filtrado y clasificación de los datos obtenidos. Para dotar de una estructura más definida al archivo, hemos elegido el formato (*xml), que cumple con todos los requisitos. Los datos han sido estructurados de tal forma que sean legibles tanto para una persona, como para un programa. Además, podemos agrupar los datos según los bloques establecidos dentro del propio archivo, y obtener más rápidamente acceso a la información que nos interese.

Dado que, para que nuestro programa funcione (se trata de un proceso ajeno y está implementado en C#), vamos a necesitar crear y leer los documentos (*xml) creados al mismo nivel que los programas escritos en MQL5, hemos decidido sacar directamente el bloque de creación de informes a una biblioteca DLL, que se usará de forma conjunta, tanto en MQL5, como en código C#. Teniendo en cuenta que, para escribir código MQL5 vamos a necesitar esta biblioteca, primero vamos a describir su proceso de creación en el presente artículo, y ya en el siguiente, se describirá el código MQL5 que trabaja con la biblioteca creada y forma los parámetros de optimización, sobre los que se hablarán en este artículo.

Estructura del informe y coeficientes requeridos

Como ya hemos mostrado en artículos anteriores, MetaTrader 5 sabe descargar automáticamente el informe de pasadas de optimización, sin embargo, este no es tan informativo como el informe generado en la pestaña Backtest al finalizar la simulación para un conjunto concreto de parámetros. Para disponer de mayor libertad al trabajar con los datos optimizados, querríamos combinar en el informe muchos de los datos representados en esta pestaña, además de tener la posibilidad de añadir datos propios al informe. Para este cometido, hemos decidido descargar un informe generado independientemente, y no el ofrecido por la solución estándar. Para comenzar, debemos determinar los tres tipos de datos necesarios para el funcionamiento de nuestro programa:

  • Ajustes del simulador (únicos para todo el informe)
  • Ajustes del robot (únicos para cada pasada de optimización)
  • Coeficientes que describen la eficiencia de las transacciones (únicos para cada pasada de optimización)

<Optimisatin_Report Created="06.10.2019 10:39:02">
        <Optimiser_Settings>
                <Item Name="Bot">StockFut\StockFut.ex5</Item>
                <Item Name="Deposit" Currency="RUR">100000</Item>
                <Item Name="Laverage">1</Item>
        </Optimiser_Settings>

Como podemos ver, cada uno de los parámetros ha sido introducido en un bloque "Item" similar, pero se divide según el atributo "Name". Para el parámetro del depósito, el nombre de la divisa se registra en el atributo "Currency"

Partiendo de ello, la estructura del archivo deberá contener 2 apartados principales: los ajustes del simulador y la propia descripción de las pasadas de optimización. Para el primer apartado, deberemos guardar 3 parámetros:

  1. La ruta hasta el robot con respecto a la carpeta con los expertos.
  2. La divisa del depósito y el depósito
  3. El apalancamiento crediticio

 El segundo apartado deberá contener una secuencia de bloques con los resultados de las optimizaciones, que a su vez contendrán el apartado con los coeficientes, así como el conjuto de parámetros del robot. 

<Optimisation_Results>
                <Result Symbol="SBRF Splice" TF="1" Start_DT="1481327340" Finish_DT="1512776940">
                        <Coefficients>
                                <VaR>
                                        <Item Name="90">-1055,18214207419</Item>
                                        <Item Name="95">-1323,65133343373</Item>
                                        <Item Name="99">-1827,30841143882</Item>
                                        <Item Name="Mx">-107,03475</Item>
                                        <Item Name="Std">739,584549199836</Item>
                                </VaR>
                                <Max_PL_DD>
                                        <Item Name="Profit">1045,9305</Item>
                                        <Item Name="DD">-630</Item>
                                        <Item Name="Total Profit Trades">1</Item>
                                        <Item Name="Total Loose Trades">1</Item>
                                        <Item Name="Consecutive Wins">1</Item>
                                        <Item Name="Consecutive Loose">1</Item>
                                </Max_PL_DD>
                                <Trading_Days>
                                        <Mn>
                                                <Item Name="Profit">0</Item>
                                                <Item Name="DD">0</Item>
                                                <Item Name="Number Of Profit Trades">0</Item>
                                                <Item Name="Number Of Loose Trades">0</Item>
                                        </Mn>
                                        <Tu>
                                                <Item Name="Profit">0</Item>
                                                <Item Name="DD">0</Item>
                                                <Item Name="Number Of Profit Trades">0</Item>
                                                <Item Name="Number Of Loose Trades">0</Item>
                                        </Tu>
                                        <We>
                                                <Item Name="Profit">1045,9305</Item>
                                                <Item Name="DD">630</Item>
                                                <Item Name="Number Of Profit Trades">1</Item>
                                                <Item Name="Number Of Loose Trades">1</Item>
                                        </We>
                                        <Th>
                                                <Item Name="Profit">0</Item>
                                                <Item Name="DD">0</Item>
                                                <Item Name="Number Of Profit Trades">0</Item>
                                                <Item Name="Number Of Loose Trades">0</Item>
                                        </Th>
                                        <Fr>
                                                <Item Name="Profit">0</Item>
                                                <Item Name="DD">0</Item>
                                                <Item Name="Number Of Profit Trades">0</Item>
                                                <Item Name="Number Of Loose Trades">0</Item>
                                        </Fr>
                                </Trading_Days>
                                <Item Name="Payoff">1,66020714285714</Item>
                                <Item Name="Profit factor">1,66020714285714</Item>
                                <Item Name="Average Profit factor">0,830103571428571</Item>
                                <Item Name="Recovery factor">0,660207142857143</Item>
                                <Item Name="Average Recovery factor">-0,169896428571429</Item>
                                <Item Name="Total trades">2</Item>
                                <Item Name="PL">415,9305</Item>
                                <Item Name="DD">-630</Item>
                                <Item Name="Altman Z Score">0</Item>
                        </Coefficients>
                        <Item Name="_lot_">1</Item>
                        <Item Name="USymbol">SBER</Item>
                        <Item Name="Spread_in_percent">3.00000000</Item>
                        <Item Name="UseAutoLevle">false</Item>
                        <Item Name="max_per">174</Item>
                        <Item Name="comission_stock">0.05000000</Item>
                        <Item Name="shift_stock">0.00000000</Item>
                        <Item Name="comission_fut">4.00000000</Item>
                        <Item Name="shift_fut">0.00000000</Item>
                </Result>
        </Optimisation_Results>
</Optimisatin_Report>

Dentro del bloque "Optimisation_Results", se repetirán los bloques "Result", cada uno de los cuales contendrá la i-ésima pasada de optimización. En cada uno de los bloques "Result" se contendrán 4 atributos:

  • Symbol
  • TF
  • Start_DT
  • Finish_DT

En esencia, son los ajustes del simulador, que cambiarán dependiendo del tipo de optimización o del intervalo de fechas en el que ha sido iniciada la optimización. Cada uno de los parámetros del robot se registra en un bloque Item con el atributo Name como único valor, según el cual podemos identificarlo. Los coeficientes del robot se registran en el bloque Corfficients. Los coeficientes que no se puedan agrupar, se enumerarán directamente en el bloque Item. El resto de coeficientes se dividen por bloques:

  • VaR
  1. 90 - cuantil 90
  2. 95 - cuantil 95
  3. 99 - cuantil 99
  4. Mx - esp. matemática
  5. Std - desviación cuadrática estándar
  • Max_PL_DD
  1. Profit - Beneficio total
  2. DD - Reducción total
  3. Total Profit Trades - Número total de transacciones rentables
  4. Total Loose Trades - Número total de transacciones con pérdidas
  5. Consecutive Wins - Beneficios consecutivos
  6. Consecutive Loose - Pérdidas consecutivas
  • Trading_Days - informe de transacciones por días 
  1. Profit - beneficio medio durante el día
  2. DD - pérdidas medias durante el día
  3. Number Of Profit Trades - número de transacciones rentables
  4. Number Of Loose Trades - número de transacciones con pérdidas

Como resultado, hemos obtenido una lista con los coeficientes de los resultados de la optimización que describen mejor los resultados de las simulaciones. Ahora, para filtrar y seleccionar los parámetros del robot existe una lista completa con los coeficientes requeridos o la mayoría de los mismos, por lo que podremos valorar los resultados del robot con mayor efectividad. 

La clase de envoltorio del informe de optimización y la clase de guardado del intervalo de fechas de optimización, así como la estructura de los resultados de las optimizaciones C#.

Para comenzar, merece la pena analizar la estructura que guarda los datos de una pasada de optimización concreta. 

public struct ReportItem
{
    public Dictionary<string, string> BotParams; // Lista de parámetros del robot
    public Coefficients OptimisationCoefficients; // Coeficientes del robot
    public string Symbol; // Símbolo
    public int TF; // Marco temporal
    public DateBorders DateBorders; // Límites de las fechas
}

Todos los coeficientes del robot se guardan en un diccionario en formato de línea. El archivo con los parámetros del robot no guarda el tipo de datos, por eso, el formato de línea es el mejor para este caso. La lista de coeficientes del robot se ha sacado a una estructura aparte con otros bloques agrupados en un informe (*xml) de optimización. Los informes por días también se guardan en el dicionario.

public Dictionary<DayOfWeek, DailyData> TradingDays;

Sin embargo, como clave, la enumeración DayOfWeek y este diccionario siempre deberán contener 5 días (de lunes a viernes), como en archivo (*xml). En la estructura de guardado de datos, la clase más interesante es DateBorders. De la misma forma que la información, se agrupa en una estructura que contiene campos que describen cada uno de los parámetros de la fecha; como en la estructura DateBorders, guardamos los límites del intervalo de fechas. 

public class DateBorders : IComparable
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="from">Fecha de inicio del límite</param>
    /// <param name="till">Fecha de finalización del límite</param>
    public DateBorders(DateTime from, DateTime till)
    {
        if (till <= from)
            throw new ArgumentException("Date 'Till' is less or equal to date 'From'");

        From = from;
        Till = till;
    }
    /// <summary>
    /// С
    /// </summary>
    public DateTime From { get; }
    /// <summary>
    /// По
    /// </summary>
    public DateTime Till { get; }
}

Para trabajar de forma totalmente operativa con el intervalo de fechas, deberemos simplemente tener la posibilidad de comparar los dos intervalos de fechas; para ello, necesitaremos sobrecargar los 2 operadores "==" y "!=". 

Como criterios de igualdad se tomará la igualdad de ambas fechas en los dos intervalos transmitidos, es decir, la fecha de inicio deberá ser igual al inicio de las transacciones del segundo intervalo, de forma análoga a la fecha de finalización de las transacciones. Sin embargo, dado que el tipo de este objeto es class, este puede ser igual a null, por lo que primero necesitaremos añadir la posibilidad de comparar con null; para ello, usaremos la palabra clave is. Después de ello, podremos comparar los parámetros entre sí, de lo contrario, al intentar comparar con null, obtendremos "null reference exception".

#region Equal
/// <summary>
/// Operador de comprobación de igualdad
/// </summary>
/// <param name="b1">Elemento 1</param>
/// <param name="b2">Elemento 2</param>
/// <returns>Resultado</returns>
public static bool operator ==(DateBorders b1, DateBorders b2)
{
    bool ans;
    if (b2 is null && b1 is null) ans = true;
    else if (b2 is null || b1 is null) ans = false;
    else ans = b1.From == b2.From && b1.Till == b2.Till;

    return ans;
}
/// <summary>
/// Operador de comprobación de diferencia
/// </summary>
/// <param name="b1">Elemento 1</param>
/// <param name="b2">Elemento 2</param>
/// <returns>Resultado de la comparación</returns>
public static bool operator !=(DateBorders b1, DateBorders b2) => !(b1 == b2);
#endregion

Para cargar el operador de diferencia, ya no será necesario escribir todos los procesos mencionados anteriormente, dado que estos ya están escritos en el operador "==". La siguiente capacidad que necesitaremos es la clasificación según el periodo temporal, por eso necesitamos sobrecargar también los operadores ">", "<", ">=", "<=".

#region (Grater / Less) than
/// <summary>
/// Comparando si el elemento actual es mayor al anterior
/// </summary>
/// <param name="b1">Elemento 1</param>
/// <param name="b2">Elemento 2</param>
/// <returns>Resultado</returns>
public static bool operator >(DateBorders b1, DateBorders b2)
{
    if (b1 == null || b2 == null)
        return false;

    if (b1.From == b2.From)
        return (b1.Till > b2.Till);
    else
        return (b1.From > b2.From);
}
/// <summary>
/// Comparando si el elemento actual es menor al anterior
/// </summary>
/// <param name="b1">Elemento 1</param>
/// <param name="b2">Elemento 2</param>
/// <returns>Resultado</returns>
public static bool operator <(DateBorders b1, DateBorders b2)
{
    if (b1 == null || b2 == null)
        return false;

    if (b1.From == b2.From)
        return (b1.Till < b2.Till);
    else
        return (b1.From < b2.From);
}
#endregion

Si cualquiera de los parámetros transmitidos al operador es igual a null, la comparación no resultará posible, por eso retornamos False. De lo contrario, realizamos una comparación por etapas. Si el primer intervalo temporal coincide, realizamos la comparación según el segundo intervalo termporal. Si no coinciden, lo haremos según el primero. Por ello, si describimos la lógica de comparación usando como ejemplo el operador "Mayor que", se considerará mayor el intervalo cuyo tiempo sea mayor al anterior, o bien según su fecha de inicio, o bien según su fecha de finalización (en el caso de que las fechas de inicio sean iguales). La lógica de comparación "Menor que" de uno de los intervalos transmitidos es análoga a la comparación usando "Mayor que". 

Los siguientes operadores que debemos sobrecargar para hacer posible la clasificación son los operadores "Mayor o igual" y "Menor o igual". 

#region Equal or (Grater / Less) than
/// <summary>
/// Comparando si es mayor o igual
/// </summary>
/// <param name="b1">Elemento 1</param>
/// <param name="b2">Elemento 2</param>
/// <returns>Resultado</returns>
public static bool operator >=(DateBorders b1, DateBorders b2) => (b1 == b2 || b1 > b2);
/// <summary>
/// Comparando si es menor o igual
/// </summary>
/// <param name="b1">Elemento 1</param>
/// <param name="b2">Elemento 2</param>
/// <returns>Resultado</returns>
public static bool operator <=(DateBorders b1, DateBorders b2) => (b1 == b2 || b1 < b2);
#endregion

Como podemos ver, la sobrecarga de los datos de los operadores ya no requiere que se describa la lógica de interna de comparación, en lugar de ello, usamos los operadores ya sobrecargados "==" y ">", "<". No obstante, aparte de sobrecargar los datos de los operadores, como ya nos sugiere Visual Studio al compilar, debemos sobrecargar otra serie de funciones heredadas de la clase básica "object".

#region override base methods (from object)
/// <summary>
/// Sobrecargando la comparación de igualdad
/// </summary>
/// <param name="obj">Elemento con el que comparamos</param>
/// <returns></returns>
public override bool Equals(object obj)
{
    if (obj is DateBorders other)
        return this == other;
    else
        return base.Equals(obj);
}
/// <summary>
/// Convirtiendo esta clase en línea y retornando su código hash
/// </summary>
/// <returns>Código hash de la línea</returns>
public override int GetHashCode()
{
    return ToString().GetHashCode();
}
/// <summary>
/// Covirtiendo la clase actual en línea
/// </summary>
/// <returns>Línea fecha DESDE - fecha HASTA</returns>
public override string ToString()
{
    return $"{From}-{Till}";
}
#endregion
/// <summary>
/// Comparando el elemento actual con el transmitido
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public int CompareTo(object obj)
{
    if (obj == null) return 1;

    if (obj is DateBorders borders)
    {
        if (this == borders)
            return 0;
        else if (this < borders)
            return -1;
        else
            return 1;
    }
    else
    {
        throw new ArgumentException("object is not DateBorders");
    }
}

Sobrecargamos el método "Equals" usando, o bien el operador sobrecargado "==" (si el objeto transmitido es del tipo DateBorders), o bien la implementación básica de este método.

Sobrecargamos el método "ToString" como una presentación de línea con fechas separadas por un guión. Esto nos ayudará a sobrecargar el método GetHashCode.

Sobrecargamos el método GetHashCode convirtiendo el objeto actual al inicio de la línea, y después retornando el código hash de esta línea. Lo que ocurre es que en el lenguaje C#, al crear un nuevo ejemplar de clase, su código hash será único, independientemente de cómo se rellene esta clase. Es decir, si no sobrecargamos este método y creamos dos ejemplares de nuestra clase DateBorders con fechas DESDE y HASTA idénticas en el interior, tendrán un código hash distinto, a pesar de que se hayan rellanado de forma idéntica. Esta ley no se extiende a las líneas, dado que en C# hay un mecanismo que no crea nuevos ejemplares de la clase String si esta línea ha sido creada con anterioridad, con lo que los códigos hash de dos líneas idénticas coincidirán. Usando nuestra sobrecarga del método ToString y tomando el código hash de la línea, hemos logrado que nuestra clase tenga el mismo comportamiento que la clase String. Ahora, al usar el método IEnumerable.Distinct, podemos garantizar que la lógica de obtención de la lista única de los límites de fecha será correcta, dado que este método se basa en los códigos hash de los objetos comparados.

Tras implementar la interfaz IComparable, de la que se hereda nuestra clase, implementamos el método CompareTo, que compara el ejemplar de la clase actual con el transmitido. Su implementación es bastante trivial y usa la sobrecarga de los operadores anteriormente sobrecargados. 

Ahora que hemos implementado todas estas sobrecargas, podemos trabajar con esta clase de una forma más cómoda. En concreto, podemos:

  • Comparar si dos ejemplares son iguales
  • Comparar si dos ejemplares son mayores/menores
  • Comparar si dos ejemplares son mayores o iguales / menores o iguales
  • Clasificar en orden ascendente / descendente
  • Obtener valores únicos de la lista de límites de fecha
  • Usar el método IEnumerable.Sort, que clasifica en orden descendente una lista y usa la interfaz IComparable.

Dado que estamos implementado una optimización móvil, en la que tendremos simulaciones históricas y en tiempo real, debemos crear un método que compare los intervalos en tiempo real con los históricos.

/// <summary>
/// Método que compara las optimizaciones en tiempo real y las históricas
/// </summary>
/// <param name="History">Matriz de optimizaciones históricas</param>
/// <param name="Forward">Matriz de optimizaciones en tiempo real</param>
/// <returns>Lista clasificada de optimizaciones históricas - en tiempo real</returns>
public static Dictionary<DateBorders, DateBorders> CompareHistoryToForward(List<DateBorders> History, List<DateBorders> Forward)
{
    // matriz de optimizaciones comparadas
    Dictionary<DateBorders, DateBorders> ans = new Dictionary<DateBorders, DateBorders>();

    // Clasificando los parámetros transmitidos
    History.Sort();
    Forward.Sort();

    // Creando un ciclo por las optimizaciones históricas
    int i = 0;
    foreach (var item in History)
    {
if(ans.ContainsKey(item))
       	    continue;

        ans.Add(item, null); // Añadiendo optimización histórica
        if (Forward.Count <= i)
            continue; // Si la matriz de optimizaciones en tiempo real es menor al índice, continuamos el ciclo

        // Ciclo por las optimizaciones en tiempo real
        for (int j = i; j < Forward.Count; j++)
        {
            // Si en la matriz de resultados se contiene la optimización en tiempo real actual, omitimos
            if (ans.ContainsValue(Forward[j]) ||
                Forward[j].From < item.Till)
            {
                continue;
            }

            // Comparando las optimizaciones en tiempo real e histórica
            ans[item] = Forward[j];
            i = j + 1;
            break;
        }
    }

    return ans;
}

Como podemos ver, este método es estático. Esto se ha hecho así para que esté disponible como una función normal, sin vinculación a un ejemplar de clase concreto. Lo primero que hace es clasificar los intervalos temporales transmitidos en orden ascendente. Esto es necesario para que podamos saber en el ciclo posterior que todos los intervalos pasados anteriormente son a ciencia cierta menores (o iguales) que los siguientes. A continuación, organizamos dos ciclos: el primero, foreach según los intervalos históricos, mientras que el ciclo incorporado se organiza según los intervalos en tiempo real.

Al inicio del ciclo de datos históricos siempre añadimos a la colección con los resultados los límites históricos (key); en lugar de los intervalos en tiempo real, establecemos temporalmente null. El ciclo por los resultados en tiempo real comienza por el parámetro i, esto es necesario para no repetir el ciclo por elementos anteriormente urilizados de la lista en tiempo real. Es decir, un intervalo en tiempo real debe siempre seguir a uno histórico, es decir, debe ser > histórico. Por eso, organizamos un ciclo por los intervalos en tiempo real, por si tenemos en la lista transmitida - en su inicio para el primer intervalo histórico - una lista de intervalos en tiempo real que antecedan al primer intervalo histórico. Será más fácil mostrar esta idea en forma de recuadro:

Histórica En tiempo real
DESDE HASTA DESDE HASTA
10.03.2016 09.03.2017 12.12.2016 09.03.2017
10.06.2016 09.06.2017 10.03.2017 09.06.2017
10.09.2016 09.09.2017 10.06.2017 09.09.2017

Es decir, el primer intervalo histórico finaliza el 09.03.2017, mientras que el primer intervalo en tiempo real comienza a partir del 12.12.2016. Resulta obvio que no se correlacionan el uno con el otro, por eso, lo omitimos en el ciclo de intervalos en tiempo real, dado que la condición se cumplirá. Asimismo, omitimos el intervalo en tiempo real que ya está contenido en el diccionario resultante. Si la j-ésima fecha en tiempo real existe también en el diccionario resultante y la fecha de comienzo del intervalo en tiempo real >= la fecha de finalización del intervalo histórico actual, guardamos el valor obtenido y salimos del ciclo de intervalos en tiempo real, dado que el valor ya ha sido encontrado. No obstante, antes de salir, asignamos a i (la variable de comienzo de la iteración por la lista en tiempo real) el valor del intervalo que sigue al intervalo en tiempo real seleccionado ahora, dado que el intervalo ya no se requerirá (debido a la clasificación inicial de datos).

La comprobación antes de añadir la optimización histórica garantiza el carácter único de las optimziaciones históricas. Como fruto de ello, obtenemos la siguiente lista en el diccionario resultante:

Key Value
10.03.2016-09.3.2017 10.03.2017-09.06.2017
10.06.2016-09.06.2017 10.06.2017-09.09.2017
10.09.2016-09.09.2017 null

Como podemos ver por los datos presentados, hemos descartado el primer intervalo en tiempo real, y no hemos podido encontrar para el último intervalo histórico uno en tiempo real, dado que no ha sido transmitido. Siguiendo esta lógica, nuestro programa comparará en lo sucesivo los datos de los intervalos históricos y en tiempo real, pudiendo así comprender de cuál de los intervalos de tiempo históricos se deben seleccionar los parámetros para las simulaciones en tiempo real.

Para trabajar con mayor comodidad con un resultado de optimización concreto, se ha creado una estructura de envoltorio para la estructura ReportItem, que contiene una serie de métodos y operadores sobrecargados adicionales para trabajar de forma más productiva con los datos de las optimizaciones. En su base hay dos 2 campos:

/// <summary>
/// Informe de la pasada de optimización
/// </summary>
public ReportItem report;
/// <summary>
/// coeficiente de clasificación
/// </summary>
public double SortBy;

El primero de ellos ya se ha descrito anteriormente, pero el segundo se ha creado para poder clasificar los datos según diversos indicadores, por ejemplo, el beneficio y el factor de recuperación; el mecanismo de esta clasificación se describirá más adelante, aunque ahora debemos decir que todos estos indicadores se reducirán a uno, que será guardado en esta variable. 

Asimismo, esta estructura contendrá las sobrecargas de la conversión de tipos:

/// <summary>
/// Operador de conversión implícita del tipo de la pasada de optimización al tipo actual
/// </summary>
/// <param name="item">Informa de la pasada de optimización</param>
public static implicit operator OptimisationResult(ReportItem item)
{
    return new OptimisationResult { report = item, SortBy = 0 };
}
/// <summary>
/// Operador de conversión explícita de tipos del actual a la estructura de la pasada de optimización
/// </summary>
/// <param name="optimisationResult">tipo actual</param>
public static explicit operator ReportItem(OptimisationResult optimisationResult)
{
    return optimisationResult.report;
}

Como resultado, tendremos la posibilidad de convertir implícitamente el tipo ReportItem a su envoltorio y ya después convertir explícitamente el envoltorio ReportItem al propio elemento del informe de transacciones. Esto puede resultar más cómodo que rellenar los campos de forma consecutiva. Asimismo, dado que en la estructura ReportItem todos sus campos están divididos en categorías, y a veces en código, para obtener algún resultado podríamos necesitar una grabación muy prolongada. Para ahorrar espacio y crear un getter más universal, hemos creado un método que obtiene los datos de los coeficientes del robot solicitados a través del enum SourtBy transmitido a él, descrito más arriba en el código GetResult(SortBy resultType). Su implementación es trivial, pero demasiado larga, por eso no la vamos a mostrar aquí. Este método en la construcción switch case itera los enum transmitidos, y a partir de ellos, retorna el valor del coeficiente solicitado. Puesto que la mayoría de los coeficientes es de tipo double, y dado que este tipo puede incluir todos los demás tipos numéricos, vamos a convertir el valor de los coeficientes precisamente a él.

Para este tipo de envoltorio se han implementado las sobrecargas de los operadores de comparación:

/// <summary>
/// Sobrecarga del operador de comparación sobre la igualdad
/// </summary>
/// <param name="result1">parámetro comparado 1</param>
/// <param name="result2">parámetro comparado 2</param>
/// <returns>resultado de la comparación</returns>
public static bool operator ==(OptimisationResult result1, OptimisationResult result2)
{
    foreach (var item in result1.report.BotParams)
    {
        if (!result2.report.BotParams.ContainsKey(item.Key))
            return false;
        if (result2.report.BotParams[item.Key] != item.Value)
            return false;
    }

    return true;
}
/// <summary>
/// Sobrecarga del operador de comparación sobre la diferencia
/// </summary>
/// <param name="result1">parámetro comparado 1</param>
/// <param name="result2">parámetro comparado 2</param>
/// <returns>resultado de la comparación</returns>
public static bool operator !=(OptimisationResult result1, OptimisationResult result2)
{
    return !(result1 == result2);
}
/// <summary>
/// Sobrecarga del operador de comparación del tipo básico
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
    if (obj is OptimisationResult other)
    {
        return this == other;
    }
    else
        return base.Equals(obj);
}

Consideraremos elementos de optimización iguales aquellos de ellos que contengan los mismos nombres y valores de los parámetros del robot. Por lo tanto, si necesitamos comparar en algún lugar del código dos pasadas optimizadas, ya tendremos los operadores sobrecargados listos para ello. Aparte de todo lo enumerado, esta estructura contiene un método que registra los datos en un archivo; si este existe, los datos son simplemente añadidos a él. El mecanismo de guardado se analizará más tarde, en el lugar en el que mostraremos la implementación de este método.

Creando un archivo para guardar el informe de optimización

Trabajar con los informes de optimización y guardar estos no es algo que se presuponga solo en el terminal, sino también en el programa creado, por eso, lo lógico será ubicar el método de creación del informe optimizado en esta biblioteca Dll. De la misma forma, debemos prever varios métodos de guardado de datos en el archivo, es decir, implementar la posibilidad tanto de guardar en un archivo, como de añadir cada elemento a un archivo ya existente, y si no ha habido archivo, tener la posibilidad de crearlo. Precisamente el último método se importará al terminal, sin embargo, dicho método también se usará en las clases de C#. Vamos a comenzar el análisis de los métodos implementados para el registro en el archivo con el informe, por la funcionalidad para añadir los datos al archivo. Con este objetivo se ha creado la clase ReportWriter. El lector podrá consultar su implementación completa en el archivo del proyecto anexo, en el artículo solo se muestran los métodos más interesantes de la misma. Comenzaremos describiendo cómo funciona esta clase. 

En primer lugar, hay que decir que contiene solo métodos estáticos: esto es necesario para poder exportar sus métodos a MQL5, por ello, la clase ha sido marcada con el mdificador público de acceso. En esta clase, hay un campo estático del tipo ReportItem, así como una serie de métodos que le añaden por turnos una serie de coeficientes y parámetros del robot.

/// <summary>
/// repositorio temporal de datos
/// </summary>
private static ReportItem ReportItem;
/// <summary>
/// depurando el repositorio temporal de datos
/// </summary>
public static void ClearReportItem()
{
    ReportItem = new ReportItem();
}

Asimismo, existe el método ClearReportItem(), que básicamente comprende la reinstalación de este campo. En este caso, además, perdemos el acceso al enlace al anterior ejemplar de este objeto, "perdiéndolo" también al mismo y comenzando el proceso de guardado desde el inicio. Los métodos que añaden los datos, los agrupan por bloques. La implementación de estos métodos es trivial, pero mostraremos sus signaturas para ofrecer una panorámica completa.  

/// <summary>
/// Añadiendo un parámetro del robot
/// </summary>
/// <param name="name">Nombre del parámetro</param>
/// <param name="value">Valor del parámetro</param>
public static void AppendBotParam(string name, string value);

/// <summary>
/// Añadiendo la lista principal de coeficientes
/// </summary>
/// <param name="payoff"></param>
/// <param name="profitFactor"></param>
/// <param name="averageProfitFactor"></param>
/// <param name="recoveryFactor"></param>
/// <param name="averageRecoveryFactor"></param>
/// <param name="totalTrades"></param>
/// <param name="pl"></param>
/// <param name="dd"></param>
/// <param name="altmanZScore"></param>
public static void AppendMainCoef(double payoff,
                                  double profitFactor,
                                  double averageProfitFactor,
                                  double recoveryFactor,
                                  double averageRecoveryFactor,
                                  int totalTrades,
                                  double pl,
                                  double dd,
                                  double altmanZScore);

/// <summary>
/// Añadiendo VaR
/// </summary>
/// <param name="Q_90"></param>
/// <param name="Q_95"></param>
/// <param name="Q_99"></param>
/// <param name="Mx"></param>
/// <param name="Std"></param>
public static void AppendVaR(double Q_90, double Q_95,
                             double Q_99, double Mx, double Std);

/// <summary>
/// Añadiendo PL / DD totales y los valores que les son propios
/// </summary>
/// <param name="profit"></param>
/// <param name="dd"></param>
/// <param name="totalProfitTrades"></param>
/// <param name="totalLooseTrades"></param>
/// <param name="consecutiveWins"></param>
/// <param name="consecutiveLoose"></param>
public static void AppendMaxPLDD(double profit, double dd,
                                 int totalProfitTrades, int totalLooseTrades,
                                 int consecutiveWins, int consecutiveLoose);

/// <summary>
/// Añadiendo un día concreto
/// </summary>
/// <param name="day"></param>
/// <param name="profit"></param>
/// <param name="dd"></param>
/// <param name="numberOfProfitTrades"></param>
/// <param name="numberOfLooseTrades"></param>
public static void AppendDay(int day,
                             double profit, double dd,
                             int numberOfProfitTrades,
                             int numberOfLooseTrades);

Debemos notar que el método que añade las estadísticas de las transacciones divididas en días, se debe llamar para cada uno de los 5 días comerciales. Si no lo llamamos para uno de los días, el archivo en el que se han guardado datos no será leído en lo sucesivo. Después de añadir los datos al campo de guardado de datos, podemos proceder a guardarlos, sin embargo, primero deberemos comprobar si existe un archivo, y si no existe, deberemos crearlo. Para crear un archivo, hemos añadido una serie de métodos.

/// <summary>
/// Método que crea un archivo si este no ha sido creado aún
/// </summary>
/// <param name="pathToBot">Ruta hasta el robot</param>
/// <param name="currency">Divisa del depósito</param>
/// <param name="balance">Balance</param>
/// <param name="laverage">Apalancamiento crediticio</param>
/// <param name="pathToFile">Ruta hasta el archivo</param>
private static void CreateFileIfNotExists(string pathToBot, string currency, double balance, int laverage, string pathToFile)
{
    if (File.Exists(pathToFile))
        return;
    using (var xmlWriter = new XmlTextWriter(pathToFile, null))
    {
        // estableciendo el formato del documento
        xmlWriter.Formatting = Formatting.Indented;
        xmlWriter.IndentChar = '\t';
        xmlWriter.Indentation = 1;

        xmlWriter.WriteStartDocument();

        // Creando la carpeta raíz del documento
        #region Document root
        xmlWriter.WriteStartElement("Optimisatin_Report");

        // Escribiendo la fecha de creación
        xmlWriter.WriteStartAttribute("Created");
        xmlWriter.WriteString(DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"));
        xmlWriter.WriteEndAttribute();

        #region Optimiser settings section 
        // Ajustes del optimizador
        xmlWriter.WriteStartElement("Optimiser_Settings");

        // Ruta hasta el robot
        WriteItem(xmlWriter, "Bot", pathToBot);
        // Depósito
        WriteItem(xmlWriter, "Deposit", balance.ToString(), new Dictionary<string, string> { { "Currency", currency } });
        // Apalancamiento crediticio
        WriteItem(xmlWriter, "Laverage", laverage.ToString());

        xmlWriter.WriteEndElement();
        #endregion

        #region Optimisation resultssection
        // nodo raíz de la lista de resultados de la optimización
        xmlWriter.WriteStartElement("Optimisation_Results");
        xmlWriter.WriteEndElement();
        #endregion

        xmlWriter.WriteEndElement();
        #endregion

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

/// <summary>
/// Guardando el elemento en el archivo
/// </summary>
/// <param name="writer">Escritor</param>
/// <param name="Name">Nombre del elemento</param>
/// <param name="Value">Valor del elemento</param>
/// <param name="Attributes">Atributos</param>
private static void WriteItem(XmlTextWriter writer, string Name, string Value, Dictionary<string, string> Attributes = null)
{
    writer.WriteStartElement("Item");

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

    if (Attributes != null)
    {
        foreach (var item in Attributes)
        {
            writer.WriteStartAttribute(item.Key);
            writer.WriteString(item.Value);
            writer.WriteEndAttribute();
        }
    }

    writer.WriteString(Value);

    writer.WriteEndElement();
}

Para ofrecer una panorámica más completa, se adjunta la implementación del método WriteItem, que contiene el código repetido de adición del elemento final con los datos y los atributos propios de este elemento al archivo. El propio elemento que crea el archivo — CreateFileIfNotExists —, después de comprobar si el archivo existe, lo crea y comienza a formar la estructura mínima imprescindible del archivo. 

En primer lugar, se crea la raíz del archivo, es decir, el tag <Optimisatin_Report/>: precisamente dentro de este tag se encuentran las estructuras hijas del archivo. A continuación, se rellenan los datos sobre la fecha de creación del archivo; esto se ha hecho así para que resulte más cómodo trabajar posteriormente con los archivos. Después de ello, creamos un nodo con los ajustes cambiados del optimizador y los introducimos de inmediato. A continuación, creamos una sección en la que guardaremos los resultados de la optimización, y la cerramos inmediatamente. Como resultado, obtendremos un archivo vacío con el bosquejo mínimo imprescindible. 


<Optimisatin_Report Created="24.10.2019 19:10:08">
        <Optimiser_Settings>
                <Item Name="Bot">Path to bot</Item>
                <Item Name="Deposit" Currency="Currency">1000</Item>
                <Item Name="Laverage">1</Item>
        </Optimiser_Settings>
        <Optimisation_Results />
</Optimisatin_Report>

Necesitamos dicho bosquejo para poder leer este archivo con la ayuda de la clase XmlDocument. Esta clase es la más cómoda para leer y modificar los documentos Xml existentes. Precisamente con su ayuda vamos a añadir los datos a un documento existente. Para que resulte más cómodo añadir los datos a un documento existente, hemos sacado de nuevo las operaciones que se repiten a módulos aparte:

/// <summary>
/// Guardando los atributos en el archivo
/// </summary>
/// <param name="item">nodo</param>
/// <param name="xmlDoc">Documento</param>
/// <param name="Attributes">Atributos</param>
private static void FillInAttributes(XmlNode item, XmlDocument xmlDoc, Dictionary<string, string> Attributes)
{
    if (Attributes != null)
    {
        foreach (var attr in Attributes)
        {
            XmlAttribute attribute = xmlDoc.CreateAttribute(attr.Key);
            attribute.Value = attr.Value;
            item.Attributes.Append(attribute);
        }
    }
}

/// <summary>
/// Añadiendo una sección
/// </summary>
/// <param name="xmlDoc">Documento</param>
/// <param name="xpath_parentSection">xpath para seleccionar los nodos padre</param>
/// <param name="sectionName">Nombre de la sección</param>
/// <param name="Attributes">Atributos</param>
private static void AppendSection(XmlDocument xmlDoc, string xpath_parentSection,
                                  string sectionName, Dictionary<string, string> Attributes = null)
{
    XmlNode section = xmlDoc.SelectSingleNode(xpath_parentSection);
    XmlNode item = xmlDoc.CreateElement(sectionName);

    FillInAttributes(item, xmlDoc, Attributes);

    section.AppendChild(item);
}

/// <summary>
/// Guardando elemento
/// </summary>
/// <param name="xmlDoc">Documento</param>
/// <param name="xpath_parentSection">xpath para seleccionar los nodos padre</param>
/// <param name="name">Nombre del elemento</param>
/// <param name="value">valor</param>
/// <param name="Attributes">Atributos</param>
private static void WriteItem(XmlDocument xmlDoc, string xpath_parentSection, string name,
                              string value, Dictionary<string, string> Attributes = null)
{
    XmlNode section = xmlDoc.SelectSingleNode(xpath_parentSection);
    XmlNode item = xmlDoc.CreateElement(name);
    item.InnerText = value;

    FillInAttributes(item, xmlDoc, Attributes);

    section.AppendChild(item);
}

El primer método FillInAttributes rellena los atributos para el nodo transmitido, y el método WriteItem guarda el elemento en la sección marcada a través de XPath, mientras que el método AppendSection añade la sección dentro de otra sección marcada a través de la ruta transmitida con la ayuda de Xpath. Estos bloques de código se usan con bastante frecuencia durante la adición de datos al archivo. El propio método que guarda los datos es bastante amplio y está dividido en bloques.

/// <summary>
/// Guardando los resultados de las transacciones en el archivo
/// </summary>
/// <param name="pathToBot">Ruta al robot</param>
/// <param name="currency">Divisa del depósito</param>
/// <param name="balance">Balance</param>
/// <param name="laverage">Apalancamiento crediticio</param>
/// <param name="pathToFile">Ruta hasta el archivo</param>
/// <param name="symbol">Símbolo</param>
/// <param name="tf">Marco temporal</param>
/// <param name="StartDT">Fecha de inicio de las transacciones</param>
/// <param name="FinishDT">Fecha de finalización de las transacciones</param>
public static void Write(string pathToBot, string currency, double balance,
                         int laverage, string pathToFile, string symbol, int tf,
                         ulong StartDT, ulong FinishDT)
{
    // Creando el archivo, si este no existe
    CreateFileIfNotExists(pathToBot, currency, balance, laverage, pathToFile);
            
    ReportItem.Symbol = symbol;
    ReportItem.TF = tf;

    // Creamos el documento y leemos el archivo con su ayuda
    XmlDocument xmlDoc = new XmlDocument();
    xmlDoc.Load(pathToFile);

    #region Append result section
    // Escribiendo la solicitud de paso a la sección con los resultados de la optimización 
    string xpath = "Optimisatin_Report/Optimisation_Results";
    // Añadiendo una nueva sección con los resultados de la optimización
    AppendSection(xmlDoc, xpath, "Result",
                  new Dictionary<string, string>
                  {
                      { "Symbol", symbol },
                      { "TF", tf.ToString() },
                      { "Start_DT", StartDT.ToString() },
                      { "Finish_DT", FinishDT.ToString() }
                  });
    // Añadiendo la sección con los coeficientes de las optimizaciones
    AppendSection(xmlDoc, $"{xpath}/Result[last()]", "Coefficients");
    // Añadiendo la sección con  VaR
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients", "VaR");
    // Añadiendo la sección con PL / DD totales
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients", "Max_PL_DD");
    // Añadiendo la sección con los resultados de las transacciones por días
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients", "Trading_Days");
    // Añadiendo la sección con los resultados de las transacciones del lunes
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Mn");
    // Añadiendo la sección con los resultados de las transacciones del martes
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Tu");
    // Añadiendo la sección con los resultados de las transacciones del miércoles
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "We");
    // Añadiendo la sección con los resultados de las transacciones del jueves
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Th");
    // Añadiendo la sección con los resultados de las transacciones del viernes
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Fr");
    #endregion

    #region Append Bot params
    // Iterando por los parámetros del robot
    foreach (var item in ReportItem.BotParams)
    {
        // Escribiendo el parámetro seleccionado del robot
        WriteItem(xmlDoc, "Optimisatin_Report/Optimisation_Results/Result[last()]",
                  "Item", item.Value, new Dictionary<string, string> { { "Name", item.Key } });
    }
    #endregion

    #region Append main coef
    // Estableciendo la ruta hasta el nodo con los coeficientes
    xpath = "Optimisatin_Report/Optimisation_Results/Result[last()]/Coefficients";

    // Guardando los coeficientes
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.Payoff.ToString(), new Dictionary<string, string> { { "Name", "Payoff" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.ProfitFactor.ToString(), new Dictionary<string, string> { { "Name", "Profit factor" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.AverageProfitFactor.ToString(), new Dictionary<string, string> { { "Name", "Average Profit factor" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.RecoveryFactor.ToString(), new Dictionary<string, string> { { "Name", "Recovery factor" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.AverageRecoveryFactor.ToString(), new Dictionary<string, string> { { "Name", "Average Recovery factor" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.TotalTrades.ToString(), new Dictionary<string, string> { { "Name", "Total trades" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.PL.ToString(), new Dictionary<string, string> { { "Name", "PL" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.DD.ToString(), new Dictionary<string, string> { { "Name", "DD" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.AltmanZScore.ToString(), new Dictionary<string, string> { { "Name", "Altman Z Score" } });
    #endregion

    #region Append VaR
    // Estableciendo la ruta hasta el nodo con VaR
    xpath = "Optimisatin_Report/Optimisation_Results/Result[last()]/Coefficients/VaR";

    // Guardando los resultados de VaR
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Q_90.ToString(), new Dictionary<string, string> { { "Name", "90" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Q_95.ToString(), new Dictionary<string, string> { { "Name", "95" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Q_99.ToString(), new Dictionary<string, string> { { "Name", "99" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Mx.ToString(), new Dictionary<string, string> { { "Name", "Mx" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Std.ToString(), new Dictionary<string, string> { { "Name", "Std" } });
    #endregion

    #region Append max PL and DD
    // Estableciendo la ruta hasta el nodo con PL / DD totales
    xpath = "Optimisatin_Report/Optimisation_Results/Result[last()]/Coefficients/Max_PL_DD";

    // Guardando los coeficientes
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.Profit.Value.ToString(), new Dictionary<string, string> { { "Name", "Profit" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.DD.Value.ToString(), new Dictionary<string, string> { { "Name", "DD" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.Profit.TotalTrades.ToString(), new Dictionary<string, string> { { "Name", "Total Profit Trades" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.DD.TotalTrades.ToString(), new Dictionary<string, string> { { "Name", "Total Loose Trades" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.Profit.ConsecutivesTrades.ToString(), new Dictionary<string, string> { { "Name", "Consecutive Wins" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.DD.ConsecutivesTrades.ToString(), new Dictionary<string, string> { { "Name", "Consecutive Loose" } });
    #endregion

    #region Append Days
    foreach (var item in ReportItem.OptimisationCoefficients.TradingDays)
    {
        // Estableciendo la ruta hasta el nodo de un día concreto
        xpath = "Optimisatin_Report/Optimisation_Results/Result[last()]/Coefficients/Trading_Days";
        // Seleccionando el día
        switch (item.Key)
        {
            case DayOfWeek.Monday: xpath += "/Mn"; break;
            case DayOfWeek.Tuesday: xpath += "/Tu"; break;

            case DayOfWeek.Wednesday: xpath += "/We"; break;
            case DayOfWeek.Thursday: xpath += "/Th"; break;
            case DayOfWeek.Friday: xpath += "/Fr"; break;
        }

        // Guardando los resultados
        WriteItem(xmlDoc, xpath, "Item", item.Value.Profit.Value.ToString(), new Dictionary<string, string> { { "Name", "Profit" } });
        WriteItem(xmlDoc, xpath, "Item", item.Value.DD.Value.ToString(), new Dictionary<string, string> { { "Name", "DD" } });
        WriteItem(xmlDoc, xpath, "Item", item.Value.Profit.Trades.ToString(), new Dictionary<string, string> { { "Name", "Number Of Profit Trades" } });
        WriteItem(xmlDoc, xpath, "Item", item.Value.DD.Trades.ToString(), new Dictionary<string, string> { { "Name", "Number Of Loose Trades" } });
    }
    #endregion

    // Guardando de nuevo el archivo con los cambios introducidos
    xmlDoc.Save(pathToFile);

    // Depurando la variable donde se guardan los resultados anotados en el archivo
    ClearReportItem();
}

Primero, cargamos todo el documento en la memoria, y después, añadimos la sección. Vamos a analizar con mayor detalle el formato Xpath de la solicitud que transmite la ruta del nodo raíz.  

$"{xpath}/Result[last()]/Coefficients"

En la variable xpath ya se ha registrado la ruta hasta el nodo donde se guardan los elementos de las pasadas optimizadas. En este nodo se guardan los nodos con los resultados de las optimizaciones, y se pueden representar en forma de matrices de estructuras. Esta construcción, Result[last()], selecciona el último elemento de la matriz presentada, después de la cual transmitimos la ruta hasta el nodo incorporado /Coefficients. Según el principio descrito, seleccionamos el nodo necesario con los resultados de la optimización. 

La siguiente etapa consistirá en añadir los parámetros del robot: en ella, añadiremos los parámetros en el ciclo directamente al directorio con los resultados, después de lo cual, añadiremos una serie de coeficientes al directorio con los coeficientes; su adición se divide en bloques. Para finalizar, guardamos los resultados y depuramos el repositorio temporal de datos. Así, obtenemos un archivo con la lista de parámetros y resultados de las optimizaciones. Para separar los flujos durante las operaciones asincrónicas iniciadas desde una serie de procesos (precisamente de esta forma se realiza la optimización en el simulador al usar varios procesadores), se ha creado otro método de guardado que utiliza la separación de flujos con la ayuda de algoritmos de exclusión mutua (mutex) nombrados.

/// <summary>
/// Guardando en el archivo con bloqueo a través de un mutex nombrado
/// </summary>
/// <param name="mutexName">Nombre del mutex</param>
/// <param name="pathToBot">Ruta al robot</param>
/// <param name="currency">Divisa del depósito</param>
/// <param name="balance">Balance</param>
/// <param name="laverage">Apalancamiento crediticio</param>
/// <param name="pathToFile">Ruta hasta el archivo</param>
/// <param name="symbol">Símbolo</param>
/// <param name="tf">Marco temporal</param>
/// <param name="StartDT">Fecha de inicio de las transacciones</param>
/// <param name="FinishDT">Fecha de finalización de las transacciones</param>
/// <returns></returns>
public static string MutexWriter(string mutexName, string pathToBot, string currency, double balance,
                                 int laverage, string pathToFile, string symbol, int tf,
                                 ulong StartDT, ulong FinishDT)
{
    string ans = "";
    // Bloqueando mutex
    Mutex m = new Mutex(false, mutexName);
    m.WaitOne();
    try
    {
        // guardando en el archivo
        Write(pathToBot, currency, balance, laverage, pathToFile, symbol, tf, StartDT, FinishDT);
    }
    catch (Exception e)
    {
        // Detectando el error, si lo ha habido
        ans = e.Message;
    }

    // Liberando el mutex
    m.ReleaseMutex();
    // Retornando el texto del error
    return ans;
}

Este método guarda los datos con la ayuda del método anterior. Sin embargo, el proceso de guardado está envuelto por un mutex y en el bloque try-catch; esto último es necesario para que, independientemente de los posible errores que sucedan durante el guardado, el mutex quede libre, ya que de lo contrario, nos arriesgamos a que el proceso quede bloqueado y la optimización no pueda continuar. Estos métodos también se usan en la estructura OptimisationResult, en el método WriteResult.

/// <summary>
/// Método que añade el parámetro actual a un archivo existente o crea un nuevo archivo con el parámetro actual
/// </summary>
/// <param name="pathToBot">Ruta relativa hasta el robot desde la carpeta con los expertos</param>
/// <param name="currency">Divisa del depósito</param>
/// <param name="balance">Balance</param>
/// <param name="laverage">Apalancamiento crediticio</param>
/// <param name="pathToFile">Ruta hasta el archivo</param>
public void WriteResult(string pathToBot,
                        string currency, double balance,
                        int laverage, string pathToFile)
{
    try
    {
        foreach (var param in report.BotParams)
        {
            ReportWriter.AppendBotParam(param.Key, param.Value);
        }
        ReportWriter.AppendMainCoef(GetResult(ReportManager.SortBy.Payoff),
                                    GetResult(ReportManager.SortBy.ProfitFactor),
                                    GetResult(ReportManager.SortBy.AverageProfitFactor),
                                    GetResult(ReportManager.SortBy.RecoveryFactor),
                                    GetResult(ReportManager.SortBy.AverageRecoveryFactor),
                                    (int)GetResult(ReportManager.SortBy.TotalTrades),
                                    GetResult(ReportManager.SortBy.PL),
                                    GetResult(ReportManager.SortBy.DD),
                                    GetResult(ReportManager.SortBy.AltmanZScore));

        ReportWriter.AppendVaR(GetResult(ReportManager.SortBy.Q_90), GetResult(ReportManager.SortBy.Q_95),
                               GetResult(ReportManager.SortBy.Q_99), GetResult(ReportManager.SortBy.Mx),
                               GetResult(ReportManager.SortBy.Std));

        ReportWriter.AppendMaxPLDD(GetResult(ReportManager.SortBy.ProfitFactor), GetResult(ReportManager.SortBy.MaxDD),
                                  (int)GetResult(ReportManager.SortBy.MaxProfitTotalTrades),
                                  (int)GetResult(ReportManager.SortBy.MaxDDTotalTrades),
                                  (int)GetResult(ReportManager.SortBy.MaxProfitConsecutivesTrades),
                                  (int)GetResult(ReportManager.SortBy.MaxDDConsecutivesTrades));


        foreach (var day in report.OptimisationCoefficients.TradingDays)
        {
            ReportWriter.AppendDay((int)day.Key, day.Value.Profit.Value, day.Value.Profit.Value,
                                   day.Value.Profit.Trades, day.Value.DD.Trades);
        }

        ReportWriter.Write(pathToBot, currency, balance, laverage, pathToFile, report.Symbol, report.TF,
                           report.DateBorders.From.DTToUnixDT(), report.DateBorders.Till.DTToUnixDT());
    }
    catch (Exception e)
    {
        ReportWriter.ClearReportItem();
        throw e;
    }
}

En este método, añadimos secuencialmente los resultados de las optimizaciones al repositorio temporal, después, tras llamar al método Write, los guardamos en el archivo existente o creamos uno nuevo, si había sido creado antes. 

El método descrito para el guardado de los datos obtenidos es imprescindible a la hora de añadir informarción a un archivo formado anteriormente. No obstante, si necesitamos guardar una serie de datos de una pasada, para ello merecerá la pena usar otro método, que ha sido implementado como una ampliación para la interfaz IEnumerable<OptimisationResult>. Una vez hecho esto, tendremos la posibilidad de guardar los datos de todas las listas heredadas de la interfaz presentada. 

public static void ReportWriter(this IEnumerable<OptimisationResult> results, string pathToBot,
                                string currency, double balance,
                                int laverage, string pathToFile)
{
    // Eliminamos el archivo, si este ya existe
    if (File.Exists(pathToFile))
        File.Delete(pathToFile);

    // Creando un escritor 
    using (var xmlWriter = new XmlTextWriter(pathToFile, null))
    {
        // Estableciendo el formato del documento
        xmlWriter.Formatting = Formatting.Indented;
        xmlWriter.IndentChar = '\t';
        xmlWriter.Indentation = 1;

        xmlWriter.WriteStartDocument();

        // Nodo raíz del documento
        xmlWriter.WriteStartElement("Optimisatin_Report");

        // Escribiendo los atributos
        WriteAttribute(xmlWriter, "Created", DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"));

        // Escribiendo los ajustes del optimizador en un archivo
        #region Optimiser settings section 
        xmlWriter.WriteStartElement("Optimiser_Settings");

        WriteItem(xmlWriter, "Bot", pathToBot); // ruta hasta el robot
        WriteItem(xmlWriter, "Deposit", balance.ToString(), new Dictionary<string, string> { { "Currency", currency } }); // Divisa y depósito
        WriteItem(xmlWriter, "Laverage", laverage.ToString()); // apalancamiento crediticio

        xmlWriter.WriteEndElement();
        #endregion

        // Escribiendo en un archivo los propios resultados de las optimizaciones
        #region Optimisation result section
        xmlWriter.WriteStartElement("Optimisation_Results");

        // Ciclo por los resultados de las optimizaciones
        foreach (var item in results)
        {
            // Escribiendo el resultado concreto
            xmlWriter.WriteStartElement("Result");

            // Escribiendo los atributos de esta pasada de optimización
            WriteAttribute(xmlWriter, "Symbol", item.report.Symbol); // Símbolo
            WriteAttribute(xmlWriter, "TF", item.report.TF.ToString()); // Marco temporal
            WriteAttribute(xmlWriter, "Start_DT", item.report.DateBorders.From.DTToUnixDT().ToString()); // Fecha de comienzo de la optimización
            WriteAttribute(xmlWriter, "Finish_DT", item.report.DateBorders.Till.DTToUnixDT().ToString()); // Fecha de finalización de la optimización

            // Guardando el resultado de la optimización
            WriteResultItem(item, xmlWriter);

            xmlWriter.WriteEndElement();
        }

        xmlWriter.WriteEndElement();
        #endregion

        xmlWriter.WriteEndElement();

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

El método presentado introduce elemento a elemento en el archivo los informes de las optimizaciones, uno tras otro, hasta que se acaben los datos en la matriz. Si el archivo sobre la ruta transmitida no existe, se sustituye por otro. Para comenzar, creamos el escritor de archivos y lo configuramos. A continuación, repitiendo la estructura del archivo que hemos descrito anteriormente, introducimos secuencialmente los ajustes del optimizador y los propios resultados de las optimizaciones. Los últimos, además, como podemos ver por el fragmento de código expuesto, se introducen en un ciclo que itera los elementos de la colección en cuyo ejemplar se ha llamado el método descrito. Dentro del ciclo, delegamos el guardado de datos en un método creado para introducir en el archivo los datos de un elemento concreto.

/// <summary>
/// Guardando una pasada de optimización concreta
/// </summary>
/// <param name="resultItem">Valor de la pasada optimizada</param>
/// <param name="writer">Escritor</param>
private static void WriteResultItem(OptimisationResult resultItem, XmlTextWriter writer)
{
    // Guardando los coeficientes
    #region Coefficients
    writer.WriteStartElement("Coefficients");

    // Escribiendo VaR
    #region VaR
    writer.WriteStartElement("VaR");

    WriteItem(writer, "90", resultItem.GetResult(SortBy.Q_90).ToString()); // Cuantil 90
    WriteItem(writer, "95", resultItem.GetResult(SortBy.Q_95).ToString()); // Cuantil 95
    WriteItem(writer, "99", resultItem.GetResult(SortBy.Q_99).ToString()); // Cuantil 99
    WriteItem(writer, "Mx", resultItem.GetResult(SortBy.Mx).ToString()); // Medio de PL
    WriteItem(writer, "Std", resultItem.GetResult(SortBy.Std).ToString()); // desviación media cuadrática de PL

    writer.WriteEndElement();
    #endregion

    // Escribiendo los parámetros de PL / DD - puntos extremos
    #region Max PL DD
    writer.WriteStartElement("Max_PL_DD");
    WriteItem(writer, "Profit", resultItem.GetResult(SortBy.MaxProfit).ToString()); // Beneficio total
    WriteItem(writer, "DD", resultItem.GetResult(SortBy.MaxDD).ToString()); // Pérdidas totales
    WriteItem(writer, "Total Profit Trades", ((int)resultItem.GetResult(SortBy.MaxProfitTotalTrades)).ToString()); // Número total de transacciones rentables
    WriteItem(writer, "Total Loose Trades", ((int)resultItem.GetResult(SortBy.MaxDDTotalTrades)).ToString()); // Número total de transacciones con pérdidas
    WriteItem(writer, "Consecutive Wins", ((int)resultItem.GetResult(SortBy.MaxProfitConsecutivesTrades)).ToString()); // Transacciones rentables seguidas
    WriteItem(writer, "Consecutive Loose", ((int)resultItem.GetResult(SortBy.MaxDDConsecutivesTrades)).ToString()); // Transacciones con pérdidas seguidas
    writer.WriteEndElement();
    #endregion

    // Escribiendo los resultados por días
    #region Trading_Days

    // Método que escribe los resultados de las transacciones
    void AddDay(string Day, double Profit, double DD, int ProfitTrades, int DDTrades)
    {
        writer.WriteStartElement(Day);

        WriteItem(writer, "Profit", Profit.ToString()); // beneficio
        WriteItem(writer, "DD", DD.ToString()); // pérdidas
        WriteItem(writer, "Number Of Profit Trades", ProfitTrades.ToString()); // número de transacciones rentables
        WriteItem(writer, "Number Of Loose Trades", DDTrades.ToString()); // número de transacciones con pérdidas

        writer.WriteEndElement();
    }

    writer.WriteStartElement("Trading_Days");

    // Lu
    AddDay("Mn", resultItem.GetResult(SortBy.AverageDailyProfit_Mn),
                 resultItem.GetResult(SortBy.AverageDailyDD_Mn),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Mn),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Mn));
    // Ma
    AddDay("Tu", resultItem.GetResult(SortBy.AverageDailyProfit_Tu),
                 resultItem.GetResult(SortBy.AverageDailyDD_Tu),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Tu),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Tu));
    // Mi
    AddDay("We", resultItem.GetResult(SortBy.AverageDailyProfit_We),
                 resultItem.GetResult(SortBy.AverageDailyDD_We),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_We),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_We));
    // Ju
    AddDay("Th", resultItem.GetResult(SortBy.AverageDailyProfit_Th),
                 resultItem.GetResult(SortBy.AverageDailyDD_Th),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Th),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Th));
    // Vi
    AddDay("Fr", resultItem.GetResult(SortBy.AverageDailyProfit_Fr),
                 resultItem.GetResult(SortBy.AverageDailyDD_Fr),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Fr),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Fr));

    writer.WriteEndElement();
    #endregion

    // Escribiendo los demás coeficientes
    WriteItem(writer, "Payoff", resultItem.GetResult(SortBy.Payoff).ToString());
    WriteItem(writer, "Profit factor", resultItem.GetResult(SortBy.ProfitFactor).ToString());
    WriteItem(writer, "Average Profit factor", resultItem.GetResult(SortBy.AverageProfitFactor).ToString());
    WriteItem(writer, "Recovery factor", resultItem.GetResult(SortBy.RecoveryFactor).ToString());
    WriteItem(writer, "Average Recovery factor", resultItem.GetResult(SortBy.AverageRecoveryFactor).ToString());
    WriteItem(writer, "Total trades", ((int)resultItem.GetResult(SortBy.TotalTrades)).ToString());
    WriteItem(writer, "PL", resultItem.GetResult(SortBy.PL).ToString());
    WriteItem(writer, "DD", resultItem.GetResult(SortBy.DD).ToString());
    WriteItem(writer, "Altman Z Score", resultItem.GetResult(SortBy.AltmanZScore).ToString());

    writer.WriteEndElement();
    #endregion

    // Escribiendo los coeficientes del robot
    #region Bot params
    foreach (var item in resultItem.report.BotParams)
    {
        WriteItem(writer, item.Key, item.Value);
    }
    #endregion
}

La implementación del método que introduce los archivos en un archivo es muy sencilla, aunque es bastante larga. Después de crear los apartados correspondientes y rellenar los atributos, este método introduce los datos de VaR de la pasada de optimización realizada y los valores que caracterizan el beneficio y las pérdidas máximos. Para guardar los resultados de las optimizaciones de un día concreto, se ha creado una función incorporada que es llamada 5 veces para cada uno de los días. Como resultado, se introducen los coeficientes que no han sido agrupados en sebgrupos, así como los parámetros del robot. Dado que la descripción del robot se realiza en un ciclo para cada uno de los elementos, los datos no se guardan en el archivo hasta que se llame el método xmlWriter.Close(); esto se hace en el método principal de guardado. De esta forma, el uso de este método de ampliación para el guardado de la matriz de datos es más rápido en comparación con el analizado anteriormente. Con esto, podemos dar por finalizados los procedimientos de guardado de datos en un archivo. Ahora, vamos a pasar al siguiente punto lógico: la lectura de datos desde el archivo obtenido.

Lectura de un archivo con el informe de optimización

Dado que para procesar la información obtenida y representar la misma es necesario leer los archivos generados, deberemos crear un mecanismo que ejecute esta acción. Como se supone, el mecanismo de lectura de datos ha sido introducido en una clase aparte, que tiene la siguiente signatura.

public class ReportReader : IDisposable
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="path">Ruta hasta el archivo</param>
        public ReportReader(string path);

        /// <summary>
        /// Proveedor del formato de número binarios
        /// </summary>
        private readonly NumberFormatInfo formatInfo = new NumberFormatInfo { NumberDecimalSeparator = "." };

        #region DataKeepers
        /// <summary>
        /// Presentación del archivo con un informe en formato POO
        /// </summary>
        private readonly XmlDocument document = new XmlDocument();

        /// <summary>
        /// Selección de los nodos del documento (colección de líneas en la tabla excel)
        /// </summary>
        private readonly System.Collections.IEnumerator enumerator;
        #endregion

        /// <summary>
        /// el elemento actual del informe ha sido leído
        /// </summary>
        public ReportItem? ReportItem { get; private set; } = null;

        #region Optimiser settings
        /// <summary>
        /// Ruta hasta el robot
        /// </summary>
        public string RelativePathToBot { get; }

        /// <summary>
        /// Balance
        /// </summary>
        public double Balance { get; }

        /// <summary>
        /// Divisa
        /// </summary>
        public string Currency { get; }

        /// <summary>
        /// Apalancamiento
        /// </summary>
        public int Laverage { get; }
        #endregion

        /// <summary>
        /// Fecha de creación del archivo
        /// </summary>
        public DateTime Created { get; }

        /// <summary>
        /// Método que lee el archivo
        /// </summary>
        /// <returns></returns>
        public bool Read();

        /// <summary>
        /// Método que retorna la línea que selecciona un elemento según su nombre (atributo Name)
        /// </summary>
        /// <param name="Name"></param>
        /// <returns></returns>
        private string SelectItem(string Name) => $"Item[@Name='{Name}']";

        /// <summary>
        /// Obteniendo el valor del resultado de las transacciones del día seleccionado
        /// </summary>
        /// <param name="dailyNode">Nodo del día dado</param>
        /// <returns></returns>
        private DailyData GetDay(XmlNode dailyNode);

        /// <summary>
        /// Reseteando el lector de cotizaciones
        /// </summary>
        public void ResetReader();

        /// <summary>
        /// Depurando el documento
        /// </summary>
        public void Dispose() => document.RemoveAll();
    }

A pesar de que hemos presentado su estructura con comentarios, vamos a describir sus componentes para mayor claridad. Esta clase se hereda de la interfaz iDisposable. No resulta imprescindible, pero se ha hecho por prevención. Ahora, vamos a describir la clase que contiene el método Dispasable, necesario para la implementación, y cuya llamada conlleva la depuración del objeto document. Aquel contiene un archivo cargado en la memoria con los resultados de la optmización.

Este enfoque resulta cómodo porque, al instanciar una clase heredada de la interfaz indicada más arriba, esta debe envolverse en la construcción using, que llama de forma automática el método establecido al salir de los límites del bloque de la estructura using. Por ello, aunque queramos, no podremos guardar en la memoria un documento leído y, por consiguiente, reduciremos la memoria cargada por nuestra aplicación durante su ejecución.

La clase descrita para la implementación de la lectura línea por línea de un documento, usa el Enumerator recibido del documento leído. Los valores leídos se introducen en una propiedad especialmente reservada, proporcionando con ello acceso a los datos. Aparte de esto, al instanciar una clase, se rellenan las propiedades que indican los principales ajustes del optimizador, así como la fecha y la hora de creación del archivo. Para nivelar la influencia de los ajustes de localización del SO, tanto al registrar datos, como al leer el archivo, se indica el formato del separador de cifras con precisión doble. Al leer el archivo pro primera vez, esta clase se debe resetear al inicio de la lista, si se necesita realizar una nueva lectura; por eso se declara el método ResetReader, que resetea el Enumerator indicado anteriormente al comienzo de la lista. El constructor de esta clase se ha implementado de tal forma que se puedan rellenar todas las propiedades necesarias, así como preparar la clase para su posterior uso.

public ReportReader(string path)
{
    // cargando el documento
    document.Load(path);

    // Obteniendo la fecha de creación del archivo
    Created = DateTime.ParseExact(document["Optimisatin_Report"].Attributes["Created"].Value, "dd.MM.yyyy HH:mm:ss", null);
    // Obteniendo enumerator
    enumerator = document["Optimisatin_Report"]["Optimisation_Results"].ChildNodes.GetEnumerator();

    // Función de obtención del parámetro
    string xpath(string Name) { return $"/Optimisatin_Report/Optimiser_Settings/Item[@Name='{Name}']"; }

    // Obtenemos la ruta hasta el robot
    RelativePathToBot = document.SelectSingleNode(xpath("Bot")).InnerText;

    // Obtenemos el balance y la divisa del depósito
    XmlNode Deposit = document.SelectSingleNode(xpath("Deposit"));
    Balance = Convert.ToDouble(Deposit.InnerText.Replace(",", "."), formatInfo);
    Currency = Deposit.Attributes["Currency"].Value;

    // Obtenemos el apalancamiento crediticio
    Laverage = Convert.ToInt32(document.SelectSingleNode(xpath("Laverage")).InnerText);
}

En primer lugar, carga el documento transmitido y rellena la fecha de su creación. El enumerator obtenido al instanciar las clases pertenece a los nodos hijos del documento, que se encuentran en el apartado Optimisatin_Report/Optimisation_Results, dicho en otras palabras, los que tienen el tag <Result/>. Para obtener los parámetros del optimizador buscados, se usa la indicación de la ruta hasta el nodo necesario del documento, con la ayuda de la marca xpath. Un análogo de esta función incorporada, solo que con una ruta más corta, es el método SelectItem, que indica la ruta hasta un elemento entre los nodos del documento que tienen el tag <Item/>, de acuerdo con su atributo Name. Método GetDay, que convierte el nodo dado de un documento en la estructura correspondiente del informe de transacciones por días. Entre todos los métodos de esta clase, solo nos queda por describir el método de lectura de datos. Puesto que su implementación es larga, lo mostraremos de forma abreviada.   

public bool Read()
{
    if (enumerator == null)
        return false;

    // Leyendo el próximo elemento
    bool ans = enumerator.MoveNext();
    if (ans)
    {
        // Nodo actual
        XmlNode result = (XmlNode)enumerator.Current;
        // elemento actual del informe
        ReportItem = new ReportItem[...]

        // Rellenando los parámetros del robot
        foreach (XmlNode item in result.ChildNodes)
        {
            if (item.Name == "Item")
                ReportItem.Value.BotParams.Add(item.Attributes["Name"].Value, item.InnerText);
        }

    }
    return ans;
}

En el segmento abreviado de código se oculta una operación de instanciación del informe de optimización y el rellenado de sus campos con los datos leídos. Esta operación es demasiado larga para mostrarla en el artículo, y tiene un formato demasiado monótono de acciones, que consisten en la conversión del formato string en el formato buscado. El ciclo formado a continuación rellena con datos los parámetros del robot leídos línea a línea desde el archivo. Toda esta operación se ejecuta solo con la condición de que no alcancemos la línea final del archivo. Como resultado de la operación se retorna un indicador que establece si se ha leído o no esta línea, que, como podemos comprender por el código y la descripción, también indica si se ha alcanzado el final del archivo.

Filtrado multifactorial y clasificación del informe con las optimizaciones

Para cumplir con las tareas establecidas, hemos creado dos enumeraciones que indican la dirección de la clasificación (SortMethd y OrderBy), cuyo contenido es equivalente. En general, podríamos prescindir de una de las dos. Sin embargo, para que resulte más cómodo trabajar y dividir los métodos de claficiación, hemos creado precisamente dos enumeraciones en lugar de una. Su tarea consiste en mostrar la dirección en orden ascendente o descendente. El tipo de proporciones de los coeficientes con el valor transmitido se indica mediante banderas, cuya esencia consiste en establecer la condición de la comparación.    

/// <summary>
/// Tipo de filtrado
/// </summary>
[Flags]
public enum CompareType
{
    GraterThan = 1, // mayor 
    LessThan = 2, // menor
    EqualTo = 4 // igual
}

Los tipos de coeficiente que posibilitan el filtrado y la clasificación son descritos por la enumeración OrderBy, que ya hemos analizado anteriormente. Regresando a los métodos de clasificación y filtrado analizados, merece la pena destacar que todos han sido implementados en forma de métodos de expansión de las colecciones heredadas de la interfaz IEnumerable<OptimisationResult>. El mecanismo de filtrado de datos es trivial; en él comprobamos elemento a elemento cada uno de los coeficientes para ver si cumplen las condiciones establecidas y rechazamos de las pasadas de optimización aquellos en los que algún coeficiente no cumpla con las condiciones establecidas. Además, usamos para el filtrado el ciclo condicional por elementos Where, contenido en la interfaz IEnumerable. El propio método se implementa de la forma siguiente.

/// <summary>
/// Método que filtra la optimización
/// </summary>
/// <param name="results">colección actual</param>
/// <param name="compareData">Colección de coeficientes y tipos de filtrado</param>
/// <returns>Colección filtrada</returns>
public static IEnumerable<OptimisationResult> FiltreOptimisations(this IEnumerable<OptimisationResult> results,
                                                                  IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData)
{
    // Función que clasifica los resultados
    bool Compare(double _data, KeyValuePair<CompareType, double> compareParams)
    {
        // Resultado de la comparación
        bool ans = false;
        // comparando si es igual
        if (compareParams.Key.HasFlag(CompareType.EqualTo))
        {
            ans = compareParams.Value == _data;
        }
        // Comparando si es mayor que el actual
        if (!ans && compareParams.Key.HasFlag(CompareType.GraterThan))
        {
            ans = _data > compareParams.Value;
        }
        // Comparando si es menor que el actual
        if (!ans && compareParams.Key.HasFlag(CompareType.LessThan))
        {
            ans = _data < compareParams.Value;
        }

        return ans;
    }
    // COndiciones de clasificación
    bool Sort(OptimisationResult x)
    {
        // Ciclo por los parámetros de clasificación transmitidos
        foreach (var item in compareData)
        {
            // comprobando si el parámetro transmitido y el actual se corresponden
            if (!Compare(x.GetResult(item.Key), item.Value))
                return false;
        }

        return true;
    }

    // Filtrado
    return results.Where(x => Sort(x));
}

Como podemos notar, dentro del método se han implmentado dos funciones, cada una de las cuales ejecuta su parte del filtrado de datos. Vamos a analizarlas comenzando por la función final:

  • Compare — su tarea consiste en comparar entre sí el valor transmitido representado en forma de KeyValuePair el valor indicado en el método buscado. Dado que durante la comparación podríamos necesitar comparar no solo si los datos son mayores o iguales, sino también condiciones mixtas, hemos utilizado las ventajas de las banderas presentadas para acortar la implementación de la tarea planteada. Como ya sabemos, la bandera supone un bit, mientras que el campo int contiene 8 bits, por consiguiente, para el campo int podemos tener hasta ocho banderas puestas o quitadas simultáneamente. La comprobación de las banderas puede realizarse de forma consecutiva, sin necesidad de describir una serie de ciclos o condiciones ingentes, por eso hemos podido reducirla a tres condiciones. Además, en la interfaz gráfica que analizaremos más tarde, también resulta cómodo utilizar las banderas para establecer los parámetros de comparación. Dentro de la función analizada, comprobamos una a una las banderas y la correspondencia de los datos comparados con las banderas solicitadas.  
  • Sort — a diferencia del método anterior, se ha diseñado para comparar los resultados no de una parámetro concreto, sino de una serie de parámetros registrados. Para la tarea establecida, iteraremos en un ciclo elemento a elemento por todas las banderas transmitidas al filtrado y usaremos la función anteriormente descrita para saber si el parámetro seleccionado se corresponde con las limitaciones establecidas. Para que podamos tomar el valor de un elemento concreto seleccionado en el ciclo sin tener que aplicar el "Switch case" del operador, se usa el método OptimidationResult.GetResult(OrderBy item), anteriormente analizado. Si el valor transmitido no cumple con lo solicitado, retornaremos false, filtrando con ello los datos que no nos convienen.

Para clasificar los datos, utilizaremos el método Where, ya mencionado, que forma automáticamente una lista a partir de los valores convenientes según las condiciones. Precisamente dicha lista se retorna en forma de resultado de la ejecución del método de expasión.  

Si en lo que respecta al filtrado de datos todo está claro, la clasificación, a su vez, podría generar ciertos malentendidos, y por ello, antes de describir la implementación del código, merece la pena que analicemos el propio mecanismo con un ejemplo concreto. Supongamos que tenemos los parámetros de los factores Profit y Recovery, y que necesitamos clasificar los datos de ambos indicadores. Si los clasificamos secuencialmente, seguiremos obteniendo como resultado los datos clasificados conforme al último indicador utilizado. Resulta obvio que necesitamos compararlos de alguna forma.

Profit Profit factor Recovery factor
5000 1 9
15000 1.2 5
-11000 0.5 -2
0 0 0
10000 2 5
7000 1 4

Como ya sabemos, ninguno de los coeficientes está normalizado dentro de sus límites, y también podemos ver por el recuadro que ambos coeficientes tienen una dispersión considerable el uno respecto al otro. De ello podemos concluir que es necesario, no solo darle a los datos una forma normalizada, sino también conservar su carácter secuencial. El modo estándar de conversión de los datos a su aspecto normalizado será la división de cada uno de ellos por el valor máximo de la serie, obteniendo también de esta manera una serie de datos que oscilará en el intervalo [0;1]. Pero antes, debemos encontrar los puntos extremos de esta serie, que se representan en el recuadro.


Profit factor  Recovery factor
Min  0 -2  
Max  2 9

Como podemos ver por el recuadro, Recovery factor tiene un valor negativo, y esto significa que partir de los principios aritméticos de nuestro enfoque no resultara efectivo, puesto que uno de los valores obtenidos claramente no puede encajar en los límites establecidos. Para nivelar esta influencia, simplemente desplazaremos toda la serie a un valor negativo, tomado según el módulo. Ahora, tenemos la posibilidad de calcular el valor normalizado de cada uno de los indicadores, lo cual representamos en el recuadro siguiente.

Profit Profit factor Recovery factor Normalized summ 
5000 0.5 1  0.75
15000 0.6 0.64  0.62
-11000 0.25 0  0.13
0 0 0.18  0.09
10000 1 0.64  0.82
7000 0.5 0.55  0.52

De esta forma, teniendo todos los coeficientes que nos interesan en la forma normalizada, podremos recurrir a la suma ponderada, donde el peso siempre será igual a la unidad dividida por n, donde n es igual al número de factores ponderados. Con ello, también obtendremos una columna igualmente normalizada, que precisamente constituirá nuestro criterio de clasificación. Si nos vemos obligados a clasificar alguno de los coeficientes no de manera ascendente, sino descendente, lo único que debemos hacer es sustraer este indicador de la unidad, cambiando así de lugar los coeficientes mayor y menor.

El código que implementa este mecanismo se muestra como dos métodos; el primero de ellos indica la forma en la que se tiene que realizar la clasificación (ascendente o descendente), mientras que el segundo implementa el propio mecanismo de clasificación. Dado que el primero de ellos, SortMethod GetSortMethod(SortBy sortBy), tiene una implementación muy trivial, no vamos a describirlo, sino que pasaremos directamente al análisis del método activo.

public static IEnumerable<OptimisationResult> SortOptimisations(this IEnumerable<OptimisationResult> results,
                                                                OrderBy order, IEnumerable<SortBy> sortingFlags,
                                                                Func<SortBy, SortMethod> sortMethod = null)
{
    // Obteniendo la lista única de banderas para la clasificación
    sortingFlags = sortingFlags.Distinct();
    // Comprobando la presencia de banderas
    if (sortingFlags.Count() == 0)
        return null;
    // Si hay una bandera, solo realizamos la clasificación según este indicador
    if (sortingFlags.Count() == 1)
    {
        if (order == OrderBy.Ascending)
            return results.OrderBy(x => x.GetResult(sortingFlags.ElementAt(0)));
        else
            return results.OrderByDescending(x => x.GetResult(sortingFlags.ElementAt(0)));
    }

    // Formando los límites mínimo y máximo según las banderas de optimización transmitidas
    Dictionary<SortBy, MinMax> Borders = sortingFlags.ToDictionary(x => x, x => new MinMax { Max = double.MinValue, Min = double.MaxValue });

    #region create Borders min max dictionary
    // Ciclo por la lista de pasadas de optimización
    for (int i = 0; i < results.Count(); i++)
    {
        // Ciclo por las banderas de clasificación
        foreach (var item in sortingFlags)
        {
            // obteniendo el valor del coeficiente actual
            double value = results.ElementAt(i).GetResult(item);
            MinMax mm = Borders[item];
            // Estableciendo los valores mínimo y máximo
            mm.Max = Math.Max(mm.Max, value);
            mm.Min = Math.Min(mm.Min, value);
            Borders[item] = mm;
        }
    }
    #endregion

    // Peso de la suma ponderada de los coeficientes normalizados
    double coef = (1.0 / Borders.Count);

    // Convirtiendo la lista de resultados de optimización en una matriz del tipo List
    // Dado que es más rápido trabajar con ella
    List<OptimisationResult> listOfResults = results.ToList();
    // Ciclo por los resultados de optimización
    for (int i = 0; i < listOfResults.Count; i++)
    {
        // Asignando el valor al coeficiente actual
        OptimisationResult data = listOfResults[i];
        // Reseteando el coeficiente de clasificación actual
        data.SortBy = 0;
        // Convirtiendo el ciclo a los límites formados de los máximos y los mínimos
        foreach (var item in Borders)
        {
            // Obteniendo el valor del resultado actual
            double value = listOfResults[i].GetResult(item.Key);
            MinMax mm = item.Value;

            // Si el mínimo es inferior a cero, desplazamos todos los datos a la magnitud del mínimo negativo
            if (mm.Min < 0)
            {
                value += Math.Abs(mm.Min);
                mm.Max += Math.Abs(mm.Min);
            }

            // Si el máximo es superior a cero, hacemos los cálculos
            if (mm.Max > 0)
            {
                // Dependiendo del método de clasificación, calculamos el coeficiente
                if ((sortMethod == null ? GetSortMethod(item.Key) : sortMethod(item.Key)) == SortMethod.Decreasing)
                {
                    // Calculamos el coeficiente para la clasificación descendente
                    data.SortBy += (1 - value / mm.Max) * coef;
                }
                else
                {
                    // Calculamos el coeficiente para la clasificación ascendente
                    data.SortBy += value / mm.Max * coef;
                }
            }
        }
        // Sustituimos el valor del coeficiente actual por el coeficiente con el parámetro de clasificación
        listOfResults[i] = data;
    }

    // Clasificamos en función del tipo de clasificación transmitido
    if (order == OrderBy.Ascending)
        return listOfResults.OrderBy(x => x.SortBy);
    else
        return listOfResults.OrderByDescending(x => x.SortBy);
}

Como podemos ver por el código, si necesitamos la clasificación de acuerdo con un parámetro, la realizamos sin recurrir a la serie normalizada, y retornamos directamente el resultado obtenido. Si la clasificación se hace de acuerdo con varios parámetros, primero debemos formar un diccionario que conste de los límites máximos y mínimos de la serie analizada. Esto es necesario para acelerar los cálculos, dado que, en caso contrario, necesitaríamos preguntar en cada iteración por los parámetros buscados, lo que generaría muchos más ciclos de los que tenemos en la variante analizada.

A continuación, se forma el peso para la suma ponderada y se realiza la operación de normalización de la serie y su suma. Para alcanzar el objetivo descrito, de nuevo se usan dos ciclos. En el ciclo interno se describe la operación, todo exactamente de la misma forma que en el esquema detallado anteriormente. La suma ponderada, obtenida como resultado del trabajo realizado, se introduce en la variable SortBy del elemento analizado de lmatriz. Al final de esta operación, cuando ya se ha formado el coeficiente final según el cual se realizará la clasificación de los datos, recurrimos al método anteriormente analizado de clasificación a través del método estádar de la matriz List<T>.OrderBy o List<T>. OrderByDescending  , en el caso de que se requiera la clasificación descendente. El método de clasficiación de miembros aparte de la suma ponderada se establece a través de un delegado transmitido como uno de los parámetros de la función. Si para este delegado dejamos el valor paramétrico por defecto, se usará el método mencionado al principio del análisis de este fragmento de código; en el caso puesto, se usará el delegado transmitido. 
  

Conclusión

Resumamos el trabajo realizado: hemos formado un mecanismo que usaremos en el futuro en muchas partes de nuestra aplicación. Este mecanismo, aparte de realizar las importantes tareas de descarga y lectura de archivos xml con formato de usuario (que por añadidura guardan información estructurada sobre las pruebas realizadas), contiene los métodos de expansión de las colecciones en C# diseñadas para filtrar y clasificar los datos obtenidos. Asimismo, se ha implementado un mecanismo de clasificación multifactorial, que, por desgracia, no está disponible en el simulador estándar del terminal. Entre las ventajas de este mecanismo de clasificación, podemos destacar la posibilidad de considerar una nutrida serie de factores durante la valoración; entre las desventajas, podemos destacar la posibilidad de comparar resultados solo dentro de la serie de factores valorados. Dicho de otra manera: la comparación de una suma ponderada obtenida en diferentes intervalos temporales resultará incorrecta, dado que para su comparación se usará otra serie de coeficientes. En la continuación del artículo se analizará un método de conversión de algoritmos que permitirá que el auto-optimizador pueda trabajar con estos, además de la propia creación del auto-optimizador.   


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

Redes neuronales: así de sencillo Redes neuronales: así de sencillo
Cada vez que hablamos de inteligencia artificial, en nuestra cabeza surgen todo tipo de ideas fantásticas, y nos parece que se trata de algo complicado e inalcanzable. Sin embargo, cada día oímos hablar de la inteligencia artificial en nuestra vida diaria. En las noticias se escribe con cada vez mayor frecuencia sobre los logros en el uso de redes neuronales. En el presente artículo, queremos mostrar al lector lo sencillo que puede resultar para cualquiera crear una red neuronal y usar los logros de la inteligencia artificial en el trading.
Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXIII): Clase comercial principal - control de parámetros permitidos Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXIII): Clase comercial principal - control de parámetros permitidos
En el presente artículo, continuaremos el desarrollo de la clase comercial, organizando esta vez el control de los valores incorrectos de los parámetros de la orden comercial e implementando la notificación sonora de los eventos comerciales.
Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXIV): Clase comercial principal - corrección automática de parámetros erróneos Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXIV): Clase comercial principal - corrección automática de parámetros erróneos
En el presente artículo, analizaremos el manejador de parámetros erróneos de la orden comercial, mejoraremos la clase comercial básica y también corregiremos el funcionamiento de la clase de eventos comerciales: ahora, todos los eventos comerciales, tanto únicos, como simultáneos en un mismo tick, serán correctamente determinados en los programas.
Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXII): Clases comerciales - Clase comercial principal, control de limitaciones Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXII): Clases comerciales - Clase comercial principal, control de limitaciones
En el artículo, comenzaremos a crear la clase comercial principal de la biblioteca, equipándola con la primera versión de la funcionalidad para la comprobacion primaria de los permisos de realización de operaciones comerciales. Asimismo, ampliaremos un poco las posibilidades y el contenido de la clase comercial básica.