Redes neurais em trading: Ator–Diretor–Crítico (Conclusão)
Introdução
No artigo anterior, exploramos os aspectos teóricos do framework Ator–Diretor–Crítico (Actor–Director–Critic), que é uma versão expandida da arquitetura Actor–Critic. A arquitetura clássica Actor–Critic tornou-se a base de muitos algoritmos bem-sucedidos de aprendizado por reforço RL. Nela, o agente é dividido em duas partes: o Ator propõe as ações, e o Crítico as avalia com base na recompensa recebida do ambiente. Essa interação permite construir gradualmente uma estratégia em que as ações se tornam cada vez mais racionais e as avaliações, cada vez mais precisas. Entretanto, apesar de sua elegância e eficiência, essa arquitetura enfrenta sérias limitações quando aplicada a tarefas reais, especialmente no trading.
O principal problema surge nas fases iniciais do treinamento. Nesse estágio, o Ator ainda não sabe quais ações são úteis, e o Crítico não é capaz de avaliá-las adequadamente, pois também está em processo de aprendizado. Isso gera o chamado efeito de “caminhada cega”: o agente executa muitas ações aleatórias, ineficazes e, às vezes, até prejudiciais, recebendo um retorno não informativo ou atrasado. Em um ambiente de mercado, onde erros podem ter custos significativos, essa abordagem torna-se excessivamente arriscada e instável.
Para resolver esse problema fundamental, os autores do framework Actor–Director–Critic propuseram adicionar um novo componente, o Diretor, que introduz um canal adicional de avaliação das ações. Diferente do Crítico, que fornece uma avaliação contínua com base na recompensa do ambiente, o Diretor classifica as ações de forma binária: “adequada” ou “inadequada” à estratégia definida. Isso permite que o agente se oriente com mais rapidez e precisão no espaço das possíveis decisões.
É importante destacar que o Diretor não atua como um filtro nem restringe a liberdade de ação do agente. Pelo contrário, ele complementa a avaliação do Crítico, oferecendo um retorno mais categórico. Enquanto o Crítico pode estar incerto quanto à qualidade de uma ação (principalmente nas fases iniciais), o Diretor indica imediatamente se ela está alinhada ao modelo comportamental que aprendeu. Dessa forma, o Ator evita repetir ações claramente erradas, economizando recursos de aprendizado e acelerando o desenvolvimento de uma estratégia estável.
O resultado é uma interação sinérgica entre três componentes: o Ator aprende a escolher as ações, o Crítico as avalia em termos da recompensa esperada, e o Diretor indica claramente quais ações devem ser totalmente evitadas. Isso cria um sistema duplo de retroalimentação, contínuo e binário, que permite eliminar rapidamente direções improdutivas e concentrar o aprendizado em estratégias mais eficazes.
A visualização autoral do framework Actor–Director–Critic é apresentada a seguir.

Na parte prática do artigo anterior, foi apresentado um detalhamento da arquitetura das redes treináveis utilizadas. E aqui, é importante mencionar que foram introduzidas modificações consideráveis em relação à implementação original descrita no framework.
Antes de tudo, adaptamos os conceitos propostos ao framework multiagente HiSSD, discutido anteriormente. Além disso, em nossa implementação, tanto o Diretor quanto o Crítico são treinados com base em características latentes das habilidades gerais do Agente, isto é, representações comprimidas obtidas das camadas internas do Codificador do estado do ambiente, em vez de usarem os atributos brutos. Graças a isso, forma-se uma representação mais generalizada dos padrões de ação, permitindo avaliá-los no contexto da lógica comportamental global, e não apenas em uma situação de mercado específica. Essa abordagem oferece um retorno mais estável e estrategicamente consistente, fator crucial em condições de informação limitada e alta incerteza, como costuma ocorrer nos mercados financeiros.
Nesta etapa, o foco do artigo desloca-se para o processo de treinamento das redes. Esse processo também passou por algumas modificações em comparação ao método original.
Uma das diferenças fundamentais foi a divisão do aprendizado em duas fases. Na primeira, realiza-se o treinamento offline de todos os componentes do sistema com base em um conjunto de dados previamente coletado. Essa fase permite que o agente acumule experiência inicial e desenvolva uma estratégia comportamental básica sem o risco de afetar as operações reais. Nesse estágio, cada modelo é treinado de acordo com sua própria função objetivo, ajustada ao seu papel dentro da arquitetura.
Na segunda fase, inicia-se o ajuste fino online das redes. Nessa etapa, ocorre a interação direta do agente com o ambiente. O Ator refina sua política de comportamento em tempo real, enquanto o Crítico e o Diretor continuam o aprendizado, aprimorando a qualidade das avaliações e classificações com base nos novos dados recebidos. Essa fase possibilita que o agente se adapte à situação atual do mercado, preservando a direção estratégica estabelecida durante o treinamento offline.
Essa abordagem em duas fases garante um equilíbrio entre estabilidade e adaptabilidade. O agente forma uma política inicial sólida e confiável, que depois é refinada de acordo com as condições reais. Como resultado, espera-se obter um sistema estável e com aprendizado eficiente, capaz de se ajustar às mudanças do mercado sem perder sua coerência estratégica.
Treinamento offline
O algoritmo da primeira etapa de aprendizado (offline) é implementado na forma de um EA localizado em "...\Experts\ADC\Study.mq5". A maior parte do seu código foi aproveitada de um projeto anterior, o que é totalmente natural. A base da arquitetura das redes treináveis foi construída sobre os desenvolvimentos do framework HiSSD, com o qual já tivemos contato.
Essa continuidade permitiu utilizar soluções já testadas, evitando a necessidade de reinventar processos. No entanto, não foi possível dispensar modificações por completo.
A adição de dois novos componentes (o Crítico e o Diretor) ampliou significativamente as capacidades do sistema. A integração deles exigiu ajustes na lógica do programa de treinamento das redes. Dentro do escopo deste artigo, analisaremos em detalhe apenas o algoritmo do método Train, que concentra praticamente todo o processo direto de aprendizado das redes.
Como antes, esse método não recebe parâmetros; todos os dados necessários são obtidos de variáveis globais previamente inicializadas. No corpo do método, são criadas e inicializadas várias variáveis locais, utilizadas para armazenar temporariamente informações intermediárias durante o processo de aprendizado.
void Train(void) { //--- vector<float> probability = vector<float>::Full(Buffer.Size(), 1.0f / Buffer.Size()); //--- vector<float> result, target, state; matrix<float> fstate = matrix<float>::Zeros(1, NForecast * BarDescr); bool Stop = false; //--- uint ticks = GetTickCount();
Após essa preparação inicial, passa-se à construção propriamente dita do processo de aprendizado das redes, organizado por meio de um sistema de laços aninhados. O laço externo percorre os minilotes de dados ao longo de um número definido de iterações de aprendizado.
for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch) { int tr = SampleTrajectory(probability); int start = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - NForecast - Batch)); if(start <= 0) { iter -= Batch; continue; }
Na fase inicial do treinamento offline, realiza-se o amostragem de uma trajetória armazenada no buffer de replay de experiências. Em seguida, dentro dessa trajetória, é selecionado aleatoriamente um estado específico do ambiente, que servirá como ponto de partida para a formação de um novo minilote de dados de aprendizado das redes. Dessa forma, garante-se a diversidade dos exemplos de treinamento.
Antes de começar a trabalhar com esse novo minilote, é executado um procedimento obrigatório de limpeza dos buffers internos temporários de todas as redes treináveis. Essa etapa é especialmente importante para os blocos recorrentes, que, como se sabe, possuem “memória” e podem reter o contexto de estados anteriores, acumulando informações de passos de tempo anteriores. No entanto, ao alternar entre segmentos de trajetória não relacionados, essa memória pode causar resultados indesejados.
O contexto armazenado nos estados ocultos do minilote anterior torna-se irrelevante. Ele não tem relação com a nova amostra e pode causar distorções nos sinais gerados. Por isso, o reset dos estados temporários antes de cada novo minilote não é apenas uma precaução, isto é, uma condição indispensável para garantir uma análise correta e independente do fragmento histórico de dados que está sendo processado.
if( !cEncoder.Clear() || !cTask.Clear() || !cActor.Clear() || !cProbability.Clear() || !cDirector.Clear() || !cCritic.Clear() ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } result = vector<float>::Zeros(NActions);
Após a inicialização do minilote, passamos para a próxima etapa: a organização do laço interno responsável pela iteração sequencial dos estados do ambiente. Esses estados são processados estritamente em ordem cronológica, exatamente como foram observados originalmente pelo agente durante sua interação com o ambiente de negociação.
A preservação da sequência temporal não é uma mera formalidade, mas um requisito fundamental para o aprendizado eficaz dos blocos recorrentes das redes. Diferentemente das camadas totalmente conectadas, que operam sobre dados isolados, as redes neurais recorrentes formam estados ocultos baseados na informação acumulada de passos anteriores. Sua força reside justamente na capacidade de identificar dependências temporais, padrões de comportamento e sinais recorrentes dentro das séries temporais.
Se a estrutura temporal dos dados for alterada, mesmo que parcialmente, a rede pode perder a capacidade de reconhecer as relações de causa e efeito que se manifestam precisamente na dinâmica dos eventos. Isso faz com que o valor desses exemplos de treinamento caia drasticamente.
Por esse motivo, cada passo temporal dentro do minilote é processado rigorosamente em sequência, isto é, sem embaralhamento, sem saltos, sem desvios. Esse método permite que os componentes recorrentes construam progressivamente uma representação interna coerente do contexto, acumulando conhecimento sobre a evolução do ambiente de mercado.
for(int i = start; i < MathMin(Buffer[tr].Total, start + Batch); i++) { if(!state.Assign(Buffer[tr].States[i].state) || MathAbs(state).Sum() == 0 || !bState.AssignArray(state)) { iter -= Batch + start - i; break; } //---
Dentro do laço interno, os dados referentes ao estado analisado do ambiente são carregados do buffer de replay de experiências, e então são formadas as harmônicas do carimbo temporal.
bTime.Clear(); double time = (double)Buffer[tr].States[i].account[7]; double x = time / (double)(D'2024.01.01' - D'2023.01.01'); bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_MN1); bTime.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_W1); bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_D1); bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); if(bTime.GetIndex() >= 0) bTime.BufferWrite();
Em seguida, é construído o vetor de descrição do estado da conta.
//--- Account float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0]; float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1]; float profit = float(bState[0] / _Point * (result[0] - result[3])); bAccount.Clear(); bAccount.Add(1); bAccount.Add((PrevEquity + profit) / PrevEquity); bAccount.Add(profit / PrevEquity); bAccount.Add(MathMax(result[0] - result[3], 0)); bAccount.Add(MathMax(result[3] - result[0], 0)); bAccount.Add((bAccount[3] > 0 ? profit / PrevEquity : 0)); bAccount.Add((bAccount[4] > 0 ? profit / PrevEquity : 0)); bAccount.Add(0); bAccount.AddArray(GetPointer(bTime)); if(bAccount.GetIndex() >= 0) bAccount.BufferWrite();
Nesta fase, é importante destacar uma técnica metodológica essencial aplicada no processo de treinamento offline. Trata-se do uso do chamado “trajeto quase perfeito”. Nesse método, o algoritmo é autorizado — de maneira controlada — a romper o isolamento cronológico, permitindo-se “espiar” estados futuros do ambiente que já estão contidos no conjunto de treinamento.
Com base nessas informações, formamos um tensor de ações aprimorado, que reflete decisões estrategicamente mais ponderadas. É evidente que, com grande probabilidade, essas decisões diferem daquelas tomadas pelo agente no momento da interação original com o mercado. Esse tensor não replica o comportamento do agente, mas serve como um padrão aproximado, isto é, uma referência para a qual o agente deve se orientar durante o processo de aprendizado. É justamente por isso que o chamamos de “trajeto quase perfeito”.
Contudo, essa estratégia gera discrepâncias naturais entre as ações reais executadas pelo agente e as ações ideais formadas posteriormente. Isso, por sua vez, exige correções em outros aspectos do modelo.
Em particular, torna-se necessário recalcular o vetor de descrição do estado da conta, para que ele corresponda às ações do “trajeto quase perfeito”. Sem esse alinhamento, o Agente seria treinado com dados inconsistentes, em que ações e resultados não se correspondem. O que inevitavelmente causaria distorções nos sinais de aprendizado.
Após preparar o conjunto de dados de entrada, chamamos sequencialmente os métodos de propagação para frente das redes treináveis, durante os quais são gerados certos valores preditivos. A tarefa do treinamento dessas redes consiste em minimizar a diferença entre esses valores previstos e os resultados esperados.
//--- Feed Forward if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!cTask.feedForward((CBufferFloat*)GetPointer(bState), 1, false, GetPointer(cEncoder), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!cActor.feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, GetPointer(cTask), -1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!cProbability.feedForward(GetPointer(cEncoder), LatentLayer, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
É importante observar que, nesta etapa, não realizamos a propagação para frente dos modelos responsáveis pela avaliação das ações do agente — o Crítico e o Diretor. Isso se deve ao uso do método do “trajeto quase perfeito”. Retornaremos a esse ponto mais adiante.
Depois da execução bem-sucedida das operações de propagação para frente das redes treináveis, passamos à formação dos valores-alvo. Inicialmente, carregamos do buffer de replay de experiências uma sequência de estados subsequentes do ambiente, dentro de um horizonte de planejamento definido.
//--- Look for target target = vector<float>::Zeros(NActions); bActions.AssignArray(target); if(!state.Assign(Buffer[tr].States[i + NForecast].state) || !state.Resize(NForecast * BarDescr) || MathAbs(state).Sum() == 0) { iter -= Batch + start - i; break; } if(!fstate.Resize(1, NForecast * BarDescr) || !fstate.Row(state, 0) || !fstate.Reshape(NForecast, BarDescr)) { iter -= Batch + start - i; break; } for(int j = 0; j < NForecast / 2; j++) { if(!fstate.SwapRows(j, NForecast - j - 1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } }
Esses dados servem como alvo para o Codificador do estado do ambiente. Assim, podemos realizar a propagação reversa dessa rede, ajustando seus parâmetros de modo a minimizar o desvio entre os valores previstos e os valores-alvo.
//--- State Encoder Result.AssignArray(fstate); if(!cEncoder.backProp(Result, (CBufferFloat*)NULL, NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
Essa mesma informação sobre os estados futuros do ambiente também serve de base para a formação do tensor da “operação de trading quase perfeita”. A construção dessa nova operação é feita levando em conta a operação anterior, o que permite modelar não ações impulsivas isoladas, mas sim uma estratégia de negociação contínua e coerente. O resultado é uma cadeia de decisões contextualmente conectadas, em que cada ação subsequente decorre logicamente da anterior. Isso é especialmente importante no contexto do treinamento offline, quando o agente não recebe feedback do ambiente em tempo real e precisa extrair padrões a partir de dados históricos já registrados.
Esse método permite neutralizar a influência do ruído de mercado, que se manifesta nos dados reais por meio de flutuações aleatórias. Na “trajetória quase perfeita”, o comportamento do agente torna-se mais suave, racional e estrategicamente consistente. Potencialmente, isso acelera o aprendizado do modelo e forma uma lógica de negociação estável, que posteriormente poderá ser adaptada ao modo online.
O processo de construção da “operação de trading quase perfeita” foi detalhado em um trabalho anterior e, portanto, não há necessidade de repeti-lo aqui.
O tensor formado dessa “operação de trading quase perfeita” serve como valor-alvo para o treinamento do nosso Ator-gerente de nível superior.
//--- Actor Policy if(!cActor.backProp(GetPointer(bActions), (CNet*)GetPointer(cTask), -1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
Neste ponto, passamos a um dos componentes mais importantes da arquitetura de aprendizado, em particular, o treinamento dos modelos responsáveis pela avaliação das ações do Ator, ou seja, o Crítico e o Diretor. São justamente esses elementos que geram o retorno de informação (feedback) que orienta a estratégia do agente, ajudando-o a distinguir ações eficazes das ineficazes.
Entretanto, deparamo-nos aqui com um problema fundamental: o espaço possível de ações em cada estado do ambiente é extremamente vasto. Avaliar todo o conjunto de decisões que o Ator poderia tomar é uma tarefa praticamente inviável dentro de limites computacionais razoáveis. Além disso, muitas dessas ações jamais seriam executadas pelo agente, o que as torna desprovidas de valor para o aprendizado.
Por esse motivo, durante o treinamento do Crítico, normalmente adotamos uma estratégia de avaliação local. Em vez de tentar abranger todo o espaço de ações, focamos em sua vizinhança, isto é, na região próxima das decisões realmente tomadas pelo agente naquele passo temporal. É nesse subespaço local que ocorre a busca por direções de otimização, ou seja, deslocamentos vetoriais que potencialmente podem aumentar a função objetivo, em outras palavras, melhorar a rentabilidade.
E é justamente aqui que o método da “trajetória quase perfeita” nos concede uma vantagem poderosa. Graças à existência de um comportamento de referência, obtido por meio da observação antecipada de estados futuros, podemos deslocar o foco da avaliação, do que o agente realmente fez para o que ele deveria ter feito. Em outras palavras, treinamos o Crítico não apenas para distinguir o bom do ruim, mas para se orientar em direção às ações próximas do ideal estratégico representado pela “trajetória quase perfeita”.
Dentro dessa abordagem, realizamos a propagação para frente do Crítico para avaliar a “operação de trading quase perfeita”.
//--- Critic if(!cCritic.feedForward(GetPointer(bActions), 1, false, (CNet*)GetPointer(cEncoder), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
Em seguida, executamos imediatamente as operações de propagação reversa, para minimizar a diferença entre a avaliação obtida e aquela calculada a partir dos dados históricos reais.
float reward = float((result[0] - result[3]) * fstate[0, 0] / Point()); Result.Clear(); if(!Result.Add(reward) || !cCritic.backProp(Result, (CNet*)GetPointer(cEncoder), LatentLayer) || !cEncoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
A situação do treinamento do Diretor é, em geral, semelhante à do Crítico, embora apresente uma série de nuances importantes. Assim como qualquer classificador, o Diretor requer um conjunto equilibrado de exemplos positivos e negativos, a partir dos quais possa aprender a distinguir as fronteiras entre as “boas” e as “más” ações do agente.
Com os exemplos positivos, tudo é relativamente claro. Eles correspondem às operações de trading formadas dentro da “trajetoria quase perfeita”. Essas ações representam decisões estrategicamente bem fundamentadas, construídas com base na análise de estados futuros do ambiente e, portanto, merecem alta avaliação.
No entanto, não é possível treinar um classificador confiável apenas com exemplos positivos. Também é necessário um conjunto representativo de exemplos negativos, que demonstrem comportamentos desalinhados com nossos objetivos. É justamente aí que surge uma dificuldade metodológica: o conjunto de treinamento, por natureza, não abrange todo o espaço potencial de ações, pois ele contém apenas as decisões que o agente realmente tomou no passado.
Para resolver esse problema, recorremos a uma técnica heurística simples, porém eficaz: usamos conjuntos de valores aleatórios, gerados dentro do intervalo permitido das ações do agente, como exemplos negativos. Tais ações aleatórias, com alta probabilidade, não correspondem às metas estratégicas do modelo e podem ser tratadas como ruído comportamental ou erros de decisão.
É importante ressaltar que o embaralhamento entre exemplos positivos e negativos durante o treinamento é feito de forma aleatória. Isso evita que o modelo se sobreajuste a uma das categorias e assegura uma fronteira de classificação mais estável. Essa abordagem torna o aprendizado do Diretor mais flexível e generalizável, além de produzir sinais mais nítidos e confiáveis, algo especialmente relevante em ambientes de alta incerteza, como os mercados financeiros.
Como resultado, o Diretor se transforma em uma bússola binária poderosa, capaz de rejeitar prontamente ações improdutivas e direcionar o agente para regiões mais promissoras do espaço estratégico.
//--- Director Result.Clear(); if((MathRand() / 32767.0) > 0.5) Result.Add(1); else { target = vector<float>::Zeros(NActions); for(int i = 0; i < NActions; i++) target[i] = float(MathRand() / 32767.0); bActions.AssignArray(target); Result.Add(0); } if(!cDirector.feedForward(GetPointer(bActions), 1, false, (CNet*)GetPointer(cEncoder), LatentLayer) || !cDirector.backProp(Result, (CNet*)GetPointer(cEncoder), LatentLayer) || !cEncoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
Na sequência, é realizado o treinamento do Controlador de baixo nível e do modelo preditivo responsável por estimar as probabilidades de direção do movimento de preço futuro. Esses blocos de código foram transferidos de um trabalho anterior sem modificações, onde já foram detalhados. Recomendo que o leitor os consulte separadamente para uma compreensão mais profunda. O código completo do EA de treinamento offline dos modelos está incluído em anexo.
Treinamento online
A próxima etapa do nosso trabalho consiste no ajuste fino das redes treináveis em modo online, onde enfrentamos um conjunto completamente diferente de tarefas e restrições. Enquanto na fase offline o foco estava em um aprendizado profundo e generalizado, baseado em dados retrospectivos e na “trajetória quase perfeita”, no modo online toda a atenção se volta para a adaptação em tempo real às condições do mercado.
Uma das principais vantagens do treinamento online é a obtenção direta de feedback do ambiente. O Ator toma uma decisão, a ação é executada e, quase imediatamente, o efeito dessa ação torna-se conhecido. Isso permite corrigir o comportamento do agente de forma ágil, reforçando as estratégias bem-sucedidas e descartando as ineficazes.
Entretanto, esse método também apresenta limitações significativas. A principal delas é a impossibilidade de “olhar para o futuro”, como era possível durante a construção da “trajetoria quase perfeita” na fase offline. No treinamento online, o agente toma decisões com base apenas no estado atual e em sua própria política, sem acesso a “informações futuras”. Essa mudança altera radicalmente as condições de aprendizado, exigindo a transição para uma abordagem mais clássica de aprendizado por reforço (Reinforcement Learning).
Nesse paradigma, o aprendizado é baseado em tentativa e erro, e a qualidade das decisões é avaliada apenas após conhecidas suas consequências. Por isso, o papel das redes de avaliação — o Crítico e o RDiretor — torna-se ainda mais crucial, pois elas funcionam como um sistema interno de orientação para o Ator, direcionando suas ações. Contudo, diferentemente do modo offline, essas redes agora passam por ajuste contínuo durante a própria atividade de negociação, adaptando-se dinamicamente às mudanças da conjuntura do mercado.
Mas vamos por partes. O algoritmo de aprendizado online é implementado no EA "...\Experts\ADC\StudyOnline.mq5". Como o espaço neste artigo é limitado, focaremos apenas na análise detalhada do método OnTick. É nesse método que o evento de chegada de um novo tick é processado. Onde implementamos o núcleo do algoritmo de aprendizado.
void OnTick() { //--- if(!IsNewBar()) return;
Antes de mais nada, é importante observar que nossas redes analisam os dados históricos apenas com base nas velas já fechadas e não foram projetadas para reagir instantaneamente a cada tick. Assim, até o fechamento de uma nova vela, não há necessidade de realizar análises detalhadas do ambiente, pois o resultado permanecerá o mesmo. Para evitar processamento desnecessário, no início do método é feita uma verificação para identificar se houve o fechamento de Uma nova barra. Caso contrário, o sistema apenas aguarda o próximo tick.
Quando ocorre o fechamento de uma nova barra, solicitamos ao terminal os dados históricos em uma profundidade predefinida e formamos os buffers de dados de entrada.
int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, 1), HistoryBars, Rates); if(!ArraySetAsSeries(Rates, true)) return; //--- RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh(); Symb.Refresh(); Symb.RefreshRates(); //--- float atr = 0; for(int b = 0; b < (int)HistoryBars; b++) { float open = (float)Rates[b].open; float rsi = (float)RSI.Main(b); float cci = (float)CCI.Main(b); atr = (float)ATR.Main(b); float macd = (float)MACD.Main(b); float sign = (float)MACD.Signal(b); if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE) continue; //--- int shift = b * BarDescr; sState.state[shift] = (float)(Rates[b].close - open); sState.state[shift + 1] = (float)(Rates[b].high - open); sState.state[shift + 2] = (float)(Rates[b].low - open); sState.state[shift + 3] = (float)(Rates[b].tick_volume / 1000.0f); sState.state[shift + 4] = rsi; sState.state[shift + 5] = cci; sState.state[shift + 6] = atr; sState.state[shift + 7] = macd; sState.state[shift + 8] = sign; } //---
Em seguida, carregamos as informações sobre o estado da conta e as posições abertas.
sState.account[0] = (float)AccountInfoDouble(ACCOUNT_BALANCE); sState.account[1] = (float)AccountInfoDouble(ACCOUNT_EQUITY); //--- double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0; double position_discount = 0; double multiplyer = 1.0 / (60.0 * 60.0 * 10.0); int total = PositionsTotal(); datetime current = TimeCurrent(); for(int i = 0; i < total; i++) { if(PositionGetSymbol(i) != Symb.Name()) continue; double profit = PositionGetDouble(POSITION_PROFIT); switch((int)PositionGetInteger(POSITION_TYPE)) { case POSITION_TYPE_BUY: buy_value += PositionGetDouble(POSITION_VOLUME); buy_profit += profit; break; case POSITION_TYPE_SELL: sell_value += PositionGetDouble(POSITION_VOLUME); sell_profit += profit; break; } position_discount += profit - (current - PositionGetInteger(POSITION_TIME)) * multiplyer * MathAbs(profit); } sState.account[2] = (float)buy_value; sState.account[3] = (float)sell_value; sState.account[4] = (float)buy_profit; sState.account[5] = (float)sell_profit; sState.account[6] = (float)position_discount; sState.account[7] = (float)Rates[0].time;
Depois disso, formamos as harmônicas do carimbo temporal.
bTime.Clear(); double time = (double)Rates[0].time; double x = time / (double)(D'2024.01.01' - D'2023.01.01'); bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_MN1); bTime.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_W1); bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_D1); bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); if(bTime.GetIndex() >= 0) bTime.BufferWrite(); //--- bAccount.Clear(); bAccount.Add((float)((sState.account[0] - PrevBalance) / PrevBalance)); bAccount.Add((float)(sState.account[1] / PrevBalance)); bAccount.Add((float)((sState.account[1] - PrevEquity) / PrevEquity)); bAccount.Add(sState.account[2]); bAccount.Add(sState.account[3]); bAccount.Add((float)(sState.account[4] / PrevBalance)); bAccount.Add((float)(sState.account[5] / PrevBalance)); bAccount.Add((float)(sState.account[6] / PrevBalance)); bAccount.AddArray(GetPointer(bTime)); //--- if(bAccount.GetIndex() >= 0) if(!bAccount.BufferWrite()) return; //--- bState.AssignArray(sState.state);
Aqui vale destacar que os dados formados para descrever o estado atual do ambiente serão utilizados em duas direções. É evidente que eles servirão para a execução da propagação para frente do nosso Agente, com a consequente geração de novas operações de trading. No entanto, as recompensas do ambiente referentes a essas ações só poderão ser obtidas após o fechamento da próxima barra. Em outras palavras, há um intervalo temporal entre ação e retorno.
Por outro lado, neste momento, já podemos avaliar a eficácia das ações executadas pelo Agente no instante anterior. E é de nosso interesse realizar essa avaliação antes de atualizar os estados das redes, que ainda contêm os resultados da análise do ambiente em seu estado anterior.
Por isso, inicialmente passamos os dados formados, a descrição atual do estado do ambiente, para os modelos-alvo, a fim de gerar uma previsão do estado futuro, considerando a política de comportamento atualmente utilizada pelo Agente.
if(!bFirstRun) { //--- Target Nets if(!cEncoder[1].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) || !cTask[1].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(cEncoder[1]), LatentLayer) || !cActor[1].feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, GetPointer(cTask[1]), -1) || !cCritic[2].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer) || !cCritic[3].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer) || !cCritic[4].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer) || !cCritic[5].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; }
O uso de modelos-alvo desempenha um papel essencial na construção de uma estratégia de comportamento coerente e equilibrada, minimizando a influência do ruído de mercado. Isso permite que o Agente se oriente não apenas pelo retorno imediato, mas também pelo retorno esperado no futuro.
Em seguida, passamos ao treinamento do Crítico. É importante lembrar que os autores do framework Actor–Director–Critic propuseram o uso de dois Críticos operando em paralelo, cada um com seu respectivo modelo-alvo. Primeiro, formamos os valores-alvo de avaliação das últimas ações do Agente, levando em conta a recompensa obtida nesta etapa para o primeiro Crítico, e realizamos a propagação direta e reversa da primeira rede Crítica.
//--- Critic 1 cCritic[2].getResults(Result); float reward = Result[0]; cCritic[4].getResults(Result); reward = (reward + Result[0]) / 2 * DiscFactor + float(sState.account[1] - PrevEquity); Result.Clear(); if(!Result.Add(reward) || !cCritic[0].backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; }
Aqui, cabe observar que, na proposta original do framework, o gradiente do erro é propagado com base na avaliação mínima das ações executadas. Em nossa implementação, porém, adotamos uma modificação. Transmitimos o gradiente do erro a partir da rede cuja avaliação apresenta o menor erro médio em relação às ações executadas. Assim, deixamos de nos basear apenas na menor estimativa e passamos a priorizar a mais precisa.
O segundo aspecto crucial do treinamento online é o momento adequado para atualizar a política do Ator. A solução mais simples e técnica seria utilizar um contador fixo de iterações, atualizando a estratégia do agente após um número determinado de passos. E, de fato, esse método é plenamente justificável em condições de aprendizado online real, onde cada estado do ambiente é único e irrepetível.
Entretanto, nosso caso é diferente: planejamos usar o poderoso recurso de simulação do testador de estratégias do MetaTrader 5, que nos permite reproduzir repetidamente as mesmas sequências de eventos, simulando um processo online com múltiplas passagens.
E aqui reside um problema potencial. Se utilizarmos uma abordagem ingênua, com um contador rígido de iterações, as atualizações da política do Ator ocorrerão sempre nos mesmos estados do ambiente a cada repetição do treinamento. Isso reduz drasticamente a variabilidade do conjunto de aprendizado, introduzindo vieses artificiais e dificultando que o agente aprenda padrões realmente consistentes.
Para evitar esse efeito, aplicamos uma abordagem estocástica ao acionamento das atualizações de política. Em vez de um contador determinístico, geramos um valor inteiro aleatório e executamos a atualização da estratégia apenas quando esse valor é múltiplo de um número predefinido. Esse mecanismo mantém a regularidade necessária das otimizações, mas torna seu momento exato imprevisível dentro do contexto temporal. Isso impede o sobreajuste a segmentos específicos dos dados.
if(cCritic[0].getRecentAverageError() <= cCritic[1].getRecentAverageError() && (MathRand() % ActorUpdate) == 0) if(!cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false) || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; }
As mesmas operações são então repetidas para o segundo Crítico.
//--- Critic 2 cCritic[3].getResults(Result); reward = Result[0]; cCritic[5].getResults(Result); reward = (reward + Result[0]) / 2 * DiscFactor + float(sState.account[1] - PrevEquity); Result.Clear(); if(!Result.Add(reward) || !cCritic[1].backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; } if(cCritic[0].getRecentAverageError() > cCritic[1].getRecentAverageError() && (MathRand() % ActorUpdate) == 0) if(!cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false) || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; }
Com o Diretor, o processo é bem mais simples desta vez. Operações de trading lucrativas são tratadas como exemplos positivos, enquanto as demais são classificadas como negativas.
//--- Director Result.Clear(); if((sState.account[1] - PrevEquity) > 0) Result.Add(1); else Result.Add(0); if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false) || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; }
Logo em seguida, ajustamos os parâmetros do modelo preditivo de direção conforme o fechamento da vela mais recente.
//--- Probability vector<float> target = vector<float>::Zeros(NActions / 3); if(sState.state[0] > 0) target[0] = 1; else if(sState.state[0] < 0) target[1] = 1; if(!Result.AssignArray(target) || !cProbability.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cEncoder[0].backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; } }
Após concluir as iterações de otimização das redes, passamos para a geração de uma nova operação de trading. Neste ponto, enviamos a descrição formada anteriormente do estado atual do ambiente para as redes treináveis, incluindo as redes de avaliação das ações do Ator.
//--- New state if(!cEncoder[0].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) || !cTask[0].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cActor[0].feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, (CNet*)GetPointer(cTask[0]), -1) || !cProbability.feedForward((CNet*)GetPointer(cEncoder[0]), LatentLayer, (CBufferFloat*)NULL) || !cDirector.feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cCritic[0].feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cCritic[1].feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; }
E salvamos em variáveis globais os dados que serão necessários no processamento da próxima barra fechada.
PrevBalance = sState.account[0]; PrevEquity = sState.account[1];
Em seguida, iniciamos a execução das operações de trading propriamente ditas. Primeiro, obtemos o vetor de resultados da atuação do nosso Ator.
vector<float> temp; cActor[0].getResults(temp); //--- if(temp.Size() < NActions) temp = vector<float>::Zeros(NActions);
A partir do tensor gerado, eliminamos os volumes mutuamente compensatórios.
double min_lot = Symb.LotsMin(); double step_lot = Symb.LotsStep(); double stops = (MathMax(Symb.StopsLevel(), 1) + Symb.Spread()) * Symb.Point(); if(temp[0] >= temp[3]) { temp[0] -= temp[3]; temp[3] = 0; } else { temp[3] -= temp[0]; temp[0] = 0; }
Depois, passamos à decodificação dos resultados produzidos pelo Ator. Se não houver volume associado a posições longas, encerramos quaisquer posições abertas anteriormente.
//--- buy control if(temp[0] < min_lot || (temp[1] * MaxTP * Symb.Point()) <= 2 * stops || (temp[2] * MaxSL * Symb.Point()) <= stops) { if(buy_value > 0) CloseByDirection(POSITION_TYPE_BUY); }
Quando há necessidade de abrir ou manter posições longas, convertemos os valores obtidos em volumes reais de operações e níveis de preço correspondentes.
else { double buy_lot = min_lot + MathRound((double)(temp[0] - min_lot) / step_lot) * step_lot; double buy_tp = NormalizeDouble(Symb.Ask() + temp[1] * MaxTP * Symb.Point(), Symb.Digits()); double buy_sl = NormalizeDouble(Symb.Ask() - temp[2] * MaxSL * Symb.Point(), Symb.Digits());
Se existirem posições abertas, executamos o trailing dos níveis de negociação.
if(buy_value > 0) TrailPosition(POSITION_TYPE_BUY, buy_sl, buy_tp);
Em seguida, ajustamos o volume da posição atual quer seja por meio de fechamento parcial, seja adicionando o volume faltante. Essa última situação também cobre o caso de abertura de uma nova posição.
if(buy_value != buy_lot) { if((buy_value - buy_lot) >= min_lot) ClosePartial(POSITION_TYPE_BUY, buy_value - buy_lot); else if((buy_lot - buy_value) >= min_lot) if(!Trade.Buy(buy_lot - buy_value, Symb.Name(), Symb.Ask(), buy_sl, buy_tp)) if(Trade.CheckResultRetcode() == 10019) { Result.Clear(); Result.Add(0); if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false) || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; } } } }
Vale observar que, se ocorrer um erro por falta de margem para abrir uma nova posição ou completar o volume faltante, o sistema envia feedback imediato ao Diretor, marcando a decisão como negativa.
O mesmo procedimento é aplicado para a ajustagem de posições curtas.
//--- sell control if(temp[3] < min_lot || (temp[4] * MaxTP * Symb.Point()) <= 2 * stops || (temp[5] * MaxSL * Symb.Point()) <= stops) { if(sell_value > 0) CloseByDirection(POSITION_TYPE_SELL); } else { double sell_lot = min_lot + MathRound((double)(temp[3] - min_lot) / step_lot) * step_lot;; double sell_tp = NormalizeDouble(Symb.Bid() - temp[4] * MaxTP * Symb.Point(), Symb.Digits()); double sell_sl = NormalizeDouble(Symb.Bid() + temp[5] * MaxSL * Symb.Point(), Symb.Digits()); if(sell_value > 0) TrailPosition(POSITION_TYPE_SELL, sell_sl, sell_tp); if(sell_value != sell_lot) { if((sell_value - sell_lot) >= min_lot) ClosePartial(POSITION_TYPE_SELL, sell_value - sell_lot); else if((sell_lot - sell_value) >= min_lot) if(!Trade.Sell(sell_lot - sell_value, Symb.Name(), Symb.Bid(), sell_sl, sell_tp)) if(Trade.CheckResultRetcode() == 10019) { Result.Clear(); Result.Add(0); if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer) || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false) || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); return; } } } }
Ao final do método, verificamos se chegou o momento de atualizar os modelos-alvo e, caso necessário, executamos o procedimento de cópia suave dos parâmetros, transferindo gradualmente os pesos das redes treináveis para suas respectivas versões-alvo.
bFirstRun = false; //--- if((int(Rates[0].time / PeriodSeconds(TimeFrame)) % TragetUpdate) == 0) { if(MathRand() / 32767.0 > 0.5) cCritic[2].WeightsUpdate(GetPointer(cCritic[0]), tau); else cCritic[4].WeightsUpdate(GetPointer(cCritic[0]), tau); if(MathRand() / 32767.0 > 0.5) cCritic[3].WeightsUpdate(GetPointer(cCritic[1]), tau); else cCritic[5].WeightsUpdate(GetPointer(cCritic[1]), tau); cEncoder[1].WeightsUpdate(GetPointer(cEncoder[0]), tau); cTask[1].WeightsUpdate(GetPointer(cTask[0]), tau); cActor[1].WeightsUpdate(GetPointer(cActor[0]), tau); } if(PrevBalance < 50) ExpertRemove(); }
Concluída essa etapa, o método é encerrado e o sistema entra em modo de espera, aguardando o fechamento da próxima vela.
O código completo do EA de treinamento online das redes pode ser consultado no anexo.
Testes
Realizamos um trabalho extenso de adaptação e implementação dos princípios fundamentais do framework Actor–Director–Critic em MQL5, integrando seus componentes à arquitetura das redes treináveis. A lógica de interação entre o Ator, o Diretor e o Crítico foi cuidadosamente elaborada, e métodos originais de aprendizado do agente foram aplicados. Chegamos, enfim, à etapa final, e talvez a mais crítica, que é a verificação da eficácia prática das soluções implementadas com base em dados históricos reais.
A validação do framework foi conduzida em um ambiente simulado com dados históricos, em condições próximas às de mercado real. Esse tipo de teste permite avaliar objetivamente até que ponto as decisões arquiteturais e algorítmicas adotadas são capazes de lidar com a dinâmica e a incerteza típicas dos mercados financeiros. Além disso, essa abordagem evidencia tanto os pontos fortes quanto as limitações da implementação atual, indicando caminhos para futuras melhorias e otimizações.
Para formar o conjunto de treinamento, foram utilizados percursos aleatórios do agente no testador de estratégias do MetaTrader 5, o que possibilitou reunir uma ampla variedade de cenários comportamentais. Como base, foram empregadas cotações históricas do par EURUSD no timeframe M1, cobrindo todo o ano de 2024.
O treinamento inicial das redes foi realizado em modo offline, sem atualização do conjunto de dados, até a estabilização dos erros de previsão dos modelos. Em seguida, o processo continuou no testador de estratégias do MetaTrader 5, realizando o ajuste fino dos parâmetros até a obtenção de resultados consistentes e estáveis.
A avaliação objetiva da qualidade da política de trading aprendida em condições reais foi feita com base em testes realizados fora da amostra de treinamento. Como período de teste, foram utilizados os dados históricos de janeiro–março de 2025. Esse intervalo temporal não foi incluído no processo de aprendizado, eliminando o risco de sobreajuste e conferindo validade prática aos resultados obtidos.
Os demais parâmetros (incluindo as condições de mercado, o timeframe, o modelo de simulação de execução e as configurações do terminal) foram mantidos inalterados. Isso garantiu uma avaliação limpa e imparcial da qualidade da estratégia aprendida, sem interferência de fatores externos.
Os resultados dos testes são apresentados a seguir e permitem visualizar de forma clara o comportamento da política do agente em ação.


Durante o período de teste, o modelo realizou 684 operações de trading. Destas, 268 foram encerradas com lucro, o que representa um pouco mais de 39% do total. No entanto, o resultado geral foi positivo, já que o lucro médio por operação vencedora foi quase o dobro da perda média nas operações negativas, garantindo, assim, um saldo final lucrativo ao término do teste.
Considerações finais
Ao longo deste trabalho, exploramos os aspectos teóricos do framework Actor–Director–Critic e implementamos uma versão prática de seus conceitos utilizando os recursos da linguagem MQL5. Além disso, realizamos sua integração completa à arquitetura multiagente já existente. O resultado foi um agente modular, flexível e de aprendizado eficiente, capaz de levar em conta não apenas as avaliações locais das ações (por meio do Crítico), mas também o contexto estratégico da lógica comportamental (por meio do Diretor). Essa combinação proporciona ao Ator uma retroalimentação mais precisa e estável, permitindo que o agente descarte rapidamente ações ineficazes e explore direções mais promissoras no espaço de políticas.
Os testes realizados confirmaram a funcionalidade e a viabilidade do método proposto, mostrando que Actor–Director–Critic é capaz de tomar decisões mais equilibradas e apresentar comportamento consistente mesmo sob condições de incerteza de mercado.
Contudo, é importante ressaltar que os programas apresentados neste artigo têm caráter demonstrativo, servindo para ilustrar as possibilidades do framework. Antes de aplicar essas soluções em condições reais de negociação, é essencial realizar um treinamento adicional das redes com uma amostra de dados mais representativa, seguido de testes abrangentes em múltiplos cenários de mercado.
Referências
Programas utilizados no artigo
| # | Nome | Tipo | Descrição |
|---|---|---|---|
| 1 | Research.mq5 | EA | EA de coleta de exemplos |
| 2 | ResearchRealORL.mq5 | EA | EA de coleta de exemplos pelo método Real-ORL |
| 3 | Study.mq5 | EA | EA de treinamento offline das redes |
| 4 | StudyOnline.mq5 | EA | EA de treinamento online das redes |
| 4 | Test.mq5 | EA | EA para testes do modelo |
| 5 | Trajectory.mqh | Biblioteca de classe | Estrutura de descrição do estado do sistema e arquitetura das redes |
| 6 | NeuroNet.mqh | Biblioteca de classe | Biblioteca de classes para criação de redes neurais |
| 7 | NeuroNet.cl | Biblioteca | Biblioteca com código da aplicação OpenCL |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/17819
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.
Trading por pares: negociação algorítmica com auto-otimização baseada na diferença de pontuação Z
Critérios de tendência. Conclusão
Desenvolvendo um Expert Advisor de Breakout Baseado em Eventos de Notícias do Calendário em MQL5
Redes neurais em trading: Detecção de anomalias no domínio da frequência (Conclusão)
- 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
https://nukadeti.ru/basni/krylov-kvartet
O analfabetismo gera analfabetismo
https://nukadeti.ru/basni/krylov-kvartet
Você pode me dizer como fazer isso?