English Русский
preview
Sistema de autoaprendizado por reforço para trading algorítmico em MQL5

Sistema de autoaprendizado por reforço para trading algorítmico em MQL5

MetaTrader 5Sistemas de negociação |
31 2
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Introdução ao universo dos sistemas de trading autoevolutivos

Imagine um robô de trading que não se limita a seguir cegamente regras predefinidas, mas que realmente aprende com seus erros e acertos, de modo semelhante a um trader humano. É exatamente esse tipo de sistema que o aprendizado por reforço oferece no contexto do trading algorítmico. Não se trata apenas de mais um indicador técnico ou de um conjunto de sinais. É uma abordagem fundamentalmente diferente para a construção de estratégias de trading, na qual a própria máquina descobre padrões do mercado por meio da interação contínua com ele.

#property copyright "BrainRL Expert v2.0"
#property version   "2.00"
#property strict

#include <Trade\Trade.mqh>
#include <Arrays\ArrayObj.mqh>

// System input parameters
input int      BrainsCount = 5;          // Number of brains in the collective
input double   LearningRate = 0.10;      // Learning rate  
input double   ExplorationRate = 0.30;   // Initial exploration level
input double   Gamma = 0.90;             // Discount factor
input double   MemoryDecay = 0.99;       // Memory decay
input bool     SaveBrains = true;        // Save trained brains
input bool     LoadBrains = false;       // Load saved brains
input int      SaveInterval = 500;       // Auto-save interval (in bars)

Nos sistemas de trading clássicos, o programador ou trader precisa descrever explicitamente cada regra: quando comprar, quando vender e onde posicionar os stops. Isso não exige apenas um profundo conhecimento do mercado, mas também parte do pressuposto de que somos capazes de formalizar todas as nuances do comportamento do mercado na forma de instruções claras. Mas e se o mercado for complexo demais para esse tipo de formalização? E se os padrões mudarem constantemente, se adaptarem e evoluírem? É justamente nesse ponto que o aprendizado por reforço revela todo o seu potencial.

O sistema analisado neste artigo foi implementado em MQL5 para a plataforma MetaTrader 5 e constitui uma arquitetura multiagente completa de aprendizado de máquina. Ele é capaz de operar de forma autônoma nos mercados financeiros, aperfeiçoando continuamente suas estratégias com base na experiência adquirida.


Filosofia da arquitetura: dos neurônios à inteligência coletiva

A arquitetura do sistema apresentado se assemelha mais à estrutura do cérebro humano do que à de um EA típico. Sua base é a ideia de uma estrutura hierárquica do conhecimento e da tomada coletiva de decisões. No nível mais baixo estão os neurônios de memória, unidades elementares de experiência, cada uma delas responsável por armazenar informações sobre um estado específico do mercado e a ação executada nesse estado.

struct MarketStateBinary
{
   int       binaryCode[32];
   double    volatility;
   double    momentum;
   double    trend;
   double    price;
   datetime  timestamp;
   double    outcome;
   int       activations;
};

Esses neurônios não existem de forma isolada. Eles são agrupados em estruturas de nível superior, que, para simplificar, chamei de "cérebros": agentes individuais capazes de tomar decisões de trading. Cada um desses cérebros funciona como um agente de trading independente, com suas próprias memórias, preferências e estilo de operação.

class CMachineBrain : public CObject
{
private:
   CArrayObj     m_neurons;
   Episode       m_episodes[];
   int           m_currentGeneration;
   double        m_brainComplexity;
   double        m_overallIntelligence;
   double        m_learningRate;
   double        m_explorationRate;
   double        m_gamma;
   double        m_memoryDecay;
   int           m_lastActivatedNeuron;
   MarketStateBinary m_currentMarketState;
   double        m_totalReward;
   int           m_totalTrades;
   int           m_successfulTrades;
   string        m_name;
   
   // Class balancing statistics
   int           m_buySignals;
   int           m_sellSignals;
   int           m_buyTrades;
   int           m_sellTrades;
   double        m_buyReward;
   double        m_sellReward;

Mas esse ainda não é o nível mais alto da hierarquia. Vários agentes-cérebro individuais se unem em uma inteligência coletiva, uma espécie de conselho de administração em que cada participante tem direito a voto, mas o peso desse voto depende do desempenho obtido anteriormente.

class CCollectiveMind
{
private:
   CMachineBrain* m_brains[];
   int            m_brainsCount;
   string         m_groupName;
   
public:
   CCollectiveMind(string name, int brainsCount, double lr, double eps, double gamma)
   {
      m_groupName = name;
      m_brainsCount = brainsCount;
      ArrayResize(m_brains, brainsCount);
      
      for(int i = 0; i < brainsCount; i++)
      {
         string brainName = name + "_Brain" + IntegerToString(i);
         double brainLR = lr * (0.8 + (MathRand() % 1000) / 10000.0 * 0.4);
         double brainEps = eps * (0.8 + (MathRand() % 1000) / 10000.0 * 0.4);
         m_brains[i] = new CMachineBrain(brainName, brainLR, brainEps, gamma);
      }
      
      Print("Collective mind '", name, "' created | Brains: ", brainsCount);
   }

Essa arquitetura de três níveis confere ao sistema várias propriedades importantes. Em primeiro lugar, permite armazenar e utilizar com eficiência uma enorme quantidade de experiência. Cada neurônio de memória representa um fragmento minúsculo de conhecimento sobre o mercado, mas, em conjunto, eles formam uma visão abrangente da dinâmica de mercado. Em segundo lugar, o uso de múltiplos agentes diversifica as decisões e reduz o risco de erros catastróficos. Mesmo que um dos cérebros chegue a uma conclusão equivocada, os demais podem corrigir a decisão final. Em terceiro lugar, o sistema se torna mais robusto a mudanças nas condições de mercado, pois diferentes agentes podem se especializar em tipos distintos de situações de mercado.


Anatomia do neurônio de memória: quantização da experiência de mercado

Vamos examinar em detalhes o componente básico do sistema: o neurônio de memória. Ele não é apenas um registro em um banco de dados, mas uma estrutura completa que encapsula um recorte coerente da experiência de mercado.

class CMemoryNeuron
{
public:
   MarketStateBinary marketState;
   double    action;
   double    reward;
   double    qValue;
   double    confidence;
   datetime  birthTime;
   int       activations;
   double    successRate;
   double    importance;
   int       generation;
   
   CMemoryNeuron()
   {
      action = 0.5;
      reward = 0;
      qValue = 0;
      confidence = 0.5;
      birthTime = TimeCurrent();
      activations = 0;
      successRate = 0.5;
      importance = 1.0;
      generation = 1;
      ArrayInitialize(marketState.binaryCode, 0);
      marketState.volatility = 0;
      marketState.momentum = 0;
      marketState.trend = 0;
      marketState.price = 0;
      marketState.timestamp = 0;
      marketState.outcome = 0;
      marketState.activations = 0;
   }
   
   void UpdateMetrics(double newReward)
   {
      activations++;
      reward = newReward;
      
      // Updating the Q-value
      qValue = qValue + LearningRate * (newReward - qValue);
      
      // Updating the success rate
      if(newReward > 0)
         successRate = successRate * 0.9 + 0.1;
      else if(newReward < 0)
         successRate = successRate * 0.9;
      
      // Updating importance
      importance = successRate * MathLog(activations + 1);
      confidence = (successRate + MathAbs(qValue)) / 2.0;
   }
};

Cada neurônio armazena uma representação binária do estado do mercado, codificada em um array de 32 bits. Por que usar uma representação binária? Porque ela permite armazenar os dados de forma compacta e comparar estados com rapidez. A codificação do estado é realizada por meio da quantização de atributos contínuos do mercado.

Além do próprio estado do mercado, o neurônio armazena a ação executada nesse estado. A ação é representada por um valor entre zero e um: valores próximos de zero correspondem à venda, valores próximos de um correspondem à compra, enquanto valores intermediários podem ser interpretados como diferentes graus de confiança em uma determinada direção ou até mesmo como uma decisão de não realizar uma operação. Essa representação contínua das ações oferece ao sistema muito mais flexibilidade do que uma escolha rígida entre compra e venda.

O neurônio também mantém estatísticas de suas ativações, registrando quantas vezes aquele estado específico ocorreu e foi utilizado na tomada de decisões. A porcentagem de operações bem-sucedidas realizadas com base nesse neurônio determina sua taxa de sucesso. A importância do neurônio é calculada combinando sua taxa de sucesso com a frequência de uso. Dessa forma, o sistema destaca os fragmentos de experiência que não apenas apresentam bons resultados, mas também correspondem a situações frequentes o suficiente para terem relevância prática.


Atributos estacionários: uma nova abordagem para representar o mercado

Ao contrário das versões anteriores do sistema, que utilizavam um conjunto limitado de características básicas, a versão 2.0 trabalha com um espaço de atributos ampliado, baseado em indicadores estacionários. Estacionariedade é um conceito fundamental na estatística de séries temporais e significa que as propriedades estatísticas de um processo não se alteram ao longo do tempo. Para sistemas de trading, isso é particularmente importante, pois a não estacionariedade das séries de preços é uma das principais causas da degradação de desempenho dos algoritmos.

O sistema extrai dos dados de mercado onze tipos de atributos estacionários. O Índice de Força Relativa (RSI) mede as condições de sobrecompra ou sobrevenda de um ativo, normalizando as informações sobre a velocidade e a magnitude das variações de preço em uma faixa de zero a cem. O Índice de Canal de Commodities (CCI) avalia o desvio do preço atual em relação à sua média estatística, permitindo identificar padrões cíclicos independentemente do nível absoluto dos preços.

O oscilador estocástico determina a posição do preço de fechamento atual em relação à faixa de preços de determinado período, fornecendo uma métrica adimensional de momentum. O Moving Average Convergence Divergence (MACD) concentra-se na interação entre tendência e momentum, identificando possíveis pontos de reversão por meio da análise da diferença entre médias móveis exponenciais.

O Average True Range (ATR) quantifica a volatilidade do mercado de forma independente da direção do movimento dos preços. As Bandas de Bollinger normalizam a posição do preço em relação à sua variabilidade estatística, formando um canal adaptativo que se expande e se contrai de acordo com as condições de mercado.

As relações entre médias móveis de diferentes períodos codificam informações sobre a força e a persistência da tendência. Os fractais de Bill Williams identificam extremos locais que podem sinalizar possíveis reversões. O Average Directional Index (ADX) mede a força da tendência sem considerar sua direção.

O Williams Percent Range, assim como o oscilador estocástico, avalia a posição do preço dentro da faixa recente, mas utiliza uma escala invertida e apresenta maior sensibilidade a períodos curtos. Por fim, as variações normalizadas de preço em diferentes janelas temporais capturam a dinâmica de curto e médio prazo sem depender de níveis específicos de preço.

void CalculateStationaryFeatures(const MqlRates &rates[], MarketStateBinary &state)
{
   int size = ArraySize(rates);
   if(size < 50) return;
   
   double prices[];
   ArrayResize(prices, size);
   for(int i = 0; i < size; i++)
      prices[i] = rates[i].close;
   
   // RSI (14 periods)
   double rsi = CalculateRSI(prices, 14);
   state.binaryCode[0] = rsi > 70 ? 1 : 0;
   state.binaryCode[1] = rsi < 30 ? 1 : 0;
   state.binaryCode[2] = rsi > 50 ? 1 : 0;
   
   // CCI (20 periods)  
   double cci = CalculateCCI(rates, 20);
   state.binaryCode[3] = cci > 100 ? 1 : 0;
   state.binaryCode[4] = cci < -100 ? 1 : 0;
   state.binaryCode[5] = cci > 0 ? 1 : 0;
   
   // Stochastic (14, 3, 3)
   double stoch = CalculateStochastic(rates, 14, 3, 3);
   state.binaryCode[6] = stoch > 80 ? 1 : 0;
   state.binaryCode[7] = stoch < 20 ? 1 : 0;
   state.binaryCode[8] = stoch > 50 ? 1 : 0;
   
   // MACD (12, 26, 9)
   double macd, signal;
   CalculateMACD(prices, 12, 26, 9, macd, signal);
   state.binaryCode[9] = macd > signal ? 1 : 0;
   state.binaryCode[10] = MathAbs(macd - signal) > 0.0001 ? 1 : 0;
   state.binaryCode[11] = macd > 0 ? 1 : 0;
   
   // ATR (14 periods)
   double atr = CalculateATR(rates, 14);
   double atrNorm = atr / rates[size-1].close;
   state.binaryCode[12] = atrNorm > 0.01 ? 1 : 0;
   state.binaryCode[13] = atrNorm > 0.02 ? 1 : 0;
   state.binaryCode[14] = atrNorm > 0.005 ? 1 : 0;
   
   // Bollinger Bands (20, 2)
   double upper, middle, lower;
   CalculateBollingerBands(prices, 20, 2, upper, middle, lower);
   double bbPos = (prices[size-1] - lower) / (upper - lower);
   state.binaryCode[15] = bbPos > 0.8 ? 1 : 0;
   state.binaryCode[16] = bbPos < 0.2 ? 1 : 0;
   state.binaryCode[17] = bbPos > 0.5 ? 1 : 0;
   
   // MA Ratios (20 vs. 50)
   double ma20 = CalculateMA(prices, 20);
   double ma50 = CalculateMA(prices, 50);
   state.binaryCode[18] = ma20 > ma50 ? 1 : 0;
   state.binaryCode[19] = (ma20 - ma50) / ma50 > 0.01 ? 1 : 0;
   state.binaryCode[20] = prices[size-1] > ma20 ? 1 : 0;
   
   // Fractals (5 periods)
   int fractal = DetectFractal(rates, 5);
   state.binaryCode[21] = fractal == 1 ? 1 : 0;
   state.binaryCode[22] = fractal == -1 ? 1 : 0;
   state.binaryCode[23] = fractal == 0 ? 1 : 0;
   
   // ADX (14 periods)
   double adx = CalculateADX(rates, 14);
   state.binaryCode[24] = adx > 25 ? 1 : 0;
   state.binaryCode[25] = adx > 40 ? 1 : 0;
   state.binaryCode[26] = adx > 15 ? 1 : 0;
   
   // Williams %R (14 periods)
   double willR = CalculateWilliamsR(rates, 14);
   state.binaryCode[27] = willR > -20 ? 1 : 0;
   state.binaryCode[28] = willR < -80 ? 1 : 0;
   state.binaryCode[29] = willR > -50 ? 1 : 0;
   
   // Price changes (10 bars)
   double priceChange = (prices[size-1] - prices[size-11]) / prices[size-11];
   state.binaryCode[30] = priceChange > 0.01 ? 1 : 0;
   state.binaryCode[31] = priceChange < -0.01 ? 1 : 0;
   
   // Additional metrics
   state.volatility = atrNorm;
   state.momentum = priceChange;
   state.trend = (ma20 - ma50) / ma50;
   state.price = prices[size-1];
   state.timestamp = rates[size-1].time;
}

Cada um desses indicadores oferece uma perspectiva própria sobre a dinâmica do mercado e, em conjunto, eles formam uma representação multidimensional do estado do mercado muito mais rica do que uma simples análise das variações de preço. Um aspecto fundamental é que todos esses atributos são estacionários ou aproximadamente estacionários: suas propriedades estatísticas permanecem relativamente estáveis ao longo do tempo, tornando o aprendizado mais eficiente e os resultados mais confiáveis.


Mecânica do aprendizado: da exploração à proficiência

O núcleo de qualquer sistema de aprendizado por reforço é o algoritmo responsável por atualizar as estimativas de valor das ações com base na experiência adquirida. Neste sistema, utilizamos uma versão modificada do Q-learning, adaptada às particularidades dos mercados financeiros. A fórmula do Q-learning é elegante em sua simplicidade: o novo valor Q é calculado somando-se ao valor anterior o produto da taxa de aprendizado pela diferença entre a recompensa observada e a estimativa atual.

Essa fórmula traduz a ideia de aproximação gradual da estimativa ao valor real das ações. Se a recompensa obtida for maior do que a estimativa atual, o valor Q aumenta, indicando ao sistema que aquela ação, naquele estado, foi melhor do que se esperava. Se o resultado for desfavorável, a estimativa diminui. A taxa de aprendizado determina com que intensidade o sistema atualiza suas estimativas: valores altos permitem adaptação rápida, mas podem provocar instabilidade, enquanto valores baixos proporcionam um aprendizado mais gradual e conservador.

void Learn(double reward)
{
   if(m_lastActivatedNeuron >= 0 && m_lastActivatedNeuron < m_neurons.Total())
   {
      CMemoryNeuron *neuron = m_neurons.At(m_lastActivatedNeuron);
      neuron.UpdateMetrics(reward);
      
      // Class-based tracking
      if(neuron.action > 0.6) // BUY
      {
         m_buyTrades++;
         m_buyReward += reward;
      }
      else if(neuron.action < 0.4) // SELL
      {
         m_sellTrades++;
         m_sellReward += reward;
      }
      
      if(reward > 0)
         m_successfulTrades++;
      
      m_totalReward += reward;
      m_totalTrades++;
   }
   
   // Periodic cleanup and evolution
   if(m_totalTrades % 100 == 0 && m_totalTrades > 0)
   {
      Prune();
      Evolve();
   }
}

No entanto, apenas atualizar os valores Q não é suficiente. O sistema precisa resolver um dilema fundamental do aprendizado por reforço: o equilíbrio entre explorar estratégias desconhecidas e aproveitar boas soluções já conhecidas. Se o agente sempre escolher a ação com o maior valor Q atual, jamais descobrirá alternativas potencialmente melhores. Por outro lado, se estiver sempre experimentando, não conseguirá aproveitar de forma eficiente o conhecimento acumulado.

double Think(double &features[])
{
   EncodeToBinary(features, m_currentMarketState);
   int similarNeuron = FindSimilarState(m_currentMarketState);
   double action = 0.5;
   double randomValue = (double)(MathRand() % 10001) / 10000.0;
   
   if(m_totalTrades < 150)
   {
      // Initial exploration phase
      int cycle = m_totalTrades % 6;
      if(cycle == 0 || cycle == 1)
         action = 0.75 + randomValue * 0.2;  // BUY
      else if(cycle == 2 || cycle == 3)
         action = 0.1 + randomValue * 0.2;   // SELL
      else
         action = 0.4 + randomValue * 0.2;   // HOLD
      
      m_lastActivatedNeuron = -1;
      
      if(m_totalTrades % 10 == 0 && ShowDetailedLog)
         Print("Exploration: Trade ", m_totalTrades, " | Action: ", DoubleToString(action, 3));
   }
   else if(similarNeuron >= 0)
   {
      CMemoryNeuron *neuron = m_neurons.At(similarNeuron);
      
      if(randomValue > m_explorationRate)
      {
         // Exploitation
         action = neuron.action;
         
         if(neuron.successRate > 0.6)
            action = MathMin(1.0, action * 1.1);
         else if(neuron.successRate < 0.4)
            action = MathMax(0.0, action * 0.9);
         
         if(m_totalTrades % 20 == 0 && ShowDetailedLog)
            Print("Exploit: Action=", DoubleToString(action, 3), 
                  " | SR=", DoubleToString(neuron.successRate, 2));
      }
      else
      {
         // Exploration
         action = randomValue;
         
         if(m_totalTrades % 20 == 0 && ShowDetailedLog)
            Print("Explore: Random=", DoubleToString(action, 3));
      }
      
      m_lastActivatedNeuron = similarNeuron;
      neuron.activations++;
   }
   else
   {
      // New state
      action = randomValue;
      m_lastActivatedNeuron = -1;
      
      if(m_totalTrades % 20 == 0 && ShowDetailedLog)
         Print("New state: Random=", DoubleToString(action, 3));
   }
   
   m_overallIntelligence = GetSuccessRate() * m_brainComplexity * 0.1;
   
   return action;
}

O sistema apresentado resolve esse dilema por meio de uma estratégia em várias etapas. Na fase inicial do aprendizado, durante as primeiras cento e cinquenta operações, o sistema permanece em modo de exploração ativa. Nessa etapa, ele não se baseia na experiência acumulada, mas testa ciclicamente diferentes tipos de ação: algumas operações de compra, algumas de venda e alguns períodos sem operar. É como um trader iniciante que ainda não formou uma visão clara do mercado e experimenta diferentes abordagens para descobrir o que funciona.

Após o término da fase inicial de exploração, o sistema passa a operar em um regime mais sofisticado. A cada decisão, é gerado um número aleatório. Se esse número for maior que o parâmetro epsilon, que define o nível de exploração, o sistema escolhe a melhor ação conhecida e aproveita a experiência acumulada. Se o número aleatório for menor que epsilon, é executada uma ação exploratória: o sistema testa uma alternativa nova, mesmo que ela pareça subótima com base no conhecimento disponível naquele momento.


Balanceamento de classes: solução para o problema do desbalanceamento

Uma das principais inovações da versão 2.0 é o sistema integrado de balanceamento de classes aplicado às decisões de trading. O problema do desbalanceamento de classes é bem conhecido em aprendizado de máquina: quando uma classe aparece com frequência muito maior nos dados de treinamento, o modelo pode aprender a prever sistematicamente a classe predominante, ignorando as menos frequentes. Em sistemas de trading, isso pode se manifestar como uma tendência a abrir apenas posições de compra ou apenas posições de venda, levando ao aproveitamento inadequado das oportunidades de mercado e ao aumento do risco.

O sistema monitora a quantidade de sinais de compra e venda, bem como o desempenho de cada classe. Quando detecta um desbalanceamento significativo, isto é, quando mais de setenta por cento dos sinais pertencem a uma única classe, o mecanismo de correção é ativado.

double ApplyClassBalance(double action)
{
   int totalSignals = m_buySignals + m_sellSignals;
   if(totalSignals < 50) return action;
   
   double buyRatio = (double)m_buySignals / totalSignals;
   double sellRatio = (double)m_sellSignals / totalSignals;
   
   // Imbalance correction
   if(buyRatio > 0.70 && action > 0.6)
   {
      action = action * 0.7 - 0.15;
      action = MathMax(0.0, action);
      
      if(ShowDetailedLog && m_totalTrades % 50 == 0)
         Print("Balance: BUY ratio=", DoubleToString(buyRatio, 2), 
               " -> Shift toward SELL");
   }
   else if(sellRatio > 0.70 && action < 0.4)
   {
      action = action * 0.7 + 0.35;
      action = MathMin(1.0, action);
      
      if(ShowDetailedLog && m_totalTrades % 50 == 0)
         Print("Balance: SELL ratio=", DoubleToString(sellRatio, 2), 
               " -> Shift toward BUY");
   }
   
   // Reinforcing the successful class
   if(m_buyTrades > 10 && m_sellTrades > 10)
   {
      double buyWinRate = m_buyTrades > 0 ? m_buyReward / m_buyTrades : 0;
      double sellWinRate = m_sellTrades > 0 ? m_sellReward / m_sellTrades : 0;
      
      if(buyWinRate > sellWinRate + 0.3 && action > 0.4 && action < 0.6)
      {
         action = action * 1.2;
         action = MathMin(1.0, action);
      }
      else if(sellWinRate > buyWinRate + 0.3 && action > 0.4 && action < 0.6)
      {
         action = action * 0.8;
         action = MathMax(0.0, action);
      }
   }
   
   // Signal tracking
   if(action > 0.6)
      m_buySignals++;
   else if(action < 0.4)
      m_sellSignals++;
   
   return action;
}

A correção é feita modificando a ação proposta. Se o sistema estiver gerando compras em excesso e produzir mais um sinal de compra, esse sinal é atenuado por meio de uma transformação matemática, deslocando seu valor para mais perto da zona neutra ou até mesmo em direção à venda. A mesma lógica é aplicada quando há excesso de sinais de venda.

Além disso, o sistema analisa o desempenho de cada classe de ação. Se as compras gerarem sistematicamente lucros muito superiores aos das vendas, o sistema passa a favorecer os sinais de compra na zona neutra de incerteza, em que a decisão não é clara. Isso permite aproveitar vantagens estatísticas de forma eficiente sem ignorar por completo a classe com desempenho inferior.

Essa abordagem garante a diversificação das decisões de trading e impede o surgimento de um viés sistemático, que pode ter consequências catastróficas quando as condições de mercado mudam. Um sistema que tenha aprendido a operar em apenas uma direção ficará praticamente sem capacidade de reação quando o mercado mudar de direção.


Gerenciamento da complexidade: pruning e evolução

À medida que o aprendizado avança, o número de neurônios no sistema cresce continuamente. Cada novo estado de mercado pode dar origem a um novo neurônio e, se esse crescimento não for controlado, o sistema rapidamente ficará grande demais para ser gerenciado com eficiência. O problema, porém, não se resume ao tamanho: um número excessivo de neurônios favorece o sobreajuste, fazendo com que o sistema memorize flutuações aleatórias do mercado em vez de identificar padrões reais.

void Prune()
{
   if(m_neurons.Total() < 1000) return;
   
   int removed = 0;
   
   for(int i = m_neurons.Total() - 1; i >= 0 && removed < 100; i--)
   {
      CMemoryNeuron *neuron = m_neurons.At(i);
      
      if(neuron.activations > 10 && neuron.successRate < 0.3)
      {
         m_neurons.Delete(i);
         removed++;
      }
   }
   
   if(removed > 0 && ShowDetailedLog)
      Print("Pruning: removed ", removed, " neurons");
}

O pruning é ativado automaticamente quando o número de neurônios ultrapassa mil. Nesse momento, o sistema passa a analisar sua memória em busca de neurônios inúteis ou prejudiciais. Os critérios de seleção são simples, mas eficazes: o neurônio precisa ter sido ativado vezes suficientes para que sua qualidade possa ser avaliada com alguma significância estatística e, ao mesmo tempo, apresentar uma baixa taxa de sucesso.

Um neurônio que tenha sido ativado mais de dez vezes, mas apresente uma taxa de sucesso inferior a trinta por cento, representa claramente uma estratégia ruim. Não se trata de um fracasso ocasional, mas de um padrão persistente de perdas. Esses neurônios são eliminados sem hesitação. Uma única operação de pruning pode remover até cem neurônios, evitando o crescimento descontrolado da rede e liberando recursos para regiões mais promissoras do espaço de estados.

A cada cem operações, além do pruning, ocorre uma transição evolutiva para uma nova geração:

void Evolve()
{
   m_currentGeneration++;
   m_explorationRate *= m_memoryDecay;
   m_explorationRate = MathMax(0.01, m_explorationRate);
   
   if(ShowDetailedLog)
      Print("Generation ", m_currentGeneration, 
            " | Exploration: ", DoubleToString(m_explorationRate, 3));
}

Isso não representa apenas o incremento de um contador, mas um marco simbólico na evolução do aprendizado do sistema. A cada nova geração, o nível de exploração diminui, indicando que o sistema já acumulou experiência suficiente para depender mais das estratégias que demonstraram bons resultados. O fator de decaimento aplicado ao parâmetro de exploração é definido como 0,99. Isso significa que, a cada geração, o nível de exploração é reduzido para noventa e nove por cento do valor anterior.


Sabedoria coletiva: tomada de decisões em conjunto

Um agente isolado, por mais sofisticado que seja, sempre está sujeito ao risco de erros sistemáticos. A inteligência coletiva reduz esse problema por meio da diversificação: em vez de depender de um único agente, o sistema utiliza um conjunto de cérebros independentes, cada um treinado com parâmetros ligeiramente diferentes e, consequentemente, capaz de desenvolver sua própria interpretação do mercado.

double ConsensusThink(double &state[])
{
   double totalAction = 0;
   double totalWeight = 0;
   
   for(int i = 0; i < m_brainsCount; i++)
   {
      if(CheckPointer(m_brains[i]) != POINTER_DYNAMIC) continue;
      
      double action = m_brains[i].Think(state);
      double weight = m_brains[i].GetSuccessRate() + 0.1;
      
      totalAction += action * weight;
      totalWeight += weight;
   }
   
   return totalWeight > 0 ? totalAction / totalWeight : 0.5;
}

Quando chega o momento de tomar uma decisão de trading, cada cérebro do conjunto gera sua recomendação de forma independente. Essas recomendações não são simplesmente submetidas a uma média: elas são ponderadas de acordo com a taxa de sucesso de cada agente. Um cérebro que apresenta bons resultados de forma consistente recebe maior peso no consenso final. Já um cérebro com desempenho inferior nas condições atuais de mercado exerce menor influência sobre a decisão.

A fórmula do consenso é elegante em sua simplicidade. Cada ação proposta por um agente é multiplicada por um peso correspondente à sua taxa de sucesso acrescida de uma pequena constante, normalmente 0,1. Essa constante garante que até mesmo um agente com taxa de sucesso igual a zero continue tendo alguma influência sobre a decisão. Em uma situação incomum, pode ser justamente sua avaliação fora do padrão que leve o sistema à melhor escolha.

void CollectiveRemember(double action)
{
   for(int i = 0; i < m_brainsCount; i++)
      if(CheckPointer(m_brains[i]) == POINTER_DYNAMIC)
         m_brains[i].Remember(action);
}

void CollectiveLearn(double reward)
{
   for(int i = 0; i < m_brainsCount; i++)
      if(CheckPointer(m_brains[i]) == POINTER_DYNAMIC)
         m_brains[i].Learn(reward);
}

O sistema também monitora métricas coletivas, como o número total de neurônios em todos os cérebros e o nível médio de inteligência dos agentes. Essas métricas fornecem uma visão de alto nível sobre a maturidade e a capacidade do sistema.


Codificação da realidade do mercado: dos preços aos padrões

Para que o sistema possa aprender de forma eficiente, ele precisa interpretar o mercado não como um fluxo caótico de preços, mas como uma sequência estruturada de estados. A codificação das informações de mercado em uma representação binária compacta exige encontrar um equilíbrio entre riqueza de informação e capacidade de generalização.

int FindSimilarState(MarketStateBinary &state)
{
   int bestMatch = -1;
   double bestSimilarity = 0;
   
   for(int i = 0; i < m_neurons.Total(); i++)
   {
      CMemoryNeuron *neuron = m_neurons.At(i);
      double similarity = CalculateSimilarity(state, neuron.marketState);
      
      if(similarity > bestSimilarity)
      {
         bestSimilarity = similarity;
         bestMatch = i;
      }
   }
   
   return bestSimilarity > 0.75 ? bestMatch : -1;
}

double CalculateSimilarity(MarketStateBinary &state1, MarketStateBinary &state2)
{
   int matches = 0;
   
   for(int i = 0; i < 32; i++)
      if(state1.binaryCode[i] == state2.binaryCode[i]) 
         matches++;
   
   return (double)matches / 32.0;
}

Quando o sistema se depara com um novo estado de mercado, calcula seu código binário e procura estados semelhantes em sua memória. A similaridade é determinada simplesmente pela contagem dos bits coincidentes. Se vinte e quatro dos trinta e dois bits forem iguais, a similaridade é de setenta e cinco por cento. O limiar de setenta e cinco por cento foi escolhido como um compromisso: alto o suficiente para garantir que os estados sejam realmente semelhantes, mas não tanto a ponto de impedir o sistema de generalizar o conhecimento adquirido para situações semelhantes.


Função de recompensa: a linguagem do feedback

Todo sistema de aprendizado precisa de um ciclo de feedback, ou seja, de uma forma de avaliar o quanto uma decisão foi boa ou ruim. Em um sistema de trading, a recompensa mais natural é o lucro ou o prejuízo obtido em uma operação.

double CalculateReward(double action, double entryPrice, double exitPrice, double spread)
{
   double reward;
   
   if(action > 0.6) // BUY
   {
      reward = (exitPrice - entryPrice - spread) / Point;
   }
   else if(action < 0.4) // SELL
   {
      reward = (entryPrice - exitPrice - spread) / Point;
   }
   else // HOLD
   {
      reward = 0;
   }
   
   // Normalization to the range [-1, +1]
   reward = MathMax(-1.0, MathMin(1.0, reward / 100.0));
   
   return reward;
}

Matematicamente, a recompensa é calculada como a diferença entre os preços, normalizada para o intervalo de menos um a mais um. Nas operações de venda, a lógica é invertida: a recompensa é positiva quando o preço cai. Um detalhe importante é que o spread, que representa um dos custos de transação, é descontado dessa diferença de preços.


Persistência do conhecimento: salvamento e restauração

O aprendizado por reforço exige recursos computacionais e tempo consideráveis. Perder toda a experiência acumulada a cada reinicialização do sistema seria um desperdício. Por isso, é fundamental poder salvar o estado dos modelos treinados e restaurá-lo sempre que necessário.

bool SaveToFile()
{
   string filename = BrainFolder + "/" + m_name + ".brain";
   int handle = FileOpen(filename, FILE_WRITE|FILE_BIN|FILE_COMMON);
   if(handle == INVALID_HANDLE) return false;
   
   // Metadata
   FileWriteInteger(handle, m_neurons.Total());
   FileWriteInteger(handle, m_currentGeneration);
   FileWriteDouble(handle, m_totalReward);
   FileWriteInteger(handle, m_totalTrades);
   FileWriteInteger(handle, m_successfulTrades);
   FileWriteDouble(handle, m_learningRate);
   FileWriteDouble(handle, m_explorationRate);
   FileWriteDouble(handle, m_gamma);
   
   // Balancing Statistics
   FileWriteInteger(handle, m_buySignals);
   FileWriteInteger(handle, m_sellSignals);
   FileWriteInteger(handle, m_buyTrades);
   FileWriteInteger(handle, m_sellTrades);
   FileWriteDouble(handle, m_buyReward);
   FileWriteDouble(handle, m_sellReward);
   
   // Neurons
   int toSave = MathMin(m_neurons.Total(), 5000);
   for(int i = 0; i < toSave; i++)
   {
      CMemoryNeuron *n = m_neurons.At(i);
      
      for(int j = 0; j < 32; j++)
         FileWriteInteger(handle, n.marketState.binaryCode[j]);
      
      FileWriteDouble(handle, n.action);
      FileWriteDouble(handle, n.qValue);
      FileWriteInteger(handle, n.activations);
      FileWriteDouble(handle, n.successRate);
      FileWriteDouble(handle, n.importance);
      FileWriteInteger(handle, n.generation);
   }
   
   FileClose(handle);
   Print("Brain '", m_name, "' saved: ", toSave, " neurons");
   return true;
}

Cada cérebro do sistema pode salvar seu estado em um arquivo binário. Esse arquivo contém todas as informações necessárias para restaurá-lo por completo: número de neurônios, geração atual, recompensa acumulada, número total de operações, quantidade de operações bem-sucedidas, parâmetros de aprendizado e estatísticas de balanceamento de classes. Em seguida, o estado completo de cada neurônio também é armazenado.

A restauração a partir do arquivo ocorre durante a inicialização do cérebro. O sistema procura o arquivo correspondente e, se ele existir, lê todos os dados e recria o array de neurônios. Após o carregamento, o sistema retorna exatamente ao estado em que se encontrava no momento do último salvamento e pode retomar o aprendizado ou o trading a partir do ponto em que havia parado.


Resultados práticos e perspectivas de desenvolvimento

Vamos analisar o teste do sistema sem qualquer treinamento prévio, ou seja, com autoaprendizado totalmente online desde o início, utilizando dados inteiramente novos, realizado ao longo de 2025 no par EURUSD, usando preços de abertura do timeframe H1:

A seguir estão as estatísticas do sistema, e os resultados são bastante satisfatórios, mesmo com uma lógica de trading extremamente simples, baseada em Stop Loss e Take Profit fixos:

Como podemos observar, a rede neural se adaptou ao mercado, identificou seus padrões e obteve um retorno de +41% em menos de um ano de trading.

Veja agora como esse sistema se comporta ao utilizar um circuito quântico complexo durante o Q-learning, combinado com um sistema mais eficiente de gestão de ordens e riscos, também no EURUSD, usando preços de abertura do timeframe H1 e com autoaprendizado completo:

Os testes com dados históricos demonstram a capacidade do sistema de se adaptar e aprender de forma autônoma. Mesmo sem treinamento prévio, ele apresenta um desempenho positivo consistente, identificando padrões de mercado por conta própria e gerando lucro.

Uma das principais melhorias da versão 2.0 é a solução para o problema do desbalanceamento de classes. Anteriormente, o sistema tendia a favorecer compras ou vendas; agora, o balanceamento integrado proporciona uma distribuição mais uniforme das decisões, aumentando a robustez e a diversificação.

O espaço de atributos ampliado, baseado em 11 tipos de indicadores estacionários, oferece ao sistema uma representação mais rica da dinâmica do mercado, permitindo detectar padrões sutis e se adaptar a diferentes regimes de mercado.

O módulo de persistência do conhecimento permite salvar e carregar modelos treinados, eliminando a necessidade de repetir o treinamento e possibilitando o aprendizado online contínuo.

As principais limitações são a elevada demanda por recursos e o risco de sobreajuste. À medida que o número de neurônios aumenta, a tomada de decisões se torna mais lenta, e o pruning resolve esse problema apenas parcialmente. Além disso, o sistema pode perder eficiência quando as condições de mercado mudam, o que exige monitoramento constante e adaptação contínua do modelo.


Conclusão

O sistema demonstra que é possível implementar em MQL5 uma estratégia de trading completa baseada em aprendizado por reforço, de forma compacta e eficiente. A arquitetura de inteligência coletiva aumenta a robustez do sistema e sua capacidade de adaptação às mudanças nas condições de mercado, enquanto cada módulo, do balanceamento de classes ao pruning, amplia a capacidade de aprendizado do sistema.

O aprendizado por reforço permite identificar padrões ocultos do mercado e ampliar a capacidade analítica humana, em vez de substituí-la. A evolução futura, com a integração de arquiteturas profundas, mecanismos de atenção e meta-aprendizado, abrirá caminho para sistemas ainda mais flexíveis e adaptativos.

Em última análise, o valor dessas soluções é determinado por sua capacidade de gerar lucros de forma consistente com risco controlado. O sistema apresentado oferece uma base sólida para as futuras gerações de sistemas de trading autoevolutivos.

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

Arquivos anexados |
BrainRL_Expert_v2.mq5 (105.98 KB)
Últimos Comentários | Ir para discussão (2)
Fedor Smirnov
Fedor Smirnov | 9 jun. 2026 em 08:52
Li o artigo de uma só vez. É uma pena que o consultor integrado não corresponda de forma alguma à descrição.
Better Trader Every Day
Ciro Soto | 13 ago. 2026 em 02:21

O arquivo mq5 fornecido neste artigo não é o mesmo mencionado no artigo...!!

Compilei e executei o arquivo fornecido, mas o desempenho foi ruim.

O autor deveria enviar o arquivo mq5 correto.

Que vergonha para o editor e os revisores que permitiram que isso acontecesse.

Caminhe em novos trilhos: Personalize indicadores no MQL5 Caminhe em novos trilhos: Personalize indicadores no MQL5
Vou agora listar todas as possibilidades novas e recursos do novo terminal e linguagem. Elas são várias, e algumas novidades valem a discussão em um artigo separado. Além disso, não há códigos aqui escritos com programação orientada ao objeto, é um tópico muito importante para ser simplesmente mencionado em um contexto como vantagens adicionais para os desenvolvedores. Neste artigo vamos considerar os indicadores, sua estrutura, desenho, tipos e seus detalhes de programação em comparação com o MQL4. Espero que este artigo seja útil tanto para desenvolvedores iniciantes quanto para experientes, talvez alguns deles encontrem algo novo.
Redes neurais no trading: modelos de refinamento iterativo de previsões (Conclusão) Redes neurais no trading: modelos de refinamento iterativo de previsões (Conclusão)
Apresentamos o framework RAFT, uma poderosa ferramenta para análise e previsão de séries temporais financeiras. Sua arquitetura flexível e otimizada proporciona precisão nas previsões, estabilidade operacional e processamento de dados mais rápido. O RAFT reduz o risco de erros e facilita o desenvolvimento de estratégias de trading eficientes.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Métodos de amostragem MCMC: algoritmo de amostragem por fatias (Slice Sampling) Métodos de amostragem MCMC: algoritmo de amostragem por fatias (Slice Sampling)
Neste artigo, estudamos o método de amostragem por fatias (slice sampling), um algoritmo MCMC adaptativo que ajusta automaticamente os parâmetros de amostragem. Sua eficiência é demonstrada em modelos bayesianos de regressão linear e logística, e os resultados são comparados com métodos frequentistas clássicos.