Otimização Walk Forward Contínua (Parte 1): Trabalhando com os Relatórios de Otimização

Andrey Azatskiy | 27 janeiro, 2020

Introdução

Nos artigos anteriores (Gerenciando Otimizações (Parte I) e Gerenciando Otimizações (Parte 2)), nós consideramos um mecanismo para iniciar a otimização na plataforma através de um processo de terceiros. Isso permite a criação de um certo Gerenciador de Otimização, que pode implementar o processo de maneira semelhante a um algoritmo de negociação que implementa um processo de negociação específico, ou seja, em um modo totalmente automatizado, sem interferência do usuário. A ideia é criar um algoritmo que gerencia o processo de otimização deslizante, no qual os períodos do histórico e de forward são alterados por um intervalo predefinido, se sobrepondo.

Essa abordagem da otimização de algoritmos pode servir como um teste de robustez da estratégia em vez de uma otimização pura, embora ela desempenhe as duas funções. Como resultado, nós podemos descobrir se um sistema de negociação é estável e podemos determinar combinações ideais de indicadores para o sistema. Como o processo descrito pode envolver diferentes filtros de coeficiente do robô e métodos para a seleção de combinações ótimas, que precisam ser verificados em cada um dos intervalos de tempo (que podem ser múltiplos), dificilmente ele poderá ser implementado manualmente. Além disso, nós podemos encontrar erros relacionados à transferência de dados ou outros erros relacionados ao fator humano. Portanto, são necessárias algumas ferramentas que gerenciem o processo de otimização sem a nossa intervenção. O programa criado atende aos objetivos estabelecidos. Para uma apresentação mais estruturada, o processo de criação do programa foi dividido em vários artigos, cada um dos quais abrange uma área específica do processo de criação do programa.

Esta parte é dedicada à criação de um kit de ferramentas para trabalhar com os relatórios de otimização, importá-los da plataforma e para filtrar e classificar os dados obtidos. Para fornecer uma melhor estrutura de apresentação, nós usaremos o formato de arquivo *xml. Os dados do arquivo podem ser lidos por humanos e programas. Além disso, os dados podem ser agrupados em blocos dentro do arquivo e, assim, as informações necessárias podem ser acessadas de maneira mais rápida e fácil.

Nosso programa é um processo de terceiros escrito em C#, ele precisa criar e ler os documentos *xml criados de maneira semelhante aos programas em MQL5. Portanto, o bloco de criação do relatório será implementado como uma DLL que pode ser usada no código MQL5 e C#. Portanto, para desenvolver um código em MQL5, nós precisaremos de uma biblioteca. Primeiro, nós descreveremos o processo de criação da biblioteca, enquanto o próximo artigo fornecerá uma descrição do código em MQL5 que trabalha com a biblioteca criada e gera os parâmetros de otimização. Vamos considerar esses parâmetros no artigo atual.

Estrutura do Relatório e as Métricas Necessárias

Como já mostrado nos artigos anteriores, a MetaTrader 5 pode fazer o download independente do relatório dos passes da otimização, no entanto, ele não fornece tanta informação quanto ao relatório gerado na guia Backtest após a conclusão de um teste com um conjunto específico de parâmetros. Para ter maior escopo no trabalho com os dados de otimização, o relatório deve incluir muitos dos dados exibidos nessa guia, além de permitir a possibilidade de adicionar mais dados personalizados ao relatório. Para esses fins, nós faremos o download de nossos próprios relatórios gerados, em vez daquele padrão. Vamos começar com a definição dos três tipos de dados necessários para o nosso programa:


<Optimisation_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="Leverage">1</Item>
        </Optimiser_Settings>

Os parâmetros são escritos no bloco "Item", cada um com o seu atributo "Name". A moeda do depósito será gravada no atributo "Currency"

Com base nisso, a estrutura do arquivo deve conter 2 seções principais: as configurações do testador e a descrição dos passes da otimização. Nós precisamos fornecer três parâmetros para a primeira seção:

  1. Caminho do robô em relação à pasta Experts
  2. Moeda de depósito e depósito
  3. Alavancagem da conta

 A segunda seção conterá uma sequência de blocos com os resultados da otimização, cada uma delas contendo uma seção com os coeficientes e um conjunto de parâmetros do robô. 

<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 Lose Trades">1</Item>
                                        <Item Name="Consecutive Wins">1</Item>
                                        <Item Name="Consecutive Lose">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 Lose 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 Lose 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 Lose 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 Lose 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 Lose 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>
</Optimisation_Report>

Dentro do bloco Optimisation_Results, os blocos Result se repetiram, cada um dos quais contendo o i-ésimo passe da otimização. Cada um dos blocos Result contém 4 atributos:

Essas são as configurações do testador que variam dependendo do intervalo de tempo em que a otimização é realizada. Cada um dos parâmetros do robô é escrito no bloco Item com o atributo Name de valor único, que serve de identificação do parâmetro. As métricas do robô são gravadas no bloco Coefficients. As métricas que não podem ser agrupadas são enumeradas diretamente no bloco Item. Outras métricas são divididas em blocos:

  1. 90 - quantil 90
  2. 95 - quantil 95
  3. 99 - quantil 99
  4. Mx - expectativa matemática
  5. Std - desvio padrão
  • Max_PL_DD
  1. Profit - lucro total
  2. DD - rebaixamento total
  3. Total Profit Trades - número total de negociações lucrativas
  4. Total Lose Trades - número total de negociações perdedoras
  5. Consecutive Wins - ganhos consecutivos
  6. Consecutive Lose - perdas consecutivas
  • Trading_Days - relatórios de negociação por dias 
  1. Profit - lucro médio por dia
  2. DD - perda média por dia
  3. Number Of Profit Trades - número de negociações lucrativas
  4. Number Of Lose Trades - número de negociações perdedoras

Como resultado, nós recebemos uma lista com as métricas dos resultados da otimização, que descrevem completamente os resultados dos testes. Agora, para filtrar e selecionar os parâmetros do robô, há uma lista completa das métricas necessárias que nos permitem avaliar com eficiência o desempenho do robô. 

A classe wrapper do relatório de otimizações, a classe que armazena as datas da otimização e a estrutura dos resultados das otimizações C#.

Vamos começar com a estrutura que armazena os dados para uma passe específico da otimização. 

public struct ReportItem
{
    public Dictionary<string, string> BotParams; // List of robot parameters
    public Coefficients OptimisationCoefficients; // Robot coefficients
    public string Symbol; // Symbol
    public int TF; // Timeframe
    public DateBorders DateBorders; // Date range
}

Todas as métricas do robô são armazenadas em um dicionário de formato de string. O arquivo com os parâmetros do robô não salva o tipo dos dados, portanto, o formato de string é melhor aqui. A lista de métricas do robô é fornecida em uma estrutura diferente, da mesma forma que outros blocos agrupados no relatório *xml de otimizações. Os relatórios de negociação por dia também são armazenados no dicionário.

public Dictionary<DayOfWeek, DailyData> TradingDays;

A enumeração DayOfWeek e o dicionário sempre devem conter 5 dias (de segunda a sexta-feira) como uma chave, semelhante ao arquivo *xml. A classe mais interessante na estrutura de armazenamento de dados é a DateBorders. Semelhante aos dados sendo agrupados em uma estrutura que contém os campos que descrevem cada um dos parâmetros de data, os intervalos de datas também são armazenados na estrutura DateBorders. 

public class DateBorders : IComparable
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="from">Range beginning date</param>
    /// <param name="till">Range ending date</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>
    /// From
    /// </summary>
    public DateTime From { get; }
    /// <summary>
    /// To
    /// </summary>
    public DateTime Till { get; }
}

Para uma operação completa com o intervalo de tempo, nós precisamos da possibilidade de criar dois intervalos de tempo. Para esse fim, substituímos 2 operadores "==" e "! =". 

Os critérios de igualdade são determinados pela igualdade de ambas as datas nos dois intervalos passados, ou seja, a data de início corresponde ao início da negociação do segundo intervalo (enquanto o mesmo também se aplica ao fim da negociação). No entanto, como o tipo de objeto é 'class', ele pode ser igual a nulo e, portanto, nós precisamos fornecer primeiro a capacidade de comparar com null. Vamos usar a palavra-chave is para esse fim. Depois disso, nós podemos comparar os parâmetros entre si, caso contrário, se tentarmos comparar com null, será retornado "null reference exception".

#region Equal
/// <summary>
/// The equality comparison operator
/// </summary>
/// <param name="b1">Element 1</param>
/// <param name="b2">Element 2</param>
/// <returns>Result</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>
/// The inequality comparison operator
/// </summary>
/// <param name="b1">Element 1</param>
/// <param name="b2">Element 2</param>
/// <returns>Comparison result</returns>
public static bool operator !=(DateBorders b1, DateBorders b2) => !(b1 == b2);
#endregion

Para sobrecarregar o operador de desigualdade, nós não precisamos mais escrever os procedimentos descritos acima, enquanto todos eles já estão escritos no operador "==". O próximo recurso que nós precisamos implementar é a classificação dos dados por períodos de tempo, é por isso que nós precisamos sobrecarregar os operadores ">", "<", ">=", "<=".

#region (Grater / Less) than
/// <summary>
/// Comparing: current element is greater than the previous one
/// </summary>
/// <param name="b1">Element 1</param>
/// <param name="b2">Element 2</param>
/// <returns>Result</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>
/// Comparing: current element is less than the previous one
/// </summary>
/// <param name="b1">Element 1</param>
/// <param name="b2">Element 2</param>
/// <returns>Result</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

Se algum dos parâmetros passados ao operador for igual a null, a comparação se torna impossível; portanto, retornamos False. Caso contrário, nós comparamos passo a passo. Se corresponder ao primeiro intervalo de tempo, comparamos com o segundo intervalo de tempo. Se eles não forem iguais, comparamos pelo primeiro intervalo. Portanto, se nós descrevermos a lógica de comparação com base no exemplo do operador "Maior", o intervalo maior será o mais antigo no tempo que o anterior, seja pela data de início ou pela data de término (se as datas de início forem iguais). A lógica de comparação "menor" é semelhante à comparação "maior". 

Os próximos operadores a serem sobrecarregados para ativar a opção de classificação são 'Maior ou igual' e 'Menor ou igual'. 

#region Equal or (Grater / Less) than
/// <summary>
/// Greater than or equal comparison
/// </summary>
/// <param name="b1">Element 1</param>
/// <param name="b2">Element 2</param>
/// <returns>Result</returns>
public static bool operator >=(DateBorders b1, DateBorders b2) => (b1 == b2 || b1 > b2);
/// <summary>
/// Less than or equal comparison
/// </summary>
/// <param name="b1">Element 1</param>
/// <param name="b2">Element 2</param>
/// <returns>Result</returns>
public static bool operator <=(DateBorders b1, DateBorders b2) => (b1 == b2 || b1 < b2);
#endregion

Como podemos ver, a sobrecarga do operador não requer a descrição da lógica de comparação interna. Em vez disso, nós usamos os operadores já sobrecarregados == e >, <. No entanto, como o Visual Studio sugere durante a compilação, além da sobrecarga desses operadores, nós precisamos sobrecarregar algumas funções herdadas da classe base "object".

#region override base methods (from object)
/// <summary>
/// Overloading of equality comparison
/// </summary>
/// <param name="obj">Element to compare to</param>
/// <returns></returns>
public override bool Equals(object obj)
{
    if (obj is DateBorders other)
        return this == other;
    else
        return base.Equals(obj);
}
/// <summary>
/// Cast the class to a string and return its hash code
/// </summary>
/// <returns>String hash code</returns>
public override int GetHashCode()
{
    return ToString().GetHashCode();
}
/// <summary>
/// Convert the current class to a string
/// </summary>
/// <returns>String From date - To date</returns>
public override string ToString()
{
    return $"{From}-{Till}";
}
#endregion
/// <summary>
/// Compare the current element with the passed one
/// </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");
    }
}

Método Equals: sobrecarregamos ele usando o operador == (se o objeto passado tiver o tipo DateBorders) ou a implementação básica do método.

Método ToString: sobrecarregamos como uma representação de string de caracteres de duas datas separadas por um hífen. Isso nos ajudará a sobrecarregar o método GetHashCode.

Método GetHashCode: sobrecarregamos ele primeiro convertendo o objeto em uma string e retornando o código hash dessa string. Quando uma nova instância da classe é criada em C#, seu código hash é exclusivo, independentemente do conteúdo da classe. Ou seja, se não sobrecarregarmos o método e criarmos duas instâncias da classe DateBorders com as mesmas datas De e Para, elas terão códigos hash diferentes, apesar do conteúdo idêntico. Essa regra não se aplica a strings, porque a C# fornece um mecanismo que impede a criação de novas instâncias da classe String se a string tiver sido criada anteriormente — portanto, seus códigos hash para string idênticas corresponderão. Usando o método ToString sobrecarregando e usando o código hash da string, fornecemos o comportamento de nossos códigos hash de classe semelhantes aos da String. Agora, ao usar o método IEnumerable.Distinct, nós podemos garantir que a lógica de receber a lista única de intervalos de tempo estará correta, pois esse método se baseia nos códigos hash dos objetos comparados.

Implementando a interface IComparable, da qual nossa classe é herdada, nós implementamos o método CompareTo que compara a instância atual da classe com a passada. Sua implementação é fácil e ela utiliza sobrecargas de operadores sobrecarregados anteriormente. 

Depois de implementar as sobrecargas necessárias, nós podemos trabalhar com essa classe com mais eficiência. Nós podemos:

Como nós estamos implementando uma otimização de rolagem, que terá backtests e testes de forward, nós precisamos criar um método para comparar os intervalos do histórico e do forward.

/// <summary>
/// Method for comparing forward and historical optimizations
/// </summary>
/// <param name="History">Array of historical optimization</param>
/// <param name="Forward">Array of forward optimizations</param>
/// <returns>Sorted list historical - forward optimization</returns>
public static Dictionary<DateBorders, DateBorders> CompareHistoryToForward(List<DateBorders> History, List<DateBorders> Forward)
{
    // array of comparable optimizations
    Dictionary<DateBorders, DateBorders> ans = new Dictionary<DateBorders, DateBorders>();

    // Sort the passed parameters
    History.Sort();
    Forward.Sort();

    // Create a historical optimization loop
    int i = 0;
    foreach (var item in History)
    {
if(ans.ContainsKey(item))
       	    continue;

        ans.Add(item, null); // Add historical optimization
        if (Forward.Count <= i)
            continue; // If the array of forward optimization is less than the index, continue the loop

        // Forward optimization loop
        for (int j = i; j < Forward.Count; j++)
        {
            // If the current forward optimization is contained in the results array, skip
            if (ans.ContainsValue(Forward[j]) ||
                Forward[j].From < item.Till)
            {
                continue;
            }

            // Compare forward and historical optimization
            ans[item] = Forward[j];
            i = j + 1;
            break;
        }
    }

    return ans;
}

Como você pode ver, o método é estático. Isso é feito para disponibilizá-lo como uma função regular, sem vincular a uma instância de classe específica. Primeiro, ele ordena os intervalos de tempo passados na ordem crescente. Assim, no próximo loop, nós podemos ter certeza de que todos os intervalos passados anteriormente são menores ou iguais aos próximos. Em seguida, implementamos dois loops: foreach para os intervalos do histórico, loop aninhado para os intervalos de forward.

No início do loop dos dados do histórico, nós sempre adicionamos os intervalos do histórico (chave) para a coleção com os resultados, além de definir temporariamente para null no lugar de intervalos de forward. O loop dos resultados de forward começa com o i-ésimo parâmetro. Isso evita a repetição do loop com elementos já usados da lista de forward. O intervalo de forward sempre deve seguir o histórico, ou seja, ele deve ser > que o histórico. É por isso que nós implementamos o loop por intervalos de forward, caso na lista passada exista um período de forward para o primeiro intervalo do histórico, que precede o primeiro intervalo do histórico. É melhor visualizarmos a ideia em uma tabela:

Histórico Forward
De Para De Para
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

Portanto, o primeiro intervalo do histórico termina em 09.03.2017 e o primeiro intervalo de forward inicia em 12.12.2016, o que não está correto. É por isso que nós pulamos nos intervalos de forward, devido à condição. Além disso, nós pulamos o intervalo de forward, que está contido no dicionário resultante. Se o j-ésimo dado de forward ainda não existir no dicionário resultante e a data de início do intervalo de forward for >= que o intervalo de término do histórico atual, salvamos o valor recebido e saímos do loop de intervalos de forward, pois o valor necessário já foi encontrado. Antes de sair, atribuímos o valor do intervalo de forward após a seleção da variável i (a variável que significa o início das iterações da lista de forward. Isso é feito porque o intervalo atual não será mais necessário (devido à ordenação inicial dos dados).

Uma verificação antes da otimização do histórica garante que todas as otimizações do histórico sejam únicas. Assim, a seguinte lista é obtida no dicionário resultante:

Chave Valor
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 pelos dados apresentados, o primeiro intervalo de forward é descartado e nenhum intervalo é encontrado para o último histórico, pois esse intervalo não foi passado. Com base nessa lógica, o programa comparará os dados dos intervalos do histórico e de forward e entenderá qual dos intervalos do histórico deve que fornecer os parâmetros de otimização para os testes de forward.

Para habilitar a operação eficiente com um resultado de otimização específico, eu criei uma estrutura wrapper para a estrutura ReportItem que contém vários métodos adicionais e operadores sobrecarregados. Basicamente, o wrapper contém dois campos:

/// <summary>
/// Optimization pass report
/// </summary>
public ReportItem report;
/// <summary>
/// Sorting factor
/// </summary>
public double SortBy;

O primeiro campo foi descrito acima. O segundo campo é criado para permitir a ordenação por múltiplos valores, por exemplo, lucro e fator de recuperação. O mecanismo de ordenação será descrito mais adiante, mas a idéia é converter esses valores em apenas um e armazená-los nessa variável. 

A estrutura também contém sobrecargas de conversão de tipos:

/// <summary>
/// The operator of implicit type conversion from optimization pass to the current type
/// </summary>
/// <param name="item">Optimization pass report</param>
public static implicit operator OptimisationResult(ReportItem item)
{
    return new OptimisationResult { report = item, SortBy = 0 };
}
/// <summary>
/// The operator of explicit type conversion from current to the optimization pass structure
/// </summary>
/// <param name="optimisationResult">current type</param>
public static explicit operator ReportItem(OptimisationResult optimisationResult)
{
    return optimisationResult.report;
}

Como resultado, nós podemos converter implicitamente o tipo ReportItem para seu wrapper e, em seguida, converter explicitamente o wrapper ReportItem no elemento do relatório de negociação. Isso pode ser mais eficiente que o preenchimento sequencial dos campos. Como todos os campos na estrutura ReportItem são divididos em categorias, às vezes nós podemos precisar de um código extenso para receber o valor desejado. Um método especial foi criado para economizar espaço e criar um getter mais universal. Ele recebe os dados solicitados das métricas do robô por meio da enumeração passada SourtBy do código GetResult(SortBy resultType) descrito acima. A implementação é simples, mas muito longa e, portanto, ela não é fornecida aqui. O método itera sobre a enum passada no construtor switch e retorna o valor da métrica solicitada. Como a maioria dos coeficientes possui o tipo double e como esse tipo pode conter todos os outros tipos numéricos, os valores da métrica são convertidos em double.

As sobrecargas do operador de comparação também foram implementadas para este tipo de wrapper:

/// <summary>
/// Overloading of the equality comparison operator
/// </summary>
/// <param name="result1">Parameter 1 to compare</param>
/// <param name="result2">Parameter 2 to compare</param>
/// <returns>Comparison result</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>
 /// Overloading of the inequality comparison operator
/// </summary>
/// <param name="result1">Parameter 1 to compare</param>
/// <param name="result2">Parameter 2 to compare</param>
/// <returns>Comparison result</returns>
public static bool operator !=(OptimisationResult result1, OptimisationResult result2)
{
    return !(result1 == result2);
}
/// <summary>
/// Overloading of the basic type comparison operator
/// </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);
}

Os elementos de otimizações que contêm os mesmos nomes e valores dos parâmetros do robô serão considerados como iguais. Portanto, se nós precisarmos comparar dois passes da otimização, nós já temos os operadores sobrecarregados prontos para uso. Essa estrutura também contém um método que grava os dados em um arquivo. Se ele existir, os dados são simplesmente adicionados ao arquivo. A explicação do elemento de escrita de dados e a implementação do método serão fornecidas abaixo.

Criação de um arquivo para armazenar o relatório de otimização

Nós vamos trabalhar com os relatórios de otimização e nós escreveremos eles não apenas na plataforma, mas também no programa criado. É por isso que nós vamos adicionar o método de criação de relatórios de otimização para esta DLL. Vamos fornecer também vários métodos para a escrita dos dados em um arquivo, ou seja, habilitar a gravação de um array de dados em um arquivo, além de permitir a adição de um elemento separado ao arquivo existente (se o arquivo não existir, ele deverá ser criado). O último método será importado para o terminal e será usado nas classes C#. Vamos começar a considerar os métodos de gravação de arquivos do relatório implementados com as funções conectadas à adição de dados em um arquivo. A classe ReportWriter foi criada para esse objetivo. A implementação da classe completa está disponível no arquivo de projeto anexado. Aqui eu vou mostrar apenas os métodos mais interessantes. Vamos primeiro descrever como essa classe funciona. 

Ele contém apenas os métodos estáticos: isso permite exportar os seus métodos para a MQL5. Para a mesma finalidade, a classe é marcada com um modificador de acesso público. Essa classe contém um campo estático do tipo ReportItem e vários métodos que adicionam alternadamente as métricas e parâmetros do EA a ela.

/// <summary>
/// temporary data keeper
/// </summary>
private static ReportItem ReportItem;
/// <summary>
/// clearing the temporary data keeper
/// </summary>
public static void ClearReportItem()
{
    ReportItem = new ReportItem();
}

Outro método é o ClearReportItem(). Ele recria a instância do campo. Nesse caso, nós perdemos o acesso à instância anterior deste objeto: ela é apagada e o processo de gravação de dados é iniciado novamente. Os métodos de adição de dados são agrupados por blocos. Aqui estão as assinaturas desses métodos.  

/// <summary>
/// Add robot parameters
/// </summary>
/// <param name="name">Parameter name</param>
/// <param name="value">Parameter value</param>
public static void AppendBotParam(string name, string value);

/// <summary>
/// Add the main list of coefficients
/// </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>
/// Add 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>
/// Add total PL / DD and associated values
/// </summary>
/// <param name="profit"></param>
/// <param name="dd"></param>
/// <param name="totalProfitTrades"></param>
/// <param name="totalLoseTrades"></param>
/// <param name="consecutiveWins"></param>
/// <param name="consecutiveLose"></param>
public static void AppendMaxPLDD(double profit, double dd,
                                 int totalProfitTrades, int totalLoseTrades,
                                 int consecutiveWins, int consecutiveLose);

/// <summary>
/// Add a specific day
/// </summary>
/// <param name="day"></param>
/// <param name="profit"></param>
/// <param name="dd"></param>
/// <param name="numberOfProfitTrades"></param>
/// <param name="numberOfLoseTrades"></param>
public static void AppendDay(int day,
                             double profit, double dd,
                             int numberOfProfitTrades,
                             int numberOfLoseTrades);

O método de adição de estatísticas de negociação divididas por dias deve ser chamado para cada um dos 5 dias úteis. Se nós não o adicionarmos em um dos dias, o arquivo escrito não será lido no futuro. Depois que os dados são adicionados ao campo de armazenamento de dados, nós podemos prosseguir com a gravação do campo. Antes disso, verificamos se o arquivo existe e criamos ele, se necessário. Alguns métodos foram adicionados para a criação do arquivo.

/// <summary>
/// The method creates the file if it has not been created
/// </summary>
/// <param name="pathToBot">Path to the robot</param>
/// <param name="currency">Deposit currency</param>
/// <param name="balance">Balance</param>
/// <param name="leverage">Leverage</param>
/// <param name="pathToFile">Path to file</param>
private static void CreateFileIfNotExists(string pathToBot, string currency, double balance, int leverage, string pathToFile)
{
    if (File.Exists(pathToFile))
        return;
    using (var xmlWriter = new XmlTextWriter(pathToFile, null))
    {
        // set document format
        xmlWriter.Formatting = Formatting.Indented;
        xmlWriter.IndentChar = '\t';
        xmlWriter.Indentation = 1;

        xmlWriter.WriteStartDocument();

        // Create document root
        #region Document root
        xmlWriter.WriteStartElement("Optimisation_Report");

        // Write the creation date
        xmlWriter.WriteStartAttribute("Created");
        xmlWriter.WriteString(DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"));
        xmlWriter.WriteEndAttribute();

        #region Optimiser settings section 
        // Optimizer settings
        xmlWriter.WriteStartElement("Optimiser_Settings");

        // Path to the robot
        WriteItem(xmlWriter, "Bot", pathToBot);
        // Deposit
        WriteItem(xmlWriter, "Deposit", balance.ToString(), new Dictionary<string, string> { { "Currency", currency } });
        // Leverage
        WriteItem(xmlWriter, "Leverage", leverage.ToString());

        xmlWriter.WriteEndElement();
        #endregion

        #region Optimization results section
        // the root node of the optimization results list
        xmlWriter.WriteStartElement("Optimisation_Results");
        xmlWriter.WriteEndElement();
        #endregion

        xmlWriter.WriteEndElement();
        #endregion

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

/// <summary>
/// Write element to a file
/// </summary>
/// <param name="writer">Writer</param>
/// <param name="Name">Element name</param>
/// <param name="Value">Element value</param>
/// <param name="Attributes">Attributes</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();
}

Eu também forneci aqui a implementação do método WriteItem que contém o código repetido para a adição de um elemento final com os dados e atributos específicos ao arquivo. O arquivo que cria o método CreateFileIfNotExists verifica se o arquivo existe, cria o arquivo e começa a formar a estrutura mínima necessária. 

Primeiramente, ele cria a raiz do arquivo, ou seja, a tag <Optimization_Report/>, dentro da qual todas as estruturas filho do arquivo estão localizadas. Então os dados de criação do arquivo são preenchidos — isso é implementado para um trabalho mais cômodo com os arquivos. Depois disso, nós criamos um nó com as configurações do otimizador inalteradas e especificamos eles. Em seguida, criamos uma seção que armazenará os resultados da otimização e fechamos ela imediatamente. Como resultado, nós temos um arquivo vazio com a formatação mínima necessária. 


<Optimisation_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="Leverage">1</Item>
        </Optimiser_Settings>
        <Optimisation_Results />
</Optimisation_Report>

Assim, nós poderemos ler este arquivo usando a classe XmlDocument. Esta é a classe mais útil para ler e editar documentos XML existentes. Nós usaremos exatamente essa classe para adicionar os dados aos documentos existentes. Operações repetidas são implementadas como métodos separados e, portanto, nós poderemos adicionar os dados a um documento existente com mais eficiência:

/// <summary>
/// Writing attributes to a file
/// </summary>
/// <param name="item">Node</param>
/// <param name="xmlDoc">Document</param>
/// <param name="Attributes">Attributes</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>
/// Add section
/// </summary>
/// <param name="xmlDoc">Document</param>
/// <param name="xpath_parentSection">xpath to select parent node</param>
/// <param name="sectionName">Section name</param>
/// <param name="Attributes">Attribute</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>
/// Write item
/// </summary>
/// <param name="xmlDoc">Document</param>
/// <param name="xpath_parentSection">xpath to select parent node</param>
/// <param name="name">Item name</param>
/// <param name="value">Value</param>
/// <param name="Attributes">Attributes</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);
}

O primeiro método FillInAttributes preenche os atributos para o nó passado, o WriteItem grava um item na seção especificada via XPath, enquanto o AppendSection adiciona uma seção dentro de outra seção, que é especificada por um caminho passado usando o Xpath. Esses blocos de código são frequentemente usados ao adicionar os dados a um arquivo. O método de gravação de dados é bastante extenso e é dividido em blocos.

/// <summary>
/// Write trading results to a file
/// </summary>
/// <param name="pathToBot">Path to the bot</param>
/// <param name="currency">Deposit currency</param>
/// <param name="balance">Balance</param>
/// <param name="leverage">Leverage</param>
/// <param name="pathToFile">Path to file</param>
/// <param name="symbol">Symbol</param>
/// <param name="tf">Timeframe</param>
/// <param name="StartDT">Trading start dare</param>
/// <param name="FinishDT">Trading end date</param>
public static void Write(string pathToBot, string currency, double balance,
                         int leverage, string pathToFile, string symbol, int tf,
                         ulong StartDT, ulong FinishDT)
{
    // Create the file if it does not yet exist
    CreateFileIfNotExists(pathToBot, currency, balance, leverage, pathToFile);
            
    ReportItem.Symbol = symbol;
    ReportItem.TF = tf;

    // Create a document and read the file using it
    XmlDocument xmlDoc = new XmlDocument();
    xmlDoc.Load(pathToFile);

    #region Append result section
    // Write a request to switch to the optimization results section 
    string xpath = "Optimisation_Report/Optimisation_Results";
    // Add a new section with optimization results
    AppendSection(xmlDoc, xpath, "Result",
                  new Dictionary<string, string>
                  {
                      { "Symbol", symbol },
                      { "TF", tf.ToString() },
                      { "Start_DT", StartDT.ToString() },
                      { "Finish_DT", FinishDT.ToString() }
                  });
    // Add section with optimization results
    AppendSection(xmlDoc, $"{xpath}/Result[last()]", "Coefficients");
    // Add section with VaR
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients", "VaR");
    // Add section with total PL / DD
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients", "Max_PL_DD");
    // Add section with trading results by days
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients", "Trading_Days");
    // Add section with trading results on Monday
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Mn");
    // Add section with trading results on Tuesday
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Tu");
    // Add section with trading results on Wednesday
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "We");
    // Add section with trading results on Thursday
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Th");
    // Add section with trading results on Friday
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Fr");
    #endregion

    #region Append Bot params
    // Iterate through bot parameters
    foreach (var item in ReportItem.BotParams)
    {
        // Write the selected robot parameter
        WriteItem(xmlDoc, "Optimisation_Report/Optimisation_Results/Result[last()]",
                  "Item", item.Value, new Dictionary<string, string> { { "Name", item.Key } });
    }
    #endregion

    #region Append main coef
    // Set path to node with coefficients
    xpath = "Optimisation_Report/Optimisation_Results/Result[last()]/Coefficients";

    // Save coefficients
    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
    // Set path to node with VaR
    xpath = "Optimisation_Report/Optimisation_Results/Result[last()]/Coefficients/VaR";

    // Save VaR results
    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
    // Set path to node with total PL / DD
    xpath = "Optimisation_Report/Optimisation_Results/Result[last()]/Coefficients/Max_PL_DD";

    // Save coefficients
    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 Lose 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 Lose" } });
    #endregion

    #region Append Days
    foreach (var item in ReportItem.OptimisationCoefficients.TradingDays)
    {
        // Set path to specific day node
        xpath = "Optimisation_Report/Optimisation_Results/Result[last()]/Coefficients/Trading_Days";
        // Select day
        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;
        }

        // Save results
        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 Lose Trades" } });
    }
    #endregion

    // Rewrite the file with the changes
    xmlDoc.Save(pathToFile);

    // Clear the variable which stored results written to a file
    ClearReportItem();
}

Primeiro, nós carregamos o documento inteiro na memória e, em seguida adicionamos as seções. Vamos considerar o formato de solicitação do Xpath que passa o caminho para o nó raiz.  

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

A variável xpath contém o caminho para o nó no qual os elementos do passe da otimização são armazenados. Este nó armazena os nós de resultados da otimização que podem ser apresentados como um array de estruturas. O construtor Result[last()] seleciona o último elemento do array, após o qual o caminho é passado para o nó aninhado /Coefficients. Seguindo o princípio descrito, nós selecionamos o nó necessário com os resultados das otimizações. 

O próximo passo é a adição dos parâmetros do robô: no loop, nós adicionamos os parâmetros diretamente ao diretório de resultados. Então, adicionamos uma série de métricas no diretório de métricas. Esta adição é dividida em blocos. Como resultado, nós salvamos os resultados e apagamos o armazenamento temporário. Como resultado, nós obtemos um arquivo com a lista de parâmetros e resultados da otimização. Para separar em threads durante as operações assíncronas iniciadas a partir de diferentes processos (é assim que a otimização no testador é executada ao usar vários processadores), outro método de gravação foi criado, que separa as threads usando os mutex nomeados.

/// <summary>
/// Write to file while locking using a named mutex
/// </summary>
/// <param name="mutexName">Mutex name</param>
/// <param name="pathToBot">Path to the bot</param>
/// <param name="currency">Deposit currency</param>
/// <param name="balance">Balance</param>
/// <param name="leverage">Leverage</param>
/// <param name="pathToFile">Path to file</param>
/// <param name="symbol">Symbol</param>
/// <param name="tf">Timeframe</param>
/// <param name="StartDT">Trading start dare</param>
/// <param name="FinishDT">Trading end date</param>
/// <returns></returns>
public static string MutexWriter(string mutexName, string pathToBot, string currency, double balance,
                                 int leverage, string pathToFile, string symbol, int tf,
                                 ulong StartDT, ulong FinishDT)
{
    string ans = "";
    // Mutex lock
    Mutex m = new Mutex(false, mutexName);
    m.WaitOne();
    try
    {
        // write to file
        Write(pathToBot, currency, balance, leverage, pathToFile, symbol, tf, StartDT, FinishDT);
    }
    catch (Exception e)
    {
        // Catch error if any
        ans = e.Message;
    }

    // Release the mutex
    m.ReleaseMutex();
    // Return error text
    return ans;
}

Esse método grava os dados usando o método anterior, mas o processo de gravação é envolvido por um mutex e em um bloco try-catch. O último permite a liberação do mutex, mesmo em caso de erro. Caso contrário, o processo poderá congelar e a otimização poderá falhar ao continuar. Esses métodos também são usados na estrutura OptimisationResult no método WriteResult.

/// <summary>
/// The method adds current parameter to the existing file or creates a new file with the current parameter
/// </summary>
/// <param name="pathToBot">Relative path to the robot from the Experts folder</param>
/// <param name="currency">Deposit currency</param>
/// <param name="balance">Balance</param>
/// <param name="leverage">Leverage</param>
/// <param name="pathToFile">Path to file</param>
public void WriteResult(string pathToBot,
                        string currency, double balance,
                        int leverage, 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, leverage, pathToFile, report.Symbol, report.TF,
                           report.DateBorders.From.DTToUnixDT(), report.DateBorders.Till.DTToUnixDT());
    }
    catch (Exception e)
    {
        ReportWriter.ClearReportItem();
        throw e;
    }
}

Nesse método, nós adicionamos alternadamente os resultados da otimização a um armazenamento temporário e, em seguida, chamamos o método Write para salvá-los em um arquivo existente ou criar um novo arquivo se ele ainda não tiver sido criado. 

O método descrito para a escrita dos dados obtidos é necessário para a adição de informações a um arquivo preparado. Existe outro método que é mais adequado quando uma série de dados precisa ser gravada. O método foi desenvolvido como uma extensão para interface IEnumerable<OptimisationResult>. Agora nós podemos salvar os dados para todas as listas herdadas da interface correspondente. 

public static void ReportWriter(this IEnumerable<OptimisationResult> results, string pathToBot,
                                string currency, double balance,
                                int leverage, string pathToFile)
{
    // Delete the file if it exists
    if (File.Exists(pathToFile))
        File.Delete(pathToFile);

    // Create writer 
    using (var xmlWriter = new XmlTextWriter(pathToFile, null))
    {
        // Set document format
        xmlWriter.Formatting = Formatting.Indented;
        xmlWriter.IndentChar = '\t';
        xmlWriter.Indentation = 1;

        xmlWriter.WriteStartDocument();

        // The root node of the document
        xmlWriter.WriteStartElement("Optimisation_Report");

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

        // Write optimizer settings to file
        #region Optimiser settings section 
        xmlWriter.WriteStartElement("Optimiser_Settings");

        WriteItem(xmlWriter, "Bot", pathToBot); // path to the robot
        WriteItem(xmlWriter, "Deposit", balance.ToString(), new Dictionary<string, string> { { "Currency", currency } }); // Currency and deposit
        WriteItem(xmlWriter, "Leverage", leverage.ToString()); // Leverage

        xmlWriter.WriteEndElement();
        #endregion

        // Write optimization results to the file
        #region Optimisation result section
        xmlWriter.WriteStartElement("Optimisation_Results");

        // Loop through optimization results
        foreach (var item in results)
        {
            // Write specific result
            xmlWriter.WriteStartElement("Result");

            // Write attributes of this optimization pass
            WriteAttribute(xmlWriter, "Symbol", item.report.Symbol); // Symbol
            WriteAttribute(xmlWriter, "TF", item.report.TF.ToString()); // Timeframe
            WriteAttribute(xmlWriter, "Start_DT", item.report.DateBorders.From.DTToUnixDT().ToString()); // Optimization start date
            WriteAttribute(xmlWriter, "Finish_DT", item.report.DateBorders.Till.DTToUnixDT().ToString()); // Optimization end date

            // Write optimization result
            WriteResultItem(item, xmlWriter);

            xmlWriter.WriteEndElement();
        }

        xmlWriter.WriteEndElement();
        #endregion

        xmlWriter.WriteEndElement();

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

O método grava os relatórios de otimização em um arquivo, um por um, até que o array não tenha mais dados. Se o arquivo já existir no caminho passado, ele será substituído por um novo. Primeiro, nós criamos um gravador do arquivo e então, configuramos ele. Então, seguindo a estrutura de arquivo já conhecida, nós escrevemos as configurações do otimizador e os resultados da otimização um por um. Como podemos ver na extração do código acima, os resultados são gravados em um loop, que percorre os elementos da coleção, na instância em que o método descrito foi chamado. Dentro do loop, a gravação de dados é delegada ao método criado para gravar os dados de um elemento específico no arquivo.

/// <summary>
/// Write a specific optimization pass
/// </summary>
/// <param name="resultItem">Optimization pass value</param>
/// <param name="writer">Writer</param>
private static void WriteResultItem(OptimisationResult resultItem, XmlTextWriter writer)
{
    // Write coefficients
    #region Coefficients
    writer.WriteStartElement("Coefficients");

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

    WriteItem(writer, "90", resultItem.GetResult(SortBy.Q_90).ToString()); // Quantile 90
    WriteItem(writer, "95", resultItem.GetResult(SortBy.Q_95).ToString()); // Quantile 95
    WriteItem(writer, "99", resultItem.GetResult(SortBy.Q_99).ToString()); // Quantile 99
    WriteItem(writer, "Mx", resultItem.GetResult(SortBy.Mx).ToString()); // Average for PL
    WriteItem(writer, "Std", resultItem.GetResult(SortBy.Std).ToString()); // Standard deviation for PL

    writer.WriteEndElement();
    #endregion

    // Write PL / DD parameters - extreme points
    #region Max PL DD
    writer.WriteStartElement("Max_PL_DD");
    WriteItem(writer, "Profit", resultItem.GetResult(SortBy.MaxProfit).ToString()); // Total profit
    WriteItem(writer, "DD", resultItem.GetResult(SortBy.MaxDD).ToString()); // Total loss
    WriteItem(writer, "Total Profit Trades", ((int)resultItem.GetResult(SortBy.MaxProfitTotalTrades)).ToString()); // Total number of winning trades
    WriteItem(writer, "Total Lose Trades", ((int)resultItem.GetResult(SortBy.MaxDDTotalTrades)).ToString()); // Total number of losing trades
    WriteItem(writer, "Consecutive Wins", ((int)resultItem.GetResult(SortBy.MaxProfitConsecutivesTrades)).ToString()); // Winning trades in a row 
    WriteItem(writer, "Consecutive Lose", ((int)resultItem.GetResult(SortBy.MaxDDConsecutivesTrades)).ToString()); // Losing trades in a row
    writer.WriteEndElement();
    #endregion

    // Write trading results by days
    #region Trading_Days

    // The method writing trading results
    void AddDay(string Day, double Profit, double DD, int ProfitTrades, int DDTrades)
    {
        writer.WriteStartElement(Day);

        WriteItem(writer, "Profit", Profit.ToString()); // Profits
        WriteItem(writer, "DD", DD.ToString()); // Losses
        WriteItem(writer, "Number Of Profit Trades", ProfitTrades.ToString()); // Number of profitable trades
        WriteItem(writer, "Number Of Lose Trades", DDTrades.ToString()); // Number of losing trades

        writer.WriteEndElement();
    }

    writer.WriteStartElement("Trading_Days");

    // Monday
    AddDay("Mn", resultItem.GetResult(SortBy.AverageDailyProfit_Mn),
                 resultItem.GetResult(SortBy.AverageDailyDD_Mn),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Mn),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Mn));
    // Tuesday
    AddDay("Tu", resultItem.GetResult(SortBy.AverageDailyProfit_Tu),
                 resultItem.GetResult(SortBy.AverageDailyDD_Tu),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Tu),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Tu));
    // Wednesday
    AddDay("We", resultItem.GetResult(SortBy.AverageDailyProfit_We),
                 resultItem.GetResult(SortBy.AverageDailyDD_We),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_We),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_We));
    // Thursday
    AddDay("Th", resultItem.GetResult(SortBy.AverageDailyProfit_Th),
                 resultItem.GetResult(SortBy.AverageDailyDD_Th),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Th),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Th));
    // Friday
    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

    // Write other coefficients
    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

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

A implementação do método que grava os dados em um arquivo é muito simples, embora ela seja bastante longa. Após criar as seções apropriadas e preencher os atributos, o método adiciona os dados no VaR do passe da otimização realizada e os valores que caracterizam o seu lucro máximo e rebaixamento. Uma função aninhada foi criada para gravar os resultados de otimização para uma data específica, que é chamada 5 vezes, para cada um dos dias. Depois disso as métricas sem agrupamento e os parâmetros raiz são adicionados. Já que o procedimento descrito é executado em um loop para cada um dos elementos, os dados não são gravados no arquivo até que o método xmlWriter.Close() é chamado (isso é feito no método principal de escrita). Portanto, esse é o método de extensão mais rápido para escrever uma matriz de dados, em comparação com os métodos considerados anteriormente. Nós consideramos os procedimentos relacionados à gravação dos dados em um arquivo. Agora vamos para a próxima parte lógica da descrição, ou seja, a leitura dos dados do arquivo resultante.

Leitura do arquivo contendo o relatório de otimização

Nós precisamos ler os arquivos para processar as informações recebidas e exibi-las. Portanto, é necessário um mecanismo apropriado para a leitura do arquivo. Ele é implementado como uma classe separada:

public class ReportReader : IDisposable
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="path">Path to file</param>
        public ReportReader(string path);

        /// <summary>
        /// Binary number format provider
        /// </summary>
        private readonly NumberFormatInfo formatInfo = new NumberFormatInfo { NumberDecimalSeparator = "." };

        #region DataKeepers
        /// <summary>
        /// Presenting the report file in OOP format
        /// </summary>
        private readonly XmlDocument document = new XmlDocument();

        /// <summary>
        /// Collection of document nodes (rows in excel table)
        /// </summary>
        private readonly System.Collections.IEnumerator enumerator;
        #endregion

        /// <summary>
        /// The read current report item
        /// </summary>
        public ReportItem? ReportItem { get; private set; } = null;

        #region Optimiser settings
        /// <summary>
        /// Path to the robot
        /// </summary>
        public string RelativePathToBot { get; }

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

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

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

        /// <summary>
        /// File creation date
        /// </summary>
        public DateTime Created { get; }

        /// <summary>
        /// File reader method
        /// </summary>
        /// <returns></returns>
        public bool Read();

        /// <summary>
        /// The method receiving the item by its name (the Name attribute)
        /// </summary>
        /// <param name="Name"></param>
        /// <returns></returns>
        private string SelectItem(string Name) => $"Item[@Name='{Name}']";

        /// <summary>
        /// Get the trading result value for the selected day
        /// </summary>
        /// <param name="dailyNode">Node of this day</param>
        /// <returns></returns>
        private DailyData GetDay(XmlNode dailyNode);

        /// <summary>
        /// Reset the quote reader
        /// </summary>
        public void ResetReader();

        /// <summary>
        /// Clear the document
        /// </summary>
        public void Dispose() => document.RemoveAll();
    }

Vamos ver a estrutura em mais detalhes. A classe é herdada da interface iDisposable. Esta não é uma condição necessária, mas é feita por precaução. Agora a classe de descrição contém o método Dispasable que limpa o objeto document. O objeto armazena o arquivo de resultados da otimização carregado na memória.

A abordagem é conveniente pois, ao criar uma instância, a classe herdada da interface mencionada acima deve ser encapsulada no construtor 'using', que chama automaticamente o método especificado quando ele ultrapassa os limites do bloco da estrutura 'using'. Isso significa que o documento lido não será mantido por muito tempo na memória e, portanto, a quantidade de memória carregada será reduzida.

A classe do leitor de documentos em linha usa o Enumerador recebido do documento lido. Os valores lidos são gravados na propriedade especial e assim nós fornecemos acesso aos dados. Além disso, os seguintes dados são preenchidos durante a instanciação da classe: as propriedades especificando as configurações do otimizador principal, data e hora da criação do arquivo. Para eliminar a influência das configurações locais do SO (ao escrever e ao ler o arquivo), o número com o formato delimitador do tipo double é indicado. Ao ler o arquivo pela primeira vez, a classe deve ser redefinida para o início da lista. Para esse fim, nós usamos o método ResetReader que redefine o Enumerador para o início da lista. O construtor da classe é implementado para preencher todas as propriedades necessárias e preparar a classe para uso posterior.

public ReportReader(string path)
{
    // load the document
    document.Load(path);

    // Get file creation date
    Created = DateTime.ParseExact(document["Optimisation_Report"].Attributes["Created"].Value, "dd.MM.yyyy HH:mm:ss", null);
    // Get enumerator
    enumerator = document["Optimisation_Report"]["Optimisation_Results"].ChildNodes.GetEnumerator();

    // Parameter receiving function
    string xpath(string Name) { return $"/Optimisation_Report/Optimiser_Settings/Item[@Name='{Name}']"; }

    // Get path to the robot
    RelativePathToBot = document.SelectSingleNode(xpath("Bot")).InnerText;

    // Get balance and deposit currency
    XmlNode Deposit = document.SelectSingleNode(xpath("Deposit"));
    Balance = Convert.ToDouble(Deposit.InnerText.Replace(",", "."), formatInfo);
    Currency = Deposit.Attributes["Currency"].Value;

    // Get leverage
    Leverage = Convert.ToInt32(document.SelectSingleNode(xpath("Leverage")).InnerText);
}

Primeiro, ele carrega o documento passado e preenche a sua data de criação. O enumerador obtido durante a instanciação da classe pertence aos nós filhos do documento localizados na seção >Optimisation_Report/Optimisation_Results, ou seja, para os nós com a tag <Result/>. Para obter os parâmetros de configuração do otimizador desejados, o caminho para o nó do documento necessário é especificado usando a marcação xpath. Um análogo dessa função interna contendo um caminho mais curto é o método SelectItem, que indica o caminho para um item entre os nós do documento que possuem a tag <Item/> de acordo com o atributo Name. O método GetDay converte o nó do documento passado na estrutura apropriada do relatório de negociação diário. O último método nesta classe é o método do leitor de dados. Abaixo, mostramos brevemente a sua implementação.   

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

    // Read the next item
    bool ans = enumerator.MoveNext();
    if (ans)
    {
        // Current node
        XmlNode result = (XmlNode)enumerator.Current;
        // current report item
        ReportItem = new ReportItem[...]

        // Fill the robot parameters
        foreach (XmlNode item in result.ChildNodes)
        {
            if (item.Name == "Item")
                ReportItem.Value.BotParams.Add(item.Attributes["Name"].Value, item.InnerText);
        }

    }
    return ans;
}

A parte do código oculto contém a operação de instanciação do relatório de otimização e o preenchimento dos campos do relatório com os dados lidos. Esta operação inclui ações semelhantes, que convertem o formato de string para o formato necessário. Um loop adicional preenche os parâmetros do robô usando os dados lidos linha por linha do arquivo. Esta operação é realizada apenas se a linha do arquivo de conclusão não foi alcançada. O resultado da operação é o retorno de uma indicação se a linha foi lida ou não. Ele também serve como uma indicação se o final do arquivo foi atingido.

Filtragem multifatorial e classificação do relatório de otimização

Para atender aos objetivos, eu criei duas enumerações que indicavam a direção da classificação (SortMethd e OrderBy). Eles são semelhantes e provavelmente apenas um deles pode ser suficiente. No entanto, para separar os métodos de filtragem e classificação, duas enumerações foram criadas em vez de uma. O objetivo das enumerações é mostrar em ordem crescente ou decrescente. O tipo da taxa das métricas com o valor passado é indicado pelas flags. O objetivo é definir a condição de comparação.    

/// <summary>
/// Filtering type
/// </summary>
[Flags]
public enum CompareType
{
    GraterThan = 1, // greater than
    LessThan = 2, // less than
    EqualTo = 4 // equal
}

Os tipos de coeficientes pelos quais os dados podem ser filtrados e classificados são descritos pela enumeração OrderBy mencionada acima. Os métodos de classificação e filtragem são implementados como métodos que expandem coleções herdadas da interface IEnumerable<OptimisationResult>. No método de filtragem, nós verificamos cada uma das métricas item por item, se ele atende aos critérios especificados e rejeitamos as passagens de otimização nas quais qualquer uma das métricas não atendem aos critérios. Para filtrar os dados, nós usamos o loop Where contido na interface IEnumerable. O método é implementado da seguinte maneira.

/// <summary>
/// Optimization filtering method
/// </summary>
/// <param name="results">Current collection</param>
/// <param name="compareData">Collection of coefficients and filtering types</param>
/// <returns>Filtered collection</returns>
public static IEnumerable<OptimisationResult> FiltreOptimisations(this IEnumerable<OptimisationResult> results,
                                                                  IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData)
{
    // Result sorting function
    bool Compare(double _data, KeyValuePair<CompareType, double> compareParams)
    {
        // Comparison result
        bool ans = false;
        // Comparison for equality
        if (compareParams.Key.HasFlag(CompareType.EqualTo))
        {
            ans = compareParams.Value == _data;
        }
        // Comparison for 'greater than current'
        if (!ans && compareParams.Key.HasFlag(CompareType.GraterThan))
        {
            ans = _data > compareParams.Value;
        }
        // Comparison for 'less than current'
        if (!ans && compareParams.Key.HasFlag(CompareType.LessThan))
        {
            ans = _data < compareParams.Value;
        }

        return ans;
    }
    // Sorting condition
    bool Sort(OptimisationResult x)
    {
        // Loop through passed sorting parameters
        foreach (var item in compareData)
        {
            // Compare the passed parameter with the current one
            if (!Compare(x.GetResult(item.Key), item.Value))
                return false;
        }

        return true;
    }

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

Duas funções são implementadas dentro do método, cada uma delas executa sua própria parte da tarefa de filtragem de dados. Vamos vê-las, começando com a função final:

O método 'Where' é usado para ordenar os dados. Ele gera automaticamente uma lista de condições adequadas, que retorna como o resultado da execução do método de extensão.  

A filtragem de dados é bastante fácil de entender. Dificuldades podem ocorrer com a ordenação. Vamos considerar o mecanismo de ordenação usando um exemplo. Suponha que nós temos os parâmetros Fator de Lucro e Fator de Recuperação. Nós precisamos ordenar os dados por esses dois parâmetros. Se nós realizarmos duas iterações de ordenação uma após a outra, nós ainda receberemos os dados ordenado pelo último parâmetro. Nós precisamos comparar esses valores de alguma maneira.

Lucro Fator de lucro Fator de recuperação
5000 1 9
15000 1.2 5
-11000 0.5 -2
0 0 0
10000 2 5
7000 1 4

Esses dois coeficientes não são normalizados dentro de seus valores limites. Eles também têm uma gama muito ampla de valores em relação um ao outro. Logicamente, nós precisamos normalizá-los primeiro, preservando sua sequência. A maneira padrão de trazer os dados para uma forma normalizada é dividir cada um deles pelo valor máximo da série: assim, nós obteremos uma série de valores que variam no intervalo [0;1]. Mas primeiro, nós precisamos encontrar os pontos extremos dessa série de valores apresentados na tabela.


Fator de lucro  Fator de recuperação
Min  0 -2  
Max  2 9

Como pode ser visto na tabela, o fator de Recuperação tem valores negativos e, portanto, a abordagem acima não é adequada aqui. Para eliminar esse efeito, nós simplesmente deslocamos a série inteira pelo valor negativo obtido em módulo. Agora nós podemos calcular o valor normalizado de cada um dos parâmetros.

Lucro Fator de lucro Fator de recuperação Soma normalizada
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

Agora que nós temos todos os coeficientes na forma normalizada, nós podemos usar a soma ponderada, na qual o peso é igual a um dividido por n (aqui n é o número de fatores que estão sendo ponderados). Como resultado, nós obtemos uma coluna normalizada que pode ser usada como um critério de classificação. Se algum dos coeficientes deve ser classificado na ordem decrescente, nós precisamos subtrair esse parâmetro de um e, assim, trocar os maiores e os menores coeficientes.

O código que implementa esse mecanismo é apresentado como dois métodos, o primeiro indica a ordem de ordenação (crescente ou decrescente) e o segundo método implementa o mecanismo de ordenação. O primeiro dos métodos, SortMethod GetSortMethod(SortBy sortBy), é bastante simples, então vamos para o segundo método.

public static IEnumerable<OptimisationResult> SortOptimisations(this IEnumerable<OptimisationResult> results,
                                                                OrderBy order, IEnumerable<SortBy> sortingFlags,
                                                                Func<SortBy, SortMethod> sortMethod = null)
{
    // Get the unique list of flags for sorting
    sortingFlags = sortingFlags.Distinct();
    // Check flags
    if (sortingFlags.Count() == 0)
        return null;
    // If there is one flag, sort by this parameter
    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)));
    }

    // Form minimum and maximum boundaries according to the passed optimization flags
    Dictionary<SortBy, MinMax> Borders = sortingFlags.ToDictionary(x => x, x => new MinMax { Max = double.MinValue, Min = double.MaxValue });

    #region create Borders min max dictionary
    // Loop through the list of optimization passes
    for (int i = 0; i < results.Count(); i++)
    {
        // Loop through sorting flags
        foreach (var item in sortingFlags)
        {
            // Get the value of the current coefficient
            double value = results.ElementAt(i).GetResult(item);
            MinMax mm = Borders[item];
            // Set the minimum and maximum values
            mm.Max = Math.Max(mm.Max, value);
            mm.Min = Math.Min(mm.Min, value);
            Borders[item] = mm;
        }
    }
    #endregion

    // The weight of the weighted sum of normalized coefficients
    double coef = (1.0 / Borders.Count);

    // Convert the list of optimization results to the List type array
    // Since it is faster to work with
    List<OptimisationResult> listOfResults = results.ToList();
    // Loop through optimization results
    for (int i = 0; i < listOfResults.Count; i++)
    {
        // Assign value to the current coefficient
        OptimisationResult data = listOfResults[i];
        // Zero the current sorting factor
        data.SortBy = 0;
        // Loop through the formed maximum and minimum borders
        foreach (var item in Borders)
        {
            // Get the current result value
            double value = listOfResults[i].GetResult(item.Key);
            MinMax mm = item.Value;

            // If the minimum is below zero, shift all data by the negative minimum value
            if (mm.Min < 0)
            {
                value += Math.Abs(mm.Min);
                mm.Max += Math.Abs(mm.Min);
            }

            // If the maximum is greater than zero, calculate
            if (mm.Max > 0)
            {
                // Calculate the coefficient according to the sorting method
                if ((sortMethod == null ? GetSortMethod(item.Key) : sortMethod(item.Key)) == SortMethod.Decreasing)
                {
                    // Calculate the coefficient to sort in descending order
                    data.SortBy += (1 - value / mm.Max) * coef;
                }
                else
                {
                    // Calculate the coefficient to sort in ascending order
                    data.SortBy += value / mm.Max * coef;
                }
            }
        }
        // Replace the value of the current coefficient with the sorting parameter
        listOfResults[i] = data;
    }

    // Sort according to the passed sorting type
    if (order == OrderBy.Ascending)
        return listOfResults.OrderBy(x => x.SortBy);
    else
        return listOfResults.OrderByDescending(x => x.SortBy);
}

Se a ordenação deve ser realizada por um parâmetro, executamos a ordenação sem recorrer à normalização da série. Em seguida, retornamos o resultado de forma imediata. Se a ordenação deve ser realizada por vários parâmetros, nós primeiro geramos um dicionário constituído de valores máximos e mínimos das séries consideradas. Isso permite acelerar os cálculos, pois, caso contrário, nós precisaríamos solicitar os parâmetros durante cada iteração. Isso geraria muito mais loops do que nós consideramos nesta implementação.

Então, o peso é formado para a soma ponderada e a operação é executada para normalizar uma série em sua soma. Aqui, são utilizados dois loops novamente, as operações descritas acima são executadas no loop interno. A soma ponderada resultante é adicionada à variável SortBy do elemento do array correspondente. No final desta operação, quando a métrica resultante a ser usado para ordenação já tiver sido formada, usamos o método de ordenação descrito anteriormente através do método padrão do array List<T>.OrderBy or List<T>. OrderByDescending   — quando a ordenação decrescente é necessária. O método de ordenação para os membros separados da soma ponderada é definido por um delegado passado como um dos parâmetros de função. Se este delegado for deixado como um valor parametrizado padrão, o método mencionado anteriormente é usado; caso contrário, o delegado aprovado será usado.
  

Conclusão

Nós criamos um mecanismo que será usado ativamente em nosso aplicativo no futuro. Além do descarregamento e da leitura de arquivos xml de um formato personalizado, que armazenam informações estruturadas sobre os testes executados, o mecanismo contém os métodos de expansão da coleção C#, que são usados para ordenar e filtrar os dados. Nós implementamos o mecanismo de ordenação multifatorial, que não está disponível no testador padrão da plataforma. Uma das vantagens do método de ordenação é a capacidade de explicar uma série de fatores. No entanto, sua desvantagem é que os resultados só podem ser comparados dentro da série fornecida. Isso significa que a soma ponderada do intervalo de tempo selecionado não pode ser comparada com outros intervalos, porque cada um deles usa uma série individual de métricas. Nos próximos artigos, nós consideraremos o método de conversão de algoritmos para ativar o aplicativo ou um otimizador automatizado para os algoritmos, bem como a criação de um otimizador automatizado.