Sistema de autoaprendizado por reforço para trading algorítmico em MQL5
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
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Caminhe em novos trilhos: Personalize indicadores no MQL5
Redes neurais no trading: modelos de refinamento iterativo de previsões (Conclusão)
Está chegando o novo MetaTrader 5 e MQL5
Métodos de amostragem MCMC: algoritmo de amostragem por fatias (Slice Sampling)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
Foi publicado o artigo “Sistema de autoaprendizagem com reforço para negociação algorítmica” no MQL5:
Autor: Yevgeniy Koshtenko
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.