Avaliando o desempenho futuro com intervalos de confiança
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
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().
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.
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.
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
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso