English Русский 中文 Deutsch 日本語 Português
Creando un ayudante para el comercio manual

Creando un ayudante para el comercio manual

MetaTrader 5Ejemplos | 10 junio 2016, 15:15
2 421 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

En este artículo voy a presentar un ejemplo más sobre cómo crear desde cero un panel comercial que ayudará a trabajar en Fórex a los partidarios del comercio manual.

1. Definiendo la funcionalidad imprescindible para el panel comercial

En primer lugar, deberíamos determinar para nosotros mismos qué resultado final queremos obtener. Tenemos que decidir qué funcionalidad esperamos de nuestro panel, qué diseño será el más cómodo para nostros y qué funciones especiales habrá que implementar en nuestro programa. En este artículo propongo mi propia visión del panel comercial, pero aceptaré sugerencias con mucho gusto. Espero poder analizarlos al detalle en futuros artículos. 

Bien, nuestro panel deberá incluir sin lugar a dudas los siguientes componentes.

  1. Botones de compra y venta.
  2. Botones de cierre de todas las posiciones según el símbolo y la cuenta o en una dirección particular (órdenes de compra o venta).
  3. La posibilidad de mostrar los niveles de stop-loss y take-profit, tanto en puntos, como en la divisa del depósito (al introducir un parámetro, el otro debe corregirse de forma automática).
  4. El panel deberá calcular de forma automática los niveles de stop-loss y take-profit según los parámetros establecidos de forma manual (p.2) y representarlos en el gráfico.
  5. El tráder deberá tener la posibilidad de desplazarse por los niveles de stop-loss y take-profit en el gráfico. Además, todos los cambios deberán reflejarse en el panel con el cambio de los valores correspondientes.
  6. El panel deberá calcular el volumen próximo de la operación según los parámetros de riesgo (en la divisa del depósito o en tanto por ciento del balance actual).
  7. El tráder deberá tener la posibilidad de definir independientemente el volumen de la operación. Además, los parámetros correspondientes que dependan de este deberán recalcularse automáticamente.
  8. El panel deberá recordar qué parámetros son introducidos por el tráder, y cuáles se calculan de forma automática. Esto es imprescindible para que los posteriores recálculos de los parámetros introducidos por el tráder no cambien dentro de lo posible.
  9. El panel deberá guardar todos los parámetros introducidos, para no tener que introducirlos de nuevo durante futuros reinicios.

2. Creando la maqueta gráfica del panel

Tomemos una hoja de papel limpia y dibujemos nuestro futuro panel, colocando en él todos los elementos necesarios.

Al pensar en el diseño del futuro panel comercial, es necesario reflexionar sobre la practicidad de su uso. En primer lugar, deberá ser lo suficientemente informativo, a la vez que fácil de leer, y por supuesto se deberá evitar la sobrecarga con elementos prescindibles. Deberemos recordar siempre que se trata de un instrumento real para el trabajo del tráder, y no simplemente una imagen bonita en la pantalla.

Esta es mi variante.


3. Construcción de la maqueta de un panel en el lenguaje MQL5

3.1.  Plantilla

Ahora que tenemos una idea del objetivo final, vamos a plasmarlo en el código MQL5. Para ello, hay que usar al máximo las bibliotecas estándar siempre que sea posible, pues estas facilitarán nuestro trabajo. En MQL5 existe la clase CAppDialog, que es una clase básica a la hora de construir ventanas de diálogo. Construiremos nuestro panel basándonos en esta clase. 
Para ello, crearemos un duplicado de la clase y lo analizaremos en la función OnInit().

#include <Controls\Dialog.mqh>

class CTradePanel : public CAppDialog
  {
public:
                     CTradePanel(void){};
                    ~CTradePanel(void){};
  };

CTradePanel TradePanel;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   // Creat Trade Panel
   if(!TradePanel.Create(ChartID(),"Trade Panel",0,20,20,320,420))
     {
      return (INIT_FAILED);
     }
   // Run Trade Panel
   TradePanel.Run();
//---
   return(INIT_SUCCEEDED);
  }

El resultado de esta sencilla manipulación será la plantilla de nuestro futuro panel.


3.2. Declarando los objetos necesarios

Ahora vamos a colocar en nuestra plantilla los elementos de control necesarios. Con este cometido, crearemos los objetos de las clases correspondientes para cada elemento de control. Crearemos los objetos con la ayuda de las clases estándar CLabel, CEdit, CButton y CBmpButton.

Añadiremos los archivos de inclusión y crearemos la función Creat() para la clase CTradePanel:

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

Los archivos "Edit.mqh" y "BmpButton.mqh" no los he incluido a propósito, puesto que ya son llamados desde "Dialog.mqh".

El siguiente paso es declarar las variables del tipo correspondiente para cada objeto en el panel en la clase CTradePanel, y allí mismo declarar el procedimiento Creat(..), en el que colocamos todos los elementos en su sitio. Preste atención: las variables declaradas y otras acciones dentro de la clase CTradePanel las declaramos en el bloque "private". Las funciones disponibles para la llamada desde fuera de los límites de la clase, tales como Creat(...), se declaran en el bloque "public".

class CTradePanel : public CAppDialog
  {
private:

   CLabel            ASK, BID;                        // Display Ask and Bid prices
   CLabel            Balance_label;                   // Display label "Account Balance"
   CLabel            Balance_value;                   // Display Account balance
   CLabel            Equity_label;                    // Display label "Account Equity"
   CLabel            Equity_value;                    // Display Account Equity
   CLabel            PIPs;                            // Display label "Pips"
   CLabel            Currency;                        // Display Account currency
   CLabel            ShowLevels;                      // Display label "Show"
   CLabel            StopLoss;                        // Display label "Stop Loss"
   CLabel            TakeProfit;                      // Display label "TakeProfit"
   CLabel            Risk;                            // Display label "Risk"
   CLabel            Equity;                          // Display label "% to Equity"
   CLabel            Currency2;                       // Display Account currency
   CLabel            Orders;                          // Display label "Opened Orders"
   CLabel            Buy_Lots_label;                  // Display label "Buy Lots"
   CLabel            Buy_Lots_value;                  // Display Buy Lots value 
   CLabel            Sell_Lots_label;                 // Display label "Sell Lots"
   CLabel            Sell_Lots_value;                 // Display Sell Lots value 
   CLabel            Buy_profit_label;                // Display label "Buy Profit"
   CLabel            Buy_profit_value;                // Display Buy Profit value 
   CLabel            Sell_profit_label;               // Display label "Sell Profit"
   CLabel            Sell_profit_value;               // Display Sell profit value 
   CEdit             Lots;                            // Display volume of next order
   CEdit             StopLoss_pips;                   // Display Stop loss in pips
   CEdit             StopLoss_money;                  // Display Stop loss in accaunt currency
   CEdit             TakeProfit_pips;                 // Display Take profit in pips
   CEdit             TakeProfit_money;                // Display Take profit in account currency
   CEdit             Risk_percent;                    // Display Risk percent to equity
   CEdit             Risk_money;                      // Display Risk in account currency
   CBmpButton        StopLoss_line;                   // Check to display StopLoss Line
   CBmpButton        TakeProfit_line;                 // Check to display TakeProfit Line
   CBmpButton        StopLoss_pips_b;                 // Select Stop loss in pips
   CBmpButton        StopLoss_money_b;                // Select Stop loss in accaunt currency
   CBmpButton        TakeProfit_pips_b;               // Select Take profit in pips
   CBmpButton        TakeProfit_money_b;              // Select Take profit in account currency
   CBmpButton        Risk_percent_b;                  // Select Risk percent to equity
   CBmpButton        Risk_money_b;                    // Select Risk in account currency
   CBmpButton        Increase,Decrease;               // Increase and Decrease buttons
   CButton           SELL,BUY;                        // Sell and Buy Buttons
   CButton           CloseSell,CloseBuy,CloseAll;     // Close buttons
   
public:
                     CTradePanel(void){};
                    ~CTradePanel(void){};
  //--- Create function
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   
  };

3.3. Creando los procedimientos de inicialización para grupos de objetos

Ha llegado el momento de escribir el cuerpo de la función Creat(...). Preste atención a que en esta función deberemos analizar todos los objetos declarados más arriba. Es fácil calcular que más arriba hemos declarado 45 objetos de 4 tipos. Eso significa que lo más conveniente es escribir 4 procedimientos de inicialización de objetos, uno para cada tipo.  Las funciones de inicialización de clases las anunciamos en el bloque "private".

Por supuesto que se podrían declarar los objetos en la matriz, pero en este caso perderíamos la conexión entre el nombre de la variable del objeto y su funcionalidad, lo que complicaría el posterior trabajo con los objetos. Por eso la elección se ha hecho en favor de la transparencia del código y la facilidad a la hora de trabajar con el mismo (no vamos a ponernos barreras para superarlas después de forma heroica).

Clase CLabel

La clase CLabel la utilizaremos para representar textos informativos en nuestro panel. Al crear la función de inicialización, conviene definir qué funciones serán únicas para todos los elementos de esta clase, cuáles de ellas se van a diferenciar y en qué. En este caso, las diferencias son las siguientes:

  • nombre del objeto;
  • texto representado;
  • coordenadas del elemento;
  • igualación del objeto con respecto a los puntos de enlace.

Después de determinar las diferencias, definimos cuáles de ellas se transmitirán como parámetros de la función, para hacer esta función universal, y cuáles podremos generar en el propio procedimiento.

Al trabajar con los objetos es mejor recordar que todos los objetos en el gráfico deberán tener nombres individuales. Como siempre, la elección depende del programador: establecer cada nombre del objeto de forma independiente o generarlo con un programa. Al crear la función universal, he elegido generar los nombres de los objetos dentro del programa. Para ello, he definido el nombre del objeto para la clase con el número ordinal añadido.

string name=m_name+"Label"+(string)ObjectsTotal(chart,-1,OBJ_LABEL);

El texto representado, las coordenadas del objeto y la igualación del objeto con respecto al punto de enlace lo transmitiremos a la función con la ayuda de los parámetros. Para que leer el código del programa sea más sencillo y el trabajo del programador sea más simple, crearemos una enumeración para igualar el objeto:

  enum label_align
     {
      left=-1,
      right=1,
      center=0
     };

Asimismo, en los parámetros del procedimiento deberemos indicar el código del gráfico, el número de la subventana y el enlace al objeto creado.

En la propia función escribiremos los procedimientos que se deberán ejecutar con cada objeto de esta clase.

  • Crearemos el objeto con la ayuda de la función Create(...) de la clase base.
  • Después ubicaremos el texto necesario en el objeto.
  • A continuación, igualamos el objeto con respecto al punto de enlace.
  • Añadimos el objeto al "contenedor" de la ventana de diálogo.
bool CTradePanel::CreateLabel(const long chart,const int subwindow,CLabel &object,const string text,const uint x,const uint y,label_align align)
  {
   // All objects must to have separate name
   string name=m_name+"Label"+(string)ObjectsTotal(chart,-1,OBJ_LABEL);
   //--- Call Create function
   if(!object.Create(chart,name,subwindow,x,y,0,0))
     {
      return false;
     }
   //--- Addjust text
   if(!object.Text(text))
     {
      return false;
     }
   //--- Aling text to Dialog box's grid
   ObjectSetInteger(chart,object.Name(),OBJPROP_ANCHOR,(align==left ? ANCHOR_LEFT_UPPER : (align==right ? ANCHOR_RIGHT_UPPER : ANCHOR_UPPER)));
   //--- Add object to controls
   if(!Add(object))
     {
      return false;
     }
   return true;
  }

Clase CButton

La clase CButton se usa para crear botones de forma rectangular con un texto. Se trata de nuestros botones estándar para abrir y cerrar órdenes.

Al empezar a trabajar con esta clase de objetos, usaremos el mismo enfoque que en el caso anterior. Además, conviene tener en cuenta las peculiaridades de su funcionamiento. Ante todo, lo que no necesitamos es igualar el texto en el botón, ya que en la clase base ya ha sido alineado en la parte media. Pero aquí ya aparece el tamaño del botón que transmitiremos en los parámetros.

Asimismo, en los botones se representa su estado actual: si está pulsado o no. Además, mientras se encuentra en el estado pulsado, el botón puede bloquearse o no. Por consiguiente, estas opciones adicionales las deberemos describir en el procedimiento de inicialización del objeto. Para nuestros botones desactivaremos el bloqueo y estableceremos el estado "Sin pulsar".

bool CTradePanel::CreateButton(const long chart,const int subwindow,CButton &object,const string text,const uint x,const uint y,const uint x_size,const uint y_size)
  {
   // All objects must to have separate name
   string name=m_name+"Button"+(string)ObjectsTotal(chart,-1,OBJ_BUTTON);
   //--- Call Create function
   if(!object.Create(chart,name,subwindow,x,y,x+x_size,y+y_size))
     {
      return false;
     }
   //--- Addjust text
   if(!object.Text(text))
     {
      return false;
     }
   //--- set button flag to unlock
   object.Locking(false);
   //--- set button flag to unpressed
   if(!object.Pressed(false))
     {
      return false;
     }
   //--- Add object to controls
   if(!Add(object))
     {
      return false;
     }
   return true;
  }

Clase CEdit

La clase CEdit se usa para crear objetos de introducción de datos. A estos objetos en nuestro panel pertenecen las celdas para introducir el volumen de la operación, los volúmenes de stop-loss y take-profit (en puntos y en la divisa del depósito) y el nivel de riesgo.

Usamos el mismo enfoque que para las dos clases descritas anteriormente. Pero, a diferencia de los botones, en el procedimiento de inicialización de esta clase se deberá indicar cómo igualar el texto en la celda. Además, conviene recordar que cualquier información que se introduzca o transmita a una celda, siempre se percibirá como texto. Por eso, al transmitir una cifra a un objeto para la representación, hay que transformarla primero en texto.

Los objetos de la clase CEdit, a diferencia de los botones, no tienen el estado "Pulsado" / "Sin pulsar", pero al mismo tiempo, esta clase está prevista para la creación de objetos inaccesibles para la edición por parte del usuario durante el funcionamiento del programa. En nuestro caso, los objetos deberán estar disponibles para la edición por parte del usuario. Indicaremos esto en nuestra función de inicialización. 

bool CTradePanel::CreateEdit(const long chart,const int subwindow,CEdit &object,const string text,const uint x,const uint y,const uint x_size,const uint y_size)
  {
   // All objects must to have separate name
   string name=m_name+"Edit"+(string)ObjectsTotal(chart,-1,OBJ_EDIT);
   //--- Call Create function
   if(!object.Create(chart,name,subwindow,x,y,x+x_size,y+y_size))
     {
      return false;
     }
   //--- Addjust text
   if(!object.Text(text))
     {
      return false;
     }
   //--- Align text in Edit box
   if(!object.TextAlign(ALIGN_CENTER))
     {
      return false;
     }
   //--- set Read only flag to false
   if(!object.ReadOnly(false))
     {
      return false;
     }
   //--- Add object to controls
   if(!Add(object))
     {
      return false;
     }
   return true;
  }

Clase CBmpButton

La clase CBmpButton se usa para crear botones no estándar, con la utilización de objetos gráficos en lugar de inscripciones. Estos botones, que son comprensibles de una forma intuitiva para cualquier usuario, se usan al crear los elementos estandarizados de control para diferentes programas aplicados. En nuestro caso, con la ayuda de la clase crearemos:

  • botones de opción para elegir, en los que se reflejará el stop-loss, el take-profit y el riesgo: en forma de dinero o en puntos (o en tanto por ciento para el riesgo);
  • casillas de verificación para registrar la representación o no de los niveles de stop-loss y take-profit en el gráfico;
  • botones de aumento o disminución del volumen de la operación.

El trabajo con esta clase de objetos es análogo al trabajo con la clase CButton. La diferencia consiste en la transmisión de objetos gráficos para el estado de pulsado y sin pulsar del botón, en lugar de texto. Para nuestro panel, utilizaremos las representaciones de los botones suministradas junto con MQL5. Además, para que se pueda difundir el producto programático ya listo en un archivo, registraremos estas imágenes en calidad de recursos.

#resource "\\Include\\Controls\\res\\RadioButtonOn.bmp"
#resource "\\Include\\Controls\\res\\RadioButtonOff.bmp"
#resource "\\Include\\Controls\\res\\CheckBoxOn.bmp"
#resource "\\Include\\Controls\\res\\CheckBoxOff.bmp"
#resource "\\Include\\Controls\\res\\SpinInc.bmp"
#resource "\\Include\\Controls\\res\\SpinDec.bmp"

Así, es mejor tener en cuenta que se bloquean (es decir, conservan el estado de "Pulsado" o "Sin pulsar") todos los elementos de esta clase, excepto los botones de aumento y disminución de lote. Por eso añadimos parámetros adicionales a la función de inicialización.

//+------------------------------------------------------------------+
//| Create BMP Button                                                |
//+------------------------------------------------------------------+
bool CTradePanel::CreateBmpButton(const long chart,const int subwindow,CBmpButton &object,const uint x,const uint y,string BmpON,string BmpOFF,bool lock)
  {
   // All objects must to have separate name
   string name=m_name+"BmpButton"+(string)ObjectsTotal(chart,-1,OBJ_BITMAP_LABEL);
   //--- Calculate coordinates
   uint y1=(uint)(y-(Y_STEP-CONTROLS_BUTTON_SIZE)/2);
   uint y2=y1+CONTROLS_BUTTON_SIZE;
   //--- Call Create function
   if(!object.Create(m_chart_id,name,m_subwin,x-CONTROLS_BUTTON_SIZE,y1,x,y2))
      return(false);
   //--- Assign BMP pictuers to button status
   if(!object.BmpNames(BmpOFF,BmpON))
      return(false);
   //--- Add object to controls
   if(!Add(object))
      return(false);
   //--- set Lock flag to true
   object.Locking(lock);
//--- succeeded
   return(true);
  }

Tras escribir la función de creación de objetos, declaramos obligatoriamente estas funciones en el bloque "private" de nuestra clase.

private:

   //--- Create Label object
   bool              CreateLabel(const long chart,const int subwindow,CLabel &object,const string text,const uint x,const uint y,label_align align);
   //--- Create Button
   bool              CreateButton(const long chart,const int subwindow,CButton &object,const string text,const uint x,const uint y,const uint x_size,const uint y_size);
   //--- Create Edit object
   bool              CreateEdit(const long chart,const int subwindow,CEdit &object,const string text,const uint x,const uint y,const uint x_size,const uint y_size);
   //--- Create BMP Button
   bool              CreateBmpButton(const long chart,const int subwindow,CBmpButton &object,const uint x,const uint y,string BmpON,string BmpOFF,bool lock);

3.4. Ubicamos los elementos en su sitio

Ahora que ya hemos escrito las funciones de inicialización para cada clase de objetos, ha llegado el momento de escribirla para nuestro panel comercial. Las tareas principales de esta función son el cálculo de coordenadas de cada uno de los objetos del panel y la creación paso a paso de todos los objetos mediante la llamada de la función de inicialización correspondiente.

Recordemos de nuevo que los elementos del panel deberán colocarse de forma cómoda para el usuario, y además todo deberá tener un aspecto estéticamente adecuado. Hemos prestado atención a este aspecto al crear la maqueta de nuestro panel, y ahora nos mantendremos fieles a esta concepción. Al mismo tiempo, es necesario saber que al usar nuestra clase en el programa final el tamaño del panel puede ser diferente. Para que al modificar las dimensiones del panel comercial se conserve el concepto de nuestro diseño, deberemos calcular las coordenadas de cada objeto, y no indicarlo imlícitamente. Con este objetivo, hemos creado unas referencias peculiares:

  • la distancia desde los límites de la ventana hasta el primer elemento de control;
  • la distancia entre elementos de control según la altura;
  • la altura del elemento de control.
   #define  Y_STEP   (int)(ClientAreaHeight()/18/4)      // height step betwine elements
   #define  Y_WIDTH  (int)(ClientAreaHeight()/18)        // height of element
   #define  BORDER   (int)(ClientAreaHeight()/24)        // distance betwine boder and elements

De esta forma, podremos calcular las coordenadas del primer elemento de control y de cada elemento siguiente con respecto al anterior.
Asimismo, tras definir las dimensiones óptimas de nuestro panel, podremos indicarlas como valores por defecto para los parámetros transmitidos de la función.

bool CTradePanel::Create(const long chart,const string name,const int subwin=0,const int x1=20,const int y1=20,const int x2=320,const int y2=420)
  {
      // At first call creat function of parents class
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
     {
      return false;
     }
   // Calculate coofrdinates and size of BID object
   // Coordinates calculate in dialog box, not in chart
   int l_x_left=BORDER;
   int l_y=BORDER;
   int y_width=Y_WIDTH;
   int y_sptep=Y_STEP;
   // Creat object
   if(!CreateLabel(chart,subwin,BID,DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits),l_x_left,l_y,left))
     {
      return false;
     }
   // Adjust font size for object
   if(!BID.FontSize(Y_WIDTH))
     {
      return false;
     }
   // Repeat same functions for other objects
   int l_x_right=ClientAreaWidth()-20;
   if(!CreateLabel(chart,subwin,ASK,DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits),l_x_right,l_y,right))
     {
      return false;
     }
   if(!ASK.FontSize(Y_WIDTH))
     {
      return false;
     }
   l_y+=2*Y_WIDTH;
...................
  }

Podrá familiarizarse con el código completo de la función en el ejemplo adjunto.

El resultado de nuestros esfuerzos es el panel que sigue abajo.


Pero, por el momento, es solo una maqueta: una imagen bonita en el gráfico. En la siguiente etapa le "insuflaremos vida".

4. "Animación de la imagen"

Ahora que hemos creado la maqueta gráfica de nuestro panel, ha llegado el momento de eneseñarle a reaccionar a los eventos que vayan sucediento. Por consiguiente, para crear y configurar el manejador de eventos, hay que decidir a qué eventos y cómo deberá reaccionar.

4.1. Cambio de precio del instrumento

Al cambiar el precio del instrumento, el terminal МТ5 genera el evento NewTick, que inicia la función OnTick() del asesor. Como consecuencia, desde esta función deberemos llamar a la función correspondiente de nuestra clase, que procesará este evento. Le daremos el nombre análogo OnTick() y la declararemos en el bloque "public", dado que se llamará desde un programa externo.

public:

   virtual void      OnTick(void);
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   TradePanel.OnTick();
  }

¿Qué cambios tienen lugar en el panel al cambiar el precio del instrumento? Lo primero que debemos hacer es cambiar los valores Ask y Bid en nuestro panel.
//+------------------------------------------------------------------+
//| Event "New Tick                                                  |
//+------------------------------------------------------------------+
void CTradePanel::OnTick(void)
  {
   //--- Change Ask and Bid prices on panel
   ASK.Text(DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_ASK),(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS)));
   BID.Text(DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_BID),(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS)));

Después, en el caso de que haya posiciones abiertas en la cuenta, cambiamos el valor de la suma de los recursos (Equity) en el panel. Para curarnos en salud, he añadido la comprobación de la correspondencia entre la suma reflejada en el panel y los recursos reales en la cuenta, incluso cuando no haya posiciones abiertas. Esto permitirá representar la suma real de los recursos después de "situaciones eventuales". De esta forma, no tendremos necesidad de comprobar la presencia de posiciones abiertas: comprobaremos directamente la correspondencia entre la suma actual de los recursos según la cuenta y la representada en el panel. En caso necesario, mostraremos en el panel el valor verdadero.

//--- Сheck and change (if neccesory) equity
   if(Equity_value.Text()!=DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2)+" "+AccountInfoString(ACCOUNT_CURRENCY))
     {
      Equity_value.Text(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
     }
   

También realizaremos iteraciones análogas para representar el balance.
Me adelantaré a la pregunta lógica: "¿Para qué comprobar el balance en cada tick, si este cambia solo al realizar operaciones comerciales?" Sí, esto es cierto, y un poco más tarde hablaremos sobre la reacción a nuestro evento comercial. Pero hay una pequeña posibilidad de realizar operaciones comerciales en los momentos en que nuestro panel no está activo o no hay conexión del terminal con el servidor. Precisamente para que en el panel siempre se represente el balance actual, incluso después de diferentes situaciones poco habituales, he añadido esta operación.

El siguiente paso al cambiar el precio es comprobar la presencia de una posición abierta del instrumento actual, y en el caso de que la haya, comprobar y corregir el valor del volumen abierto y del beneficio actual de la posición en los campos Buy y Sell.

//--- Check and change (if neccesory) Buy and Sell lots and profit value.
   if(PositionSelect(_Symbol))
     {
      switch((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
           Buy_profit_value.Text(DoubleToString(PositionGetDouble(POSITION_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
           if(Buy_Lots_value.Text()!=DoubleToString(PositionGetDouble(POSITION_VOLUME),2))
              {
               Buy_Lots_value.Text(DoubleToString(PositionGetDouble(POSITION_VOLUME),2));
              }
           if(Sell_profit_value.Text()!=DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY))
              {
               Sell_profit_value.Text(DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
              }
           if(Sell_Lots_value.Text()!=DoubleToString(0,2))
              {
               Sell_Lots_value.Text(DoubleToString(0,2));
              }
           break;
         case POSITION_TYPE_SELL:
           Sell_profit_value.Text(DoubleToString(PositionGetDouble(POSITION_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
           if(Sell_Lots_value.Text()!=DoubleToString(PositionGetDouble(POSITION_VOLUME),2))
              {
               Sell_Lots_value.Text(DoubleToString(PositionGetDouble(POSITION_VOLUME),2));
              }
           if(Buy_profit_value.Text()!=DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY))
              {
               Buy_profit_value.Text(DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
              }
           if(Buy_Lots_value.Text()!=DoubleToString(0,2))
              {
               Buy_Lots_value.Text(DoubleToString(0,2));
              }
           break;
        }
     }
   else
     {
      if(Buy_Lots_value.Text()!=DoubleToString(0,2))
        {
         Buy_Lots_value.Text(DoubleToString(0,2));
        }
      if(Sell_Lots_value.Text()!=DoubleToString(0,2))
        {
         Sell_Lots_value.Text(DoubleToString(0,2));
        }
      if(Buy_profit_value.Text()!=DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY))
        {
         Buy_profit_value.Text(DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
        }
      if(Sell_profit_value.Text()!=DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY))
        {
         Sell_profit_value.Text(DoubleToString(0,2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
        }
     }

Asimismo, no debemos olvidarnos de comprobar el estado de las casillas de verificación de los niveles de stop-loss y take-profit en el gráfico. En caso necesario, corregiremos la posición de las líneas. Añadimos al código la llamada a estas funciones. Hablaremos sobre ellas con más detalle posteriormente.

   //--- Move SL and TP lines if necessary
   if(StopLoss_line.Pressed())
     {
      UpdateSLLines();
     }
   if(TakeProfit_line.Pressed())
     {
      UpdateTPLines();
     }
   return;
  }

4.2. Introduciendo los valores en los campos editables.

En nuestro panel hay una serie entera de campos editables, y por supuesto, deberemos configurar la obtención y procesamiento de la información introducida.

La introducción de información en los campos editables es un evento de cambio del objeto gráfico que pertenece al grupo de eventos ChartEvent. Los eventos de este grupo se procesan con la función OnChartEvent. Tiene 4 parámetros de entrada: el identificador de eventos y 3 parámetros que caracterizan el evento, pertenecientes a los tipos long, double y string. Al igual que en el caso anterior, creamos un manejador de eventos en nuestra clase y lo llamamos desde la función OnChartEvent, transmitiendo además todos los parámetros de entrada que caracterizan al evento. Adelantándome un poco, querría decir que con esta función se procesarán también los eventos de pulsación del botón del panel comercial. Por eso esta función será el controlador que, tras analizar el evento, llamará a la función de procesamiento de un evento concreto. A continuación, transmitiremos la información sobre el evento a la función de la clase base para ejecutar los procedimientos escritos en la clase base.

public:

   virtual bool      OnEvent(const int id,const long &lparam, const double &dparam, const string &sparam);

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   TradePanel.OnEvent(id, lparam, dparam, sparam);
  }

Para construir este tipo de controlador, utilizaremos macro substituciones.

//+------------------------------------------------------------------+
//| Event Handling                                                   |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CTradePanel)
   ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit)
EVENT_MAP_END(CAppDialog)

Por consiguiente, todas las funciones de procesamiento de eventos las deberemos declarar en el bloque "private" de nuestra clase

private:

   //--- On Event functions
   void              LotsEndEdit(void);                              // Edit Lot size
   void              SLPipsEndEdit(void);                            // Edit Stop Loss in pips
   void              TPPipsEndEdit(void);                            // Edit Take Profit in pips
   void              SLMoneyEndEdit(void);                           // Edit Stop Loss in money
   void              TPMoneyEndEdit(void);                           // Edit Take Profit in money
   void              RiskPercentEndEdit(void);                       // Edit Risk in percent
   void              RiskMoneyEndEdit(void);                         // Edit Risk in money

 Para guardar los datos obtenidos de los campos editables, introduciremos variables adicionales en el bloque "private"

private:

   //--- variables of current values
   double            cur_lot;                         // Lot of next order
   int               cur_sl_pips;                     // Stop Loss in pips
   double            cur_sl_money;                    // Stop Loss in money
   int               cur_tp_pips;                     // Take Profit in pips
   double            cur_tp_money;                    // Take Profit in money
   double            cur_risk_percent;                // Risk in percent
   double            cur_risk_money;                  // Risk in money

Veamos un ejemplo de un evento concreto: la introducción del volumen de la operación preparada. Recordemos que la introducción de cualquier información en campos semejantes se percibe como introducción de texto, independientemente de su contenido. En sí mismo, al introducir el texto en el campo, se genera una serie entera de eventos: se apunta el cursor del ratón a un objeto, se pulsa un botón del ratón, se comienza a editar el campo, se pulsa un botón del teclado, se finaliza la edición del campo, etc. A nosotros solo nos interesa el último evento, cuando se ha terminado de introducir la información. Por eso, la llamada de la función la realizaremos conforme al evento "ON_END_EDIT".

Lo primero que deberemos hacer en la función de procesamiento de este evento es leer el texto introducido e intentar transformarlo en un valor del tipo double.

Después será necesario realizar una "normalización" del valor obtenido, es decir, adaptarlo de acuerdo con las condiciones del instrumento comercial (volumen máximo y mínimo de una orden, así como el salto de cambio del volumen). Para realizar esta operación, escribiremos una función aparte, porque la necesitaremos también al pulsar los botones de aumento y disminución del volumen de la operación. El valor obtenido lo deberemos devolver al panel, para informar al tráder sobre el volumen real de la futura operación.

//+------------------------------------------------------------------+
//| Read lots value after edit                                       |
//+------------------------------------------------------------------+
void CTradePanel::LotsEndEdit(void)
  {
   //--- Read and normalize lot value
   cur_lot=NormalizeLots(StringToDouble(Lots.Text()));
   //--- Output lot value to panel
   Lots.Text(DoubleToString(cur_lot,2));

Aparte de esto, dependiendo de las instalaciones actuales de los botones de opción, deberemos recalcular y cambiar en el panel el valor de los campos editables restantes. Esto es necesario porque al cambiar el volumen de la operación cambiará la suma del riesgo al cerrar una operación por stop-loss (en el caso de que el stop-loss esté indicado en puntos) o el nivel de stop-loss en puntos (en caso de que el stop-loss haya sido indicado en equivalente monetario). Al stop-loss le irá siguiendo el nivel de riesgo. Una situación análoga se da también con los valores del take-profit. Por supuesto, estas operaciones se organizarán a través de las funciones correspondientes.

   //--- Check and modify value of other labels 
   if(StopLoss_money_b.Pressed())
     {
      StopLossPipsByMoney();
     }
   if(TakeProfit_money_b.Pressed())
     {
      TakeProfitPipsByMoney();
     }
   if(StopLoss_pips_b.Pressed())
     {
      StopLossMoneyByPips();
     }
   if(TakeProfit_pips_b.Pressed())
     {
      TakeProfitMoneyByPips();
     }

Cuando construimos un instrumento para el trabajo diario del usuario, siempre deberemos recordar el concepto de "usabilidad" (comododidad de uso). Aquí deberemos recordar el punto 8, descrito al inicio de la funcionalidad de nuestro panel comercial: "El panel deberá recordar qué parámetros son introducidos por el tráder, y cuáles se calculan de forma automática. Esto es imprescindible para que los posteriores recálculos de los parámetros introducidos por el tráder no cambien dentro de lo posible." En otras palabras, en lo sucesivo, al cambiar el stop-loss en puntos, deberemos recordar qué ha cambiado el tráder en último lugar: el volumen de la operación o el nivel de riesgo. En caso necesario, podría hacer falta no tocar los últimos datos introducidos.
Con este objetivo introducimos en el bloque "private" la variable RiskByValue, y en la función de procesamiento de este evento le adjudicamos el valor true.

private:
   bool              RiskByValue;                     // Flag: Risk by Value or Value by Risk
   RiskByValue=true;
   return;
  }

Los principios de organización de la función de corrección de los campos de edición relacionados los veremos usando como ejemplo la función StopLossMoneyByPips, dado que posee una funcionalidad más completa.

1. En esencia, estas función será llamada en tres casos: al cambiar el lote, al introducir un valor en el campo de stop-loss o al desplazar la línea de stop-loss. Por eso, lo primero que deberemos hacer es comprobar el valor actual del volumen de la próxima operación. Además, si la especificación del instrumento no se corresponde con la realidad del mercado, será necesario corregir el valor mostrado en el panel.

//+------------------------------------------------------------------+
//|  Modify SL money by Order lot and SL pips                        |
//+------------------------------------------------------------------+
void CTradePanel::StopLossMoneyByPips(void)
  {
   //--- Read and normalize lot value
   cur_lot=NormalizeLots(StringToDouble(Lots.Text()));
   //--- Output lot value to panel
   Lots.Text(DoubleToString(cur_lot,2));

2. El segundo componente para el cálculo del riesgo posible en dinero es la suma del cambio de los recursos al cambiar el precio del instrumento en un tick con una posición abierta de 1 lote. Para ello, obtenemos el coste de un tick y el tamaño mínimo del cambio del precio del instrumento:

   double tick_value=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE);
   double tick_size=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE);

3. Calculamos las posibles pérdidas basándonos en los datos obtenidos, y el valor conseguido lo mostramos en el panel en el campo existente.

   cur_sl_money=NormalizeDouble(tick_value*cur_lot*(tick_size/_Point)*cur_sl_pips,2);
   StopLoss_money.Text(DoubleToString(cur_sl_money,2));

4 Conviene prestar atención a que la suma de las posibles pérdidas al cerrar una orden por stop-loss, en realidad, constituye precisamente nuestro riesgo en términos monetarios. Por consiguiente, deberemos duplicar el valor calculado en el campo de riesgo en dinero, y después calcular el valor relativo del riesgo (riesgo en tanto por ciento).

   cur_risk_money=cur_sl_money;    Risk_money.Text(DoubleToString(cur_risk_money,2));    cur_risk_percent=NormalizeDouble(cur_risk_money/AccountInfoDouble(ACCOUNT_BALANCE)*100,2);    Risk_percent.Text(DoubleToString(cur_risk_percent,2));
return;
}

La función de cálculo de stop-loss en puntos partiendo de su valor en dinero es la función opuesta a la descrita más arriba, con la excepción de que no cambia el riesgo, pero es necesario corregir la posición de las líneas de representación del nivel de stop-loss en el gráfico.

De forma análoga se adjundican las funciones para la corrección de los valores de take-profit.

De forma similar elaboramos la función para el procesamiento de eventos de edición del resto de los campos. Además, conviene recordar que al redactar los campos también deberemos cambiar el estado de los botones de opción. Además, para no duplicar la escritura del estado de los botones en cada función, llamaremos a la función de procesamiento de pulsación del botón correspondiente. 

4.3. Procesando los eventos de los botones de opción.

Un botón de opción es un elemento de la interfaz que permite al usuario elegir una opción (punto) de un conjunto determinado (grupo).
Por consiguiente, al pulsar un botón de opción deberemos cambiar el estado de los botones relacionados por sentido. Al mismo tiempo, la propia conexión de los botones de opción no provoca el recálculo de algún parámetro.
De esta forma, las funciones de procesamiento de eventos de pulsación de los botones de opción cambiarán solo el estado de los botones de opción interrelacionados, es decir, un botón de opción pulsado activa el estado "Pulsado", y otros botones independientes activan el estado "Sin pulsar".

En lo que respecta a la parte técnica, la pulsación del botón pertenece al grupo de eventos ChartEvent. Por lo tanto, el procesamiento se producirá de la misma forma que le edición de un campo. Declaramos la función de procesamiento de eventos en el bloque "private":

private:

   //--- On Event functions
   void              SLPipsClick();                                  // Click Stop Loss in pips
   void              TPPipsClick();                                  // Click Take Profit in pips
   void              SLMoneyClick();                                 // Click Stop Loss in money
   void              TPMoneyClick();                                 // Click Take Profit in money
   void              RiskPercentClick();                             // Click Risk in percent
   void              RiskMoneyClick();                               // Click Risk in money

Completamos las macro sustituciones del manejador de eventos:

//+------------------------------------------------------------------+
//| Event Handling                                                   |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CTradePanel)
   ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit)
   ON_EVENT(ON_CLICK,StopLoss_pips_b,SLPipsClick)
   ON_EVENT(ON_CLICK,TakeProfit_pips_b,TPPipsClick)
   ON_EVENT(ON_CLICK,StopLoss_money_b,SLMoneyClick)
   ON_EVENT(ON_CLICK,TakeProfit_money_b,TPMoneyClick)
   ON_EVENT(ON_CLICK,Risk_percent_b,RiskPercentClick)
   ON_EVENT(ON_CLICK,Risk_money_b,RiskMoneyClick)
EVENT_MAP_END(CAppDialog)

En sí, la función de procesamiento del evento tendrá el aspecto siguiente:

//+------------------------------------------------------------------+
//| Click Stop Loss in pips                                          |
//+------------------------------------------------------------------+
void CTradePanel::SLPipsClick(void)
  {
   StopLoss_pips_b.Pressed(cur_sl_pips>0);
   StopLoss_money_b.Pressed(false);
   Risk_money_b.Pressed(false);
   Risk_percent_b.Pressed(false);
   return;
  }

 Es posible familiarizarse con todas las funciones de procesamiento de eventos en el código adjunto.

4.4. Pulsando los botones de cambio de volumen de la operación.

A diferencia de los botones de opción, al pulsar los botones de cambio de volumen de la operación, el programa deberá ejecutar una serie de operaciones que deberemos registrar en el código. Ante todo, se trata del aumento o la disminución del valor de la variable cur_lot por la magnitud del salto del cambio de volumen de la operación. Después hay que comparar el valor obtenido con el valor máximo y mínimo posible para el instrumento. Como opción adicional, propondría además comprobar la presencia de recursos libres para la apertura de una orden de tal volumen, puesto que como consecuencia, al abrir el tráder un orden, los recursos en la cuenta podrían resultar insuficientes. A continuación, deberemos mostrar los nuevos valores del volumen de la operación en el panel y editar los valores correspondientes, como en el caso de la introducción manual del valor del volumen de la operación en el campo editable.

Asimismo, como se ha hecho anteriormente, declaramos nuestras funciones en el bloque private:

private:
................
   //--- On Event functions
................
   void              IncreaseLotClick();                             // Click Increase Lot
   void              DecreaseLotClick();                             // Click Decrease Lot

Completamos con macro sustituciones la función de procesamiento de interrupciones:

//+------------------------------------------------------------------+
//| Event Handling                                                   |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CTradePanel)
   ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit)
   ON_EVENT(ON_CLICK,StopLoss_pips_b,SLPipsClick)
   ON_EVENT(ON_CLICK,TakeProfit_pips_b,TPPipsClick)
   ON_EVENT(ON_CLICK,StopLoss_money_b,SLMoneyClick)
   ON_EVENT(ON_CLICK,TakeProfit_money_b,TPMoneyClick)
   ON_EVENT(ON_CLICK,Risk_percent_b,RiskPercentClick)
   ON_EVENT(ON_CLICK,Risk_money_b,RiskMoneyClick)
   ON_EVENT(ON_CLICK,Increase,IncreaseLotClick)
   ON_EVENT(ON_CLICK,Decrease,DecreaseLotClick)
EVENT_MAP_END(CAppDialog)

Veamos la función de procesamiento del evento:

//+------------------------------------------------------------------+
//|  Increase Lot Click                                              |
//+------------------------------------------------------------------+
void CTradePanel::IncreaseLotClick(void)
  {
   //--- Read and normalize lot value
   cur_lot=NormalizeLots(StringToDouble(Lots.Text())+SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP));
   //--- Output lot value to panel
   Lots.Text(DoubleToString(cur_lot,2));
   //--- Call end edit lot function
   LotsEndEdit();
   return;
  }

En primer lugar, leemos el valor actual de volumen de la operación y lo aumentamos en el salto de la especificación del instrumento. Después adecuamos el valor obtenido a la especificación del instrumento con la función NormalizeLots, que ya hemos visto antes.

A continuación llamamos a la función de procesamiento del cambio del volumen del lote en la ventana de introducción, puesto que con esta función ya registramos con anterioridad todos los procedimientos necesarios.

Construimos de forma análoga la función de reducción de lote.

4.5. Cambiando el estado de las casillas de verificación.

En la siguiente etapa construimos el manejador de eventos que reaccionan a la pulsación de las casillas de verificación. En nuestro panel hay dos casillas de verificación para activar o desactivar la muestra de los niveles de stop-loss y take-profit en el gráfico.

¿Qué deberá suceder al cambiar el estado de la casilla de verificación? En principio, la función principal de este evento debe ser la representación de la líneas en el gráfico. Esta tarea se puede resolver mediante dos métodos:

  1. crear o elminar líneas con cada pulsación;
  2. crear una vez las líneas en el gráfico junto con todos los objetos del panel, y al cambiar el estado de las casillas de verificación, representarlas u ocultarlas.

Yo he preferido la segunda opción. Con este objetivo, conectaremos otra biblioteca mas:

#include <ChartObjects\ChartObjectsLines.mqh>

Después, en el bloque private declararemos los objetos de las líneas horizontales y declararemos la función de su inicialización:

private:
.................
   CChartObjectHLine BuySL, SellSL, BuyTP, SellTP;    // Stop Loss and Take Profit Lines
   
   //--- Create Horizontal line
   bool              CreateHLine(long chart, int subwindow,CChartObjectHLine &object,color clr, string comment);

Escribimos el procedimiento de inicialización de las líneas horizontales. Al principio creamos una línea en el gráfico.

//+------------------------------------------------------------------+
//| Create horizontal line                                           |
//+------------------------------------------------------------------+
bool CTradePanel::CreateHLine(long chart, int subwindow,CChartObjectHLine &object,color clr, string comment)
  {
   // All objects must to have separate name
   string name="HLine"+(string)ObjectsTotal(chart,-1,OBJ_HLINE);
   //--- Create horizontal line
   if(!object.Create(chart,name,subwindow,0))
      return false;

Después establecemos el color, el tipo de línea y añadimos el comentario, que se representará al poner el cursor encima del objeto.

   //--- Set color of line
   if(!object.Color(clr))
      return false;
   //--- Set dash style to line
   if(!object.Style(STYLE_DASH))
      return false;
   //--- Add comment to line
   if(!object.Tooltip(comment))
      return false;

Ocultamos la línea del gráfico y pasamos al fondo la representación de la línea.

   //--- Hide line 
   if(!object.Timeframes(OBJ_NO_PERIODS))
      return false;
   //--- Move line to background
   if(!object.Background(true))
      return false;

Puesto que una de las opciones de nuestro panel es dar la posibilidad al tráder de desplazar las líneas de los niveles de stop-loss y take-profit en el gráfico, proporcionaremos al usuario la posibilidad de destacar las líneas:

   if(!object.Selectable(true))
      return false;
   return true;
  }

Ahora añadimos la inicialización de las líneas a la función de creación de nuestro panel comercial.

//+------------------------------------------------------------------+
//| Creat Trade Panel function                                       |
//+------------------------------------------------------------------+
bool CTradePanel::Create(const long chart,const string name,const int subwin=0,const int x1=20,const int y1=20,const int x2=320,const int y2=420)
  {
...................
...................
   //--- Create horizontal lines of SL & TP
   if(!CreateHLine(chart,subwin,BuySL,SL_Line_color,"Buy Stop Loss"))
     {
      return false;
     }
   if(!CreateHLine(chart,subwin,SellSL,SL_Line_color,"Sell Stop Loss"))
     {
      return false;
     }
   if(!CreateHLine(chart,subwin,BuyTP,TP_Line_color,"Buy Take Profit"))
     {
      return false;
     }
   if(!CreateHLine(chart,subwin,SellTP,TP_Line_color,"Sell Take Profit"))
     {
      return false;
     }
    return true;
  }

Después de que hayamos creado las líneas, escribimos propiamente la función de procesamiento del evento. La función de procesamiento del evento la construiremos conforme al mismo esquema que las funciones de procesamiento de los eventos precedentes. Declaramos la función de procesamiento de eventos en el bloque private:

private:
...............
   void              StopLossLineClick();                            // Click StopLoss Line 
   void              TakeProfitLineClick();                          // Click TakeProfit Line

Añadimos la llamada de la función al manejador de eventos:

//+------------------------------------------------------------------+
//| Event Handling                                                   |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CTradePanel)
   ON_EVENT(ON_END_EDIT,Lots,LotsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_pips,SLPipsEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_pips,TPPipsEndEdit)
   ON_EVENT(ON_END_EDIT,StopLoss_money,SLMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,TakeProfit_money,TPMoneyEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_percent,RiskPercentEndEdit)
   ON_EVENT(ON_END_EDIT,Risk_money,RiskMoneyEndEdit)
   ON_EVENT(ON_CLICK,StopLoss_pips_b,SLPipsClick)
   ON_EVENT(ON_CLICK,TakeProfit_pips_b,TPPipsClick)
   ON_EVENT(ON_CLICK,StopLoss_money_b,SLMoneyClick)
   ON_EVENT(ON_CLICK,TakeProfit_money_b,TPMoneyClick)
   ON_EVENT(ON_CLICK,Risk_percent_b,RiskPercentClick)
   ON_EVENT(ON_CLICK,Risk_money_b,RiskMoneyClick)
   ON_EVENT(ON_CLICK,Increase,IncreaseLotClick)
   ON_EVENT(ON_CLICK,Decrease,DecreaseLotClick)
   ON_EVENT(ON_CLICK,StopLoss_line,StopLossLineClick)
   ON_EVENT(ON_CLICK,TakeProfit_line,TakeProfitLineClick)
EVENT_MAP_END(CAppDialog)

Y al fin, escribimos la propia función de procesamiento del evento. Al principio de la función comprobamos el estado de la casilla de verificación. Las acciones posteriores dependen del estado de la casilla. Si está pulsada, entonces, antes de la representación de las líneas deberemos actualizar los niveles de representación. Después mostraremos las líneas en el gráfico.

//+------------------------------------------------------------------+
//| Show and Hide Stop Loss Lines                                    |
//+------------------------------------------------------------------+
void CTradePanel::StopLossLineClick()
  {
   if(StopLoss_line.Pressed()) // Button pressed
     {
      if(BuySL.Price(0)<=0)
        {
         UpdateSLLines();
        }
      BuySL.Timeframes(OBJ_ALL_PERIODS);
      SellSL.Timeframes(OBJ_ALL_PERIODS);
     }

Si la casilla de verificación no está pulsada, entonces las líneas se ocultan.

   else                         // Button unpressed
     {
      BuySL.Timeframes(OBJ_NO_PERIODS);
      SellSL.Timeframes(OBJ_NO_PERIODS);
     }
   ChartRedraw();
   return;
  }

Al final de la función llamamos al redibujado del gráfico.

4.6. Operaciones comerciales

Ahora que las funciones del manejador de eventos para los principales elementos de control del panel han sido descritas, procederemos a procesar los eventos de pulsación del botón de operaciones comerciales. Para realizar las operaciones comerciales en la cuenta usaremos también la biblioteca estándar MQL5 "Trade.mqh", en la que se describe la clase de operaciones comerciales CTrade.

#include <Trade\Trade.mqh>

Declaramos la clase de las operaciones comerciales en el bloque private:

private:
................
   CTrade            Trade;                           // Class of trade operations

En la función de inicialización de nuestra clase también realizararemos la inicialización de la clase comercial. Aquí definiremos el número mágico de las operaciones, el nivel de deslizamiento para la ejecución de operaciones comerciales y la política de ejecución órdenes comerciales.

//+------------------------------------------------------------------+
//| Class initialization function                                    |
//+------------------------------------------------------------------+
CTradePanel::CTradePanel(void)
  {
   Trade.SetExpertMagicNumber(0);
   Trade.SetDeviationInPoints(5);
   Trade.SetTypeFilling((ENUM_ORDER_TYPE_FILLING)0);
   return;
  }

Si lo desea, puede añadir aquí funciones adicionales para establecer el número mágico y el nivel de deslizamiento desde un programa externo. No olvide que estas funciones se deberán declarar en el bloque public.

Después de realizar el trabajo preparatorio, escribiremos la función de procesamiento de eventos de pulsación de botón. Al principio, como en la vez anterior, declaramos la función en el bloque private:

private:
.....................
   void              BuyClick();                                     // Click BUY button
   void              SellClick();                                    // Click SELL button
   void              CloseBuyClick();                                // Click CLOSE BUY button
   void              CloseSellClick();                               // Click CLOSE SELL button
   void              CloseClick();                                   // Click CLOSE ALL button

Después completamos el operador del procesamiento de eventos con nuevas funciones:

//+------------------------------------------------------------------+
//| Event Handling                                                   |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CTradePanel)
...................
   ON_EVENT(ON_CLICK,BUY,BuyClick)
   ON_EVENT(ON_CLICK,SELL,SellClick)
   ON_EVENT(ON_CLICK,CloseBuy,CloseBuyClick)
   ON_EVENT(ON_CLICK,CloseSell,CloseSellClick)
   ON_EVENT(ON_CLICK,CloseAll,CloseClick)
EVENT_MAP_END(CAppDialog)

Y por supuesto, escribiremos propiamente las funciones del procesamiento de eventos. Veamos por ejemplo la función de compra. ¿Qué acciones deberá cumplir nuestro programa al pulsar el botón "BUY"?

En primer lugar, seguramente deberemos actualizar el volumen de la operación próxima. Leemos el valor en el campo del lote, lo adaptamos de acuerdo con las especificaciones del instrumento y comprobamos que los recursos para abrir una orden sean suficientes, después de lo cual, devolvemos el valor actualizado al panel.

void CTradePanel::BuyClick(void)
  {
   cur_lot=NormalizeLots(StringToDouble(Lots.Text()));
   Lots.Text(DoubleToString(cur_lot,2));

El siguiente paso será obtener el precio de mercado del instrumento y calcular los niveles de precio del stop-loss y el take-profit, de acuerdo con los parámetros establecidos en el panel:

   double price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   double SL=(cur_sl_pips>0 ? NormalizeDouble(price-cur_sl_pips*_Point,_Digits) : 0);
   double TP=(cur_tp_pips>0 ? NormalizeDouble(price+cur_tp_pips*_Point,_Digits) : 0);

Y al final enviamos al servidor del bróker una solicitud para colocar la orden. Asimismo, si aparece un error, hay que añadir aquí una función que informe al tráder con un mensaje sobre dicho error.

   if(!Trade.Buy(NormalizeLots(cur_lot),_Symbol,price,SL,TP,"Trade Panel"))
      MessageBox("Error of open BUY ORDER "+Trade.ResultComment(),"Trade Panel Error",MB_ICONERROR|MB_OK);;
   return;
  }

De forma análoga, construimos la función de procesamiento de las pulsaciones de los restantes botones comerciales. Podrá familiarizarse con mayor detalle con el código de estas funciones en el archivo adjunto.

5. Desplazamiento "manual" de los niveles de stop-loss y take-profit.

Recordemos que los tráders con bastante frecuencia desplazan los niveles de stop-loss y take-profit a ciertos niveles significativos en el gráfico. A mi parecer, sería incorrecto obligar al usuario a calcular la cantidad de puntos del precio actual hasta ese nivel. Por eso proporcionaremos al usuario la posibilidad de desplazar simplemente la línea al punto necesario, del resto se encargará el programa.

Para mí mismo, he decidido no sobrecargar el programa con el código de procesamiento de los movimientos del ratón en el gráfico, y he usado la función estándar del terminal para desplazar objetos. Con este objetivo, hemos dejado al usuario la posibilidad de seleccionar y desplazar las líneas horizontales. Procesaremos el evento "CHARTEVENT_OBJECT_DRAG" con el propio programa.

Como siempre, primero declaramos la función de procesamiento de evento en el bloque public, puesto que esta función la llamaremos desde un programa externo:

public:
................
   virtual bool      DragLine(string name);
La llamada de esta función la realizaremos desde la función OnChartEvent del programa principal, cuando se dé el evento con la transmisión de la denominación del objeto.
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id==CHARTEVENT_OBJECT_DRAG)
     {
      if(TradePanel.DragLine(sparam))
        {
         ChartRedraw();
        }
     }
...........

En la propia función de procesamiento de evento deberemos:

  • determinar qué línea precisamente ha sido desplazada (stop-loss o take-profit);
  • calcular el valor del índice en puntos;
  • mostrar el valor resultante en la celda correspondiente del panel;
  • recalcular el valor de todos los índices relacionados en el panel;
  • en caso necesario, cambiar el valor de los botones de opción.

Los primeros tres puntos los realizaremos en la función de procesamiento del evento, y los últimos puntos se ejecutarán llamando a la función de edición "manual" del campo correspondiente. Y por supuesto, después de procesar el evento, quitaremos la selección de las líneas.

//+------------------------------------------------------------------+
//| Function of moving horizontal lines                              |
//+------------------------------------------------------------------+
bool CTradePanel::DragLine(string name)
  {
   if(name==BuySL.Name())
     {
      StopLoss_pips.Text(DoubleToString(MathAbs(BuySL.Price(0)-SymbolInfoDouble(_Symbol,SYMBOL_ASK))/_Point,0));
      SLPipsEndEdit();
      BuySL.Selected(false);
      return true;
     }
   if(name==SellSL.Name())
     {
      StopLoss_pips.Text(DoubleToString(MathAbs(SellSL.Price(0)-SymbolInfoDouble(_Symbol,SYMBOL_BID))/_Point,0));
      SLPipsEndEdit();
      SellSL.Selected(false);
      return true;
     }
   if(name==BuyTP.Name())
     {
      TakeProfit_pips.Text(DoubleToString(MathAbs(BuyTP.Price(0)-SymbolInfoDouble(_Symbol,SYMBOL_ASK))/_Point,0));
      TPPipsEndEdit();
      BuyTP.Selected(false);
      return true;
     }
   if(name==SellTP.Name())
     {
      TakeProfit_pips.Text(DoubleToString(MathAbs(SellTP.Price(0)-SymbolInfoDouble(_Symbol,SYMBOL_BID))/_Point,0));
      TPPipsEndEdit();
      SellTP.Selected(false);
      return true;
     }
   return false;
  }

6. Cómo guardar los parámetros actuales al reiniciar

Recordemos que al reiniciar el programa, el usuario seguramente no querrá introducir todos los valores en el panel de nuevo. Además, a veces molesta a alguna gente estar siempre tirando del panel a la zona del gráfico más cómoda para el usuario. Es posible que muchos aceptaran esto si fuese una tarea que se realiza solo al reiniciar el terminal. Pero no olvidemos que el reinicio del programa sucede también simplemente con cambiar el marco temporal del gráfico. Y esto tiene lugar con bastante más frecuencia. Además, muchos sistemas comerciales necesitan estudiar gráficos en varios marcos temporales. Por eso, es imprescindible que guardemos el estado de los botones de opción y las casillas de verificación, así como los valores de todos los campos introducidos por el usuario de forma manual. Y por supuesto, el panel deberá recordar el estado y la posición de las ventanas.

En lo que respecta a la última acción, ya ha sido implementada en la clase base. Solo nos queda implementar la lectura de la información guardada al iniciar el programa.

En lo que respecta a los campos editables y al estado de los botones, tendremos que trabajar un poco. Quiero decir que gran parte del trabajo ya ha sido realizada por los desarrolladores, cosa que les agradezco mucho.

No voy a hablar en profundidad de la herencia de clases, pero sí diré que, a partir del primer progenitor de la clase CObject, todas las clases derivadas tienen las funciones Save y Load. Y nuestra clase CTradePanel ha heredado de su clase padre la llamada a la función de guardado de todos los objetos activados al desinicializar la clase. Sin embargo, aquí nos espera una sorpresa desagradable: las clases CEdit y CBmpButton han heredado funciones "vacías":

   //--- methods for working with files
   virtual bool      Save(const int file_handle)                         { return(true);   }
   virtual bool      Load(const int file_handle)                         { return(true);   }
Por consiguiente, necesitaremos escribir estas funciones para los objetos cuyos datos queremos guardar. Con este objetivo vamos a crear dos clases nuevas: CEdit_new y CBmpButton_new, que serán herederas de las clases CEdit y CBmpButton, respectivamente. En ellas escribiremos las funciones de guardado y lectura de datos.
class CEdit_new : public CEdit
  {
public:
                     CEdit_new(void){};
                    ~CEdit_new(void){};
   virtual bool      Save(const int file_handle)
     {
      if(file_handle==INVALID_HANDLE)
        {
         return false;
        }
      string text=Text();
      FileWriteInteger(file_handle,StringLen(text));
      return(FileWriteString(file_handle,text)>0); 
     }
   virtual bool      Load(const int file_handle)
     {
      if(file_handle==INVALID_HANDLE)
        {
         return false;
        }
      int size=FileReadInteger(file_handle);
      string text=FileReadString(file_handle,size);
      return(Text(text));
     }
   
  };

class CBmpButton_new : public CBmpButton
  {
public:
                     CBmpButton_new(void){};
                    ~CBmpButton_new(void){};
   virtual bool      Save(const int file_handle)
    {
     if(file_handle==INVALID_HANDLE)
        {
         return false;
        }
      return(FileWriteInteger(file_handle,Pressed()));
     }
   virtual bool      Load(const int file_handle)
     {
      if(file_handle==INVALID_HANDLE)
        {
         return false;
        }
      return(Pressed((bool)FileReadInteger(file_handle)));
     }
  };

Y por supuesto, cambiamos los tipos de los objetos guardados por nuevos.

   CEdit_new         Lots;                            // Display volume of next order
   CEdit_new         StopLoss_pips;                   // Display Stop loss in pips
   CEdit_new         StopLoss_money;                  // Display Stop loss in accaunt currency
   CEdit_new         TakeProfit_pips;                 // Display Take profit in pips
   CEdit_new         TakeProfit_money;                // Display Take profit in account currency
   CEdit_new         Risk_percent;                    // Display Risk percent to equity
   CEdit_new         Risk_money;                      // Display Risk in account currency
   CBmpButton_new    StopLoss_line;                   // Check to display StopLoss Line
   CBmpButton_new    TakeProfit_line;                 // Check to display TakeProfit Line
   CBmpButton_new    StopLoss_pips_b;                 // Select Stop loss in pips
   CBmpButton_new    StopLoss_money_b;                // Select Stop loss in accaunt currency
   CBmpButton_new    TakeProfit_pips_b;               // Select Take profit in pips
   CBmpButton_new    TakeProfit_money_b;              // Select Take profit in account currency
   CBmpButton_new    Risk_percent_b;                  // Select Risk percent to equity
   CBmpButton_new    Risk_money_b;                    // Select Risk in account currency

Pero guardar la información no es suficiente, también hay que leerla. Para ello, reescribiremos la función de inicio de nuestro panel comercial:

public:
.................
   virtual bool      Run(void);

Primero leemos los datos guardados:

//+------------------------------------------------------------------+
//| Run of Trade Panel                                               |
//+------------------------------------------------------------------+
bool CTradePanel::Run(void)
  {
   IniFileLoad();

Después actualizamos las variables de los valores:

   cur_lot=StringToDouble(Lots.Text());
   cur_sl_pips=(int)StringToInteger(StopLoss_pips.Text());     // Stop Loss in pips
   cur_sl_money=StringToDouble(StopLoss_money.Text());         // Stop Loss in money
   cur_tp_pips=(int)StringToInteger(TakeProfit_pips.Text());   // Take Profit in pips
   cur_tp_money=StringToDouble(TakeProfit_money.Text());       // Take Profit in money
   cur_risk_percent=StringToDouble(Risk_percent.Text());       // Risk in percent
   cur_risk_money=StringToDouble(Risk_money.Text());           // Risk in money
   RiskByValue=true;
Y al fin, llamamos a las funciones de procesamiento de las pulsaciones de las casillas de verificación, que actualizan el estado de los niveles de los stop-loss y take-profit:
   StopLossLineClick();
   TakeProfitLineClick();
   return(CAppDialog::Run());
  }

7. "Limpieza general"

Hemos hecho un trabajo muy grande, y esperamos que el usuario quede satisfecho. Sin embargo, llega el momento en el que el usuario, por el motivo que sea, desconecta nuestro programa. Y después de salir, debemos dejarlo todo limpio: eliminar del gráfico todos los objetos que hemos creado, pero dejar los objetos creados por el usuario o terceros programas.

Al desinicializar el programa, se genera el evento Deinit, que llama a la función OnDeinit con la indicación del motivo de la desinicialización. Por consiguiente, desde la función indicada del programa principal, deberemos llamar a la función de desinicialización de nuestra clase:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   TradePanel.Destroy(reason);
   return;
  }

Deberemos declarar esta función en el bloque public de nuestra clase:

public:
.............
   virtual void      Destroy(const int reason);
En el cuerpo de esta función eliminaremos las líneas horizontales del gráfico y llamaremos a la función de desinicialización de la clase base, que guarda toda la información necesaria y eliminará del gráfico los objetos del panel comercial.
//+------------------------------------------------------------------+
//| Application deinitialization function                            |
//+------------------------------------------------------------------+
void CTradePanel::Destroy(const int reason)
  {
   BuySL.Delete();
   SellSL.Delete();
   BuyTP.Delete();
   SellTP.Delete();
   CAppDialog::Destroy(reason);
   return;
  }

Conclusión

¡Querido lectores, colegas y amigos!

Espero sinceramente que hayáis leído el artículo hasta el final, me gustaría mucho que os fuese de utilidad.

He intentado hablar con un lenguaje accesible sobre la experiencia que ha supuesto crear paneles comerciales y proporcionaros un instrumento preparado para el trabajo en el mercado.

Os ruego que me mandéis ruegos y sugerencias sobre lo que os gustaría ver en vuestro panel. Por mi parte, os prometo plasmar otras ideas interesantes y hablar sobre ello en futuros artículos.

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/2281

Archivos adjuntos |
tradepanel.ex5 (319.32 KB)
tradepanel.mq5 (56.85 KB)
¿Cómo copiar señales con la ayuda de un asesor según sus propias normas? ¿Cómo copiar señales con la ayuda de un asesor según sus propias normas?
Al suscribirse a una señal puede darse la situación siguiente: su cuenta comercial tiene un apalancamiento de 1:100, el proveedor tiene un apalancamiento de 1:500 y comercia con un lote mínimo, y sus balances comerciales son prácticamente iguales, además, el coeficiente de copiado es del 10% al 15%. En este artículo hablaremos de cómo aumentar el coeficiente de copiado en ese caso.
Hablando de nuevo sobre los mapas de Kohonen Hablando de nuevo sobre los mapas de Kohonen
El artículo describe los métodos de funcionamiento de los mapas de Kohonen. Le resultará interesante tanto a los investigadores del mercado con habilidades básicas de programación en MQL4 y MQL5, como a los programadores expertos que sufren dificultades con la aplicación de los mapas de Kohonen en sus proyectos.
Por dónde comenzar a crear un robot comercial para la Bolsa de Moscú MOEX Por dónde comenzar a crear un robot comercial para la Bolsa de Moscú MOEX
Muchos tráders de la Bolsa de Moscú querrían automatizar sus algoritmos comerciales, pero no saben por dónde empezar. El lenguaje MQL5 propone no solo un conjunto enorme de funciones comerciales, sino también clases preparadas, que facilitan al máximo los primeros pasos en el trading automático.
Cómo crear un bot para Telegram en el lenguaje MQL5 Cómo crear un bot para Telegram en el lenguaje MQL5
Este artículo es una guía paso a paso para crear un bot para Telegram en el lenguaje MQL5 El material será de interés para aquellos que quieren vincular un bot comercial a su dispositivo móvil. En el artículo se dan ejemplos de bots que envían señales comerciales, buscan información en páginas web y mandan información sobre el estado de la cuenta comercial, cotizaciones y capturas de pantalla de gráficos a su teléfono inteligente.