English Русский 中文 Español Deutsch 日本語
preview
Redes neurais de maneira fácil (Parte 19): Regras de associação usando MQL5

Redes neurais de maneira fácil (Parte 19): Regras de associação usando MQL5

MetaTrader 5Sistemas de negociação | 24 outubro 2022, 10:29
288 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Conteúdo


Introdução

No artigo anterior, começamos a nos familiarizar com algoritmos para encontrar regras de associação, que estão relacionadas a métodos de aprendizado não supervisionado. Nós consideramos 2 algoritmos para resolver este tipo de problema: Apriori e FP-Growth. O gargalo do algoritmo Apriori é as inúmeras chamadas ao banco de dados para determinar os suportes de candidatos a padrões frequentes. O método FP-Growth resolve isso construindo uma árvore na qual todo o banco de dados é compactado. E todo o trabalho subsequente é realizado com a árvore FP, sem acessar o banco de dados. Isso permite aumentar a velocidade de resolução do problema. Já que a árvore FP cabe na memória RAM. E o acesso a ela é muito mais rápido do que uma busca completa no banco de dados.


1. Variante do uso do método na negociação

Antes de seguir para a construção de um algoritmo para buscar regras de associação usando MQL5, vamos pensar em como podemos usá-las em nossa maneira de negociar.

Algoritmos de busca de regras de associação foram criados para buscar dependências estáveis entre caraterísticas binárias no banco de dados. Portanto, com a ajuda de tais algoritmos, podemos encontrar algumas relações estáveis entre várias caraterísticas. Estas podem ser distintos padrões consistindo em vários indicadores e/ou instrumentos. Ao mesmo tempo, o algoritmo não se importa se cada caraterística individual representa diferentes indicadores ou se são os valores de um indicador em diferentes intervalos de tempo. O algoritmo avalia cada caraterística como independente. Assim, por que não tentamos cruzar esse algoritmo com os progressos em métodos de aprendizado supervisionado. Vamos pegar e adicionar algumas caraterísticas de destino à amostra de treinamento de dados históricos. E pediremos ao algoritmo que nos encontre regras de associação, que ao serem aplicadas darão como resultado nossos valores-alvo. 

Temos um algoritmo e uma ideia de como usá-lo para resolver nossos problemas práticos. Vejamos como implementá-lo usando MQL5. E em seguida vamos testar nossa ideia na prática.


2. Implementação do algoritmo FP-Growth

Antes de começar a implementar o algoritmo FP-Growth discutido no artigo anterior, lembremos que sua construção é baseada em uma árvore de decisão. A biblioteca padrão MQL5 fornece a classe CTree para construir uma árvore binária. Infelizmente, a árvore binária não é uma opção totalmente conveniente para nós, pois no processo de construção de uma árvore FP, o número de ramificações do nó pode ser superior a 2 (o fornecido pela implementação binária). Portanto, antes de construir o algoritmo propriamente dito, criaremos a classe CMyTreeNode para criar um nó de árvore com vários ramos.

2.1. Implementação da classe de nó de árvore

Criaremos esta classe como sucessora da classe padrão de array dinâmico de objetos em MQL5 CArrayObj. A escolha desta classe como classe pai se deve à presença de funcionalidade para criação e manutenção de um array dinâmico de objetos, que serão nossos nós de ramificação.

Além disso, para implementar a funcionalidade necessária dentro da estrutura do algoritmo que está sendo construído, adicionamos 3 variáveis à nova classe:

  • m_cParent é um ponteiro para o objeto do nó pai anterior. Para a raiz da árvore, o ponteiro permanece vazio;
  • m_iIndex é o índice da caraterística no banco de dados inicial será usado para identificar as caraterísticas;
  • m_dSupport é uma variável para gravar o suporte à caraterística.

class CMyTreeNode : public CArrayObj
  {
protected:
   CMyTreeNode      *m_cParent;
   ulong             m_iIndex;
   double            m_dSupport;

public:
                     CMyTreeNode();
                    ~CMyTreeNode();
  }

No construtor da classe, definimos os valores iniciais das variáveis e limpamos o array dinâmico. Deixamos o destruidor de classe vazio.

CMyTreeNode::CMyTreeNode() :  m_iIndex(ULONG_MAX),
                              m_dSupport(0)
  {
   Clear();
  }

Para trabalhar com variáveis de classe ocultas, criaremos vários métodos que usaremos no processo de construção do algoritmo FP-Growth. Conheceremos a finalidade dos métodos à medida que os utilizarmos.

class CMyTreeNode : public CArrayObj
  {
   ........
public:
   ........
   //--- methods of access to protected data
   CMyTreeNode*      Parent(void)           const { return(m_cParent); }
   void              Parent(CMyTreeNode *node)  {  m_cParent = node; }
   void              IncreaseSupport(double support)  { m_dSupport += support; }
   double            GetSupport(void)       {  return m_dSupport;  }
   void              SetSupport(double support)       { m_dSupport = support;  }
   ulong             ID(void)             {  return m_iIndex;  }
   void              ID(ulong ID)         {  m_iIndex = ID; }
  };

Vamos criar o método GetConfidence para calcular o nível de confiança. Aqui primeiro verificamos o ponteiro para o nó anterior e se for válido dividimos o suporte do nó atual pelo suporte do nó pai.

Observem que o algoritmo de construção da árvore FP é organizado de tal forma que o suporte de qualquer nó não pode ser maior que o suporte do nó pai. Portanto, o resultado das operações deste método será sempre positivo e não maior que “1”.

Também não realizamos uma verificação de divisão por zero, pois os nós na árvore são adicionados a partir de transações existentes. Portanto, se um nó estiver na árvore é porque sua caraterística apareceu no banco de dados pelo menos uma vez e tem suporte mínimo.

double CMyTreeNode::GetConfidence(void)
  {
   CMyTreeNode *parent = Parent();
   if(!parent)
      return 1;
//---
   double result = m_dSupport / parent.GetSupport();
   return result;
  }

Também adicionaremos um método para criar um novo nó de ramificação, AddNode. Nos parâmetros do método, passaremos tanto o identificador da caraterística no banco de dados inicial da amostra de treinamento como o suporte para o nó que está sendo criado. Nosso método retornará um ponteiro para o objeto criado.

No corpo do método, criamos uma nova instância do nó de nossa árvore e verificamos imediatamente o resultado da operação. Se ocorrer um erro, retornamos ponteiro de objeto inválido.

Em seguida, especificamos o identificador do nó criado e passamos um ponteiro para o objeto atual como o objeto pai.

Vamos adicionar um novo objeto ao array dinâmico do nó atual e verificar o resultado da operação. Em caso de erro ao adicionar um objeto ao array, excluímos o objeto criado e saímos do método, retornando um ponteiro inválido.

Ao final do método, salvamos o suporte especificado nos parâmetros em um novo objeto e saímos do método.

CMyTreeNode *CMyTreeNode::AddNode(const ulong ID, double support = 0)
  {
   CMyTreeNode *node = new CMyTreeNode();
   if(!node)
      return node;
   node.ID(ID);
   if(!Add(node))
     {
      delete node;
      return node;
     }
   node.Parent(GetPointer(this));
//---
   if(support > 0)
      node.SetSupport(support);
   return node;
  }

Uma vez criado um novo objeto, devemos ser capazes de excluí-lo. O método para excluir um objeto por seu número de série em um array dinâmico já existe na classe pai. Para expandir a funcionalidade, criaremos um método para excluir um nó pelo identificador de caraterística DeleteNode.

Nos parâmetros do método, passaremos o identificador da caraterística a ser removida e retornaremos o resultado lógico da operação.

No corpo do método, criamos um loop para procurar um nó com um determinado identificador no array dinâmico do nó atual. Este loop irá iterar sobre elementos no intervalo de 0 ao valor da variável m_data_total. Essa variável contém o número de elementos ativos do array dinâmico e é controlada pela classe pai.

No corpo do método, removemos o próximo elemento do array dinâmico e verificamos a validade do ponteiro. Todo elemento com um ponteiro inválido é imediatamente excluído chamando o método Delete da classe pai, para tal deve ser usado o número ordinal do elemento a ser excluído.

Observem que o método Delete retorna o resultado booleano da operação. E após a remoção bem-sucedida de um elemento de um array dinâmico, decrementamos o contador de iteração do loop e passamos para o próximo array. Apenas decrementamos o contador de iteração do loop e não alteramos o valor da variável m_data_total, porque seu valor já foi alterado no método Delete da classe pai.

Em caso de erro ao remover um elemento inválido de um array dinâmico, simplesmente passamos para o próximo elemento do array. Não finalizamos o método com um resultado false, pois a tarefa do método não é limpar o array dinâmico deixando-o sem objetos inválidos. Este é apenas um recurso auxiliar. A principal tarefa do método é remover um elemento específico. Portanto, continuamos a execução do método até que o elemento que precisamos seja encontrado.

Quando o elemento requerido é encontrado no array dinâmico, chamamos o já mencionado método Delete da classe pai. E desta vez saímos do método, retornando o resultado da exclusão do objeto desejado.

Se, como resultado de uma enumeração completa de todos os elementos do array dinâmico, não encontramos o elemento necessário, saímos do método com o resultado false.

bool CMyTreeNode::DeleteNode(const ulong ID)
  {
   for(int i = 0; i < m_data_total; i++)
     {
      CMyTreeNode *temp = m_data[i];
      if(!temp)
        {
         if(!Delete(i))
            continue;
         return DeleteNode(ID);
        }
      if(temp.ID() != ID)
         continue;
      return Delete(i);
     }
//---
   return false;
  }

Continuando a conversa sobre os métodos da nova classe de nó de árvore CMyTreeNode, também gostaria de me debruçar sobre o método Mining. Este método é responsável por encontrar caminhos em nossa árvore FP para a caraterística analisada. Antes de proceder à descrição do algoritmo do método, deve-se dizer que ele é construído levando em consideração as particularidades inerentes ao seu uso posterior em um ambiente de negociação. Por isso, eu me desviei um pouco do algoritmo básico.

Em primeiro lugar, não definiremos regras de associação para todas as caraterísticas, mas apenas para nossos valores-alvo. Portanto, no processo de construção de árvores de regras, é muito provável que encontremos uma situação em que a caraterística desejada não seja uma folha da árvore, mas um nó contendo elementos consecutivos ao longo do caminho. E aqui devemos entender que não podemos ignorar nós subsequentes, uma vez que eles podem aumentar a probabilidade de um resultado alvo. Desse modo, devemos levá-los em consideração ao selecionar os caminhos antes de chegar à caraterística analisada.

Este é outro ponto a ter em atenção ao construir este método. De acordo com o algoritmo, primeiro precisamos encontrar todos os caminhos até a caraterística analisada na árvore FP. E em seguida devemos calcular o suporte para cada caraterística nos caminhos selecionados. Decidi realizar 2 dessas subtarefas dentro de um método.

Deve ser dito desde já que, para construir uma árvore FP, planejamos usar apenas instâncias da classe CMyTreeNode. Portanto, para realizar uma busca em profundidade, usaremos uma chamada recursiva para o método em questão.

E agora vamos ver a implementação dessas tarefas no método Mining de nossa classe. Nos parâmetros do método, passaremos ponteiros para um vetor para registro de suportes de elementos, uma matriz para registro de caminhos, um identificador da caraterística que está sendo analisada e um nível mínimo de confiança. Nosso método retornará o valor booleano da operação.

No corpo do método, primeiro verificamos se o nó analisado é a característica desejada. Para fazer isso, vamos comparar o identificador do nosso nó e o identificador do nó desejado recebido nos parâmetros. Se os identificadores forem iguais, verificamos o grau de confiança do nó na ramificação atual, grau esse que determinamos usando o método GetConfidence discutido acima. O grau de confiança não deve ser inferior ao mínimo. Caso contrário, saímos do método com um resultado true.

bool CMyTreeNode::Mining(vector &supports, matrix &paths, const ulong ID, double min_conf)
  {
   if(ID == m_iIndex)
      if(GetConfidence() < min_conf)
         return true;

O próximo bloco gera o avanço da busca na profundidade da árvore. Aqui, primeiro armazenamos o valor de suporte do nó atual em uma variável local. E em seguida criamos um loop para percorrer todas as ramificações desde o nó atual até as profundezas da árvore. Ao fazer isso, chamaremos recursivamente esse método para todas as ramificações.

Aqui é necessário entender algo: com o método recursivo passamos o identificador desejado apenas até encontrarmos o nó correspondente. Depois disso, passaremos a constante ULONG_MAX para as profundezas da árvore em vez do identificador desejado. Isso se deve ao fato de que, devido às peculiaridades da construção de uma árvore FP, antes de encontrar o caminho para o elemento desejado, o grau de confiança no padrão provavelmente será inferior a 100%. À medida que se avança no caminho, a probabilidade de ter a característica desejada será de 100%. Caso contrário, teríamos construído um caminho diferente, ignorando o nó desejado.

Obviamente, tal situação é excluída com o uso completo do algoritmo do autor. Afinal, ao definir regras para todas as caraterísticas, quando começarmos a trabalhar em qualquer uma delas em nossa árvore FP, a árvore em questão será uma folha sem nós subsequentes, uma vez que todas as caraterísticas com menos suporte já terão sido trabalhadas e retiradas da árvore.

Assim, para qualquer desvio em relação ao algoritmo, devemos avaliar o impacto das alterações feitas em todo o processo e fazer os ajustes apropriados no algoritmo. Nesse caso, teremos que adicionar à lista todos os caminhos que incluem a caraterística desejada. E isso envolve caminhos tanto desde a caraterística desejada até a raiz da árvore como todas as variantes de caminhos desde um nó subsequente até a raiz da árvore, variantes essas que passam pela caraterística desejada. E para isso, devemos indicar aos nós subsequentes que a caraterística desejada já foi encontrada entre eles e a raiz da árvore. Tal sinalizador de sinal será uma mudança no identificador da característica desejada para a constante ULONG_MAX.

Após o resultado positivo do uso do método recursivo para o nó subsequente, o valor de suporte deve ser subtraído por nós da variável local criada antes do loop com o suporte do nó atual. E se o identificador do nó subsequente for igual ao desejado, excluímos o nó usado.

   double support = m_dSupport;
   for(int i = 0; i < m_data_total; i++)
     {
      CMyTreeNode *temp = m_data[i];
      if(!temp)
        {
         if(Delete(i))
            i--;
         continue;
        }
      if(!temp.Mining(supports, paths, (ID == m_iIndex ? ULONG_MAX : ID), min_conf))
         return false;
      support -= temp.GetSupport();
      if(temp.ID() == ID)
         if(Delete(i))
            i--;
     }

Você pode ver, que no bloco anterior, apenas chamamos o mesmo método recursivamente para nós subsequentes, mas não salvamos os caminhos encontrados. Iremos realizar esta funcionalidade no próximo bloco do método. Esse bloco será executado para nós com a caraterística desejada e para nós subsequentes. Para fazer isso, realizamos uma verificação do identificador do nó atual e do recebido nos parâmetros. Além disso, o suporte do nó atual, remanescente após a execução dos métodos recursivos, deve ser maior que "0". E o nó atual não pode ser a raiz. Ou seja, deve ter pelo menos um nó predecessor. Afinal, devemos colocar algo na premissa da regra.

Depois de passar com sucesso os controles descritos acima, aumentamos o tamanho da matriz de caminho em 1 linha e preenchemos a linha adicionada com valores zero.

Em seguida, realizamos um loop de incremento desde o nó atual até a raiz da árvore. Para os nós atuais e todos os anteriores, colocamos o suporte restante do nó atual na linha do nosso caminho. E também adicionamos o mesmo valor no vetor de suporte acumulativo para os elementos correspondentes.

Após a conclusão do ciclo de iteração sobre os elementos pai, saímos do método com um resultado positivo.

   if(ID == m_iIndex || ID == ULONG_MAX)
      if(support > 0 && !!m_cParent)
        {
         CMyTreeNode *parent = m_cParent;
         ulong row = paths.Rows();
         if(!paths.Resize(row + 1, paths.Cols()))
            return false;
         if(!paths.Row(vector::Zeros(paths.Cols()), row))
            return false;
         supports[m_iIndex] += support;
         while(!!parent)
           {
            if(parent.ID() != ULONG_MAX)
              {
               supports[parent.ID()] += support;
               paths[row, parent.ID()] = support;
              }
            parent = parent.Parent();
           }
        }
//---
   return true;
  }

Talvez valha a pena explicar como o método funciona com um pequeno exemplo. Afinal, a construção do método vai um pouco além do escopo do algoritmo FP-Growth descrito anteriormente. Suponha que, no banco de dados inicial, tenhamos repetido a transação "AB" 2 vezes, "ABC" 1 vez, "ABCD" 3 vezes e "ABCDE" 1 vez. Como resultado, o caminho "A7-B7-C5-D4-E1" foi formado na árvore FP. Ao analisar a caraterística "C", precisamos restaurar da árvore todos os caminhos que contêm a caraterística fornecida.

Começamos chamando um método para o elemento raiz "A" e instruindo-o a encontrar a caraterística "C". Nele, chamamos recursivamente o método para o nó "B". E assim sucessivamente até a folha "E". Como a folha "E" não possui nós subsequentes, começamos a processar o 2º bloco do nosso método e anotamos os caminhos. Aqui primeiro armazenamos o caminho "ABCDE" e escrevemos todos os nós de suporte 1. Isso significa que havia 1 caminho desse tipo no banco de dados de origem, e saímos do método, passando o controle para um nível superior.

No nível do nó "D", armazenaremos o caminho "ABCD". Neste caso, do suporte do nó "D" subtraímos o suporte da folha "E" (4-1=3) e colocamos o valor resultante como os suportes de todos os elementos deste caminho. Como se pode ver, isso corresponde aos dados iniciais sobre a existência de 3 transações semelhantes na amostra de treinamento com tal conjunto de caraterísticas. Só que não começamos a repetir a transação 3 vezes, mas a expressamos através do suporte de sinais.

Da mesma forma, armazenaremos o caminho "ABC" com suporte igual a 1. Não salvamos o caminho "AB", pois não contém a caraterística analisada.

Você pode encontrar o código completo de todos os métodos da classe considerada no anexo no arquivo "MyTreeNode.mqh".

2.2. Implementação da classe de busca de regras de associação

E continuamos a construir um algoritmo para procurar regras de associação FP-Growth. E vamos descrever a funcionalidade principal em outra classe CAssocRules. A estrutura da classe gerada é mostrada abaixo. Como se pode ver, a maioria dos métodos está escondida "sob o capô". Mas antes de mais nada vejamos.

class CAssocRules : public CObject
  {
protected:
   CMyTreeNode       m_cRoot;
   CMyTreeNode       m_cBuyRules;
   CMyTreeNode       m_cSellRules;
   vector            m_vMin;
   vector            m_vStep;
   int               m_iSections;
   matrix            m_mPositions;
   matrix            m_BuyPositions;
   matrix            m_SellPositions;
   //---
   bool              NewPath(CMyTreeNode *root, matrix &path);
   CMyTreeNode      *CheckPath(CMyTreeNode *root, vector &path);
   //---
   bool              PrepareData(matrix &data, matrix &bin_data, 
                                 vector &buy, vector &sell,
                                 const int sections = 10, const double min_sup = 0.03);
   matrix            CreatePath(vector &bin_data, matrix &positions);
   matrix            CreatePositions(vector &support, const double min_sup = 0.03);
   bool              GrowsTree(CMyTreeNode *root, matrix &bin_data, matrix &positions);
   double            Probability(CMyTreeNode *root, vector &data, matrix &positions);

public:
                     CAssocRules();
                    ~CAssocRules();
   //---
   bool              CreateRules(matrix &data, vector &buy, 
                                 vector &sell, int sections = 10, 
                                 double min_freq = 0.03,
                                 double min_prob = 0.3);
   bool              Probability(vector &data, double &buy, double &sell);
   //--- methods for working with files
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual bool      Save(const string file_name);
   virtual bool      Load(const string file_name);
  };

Entre as variáveis de classe, podemos notar 3 instâncias dos nós de árvore descritos acima:

  • m_cRoot - usaremos para escrever nossa árvore FP;
  • m_cBuyRules - usaremos para registrar as regras de compra;
  • m_cSellRules  - usaremos para registrar as regras de venda.

E as matrizes m_mPositions, m_BuyPositions e m_SellPositions conterão caraterísticas e seus suportes, classificados em ordem decrescente.

Deixe-me lembrá-lo que ao testar todos os modelos anteriores, verificamos a possibilidade de identificar um fractal antes da formação da 3ª vela do padrão. Portanto, definirei 2 árvores de regras de busca para identificar fractais de compra e de venda. Se os requisitos de sua tarefa envolverem a definição de regras para um número maior de caraterísticas-alvo, você precisará criar mais árvores de regras privadas.

Por exemplo, você pode ter vários níveis-alvo para compra e vários para venda. Infelizmente, os algoritmos de busca de regras de associação operam apenas com tabelas binárias. Portanto, para cada nível-alvo, você terá que criar uma caraterística separada e encontrar regras de associação para ela. Para excluir inúmeras variáveis privadas, você pode usar arrays dinâmicos.

Deixei o construtor e o destruidor do método vazios, pois nessa classe não usei ponteiros dinâmicos para objetos de construção de árvores. 

Já mencionamos acima que algoritmos para busca de regras de associação funcionam apenas com matrizes binárias. E os dados sobre o estado da situação do mercado poderão dificilmente ser relacionados com elas. Portanto, antes de usá-los, é necessário um pré-tratamento.

Para simplificar o uso posterior da classe, não impus pré-processamento de dados ao usuário, mas o elaborei no método PrepareData. Em parâmetros, o método recebe ponteiros para 2 matrizes, 2 vetores e 2 constantes. Uma matriz contém os dados iniciais e a outra serve para registrar os dados binários processados. Os vetores contêm os valores alvo. No nosso caso, eles são representados por dados binários e não requerem pré-processamento. O que não pode ser dito sobre os dados iniciais.

Para converter as unidades escalares dos dados iniciais em binárias, pegaremos o intervalo de valores de cada caraterística e o dividiremos em vários intervalos. O número de tais intervalos é definido pelo parâmetro sections. Armazenaremos os valores mínimos e o tamanho do incremento para cada caraterística nos vetores correspondentes m_vMin e m_vStep. Vamos precisar deles para a posterior conversão dos dados iniciais em binários durante a operação comercial do modelo.

Aqui vamos preparar nossa matriz binária, dando-lhe o tamanho necessário e preenchendo-a com valores zero. Podemos especificar imediatamente identificadores para caraterísticas alvo, que adicionaremos posteriormente como as últimas colunas da matriz.

bool CAssocRules::PrepareData(matrix &data,
                              matrix &bin_data,
                              vector &buy, 
                              vector &sell,
                              const int sections = 10,
                              const double min_sup = 0.03)
  {
//---
   m_iSections = sections;
   m_vMin = data.Min(0);
   vector max = data.Max(0);
   vector delt = max - m_vMin;
   m_vStep = delt / sections + 1e-8;
   m_cBuyRules.ID(data.Cols() * m_iSections);
   m_cSellRules.ID(m_cBuyRules.ID() + 1);
   bin_data = matrix::Zeros(data.Rows(), m_cSellRules.ID() + 1);

Em seguida, fazemos um loop para percorrer todas as linhas da matriz de dados iniciais. No corpo do loop, subtraímos o vetor de valores mínimos de cada linha e dividimos a diferença resultante pelo tamanho do incremento. Para excluir "ataques", limitaremos os valores superior e inferior dos elementos do vetor. Como resultado dessas operações, a parte inteira do número em cada elemento do vetor nos informará a qual intervalo de valores devemos atribuir o elemento correspondente dos dados iniciais. E cada intervalo para nossa matriz binária será uma caraterística separada.

Criamos um loop aninhado e preenchemos a linha correspondente da matriz binária. Se a caraterística estiver ativa, alteramos seu valor para "1". As caraterísticas inativas permanecerão com valores nulos.

   for(ulong r = 0; r < data.Rows(); r++)
     {
      vector pos = (data.Row(r) - m_vMin) / m_vStep;
      if(!pos.Clip(0, m_iSections - 1))
         return false;
      for(ulong c = 0; c < pos.Size(); c++)
         bin_data[r, c * sections + (int)pos[c]] = 1;
     }
   if(!bin_data.Col(buy, m_cBuyRules.ID()) ||
      !bin_data.Col(sell, m_cSellRules.ID()))
      return false;

Após preencher a matriz binária, calcularemos imediatamente os suportes para cada caraterística e as classificaremos em ordem decrescente no método CreatePositions. Após a conclusão da classificação de caraterísticas, saímos do método com um resultado positivo.

   vector supp = bin_data.Sum(0) / bin_data.Rows();
   m_mPositions = CreatePositions(supp, min_sup);
//---
   return true;
  }

Já que tocamos no método de classificação de caraterísticas CreatePositions, proponho considerar seu algoritmo imediatamente. Nos parâmetros, o método recebe um vetor de suportes e um nível mínimo de suporte.

No corpo do método, faremos um pequeno trabalho preparatório. Na realidade os suportes obtidos são representados por um vetor, cujos valores de elementos contêm suportes. E os números de série dos elementos indicam as caraterísticas. Com uma simples ordenação dos elementos do vetor, perderemos a conexão com as características dos dados iniciais. Portanto, precisamos criar pares "identificador de caraterística - suporte". E vamos salvar os dados do par em uma matriz.

Para fazer isso, primeiro criamos uma matriz identidade com 2 colunas e o número de linhas igual ao número de características na amostra inicial. Em seguida, calcularemos as somas cumulativas dos elementos por colunas e reduziremos os valores da matriz resultante em "1". Assim, obtemos uma matriz em que as colunas são valores em ordem crescente a partir de "0" e correspondem ao índice da linha. Resta-nos substituir uma coluna pelo vetor de suporte recebido. E temos uma matriz pronta, e em cada linha dela haverá um identificador de caraterística e um valor de suporte correspondente a ele.

matrix CAssocRules::CreatePositions(vector &support, const double min_sup = 0.03)
  {
   matrix result = matrix::Ones(support.Size(), 2);
   result = result.CumSum(0) - 1;
   if(!result.Col(support, 1))
      return matrix::Zeros(0, 0);

Agora nos resta ordenar as linhas da matriz em ordem decrescente de suportes de caraterísticas. Para fazer isso, programamos um loop no qual elaboramos um algoritmo de ordenação por flutuação.

   bool change = false;
   do
     {
      change = false;
      ulong total = result.Rows() - 1;
      for(ulong i = 0; i < total; i++)
        {
         if(result[i, 1] >= result[i + 1, 1])
            continue;
         if(result.SwapRows(i, i + 1))
            change = true;
        }
     }
   while(change);

Após sair do loop, teremos uma matriz com características ordenadas. Resta-nos remover as caraterísticas que não atendem ao requisito mínimo de suporte. Para fazer isso, encontraremos a primeira caraterística abaixo do nível de suporte mínimo e "cortaremos" todas as caraterísticas abaixo desse nível.

   int i = 0;
   while(result[i, 1] >= min_sup)
      i++;
   if(!result.Resize(i, 2))
      return matrix::Zeros(0, 0);
//---
   return result;
  }

Após redimensionar com sucesso a matriz, saímos do método retornando a matriz resultante.

Antes de proceder ao estudo dos métodos públicos, vejamos mais alguns métodos nos quais executaremos algumas das funções repetitivas. Antes de mais nada, isso aborda a criação de um caminho a partir de dados binários para transferência para a árvore FP. Essa funcionalidade será executada no método CreatePath. Nos parâmetros do método, passaremos ponteiros para o vetor de dados binários e a matriz de características ordenadas. O que o método retornará será uma matriz de caminho que conterá os pares "identificador de caraterística - suporte" para adicionar à árvore FP.

Devemos prestar atenção imediatamente à diferença entre a matriz de caraterísticas classificada, que recebemos ao preparar os dados, e a matriz para adicionar o caminho à árvore FP. Ambas as matrizes conterão pares "identificador de caraterística - suporte". Apenas a primeira matriz contém todas as caraterísticas disponíveis nos dados iniciais e seu suporte na amostra de treinamento. A segunda matriz (caminhos) conterá apenas as características presentes na transação analisada e os suportes desta transação, que são indicados na matriz binária.

Sim, estamos lidando com uma matriz binária e o suporte a caraterísticas em cada transação deve ser o mesmo. Mas mais tarde usaremos o mesmo método para construir árvores de regras privadas. Acima demos um exemplo ao descrever o método CMyTreeNode::Mining. E lá, em vez de repetir um caminho, usamos os níveis de suporte várias vezes. É para a unificação das operações que utilizaremos 1 método em 2 subprocessos. Aqui a introdução do nível de suporte será muito útil para nós.

No início do método, armazenaremos em variáveis locais os tamanhos do vetor de dados iniciais e o número de caraterísticas analisadas, que será menor que o tamanho do vetor de dados inicial pelo número de caraterísticas aleatórias com suporte abaixo do mínimo .

Imediatamente, prepararemos uma matriz para registro dos resultados, que não pode ser maior que a matriz das características analisadas. E vamos introduzir uma variável indicando o tamanho do nosso caminho. Nesta fase, é igual a "0".

A seguir, programamos um loop no qual iteraremos todas as caraterísticas analisadas em ordem decrescente de seus suportes. No corpo do loop, extraímos o identificador da próxima caraterística verificada. E verificamos seu valor no vetor binário dos dados iniciais. Se a caraterística não estiver ativa, avançamos para a próxima caraterística.

Se a caraterística estiver ativa, adicionamos o identificador da caraterística e seu suporte de vetor binário dos dados iniciais à matriz de caminho. E então aumentamos a variável de tamanho do caminho.

Depois de sair do loop, reduzimos o tamanho da matriz de caminho para o número de elementos preenchidos e saímos do método.

matrix CAssocRules::CreatePath(vector &bin_data, matrix &positions)
  {
   ulong size = bin_data.Size();
//---
   ulong total = positions.Rows();
   int vect_pos = 0;
   matrix path = matrix::Zeros(2, total);
   for(ulong c = 0; c < total; c++)
     {
      ulong pos = (ulong)positions[c, 0];
      if(pos >= size)
         continue;
      if(bin_data[pos] == 0)
         continue;
      path[0, vect_pos] = (double)pos;
      path[1, vect_pos] = bin_data[pos];
      vect_pos++;
     }
   if(!path.Resize(2, vect_pos))
      return matrix::Zeros(0, 0);
//---
   return path;
  }

Outro método do qual precisaremos é o método NewPath para adicionar um caminho à nossa árvore FP. Nos parâmetros, o método recebe um ponteiro para o nó raiz da árvore e a matriz de caminhos criada acima. Tal método retornará o resultado lógico das operações. No corpo do método, primeiro verificamos o tamanho do caminho resultante. Deve ser maior que "0". Em seguida, aumentamos o suporte para o nó raiz e iniciamos um loop por todos os elementos do caminho.

No corpo do loop, verificamos a presença do próximo nó com o identificador necessário e, se necessário, criamos um novo nó. Em seguida, aumentamos o tamanho do suporte do nó. E passamos a encontrar o próximo nó no caminho.

Após a conclusão da iteração de todos os elementos do caminho, saímos do método. 

bool CAssocRules::NewPath(CMyTreeNode *root, matrix &path)
  {
   ulong total = path.Cols();
   if(total <= 0)
      return false;
   CMyTreeNode *parent = root;
   root.IncreaseSupport(path[1, 0]);
   for(ulong i = 0; i < total; i++)
     {
      CMyTreeNode *temp = parent.GetNext((ulong)path[0, i]);
      if(!temp)
        {
         temp = parent.AddNode((int)path[0, i], 0);
         if(!temp)
            return false;
        }
      temp.IncreaseSupport(path[1, i]);
      parent = temp;
     }
//---
   return true;
  }

Finalmente, procedemos à criação do método GrowsTree para o cultivo de uma árvore FP. Como parâmetros, o método recebe um ponteiro para o nó raiz da árvore, uma matriz binária de dados iniciais e uma matriz de características analisadas ordenadas. Dentro do método, organizamos um loop percorrendo todas as linhas dos dados iniciais.

No corpo do loop, extraímos a próxima transação da amostra de treinamento e criamos um caminho para adicionar à árvore usando o método CreatePath. Verificamos o tamanho do caminho recebido, deve ser maior que "0". E chamamos o método NewPath para adicionar o caminho criado à nossa árvore árvore FP. Ao mesmo tempo, não nos esquecemos de verificar o resultado das operações de inicialização do buffer.

Após uma iteração bem-sucedida de todas as transações dos dados iniciais, saímos do método com um resultado positivo.

bool CAssocRules::GrowsTree(CMyTreeNode * root, matrix & bin_data, matrix &positions)
  {
   ulong rows = bin_data.Rows();
   for(ulong r = 0; r < rows; r++)
     {
      matrix path = CreatePath(bin_data.Row(r), positions);
      ulong size = path.Cols();
      if(size <= 0)
         continue;
      if(!NewPath(root, path))
         return false;
     }
//---
   return true;
  }

E agora vamos combinar todos os métodos descritos acima no método público CreateRules. Nos parâmetros do método, passaremos uma matriz de dados iniciais escalares (não binários), vetores binários de valores alvo, o número de intervalos para conversão de valores escalares em binários, os níveis de suporte mínimo e o grau mínimo de confiança.

No corpo do método, primeiro verificamos os dados iniciais recebidos. Em primeiro lugar, é importante para nós que as dimensões dos vetores da matriz obtidos correspondam, e o número de intervalos deve ser maior que "0".

Depois de passar o bloco de verificações, converteremos os dados iniciais escalares em uma forma binária. Para fazer isso, usaremos os métodos PrepareData descritos acima.

bool CAssocRules::CreateRules(matrix &data,
                              vector &buy,
                              vector &sell,
                              int sections = 10,
                              double min_sup = 0.03,
                              double min_conf = 0.3)
  {
   if(data.Rows() <= 0 || data.Cols() <= 0 || sections <= 0 ||
      data.Rows() != buy.Size() || data.Rows() != sell.Size())
      return false;
//---
   matrix binary_data;
   if(!PrepareData(data, binary_data, buy, sell, sections))
      return false;

Além disso, para passar para o plano de unidades relativas, dividiremos os valores de nossa matriz binária pelo número de transações na amostra de treinamento e construiremos nossa árvore FP usando o método GrowsTree.

   double k = 1.0 / (double)(binary_data.Rows());
   if(!GrowsTree(GetPointer(m_cRoot), binary_data * k, m_mPositions))
      return false;

Depois de construir a árbore FP, podemos proceder à definição de regras. Aqui vamos primeiro preparar um vetor para escrever suportes e uma matriz para escrever caminhos. E a seguir chamamos o método Mining de nossa árvore FP para encontrar todos os caminhos com caraterística de compra.

   vector supports = vector::Zeros(binary_data.Cols());
   binary_data = matrix::Zeros(0, binary_data.Cols());
   if(!m_cRoot.Mining(supports, binary_data, m_cBuyRules.ID(),min_conf))
      return false;

Após extrair todos os caminhos com sucesso, redefiniremos o suporte para a caraterística de compra, removendo-o do processamento de todos os caminhos e classificando os suportes privados em ordem decrescente. Chamamos o método CreatePositions e escrevemos o resultado das operações na matriz m_BuyPositions. Se, depois de ordenar as caraterísticas, ainda tivermos a possibilidade de construir regras (há caraterísticas na matriz ordenada para distribuição à premissa da regra), então chamamos o método de crescimento da árvore e passamos os caminhos previamente selecionados para ele como dados iniciais.

Como resultado dessas operações, obteremos uma árvore de regras privada enraizada no nó m_cBuyRules.

   supports[m_cBuyRules.ID()] = 0;
   m_BuyPositions = CreatePositions(supports, min_sup);
   if(m_BuyPositions.Rows() > 0)
      if(!GrowsTree(GetPointer(m_cBuyRules), binary_data, m_BuyPositions))
         return false;

Da mesma forma, criamos uma árvore de regras para caraterísticas de venda.

   supports = vector::Zeros(binary_data.Cols());
   binary_data = matrix::Zeros(0, binary_data.Cols());
   if(!m_cRoot.Mining(supports, binary_data, m_cSellRules.ID(),min_conf))
      return false;
   supports[m_cSellRules.ID()] = 0;
   m_SellPositions = CreatePositions(supports, min_sup);
   if(m_SellPositions.Rows() > 0)
      if(!GrowsTree(GetPointer(m_cSellRules), binary_data, m_SellPositions))
         return false;
//---
   m_cRoot.Clear();
//---
   return true;
  }

Depois de selecionar todas as regras, limpamos o objeto da árvore FP inicial, liberando assim as caraterísticas do nosso computador. E saímos do método com um resultado positivo.

Para «operação industrial», foi criado o método de Probability. Como parâmetros, o método recebe um vetor escalar de dados iniciais e ponteiros para 2 variáveis do tipo double, que serão utilizadas para armazenar o grau de confiança em um determinado padrão. O algoritmo do método usa todos os métodos discutidos acima. E você pode ver no anexo.

O código completo de todas as classes e seus métodos é fornecido no anexo.


3. Teste

Para testar o trabalho da classe que construímos com base em dados reais, criamos o EA "assocrules.mq5". Devo dizer que o teste do Expert Advisor foi realizado em total conformidade com todos os parâmetros usados nos testes anteriores. Não posso dizer que o método determinou todos os fractais sem erros. Mas o Expert Advisor criado mostrou resultados interessantes, que são mostrados na captura de tela abaixo.

Resultados de teste da classe de regras de associação 


Considerações finais

Neste artigo, conhecemos outro tipo de tarefas resolvidas por métodos de aprendizado não supervisionado, em particular a busca por regras de associação. Criamos uma classe para criar regras de associação usando o algoritmo FP-Growth. Para testar o método, foi criado um Expert Advisor, que apresentou bons resultados. Como resultado, podemos concluir que tais algoritmos podem ser usados para resolver problemas práticos de negociação.

Referências

  1. Redes neurais de maneira fácil (Parte 14): agrupamento de dados
  2. Redes neurais de maneira fácil (Parte 15): agrupamento de dados via MQL5
  3. Redes neurais de maneira fácil (Parte 16): uso prático do agrupamento
  4. Redes neurais de maneira fácil (Parte 17): redução de dimensionalidade
  5. Redes neurais de maneira fácil (Parte 18): regras de associação

Programas utilizados no artigo

# Nome Tipo Descrição
1 assocrules.mq5 EA   EA para treinar e testar o modelo
2 AssocRules.mqh Biblioteca de classe
Biblioteca de classe para preparar o algoritmo FP-Growth
3 MyTreeNode.mqh Biblioteca de classe Biblioteca da classe de criação do nó de árvore 


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

Arquivos anexados |
MQL5.zip (7.78 KB)
Aprendendo a construindo um EA que opera de forma automática (Parte 03): Novas funções Aprendendo a construindo um EA que opera de forma automática (Parte 03): Novas funções
Aprenda como criar um EA que opera de forma automática, isto de forma simples e o mais seguro possível. No artigo anterior começamos o desenvolvimento do sistema de ordens, para ser utilizado no EA automático, no entanto, ali montamos apenas e somente uma das funções.
Aprendendo a construindo um EA que opera de forma automática (Parte 02): Iniciando a programação Aprendendo a construindo um EA que opera de forma automática (Parte 02): Iniciando a programação
Aprenda como criar um EA que opera de forma automática, isto de forma simples e o mais seguro possível. No artigo anterior apresentei as primeiras etapas das quais você precisa compreender, antes mesmo de iniciar a construção de um EA, que opere de forma automática, ali mostrei.
Como desenvolver um sistema de negociação baseado no indicador Índice de Força Como desenvolver um sistema de negociação baseado no indicador Índice de Força
Seja bem-vindo a este novo artigo em nossa série sobre como desenvolver um sistema de negociação com base no indicador técnico mais popular. Neste artigo, nós aprenderemos sobre um novo indicador técnico e como criar um sistema de negociação usando o indicador Índice de Força.
DoEasy. Controles (Parte 11): Objetos WinForms - grupos, objeto WinForms CheckedListBox DoEasy. Controles (Parte 11): Objetos WinForms - grupos, objeto WinForms CheckedListBox
Neste artigo, consideraremos como agrupar objetos WinForms e criar um objeto-lista de objetos CheckBox.