English Русский 中文 Español Deutsch 日本語
preview
Criação de um painel administrativo de negociação em MQL5 (Parte XII): Integração de uma calculadora Forex

Criação de um painel administrativo de negociação em MQL5 (Parte XII): Integração de uma calculadora Forex

MetaTrader 5 — Exemplos |
23 10
Clemence Benjamin
Clemence Benjamin

Sumário


Introdução

Neste artigo, vamos abordar as limitações de calcular manualmente as métricas de negociação ou recorrer a ferramentas externas. Para isso, integraremos uma calculadora Forex diretamente ao Painel de Gerenciamento de Operações, um dos painéis secundários do nosso novo EA administrativo.

Durante muito tempo, muitos traders recorreram a sites externos para realizar esses cálculos. Essas ferramentas sempre foram extremamente úteis, e seus desenvolvedores merecem reconhecimento pelos serviços que disponibilizam. Ainda hoje, alguns traders continuam utilizando essas calculadoras online, afinal, trata-se de uma questão de preferência.

No entanto, graças aos recursos do MQL5 e às suas ferramentas de desenvolvimento de interfaces gráficas, agora podemos criar soluções integradas diretamente ao terminal de negociação. Com essa abordagem, eliminamos a necessidade de alternar entre aplicativos e simplificamos a rotina operacional, reunindo todas as ferramentas necessárias em um só lugar.

Também vale destacar o MetaTrader 5, que disponibiliza uma API robusta, permitindo acessar notícias do mercado e fluxos de dados diretamente no terminal. Embora existam APIs de terceiros para calculadoras e feeds de notícias, nosso objetivo é desenvolver um algoritmo próprio de cálculo, adaptado especificamente ao nosso painel.

Este projeto não pretende desmerecer as soluções existentes, mas ampliar as opções disponíveis para os traders. Além disso, permite conhecer melhor os recursos do MetaTrader 5 e incentiva os usuários a aproveitar a plataforma de maneira mais eficiente. Ao reunir um conjunto de ferramentas totalmente integrado ao terminal, buscamos proporcionar uma experiência de negociação mais fluida e produtiva, demonstrando como os avanços tecnológicos continuam transformando o setor de trading.

Entre os valores que serão calculados, estão:

  • Tamanho da posição
  • Valor em risco
  • Valor do ponto
  • Margem necessária
  • Estimativa de lucro/prejuízo
  • Swaps / Taxas overnight
  • Relação risco-retorno
  • Nível de margem
  • Custo do spread
  • Preço de equilíbrio
  • Retorno esperado
  • Impacto da alavancagem, entre outros.

Esses cálculos são essenciais para os traders do mercado Forex, pois oferecem uma base estruturada para gerenciar riscos, otimizar os setups de negociação e preservar a estabilidade da conta. O cálculo do tamanho da posição e do valor em risco permite limitar o capital exposto ao risco em cada operação a uma parcela previamente definida, protegendo a conta contra perdas significativas. O cálculo do valor do ponto e a estimativa de lucro/prejuízo permitem planejar as operações com precisão, ajudando os traders a estabelecer metas realistas e níveis adequados de stop-loss. Já os cálculos da margem necessária e do nível de margem ajudam a evitar a alavancagem excessiva, que pode resultar em chamadas de margem (margin calls) ou até na perda total dos recursos da conta. As taxas de swap (overnight) são especialmente importantes para traders que mantêm posições por períodos prolongados, principalmente aqueles que utilizam estratégias de carry trade, pois afetam os custos de manutenção das posições.

A relação risco-retorno ajuda a selecionar operações cujo lucro potencial justifique o risco assumido. Outras métricas, como o custo do spread, o preço de equilíbrio, o retorno esperado e o impacto da alavancagem, ajudam os traders a avaliar os custos de transação, a viabilidade da estratégia e a exposição geral ao risco. Em conjunto, essas ferramentas favorecem uma atuação mais criteriosa e disciplinada, alinhando suas operações aos objetivos financeiros e às condições do mercado. Isso contribui para melhorar a consistência dos resultados e a rentabilidade.

Na próxima seção, apresentarei brevemente como conduziremos o desenvolvimento.


Visão geral

A arquitetura modular que adotamos ao longo desta série de artigos nos permite trabalhar em partes específicas do programa sem comprometer o funcionamento dos demais componentes. Essa flexibilidade agora nos permite atualizar o Painel de Gerenciamento de Operações para abrir espaço à integração das ferramentas de cálculo.

Para isso, utilizaremos classes adicionais da biblioteca padrão do MQL5. Em vez de manter seções de entrada separadas para cada tipo de ordem, implementaremos um menu suspenso para selecionar o tipo de ordem, combinado com uma única linha de campos de entrada. Essa reorganização da interface liberará espaço para os componentes da nossa calculadora.

Embora não seja necessário exibir todas as métricas de negociação, alguns valores são essenciais para a tomada de decisões fundamentadas e precisam estar facilmente acessíveis. Além disso, nem todos exigem cálculos específicos, pois alguns já podem ser obtidos diretamente dos dados de mercado em tempo real disponibilizados pelo MQL5.

Começaremos examinando detalhadamente os principais termos e valores utilizados no mercado Forex, incluindo suas definições, fórmulas e formas de representação no MQL5. Em seguida, passaremos à implementação, começando pela adaptação da seção de Ordens no Painel de Gerenciamento de Operações para acomodar a interface da calculadora.

Alterações no Painel de Gerenciamento de Operações

Alterações no Painel de Gerenciamento de Operações

Na seção identificada pela letra A na ilustração acima, utilizaremos a classe ComboBox para exibir uma lista e permitir a seleção do tipo de ordem. A seção B será reorganizada em uma única linha, enquanto a seção de data de expiração (C) será aprimorada com um seletor de datas (DatePicker), tornando a interface mais prática.

Após ajustar o layout, integraremos tanto a lógica dos cálculos quanto os componentes da interface gráfica responsáveis pela entrada de dados. Em geral, cada cálculo exigirá no máximo três campos de entrada.

Por fim, apresentarei os procedimentos e resultados dos testes, encerrando o artigo com uma avaliação das novas funcionalidades.


Cálculos e fórmulas de Forex

A tabela abaixo apresenta alguns termos e métricas comuns do mercado Forex cujos valores precisam ser calculados, juntamente com suas respectivas fórmulas e funções personalizadas em MQL5 utilizadas para obter os resultados. Os exemplos não abrangem todas as possibilidades. Dependendo da estratégia implementada, você poderá precisar de cálculos adicionais. As fórmulas apresentadas na tabela foram reunidas a partir de uma pesquisa abrangente, combinando conceitos matemáticos encontrados em diversas fontes online. Para aprofundar seus conhecimentos ou conferir as informações, você pode consultar o Google ou outras fontes confiáveis.

Termo do mercado Forex e descrição Fórmula geral Fórmula em MQL5 (código)
Tamanho da posição

Calcula o volume da operação em lotes com base no saldo da conta, no percentual de risco e na distância do stop-loss, mantendo o risco da operação alinhado à estratégia do trader.




double CalculatePositionSize(double accountBalance, 
   double riskPercent, double stopLossPips, 
   string symbol)
{
   if (accountBalance <= 0 || riskPercent <= 0 || 
       stopLossPips <= 0) return 0.0;
   double pipValue = CalculatePipValue(symbol, 1.0, 
       AccountCurrency());
   if (pipValue == 0) return 0.0;
   double positionSize = (accountBalance * (riskPercent / 
       100.0)) / (stopLossPips * pipValue);
   double lotStep = MarketInfo(symbol, 
       MODE_LOTSTEP);
   double minLot = MarketInfo(symbol, 
       MODE_MINLOT);
   double maxLot = MarketInfo(symbol, 
       MODE_MAXLOT);
   return NormalizeDouble(
       MathMax(minLot, MathMin(maxLot, 
       positionSize)), (int)-MathLog10(lotStep));
}
                


Valor em risco

Determina o valor monetário que pode ser perdido em uma operação, considerando o tamanho da posição e o stop-loss, para manter as perdas dentro dos limites aceitáveis.




double CalculateRiskAmount(double positionSize, 
   double stopLossPips, string symbol)
{
   if (positionSize <= 0 || stopLossPips <= 0) 
       return 0.0;
   double pipValue = CalculatePipValue(symbol, positionSize, 
       AccountCurrency());
   return NormalizeDouble(positionSize * stopLossPips * 
       pipValue, 2);
}
                


Valor do ponto

Calcula o valor monetário de uma variação de um pip para determinado volume em lotes, sendo essencial para calcular o risco e o lucro potencial.




double CalculatePipValue(string symbol, 
   double lotSize, string accountCurrency)
{
   double tickSize = MarketInfo(symbol, 
       MODE_TICKSIZE);
   double tickValue = MarketInfo(symbol, 
       MODE_TICKVALUE);
   double pipSize = StringFind(symbol, 
       "JPY") >= 0 ? 0.01 : 0.0001;
   double conversionRate = 1.0;
   if (accountCurrency != SymbolInfoString(symbol, 
       SYMBOL_CURRENCY_PROFIT)) {
      string conversionPair = SymbolInfoString(
          symbol, SYMBOL_CURRENCY_PROFIT) + accountCurrency;
      if (SymbolSelect(conversionPair, true)) {
         conversionRate = MarketInfo(conversionPair, 
             MODE_BID);
      } else {
         Print("Warning: Conversion pair ", 
             conversionPair, " not found, using 1.0");
      }
   }
   if (tickSize == 0) return 0.0;
   return NormalizeDouble((tickValue / tickSize) * 
       pipSize * lotSize * conversionRate, 2);
}
                


Margem necessária

Determina o capital exigido para abrir uma posição com base no volume em lotes, no tamanho do contrato e na alavancagem, ajudando a evitar a alavancagem excessiva.




double CalculateMarginRequirement(double lotSize, 
   string symbol)
{
   double marginRequired = MarketInfo(symbol, 
       MODE_MARGINREQUIRED);
   if (marginRequired == 0) {
      Print("Error: Margin requirement not available ", 
          symbol);
      return 0.0;
   }
   return NormalizeDouble(lotSize * marginRequired, 
       2);
}
                


Estimativa de lucro/prejuízo

Estima o lucro ou prejuízo potencial com base nos preços de entrada e saída, ajudando a definir metas realistas para as operações.




double CalculateProfitLoss(double entryPrice, 
   double exitPrice, double lotSize, 
   string symbol)
{
   if (lotSize <= 0 || entryPrice <= 0 || 
       exitPrice <= 0) return 0.0;
   double contractSize = MarketInfo(symbol, 
       MODE_LOTSIZE);
   double conversionRate = 1.0;
   if (AccountCurrency() != SymbolInfoString(symbol, 
       SYMBOL_CURRENCY_PROFIT)) {
      string conversionPair = SymbolInfoString(
          symbol, SYMBOL_CURRENCY_PROFIT) + AccountCurrency();
      if (SymbolSelect(conversionPair, true)) {
         conversionRate = MarketInfo(conversionPair, 
             MODE_BID);
      }
   }
   double priceDiff = exitPrice - entryPrice;
   double pips = priceDiff / (StringFind(symbol, 
       "JPY") >= 0 ? 0.01 : 0.0001);
   return NormalizeDouble(pips * CalculatePipValue(symbol, 
       lotSize, AccountCurrency()), 2);
}
                


Swaps / Taxas overnight

Calcula os juros creditados ou cobrados pela manutenção de posições de um dia para o outro, um fator importante para operações de longo prazo.




double CalculateSwap(double lotSize, 
   string symbol, bool isBuy, 
   int days = 1)
{
   double swapLong = MarketInfo(symbol, 
       MODE_SWAPLONG);
   double swapShort = MarketInfo(symbol, 
       MODE_SWAPSHORT);
   if (swapLong == 0 && swapShort == 0) {
      Print("Error: Swap rates not available ", 
          symbol);
      return 0.0;
   }
   double swap = isBuy ? swapLong : swapShort;
   datetime currentTime = TimeCurrent();
   if (TimeDayOfWeek(currentTime) == 3) 
       days *= 3;
   double totalSwap = lotSize * swap * days;
   return NormalizeDouble(totalSwap, 2);
}
                


Relação risco-retorno

Compara o lucro potencial com a perda potencial, ajudando a selecionar operações com expectativa matemática positiva.




double CalculateRiskRewardRatio(double takeProfitPips, 
   double stopLossPips)
{
   if (stopLossPips <= 0 || takeProfitPips <= 0) 
       return 0.0;
   return NormalizeDouble(takeProfitPips / stopLossPips, 
       2);
}
                


Nível de margem

Indica a relação percentual entre o patrimônio líquido da conta e a margem utilizada, permitindo monitorar a situação da conta e evitar chamadas de margem (margin calls).




double CalculateMarginLevel()
{
   double equity = AccountEquity();
   double margin = AccountMargin();
   if (margin == 0) return 0.0;
   return NormalizeDouble((equity / margin) * 100, 
       2);
}
                


Custo do spread

Calcula o custo monetário do spread (diferença entre os preços de compra e venda) de uma operação, um fator especialmente importante para estratégias de negociação de curto prazo.




double CalculateSpreadCost(double lotSize, 
   string symbol)
{
   double spreadPips = MarketInfo(symbol, 
       MODE_SPREAD) / 10.0;
   double pipValue = CalculatePipValue(symbol, lotSize, 
       AccountCurrency());
   return NormalizeDouble(spreadPips * pipValue * lotSize, 
       2);
}
                


Impacto da alavancagem

Mede a alavancagem efetiva utilizada em uma operação, indicando a exposição financeira da posição em relação ao patrimônio da conta.




double CalculateLeverageImpact(double positionSize, 
   string symbol, double accountEquity)
{
   if (positionSize <= 0 || accountEquity <= 0) 
       return 0.0;
   double contractSize = MarketInfo(symbol, 
       MODE_LOTSIZE);
   double marketPrice = MarketInfo(symbol, 
       MODE_BID);
   return NormalizeDouble((positionSize * contractSize * 
       marketPrice) / accountEquity, 2);
}
                



Na próxima seção, dedicada à implementação, utilizaremos a classe CComboBox da biblioteca padrão do MQL5 para otimizar o espaço disponível e acomodar os controles da calculadora que será integrada ao Painel de Gerenciamento de Operações. Essa abordagem também nos ajudará a entender como projetar interfaces gráficas eficientes e gerenciar seus controles. Além disso, adicionaremos o componente DatePicker para facilitar a seleção da data de expiração das ordens.


Implementação

Para avançarmos de maneira estruturada, dividiremos o desenvolvimento em quatro etapas principais:

Assim que essas etapas forem concluídas, atualizaremos o Expert Advisor (EA) NewAdminPanel para oferecer suporte aos novos recursos e realizaremos os testes. É importante acompanhar cada etapa com atenção para não deixar passar detalhes essenciais, especialmente ao trabalhar com os componentes ComboBox e DatePicker.


(1) Ajuste da seção de ordens pendentes para liberar espaço para os novos controles.

Agora, vamos separar a seção de ordens pendentes do cabeçalho do Painel de Gerenciamento de Operações, facilitando a implementação dos componentes ComboBox e DatePicker. Também adicionaremos um botão para enviar a ordem, que o usuário deverá acionar após configurar todos os parâmetros da ordem.

Declaração das variáveis-membro dos controles de ordens pendentes

Essas variáveis-membro pertencem à seção "Ordens pendentes" da classe CTradeManagementPanel. Começaremos declarando um rótulo que será exibido acima dos controles, identificando a seção com o título "Ordens pendentes:".

//  Pending Orders
CLabel      m_secPendingLabel;    // “Pending Orders:” header
CLabel      m_pendingPriceHeader; // “Price:” column header
CLabel      m_pendingTPHeader;    // “TP:” column header
CLabel      m_pendingSLHeader;    // “SL:” column header
CLabel      m_pendingExpHeader;   // “Expiration:” column header

CComboBox   m_pendingOrderType;   // Combobox for “Buy Limit / Buy Stop / Sell Limit / Sell Stop”
CEdit       m_pendingPriceEdit;   // Edit box for pending‐order price
CEdit       m_pendingTPEdit;      // Edit box for pending‐order take‐profit
CEdit       m_pendingSLEdit;      // Edit box for pending‐order stop‐loss
CDatePicker m_pendingDatePicker;  // DatePicker for expiration date
CButton     m_placePendingButton; // “Place Order” button for pending orders

Logo abaixo, teremos outros quatro rótulos que funcionarão como cabeçalhos dos campos: "Preço:", "TP:", "SL:" e "Expiração:". Abaixo desses rótulos, adicionaremos um ComboBox para que o usuário possa selecionar um dos quatro tipos de ordens pendentes: Buy Limit, Buy Stop, Sell Limit e Sell Stop. À direita do ComboBox, teremos três campos de entrada para informar, respectivamente, o preço da ordem pendente, o take-profit (TP) e o stop-loss (SL). Ao lado desses campos, posicionaremos um seletor de datas (DatePicker), que facilitará a definição da data de expiração da ordem. Por fim, declararemos um botão com o texto "Enviar ordem", responsável por iniciar o envio da ordem pendente com os parâmetros configurados.

Essa seção independente reúne os seis controles e cinco rótulos necessários para criar e gerenciar ordens pendentes. Essa separação facilita tanto a explicação quanto a refatoração da lógica relacionada às ordens pendentes, sem interferir nos demais componentes do painel.

Criação dos controles de ordens pendentes no método Create(...)

Dentro do método Create(...), criamos toda a seção "Ordens pendentes" logo após a linha divisória abaixo da seção "Calculadora Forex". Primeiro, adicionamos um pequeno espaçamento vertical para separá-la visualmente da calculadora. Em seguida, criamos o rótulo "Ordens pendentes:", em negrito, para destacar o título da seção.

À direita desse título, posicionamos um ComboBox para selecionar o tipo de ordem. Depois de adicionar o ComboBox e ajustar a posição vertical, criamos os quatro cabeçalhos de coluna: "Preço:", "TP:", "SL:" e "Expiração:". Cada rótulo é posicionado horizontalmente, com espaçamento uniforme, para ficar alinhado acima do respectivo campo de entrada.

// In CTradeManagementPanel::Create(...), after Section separator:

// 10px vertical offset before “Section 3” header
curY += 10;
if(!CreateLabelEx(m_secPendingLabel, curX, curY, DEFAULT_LABEL_HEIGHT, 
                  "SecPend", "Pending Orders:", clrNavy))
   return(false);
m_secPendingLabel.Font("Arial Bold");
m_secPendingLabel.FontSize(10);

// Create the Combobox for order types
if(!CreateComboBox(m_pendingOrderType, "PendingOrderType", 
                   curX + SECTION_LABEL_WIDTH + GAP, curY, DROPDOWN_WIDTH, EDIT_HEIGHT))
   return(false);
curY += EDIT_HEIGHT + GAP;

// Column headers: Price, TP, SL, Expiration
int headerX = curX;
if(!CreateLabelEx(m_pendingPriceHeader, headerX, curY, DEFAULT_LABEL_HEIGHT, 
                  "PendPrice", "Price:", clrBlack))
   return(false);
if(!CreateLabelEx(m_pendingTPHeader, headerX + EDIT_WIDTH + GAP, curY, DEFAULT_LABEL_HEIGHT, 
                  "PendTP", "TP:", clrBlack))
   return(false);
if(!CreateLabelEx(m_pendingSLHeader, headerX + 2 * (EDIT_WIDTH + GAP), curY, DEFAULT_LABEL_HEIGHT, 
                  "PendSL", "SL:", clrBlack))
   return(false);
if(!CreateLabelEx(m_pendingExpHeader, headerX + 3 * (EDIT_WIDTH + GAP), curY, DEFAULT_LABEL_HEIGHT, 
                  "PendExp", "Expiration:", clrBlack))
   return(false);
curY += DEFAULT_LABEL_HEIGHT + GAP;

// Pending orders inputs row:
//  • Pending Price
int inputX = curX;
if(!CreateEdit(m_pendingPriceEdit, "PendingPrice", inputX, curY, EDIT_WIDTH, EDIT_HEIGHT))
   return(false);
double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
m_pendingPriceEdit.Text(DoubleToString(ask, 5));

//  • Pending TP
int input2X = inputX + EDIT_WIDTH + GAP;
if(!CreateEdit(m_pendingTPEdit, "PendingTP", input2X, curY, EDIT_WIDTH, EDIT_HEIGHT))
   return(false);
m_pendingTPEdit.Text("0.00000");

//  • Pending SL
int input3X = input2X + EDIT_WIDTH + GAP;
if(!CreateEdit(m_pendingSLEdit, "PendingSL", input3X, curY, EDIT_WIDTH, EDIT_HEIGHT))
   return(false);
m_pendingSLEdit.Text("0.00000");

//  • Pending Expiration (DatePicker)
int input4X = input3X + EDIT_WIDTH + GAP;
if(!CreateDatePicker(m_pendingDatePicker, "PendingExp", 
                     input4X, curY, DATEPICKER_WIDTH + 20, EDIT_HEIGHT))
   return(false);
datetime now = TimeCurrent();
datetime endOfDay = now - (now % 86400) + 86399;
m_pendingDatePicker.Value(endOfDay);

//  • Place Order button
int buttonX = input4X + DATEPICKER_WIDTH + GAP;
if(!CreateButton(m_placePendingButton, "Place Order", 
                 buttonX + 20, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrBlue))
   return(false);
curY += BUTTON_HEIGHT + GAP * 2;

Com os cabeçalhos posicionados, deslocamos a posição vertical para baixo e começamos a criar a linha de entrada de dados. O primeiro campo corresponde ao preço da ordem pendente e é preenchido inicialmente com o preço ask atual, oferecendo ao usuário uma referência inicial. À direita, adicionamos o campo de take-profit (TP), inicializado com "0.00000", seguido pelo campo de stop-loss (SL), também inicializado com "0.00000". Ao lado desses campos, criamos o seletor de datas (DatePicker), configurado por padrão para o final do dia (23:59:59).

Por fim, criamos o botão "Enviar ordem", posicionado ao lado do seletor de datas para aproveitar melhor o espaço disponível sem sobrecarregar a interface. Após criar todos os controles, deslocamos novamente a posição vertical para baixo, reservando espaço para os próximos elementos. Com isso, reunimos todos os controles necessários para que o usuário configure uma ordem pendente, incluindo tipo, preço, TP, SL e data de expiração, e depois clique no botão para enviá-la.

Tratadores de eventos das ordens pendentes

Esses métodos respondem às interações do usuário na seção "Ordens pendentes":

void CTradeManagementPanel::OnChangePendingOrderType()
{
   string selected = m_pendingOrderType.Select();
   int    index    = (int)m_pendingOrderType.Value();
   Print("OnChangePendingOrderType: Selected='", selected, "', Index=", index);

   double price = 0.0;
   if(selected == "Buy Limit" || selected == "Buy Stop")
      price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
   else
      price = SymbolInfoDouble(Symbol(), SYMBOL_BID);

   m_pendingPriceEdit.Text(DoubleToString(price, 5));
   ChartRedraw();
}

void CTradeManagementPanel::OnChangePendingDatePicker()
{
   datetime selected = m_pendingDatePicker.Value();
   Print("OnChangePendingDatePicker: Selected='", 
         TimeToString(selected, TIME_DATE|TIME_MINUTES), "'");
   ChartRedraw();
}

Ao alterar o tipo de ordem: Quando o usuário escolhe outra opção no ComboBox, por exemplo, ao mudar de "Buy Limit" para "Sell Limit", recuperamos o texto da opção selecionada e verificamos se ele começa com "Buy" ou "Sell". Se começar com "Buy", obtemos o preço ask atual; caso contrário, utilizamos o preço bid atual. Em seguida, atualizamos imediatamente o campo de preço com o valor de mercado correspondente. Assim, o usuário sempre visualiza uma cotação de mercado atualizada como referência inicial para o tipo de ordem selecionado. Por fim, redesenhamos a interface no gráfico para que o novo preço seja exibido imediatamente.

Ao alterar a data de expiração: Sempre que o usuário seleciona ou modifica a data de expiração no DatePicker, recuperamos a nova data e a registramos no log para fins de depuração. Em seguida, redesenhamos a interface no gráfico para refletir imediatamente as alterações, caso outros componentes do painel dependam visualmente da data selecionada. Nesta etapa, não realizamos nenhuma validação adicional; qualquer data válida do calendário é aceita.

Ao manter esses tratadores de eventos simples e com responsabilidades bem definidas, mantemos os controles atualizados conforme as interações do usuário, reduzindo erros na configuração das ordens. A prevenção de preços inválidos e datas de expiração vencidas, porém, depende de verificações específicas.

Função auxiliar de validação de ordens pendentes

Antes de enviar uma ordem pendente à corretora, verificamos se os dados informados pelo usuário são válidos. Essa função auxiliar centraliza três regras de validação:

  • O volume deve ser positivo. Se o volume em lotes for igual ou inferior a zero, registramos um erro no log e rejeitamos a ordem.
  • O preço deve ser positivo. Um preço igual ou inferior a zero não pode ser utilizado para criar uma ordem pendente válida.

bool CTradeManagementPanel::ValidatePendingParameters(double volume, double price, string orderType)
{
   if(volume <= 0)
   {
      Print("Invalid volume for pending order");
      return(false);
   }
   if(price <= 0)
   {
      Print("Invalid price for pending order");
      return(false);
   }
   double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
   double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);

   if(orderType == "Buy Limit" && price >= ask)
   {
      Print("Buy Limit price must be below Ask");
      return(false);
   }
   if(orderType == "Buy Stop" && price <= ask)
   {
      Print("Buy Stop price must be above Ask");
      return(false);
   }
   if(orderType == "Sell Limit" && price <= bid)
   {
      Print("Sell Limit price must be above Bid");
      return(false);
   }
   if(orderType == "Sell Stop" && price >= bid)
   {
      Print("Sell Stop price must be below Bid");
      return(false);
   }
   return(true);
}

Validação das condições de mercado:

Por exemplo:

  1. Para uma ordem "Buy Limit", verificamos se o preço limite está estritamente abaixo do preço ask atual.
  2. Para uma ordem "Buy Stop", verificamos se o preço de disparo está estritamente acima do preço ask atual.

Se todas as validações forem concluídas com sucesso, a função auxiliar retornará true, indicando que a ordem pode ser enviada. Ao centralizar essas verificações, evitamos erros comuns, como posicionar uma ordem Buy Limit acima do preço de mercado ou uma ordem Sell Stop em um preço igual ou superior ao bid atual. Além disso, quando os dados informados são inválidos, as mensagens registradas no log permitem identificar a causa da falha.

Tratador do clique no botão "Enviar ordem pendente" (Place Pending)

void CTradeManagementPanel::OnClickPlacePending()
{
   Print("OnClickPlacePending called");
   string     orderType = m_pendingOrderType.Select();
   double     price     = StringToDouble(m_pendingPriceEdit.Text());
   double     tp        = StringToDouble(m_pendingTPEdit.Text());
   double     sl        = StringToDouble(m_pendingSLEdit.Text());
   double     volume    = StringToDouble(m_volumeEdit.Text());      // reuse market‐order volume
   datetime   expiry    = m_pendingDatePicker.Value();
   ENUM_ORDER_TYPE_TIME type_time = (expiry == 0) ? ORDER_TIME_GTC : ORDER_TIME_SPECIFIED;

   // Validate inputs
   if(!ValidatePendingParameters(volume, price, orderType))
      return;

   // Place the correct type of pending order
   if(orderType == "Buy Limit")
      m_trade.BuyLimit(volume, price, Symbol(), sl, tp, type_time, expiry, "");
   else if(orderType == "Buy Stop")
      m_trade.BuyStop(volume, price, Symbol(), sl, tp, type_time, expiry, "");
   else if(orderType == "Sell Limit")
      m_trade.SellLimit(volume, price, Symbol(), sl, tp, type_time, expiry, "");
   else if(orderType == "Sell Stop")
      m_trade.SellStop(volume, price, Symbol(), sl, tp, type_time, expiry, "");
}

Quando o usuário clica no botão "Enviar ordem", esse tratador reúne todos os dados necessários:

  • O tipo de ordem selecionado no ComboBox.
  • O preço da ordem pendente informado no campo correspondente.
  • Os valores de take-profit (TP) e stop-loss (SL) informados nos respectivos campos.
  • O volume da operação, obtido do campo de volume da seção "Execução rápida" (Quick Execution).
  • A data de expiração selecionada no DatePicker.

Em seguida, verificamos se a data e a hora de expiração selecionadas são iguais a zero para determinar se devemos utilizar o modo GTC (válido até o cancelamento) ou um modo com prazo de validade definido. Depois, chamamos nossa função auxiliar de validação. Se alguma verificação falhar, encerramos a execução do tratador sem enviar a ordem.

Se todas as validações forem concluídas com sucesso, chamamos apenas um dos quatro métodos da classe CTrade: BuyLimit, BuyStop, SellLimit ou SellStop. Nessa chamada, passamos o volume, o preço, o símbolo, o stop-loss (SL), o take-profit (TP), o modo de expiração e a data de expiração. Cada método utiliza os parâmetros informados pelo usuário, de modo que o tratador envia à corretora uma solicitação de ordem pendente com os parâmetros configurados, cuja aceitação deve ser confirmada pela verificação do resultado da operação. As falhas de validação ficam registradas no log para facilitar o diagnóstico.

Roteamento de eventos no OnEvent(...) para ordens pendentes

bool CTradeManagementPanel::OnEvent(const int id, const long &lparam, 
                                    const double &dparam, const string &sparam)
{
   // 1) Forward all events to the calculator first
   if(m_calculator.OnEvent(id, lparam, dparam, sparam))
      return(true);

   // 2) Dispatch Pending‐section events
   if(id == CHARTEVENT_OBJECT_CLICK)
   {
      if(sparam == m_placePendingButton.Name())
      {
         OnClickPlacePending();
         return(true);
      }
   }
   else if(id == CHARTEVENT_OBJECT_CHANGE)
   {
      if(sparam == m_pendingOrderType.Name())
      {
         OnChangePendingOrderType();
         return(true);
      }
      else if(sparam == m_pendingDatePicker.Name())
      {
         OnChangePendingDatePicker();
         return(true);
      }
   }

   // 3) Fallback to the base class for any other events
   return CAppDialog::OnEvent(id, lparam, dparam, sparam);
}

No método principal OnEvent(...) da classe CTradeManagementPanel, os eventos relacionados às ordens pendentes são encaminhados da seguinte maneira:

Primeiro, a calculadora: Encaminhamos todos os eventos à calculadora integrada. Se a calculadora tratar o evento, como uma alteração em um campo utilizado no cálculo do valor do ponto, encerramos o processamento.

Lógica das ordens pendentes:

  • Se o evento for um clique (click) e o nome do objeto clicado corresponder ao nome do botão de ordem pendente, chamamos o tratador "Enviar ordem pendente" (Place Pending).
  • Se o evento for uma alteração de objeto (object change) e o nome do objeto modificado corresponder ao nome do ComboBox ou do DatePicker, chamamos o tratador apropriado (OnChangePendingOrderType ou OnChangePendingDatePicker).
  • Tratamento padrão: Qualquer outro evento é encaminhado ao método base CAppDialog::OnEvent(...), permitindo que as seções "Execução rápida" (Quick Execution) e "Todas as operações" (All-Ops) processem os cliques ou as alterações correspondentes.

Esse roteamento mantém o tratamento dos eventos de ordens pendentes separado dos demais componentes, evitando interferências no funcionamento das outras seções do painel.

Teste da implementação do ComboBox e do DatePicker

Teste da implementação do ComboBox e do DatePicker


(2) Desenvolvimento da classe de controle ForexValuesCalculator

Antes de definir as classes, incluímos cinco arquivos de cabeçalho da biblioteca padrão do MQL5, localizados no diretório Controls. Cada arquivo disponibiliza uma classe de controle gráfico que utilizaremos na CForexCalculator:

#include <Controls\Dialog.mqh>
#include <Controls\ComboBox.mqh>
#include <Controls\Edit.mqh>
#include <Controls\Label.mqh>
#include <Controls\Button.mqh>

Dialog.mqh

Disponibiliza a classe base CAppDialog, responsável por gerenciar um conjunto de controles, organizar o layout e encaminhar eventos. Embora CForexCalculator não herde diretamente de CAppDialog, ela precisa ser integrada a um diálogo pai, como CTradeManagementPanel. Por isso, a inclusão de Dialog.mqh permite compilar corretamente as chamadas utilizadas para adicionar os controles da calculadora (AddToDialog) e encaminhar os eventos. Sem Dialog.mqh, não poderíamos chamar dlg.Add(...) para incorporar nossos rótulos, campos de entrada e botões à interface principal.

ComboBox.mqh

Disponibiliza a classe CComboBox, utilizada para criar o menu suspenso com as opções de cálculo. Ao incluir esse arquivo, podemos criar e gerenciar uma instância de CComboBox (m_dropdown), chamar m_dropdown.Create(...) para definir sua posição, preencher a lista com AddItem e responder ao evento CHARTEVENT_OBJECT_CHANGE quando o usuário selecionar outra opção de cálculo. Sem essa inclusão, o compilador não reconheceria a classe CComboBox.

Edit.mqh

Define a classe CEdit, utilizada em todos os campos de entrada numéricos e de texto, como saldo da conta, percentual de risco, stop-loss, símbolo, entre outros. Criamos dinamicamente uma quantidade variável de controles CEdit no array m_inputs[], conforme o tipo de cálculo selecionado. Cada controle CEdit precisa ser criado, adicionado ao diálogo e posteriormente convertido de volta para CEdit nos métodos GetInputValue ou GetInputString. Sem a inclusão de Edit.mqh, nenhuma dessas chamadas seria compilada.

Label.mqh

Disponibiliza a classe CLabel, utilizada sempre que precisamos exibir textos estáticos na interface, como o rótulo "Opção de cálculo:" (m_calcOptionLabel), os rótulos individuais dos campos de entrada (saldo da conta, percentual de risco, entre outros) e o rótulo "Resultado:" (m_resultLabel). Cada CLabel precisa ser criado para que o usuário saiba quais dados deve informar em cada campo CEdit. Sem a inclusão de Label.mqh, não poderíamos identificar adequadamente a finalidade de cada campo de entrada.

Button.mqh

Disponibiliza a classe CButton, que utilizamos para criar o botão "Calcular" (m_calculateButton). Ao incluir esse arquivo, podemos chamar m_calculateButton.Create(...), configurar a cor de fundo, definir o texto do botão e detectar seus cliques por meio de OnEvent. Sem Button.mqh, o compilador não reconheceria CButton, impedindo o tratamento dos cliques no botão "Calcular".

Organização das inclusões no projeto

No projeto, o arquivo ForexValuesCalculator.mqh depende dos seguintes controles:

ForexValuesCalculator.mqh precisa dos cinco arquivos de cabeçalho Controls\*.mqh, pois implementa um "minidiálogo" independente e reutilizável para calcular diferentes valores cambiais. Sempre que utilizamos CLabel, CEdit, CComboBox ou CButton, o respectivo arquivo de cabeçalho precisa estar incluído para que o pré-processador do MQL5 encontre a definição da classe.

Ao concentrar todas as inclusões relacionadas à interface gráfica (GUI) no início do arquivo, permitimos que qualquer outro EA ou painel, como TradeManagementPanel.mqh, utilize simplesmente #include "ForexValuesCalculator.mqh" para ter acesso a todos os controles gráficos necessários, sem precisar adicionar inclusões em vários pontos do código.

Declaração dos membros da classe

A classe CForexCalculator começa com a declaração de diversos controles de interface e estruturas de dados que, em conjunto, compõem a calculadora. Na parte superior, um rótulo (m_calcOptionLabel) e um menu suspenso (m_dropdown) permitem que o usuário escolha o cálculo desejado, como tamanho da posição, valor em risco, valor do ponto, lucro/prejuízo ou relação risco-retorno. Abaixo desses controles, temos o botão "Calcular" (m_calculateButton), que o usuário aciona depois de preencher todos os dados necessários. Para apresentar os resultados, utilizamos um campo de edição somente leitura (m_resultField) e um rótulo (m_resultLabel). O rótulo exibe a descrição do cálculo, como "Resultado:", enquanto o campo apresenta o valor numérico correspondente.
// Forex Calculator Class
class CForexCalculator {
private:
   CLabel      m_calcOptionLabel;   // “Calculation Option:” label
   CComboBox   m_dropdown;          // Dropdown for selecting calculation term
   CEdit       m_resultField;       // Read-only field to display result
   CLabel      m_resultLabel;       // Label preceding the result (e.g., “Result:”)
   CButton     m_calculateButton;   // “Calculate” button
   CWnd       *m_inputs[];          // Dynamically added label+edit pairs
   long        m_chart_id;          // Chart identifier
   string      m_name;              // Prefix for control names
   int         m_originX;           // X-coordinate origin for dynamic fields
   int         m_originY;           // Y-coordinate origin for dynamic fields

   InputField  m_positionSizeInputs[4];
   InputField  m_riskAmountInputs[3];
   InputField  m_pipValueInputs[3];
   InputField  m_profitLossInputs[4];
   InputField  m_riskRewardInputs[2];

   // … (other private methods follow) …
public:
   CForexCalculator();
   bool Create(const long chart, const string &name, const int subwin,
               const int x, const int y, const int w, const int h);
   bool AddToDialog(CAppDialog &dlg);
   void UpdateResult(const string term);
   double GetInputValue(const string name);
   string GetInputString(const string &name);
   CEdit* GetInputEdit(const string &name);
   string GetSelectedTerm();
   bool OnEvent(const int id, const long &lparam,
                const double &dparam, const string &sparam);
   ~CForexCalculator();
};

Os controles dos campos de entrada dinâmicos, organizados em pares de rótulos e campos de edição, são armazenados no array m_inputs[]. Internamente, a classe também contém cinco arrays de tamanho fixo, formados por estruturas InputField (m_positionSizeInputs, m_riskAmountInputs, m_pipValueInputs, m_profitLossInputs, m_riskRewardInputs). Cada estrutura InputField contém um nome, o texto do rótulo e um valor numérico padrão. Por fim, m_originX e m_originY definem a posição inicial do painel da calculadora dentro do diálogo pai, enquanto m_chart_id e m_name armazenam, respectivamente, o identificador do gráfico e o prefixo utilizado para gerar nomes exclusivos para os controles. Em conjunto, esses membros definem tanto o layout da calculadora quanto os dados necessários para cada tipo de cálculo de Forex.

Inicialização dos valores padrão estáticos (InitInputs)

O método InitInputs é executado uma única vez, quando o objeto da calculadora é criado. Ele preenche os cinco arrays de estruturas InputField com rótulos descritivos e valores numéricos provisórios. Por exemplo, o grupo "Tamanho da posição" contém campos para saldo da conta, percentual de risco, stop-loss em pips e símbolo. Já o grupo "Valor em risco" contém campos para tamanho da posição, stop-loss em pips e símbolo. Cada array é configurado para que, posteriormente, quando o usuário selecionar um tipo de cálculo, os dados do array de estruturas InputField correspondente sejam transferidos para os controles dinâmicos da interface. Nesta etapa, o campo "Saldo da conta" recebe o valor provisório 0.0, que será substituído durante a execução, enquanto o percentual de risco e as distâncias em pips recebem valores padrão como 1% ou 20 pips. Essa inicialização estática garante que os campos de cada cálculo sejam apresentados com rótulos claros e valores numéricos iniciais.

void InitInputs()
{
   // Position Size inputs
   m_positionSizeInputs[0].name         = "accountBalance";
   m_positionSizeInputs[0].label        = "Account Balance (" + AccountInfoString(ACCOUNT_CURRENCY) + ")";
   m_positionSizeInputs[0].defaultValue = 0.0;  // updated at runtime
   m_positionSizeInputs[1].name         = "riskPercent";
   m_positionSizeInputs[1].label        = "Risk Percentage (%)";
   m_positionSizeInputs[1].defaultValue = 1.0;
   m_positionSizeInputs[2].name         = "stopLossPips";
   m_positionSizeInputs[2].label        = "Stop Loss (Pips)";
   m_positionSizeInputs[2].defaultValue = 20.0;
   m_positionSizeInputs[3].name         = "symbol";
   m_positionSizeInputs[3].label        = "Symbol";
   m_positionSizeInputs[3].defaultValue = 0.0;

   // Risk Amount inputs
   m_riskAmountInputs[0].name = "positionSize";
   m_riskAmountInputs[0].label = "Position Size (Lots)";
   m_riskAmountInputs[0].defaultValue = 0.1;
   m_riskAmountInputs[1].name = "stopLossPips";
   m_riskAmountInputs[1].label = "Stop Loss (Pips)";
   m_riskAmountInputs[1].defaultValue = 20.0;
   m_riskAmountInputs[2].name = "symbol";
   m_riskAmountInputs[2].label = "Symbol";
   m_riskAmountInputs[2].defaultValue = 0.0;

   // Pip Value inputs
   m_pipValueInputs[0].name = "lotSize";
   m_pipValueInputs[0].label = "Lot Size";
   m_pipValueInputs[0].defaultValue = 0.1;
   m_pipValueInputs[1].name = "symbol";
   m_pipValueInputs[1].label = "Symbol";
   m_pipValueInputs[1].defaultValue = 0.0;
   m_pipValueInputs[2].name = "accountCurrency";
   m_pipValueInputs[2].label = "Account Currency";
   m_pipValueInputs[2].defaultValue = 0.0;

   // Profit/Loss inputs
   m_profitLossInputs[0].name = "entryPrice";
   m_profitLossInputs[0].label = "Entry Price";
   m_profitLossInputs[0].defaultValue = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   m_profitLossInputs[1].name = "exitPrice";
   m_profitLossInputs[1].label = "Exit Price";
   m_profitLossInputs[1].defaultValue = SymbolInfoDouble(_Symbol, SYMBOL_BID) + 0.0020;
   m_profitLossInputs[2].name = "lotSize";
   m_profitLossInputs[2].label = "Lot Size";
   m_profitLossInputs[2].defaultValue = 0.1;
   m_profitLossInputs[3].name = "symbol";
   m_profitLossInputs[3].label = "Symbol";
   m_profitLossInputs[3].defaultValue = 0.0;

   // Risk-to-Reward inputs
   m_riskRewardInputs[0].name = "takeProfitPips";
   m_riskRewardInputs[0].label = "Take Profit (Pips)";
   m_riskRewardInputs[0].defaultValue = 40.0;
   m_riskRewardInputs[1].name = "stopLossPips";
   m_riskRewardInputs[1].label = "Stop Loss (Pips)";
   m_riskRewardInputs[1].defaultValue = 20.0;
}

Definição dos valores padrão durante a execução (SetDynamicDefaults)

Como o saldo real da conta do usuário só pode ser obtido durante a execução, o método SetDynamicDefaults atualiza m_positionSizeInputs[0].defaultValue (campo "Saldo da conta") com o valor retornado por AccountInfoDouble(ACCOUNT_BALANCE). Assim, quando os campos do cálculo de "Tamanho da posição" forem exibidos, o campo de saldo da conta já estará preenchido com o saldo atual do trader. Outros valores padrão dinâmicos, como os preços bid/ask ou as taxas de conversão, também poderão ser atualizados logo após a criação da calculadora. Ao separar os valores padrão estáticos daqueles definidos durante a execução, mantemos a flexibilidade da classe: InitInputs concentra a inicialização dos valores predefinidos, enquanto SetDynamicDefaults atualiza os valores padrão que dependem dos dados atuais da conta ou do mercado.

void SetDynamicDefaults()
{
   // Overwrite the “Account Balance” default with the real balance at runtime
   m_positionSizeInputs[0].defaultValue = AccountInfoDouble(ACCOUNT_BALANCE);
}

Métodos auxiliares para os principais cálculos

Após os arrays de entrada, temos uma série de métodos auxiliares responsáveis por executar cada fórmula:

1. CalculatePipValue (calcular o valor do ponto)

double CalculatePipValue(const string symbol, const double lotSize, const string accountCurrency)
{
   double tickSize  = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
   double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
   double pipSize   = (StringFind(symbol, "JPY") >= 0) ? 0.01 : 0.0001;
   double rate      = 1.0;
   string profitCcy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
   if(accountCurrency != profitCcy)
   {
      string pair = profitCcy + accountCurrency;
      if(SymbolSelect(pair, true))
         rate = SymbolInfoDouble(pair, SYMBOL_BID);
   }
   if(tickSize == 0.0) return 0.0;
   return NormalizeDouble((tickValue / tickSize) * pipSize * lotSize * rate, 2);
}

O método CalculatePipValue calcula o valor monetário de um pip na moeda da conta, considerando determinado símbolo e volume em lotes. Primeiro, chama SymbolInfoDouble para obter os valores de SYMBOL_TRADE_TICK_SIZE e SYMBOL_TRADE_TICK_VALUE. Em seguida, define o tamanho do pip (pip size) como 0.01 para pares cotados em ienes, como USDJPY ou EURJPY, e 0.0001 para os demais pares convencionais. Se a moeda em que o lucro do par é calculado for diferente da moeda da conta, o método combina as duas moedas para formar um símbolo de conversão (por exemplo, "EURUSD", quando o lucro é calculado em EUR e a conta está denominada em USD), seleciona esse símbolo e obtém seu preço bid atual como taxa de conversão (rate). Por fim, divide tickValue por tickSize, multiplica o resultado por pipSize, lotSize e rate, e retorna o valor arredondado para duas casas decimais. O retorno de 0.0 indica que algum dado de entrada é inválido, como no caso de tickSize ser igual a zero.

2. CalculatePositionSize (calcular o tamanho da posição)

double CalculatePositionSize(double bal, double pct, double sl, string sym)
{
   double pv = CalculatePipValue(sym, 1.0, AccountInfoString(ACCOUNT_CURRENCY));
   if(bal <= 0 || pct <= 0 || sl <= 0 || pv <= 0) return 0.0;
   double size = (bal * (pct / 100.0)) / (sl * pv);
   double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
   double minL = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
   double maxL = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
   int dp = (int)-MathLog10(step);
   return NormalizeDouble(MathMax(minL, MathMin(maxL, size)), dp);
}

Com o saldo da conta, o percentual de risco e o stop-loss em pips, o método CalculatePositionSize calcula o tamanho da posição em lotes com base no risco definido. Primeiro, chama CalculatePipValue(sym, 1.0, AccountInfoString(ACCOUNT_CURRENCY)) para obter o valor de um pip para um lote. Se algum dado de entrada ou o valor do pip (pv) for igual ou inferior a zero, a função retornará 0.

Caso contrário, utiliza a fórmula:

positionSize = (balance × (riskPercent / 100)) ÷ (stopLossPips × pipValue)

Em seguida, a função obtém os valores de SYMBOL_VOLUME_STEP, SYMBOL_VOLUME_MIN e SYMBOL_VOLUME_MAX do instrumento para ajustar o tamanho calculado da posição aos limites e incrementos de volume permitidos. O número de casas decimais utilizado no arredondamento (dp) é calculado a partir de -MathLog10(step). Essa abordagem atende aos incrementos decimais usuais, como 0.01 e 0.1, mas a adequação do volume aos incrementos permitidos pela corretora deve ser verificada separadamente.

3. CalculateRiskAmount (calcular o valor em risco)

Quando o usuário conhece o tamanho da posição (ps, em lotes) e o stop-loss em pips (sl), o método CalculateRiskAmount calcula o valor do capital exposto ao risco, expresso na moeda da conta. Primeiro, obtém o valor do pip para o volume ps por meio de CalculatePipValue(sym, ps, ...) e multiplica esse resultado por sl para calcular o valor em risco. O volume não deve ser aplicado novamente se já estiver incorporado ao valor retornado pela função. O resultado é normalizado para duas casas decimais. Se algum dos parâmetros de entrada for igual ou inferior a zero, a função retornará 0.0, indicando que os dados são inválidos.

double CalculateRiskAmount(double ps, double sl, string sym)
{
   if(ps <= 0 || sl <= 0) return 0.0;
   double pv = CalculatePipValue(sym, ps, AccountInfoString(ACCOUNT_CURRENCY));
   return NormalizeDouble(ps * sl * pv, 2);
}

4. CalculateProfitLoss (calcular o lucro/prejuízo)

double CalculateProfitLoss(double entry, double exit, double lotSize, string sym)
{
   if(entry <= 0 || exit <= 0 || lotSize <= 0) return 0.0;
   double pipSz = (StringFind(sym, "JPY") >= 0) ? 0.01 : 0.0001;
   double diff  = (exit - entry) / pipSz;
   return NormalizeDouble(diff * CalculatePipValue(sym, lotSize, AccountInfoString(ACCOUNT_CURRENCY)), 2);
}

O método CalculateProfitLoss determina o lucro ou prejuízo estimado na moeda da conta, considerando o preço de entrada, o preço de saída, o volume em lotes e o símbolo. Primeiro, calcula a variação em pips pela fórmula (preçoSaída − preçoEntrada) ÷ tamanhoDoPip para posições compradas; para posições vendidas, inverte-se o sinal da diferença. O tamanhoDoPip corresponde a 0.01 para pares cotados em ienes, como USDJPY e EURJPY, e 0.0001 para os demais pares convencionais. Em seguida, multiplica a variação em pips pelo valor retornado por CalculatePipValue(sym, lotSize, accountCurrency), convertendo-a em lucro ou prejuízo na moeda da conta. O resultado final é arredondado para duas casas decimais. Se algum dos valores numéricos informados for inválido, o método retornará 0.0.

5. CalculateRiskRewardRatio (calcular a relação risco-retorno)

double CalculateRiskRewardRatio(double tp, double sl)
{
   if(tp <= 0 || sl <= 0) return 0.0;
   return NormalizeDouble(tp / sl, 2);
}

Para calcular a "Relação risco-retorno", o usuário precisa informar apenas o take-profit em pips (tp) e o stop-loss em pips (sl). Desde que ambos os valores sejam positivos, a função retorna a relação tp / sl, arredondada para duas casas decimais. Se algum dos parâmetros de entrada for igual ou inferior a zero, ela retornará 0.0, indicando que os dados são inválidos.

Métodos auxiliares de layout: Adição de campos individuais (AddField)

O método AddField é responsável por criar um conjunto formado por um rótulo e um campo de edição para cada estrutura InputField. Ele recebe uma referência a InputField, que contém o nome, o texto do rótulo e o valor padrão, além da posição vertical atual y. O método calcula x0 = m_originX + CALC_INDENT_LEFT para manter todos os rótulos alinhados com um recuo uniforme à esquerda.

bool AddField(const InputField &f, int &y)
{
   int x0 = m_originX + CALC_INDENT_LEFT;

   // Create label
   CLabel *lbl = new CLabel();
   if(!lbl.Create(m_chart_id, m_name + "Lbl_" + f.name, 0,
                  x0, y, 
                  x0 + CALC_LABEL_WIDTH, y + CALC_EDIT_HEIGHT))
   {
      delete lbl;
      return false;
   }
   lbl.Text(f.label);
   ArrayResize(m_inputs, ArraySize(m_inputs) + 1);
   m_inputs[ArraySize(m_inputs) - 1] = lbl;

   // Create edit
   CEdit *edt = new CEdit();
   if(!edt.Create(m_chart_id, m_name + "Inp_" + f.name, 0,
                  x0 + CALC_EDIT_OFFSET + RESULT_BUTTON_GAP, y,
                  x0 + CALC_EDIT_OFFSET + RESULT_BUTTON_GAP + CALC_EDIT_WIDTH,
                  y + CALC_EDIT_HEIGHT))
   {
      delete edt;
      return false;
   }
   if(f.name == "symbol")
      edt.Text(_Symbol);
   else if(f.name == "accountCurrency")
      edt.Text(AccountInfoString(ACCOUNT_CURRENCY));
   else
      edt.Text(StringFormat("%.2f", f.defaultValue));

   ArrayResize(m_inputs, ArraySize(m_inputs) + 1);
   m_inputs[ArraySize(m_inputs) - 1] = edt;

   y += CALC_EDIT_HEIGHT + CALC_CONTROLS_GAP_Y;
   return true;
}

O método AddField recebe dois argumentos: uma referência à estrutura InputField, que contém o nome, o rótulo e o valor padrão, e a posição vertical atual y. Primeiro, calcula x0 = m_originX + CALC_INDENT_LEFT para definir a posição da borda esquerda do rótulo. Em seguida, cria um novo CLabel com o nome m_name + "Lbl_" + f.name, posicionado em (x0, y), com largura e altura fixas. O texto do rótulo é definido por f.label, e o controle é adicionado ao array m_inputs[].

Depois, cria um CEdit na posição (x0 + CALC_EDIT_OFFSET + RESULT_BUTTON_GAP, y), mantendo todos os campos de edição alinhados. Se f.name for igual a "symbol", o campo será preenchido inicialmente com _Symbol; se for "accountCurrency", receberá a moeda da conta. Nos demais casos, f.defaultValue será formatado com duas casas decimais. O novo controle CEdit também é adicionado ao array m_inputs[]. Por fim, incrementamos y pela altura do controle mais CALC_CONTROLS_GAP_Y, definindo a posição do próximo campo. Ao adicionar cada conjunto de rótulo e campo de edição ao array m_inputs[], AddField permite que esses controles sejam posteriormente incorporados ao diálogo e gerenciados corretamente.

Criação de todos os campos de entrada para o cálculo selecionado (CreateInputFields)

Sempre que o usuário seleciona outro tipo de cálculo, ou durante a criação inicial da calculadora, o método CreateInputFields libera o buffer do array dinâmico m_inputs por meio de ArrayFree(m_inputs) e define a posição y logo abaixo do menu suspenso. Essa operação não destrói automaticamente os objetos alocados; sua gestão exige tratamento específico. Em seguida, identifica o cálculo selecionado: "Tamanho da posição" (4 campos de entrada), "Valor em risco" (3), "Valor do ponto" (3), "Lucro/Prejuízo" (4) ou "Relação risco-retorno" (2). Para cada estrutura InputField do array correspondente, chama AddField(...). Se alguma chamada de AddField falhar, o método retornará false, interrompendo a criação dos demais controles. Se todos os campos forem adicionados com sucesso, retornará true. Dessa forma, durante a execução, a interface exibe apenas os conjuntos de rótulos e campos de edição necessários para o cálculo selecionado, alinhados verticalmente e com espaçamento uniforme.

bool CreateInputFields(const string term)
{
   ArrayFree(m_inputs);
   int y = m_originY + CALC_INDENT_TOP + CALC_EDIT_HEIGHT + CALC_CONTROLS_GAP_Y;

   if(term == "Position Size")
      for(int i = 0; i < 4; i++)
         if(!AddField(m_positionSizeInputs[i], y)) return false;
   else if(term == "Risk Amount")
      for(int i = 0; i < 3; i++)
         if(!AddField(m_riskAmountInputs[i], y)) return false;
   else if(term == "Pip Value")
      for(int i = 0; i < 3; i++)
         if(!AddField(m_pipValueInputs[i], y)) return false;
   else if(term == "Profit/Loss")
      for(int i = 0; i < 4; i++)
         if(!AddField(m_profitLossInputs[i], y)) return false;
   else if(term == "Risk-to-Reward")
      for(int i = 0; i < 2; i++)
         if(!AddField(m_riskRewardInputs[i], y)) return false;
   else
      return false;

   return true;
}

Criação do painel (Create)

Quando o método Create é chamado, a interface gráfica da calculadora é criada dentro do diálogo pai. Primeiro, armazenamos o identificador do gráfico, o prefixo utilizado nos nomes dos controles e as coordenadas iniciais (x, y). Em seguida, criamos os seguintes componentes:

  • Rótulo da opção de cálculo

O rótulo estático m_calcOptionLabel é criado na posição (x, y), com o texto "Opção de cálculo:". Ele fica posicionado à esquerda do menu suspenso.

  • Menu suspenso

O CComboBox (m_dropdown) é criado na posição (comboX + 70, y), à direita do rótulo "Opção de cálculo:". A lista é preenchida com cinco opções de cálculo. A chamada m_dropdown.Select(0) define "Tamanho da posição" como a opção selecionada por padrão.

  • Botão "Calcular"

O CButton (m_calculateButton) é posicionado na parte inferior da área do painel, com suas coordenadas definidas pelos cálculos de btnX e btnY. O botão recebe o texto "Calcular", com fundo azul-aço e texto branco. Quando acionado, executará o método UpdateResult.

bool Create(const long chart, const string &name, const int subwin,
            const int x, const int y, const int w, const int h)
{
   m_chart_id = chart;
   m_name     = name + "_Calc_";
   m_originX  = x;
   m_originY  = y;

   // 1) “Calculation Option:” label
   if(!m_calcOptionLabel.Create(chart, m_name + "CalcOptLbl", subwin,
                                x, y, x + CALC_LABEL_WIDTH, y + CALC_EDIT_HEIGHT))
      return false;
   m_calcOptionLabel.Text("Calculation Option:");

   // 2) Dropdown immediately to the right
   int comboX = x + CALC_LABEL_WIDTH + DROPDOWN_LABEL_GAP;
   if(!m_dropdown.Create(chart, m_name + "Dropdown", subwin,
                        comboX, y, comboX + (w - CALC_LABEL_WIDTH - DROPDOWN_LABEL_GAP), y + CALC_EDIT_HEIGHT))
      return false;
   m_dropdown.AddItem("Position Size");
   m_dropdown.AddItem("Risk Amount");
   m_dropdown.AddItem("Pip Value");
   m_dropdown.AddItem("Profit/Loss");
   m_dropdown.AddItem("Risk-to-Reward");
   m_dropdown.Select(0);

   // 3) “Calculate” button near the bottom of this panel area
   int btnX = x + w - CALC_BUTTON_WIDTH - 120;
   int btnY = y + h - CALC_BUTTON_HEIGHT + 30;
   if(!m_calculateButton.Create(chart, m_name + "CalcBtn", subwin,
                                btnX, btnY, btnX + CALC_BUTTON_WIDTH, btnY + CALC_BUTTON_HEIGHT))
      return false;
   m_calculateButton.Text("Calculate");
   m_calculateButton.ColorBackground(clrSteelBlue);
   m_calculateButton.Color(clrWhite);

   // 4) Result label and read-only field to the right of the button
   int blockX = btnX + CALC_BUTTON_WIDTH + RESULT_BUTTON_GAP;
   int lblY = btnY - 20;
   if(!m_resultLabel.Create(chart, m_name + "ResultLbl", subwin,
                            blockX, lblY, blockX + CALC_LABEL_WIDTH, lblY + CALC_EDIT_HEIGHT))
      return false;
   m_resultLabel.Text("Result:");

   int fldY = lblY + CALC_EDIT_HEIGHT + RESULT_VERTICAL_GAP;
   if(!m_resultField.Create(chart, m_name + "ResultFld", subwin,
                            blockX, fldY, blockX + CALC_EDIT_WIDTH, fldY + CALC_EDIT_HEIGHT))
      return false;
   m_resultField.ReadOnly(true);

   // 5) Populate dynamic defaults and input rows
   SetDynamicDefaults();
   string initialTerm = m_dropdown.Select();
   CreateInputFields(initialTerm);
   UpdateResult(initialTerm);

   return true;
}

Rótulo e campo de resultado

À direita do botão, criamos uma seção separada com o rótulo "Resultado:". Logo abaixo, adicionamos um campo de edição somente leitura (m_resultField). Esse campo exibe o resultado numérico do cálculo realizado.

Linhas dinâmicas

O método SetDynamicDefaults() atualiza o valor padrão do saldo da conta. Em seguida, recuperamos a opção de cálculo selecionada no momento (m_dropdown.Select()) e chamamos CreateInputFields(...) passando a opção de cálculo selecionada para gerar os respectivos conjuntos de rótulos e campos de entrada. Por fim, passamos a mesma opção para UpdateResult(...), que preenche o campo de resultado com o cálculo inicial.

Como o menu suspenso, o botão de cálculo e a área de resultado já foram posicionados, os campos dinâmicos são inseridos entre esses componentes, mantendo o alinhamento e o espaçamento uniformes. Se alguma chamada de criação falhar, o método Create retornará false, permitindo que o código responsável pela chamada identifique que a inicialização da calculadora não foi concluída.

Adição dos controles ao diálogo pai (AddToDialog)

Após a criação bem-sucedida de todos os controles em Create(...), o EA ou painel pai chama o método AddToDialog. Esse método adiciona cada controle estático, m_calcOptionLabel, m_dropdown, m_calculateButton, m_resultLabel e m_resultField, à lista interna de controles do diálogo. Em seguida, percorre o array dinâmico m_inputs[], que armazena individualmente os controles CLabel e CEdit de cada par de rótulo e campo de edição, e também os adiciona ao diálogo. Se alguma chamada de Add(...) falhar, o método retornará false, indicando ao código responsável pela chamada que a integração da calculadora não foi concluída.

bool AddToDialog(CAppDialog &dlg)
{
   if(!dlg.Add(&m_calcOptionLabel)) return false;
   if(!dlg.Add(&m_dropdown))        return false;
   if(!dlg.Add(&m_calculateButton)) return false;
   if(!dlg.Add(&m_resultLabel))     return false;
   if(!dlg.Add(&m_resultField))     return false;

   for(int i = 0; i < ArraySize(m_inputs); i++)
      if(!dlg.Add(m_inputs[i])) return false;

   return true;
}

Atualização da exibição do resultado (UpdateResult)

void UpdateResult(const string term)
{
   double res = 0.0;
   string txt = "Result: ";

   if(term == "Position Size")
   {
      double bal = GetInputValue("accountBalance");
      double pct = GetInputValue("riskPercent");
      double sl  = GetInputValue("stopLossPips");
      string sym = GetInputString("symbol");
      if(bal > 0 && pct > 0 && sl > 0 && SymbolSelect(sym, true))
      {
         res = CalculatePositionSize(bal, pct, sl, sym);
         txt += "Position Size (lots)";
      }
      else txt += "Invalid Input";
   }
   else if(term == "Risk Amount")
   {
      double ps  = GetInputValue("positionSize");
      double slp = GetInputValue("stopLossPips");
      string sym = GetInputString("symbol");
      if(ps > 0 && slp > 0 && SymbolSelect(sym, true))
      {
         res = CalculateRiskAmount(ps, slp, sym);
         txt += "Risk Amount (" + AccountInfoString(ACCOUNT_CURRENCY) + ")";
      }
      else txt += "Invalid Input";
   }
   else if(term == "Pip Value")
   {
      double ls  = GetInputValue("lotSize");
      string sym = GetInputString("symbol");
      string cur = GetInputString("accountCurrency");
      if(ls > 0 && SymbolSelect(sym, true))
      {
         res = CalculatePipValue(sym, ls, cur);
         txt += "Pip Value (" + cur + ")";
      }
      else txt += "Invalid Input";
   }
   else if(term == "Profit/Loss")
   {
      double e   = GetInputValue("entryPrice");
      double x   = GetInputValue("exitPrice");
      double ls  = GetInputValue("lotSize");
      string sym = GetInputString("symbol");
      if(e > 0 && x > 0 && ls > 0 && SymbolSelect(sym, true))
      {
         res = CalculateProfitLoss(e, x, ls, sym);
         txt += "Profit/Loss (" + AccountInfoString(ACCOUNT_CURRENCY) + ")";
      }
      else txt += "Invalid Input";
   }
   else if(term == "Risk-to-Reward")
   {
      double tp  = GetInputValue("takeProfitPips");
      double slp = GetInputValue("stopLossPips");
      if(tp > 0 && slp > 0)
      {
         res = CalculateRiskRewardRatio(tp, slp);
         txt += "Risk-to-Reward Ratio";
      }
      else txt += "Invalid Input";
   }

   m_resultField.Text(StringFormat("%.2f", res));
   m_resultLabel.Text(txt);
}

O método UpdateResult identifica o tipo de cálculo selecionado (term) e utiliza a combinação apropriada de GetInputValue e GetInputString para obter todos os dados necessários. Por exemplo:

  • Tamanho da posição: obtém "accountBalance", "riskPercent", "stopLossPips" e "symbol". Se os dados forem válidos, chama CalculatePositionSize(...) e define o texto do rótulo como "Tamanho da posição (lotes)".
  • Valor em risco: obtém os valores de "positionSize", "stopLossPips" e "symbol". Se os dados forem válidos, chama CalculateRiskAmount(...) e define o texto como "Valor em risco (USD)".
  • Valor do ponto: obtém "lotSize", "symbol" e "accountCurrency". Em seguida, chama CalculatePipValue(...) e define o texto como "Valor do ponto (USD)".
  • Lucro/Prejuízo: obtém "entryPrice", "exitPrice", "lotSize" e "symbol". Em seguida, chama CalculateProfitLoss(...) e define o texto como "Lucro/Prejuízo (USD)".
  • Relação risco-retorno: obtém "takeProfitPips" e "stopLossPips". Em seguida, chama CalculateRiskRewardRatio(...) e define o texto como "Relação risco-retorno".

Se algum parâmetro de entrada for inválido ou não for possível selecionar o símbolo, o método define txt = "Result: Invalid Input" ("Resultado: Entrada inválida"). Em todos os casos, atualiza m_resultField.Text com o resultado numérico (res), formatado com duas casas decimais, e chama m_resultLabel.Text(txt) para ajustar o texto descritivo exibido acima do campo. Dessa forma, sempre que o usuário clicar no botão "Calcular" ou alterar a opção do menu suspenso, o campo numérico será atualizado com o valor calculado, enquanto o rótulo exibirá a descrição do cálculo ou a mensagem de erro correspondente.

Leitura dos dados informados pelo usuário (GetInputValue e GetInputString)

double GetInputValue(const string name)
{
   for(int i = 0; i < ArraySize(m_inputs); i++)
      if(m_inputs[i].Name() == m_name + "Inp_" + name)
         return StringToDouble(((CEdit*)m_inputs[i]).Text());
   return 0.0;
}

string GetInputString(const string &name)
{
   for(int i = 0; i < ArraySize(m_inputs); i++)
      if(m_inputs[i].Name() == m_name + "Inp_" + name)
         return ((CEdit*)m_inputs[i]).Text();
   return "";
}

Esses métodos auxiliares simplificam a localização do campo de edição correspondente no array dinâmico m_inputs[]. Ao receber o nome de um campo, como "stopLossPips", GetInputValue percorre todos os elementos m_inputs[i], verifica se o valor retornado por Name() corresponde a m_name + "Inp_stopLossPips" e, em seguida, obtém o texto retornado por Text(), converte seu conteúdo em um valor numérico e retorna o resultado. De maneira semelhante, GetInputString retorna o texto sem conversão, como "EURUSD", quando recebe nomes de campos como "symbol" ou "accountCurrency". Se nenhum controle correspondente for encontrado, os métodos retornam 0.0 ou uma string vazia, respectivamente, indicando que o campo não foi localizado.

Roteamento das ações do usuário (OnEvent)

bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if(id == CHARTEVENT_OBJECT_CHANGE && sparam == m_name + "Dropdown")
   {
      long idx = m_dropdown.Value();
      string term = GetSelectedTerm();
      CreateInputFields(term);
      UpdateResult(term);
      return true;
   }
   if(id == CHARTEVENT_OBJECT_CLICK && sparam == m_name + "CalcBtn")
   {
      string term = GetSelectedTerm();
      UpdateResult(term);
      return true;
   }
   return false;
}

A calculadora trata dois tipos de eventos:

1. Alterações no menu suspenso (CHARTEVENT_OBJECT_CHANGE)

  • Se sparam corresponder ao nome do controle do menu suspenso, recuperamos a nova opção selecionada por meio de m_dropdown.Select().
  • Chamamos CreateInputFields(...) passando a opção de cálculo selecionada para recriar os pares de rótulos e campos de edição correspondentes.
  • Em seguida, UpdateResult(...) executa novamente o cálculo com os valores padrão ou com os dados já informados, permitindo visualizar imediatamente uma prévia do resultado.
  • O retorno de true informa ao diálogo pai que o evento foi tratado.

2. Cliques no botão "Calcular" (CHARTEVENT_OBJECT_CLICK)

  • Se sparam corresponder a m_name + "CalcBtn", recuperamos novamente a opção de cálculo selecionada e chamamos UpdateResult(...).
  • Isso permite que o usuário altere qualquer valor de entrada, como a distância do stop-loss em pips, e depois clique em "Calcular" para atualizar o resultado.

Para qualquer outro evento, o método retorna false, permitindo que o CAppDialog pai ou outro componente trate o evento, se necessário. Essa separação garante que apenas as interações relevantes, como a seleção de outro tipo de cálculo ou o clique no botão, acionem novos cálculos ou atualizações da interface.

Liberação de recursos (~CForexCalculator)

~CForexCalculator()
{
   for(int i = 0; i < ArraySize(m_inputs); i++)
      delete m_inputs[i];
}

Quando o objeto da calculadora é destruído, seu destrutor percorre m_inputs[] e libera os controles que ainda gerencia. ArrayFree(m_inputs) libera o buffer do array, mas não destrói automaticamente os objetos criados dinamicamente. Ao trocar o tipo de cálculo, os controles substituídos precisam ser tratados antes que suas referências sejam perdidas, de acordo com a propriedade dos objetos no diálogo. Ocultar ou fechar visualmente o painel não equivale, por si só, a destruir a instância.


(3) Integração da calculadora de valores cambiais ao Painel de Gerenciamento de Operações.

A integração de CForexCalculator à classe CTradeManagementPanel começa com a declaração de uma instância da calculadora como variável-membro do painel. Ao declarar m_calculator entre os membros protegidos, incorporamos a instância de CForexCalculator ao estado do painel. A calculadora passa a gerenciar seus próprios controles, incluindo o menu suspenso, os rótulos, os campos de edição e os botões, cujos objetos podem ser alocados separadamente na memória.

CForexCalculator m_calculator;

Como o arquivo de cabeçalho do painel já inclui ForexValuesCalculator, o compilador conhece a estrutura da classe CForexCalculator e todas as suas dependências. Na prática, isso significa que não precisamos duplicar os controles da calculadora nem reorganizar seu código. Em vez disso, utilizamos a composição de classes. O painel pode gerenciar m_calculator como qualquer outro controle, criando-o, definindo suas dimensões, adicionando-o ao diálogo e encaminhando eventos, sem precisar acessar seus membros privados.

#include <ForexValuesCalculator.mqh>

Criação e organização do layout no método Create()

A próxima etapa ocorre dentro do método Create() do painel, no qual organizamos sequencialmente as quatro seções. Após configurar o layout da seção "Execução rápida" e desenhar a primeira linha divisória, passamos à seção "Calculadora Forex", começando pela criação do rótulo com o título da seção:

if(!CreateLabelEx(m_secCalcLabel, curX, curY, DEFAULT_LABEL_HEIGHT, 

                  "SecCalc", "Forex Values Calculator:", clrNavy))

   return(false)

m_secCalcLabel.Font("Arial Bold");

m_secCalcLabel.FontSize(10);

curY += DEFAULT_LABEL_HEIGHT + GAP;

Logo depois, chamamos o método Create da própria calculadora, passando o identificador do gráfico atual, um prefixo exclusivo (por exemplo, name + "_ForexCalc"), o índice da subjanela e as coordenadas exatas (x, y), juntamente com CALCULATOR_WIDTH e CALCULATOR_HEIGHT:

string calcName = name + "_ForexCalc";

if(!m_calculator.Create(chart, calcName, subwin, 

                        curX, curY, CALCULATOR_WIDTH, CALCULATOR_HEIGHT))

   return(false);

if(!m_calculator.AddToDialog(this))

   return(false);

curY += CALCULATOR_HEIGHT + GAP * 2;

Internamente, CForexCalculator::Create utiliza o mesmo sistema de coordenadas para posicionar o menu suspenso na parte superior da área da calculadora e reservar espaço para os campos de entrada dinâmicos logo abaixo. Como fornecemos uma altura fixa, a classe consegue determinar exatamente onde posicionar o rótulo e o campo de resultado na parte inferior. Assim que m_calculator.Create() retorna true, chamamos imediatamente m_calculator.AddToDialog(this), que percorre todos os controles internos, incluindo m_dropdown, os conjuntos de CLabel/CEdit criados dinamicamente, o botão "Calcular" e os componentes de exibição do resultado, adicionando-os ao CAppDialog pai. Esse registro é essencial, pois permite integrar os controles da calculadora ao tratamento de eventos e à interface do diálogo. A ordem de sobreposição (z-order) também depende do posicionamento e das propriedades dos controles.

Definição das dimensões, posicionamento e espaçamento

Manter o espaçamento adequado entre as seções é fundamental para evitar a sobreposição visual dos componentes. Após adicionar a calculadora, incrementamos curY pela altura total definida em CALCULATOR_HEIGHT, acrescida do dobro do espaçamento entre as seções. Dessa forma, garantimos que a próxima linha divisória ou a seção "Ordens pendentes" comece exatamente abaixo da área da calculadora, com limites de posicionamento bem definidos. Durante essa organização do layout, não precisamos ajustar manualmente a posição de cada controle em relação aos demais. A sequência de criação do rótulo de título, posicionamento da calculadora em coordenadas conhecidas e posterior atualização da posição vertical garante que a seção "Calculadora" permaneça independente das demais.

Como definimos constantes específicas, CALCULATOR_WIDTH e CALCULATOR_HEIGHT, o painel não precisa saber quantas linhas de entrada a calculadora exibirá. A lógica interna da calculadora ajusta dinamicamente o tamanho de m_inputs[], sem alterar as dimensões totais da área reservada. Portanto, se futuramente adicionarmos mais campos de entrada, como um campo "Taxa de swap", será necessário verificar se os novos campos de entrada cabem na altura reservada e, se necessário, ajustar o posicionamento do campo de resultado ou a constante CALCULATOR_HEIGHT. Com esses ajustes, o painel poderá continuar funcionando sem precisar conhecer os detalhes internos da calculadora.

Roteamento e priorização de eventos

O gerenciamento de eventos é igualmente importante. Quando o usuário interage com algum controle da calculadora, por exemplo, ao selecionar outro tipo de cálculo no menu suspenso ou clicar no botão "Calcular", os eventos associados a esses objetos são recebidos por CTradeManagementPanel::OnEvent(...). Logo no início de OnEvent, encaminhamos cada evento à calculadora:

if(m_calculator.OnEvent(id, lparam, dparam, sparam))

{
   Print("Calculator handled event: ", sparam);

   return(true);
}

Se a calculadora reconhecer o evento, ou seja, se o tipo de evento for reconhecido e sparam corresponder ao nome de um dos controles tratados pela calculadora, como "MyPanel_ForexCalcDropdown" ou "MyPanel_ForexCalcCalcBtn", ela processará o evento e retornará true, encerrando imediatamente o tratamento no painel. Esse retorno antecipado garante prioridade à lógica da calculadora, seja para recriar os campos de entrada ou atualizar o rótulo de resultado.

Somente quando m_calculator.OnEvent(...) retorna false continuamos a tratar os demais eventos específicos do painel, como os cliques nos botões das seções "Execução rápida" ou "Ordens pendentes". Dessa forma, a calculadora funciona, na prática, como um subdiálogo lógico dentro do painel: pode adicionar e remover controles dinâmicos, responder às interações do usuário e atualizar sua interface sem interferir no funcionamento dos outros controles do painel nem sofrer interrupções causadas por eles.


(4) Atualização do NewAdminPanel para incorporar as novas funcionalidades

A chamada ao método g_tradePanel.Run(), seja na função de inicialização do EA ou na rotina responsável por exibir o painel, é essencial para o funcionamento correto de todos os controles interativos da interface gráfica, especialmente os menus suspensos (ComboBox) e os seletores de datas (DatePicker). O método Run() coloca o diálogo em execução. O tratamento das interações com seus controles depende dos mecanismos de recebimento e encaminhamento dos eventos do gráfico, incluindo os métodos ChartEvent e OnEvent. Sem a chamada a Run(), o diálogo não é colocado corretamente em execução, o que pode comprometer a interação com seus controles. Na prática, sem a ativação adequada do diálogo e o encaminhamento dos eventos do gráfico, a seleção de uma opção no ComboBox ou a alteração da data no DatePicker pode deixar de ser processada pelo painel.

Após a chamada de g_tradePanel.Run(), o diálogo passa a utilizar seu mecanismo interno de processamento de mensagens. Os eventos relevantes gerados pelo menu suspenso e pelo seletor de datas são encaminhados ao OnEvent(...) do painel, que identifica cada evento e chama OnChangePendingOrderType() ou OnChangePendingDatePicker(), conforme o caso. Em resumo, Run() coloca o diálogo em execução, enquanto a resposta às ações do usuário depende também do encaminhamento correto dos eventos. Sem essa ativação, as interações com o ComboBox e o DatePicker podem não ser processadas como esperado.

void HandleTradeManagement()
{
    if(g_tradePanel)
    {
        if(g_tradePanel.IsVisible())
            g_tradePanel.Hide();
        else
            g_tradePanel.Show();
        ChartRedraw();
        return;
    }
    g_tradePanel = new CTradeManagementPanel();
    if(!g_tradePanel.Create(g_chart_id, "TradeManagementPanel", g_subwin, 310, 20, 875, 700))
    {
        delete g_tradePanel;
        g_tradePanel = NULL;
        return;
    }
    // ← This line activates the dialog’s own message loop
    g_tradePanel.Run();
    g_tradePanel.Show();

    ChartRedraw();
}

Uso de ChartRedraw()

Outro aspecto igualmente importante para a experiência do usuário é utilizar a função ChartRedraw() nos momentos adequados, especialmente após exibir ou ocultar diálogos e atualizar elementos visuais. Sempre que chamamos Show() ou Hide() para um diálogo ou controle individual, como ComboBox, DatePicker ou campos da calculadora, precisamos redesenhar a área do gráfico para que a exibição dos controles mostrados ou ocultados seja atualizada no gráfico. No código do nosso EA, podemos observar chamadas frequentes a ChartRedraw() em tratadores como HandleTradeManagement() e ToggleInterface(), além de chamadas dentro de OnEvent(...) após o processamento dos eventos.

Cada chamada de ChartRedraw() força o MetaTrader 5 a redesenhar todos os objetos do gráfico e os componentes da interface gráfica, permitindo que as alterações nos menus suspensos, nos calendários do DatePicker e nos valores da calculadora sejam refletidas visualmente no gráfico. A exibição correta e a ordem de sobreposição dos controles dependem também de sua implementação e configuração. Sem ChartRedraw(), o gráfico pode continuar exibindo informações desatualizadas por uma fração perceptível de segundo após uma mudança de estado, causando a impressão de que a interface não responde. Por exemplo, o usuário pode selecionar outra opção no menu suspenso, mas continuar vendo a seleção anterior até a chegada do próximo tick ou de uma atualização automática. Ao solicitar explicitamente o redesenho após cada alteração relevante, seja ao exibir ou ocultar um painel, atualizar rótulos ou recalcular resultados, buscamos manter a interface visualmente atualizada. Assim, o redesenho exibe as seleções do ComboBox, os calendários do DatePicker e os resultados já calculados nos campos da calculadora.

// Toggling the main interface buttons
void ToggleInterface()
{
    bool state = ObjectGetInteger(0, toggleButtonName, OBJPROP_STATE);
    ObjectSetInteger(0, toggleButtonName, OBJPROP_STATE, !state);
    UpdateButtonVisibility(!state);
    // Redraw immediately so button positions update on screen
    ChartRedraw();
}

// In the OnEvent handler, after forwarding to sub‐panels:
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
    if(id == CHARTEVENT_OBJECT_CLICK)
    {
        // ... handle panel toggles ...
        ChartRedraw();  // Ensure any Show()/Hide() calls are rendered

        // Forward to communication panel
        if(g_commPanel && g_commPanel.IsVisible())
            g_commPanel.OnEvent(id, lparam, dparam, sparam);
        ChartRedraw();  // Redraw after commPanel’s changes

        // Forward to trade panel
        if(g_tradePanel && g_tradePanel.IsVisible())
            g_tradePanel.OnEvent(id, lparam, dparam, sparam);
        ChartRedraw();  // Redraw after tradePanel’s updates (e.g., combobox or date change)

        // Forward to analytics panel
        if(g_analyticsPanel && g_analyticsPanel.IsVisible())
            g_analyticsPanel.OnEvent(id, lparam, dparam, sparam);
        ChartRedraw();  // Final redraw to reflect any analytics updates
    }
}

  • Redesenho após alterações de visibilidade: Na função HandleTradeManagement(), chamamos ChartRedraw() imediatamente após Show() ou Hide(). Isso faz com que o painel apareça ou desapareça sem atrasos, evitando que a tela mantenha o estado anterior até que alguma atividade externa provoque uma atualização do gráfico.
  • Redesenho após o encaminhamento de eventos: Dentro de OnChartEvent(...), após encaminhar o evento para g_tradePanel.OnEvent(...), chamamos ChartRedraw() novamente. Se o usuário interagir com o menu suspenso da calculadora, por exemplo, selecionando "Valor em risco", a calculadora poderá recriar seus campos de entrada ou atualizar o rótulo de resultado. A chamada subsequente a ChartRedraw() solicita a exibição dos novos campos e rótulos; a ausência de artefatos visuais depende também da implementação dos controles.
  • Resposta visual fluida: Ao chamar ChartRedraw() nas etapas apropriadas, após alterar o estado dos botões da interface, exibir ou ocultar o painel e encaminhar eventos aos painéis secundários, proporcionamos uma experiência de uso fluida e responsiva. Isso ajuda a manter menus, calendários e resultados da calculadora visualmente atualizados após cada interação.

Agora, podemos passar aos testes das novas funcionalidades na próxima seção.


Testes

Os testes descritos a seguir foram realizados no MetaTrader 5 após a compilação bem-sucedida do código. O painel TradeManagementPanel atualizado oferece um procedimento aprimorado para o envio de ordens pendentes, além de uma calculadora integrada para calcular as principais métricas de negociação do mercado Forex, contribuindo para decisões de negociação mais bem fundamentadas.

Teste da calculadora de valores Forex

Teste da calculadora de valores Forex


Conclusão

Ao longo deste artigo, exploramos diversos conceitos importantes em detalhes, e fico satisfeito por termos alcançado nosso principal objetivo. Abordamos alguns dos cálculos fundamentais do mercado Forex, como tamanho da posição, valor do ponto (calculado aqui para uma variação de um pip) e relação risco-retorno, entre outros, explicando os fundamentos matemáticos que todo trader deve conhecer. A implementação dessas fórmulas em MQL5 ajuda os traders a consolidar os conhecimentos teóricos e permite que os desenvolvedores apliquem esses cálculos corretamente em seus próprios projetos.

Um dos principais aprendizados do desenvolvimento do TradeManagementPanel foi a utilização dos controles da biblioteca padrão do MQL5, especialmente CComboBox e CDatePicker. Com esses componentes, melhoramos a organização do layout e facilitamos o acesso aos campos de entrada, além de simplificar a definição da data de expiração das ordens pendentes. Isso representa uma economia considerável de tempo em comparação com a digitação manual das datas e reduz a possibilidade de erros do usuário.

Durante o desenvolvimento, priorizamos a arquitetura modular, separando a calculadora, os controles de ordens pendentes e os botões de execução rápida em classes separadas, com responsabilidades bem definidas e interfaces de interação claras. O tratamento adequado dos eventos de ComboBox e DatePicker no EA exemplifica uma solução robusta e reutilizável. Cada componente desenvolvido pode ser aproveitado e integrado a projetos futuros com poucas modificações.

Embora a interface gráfica já esteja estável, ainda podemos aperfeiçoar e otimizar a lógica utilizada nos cálculos. Gostaria de conhecer suas opiniões e sugestões nos comentários, pois suas ideias podem contribuir muito para aprimorar as soluções apresentadas. Espero que este artigo tenha sido útil para ampliar seus conhecimentos e aguardo nosso próximo encontro. Acompanhe as próximas publicações!


Abaixo, apresentamos a lista de todos os arquivos utilizados neste projeto:

Arquivo anexado Descrição
TradeManagementPanel.mqh Contém a lógica principal da interface do painel de negociação, incluindo o gerenciamento de ordens a mercado e ordens pendentes, cálculos de risco e a calculadora integrada para traders. Disponibiliza controles gráficos, como menus suspensos, seletores de datas e botões de ação, reunidos em um painel derivado de CAppDialog. Desempenha um papel fundamental no processamento das operações de negociação e na interação com o usuário.
ForexValuesCalculator.mqh Implementa o mecanismo principal de cálculo utilizado no Painel de Gerenciamento de Operações para determinar parâmetros das operações, como valor do ponto, tamanho da posição, valor em risco, lucro/prejuízo e relação risco-retorno.
New_Admin_Panel.mq5 É o principal ponto de entrada do EA, responsável por integrar todos os módulos do sistema, como gerenciamento de operações, comunicação e análise, em uma única interface gráfica. Gerencia a criação das instâncias dos painéis, o roteamento de eventos, a criação de objetos gráficos e a organização geral do layout. Também solicita a atualização visual da interface por meio de chamadas a ChartRedraw() e ativa as funcionalidades do painel utilizando o método .Run().
Images.zip Contém uma coleção de recursos gráficos em formato bitmap, utilizados nos botões e demais elementos visuais da interface. Inclui arquivos como TradeManagementPanelButton.bmp, expand.bmp, collapse.bmp, entre outros, que fornecem resposta visual às interações do usuário por meio dos diferentes estados dos botões (normal/pressionado). Esses recursos são essenciais para facilitar a identificação visual dos controles e melhorar a usabilidade do aplicativo.
Communications.mqh Define o painel de comunicação, que permite aos usuários enviar e receber mensagens por meio de um bot do Telegram. Inclui componentes gráficos para inserir as credenciais (Chat ID e token do bot), além de um campo para digitar mensagens. O painel também está preparado para receber futuras funcionalidades de gerenciamento de contatos e foi desenvolvido utilizando CChartCanvas, CBmpButton e CEdit.
AnalyticsPanel.mqh Disponibiliza um resumo analítico baseado nos dados do gráfico, incluindo a avaliação de sinais ou o acompanhamento do desempenho. O painel está integrado ao EA principal e é exibido por meio de g_analyticsPanel. Sua estrutura segue a mesma abordagem modular baseada em CAppDialog, permitindo manter a lógica independente e ampliar as funcionalidades.
Telegram.mqh Gerencia a comunicação de rede de baixo nível e a formatação JSON necessárias para interagir com a API do bot do Telegram. Inclui funções para enviar mensagens de texto. Esse módulo funciona como o mecanismo interno de comunicação utilizado pelo painel correspondente.
Authentication.mqh Implementa a autenticação opcional de dois fatores para o painel administrativo, utilizando o Telegram como canal de verificação. Envia mensagens de confirmação de login ao Chat ID informado e verifica a senha informada pelo usuário. Normalmente, esse módulo é chamado durante a inicialização do EA para autenticar o usuário e impedir acessos não autorizados. Atualmente, está desativado para evitar solicitações repetidas de autenticação durante os testes frequentes e o desenvolvimento.

Salve todos os arquivos de cabeçalho no diretório MQL5\include e extraia o conteúdo de Images.zip para a pasta MQL5\Images. Em seguida, compile o arquivo New_Admin_Panel.mq5 e execute o EA no terminal MetaTrader 5.


Voltar ao sumário

Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/18289

Arquivos anexados |
New_Admin_Panel.mq5 (17.98 KB)
Images.zip (6.17 KB)
Communications.mqh (30.32 KB)
AnalyticsPanel.mqh (32.77 KB)
Telegram.mqh (1.85 KB)
Authentication.mqh (8.92 KB)
Últimos Comentários | Ir para discussão (10)
Clemence Benjamin
Clemence Benjamin | 21 jun. 2025 em 08:58
CapeCoddah o formato de string seria mais fácil.


Até mais! Coma seus Wheaties e boa programação


Cape CoddaH

Obrigado,@CapeCoddah, por todos os seus comentários e pelo esforço que você dedicou — isso realmente contribui para uma versão mais estável dessa ferramenta de negociação com painéis múltiplos.

Agradeço muito o tempo que você está dedicando para explorar e entender como tudo funciona.

No momento, estou analisando os problemas que você destacou e também verificarei as modificações que você enviou. As melhorias estão a caminho.

Atenciosamente,

Clemence Benjamin

Clemence Benjamin
Clemence Benjamin | 21 jun. 2025 em 09:04
Oluwafemi Olabisi #:

Olá,

Eu estava tentando instalar, mas nenhum botão apareceu; só consigo ver duas caixas de seleção. Extraí os arquivos para a pasta “Include”, conforme indicado, e as imagens foram extraídas para a pasta “images”

Olá, @Oluwafemi Olabisi,

Você poderia, por favor, compartilhar uma captura de tela para que eu possa ajudá-lo de forma mais eficaz?

Oluwafemi Olabisi
Oluwafemi Olabisi | 22 jun. 2025 em 18:48
Clemence Benjamin #:

Olá, @Oluwafemi Olabisi,

Você poderia, por favor, compartilhar uma captura de tela para que eu possa ajudá-lo de forma mais eficaz?

Anexei aqui como os arquivos foram extraídos para os diretórios INCLUDE e IMAGES, respectivamente.
CapeCoddah
CapeCoddah | 23 jun. 2025 em 12:57

Oi, Clemence,

Tenho algumas perguntas e talvez você possa esclarecer algumas delas.

A primeira é sobre o Strategy Tester

Quando executo meu EA nele, nenhum texto, painel, botão etc. é exibido na máquina de teste. Percebi que alguns dos seus são exibidos. Você tem alguma ideia do que causa essa diferença? Estou planejando incorporar o seu EA ao meu e tentar descobrir o que causa essas diferenças.

Segundo: como você entra em contato com a MetaQuotes para relatar bugs e enviar sugestões de melhorias? Passei um tempo considerável no site MQL5.com e não consigo encontrar uma maneira de fazer isso.

CapeCoddah
CapeCoddah | 23 jun. 2025 em 12:59
Oluwafemi Olabisi #:
Anexei aqui como os arquivos foram extraídos para os diretórios INCLUDE e IMAGES, respectivamente.

O EA deve estar na pasta “experts”, e não na pasta “include”. Depois de movê-lo, é preciso parar o EA e reiniciá-lo para que ele apareça no Painel do Navegador. Essa é uma das coisas que o MQ deveria mudar. Pelo menos permitir que os usuários ocultem a pasta, seja “Indicadores” ou “EAs”, e então atualizem a lista ao executar o comando de expansão, em vez de parar o Terminal, reiniciá-lo e abrir todos os subdiretórios até chegar ao destino. Melhor ainda, isso deveria acontecer automaticamente sempre que um novo executável fosse colocado no subdiretório.

Criação de EAs auto-otimizáveis em MQL5 (Parte 8): Análise de múltiplas estratégias Criação de EAs auto-otimizáveis em MQL5 (Parte 8): Análise de múltiplas estratégias
Qual é a melhor maneira de combinar diferentes estratégias para criar uma estratégia de ensemble poderosa? Vamos explorar como integrar três estratégias distintas em nosso aplicativo de negociação. Os traders costumam utilizar estratégias especializadas na abertura e no fechamento de posições, e queremos descobrir se as máquinas conseguem executar essa tarefa com melhores resultados. Para começar, vamos conhecer os recursos do testador de estratégias e os princípios da programação orientada a objetos necessários para resolver esse problema.
Dominando os Registros de Log (Parte 7): Como Exibir Logs Diretamente no Gráfico Dominando os Registros de Log (Parte 7): Como Exibir Logs Diretamente no Gráfico
Aprenda a exibir logs diretamente no gráfico do MetaTrader de forma organizada, com molduras, títulos e atualização com deslocamento automático. Neste artigo, mostraremos como criar um sistema visual de logs utilizando MQL5, ideal para acompanhar em tempo real o que seu robô está fazendo.
Recursos do Assistente MQL5 que você precisa conhecer (Parte 70): Uso dos padrões SAR e RVI com uma rede neural cujos kernels crescem exponencialmente Recursos do Assistente MQL5 que você precisa conhecer (Parte 70): Uso dos padrões SAR e RVI com uma rede neural cujos kernels crescem exponencialmente
No artigo anterior, apresentamos uma combinação dos indicadores SAR e RVI. Neste artigo, veremos como aprimorá-la com técnicas de machine learning. Juntos, o SAR e o RVI combinam informações de tendência e momentum. Nossa abordagem utiliza uma rede neural convolucional (convolutional neural network) com um kernel exponencial (Exponential kernel) para definir o tamanho dos kernels e o número de canais da rede, utilizada para aprimorar as previsões baseadas nesses dois indicadores. Como de costume, essa abordagem é implementada em um arquivo personalizado da classe de sinais (signal class), que interage com o Assistente MQL5 para gerar um EA.
Técnicas do Assistente MQL5 que você deve conhecer (Parte 62): Utilizando padrões de ADX e CCI com aprendizado por reforço via TRPO Técnicas do Assistente MQL5 que você deve conhecer (Parte 62): Utilizando padrões de ADX e CCI com aprendizado por reforço via TRPO
Os osciladores ADX e CCI são indicadores de acompanhamento de tendência e momentum que podem ser combinados no desenvolvimento de um Expert Advisor. Damos continuidade ao ponto em que paramos no último artigo, analisando como o treinamento durante o uso e a atualização do modelo desenvolvido podem ser realizados por meio do aprendizado por reforço. Utilizaremos um algoritmo que ainda não abordamos nesta série, conhecido como Trust Region Policy Optimization (TRPO). E, como sempre, a montagem de Expert Advisors por meio do Assistente MQL5 nos permite preparar nossos modelos para testes de forma muito mais rápida, além de possibilitar sua distribuição e avaliação com diferentes tipos de sinais.