Técnicas do MQL5 Wizard que você deve saber (Parte 59): Aprendizado por Reforço (DDPG) com Padrões da Média Móvel e do Oscilador Estocástico
Introdução
No último artigo, introduzimos o DDPG, um algoritmo de aprendizado por reforço, e analisamos 3 de suas classes cruciais conforme implementadas em Python. A classe de buffer de replay, a classe da rede ator e a classe da rede crítica. O que não foi abordado foi a classe agente DDPG; a importação de dados de preços do MetaTrader 5 para o Python; funções para a Média Móvel e o Oscilador Estocástico; uma função get-pattern para reunir dados dos dois indicadores em um vetor binário de entrada para a rede de aprendizado supervisionado (implementada no artigo anterior sobre aprendizado supervisionado via MQL5); e, por fim, um loop de simulação de ambiente para treinar as redes ator e crítica.
Tudo isso faz parte do Aprendizado por Reforço (RL), que estamos analisando como uma transição entre o Aprendizado Supervisionado (SL) e o Aprendizado por inferência (IL), no sentido aqui adotado de aprendizado não supervisionado Qualquer um desses modos pode ser utilizado individualmente para treinar e usar um modelo, porém estes artigos procuram defender a ideia de que eles podem ser usados em conjunto para construir algo mais interessante. Portanto, continuamos nossa análise do RL abordando a importantíssima classe Agente DDPG.
Agente DDPG
A arquitetura principal e a inicialização desta classe podem ser definidas da seguinte forma:
def __init__(self, state_dim, action_dim): # Actor networks self.actor = Actor(state_dim, action_dim, HIDDEN_DIM).to(device) self.actor_target = Actor(state_dim, action_dim, HIDDEN_DIM).to(device) self.actor_target.load_state_dict(self.actor.state_dict()) self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=LR_ACTOR) # Critic networks self.critic = Critic(state_dim, action_dim, HIDDEN_DIM).to(device) self.critic_target = Critic(state_dim, action_dim, HIDDEN_DIM).to(device) self.critic_target.load_state_dict(self.critic.state_dict()) self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=LR_CRITIC) self.replay_buffer = ReplayBuffer(BUFFER_SIZE)
Os componentes críticos aqui são a arquitetura de rede dupla, a configuração dos otimizadores e o gerenciamento de experiências. A arquitetura dupla mantém uma política separada (rede ator) e um valor separado (rede crítica) das duas redes principais de política e valor. Ela implementa redes-alvo para ambas, o que é importante para a estabilidade durante o treinamento. A inicialização dos respectivos alvos é realizada com os mesmos pesos de suas redes principais.
A configuração dos otimizadores apresenta otimizadores Adam separados para as redes ator e crítica. Além disso, como normalmente ocorre, utilizamos taxas de aprendizado separadas para as redes de política e de valor. Por fim, no gerenciamento de experiências, garantimos que o buffer de replay armazene transições para aprendizado off-policy e, ao fixar o tamanho do buffer, evitamos o uso ilimitado de memória. Selecionamos ações incorporando exploração da seguinte forma:
def select_action(self, state, noise_scale=0.1): state = torch.FloatTensor(state).unsqueeze(0).to(device) action = self.actor(state).cpu().data.numpy().flatten() action += noise_scale * np.random.randn(self.action_dim) return np.clip(action, -1, 1)
Os principais mecanismos aqui são processamento de estado, estratégia de exploração e gerenciamento de dispositivos. O processamento de estado cuida da conversão de arrays NumPy para o formato adequado de tensor, a adição de uma dimensão de lote (através do unsqueeze) e, por fim, garante que os cálculos sejam realizados no dispositivo correto.
A estratégia de exploração adiciona ruído Gaussiano à saída determinística da política. A escala do ruído controla a magnitude da exploração e o clipping mantém um intervalo de ação válido. O gerenciamento de dispositivos garante movimentação eficiente entre GPU e CPU, quando aplicável. Além disso, a função retorna como saída final um array NumPy para compatibilidade com o ambiente. O mecanismo de atualização de aprendizado é o seguinte:
def update(self): if len(self.replay_buffer) < BATCH_SIZE: return
Esta cláusula if atua como um controle de atualização, onde as atualizações são ignoradas até que experiências suficientes, em quantidade igual ao tamanho do lote, sejam coletadas. Isso garante estatísticas de lote significativas. A atualização das 2 redes críticas é realizada da seguinte forma:
# Sample batch states, actions, rewards, next_states, dones = self.replay_buffer.sample(BATCH_SIZE) # Target Q calculation next_actions = self.actor_target(next_states) target_q = self.critic_target(next_states, next_actions) target_q = rewards + (1 - dones) * GAMMA * target_q # Current Q estimation current_q = self.critic(states, actions) # Loss computation and backpropagation critic_loss = nn.MSELoss()(current_q, target_q.detach()) self.critic_optimizer.zero_grad() critic_loss.backward() self.critic_optimizer.step()
Os principais aspectos abordados por este código são o cálculo do valor-alvo, o cálculo da perda e o gerenciamento de gradientes. O cálculo do valor-alvo utiliza redes-alvo para obter alvos Q estáveis. Ele implementa a equação de Bellman com tratamento de término, conforme definido pelo parâmetro de experiência “dones”. Um fator de desconto GAMMA controla a importância das recompensas futuras.
Para o cálculo da perda, é determinado o erro quadrático médio entre os valores Q atuais e os valores Q-alvo. O método detach() impede que os gradientes dos alvos fluam (ou sejam transportados pelo tensor para transferibilidade). E o aprendizado padrão por diferença temporal é aplicado. O gerenciamento de gradientes simplesmente garante que todos os gradientes sejam redefinidos para zero, e a otimização da rede crítica é realizada em uma etapa separada. As atualizações da rede ator também são realizadas da seguinte forma:
actor_loss = -self.critic(states, self.actor(states)).mean() self.actor_optimizer.zero_grad() actor_loss.backward() self.actor_optimizer.step()
Os aspectos específicos do gradiente de política tratados aqui são a maximização dos valores Q através da minimização do Q negativo, a diferenciação através das redes ator e crítica, bem como a aplicação de uma abordagem de gradiente de política puro sem probabilidades logarítmicas (por ser determinística). As atualizações da rede-alvo são as seguintes:
for target, param in zip(self.actor_target.parameters(), self.actor.parameters()): target.data.copy_(TAU * param.data + (1 - TAU) * target.data) for target, param in zip(self.critic_target.parameters(), self.critic.parameters()): target.data.copy_(TAU * param.data + (1 - TAU) * target.data)
Esse mecanismo de atualização suave utiliza a média de Polyak (Polyak averaging) com um valor TAU que normalmente é inferior a 1. Os pesos da rede são acompanhados lentamente, fornecendo uma alternativa às atualizações rígidas periódicas. Esse processo, como um todo, mantém a estabilidade enquanto permite o aprendizado. Nosso modelo precisa ser persistente. Ele deve ser capaz de carregar pesos de rede previamente salvos e também salvá-los após o treinamento. Realizamos isso da seguinte forma:
def save(self, filename): torch.save({ 'actor': self.actor.state_dict(), 'critic': self.critic.state_dict(), 'actor_target': self.actor_target.state_dict(), 'critic_target': self.critic_target.state_dict(), }, filename) def load(self, filename): checkpoint = torch.load(filename) self.actor.load_state_dict(checkpoint['actor']) self.critic.load_state_dict(checkpoint['critic']) self.actor_target.load_state_dict(checkpoint['actor_target']) self.critic_target.load_state_dict(checkpoint['critic_target'])
Os principais recursos da listagem acima são: salvar/carregar todos os estados da rede; manter a consistência das redes-alvo; permitir a continuidade do treinamento; e dar suporte à avaliação do modelo. Resumindo a classe agente, ao implementar um agente DDPG existem algumas escolhas críticas de projeto que precisam ser feitas. Essas escolhas podem ser agrupadas em três categorias principais: seleção dos componentes específicos do DDPG, aproveitamento dos pontos fortes da implementação e realização de melhorias potenciais.
Os componentes do DDPG utilizados são principalmente as redes-alvo, a política determinística e taxas de aprendizado separadas. As redes-alvo são muito importantes para o aprendizado estável das recompensas das ações executadas (Q-Learning) ao lidar com espaços de ação contínuos. O uso de espaços contínuos torna isso ainda mais importante. Essa política determinística exige então exploração externa por meio de ruído para aumentar a robustez. O uso de taxas de aprendizado separadas também é uma prática comum, onde a política (rede ator) possui uma taxa de aprendizado mais lenta que a rede de valor.
As escolhas realizadas que tornam esta uma implementação relativamente forte incluem uma clara “separação de responsabilidades”, onde possuímos métodos bem definidos para seleção de ações e também para atualizações. Além disso, existe uma gestão de execução em CPU/GPU(device-awareness) que garante o tratamento consistente das transições entre eles. O processamento em lote também é utilizado para tornar as operações com tensores mais eficientes e, por fim, a segurança de formato (shape-safety) é verificada em vários pontos para garantir consistência na dimensionalidade dos tensores.
As melhorias potenciais, porém, poderiam incluir: clipping de gradiente para evitar gradientes explosivos; utilização de um cronograma de taxa de aprendizado para refinar e controlar melhor o processo de aprendizado; uso de replay priorizado para amostragem mais eficiente, embora isso esteja relacionado ao buffer de replay já mencionado no artigo anterior; e, por fim, exploração paralela, onde múltiplas instâncias do ator podem ser utilizadas para coleta de dados mais rápida.
Também existem algumas dinâmicas de treinamento dignas de nota, relacionadas à sequência de atualização e às considerações sobre hiperparâmetros. A sequência de atualização prioriza a atualização das redes críticas. Isso ocorre porque valores Q mais precisos orientam melhor a melhoria da política. Para introduzir estabilidade adicional, também pode ser implementado o atraso nas atualizações da política. Por fim, atualizações frequentes das redes-alvo são realizadas para acompanhar lentamente os parâmetros (pesos da rede) aprendidos.
As considerações sobre hiperparâmetros devem incluir um foco especial em TAU, pois ele controla a velocidade das redes-alvo e, portanto, é um fator fundamental para a estabilidade de todo o processo de aprendizado. Também deve ser utilizada uma escala de ruído que permita decaimento ao longo do tempo. O dimensionamento do buffer também é crítico, pois afeta a eficiência do aprendizado, enquanto o tamanho do lote influencia a variância das atualizações.
Funções de Média Móvel e Estocástico
Essas duas funções são implementadas em Python para aprendizado por reforço (RL), ao contrário do artigo sobre aprendizado supervisionado, onde fizemos isso em MQL5 e simplesmente exportamos os dados de entrada da rede para o Python para treinamento. Em nossa implementação aqui, estamos utilizando o módulo Python do MetaTrader 5 para conectar a uma instância em execução do terminal e então recuperar dados de preços. Existem guias na documentação aqui explicando como fazer isso. As funções dos indicadores abaixo transformam dados brutos de preços em dados de indicadores técnicos que serviram como entradas para nosso modelo de aprendizado supervisionado após serem convertidos/normalizados em um vetor binário de entrada representando padrões.
As saídas do modelo de aprendizado supervisionado são o que definimos como estados, pois, em essência, elas preveem mudanças na ação do preço. Esses estados são então utilizados como entradas para o agente DDPG de RL. Nossa função de Média Móvel recebe como entrada um DataFrame pandas contendo preços provenientes do módulo Python do MetaTrader 5. Esse DataFrame precisa ser validado e preparado da seguinte forma:
p = np.asarray(p).flatten() # Convert to 1D array if not already if len(p) < window: raise ValueError("Window size cannot be larger than the number of prices.")
O que estamos fazendo aqui é padronizar o array para garantir um formato de entrada unidimensional consistente, independentemente da forma da entrada. Também temos tratamento de erros para evitar tamanhos de janela inválidos que poderiam causar erros de cálculo. A integridade dos dados também mantém um fluxo limpo de informações ao longo do pipeline de processamento. O mecanismo de cálculo é o seguinte:
return np.convolve(p, np.ones(window), 'valid') / window
Essa implementação utiliza convolução para realizar de forma eficiente o cálculo da média móvel. O uso do parâmetro de entrada 'valid' garante que apenas janelas completamente calculadas sejam retornadas. A normalização também é feita pelo tamanho da janela para produzir uma média real. Toda a operação é vetorizada para desempenho ideal. A importância financeira disso é que ela suaviza os dados de preço para ajudar na identificação de tendências, e o tamanho da janela utilizada (também conhecido como período de média) determina a sensibilidade às mudanças de preço. A função do oscilador estocástico valida suas entradas da seguinte forma:
p = np.asarray(p).flatten() if len(p) < k_window: raise ValueError("Window size for %K cannot be larger than the number of prices.")
As considerações de projeto aqui são a consistência da formatação de entrada com a função de Média Móvel. É necessária uma validação separada para a janela de cálculo do %K, com geração de erro como falha antecipada para parâmetros inválidos. O cálculo do %K é o seguinte:
for i in range(k_window - 1, len(p)): current_close = p[i] lowest_low = min(p[i - k_window + 1:i + 1]) highest_high = max(p[i - k_window + 1:i + 1]) K = ((current_close - lowest_low) / (highest_high - lowest_low)) * 100 K_values.append(K)
Os componentes importantes aqui são a análise de janela deslizante, o contexto de mercado e a implementação geral. A análise de janela deslizante precisa examinar a faixa de preços ao longo de um período retrospectivo. Isso ajuda a identificar a posição relativa do preço de fechamento atual, sendo aplicada uma escala padrão de 0 a 100. O contexto de mercado ajuda a avaliar condições de sobrecompra e sobrevenda. Valores próximos de 100 sugerem uma possível reversão para baixo, enquanto valores próximos de 0 sugerem uma possível reversão para cima. A implementação geral utiliza um laço explícito para maior clareza, emprega indexação adequada da janela para lidar com casos extremos e preserva a ordem temporal dos resultados. O cálculo do %D é o seguinte:
D_values = MA(K_values, d_window)
Essencialmente, trata-se de um refinamento de sinal onde é utilizada uma versão suavizada da média móvel do %K. A atribuição típica para esse período de média é 3, e é isso que estamos utilizando. Esse buffer adicional fornece confirmação para as oscilações do %K e, assim, ajuda a reduzir sinais falsos provenientes do %K bruto.
Função Get Pattern
Esta função é utilizada para integrar os dados dos dois buffers de indicadores acima ao pipeline de aprendizado. Ela desempenha um papel de engenharia de atributos. Isso ocorre porque: ajuda na redução de dimensionalidade, já que transforma preços brutos em sinais mais significativos; ajuda na melhoria da estacionariedade, pois os indicadores geralmente são mais estáveis do que os preços brutos; e, por fim, permite capturar contexto temporal, uma vez que cálculos baseados em janelas mantêm dependências temporais (um vetor de entrada gerado, por exemplo, [1,0,0,1], pode ser associado ao momento em que foi produzido, da mesma forma que qualquer valor de indicador ou preço bruto também é marcado com o instante em que é gerado).
Principalmente, porém, ela é utilizada para a preparação do aprendizado supervisionado. Os atributos que produz em um vetor binário de 0s e 1s treinam o modelo para prever as próximas mudanças de preço. A Média Móvel fornece informações de tendência, enquanto a função STO nos fornece informações de momentum e reversão. Abordamos os padrões complementares combinados de ambos os indicadores no artigo 57. As saídas de previsão de mudança de preço passam então a servir como representação de estado para o RL.
Isso implica que as previsões do nosso modelo de aprendizado supervisionado tornam-se entradas de estado para o DDPG. Portanto, os indicadores utilizados, MM e STO, acabam auxiliando o agente DDPG ao fornecer contexto de mercado para ajudar na compreensão de um determinado regime de mercado. Isso reduz a necessidade de utilizar preços históricos brutos na definição do estado.
Os pontos fortes da implementação incluem robustez por meio da validação de entradas para evitar falhas silenciosas, tratamento de dimensionalidade para garantir formatos consistentes dos arrays e mensagens de erro claras em caso de uso inadequado. Também existem considerações de desempenho, como o uso de operações vetorizadas sempre que possível, laços explícitos quando necessários e eficiência de memória graças a um design favorável ao processamento em fluxo. Ela continua relevante para os traders, sem se perder em tecnicismos. Isso porque indicadores amplamente utilizados pela indústria estão sendo empregados para gerar estados. Os indicadores são complementares, pois combinam métricas de tendência e momentum, e as saídas de estado estão em uma faixa normalizada, algo importante para a consistência.
Possíveis melhorias incluem otimização computacional através de uma implementação vetorizada do cálculo do %K, utilização da aceleração com numba (importado via JIT) para acelerar os laços da função STO e armazenamento em cache de cálculos intermediários. Funcionalidades estendidas também podem ser adicionadas através de validações adicionais para valores NaN/inf. Este código que implementa aprendizado por reforço com DDPG é extenso e, embora fosse apropriado comentar suas principais seções, limitarei-me a anexar ao final deste artigo as partes que ainda não foram abordadas. Entre elas, destaca-se esta função get pattern.
Testes
Dos 10 padrões que testamos no artigo sobre aprendizado supervisionado, nº 57, apenas 7 conseguiram avançar de forma lucrativa durante um ano, tendo sido treinados no ano anterior. Como cada padrão resultou em sua própria rede, também precisamos gerar redes e ambientes de aprendizado por reforço para cada padrão. Seguimos uma metodologia semelhante à utilizada no artigo 57, treinando o par EUR USD para o ano de 2023 no período gráfico diário. Neste caso, treinamos nossas redes de aprendizado por reforço simulando o ano de 2023 como um ambiente de “mercado ao vivo”. Como argumentado nos dois artigos anteriores, o aprendizado por reforço é um sistema destinado a apoiar e proteger um modelo já estabelecido e treinado, que em nosso caso é a rede treinada por aprendizado supervisionado no artigo 57.
Ele faz isso realizando retropropagação enquanto está em produção ou em ambientes ao vivo, e não sobre dados históricos. Como a retropropagação de uma rede ONNX a partir do MQL5 não é viável, estamos “simulando” um ambiente ao vivo, que no nosso caso continua sendo o ano de 2023.
Em vez da pergunta que fizemos no aprendizado supervisionado, “o que o preço fará a seguir?”, fazemos a seguinte pergunta: “dadas essas mudanças de preço recebidas, quais ações o trader deve tomar?”. Assim, realizamos os treinamentos de simulação descritos acima para o ano de 2023 e depois executamos um teste forward para o ano de 2024, onde nossas condições de entrada são ligeiramente modificadas.
Em vez de basearmos nossas posições compradas ou vendidas apenas no que o preço fará a seguir, também consideramos quais ações realmente precisamos tomar diante do que o preço fará. Também levamos em conta se as recompensas serão lucrativas. Dos 7 padrões que avançaram nos testes forward no artigo 57, apenas 3 avançam de forma significativa quando o aprendizado por reforço é utilizado. Utilizando nossa indexação dos 10 padrões, numerados de 0 a 9, esses padrões são 1, 2 e 5. Seus relatórios são apresentados abaixo:
Para o padrão 1:


Para o padrão 2:


Para o padrão 5:


O Expert Advisor testado, como sempre, é construído com uma classe de sinal personalizada cujo código está anexado abaixo. Realizamos alterações no arquivo da classe de sinal utilizado no artigo 57, renomeando a função 'IsPattern' para 'Supervise'. Também introduzimos uma nova função chamada 'Reinforce'. O código de ambas é compartilhado abaixo:
//+------------------------------------------------------------------+ //| Supervised Learning Model Forward Pass. | //+------------------------------------------------------------------+ double CSignal_DDPG::Supervise(int Index, ENUM_POSITION_TYPE T) { vectorf _x = Get(Index, m_time.GetData(X()), m_close, m_ma, m_ma_lag, m_sto); vectorf _y(1); _y.Fill(0.0); int _i=Index; if(_i==8) { _i -= 2; } ResetLastError(); if(!OnnxRun(m_handles[_i], ONNX_NO_CONVERSION, _x, _y)) { printf(__FUNCSIG__ + " failed to get y forecast, err: %i", GetLastError()); return(double(_y[0])); } if(T == POSITION_TYPE_BUY && _y[0] > 0.5f) { _y[0] = 2.0f * (_y[0] - 0.5f); } else if(T == POSITION_TYPE_SELL && _y[0] < 0.5f) { _y[0] = 2.0f * (0.5f - _y[0]); } return(double(_y[0])); } //+------------------------------------------------------------------+ //| Reinforcement Learning Model Forward Pass. | //+------------------------------------------------------------------+ double CSignal_DDPG::Reinforce(int Index, ENUM_POSITION_TYPE T, double State) { vectorf _x(1); _x.Fill(float(State)); vectorf _y(1); _y.Fill(0.0); vectorf _y_state(1); _y_state.Fill(float(State)); vectorf _y_action(1); _y_action.Fill(0.0); vectorf _z(1); _z.Fill(0.0); int _i=Index; if(_i==8) { _i -= 2; } ResetLastError(); if(!OnnxRun(m_handles_a[_i], ONNX_NO_CONVERSION, _x, _y)) { printf(__FUNCSIG__ + " failed to get y action forecast, err: %i", GetLastError()); } _y_action[0] = _y[0]; ResetLastError(); if(!OnnxRun(m_handles_c[_i], ONNX_NO_CONVERSION, _y_state, _y_action, _z)) { printf(__FUNCSIG__ + " failed to get z reward forecast, err: %i", GetLastError()); } //normalize action output & check for state-action alignment if(T == POSITION_TYPE_BUY && _y[0] > 0.5f) { _y[0] = 2.0f * (_y[0] - 0.5f); } else if(T == POSITION_TYPE_SELL && _y[0] < 0.5f) { _y[0] = 2.0f * (0.5f - _y[0]); } else { _y[0] = 0.0f; } return(double(_y[0]*_z[0])); }
Este arquivo de classe de sinal personalizada destina-se a ser montado em um Expert Advisor através do MQL5 Wizard e, para leitores iniciantes, é possível encontrar orientações aqui e aqui sobre como fazer isso.
Conclusão
Analisamos a aplicação do aprendizado por reforço quando os modelos estão em implantação/produção. Nosso aprendizado por reforço utilizou o algoritmo Deep Deterministic Policy Gradient e essa implementação continha as classes: buffer de replay, ator, crítico e agente, abordadas neste e no artigo anterior. O aprendizado por reforço em implantação/produção serve como um meio de manter o modelo focado no que foi aprendido durante a fase de aprendizado supervisionado (exploitation/aproveitamento), ao mesmo tempo em que observa novas mudanças desconhecidas no ambiente/condições de mercado que devem ser consideradas na tomada de decisões futuras (exploitation/exploração). Ao fazer isso corretamente, inerentemente precisamos realizar retropropagação e treinar um modelo enquanto ele está sendo utilizado.
No entanto, como o treinamento de um modelo ONNX no MQL5 não é suportado, optamos por uma simulação de condições de negociação ao vivo utilizando dados históricos. Após a simulação, testamos os modelos de aprendizado por reforço treinados no ano subsequente ao ano de treinamento e apenas 3 dos 7 conseguiram avançar nos testes forward, embora com resultados de negociação enviesados, já que as posições foram mantidas predominantemente apenas compradas ou apenas vendidas. Como argumentamos no artigo 57, isso provavelmente se deve a uma janela de testes pequena, o que significa que treinamento e testes extensivos em uma quantidade maior de dados devem corrigir esse problema. Agora analisaremos o aprendizado por inferência.
| Tipo | Descrição |
|---|---|
| *.*onnx files | Arquivos de modelo ONNX na subpasta Python dentro do local do arquivo da classe de sinal personalizada |
| *.*mqh files | Arquivo da classe de sinal personalizada e arquivo com função para processamento dos dados de entrada da rede (57_X) |
| *.*mq5 files | Expert Advisor montado pelo Wizard cujo cabeçalho mostra os arquivos utilizados. |
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/17684
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
Gerenciamento Avançado de Memória e Técnicas de Otimização em MQL5
Está chegando o novo MetaTrader 5 e MQL5
Algoritmo do Duelista - Duelist Algorithm
- 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