English Русский 中文 Español Deutsch 日本語
preview
Técnicas do MQL5 Wizard que você deve conhecer (Parte 22): GANs Condicionais

Técnicas do MQL5 Wizard que você deve conhecer (Parte 22): GANs Condicionais

MetaTrader 5Sistemas de negociação |
179 0
Stephen Njuki
Stephen Njuki

Introdução

Redes Gerativas Adversariais Condicionais (cGAN) são um tipo de GAN que permite a personalização do tipo de dado de entrada em sua rede gerativa. Como pode ser visto no link compartilhado e ao ler sobre o assunto, as GANs são um par de redes neurais: uma geradora e uma discriminadora. Ambas são treinadas ou treinam entre si, com a geradora melhorando na geração de uma saída-alvo, enquanto a discriminadora é treinada para identificar dados (ou seja, os dados falsos) da geradora.

A aplicação disso é tipicamente em análise de imagens, onde uma rede geradora é usada para criar imagens, e a rede discriminadora identifica se a imagem fornecida como entrada foi criada pela rede geradora ou se é real. O treinamento entre elas ocorre alimentando a discriminadora com imagens da geradora alternadas com imagens reais e, como em qualquer rede, a retropropagação ajusta adequadamente os pesos da discriminadora. Por outro lado, a geradora, em configurações não condicionais ou típicas, recebe dados de entrada aleatórios e deve criar imagens que sejam o mais realistas possível, independentemente disso..

Em um cenário de GAN condicional (cGAN), fazemos uma leve modificação alimentando a rede gerativa com um certo tipo de dado como entrada, e não dados aleatórios. Isso é aplicável ou útil em situações onde o tipo de dado que alimentamos a discriminadora é pareado ou está em 2 partes, e o objetivo da rede discriminadora é determinar se o dado de entrada pareado é válido ou foi inventado.

A maioria das aplicações de GAN e cGAN parece estar em reconhecimento ou processamento de imagens, mas neste artigo exploramos como um modelo muito simples para previsão de séries temporais financeiras pode ser construído ao redor delas. Como mencionado no título, adotaremos um cGAN em vez de um GAN. Para ilustrar a diferença entre os dois, podemos considerar os dois diagramas abaixo:

fonte



fonte

Ambas as imagens, cujos links de origem estão compartilhados acima, apontam para configurações onde a saída da rede geradora é alimentada na rede discriminadora para teste ou verificação. As GANs são adversariais no sentido de que a geradora é treinada para se aperfeiçoar em enganar a discriminadora, enquanto a discriminadora é treinada para se tornar boa em identificar a saída da geradora de dados reais ou dados que não vêm da rede geradora. A principal diferença entre essas duas configurações, entretanto, é que com as GANs, a rede geradora recebe dados de entrada aleatórios e os usa para gerar dados que a discriminadora não consegue distinguir de dados reais. Para nossos propósitos na previsão de séries temporais financeiras, isso tende a ter aplicabilidade e uso limitados.

No entanto, com a cGAN, o que é referido como ruído no diagrama é essencialmente dados independentes ou dados para os quais a geradora está tentando gerar um rótulo (no nosso caso de adaptação). Não estamos inserindo rótulos na rede geradora, como é representado no diagrama acima. No entanto, a rede discriminadora recebe um pareamento de dados de ruído (ou dados independentes) e seu respectivo rótulo, e então tenta determinar se esse pareamento vem de dados reais ou se o rótulo atribuído aos dados independentes foi gerado por uma geradora.

Quais são os benefícios do cGAN para a previsão de séries temporais financeiras? Bem, como dizem, "a prova está no pudim", e é por isso que faremos alguns testes ao final deste artigo, como de praxe. No entanto, no reconhecimento de imagens, as GANs certamente têm peso, embora não se saiam tão bem quanto as CNNs ou as ViTs, devido ao alto custo computacional. Elas são relatadas como melhores, no entanto, em síntese e aumento de imagens.


Configurando o Ambiente

Para construir nosso modelo cGAN, usaremos a rede perceptron multicamadas apresentada neste artigo como a classe base. Esta classe base representa todas as ‘ferramentas e bibliotecas’ de que precisaríamos para colocar nosso cGAN em funcionamento, já que tanto a rede geradora quanto a rede discriminadora serão tratadas como instâncias de uma perceptron multicamadas. Essa classe base possui apenas duas funções principais: o método feed forward ‘Forward()’ e a função de retropropagação ‘Backward()’. Há, claro, o construtor da classe, que recebe as configurações da rede, e alguns métodos de manutenção que permitem que os pesos treinados sejam salvos como um arquivo, além de algumas outras funções que definem os alvos de treinamento e leem os resultados do feed forward.

Apesar de usarmos nossa típica classe base de perceptron multicamadas para esse cGAN, precisamos fazer algumas mudanças específicas de GAN na forma como a rede geradora realiza sua retropropagação ou aprendizado. A perda do gerador pode ser calculada a partir da seguinte fórmula:

−log(D(G(zy)y))

onde:

  • D() é a função de saída da discriminadora
  • G() é a função de saída da geradora
  • z são os dados independentes
  • y são os dados dependentes, ou rótulo, ou dados de previsão

Assim, esse valor de perda do gerador, que tipicamente estaria em forma de vetor, dependendo do tamanho da saída, atuaria como um peso para o valor de erro de cada passagem forward ao iniciar a retropropagação. Fazemos essas mudanças em nossa classe de rede da seguinte forma:

//+------------------------------------------------------------------+
//| Backward pass through the neural network to update weights       |
//| and biases using gradient descent                                |
//+------------------------------------------------------------------+
void Cgan::Backward(vector<double> &DiscriminatorOutput, double LearningRate = 0.05)
{  if(target.Size() != output.Size())
   {  printf(__FUNCSIG__ + " Target & output size should match. ");
      return;
   }
   if(ArraySize(weights) != hidden_layers + 1)
   {  printf(__FUNCSIG__ + " weights matrix array size should be: " + IntegerToString(hidden_layers + 1));
      return;
   }

        ...

// Update output layer weights and biases
   vector _output_error = -1.0*MathLog(DiscriminatorOutput)*(target - output);//solo modification for GAN
   Back(_output_error, LearningRate);
}

Nossa função ‘Backward ()’ se torna sobrecarregada, pois uma variante geralmente usa a saída do discriminador como entrada, e ambas essas funções sobrecarregadas chamam uma função ‘Back()’, que essencialmente contém a maior parte do código que tínhamos na função antiga de retropropagação e foi introduzida aqui para reduzir a duplicidade. O que essa ponderação faz é garantir que, ao treinar o gerador, não estamos apenas melhorando nossa capacidade de prever a próxima variação no preço de fechamento que precisamos prever, mas também estamos ‘melhorando’ em enganar o discriminador, fazendo-o acreditar que os dados gerados são reais. Enquanto isso, o discriminador está treinando 'na direção oposta', tentando se tornar eficiente em distinguir os dados gerados dos dados reais.

Em contraste, se fôssemos implementar essa configuração em um aplicativo de terceiros, definir uma rede semelhante com o tensorflow em python exigiria que cada camada fosse adicionada com um comando ou linha de código separado. Essa opção em python, é claro, oferece mais personalizações que nossa classe básica não fornece, mas como uma ferramenta de prototipagem para testar cGANs no ambiente MQL5, não deveria ser a escolha preferida. Sem mencionar que usar python e qualquer uma de suas bibliotecas de redes neurais requer a implementação de ‘adaptadores’, como ONNX ou uma implementação personalizada equivalente que permita exportar os resultados do treinamento de volta para o MQL5. Esses adaptadores têm suas vantagens quando o modelo é projetado para ser treinado uma vez durante o desenvolvimento e depois implantado ou treinado periodicamente, mas enquanto estiver offline (não implantado).

Em cenários onde o treinamento de uma rede neural precisa ser ao vivo ou feito durante a implantação, então os muitos ‘adaptadores’ de e para o python podem se tornar complicados, embora ainda seja possível.


Projetando a Classe de Sinais Personalizada

Classes de sinal, como vimos ao longo da série, apresentam funções padrão para inicialização, validação e avaliação das condições de mercado. Além disso, um número ilimitado de funções pode ser adicionado para personalizar o sinal, seja com um indicador personalizado ou uma combinação de indicadores típicos já disponíveis na biblioteca MQL5. Como estamos construindo uma cGAN baseada em perceptrons de múltiplas camadas, começaremos com um número adicional de funções semelhantes às que adotamos no artigo anterior, que também utilizava nossa classe base de perceptron.

Essas serão as funções ‘GetOutput()’, ‘Setoutput()’ e ‘Norm()’. Seu papel aqui será muito semelhante ao que tivemos no artigo anterior, onde a função de obtenção será a função âncora responsável por determinar as condições de mercado, enquanto a função de configuração estará, como antes, disponível para gravar os pesos da rede após cada passagem de treinamento, enquanto a função de normalização desempenha o papel crucial de normalizar nossos dados de entrada antes de alimentar a rede.

Há 3 novas funções adicionais que introduzimos para a classe de sinal personalizada da cGAN e que são relacionadas à separação do processamento da rede geradora da rede discriminadora.

A arquitetura da rede geradora é escolhida arbitrariamente como tendo 7 camadas, sendo uma camada de entrada, 5 camadas ocultas e uma camada de saída. A determinação adequada disso pode ser feita com uma pesquisa de arquitetura neural, que abordamos no artigo mencionado, mas para nossos propósitos aqui, essas suposições serão suficientes para demonstrar uma cGAN. Essas configurações de rede são definidas em um array de ‘configurações’ que usamos para inicializar uma instância de uma classe de rede, que chamamos de ‘GEN’.

Nossa rede geradora terá variações anteriores no preço de fechamento como entradas e uma única previsão de variação também no preço de fechamento como saída. Isso não é muito diferente da implementação que fizemos quando abordamos a pesquisa de arquitetura neural no artigo já mencionado. A previsão da saída será a variação no preço de fechamento que segue as 4 variações que servem como entradas.

Portanto, o emparelhamento dessas 4 variações anteriores com o valor de previsão é o que constituirá os dados de entrada para a rede discriminadora, que analisaremos mais tarde. A classe base da rede que estamos usando realiza sua ativação por meio da função softplus, que é fixa. Como o código-fonte completo é fornecido, os leitores podem personalizar isso facilmente para o que for adequado para sua configuração. Os únicos parâmetros ajustáveis que nossa classe de sinal aceitará serão, portanto, a taxa de aprendizado, o número de épocas de treinamento e o tamanho do conjunto de dados de treinamento. Esses são atribuídos com os nomes ‘m_learning_rate’, ‘m_epochs’ e ‘m_train_set’, respectivamente. Dentro da função de obtenção de saída, é assim que carregamos os dados de entrada da rede, alimentamos o forward e treinamos a rede em cada nova barra:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CSignalCGAN::GetOutput(double &GenOut, bool &DisOut)
{  GenOut = 0.0;
   DisOut = false;
   for(int i = m_epochs; i >= 0; i--)
   {  for(int ii = m_train_set; ii >= 0; ii--)
      {  vector _in, _out;
         vector _in_new, _out_new, _in_old, _out_old;
         _in_new.CopyRates(m_symbol.Name(), m_period, 8, ii + 1, __GEN_INPUTS);
         _in_old.CopyRates(m_symbol.Name(), m_period, 8, ii + 1 + 1, __GEN_INPUTS);
         _in = Norm(_in_new, _in_old);
         GEN.Set(_in);
         GEN.Forward();
         if(ii > 0)// train
         {  _out_new.CopyRates(m_symbol.Name(), m_period, 8, ii, __GEN_OUTPUTS);
            _out_old.CopyRates(m_symbol.Name(), m_period, 8, ii + 1, __GEN_OUTPUTS);
            _out = Norm(_out_new, _out_old);
            
                ...

         }
         else if(ii == 0 && i == 0)
         { 
                ...
         }
      }
   }
}

Nosso GAN é condicional porque as entradas para a rede geradora não são aleatórias e as da rede discriminadora são duplas, capturando a entrada para o gerador e sua saída. O papel da rede discriminadora, portanto, é determinar se seus dados de entrada vêm de uma sequência real de séries temporais com 5 variações consecutivas no preço de fechamento, OU se é um emparelhamento dos dados de entrada da rede geradora com sua saída. Em outras palavras, ela determina se os dados de entrada são ‘reais’ ou ‘falsos’, respectivamente.

Isso implica que a saída da rede discriminadora é muito simples, booleana. Ou os dados de entrada vêm inteiramente dos mercados (verdadeiro) ou foram parcialmente gerados pelo gerador (falso). Nós representamos isso com 1 e 0, respectivamente, e a partir de testes após o treinamento, o valor retornado é um número de ponto flutuante entre 0,0 e 1,0. Então, para treinar nossa rede discriminadora, vamos alternadamente alimentá-la com variações reais de preço de fechamento como 5 pontos de dados (sendo 5 variações consecutivas) e outras 5 variações de preço de fechamento, das quais apenas 4 são reais e a 5a é a previsão da rede geradora. O treinamento com dados reais é tratado em parte pela função ‘R’, cujo código está abaixo:

//+------------------------------------------------------------------+
//| Process Real Data in Discriminator                               |
//+------------------------------------------------------------------+
void CSignalCGAN::R(vector &IN, vector &OUT)
{  vector _out_r, _out_real, _in_real;
   _out_r.Copy(OUT);
   _in_real.Copy(IN);
   Sum(_in_real, _out_r);
   DIS.Set(_in_real);
   DIS.Forward();
   _out_real.Resize(__DIS_OUTPUTS);
   _out_real.Fill(1.0);
   DIS.Get(_out_real);
   DIS.Backward(m_learning_rate);
}

e o treinamento com dados falsos é tratado pela função ‘F’, cujo código também é fornecido aqui:

//+------------------------------------------------------------------+
//| Process Fake Data in Discriminator                               |
//+------------------------------------------------------------------+
void CSignalCGAN::F(vector &IN, vector &OUT)
{  vector _out_f, _out_fake, _in_fake;
   _out_f.Copy(OUT);
   _in_fake.Copy(IN);
   Sum(_in_fake, _out_f);
   DIS.Set(_in_fake);
   DIS.Forward();
   _out_fake.Resize(__DIS_OUTPUTS);
   _out_fake.Fill(0.0);
   DIS.Get(_out_fake);
   DIS.Backward(m_learning_rate);
}

Essas duas funções são chamadas dentro da função de obtenção de saída, conforme mostrado abaixo:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CSignalCGAN::GetOutput(double &GenOut, bool &DisOut)
{  GenOut = 0.0;
   DisOut = false;
   for(int i = m_epochs; i >= 0; i--)
   {  for(int ii = m_train_set; ii >= 0; ii--)
      {  
                ...

         if(ii > 0)// train
         {  _out_new.CopyRates(m_symbol.Name(), m_period, 8, ii, __GEN_OUTPUTS);
            _out_old.CopyRates(m_symbol.Name(), m_period, 8, ii + 1, __GEN_OUTPUTS);
            _out = Norm(_out_new, _out_old);
            //
            int _dis_sort = MathRand()%2;
            if(_dis_sort == 0)
            {  F(_in, GEN.output);
               GEN.Get(_out);
               GEN.Backward(DIS.output, m_learning_rate);
               R(_in, _out);
            }
            else if(_dis_sort == 1)
            {  R(_in, _out);
               GEN.Get(_out);
               GEN.Backward(DIS.output, m_learning_rate);
               F(_in, GEN.output);
            }
         }
         else if(ii == 0 && i == 0)
         {  GenOut = GEN.output[0];
            DisOut = (((DIS.output[0] >= 0.5 && GenOut >= 0.5)||(DIS.output[0] < 0.5 && GenOut < 0.5)) ? true : false);
         }
      }
   }
}

Usamos a função ‘Sum’ para emparelhar 4 variações de preço de fechamento com a próxima variação de preço de fechamento, no caso de estarmos interessados em obter dados reais, ou a previsão do gerador, se estivermos interessados em obter dados ‘falsos’. Assim, após o treinamento subsequente, o gerador, como seria de esperar de qualquer perceptron, torna-se melhor em fazer previsões que podemos então usar para avaliar as condições de mercado. Mas o que fazemos com os esforços de treinamento do discriminador então?

Bem, primeiro, como mencionado acima, o treinamento também ajuda a aprimorar os pesos da rede geradora, já que usamos o peso da perda do gerador para ajustar o valor da perda utilizado na retropropagação da rede geradora. Em segundo lugar, após o treinamento da rede e em sua implantação, o discriminador ainda pode ser usado para verificar as previsões do gerador. Se ele não conseguir identificar que são do gerador, isso serve como confirmação de que nossa rede geradora está realizando bem seu trabalho.


Integração da cGAN com a Classe de Sinais MQL5

Para que isso funcione dentro de uma classe de sinal, precisaríamos codificar as funções de condição de compra e venda para chamar a função ‘GetOutput()’, que retorna 2 coisas. A variação estimada no preço de fechamento, que é capturada pela variável do tipo double ‘GenOut’, e a variável booleana ‘DisOut’, que indica se essa variação prevista no fechamento foi capaz de enganar a rede discriminadora. O leitor é livre para experimentar configurações em que apenas a saída do gerador seja usada para determinar as condições de mercado, como é normalmente o caso na geração de imagens, que é o uso mais comum de GANs. No entanto, ter a rede discriminadora verificando essas previsões atua como um passo extra de segurança na avaliação das condições, e é por isso que está incluído aqui.

Os valores de entrada da rede são todos normalizados para estarem na faixa de -1,0 a +1,0 e, da mesma forma, esperamos que as saídas estejam em uma faixa semelhante. Isso significa que nosso gerador está nos fornecendo uma previsão de variação percentual no preço de fechamento. Como são percentuais, podemos multiplicá-los por 100 para obter um valor que não exceda 100. O sinal desse valor, seja positivo ou negativo, indicaria se devemos comprar ou vender, respectivamente. Assim, para processar as condições e obter uma saída inteira na faixa de 0 a 100, como esperado pelas funções de condição de compra e venda, teríamos nossas funções de condição de compra indicadas abaixo:

//+------------------------------------------------------------------+
//| "Voting" that price will grow.                                   |
//+------------------------------------------------------------------+
int CSignalCGAN::LongCondition(void)
{  int result = 0;
   double _gen_out = 0.0;
   bool _dis_out = false;
   GetOutput(_gen_out, _dis_out);
   _gen_out *= 100.0;
   if(_dis_out && _gen_out > 50.0)
   {  result = int(_gen_out);
   }
   //printf(__FUNCSIG__ + " generator output is: %.5f, which is backed by discriminator as: %s", _gen_out, string(_dis_out));
   return(result);
}

A condição de venda é muito semelhante, com a exceção de que, claro, a porcentagem prevista precisa ser negativa para que o resultado receba um valor diferente de zero e que esse valor seja o valor absoluto da porcentagem prevista após a multiplicação por 100.


Testes e Validação

Se realizarmos testes com um Expert Advisor montado via o assistente MQL5 (as diretrizes para isso estão aqui e aqui), obteremos os seguintes resultados em uma das execuções:

r2

c2

Ao processar os sinais para obter essas execuções, randomizamos a ordem em que a rede discriminadora é treinada, ou seja, às vezes treinamos com dados reais primeiro e, em outras vezes, treinamos com dados falsos primeiro. Isso, com base nos testes, eliminou o viés da rede discriminadora de não se inclinar sempre para um lado, como ficar apenas comprada ou apenas vendida, pois ter uma ordem de teste estrita tende a enviesar a rede discriminadora. E como mencionado acima, o uso típico de GANs não requer a verificação da rede discriminadora; é apenas algo que decidimos adotar aqui em um esforço para sermos mais diligentes.

Devido a essa adição de verificação, nossos resultados não são facilmente repetíveis em cada execução de teste, especialmente porque nossa arquitetura de rede é muito pequena, considerando que usamos apenas 5 camadas ocultas e cada uma com um tamanho de apenas 5. Se alguém desejar obter resultados mais consistentes com essa verificação da rede discriminadora, então deverá treinar redes com 5 a 25 camadas ocultas, onde o tamanho de cada camada provavelmente não deve ser inferior a 100. O tamanho da camada, mais do que o número de camadas, tende a ser um fator chave para gerar resultados de rede mais confiáveis.

Se, no entanto, deixarmos de usar a verificação da rede discriminadora, nossa rede deve fornecer resultados de teste menos voláteis, embora com alguns problemas de desempenho. Um compromisso poderia ser adicionar um parâmetro de entrada extra que permita ao usuário escolher se a verificação da rede discriminadora está ativada ou não.


Conclusão

Em resumo, vimos como redes adversariais generativas condicionais (cGANs) podem ser desenvolvidas em uma classe de sinal personalizada que pode ser montada em um Expert Advisor graças ao assistente MQL5. A cGAN é uma modificação da GAN em que utiliza dados não aleatórios ao treinar a rede geradora, e os dados de entrada para a rede discriminadora, em nosso caso, foram um emparelhamento dos dados de entrada da rede geradora com os dados de saída do gerador, como já demonstrado. Redes neurais em treinamento aprendem pesos, e, portanto, é uma boa prática ter e usar provisões para registrar esses pesos de uma rede sempre que um processo de treinamento é concluído. Não consideramos ou exploramos esses benefícios nos testes realizados para este artigo.

Além disso, não exploramos os potenciais benefícios e desvantagens de empregar diferentes regimes de treinamento de rede. Por exemplo, neste artigo treinamos a rede em cada nova barra, o que tem como objetivo permitir maior flexibilidade e adaptabilidade da rede e do sistema de negociação às condições de mercado potencialmente mutáveis. No entanto, um argumento contrário, e provavelmente credível, contra isso poderia ser que, ao treinar sempre uma rede em cada nova barra, ela está sendo treinada desnecessariamente em ruído; em contraste, um regime onde o treinamento seria feito uma vez a cada 6 meses, de forma que apenas os aspectos ‘de longo prazo’ cruciais dos mercados fossem usados como pontos de dados de treinamento, poderia oferecer resultados mais sustentáveis.

Além disso, a sempre importante questão pré-requisito de pesquisa de arquitetura neural foi ‘omitida’ porque não era nosso tema principal, no entanto, como qualquer pessoa familiar com redes neurais sabe, este é um aspecto muito sensível ao desempenho das redes neurais, que requer alguma diligência antes que qualquer rede seja treinada e eventualmente implantada. Portanto, esses três aspectos-chave não foram devidamente abordados, embora sejam importantes, o que significa que o leitor é incentivado a usá-los como ponto de partida para desenvolver e aprimorar esta classe cGAN antes que ela possa ser considerada digna de negociação. Como sempre, isso não é um conselho de investimento, e espera-se que o leitor faça a devida diligência em qualquer e todas as ideias compartilhadas neste artigo antes de usá-las. Boa caçada.

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

Arquivos anexados |
cgan.mq5 (6.54 KB)
SignalWZ_22_1.mqh (11.53 KB)
Cgan.mqh (12.51 KB)
Desenvolvendo um EA multimoeda (Parte 11): Início da automação do processo de otimização Desenvolvendo um EA multimoeda (Parte 11): Início da automação do processo de otimização
Para obter um bom EA, precisamos selecionar muitos bons conjuntos de parâmetros para as instâncias das estratégias de trading. Isso pode ser feito manualmente, executando a otimização em diferentes símbolos e, em seguida, escolhendo os melhores resultados. Mas é melhor delegar esse trabalho para um programa e se concentrar em atividades mais produtivas.
Do básico ao intermediário: Array (I) Do básico ao intermediário: Array (I)
Este é um artigo de transição entre o que foi visto até agora, para uma nova etapa de estudos. O pré-requisito para conseguir entender este artigo é ter compreendido os artigos anteriores. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como sendo, uma aplicação cuja finalidade não venha a ser o aprendizado e estudo dos conceitos mostrados.
Redes neurais de maneira fácil (Parte 91): previsão na área de frequência (FreDF) Redes neurais de maneira fácil (Parte 91): previsão na área de frequência (FreDF)
Continuamos a explorar a análise e previsão de séries temporais na área de frequência. E nesta matéria, apresentaremos um novo método de previsão nessa área, que pode ser adicionado a muitos dos algoritmos que já estudamos anteriormente.
Técnicas do MQL5 Wizard que você deve conhecer (Parte 21): Testando com Dados do Calendário Econômico Técnicas do MQL5 Wizard que você deve conhecer (Parte 21): Testando com Dados do Calendário Econômico
Os dados do Calendário Econômico não estão disponíveis para testes com Expert Advisors no Strategy Tester, por padrão. Vamos explorar como bancos de dados poderiam ajudar a contornar essa limitação. Portanto, neste artigo, exploramos como os bancos de dados SQLite podem ser usados para arquivar notícias do Calendário Econômico, de modo que os Expert Advisors montados pelo Wizard possam usá-los para gerar sinais de trade.