Otimização Walk Forward Contínua (Parte 2): Mecanismo para a criação de um relatório de otimização para qualquer robô

24 fevereiro 2020, 09:07
Andrey Azatskiy
0
1 087

Introdução

Este é o próximo artigo da série dedicada à criação de um otimizador automatizado, que pode executar uma otimização Walk Forward das estratégias de negociação. O artigo anterior descreveu a criação de uma DLL a ser usada em nosso otimizador automático e nos Expert Advisors. Esta nova parte é totalmente dedicada à linguagem MQL5. Nós vamos considerar os métodos de geração de relatórios de otimização e a aplicação dessa funcionalidade em nossos algoritmos. 

O testador de estratégia não permite o acesso aos seus dados a partir de um Expert Advisor, enquanto os resultados fornecidos carecem de detalhes; portanto, nós usaremos a funcionalidade de download do relatório de otimização implementada em meus artigos anteriores. Como as partes dessa funcionalidade foram modificadas separadamente, enquanto outras não foram totalmente abordadas em artigos anteriores, nós vamos considerar esses recursos mais uma vez, pois constituem as partes principais do nosso programa. Vamos começar com um dos novos recursos: adição de comissão personalizada. Todas as classes e funções descritas neste artigo estão localizadas na pasta do gerenciador de Include/History.

Implementação da comissão personalizada e slippage

O testador da plataforma MetaTrader 5 oferece muitas possibilidades interessantes. No entanto, algumas corretoras não adicionam a comissão do negócio ao histórico. Além disso, às vezes você pode querer adicionar uma comissão adicional para os testes extras da estratégia. Para esses propósitos, eu adicionei uma classe que armazena a comissão para cada símbolo de forma separado. Após a chamada de um método apropriado, a classe retorna a comissão e o slippage especificado. A classe em si é intitulado da seguinte maneira:

class CCCM
  {
private:
   struct Keeper
     {
      string            symbol;
      double            comission;
      double            shift;
     };

   Keeper            comission_data[];
public:

   void              add(string symbol,double comission,double shift);

   double            get(string symbol,double price,double volume);
   void              remove(string symbol);
  };

A estrutura do Keeper foi criada para esta classe, que armazena a comissão e o slippage para o ativo especificado. Um array foi criado para armazenar todas as comissões passadas e valores de slippage. Os três métodos declarados adicionam, recebem e excluem os dados. O método de adição dos ativos é implementado da seguinte maneira: 

void CCCM::add(string symbol,double comission,double shift)
{
 int s=ArraySize(comission_data);

 for(int i=0;i<s;i++)
   {
    if(comission_data[i].symbol==symbol)
        return;
   }

 ArrayResize(comission_data,s+1,s+1);

 Keeper keeper;
 keeper.symbol=symbol;
 keeper.comission=MathAbs(comission);
 keeper.shift=MathAbs(shift);

 comission_data[s]=keeper;
}

Esse método implementa a adição de um novo ativo à coleção, após a verificação preliminar se o mesmo ativo já foi adicionado anteriormente. Observe que o slippage e a comissão foram adicionadas ao módulo. Assim, quando todos os custos forem somados, o sinal não afetará o cálculo. Outro ponto a se prestar atenção são as unidades de cálculo.

  • Comissão: dependendo do tipo do ativo, a comissão pode ser adicionada na moeda de lucro ou como um percentual do volume negociado.
  • Slippage: sempre especificado em pontos. 

Observe também que esses valores não são adicionados por uma posição completa (por exemplo, abertura + fechamento), mas por cada negociação. Assim, a posição terá o seguinte valor: n*comissão + n*slippage, onde n é o número de todas as negociações dentro de uma posição.

O método remove exclui o ativo selecionado. O nome do símbolo é usado para a chave.

void CCCM::remove(string symbol)
{
 int total=ArraySize(comission_data);
 int ind=-1;
 for(int i=0;i<total;i++)
   {
    if(comission_data[i].symbol==symbol)
      {
       ind=i;
       break;
      }
   }
 if(ind!=-1)
    ArrayRemove(comission_data,ind,1);
}

Se o símbolo apropriado não for encontrado, o método termina sem excluir nenhum ativo.

O método get é usado para obter o turno e a comissão selecionada. A implementação do método é diferente para os diferentes tipos de ativos. 

double CCCM::get(string symbol,double price,double volume)
{

 int total=ArraySize(comission_data);
 for(int i=0;i<total;i++)
   {
    if(comission_data[i].symbol==symbol)
      {
       ENUM_SYMBOL_CALC_MODE mode=(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol,SYMBOL_TRADE_CALC_MODE);

       double shift=comission_data[i].shift*SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE);

       double ans;
       switch(mode)
         {
          case SYMBOL_CALC_MODE_FOREX :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_FUTURES :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_CFD :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_CFDINDEX :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_CFDLEVERAGE :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_EXCH_STOCKS :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_EXCH_FUTURES :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_EXCH_BONDS :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_EXCH_BONDS_MOEX :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_SERV_COLLATERAL :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          default: ans=0; break;
         }

       if(ans!=0)
          return -ans;

      }
   }

 return 0;
}

Busca pelo símbolo especificado no array. Como os diferentes tipos de cálculo da comissão são usados para os diferentes tipos de símbolos, os tipos de configuração de comissão também são diferentes. Por exemplo, a comissão de ações e títulos é definida como um percentual da rotatividade (turnover), enquanto a rotatividade é calculada como o produto do número de lotes pelo número de contratos por lote e pelo preço do negócio.

Como resultado, nós obtemos o equivalente monetário da operação realizada. O resultado da execução do método é sempre a soma da comissão e o slippage em termos monetários. O slippage é calculado com base no valor do tick. Além disso, a classe descrita será usada na próxima classe de download dos relatórios. Os parâmetros de comissão para cada um dos ativos podem ser codificados ou solicitados automaticamente a partir de um banco de dados; alternativamente, ele pode ser passado para o EA como entradas. Nos meus algoritmos, eu prefiro o último método. 

Inovação na classe CDealHistoryGetter

As classes consideradas nesta parte adiante foram mencionadas em artigos anteriores. É por isso que eu não vou me aprofundar nos detalhes para as classes discutidas anteriormente. No entanto, eu tentarei fornecer descrições compreensivas para as novas classes, porque o principal algoritmo no algoritmo de download do relatório de negociação é a criação do relatório baixado.  

Vamos começar com a classe CDealHistoryGetter, que tem sido usada com algumas modificações desde o primeiro artigo. O primeiro artigo foi dedicado principalmente à descrição dessa classe. A versão mais recente está anexada abaixo. Ele inclui algumas novas funcionalidades e pequenas correções. O mecanismo para baixar o relatório no formato de fácil leitura é descrito em detalhes no primeiro artigo. Neste artigo, nós consideraremos com mais detalhes a adição da comissão e do slippage ao relatório. De acordo com o princípio POO, que implica que um objeto deve executar uma função específica designada, esse objeto é criado para receber todos os tipos de resultados de relatórios de negociação. Ele contém os seguintes métodos públicos, cada um executando sua função específica:

  • getHistory — esse método permite baixar o histórico de negociação agrupado por posições. Se nós fizermos o download do histórico de negociação em um ciclo usando os métodos padrão, sem filtro, nós receberemos a descrição dos negócios apresentados pela estrutura DealData: 

struct DealData
  {
   long              ticket;        // Deal ticket
   long              order;         // The number of the order that opened the position
   datetime          DT;            // Position open date
   long              DT_msc;        // Position open date in milliseconds
   ENUM_DEAL_TYPE    type;          // Open position type
   ENUM_DEAL_ENTRY   entry;         // Position entry type
   long              magic;         // Unique position number
   ENUM_DEAL_REASON  reason;        // Order placing reason
   long              ID;            // Position ID
   double            volume;        // Position volume (lots)
   double            price;         // Position entry price
   double            comission;     // Commission paid
   double            swap;          // Swap
   double            profit;        // Profit / loss
   string            symbol;        // Symbol
   string            comment;       // Comment specified when at opening
   string            ID_external;   // External ID
  };

Os dados recebidos serão classificados pelo horário da posição aberta e não serão agrupados de nenhuma outra maneira. Este artigo contém exemplos, mostrando a dificuldade de ler o relatório neste formulário, porque pode ocorrer confusão entre negociações ao negociar com vários algoritmos. Especialmente se você usar técnicas de aumento de posição que compram ou vendem adicionalmente um ativo de acordo com os algoritmos subjacentes. Como resultado, nós recebemos uma grande quantidade de negócios de entrada e saída que não refletem as imagens completas.

Nosso método agrupa esses negócios por posições. Embora haja confusão com as ordens, nós eliminamos os negócios desnecessários que não se referem à posição analisada. O resultado é salvo como uma estrutura que armazena um array da estrutura de negócio mostrada acima.  

struct DealKeeper
  {
   DealData          deals[]; /* List of all deals for this position
                              (or several positions in case of position reversal)*/
   string            symbol;  // Symbol
   long              ID;      // ID of the position (s)
   datetime          DT_min;  // Open date (or the date of the very first position)
   datetime          DT_max;  // Close date
  };

Observe que essa classe não leva em consideração os números mágicos no agrupamento, porque quando dois ou mais algoritmos são negociados em uma posição, eles geralmente se cruzam. Pelo menos uma separação completa é tecnicamente impossível na Bolsa de Moscow, principal mercado que eu escrevo os meus algoritmos. Além disso, a ferramenta foi projetada para baixar os resultados de negociação ou os resultados de teste/otimização. No primeiro caso, as estatísticas do símbolo selecionado são suficientes, enquanto no segundo caso o número mágico não importa, porque o testador de estratégia executa um algoritmo de cada vez.

A implementação do núcleo do método não mudou desde o primeiro artigo. Agora nós adicionamos a comissão personalizada a ele. Para esta tarefa, a classe CCCM discutida acima é passada por referência ao construtor da classe e ela é salva no campo correspondente. Em seguida, no momento do preenchimento da estrutura do DealData, ou seja, no momento do preenchimento da comissão, a comissão personalizada que foi armazenada na classe passada do CCCM é adicionada. 

#ifndef ONLY_CUSTOM_COMISSION
               if(data.comission==0 && comission_manager != NULL)
                 {
                  data.comission=comission_manager.get(data.symbol,data.price,data.volume);
                 }
#else
               data.comission=comission_manager.get(data.symbol,data.price,data.volume);
#endif

A comissão é adicionada diretamente e condicionalmente. Se antes de conectar um arquivo a essa classe no robô, nós definirmos o parâmetro ONLY_CUSTOM_COMISSION, o campo da comissão sempre conterá a comissão passada em vez do valor fornecido pela corretora. Se esse parâmetro não for definido, a comissão passada será incluída condicionalmente: somente se a corretora não fornecer as cotações. Em todos os outros casos, o valor da comissão do usuário será ignorado.

  • getIDArr — retorna um array dos IDs de posições que foram abertas para todos os símbolos durante o período solicitado. Os IDs de posição permitem a combinação de todos os negócios das posições em nosso método. Na verdade, esta é uma lista exclusiva do campo DealData.ID. 
  • getDealsDetales — o método é semelhante ao getHistory, no entanto, ele fornece menos detalhes. A ideia do método é fornecer uma tabela de posições em um formato fácil de ler, no qual cada linha corresponde a um negócio específico. Cada posição é descrita pela seguinte estrutura: 
    struct DealDetales
      {
       string            symbol;        // Symbol
       datetime          DT_open;       // Open date
       ENUM_DAY_OF_WEEK  day_open;      // Open day
       datetime          DT_close;      // Cloe date
       ENUM_DAY_OF_WEEK  day_close;     // Close day
       double            volume;        // Volume (lots)
       bool              isLong;        // Long/Short
       double            price_in;      // Position entry price
       double            price_out;     // Position exit price
       double            pl_oneLot;     // Profit / loss is trading one lot
       double            pl_forDeal;    // Real profit/loss taking into account commission
       string            open_comment;  // Comment at the time of opening
       string            close_comment; // Comment at the time of closing
      };
    
    
    Eles representam uma tabela de posições classificadas por datas de fechamento de posição. O array desses valores será usada para calcular as métricas na próxima classe. Além disso, nós receberemos o relatório final de teste com base nos dados apresentados. Além disso, com base nesses dados, o testador cria a linha do gráfico PL após a negociação.

    Quanto ao testador, observe que em outros cálculos, o Fator de Recuperação calculado pelo terminal será diferente do calculado com base nos dados recebidos. Isso ocorre porque, embora o download dos dados esteja correto e as fórmulas de cálculo sejam iguais, os dados de origem são diferentes. O testador calcula o fator de recuperação usando a linha verde, isto é, o relatório detalhado, enquanto nós o calcularemos usando a linha azul, isto é, os dados ignorando as flutuações de preço que ocorrem entre a abertura e o fechamento da posição.   
  • getBalance — esse método foi projetado para obter os dados do saldo que não levam em consideração as operações de negociação na data especificada. 
    double CDealHistoryGetter::getBalance(datetime toDate)
      {
       if(HistorySelect(0,(toDate>0 ? toDate : TimeCurrent())))
         {
          int total=HistoryDealsTotal(); // Get the total number of positions
          double balance=0;
          for(int i=0; i<total; i++)
            {
             long ticket=(long)HistoryDealGetTicket(i);
    
             ENUM_DEAL_TYPE dealType=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE);
             if(dealType==DEAL_TYPE_BALANCE ||
                dealType == DEAL_TYPE_CORRECTION ||
                dealType == DEAL_TYPE_COMMISSION)
               {
                balance+=HistoryDealGetDouble(ticket,DEAL_PROFIT);
    
                if(toDate<=0)
                   break;
               }
            }
          return balance;
         }
       else
          return 0;
      }
    
    

Para realizar a tarefa, o histórico de todas os negócios, desde o primeiro intervalo de tempo até o especificado, é solicitado primeiro. Depois disso, o saldo é salvo em um ciclo, enquanto todos os depósitos e saques são adicionados ao saldo original, levando em consideração as comissões e correções fornecidas pela corretora. Se uma data zero for passada como entrada, somente o saldo a partir da primeira data que será solicitado.

  • getBalanceWithPL — o método é semelhante ao anterior, mas, além das alterações no saldo, ele leva em consideração o lucro/perda das operações realizadas, incluindo as comissões de acordo com o princípio mencionado.

Classe que cria o relatório de otimização — Estruturas usadas nos cálculos

Outra classe que já foi mencionada em artigos anteriores é o CReportCreator. Ele foi brevemente descrito no artigo Os 100 Melhores Passes de Otimização na seção "Cálculos". Agora é hora de fornecer uma descrição mais detalhada, porque essa classe calcula todas as métricas, com base nos quais o otimizador automático decidirá se essa combinação de parâmetros do algoritmo corresponde aos critérios solicitados. 

Primeiro, vamos descrever a ideia básica da abordagem usada na implementação de classes. Uma classe semelhante com menos possibilidades funcionais foi implementada em meu primeiro artigo. Mas foi muito lento, porque, para calcular o próximo grupo de parâmetros solicitados ou o próximo gráfico, ele precisou fazer o download de todo o histórico de negociação novamente e percorrê-lo. Isso foi feito em cada solicitação de parâmetro.

Às vezes, no caso de muitos dados, a abordagem pode levar alguns segundos. Para acelerar os cálculos. Eu usei outra implementação de classe, que também fornece muito mais dados (incluindo aqueles que não estão disponíveis nos resultados da otimização padrão). Você pode observar que os dados semelhantes são necessários para o cálculo de muitas métricas, como, por exemplo, lucro/perda máxima ou lucro/perda acumulada e similares.

Portanto, calculando as métricas em um loop e salvando-os nos campos da classe, nós podemos aplicar esses dados para calcular todos os outros parâmetros nos quais esses dados são necessários. Assim, nós obtemos uma classe que percorre uma vez o histórico baixado, calcula todos os parâmetros necessários e os armazena até o próximo cálculo. Quando nós precisamos obter o parâmetro necessário, a classe copia os dados salvos em vez de recalculá-los, o que acelera bastante a operação.

Agora vamos ver como os parâmetros são calculados. Vamos começar com os objetos que armazenam os dados usados para os cálculos adicionais. Esses objetos são criados como objetos de classe aninhados declarados em escopo privado. Isso é feito por dois motivos. Primeiro, para impedir seu uso em outras classes que usarão essa funcionalidade. O grande número de estruturas e classes declaradas é confuso: algumas são necessárias para os cálculos externos, outras são técnicas, ou seja, usadas para cálculos internos. E assim, a segunda razão é enfatizar seu objetivo puramente técnico. 

A estrutura PL_Keeper:

struct PL_keeper
{
 PLChart_item      PL_total[];
 PLChart_item      PL_oneLot[];
 PLChart_item      PL_Indicative[];
};

Essa estrutura é criada para armazenar todos os gráficos de ganhos e perdas possíveis. Eles foram descritos em detalhes no meu primeiro artigo (veja o link acima). Abaixo da declaração da estrutura, são criadas as suas instâncias:

PL_keeper         PL,PL_hist,BH,BH_hist;

Cada instância armazena 4 tipos de gráficos apresentados para diferentes dados de origem. Os dados com o prefixo PL são calculados com base na linha azul mencionada anteriormente do gráfico PL disponível no terminal. Os dados com o prefixo BH são calculados com base no gráfico de ganhos e perdas obtido pela estratégia Buy and Hold. Os dados com o sufixo 'hist' são calculados com base no histograma de ganhos e perdas.

Estrutura DailyPL_keeper:

// The structure of Daily PL graphs
struct DailyPL_keeper
{
 DailyPL           avarage_open,avarage_close,absolute_open,absolute_close;
};

Essa estrutura armazena os quatro tipos possíveis de gráfico de lucro/perda diária. As instâncias da estrutura DailyPL com o prefixo 'average' são calculadas usando os dados médios dos lucros/perdas. Aqueles com o prefixo 'absolute' usam os valores totais de lucros e perdas. Consequentemente, as diferenças entre eles são óbvias. No primeiro caso, ele reflete o lucro médio diário para todo o período de negociação, no segundo caso, o lucro total é mostrado. Os dados do prefixo 'aberto' são classificados por dia, de acordo com a data de abertura, enquanto os dados com o prefixo 'fechado' são classificados de acordo com a data de fechamento. A declaração da instância da estrutura é exibida no código abaixo.

A estrutura RationTable_keeper:

// Table structure of extreme points
struct RatioTable_keeper
  {
   ProfitDrawdown    Total_max,Total_absolute,Total_percent;
   ProfitDrawdown    OneLot_max,OneLot_absolute,OneLot_percent;
  };

Essa estrutura consiste em instâncias da estrutura ProfitDrawdown.

struct ProfitDrawdown
  {
   double            Profit; // In some cases Profit, in other Profit / Loss
   double            Drawdown; // Drawdown
  };

Ele armazena a relação de lucros e perdas de acordo com certos critérios. Os dados com o prefixo 'Total' são calculados usando a construção do gráfico de lucros e perdas, levando em consideração as alterações do lote. Os dados com o prefixo 'OneLot' são calculados como se um lote fosse negociado o tempo todo. A ideia de cálculo do lote não padronizado é descrita no primeiro artigo mencionado acima. Em suma, este método foi criado para avaliar os resultados do sistema de negociação. Ele permite avaliar de onde vem o maior resultado: gerenciamento oportuno de lotes ou da lógica do próprio sistema. O sufixo 'max' mostra que a instância apresenta os dados sobre o maior lucro e levantamento encontrado durante o histórico de negociação. O sufixo 'absolute' significa que a instância contém os dados totais de lucro e do rebaixamento para todo o histórico de negociação. O sufixo 'percent' significa que os valores do lucro e do rebaixamento são calculados como uma razão percentual em relação ao valor máximo da curva PL dentro do período testado. A declaração da estrutura é simples e é mostrada no código anexo ao artigo.

O próximo grupo de estruturas não é declarado como um campo de classe, mas ele é usado como uma declaração local no método principal Create. Todas as estruturas descritas são combinadas, portanto, vamos ver a declaração de todas elas. 

// Structures for calculating consecutive profits and losses
   struct S_dealsCounter
     {
      int               Profit,DD;
     };
   struct S_dealsInARow : public S_dealsCounter
     {
      S_dealsCounter    Counter;
     };
   // Structures for calculating auxiliary data
   struct CalculationData_item
     {
      S_dealsInARow     dealsCounter;
      int               R_arr[];
      double            DD_percent;
      double            Accomulated_DD,Accomulated_Profit;
      double            PL;
      double            Max_DD_forDeal,Max_Profit_forDeal;
      double            Max_DD_byPL,Max_Profit_byPL;
      datetime          DT_Max_DD_byPL,DT_Max_Profit_byPL;
      datetime          DT_Max_DD_forDeal,DT_Max_Profit_forDeal;
      int               Total_DD_numDeals,Total_Profit_numDeals;
     };
   struct CalculationData
     {
      CalculationData_item total,oneLot;
      int               num_deals;
      bool              isNot_firstDeal;
     };


As estruturas S_dealsCounter e S_dealsInARow são essencialmente uma única entidade. Uma combinação tão estranha de associação e herança ao mesmo tempo está ligada ao cálculo específico de seus parâmetros. A estrutura S_dealsInARow é criada para armazenar e calcular o número de operações (na verdade, para calcular posições, isto é, da abertura da posição ao fechamento) sucessivamente, sendo positiva ou negativa. A instância aninhada da estrutura S_dealsCounter é declarada para armazenar os resultados dos cálculos intermediários. Os campos herdados armazenam o total. Nós voltaremos à operação contando os negócios lucrativos/perdedores posteriormente.     

A estrutura CalculationData_item contém os campos necessários para o cálculo das métricas. 

  • R_arr — array de negócios lucrativos/perdedores consecutivos, mostradas como 1 / 0, respectivamente. O array é usada para o cálculo do Z score.
  • DD_percent — percentual de rebaixamento.
  • Accomulated_DD, Accomulated_Profit — armazena os valores totais da perda e lucro.
  • PL — lucro / prejuízo.
  • Max_DD_forDeal, Max_Profit_forDeal — como a nomeação sugere, eles armazenam o rebaixamento máximo e o lucro entre todas as negociações.
  • Max_DD_byPL, MÁx_Profit_byPL — armazena o rebaixamento máximo e o lucro calculado pelo gráfico PL. 
  • DT_Max_DD_byPL, DT_Max_Profit_byPL — armazena as datas de maior rebaixamento e lucro pelo gráfico PL. 
  • DT_Max_DD_forDeal, DT_Max_Profit_forDeal — datas de maior rebaixamento e lucro por operação.
  • Total_DD_numDeals, TotalProfit_numDeals — número total de negociações lucrativas e perdedoras. 

Os cálculos adicionais são baseados nos dados acima.

CalculationData é uma estrutura acumulativa que combina todas as estruturas descritas. Ele armazena todos os dados necessários. Ele também contém o campo num_deals, que na verdade é a soma de CalculationData_item::Total_DD_numDeals e CalculationData_item::TotalProfit_numDeals. O campo sNot_firstDeal é uma flag técnica que indica que o cálculo não é realizado para a primeira negociação.

A estrutura CoefChart_keeper:

struct CoefChart_keeper
     {
      CoefChart_item    OneLot_ShartRatio_chart[],Total_ShartRatio_chart[];
      CoefChart_item    OneLot_WinCoef_chart[],Total_WinCoef_chart[];
      CoefChart_item    OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[];
      CoefChart_item    OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[];
      CoefChart_item    OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[];
     };

Destina-se a armazenar as métricas dos gráficos. Como a classe cria não apenas os gráficos de lucro e lote, mas também alguns gráficos de métricas, outra estrutura foi criada para os tipos de dados descritos. O prefixo 'OneLot' mostra que a instância armazenará os dados recebidos da análise de lucros e perdas se negociar um lote. 'Total' significa o cálculo considerando o gerenciamento do lote. Se nenhum gerenciamento de lotes for usado na estratégia, os dois gráficos serão idênticos.

A classe СHistoryComparer:

Da mesma forma, uma classe a ser usada na classificação de dados é definida. O artigo "Os 100 Melhores Passes de Otimização" contém a descrição da classe CGenericSorter, que pode classificar qualquer tipo de dados em ordem crescente e decrescente. Além disso, ela precisa de uma classe que possa comparar os tipos passados. Essa classe é a isHisoryComparer.

class CHistoryComparer : public ICustomComparer<DealDetales>
     {
   public:
      int               Compare(DealDetales &x,DealDetales &y);
     };

A implementação do método é simples: ele compara as datas de fechamento à medida que a classificação é realizada por datas de fechamento:

int CReportCreator::CHistoryComparer::Compare(DealDetales &x,DealDetales &y)
  {
   return(x.DT_close == y.DT_close ? 0 : (x.DT_close > y.DT_close ? 1 : -1));
  }

Também existe uma classe semelhante para classificar os gráficos de métricas. Essas duas classes e a classe que realiza a ordenação são instanciadas como um campo global da classe CReportCreator descrita. Além dos objetos descritos, existem outros dois campos. Seus tipos são descritos como objetos separados e não aninhados:

PL_detales        PL_detales_data;
DistributionChart OneLot_PDF_chart,Total_PDF_chart;

A estrutura PL_detales contém breves informações sobre a negociação para posições rentáveis e perdedoras:

//+------------------------------------------------------------------+
struct PL_detales_PLDD
  {
   int               orders; // Number of deals
   double            orders_in_Percent; // Number of orders as % of total number of orders
   int               dealsInARow; // Deals in a row
   double            totalResult; // Total result in money
   double            averageResult; // Average result in money
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
struct PL_detales_item
  {
   PL_detales_PLDD   profit; // Information on profitable deals
   PL_detales_PLDD   drawdown; // Information on losing deals
  };
//+-------------------------------------------------------------------+
//| A brief PL graph summary divided into 2 main blocks               |
//+-------------------------------------------------------------------+
struct PL_detales
  {
   PL_detales_item   total,oneLot;
  };

A segunda estrutura DistributionChart contém vários valores de VaR, bem como o gráfico da distribuição com base na qual essas métricas foram calculadas. A distribuição é calculada como uma distribuição normal.

//+------------------------------------------------------------------+
//| Structure used for saving distribution charts                    |
//+------------------------------------------------------------------+
struct Chart_item
  {
   double            y; // y axis
   double            x; // x axis
  };
//+------------------------------------------------------------------+
//| Structure contains the VaR value                                 |
//+------------------------------------------------------------------+
struct VAR
  {
   double            VAR_90,VAR_95,VAR_99;
   double            Mx,Std;
  };
//+------------------------------------------------------------------+
//| Structure - it is used to store distribution charts and          |
//| the VaR values                                                   |
//+------------------------------------------------------------------+
struct Distribution_item
  {
   Chart_item        distribution[]; // Distribution chart
   VAR               VaR; // VaR
  };
//+------------------------------------------------------------------+
//| Structure - Stores distribution data. Divided into 2 blocks      |
//+------------------------------------------------------------------+
struct DistributionChart
  {
   Distribution_item absolute,growth;
  };

As métricas do VaR são calculadas de acordo com uma fórmula: VaR histórico, que pode não ser preciso o suficiente, mas é bastante adequado para a implementação atual. 

Métodos para calcular as métricas que descrevem os resultados de negociação

Agora que nós consideramos as estruturas de armazenamento de dados, você pode imaginar a enorme quantidade de estatísticas calculadas por essa classe. Vamos ver os métodos específicos para calcular os valores descritos um por um, como eles são nomeados na classe CReportCreator.

O CalcPL é criado para o cálculo do gráfico PL. Ele é implementado da seguinte maneira:

void CReportCreator::CalcPL(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type)
  {
   PLChart_item item;
   ZeroMemory(item);
   item.DT=deal.DT_close; // Saving the date

   if(type!=_Indicative)
     {
      item.Profit=(type==_Total ? data.total.PL : data.oneLot.PL); // Saving the profit
      item.Drawdown=(type==_Total ? data.total.DD_percent : data.oneLot.DD_percent); // Saving the drawdown
     }
   else // Calculating the indicative chart
     {
      if(data.isNot_firstDeal)
        {
         if(data.total.PL!=0)
           {
            if(data.total.PL > 0 && data.total.Max_DD_forDeal < 0)
               item.Profit=data.total.PL/MathAbs(data.total.Max_DD_forDeal);
            else
               if(data.total.PL<0 && data.total.Max_Profit_forDeal>0)
                  item.Profit=data.total.PL/data.total.Max_Profit_forDeal;
           }
        }
     }
// Adding data to array
   int s=ArraySize(pl_out);
   ArrayResize(pl_out,s+1,s+1);
   pl_out[s]=item;
  }

Como visto na implementação, todos os seus cálculos são baseados em dados de estruturas descritas anteriormente que são passadas como entrada.

Se você precisar calcular um gráfico PL não indicativo, simplesmente copie os dados conhecidos. Caso contrário, o cálculo está sujeito a duas condições: a primeira iteração não foi encontrada no ciclo e o gráfico PL é diferente de zero. O cálculo é realizado de acordo com a seguinte lógica:

  • Se o PL for maior que zero e o rebaixamento for menor, divida o valor atual do PL pelo valor do rebaixamento. Assim, nós obtemos uma métrica indicando quantos rebaixamentos máximos consecutivos são necessários para reduzir o PL atual para zero. 
  • Se o PL for menor que zero e o lucro máximo de todas as negociações for maior que zero, nós dividimos o valor do PL (que atualmente é o rebaixamento) pelo lucro máximo alcançado. Assim, nós obtemos uma métrica que mostra quantos lucros máximos seguidos seriam necessários para reduzir o rebaixamento atual para zero.

O próximo método CalcPLHist é baseado em um mecanismo semelhante, mas usa outros campos das estruturas para o cálculo: data.oneLot.Accomulated_DD, data.total.Accomulated_DD e data.oneLot.Accomulated_Profit, data.total.Accomulated_Profit. Nós já consideramos o seu algoritmo anteriormente, portanto, vamos para os próximos dois métodos.

CalcData e CalcData_item:

Esses métodos calculam todos as métricas auxiliares e principais. Vamos começar com o CalcData_item. Seu objetivo é calcular as métricas adicionais descritas acima, com base nos quais as principais métricas são calculadas.  

//+------------------------------------------------------------------+
//| Calculating auxiliary data                                       |
//+------------------------------------------------------------------+
void CReportCreator::CalcData_item(const DealDetales &deal,CalculationData_item &out,
                                   bool isOneLot)
  {
   double pl=(isOneLot ? deal.pl_oneLot : deal.pl_forDeal); //PL
   int n=0;
// Number of profits and losses
   if(pl>=0)
     {
      out.Total_Profit_numDeals++;
      n=1;
      out.dealsCounter.Counter.DD=0;
      out.dealsCounter.Counter.Profit++;
     }
   else
     {
      out.Total_DD_numDeals++;
      out.dealsCounter.Counter.DD++;
      out.dealsCounter.Counter.Profit=0;
     }
   out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD);
   out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit);

// Series of profits and losses
   int s=ArraySize(out.R_arr);
   if(!(s>0 && out.R_arr[s-1]==n))
     {
      ArrayResize(out.R_arr,s+1,s+1);
      out.R_arr[s]=n;
     }

   out.PL+=pl; //Total PL
// Max Profit / DD
   if(out.Max_DD_forDeal>pl)
     {
      out.Max_DD_forDeal=pl;
      out.DT_Max_DD_forDeal=deal.DT_close;
     }
   if(out.Max_Profit_forDeal<pl)
     {
      out.Max_Profit_forDeal=pl;
      out.DT_Max_Profit_forDeal=deal.DT_close;
     }
// Accumulated Profit / DD
   out.Accomulated_DD+=(pl>0 ? 0 : pl);
   out.Accomulated_Profit+=(pl>0 ? pl : 0);
// Extreme profit values
   double maxPL=MathMax(out.Max_Profit_byPL,out.PL);
   if(compareDouble(maxPL,out.Max_Profit_byPL)==1/* || !isNot_firstDeal*/)// another check is needed to save the date
     {
      out.DT_Max_Profit_byPL=deal.DT_close;
      out.Max_Profit_byPL=maxPL;
     }
   double maxDD=out.Max_DD_byPL;
   double DD=0;
   if(out.PL>0)
      DD=out.PL-maxPL;
   else
      DD=-(MathAbs(out.PL)+maxPL);
   maxDD=MathMin(maxDD,DD);
   if(compareDouble(maxDD,out.Max_DD_byPL)==-1/* || !isNot_firstDeal*/)// another check is needed to save the date
     {
      out.Max_DD_byPL=maxDD;
      out.DT_Max_DD_byPL=deal.DT_close;
     }
   out.DD_percent=(balance>0 ?(MathAbs(DD)/(maxPL>0 ? maxPL : balance)) :(maxPL>0 ?(MathAbs(DD)/maxPL) : 0));
  }

Primeiramente, o PL é calculado na i-ésima iteração. Então, se houver lucro nessa iteração, nós aumentamos o contador lucrativo e zeramos o contador de perdas consecutivas. Além disso, nós definimos o valor 1 para a variável n, o que significa que o negócio foi lucrativo. E se o PL estava abaixo de zero, aumentamos o contador perdedor e zeramos o contador lucrativo. Depois disso nós atribuímos o número máximo de séries lucrativas e perdedoras consecutivas.

O próximo passo é calcular a série de negócios lucrativos e perdedores. Uma série significa os negócios consecutivos de ganho ou perda. Neste array, zero é sempre seguido por um, enquanto um é sempre seguido por zero. Isso mostra a alternância das operações ganhadoras e perdedoras, no entanto, 0 ou 1 pode significar vários negócios. Esse array será usado para calcular a pontuação Z, que mostra o grau de aleatoriedade da negociação. O próximo passo é atribuir os valores de lucro/rebaixamento máximo e calcular o lucro/perda acumulado. No final deste método, os pontos extremos são calculados, ou seja, as estruturas com os valores máximos de ganho e perda são preenchidas.

Os dados do CalcData já usam os dados intermediários obtidos para calcular as métricas necessários e atualizar os cálculos a cada iteração. Ele é implementado da seguinte maneira:

void CReportCreator::CalcData(const DealDetales &deal,CalculationData &out,bool isBH)
  {
   out.num_deals++; // Counting the number of deals
   CalcData_item(deal,out.oneLot,true);
   CalcData_item(deal,out.total,false);

   if(!isBH)
     {
      // Fill PL graphs
      CalcPL(deal,out,PL.PL_total,_Total);
      CalcPL(deal,out,PL.PL_oneLot,_OneLot);
      CalcPL(deal,out,PL.PL_Indicative,_Indicative);

      // Fill PL Histogram graphs
      CalcPLHist(deal,out,PL_hist.PL_total,_Total);
      CalcPLHist(deal,out,PL_hist.PL_oneLot,_OneLot);
      CalcPLHist(deal,out,PL_hist.PL_Indicative,_Indicative);

      // Fill PL graphs by days
      CalcDailyPL(DailyPL_data.absolute_close,CALC_FOR_CLOSE,deal);
      CalcDailyPL(DailyPL_data.absolute_open,CALC_FOR_OPEN,deal);
      CalcDailyPL(DailyPL_data.avarage_close,CALC_FOR_CLOSE,deal);
      CalcDailyPL(DailyPL_data.avarage_open,CALC_FOR_OPEN,deal);

      // Fill Profit Factor graphs
      ProfitFactor_chart_calc(CoefChart_data.OneLot_ProfitFactor_chart,out,deal,true);
      ProfitFactor_chart_calc(CoefChart_data.Total_ProfitFactor_chart,out,deal,false);

      // Fill Recovery Factor graphs
      RecoveryFactor_chart_calc(CoefChart_data.OneLot_RecoveryFactor_chart,out,deal,true);
      RecoveryFactor_chart_calc(CoefChart_data.Total_RecoveryFactor_chart,out,deal,false);

      // Fill winning coefficient graphs
      WinCoef_chart_calc(CoefChart_data.OneLot_WinCoef_chart,out,deal,true);
      WinCoef_chart_calc(CoefChart_data.Total_WinCoef_chart,out,deal,false);

      // Fill Sharpe Ration graphs
      ShartRatio_chart_calc(CoefChart_data.OneLot_ShartRatio_chart,PL.PL_oneLot,deal/*,out.isNot_firstDeal*/);
      ShartRatio_chart_calc(CoefChart_data.Total_ShartRatio_chart,PL.PL_total,deal/*,out.isNot_firstDeal*/);

      // Fill Z Score graphs
      AltmanZScore_chart_calc(CoefChart_data.OneLot_AltmanZScore_chart,(double)out.num_deals,
                              (double)ArraySize(out.oneLot.R_arr),(double)out.oneLot.Total_Profit_numDeals,
                              (double)out.oneLot.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal);
      AltmanZScore_chart_calc(CoefChart_data.Total_AltmanZScore_chart,(double)out.num_deals,
                              (double)ArraySize(out.total.R_arr),(double)out.total.Total_Profit_numDeals,
                              (double)out.total.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal);
     }
   else // Fill PL Buy and Hold graphs
     {
      CalcPL(deal,out,BH.PL_total,_Total);
      CalcPL(deal,out,BH.PL_oneLot,_OneLot);
      CalcPL(deal,out,BH.PL_Indicative,_Indicative);

      CalcPLHist(deal,out,BH_hist.PL_total,_Total);
      CalcPLHist(deal,out,BH_hist.PL_oneLot,_OneLot);
      CalcPLHist(deal,out,BH_hist.PL_Indicative,_Indicative);
     }

   if(!out.isNot_firstDeal)
      out.isNot_firstDeal=true; // Flag "It is NOT the first deal"
  }

Primeiramente, as métricas intermediárias são calculadas para um sistema de negociação de um lote e lotes gerenciados, chamando o método descrito para os dois tipos de dados. Então o cálculo é dividido em métricas para BH e os dados do tipo oposto. As métricas interpretáveis são calculadas dentro de cada bloco. Somente os gráficos são calculados para a Estratégia Buy and Hold e, portanto, os métodos de cálculo da métrica não são chamados.  

O próximo grupo de métodos calcula o lucro/perda dividido por dia:

//+------------------------------------------------------------------+
//| Create a structure of trading during a day                       |
//+------------------------------------------------------------------+
void CReportCreator::CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,const DealDetales &deal)
  {
   cmpDay(deal,MONDAY,out.Mn,calcBy);
   cmpDay(deal,TUESDAY,out.Tu,calcBy);
   cmpDay(deal,WEDNESDAY,out.We,calcBy);
   cmpDay(deal,THURSDAY,out.Th,calcBy);
   cmpDay(deal,FRIDAY,out.Fr,calcBy);
  }
//+------------------------------------------------------------------+
//| Save resulting PL/DD for the day                                 |
//+------------------------------------------------------------------+
void CReportCreator::cmpDay(const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy)
  {
   ENUM_DAY_OF_WEEK day=(calcBy==CALC_FOR_CLOSE ? deal.day_close : deal.day_open);
   if(day==etalone)
     {
      if(deal.pl_forDeal>0)
        {
         ans.Profit+=deal.pl_forDeal;
         ans.numTrades_profit++;
        }
      else
         if(deal.pl_forDeal<0)
           {
            ans.Drawdown+=MathAbs(deal.pl_forDeal);
            ans.numTrades_drawdown++;
           }
     }
  }
//+------------------------------------------------------------------+
//| Average resulting PL/DD for the day                              |
//+------------------------------------------------------------------+
void CReportCreator::avarageDay(PLDrawdown &day)
  {
   if(day.numTrades_profit>0)
      day.Profit/=day.numTrades_profit;
   if(day.numTrades_drawdown > 0)
      day.Drawdown/=day.numTrades_drawdown;
  }



O trabalho principal que consiste em dividir o lucro/DD por dia é realizado no método cmpDay, que primeiro verifica se o dia corresponde ao dia solicitado ou não e, em seguida, adiciona os valores de ganhos e perdas. As perdas são somadas em módulo. CalcDailyPL é um método de agregação, no qual é feita uma tentativa de adicionar o PL atual passado a um dos cinco dias úteis. O método avarageDay é chamado para calcular a média de ganhos/perdas no método principal Create. Esse método não executa nenhuma ação específica, enquanto ele calcula apenas a média com base nos valores absolutos do lucro/perda calculados anteriormente. 

Método de cálculo do Fator de Lucro

//+------------------------------------------------------------------+
//| Calculate Profit Factor                                          |
//+------------------------------------------------------------------+
void CReportCreator::ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot)
  {
   CoefChart_item item;
   item.DT=deal.DT_close;
   double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit);
   double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD);
   if(dd==0)
      item.coef=0;
   else
      item.coef=profit/dd;
   int s=ArraySize(out);
   ArrayResize(out,s+1,s+1);
   out[s]=item;
  }

O método calcula um gráfico refletindo a alteração do Fator de Lucro ao longo da negociação. O último valor é o mostrado no relatório de teste. A fórmula é simples = lucro acumulado / rebaixamento acumulado. Se o rebaixamento for zero, então a métrica será igual a zero, pois na aritmética clássica é impossível dividir por zero sem usar limites, e a mesma regra se aplica a linguagem. Portanto, nós adicionaremos as verificações do divisor para todas as operações aritméticas.

O princípio do cálculo do Fator de Recuperação é semelhante:

//+------------------------------------------------------------------+
//| Calculate Recovery Factor                                        |
//+------------------------------------------------------------------+
void CReportCreator::RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot)
  {
   CoefChart_item item;
   item.DT=deal.DT_close;
   double pl=(isOneLot ? data.oneLot.PL : data.total.PL);
   double dd=MathAbs(isOneLot ? data.oneLot.Max_DD_byPL : data.total.Max_DD_byPL);
   if(dd==0)
      item.coef=0;//ideally it should be plus infinity
   else
      item.coef=pl/dd;
   int s=ArraySize(out);
   ArrayResize(out,s+1,s+1);
   out[s]=item;
  }

Fórmula de cálculo da métrica: lucro na i-ésima iteração / rebaixamento na i-ésima iteração. Observe também que, como o lucro pode ser zero ou negativo durante o cálculo da métrica, a própria métrica pode ser zero ou negativo.

Taxa de acerto

//+------------------------------------------------------------------+
//| Calculate Win Rate                                               |
//+------------------------------------------------------------------+
void CReportCreator::WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot)
  {
   CoefChart_item item;
   item.DT=deal.DT_close;
   double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit);
   double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD);
   int n_profit=(isOneLot ? data.oneLot.Total_Profit_numDeals : data.total.Total_Profit_numDeals);
   int n_dd=(isOneLot ? data.oneLot.Total_DD_numDeals : data.total.Total_DD_numDeals);
   if(n_dd == 0 || n_profit == 0)
      item.coef = 0;
   else
      item.coef=(profit/n_profit)/(dd/n_dd);
   int s=ArraySize(out);
   ArrayResize(out,s+1,s+1);
   out[s]=item;
  }

Fórmula de cálculo da taxa de ganhos = (lucro / número de negociações lucrativas) / (rebaixamento / número de negócios perdedores). Essa métrica também pode ser negativa, se não houver lucro no momento do cálculo. 

O cálculo do Sharpe Ratio é um pouco mais complicado:

//+------------------------------------------------------------------+
//| Calculate Sharpe Ratio                                           |
//+------------------------------------------------------------------+
double CReportCreator::ShartRatio_calc(PLChart_item &data[])
  {
   int total=ArraySize(data);
   double ans=0;
   if(total>=2)
     {
      double pl_r=0;
      int n=0;
      for(int i=1; i<total; i++)
        {
         if(data[i-1].Profit!=0)
           {
            pl_r+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit;
            n++;
           }
        }
      if(n>=2)
         pl_r/=(double)n;
      double std=0;
      n=0;
      for(int i=1; i<total; i++)
        {
         if(data[i-1].Profit!=0)
           {
            std+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-pl_r,2);
            n++;
           }
        }
      if(n>=2)
         std=MathSqrt(std/(double)(n-1));

      ans=(std!=0 ?(pl_r-r)/std : 0);
     }
   return ans;
  }

No primeiro ciclo, é calculado o lucro médio pelo gráfico PL, na qual o i-ésimo lucro é calculado como a razão do aumento sobre o valor do PL anterior. O cálculo é baseado no exemplo da normalização das séries de preço usada para a avaliação das séries temporais. 

No próximo ciclo, a volatilidade é calculada usando a mesma série de lucro normalizado.

Depois disso, a métrica em si é calculada usando a fórmula (lucro médio - taxa livre de risco) / volatilidade (desvio padrão dos retornos).

Talvez eu tenha aplicado uma abordagem não tradicional na normalização das séries e provavelmente até na fórmula, mas esse cálculo parece bastante razoável. Se você encontrar algum erro, adicione um comentário ao artigo.

Cálculo do VaR e gráfico de distribuição normal. Esta parte consiste em três métodos. Dois deles são para o cálculo, o terceiro agrega todos os cálculos. Vamos considerar esses métodos.

//+------------------------------------------------------------------+
//| Distribution calculation                                         |
//+------------------------------------------------------------------+
void CReportCreator::NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[])
  {
   double Mx_absolute=0,Mx_growth=0,Std_absolute=0,Std_growth=0;
   int total=ArraySize(data);
   ZeroMemory(out.absolute);
   ZeroMemory(out.growth);
   ZeroMemory(out.absolute.VaR);
   ZeroMemory(out.growth.VaR);
   ArrayFree(out.absolute.distribution);
   ArrayFree(out.growth.distribution);

// Calculation of distribution parameters
   if(total>=2)
     {
      int n=0;
      for(int i=0; i<total; i++)
        {
         Mx_absolute+=data[i].Profit;
         if(i>0 && data[i-1].Profit!=0)
           {
            Mx_growth+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit;
            n++;
           }
        }
      Mx_absolute/=(double)total;
      if(n>=2)
         Mx_growth/=(double)n;

      n=0;
      for(int i=0; i<total; i++)
        {
         Std_absolute+=MathPow(data[i].Profit-Mx_absolute,2);
         if(i>0 && data[i-1].Profit!=0)
           {
            Std_growth+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-Mx_growth,2);
            n++;
           }
        }
      Std_absolute=MathSqrt(Std_absolute/(double)(total-1));
      if(n>=2)
         Std_growth=MathSqrt(Std_growth/(double)(n-1));

      // Calculate VaR
      out.absolute.VaR.Mx=Mx_absolute;
      out.absolute.VaR.Std=Std_absolute;
      out.absolute.VaR.VAR_90=VaR(Q_90,Mx_absolute,Std_absolute);
      out.absolute.VaR.VAR_95=VaR(Q_95,Mx_absolute,Std_absolute);
      out.absolute.VaR.VAR_99=VaR(Q_99,Mx_absolute,Std_absolute);
      out.growth.VaR.Mx=Mx_growth;
      out.growth.VaR.Std=Std_growth;
      out.growth.VaR.VAR_90=VaR(Q_90,Mx_growth,Std_growth);
      out.growth.VaR.VAR_95=VaR(Q_95,Mx_growth,Std_growth);
      out.growth.VaR.VAR_99=VaR(Q_99,Mx_growth,Std_growth);

      // Calculate distribution
      for(int i=0; i<total; i++)
        {
         Chart_item  item_a,item_g;
         ZeroMemory(item_a);
         ZeroMemory(item_g);
         item_a.x=data[i].Profit;
         item_a.y=PDF_calc(Mx_absolute,Std_absolute,data[i].Profit);
         if(i>0)
           {
            item_g.x=(data[i-1].Profit != 0 ?(data[i].Profit-data[i-1].Profit)/data[i-1].Profit : 0);
            item_g.y=PDF_calc(Mx_growth,Std_growth,item_g.x);
           }
         int s=ArraySize(out.absolute.distribution);
         ArrayResize(out.absolute.distribution,s+1,s+1);
         out.absolute.distribution[s]=item_a;
         s=ArraySize(out.growth.distribution);
         ArrayResize(out.growth.distribution,s+1,s+1);
         out.growth.distribution[s]=item_g;
        }
      // Ascending
      sorter.Sort<Chart_item>(out.absolute.distribution,&chartComparer);
      sorter.Sort<Chart_item>(out.growth.distribution,&chartComparer);
     }
  }
//+------------------------------------------------------------------+
//| Calculate VaR                                                    |
//+------------------------------------------------------------------+
double CReportCreator::VaR(double quantile,double Mx,double Std)
  {
   return Mx-quantile*Std;
  }
//+------------------------------------------------------------------+
//| Distribution calculation                                         |
//+------------------------------------------------------------------+
double CReportCreator::PDF_calc(double Mx,double Std,double x)
  {
   if(Std!=0)
      return MathExp(-0.5*MathPow((x-Mx)/Std,2))/(MathSqrt(2*M_PI)*Std);
   else
      return 0;
  }

O método de cálculo do VaR é o mais simples. Ele usa o modelo do VaR histórico nos cálculos.

O método de cálculo da distribuição normalizada é o disponível no pacote de análise estatística Matlab.

O cálculo da distribuição normalizada e o método de construção do gráfico é um método agregador, no qual os métodos descritos acima são aplicados. No primeiro ciclo, o valor médio do lucro é calculado. No segundo ciclo, o desvio padrão dos retornos é calculado. Os retornos para o gráfico e o VaR calculado pelo crescimento também são calculados como uma série temporal normalizada. Além disso, após preencher o valor do VaR, o gráfico de distribuição normal é calculado usando o método acima. Como eixo x, nós usamos a rentabilidade para o gráfico baseado em crescimento e os valores absolutos de lucro para o gráfico baseado em lucro.

Para calcular o Z score, eu usei uma fórmula de um dos artigos deste site. Sua implementação completa está disponível nos arquivos anexados. 

Observe que todos os cálculos começam com o método Calculate com a seguinte assinatura de chamada

void CReportCreator::Create(DealDetales &history[],DealDetales &BH_history[],const double _balance,const string &Symb[],double _r);

Sua implementação foi descrita no artigo mencionado anteriormente "Os 100 Melhores Passes de Otimização". Todos os métodos públicos não executam nenhuma operação lógica, mas servem como getters que formam os dados solicitados de acordo com os parâmetros de entrada, indicando o tipo de informação necessária.  

Conclusão

No artigo anterior, nós consideramos o processo de desenvolvimento das bibliotecas na linguagem C#. Neste artigo, nós passamos para a próxima etapa — a criação de um relatório de negociação, que nós podemos obter usando os métodos criados. O mecanismo de geração de relatórios já foi considerado em artigos anteriores. Mas isso foi aprimorado e revisado. Este artigo apresenta as versões mais recentes desses desenvolvimentos. A solução oferecida foi testada em várias otimizações e processos de teste.

 

Duas pastas estão disponíveis no arquivo anexado. Descompacte os dois no diretório MQL/Include. 

Os seguintes arquivos estão incluídos no anexo:

  1. CustomGeneric
    • GenericSorter.mqh
    • ICustomComparer.mqh
  2. History manager
    • CustomComissionManager.mqh
    • DealHistoryGetter.mqh
    • ReportCreator.mqh

Traduzido do russo pela MetaQuotes Software Corp.
Artigo original: https://www.mql5.com/ru/articles/7452

Arquivos anexados |
Include.zip (23.27 KB)
SQLite: trabalho nativo com bancos de dados SQL em MQL5 SQLite: trabalho nativo com bancos de dados SQL em MQL5

O desenvolvimento de estratégias de negociação está associado ao processamento de grandes quantidades de dados. Agora, em MQL5, você pode trabalhar com bancos de dados usando consultas SQL baseadas no SQLite. Uma vantagem importante desse mecanismo é que todo o banco de dados está contido em um único arquivo, localizado no computador do usuário.

Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXIV): classe básica de negociação, correção automática de parâmetros errados Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXIV): classe básica de negociação, correção automática de parâmetros errados

No artigo, analisaremos um manipulador de parâmetros errôneos de uma ordem de negociação, finalizaremos a classe básica de negociação e também corrigiremos o funcionamento da classe de eventos de negociação - agora todos os eventos de negociação serão detectados corretamente nos programas.

Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXV): processamento de erros retornados pelo servidor de negociação Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXV): processamento de erros retornados pelo servidor de negociação

Depois de enviarmos uma ordem de negociação para o servidor, não devemos assumir que o trabalho está concluído, uma vez que é necessário verificar quer os códigos de erro quer a ausência de erros. No artigo, veremos o processamento de erros retornados pelo servidor de negociação e prepararemos a base para a criação de ordens de negociação pendentes.

Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXVI): trabalho com ordens de negociação pendentes - primeira implementação (abertura de posições) Biblioteca para criação simples e rápida de programas para MetaTrader (Parte XXVI): trabalho com ordens de negociação pendentes - primeira implementação (abertura de posições)

No artigo, abordaremos o armazenamento de alguns dados no valor do número mágico de ordens e posições, e implementaremos ordens pendentes. Para examinar a ideia, criaremos a primeira ordem pendente de teste para abrir posições a mercado quando recebermos um erro do servidor requerendo aguardar e enviar uma segunda solicitação.