English Русский 中文 Español Deutsch 日本語
Rede neural: Expert Advisor auto-otimizável

Rede neural: Expert Advisor auto-otimizável

MetaTrader 5Sistemas de negociação | 17 outubro 2016, 10:23
6 185 1
Jose Miguel Soriano
Jose Miguel Soriano

Introdução

Uma vez que o trader tem selecionado a estratégia e a implementado no Expert Advisor, ele enfrenta dois problemas, que, se não forem resolvidos, desvalorizarão a estratégia.

  • Que valores especificar como parâmetros de entrada?
  • Quanto tempo esses valores serão confiáveis? Quando é necessário re-otimizar?
É evidente que ao contrário de dos parâmetros que você definir antecipadamente (o par de trabalho, período, etc) há outros que são redimensionáveis e que são, na verdade, o problema: período de cálculo dos indicadores, níveis de compra e venda, níveis de TP/SL e afins.

Será que é possível criar um EA que otimize, em intervalos especificados, os critérios para abertura e fechamento de posições?

O que acontecerá se, no EA, nós implementarmos uma de rede neural (NN) do tipo perceptron multi-camada (MCP) que seja o módulo para analisar o histórico e avaliar a estratégia? Será que é possível dar ao código um comando para uma otimização mensal (semanal, diária ou por hora) de rede neural com um processo subsequente? Desse modo, é possível criar um Expert Advisor que se auto-otimize.

Esta não é a primeira vez que o tema "MQL e rede neural" é tratado neste fórum. No entanto, a discussão muitas vezes se resume a usar as informações fornecidas pela rede neural externa (às vezes até manualmente), ou para casos onde a rede neural é otimizada pelo trader (num exercício de "treinamento não supervisionado”) usando o otimizador de MT5/MT4. Finalmente, acontece a substituição de parâmetros de entrada do EA pelos os parâmetros de entrada da rede, tal como neste artigo.

Neste artigo não vamos descrever um robô de negociação. Nós vamos desenvolver e implementar modularmente um layout de EA que, com ajuda de um perceptron multi-camada (MCP, implementado em mql5 usando a biblioteca ALGLIB), conheça o algoritmo expressado nos parágrafos anteriores. Em seguida, para a rede neural, definiremos dois problemas matemáticos cujos resultados sejam facilmente verificados usando outro algoritmo. Isto nos permitirá analisar as soluções e tarefas que estabelece um MPC, segundo sua estrutura interna, e buscar o critério para posteriormente implementar o Expert Advisor mudando apenas o módulo de entrada de dados. O layout criado por nós deve resolver a questão da outo-otimização.

Pressupõe-se que o leitor conhece a teoria geral de redes neurais: estrutura e formas de organização, número de camadas, número de neurônios em cada camada, ligações e peso, etc. Em qualquer caso, é possível encontrar informações relevantes em artigos sobre o assunto.


1. Algoritmo básico

  1. Criação de uma rede neural.
  2. Preparação dos dados de entrada (e dos respectivos de saída) usando a carga numa matriz de dados.
  3. Normalização dos dados num determinado intervalo (normalmente [0, 1] ou [-1, 1]).
  4. Treinamento e otimização da rede neural.
  5. Cálculo e uso da previsão da rede de acordo com a estratégia do Expert Advisor.
  6. Auto-otimização: retorno ao ponto 2 e reiteração do processo com sua inclusão na função OnTimer().

Segundo o algoritmo descrito, o robô irá realizar periodicamente a otimização de acordo com o intervalo de tempo definido pelo usuário. O usuário não precisará se preocupar por esta faceta. O passo 5 não está incluído no processo repetitivo. O Expert Advisor terá sempre à sua disposição os valores preditivos, mesmo quando ele está em processo de otimização. Vamos ver como isso acontece.

 

2. Biblioteca ALGLIB

Esta biblioteca foi publicada e discutida no artigo do Sergey Bochkanov e no site do projeto ALGLIB, http://www.alglib.net/, onde tem sido descrita como um híbrido entre a plataforma de análise numérica e uma libraria de processamento de dados biblioteca. É compatível com diferentes linguagens de programação (C++, C#, Pascal, VBA) e sistemas operacionais (Windows, Linux, Solaris). ALGLIB tem uma ampla funcionalidade. Recursos incluídos nela:

  • Álgebra linear (direct algorithms, EVD/SVD)
  • Assistentes de soluções de equações (lineares e não-lineares)
  • Interpolação
  • Otimização
  • Transformada rápida de Fourier
  • Integração numérica
  • Quadrados mínimos lineares e não-lineares
  • Equações diferenciais ordinárias
  • Funções especiais
  • Estatísticas (estatísticas descritivas, testes de hipóteses)
  • Análise de dados (classificação/regressão, incluindo redes neurais)
  • Implementação de algoritmos de álgebra linear, interpolação, etc. na aritmética de alta precisão (usando MPFR)

Para trabalhar com a biblioteca, são usadas duas funções estáticas da classe CAlglib, para ela foram mudadas todas as funções da biblioteca.

Contém os scripts de teste testclasses.mq5 e testinterfaces.mq5 junto com uma demonstração simples do script usealglib.mq5. Os arquivos do mesmo nome incluídos (testclasses.mqh e testinterfaces.mqh) são usados para executar em casos de teste. Eles devem ser colocados em \MQL5\Scripts\Alglib\Testcases\.

Entre os muitos arquivos e centenas de funções que ele contém, estaremos principalmente interessados nos arquivos:

Pacotes 
Descrição 
alglib.mqh
O pacote básico da biblioteca inclui funções personalizadas. É necessário chamar estas funções para trabalhar com a biblioteca.
dataanalysis.mqhClasses de análise de dados:
  1. CMLPBase — perceptron multi-camada.
  2. Cmlptrain — formação do perceptron multi-camada.
  3. CMLPE — conjuntos de redes neurais.

Ao baixar a biblioteca, o caminho de instalação enviará os arquivos para "MQL5\Include\Math\Alglib\". Para usá-la, basta incluir no código do programa o comando

#include <Math\Alglib\alglib.mqh>

De todas as funções contidas nestes dois arquivos, aplicamos -para a solução que propomos- a classe CAlglib.

//--- create neural networks
static void    MLPCreate0(const int nin,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreate1(const int nin,int nhid,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreate2(const int nin,const int nhid1,const int nhid2,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreateR0(const int nin,const int nout,double a,const double b,CMultilayerPerceptronShell &network);
static void    MLPCreateR1(const int nin,int nhid,const int nout,const double a,const double b,CMultilayerPerceptronShell &network);
static void    MLPCreateR2(const int nin,const int nhid1,const int nhid2,const int nout,const double a,const double b,CMultilayerPerceptronShell &network);
static void    MLPCreateC0(const int nin,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreateC1(const int nin,int nhid,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreateC2(const int nin,const int nhid1,const int nhid2,const int nout,CMultilayerPerceptronShell &network)

As funções "MLPCreate" criarão uma rede neural com saída linear. Nos exemplos, apresentados neste artigo, nós criamos este tipo este tipo de rede.

As funções "MLPCreateR" criarão uma rede neural com saída nos limites do intervalo [a, b].

As funções "MLPCreateC" criarão uma rede neural com saída classificada por «classes» (por exemplo, 0 ou 1; -1, 0 ou 1). 

//--- Properties and error of the neural network

static void    MLPProperties(CMultilayerPerceptronShell &network,int &nin,int &nout,int &wcount);
static int     MLPGetLayersCount(CMultilayerPerceptronShell &network);
static int     MLPGetLayerSize(CMultilayerPerceptronShell &network,const int k);
static void    MLPGetInputScaling(CMultilayerPerceptronShell &network,const int i,double &mean,double &sigma);
static void    MLPGetOutputScaling(CMultilayerPerceptronShell &network,const int i,double &mean,double &sigma);
static void    MLPGetNeuronInfo(CMultilayerPerceptronShell &network,const int k,const int i,int &fkind,double &threshold);
static double  MLPGetWeight(CMultilayerPerceptronShell &network,const int k0,const int i0,const int k1,const int i1);
static void    MLPSetNeuronInfo(CMultilayerPerceptronShell &network,const int k,const int i,int fkind,double threshold);
static void    MLPSetWeight(CMultilayerPerceptronShell &network,const int k0,const int i0,const int k1,const int i1,const double w);
static void    MLPActivationFunction(const double net,const int k,double &f,double &df,double &d2f);
static void    MLPProcess(CMultilayerPerceptronShell &network,double &x[],double &y[]);
static double  MLPError(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int ssize);
static double  MLPRMSError(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int npoints);



//--- training neural networks
static void    MLPTrainLM(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int npoints,const double decay,const int restarts,int &info,CMLPReportShell &rep);
static void    MLPTrainLBFGS(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int npoints,const double decay,const int restarts,const double wstep,int maxits,int &info,CMLPReportShell &rep);

Estas funções permitem criar e otimizar redes neuronais que contêm dois ou três camadas (camada de entrada, saída e zero, uma ou duas camadas ocultas). Os parâmetros principais de entrada quase são definidos por seu nome:

  • nin: número de neurônios na camada de entrada.
  • nout: camada de saída.
  • nhid1: camada oculta 1.
  • nhid2: camada oculta 2.
  • network: objeto da classe CMultilayerPerceptronShell que conterá a identificação das ligações, os pesos entre os neurônios e as funções de ativação.
  • xy: objeto da classe CMatrixDouble que conterá os dados de entrada/saída para realizar o treinamento e otimização de redes neurais.

O treinamento/otimização será executado usando o algoritmo de Levenberg-Marquardt (MLPTrainLM()) ou L-BFGS com regularização (MLPTrainLBFGS ()). Este último será usado se a rede tiver mais de 500 ligações/pesos; as informações sobre a função advertem: "para redes com centenas de pesos. Estes algoritmos são mais eficientes do que o assim chamado «retro-propagação» "backpropagation", que é comumente usado na NN. A biblioteca oferece outras funções de otimização. O leitor poderá analisá-las, se seu objetivo não for alcançado por meio de duas as funções descritas acima.

 

3. Implementação em MQL

Definimos o número de neurônios em cada camada como parâmetros de entrada. Também definimos as variáveis que contêm os intervalos de normalização opcionais.

input int nNeuronEntra= 35;      //Núm. neurônios na camada de entrada
input int nNeuronSal= 1;         //Núm. neurônios na camada de saída
input int nNeuronCapa1= 45;      //Número de neurônios na camada oculta 1 (<1 não existe)
input int nNeuronCapa2= 10;      //Número de neurônios na camada oculta 2 (<1 não existe)

input string intervEntrada= "0;1";        //Normalização de entrada: mín. e máx. desejados.  (vazio= NÃO normaliza)
input string intervSalida= "";            //Normalização de saída: mín. e máx. desejados.  (vazio= NÃO normaliza)

Outras variáveis externas:

input int velaIniDesc= 15;
input int historialEntrena= 1500;

Eles podem especificar o número de barra (velaIniDesc), a partir da qual é preciso iniciar a descarga de dados históricos para treinar a rede e o número total de dados da barra que será descarregada(historialEntrena).

Como variáveis globais públicas, definimos o objeto de rede e o objeto matriz dupla "arDatosAprende". 
CMultilayerPerceptronShell *objRed;
CMatrixDouble arDatosAprende(0, 0);

"arDatosAprende" conterá as cadeias de dados de entrada/saída para treinamento da rede. Esta é uma matriz bidimensional do tipo double (sabe-se que em mql5 é permitido criar matrizes dinâmicas somente unidimensionais. Ao criar matrizes multidimensionais, é necessário indicar todas as dimensões, exceto a primeira). 

Os pontos 1 — 4 do algoritmo base são implementados na função "gestionRed()".

//---------------------------------- CRIA E OTIMIZA A REDE NEURAL--------------------------------------------------
bool gestionRed(CMultilayerPerceptronShell &objRed, string simb, bool normEntrada= true , bool normSalida= true,
                bool imprDatos= true, bool barajar= true)
{
   double tasaAprende= 0.001;             //Taxa de treinamento da rede
   int ciclosEntren= 2;                   //Número de ciclos de treinamento
   ResetLastError();
   bool creada= creaRedNeuronal(objRed);                                //criação da rede neural
  if(creada) 
   {
      preparaDatosEntra(objRed, simb, arDatosAprende);                  //carda dados de entrada/saída em arDatosAprende
      if(imprDatos) imprimeDatosEntra(simb, arDatosAprende);            //imprimir os dados para avaliar a sua veracidade
      if(normEntrada || normSalida) normalizaDatosRed(objRed, arDatosAprende, normEntrada, normSalida); //normalização opcional de dados de entrada/saída
      if(barajar) barajaDatosEntra(arDatosAprende, nNeuronEntra+nNeuronSal);    //mexemos as cadeias da matriz de dados
      errorMedioEntren= entrenaEvalRed(objRed, arDatosAprende, ciclosEntren, tasaAprende);      //executamos o treinamento /otimização
      salvaRedFich(arObjRed[codS], "copiaSegurRed_"+simb);      //salvamos o arquivo de disco, a rede
   }
   else infoError(GetLastError(), __FUNCTION__);
   
   return(_LastError==0);
}


Aqui nós criamos a NN na função (creaRedNeuronal(objRed)); e, em seguida, carregamos os dados em "arDatosAprende" usando a função preparaDatosEntra(). Conseguiremos imprimir os dados para avaliar sua confiabilidade usando a função imprimeDatosEntra(). Se for necessário normalizar os dados de entrada e saída, usamos a função normalizaDatosRed(). Também, se nós quisermos baralhar as cadeias da matriz de dados antes da otimização, executamos barajaDatosEntra(). Executamos o treinamento usando entrenaEvalRed(), que retorna o erro de otimização obtido. No final, armazenamos a rede no disco para sua possível recuperação sem a necessidade de criar e otimizar novamente.

No início da função gestionRed(), existem duas variáveis (tasaAprende e ciclosEntrena) que definem a taxa e ciclos de treinamento da NN. Alglib adverte que eles normalmente são usados nos valores que reflete a função. Mas a coisa é que nos inúmeros testes feitos com os dois algoritmos de otimização propostos, o ajuste é tal que a alteração nos valores dessas variáveis praticamente não têm efeito sobre os resultados. Em princípio, estas duas variáveis foram codificadas como parâmetros de entrada, mas por causa de sua importância limitada foram dispostas no interior da função.

A função normalizaDatosRed() será aplicada para normalizar os dados de entrada de treinamento da NN nos limites do intervalo definido, somente se os dados reais -para previsão da NN- também estiverem nesse intervalo. Caso contrário, a normalização não será necessária. Além disso, é necessário levar em conta a normalização de dados reais, antes de solicitar uma previsão, se os dados de treinamento já foram normalizados.

3.1 Criação da rede neural (NN) 

//--------------------------------- CRIA A REDE NEURAL --------------------------------------
bool creaRedNeuronal(CMultilayerPerceptronShell &objRed)
{
   bool creada= false;
   int nEntradas= 0, nSalidas= 0, nPesos= 0;
   if(nNeuronCapa1<1 && nNeuronCapa2<1) CAlglib::MLPCreate0(nNeuronEntra, nNeuronSal, objRed);   //SAÍDA LINEAR   
   else if(nNeuronCapa2<1) CAlglib::MLPCreate1(nNeuronEntra, nNeuronCapa1, nNeuronSal, objRed);   //SAÍDA LINEAR
   else CAlglib::MLPCreate2(nNeuronEntra, nNeuronCapa1, nNeuronCapa2, nNeuronSal, objRed);   		//SAÍDA LINEAR                    
   creada= existeRed(objRed);
   if(!creada) Print("Erro de criação da REDE NEURAL==> ", __FUNCTION__, " ", _LastError);
   else
   {
      CAlglib::MLPProperties(objRed, nEntradas, nSalidas, nPesos);
      Print("Criada rede de nº camadas", propiedadRed(objRed, N_CAPAS));
      Print("Nº de neurônios na camada de entrada ", nEntradas);
      Print("Nº de neurônios na camada oculta 1 ", nNeuronCapa1);
      Print("Nº de neurônios na camada oculta 2 ", nNeuronCapa2);
      Print("Nº de neurônios na camada de saída ", nSalidas);
      Print("Nº de pesos", nPesos);
   }
   return(creada);
}

A função acima cria a NN segundo o número de camadas e neurônios (nNeuronEntra, nNeuronCapa1, nNeuronCapa2, nNeuronSal) e, em seguida, verifica se a rede foi criada corretamente usando a função:

//--------------------------------- EXISTE REDE--------------------------------------------
bool existeRed(CMultilayerPerceptronShell &objRed)
{
   bool resp= false;
   int nEntradas= 0, nSalidas= 0, nPesos= 0;
   CAlglib::MLPProperties(objRed, nEntradas, nSalidas, nPesos);
   resp= nEntradas>0 && nSalidas>0;
   return(resp);
}

Se a rede for criada corretamente, a função informará ao usuário sobre as configurações com as quais, por sua vez, tem sido criada a partir da função MLPProperties() da classe CAlglib localizada na biblioteca AlgLib.

Conforme mencionado na seção 2, a ALGLIB possui outras funções -que permitem criar NN projetadas para classificação (na saída, nós obtemos etiqueta de classe)- ou rede para resolver o problema da regressão (na saída, ela tem um valor numérico específico).

Após criar a NN, para obter algumas das suas configurações em outras partes do Expert Advisor, é possível definir a função "propiedadRed()":

enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS};

//---------------------------------- PROPRIEDADES DA REDE  -------------------------------------------
int propiedadRed(CMultilayerPerceptronShell &objRed, mis_PROPIEDADES_RED prop= N_CAPAS, int numCapa= 0)
{           //se for pedido o número de neurônios N_NEURONAS, será necessário indicar o número de camadas numCapa
   int resp= 0, numEntras= 0, numSals= 0, numPesos= 0;
   if(prop>N_NEURONAS) CAlglib::MLPProperties(objRed, numEntras, numSals, numPesos);    
   switch(prop)
   {
      case N_CAPAS:
         resp= CAlglib::MLPGetLayersCount(objRed);
         break;
      case N_NEURONAS:
         resp= CAlglib::MLPGetLayerSize(objRed, numCapa);
         break;
      case N_ENTRADAS:
         resp= numEntras;
         break;
      case N_SALIDAS:
         resp= numSals;
         break;
      case N_PESOS:
         resp= numPesos;
   }
   return(resp);
}  

3.2  Preparação de dados de entrada/saída 

A função proposta pode variar dependendo de quantos e quais dados de entrada sejam usados.

//---------------------------------- PREPARA OS DADOS DE ENTRADA/SAÍDA --------------------------------------------------
void preparaDatosEntra(CMultilayerPerceptronShell &objRed, string simb, CMatrixDouble &arDatos, bool normEntrada= true , bool normSalida= true)
{
   int fin= 0, fila= 0, colum= 0,
       nEntras= propiedadRed(objRed, N_ENTRADAS),
       nSals= propiedadRed(objRed, N_SALIDAS);
   double valor= 0, arResp[];   
   arDatos.Resize(historialEntrena, nEntras+nSals);
   fin= velaIniDesc+historialEntrena;
   for(fila= velaIniDesc; fila<fin; fila++)
   {                   
      for(colum= 0; colum<NUM_INDIC;  colum++)
      {
         valor= valorIndic(codS, fila, colum);
         arDatos[fila-1].Set(colum, valor);
      }
      calcEstrat(fila-nVelasPredic, arResp);
      for(colum= 0; colum<nSals; colum++) arDatos[fila-1].Set(colum+nEntras, arResp[colum]);
   }
   return;
}

No processo descrito, percorremos todo o histórico -de "velaIniDesc" a "velaIniDesc+historialEntrena"- e, em cada barra, obtemos o valor de cada indicador usado na estratégia (NUM_INDIC), em seguida, vamos carregá-lo na coluna apropriada da matriz bidimensional CMatrixDouble. Também inserimos, para cada barra, o resultado da estratégia ("calcEstrat()") que corresponte com os valores dos indicadores indicados. A variável "nVelasPredic" permite extrapolar esses valores dos indicadores n velas para frente. Normalmente, "nVelasPredic" será definida como parâmetro externo.

Ou seja, em cada cadeia de caracteres da matriz "arDatos" da classe CMatrixDouble, teremos tantas colunas quanto dados de entrada ou valores de indicadores utilizados na estratégia e tantas colunas quanto dados de saída defina nossa estratégia. Em "arDatos", haverá tantas cadeias de caracteres quanto seja definido pelo valor em "historialEntrena".

3.3 Impressão da matriz de dados de entrada/saída

Se nós quisermos imprimir o conteúdo da matriz bidimensional para verificar a exatidão dos dados de entrada e de saída, podemos usar a função "imprimeDatosEntra()".

//---------------------------------- IMPRIME DADOS DE ENTRADA/SAÍDA--------------------------------------------------
void imprimeDatosEntra(string simb, CMatrixDouble &arDatos)
{
   string encabeza= "indic1;indic2;indic3...;resultEstrat",     //nomes dos indicadores separados por ";"
          fichImprime= "dataEntrenaRed_"+simb+".csv";
   bool entrar= false, copiado= false;
   int fila= 0, colum= 0, resultEstrat= -1, nBuff= 0,
       nFilas= arDatos.Size(),
       nColum= nNeuronEntra+nNeuronSal,
       puntFich= FileOpen(fichImprime, FILE_WRITE|FILE_CSV|FILE_COMMON);
   FileWrite(puntFich, encabeza);
   for(fila= 0; fila<nFilas; fila++)
   {
      linea= IntegerToString(fila)+";"+TimeToString(iTime(simb, PERIOD_CURRENT, velaIniDesc+fila), TIME_MINUTES)+";";                
      for(colum= 0; colum<nColum;  colum++) 
         linea= linea+DoubleToString(arDatos[fila][colum], 8)+(colum<(nColum-1)? ";": "");
      FileWrite(puntFich, linea);
   }
   FileFlush(puntFich);
   FileClose(puntFich);
   Alert("Download file= ", fichImprime);
   Alert("Path= ", TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\Files");
   return;
}

A função itera, por cadeia de caracteres, a matriz que cria em cada passo a cadeia "línea" (fila) com todos os valores na cadeia de caracteres separada por ";". Logo após estes dados são imprimidos no arquivo do tipo .csv, que é criado usando a função FileOpen(). Para o tema deste artigo, esta é uma função secundária, por isso não vou comentar sobre isso. Para verificar o arquivo de tipo .csv, é possível usar o Excel.

3.4  Normalização de dados num determinado intervalo

Normalmente, antes de começar a otimizar a rede, considera-se adequado que os dados de entrada fiquem dentro de um determinado intervalo, ou seja, normalizados. Para este fim, propomos a seguinte função, ela normalizará, de modo opcional, os dados de entrada ou saída localizados na matriz "arDatos" da classe CMatrixDouble

//------------------------------------ NORMALIZA OS DADOS DE ENTRADA/SAÍDA-------------------------------------
void normalizaDatosRed(CMultilayerPerceptronShell &objRed, CMatrixDouble &arDatos, bool normEntrada= true, bool normSalida= true)
{
   int fila= 0, colum= 0, maxFila= arDatos.Size(),
       nEntradas= propiedadRed(objRed, N_ENTRADAS),
       nSalidas= propiedadRed(objRed, N_SALIDAS);
   double maxAbs= 0, minAbs= 0, maxRel= 0, minRel= 0, arMaxMinRelEntra[], arMaxMinRelSals[];
   ushort valCaract= StringGetCharacter(";", 0);
   if(normEntrada) StringSplit(intervEntrada, valCaract, arMaxMinRelEntra);
   if(normSalida) StringSplit(intervSalida, valCaract, arMaxMinRelSals);
   for(colum= 0; normEntrada && colum<nEntradas; colum++)
   {
      maxAbs= arDatos[0][colum];
      minAbs= arDatos[0][colum];
      minRel= StringToDouble(arMaxMinRelEntra[0]);
      maxRel= StringToDouble(arMaxMinRelEntra[1]); 
      for(fila= 0; fila<maxFila; fila++)                //definimos maxAbs e minAbs de cada coluna de dados
      {
         if(maxAbs<arDatos[fila][colum]) maxAbs= arDatos[fila][colum];
         if(minAbs>arDatos[fila][colum]) minAbs= arDatos[fila][colum];            
      }
      for(fila= 0; fila<maxFila; fila++)                //estabelecemos o novo valor normalizado
         arDatos[fila].Set(colum, normValor(arDatos[fila][colum], maxAbs, minAbs, maxRel, minRel));
   }
   for(colum= nEntradas; normSalida && colum<(nEntradas+nSalidas); colum++)
   {
      maxAbs= arDatos[0][colum];
      minAbs= arDatos[0][colum];
      minRel= StringToDouble(arMaxMinRelSals[0]);
      maxRel= StringToDouble(arMaxMinRelSals[1]);
      for(fila= 0; fila<maxFila; fila++)
      {
         if(maxAbs<arDatos[fila][colum]) maxAbs= arDatos[fila][colum];
         if(minAbs>arDatos[fila][colum]) minAbs= arDatos[fila][colum];            
      }
      minAbsSalida= minAbs;
      maxAbsSalida= maxAbs;
      for(fila= 0; fila<maxFila; fila++)
         arDatos[fila].Set(colum, normValor(arDatos[fila][colum], maxAbs, minAbs, maxRel, minRel));
   }
   return;
}

Reitera-se que se for decidido normalizar os dados de entrada de treinamento para a NN num determinado intervalo, será necessário assegurar que os dados reais -que vão ser utilizados no futuro para pedir à NN uma previsão- estejam dentro desse intervalo. No caso oposto, a normalização não será necessária.

Deve ser lembrado que "intervEntrada" e "intervSalida" são variáveis de tipo cadeias definidas como parâmetros de entrada (ver o início da seção «Implementação em MQL5»). Elas podem ter uma forma, por exemplo, "0;1" ou "-1;1", quer dizer, contêm máximos e mínimos relativos. A função "StringSplit()" envia uma cadeia de caracteres para a matriz que conterá os extremos relativos. Para cada coluna será necessário:

  1. Determinar os máximos e mínimos absolutos (variáveis "maxAbs" e "minAbs").
  2. Passar por toda a coluna, normalizando os valores entre "maxRel" e "minRel": veja abaixo a função "normValor()".
  3. Definir, em "arDatos", o novo valor normalizado usando o método .set. da classe CMatrixDouble.
//------------------------------------ FUNÇÃO DE NORMALIZAÇÃO---------------------------------
double normValor(double valor, double maxAbs, double minAbs, double maxRel= 1, double minRel= -1)
{
   double valorNorm= 0;
   if(maxAbs>minAbs) valorNorm= (valor-minAbs)*(maxRel-minRel))/(maxAbs-minAbs) + minRel;
   return(valorNorm);
}
3.5 Iteração de dados de entrada/saída

Para evitar tendências associadas com a herança de valores dentro da matriz de dados, nós podemos alterar arbitrariamente (tocar) a ordem das cadeias dentro da matriz. Para fazer isso, aplicamos a seguinte função "barajaDatosEntra", que, percorrendo a matriz CMatrixDouble, define para cada cadeia de caracteres uma nova cadeia de meta, respeitando a posição de cada coluna de dados e realizando a deslocação de dados usando o método da bolha (variável "filaTmp") para cada cadeia de caracteres. 

//------------------------------------ ITERA OS DADOS DE ENTRADA/SAÍDA  POR CADEIAS DE CARATERES COMPLETAS-----------------------------------
void barajaDatosEntra(CMatrixDouble &arDatos, int nColum)
{
   int fila= 0, colum= 0, filaDestino= 0, nFilas= arDatos.Size();
   double filaTmp[];
   ArrayResize(filaTmp, nColum);
   MathSrand(GetTickCount());          /reinicia arbitrariamente a série semente (filha)
   while(fila<nFilas)
   {
      filaDestino= randomEntero(0, nFilas-1);   //obtém arbitrariamente uma nova cadeia de caracteres meta
      if(filaDestino!=fila)
      {
         for(colum= 0; colum<nColum; colum++) filaTmp[colum]= arDatos[filaDestino][colum];
         for(colum= 0; colum<nColum; colum++) arDatos[filaDestino].Set(colum, arDatos[fila][colum]);
         for(colum= 0; colum<nColum; colum++) arDatos[fila].Set(colum, filaTmp[colum]);
         fila++;
      }
   }
   return;
}

Após a reinicialização da semente da série arbitrária "MathSrand (GetTcikCount())", a função responsável da "destino final" aleatório de cada cadeia de caracteres "randomEntero()".

//---------------------------------- DESLOCAÇÃO ARBITRÁRIA-----------------------------------------------
int randomEntero(int minRel= 0, int maxRel= 1000)
{
   int num= (int)MathRound(randomDouble((double)minRel, (double)maxRel));
   return(num);
}

3.6  Treinamento/otimização da rede neural
A biblioteca AlgLib permite usar os algoritmos de configuração da rede que reduzem significativamente o tempo de treinamento e otimização em comparação com o sistema tradicional aplicados ao perceptron multi-camada: «retro-propagação» ou «backpropagation». Como mencionado no início, vamos usar:

  • o algoritmo Levenberg-Marquardt com regularização e cálculo exato Hessians (MLPTrainLM()), ou
  • o algoritmoL-BFGS com regularização (MLPTrainLBFGS()).

O segundo algoritmo irá ser utilizado para optimizar a rede com um número de pesos superior a 500.

//---------------------------------- TREINAMENTO DA REDE-------------------------------------------
double entrenaEvalRed(CMultilayerPerceptronShell &objRed, CMatrixDouble &arDatosEntrena, int ciclosEntrena= 2, double tasaAprende= 0.001)
{
   bool salir= false;
   double errorMedio= 0; string mens= "Entrenamiento Red";
   int k= 0, i= 0, codResp= 0,
       historialEntrena= arDatosEntrena.Size();
   CMLPReportShell infoEntren;
   ResetLastError();
   datetime tmpIni= TimeLocal();
   Alert("Início de otimização da rede neural...");
   Alert("Aguarde alguns minutos, de acordo com a quantidade de histórico envolvido.");
   Alert("...///...");   
   if(propiedadRed(objRed, N_PESOS)<500)
      CAlglib::MLPTrainLM(objRed, arDatosEntrena, historialEntrena, tasaAprende, ciclosEntrena, codResp, infoEntren);
   else
      CAlglib::MLPTrainLBFGS(objRed, arDatosEntrena, historialEntrena, tasaAprende, ciclosEntrena, 0.01, 0, codResp, infoEntren);
   if(codResp==2 || codResp==6) errorMedio= CAlglib::MLPRMSError(objRed, arDatosEntrena, historialEntrena);
   else Print("Cod entrena Resp: ", codResp);
   datetime tmpFin= TimeLocal();
   Alert("NGrad ", infoEntren.GetNGrad(), " NHess ", infoEntren.GetNHess(), " NCholesky ", infoEntren.GetNCholesky());
   Alert("codResp ", codResp," Erro médio de treinamento"+DoubleToString(errorMedio, 8), " ciclosEntrena ", ciclosEntrena);
   Alert("tmpEntren ", DoubleToString(((double)(tmpFin-tmpIni))/60.0, 2), " min", "---> tmpIni ", TimeToString(tmpIni, _SEG), " tmpFin ", TimeToString(tmpFin, _SEG));
   infoError(GetLastError(), __FUNCTION__);
   return(errorMedio);
}

Como pode ser visto, como parâmetros de entrada, a função recebe o "objeto de rede" e a matriz de dados de entrada/saída que, nesta fase, tem sido normalizada. Nós também definimos os ciclos ou épocas de treinamento ("ciclosEntrena"; ou o número de vezes que o algoritmo realizará o ajuste procurando o menor "erro de trenamento" possível); na documentação é recomendado 2. Os testes, feitos por mim, me mostraram resultados melhorados com um aumento no número de épocas de treinamento. Nós também são falamos sobre a o parâmetro "Taxa de treinamento" ( "tasaAprende").

Definimos, no início da função, o objeto "infoEntren" (da classe CMLPReportShell) que coletará informações sobre o resultado do treinamento e que, em seguida, obteremos com ajuda dos métodos GetNGrad() e GetNCholesky(). O erro médio de treinamento (erro médio quadrado de todos os dados de saída originais em relação aos dados de saída obtidos após o ajuste com o algoritmo) é obtido utilizando a função "MLPRMSError()". Além disso, nós informamos ao usuário sobre o tempo gasto na otimização. Para fazer isso tomamos a hora de início e fim nas variáveis tmpIni e tmpFin.

Estas funções de otimização retornam o código de erro de execução («codResp») que pode tomar os valores:

  • -2, se a amostra de treinamento possuir um maior número de dados de saída do que neurônios na camada de saída.
  • -1, se qualquer parâmetro de entrada à função for inválido.
  • 2, execução correta e dimensão de erro inferior ao critério de parada ("MLPTrainLM()").
  • 6, idem acima para a função "MLPTrainLBFGS()".

Assim, a execução correta retornará um 2 ou 6, em conformidade com o número de pesos da rede optimizada. 

Estes algoritmos executam o ajuste de modo que a repetição de ciclos de treinamento (variável "ciclosEntrena") não tem praticamente nenhum efeito sobre o erro obtido, em contraste com o algoritmo de "retro-propagação", onde a reiteração pode alterar significativamente a precisão resultante. Uma rede de 4 camadas com 35, 45, 10 e 2 neurônios e uma matriz de entrada de 2000 cadeias de caracteres pode ser otimizada por meio da função descrita acima em 4-6 minutos (I5, core 4, RAM de 8 GB) com um erro de cerca de 2-4 cem milésimos (4x10^-5).

3.7 Salvar a rede num arquivo de texto ou restaurar a partir dele

Neste ponto do artigo, nós criamos uma NN, preparamos os dados de entrada/saída e temos treinada a NN. Como medida de precaução, é preciso salvar a rede no disco rígido em caso de erros inesperados durante a operação do Expert Advisor. Para fazer isso, é preciso utilizar as funções oferecidas por AlgLib, a fim de obter as características e os valores internos da rede (número de camadas e neurónios por cada camada, valor dos pesos, etc.) e gravar estes dados num arquivo de texto localizado no disco rígido.

//-------------------------------- SALVAR REDE NO DISCO-------------------------------------------------
bool salvaRedFich(CMultilayerPerceptronShell &objRed, string nombArch= "")
{
   bool redSalvada= false;
   int k= 0, i= 0, j= 0, numCapas= 0, arNeurCapa[], neurCapa1= 1, funcTipo= 0, puntFichRed= 9999;
   double umbral= 0, peso= 0, media= 0, sigma= 0;
   if(nombArch=="") nombArch= "copiaSegurRed";
   nombArch= nombArch+".red";
   FileDelete(nombArch, FILE_COMMON);
   ResetLastError();
   puntFichRed= FileOpen(nombArch, FILE_WRITE|FILE_BIN|FILE_COMMON);
   redSalvada= puntFichRed!=INVALID_HANDLE;
   if(redSalvada)
   {
      numCapas= CAlglib::MLPGetLayersCount(objRed);   
      redSalvada= redSalvada && FileWriteDouble(puntFichRed, numCapas)>0;
      ArrayResize(arNeurCapa, numCapas);
      for(k= 0; redSalvada && k<numCapas; k++)
      {
         arNeurCapa[k]= CAlglib::MLPGetLayerSize(objRed, k);
         redSalvada= redSalvada && FileWriteDouble(puntFichRed, arNeurCapa[k])>0;
      }
      for(k= 0; redSalvada && k<numCapas; k++)
      {
         for(i= 0; redSalvada && i<arNeurCapa[k]; i++)
         {
            if(k==0)
            {
               CAlglib::MLPGetInputScaling(objRed, i, media, sigma);
               FileWriteDouble(puntFichRed, media);
               FileWriteDouble(puntFichRed, sigma);
            }
            else if(k==numCapas-1)
            {
               CAlglib::MLPGetOutputScaling(objRed, i, media, sigma);
               FileWriteDouble(puntFichRed, media);
               FileWriteDouble(puntFichRed, sigma);
            }
            CAlglib::MLPGetNeuronInfo(objRed, k, i, funcTipo, umbral);
            FileWriteDouble(puntFichRed, funcTipo);
            FileWriteDouble(puntFichRed, umbral);
            for(j= 0; redSalvada && k<(numCapas-1) && j<arNeurCapa[k+1]; j++)
            {
               peso= CAlglib::MLPGetWeight(objRed, k, i, k+1, j);
               redSalvada= redSalvada && FileWriteDouble(puntFichRed, peso)>0;
            }
         }      
      }
      FileClose(puntFichRed);
   }
   if(!redSalvada) infoError(_LastError, __FUNCTION__);
   return(redSalvada);
} 

Como pode ser visto na sexta cadeia de caracteres de código, atribuímos ao arquivo a extensão ".red", o que subsequentemente facilitará a busca e exame. Nesta função, gastamos horas de depuração, mas funciona!

Se, após o evento que parou a execução do Expert Advisor, for necessário continuar o trabalho, restauraremos a rede a partir de um arquivo no disco usando a função inversa acima. Esta função irá criar um objeto de rede e preenchê-lo com dados, lendo-os a partir do arquivo de texto onde nós salvamos a NN.

//-------------------------------- RESTAURA A REDE A PARTIR DO DISCO-------------------------------------------------
bool recuperaRedFich(CMultilayerPerceptronShell &objRed, string nombArch= "")
{
   bool exito= false;
   int k= 0, i= 0, j= 0, nEntradas= 0, nSalidas= 0, nPesos= 0,
       numCapas= 0, arNeurCapa[], funcTipo= 0, puntFichRed= 9999;
   double umbral= 0, peso= 0, media= 0, sigma= 0;
   if(nombArch=="") nombArch= "copiaSegurRed";
   nombArch= nombArch+".red";
   puntFichRed= FileOpen(nombArch, FILE_READ|FILE_BIN|FILE_COMMON);
   exito= puntFichRed!=INVALID_HANDLE;
   if(exito)
   {
      numCapas= (int)FileReadDouble(puntFichRed);
      ArrayResize(arNeurCapa, numCapas);
      for(k= 0; k<numCapas; k++) arNeurCapa[k]= (int)FileReadDouble(puntFichRed); 
      if(numCapas==2) CAlglib::MLPCreate0(nNeuronEntra, nNeuronSal, objRed);
      else if(numCapas==3) CAlglib::MLPCreate1(nNeuronEntra, nNeuronCapa1, nNeuronSal, objRed);
      else if(numCapas==4) CAlglib::MLPCreate2(nNeuronEntra, nNeuronCapa1, nNeuronCapa2, nNeuronSal, objRed);
      exito= existeRed(arObjRed[0]);
      if(!exito) Print("erro de criação de rede neural==> ", __FUNCTION__, " ", _LastError);
      else
      {
         CAlglib::MLPProperties(objRed, nEntradas, nSalidas, nPesos);
         Print("Recuperada rede de nº camadas", propiedadRed(objRed, N_CAPAS));
         Print("Nº de neurônios na camada de entrada ", nEntradas);
         Print("Nº de neurônios na camada oculta 1 ", nNeuronCapa1);
         Print("Nº de neurônios na camada oculta 2 ", nNeuronCapa2);
         Print("Nº de neurônios na camada de saída ", nSalidas);
         Print("Nº pesos", nPesos);
         for(k= 0; k<numCapas; k++)
         {
            for(i= 0; i<arNeurCapa[k]; i++)
            {
               if(k==0)
               {
                  media= FileReadDouble(puntFichRed);
                  sigma= FileReadDouble(puntFichRed);
                  CAlglib::MLPSetInputScaling(objRed, i, media, sigma);
               }
               else if(k==numCapas-1)
               {
                  media= FileReadDouble(puntFichRed);
                  sigma= FileReadDouble(puntFichRed);
                  CAlglib::MLPSetOutputScaling(objRed, i, media, sigma);
               }
               funcTipo= (int)FileReadDouble(puntFichRed);
               umbral= FileReadDouble(puntFichRed);
               CAlglib::MLPSetNeuronInfo(objRed, k, i, funcTipo, umbral);
               for(j= 0; k<(numCapas-1) && j<arNeurCapa[k+1]; j++)
               {
                  peso= FileReadDouble(puntFichRed);
                  CAlglib::MLPSetWeight(objRed, k, i, k+1, j, peso);
               }
            }      
         }
      }
   }
   FileClose(puntFichRed);
   return(exito);
} 

Para obter a previsão da rede, ao carregá-la com dados atuais, nós chamamos a função "respuestaRed()":

//--------------------------------------- SOLICITA RESPOSTA DA REDE---------------------------------
double respuestaRed(CMultilayerPerceptronShell &ObjRed, double &arEntradas[], double &arSalidas[], bool desnorm= false)
{
   double resp= 0, nNeuron= 0;
   CAlglib::MLPProcess(ObjRed, arEntradas, arSalidas);   
   if(desnorm)             //Se for necessário alterar a normalização de dados de saída
   {
      nNeuron= ArraySize(arSalidas);
      for(int k= 0; k<nNeuron; k++)
         arSalidas[k]= desNormValor(arSalidas[k], maxAbsSalida, minAbsSalida, arMaxMinRelSals[1], arMaxMinRelSals[0]);
   }
   resp= arSalidas[0];
   return(resp);
}

Esta função assume a possibilidade de mudar a normalização aplicada aos dados de saída na matriz de treinamento.

 

4. Auto-optimização

Após o Expert Advisor otimizar a rede neural (e portanto, indiretamente, os valores de entrada aplicados ao EA) no processo de seu próprio desempenho, sem a ajuda de otimização no Testador de estratégias, será preciso repetir o algoritmo básico descrito na seção 1.

Além disso, somos confrontados com uma tarefa importante: o Expert Advisor deve monitorar continuamente o mercado, é importante evitar a perda de controle durante a otimização da NN, que envolve o uso de grandes recursos de computação.

Nós definimos o tipo de enumeração "mis_PLAZO_OPTIM" que descreve os intervalos de tempo que o usuário pode optar para repetir o algoritmo básico (diariamente, seletivamente ou nos fins de semana). Definimos outra enumeração, de modo que o usuário consiga decidir se o Expert Advisor agirá como "otimizador" de rede ou "executor" da estratégia.

enum mis_PLAZO_OPTIM {_DIARIO, _DIA_ALTERNO, _FIN_SEMANA};
enum mis_TIPO_EAred {_OPTIMIZA, _EJECUTA};

Agora recordamos que a MetaTrader 5 permite a execução simultânea do Expert Advisor em cada gráfico aberto. Assim, no primeiro gráfico, carregaremos o Expert Advisor no modo de execução, e no segundo, no modo de otimização. No primeiro gráfico, o Expert Advisor vai tratar o controle da estratégia, no segundo, somente a otimização da rede neural. Assim, resolvemos o segundo problema descrito. No primeiro gráfico, o Expert Advisor "usa" a rede neural "lendo-a" a partir de um arquivo de texto que o EA gera no modo "otimizador" cada vez que otimiza a NN.

Anteriormente, foi dito que nos testes de otimização foram gastos entre 4 e 6 minutos de tempo de computação. A aplicação deste método aumenta ligeiramente o tempo de processamento: até 8 e 15 minutos, dependendo do tempo da atividade do mercado asiático ou europeu, no entanto o controle da estratégia não pára.

Para atender ao exposto no parágrafo anterior, definimos os seguintes parâmetros de entrada.

input mis_TIPO_EAred tipoEAred            = _OPTIMIZA;        //Tipo de tarefa para executar
input mis_PLAZO_OPTIM plazoOptim          = _DIARIO;          //Intervalo de tempo para otimizar a rede
input int horaOptim                       = 3;                //Tempo mensal para otimizar a rede

O parâmetro "horaOptim" salva a hora local, segundo a qual deve ser realizada a otimização. O tempo deve estar de acordo com a atividade de mercado baixa ou nula: na Europa, por exemplo, no início da manhã (03:00 h como valor padrão) ou nos fins de semana. Se você quiser sempre otimizar, após executado o Expert Advisor, sem esperar o tempo e dias definidos, também será preciso estabelecer o seguinte:

input bool optimInicio                    = true;         //Otimizar a rede neural após a execução do Expert Advisor

Para controlar se a rede é considerada otimizada (modo "otimizador") e o momento em que foi realizada a última leitura de arquivos de rede (modo "Executor"), definimos as seguintes variáveis públicas:

double fechaUltLectura;
bool reOptimizada= false;

Para resolver o primeiro problema, o bloco de processamento do método indicado e codificado na função OnTimer() que será executada de acordo com o período "tmp", por sua vez, estabelecida co EventSetTimer(tmp) em OnInit (), pelo menos a cada hora. Assim, a cada tmp o Expert Advisor "otimizador" verificará se deve re-otimizar a rede, e o EA "executor" verificará se deve ler o novo arquivo de rede, porque ele foi atualizado pelo Expert Advisor "otimizador".

/---------------------------------- ON TIMER --------------------------------------
void OnTimer()
{
   bool existe= false;
   string fichRed= "";
   if(tipoEAred==_OPTIMIZA)            //o Expert Advisor trabalha em modo «otimizador»
   {
      bool optimizar= false;
      int codS= 0,
          hora= infoFechaHora(TimeLocal(), _HORA);    //obtemos a hora enteira atual
      if(!redOptimizada) optimizar= horaOptim==hora && permReoptimDia();
      fichRed= "copiaSegurRed_"+Symbol()+".red";      //define o nome do arquivo da rede neural
      existe= buscaFich(fichRed, "*.red");            //procura, no disco, o arquivo onde foi armazenada a rede neural
      if(!existe || optimizar)
         redOptimizada= gestionRed(objRed, simb, intervEntrada!="", intervSalida!="", imprDatosEntrena, barajaDatos);
      if(hora>(horaOptim+6)) redOptimizada= false;    //após 6 horas a partir da hora prevista, considera obsoleta a atual rede otimizada
      guardaVarGlobal(redOptimizada);                 //salva no disco o valor "re-otimizada»
   }
   else if(tipoEAred==_EJECUTA)        //o Expert Advisor trabalha em modo «executor»
   {
      datetime fechaUltOpt= 0;
      fichRed= "copiaSegurRed_"+Symbol()+".red";      //define o nome do arquivo da rede neural
      existe= buscaFich(fichRed, "*.red");            //procura, no disco, o arquivo onde foi armazenada a rede neural
      if(existe)
      {
         fechaUltOpt= fechaModifFich(0, fichRed);     //define a data da última otimização (modificação do arquivo da rede)
         if(fechaUltOpt>fechaUltLectura)              //se a data da otimização for posterior à última leitura
         {
            recuperaRedFich(objRed, fichRed);         //lê e gera uma nova rede neural
            fechaUltLectura= (double)TimeCurrent();
            guardaVarGlobal(fechaUltLectura);         //salva no disco a nova data de leitura
            Print("Rede recuperada após a otimização... "+simb);      //exibe na tela a mensagem
         }
      }
      else Alert("tipoEAred==_EJECUTA --> não existe o arquivo da rede neural: "+fichRed+".red");
   }
   return;
}

As seguintes são funções adicionais que não são comentadas aqui:

//--------------------------------- PERMISSÃO PARA RE-OTIMIZAR---------------------------------
bool permReoptimDia()
{
   int diaSemana= infoFechaHora(TimeLocal(), _DSEM);
   bool permiso= (plazoOptim==_DIARIO && diaSemana!=6 && diaSemana!=0) ||     //otimiza [a cada dia, de terça a sábado]
                 (plazoOptim==_DIA_ALTERNO && diaSemana%2==1) ||              //otimiza [terça, quinta e sábado]
                 (plazoOptim==_FIN_SEMANA && diaSemana==5);                   //otimiza [sábado]
   return(permiso);
}

//-------------------------------------- PROCURA ARQUIVO--------------------------------------------
bool buscaFich(string fichBusca, string filtro= "*.*", int carpeta= FILE_COMMON)
{
   bool existe= false;
   string fichActual= "";
   long puntBusca= FileFindFirst(filtro, fichActual, carpeta);
   if(puntBusca!=INVALID_HANDLE)
   {
      ResetLastError();
      while(!existe)
      {
         FileFindNext(puntBusca, fichActual);
         existe= fichActual==fichBusca;
      }
      FileFindClose(puntBusca);
   }
   else Print("Arquivo não encontrado!");
   infoError(_LastError, __FUNCTION__);
   return(existe);

O algoritmo descrito atualmente é usado no EA que nós testamos. Isso nos permite controlar completamente a estratégia, em quanto, todas as noites, a partir das 03:00 hora local, a rede neural é re-otimizada com os dados da vela H1 dos últimos 3 meses: 35 neurônios na camada de entrada, 45 na primeira camada oculta, 8 na segunda camada oculta e 2 na camada de saída; a otimização dura 35-45 minutos.

5. Problema 1: conversor binário-decimal

Para verificar o sistema comentado, vamos resolver o problema, porque sabemos de antemão a solução exata (existe um algoritmo que facilita isso) e compará-la com a que fornece a rede neural. Fazemos um conversor binário-decimal. Para teste, consideramos o seguinte script:

#property script_show_confirm
#property script_show_inputs

#define FUNC_CAPA_OCULTA   1  
#define FUNC_SALIDA        -5
            //1= tangente hiperbólica; 2= e^(-x^2); 3= x>=0 raizC(1+x^2) x<0 e^x; 4= função sigmoidal;
            //5= binomial x>0.5? 1: 0; -5= função linear
#include <Math\Alglib\alglib.mqh>

enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS};
//---------------------------------  Parâmetros de entrada  ---------------------
sinput int nNeuronEntra= 10;                 //Núm. neurônios na camada de entrada
                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronCapa1= 0;                  //Núm. neurônios na primeira camada oculta (<1 não existe)
sinput int nNeuronCapa2= 0;                  //Núm. neurônios na segunda camada oculta (<1 não existe)                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronSal= 1;                    //Núm. neurônios na camada de saída

sinput int    historialEntrena= 800;         //Histórico de treinamento
sinput int    historialEvalua= 200;          //Histórico de avaliação
sinput int    ciclosEntrena= 2;              //Ciclos de treinamento
sinput double tasaAprende= 0.001;            //Nível de treinamento da rede
sinput string intervEntrada= "";             //Normalização de entrada: mín y máx deseados  (vazio= NÃO normaliza)
sinput string intervSalida= "";              //Normalização de saída: mín y máx deseados  (vazio= NÃO normaliza)
sinput bool   imprEntrena= true;             //Imprimir dados de treinamento/avalização
      
// ------------------------------ VARIÁVEIS GLOBAIS-----------------------------     
int puntFichTexto= 0;
ulong contFlush= 0; 
CMultilayerPerceptronShell redNeuronal;
CMatrixDouble arDatosAprende(0, 0);
CMatrixDouble arDatosEval(0, 0);
double minAbsSalida= 0, maxAbsSalida= 0;
string nombreEA= "ScriptBinDec";

//+------------------------------------------------------------------+
void OnStart()              //Conversor binário-decimal
{
   string mensIni= "Script conversor BINARIO-DECIMAL",
          mens= "", cadNumBin= "", cadNumRed= "";
   int contAciertos= 0, arNumBin[],
       inicio= historialEntrena+1,
       fin= historialEntrena+historialEvalua;
   double arSalRed[], arNumEntra[], salida= 0, umbral= 0, peso= 0;
   double errorMedioEntren= 0;
   bool normEntrada= intervEntrada!="", normSalida= intervSalida!="", correcto= false,
        creada= creaRedNeuronal(redNeuronal);        
   if(creada) 
   {
      iniFichImprime(puntFichTexto, nombreEA+"-infRN", ".csv",mensIni);
      preparaDatosEntra(redNeuronal, arDatosAprende, intervEntrada!="", intervSalida!="");
      normalizaDatosRed(redNeuronal, arDatosAprende, normEntrada, normSalida);
      errorMedioEntren= entrenaEvalRed(redNeuronal, arDatosAprende);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("RESPUESTA RED------------", puntFichTexto);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("numBinEntra;numDecSalidaRed;correcto", puntFichTexto);
      for(int k= inicio; k<=fin; k++)
      {
         cadNumBin= dec_A_baseNumerica(k, arNumBin, 2, nNeuronEntra);
         ArrayCopy(arNumEntra, arNumBin);
         salida= respuestaRed(redNeuronal, arNumEntra, arSalRed);
         salida= MathRound(salida);
         correcto= k==(int)salida;
         escrTexto(cadNumBin+";"+IntegerToString((int)salida)+";"+correcto, puntFichTexto);
         cadNumRed= "";
      }
   }      
   deIniFichImprime(puntFichTexto);
   return;
}

Criamos a NN e treinamo-la com os primeiros 800 números naturais em forma binária (10 caracteres, 10 neurônios de entrada, 1 de saída). Em seguida, transformamos esses 200 números naturais para a forma binária (801-1000 em forma binária), e comparamos o resultado real com o previsto pela NN. Por exemplo, se definirmos para a rede 1100110100 (820 no formato binário, 10 caracteres, 10 neurônios de entrada), a rede deve receber, na saída, 820 ou um número próximo. O método For descrito acima, é responsável por obter a previsão de rede para esses 200 números e comparar os resultados esperados com os previstos.

Após executar o script com os parâmetros dados (NN sem camadas ocultas, 10 neurônios de entrada e 1 de saída), obtemos um grande resultado. O arquivo "ScriptBinDec-infRN.csv" gerado na pasta "Terminal\Common\Files" dá-nos as seguintes informações:


 

A figura mostra que o script imprimiu a matriz de treinamento ató ao número 800 na forma binária (na entrada) e na forma decimal (na saída). Foi trenada a NN, e nós imprimimos a resposta, começando com o número 801. Na terceira coluna, nós obtivemos true. Este é o resultado ao comparar o esperado com o resultado obtido. Como já mencionado, este é um bom resultado.

No entanto, se definirmos a estrutura do NN como «10 neurônios de entrada, 20 neurônios da camada oculta 1, 8 neurônios na segunda camada oculta e 1 neurônio de saída», iremos obter o seguinte resultado:


 

Este é um resultado inaceitável! O fato é que aqui estamos diante de um enorme problema no processamento de redes neurais: qual configuração interna (número de camadas, número de neurônios e funções de ativação) é a mais adequada? Sabe-se que este problema é resolvido apenas pela experiência, milhares de teste feitos pelo usuário e a leitura de artigos tais como: "Avaliação e seleção de variáveis em modelos de treinamento de máquinas". Além disso, foram utilizados dados da matriz de treinamento no programa de análise estatística Rapid Miner para tentar encontrar a estrutura mais eficaz antes da sua aplicação em MQL5.

No final, em anexo, o script completo. 

 

6. Problema 2: detetar números primos

Agora, consideremos o mesmo problema, só que desta vez a NN irá determinar se se trata de um número primo ou não. A matriz de treinamento conterá 10 colunas com os 10 caracteres de cada número natural na forma binária até 800 e uma coluna que indicará se esse número é primo ("1") ou não ("0"). Ou seja, teremos 800 cadeias e 11 colunas. Em seguida, pediremos à NN analisar os seguintes 200 números naturais em forma binária (801-1000) e determinar qual é primo e qual, não. No fim, prevendo que este problema é o mais difícil, imprimimos as estatísticas das coincidências obtidas.

#include <Math\Alglib\alglib.mqh>

enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS};
//---------------------------------  Parâmetros de entrada  ---------------------
sinput int nNeuronEntra= 10;                 //Núm. neurônios na camada de entrada
                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronCapa1= 20;                 //Número de neurônios na primeira camada oculta (<1 não existe)
sinput int nNeuronCapa2= 0;                  //Núm. neurônios na segunda camada oculta (<1 não existe)                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronSal= 1;                    //Núm. neurônios na camada de saída

sinput int    historialEntrena= 800;         //Histórico de treinamento
sinput int    historialEvalua= 200;          //Histórico da previsão
sinput int    ciclosEntrena= 2;              //Ciclos de treinamento
sinput double tasaAprende= 0.001;            //Taxa de treinamento da rede
sinput string intervEntrada= "";             //Normalização de entrada: mín y máx deseados  (vazio= NÃO normaliza)
sinput string intervSalida= "";              //Normalização de saída: mín y máx deseados  (vazio= NÃO normaliza)
sinput bool   imprEntrena= true;             //Imprimir dados de treinamento/avalização
      
// ------------------------------ VARIÁVEIS GLOBAIS-----------------------------     
int puntFichTexto= 0;
ulong contFlush= 0; 
CMultilayerPerceptronShell redNeuronal;
CMatrixDouble arDatosAprende(0, 0);
double minAbsSalida= 0, maxAbsSalida= 0;
string nombreEA= "ScriptNumPrimo";

//+----------------------- Detetor de números primos-------------------------------------------+
void OnStart()
{
   string mensIni= "Script comprobación NÚMEROS PRIMOS", cadNumBin= "", linea= "";
   int contAciertos= 0, totalPrimos= 0, aciertoPrimo= 0, arNumBin[],
       inicio= historialEntrena+1,
       fin= historialEntrena+historialEvalua;
   double arSalRed[], arNumEntra[], numPrimoRed= 0;
   double errorMedioEntren= 0;
   bool correcto= false,
        esNumPrimo= false, 
        creada= creaRedNeuronal(redNeuronal);        
   if(creada) 
   {
      iniFichImprime(puntFichTexto, nombreEA+"-infRN", ".csv",mensIni);
      preparaDatosEntra(redNeuronal, arDatosAprende, intervEntrada!="", intervSalida!="");
      normalizaDatosRed(redNeuronal, arDatosAprende, normEntrada, normSalida);
      errorMedioEntren= entrenaEvalRed(redNeuronal, arDatosAprende);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("RESPUESTA RED------------", puntFichTexto);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("numDec;numBin;numPrimo;numPrimoRed;correcto", puntFichTexto);
      for(int k= inicio; k<=fin; k++)
      {
         cadNumBin= dec_A_baseNumerica(k, arNumBin, 2, nNeuronEntra);
         esNumPrimo= esPrimo(k);
         ArrayCopy(arNumEntra, arNumBin);
         numPrimoRed= respuestaRed(redNeuronal, arNumEntra, arSalRed);
         numPrimoRed= MathRound(numPrimoRed);
         correcto= esNumPrimo==(int)numPrimoRed;
         if(esNumPrimo)
         {
            totalPrimos++;
            if(correcto) aciertoPrimo++;  
         }
         if(correcto) contAciertos++;
         linea= IntegerToString(k)+";"+cadNumBin+";"+esNumPrimo+";"+(numPrimoRed==0? "false": "true")+";"+correcto;
         escrTexto(linea, puntFichTexto);
      }
   }     
   escrTexto("porc Aciertos / total;"+DoubleToString((double)contAciertos/(double)historialEvalua*100, 2)+" %", puntFichTexto); 
   escrTexto("Aciertos primos;"+IntegerToString(aciertoPrimo)+";"+"total primos;"+IntegerToString(totalPrimos), puntFichTexto); 
   escrTexto("porc Aciertos / total primos;"+DoubleToString((double)aciertoPrimo/(double)totalPrimos*100, 2)+" %", puntFichTexto); 
   deIniFichImprime(puntFichTexto);
   return;
}

Após a execução do script com os dados especificados (NN sem camadas ocultas, 10 neurônios de entrada, 20 neurônios na primeira camada ocultos e 1 na camada de saída), o resultado é pior do que no problema anterior. O arquivo "ScriptNumPrimo-infRN.csv" gerado na pasta "Terminal\Common\Files" nos dá as seguintes informações:


 

Aqui vemos que o primeiro número primo após 800 (809) não foi detectado pela rede (correto = false). Resumo estatístico:


 

Informa-nos que a NN teve sucesso 78% das 200 vezes, ao classificar o número como primo, no intervalo (801-200) de avaliação. No entanto, dos 29 números primos que existem neste intervalo, ela detectou apenas 13 (44,83%).

Se nós fizermos o teste com a seguinte estrutura de rede: "10 neurônios na camada de entrada, 35 na primeira camada oculta, 10 na segunda camada oculta e 1 na camada de saída", o script exibirá as seguintes informações, na medida da sua execução:


 

Como pode ser visto na figura abaixo, em 0,53 minutos e com uma média de erro de treinamento de 0,04208383, os resultados são piores.


 

Assim, voltamos à pergunta anterior: como determinar a estrutura interna da rede da melhor maneira?

No final, em anexo, o script completo.

 

Conclusão.

Procurando por um Expert Advisor auto-optimizável, implementamos o código de otimização da rede neural da biblioteca ALGLIB num programa MQL5. Propusemos uma solução para o problema que impede o Expert Advisor gerenciar a estratégia de negociação em períodos quando ele configura a rede associada com um grande custo computacional.

Em seguida, usamos parte do código proposto para resolver dois problemas a partir do programa MQL5: conversão binário-decimal, definição de números primos e observação de resultados de acordo com a estrutura interna da NN.

Será que o material fornecido vau ser útil para implementação de uma estratégia de negociação rentável? Estamos trabalhando nisso. Nesta fase, nós nos limitamos apenas a essa contribuição.

Traduzido do espanhol pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/es/articles/2279

Outros artigos do autor

Últimos Comentários | Ir para discussão (1)
Rodrigo da Silva Boa
Rodrigo da Silva Boa | 25 out 2016 em 02:39

Parabéns pelas informações,

Faltou script. 

Interfaces Gráficas VIII: O Controle Calendário (Capítulo 1) Interfaces Gráficas VIII: O Controle Calendário (Capítulo 1)
Na parte VIII da série de artigos dedicados à criação de interfaces gráficas no MetaTrader, nós vamos introduzir os controles compostos complexos como os calendários, lista hierárquica e o navegador de arquivos. Devido à grande quantidade de informações, os artigos foram escritos separadamente para cada assunto. O primeiro capítulo desta parte descreve o controle calendário e sua versão expandida — um calendário suspenso.
Trabalhando com cesta de moedas no mercado Forex Trabalhando com cesta de moedas no mercado Forex
O artigo descreve como os pares de moedas podem ser divididos em grupos (cestas), bem como a forma de obter dados sobre o seu status (por exemplo, compradas e vendidas) usando certos indicadores e como aplicar esses dados na negociação.
LifeHack para traders: otimização "silenciosa" ou traço da distribuição de negociações LifeHack para traders: otimização "silenciosa" ou traço da distribuição de negociações
Análise do histórico de negociação e construção de gráficos HTML de distribuição de resultados de negociação, dependendo do momento da entrada no mercado. Os gráficos são exibidos em três seções, isto é: por horas, dias, semanas e meses.
Guia Prático MQL5 - Sinais de negociação de canais móveis Guia Prático MQL5 - Sinais de negociação de canais móveis
O artigo descreve o processo de desenvolvimento e implementação de uma classe para envio de sinais com base nos canais móveis. Cada versão do sinal é seguido por uma estratégia de negociação com os resultados dos testes. As classes da Biblioteca Padrão são utilizadas para criar classes derivadas.