English Русский 中文 Español Deutsch 日本語
preview
Avaliação visual de resultados de otimização

Avaliação visual de resultados de otimização

MetaTrader 5Testador | 15 março 2022, 07:15
1 041 1
Aleksandr Slavskii
Aleksandr Slavskii

Introdução

Os critérios de otimização personalizados são parâmetros muito convenientes e flexíveis para otimizar Expert Advisors, porém, para verificar vários critérios, precisamos executar várias otimizações, o que consome muito tempo. Eu gostaria de poder verificar vários critérios personalizados de uma só vez em uma otimização. E não apenas ficar por aí, senão que também ver imediatamente o gráfico de saldo e capital líquido.

Parafraseando o famoso escritor Sergei Mikhalkov, podemos dizer o seguinte: diferentes visualizações são necessárias, diferentes visualizações são importantes. E esta será uma afirmação verdadeira, já que mais de oitenta por cento das informações são recebidas pelo nosso cérebro através dos olhos. Por isso, a discussão neste artigo se concentrará em como construir gráficos que abarquem todas as otimizações e como selecionar o critério personalizado ideal.

Além disso, falaremos sobre como programarmos o que quisermos, simplesmente recorrendo a um conhecimento mínimo em MQL5, a uma grande vontade, ao uso dos artigos do site e aos comentários do fórum.


Definamos a tarefa

  1. Coletar dados de cada passagem de otimização.
  2. Plotar gráficos de saldo/capital líquido para cada passagem de otimização.
  3. Calcular vários critérios personalizados de otimização.
  4. Classificar os gráficos por critério de otimização personalizado crescente.
  5. Mostrar os melhores resultados de todos os critérios personalizados.


Etapas para resolver o problema

Como não podemos ficar sem fazer alterações no código do EA, tentaremos pelo menos minimizá-las.

  • Por isso, todo o código destinado à coleta de dados será escrito no arquivo incluído SkrShotOpt.mqh, e o critério personalizado será calculado no arquivo CustomCriterion.mqh,
  • O script ScreenShotOptimization.mq5 desenhará gráficos e salvará capturas de tela.

Assim, apenas algumas linhas de código precisarão ser adicionadas ao EA.


1. Coleta de dados.   SkrShotOpt.mqh

A função OnTick() armazenará os valores de capital líquido máximo e mínimo.

   double _Equity = AccountInfoDouble(ACCOUNT_EQUITY);
   if(tempEquityMax < _Equity)
      tempEquityMax = _Equity;
   if(tempEquityMin > _Equity)
      tempEquityMin = _Equity;

Para não verificar as alterações de posição a cada tick, rastrearemos as alterações de posição na função OnTradeTransaction()

void IsOnTradeTransaction(const MqlTradeTransaction & trans,
                          const MqlTradeRequest & request,
                          const MqlTradeResult & result)
  {
   if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
      if(HistoryDealSelect(trans.deal))
        {
         if(_deal_entry != DEAL_ENTRY_OUT && _deal_entry != DEAL_ENTRY_OUT_BY)
            _deal_entry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY);
         if(trans.deal_type == DEAL_TYPE_BUY || trans.deal_type == DEAL_TYPE_SELL)
            if(_deal_entry == DEAL_ENTRY_IN || _deal_entry == DEAL_ENTRY_OUT || _deal_entry == DEAL_ENTRY_INOUT || _deal_entry == DEAL_ENTRY_OUT_BY)
               allowed = true;
        }
  }

Ao alterar o número de negócios abertos, preenchemos as matrizes de saldo e de capital líquido.

   if(allowed) // if there was a trade
     {
      double accBalance = AccountInfoDouble(ACCOUNT_BALANCE);
      double accEquity = AccountInfoDouble(ACCOUNT_EQUITY);

      ArrayResize(balance, _size + 1);
      ArrayResize(equity, _size + 1);
      balance[_size] = accBalance;

      if(_deal_entry != DEAL_ENTRY_OUT && _deal_entry != DEAL_ENTRY_OUT_BY) // if a new position appeared
         equity[_size] = accEquity;
      else // if position closed
        {
         if(changesB < accBalance)
            equity[_size] = tempEquityMin;
         else
            switch(s_view)
              {
               case  min_max_E:
                  equity[_size] = tempEquityMax;
                  break;
               default:
                  equity[_size] = tempEquityMin;
                  break;
              }
         tempEquityMax = accEquity;
         tempEquityMin = accEquity;
        }

      _size = _size + 1;
      changesPos = PositionsTotal();
      changesB = accBalance;
      _deal_entry = -1;
      allowed = false;
     }

Como o arquivo que contém quadros não é de borracha aumentando para tamanhos não processáveis devido a inúmeros negócios, teremos de registrar o mínimo necessário de informações.

Ao abrir um negócio, registramos o valor do saldo e do capital líquido:

  • se o negócio for fechado no vermelho, registramos o valor do capital líquido máximo
  • já se o negócio for fechado no positivo, registramos o valor do capital líquido mínimo 

Assim, quase todos os negócios possuem quatro valores armazenados em matrizes: saldo e capital líquido na abertura, saldo e capital líquido máximo/mínimo no fechamento.

Acontece que, quando em um tick uma posição é fechada e outra é aberta, será registrada apenas uma posição e não duas. Isso não afetará os cálculos e a visualização dos gráficos, mas as matrizes diminuirão.

 

Salvando os dados coletados em um arquivo

Faz sentido coletar apenas execuções de otimizações rentáveis. Este parâmetro é movido para as configurações, portanto, se desejarmos, podemos gravar as não lucrativas. Todas as passagens forward são registradas.

Os dados coletados serão salvos em um arquivo utilizando a função FrameAdd(), ao final de cada execução, após ocorrido o evento Tester, este evento será processado pela função OnTester().

bool  FrameAdd( 
   const string  name,        // public name/tag
   long          id,          // public id 
   double        value,       // value
   const void&   data[]       // array of any type
   );

Para mais informações sobre como usar a função FrameAdd() consulte:  https://www.mql5.com/ru/forum/11277/page4#comment_469771

Como FrameAdd() pode escrever apenas uma matriz e um valor numérico value, e, além do saldo e do capital líquido, quero transferir todos os valores da enumeração ENUM_STATISTICS para análise posterior, decidi escrever tudo em uma matriz sequencialmente e gravar o tamanho da mesma no valor numérico transmitido value.

   if(id == 1)  // if it is a backward pass
     {
      // if profit % and the number of trades exceed those specified in the settings, the pass is written into the file
      if(TesterStatistics(STAT_PROFIT) / TesterStatistics(STAT_INITIAL_DEPOSIT) * 100 > _profit && TesterStatistics(STAT_TRADES) >= trades)
        {
         double TeSt[42]; // total number of elements in the ENUM_STATISTICS enumeration is 41
         IsRecordStat(TeSt); // writing testing statistics to the array
         IsCorrect(); // adjusting balance and equity arrays

         if(m_sort != none)
           {
            while((sort)size_sort != none)
               size_sort++;
            double LRB[], LRE[], coeff[];
            Coeff = Criterion(balance, equity, LRB, LRE, TeSt, coeff, 3);// calculating custom criterion
            ArrayInsert(balance, equity, _size + 1, 0);     // joining balance and equity arrays into one
            ArrayInsert(balance, TeSt, (_size + 1) * 2, 0); // add array with ENUM_STATISTICS data to the resulting array
            FrameAdd(name, id, _size + 1, balance);         // write the frame into the file
           }
         else
           {
            ArrayInsert(balance, equity, _size + 1, 0);     // joining balance and equity arrays into one
            ArrayInsert(balance, TeSt, (_size + 1) * 2, 0); // add array with ENUM_STATISTICS data to the resulting array
            FrameAdd(name, id, _size + 1, balance);         // write the frame into the file
           }
        }
     }

As passagens forward são processadas da mesma forma que as backward, mas na verdade elas são o resultado da otimização, portanto, apenas os valores de saldo e capital líquido serão salvos, já o valor de enumeração ENUM_STATISTICS não.

Acontece que, quando no final do teste há uma posição aberta, o testador irá fechá-la por conta própria.

Por isso, fechamos o negócio virtualmente (registramos o saldo atual e o capital líquido) se a variável que armazena o número de negócios abertos for diferente de zero no final do teste.

void IsCorrect()
  {
   if(changesPos > 0) // if there is an open position by the testing end time, it should be virtually closed as the tester will close such a position
     {
      _size++;
      ArrayResize(balance, _size + 1);
      ArrayResize(equity, _size + 1);
      if(balance[_size - 2] > AccountInfoDouble(ACCOUNT_BALANCE))
        {
         balance[_size - 1] = AccountInfoDouble(ACCOUNT_BALANCE);
         switch(s_view)
           {
            case  min_max_E:
               equity[_size - 1] = tempEquityMax;
               break;
            default:
               equity[_size - 1] = tempEquityMin;
               break;
           }
        }
      else
        {
         balance[_size - 1] = AccountInfoDouble(ACCOUNT_BALANCE);
         equity[_size - 1] = tempEquityMin;
        }
      balance[_size] = AccountInfoDouble(ACCOUNT_BALANCE);
      equity[_size] = AccountInfoDouble(ACCOUNT_EQUITY);
     }
   else
     {
      ArrayResize(balance, _size + 1);
      ArrayResize(equity, _size + 1);
      balance[_size] = AccountInfoDouble(ACCOUNT_BALANCE);
      equity[_size] = AccountInfoDouble(ACCOUNT_EQUITY);
     }
  }

Isso completa o registro de dados.


Leitura de dados a partir de um arquivo. ScreenShotOptimization.mq5

Após a otimização, é criado um arquivo com quadros:  C:\Users\nome de usuário\AppData\Roaming\MetaQuotes\Terminal\identificador do terminal\MQL5\Files\Tester com extensão: nome do EA.símbolo.timeframe.mqd. Após a otimização, perde-se o acesso a este arquivo, mas após reiniciar o terminal, ele pode ser acessado usando funções de arquivo comuns.

Encontramos o arquivo necessário com quadros, ele está na pasta C:\Users\nome de usuário\AppData\Roaming\MetaQuotes\Terminal\ID do terminal\MQL5\Files\Tester.

   int count = 0;
   long search_handle = FileFindFirst("Tester\\*.mqd", FileName);
   do
     {
      if(FileName != "")
         count++;
      FileName = "Tester\\" + FileName;
     }
   while(FileFindNext(search_handle, FileName));
   FileFindClose(search_handle);

Primeiro, a leitura entra na estrutura.

FRAME Frame = {0};
FileReadStruct(handle, Frame);
struct FRAME
  {
   ulong             Pass;
   long              ID;
   short             String[64];
   double            Value;
   int               SizeOfArray;
   long              Tmp[2];

   void              GetArrayB(int handle, Data & m_FB)
     {
      ArrayFree(m_FB.Balance);
      FileReadArray(handle, m_FB.Balance, 0, (int)Value);
      ArrayFree(m_FB.Equity);
      FileReadArray(handle, m_FB.Equity, 0, (int)Value);
      ArrayFree(m_FB.TeSt);
      FileReadArray(handle, m_FB.TeSt, 0, (SizeOfArray / sizeof(m_FB.TeSt[0]) - (int)Value * 2));
     }
   void              GetArrayF(int handle, Data & m_FB, int size)
     {
      FileReadArray(handle, m_FB.Balance, size, (int)Value);
      FileReadArray(handle, m_FB.Equity, size, (int)Value);
     }
  };

Nas funções da estrutura FRAME, são preenchidas as matrizes da estrutura Data que permitirão construir gráficos posteriormente.

struct Data
  {
   ulong             Pass;
   long              id;
   int               size;
   double            Balance[];
   double            Equity[];
   double            LRegressB[];
   double            LRegressE[];
   double            coeff[];
   double            TeSt[];
  };
Data                 m_Data[];

Como desenhar vários milhares de capturas de tela é demorado, devemos especificar um parâmetro nas configurações do script para salvar capturas de tela só com rendimentos superiores a uma determinada porcentagem.

Todo o arquivo com quadros é processado em um loop. 

Para desenhar um gráfico, é necessária uma matriz com dados, por isso, primeiro, são registradas todas as passagens backward que satisfaçam a porcentagem de lucro especificada nas configurações do script.

Em seguida, são percorridas todas as passagens backward, sendo as passagens forward selecionadas em conformidade pelo número da mesma, e a matriz de saldo da passagem forward é adicionada à matriz de saldo da passagem backward.

Também é previsto o desenho de dois tipos de gráficos - um é o mesmo do testador de estratégia, o que quer dizer que a execução forward começa com o depósito inicial.

A segunda variante da passagem forward começa com o depósito que encerrou a passagem backward. Nesse caso, o valor do saldo da passagem backward é somado aos valores de saldo e capital líquido da passagem forward, e também é adicionado ao final da matriz com a passagem backward.

Obviamente, isso é feito se a otimização foi realizada com uma forward.

   int handle = FileOpen(FileName, FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_BIN);
   if(handle != INVALID_HANDLE)
     {
      FileSeek(handle, 260, SEEK_SET);

      while(Res && !IsStopped())
        {
         FRAME Frame = {0};
         // read from the file to the Frame structure
         Res = (FileReadStruct(handle, Frame) == sizeof(Frame));
         if(Res)
            if(Frame.ID == 1) // if it is a Backward pass, write data to the m_Data structure
              {
               ArrayResize(m_Data, size + 1);
               m_Data[size].Pass = Frame.Pass;
               m_Data[size].id = Frame.ID;
               m_Data[size].size = (int)Frame.Value;
               Frame.GetArrayB(handle, m_Data[size]);  // write data to the m_Data structure arrays
               // if profit of this pass corresponds to the input settings, immediately calculate optimization criteria
               if(m_Data[size].TeSt[STAT_PROFIT] / m_Data[size].TeSt[STAT_INITIAL_DEPOSIT] * 100 >= profitPersent)
                 {
                  Criterion(m_Data[size].Balance, m_Data[size].Equity, m_Data[size].LRegressB, m_Data[size].LRegressE, m_Data[size].TeSt, m_Data[size].coeff, m_lineR);
                  size++;
                 }
              }
            else  // if it is a Forward pass, write to the end of the m_Data data structures
               if(m_Forward != BackOnly) // if drawing of only Backward passes is not selected in settings
                  for(int i = 0; i < size; i++)
                    {
                     if(Frame.Pass == m_Data[i].Pass) // if Back and Forward pass numbers match
                       {
                        int m = 0;
                        if(m_Forward == Back_Next_Forward) // if selected drawing of Forward graph as a continuation of Backward
                          {
                           Frame.GetArrayF(handle, m_Data[i], m_Data[i].size - 1); // write data at the end of the the m_Data structure array, with a one-trade shift
                           for(int x = m_Data[i].size - 1; x < m_Data[i].size + (int)Frame.Value - 1; x++)
                             {
                              m_Data[i].Balance[x] = m_Data[i].Balance[x] + m_Data[i].TeSt[STAT_PROFIT]; //  add profit of the Backward test to the Forward pass
                              m_Data[i].Equity[x] = m_Data[i].Equity[x] + m_Data[i].TeSt[STAT_PROFIT];
                             }
                           m = 1;
                          }
                        else
                           Frame.GetArrayF(handle, m_Data[i], m_Data[i].size); // is drawing of a Forward pass from a starting balance is selected

                        m_Data[i].coeff[Forward_Trade] = (int)(Frame.Value / 2); // number of forward trades (not exact))
                        m_Data[i].coeff[Profit_Forward] = m_Data[i].Balance[m_Data[i].size + (int)Frame.Value - m - 1] - m_Data[i].Balance[m_Data[i].size - m];
                        break;
                       }
                     if(i == size - 1) // if no Backward is found for this Forward pass, move the file pointer to the end of writing
                        FileSeek(handle, Frame.SizeOfArray, SEEK_CUR); // of this frame as if we read array data from the file
                    }
        }
      FileClose(handle);
      //---


Plotagem

Função de gráficos.

string _GraphPlot(double& y1[],
                  double& y2[],
                  double& LRegressB[],
                  double& LRegressE[],
                  double& coeff[],
                  double& TeSt[],
                  ulong pass)
  {
   CGraphic graphic;
//--- create graphic
   bool res = false;
   if(ObjectFind(0, "Graphic") >= 0)
      res = graphic.Attach(0, "Graphic");
   else
      res = graphic.Create(0, "Graphic", 0, 0, 0, _width, _height);

   if(!res)
      return(NULL);

   graphic.BackgroundMain(FolderName);  // print the Expert Advisor name
   graphic.BackgroundMainSize(FontSet + 1); // font size for the Expert Advisor name

   graphic.IndentLeft(FontSet);
   graphic.HistoryNameSize(FontSet); // font size for the line names
   graphic.HistorySymbolSize(FontSet);

   graphic.XAxis().Name("pass " + IntegerToString(pass)); // show the pass number along the X axis
   graphic.XAxis().NameSize(FontSet + 1);

   graphic.XAxis().ValuesSize(12); // price font size
   graphic.YAxis().ValuesSize(12);

//--- add curves
   CCurve *curve = graphic.CurveAdd(y1, ColorToARGB(clrBlue), CURVE_POINTS_AND_LINES, "Balance"); // plot the balance graph
   curve.LinesWidth(widthL);  // graph line width
   curve.PointsSize(widthL + 1); // size of dots on the balance graph

   CCurve *curve1 = graphic.CurveAdd(y2, ColorToARGB(clrGreen), CURVE_LINES, "Equity");  // plot the equity graph
   curve1.LinesWidth(widthL);

   int size = 0;
   switch(m_lineR) // plot the regression line
     {
      case  lineR_Balance: // balance regression line
        {
         size = ArraySize(LRegressB);
         CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance");
         curve2.LinesWidth(widthL);
        }
      break;
      case  lineR_Equity: // equity regression line
        {
         size = ArraySize(LRegressE);
         CCurve *curve2 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity");
         curve2.LinesWidth(widthL);
        }
      break;
      case  lineR_BalanceEquity: // balance and equity regression line
        {
         size = ArraySize(LRegressB);
         CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance");
         curve2.LinesWidth(widthL);

         CCurve *curve3 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity");
         curve2.LinesWidth(widthL);
        }
      break;
      default:
         break;
     }
//--- plot curves
   graphic.CurvePlotAll();

// Important!!!  All lines and captions must be created after creating the graph; otherwise, the graph will override them

   if(size == 0)
     {
      size = ArraySize(LRegressE);
      if(size == 0)
         size = ArraySize(LRegressB);
     }

   int x1 = graphic.ScaleX(size - 1); //Scales the value of the number of trades along the X axis
   graphic.LineAdd(x1, 30, x1, _height - 45, ColorToARGB(clrBlue), LINE_END_BUTT); // construct a vertical line which indicates the end of the Backward pass

   string txt = "";
   int txt_x = 70;// text indent along the X axis
   int txt_y = 30;// text indent along the Y axis

   graphic.FontSet("Arial", FontSet);// Set current font parameters

   for(int i = 0; i < size_sort; i++)  // Write all coefficients and criteria on the chart
     {
      if(coeff[i] == 0)
         continue;
      if(i == 1 || i == 3)
         txt = StringFormat("%s = %d", EnumToString((sort)i), (int)coeff[i]);
      else
         if(i == 0 || i == 2)
            txt = StringFormat("%s = %.2f", EnumToString((sort)i), coeff[i]);
         else
            txt = StringFormat("%s = %.4f", EnumToString((sort)i), coeff[i]);
      graphic.TextAdd(txt_x, txt_y + FontSet * i, txt, ColorToARGB(clrGreen));
     }

   txt_y = txt_y + FontSet * (size_sort - 1);
   txt = StringFormat("Profitability = %.2f", TeSt[STAT_PROFIT_FACTOR]);
   graphic.TextAdd(txt_x, txt_y + FontSet, txt, ColorToARGB(clrGreen));
   txt = StringFormat("Expected  payoff = %.2f", TeSt[STAT_EXPECTED_PAYOFF]);
   graphic.TextAdd(txt_x, txt_y + FontSet * 2, txt, ColorToARGB(clrGreen));

   graphic.Update();
//--- return resource name
   return graphic.ChartObjectName();
  }


Para entender como trabalhar com CGraphic, podemos ler os artigos:


As capturas de tela do gráfico são salvas em uma pasta separada - Files. O nome da pasta contém: nome do EA.símbolo.timeframe.

bool BitmapObjectToFile(const string ObjName, const string _FileName, const bool FullImage = true)
  {
   if(ObjName == "")
      return(true);

   const ENUM_OBJECT Type = (ENUM_OBJECT)ObjectGetInteger(0, ObjName, OBJPROP_TYPE);
   bool Res = (Type == OBJ_BITMAP_LABEL) || (Type == OBJ_BITMAP);

   if(Res)
     {
      const string Name = __FUNCTION__ + (string)MathRand();

      ObjectCreate(0, Name, OBJ_CHART, 0, 0, 0);
      ObjectSetInteger(0, Name, OBJPROP_XDISTANCE, -5e3);

      const long chart = ObjectGetInteger(0, Name, OBJPROP_CHART_ID);

      Res = ChartSetInteger(chart, CHART_SHOW, false) && ObjectCreate(chart, Name, OBJ_BITMAP_LABEL, 0, 0, 0) &&
            ObjectSetString(chart, Name, OBJPROP_BMPFILE, ObjectGetString(0, ObjName, OBJPROP_BMPFILE)) &&
            (FullImage || (ObjectSetInteger(chart, Name, OBJPROP_XSIZE, ObjectGetInteger(0, ObjName, OBJPROP_XSIZE)) &&
                           ObjectSetInteger(chart, Name, OBJPROP_YSIZE, ObjectGetInteger(0, ObjName, OBJPROP_YSIZE)) &&
                           ObjectSetInteger(chart, Name, OBJPROP_XOFFSET, ObjectGetInteger(0, ObjName, OBJPROP_XOFFSET)) &&
                           ObjectSetInteger(chart, Name, OBJPROP_YOFFSET, ObjectGetInteger(0, ObjName, OBJPROP_YOFFSET)))) &&
            ChartScreenShot(chart, FolderName + "\\" + _FileName, (int)ObjectGetInteger(chart, Name, OBJPROP_XSIZE), (int)ObjectGetInteger(chart, Name, OBJPROP_YSIZE));
      ObjectDelete(0, Name);
     }

   return(Res);
  }


Aqui estão os gráficos resultantes.

É assim que os gráficos ficam na pasta.

Se for selecionado imprimir todas as telas, os nomes das capturas de tela serão: o critério personalizado selecionado para classificação + lucro + número da passagem. 

Se for selecionado imprimir apenas as melhores, os nomes das capturas de tela serão: critério personalizado + lucro.

O gráfico desenhado pelo script fica assim:


Bem, para comparação, o mesmo gráfico do testador de estratégia


Aqui mostrei os gráficos mais parecidos, mas na maioria dos casos os gráficos serão muito diferentes dos de teste. Em primeiro lugar, devido ao fato de que no gráfico do testador, os negócios no eixo X estão vinculados ao tempo e o script desenha gráficos nos quais o eixo X está vinculado ao número de negócios. Além disso, devido ao fato de termos que escrever um mínimo de informações no quadro para não inflar o arquivo, os valores de capital líquido claramente não são suficientes para analisar completamente os gráficos desenhados pelo script, mas essas informações são suficientes para obter uma avaliação inicial da eficácia de uma passagem de otimização específica e também para calcular um critério de otimização personalizado.

Após a otimização, devemos reiniciar o terminal antes de executar o script ScreenShotOptimization!

Inicialmente, queria ver todos os gráficos de otimização, mas após a implementação vi sete mil capturas de tela na pasta, e notei que era impossível trabalhar com tantas. É necessário escolher as melhores e definir critérios para isso.

Há muito tempo eu tenho notado que os algotraders se enquadram em duas categorias:

  1. Aqueles que acreditam que o EA precisa ser otimizado durante um período de tempo muito longo, de preferência várias décadas.
  2. Aqueles que pensam que o EA precisa ser reotimizado regularmente usando um curto período de tempo, como um mês de otimização + uma semana de negociação, três meses de otimização + um mês de negociação ou outro período de reotimização.

Eu pertenço ao segundo tipo.

Por isso, optou-se por buscar critérios de otimização personalizados que servirão de filtro para selecionar as melhores.


Criando critérios de otimização personalizados

Os cálculos de todos os critérios de otimização personalizados serão feitos pelo arquivo incluído CustomCriterion.mqh, pois todos eles serão usados tanto no trabalho do script para desenhar gráficos quanto no trabalho do Expert Advisor a ser otimizado.

Antes de criar meu próprio critério de otimização, li tudo o que encontrei sobre esse tópico no site.

R-quadrado como uma estimativa da qualidade da curva de saldo da estratégia

O artigo descreve detalhadamente o que é regressão linear e como calculá-la usando a biblioteca AlgLib, bem como o coeficiente de determinação R^2 e sua aplicação na avaliação de resultados de testes. Recomendo a sua leitura.

Função para calcular regressão linear, R^2, bem como ProfitStability:

void Coeff(double& Array[], double& LR[], double& coeff[], double& TeSt[], int total, int c)
  {
//-- Fill the matrix: Y - Array value, X - ordinal number of the value
   CMatrixDouble xy(total, 2);
   for(int i = 0; i < total; i++)
     {
      xy[i].Set(0, i);
      xy[i].Set(1, Array[i]);
     }

//-- Find coefficients a and b of the linear model y = a*x + b;
   int retcode = 0;
   double a, b;
   CLinReg::LRLine(xy, total, retcode, b, a);

//-- Generate the linear regression values for each X;
   ArrayResize(LR, total);
   for(int x = 0; x < total; x++)
      LR[x] = x * a + b;

   if(m_calc == c)
     {
      //-- Find the coefficient of correlation of values with their linear regression
      corr = CAlglib::PearsonCorr2(Array, LR);

      //-- Find R^2 and its sign
      coeff[r2] = MathPow(corr, 2.0);
      int sign = 1;
      if(Array[0] > Array[total - 1])
         sign = -1;
      coeff[r2] *= sign;

      //-- Find LR Standard Error
      if(total - 2 == 0)
         stand_err = 0;
      else
        {
         for(int i = 0; i < total; i++)
           {
            double delta = MathAbs(Array[i] - LR[i]);
            stand_err =  stand_err + delta * delta;
           }
         stand_err = MathSqrt(stand_err / (total - 2));
        }
     }
//-- Find ProfitStability = Profit_LR/stand_err
   if(stand_err == 0)
      coeff[ProfitStability] = 0;
   else
      coeff[ProfitStability] = (LR[total - 1] - LR[0]) / stand_err;
  }


Otimizamos a estratégia de acordo com o gráfico de saldo e comparamos os resultados com o critério "balance + max sharpe ratio"

A partir deste artigo, fiz o cálculo do critério de otimização personalizado ProfitStability. É calculado de forma simples: primeiro, calculamos o LR Standard error, isto é, o desvio médio da linha de regressão em relação ao gráfico de saldo ou patrimônio líquido. Em seguida, subtraímos o valor inicial do valor final da linha de regressão, assim obtemos TrendProfit.

ProfitStability é calculado como a razão entre TrendProfit e LR Standard error.

O artigo descreve detalhadamente todos os prós e contras deste critério de otimização, e também mostra diversos testes para comparar ProfitStability com outros critérios de otimização.

Como a regressão linear pode ser calculada tanto a partir do saldo quanto do capital líquido e ProfitStability está vinculado ao cálculo da regressão linear, o cálculo de ProfitStability é movido para a função de cálculo da regressão linear.


Criando critérios de otimização próprios para parâmetros do EA

O artigo é bem antigo, de 2011, mas interessante e ainda relevante. A partir dele tirei o cálculo do coeficiente de segurança do sistema de negociação (CSSN)

CSSN = Avg.Win / Avg.Loss ((110% - %Win) / (%Win-10%) + 1)

   if(TeSt[STAT_PROFIT_TRADES] == 0 || TeSt[STAT_LOSS_TRADES] == 0 || TeSt[STAT_TRADES] == 0)
      coeff[TSSF] = 0;
   else
     {
      double  avg_win = TeSt[STAT_GROSS_PROFIT] / TeSt[STAT_PROFIT_TRADES];
      double  avg_loss = -TeSt[STAT_GROSS_LOSS] / TeSt[STAT_LOSS_TRADES];
      double  win_perc = 100.0 * TeSt[STAT_PROFIT_TRADES] / TeSt[STAT_TRADES];
      //   Calculate safe ratio for this percentage of profitable trades:
      if((win_perc - 10.0) + 1.0 == 0)
         coeff[TSSF] = 0;
      else
        {
         double  teor = (110.0 - win_perc) / (win_perc - 10.0) + 1.0;
         //  Calculate real ratio:
         double  real = avg_win / avg_loss;
         if(teor != 0)
            coeff[TSSF] = real / teor;
         else
            coeff[TSSF] = 0;
        }
     }


Abordagem ideal para o desenvolvimento e análise de sistemas de negociação

A partir deste artigo, tirei o fator linear (LinearFactor), que é calculado da seguinte forma:

  • LinearFactor = MaxDeviation/EndBalance
  • MaxDeviaton = Max(MathAbs(Balance[i]-AverageLine))
  • AverageLine=StartBalance+K*i
  • K=(EndBalance-StartBalance)/n
  • n - número de negócios no teste

Mais detalhes podem ser encontrados no artigo do autor. O artigo é muito interessante e, embora tenha sido criticado decentemente nos comentários (na minha humilde opinião, sem razão), você pode aprender muitas coisas úteis com ele.

Olhando um pouco adiante, direi que não encontrei um "critério de otimização personalizado" universal adequado para qualquer Expert Advisor. Para diferentes Expert Advisors, diversos critérios fornecem os melhores resultados.

LinearFactor em alguns Expert Advisors deu resultados maravilhosos.

   double MaxDeviaton = 0;
   double K = (Balance[total - 1] - Balance[0]) / total;
   for(int i = 0; i < total; i++)
     {
      if(i == 0)
         MaxDeviaton = MathAbs(Balance[i] - (Balance[0] + K * i));
      else
         if(MathAbs(Balance[i] - (Balance[0] + K * i) > MaxDeviaton))
            MaxDeviaton = MathAbs(Balance[i] - (Balance[0] + K * i));
     }
   if(MaxDeviaton ==0 || Balance[0] == 0)
      coeff[LinearFactor] = 0;
   else
      coeff[LinearFactor] = 1 / (MaxDeviaton / Balance[0]);

Em correspondência pessoal, o autor disse que esse critério pode ser reforçado e explicou como, mas não consegui transformar essas dicas em código.


Assim, acabei por adicionar quatro critérios de otimização personalizados ao código.

  1. R^2 - coeficiente de determinação.
  2. ProfitStability.
  3. CSSN - coeficiente de segurança do sistema de negociação.
  4. LinearFactor.

Adicionamos todos esses critérios de otimização ao nosso projeto.

É uma pena que não tenha sido possível adicionar o "Máximo do critério complexo", mas não consegui descobrir como é calculado. 


Meu critério de otimização

Agora, com base em todos esses artigos, você pode começar a criar seu próprio critério de otimização.

Qual gráfico de saldo você gostaria de ver? Bem, é claro, na forma de uma linha reta indo para o céu com o máximo ângulo. 😃 Sonhar não custa nada😁.

Vejamos um exemplo de gráfico com lucro.



Considerando que ao otimizar comparamos os resultados de um Expert Advisor, e não de vários Expert Advisors, decidi não levar em consideração o tempo durante o qual o Expert Advisor trabalhou.

Além disso, não levo em consideração os volumes, mas se o lote for calculado dinamicamente, os volumes devem ser incluídos no cálculo do critério personalizado (não feito).

E aí o número de negócios?  Não me importa quantos negócios me tragam mil tugriques da Mongólia, por isso não vou contar seu número. Mas é necessário levar em consideração que, se houver poucos negócios, a regressão linear será calculada incorretamente.

O que é importante para mim neste gráfico?  Antes de mais nada, o lucro. Resolvi levar em conta o lucro relativo - em relação ao saldo inicial.

Relative_Prof = TeSt[STAT_PROFIT] / TeSt[STAT_INITIAL_DEPOSIT];

Igualmente importante, e talvez até o parâmetro mais importante, é o rebaixamento

Como o rebaixamento é calculado no testador? A quantidade máxima de fundos à esquerda é obtida e comparada com a quantidade mínima de fundos à direita.


Eu chamo os fundos acima do saldo de "estou bravo". Mas quando os fundos estão abaixo do saldo, já não estou mais bravo, senão que "dói".

Por isso, para mim é mais importante o rebaixamento máximo, aquele que está abaixo do saldo. Dito de outro modo, ignoramos "estou bravo", para que não exista muito "dói".


double equityDD(const double & Balance[], const double & Equity[], const double & TeSt[], const double & coeff[], const int total)
  {
   if(TeSt[STAT_INITIAL_DEPOSIT] == 0)
      return(0);

   double Balance_max = Balance[0];
   double Equity_min = Equity[0];
   difference_B_E = 0;
   double Max_Balance = 0;

   switch((int)TeSt[41])
     {
      case  0:
         difference_B_E = TeSt[STAT_EQUITY_DD];
         break;
      default:
         for(int i = 0; i < total - 1; i++)
           {
            if(Balance_max < Balance[i])
               Balance_max = Balance[i];
            if(Balance[i] == 10963)
               Sleep(1);
            if(Balance_max - Equity[i + 1] > difference_B_E)
              {
               Equity_min = Equity[i + 1];
               difference_B_E = Balance_max - Equity_min;
               Max_Balance = Balance_max;
              }
           }
         break;
     }

   return(1 - difference_B_E / TeSt[STAT_INITIAL_DEPOSIT]);
  }

Como os valores do critério personalizado devem ser levados em consideração em ordem crescente, subtraí o rebaixamento da unidade. Resultou que quanto maior o valor, menor o rebaixamento.

Chamei o valor resultante de equity_rel, isto é, rebaixamento dos fundos em relação ao saldo inicial

Como se viu, para calcular o equity_rel corretamente, é impossível coletar valores de capital líquido, como fiz no início. Como alguns valores do capital líquido mínimo são perdidos, tivemos que fazer duas variantes para economizá-lo. A primeira variante preserva o capital líquido dos valores máximos ao fechar uma posição com prejuízo e os valores mínimos ao fechar uma posição no positivo, a segunda variante preserva apenas os valores mínimos do capital líquido.

Para que o script saiba como coletamos o capital líquido, tivemos que gravar essas variantes em uma matriz com estatísticas do testador TeSt[41] e calcular equity_rel e Difference_B_E na função EquityDD() com base na variante de coleta de capital líquido.

//---

  Decidi apenas misturar tudo isso de diferentes formas e ver o resultado.

//---

Com equity_rel, podemos calcular o fator de recuperação alternativo. 

difference_B_E é o rebaixamento máximo de fundos em dinheiro.

coeff[c_recovery_factor] = coeff[Profit_Bak] / difference_B_E;

Mas gostaria que o gráfico ficasse mais próximo de uma linha reta, por isso, adicionei R^2 ao segundo fator de recuperação alternativo

coeff[c_recovery_factor_r2] = coeff[Profit_Bak] / difference_B_E * coeff[r2];

Como nas configurações você pode optar por calcular a correlação do saldo ou do capital líquido, se este último foi registrado apenas pelos valores mínimos, R^2 se correlacionará com o rebaixamento.

A fórmula lucro relativo * R^2 pode dar resultados interessantes a nível de critério personalizado.

coeff[profit_r2] = relative_prof * coeff[r2];

A correlação é boa, mas se considerarmos o tamanho, não é inútil, portanto o próximo critério personalizado é este.

Lucro relativo *R^2 / Standard Error

   if(stand_err == 0)
      coeff[profit_r2_Err] = 0;
   else
      coeff[profit_r2_Err] = relative_prof * coeff[r2] / stand_err;

Há um lucro relativo, há um rebaixamento de fundos em relação ao saldo inicial, há R^2, sugere-se uma fórmula que leva em consideração o lucro e o rebaixamento e a aproximação máxima do gráfico a uma linha reta

relative_prof + equity_rel + r2;

Mas se de repente você quiser tornar um desses parâmetros mais significativo? Para fazer isso, eu introduzi uma variável de peso, ratio 

Resultaram mais três critérios de otimização personalizados.

coeff[profit_R_equity_r2] = relative_prof * ratio + coeff[equity_rel] + coeff[r2];

coeff[profit_equity_R_r2] = relative_prof + coeff[equity_rel] * ratio + coeff[r2];

coeff[profit_equity_r2_R] = relative_prof + coeff[equity_rel] + coeff[r2] * ratio;


No total, foram obtidos doze critérios de otimização personalizados.

1. R^2 - coeficiente de determinação.

2.  ProfitStability.

3.  CSSN - coeficiente de segurança do sistema de negociação.

4.  LinearFactor

5.  equity_rel  

6. c_recovery_factor

7. c_recovery_factor_r2

8. profit_r2

9. profit_r2_Err

10. profit_R_equity_r2

11. profit_equity_R_r2

12. profit_equity_r2_R


Verificando o resultado

Para verificar, vamos escrever um Expert Advisor simples...

De acordo com o plano preliminar do artigo, deveria haver um código simples de Expert Advisor aqui, mas, infelizmente, dois Expert Advisors simples não mostraram os resultados que eu gostaria de mostrar.

Por isso, eu tive que pegar um dos EAs que encomendei, e já mostrar os resultados de tudo descrito aqui com ele (desfoquei o nome do EA).

Vamos imaginar que temos o final de abril e planejamos rodar o Expert Advisor em uma conta real, mas como podemos descobrir por qual critério de otimização o Expert Advisor precisa ser otimizado para que negocie com lucro?

Desse modo, começamos a otimização: três meses de otimização, um mês forward.  



Após a otimização, reinicie o terminal.

Eu inicio o script, seleciono apenas os melhores resultados nas configurações. Eu recebo essas miniaturas na pasta.


Eu escolho visualmente qual dessas execuções eu gosto mais. Houve vários resultados idênticos, escolhi profit_equity_R_r2, pois ao otimizar de acordo com esse critério, é priorizado um rebaixamento menor.


No testador, a mesma execução fica assim:


Para comparação, o saldo máximo:


O máximo de testes complexos fica assim:


Como se pode ver, com as configurações da melhor passagem profit_equity_R_r2, há muito menos negócios no gráfico do que com o saldo máximo e o complexo máximo, o lucro é aproximadamente o mesmo, mas o gráfico em si é muito mais suave.


Sendo assim, decidimos por o critério personalizado profit_equity_R_r2, agora vamos ver o que aconteceria se tivéssemos realizado a otimização para os últimos três meses e com as melhores configurações de resultados dessa otimização, decidimos negociar em maio.

Começamos a otimização com uma passagem forward.

Configurações de otimização.  


Nas configurações do EA, devemos definir o critério personalizado pelo qual otimizaremos.

Como resultado, vemos que, se otimizarmos o EA nos últimos três meses com um critério personalizado profit_equity_R_r2,

e, em seguida, negociarmos de 1 de abril a 1 de maio com essas configurações, o EA ganharia 750 MNT, com um rebaixamento de capital de 300 tugriques da Mongólia.



Bem, um tiro de controle na cabeça do EA —  EA Validate feito por fxsaber!!!

Vamos verificar como o EA negociaria por quatro meses. Configurações Validate: três meses de otimização, mês de negociação.


Como se pode ver, o EA sobreviveu mesmo após o controle!!!

Para comparação, temos um gráfico que ficou com as mesmas configurações, mas otimizado de acordo com o "máximo do critério complexo".



Vemos que o EA também sobreviveu, mas...


Conclusão

Vantagens:

  1. Pode-se visualizar todos os gráficos de resultados de otimização de uma só vez.
  2. Dá para escolher o critério de otimização personalizado ideal para seu Expert Advisor.

Desvantagens:

Devido à quantidade limitada de dados registrados, os gráficos são menos informativos do que no testador de estratégia.

Com um grande número de negócios, o arquivo com quadros aumenta para um tamanho ilegível.

//---

Como os experimentos mostraram, não existe um critério de otimização super legal, para diferentes EAs, os melhores resultados são dados por diferentes critérios e precisam ser selecionados individualmente.

Mas já temos um conjunto completo de critérios e, se os membros do fórum apoiarem e compartilharem seus critérios de otimização, a escolha será ainda maior.

//---

Um dos testadores sugeriu descrever as configurações do script no artigo, para que as pessoas com preguiça de ler e entender pudessem simplesmente descobrir as configurações e usar esse código sem se aprofundar na essência.


Como usar isso

Para usar este código, é necessário baixar o arquivo anexado na parte inferior do artigo, descompactá-lo, copiá-lo para a pasta MQL5,

em seguida, no terminal, pressionamos -> arquivo -> abrir diretório de dados -> clique com o botão direito do mouse em um local vazio da pasta aberta e clique em "colar", uma janela pode aparecer perguntando "Substituir arquivos na pasta de destino" -> clique em substituir .

A seguir, execute o MetaEditor, abra seu Expert Advisor e adicione as seguintes alterações:

1. Na função OnTick() cole --> IsOnTick();

2. Cole este código na parte inferior do EA:

  #include <SkrShotOpt.mqh>     

  double OnTester() {return(IsOnTester());}

  void OnTradeTransaction(const MqlTradeTransaction & trans, const MqlTradeRequest & request,const MqlTradeResult & result) 
    {
      IsOnTradeTransaction(trans, request, result);
     }
 Se o EA já tiver a função OnTradeTransaction(), basta inserir nela: IsOnTradeTransaction(trans, request, result);

3. Pressione o botão "Compilar". 

Se de repente houver erros que correspondam aos nomes das variáveis, você terá que alterar os nomes; se não tiver certeza de suas ações, é aconselhável recorrer a alguém que saiba como fazer isso.


Opções

Depois de inserir o código nas configurações do EA na parte inferior, serão adicionadas mais algumas linhas com configurações.

Não marque as caixas para otimizar essas configurações!!! Essas configurações não afetam os resultados da otimização, por isso, não há necessidade de otimizá-las.


  • Se houver mais negócios, registramos a passagem - se o seu Expert Advisor fizer muitas negociações, esse parâmetro pode ser aumentado para reduzir a quantidade de dados gravados no arquivo com quadros.
  • Registramos a passagem se o lucro for maior que %, assim, as passagens não lucrativas serão filtradas por padrão. É possível alterar isso se não nos importarmos com o lucro inferior a uma certa porcentagem em relação ao saldo inicial.
  • Seleção de valores de capita líquido - selecionamos "armazenar só min equity", se os seguintes critérios de usuário devem ser calculados corretamente: quity_rel, c_recovery_factor, c_recovery_factor_r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R  
    se você precisar de gráficos mais semelhantes aos gráficos do testador, precisará selecionar "salvar min e max equity"

  • Critério personalizado - se definido como "none", os quadros são gravados no arquivo, o critério personalizado não é calculado (para não aumentar o tempo de otimização),

Assim, é impossível definir a otimização de acordo com um critério personalizado. Também os parâmetros abaixo não importam.

Você precisa selecionar algum critério personalizado para este parâmetro se quiser executar a otimização por um critério personalizado. 

Vale a pena lembrar que o cálculo de critério personalizado depende da escolha do parâmetro "seleção de valores de capital líquido" e do parâmetro "cálculo de critério por"

  • cálculo de critério por - seleção do cálculo de R^2 com base no saldo ou capital líquido, respectivamente todos os valores dos critérios personalizados cujo cálculo envolve R^2 serão alterados
r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R

    //----

    Configurações do script.


    • Desenho da linha de regressão - seleção da linha de regressão a ser desenhada, saldo, capital líquido, saldo e capital líquido, não desenhar linha de regressão.
    • Porcentagem de lucro maior - como imprimir milhares de capturas de tela demora muito, só podemos imprimir capturas de tela com lucro maior do que o especificado nesta configuração.
    • Apenas melhores resultados - se true, salva as telas apenas com o melhor resultado de cada critério personalizado, caso contrário, salva todas as telas.
    • Critério personalizado - se estiver selecionado imprimir todas as capturas de tela, com este parâmetro será possível definir um critério personalizado para classificar as capturas de tela na pasta.
    • ratio - coeficiente de peso para calcular os critérios personalizados profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R.
    • cálculo de critério por - seleção do cálculo de R^2 com base no saldo ou capital líquido, respectivamente todos os valores dos critérios personalizados cujo cálculo envolve R^2 serão alterados

             r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R.

    • Gráfico - seleção entre, imprimir o gráfico como no testador "Back separadamente Forward", ou seja, forward começa a partir do saldo inicial,  

    ou "Back continuação Forward" neste caso, forward continuará a partir do último valor de saldo da passagem backward.

    //---

    Os artigos deste site me ajudaram muito a escrever o programa.

    Expresso minha profunda gratidão a todos os autores dos artigos aqui listados!


    Traduzido do russo pela MetaQuotes Ltd.
    Artigo original: https://www.mql5.com/ru/articles/9922

    Arquivos anexados |
    SkrShotOpt.mqh (17.56 KB)
    MQL5.zip (11.59 KB)
    Últimos Comentários | Ir para discussão (1)
    601a884a
    601a884a | 16 mar 2022 em 14:01
    Eu não sou uma trader só quero resgatar o dinheiro que investi eu tenho 800 reais investido desde 28/01/2022
    Gráficos na biblioteca do DoEasy (Parte 92): classe de memória de objetos gráficos padrão. Histórico de mudanças de propriedades do objeto Gráficos na biblioteca do DoEasy (Parte 92): classe de memória de objetos gráficos padrão. Histórico de mudanças de propriedades do objeto
    Neste artigo, criaremos uma classe de memória de objeto gráfico padrão. Com ela conseguiremos salvar os estados do objeto quando modificado, o que, por sua vez, nós permite retornar a estados anteriores do objeto gráfico a qualquer momento.
    Indicadores múltiplos em um gráfico (Parte 06): Transformando o MetaTrader 5 em um sistema RAD (II) Indicadores múltiplos em um gráfico (Parte 06): Transformando o MetaTrader 5 em um sistema RAD (II)
    No artigo anterior mostrei como criar um Chart Trade usando os objetos do MetaTrader 5, transformando a plataforma em um sistema RAD, o sistema funciona muito bem, e acredito que muitos tenham pensado em criar uma biblioteca para ter cada vez mais funcionalidade no sistema proposto, e assim conseguir desenvolver um EA que seja mais intuitivo ao mesmo tempo que tenha uma interface mais agradável e simples de usar.
    Desenvolvendo um EA de negociação do zero (Parte 07): Adicionando o Volume At Price (I) Desenvolvendo um EA de negociação do zero (Parte 07): Adicionando o Volume At Price (I)
    Este é um dos indicadores mais poderosos que existe. Para quem opera e tenta ter um certo grau de assertividade, não pode deixar de ter este indicador em seu gráfico, apesar de ele ser mais utilizado por quem opera observando o Fluxo (Tape Reading ) ele também pode ser usado por aqueles que fazem uso apenas do Price Action.
    Indicadores múltiplos em um gráfico (Parte 05): Transformando o MetaTrader 5 em um sistema RAD (I) Indicadores múltiplos em um gráfico (Parte 05): Transformando o MetaTrader 5 em um sistema RAD (I)
    Muita gente não sabe de fato como programar, mas são bem criativas, tendo excelentes ideias, mas a falta de conhecimento ou entendimento sobre programação as proíbe de fazer algumas coisas. Aprenda com criar um Chart Trade, mas usando a própria plataforma MT5, como se fosse uma IDE.