Testes de permutação de Monte Carlo no MetaTrader 5
Francis Dube | 1 fevereiro, 2024
Introdução
Aleksei Nikolaev escreveu um artigo interessante intitulado "Aplicação do método de Monte Carlo para otimizar estratégias de negociação". Nele, ele explora testes de permutação, caracterizados pela alternância aleatória das transações. O autor também faz uma menção breve a outro estilo de teste de permutação, onde há uma alteração aleatória na sequência de dados de preços. Neste contexto, o desempenho de um Expert Advisor é avaliado em comparação com os resultados obtidos em múltiplas variações da mesma série de preços.
Na minha opinião, o autor supôs erroneamente que tais testes de permutação não podem ser realizados em um Expert Advisor arbitrário usando o MetaTrader 5. Pelo menos, não completamente. Neste artigo, iremos demonstrar a execução de um teste de permutação envolvendo séries de preços aleatoriamente permutadas utilizando o MetaTrader 5. Forneceremos o código necessário para realizar a permutação das séries de preços, assim como um script que facilita os procedimentos iniciais para a realização de um teste de permutação completo em um Expert Advisor.
Visão geral dos testes de permutação
O teste de permutação que descreveremos aqui envolve uma amostra de dados de preços. É preferível que o teste seja realizado com uma amostra. Após aplicar o teste nesta série de preços, anotamos todos os critérios de desempenho relevantes. Em seguida, embaralhamos a ordem da série de preços original, provamos o Expert Advisor e registramos seu desempenho.
Fazemos isso muitas vezes, a cada vez alterando as séries de preços e anotando os critérios de desempenho, que são essenciais para comparações futuras. Isso deve ser feito, no mínimo, cem vezes, mas idealmente várias milhares de vezes. Quanto mais vezes reorganizarmos e testarmos, mais confiáveis serão os resultados. Então, o que esperamos encontrar nesses resultados?
Por que são necessários testes de permutação?
Após uma série de testes iterativos, acumulamos um conjunto de indicadores de desempenho para cada permutação. Independente do indicador de desempenho escolhido – seja o índice de Sharpe, a taxa de lucro ou o saldo final/lucro líquido –, analisamos os resultados. Imagine que realizamos 99 permutações (ou 100, incluindo o teste original sem permutações), resultando em 100 indicadores de desempenho para análise.
O próximo passo é determinar quantas vezes o desempenho no teste original foi superado e expressar esse número como uma fração do total de testes, neste caso, 100. Essa fração representa a probabilidade de alcançar um resultado igual ou superior no teste sem permutação, como se o Expert Advisor não tivesse capacidade de gerar lucro. Em estatística, isso é conhecido como valor-p, um elemento crucial em testes de hipóteses.
Em nosso exemplo hipotético com 100 iterações, descobrimos que 29 indicadores de desempenho com permutações superaram o teste original. Isso nos leva a um valor-p de 0,3, ou seja, 29+1/100. Significa que existe uma chance de 0,3 de um Expert Advisor não lucrativo apresentar um desempenho igual ou superior ao observado no teste original. Embora isso possa parecer promissor, idealmente queremos que os valores-p sejam o mais próximos possível de zero, em torno de 0,05 ou menos.
A fórmula completa é:
z+1/r+1
onde r representa o número de permutações e z é o número total de testes com melhor desempenho. O procedimento de permutação é importante para a execução adequada do teste.
Permutação da série de preços
Para permutar corretamente um conjunto de dados, é importante assegurar que cada sequência possível tenha a mesma probabilidade de ocorrência. Isso requer a geração de um número aleatório uniformemente distribuído entre 0 e 1. A ferramenta necessária para isso é fornecida pela biblioteca padrão MQL5, especificamente na seção estatística. Com essa ferramenta, podemos definir o intervalo de valores que desejamos.
//+------------------------------------------------------------------+ //| Random variate from the Uniform distribution | //+------------------------------------------------------------------+ //| Computes the random variable from the Uniform distribution | //| with parameters a and b. | //| | //| Arguments: | //| a : Lower endpoint (minimum) | //| b : Upper endpoint (maximum) | //| error_code : Variable for error code | //| | //| Return value: | //| The random value with uniform distribution. | //+------------------------------------------------------------------+ double MathRandomUniform(const double a,const double b,int &error_code) { //--- check NaN if(!MathIsValidNumber(a) || !MathIsValidNumber(b)) { error_code=ERR_ARGUMENTS_NAN; return QNaN; } //--- check upper bound if(b<a) { error_code=ERR_ARGUMENTS_INVALID; return QNaN; } error_code=ERR_OK; //--- check ranges if(a==b) return a; //--- return a+MathRandomNonZero()*(b-a); }
Quando se trata de permutar dados de preço, há condições específicas. Não podemos simplesmente trocar a posição de um valor de preço, já que isso interromperia as relações temporais inerentes às séries temporais financeiras. Por isso, optamos por permutar as variações de preço, em vez dos preços em si. Ao registrar os preços antes de compará-los, reduzimos a distorção nas diferenças de preços originais.
Neste método, devemos manter o primeiro valor de preço e excluí-lo da permutação. Ao reconstruir a série, isso assegura a preservação do padrão de tendência da sequência de preços original A única alteração ocorre nos movimentos internos dos preços, mantendo-se o primeiro e o último preço da série original.
Antes de realmente alterar a série de preços, devemos decidir quais dados utilizaremos. No MetaTrader 5, os dados dos gráficos são apresentados em colunas que derivam de dados de ticks. Permutar uma série de preços é muito mais simples do que permutar informações de barras, por isso, escolhemos dados de ticks. A utilização de ticks, contudo, introduz outras complexidades, já que os ticks englobam mais do que apenas os preços brutos. Eles contêm informações sobre volume, tempo e flags de tick.
Inicialmente, as informações de tempo e flag de tick são mantidas constantes, de modo que nosso procedimento de permutação não deve alterá-las. Nosso foco recai sobre o bid, ask e volume. Surge uma segunda complexidade: a possibilidade de esses valores serem zero, o que dificultaria a aplicação de transformações logarítmicas. Para ilustrar como contornar esses desafios, vamos analisar o código.
Implementando o algoritmo de permutação de ticks
A CPermuteTicks, contida no arquivo PermuteTicks.mqh, é a classe responsável pelo procedimento de permutação de ticks. Dentro do PermuteTicks.mqh, incluímos Uniform.mqh da biblioteca padrão para acessar a funcionalidade que gera números aleatórios distribuídos uniformemente dentro de um intervalo especificado. As definições que se seguem estabelecem esse intervalo. Tenha cuidado ao ajustar esses valores, assegurando que o mínimo esteja sempre abaixo do máximo estabelecido.
//+------------------------------------------------------------------+ //| PermuteTicks.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #include<Math\Stat\Uniform.mqh> //+-----------------------------------------------------------------------------------+ //| defines: representing range of random values from random number generator | //+-----------------------------------------------------------------------------------+ #define MIN_THRESHOLD 1e-5 #define MAX_THRESHOLD 1.0
A estrutura CMqlTick representa os membros da estrutura embutida MqlTick, que é com a qual a classe interage. As demais informações dos ticks não são afetadas por este processo.
//+------------------------------------------------------------------+ //| struct to handle tick data to be worked on | //+------------------------------------------------------------------+ struct CMqlTick { double ask_d; double bid_d; double vol_d; double volreal_d; };
A classe CPermuteTicks possui três propriedades privadas de array: m_ticks, que armazena os ticks originais; m_logticks, que contém os ticks transformados logisticamente; e m_differenced, onde estão reunidos os ticks de diferença.
//+------------------------------------------------------------------+ //| Class to enable permutation of a collection of ticks in an array | //+------------------------------------------------------------------+ class CPermuteTicks { private : MqlTick m_ticks[]; //original tick data to be shuffled CMqlTick m_logticks[]; //log transformed tick data of original ticks CMqlTick m_differenced[]; //log difference of tick data bool m_initialized; //flag representing proper preparation of a dataset //helper methods bool LogTransformTicks(void); bool ExpTransformTicks(MqlTick &out_ticks[]); public : //constructor CPermuteTicks(void); //desctrucotr ~CPermuteTicks(void); bool Initialize(MqlTick &in_ticks[]); bool Permute(MqlTick &out_ticks[]); };
Um flag lógico, m_initialized, sinaliza quando a operação de pré-processamento foi concluída com sucesso, tornando possível realizar as permutações.
Para usar a classe, o usuário deve chamar o método Initialize() após criar uma instância do objeto. O método requer um array de ticks que precisam ser permutados. No método, os tamanhos dos arrays internos da classe são ajustados, e o LogTransformTicks() é aplicado para transformar os dados dos ticks. Isso inclui a remoção de valores nulos ou negativos, substituindo-os por 1,0. Após a permutação, os dados dos ticks logisticamente transformados são revertidos ao seu estado original pelo método privado ExpTransformTicks().
//+--------------------------------------------------------------------+ //|Initialize the permutation process by supplying ticks to be permuted| //+--------------------------------------------------------------------+ bool CPermuteTicks::Initialize(MqlTick &in_ticks[]) { //---set or reset initialization flag m_initialized=false; //---check arraysize if(in_ticks.Size()<5) { Print("Insufficient amount of data supplied "); return false; } //---copy ticks to local array if(ArrayCopy(m_ticks,in_ticks)!=int(in_ticks.Size())) { Print("Error copying ticks ", GetLastError()); return false; } //---ensure the size of m_differenced array if(m_differenced.Size()!=m_ticks.Size()-1) ArrayResize(m_differenced,m_ticks.Size()-1); //---apply log transformation to relevant tick data members if(!LogTransformTicks()) { Print("Log transformation failed ", GetLastError()); return false; } //---fill m_differenced with differenced values, excluding the first tick for(uint i=1; i<m_logticks.Size(); i++) { m_differenced[i-1].bid_d=(m_logticks[i].bid_d)-(m_logticks[i-1].bid_d); m_differenced[i-1].ask_d=(m_logticks[i].ask_d)-(m_logticks[i-1].ask_d); m_differenced[i-1].vol_d=(m_logticks[i].vol_d)-(m_logticks[i-1].vol_d); m_differenced[i-1].volreal_d=(m_logticks[i].volreal_d)-(m_logticks[i-1].volreal_d); } //---set the initilization flag m_initialized=true; //--- return true; }
O método Permute() é o responsável por gerar os ticks permutados. Ele necessita de um array dinâmico MqlTick para receber os ticks permutados. O processo de reorganização ocorre dentro de um loop while, alterando a posição do valor diferencial do tick baseado no número aleatório gerado a cada iteração.
//+------------------------------------------------------------------+ //|Public method which applies permutation and gets permuted ticks | //+------------------------------------------------------------------+ bool CPermuteTicks::Permute(MqlTick &out_ticks[]) { //---zero out tick array ZeroMemory(out_ticks); //---ensure required data already supplied through initialization if(!m_initialized) { Print("not initialized"); return false; } //---resize output array if necessary if(out_ticks.Size()!=m_ticks.Size()) ArrayResize(out_ticks,m_ticks.Size()); //--- int i,j; CMqlTick tempvalue; i=(int)m_ticks.Size()-1; int error_value; double unif_rando; ulong time = GetTickCount64(); while(i>1) { error_value=0; unif_rando=MathRandomUniform(MIN_THRESHOLD,MAX_THRESHOLD,error_value); if(!MathIsValidNumber(unif_rando)) { Print("Invalid random value ",error_value); return(false); } j=(int)(unif_rando*i); if(j>=i) j=i-1; --i; //---swap tick data randomly tempvalue.bid_d=m_differenced[i].bid_d; tempvalue.ask_d=m_differenced[i].ask_d; tempvalue.vol_d=m_differenced[i].vol_d; tempvalue.volreal_d=m_differenced[i].volreal_d; m_differenced[i].bid_d=m_differenced[j].bid_d; m_differenced[i].ask_d=m_differenced[j].ask_d; m_differenced[i].vol_d=m_differenced[j].vol_d; m_differenced[i].volreal_d=m_differenced[j].volreal_d; m_differenced[j].bid_d=tempvalue.bid_d; m_differenced[j].ask_d=tempvalue.ask_d; m_differenced[j].vol_d=tempvalue.vol_d; m_differenced[j].volreal_d=tempvalue.volreal_d; } //---undo differencing for(uint k = 1; k<m_ticks.Size(); k++) { m_logticks[k].bid_d=m_logticks[k-1].bid_d + m_differenced[k-1].bid_d; m_logticks[k].ask_d=m_logticks[k-1].ask_d + m_differenced[k-1].ask_d; m_logticks[k].vol_d=m_logticks[k-1].vol_d + m_differenced[k-1].vol_d; m_logticks[k].volreal_d=m_logticks[k-1].volreal_d + m_differenced[k-1].volreal_d; } //---copy the first tick out_ticks[0].bid=m_ticks[0].bid; out_ticks[0].ask=m_ticks[0].ask; out_ticks[0].volume=m_ticks[0].volume; out_ticks[0].volume_real=m_ticks[0].volume_real; out_ticks[0].flags=m_ticks[0].flags; out_ticks[0].last=m_ticks[0].last; out_ticks[0].time=m_ticks[0].time; out_ticks[0].time_msc=m_ticks[0].time_msc; //---return transformed data return ExpTransformTicks(out_ticks); } //+------------------------------------------------------------------+
Após finalizar todas as iterações, o array m_logticks é reconstituído, revertendo as diferenças usando os dados dos ticks permutados m_differenced. O único argumento do método Permute() é então preenchido com os dados de m_logtick revertidos ao seu domínio original, e as informações de tempo e flag de tick são copiadas da série original de ticks.
//+-------------------------------------------------------------------+ //|Helper method applying log transformation | //+-------------------------------------------------------------------+ bool CPermuteTicks::LogTransformTicks(void) { //---resize m_logticks if necessary if(m_logticks.Size()!=m_ticks.Size()) ArrayResize(m_logticks,m_ticks.Size()); //---log transform only relevant data members, avoid negative and zero values for(uint i=0; i<m_ticks.Size(); i++) { m_logticks[i].bid_d=(m_ticks[i].bid>0)?MathLog(m_ticks[i].bid):MathLog(1e0); m_logticks[i].ask_d=(m_ticks[i].ask>0)?MathLog(m_ticks[i].ask):MathLog(1e0); m_logticks[i].vol_d=(m_ticks[i].volume>0)?MathLog(m_ticks[i].volume):MathLog(1e0); m_logticks[i].volreal_d=(m_ticks[i].volume_real>0)?MathLog(m_ticks[i].volume_real):MathLog(1e0); } //--- return true; } //+-----------------------------------------------------------------------+ //|Helper method undoes log transformation before outputting permuted tick| //+-----------------------------------------------------------------------+ bool CPermuteTicks::ExpTransformTicks(MqlTick &out_ticks[]) { //---apply exponential transform to data and copy original tick data member info //---not involved in permutation operations for(uint k = 1; k<m_ticks.Size(); k++) { out_ticks[k].bid=(m_logticks[k].bid_d)?MathExp(m_logticks[k].bid_d):0; out_ticks[k].ask=(m_logticks[k].ask_d)?MathExp(m_logticks[k].ask_d):0; out_ticks[k].volume=(m_logticks[k].vol_d)?(ulong)MathExp(m_logticks[k].vol_d):0; out_ticks[k].volume_real=(m_logticks[k].volreal_d)?MathExp(m_logticks[k].volreal_d):0; out_ticks[k].flags=m_ticks[k].flags; out_ticks[k].last=m_ticks[k].last; out_ticks[k].time=m_ticks[k].time; out_ticks[k].time_msc=m_ticks[k].time_msc; } //--- return true; }
Com este algoritmo, podemos processar permutações em séries de preços, mas isso é apenas a metade do processo. A próxima etapa envolve a realização de testes.
Teste de permutação
O teste de permutação envolve duas funções do terminal MetaTrader 5. A primeira função permite criar símbolos personalizados e especificar suas propriedades, enquanto a segunda possibilita a otimização de Expert Advisors com base nos símbolos presentes na lista "Market Watch". Essencialmente, isso adiciona pelo menos mais dois passos ao processo.
Podemos permutar ticks e criar símbolos personalizados a partir de qualquer um existente. Ao fazer isso, para cada símbolo personalizado é especificada uma única permutação de ticks para o símbolo usado como base. Embora os símbolos possam ser criados manualmente, automatizar a criação de símbolos e a adição de ticks permutados é mais sensato.
É exatamente isso que o script PrepareSymbolsForPermutationTests faz. Ele utiliza dados de entrada do usuário para definir o símbolo base, o intervalo de datas para os ticks a serem usados nas permutações, o número de permutações desejadas (que se traduz no número de símbolos personalizados a serem criados), e um identificador de string opcional para ser adicionado aos nomes dos novos símbolos personalizados.//+------------------------------------------------------------------+ //| PrepareSymbolsForPermutationTests.mq5 | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include<GenerateSymbols.mqh> #property script_show_inputs //--- input parameters input string BaseSymbol="EURUSD"; input datetime StartDate=D'2023.06.01 00:00'; input datetime EndDate=D'2023.08.01 00:00'; input uint Permutations=100; input string CustomID="";//SymID to be added to symbol permutation names //--- CGenerateSymbols generateSymbols(); //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- if(!generateSymbols.Initiate(BaseSymbol,CustomID,StartDate,EndDate)) return; //--- Print("Number of newly generated symbols is ", generateSymbols.Generate(Permutations)); //--- } //+------------------------------------------------------------------+
Automaticamente, o script gera nomes de símbolos, anexando uma enumeração ao nome do símbolo base. O código para essa funcionalidade está no arquivo GenerateSymbols.mqh, que contém a definição da classe CGenerateSymbols. Esta classe depende de outras duas: NewSymbol.mqh, que contém a definição da classe CNewSymbol, adaptada do código contido no artigo "Guia Prático do MQL5: Teste de estresse de uma estratégia de negociação utilizando os símbolos personalizados".
//+------------------------------------------------------------------+ //| Class CNewSymbol. | //| Purpose: Base class for a custom symbol. | //+------------------------------------------------------------------+ class CNewSymbol : public CObject { //--- === Data members === --- private: string m_name; string m_path; MqlTick m_tick; ulong m_from_msc; ulong m_to_msc; uint m_batch_size; bool m_is_selected; //--- === Methods === --- public: //--- constructor/destructor void CNewSymbol(void); void ~CNewSymbol(void) {}; //--- create/delete int Create(const string _name,const string _path="",const string _origin_name=NULL, const uint _batch_size=1e6,const bool _is_selected=false); bool Delete(void); //--- methods of access to protected data string Name(void) const { return(m_name); } bool RefreshRates(void); //--- fast access methods to the integer symbol properties bool Select(void) const; bool Select(const bool select); //--- service methods bool Clone(const string _origin_symbol,const ulong _from_msc=0,const ulong _to_msc=0); bool LoadTicks(const string _src_file_name); //--- API bool SetProperty(ENUM_SYMBOL_INFO_DOUBLE _property,double _val) const; bool SetProperty(ENUM_SYMBOL_INFO_INTEGER _property,long _val) const; bool SetProperty(ENUM_SYMBOL_INFO_STRING _property,string _val) const; double GetProperty(ENUM_SYMBOL_INFO_DOUBLE _property) const; long GetProperty(ENUM_SYMBOL_INFO_INTEGER _property) const; string GetProperty(ENUM_SYMBOL_INFO_STRING _property) const; bool SetSessionQuote(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index, const datetime _from,const datetime _to); bool SetSessionTrade(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index, const datetime _from,const datetime _to); int RatesDelete(const datetime _from,const datetime _to); int RatesReplace(const datetime _from,const datetime _to,const MqlRates &_rates[]); int RatesUpdate(const MqlRates &_rates[]) const; int TicksAdd(const MqlTick &_ticks[]) const; int TicksDelete(const long _from_msc,long _to_msc) const; int TicksReplace(const MqlTick &_ticks[]) const; //--- private: template<typename PT> bool CloneProperty(const string _origin_symbol,const PT _prop_type) const; int CloneTicks(const MqlTick &_ticks[]) const; int CloneTicks(const string _origin_symbol) const; };
A classe ajuda a criar novos símbolos personalizados com base nos existentes. A última dependência necessária é a PermuteTicks.mqh, um arquivo já mencionado anteriormente.
//+------------------------------------------------------------------+ //| GenerateSymbols.mqh | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #include<PermuteTicks.mqh> #include<NewSymbol.mqh> //+------------------------------------------------------------------+ //| defines:max number of ticks download attempts and array resize | //+------------------------------------------------------------------+ #define MAX_DOWNLOAD_ATTEMPTS 10 #define RESIZE_RESERVE 100 //+------------------------------------------------------------------+ //|CGenerateSymbols class | //| creates custom symbols from an existing base symbol's tick data | //| symbols represent permutations of base symbol's ticks | //+------------------------------------------------------------------+ class CGenerateSymbols { private: string m_basesymbol; //base symbol string m_symbols_id; //common identifier added to names of new symbols long m_tickrangestart; //beginning date for range of base symbol's ticks long m_tickrangestop; //ending date for range of base symbol's ticks uint m_permutations; //number of permutations and ultimately the number of new symbols to create MqlTick m_baseticks[]; //base symbol's ticks MqlTick m_permutedticks[];//permuted ticks; CNewSymbol *m_csymbols[]; //array of created symbols CPermuteTicks *m_shuffler; //object used to shuffle tick data public: CGenerateSymbols(void); ~CGenerateSymbols(void); bool Initiate(const string base_symbol,const string symbols_id,const datetime start_date,const datetime stop_date); uint Generate(const uint permutations); };
CGenerateSymbols oferece duas funções principais. O método Initiate() deve ser chamado imediatamente após a criação do objeto e recebe quatro parâmetros que correspondem com as entradas fornecidas pelo usuário.
//+-----------------------------------------------------------------------------------------+ //|set and check parameters for symbol creation, download ticks and initialize tick shuffler| //+-----------------------------------------------------------------------------------------+ bool CGenerateSymbols::Initiate(const string base_symbol,const string symbols_id,const datetime start_date,const datetime stop_date) { //---reset number of permutations previously done m_permutations=0; //---set base symbol m_basesymbol=base_symbol; //---make sure base symbol is selected, ie, visible in WatchList if(!SymbolSelect(m_basesymbol,true)) { Print("Failed to select ", m_basesymbol," error ", GetLastError()); return false; } //---set symbols id m_symbols_id=symbols_id; //---check, set ticks date range if(start_date>=stop_date) { Print("Invalid date range "); return false; } else { m_tickrangestart=long(start_date)*1000; m_tickrangestop=long(stop_date)*1000; } //---check shuffler object if(CheckPointer(m_shuffler)==POINTER_INVALID) { Print("CPermuteTicks object creation failed"); return false; } //---download ticks Comment("Downloading ticks"); uint attempts=0; int downloaded=-1; while(attempts<MAX_DOWNLOAD_ATTEMPTS) { downloaded=CopyTicksRange(m_basesymbol,m_baseticks,COPY_TICKS_ALL,m_tickrangestart,m_tickrangestop); if(downloaded<=0) { Sleep(500); ++attempts; } else break; } //---check download result if(downloaded<=0) { Print("Failed to get tick data for ",m_basesymbol," error ", GetLastError()); return false; } Comment("Ticks downloaded"); //---return shuffler initialization result return m_shuffler.Initialize(m_baseticks); }
O método Generate(), por sua vez, processa o número especificado de permutações e informa quantos novos símbolos personalizados foram adicionados ao "Market Watch".
O resultado da execução do script é exibido na aba "Experts" do terminal.
//+------------------------------------------------------------------+ //| generate symbols return newly created or refreshed symbols | //+------------------------------------------------------------------+ uint CGenerateSymbols::Generate(const uint permutations) { //---check permutations if(!permutations) { Print("Invalid parameter value for Permutations "); return 0; } //---resize m_csymbols if(m_csymbols.Size()!=m_permutations+permutations) ArrayResize(m_csymbols,m_permutations+permutations,RESIZE_RESERVE); //--- string symspath=m_basesymbol+m_symbols_id+"_PermutedTicks"; int exists; //---do more permutations for(uint i=m_permutations; i<m_csymbols.Size(); i++) { if(CheckPointer(m_csymbols[i])==POINTER_INVALID) m_csymbols[i]=new CNewSymbol(); exists=m_csymbols[i].Create(m_basesymbol+m_symbols_id+"_"+string(i+1),symspath,m_basesymbol); if(exists>0) { Comment("new symbol created "+m_basesymbol+m_symbols_id+"_"+string(i+1) ); if(!m_csymbols[i].Clone(m_basesymbol) || !m_shuffler.Permute(m_permutedticks)) break; else { m_csymbols[i].Select(true); Comment("adding permuted ticks"); if(m_csymbols[i].TicksAdd(m_permutedticks)>0) m_permutations++; } } else { Comment("symbol exists "+m_basesymbol+m_symbols_id+"_"+string(i+1) ); m_csymbols[i].Select(true); if(!m_shuffler.Permute(m_permutedticks)) break; Comment("replacing ticks "); if(m_csymbols[i].TicksReplace(m_permutedticks)>0) m_permutations++; else break; } } //---return successful number of permutated symbols Comment(""); //--- return m_permutations; }
O próximo passo envolve realizar a otimização no testador de estratégias, selecionar o método de otimização mais recente e especificar o Expert Advisor para o teste. Este processo pode ser demorado. Após sua conclusão, obtemos um conjunto de dados de desempenho detalhado.
Exemplo
Vejamos como isso funciona na prática, realizando um teste com o Expert Advisor MACD Sample, incluído no pacote. O teste ocorrerá no símbolo AUDUSD, com 100 permutações definidas no script.
Após rodar o script, acabamos com 100 símbolos adicionais, todos baseados em ticks permutados do símbolo AUDUSD.
Finalmente, iniciaremos o teste de otimização.
As configurações do Expert Advisor são mostradas abaixo.
Resultados do teste.
Na aba de resultados do testador de estratégias, encontramos todos os indicadores de desempenho relevantes, organizados em ordem decrescente de acordo com o critério de desempenho escolhido, acessível através de um menu suspenso no canto superior direito da janela do testador. Neste layout, é possível calcular o valor-p manualmente ou automaticamente ao processar o arquivo .xml, que pode ser facilmente exportado usando o clique direito do mouse no testador.
Neste exemplo específico, os cálculos são desnecessários para ver que os resultados de teste do símbolo original ficaram bem abaixo na lista de resultados, enquanto mais de 10 símbolos permutados apresentaram um desempenho superior. Isso sugere que o valor-p é maior que 0,05.
No entanto, deve-se encarar os resultados deste teste com uma dose de ceticismo, pois o período de teste selecionado foi relativamente curto. É recomendável escolher um período de teste mais extenso e representativo das condições de negociação real.
Como mencionado anteriormente, temos várias maneiras de processar os resultados para calcular os valores-p. O foco subsequente será na análise dos dados do arquivo XML exportado do testador de estratégias. Demonstraremos o uso de um aplicativo de planilha eletrônica para processar o arquivo com facilidade.
Depois de exportar o arquivo, lembre-se de registrar onde foi salvo. Abra-o com qualquer aplicativo de planilha eletrônica. A imagem a seguir mostra o uso do OpenOffice Calc, onde uma nova linha foi adicionada no final da tabela. Antes de continuar, pode ser prudente excluir as linhas referentes aos símbolos que não serão considerados nos cálculos. Abaixo de cada coluna apropriada, o valor-p é calculado por meio de um macro personalizado. Este macro utiliza tanto os indicadores de desempenho do símbolo permutado (na linha 18 do documento exibido) quanto os indicadores de desempenho de todos os símbolos permutados correspondentes. A fórmula exata do macro é mostrada na imagem.
Além do uso de um aplicativo de planilha eletrônica, Python é outra opção viável, com seus vários módulos para análise de arquivos XML. Se familiarizado com MQL5, os arquivos também podem ser processados usando um script simples. Apenas certifique-se de escolher um diretório facilmente acessível ao exportar os resultados da otimização do testador.
Conclusão
Demonstramos a aplicabilidade do teste de permutação a qualquer Expert Advisor, mesmo sem acesso ao código-fonte. Este método de teste é extremamente valioso, pois fornece uma abordagem estatística robusta que não depende de suposições sobre a distribuição dos dados. Tal característica o distingue de muitos outros testes estatísticos comumente utilizados no desenvolvimento de estratégias.
Contudo, a principal desvantagem reside no tempo e nos recursos computacionais exigidos para realizar o teste. Um processador potente e considerável espaço em disco são indispensáveis, já que a criação de novos ticks e símbolos demanda uma quantidade substancial de armazenamento. Na minha opinião, quem se dedica à aquisição de Expert Advisors deveria considerar este método de análise. Apesar de exigir tempo, ele pode prevenir decisões equivocadas.
A análise baseada em dados de preços permutados oferece múltiplas aplicações. Esse método pode ser empregado tanto para avaliar o comportamento dos indicadores quanto em diferentes fases do desenvolvimento estratégico. As possibilidades são vastas. Em certas ocasiões, durante o desenvolvimento ou teste de estratégias, os dados disponíveis podem parecer insuficientes. A utilização de séries de preços permutados amplia significativamente o volume de dados disponíveis para testes. Os códigos-fonte de todos os programas mql5 descritos aqui estão anexados ao artigo, esperando ser de utilidade para os leitores.
Nome do Arquivo | Tipo de Programa | Descrição |
---|---|---|
GenerateSymbols.mqh | Arquivo Include | Definição da classe CGenerateSymbols para geração de símbolos com dados de ticks permutados de um símbolo base selecionado |
NewSymbol.mqh | Arquivo Include | Definição da classe CNewSymbol para a criação de símbolos personalizados |
PermuteTicks.mqh | Arquivo Include | Definição da classe CPermuteTicks para a criação de permutações de um array de dados de ticks |
PrepareSymbolsForPermutationTests.mq5 | Arquivo de Script | Script que automatiza a criação de símbolos personalizados com permutação de ticks na preparação de um teste de permutação |