English Русский Español Deutsch 日本語
preview
Teoria das Categorias em MQL5 (Parte 21): Transformações naturais com LDA

Teoria das Categorias em MQL5 (Parte 21): Transformações naturais com LDA

MetaTrader 5Sistemas de negociação | 5 março 2024, 16:18
102 0
Stephen Njuki
Stephen Njuki

Introdução

Até agora, examinamos muitos aspectos da teoria das categorias, que podem ser aplicados fora do ambiente acadêmico, incluindo: conjuntos e morfismos, comutação, registros de ontologias, produtos, coprodutos, limites, colimites, monoides, grupos, grafos, ordens, funtores, e agora transformações naturais. A teoria das categorias é muito mais ampla do que tudo que cobrimos, e esses tópicos foram escolhidos por sua facilidade de aplicação e uso em outras disciplinas matemáticas. Se você estiver interessado em uma exploração mais detalhada do tema, posso recomendar este livro. Ele é considerado por muitos como a Bíblia da teoria das categorias.

Continuando a explorar as transformações naturais, examinaremos outras possíveis aplicações na previsão de séries temporais. As transformações naturais frequentemente podem ser encontradas em conjuntos de dados mapeados, e é exatamente por aí que quero começar o artigo.

Então, vejamos o desafio. Uma startup cria um banco de dados para seus clientes para acompanhar suas compras ao longo do tempo, e inicialmente ele consiste em, digamos, três colunas: chave primária, nome do produto e valor pago. Com o tempo, a empresa nota repetições frequentes na coluna do produto, o que significa que um produto específico é comprado com frequência. Decide-se começar a registrar mais informações relacionadas aos produtos, para distinguir melhor as preferências dos clientes e, possivelmente, explorar a possibilidade de desenvolver novos produtos. Para isso, a coluna do produto é dividida em 3 colunas, isto é: versão, modo de assinatura e nome do build. Ou a empresa pode precisar de mais detalhes nas informações de pagamento e decidir dividir a coluna de pagamento em três partes, por exemplo, para modo de pagamento, moeda (ou padrão linguístico) e valor do pagamento. Novamente, tal divisão não será exaustiva, pois em uma fase futura podem ser necessários mais dados, dependendo das compras dos clientes e de suas preferências.

Cada uma dessas novas colunas criadas será mapeada para a coluna anterior do produto. E se, por exemplo, estabelecêssemos algumas correlações chave entre a coluna anterior de um produto e, digamos, a coluna do valor pago ou qualquer outra coluna na tabela no banco de dados, então a recuperação dessas correlações na nova estrutura da tabela seria um processo tedioso. Claro, a empresa tem muitas maneiras de resolver esse problema, mas as transformações naturais oferecem uma boa solução.

Primeiro, vamos tentar enquadrar o problema em termos de duas categorias. Na categoria do domínio, teremos uma lista de tabelas da empresa em seu banco de dados, e na categoria do codomínio, teremos duas versões das tabelas com informações sobre os clientes. Para simplificar, se tomarmos a lista como um único objeto no domínio e cada uma de nossas tabelas como objetos separados no codomínio, então dois funtores da lista, um para cada tabela, de fato implicam uma transformação natural entre as duas tabelas. Assim, um funtor será mapeado para as tabelas antigas, que em nosso caso representam uma tabela simples com três colunas, enquanto o segundo funtor será mapeado para as mudanças na estrutura da tabela. Se essa é a versão 1, então o segundo funtor é mapeado para uma tabela de 5 colunas.

O uso de transformações naturais não significa apenas que podemos quantificar as diferenças entre essas duas tabelas usando uma função algorítmica de mapeamento, como: uma equação linear, uma equação quadrática, um perceptron multicamadas, uma floresta de distribuição aleatória ou análise discriminante linear. Isso significa que podemos usar esses pesos para restaurar as correlações anteriores com a tabela antiga e desenvolver novas para as colunas criadas.


Visão geral

Como lembramos, as transformações naturais são a diferença entre os objetos alvo de dois funtores. O uso dessa diferença foi enfatizado nesta série de artigos por meio do quadrado de naturalidade, uma composição comutativa de objetos na categoria do codomínio dos funtores. A descrição pode ser encontrada no artigo 18. Também exploramos a indução do quadrado de naturalidade no artigo 19.

O conceito de dados de séries temporais não é estranho à maioria dos traders, já que muitos de nós não apenas estamos familiarizados com gráficos de preços e indicadores embutidos do terminal MetaTrader, mas também desenvolvemos nossos próprios indicadores e EAs. Algumas elaborações sobre tópicos como este e aquele serão familiares a muitos. No entanto, se olharmos para o gráfico de preços, digamos, de um par de moedas forex, veremos que ele é discreto, pois temos um preço específico em cada intervalo. Isso apesar de, quando os mercados estão abertos, o que acontece na maior parte da semana, esse preço sempre estará mudando, ou seja, na realidade, trata-se de uma série temporal contínua. Por isso, a representação de séries temporais discretas é usada para facilitar a análise.

Bem, a capacidade de realizar análise se baseia na existência de preços "consensuais" para um determinado título financeiro em um momento específico. Ao tentar fazer previsões, torna-se importante estudar diferentes séries em diferentes períodos de tempo. É por isso que visualizar e comparar dados com atraso no tempo pode ser visto como um método mais construtivo para obter resultados precisos.

Então, como já disse, neste artigo vamos cobrir dois conjuntos de dados. Eles serão similares, só que um é mais complexo que o outro. Vamos realizar a transformação natural deles com atraso, para ajudar na elaboração de previsões. Nossos dois conjuntos de dados, que podem ser representados como tabelas, serão muito simples. O primeiro conterá os valores da média móvel e o segundo, os preços que compõem essa média.


Descrição dos conjuntos de dados

Bem, uma tabela simples terá apenas duas colunas: Coluna de rótulo de tempo e coluna de média móvel. O número de linhas nessas e nas tabelas compostas será definido pelo parâmetro de entrada m_data. Esta tabela simples será deslocada no tempo em relação à tabela composta com base no tamanho do período da média móvel utilizado. Assim, se o período da nossa média móvel for 5, então os valores nessa tabela serão as barras de 5 períodos da tabela composta.

A tabela composta, que é defasada no tempo, também terá uma coluna de rótulo de tempo e outras colunas, cada uma indicando o preço em diferentes momentos. O número dessas colunas adicionais além do rótulo de tempo será determinado pelo período da média móvel, por isso, novamente, se nossa média móvel abranger mais de 5 a barra de preços, então nesta tabela haverá uma coluna com um único rótulo de tempo e 5 colunas de preços.

Esses dois conjuntos de dados, que têm uma transformação natural, podem ser definidos de várias maneiras, que já foram listadas na introdução. Neste artigo, vamos utilizar o método de análise discriminante linear (Linear Discriminant Analysis, LDA). Eu já demonstrei sua aplicação com o assistente MQL5 e a biblioteca Alglib neste artigo, mas, penso que será útil repetir os pontos mais importantes.

Uma definição mais específica pode ser encontrada aqui, mas em um sentido amplo, o LDA é um classificador. Se olharmos para qualquer conjunto típico de dados de treinamento, ele sempre tem variáveis independentes (valores que se presume influenciem o resultado final) e variável(is) classificadora(s), que servem como "resultado final". Com o LDA, temos a capacidade de classificar esse resultado final em n classes, onde n é um número natural. Este algoritmo, desenvolvido por Sir Ronald Fisher, gera um vetor de pesos que ajuda a determinar o centroide principal de cada classificador, e também pode estimar a posição de um centroide desconhecido (novo ou desconhecido ponto de dados). Com essa informação, é possível simplesmente comparar a posição do centroide desconhecido com a posição do conhecido para descobrir a qual ele está mais próximo e, consequentemente, qual é sua classificação. Para ilustração, o "vetor de pesos" pode ser considerado como a equação de uma linha que separa pontos que têm apenas dois classificadores. Se houver três classificadores, então essa equação é de um plano. Se for um, então são coordenadas em uma reta numérica. Em qualquer cenário, você pode fazer distinções entre um conjunto de dados de treinamento, atribuindo a cada ponto de dados um conjunto de coordenadas.


Transformação natural para previsão de séries temporais

Assim, como mencionamos nos artigos anteriores 18 e 19, ao considerar os quadrados de naturalidade, geralmente tudo o que importa são os objetos do codomínio, e, portanto, ao mostrar como dois conjuntos diferentes de pontos de dados de preços e médias móveis podem ter uma transformação natural para este artigo, não dissemos nada sobre a categoria de origem do funtor ou sobre o(s) objeto(s). Eles não são críticos. No entanto, se temos vários objetos na categoria de origem, pode-se esperar que haja vários casos proporcionais tanto do conjunto de dados simples quanto do composto. Eu já havia apontado isso quando estávamos considerando a indução do quadrado da naturalidade no artigo 19.

Bem, do ponto de vista do mapeamento de diagramas, nossa transformação natural não deve ser demasiadamente complicada. As colunas de rótulo de tempo dos conjuntos de dados serão relacionadas, e todas as colunas de preços no conjunto de dados composto serão mapeadas para a coluna de média móvel no conjunto de dados simples. Isso geralmente é apresentado no código MQL5 por motivos ilustrativos e lógicos e, para esse propósito, temos instâncias dos conjuntos de dados simples e compostos, declarados como "m_o_s" e "m_o_c", respectivamente. Essas instâncias de classes agora são chamadas de "objetos", e não "domínios", uma vez que o termo "domínio" é uma propriedade de um dos objetos relacionados ao morfismo e não necessariamente um substantivo por si só. O que eu chamava de domínio na maioria dos nossos artigos anteriores é mais frequentemente chamado de objeto. Eu me abstive de usar a palavra "objeto" para evitar confusão com as classes internas do MQL5. Esta abordagem é menos propensa a erros de lógica que poderiam ser facilmente cometidos se agíssemos de maneira mais direta e copiássemos os dados de preços diretamente para a nossa função de mapeamento. Esse é o tipo de erro que não se manifesta nem mesmo durante o teste do Expert Advisor, pois ele compila normalmente.

Assim, a transformação natural é implementada usando LDA, e o mapeamento crítico será feito a partir dos pontos de preço atrasados para o valor futuro da média móvel de preço. A coluna de rótulo de tempo não será usada, mas é mencionada para que o leitor possa entender como os dados estão estruturados. Como mencionado anteriormente, o atraso temporal será igual à duração do período da média móvel. Portanto, durante o treinamento, usaremos preços que estão n barras atrás do índice inicial, onde n é o comprimento do período da nossa média móvel. Isso também significa que, na previsão, obviamente usaremos os preços mais recentes, mas nossa previsão será calculada para n barras no futuro, não para a média móvel atual.


Aplicação da transformação natural para previsão

O código para isso será principalmente tratado por duas funções dentro da classe de sinais, porque, como já mencionado, não codificamos todas as estruturas de classe que normalmente descrevem categorias que são morfismos ou funtores, como fazíamos no passado; em vez disso, usaremos apenas classes de objetos e elementos para mapear o que está na categoria do codomínio. O resultado final deve ser idêntico ao abordado aqui, uma vez que é mais eficiente em termos de uso de recursos computacionais e, consequentemente, mais fácil de ser testado no testador de estratégias. No entanto, isso requer maior cuidado para evitar erros na lógica das categorias e seus funtores, uma vez que os erros cometidos podem não se manifestar durante a compilação ou a execução dos testes de estratégia. A função de atualização ficará assim:

//+------------------------------------------------------------------+
//| Refresh function to update objects.                              |
//+------------------------------------------------------------------+
void CSignalCT::Refresh(int DataPoints=1)
   {
      m_time.Refresh(-1);
      m_close.Refresh(-1);
      
      for(int v=0;v<DataPoints;v++)
      {
         m_e_s.Let(); m_e_s.Cardinality(2);
         m_e_c.Let(); m_e_c.Cardinality(m_independent+1);
         
         m_e_s.Set(0,TimeToString(m_time.GetData(v)));
         m_e_c.Set(0,TimeToString(m_time.GetData(v)));
      
         double _s_unit=0.0;
         //set independent variables..
         for(int vv=0;vv<m_independent;vv++)
         {
            double _c_unit=m_close.GetData(StartIndex()+v+vv+m_independent);
            
            m_e_c.Set(vv+1,DoubleToString(_c_unit));
         }
         
         m_o_c.Set(0,m_e_c);
         
         //get dependent variable, the MA..
         for(int vv=v;vv<v+m_independent;vv++)
         {
            _s_unit+=m_close.GetData(StartIndex()+vv);
         }
         
         _s_unit/=m_independent;
         
         m_e_s.Set(1,DoubleToString(_s_unit));
         
         m_o_s.Set(0,m_e_s);
      }
   }


Esta função de atualização será chamada pela função get direction, cuja lista será a seguinte:

//+------------------------------------------------------------------+
//| Get Direction function from implied naturality square.           |
//+------------------------------------------------------------------+
double CSignalCT::GetDirection()
   {
      double _da=0.0;
      
      int _info=0;
      CMatrixDouble _w,_xy,_z;
      _xy.Resize(m_data,m_independent+1);
      
      double _point=0.00001;
      if(StringFind(m_symbol.Name(),"JPY")>=0){ _point=0.001; }
      
      for(int v=0;v<m_data;v++)
      {
         Refresh(v+1);
         
         ...
         
         //training classification
         _xy.Set(v,m_independent,(fabs(_ma-_lag_ma)<=m_regularizer*_point?1:(_ma-_lag_ma>0.0?2:0)));
      }
      
      m_lda.FisherLDAN(_xy,m_data,m_independent,__CLASSES,_info,_w);
      
      if(_info>0)
      {
         double _centroids[__CLASSES],_unknown_centroid=0.0; ArrayInitialize(_centroids,0.0);
         
         _z.Resize(1,m_independent+1);
         
         m_o_c.Get(0,m_e_c);
         
         for(int vv=0;vv<m_independent;vv++)
         {
            string _c="";
            m_e_c.Get(vv+1,_c);
            
            double _c_value=StringToDouble(_c);
            _z.Set(0,vv,_c_value);
         }
         
         for(int v=0;v<m_data;v++)
         {
            for(int vv=0;vv<m_independent;vv++)
            {
               _centroids[int(_xy[v][m_independent])]+= (_w[0][vv]*_xy[v][vv]);
            }
         }
         
         // best vector is the first 
         for(int vv=0;vv<m_independent;vv++){ _unknown_centroid+= (_w[0][vv]*_z[0][vv]); }
         
         
... 
      }
      else
      {
         
... 
      }
      
      return(_da);
   }

Nossa previsão assume uma mudança na média móvel futura dos preços, portanto, uma mudança negativa indicará uma tendência de baixa, uma mudança positiva indicará uma tendência de alta, e a ausência de mudanças indicará uma lateralização. Para a avaliação quantitativa da ausência de mudanças, temos o parâmetro regulador m_regulizer, que estabelece a quantidade mínima prevista para que a mudança seja considerada de baixa ou de alta. Este é um número inteiro que definimos, multiplicando-o pelo tamanho do ponto do símbolo.

Bem, este é um breve resumo do nosso código, que implementa a transformação. A declaração de variáveis importantes é realizada, como sempre, no manifesto da classe. Além das declarações típicas para a classe de sinais, adicionamos às nossas declarações instâncias de elementos simples e compostos, bem como uma instância da nossa classe de discriminante linear.

Então, em cada nova a barra, atualizamos os valores desses elementos e, consequentemente, seus objetos usando a função de atualização. Isso inclui a atribuição de variáveis independentes, isto é, simplesmente atribuindo uma série de preços, cuja quantidade é igual ao comprimento do período da média móvel de entrada. Assim, passamos esses preços ao elemento composto e aos objetos. Usamos 3 classificadores para nosso LDA: 2 para o mercado de alta, 1 para o mercado lateral e 0 para o mercado de baixa. Dessa forma, cada ponto dos dados de treinamento é classificado com base na diferença entre a média móvel atual (baseada no índice no conjunto de treinamento) e a média móvel atrasada. Ambas as médias são calculadas com o mesmo comprimento, que já foi mencionado como parte dos dados de entrada, e o atraso também é igual a esse comprimento.

A atribuição dos classificadores é parte do treinamento dentro da implementação da análise discriminante linear em Alglib. Talvez valha a pena mencionar também nosso modo de regularização, que simplesmente determina quais sinais ignorar, ou seja, o que considerar como ruído branco. Para responder a essa questão, pegamos qualquer diferença entre as duas médias móveis que seja menor que o parâmetro de entrada m_regularizer, um número inteiro que multiplicamos pelo tamanho do ponto do símbolo para torná-lo comparável à variação das médias móveis dos preços.

Com isso, executamos a função de Fisher para obter a matriz de coeficientes (ou pesos) w, que, como já mencionado, forma a equação para o plano decisório entre nossos classificadores.

A matriz Z, que representa os preços atuais para a próxima previsão, é preenchida com o último array de preços e, em seguida, lhe é atribuído o produto escalar com a matriz w da função de Fisher para obter o valor do seu centroide, definido pela matriz w. Este valor é o nosso centroide desconhecido.

Da mesma forma, os valores dos centroides dos nossos três classificadores também são preenchidos pelo produto escalar dessa matriz com a matriz de variáveis independentes.

Após obter todos os três valores dos centroides dos classificadores e o valor do centroide do nosso desconhecido, surge agora a questão de comparar este desconhecido com os três classificadores e determinar a qual deles nosso desconhecido está mais próximo.


Uso pratico

Como exemplo, realizaremos um teste com o par GBPUSD desde o início deste ano até o primeiro de junho. O relatório é apresentado abaixo:

r1


Avançando até agosto, obtemos resultados negativos, que são apresentados no relatório a seguir:

r2


A precisão da previsão, baseada no relatório de previsão, parece estar em questão, o que pode estar relacionado a uma otimização incompleta (realizada apenas para as primeiras três gerações) ou a uma janela de teste muito pequena, já que consideramos apenas três anos, e sistemas confiáveis requerem períodos mais longos. O código-fonte está anexado.


Considerações finais

No dia a dia, é bastante comum que nossas tabelas de banco de dados ou formatos de armazenamento de dados cresçam não apenas em tamanho, mas também em complexidade. Isso foi demonstrado aqui ao considerar um conjunto de dados que se torna mais complexo com a adição de colunas de dados. Tentamos usar isso a nosso favor, examinando conjuntos de dados distribuídos ao longo do tempo para fazer previsões. Embora os resultados da otimização tenham mostrado potencial considerando apenas três gerações, os resultados subsequentes ficaram longe do ideal. Isso pode ser corrigido combinando esta classe de sinais com outra ou realizando testes mais extensivos em períodos mais longos.

Quanto ao tema do artigo, as transformações naturais são bastante adequadas para lidar com diferentes estruturas de dados não só em casos em que o conjunto de dados evolui devido às necessidades de negócios ou análise, mas talvez em casos em que é necessário comparação e os tamanhos (número de colunas) dos dois conjuntos de dados não são iguais. Essa funcionalidade certamente será útil em várias disciplinas.


Referências

As referências são, como sempre, principalmente de artigos da Wikipedia.

O arquivo de sinais anexado deve ser compilado usando o Assistente MQL5. Este artigo pode ajudar aqueles que não estão familiarizados com as classes do Assistente.


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

Arquivos anexados |
ct_21.mqh (31.39 KB)
SignalCT_21_r2.mqh (11.52 KB)
Desenvolvimento de um Cliente MQTT para o MetaTrader 5: metodologia TDD (Parte 3) Desenvolvimento de um Cliente MQTT para o MetaTrader 5: metodologia TDD (Parte 3)
Este artigo faz parte de uma série que descreve as etapas do desenvolvimento de um cliente MQL5 nativo para o protocolo MQTT. Nesta parte, descrevemos em detalhes como aplicar o princípio do desenvolvimento orientado por testes para implementar a troca de pacotes CONNECT/CONNACK. Ao final desta etapa, nosso cliente DEVE ser capaz de agir apropriadamente ao trabalhar com todos os possíveis resultados do servidor ao tentar se conectar.
Algoritmos de otimização populacionais: salto de sapo embaralhado Algoritmos de otimização populacionais: salto de sapo embaralhado
O artigo apresenta uma descrição detalhada do algoritmo salto de sapo embaralhado (Shuffled Frog Leaping Algorithm, SFL) e suas capacidades na solução de problemas de otimização. O algoritmo SFL é inspirado no comportamento dos sapos em seu ambiente natural e oferece uma nova abordagem para a otimização de funções. O algoritmo SFL é uma ferramenta eficaz e flexível, capaz de lidar com diversos tipos de dados e alcançar soluções ótimas.
Avaliando o desempenho futuro com intervalos de confiança Avaliando o desempenho futuro com intervalos de confiança
Neste artigo, vamos explorar o uso do bootstrapping como um meio de avaliar a eficácia futura de uma estratégia automatizada.
Teoria das Categorias em MQL5 (Parte 20): autoatenção e transformador Teoria das Categorias em MQL5 (Parte 20): autoatenção e transformador
Vamos nos afastar um pouco de nossos tópicos mais comuns e analisar uma parte do algoritmo do ChatGPT. Ele possui algumas semelhanças ou conceitos emprestados das transformações naturais? Vamos tentar responder a essas e outras perguntas usando nosso código no formato de classe de sinal.