English Русский 中文 Deutsch 日本語
preview
Avaliando o desempenho futuro com intervalos de confiança

Avaliando o desempenho futuro com intervalos de confiança

MetaTrader 5Testador | 7 março 2024, 09:38
168 0
Francis Dube
Francis Dube

Introdução

Desenvolver sistemas de negociação automatizados lucrativos é uma tarefa desafiadora. Mesmo se alguém conseguir desenvolver um EA lucrativo, ainda resta a questão da justificativa do risco. Podemos estar satisfeitos pelo fato de nossa estratégia não destruir todo o capital a ela destinado, mas isso não é razão para entrar imediatamente no mercado real. Ultimamente, o principal indicador é o lucro, e se, depois de algum tempo, descobrirmos que nossa estratégia não é suficientemente lucrativa para justificar o risco, ou traz baixo retorno em comparação com outras oportunidades de investimento, sem dúvida, lamentaremos o tempo gasto.

Por isso, neste artigo, vamos considerar métodos emprestados da estatística que podem nos ajudar a avaliar o desempenho futuro de um sistema de negociação automática, usando dados coletados durante testes fora da amostra.


O EA é suficientemente bom?

Quando testamos um sistema de negociação, obtemos um conjunto de diferentes indicadores de desempenho. Esses dados intuitivamente nos dão uma ideia do potencial de lucro do sistema, mas essa intuição pode não ser suficiente. Uma estratégia que gerou grande lucro nos testes pode não se sair tão bem no mercado real. Existe alguma forma de saber se o desempenho observado durante os testes permanecerá no mesmo nível? E se não, quão pior ele pode ficar?

Aqui, métodos estatísticos padrão podem ajudar. Os métodos que discutiremos não são destinados para avaliações precisas. Em vez disso, eles permitem identificar estratégias com alta probabilidade de gerar um lucro significativo ou aceitável.

Conheço traders que usam valores brutos do índice de Sharpe para suposições probabilísticas sobre resultados futuros. Isso é arriscado. Lembre-se de que resultados passados não são indicativos de lucros futuros. Com os mercados financeiros, não se pode brincar. Os gráficos de preços frequentemente se movem para cima ou para baixo por razões desconhecidas. Queremos fazer previsões corretas de desempenho, baseadas em probabilidades que podemos aplicar em nossos processos de tomada de decisão.


Intervalos de confiança

Intervalo de Confiança


O intervalo de confiança é a probabilidade de que uma certa estatística de um conjunto de dados ou população estará dentro de um determinado intervalo durante um período específico de tempo. Ele mede o grau de confiança, calculando a probabilidade de que os níveis calculados conterão a verdadeira estatística estimada. As estatísticos normalmente usam níveis de confiança de 90% a 99%. Esses intervalos podem ser calculados por vários métodos. Neste artigo, vamos focar em alguns métodos comuns de bootstrapping.


Bootstrapping

O bootstrapping em estatística é um procedimento no qual um conjunto de dados é usado para criar múltiplos novos conjuntos de dados por meio de seleção aleatória ou amostragem do original. Os novos conjuntos de dados terão os mesmos elementos que o original, mas alguns elementos nos novos conjuntos de dados serão duplicados.

Original
Bootstrap1
Bootstrap2
Bootstrap3
 Bootstrap4
A
A
A
A
B
B
A
B
B
B
C
B
B
B
C
D
C
D
C
D
E
D
E
C
E


A coluna Original contém o conjunto de dados original, enquanto as outras colunas representam conjuntos de dados criados com base no Original. Como se pode ver, as colunas de bootstrap têm uma ou mais duplicatas. Fazendo isso muitas vezes, podemos gerar um grande número de dados que podem representar amostras que atualmente não podemos observar ou que seriam desconhecidas. Já vimos exemplos de aplicação do bootstrapping no trading no artigo "Aplicação do método de Monte Carlo para otimizar estratégias de negociação".

O ponto central da teoria do bootstrapping é que o conjunto de dados original deve ser representativo de um conjunto de dados maior, a população geral (população), que não pode ser observada e está tentando ser modelada. Por isso, quando criamos esses bootstraps, eles se tornam substitutos da coleção não observável. As propriedades estatísticas desses bootstraps, junto com a amostra original, podem ser usadas para fazer inferências sobre a população desconhecida e/ou não observável.


Bootstrapping de intervalos de confiança

Veremos três métodos de bootstrapping para intervalos de confiança: o método de pivotamento (pivot method), o método de percentis (percentile method) e, finalmente, o método corrigido de viés e aceleração (bias corrected and accelerated method, BCA).

O método de pivotamento envolve a criação de múltiplos bootstraps que serão usados para calcular a estatística de teste. A estatística de teste é a qualquer característica da população que estamos tentando estimar, isso pode ser seu valor médio ou mediana. Então, as fronteiras calculadas são encontradas ajustando o valor da estatística de teste do conjunto de dados original em relação ao necessário para aumentar o valor esperado das amostras bootstrap para o original.

O método dos percentis leva em consideração a distribuição da estatística de teste calculada a partir das amostras bootstrap. Supõe-se que esta distribuição seja semelhante à distribuição da população desconhecida. As fronteiras es tornam os intervalos entre os percentis da distribuição da estatística de teste calculada, obtida a partir das amostras bootstrap.  

O método com correção de viés e aceleração é um pouco mais complexo. Após a criação de nossos bootstraps e o cálculo da estatística de teste para cada um deles, calculamos o coeficiente de correção de viés, que representa a proporção de estimativas de bootstrap menores do que o conjunto de dados original. Em seguida, o coeficiente de aceleração é calculado usando o método do canivete. Este é outro método de reamostragem usado para estimar o grau de dependência da variância da estatística de teste transformada em relação ao seu valor.

Então, o método dos percentis é usado para calcular os limites inferiores e superiores, que são ajustados de acordo com os coeficientes de correção de viés e aceleração. Os intervalos de confiança finais são obtidos dos valores ajustados após a ordenação.

Vamos ver como esses métodos podem ser implementados em código.


Classe CBootstrap

CBootstrap é uma classe que encapsula o cálculo de intervalos de confiança usando os três métodos de bootstrapping recém-descritos. Com sua ajuda, os usuários poderão calcular intervalos de confiança para várias probabilidades personalizáveis, além de especificar o número de bootstraps gerados.

#include<Math\Alglib\specialfunctions.mqh>
#include<Math\Stat\Math.mqh>
#include<UniformRandom.mqh>


A definição da classe começa com a inclusão de algumas ferramentas matemáticas importantes da biblioteca padrão.

//+------------------------------------------------------------------+
//|Function pointer                                                  |
//+------------------------------------------------------------------+
typedef double(*BootStrapFunction)(double &in[],int stop=-1);


O ponteiro de função BootStrapFunction define a assinatura da função para calcular a estatística do teste ou o parâmetro da população.

//+------------------------------------------------------------------+
//|Boot strap types                                                  |
//+------------------------------------------------------------------+
enum ENUM_BOOSTRAP_TYPE
  {
   ENUM_BOOTSTRAP_PIVOT=0,
   ENUM_BOOTSTRAP_PERCENTILE,
   ENUM_BOOTSTRAP_BCA
  };


O enum ENUM_BOOSTRAP_TYPE facilita a escolha de um método específico de cálculo do bootstrap - resampling, percentis ou BCA.

//+------------------------------------------------------------------+
//|Constructor                                                       |
//+------------------------------------------------------------------+
CBootstrap::CBootstrap(const ENUM_BOOSTRAP_TYPE boot_type,const uint nboot,const BootStrapFunction function,double &in_samples[])
  {
//--- set the function pointer
   m_function=function;
//--- optimistic initilization of flag
   m_initialized=true;
//--- set method of boostrap to be applied
   m_boot_type=boot_type;
//--- set number of boostrap iterations
   m_replications=nboot;
//---make sure there are at least 5 boostraps
   if(m_replications<5)
      m_initialized=false;
//--- initilize random number generator
   m_unifrand=new CUniFrand();
   if(m_unifrand!=NULL)
      m_unifrand.SetSeed(MathRand());
   else
      m_initialized=false;
//--- copy samples to internal buffer
   if(ArrayCopy(m_data,in_samples)!=ArraySize(in_samples))
     {
      Print("Data Copy error ", GetLastError());
      m_initialized=false;
     }
//--- initialize shuffled buffer
   if(ArrayCopy(m_shuffled,in_samples)!=ArraySize(in_samples))
     {
      Print("Data Copy error ", GetLastError());
      m_initialized=false;
     }
//--- set memory for bootstrap calculations container
   if(ArrayResize(m_rep_cal,(int)m_replications)!=(int)m_replications)
     {
      Print("Memory allocation error ", GetLastError());
      m_initialized=false;
     }
//--- check function pointer
   if(m_function==NULL)
     {
      Print("Invalid function pointer");
      m_initialized=false;
     }
  }

CBootstrap é definido por um construtor paramétrico, cujos parâmetros de entrada determinam a natureza da operação de bootstrap:

  • boot_type - método de cálculo do bootstrap
  • nboot - número desejado de amostras bootstrap. É recomendado ter pelo menos 100, embora idealmente se gere milhares para obter resultados confiáveis.
  • function - indica uma definição de função fornecida pelo usuário para calcular o parâmetro estimado da população. Os parâmetros dessa função são um array de amostras de dados usadas para calcular a estatística do teste. O parâmetro inteiro do ponteiro de função por padrão determina o número de membros do array que serão utilizados no cálculo.
  • O array in_samples é um contêiner de dados do qual os bootstraps serão gerados. O mesmo conjunto de dados e suas variantes bootstrap serão passados ao ponteiro de função para o cálculo da estatística do teste.
//+------------------------------------------------------------------+
//| public method for calculating confidence intervals               |
//+------------------------------------------------------------------+
bool CBootstrap::CalculateConfidenceIntervals(double &in_out_conf[])
  {
//--- safety check
   if(!m_initialized)
     {
      ZeroMemory(in_out_conf);
      return m_initialized;
     }
//--- check input parameter values
   if(ArraySize(in_out_conf)<=0 ||
      in_out_conf[ArrayMaximum(in_out_conf)]>=1 ||
      in_out_conf[ArrayMinimum(in_out_conf)]<=0)
     {
      Print("Invalid input values for function ",__FUNCTION__,"\n All values should be probabilities between 0 and 1");
      return false;
     }
//--- do bootstrap based on chosen method
   switch(m_boot_type)
     {
      case ENUM_BOOTSTRAP_PIVOT:
         return pivot_boot(in_out_conf);
      case ENUM_BOOTSTRAP_PERCENTILE:
         return percentile_boot(in_out_conf);
      case ENUM_BOOTSTRAP_BCA:
         return bca_boot(in_out_conf);
      default:
         return false;
     }
//---
  }


Um dos dois métodos públicos da classe, CalculateConfidenceIntervals(), aceita um array de valores de probabilidade na quantidade necessária para o usuário. Esses valores determinam a probabilidade de que o valor verdadeiro do parâmetro esteja dentro do intervalo calculado.

Por exemplo, para calcular os intervalos de confiança com uma probabilidade de 90%, o usuário deve fornecer um array com o valor 0,9, após o qual o método retornará um par de valores. Esses valores retornados serão gravados no mesmo array fornecido como entrada. Para cada membro individual no array de entrada, o método substituirá por um par de valores, sendo o primeiro de cada par o limite inferior do intervalo e o segundo, o superior.

Como mencionado, é possível solicitar mais de um intervalo de confiança com diferentes probabilidades. Na saída, as fronteiras serão dispostas em ordem da menor para a maior probabilidade, conforme indicado nos dados de entrada.

Antes de demonstrar o uso da classe, precisamos determinar quais dados usaremos para medir a eficácia da estratégia de negociação. A prática padrão é classificar a eficácia da estratégia por rentabilidade. Para calcular esse valor, é necessário examinar a curva de capital e a série de rentabilidade.

Usando as séries de rentabilidade da estratégia, podemos calcular diversos indicadores de eficácia. Para simplicidade, usaremos a média anual de rentabilidade como a estatística de teste, cujo valor futuro queremos estimar com uma certa confiabilidade.

Usando essa estatística de teste, podemos estimar a menor média de rentabilidade que podemos esperar da estratégia. Além disso, o nível superior de confiabilidade oferece uma ideia aproximada de quão boa será a performance, se tudo correr bem.


Classe CReturns

Usaremos a classe CReturns para coletar a série de rentabilidades necessárias para aproximar a futura média de rentabilidade. A classe é adaptada do código apresentado no artigo "Matemática no trading: Índice de Sharpe e Sortino". Uma característica dessa versão é a possibilidade de escolher o tipo de série de rentabilidade que será usada nos cálculos de desempenho.

//+------------------------------------------------------------------+
//| Class for calculating Sharpe Ratio in the tester                 |
//+------------------------------------------------------------------+
class CReturns
  {
private:
   CArrayDouble*     m_all_bars_equity;
   CArrayDouble*     m_open_position_bars_equity;
   CArrayDouble*     m_trade_equity;

   CArrayDouble*     m_all_bars_returns;
   CArrayDouble*     m_open_position_bars_returns;
   CArrayDouble*     m_trade_returns;

   int               ProcessHistory(void);
   void              CalculateReturns(CArrayDouble &r,CArrayDouble &e);

public:
                     CReturns(void);
                    ~CReturns(void);

   void              OnNewTick(void);

   bool              GetEquityCurve(const ENUM_RETURNS_TYPE return_type,double &out_equity[]);
   bool              GetReturns(const ENUM_RETURNS_TYPE return_type,double &out_returns[]);
  };



return.mqh estabelece uma enumeração que define o tipo de série de rentabilidade. ENUM_RETURNS_ALL_BARS define a série de rentabilidades barra a barra para todas as barras do período de teste. ENUM_RETURNS_POSITION_OPEN_BARS representa a série de rentabilidades que compõem a rentabilidade barra a barra para aquelas barras em que uma posição foi aberta. ENUM_RETURNS_TRADES define apenas a série de retornos de transações concluídas. Com esta opção, não se coleta informação sobre as barras.

//+------------------------------------------------------------------+
//| Enumeration specifying granularity of return                     |
//+------------------------------------------------------------------+
enum ENUM_RETURNS_TYPE
  {
   ENUM_RETURNS_ALL_BARS=0,//bar-by-bar returns for all bars
   ENUM_RETURNS_POSITION_OPEN_BARS,//bar-by-bar returns for bars with open trades
   ENUM_RETURNS_TRADES//trade returns
  };

Usando a classe CReturns, pode-se obter uma série de valores de equity, definindo a curva de equity através do método GetEquityCurve().

//+------------------------------------------------------------------+
//| get equity curve                                                 |
//+------------------------------------------------------------------+
bool CReturns::GetEquityCurve(const ENUM_RETURNS_TYPE return_type,double &out_equity[])
  {
   int m_counter=0;
   CArrayDouble *equity;
   ZeroMemory(out_equity);
//---
   switch(return_type)
     {
      case ENUM_RETURNS_ALL_BARS:
         m_counter=m_all_bars_equity.Total();
         equity=m_all_bars_equity;
         break;
      case ENUM_RETURNS_POSITION_OPEN_BARS:
         m_counter=m_open_position_bars_equity.Total();
         equity=m_open_position_bars_equity;
         break;
      case ENUM_RETURNS_TRADES:
         m_counter=(m_trade_equity.Total()>1)?m_trade_equity.Total():ProcessHistory();
         equity=m_trade_equity;
         break;
      default:
         return false;
     }
//--- if there are no bars, return 0
   if(m_counter < 2)
      return false;
//---
   if(ArraySize(out_equity)!=m_counter)
      if(ArrayResize(out_equity,equity.Total()) < m_counter)
         return false;
//---
   for(int i=0; i<equity.Total(); i++)
      out_equity[i]=equity[i];
//---
   return(true);
//---
  }

De maneira similar, GetReturns() pode produzir uma série de rentabilidade. Ambos os métodos aceitam como dados de entrada a série de resultados desejada, bem como um array onde os valores serão obtidos.

//+------------------------------------------------------------------+
//|Gets the returns into array                                       |
//+------------------------------------------------------------------+
bool CReturns::GetReturns(const ENUM_RETURNS_TYPE return_type,double &out_returns[])
  {
//---
   CArrayDouble *returns,*equity;
   ZeroMemory(out_returns);
//---
   switch(return_type)
     {
      case ENUM_RETURNS_ALL_BARS:
         returns=m_all_bars_returns;
         equity=m_all_bars_equity;
         break;
      case ENUM_RETURNS_POSITION_OPEN_BARS:
         returns=m_open_position_bars_returns;
         equity=m_open_position_bars_equity;
         break;
      case ENUM_RETURNS_TRADES:
         if(m_trade_equity.Total()<2)
            ProcessHistory();
         returns=m_trade_returns;
         equity=m_trade_equity;
         break;
      default:
         return false;
     }
//--- if there are no bars, return 0
   if(equity.Total() < 2)
      return false;
//--- calculate average returns
   CalculateReturns(returns,equity);
//--- return the mean return
   if(returns.Total()<=0)
      return false;
//---
   if(ArraySize(out_returns)!=returns.Total())
      if(ArrayResize(out_returns,returns.Total()) < returns.Total())
         return false;
//---
   for(int i=0; i<returns.Total(); i++)
      out_returns[i]=returns[i];
//---
   return(true);
//---
  }



Exemplo

O código do Expert Advisor abaixo demonstra como usar o CReturns para coletar uma série de rentabilidades. Em nosso exemplo, a série de rentabilidades é salva em um arquivo binário. Embora o cálculo do intervalo de confiança possa ser realizado com CBootstrap em OnTester, em nosso exemplo, analisaremos essa série por meio de um programa separado.

//+------------------------------------------------------------------+
//|                                           MovingAverage_Demo.mq5 |
//|                        Copyright 2023, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Returns.mqh>
#include <Bootstrap.mqh>
#include <Files\FileBin.mqh>
#include <Trade\Trade.mqh>

input double MaximumRisk        = 0.02;    // Maximum Risk in percentage
input double DecreaseFactor     = 3;       // Descrease factor
input int    MovingPeriod       = 12;      // Moving Average period
input int    MovingShift        = 6;       // Moving Average shift
input ENUM_RETURNS_TYPE rtypes  = ENUM_RETURNS_ALL_BARS; // return types to record
input uint BootStrapIterations  = 10000;
input double BootStrapConfidenceLevel = 0.975;
input ENUM_BOOSTRAP_TYPE AppliedBoostrapMethod=ENUM_BOOTSTRAP_BCA;
input bool   SaveReturnsToFile = true;
input string ReturnsFileName = "MovingAverage_Demo";

//---
int    ExtHandle=0;
bool   ExtHedging=false;
CTrade ExtTrade;
CReturns ma_returns;
#define MA_MAGIC 1234501
//+------------------------------------------------------------------+
//| Calculate optimal lot size                                       |
//+------------------------------------------------------------------+
double TradeSizeOptimized(void)
  {
   double price=0.0;
   double margin=0.0;
//--- select lot size
   if(!SymbolInfoDouble(_Symbol,SYMBOL_ASK,price))
      return(0.0);
   if(!OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,1.0,price,margin))
      return(0.0);
   if(margin<=0.0)
      return(0.0);

   double lot=NormalizeDouble(AccountInfoDouble(ACCOUNT_MARGIN_FREE)*MaximumRisk/margin,2);
//--- calculate number of losses orders without a break
   if(DecreaseFactor>0)
     {
      //--- select history for access
      HistorySelect(0,TimeCurrent());
      //---
      int    orders=HistoryDealsTotal();  // total history deals
      int    losses=0;                    // number of losses orders without a break

      for(int i=orders-1; i>=0; i--)
        {
         ulong ticket=HistoryDealGetTicket(i);
         if(ticket==0)
           {
            Print("HistoryDealGetTicket failed, no trade history");
            break;
           }
         //--- check symbol
         if(HistoryDealGetString(ticket,DEAL_SYMBOL)!=_Symbol)
            continue;
         //--- check Expert Magic number
         if(HistoryDealGetInteger(ticket,DEAL_MAGIC)!=MA_MAGIC)
            continue;
         //--- check profit
         double profit=HistoryDealGetDouble(ticket,DEAL_PROFIT);
         if(profit>0.0)
            break;
         if(profit<0.0)
            losses++;
        }
      //---
      if(losses>1)
         lot=NormalizeDouble(lot-lot*losses/DecreaseFactor,1);
     }
//--- normalize and check limits
   double stepvol=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
   lot=stepvol*NormalizeDouble(lot/stepvol,0);

   double minvol=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   if(lot<minvol)
      lot=minvol;

   double maxvol=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);
   if(lot>maxvol)
      lot=maxvol;
//--- return trading volume
   return(lot);
  }
//+------------------------------------------------------------------+
//| Check for open position conditions                               |
//+------------------------------------------------------------------+
void CheckForOpen(void)
  {
   MqlRates rt[2];
//--- go trading only for first ticks of new bar
   if(CopyRates(_Symbol,_Period,0,2,rt)!=2)
     {
      Print("CopyRates of ",_Symbol," failed, no history");
      return;
     }
   if(rt[1].tick_volume>1)
      return;
//--- get current Moving Average
   double   ma[1];
   if(CopyBuffer(ExtHandle,0,0,1,ma)!=1)
     {
      Print("CopyBuffer from iMA failed, no data");
      return;
     }
//--- check signals
   ENUM_ORDER_TYPE signal=WRONG_VALUE;

   if(rt[0].open>ma[0] && rt[0].close<ma[0])
      signal=ORDER_TYPE_SELL;    // sell conditions
   else
     {
      if(rt[0].open<ma[0] && rt[0].close>ma[0])
         signal=ORDER_TYPE_BUY;  // buy conditions
     }
//--- additional checking
   if(signal!=WRONG_VALUE)
     {
      if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>100)
         ExtTrade.PositionOpen(_Symbol,signal,TradeSizeOptimized(),
                               SymbolInfoDouble(_Symbol,signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK),
                               0,0);
     }
//---
  }
//+------------------------------------------------------------------+
//| Check for close position conditions                              |
//+------------------------------------------------------------------+
void CheckForClose(void)
  {
   MqlRates rt[2];
//--- go trading only for first ticks of new bar
   if(CopyRates(_Symbol,_Period,0,2,rt)!=2)
     {
      Print("CopyRates of ",_Symbol," failed, no history");
      return;
     }
   if(rt[1].tick_volume>1)
      return;
//--- get current Moving Average
   double   ma[1];
   if(CopyBuffer(ExtHandle,0,0,1,ma)!=1)
     {
      Print("CopyBuffer from iMA failed, no data");
      return;
     }
//--- positions already selected before
   bool signal=false;
   long type=PositionGetInteger(POSITION_TYPE);

   if(type==(long)POSITION_TYPE_BUY && rt[0].open>ma[0] && rt[0].close<ma[0])
      signal=true;
   if(type==(long)POSITION_TYPE_SELL && rt[0].open<ma[0] && rt[0].close>ma[0])
      signal=true;
//--- additional checking
   if(signal)
     {
      if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>100)
         ExtTrade.PositionClose(_Symbol,3);
     }
//---
  }
//+------------------------------------------------------------------+
//| Position select depending on netting or hedging                  |
//+------------------------------------------------------------------+
bool SelectPosition()
  {
   bool res=false;
//--- check position in Hedging mode
   if(ExtHedging)
     {
      uint total=PositionsTotal();
      for(uint i=0; i<total; i++)
        {
         string position_symbol=PositionGetSymbol(i);
         if(_Symbol==position_symbol && MA_MAGIC==PositionGetInteger(POSITION_MAGIC))
           {
            res=true;
            break;
           }
        }
     }
//--- check position in Netting mode
   else
     {
      if(!PositionSelect(_Symbol))
         return(false);
      else
         return(PositionGetInteger(POSITION_MAGIC)==MA_MAGIC); //---check Magic number
     }
//--- result for Hedging mode
   return(res);
  }
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- prepare trade class to control positions if hedging mode is active
   ExtHedging=((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
   ExtTrade.SetExpertMagicNumber(MA_MAGIC);
   ExtTrade.SetMarginMode();
   ExtTrade.SetTypeFillingBySymbol(Symbol());
//--- Moving Average indicator
   ExtHandle=iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE);
   if(ExtHandle==INVALID_HANDLE)
     {
      printf("Error creating MA indicator");
      return(INIT_FAILED);
     }
//--- ok
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   ma_returns.OnNewTick();
//---
   if(SelectPosition())
      CheckForClose();
   else
      CheckForOpen();
//---
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
  {
   double returns[],confidence[],params[];
   ArrayResize(confidence,1);
   confidence[0]=BootStrapConfidenceLevel;
//---
   double ret=0.0;
//---
   if(ma_returns.GetReturns(rtypes,returns))
     {
      CBootstrap minreturn(AppliedBoostrapMethod,BootStrapIterations,MeanReturns,returns);

      if(minreturn.CalculateConfidenceIntervals(confidence))
        {
         ret=confidence[0];
         string fname=ReturnsFileName+"_"+_Symbol+".returns";
         CFileBin file;
         if(SaveReturnsToFile && file.Open(fname,FILE_WRITE|FILE_COMMON)!=INVALID_HANDLE)
            file.WriteDoubleArray(returns);

        }

     }
//---
   return(ret);
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//|the bootstrap function                                            |
//+------------------------------------------------------------------+
double MeanReturns(double &rets[], int upto=-1)
  {
   int stop=(upto<=0)?ArraySize(rets):upto;

   if(!stop)
     {
      Print("in danger of zero divide error ",__FUNCTION__);
      return 0;
     }

   double sum=0;
   for(int i=0; i<stop; i++)
      sum+=rets[i];

   sum/=double(stop);

   switch(Period())
     {
      case PERIOD_D1:
         sum*=252;
         return sum;
      case PERIOD_W1:
         sum*=52;
         return sum;
      case PERIOD_MN1:
         sum*=12;
         return sum;
      default:
         sum*=double(PeriodSeconds(PERIOD_D1) / PeriodSeconds());
         return sum*=252;
     }

  }


O script lê os dados salvos e os passa para uma instância de CBootstrap. A estatística de teste é calculada pela função MeanReturns(), cuja assinatura coincide com a assinatura do ponteiro de função BootStrapFunction. Chamamos CalculateConfidenceIntervals() com um array com os valores 0,9, 0,95, 0,975, correspondendo aos intervalos de confiança de 90%, 95% e 97,5%.

//+------------------------------------------------------------------+
//|                                       ApproximateMeanReturns.mq5 |
//|                        Copyright 2023, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs
#include<Math\Stat\Math.mqh>
#include<Files\FileBin.mqh>
#include<Bootstrap.mqh>
//--- input parameters
input string   FileName="MovingAverage_Demo_EURUSD.returns";//returns file name
input ENUM_BOOSTRAP_TYPE AppliedBoostrapMethod=ENUM_BOOTSTRAP_BCA;
input uint BootStrapIterations=10000;
input string BootStrapProbability="0.975,0.95,0.90";
//---
CBootstrap *meanreturns;
double logreturns[],bounds[],bootstraps[];
string sbounds[];
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   int done=StringSplit(BootStrapProbability,StringGetCharacter(",",0),sbounds);
//---
   if(done)
     {
      ArrayResize(bounds,done);
      for(int i=0; i<done; i++)
         bounds[i]=StringToDouble(sbounds[i]);
      if(ArraySort(bounds))
         for(int i=0; i<done; i++)
            sbounds[i]=DoubleToString(bounds[i]);
     }
//---
   if(!done)
     {
      Print("error parsing inputs ", GetLastError());
      return;
     }
//---
   if(!LoadReturns(FileName,logreturns))
      return;
//---
   meanreturns=new CBootstrap(AppliedBoostrapMethod,BootStrapIterations,MeanReturns,logreturns);
//---
   if(meanreturns.CalculateConfidenceIntervals(bounds))
     {
      for(int i=0; i<done; i++)
         Print(EnumToString(AppliedBoostrapMethod)," ",sbounds[i],": ","(",bounds[i*2]," ",bounds[(i*2)+1],")");
     }
//---
   delete meanreturns;
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Load returns from file                                           |
//+------------------------------------------------------------------+
bool LoadReturns(const string fname,double &out_returns[])
  {
   CFileBin file;
//---
   if(file.Open(fname,FILE_READ|FILE_COMMON)==INVALID_HANDLE)
      return false;
//---
   if(!file.ReadDoubleArray(out_returns))
     {
      Print("File read error ",GetLastError());
      return false;
     }
//---
   return true;
  }
//+------------------------------------------------------------------+
//|the bootstrap function                                            |
//+------------------------------------------------------------------+
double MeanReturns(double &rets[], int upto=-1)
  {
   int stop=(upto<=0)?ArraySize(rets):upto;

   if(!stop)
     {
      Print("in danger of zero divide error ",__FUNCTION__);
      return 0;
     }

   double sum=0;
   for(int i=0; i<stop; i++)
      sum+=rets[i];

   sum/=double(stop);

   switch(Period())
     {
      case PERIOD_D1:
         sum*=252;
         return sum;
      case PERIOD_W1:
         sum*=52;
         return sum;
      case PERIOD_MN1:
         sum*=12;
         return sum;
      default:
         sum*=double(PeriodSeconds(PERIOD_D1) / PeriodSeconds());
         return sum*=252;
     }

  }
//+------------------------------------------------------------------+



Antes de olhar para o resultado final dos intervalos calculados, sempre é útil observar o gráfico de distribuição das estatísticas do teste de bootstrap. Isso pode ser feito plotando os dados disponíveis através de GetBootStrapStatistics().

Bootstrap distribution



Analisando os resultados do Moving Average, vemos que OnTester retorna um número negativo, indicando que o desempenho no futuro pode piorar, apesar dos resultados positivos mostrados em um teste. -0,12 é a pior média de rentabilidade que podemos esperar.  

Resultados


A seguir, são apresentados os resultados para diferentes intervalos de confiança.

ApproximateMeanReturns (EURUSD,D1)      ENUM_BOOTSTRAP_BCA 0.90000000: (-0.07040966776550685 0.1134376873958945)
ApproximateMeanReturns (EURUSD,D1)      ENUM_BOOTSTRAP_BCA 0.95000000: (-0.09739322056041048 0.1397669758772337)
ApproximateMeanReturns (EURUSD,D1)      ENUM_BOOTSTRAP_BCA 0.97500000: (-0.12438450770122121 0.1619709975134838)

Este exemplo demonstra o cálculo da rentabilidade média prevista com base na probabilidade para o Moving Average. O mesmo princípio pode ser aplicado a outros indicadores de desempenho. No entanto, deve-se ter em mente que os indicadores de desempenho baseados em relações podem ser problemáticos devido ao denominador ao calcular a métrica. Se for muito pequeno, obteremos números muito grandes.

A melhor maneira de determinar a adequação do uso desses métodos para avaliar o desempenho futuro de acordo com um indicador específico é estudar a distribuição das estatísticas da amostra de bootstrap. Procuramos por "caudas pesadas" nas distribuições. Os resultados obtidos de distribuições com "caudas pesadas" devem ser usados com cautela.

Vamos ver um exemplo de avaliação do pior índice de Sharpe para o mesmo EA. Isso é alcançado reescrevendo a função passada para o parâmetro do ponteiro de função do construtor CBootstrap.

Os resultados do teste novamente indicam um desempenho muito pior em comparação com o resultado de um só teste.

Avaliação do Coeficiente de Sharpe


Conclusão

Saber a faixa de desempenho pode nos ajudar a tomar decisões de investimento mais fundamentadas em relação à escolha da estratégia. Embora o método apresentado seja baseado em dados estatísticos de livros didáticos, os usuários devem estar cientes de suas limitações inerentes.

Os intervalos de confiança calculados são tão eficazes quanto os dados em que se baseiam. Se as amostras usadas nos cálculos forem incorretas, encontramo-nos na clássica situação de "lixo entra, lixo sai". É sempre importante usar amostras apropriadas, representativas das condições que podem surgir no futuro.

Nome do arquivo
Descrição
Mql5files\include\Bootstrap.mqh
Contém a definição da classe CBootstrap
Mql5files\include\Returns.mqh
Contém a definição da classe CReturns
Mql5files\include\UniformRandom.mqh
Classe para gerar números uniformemente distribuídos de 0 a 1
Mql5files\scripts\ApproximateMeanReturns.mq5
Script que lê o arquivo salvo do testador de estratégias e calcula os intervalos de confiança da média de rentabilidade do projeto
Mql5files\experts\MovingAverage_Demo.mq5
EA usado para demonstrar a aplicação de CBootstrap e CReturns


Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/13426

Arquivos anexados |
Bootstrap.mqh (12.73 KB)
Returns.mqh (9.71 KB)
UniformRandom.mqh (2.84 KB)
mql5files.zip (10.97 KB)
Modelos prontos para integrar indicadores nos Expert Advisors (Parte 3): Indicadores de tendência Modelos prontos para integrar indicadores nos Expert Advisors (Parte 3): Indicadores de tendência
Neste artigo de referência, vamos dar uma olhada nos indicadores padrão da categoria Indicadores de tendência. Criaremos modelos prontos a serem usados em Expert Advisors, modelos esses que incluirão: declaração e configuração de parâmetros, inicialização/desinicialização de indicadores e recuperação de dados/sinais a partir de buffers de indicador em EAs.
Desenvolvimento de um Cliente MQTT para o MetaTrader 5: metodologia TDD (Parte 3) Desenvolvimento de um Cliente MQTT para o MetaTrader 5: metodologia TDD (Parte 3)
Este artigo faz parte de uma série que descreve as etapas do desenvolvimento de um cliente MQL5 nativo para o protocolo MQTT. Nesta parte, descrevemos em detalhes como aplicar o princípio do desenvolvimento orientado por testes para implementar a troca de pacotes CONNECT/CONNACK. Ao final desta etapa, nosso cliente DEVE ser capaz de agir apropriadamente ao trabalhar com todos os possíveis resultados do servidor ao tentar se conectar.
Algoritmos de otimização populacionais: Algoritmo de evolução da mente (Mind Evolutionary Computation, MEC) Algoritmos de otimização populacionais: Algoritmo de evolução da mente (Mind Evolutionary Computation, MEC)
Este artigo discute um algoritmo da família MEC, denominado algoritmo simples de evolução da mente (Simple MEC, SMEC). O algoritmo se destaca pela beleza da ideia subjacente e pela simplicidade de implementação.
Teoria das Categorias em MQL5 (Parte 21): Transformações naturais com LDA Teoria das Categorias em MQL5 (Parte 21): Transformações naturais com LDA
Este artigo, o 21º de nossa série, continua nossa análise das transformações naturais e de como elas podem ser implementadas usando a análise discriminante linear. Assim como no artigo anterior, a implementação é apresentada no formato de uma classe de sinal.