Otimização Walk Forward Contínua (Parte 1): Trabalhando com os Relatórios de Otimização
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:
- Configurações do testador (as mesmas configurações para todo o relatório)
- Configurações do robô de negociação (única para cada passe da otimização)
- Coeficientes que descrevem os resultados da negociação (únicos para cada passe da otimização)
<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:
- Caminho do robô em relação à pasta Experts
- Moeda de depósito e depósito
- 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:
- Symbol
- TF
- Start_DT
- Finish_DT
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:
- VaR
- 90 - quantil 90
- 95 - quantil 95
- 99 - quantil 99
- Mx - expectativa matemática
- Std - desvio padrão
- Max_PL_DD
- Profit - lucro total
- DD - rebaixamento total
- Total Profit Trades - número total de negociações lucrativas
- Total Lose Trades - número total de negociações perdedoras
- Consecutive Wins - ganhos consecutivos
- Consecutive Lose - perdas consecutivas
- Trading_Days - relatórios de negociação por dias
- Profit - lucro médio por dia
- DD - perda média por dia
- Number Of Profit Trades - número de negociações lucrativas
- 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:
- Comparar duas instâncias para igualdade
- Comparar duas instâncias para maior/menor que
- Comparar duas instâncias para maior ou igual/menor ou igual
- Ordenar em ordem crescente/decrescente
- Obter valores únicos de uma lista de intervalos de tempo
- Usar o método IEnumerable.Sort que ordena a lista na ordem decrescente e usa a interface IComparable.
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:
- Compare — seu objetivo é comparar o valor passado apresentado como KeyValuePair e o valor especificado no método. Além da comparação maior/menor e de igualdade, nós podemos precisar verificar outras condições. Para esse fim, nós utilizaremos as flags. Uma flag equivale a um bit, enquanto o campo int armazena 8 bits. Assim, nós podemos ter até 8 flags configuradas ou removidas simultaneamente para o campo int. As flags podem ser verificadas sequencialmente, sem a necessidade de criar vários loops ou condições enormes, e, portanto, nós temos apenas três condições. Além disso, também é conveniente usar flags na interface gráfica, que nós consideraremos posteriormente, para definir os parâmetros de comparação necessários. Nós verificamos sequencialmente as flags nesta função e também verificamos se os dados correspondem a essas flags.
- Sort — ao contrário do método anterior, este é projetado para verificar vários parâmetros escritos em vez de apenas um. Nós executamos um loop item a item através de todos as flags passadas para a filtragem e usamos a função descrita anteriormente para descobrir se o parâmetro selecionado atende aos critérios especificados. Para habilitar o acesso a um valor do item selecionado no loop sem usar o operador "Switch case", o método OptimisationResult.GetResult(OrderBy item), que foi mencionado acima é usado. Se o valor passado não corresponder ao solicitado, retornamos false e descartamos os valores inadequados.
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.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/7290
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso