English Русский 中文 Español Deutsch 日本語 Türkçe
preview
Força bruta para encontrar padrões (Parte IV): funcionalidade mínima

Força bruta para encontrar padrões (Parte IV): funcionalidade mínima

MetaTrader 5Testador | 17 maio 2021, 12:34
2 072 0
Evgeniy Ilin
Evgeniy Ilin
Neste artigo, apresentarei uma nova versão do meu programa, que ganhou uma funcionalidade mínima capaz de encontrar configurações de negociação para você e para mim. As mudanças têm como finalidade principal facilitar o uso e reduzir o limite de entrada ao mínimo. O objetivo principal ainda é envolver o maior número possível de usuários para a busca de configurações e pesquisas de mercado. Neste artigo, em vez de me aprofundar no caos do trading, vou fornecer o máximo de informações úteis possível. Tentarei explicar de forma entendível como usar este método, e falarei sobre todos os seus prós e contras, bem como as perspectivas de aplicação prática.


Mudanças na nova versão

Como no artigo anterior, o programa mudou bastante para melhor em termos de funcionalidade e, o mais importante, de usabilidade. As versões anteriores eram muito inconvenientes, tinham um monte de problemas, bugs e deficiências. Nesta versão, essas mudanças são máximas. Modernizamos amplamente os modelos de EAs e o próprio software. Lista de mudanças:

  1. Interface redesenhada
  2. Adicionado outro polinômio para implementar força bruta (com base na série revisada de Fourier)
  3. Expandido o mecanismo de geração de números aleatórios
  4. Expandido o conceito de método amigável
  5. Adicionados mecanismos de variação de lote para seções ultracurtas
  6. Adicionado um mecanismo para calcular o spread
  7. Adicionado um mecanismo para cortar o ruído de spread
  8. Muitos bugs corrigidos

Muitas das modificações planejadas foram executadas. No futuro modificaremos o algoritmo serão, mas não de maneira tão global.


Primeira demonstração de trabalho e nova ideia

No processo de criação de EAs, percebi que nem sempre quero inventar nomes e que nem sempre quero mexer nos terminais e limpar as configurações dos robôs a toda hora. Afinal, pode ser que já tivéssemos um robô com o mesmo nome e houvesse configurações dele em algum lugar do terminal e eu realmente não quero limpá-los sempre. A solução para esse problema foi o EA-coletor de configurações. Em outras palavras, o programa gera um arquivo de configuração no formato txt e o Expert Advisor simplesmente o lê. Essa abordagem acelera o trabalho com esta solução e a torna mais simples e compreensível. O esquema para solucionar isso fica assim:

Forex Awaiter Usage


Claro, eu deixei a versão do programa que gera robôs para apresentar este método mais facilmente ao usuário médio; criei um novo conceito especificamente para os terminais MetaTrader 4 e MetaTrader 5, tornando possível usar esta solução da forma mais simples e rápida possível. Eu acho que a coisa mais conveniente nesta solução é que a configuração funciona da mesma forma no MetaTrader 4 e no MetaTrader 5.

Vou mostrar uma parte da nova interface, já que é muito grande e vai ocupar muito espaço no artigo. Vou mostrar apenas a primeira guia.

New Awaiter interface


Todos os elementos são divididos em seções organizadas para facilitar a percepção, todos os elementos não utilizados são bloqueados se seu uso não fizer sentido.

Também criei um vídeo orientado ao artigo, nele demonstro como o programa funciona.



Por que não transferir as configurações por meio dos velhos conhecidos arquivos set? Tudo é muito simples, os arquivos set não pode conter arrays, e neste método eles estão presentes como configurações de entrada, além disso, têm um comprimento flutuante. Neste caso, será mais conveniente usar arquivos de texto comuns.


Novo polinômio baseado na série modificada de Fourier

Muitas pessoas familiarizadas com o aprendizado de máquina usam ativamente a série de Fourier e a aplicam amplamente. Inicialmente, a série de Fourier foi inventada para a expansão de funções no intervalo [-n;n]. Além disso, também precisamos saber como expandir a função para esta série e, em geral, por que e precisamos dessa expansão? Além disso, precisamos conhecer as nuances do método de substituição de variável, porque podemos precisar de uma expansão num intervalo completamente diferente de [-n;n]. Tudo isso requer um bom treinamento matemático e conhecimento das sutilezas, bem como compreensão, há algum sentido nisso para a negociação? A visão geral da série Fourier é assim:

Visão geral da série Fourier

Desta forma, este polinômio só pode ser útil para representar padrões de negociação de uma forma mais amigável, bem como para tentar prever o movimento do preço, assumindo que o preço é um processo de onda. Muito provavelmente, esta suposição é válida, mas nesta forma é inaplicável na abordagem de força bruta, pois para isso seria necessário mudar radicalmente todo o conceito do método. Em vez disso, podemos pensar em como transformar um determinado polinômio para que seja adequado para um determinado método. Em geral, pode haver muitas variações, mas se quisermos deixar a fórmula o mais semelhante possível à série de Fourier e ao mesmo tempo não usá-la para o propósito pretendido, então, em minha opinião, podemos fazer esta:

Primeira transformação

Nada mudou aqui, exceto que a linha ganhou maior liberdade e agora seu período está flutuando tanto para mais quanto para menos. Bem, exceto que há um número finito de termos. Isso se deve ao fato de que o array C[] são nossos coeficientes, que combinaremos para encontrar uma fórmula adequada, e seu número é limitado. Não podemos escrever esta série indefinidamente, mas teremos que nos limitar a apenas "m" barras. Entre outras coisas, removi o primeiro termo para ganhar maior simetria, para que os valores da fórmula fornecessem os sinais mais simétricos em ambos os intervalos "+" e "-". Mas desta forma só podemos selecionar uma função que dependa de 1 barra! Precisamos ter certeza de que os valores de todas as barras estão presentes na fórmula, além disso, a barra não tem 1 parâmetro, mas 6. Eu dei esses 6 parâmetros no artigo nº 2 deste ciclo. É claro que teremos que sacrificar a precisão de processamento de 1 barra para poder levar em consideração todo o resto. O ideal é envolver essa quantidade em outra. Mas não quero complicar o polinômio e vou me limitar à versão mais simples por enquanto:

Polinômio final

Na verdade, a função mudou de unidimensional para multidimensional, mas isso não significa que este polinômio seja capaz de descrever qualquer função multidimensional no hipercubo multidimensional selecionado. Porém, ao mesmo tempo, esta fórmula nos apresentará uma outra família de funções multidimensionais que serão capazes de descrever aquelas regularidades que a série de Taylor não será capaz de fazer ou onde esta série não pode descrever a função exigida com qualidade suficiente. Acontece que temos uma chance melhor de encontrar um padrão melhor na mesma amostra de dados. 

No código, essa função ficará assim:

if ( Method == "FOURIER" )
   {
      for ( int i=0; i<CNum; i++ )
         {
         Val+=C1[iterator]*MathSin(C1[iterator+1]*(Close[i+1]-Open[i+1])/_Point)+C1[iterator+2]*MathCos(C1[iterator+3]*(Close[i+1]-Open[i+1])/_Point);
         iterator+=4;
         }

      for ( int i=0; i<CNum; i++ )
         {
         Val+=C1[iterator]*MathSin(C1[iterator+1]*(High[i+1]-Open[i+1])/_Point)+C1[iterator+2]*MathCos(C1[iterator+3]*(High[i+1]-Open[i+1])/_Point);
         iterator+=4;
         }

      for ( int i=0; i<CNum; i++ )
         {
         Val+=C1[iterator]*MathSin(C1[iterator+1]*(Open[i+1]-Low[i+1])/_Point)+C1[iterator+2]*MathCos(C1[iterator+3]*(Open[i+1]-Low[i+1])/_Point);
         iterator+=4;
         }

      for ( int i=0; i<CNum; i++ )
         {
         Val+=C1[iterator]*MathSin(C1[iterator+1]*(High[i+1]-Close[i+1])/_Point)+C1[iterator+2]*MathCos(C1[iterator+3]*(High[i+1]-Close[i+1])/_Point);
         iterator+=4;
         }

      for ( int i=0; i<CNum; i++ )
         {
         Val+=C1[iterator]*MathSin(C1[iterator+1]*(Close[i+1]-Low[i+1])/_Point)+C1[iterator+2]*MathCos(C1[iterator+3]*(Close[i+1]-Low[i+1])/_Point);
         iterator+=4;
         }         

   return Val;
   }

Aqui não está a função completa, mas apenas a parte que implementa o polinômio dado. Apesar da simplicidade do design, mesmo essas fórmulas são adaptáveis ao mercado.

Fiz o ataque de força bruta do último ano do histórico mais rápido usando esse método, apenas para mostrar que a fórmula funciona. Não sei se piorou ou melhorou. Precisamos descobrir isso. A rigor, ainda não consegui encontrar algo que realmente funcionasse com essa fórmula, mas acho que isso se deve ao fato de que eu realmente não tinha tempo nem capacidade de computação. Gastei muito tempo na pesquisa da versão inicial. Aqui está o que consegui no par de moedas USDJPY M15 no último ano do histórico:

FOURIER Method


A única coisa de que não gostei nesta fórmula é que ela era muito instável no que diz respeito ao mecanismo de supressão de ruído de spread, aparentemente, essas são as características do uso de funções harmônicas no âmbito deste método. Talvez eu não tenha formulado corretamente a fórmula, mas por alguma razão não me parece assim. Neste método, sempre devemos incluir marcar o item "Spread Control" na segunda guia, o mecanismo de supressão de ruído de spread é desativado durante a otimização e oferece variantes muito boas. Acho que está tudo bem, só que essa fórmula acabou sendo muito "meiga". No entanto, como podemos ver, ela também consegue encontrar variantes muito boas.


Um pouco sobre a implementação de software por dentro

Não abordei muito este assunto nos artigos anteriores do ciclo e decidi abrir um pouco o véu do sigilo, e revelar como tudo funciona por dentro. A parte mais interessante e fácil é gerar os coeficientes para a fórmula. Esta parte, em primeiro lugar, deve ser abordada porque tornará mais claro como os coeficientes são gerados:

public void GenerateC(Tester CoreWorker)
   {
   double RX;
   TYPE_RANDOM RT;
   RX = RandomX.NextDouble();
   if (RandomType == TYPE_RANDOM.RANDOM_TYPE_R) RT = (TYPE_RANDOM)RandomX.Next(0, Enum.GetValues(typeof(TYPE_RANDOM)).Length-1);
   else RT = RandomType;

   for (int i = 0; i < CoreWorker.Variant.ANum; i++)
      {
      if (RT == TYPE_RANDOM.RANDOM_TYPE_0) 
         {
         if (i > 0) CoreWorker.Variant.Ci[i] = CoreWorker.Variant.Ci[i-1]*RandomX.NextDouble();
         else CoreWorker.Variant.Ci[0]=1.0;
         }
      if (RT == TYPE_RANDOM.RANDOM_TYPE_5)
         {
         if (RandomX.NextDouble() >= 0.5)
            {
            if (i > 0) CoreWorker.Variant.Ci[i] = CoreWorker.Variant.Ci[i - 1] * RandomX.NextDouble();
            else CoreWorker.Variant.Ci[0] = 1.0;
            }
         else
            {
            if (i > 0) CoreWorker.Variant.Ci[i] = CoreWorker.Variant.Ci[i - 1] * (-RandomX.NextDouble());
            else CoreWorker.Variant.Ci[0] = -1.0;
            }
         }
      if (RT == TYPE_RANDOM.RANDOM_TYPE_1) CoreWorker.Variant.Ci[i] = RandomX.NextDouble();
      if (RT == TYPE_RANDOM.RANDOM_TYPE_2)
         {
         if (RandomX.NextDouble() >= 0.5) CoreWorker.Variant.Ci[i] = RandomX.NextDouble();
         else CoreWorker.Variant.Ci[i] = -RandomX.NextDouble();
         }
      if (RT == TYPE_RANDOM.RANDOM_TYPE_3)
         {
         if (RandomX.NextDouble() >= RX)
            {
            if (RandomX.NextDouble() >= RX + (1.0 - RX) / 2.0) CoreWorker.Variant.Ci[i] = RandomX.NextDouble();
            else CoreWorker.Variant.Ci[i] = -RandomX.NextDouble();
            }
         else CoreWorker.Variant.Ci[i] = 0.0;
         }
      if (RT == TYPE_RANDOM.RANDOM_TYPE_4)
         {
         if (RandomX.NextDouble() >= RX) CoreWorker.Variant.Ci[i] = RandomX.NextDouble();
         else CoreWorker.Variant.Ci[i] = 0.0;
         }
      }
   }

É muito simples, existem vários tipos fixos de geração de números aleatórios e existe um tipo geral que implementa tudo de uma vez. Cada um dos tipos de geração foi testado na prática, e o tipo geral de geração "RANDOM_TYPE_R" funciona da forma mais eficiente possível. Tipos fixos nem sempre fornecem um resultado, uma vez que a natureza das cotações em diferentes instrumentos e prazos é diferente em quase todos os lugares. Na maioria dos casos, essas diferenças não podem ser vistas visualmente, mas a máquina vê tudo. Embora alguns tipos fixos em alguns intervalos de tempo sejam capazes de dar mais sinais com indicadores de máxima qualidade. Percebi que, por exemplo, no par NZDUSD H1, há um salto acentuado na qualidade dos resultados, se usarmos RANDOM_TYPE_4, que significa "apenas zeros e números positivos", o que pode ser uma alusão clara aos processos de onda ocultos inacessíveis ao olho. Eu adoraria explorar diferentes ferramentas com mais profundidade, mas, infelizmente, isso não é possível sozinho.


Novo mecanismo para suprimir o ruído de spread e calcular o spread

Conforme observado no artigo anterior, o spread distorce os dados de preços de tal forma que a maioria dos padrões encontrados fica dentro do spread. Na verdade, o spread é o pior inimigo de qualquer estratégia pela simples razão de que, ao contrário de nossas expectativas, a maioria das estratégias não fornece expectativa matemática suficiente para superar o spread. Nesse caso, não devemos ser enganados por um backtest ou estatísticas positivas de negociação numa conta real durante um mês ou mesmo um ano, porque esta é uma amostra muito pequena de dados para avaliar o desempenho futuro. Portanto, há toda uma classe de estratégias e sistemas de negociação automatizados chamados de scalpers noturnos. Esses robôs obtêm um pequeno lucro num período muito limitado. As corretoras lutam ativamente contra esses sistemas, ampliando os spreads após a meia-noite. O spread é simplesmente definido num nível que a maioria das estratégias não são lucrativas e seu dinheiro é enviado com segurança para a corretora.

Existe um valor que é quase inalterado para a maioria delas:

  • Spread = (Ask - Bid) / _Point
  • MidPrice = ( Ask + Bid ) / 2

Esse preço é destacado em verde. Isso nada mais é do que o meio do livro de ofertas. O livro de ofertas, via de regra, é alinhado em relação a um determinado preço, e tudo isso de forma que ambos os preços fiquem à mesma distância desse preço. Na verdade, se usarmos a definição clássica de livro de ofertas, esse preço não faz sentido, uma vez que não existem ordens de negociação, mas se assumirmos que todas as corretoras têm seus próprios spreads, então podemos concluir que esse preço, que não tem significado para todos são quase idênticos. Vou desenhar isto:

Spread

A figura superior mostra a série de preços de duas corretoras escolhidas aleatoriamente. Há sempre um preço "Ask" e um preço "Bid", que simbolizam os preços de compra ou venda. Mas a linha preta é comum para ambas as faixas de preço. Este valor é considerado de forma muito simples, como mostrei acima. O mais importante é que esse valor praticamente não depende da expansão ou redução dos spreads de uma determinada corretora, uma vez que o estreitamento ou expansão do livro de ofertas ocorre quase que uniformemente em relação a um determinado preço.

A figura inferior mostra uma situação real que realmente acontece com cotações de diferentes corretoras. Acontece que mesmo esse preço médio é diferente em diferentes threads. Eu pessoalmente não conheço os motivos, mas na mesma não são interessantes, uma vez que dificilmente é possível extrair algo útil para a negociação dessas informações. Eu descobri isso quando estava engajado na negociação de arbitragem, porque todas essas nuances são extremamente importantes aí. Em relação ao nosso método, é apenas importante:

  • MidPrice1=f(t)
  • MidPrice2=MidPrice1-D
  • MidPrice1 '(t) =  MidPrice2 '(t)

Ou seja, o preço médio de ambas as séries de preços, se representado como funções de tempo, possui a mesma derivada, uma vez que essas funções diferem apenas na constante “D”. Como nosso polinômio usa não tanto os preços em si quanto suas diferenças, todos esses valores serão funcionais da derivada dessas funções do preço médio. Além disso, como esses derivativos são iguais para todas as corretoras, isso nos dá, além de abstrair dos saltos de spread, e também a confiança de que nossas configurações serão igualmente eficazes em outra corretora. No caso de uma alternativa, as configurações que encontrarmos para nos terão uma chance extremamente baixa de serem testadas novamente com base em ticks reais e, portanto, uma chance ainda menor de que nossas configurações sejam aplicadas a outra corretora. Se usarmos esse conceito, todos esses problemas desaparecem, que já testei na prática.

Para implementar esse mecanismo, tive que fazer as modificações apropriadas em todos os elementos da solução. Em primeiro lugar, a fim de implementar tal abordagem, precisamos registrar adicionalmente os spreads em todos os pontos importantes da barra ao escrever o arquivo de cotação, como Open[], Close[], High[], Low[], a fim de corrigirmos ainda mais esses valores usando o mesmo spread que, de fato, nos dá o preço "Ask" pois as barras são desenhadas nos preços "Bid". Além disso, os Expert Advisors para registro de cotações mudaram de barra a barra para barra a barra. A função para gravar essas barras agora é:

void WriteBar()
   {
   FileWriteString(Handle0x,"\r\n");
   FileWriteString(Handle0x,DoubleToString(Close[1],8)+"\r\n");
   FileWriteString(Handle0x,DoubleToString(Open[1],8)+"\r\n");
   FileWriteString(Handle0x,DoubleToString(High[1],8)+"\r\n");
   FileWriteString(Handle0x,DoubleToString(Low[1],8)+"\r\n");         
   FileWriteString(Handle0x,IntegerToString(int(Time[1]))+"\r\n");
   FileWriteString(Handle0x,IntegerToString(PrevSpread)+"\r\n");
   FileWriteString(Handle0x,IntegerToString(CurrentSpread)+"\r\n");
   FileWriteString(Handle0x,IntegerToString(PrevHighSpread)+"\r\n");
   FileWriteString(Handle0x,IntegerToString(PrevLowSpread)+"\r\n");   
   MqlDateTime T;
   TimeToStruct(Time[1],T);
   FileWriteString(Handle0x,IntegerToString(int(T.hour))+"\r\n");
   FileWriteString(Handle0x,IntegerToString(int(T.min))+"\r\n");
   FileWriteString(Handle0x,IntegerToString(int(T.day_of_week))+"\r\n");         
   }      

Em verde são destacadas 4 linhas, que registram o spread em todos os quatro pontos da barra em que foi desenhada. Na versão anterior, esses valores não eram registrados e não eram considerados nos cálculos. Tanto escrever esses dados como obtê-los não é um problema. Para obter o spread em "High" e "Low", foi introduzida uma função simples que funciona com base em ticks:

void RecalcHighLowSpreads()
   {
   if ( Close[0] > LastHigh )
      {
      LastHigh=Close[0];
      HighSpread=int(SymbolInfoInteger(_Symbol,SYMBOL_SPREAD));
      }
   if ( Close[0] < LastLow )
      {
      LastLow=Close[0];
      LowSpread=int(SymbolInfoInteger(_Symbol,SYMBOL_SPREAD));
      }      
   }

Esta função apenas determina o spread no ponto mais alto e mais baixo da barra enquanto a barra atual está sendo formada. Quando uma nova barra aparece, a barra atual é considerada totalmente formada e seus dados são gravados no arquivo. Esta função sozinha não é suficiente, ela funciona em conjunto com uma baseada em barras:

bool bNewBar()
   {
   ArraySetAsSeries(Close,false);                        
   ArraySetAsSeries(Open,false);                           
   ArraySetAsSeries(High,false);                        
   ArraySetAsSeries(Low,false);                              
   CopyOpen(_Symbol,_Period,0,2,Open);
   CopyClose(_Symbol,_Period,0,2,Close);
   CopyHigh(_Symbol,_Period,0,2,High);
   CopyLow(_Symbol,_Period,0,2,Low);
   ArraySetAsSeries(Close,true);                        
   ArraySetAsSeries(Open,true);                           
   ArraySetAsSeries(High,true);                        
   ArraySetAsSeries(Low,true);                                 
   if ( Time0 < Time[1] )
      {
      if (Time0 != 0)
         {
         Time0=Time[1];
         PrevHighSpread=HighSpread;
         PrevLowSpread=LowSpread;         
         PrevSpread=CurrentSpread;
         CurrentSpread=int(SymbolInfoInteger(_Symbol,SYMBOL_SPREAD));
         HighSpread=CurrentSpread;
         LowSpread=CurrentSpread;         
         return true;
         }
      else
         {
         Time0=Time[1];
         return false;
         }
      }
   else return false;
   }

A função é um predicado e um elemento importante da lógica, em que todos os 4 spreads são finalmente determinados em todos os pontos importantes das barras. Tudo isso é implementado de forma semelhante dentro do programa. No manipulador OnTick isso funciona de forma muito simples:

RecalcHighLowSpreads();
if ( bNewBar()) WriteBar();

No arquivo onde localizada a cotação, agora terá a seguinte aparência:

Bar Structure

Dentro do programa, o array com preços médios é implementado de forma absolutamente idêntica:

OpenX[1]=Open[1]+(double(PrevSpread)/2.0)*_Point;
CloseX[1]=Close[1]+(double(Spread)/2.0)*_Point;
HighX[1]=High[1]+(double(PrevHighSpread)/2.0)*_Point;
LowX[1]=Low[1]+(double(PrevLowSpread)/2.0)*_Point;

No que diz respeito à implementação de toda essa abordagem nos Expert Advisors, à primeira vista parece que é possível implementar a supressão do ruído de spread de forma idêntica, mas o problema é que para isso é necessário coletar um certo número de ticks, e quanto maior o período de tempo, mais ticks você precisa coletar por barra e, quanto mais ticks, mais tempo a coletá-los. Se os preços "Ask", ou pelo menos os spreads, também fossem mantidos nas barras, seria fácil de fazer, mas neste caso é mais fácil calcular aos preços "Bid".

Como opção adicional, foi adicionado um mecanismo para levar em consideração o spread ao otimizar os resultados. Este mecanismo é totalmente opcional, como os testes mostraram, mas com poder de computação suficiente, ele pode fornecer resultados muito bons. O objetivo é exigir que o algoritmo abra e feche ordens apenas se o spread não exceder o valor exigido. Graças a que a versão atual da solução também grava spreads nos dados da barra, podemos controlar facilmente esse valor e calcular os indicadores de teste reais menos o spread.


Variação de lote em seções curtas

Na verdade, a variação de lote pode ser usada não apenas em seções muito curtas, mas também em seções longas, só que numa das versões. Existem apenas 2 mecanismos para gerenciamento de riscos ou lotes, como você quiser.

  • Aumento de lotes
  • Diminuição de lotes

O primeiro mecanismo deve ser usado com negociação invertida, quando apostamos que o sinal logo será invertido. A segunda opção só funciona se soubermos que o sinal é estável e durará muito tempo. Uma ampla variedade de funções de controle pode ser selecionada. Peguei as mais simples - as lineares. Em outras palavras, os lotes mudam com o tempo. Agora vou mostrar como esses mecanismos funcionam na prática.

Para esses fins, usei a cotação M15 para o par USDJPY. Neste caso, escolhi a versão MQL4 do robô para demonstração, já que ele não realiza backtests com base em ticks reais, pois negocia segundo os pontos de maior spread. Eu queria provar que uma abordagem de força bruta de qualidade suficientemente alta em intervalos de tempo baixos pode fornecer um bom período 'forward' com o tamanho da área onde a força bruta foi executada e superior. Tendo em vista que minhas capacidades agora são extremamente limitadas, tive que dedicar um mínimo de tempo a este assunto. Mas este resultado é suficiente para apresentar um período de trabalho progressivo bastante longo e dois mecanismos para trabalhar com lotes nessas seções futuras. Vamos começar apresentando a variante encontrada no site onde pesquisamos. Tamanho da parcela é de um ano:

USDJPY M15 bruteforce piece of history

Neste caso, o valor esperado é de pouco mais de 12 pips, se não levarmos em conta o spread, mas, visto que estamos nos abstraindo do spread, não estamos particularmente interessados nisso.< Vejamos o fator de lucro. O teste para um ano no futuro é assim:

1 year to future

Chama nossa atenção que apesar de a busca ter sido realizada apenas ao longo de um ano, a duração do padrão foi de pelo menos 1 ano, o que é muito bom. Na prática, isso significa que, se tiver um bom recursos de máquina, poderemos analisar todos os principais pares de moedas com spreads baixos numa ou duas semanas e, em seguida, escolher as melhores opções e explorar o padrão por pelo menos mais um ano. Claro, desde que tenha resistido ao backtest com base em ticks reais no MetaTrader 5. Claro que, para confirmar essa afirmação, precisamos de muito mais tempo, de um grande número de bons servidores e de diligência, mas esta análise pode ser realizada por várias pessoas se desejado, embora não seja necessário aplicar qualquer esforço titânico, porque todo o trabalho é feito pela máquina, e nossa tarefa é apenas coletar resultados e compilar estatísticas.

Agora vamos dar uma olhada no backtest a partir do futuro, no início há uma redução muito grande, o que geralmente é muito comum quando estamos procurando padrões em áreas pequenas, como um ano, no entanto, podemos usar essa redução para nossos próprios fins. No backtest de um ano, esta área é destacada com uma borda vermelha. Limitei o tempo de trabalho do EA nas configurações para que ele opere exatamente 50 dias de trading (sábado e domingo não são contados), e também inverti o sinal para que o rebaixamento se tornasse um lucro. Fiz isso para cortar a parte do gráfico que começa a subir após um dado rebaixamento, pois se tornará negativo quando invertido. Como resultado, o backtest ficará assim:

invert + fix lots 50 days to future

Preste atenção ao fator lucro. Vamos aumentá-lo. Na verdade, você nunca sabe realmente se haverá uma reversão e se essa reversão avançará muito, mas geralmente a reversão cobre uma parte considerável do movimento ocorrido no segmento onde aplicada força bruta. Se aplicarmos um aumento linear do lote, do mínimo ao máximo, obteremos esse backtest e um aumento no fator de lucro:

invert + increase lots 50 days to future

Agora vou demonstrar o mecanismo reverso, no backtest para 1 ano no futuro. Ele é destacado em verde. Nesta seção, há apenas uma seção crescente bastante grande no final - uma reversão do padrão. Corrigiremos essa situação com a ajuda de lotes moderados. Eu defino as configurações do robô para que a negociação vá direto para a borda desta área. Primeiro, vamos testá-lo com um lote fixo, de modo que tenhamos algo para comparar nosso fator de lucro resultante, fator esse que obtemos depois de aplicar nosso segundo mecanismo:

Green box fix lot

Agora eu ativo um mecanismo que implementa uma diminuição nos lotes ao longo do tempo, e também obtemos um aumento no fator de lucro, só que o gráfico fica muito mais agradável e suave, além disso, nos livramos do rebaixamento no final:

GreenBox + lot decrease

Eu vi essas técnicas em robôs de muitos vendedores. Se você aplicá-los no momento certo e no lugar certo, poderá aumentar a lucratividade e reduzir as perdas. Em teoria, tudo parece bonito, mas na prática tudo é muito mais complicado, no entanto, esses mecanismos agora estão presentes nos EAs que meu programa gera e podem ser ligados e desligados dependendo da situação.


Variantes práticas baseadas no histórico global

Acho que muitos estariam interessados em ver com seus próprios olhos e experimentar pelo menos algumas variantes a nível de configurações de trabalho capazes de passar por backtests globais com base no histórico real de ticks. Essas configurações foram encontradas por mim. Visto que o poder de computação era limitado e que fui eu que aplicou sozinho a força bruta, essas pesquisas demoraram bastante, mas mesmo assim já consegui encontrar várias, aqui estão elas:

USDCAD H1 2010-2020

USDJPY H1 2017-2021

EURUSD H1 2010-2021

Essas configurações serão anexadas ao artigo e todos poderão testá-las independentemente, se assim o desejarem. Já que, claro, ninguém se preocupa em procurar suas configurações e verificá-las com backtests em contas de demonstração. Em geral, a partir desta versão da solução, qualquer pessoa pode tentar este método.


Matemática por trás da força bruta

Esta seção deve ser abordada com o máximo de detalhes possível para que qualquer usuário tenha o mínimo de perguntas sobre ele. Vamos começar explicando como funciona a primeira guia do programa e como interpretar seus resultados.

Força bruta na primeira guia

Na verdade, tudo o que acontece em qualquer algoritmo de força bruta sempre obedece à teoria da probabilidade - tudo porque sempre temos algum tipo de modelo e sempre há uma iteração. A iteração é entendida como um ciclo completo de análise da versão atual da estratégia. Um ciclo completo pode consistir num ou dez testes, tudo depende da abordagem específica. Não é tão importante quantos testes e quantos usos de uma determinada variante são realizados pelo algoritmo do método, tudo isso pode ser classificado como uma iteração. Uma iteração pode ser considerada bem-sucedida ou malsucedida, dependendo dos requisitos para o resultado. Uma variedade de indicadores quantitativos pode servir de critério para um bom resultado, tudo depende, novamente, de qual é o método de análise.

Na saída do algoritmo, sempre há uma ou mais variantes que atendem aos nossos requisitos. Podemos apenas dizer ao algoritmo quantos dados de resultado armazenar na memória. Todos os outros resultados que atendam aos requisitos, mas que não tenham espaço de armazenamento suficiente, serão inevitavelmente descartados. Não importa quantas etapas haja em nossa implementação da força bruta, esse processo sempre ocorrerá simplesmente porque esse processo é inevitável. Como alternativa, correremos o risco de perder tempo processando dados de baixa qualidade deliberadamente. Desta forma, não perderemos uma única variante, mas podemos reduzir a velocidade de pesquisa. A abordagem a ser usada depende de você.

Agora vou direto ao ponto. Qualquer processo de busca de resultados acaba se resumindo a um processo de testes independentes de acordo com o esquema de Bernoulli, se o algoritmo for fixo. Tudo depende da probabilidade de conseguir uma boa variante. Essa probabilidade é sempre fixa para um algoritmo fixo. No nosso caso, essa probabilidade depende dos seguintes valores:

  • Tamanho da amostra
  • Variabilidade do algoritmo
  • Proximidade com a base
  • A rigidez dos requisitos para o resultado final

A quantidade e a qualidade dos resultados obtidos, nesse sentido, cresce com o número de iterações, exatamente de acordo com a fórmula de Bernoulli. Mas vale lembrar que este é um processo puramente probabilístico! Ou seja, é impossível prever com cem por cento de probabilidade qual conjunto de resultados obteremos, em vez disso, só podemos calcular a probabilidade de que o resultado desejado seja encontrado:

  • Pk - a probabilidade de que a iteração irá produzir uma versão de trabalho com os requisitos dados (dependendo dos requisitos, esta probabilidade aumenta fortemente)
  • C(n,m) - quantidade de combinações de "n" a "m"
  • Pa=Сумм(m0...m...n)[C(n,m)*Pow(Pk ,m)*Pow(1-Pk ,n-m)] - probabilidade de que após "n" iterações obteremos pelo menos "m0" variantes primárias que satisfaçam nossos requisitos
  • m0 - número mínimo de protótipos satisfatórios
  • Pa - probabilidade de que, como resultado do desenvolvimento, obteremos pelo menos "m0" ou mais de "n" iterações dentro do software.
  • n - número máximo disponível de ciclos de pesquisa para protótipos funcionais (quanto tempo estamos prontos para esperar pelos resultados)

O número de ciclos também pode ser expresso em termos de tempo, se tomarmos a velocidade da força bruta do contador na primeira guia e o tempo que estamos prontos para gastar no processamento dos dados atuais:

  • Sh - velocidade das iterações por hora
  • T - tempo em horas que estamos prontos para esperar
  • n = Sh*T

Mais ou menos semelhante, podemos calcular a probabilidade de encontrar variantes sujeitas aos requisitos de qualidade. Acima havia fórmulas para encontrar facilmente as variantes que se enquadram no filtro "Deviation", este é um requisito para a linearidade do resultado. Se esse filtro não for incluído, cada iteração será bem-sucedida e alguma variante sempre será encontrada, independentemente da medição encontrada. As variantes serão classificadas por índice de qualidade. Sujeito aos requisitos de qualidade, o valor "Ps" será uma função do valor de qualidade obtido de acordo com o módulo. Quanto maior for a qualidade de que precisamos, menor será o valor desta função:

  • Ps - probabilidade de um resultado atender certos requisitos de qualidade adicionais
  • q - qualidade exigida que queremos obter
  • qMax - a mais alta qualidade disponível
  • Ps = Ps(|q|) = K * Px(|q|) , q <= qMax
  • K = Pk - este coeficiente leva em consideração a probabilidade de cair de qualquer variante arbitrária (e já as variantes são selecionadas em termos de qualidade)
  • Ps ' (|q|) < 0
  • Lim (q-->qMax) [  Ps(|q|) ] = 0

A primeira derivada dessa função é negativa, simbolizando que conforme os requisitos aumentam, a probabilidade de ser atendida tende a zero. Quando "q" tende para o valor máximo disponível, o valor dessa função tende para "0", pois isso é uma probabilidade. Se "q" for maior que o valor máximo, esta função não tem sentido, uma vez que uma qualidade superior é inatingível para o algoritmo selecionado. Esta função é uma consequência da função de densidade de probabilidade da variável aleatória "q". Ps(q) e a densidade de probabilidade da variável aleatória P(q) são mostrados abaixo, bem como quantidades adicionais importantes para compreender isto:

Variety

Com base nessas ilustrações, você pode escrever:

  • Integral(q0,qMax) [P(q)] = Integral(-qMax,-q0) [P(q)] =  K*Px(|q|) = Ps(|q|) - probabilidade de nesta iteração atual surge a variante com |q| de q0 a qMax.
  • Integral(q1,q2) [P(q)] - probabilidade de que, como resultado da iteração, um valor de qualidade na faixa de q1 a q2 caia (apenas por exemplo, como interpretar as leituras da função de distribuição de uma variável aleatória)

Percebe-se que quanto mais qualidade queremos, mais tempo temos para despender e menos variantes serão encontradas dependendo do nosso objetivo. Além disso, qualquer método tem um limite superior no que diz respeito ao valor da qualidade, que depende tanto dos dados que analisamos quanto do aperfeiçoamento do nosso método.

Otimização na segunda guia

O processo de otimização na segunda guia é um pouco diferente do processo de pesquisa principal, mas, em geral, a mesma iteração e a probabilidade de uma variante que atenda aos nossos requisitos são tomadas como base. Existem muito mais filtros aqui, logo, a probabilidade de bons resultados também é menor. Mas, como as variantes já processadas são melhoradas na segunda guia, quanto melhores e mais bonitas forem as variantes na primeira, melhores serão os indicadores que obteremos na segunda guia com maior probabilidade. O processo de melhoria de uma variante específica não pode mais ser descrito tão simplesmente pela equação de Bernoulli, mesmo assim a fórmula final será um tanto semelhante a ela. Estaremos interessados na probabilidade de obter pelo menos uma variante que se enquadre em nossos filtros. Vou escrever tudo isso:

  • Py = Soma(1...m...n)[ Soma(0... i ... C(n,m)-1) {  Produto(0 .. j .. i-1 )[Pk[j]) * Produto(i .. j .. m) [1 - Pk[j]] } ] - probabilidade de obter pelo menos uma variante satisfatória que atenda aos requisitos do filtro
  • Pk[i] - probabilidade de obter uma variante que atenda aos requisitos dos filtros da segunda guia
  • n - divisão do intervalo de otimização (Interval Points do valor na 2ª guia)

A otimização ocorre exatamente da mesma maneira que nos otimizadores dos terminais MetaTrader 4 e MetaTrader 5, com a única diferença de que apenas um parâmetro é otimizado, que é o nosso sinal de compra ou venda. A etapa de otimização é calculada automaticamente, com base em quantas partes dividimos o intervalo de otimização (Interval Points) O valor superior do número que estamos otimizando é calculado durante o processo de pesquisa na primeira guia. Depois que o processo na primeira guia tiver passado, saberemos a faixa de flutuações nos valores do número otimizado; e, na segunda guia, só precisamos configurar a grade para subdividir esse intervalo. Como resultado da otimização, esta opção ocupou 1 slot de variantes, já na segunda guia ela simplesmente será atualizada à medida que ganhe melhor qualidade. 

Da mesma forma que na primeira guia, a probabilidade de obter alguma variante com requisitos de qualidade também terá alguma função de distribuição semelhante à que era maior. Consequentemente, isso significa que podemos aplicar as mesmas fórmulas a este processo, com uma pequena diferença:

  • Integral(q0,qMax) [P(q)] = Integral(-qMax,-q0) [P(q)] =  K*Px(|q|) = Pz(|q|) - probabilidade de que na iteração atual obtemos a variante com |q| de q0 a qMax.
  • K = Py

A única diferença aqui é o coeficiente "K", que é igual à nova probabilidade, que obtivemos, é um pouco mais alto. A probabilidade de não atingir a qualidade exigida a partir de uma variante é insignificante, mas, afinal, na primeira guia temos muitas dessas variantes e, quanto mais houver, melhor para nós. Mesmo a nível visceral, isso significa que quanto mais variantes primárias, mais e melhores aparecerão na segunda guia. Tudo isso é considerado igual. Infelizmente, a fórmula de Bernoulli não é aplicável aqui, mas a construção anterior, que a substituiu, é aplicável. Aqui já interpretamos a otimização de uma variante como uma iteração separada. O número de iterações, portanto, será exatamente igual a este número. Precisamos de pelo menos uma variante que atenda aos nossos requisitos, por esse motivo, a fórmula anterior é ideal, só que precisamos substituir o valor de Pk por Pz, que será determinado pela família de funções Pz[j](|q|), pois cada variante de otimização possui essa função uma vez que as variantes são diferentes.

  • Pb = Soma(1...m...n)[ Soma(0... i ... C(n,m)-1) {  Produto(0.. j .. i-1 )[Pz[j]) * Produto(i.. j .. m) [1 - Pz[j]] } ]
  • n - número de variantes encontradas na primeira guia

O resultado final é que quanto mais implementamos força bruta, mais qualidade obtemos, só que não nos esquecemos de que absolutamente todos os parâmetros afetam tanto as probabilidades quanto o resultado. As configurações devem ser definidas com sabedoria e não desperdiçar recursos de computação. As máquinas modernas são muito potentes, mas não se esqueça de que o ajuste competente e o conhecimento dos detalhes irão aumentar sua performance muitas vezes.


Falando um pouco sobre o acréscimo de dados ao histórico e retreinamento

O "acréscimo de dados ao histórico" ou "retreinamento" é um dos problemas presentes em inúmeros sistemas de negociação automatizados. Na verdade, os conceitos são diferentes, mas na realidade significam a mesma coisa. Podemos criar um sistema de negociação que mostre um desempenho incrível, até 1000% ao mês. Na verdade, esses sistemas nunca funcionarão na realidade. Parte dos usuários ganhará e ficará incrivelmente feliz, enquanto a segunda parte ficara furioso com o sistema de negociação. Nunca fiquei surpreso com esse fato. O comportamento das pessoas, especialmente do comprador, se presta à análise estatística e se encaixa muito bem na teoria da probabilidade.

Agora vamos direto ao ponto. Quanto mais parâmetros de entrada um sistema de negociação tiver e quanto mais diversa for a lógica do Expert Advisor, melhor será a performance deste último no que diz respeito ao histórico. Acontece que temos um processo muito simples de conversão de uma cotação em outro formato de dados. Sempre há funções de conversão para frente e para trás que podem fornecer processos de conversão de dados para frente e para trás. Pode ser comparado à criptografia e descriptografia. Por exemplo, o arquivo WinRar é um exemplo típico de criptografia se definirmos uma senha. Também tem compressão, mas vamos omitir isso. No contexto de nossa tarefa, o algoritmo de criptografia é uma combinação do processo de otimização e a presença de lógica de negociação. Um número suficiente de backtests no otimizador e uma certa lógica flexível podem fazer um milagre - o graal do teste. A mesma lógica de negociação é o mesmo decodificador que decodifica os preços futuros com base nas leituras do passado.

Infelizmente, todos os Expert Advisors se comportam, até certo ponto, como um graal durante os testes, a única diferença está no grau com que cada um deles se ajusta ao histórico. Porém, devemos ter em mente que uma parte da lógica é acrescentada e outra que é responsável pelo correto funcionamento no futuro. Na verdade, esse algoritmo é extremamente difícil de obter. A dificuldade reside no fato de não sabermos quais são as possibilidades máximas de "previsão justa" de um determinado algoritmo e, por isso, não podemos determinar os limites do sobreajuste. Para nos abstrairmos o máximo possível desse processo negativo, devemos criar um algoritmo que provavelmente possa prever o movimento do próximo candle; e quanto mais forte o grau de compressão dos dados de preço, mais confiança nesse algoritmo. Por exemplo, tomamos alguma função como sin(w*t), sabemos que esta função corresponde a um número infinito de pontos [X[i],Y[i]] - esta é uma matriz de dados de comprimento infinito, que é compactada num registro de função seno curto. Nesse caso, temos uma compressão de dados perfeita. Na verdade, tal compressão é impossível e sempre temos algum tipo de taxa de compressão de dados. Quanto maior esse coeficiente, maior a qualidade com que essa fórmula de mercado será definida.

No meu método, a quantidade de dados variáveis é fixa, mas, como em qualquer outro método, é possível acrescentar mais deles ao histórico. A única maneira de lidar com tal acréscimo é aumentar a taxa de compactação de dados. Isso é gerado apenas por meio de um aumento no tamanho da seção analisada do histórico. Existe também uma segunda maneira, isto é, reduzindo o número de barras analisadas na fórmula (Bars To Equation). Na realidade, é melhor recorrer ao primeiro método, porque ao reduzir o número de barras na fórmula, conscientemente baixamos o limite superior de "qMax" e gostaríamos de aumentá-lo. Resumindo, é melhor usar uma amostra de treinamento grande, e não economizar "Bars To Equation ". Além disso, deve ser lembrado que um aumento excessivo neste valor reduz a velocidade de nossa força bruta e inevitavelmente o volume do histórico pode aumentar.


Recomendações de uso

Durante o teste da solução, foram identificadas algumas sutilezas importantes para configurar o programa principal Awaiter.exe. Agora tentarei listar as mais importantes dessas sutilezas abaixo:

  1. Depois de definir as configurações em todas as guias da maneira que desejarmos, devemos salvá-las (botão Save Settings)
  2. Na segunda guia, podemos habilitar Spread Control
  3. Ao gerar cotações através do EA HistoryWriter, usamos o máximo de amostra possível (pelo menos 10 anos de histórico)
  4. Na primeira guia, podemos salvar mais variantes, 1000 é suficiente (Variants Memory)
  5. Na guia de otimização não definimos muitos Interval Points (20-100 é suficiente)
  6. Se quisermos obter configurações mais ou menos normais, que têm a chance de passar no backtest com base em ticks reais, não devemos exigir do otimizador um grande número de ordens nas variantes (Min Ordens)
  7. É necessário controlar a velocidade de pesquisa de variantes (se você estiver pesquisando há muito tempo e não encontrar variantes, você deverá pensar em alterar as configurações)
  8. Para obter resultados mais consistentes, use Deviation no intervalo "0,1 - 0,2", melhor se 0,1
  9. Ao usar a equação "FOURIER" na guia de otimização selecione "Spread Control" (a fórmula é muito suave e extremamente sensível ao ruído de spread)


Fim do artigo

Concluindo, devo dizer que esta solução não é um Graal. Na realidade, é apenas uma ferramenta. É bastante difícil implementar uma solução que seja suficientemente eficaz e ao mesmo tempo conveniente para qualquer usuário, que precise de pouca programação e do processo de otimização nos terminais MetaTrader 4 e MetaTrader 5. Mas acredito que a tarefa mínima ainda está concluída e a solução está pronta para uso geral. Claro, tenho dúvidas sobre o fato de alguém gostar, mas em geral não é tão importante.

Acho que haverá pessoas que acharão esse método útil. Claro, ainda há muito espaço para melhorias, assim como imperfeições e bugs, mas em geral já existe uma ferramenta de trabalho tanto para pesquisa de mercado quanto para negociação. Já a boa notícia é que agora tudo depende mais do poder de computação, e não de melhorias. Apesar da estar pronta, ainda lhe falta melhorar muito, mas espero que no futuro tudo seja corrigido.

Também há ideias em relação ao projeto, espero implementar tudo gradativamente conforme o tempo livre apareça. Uma dessas ideias é a construção de um polinômio lógico baseado nos mais famosos indicadores de osciladores e indicadores de preços, como as Bandas de Bollinger ou Moving Average. Mas este conceito precisa ser cuidadosamente pensado primeiro. Eu não gostaria de descer para o nível "olha aí um cruzamento de indicadores, abra um trade"; ainda podemos aplicar corretamente os sinais dos indicadores, já tenho algumas ideias. Espero também ter sido capaz de apresentar algo novo e algumas informações gerais úteis, pelo menos a nível de teoria.


Links para artigos anteriores da série

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

Arquivos anexados |
Awaiter_Project.zip (5961.38 KB)
Outras classes na biblioteca DoEasy (Parte 67): classe de objeto-gráfico Outras classes na biblioteca DoEasy (Parte 67): classe de objeto-gráfico
Neste artigo, vamos criar uma classe de um objeto-gráfico (um gráfico de um instrumento de negociação) e modificar a classe-coleção de objetos de sinal mql5 para que cada objeto-sinal armazenado na coleção também atualize todos os seus parâmetros quando a lista é atualizada.
Aprendizado de máquina em sistemas de negociação baseados em grade e martingale. Deveríamos apostar nele? Aprendizado de máquina em sistemas de negociação baseados em grade e martingale. Deveríamos apostar nele?
Este artigo apresentará ao leitor a técnica de aprendizado de máquina para negociação baseada em grade e martingale. Para minha surpresa, essa abordagem, por algum motivo, não é afetada de forma alguma na rede global. Após ler o artigo, podemos criar nossos próprios bots.
Padrão de design MVC e a possibilidade de usá-lo Padrão de design MVC e a possibilidade de usá-lo
Este artigo falará sobre um padrão MVC comum, bem como sobre os prós e os contras de seu uso em programas MQL. Seu propósito é o de "dividir" o código existente em três componentes separados: Modelo (Model), Visualização (View) e Controlador (Controller).
Redes Neurais de Maneira Fácil (Parte 12): Dropout Redes Neurais de Maneira Fácil (Parte 12): Dropout
Como a próxima etapa no estudo das redes neurais, eu sugiro considerar os métodos de aumentar a convergência durante o treinamento da rede neural. Existem vários desses métodos. Neste artigo, nós consideraremos um deles intitulado Dropout.