English Русский 中文 Deutsch 日本語
preview
Creación de un Panel de administración de operaciones en MQL5 (Parte XII): Integración de una calculadora de valores Forex

Creación de un Panel de administración de operaciones en MQL5 (Parte XII): Integración de una calculadora de valores Forex

MetaTrader 5Ejemplos |
53 10
Clemence Benjamin
Clemence Benjamin

Contenido:


Introducción

En este artículo abordamos el problema de calcular manualmente o mediante herramientas externas los valores de las operaciones mediante la integración de una calculadora de divisas directamente en el Panel de gestión de operaciones, un subpanel del nuevo Panel de administración de EA.

En el pasado, muchos operadores recurrían a sitios web externos para realizar estos cálculos. Estas herramientas han resultado de gran ayuda, y hay que reconocer el mérito de los desarrolladores de estas plataformas por ofrecer servicios tan valiosos. Incluso hoy en día, algunos operadores siguen utilizando estas calculadoras en línea; al fin y al cabo, es una cuestión de gustos.

Sin embargo, gracias al potencial de MQL5 y a sus funciones de interfaz gráfica de usuario, ahora tenemos la oportunidad de crear soluciones más eficientes e integradas directamente desde la plataforma de negociación. Este enfoque elimina la necesidad de cambiar de una aplicación a otra y mejora el flujo de trabajo al reunir todas las herramientas esenciales en un solo lugar.

Agradecemos a MetaTrader 5 por ofrecer una API sólida que permite acceder fácilmente a las noticias del mercado y a los flujos de datos desde la propia plataforma. Aunque existen API de terceros para calculadoras y fuentes de noticias, nuestro objetivo es desarrollar un algoritmo de calculadora nativo diseñado específicamente para nuestro panel.

Este proyecto no socava las soluciones existentes, sino que amplía la gama de opciones a disposición de los operadores. Promueve una comprensión más profunda de las capacidades de MetaTrader 5 y anima a los usuarios a interactuar con la plataforma de forma más eficaz. Al ofrecer un conjunto de herramientas totalmente integrado dentro de la terminal, nuestro objetivo es brindar una experiencia de negociación más fluida y productiva, demostrando cómo los avances en las tecnologías de negociación continúan revolucionando la industria.

Algunos de los valores que se deben calcular son:

  • Tamaño de posición
  • Monto del riesgo
  • Valor del pip
  • Requisito de margen
  • Estimación de ganancias/pérdidas
  • Tarifas de intercambio/entrega nocturna
  • Relación riesgo-recompensa
  • Nivel de margen
  • Coste del spread
  • Precio de equilibrio
  • Beneficio esperado
  • Impacto del apalancamiento, etc.

Estos cálculos son vitales para los operadores de Forex porque proporcionan un marco estructurado para gestionar el riesgo, optimizar las configuraciones de las operaciones y mantener la sostenibilidad de la cuenta. El tamaño de la posición y el nivel de riesgo, así como los cálculos correspondientes, garantizan que los operadores arriesguen solo una pequeña parte predefinida de su capital, protegiéndose así contra pérdidas significativas. Las estimaciones del valor del pip y de las ganancias/pérdidas permiten una planificación precisa de las operaciones, ayudando a los operadores a establecer objetivos realistas y niveles de stop-loss. Los requisitos de margen y los cálculos del nivel de margen evitan el apalancamiento excesivo, que podría provocar llamadas de margen o desplomes en las cuentas. Las comisiones por swaps son fundamentales para los operadores a largo plazo, especialmente para aquellos que utilizan operaciones de carry trade, ya que afectan a los costes de mantenimiento de posiciones.

La relación riesgo-beneficio orienta la selección de operaciones, garantizando que las recompensas potenciales justifiquen los riesgos. Otras métricas, como el coste del spread, el precio de equilibrio, el beneficio esperado y el impacto del apalancamiento, mejoran la toma de decisiones al tener en cuenta los costes de transacción, la viabilidad de la estrategia y la exposición general al riesgo. En conjunto, estas herramientas permiten a los operadores tomar decisiones informadas y disciplinadas, alineando sus operaciones con sus objetivos financieros y las condiciones del mercado, lo que en última instancia mejora la consistencia y la rentabilidad.

En la siguiente sección, ofreceré un resumen de cómo abordaremos el desarrollo actual.


Descripción general

Desde que introdujimos el diseño modular en esta serie, hemos logrado centrarnos en secciones individuales del programa sin interrumpir otros componentes. Esta flexibilidad nos permite ahora actualizar el TradeManagementPanel para dar cabida a la integración de herramientas de cálculo.

Para ello, utilizaremos clases adicionales de la Biblioteca estándar de MQL5. En lugar de mantener secciones de entrada separadas para cada tipo de orden, implementaremos un menú desplegable para la selección del pedido, acompañado de una única fila de entrada. Este diseño optimizado liberará espacio para los componentes de nuestra calculadora.

Si bien no es necesario mostrar todos los parámetros operativos, ciertos valores clave son esenciales para la toma de decisiones informadas y deben estar disponibles. Algunos de estos valores no necesitan calcularse en absoluto, ya que son accesibles a través de datos de mercado en tiempo real en MQL5.

Comenzaremos con un análisis detallado de los términos y valores clave del mercado de divisas, incluyendo sus definiciones, fórmulas y cómo se representan dentro de MQL5. A partir de ahí, pasaremos a la fase de implementación, comenzando por ajustar la sección de Órdenes del Panel de Gestión de Operaciones para adaptarla a la interfaz de usuario de la calculadora.

Próximos cambios en el panel de gestión comercial

Mejoras en el TradeManagementPanel

En la sección denominada A de la ilustración anterior, utilizaremos la clase ComboBox para mostrar y seleccionar el tipo de orden. La sección B se adaptará a un diseño de una sola fila, y la fecha de caducidad, C, se mejorará mediante un selector de fechas para facilitar su uso.

Una vez realizados los ajustes de diseño, integraremos tanto la lógica de cálculo como la lógica de entrada de la interfaz gráfica de usuario, lo que suele requerir menos de tres campos de entrada por cálculo.

Por último, voy a explicar el proceso de pruebas y los resultados, y concluiremos con una evaluación de la nueva funcionalidad.


Cálculos y fórmulas de Forex 

En la tabla siguiente, presento algunos términos habituales del mercado de divisas que suelen requerir cálculos, junto con las fórmulas correspondientes y las funciones MQL5 personalizadas que se utilizan para calcularlos. Estos ejemplos no son exhaustivos; como operador, es posible que tengas que realizar cálculos adicionales en función de la estrategia concreta que estés aplicando. Las fórmulas que figuran en la tabla siguiente son el resultado de una exhaustiva investigación y de la combinación de conocimientos matemáticos procedentes de diversas fuentes en línea. Si desea profundizar en el tema o contrastar la información, le recomendamos que busque más datos en Google u otras fuentes fiables.

Término y descripción de Forex Fórmula general Fórmula codificada MQL5
Tamaño de posición

Calcula el número de lotes a negociar en función del saldo de la cuenta, el porcentaje de riesgo y el límite de pérdidas, garantizando que el riesgo se ajuste a la estrategia del operador.




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));
}
                


Monto del riesgo

Cuantifica la cantidad monetaria en riesgo en una operación, en función del tamaño de la posición y el límite de pérdidas, garantizando que las pérdidas se mantengan dentro de límites aceptables.




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 del pip

Calcula el valor monetario de un movimiento de un pip para un tamaño de lote determinado, algo esencial para los cálculos de riesgo y beneficio.




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);
}
                


Requisito de margen

Determina los fondos necesarios para abrir una posición, en función del tamaño del lote, el tamaño del contrato y el apalancamiento, para evitar un apalancamiento excesivo.




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);
}
                


Estimación de ganancias/pérdidas

Estima las posibles ganancias o pérdidas en función de los precios de entrada y salida, lo que ayuda a establecer objetivos comerciales realistas.




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);
}
                


Comisiones por swap/mantener posiciones durante la noche

Calcula los intereses cobrados o devengados por mantener posiciones durante la noche, lo cual es importante para las operaciones a largo plazo.




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);
}
                


Relación riesgo-rentabilidad

Mide la ganancia potencial en relación con la pérdida potencial, lo que orienta la selección de operaciones para obtener una expectativa positiva.




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


Nivel de margen

Muestra la relación porcentual entre el capital (equity) de la cuenta y el margen utilizado, lo que permite supervisar el estado de la cuenta para evitar llamadas de margen.




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


Coste del spread

Calcula el coste monetario del spread entre el precio de compra y el de venta de una operación, lo cual es fundamental para las estrategias de negociación a corto plazo.




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 del apalancamiento

Mide el apalancamiento efectivo utilizado en una operación, poniendo de relieve la exposición al riesgo en relación con el saldo de la cuenta.




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);
}
                



En la siguiente sección dedicada a la implementación, utilizaremos el CCombobox de la biblioteca estándar de MQL5 para optimizar el uso del espacio en los controles de la calculadora que se integrarán en el TradeManagementPanel. Este enfoque ofrece valiosas lecciones sobre el diseño eficiente de la interfaz de usuario y la gestión de controles. Además, incorporaremos un componente DatePicker para mejorar la experiencia del usuario a la hora de seleccionar la fecha de caducidad de un pedido.


Implementación

Para garantizar un progreso constante, dividiremos nuestro desarrollo en cuatro etapas principales:

Una vez completados estos pasos, actualizaremos el EA de NewAdminPanel para que sea compatible con las nuevas funciones y realizaremos las pruebas correspondientes. Es importante prestar mucha atención durante todo el proceso para no pasar por alto detalles importantes, sobre todo al trabajar con los componentes ComboBox y DatePicker.


(1) Modificar la sección «Órdenes pendientes» para dejar espacio a los nuevos controles

Ahora extraeremos la sección «Órdenes pendientes» del encabezado TradeManagementPanel para aislarla y facilitar así la implementación de los componentes ComboBox y DatePicker. Además, añadiremos un botón para colocar la orden, que se pulsará una vez que el pedido se haya configurado por completo.

Declaraciones de los miembros sobre órdenes pendientes

Estas variables de miembro se encuentran en la sección «Órdenes pendientes» de la clase CTradeManagementPanel. Empezamos definiendo una etiqueta, que aparece encima de los controles de órdenes pendientes como título de sección («Órdenes pendientes:»).

//  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

Justo debajo, hay otras cuatro etiquetas que sirven de encabezados de columna: «Precio:», «TP:», «SL:» y «Vencimiento:». Debajo de las etiquetas, incluimos un ComboBox para que el usuario pueda elegir entre cuatro tipos de órdenes pendientes: compra con límite, compra stop, venta con límite y venta stop. A la derecha de ese ComboBox, hay tres campos de edición que permiten al usuario introducir el precio de la orden pendiente, el take-profit (TP) y el stop-loss (SL), respectivamente. Junto a estos campos de edición hay un selector de fechas, que facilita la elección de una fecha de caducidad. Por último, declaramos un botón con el texto Realizar pedido; al pulsarlo, se activará la realización efectiva de la orden pendiente con los parámetros indicados.

Al agrupar estos seis controles y estas cinco etiquetas en esta sección, reunimos todo lo necesario para crear y gestionar las órdenes pendientes. Esta separación facilita explicar o refactorizar únicamente la lógica de los pedidos pendientes sin alterar el resto del panel.

Creación de controles de órdenes pendientes en Create(...)

Dentro del método Create(...), creamos toda la sección de Órdenes pendientes inmediatamente después de dibujar una línea separadora debajo de la Calculadora de divisas. En primer lugar, añadimos un pequeño espacio vertical para separarlo visualmente de la calculadora que hay encima. A continuación, se crea un encabezado de sección («Pedidos pendientes:») y se le aplica un estilo en negrita para distinguirlo del resto de secciones.

A continuación, el ComboBox para seleccionar el tipo de orden se coloca a la derecha de este encabezado. Tras añadir el ComboBox y desplazar su posición vertical, creamos cuatro encabezados de columna: «Precio:», «TP:», «SL:» y «Vencimiento:». Cada encabezado se coloca horizontalmente con el mismo espaciado, de modo que queden alineados sobre la fila 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;

Una vez colocados los encabezados, desplazamos la posición vertical hacia abajo y comenzamos la fila de entrada. En primer lugar, aparece un campo de edición para el precio de la orden pendiente, que rellenamos inmediatamente con el precio de venta actual para ofrecer al usuario un valor predeterminado válido. A su derecha, colocamos el campo de edición TP (inicializado en «0,00000») y, a continuación, el campo de edición SL (también inicializado en «0,00000»). Además, se crea el selector de fecha, que por defecto está configurado en «fin de día» (23:59:59).

Por último, se crea un botón «Realizar pedido» y se coloca junto al selector de fecha para que no obstaculice el uso de los demás controles. Una vez que todos los controles se hayan creado correctamente, desplazamos el cursor vertical hacia abajo para dejar espacio libre. En conjunto, estos pasos describen todos los controles necesarios para que un usuario configure una orden pendiente —tipo, precio, TP, SL, fecha de vencimiento— y, a continuación, pulse un botón para ejecutarla.

Gestores de eventos para pedidos pendientes

Estos métodos responden a las interacciones del usuario en la sección «Órdenes pendientes»:

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();
}

Al seleccionar un tipo de orden diferente: cada vez que el usuario elige un nuevo tipo de orden en el ComboBox (por ejemplo, al cambiar de «Límite de compra» a «Límite de venta»), leemos el texto recién seleccionado y comprobamos si empieza por «Compra» o «Venta». Si empieza por «Compra», recuperamos el precio de venta actual; en caso contrario, recuperamos el precio de compra actual. A continuación, rellenamos inmediatamente el campo de edición del precio con ese valor de mercado. De este modo, se garantiza que el usuario vea siempre un precio predeterminado válido y actualizado que se corresponda con el tipo de orden que haya elegido. Por último, actualizamos la interfaz del gráfico para que el nuevo precio aparezca de inmediato.

Al cambiar la fecha de caducidad: cada vez que el usuario selecciona o modifica la fecha de caducidad en el selector de fechas, recuperamos la nueva fecha y la registramos con fines de depuración. A continuación, actualizamos la interfaz del gráfico para que refleje inmediatamente cualquier cambio si, por ejemplo, otras partes del panel dependen visualmente de la fecha de vencimiento seleccionada. En esta fase no se realiza ninguna otra comprobación; se acepta cualquier fecha del calendario válida.

Al mantener estos controladores compactos y específicos, nos aseguramos de que el ComboBox y el selector de fechas se mantengan en sintonía con las condiciones actuales del mercado, evitando así que el usuario realice accidentalmente un pedido con un precio no válido o seleccione sin darse cuenta una fecha caducada.

Asistente de validación para pedidos pendientes

Antes de que una orden pendiente se envíe realmente al bróker, comprobamos que los datos introducidos por el usuario sean correctos. Esta única función auxiliar aplica tres reglas:

  • El volumen debe ser positivo. Si el tamaño del lote es cero o negativo, registramos un error y rechazamos el pedido.
  • El precio debe ser positivo. Un precio negativo no puede constituir una orden pendiente 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);
}

Comprobaciones de las condiciones del mercado:

Por ejemplo;

  1. En el caso de una «orden de compra con límite», asegúrate de que el precio límite sea claramente inferior al precio de venta actual.
  2. Si se trata de una «orden de compra stop», asegúrate de que el precio de stop esté claramente por encima del precio de venta actual.

Si se superan todas las comprobaciones, la función auxiliar devuelve «true», lo que indica que el pedido puede procesarse. Al estructurar la validación de esta manera, evitamos errores habituales —como colocar una orden de compra limitada por encima del precio de mercado o una orden de venta stop en el precio de compra o por encima de él— y ofrecemos una respuesta inmediata y clara cuando los datos introducidos no son válidos.

Handler del botón «Colocar pendiente»

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, "");
}

Cuando el usuario hace clic en el botón «Realizar pedido», este controlador recopila todos los datos necesarios:

  • El tipo de orden seleccionado en el ComboBox.
  • El precio de la orden pendiente de la edición correspondiente.
  • Valores de toma de ganancias y stop loss a partir de sus ediciones.
  • Volumen de negociación, reutilizado de la edición de volumen de la sección de Ejecución rápida.
  • Fecha de caducidad seleccionada con el selector de fechas.

A continuación, determinamos si se debe utilizar GTC (Good Till Canceled) o un modo de caducidad específico, en función de si la fecha y hora de vencimiento seleccionadas son cero. A continuación, llamamos a nuestra función auxiliar de validación. Si falla alguna comprobación, salimos sin tomar ninguna medida.

Si la validación es exitosa, llamamos a uno de los cuatro métodos de CTrade (BuyLimit, BuyStop, SellLimit o SellStop), pasando el volumen, el precio, el símbolo, el SL, el TP, el modo de tiempo y la fecha de vencimiento. Cada llamada utiliza los datos introducidos por el usuario, por lo que, cuando este controlador finaliza, el agente ya ha recibido la solicitud de orden pendiente correcta. Si algún parámetro no es válido, simplemente devolvemos un valor, basándonos en los diagnósticos registrados para señalar el fallo.

Enrutamiento OnEvent(…) para pedidos pendientes

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);
}

Dentro del método principal OnEvent(...) de CTradeManagementPanel, los eventos de órdenes pendientes se enrutan de la siguiente manera:

Calculadora primero: Enviamos todos los eventos a la calculadora integrada. Si la calculadora gestiona el evento (por ejemplo, el usuario cambia un valor de punto decimal), nos detenemos ahí.

Lógica de pedidos pendientes:

  • Si el evento es un "clic" y el nombre del objeto pulsado coincide con el botón de pedido pendiente, llamamos al controlador "Colocar pendiente".
  • Si el evento es un "cambio de objeto" y el nombre del objeto modificado coincide con el ComboBox o el selector de fecha, llamamos al controlador apropiado (OnChangePendingOrderType o OnChangePendingDatePicker).
  • Alternativa: Cualquier otro evento se delega a la clase base CAppDialog::OnEvent(...), para que las secciones Ejecución rápida y Todas las operaciones tengan la oportunidad de procesar clics o ediciones.

Este enrutamiento garantiza que las interacciones con órdenes pendientes se gestionen de forma limpia y aislada, sin interferir con otras secciones del panel.

Probando la implementación de ComboBox y DatePicker.

Panel de gestión comercial adaptado (implementación de ComboBox y selector de fechas)


(2) Desarrollo de la clase de control ForexValuesCalculator

Antes de definir ninguna clase, incluimos cinco archivos de encabezado de la biblioteca estándar de MQL5 en el directorio «Controls». Cada una de ellas proporciona una clase de control de interfaz gráfica de usuario que utilizaremos dentro de CForexCalculator:

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

Dialog.mqh

Proporciona la clase base CAppDialog, que gestiona una colección de controles, se encarga del diseño y enruta los eventos. Aunque CForexCalculator no deriva directamente de CAppDialog, debe integrarse en un cuadro de diálogo padre (como CTradeManagementPanel), por lo que la presencia de Dialog.mqh garantiza que cualquier llamada para añadir los controles de nuestra calculadora (AddToDialog) y reenviar eventos se compile correctamente. Sin Dialog.mqh, no podríamos llamar a dlg.Add(...) para adjuntar nuestras etiquetas, ediciones y botones a la interfaz de usuario principal.

ComboBox.mqh

Expone la clase CComboBox, que utilizamos para el menú desplegable de opciones de cálculo. Al incluir este archivo, podemos crear y manipular una instancia de CComboBox (m_dropdown), llamar a m_dropdown.Create(...) para colocarla, rellenarla con AddItem y responder al evento CHARTEVENT_OBJECT_CHANGE cuando el usuario seleccione un término diferente. Sin él, el compilador no sabría qué es un CComboBox.

Edit.mqh

Define la clase CEdit, que se utiliza para todos los campos de entrada numérica y de texto (por ejemplo, saldo de la cuenta, porcentaje de riesgo, stop-loss, símbolo, etc.). Creamos dinámicamente un número variable de controles CEdit dentro de m_inputs[] en función del término de cálculo que se haya seleccionado. Cada objeto CEdit debe crearse, añadirse al cuadro de diálogo y, posteriormente, volver a convertirlo a CEdit* mediante GetInputValue o GetInputString. Si omitimos Edit.mqh, ninguna de esas llamadas se compilaría.

Label.mqh

Incluye CLabel, que utilizamos siempre que queremos mostrar texto estático en pantalla: la etiqueta «Opción de cálculo:» (m_calcOptionLabel), cada una de las etiquetas de entrada (para el saldo de la cuenta, el porcentaje de riesgo, etc.) y la etiqueta «Resultado:» (m_resultLabel). Es necesario crear cada CLabel para que el usuario sepa qué debe escribir en cada CEdit. Sin Label.mqh, no podríamos proporcionar contexto a cada cuadro de edición.

Button.mqh

Proporciona la clase CButton. Utilizamos CButton para el botón «Calcular» (m_calculateButton). Al incluir este encabezado, podemos llamar a m_calculateButton.Create(...), asignarle un color de fondo, configurar su texto y detectar los clics sobre él mediante OnEvent. Si omitimos Button.mqh, el compilador no reconocería CButton y no podríamos responder a los clics en «Calcular».

Plan de inclusiones a nivel de proyecto

En el proyecto general, tenemos dos elementos que dependen de estos controles:

El archivo ForexValuesCalculator.mqh necesita los cinco encabezados de Controls\*.mqh, ya que crea un «minidiálogo» autónomo y reutilizable para calcular diversos valores de divisas. Siempre que utilicemos CLabel, CEdit, CComboBox o CButton, debe estar presente el encabezado correspondiente para que el preprocesador de MQL5 pueda localizar las definiciones de clase.

Al agrupar todos los archivos de inclusión relacionados con la interfaz gráfica de usuario (GUI) al principio, nos aseguramos de que cualquier otro EA o panel (por ejemplo, TradeManagementPanel.mqh) pueda simplemente incluir #include «ForexValuesCalculator.mqh» y tener acceso inmediato a todos los controles de la interfaz gráfica de usuario necesarios, sin tener que dispersar archivos de inclusión adicionales por todo el código.

Declaraciones de los miembros 

La clase CForexCalculator comienza declarando varios controles de interfaz de usuario y estructuras de datos que, en conjunto, conforman la interfaz de la calculadora. En la parte superior, una etiqueta (m_calcOptionLabel) y un menú desplegable (m_dropdown) permiten al usuario seleccionar qué cálculo desea realizar (por ejemplo, tamaño de la posición, importe del riesgo, valor del pip, ganancias/pérdidas o relación riesgo-recompensa). Debajo de estos campos hay un botón «Calcular» (m_calculateButton) en el que el usuario hace clic una vez que ha rellenado todos los campos. Para mostrar los resultados, se combina un campo de edición de solo lectura (m_resultField) con otra etiqueta (m_resultLabel) que muestra un texto descriptivo como «Resultado: …», seguido del valor numérico.
// 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();
};

Todos los campos de entrada variables —cada uno compuesto por una etiqueta y un cuadro de edición— se almacenan en una matriz dinámica (m_inputs[]). En segundo plano, la clase contiene cinco matrices de tamaño fijo compuestas por estructuras InputField (m_positionSizeInputs, m_riskAmountInputs, m_pipValueInputs, m_profitLossInputs, m_riskRewardInputs). Cada entrada InputField contiene un nombre, una cadena de etiqueta y un valor numérico predeterminado. Por último, m_originX y m_originY indican dónde comienza el panel de la calculadora dentro del cuadro de diálogo principal, mientras que m_chart_id y m_name almacenan el identificador del gráfico y un prefijo para los nombres únicos de los controles. En conjunto, estos elementos definen tanto el diseño de la calculadora como los datos necesarios para cada tipo de cálculo de divisas.

Inicialización de valores predeterminados estáticos (InitInputs)

El método InitInputs se ejecuta una vez al crear el objeto calculadora. Rellena las cinco matrices de estructuras InputField con etiquetas descriptivas y valores predeterminados. Por ejemplo, el grupo «Tamaño de la posición» incluye campos para el saldo de la cuenta, el porcentaje de riesgo, el stop-loss en pips y el símbolo. El grupo «Importe de riesgo» incluye el tamaño de la posición, los pips del stop-loss y el símbolo. Cada matriz se configura de tal manera que, más adelante, cuando el usuario seleccione un tipo de cálculo, la matriz InputField correspondiente se copie en los controles dinámicos. En esta fase, al campo «saldo de la cuenta» se le asigna un valor predeterminado provisional de 0,0 (que se sustituirá en tiempo de ejecución), mientras que a los porcentajes de riesgo y a los valores en pips se les asignan pequeños valores predeterminados, como el 1 % o 20 pips. Esta inicialización estática garantiza que los datos de entrada de cada cálculo aparezcan con etiquetas claras y unos valores numéricos iniciales.

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;
}

Configuración de los valores predeterminados de tiempo de ejecución (SetDynamicDefaults)

Dado que el saldo real de la cuenta del usuario solo se conoce en tiempo de ejecución, el método SetDynamicDefaults sobrescribe m_positionSizeInputs[0].defaultValue (el campo «Saldo de la cuenta») con AccountInfoDouble(ACCOUNT_BALANCE). De este modo, cuando aparezcan en pantalla los campos de «Tamaño de la posición», el cuadro de edición del saldo de la cuenta ya estará rellenado con el saldo real del operador. Cualquier otro valor predeterminado dinámico —como los tipos de compra y venta o los tipos de cambio— se actualizará de igual modo tan pronto como se cree la calculadora. Al separar los valores predeterminados estáticos de los valores predeterminados en tiempo de ejecución, la clase mantiene su flexibilidad: la inicialización en tiempo de diseño se lleva a cabo en InitInputs, mientras que los ajustes rápidos en los campos que dependen del mercado se realizan en SetDynamicDefaults.

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

Ayudas para el cálculo básico

Debajo de las matrices de entrada, una serie de métodos auxiliares ejecutan cada fórmula:

1. CalculatePipValue

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);
}

La función CalculatePipValue calcula el valor de un pip en la divisa de la cuenta para un símbolo y un tamaño de lote determinados. En primer lugar, llama a SymbolInfoDouble para obtener SYMBOL_TRADE_TICK_SIZE y SYMBOL_TRADE_TICK_VALUE. A continuación, elige entre 0,01 (para los pares con JPY) o 0,0001 como «tamaño del pip». Si la divisa de las ganancias del par difiere de la divisa de la cuenta, las concatena (por ejemplo, «EURUSD» si las ganancias son en EUR y la cuenta en USD), selecciona ese símbolo de conversión y recupera su cotización de compra actual como tipo de cambio. Por último, divide tickValue entre tickSize, lo multiplica por pipSize, lotSize y rate, y devuelve el resultado redondeado a dos decimales. Un valor de 0,0 indica que los datos introducidos no son válidos (por ejemplo, si tickSize era cero).

2. CalculatePositionSize

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);
}

A partir del saldo de la cuenta, un porcentaje de riesgo y un stop-loss en pips, la función CalculatePositionSize devuelve el tamaño de lote óptimo. En primer lugar, llama a la función CalculatePipValue(sym, 1.0, AccountInfoString(ACCOUNT_CURRENCY)) para calcular el valor del pip por lote. Si alguna entrada o el valor del pip es cero o negativo, devuelve 0.

De lo contrario, utiliza la fórmula:

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

A continuación, recupera los valores SYMBOL_VOLUME_STEP, SYMBOL_VOLUME_MIN y SYMBOL_VOLUME_MAX del instrumento para limitar y redondear el tamaño de lote calculado. Los decimales de redondeo (dp) se obtienen de -MathLog10(step), lo que garantiza que el valor devuelto se ajuste a los incrementos permitidos por el bróker (por ejemplo, 0,01; 0,1).

3. CalculateRiskAmount

Cuando el usuario conoce el tamaño de su posición (ps en lotes) y los pips del stop-loss (sl), la función CalculateRiskAmount calcula cuánto capital estará en riesgo en la moneda de la cuenta. Obtiene el valor del pip para ese ps mediante CalculatePipValue(sym, ps, ...) y, a continuación, multiplica ps × sl × pipValue. El resultado se redondea a dos decimales. Si alguno de los valores de entrada es cero o negativo, la función devuelve 0,0, lo que indica que las entradas no son válidas.

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

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);
}

La función «CalculateProfitLoss» calcula las pérdidas y ganancias netas en la divisa de la cuenta para un precio de entrada, un precio de salida, un tamaño de lote y un símbolo determinados. Calcula el número de pips ganados o perdidos como (salida − entrada) ÷ pipSize, donde pipSize es 0,01 para los pares con el yen japonés y 0,0001 en los demás casos. A continuación, multiplica la diferencia en pips por CalculatePipValue(sym, lotSize, accountCurrency) para convertir los pips en beneficios en la moneda de la cuenta. El resultado final se redondea a dos decimales. Si alguno de los datos numéricos introducidos no es válido, el método devuelve 0,0.

5. CalculateRiskRewardRatio

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

En el caso de la «relación riesgo-beneficio», el usuario solo necesita los pips de toma de ganancias (TP) y los pips de stop-loss (SL). Siempre que ambos valores sean positivos, la función devuelve la relación tp / sl redondeada a dos decimales. Si cualquiera de los valores de entrada es cero o negativo, devuelve 0,0, lo que indica que los datos no son válidos.

Ayudas de diseño: cómo añadir campos individuales (AddField)

El método AddField se encarga de crear un par de etiqueta y campo de edición para un InputField. Recibe una referencia a un InputField (que contiene un nombre, un texto de etiqueta y un valor predeterminado) y la posición vertical actual del cursor en la coordenada y. El método calcula x0 = m_originX + CALC_INDENT_LEFT para que todas las etiquetas comiencen con un margen izquierdo uniforme.

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;
}

AddField acepta una referencia a InputField (que contiene el nombre, la etiqueta y el valor por defecto) y la posición vertical actual y. En primer lugar, calcula x0 = m_originX + CALC_INDENT_LEFT para situar el borde izquierdo de la etiqueta. Se crea un nuevo CLabel llamado m_name + «Lbl_» + f.name en (x0, y) con un ancho y una altura fijos. Su texto se establece en «f.label» y se añade a m_inputs[].

A continuación, se crea un CEdit en (x0 + CALC_EDIT_OFFSET + RESULT_BUTTON_GAP, y) para que todos los cuadros de edición queden alineados de forma coherente. Si f.name es igual a «symbol», se rellena automáticamente _Symbol; si es «accountCurrency», se rellena automáticamente la moneda de la cuenta; en caso contrario, se formatea f.defaultValue con dos decimales. El nuevo control de edición se añade a m_inputs[]. Por último, se incrementa y en la altura de control más CALC_CONTROLS_GAP_Y, lo que establece la posición para el siguiente campo. Al insertar cada nueva etiqueta+edición en m_inputs[], AddField garantiza que se añadirán al cuadro de diálogo más adelante y se gestionarán correctamente.

Crear todos los campos de entrada para un término determinado (CreateInputFields)

Cada vez que el usuario selecciona un nuevo término de cálculo (o al crearlo por primera vez), CreateInputFields borra todos los controles generados anteriormente (ArrayFree(m_inputs)) y, a continuación, coloca y justo debajo del menú desplegable. Comprueba qué parámetro se ha seleccionado: «Tamaño de la posición» (4 valores), «Importe de riesgo» (3), «Valor del pip» (3), «Ganancias/pérdidas» (4) o «Relación riesgo-recompensa» (2). Para cada InputField de la matriz correspondiente, llama a AddField(...). Si falla alguna llamada a AddField, el método devuelve «false», lo que detiene el resto del diseño. Si todos los campos se añaden correctamente, devuelve «true». El resultado es que, durante la ejecución, solo aparecen en pantalla los pares de etiquetas y campos de edición relevantes para el cálculo seleccionado, perfectamente alineados y con un espaciado 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;
}

Construcción de paneles (Create)

Cuando se invoca la función «Create», la interfaz de usuario de la calculadora se crea dentro de un cuadro de diálogo principal. En primer lugar, se almacenan el identificador del gráfico, un prefijo de nombre y las coordenadas de origen (x, y). Entonces:

  • Etiqueta de opción

Se crea una etiqueta estática m_calcOptionLabel en (x, y) con el texto «Opción de cálculo:». Esto aparece encima del menú desplegable.

  • Menú desplegable

El ComboBox (m_dropdown) se crea en la posición (comboX + 70, y), a la derecha de la etiqueta «Opción de cálculo». Contiene los cinco términos del cálculo. m_dropdown.Select(0) establece «Tamaño de la posición» como valor predeterminado.

  • Botón «Calcular»

Se coloca un botón CButton (m_calculateButton) cerca de la parte inferior del bloque del panel (utilizando los cálculos btnX y btnY). Lleva la etiqueta «Calcular» y tiene un fondo azul acero con texto en blanco. Al hacer clic en él, se activará 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;
}

Etiqueta y campo del resultado

Se crea un bloque independiente con la etiqueta «Resultado:» a la derecha del botón, seguido inmediatamente por un campo de edición de solo lectura (m_resultField) justo debajo. Esta edición muestra el resultado numérico del cálculo que se realice.

Filas dinámicas

La función SetDynamicDefaults() actualiza el valor predeterminado del saldo de la cuenta. A continuación, se recupera el término seleccionado actualmente (m_dropdown.Select()) y se invoca a CreateInputFields(term) para generar los pares de etiqueta y campo de edición correspondientes. Por último, UpdateResult(term) rellena el campo de resultado con el cálculo inicial.

Dado que el menú desplegable, el botón «Calcular» y el área de resultados se han dispuesto en primer lugar, las filas dinámicas posteriores aparecen intercaladas entre ellos, todas ellas basadas en desplazamientos uniformes. Si falla alguna llamada de creación, Create devuelve «false», lo que permite al código que realiza la llamada saber que la inicialización de la calculadora no se ha completado.

Añadir controles al cuadro de diálogo principal (AddToDialog)

Una vez creados correctamente todos los controles en Create(...), el EA o panel principal llama a AddToDialog. Este método añade cada control estático —m_calcOptionLabel, m_dropdown, m_calculateButton, m_resultLabel y m_resultField— a la lista interna de controles del cuadro de diálogo. A continuación, recorre la matriz dinámica m_inputs[] (que contiene cada par de etiqueta y campo de edición) y los añade también. Si falla alguna llamada a Add(...), el método devuelve «false», de modo que el módulo que invoca el método sabrá que la calculadora no se ha integrado por completo.

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;
}

Actualización de la visualización de resultados (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);
}

UpdateResult lee el término de cálculo seleccionado actualmente (término) y, utilizando la combinación adecuada de GetInputValue y GetInputString, recopila todos los datos de entrada necesarios. Por ejemplo:

  • Tamaño de la posición: recupera «accountBalance», «riskPercent», «stopLossPips» y «symbol». Si los datos son válidos, llama a CalculatePositionSize(...) y añade «Tamaño de la posición (lotes)» a la etiqueta.
  • Importe del riesgo: recupera «positionSize», «stopLossPips» y «symbol». Si son válidos, llama a CalculateRiskAmount(...) y añade «Importe del riesgo (USD)».
  • Valor del pip: recupera «lotSize», «symbol» y «accountCurrency». A continuación, ejecuta CalculatePipValue(...) y añade «Valor del pip (USD)».
  • Ganancias/pérdidas: recupera «entryPrice», «exitPrice», «lotSize» y «symbol». A continuación, ejecuta CalculateProfitLoss(...) y añade «Ganancias/pérdidas (USD)».
  • Relación riesgo-recompensa: recupera «takeProfitPips» y «stopLossPips». A continuación, calcula CalculateRiskRewardRatio(...) y añade «Relación riesgo-recompensa».

Si algún dato introducido no es válido o no se puede seleccionar el símbolo, el método establece txt = «Resultado: dato no válido». En todos los casos, actualiza m_resultField.Text con el resultado numérico formateado con dos decimales y llama a m_resultLabel.Text(txt) para ajustar el texto descriptivo que aparece encima. Este método garantiza que, al hacer clic en «Calcular» o al cambiar la selección del menú desplegable, tanto la etiqueta como el campo numérico se actualicen siempre con el último cálculo o con un mensaje de error.

Lectura de entradas del usuario (GetInputValue y 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 "";
}

Estos métodos auxiliares se encargan de localizar el control de edición adecuado dentro de la matriz dinámica m_inputs[]. Dado un nombre de campo como «stopLossPips», GetInputValue recorre todos los elementos de m_inputs[i], comprueba si su Name() coincide con m_name + «Inp_stopLossPips» y, a continuación, devuelve el valor numérico de su Text(). Del mismo modo, GetInputString devuelve el texto sin formato (por ejemplo, «EURUSD») cuando se le pasan nombres como «symbol» o «accountCurrency». Si no se encuentra ninguna coincidencia, devuelven 0,0 o una cadena vacía, respectivamente, lo que indica que falta información.

Enrutamiento de acciones del usuario (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;
}

La calculadora admite dos tipos de eventos:

1. Cambios en el menú desplegable (CHARTEVENT_OBJECT_CHANGE)

  • Si «sparam» coincide con el nombre del control del menú desplegable, recuperamos el nuevo término mediante m_dropdown.Select().
  • Llamamos a CreateInputFields(término) para reconstruir todos los pares dinámicos de etiqueta y campo de edición para ese término.
  • A continuación, para mostrar una vista previa inmediata, UpdateResult(term) vuelve a realizar el cálculo utilizando los valores predeterminados o los datos existentes.
  • Si se devuelve «true», se indica al cuadro de diálogo principal que el evento se ha procesado.

2. Clics en el botón «Calcular» (CHARTEVENT_OBJECT_CLICK)

  • Si sparam coincide con m_name + «CalcBtn», volvemos a leer el término seleccionado y llamamos a UpdateResult(term).
  • Esto permite al usuario modificar cualquier valor introducido (por ejemplo, ajustar los pips del stop-loss) y, a continuación, pulsar «Calcular» para actualizar los resultados.

Cualquier otro evento devuelve «false», por lo que el CAppDialog principal (u otro código) puede gestionarlos si es necesario. Esta clara separación garantiza que solo las interacciones relevantes —cambios en los términos o pulsaciones de botones— activen nuevos cálculos o actualizaciones de la interfaz de usuario

Limpieza (~CForexCalculator)

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

Cuando se destruye el objeto calculadora, el destructor recorre m_inputs[] y elimina cada control asignado dinámicamente (etiquetas y campos de edición). Esto evita las fugas de memoria. Dado que cada vez que el usuario cambia de término, CreateInputFields utiliza ArrayFree para eliminar los controles antiguos, estos deben eliminarse posteriormente. La limpieza final del destructor garantiza que, si se cierra todo el panel de la calculadora o se cierra la aplicación, todos los controles creados por esta clase se liberen correctamente.


(3) Integración de la calculadora de valores de divisas en el panel de gestión de operaciones

Para integrar CForexCalculator en CTradeManagementPanel, basta con declarar una instancia de la clase de la calculadora como uno de los campos miembros del panel. Al incluir m_calculator entre los miembros protegidos, reservamos efectivamente una parte de la memoria del panel para el estado interno de la calculadora (su menú desplegable, etiquetas, campos de edición y botones).

CForexCalculator m_calculator;

Dado que el encabezado del panel ya incluye el encabezado de ForexValuesCalculator, el compilador sabe exactamente cómo está estructurada la clase CForexCalculator y qué dependencias tiene. En la práctica, esto significa que no copiamos y pegamos los controles de la calculadora ni reordenamos su código; en su lugar, recurrimos a la composición. El panel puede tratar a `m_calculator` como cualquier otro control —crearlo, ajustar su tamaño, añadirlo al cuadro de diálogo y reenviarle eventos— sin acceder a sus miembros privados.

#include <ForexValuesCalculator.mqh>

Creación y diseño en el método Create()

El siguiente paso tiene lugar dentro del método Create() del panel, donde creamos las cuatro secciones de forma secuencial. Tras diseñar la sección «Ejecución rápida de órdenes» y dibujar el primer separador, pasamos a la sección «Calculadora de Forex» dibujando primero una etiqueta de encabezado de sección:

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;

Inmediatamente después, invocamos el método Create de la calculadora, pasando como parámetros el gráfico actual, un prefijo único (por ejemplo, nombre + «_ForexCalc»), el índice de la subventana y la ubicación exacta (x, y), junto con CALCULATOR_WIDTH y 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 ese mismo sistema de coordenadas para situar su menú desplegable en la esquina superior izquierda del bloque de la calculadora y para reservar su propio espacio dinámico para los campos de entrada más abajo. Dado que especificamos una altura fija, la clase de la calculadora sabe exactamente dónde colocar la etiqueta y el campo del resultado en la parte inferior. Una vez que m_calculator.Create() devuelve «true», llamamos inmediatamente a m_calculator.AddToDialog(this), lo que recorre todos los subcontroles (m_dropdown, todos los pares CLabel/CEdit creados dinámicamente, el botón «Calcular» y el campo de visualización del resultado) y los añade al CAppDialog principal. Este paso del registro es fundamental: el bucle de eventos interno del cuadro de diálogo incluirá ahora los controles de la calculadora y los mostrará con el orden de superposición correcto.

Tamaño, ubicación y espaciado

Mantener una separación adecuada entre las secciones es fundamental para evitar que se superpongan visualmente. Tras añadir la calculadora, desplazamos nuestro cursor curY una distancia igual a la altura total de la calculadora más el doble del espacio entre secciones. De este modo, el siguiente separador o la sección «Órdenes pendientes» que le siga comenzará justo debajo del bloque de la calculadora, sin dejar lugar a dudas sobre los límites de control. Durante este proceso de maquetación, ningún control se reposiciona manualmente con respecto a otro; en su lugar, la secuencia que consiste en dibujar una etiqueta de encabezado, crear la calculadora en un origen conocido y, a continuación, desplazar el cursor vertical garantiza que la región «Calculadora» permanezca autónoma.

Como hemos definido unas constantes claras —CALCULATOR_WIDTH y CALCULATOR_HEIGHT—, el panel no necesita saber cuántas filas de entrada mostrará la calculadora. La lógica interna de la calculadora ajusta dinámicamente el tamaño de m_inputs[], pero nunca modifica el bloque reservado global. Por lo tanto, si en el futuro añadimos más filas de entrada (por ejemplo, un campo «Tipo de swap»), la calculadora simplemente desplazará su propio campo de resultado hacia abajo dentro de esa altura fija; el panel puede ignorar esos detalles.

Reenvío y priorización de eventos

Igualmente importante es la gestión de eventos. Si un usuario interactúa con cualquiera de los controles de la calculadora —por ejemplo, selecciona un nuevo término en el menú desplegable o hace clic en el botón «Calcular»—, esos eventos basados en objetos llegan a CTradeManagementPanel::OnEvent(...). Justo al principio de OnEvent, reenviamos todos los eventos a:

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

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

   return(true);
}

Si la calculadora reconoce el evento (es decir, si sparam coincide con uno de los nombres de sus controles secundarios, como «MyPanel_ForexCalcDropdown» o «MyPanel_ForexCalcCalcBtn»), devuelve «true» tras el procesamiento y salimos inmediatamente. Este mecanismo de retorno anticipado garantiza que la lógica de la calculadora para reconstruir los campos de entrada o actualizar la etiqueta del resultado tenga siempre prioridad.

Solo si m_calculator.OnEvent(...) devuelve «false» continuamos procesando otros eventos específicos del panel, como los clics en los botones de las secciones «Pedido rápido» o «Pedidos pendientes». De este modo, la calculadora dispone, en la práctica, de su propio subdiálogo: puede añadir y eliminar sus controles dinámicos, responder a las entradas del usuario y actualizar su pantalla sin interferir con los demás controles del panel ni verse afectada por ellos.


(4) Ajustes en el EA de NewAdminPanel para garantizar su compatibilidad con las nuevas actualizaciones

Es absolutamente fundamental llamar a g_tradePanel.Run() en la rutina de inicialización o de visualización del panel del EA para que todos los elementos interactivos de la interfaz gráfica de usuario —especialmente los cuadros combinados y los selectores de fecha— funcionen correctamente. Internamente, Run() transfiere el control al bucle de procesamiento de eventos de la clase base CAppDialog, que detecta de forma activa los clics del ratón, las pulsaciones de teclas y otros eventos del gráfico dirigidos a los controles secundarios del cuadro de diálogo. Si no se invoca Run(), la instancia de CTradeManagementPanel simplemente permanece en memoria, pero no se registra en el tiempo de ejecución de MQL5 como un cuadro de diálogo activo. En la práctica, esto significa que al seleccionar un elemento del ComboBox «Tipo de orden pendiente» o al modificar la fecha de vencimiento mediante CDatePicker, no se generarían los eventos CHARTEVENT_OBJECT_CHANGE o CHARTEVENT_OBJECT_ENDEDIT necesarios para que el panel los gestionara.

En cuanto se llama a g_tradePanel.Run(). Sin embargo, el cuadro de diálogo entra en su propio bucle de mensajes: cada vez que se hace clic en el menú desplegable o en el selector de fechas, se activa el método OnEvent(...) del panel, que comprueba y reenvía la llamada a OnChangePendingOrderType() o a OnChangePendingDatePicker(). En resumen, Run() es lo que convierte un conjunto estático de controles en una interfaz de usuario receptiva e interactiva. Sin él, el ComboBox se quedaría bloqueado en su valor inicial, y el selector de fechas nunca activaría un evento para actualizar la lógica de precios de los pedidos pendientes ni la visualización del calendario.

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()

Igualmente importante para la experiencia del usuario es el uso adecuado de ChartRedraw() inmediatamente después de mostrar u ocultar cuadros de diálogo y tras actualizar cualquier elemento visual. Cada vez que se llama a Show() o Hide() en un cuadro de diálogo o en un control concreto —como el ComboBox, el DatePicker o los campos de la calculadora—, es necesario volver a dibujar el lienzo subyacente del gráfico para que los nuevos controles aparezcan en pantalla (o los antiguos desaparezcan). En nuestro código de EA, se observan frecuentes llamadas a ChartRedraw() en controladores como HandleTradeManagement(), ToggleInterface(), y dentro de OnEvent(...) una vez que se ha procesado un evento.

Cada llamada a ChartRedraw() obliga a MetaTrader 5 a volver a renderizar todos los objetos del gráfico y los controles de la interfaz gráfica de usuario, lo que garantiza que las listas desplegables se abran correctamente, que los calendarios de DatePicker se superpongan sin problemas y que los valores recién calculados en los campos de la calculadora se muestren sin parpadeos ni retrasos. Si no se llama a ChartRedraw(), el gráfico puede permanecer «desactualizado» durante una fracción de segundo apreciable tras los cambios de estado, lo que provoca un comportamiento poco receptivo: el usuario podría hacer clic en un elemento diferente del menú desplegable, pero seguiría viendo la selección anterior hasta la siguiente actualización o la actualización automática. Al solicitar explícitamente un redibujado tras cada cambio significativo —ya sea al activar o desactivar la visibilidad de un panel, actualizar etiquetas o recalcular resultados—, garantizamos una interfaz fluida y en tiempo real en la que las selecciones del ComboBox aparecen al instante, los calendarios del selector de fechas se muestran sin retrasos y los resultados de la calculadora se actualizan de inmediato.

// 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
    }
}

  • Actualizar el gráfico tras cambios en la visibilidad: En HandleTradeManagement(), llamamos a ChartRedraw() inmediatamente después de Show() o Hide(). De este modo, el panel aparece o desaparece de inmediato, evitando que la pantalla se quede estática mientras el panel permanece oculto o visible hasta que se produzca alguna acción en el gráfico.
  • Actualización del gráfico tras la delegación de eventos: Dentro de OnChartEvent(...), tras reenviar el evento a g_tradePanel.OnEvent(...), volvemos a llamar a ChartRedraw(). Si el usuario ha interactuado con el ComboBox de la calculadora —por ejemplo, seleccionando «Importe del riesgo»—, la calculadora habrá regenerado sus campos de entrada o actualizado la etiqueta del resultado. La llamada posterior a ChartRedraw() garantiza que esos nuevos cuadros de entrada y etiquetas de valor se representen de inmediato, evitando así el parpadeo o que los elementos de la interfaz de usuario queden a medio dibujar.
  • Respuesta fluida e inmediata: Al llamar a ChartRedraw() en cada momento clave —tras pulsar los botones de la interfaz, al mostrar u ocultar un panel y al reenviar eventos a los subpaneles—, garantizamos una experiencia de usuario fluida y con gran capacidad de respuesta. Las listas desplegables de los cuadros combinados se abren al instante, las ventanas emergentes del selector de fechas se muestran correctamente y los valores recién calculados en el campo de la calculadora de divisas se ven sin ningún retraso apreciable.

Ahora podemos pasar a probar las nuevas funciones en la siguiente sección.


Pruebas

Lo siguiente se ejecutó en MetaTrader 5 tras una compilación satisfactoria. La versión actualizada de TradeManagementPanel incluye un flujo de trabajo mejorado para la colocación de órdenes pendientes, así como una calculadora de valores de divisas integrada que permite calcular métricas clave del mercado de divisas y tomar decisiones de negociación más fundamentadas.

Prueba de la calculadora ForexValuesCalculator

Prueba de la calculadora de valores de Forex integrada en TradeManagementPanel


Conclusión

Ha sido un debate increíble y muy profundo, y me alegra que hayamos logrado nuestro objetivo principal. Hemos analizado varios conceptos fundamentales del mercado de divisas —como el cálculo del tamaño de la posición, el valor del pip y la relación riesgo-rentabilidad, entre otros— y hemos explicado los fundamentos matemáticos que todo operador de divisas debería comprender. La conversión de estas fórmulas a código MQL5 refuerza los conocimientos teóricos de los operadores y, además, ayuda a los desarrolladores a implementar correctamente estos cálculos en sus propios proyectos.

Una de las conclusiones clave de nuestro trabajo en el TradeManagementPanel fue el aprovechamiento de los widgets de la biblioteca estándar de MQL5, concretamente CComboBox y `CDatePicker`. Al utilizar estos controles, mejoramos el diseño y la accesibilidad de los campos de entrada relacionados y simplificamos el proceso de configuración de la fecha de vencimiento de las órdenes pendientes. Esto supone un ahorro de tiempo considerable en comparación con la introducción manual de fechas y reduce la posibilidad de que se produzcan errores por parte del usuario.

A lo largo del proceso, nos centramos en el diseño modular: separamos la calculadora, los controles de órdenes pendientes y los botones de ejecución rápida en clases distintas que interactúan entre sí de forma fluida. Garantizar que los eventos de nuestro ComboBox y DatePicker respondan correctamente dentro del EA pone de manifiesto un patrón sólido y reutilizable. Cada componente que hemos creado se puede extraer e integrar en proyectos futuros con un mínimo de modificaciones.

Dicho esto, aunque la interfaz de usuario ya es sólida, todavía hay margen para perfeccionar y optimizar nuestra lógica de cálculo del valor. Agradezco vuestros comentarios y sugerencias en la sección de comentarios; vuestras ideas para mejorar los conceptos actuales serán de gran ayuda. Espero que este ejercicio te haya resultado instructivo, y espero con interés nuestra próxima publicación. ¡No te lo pierdas!


A continuación encontrarás todos los archivos relacionados con este proyecto:

Archivo adjunto Descripción
TradeManagementPanel.mqh Contiene la lógica principal de la interfaz de negociación, incluida la gestión de órdenes de mercado y pendientes, los cálculos de riesgo y una calculadora de divisas integrada. Ofrece controles de interfaz gráfica de usuario, como menús desplegables, selectores de fecha y botones de acción, todos ellos encapsulados en un panel derivado de CAppDialog. Desempeña un papel fundamental en la gestión de las operaciones de trading y las entradas interactivas de los usuarios.
ForexValuesCalculator.mqh Implementa el motor de cálculo principal que se utiliza en el Panel de gestión de operaciones para calcular parámetros de las operaciones, como el valor del pip, el margen, el tamaño de la posición y las relaciones riesgo-rentabilidad. 
New_Admin_Panel.mq5 El punto de acceso principal del Asesor Experto que agrupa todos los módulos individuales —Gestión de operaciones, Comunicaciones, Análisis— en una interfaz gráfica unificada. Se encarga de la instanciación de paneles, el enrutamiento de eventos, la creación de objetos de gráficos y el control general del diseño. Además, garantiza una respuesta fluida mediante llamadas frecuentes a ChartRedraw() y activa la funcionalidad del panel mediante .Run().
Images.zip Una colección de recursos de mapas de bits utilizados para botones de interfaz y elementos visuales. Incluye archivos como TradeManagementPanelButton.bmp, expand.bmp, collapse.bmp y otros que proporcionan retroalimentación interactiva a través de los estados de los botones (normal/pulsado). Estos recursos son fundamentales para la identidad visual y la usabilidad de la aplicación.
Communications.mqh Define el panel de comunicaciones, lo que permite a los usuarios enviar y recibir mensajes a través de un bot de Telegram. Incluye componentes de interfaz gráfica de usuario para introducir credenciales (ID de chat, token del bot) y un campo de entrada de mensajes. Este panel también es compatible con futuras funciones de gestión de contactos y está creado con los controles CChartCanvas, CBmpButton y CEdit.
AnalyticsPanel.mqh Ofrece un resumen analítico en forma de gráficos, que incluye la evaluación de señales o el seguimiento del rendimiento. El panel está integrado en la aplicación principal de EA y se muestra a través de g_analyticsPanel. Su estructura sigue el mismo enfoque modular de CAppDialog, lo que permite una lógica independiente y funciones ampliables.
Telegram.mqh Se encarga de las tareas de red de bajo nivel y del formateo JSON necesarios para comunicarse con la API de Telegram Bot. Incluye funciones para enviar mensajes de texto. Este módulo actúa como motor de fondo para el Panel de comunicaciones.
Authentication.mqh Implementa la autenticación de dos factores opcional para el panel de administración, utilizando Telegram como canal de verificación. Envía confirmaciones de inicio de sesión al ID de chat facilitado y verifica la contraseña introducida por el usuario. Este módulo suele activarse durante la inicialización de EA para garantizar la autenticación de los usuarios y bloquear el acceso no autorizado. Actualmente está desactivada para evitar que aparezcan mensajes repetidos durante los ciclos frecuentes de pruebas y desarrollo.

Guarda todos los archivos de encabezado en el directorio MQL5\include y extrae el contenido del archivo Images.zip en la carpeta MQL5\Images. A continuación, compila New_Admin_Panel.mq5 para ejecutarlo en la terminal MetaTrader 5.


Volver al índice

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/18289

Archivos adjuntos |
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)
Clemence Benjamin
Clemence Benjamin | 21 jun 2025 en 08:58
CapeCoddah el formato de cadena sería más fácil.


Saludos, coman sus Wheaties y feliz programación.


Cabo CoddaH

Gracias@CapeCoddah por todos tus comentarios y el esfuerzo que has invertido - realmente contribuye hacia una versión más estable de esta herramienta de comercio multipanel.

Realmente aprecio el tiempo que estás tomando para explorar y resolver las cosas.

Actualmente estoy revisando los problemas que has señalado y también revisaré las modificaciones que has enviado. Las mejoras están en camino.

Saludos cordiales,

Clemence Benjamin

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

Hola,

Estaba intentando instalarlo pero no aparecía ningún botón, sólo puedo ver dos casillas de verificación. Extraje los archivos en la carpeta Include como se menciona y las imágenes se extrajeron en la carpeta images

Hola @Oluwafemi Olabisi,

¿Podrías compartir una captura de pantalla para que pueda ayudarte mejor?

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

Hola @Oluwafemi Olabisi,

¿Podrías compartir una captura de pantalla para que pueda ayudarte mejor?

He adjuntado aquí, cómo se extrajeron los archivos en los directorios INCLUDE y IMAGES respectivamente.
CapeCoddah
CapeCoddah | 23 jun 2025 en 12:57

Hola Clemence,

Tengo algunas preguntas y tal vez puedas resolver algunas de ellas.

La primera es el Probador de Estrategias

Cuando ejecuto mi EA en él, ninguno de los textos, botones de los paneles, etc. se muestran en la máquina de pruebas. Me he dado cuenta de que algunos de los tuyos sí se muestran. ¿Tienes idea de cuál es la causa de esta diferencia? Estoy planeando incorporar tu EA al mío e intentar determinar cuál es la causa de las diferencias.

En segundo lugar, ¿cómo ponerse en contacto con MetaQuotes para transmitir errores y sugerencias de mejora para ellos. He pasado mucho tiempo en MQL5.com y no puede encontrar una manera.

CapeCoddah
CapeCoddah | 23 jun 2025 en 12:59
Oluwafemi Olabisi #:
He adjuntado aquí, como se extrajeron los archivos en los directorios INCLUDE e IMAGES respectivamente.

El EA debería estar en la carpeta de expertos no en la carpeta include. Después de moverlo, hay que parar el EA y reiniciarlo para que aparezca en el panel de navegación. Es una de las cosas que MQ debería cambiar. Al menos permitir a los usuarios contraer la carpeta, ya sea Indicadores o EXpertos, y luego actualizar la lista durante el comando de expansión en lugar de detener el Terminal y reiniciarlo y luego abrir todos los subdirectorios hasta llegar a su objetivo. Mejor aún, deberían hacerlo automáticamente cuando un nuevo ejecutable se coloca en el subdirectorio.

Programación gráfica para principiantes (Parte I): Aprendiendo CCanvas con Crazy Scalper Programación gráfica para principiantes (Parte I): Aprendiendo CCanvas con Crazy Scalper
Este artículo introduce la librería CCanvas en MQL5 mediante el desarrollo paso a paso de un minijuego que se ejecuta sobre el gráfico de MetaTrader 5. Se explican el sistema de coordenadas, el renderizado vectorial de formas, el canal alfa para transparencias, el bucle con temporizador, la máquina de estados, la física básica y la detección de colisiones AABB, además de la captura de teclado. Al finalizar, podrá crear superficies graficas interactivas y sentar las bases de paneles y minijuegos propios.
Análisis de las brechas temporales de precios en MQL5 (Parte II): Creamos un mapa de calor de la distribución de liquidez a lo largo del tiempo Análisis de las brechas temporales de precios en MQL5 (Parte II): Creamos un mapa de calor de la distribución de liquidez a lo largo del tiempo
Hoy veremos una guía detallada sobre cómo crear un indicador de mapa de calor para MetaTrader 5 que visualice la distribución de precios a lo largo del tiempo como un mapa de calor. El artículo revela la base matemática del análisis de densidad temporal, donde cada nivel de precio está coloreado desde el rojo (tiempo mínimo de estancia) hasta el azul (tiempo máximo de estancia).
Redes neuronales en el trading: Segmentación periódica adaptativa (Final) Redes neuronales en el trading: Segmentación periódica adaptativa (Final)
Le propongo sumergirse en el apasionante mundo de LightGTS, un framework de predicción de series temporales ligero pero potente que combina la convolución adaptativa y la codificación RoPE con métodos de atención innovadores. En el artículo de hoy, encontrará una descripción detallada de todos los componentes, desde la creación de parches hasta una compleja combinación de asesores expertos en un decodificador, listo para su integración en proyectos MQL5. ¡Descubra cómo LightGTS lleva el trading automatizado al siguiente nivel!
Indicador de estacionalidad por horas, días de la semana y meses Indicador de estacionalidad por horas, días de la semana y meses
Este artículo explica cómo desarrollar una herramienta para analizar patrones de precios recurrentes en los mercados financieros, ya sea por el día del mes (1-31), el día de la semana (lunes a domingo) o la hora del día (0-23). El indicador analiza datos históricos, calcula la rentabilidad media de cada periodo y muestra los resultados en forma de histograma con una previsión. Incluye parámetros personalizables: tipo de estacionalidad, número de barras analizadas, visualización como porcentajes o valores absolutos, colores del gráfico.