English Русский 中文 Deutsch 日本語 Português
preview
Cómo ver las transacciones directamente en el gráfico sin tener que perderse en el historial de transacciones

Cómo ver las transacciones directamente en el gráfico sin tener que perderse en el historial de transacciones

MetaTrader 5Ejemplos | 12 diciembre 2024, 12:26
290 0
Artyom Trishkin
Artyom Trishkin

Contenido


Introducción

Los traders modernos a menudo se enfrentan al problema de analizar grandes cantidades de datos relacionados con las operaciones comerciales. En la terminal de cliente MetaTrader 5, la visualización del historial de operaciones puede resultar ineficaz y confusa debido a que el gráfico está sobrecargado con etiquetas de posiciones abiertas y cerradas. Esto es especialmente cierto para los traders que trabajan con una gran cantidad de operaciones, donde el gráfico se llena rápidamente, lo que hace casi imposible analizar la actividad comercial y tomar decisiones informadas.

El propósito de este artículo es ofrecer una solución que facilite la percepción y el análisis del historial comercial. Desarrollaremos un mecanismo para visualizar paso a paso las posiciones cerradas y mejorar la información de las transacciones. Esto permitirá a los comerciantes centrarse en cada operación individual y obtener una comprensión más profunda de sus operaciones comerciales.

Implementaremos las funciones que nos permitirán:

  • Ver las posiciones cerradas una por una utilizando las teclas de navegación.
  • Mejorar la información sobre herramientas proporcionando información más detallada sobre cada oferta.
  • Centrarse en el gráfico para que los elementos más importantes estén siempre visibles.

Además, se examinará en detalle la estructura de datos necesaria para implementar esta funcionalidad y se propondrán los principios básicos de contabilidad de transacciones y posiciones en MetaTrader 5. Como resultado, los comerciantes podrán administrar su historial comercial de manera más efectiva y tomar decisiones informadas basadas en la información que reciben.

En la terminal cliente MetaTrader 5, podemos visualizar el historial de operaciones marcando la opción "Mostrar historial de operaciones" en la pestaña Mostrar de la configuración del gráfico (tecla F8):


Al marcar la opción, permitimos que el terminal muestre todo el historial comercial en el gráfico en forma de etiquetas para posiciones de apertura/cierre conectadas por líneas. En este caso, se muestran íntegramente todas las transacciones realizadas en el símbolo:


Si hay muchas ofertas, el gráfico se llena de etiquetas, lo que dificulta ver algo en él. Las descripciones emergentes que aparecen al pasar el cursor sobre la etiqueta de una operación o una línea que conecta operaciones no son particularmente informativas:


La solución presentada aquí mostrará solo la última posición cerrada cuando se lance. El movimiento a través de las posiciones restantes se realizará pulsando las teclas del cursor:

  • la tecla arriba mostrará la primera posición cerrada;
  • la tecla hacia abajo mostrará la última posición cerrada;
  • la tecla izquierda mostrará la posición cerrada anterior;
  • la tecla de flecha derecha mostrará la siguiente posición cerrada.

Las descripciones emergentes de ofertas y líneas de conexión sirven para mostrar información más útil. Además, al mantener presionada la tecla Shift (mayúsculas), se mostrará en el gráfico información sobre la posición cerrada seleccionada actualmente:


Al mostrar cada posición cerrada subsiguiente, centraremos el gráfico de modo que las etiquetas de apertura/cierre de la posición y la línea que las conecta estén ubicadas en el centro.

En caso de que las etiquetas de apertura y cierre no quepan dentro de una pantalla de gráfico, centre el gráfico de modo que la etiqueta de apertura esté ubicada en la segunda barra visible a la izquierda del gráfico.

Veamos los principios básicos de contabilidad de transacciones y posiciones, así como la estructura de las clases que vamos a crear aquí.

¿Cómo se forma una posición? En primer lugar, se envía una solicitud comercial al servidor: una orden (orden comercial). La orden puede ser cancelada o ejecutada. Cuando se ejecuta una orden, recibimos un trato: el hecho de que la solicitud comercial se ha ejecutado. Si no había posiciones en el momento en que se ejecutó la orden, la operación crea una posición en una dirección determinada. Si hubiera una posición, entonces hay varias opciones, dependiendo del tipo de contabilidad de la posición. En el caso de las cuentas de compensación (netting), sólo es posible una posición en el símbolo. En consecuencia, la operación generada por la orden comercial modifica una posición existente. Puede serlo:

  1. Por cierre: Si se ha realizado una venta para una posición larga con un volumen igual al volumen de la posición:
    Posición 1.0 Compra - Operación 1.0 Venta = volumen 0 (cerrar una posición);

  2. Cierre parcial: Si se ha realizado una venta de una posición larga con un volumen inferior al volumen de la posición: Posición 1.0 Compra - Comercio 0.5 Venta = volumen 0.5 (cierre parcial de posición);

  3. Añadió volumen: Si se realizó una compra para una posición larga:
    Posición 1.0 Comprar + Comercio 0.5 Comprar = volumen 1.5
    (aumento del volumen de posiciones);

  4. Reversión: Si se ha realizado una venta de una posición larga con un volumen superior al volumen de la posición: Posición 1.0 Compra - Operación 1.5 Venta = Posición 0.5 Venta (inversión de posición);

En el caso de un tipo de cuenta de compensación (netting), cada operación puede modificar una posición existente o generar una nueva. Para modificar una posición existente (cierre o cierre parcial), debemos especificar la ID de la posición con la que queremos realizar una operación en la orden de negociación. La ID de la posición es un número único asignado a cada posición nueva que permanece invariable durante toda la vida de la posición:

  1. Abrir una nueva posición: Realizar una compra o una venta mientras existe otra posición:
    Posición 1.0 Compra + Comercio 1.0 Venta = 2 posiciones independientes 1.0 Compra y 1.0 Venta
    (abrir una posición), o
    Posición 1.0 Compra + Comercio 1.5 Compra = 2 posiciones independientes 1.0 Compra y 1.5 Compra(apertura de una posición), etc.;

  2. Cerrar una posición existente: Si se realiza una venta con la ID de una posición larga ya existente:
    Posición 1.0 Compra con ID 123 - Operación 1.0 Venta con ID 123 = cierre 1.0 Compra con ID 123 (cerrando una posición con ID 123);

  3. Cierre parcial de una posición existente: Si se realiza una venta con la ID de una posición larga ya existente con un volumen inferior al de la posición existente con la ID especificada:
    Posición 1.0 Comprar con ID 123 - Comercio 0.5 Vender con ID 123 = volumen 0.5 Comprar con ID 123 (cierre parcial de una posición con ID 123);

Obtenga más información sobre pedidos, transacciones y posiciones en el artículo "Órdenes, posiciones y transacciones en MetaTrader 5".

Cada orden enviada al servidor comercial permanece en la lista de órdenes activas en la terminal hasta que se ejecuta. Cuando se activa, aparece una operación, generando una nueva posición o modificando una existente. En el momento en que se ejecuta la orden, se coloca en la lista de órdenes históricas y se crea una operación, que también se coloca en la lista de operaciones históricas. Si una transacción crea una posición, se crea una nueva posición y se coloca en la lista de posiciones activas.

No hay una lista de posiciones históricas (cerradas) en la terminal. Además, no hay una lista de transacciones activas en la terminal: se consideran inmediatamente históricas y se ubican en la lista correspondiente.
La explicación es sencilla: una orden es una instrucción para realizar operaciones comerciales en una cuenta. Existe hasta que se cumple. Se encuentra en la lista de pedidos existentes. Una vez que se ejecuta una orden, deja de existir (idealmente) y se coloca en la lista de órdenes comerciales ejecutadas. Al hacerlo, se crea un trato (deal). Un trato es el hecho de ejecutar una orden comercial. Este es un evento único: se ejecuta una orden: tenemos un trato. Eso es todo. Se agrega el trato (deal) a la lista de eventos de cuenta completados.
Un trato crea una posición que existe hasta que se cierra. La posición siempre se encuentra en la lista de posiciones activas. Una vez cerrado se elimina de la lista.

No existe una lista de posiciones cerradas en la terminal. Pero hay una lista de acuerdos completados. Por lo tanto, para crear una lista de posiciones cerradas, es necesario crearlas a partir de operaciones pertenecientes a una posición previamente cerrada. La lista de ofertas siempre está disponible para nosotros. Las propiedades de cada operación contienen una ID de la posición correspondiente. De esta forma, tenemos acceso a todo lo necesario para recrear el historial de vida de cada una de las posiciones que existían previamente en la cuenta: la ID de la posición y la lista de sus transacciones.

Con base en lo anterior, obtenemos el siguiente algoritmo para recrear posiciones previamente cerradas a partir de la lista histórica de transacciones existentes:

  • Obtenga una lista de todos los tratos (deals) para el símbolo requerido;
  • Recorra la lista y obtenga cada próximo trato de la lista;
  • Verifique la ID de posición en las propiedades del trato;
  • Si no existe ninguna posición con dicha ID en nuestra propia lista de posiciones históricas, crear un nuevo objeto de posición histórica;
  • Obtener el objeto de posición histórica utilizando la ID recibida previamente;
  • Añada la transacción actual a la lista de transacciones de objetos de posición histórica.

Al finalizar el recorrido por la lista de operaciones de la terminal, tendremos una lista completa de todas las posiciones cerradas en la cuenta por símbolo, que contiene listas de operaciones que pertenecen a cada posición.

Ahora, cada posición histórica contará con una lista de transacciones involucradas en el cambio de esta posición, y podremos obtener información completa sobre todos los cambios en una posición cerrada durante su existencia.

Para implementar la lógica descrita anteriormente, necesitamos crear tres clases:

  1. Clase de trato. La clase contendrá la lista de todas las propiedades de los tratos históricos, y los métodos para acceder a las propiedades (configuración y obtención), además de métodos adicionales para mostrar los datos de los tratos;
  2. Clase de posición. La clase contendrá la lista de todas las transacciones, los métodos para acceder a ellas y métodos adicionales para mostrar información sobre la posición y sus transacciones;
  3. Clase de gestión de posición histórica. La clase contendrá la lista de todas las posiciones históricas, su creación y actualización, así como métodos para acceder a las propiedades de las posiciones y sus transacciones, además de métodos adicionales para mostrar información sobre las posiciones y sus transacciones.

Vamos a empezar.


Clase de trato

Para obtener y guardar las propiedades de un trato, necesitamos seleccionar el historial de pedidos y tratos (HistorySelect()), recorrer el historial de tratos en un bucle y obtener un ticket de trato por el índice del bucle (HistoryDealGetTicket()) de la lista de tratos históricos. En este caso, se seleccionará el trato para obtener sus propiedades mediante las funciones HistoryDealGetInteger(), HistoryDealGetDouble() y HistoryDealGetString().

En la clase trato, supondremos que el trato ya ha sido seleccionado y sus propiedades pueden obtenerse inmediatamente. Además de registrar y recuperar las propiedades de la operación, la clase nos permitirá crear una etiqueta gráfica en un gráfico que muestre la operación. La etiqueta se dibujará sobre lienzo, de forma que podamos dibujar la etiqueta deseada para cada tipo de trato en lugar de utilizar etiquetas de un conjunto predefinido de símbolos de fuentes. Al crear un objeto para cada operación, recibiremos un historial de ticks de los precios en el momento en que apareció la operación, lo que nos permitirá calcular el diferencial en el momento en que se realizó la operación y mostrarlo en la descripción de los parámetros de la operación.

Dado que cada posición puede tener muchos tratos que hayan participado en su cambio, todos los tratos se localizarán finalmente en la clase del array dinámico de punteros a las instancias de la clase CObject y sus CArrayObj descendientes.
En consecuencia, la clase deal se heredará de la clase objeto base Standard Library CObject.

Una operación tiene un conjunto de propiedades enteras, reales y de cadena. Cada objeto de la operación estará en la lista de objetos de la clase de posición histórica. Para implementar la búsqueda del trato requerido, será necesario escribir una enumeración de parámetros. Utilizando los valores de esta enumeración, será posible buscar tratos en la lista y ordenar la lista por estos mismos valores. Necesitaremos la búsqueda únicamente por propiedades: ticket de transacción y tiempo en milisegundos. El ticket nos permitirá determinar si dicha transacción ya está presente en la lista o no. El tiempo en milisegundos nos permitirá ordenar las operaciones por orden estrictamente cronológico. Pero como las clases implican extensión, la lista de propiedades del trato contendrá todas sus propiedades.

Para organizar la búsqueda y ordenación en listas CArrayObj de la biblioteca estándar, en la clase CObject se devuelve el método virtual Compare() que devuelve el valor 0:

   //--- method of comparing the objects
   virtual int       Compare(const CObject *node,const int mode=0) const { return(0);      }

En otras palabras, este método siempre devuelve la paridad. En cada uno de los objetos de clases heredadas de CObject, este método debe redefinirse, de forma que al comparar una propiedad (especificada en el parámetro mode) de dos objetos comparados, el método devuelva:

  • -1 - si el valor de la propiedad del objeto actual es menor que el del objeto comparado;
  •  1 - si el valor de la propiedad del objeto actual supera al del objeto comparado;
  •  0 - si los valores de la propiedad especificada son iguales para ambos objetos comparados.

Así, basándonos en las explicaciones anteriores, para cada una de las clases creadas hoy, necesitaremos crear adicionalmente enumeraciones de las propiedades de los objetos de clase y métodos para compararlas.

En la carpeta \MQL5\Include\, cree la carpeta PositionsViewer\ que contiene el nuevo archivo Deal.mqh de la clase CDeal:


La clase CObject de la Biblioteca Estándar debería ser la clase base:


Como resultado, obtendremos la siguiente plantilla para el archivo de clase del objeto deal:

//+------------------------------------------------------------------+
//|                                                         Deal.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
class CDeal : public CObject
  {
private:

public:
                     CDeal();
                    ~CDeal();
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CDeal::CDeal()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CDeal::~CDeal()
  {
  }
//+------------------------------------------------------------------+


Incluir los ficheros de las clases de objetos estándar de la librería CObject y la clase CCanvas al fichero creado de la clase de objetos deal y escribir la enumeración para ordenar por propiedades de objetos deal:

//+------------------------------------------------------------------+
//|                                                         Deal.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include <Object.mqh>
#include <Canvas\Canvas.mqh>

enum ENUM_DEAL_SORT_MODE
  {
   SORT_MODE_DEAL_TICKET = 0,          // Mode of comparing/sorting by a deal ticket
   SORT_MODE_DEAL_ORDER,               // Mode of comparing/sorting by the order a deal is based on
   SORT_MODE_DEAL_TIME,                // Mode of comparing/sorting by a deal time
   SORT_MODE_DEAL_TIME_MSC,            // Mode of comparing/sorting by a deal time in milliseconds
   SORT_MODE_DEAL_TYPE,                // Mode of comparing/sorting by a deal type
   SORT_MODE_DEAL_ENTRY,               // Mode of comparing/sorting by a deal direction
   SORT_MODE_DEAL_MAGIC,               // Mode of comparing/sorting by a deal magic number
   SORT_MODE_DEAL_REASON,              // Mode of comparing/sorting by a deal reason or source
   SORT_MODE_DEAL_POSITION_ID,         // Mode of comparing/sorting by a position ID
   SORT_MODE_DEAL_VOLUME,              // Mode of comparing/sorting by a deal volume
   SORT_MODE_DEAL_PRICE,               // Mode of comparing/sorting by a deal price
   SORT_MODE_DEAL_COMMISSION,          // Mode of comparing/sorting by commission
   SORT_MODE_DEAL_SWAP,                // Mode of comparing/sorting by accumulated swap on close
   SORT_MODE_DEAL_PROFIT,              // Mode of comparing/sorting by a deal financial result
   SORT_MODE_DEAL_FEE,                 // Mode of comparing/sorting by a deal fee
   SORT_MODE_DEAL_SL,                  // Mode of comparing/sorting by Stop Loss level
   SORT_MODE_DEAL_TP,                  // Mode of comparing/sorting by Take Profit level
   SORT_MODE_DEAL_SYMBOL,              // Mode of comparing/sorting by a name of a traded symbol
   SORT_MODE_DEAL_COMMENT,             // Mode of comparing/sorting by a deal comment
   SORT_MODE_DEAL_EXTERNAL_ID,         // Mode of comparing/sorting by a deal ID in an external trading system
  };
//+------------------------------------------------------------------+
//| Deal class                                                       |
//+------------------------------------------------------------------+
class CDeal : public CObject
  {
  }

Declara todas las variables y métodos necesarios para que la clase funcione en las secciones private, protected y public:

//+------------------------------------------------------------------+
//| Deal class                                                       |
//+------------------------------------------------------------------+
class CDeal : public CObject
  {
private:
   MqlTick           m_tick;           // Deal tick structure
//--- CCanvas object
   CCanvas           m_canvas;         // Canvas
   long              m_chart_id;       // Chart ID
   int               m_width;          // Canvas width
   int               m_height;         // Canvas height
   string            m_name;           // Graphical object name
   
//--- Create a label object on the chart
   bool              CreateLabelObj(void);

//--- Draw (1) a mask, (2) Buy arrow on the canvas
   void              DrawArrowMaskBuy(const int shift_x, const int shift_y);
   void              DrawArrowBuy(const int shift_x, const int shift_y);
   
//--- Draw (1) a mask, (2) Sell arrow on the canvas
   void              DrawArrowMaskSell(const int shift_x, const int shift_y);
   void              DrawArrowSell(const int shift_x, const int shift_y);
   
//--- Draw the label appearance
   void              DrawLabelView(void);
   
//--- Get a (1) deal tick and (2) a spread of the deal minute bar
   bool              GetDealTick(const int amount=20);
   int               GetSpreadM1(void);

//--- Return time with milliseconds
   string            TimeMscToString(const long time_msc,int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const;
   
protected:
//--- Integer properties
   long              m_ticket;            // Deal ticket. Unique number assigned to each deal
   long              m_order;             // Deal order number
   datetime          m_time;              // Deal execution time
   long              m_time_msc;          // Deal execution time in milliseconds since 01.01.1970
   ENUM_DEAL_TYPE    m_type;              // Deal type
   ENUM_DEAL_ENTRY   m_entry;             // Deal entry - entry in, entry out, reverse
   long              m_magic;             // Magic number for a deal (see ORDER_MAGIC)
   ENUM_DEAL_REASON  m_reason;            // Deal execution reason or source
   long              m_position_id;       // The ID of the position opened, modified or closed by the deal
   
//--- Real properties
   double            m_volume;            // Deal volume
   double            m_price;             // Deal price
   double            m_commission;        // Deal commission
   double            m_swap;              // Accumulated swap when closing
   double            m_profit;            // Deal financial result
   double            m_fee;               // Fee for making a deal charged immediately after performing a deal
   double            m_sl;                // Stop Loss level
   double            m_tp;                // Take Profit level

//--- String properties
   string            m_symbol;            // Name of the symbol for which the deal is executed
   string            m_comment;           // Deal comment
   string            m_external_id;       // Deal ID in an external trading system (on the exchange)
   
//--- Additional properties
   int               m_digits;            // Symbol digits
   double            m_point;             // Symbol point
   double            m_bid;               // Bid when performing a deal
   double            m_ask;               // Ask when performing a deal
   int               m_spread;            // Spread when performing a deal
   color             m_color_arrow;       // Deal label color
   
//--- Draws an arrow corresponding to the deal type. It can be redefined in the inherited classes
   virtual void      DrawArrow(void);

public:
//--- Set the properties
//--- Integer properties
   void              SetTicket(const long ticket)              { this.m_ticket=ticket;       }  // Ticket
   void              SetOrder(const long order)                { this.m_order=order;         }  // Order
   void              SetTime(const datetime time)              { this.m_time=time;           }  // Time
   void              SetTimeMsc(const long value)              { this.m_time_msc=value;      }  // Time in milliseconds
   void              SetTypeDeal(const ENUM_DEAL_TYPE type)    { this.m_type=type;           }  // Type
   void              SetEntry(const ENUM_DEAL_ENTRY entry)     { this.m_entry=entry;         }  // Direction
   void              SetMagic(const long magic)                { this.m_magic=magic;         }  // Magic number
   void              SetReason(const ENUM_DEAL_REASON reason)  { this.m_reason=reason;       }  // Deal execution reason or source
   void              SetPositionID(const long id)              { this.m_position_id=id;      }  // Position ID
   
//--- Real properties
   void              SetVolume(const double volume)            { this.m_volume=volume;       }  // Volume
   void              SetPrice(const double price)              { this.m_price=price;         }  // Price
   void              SetCommission(const double value)         { this.m_commission=value;    }  // Commission
   void              SetSwap(const double value)               { this.m_swap=value;          }  // Accumulated swap when closing
   void              SetProfit(const double value)             { this.m_profit=value;        }  // Financial result
   void              SetFee(const double value)                { this.m_fee=value;           }  // Deal fee
   void              SetSL(const double value)                 { this.m_sl=value;            }  // Stop Loss level
   void              SetTP(const double value)                 { this.m_tp=value;            }  // Take Profit level

//--- String properties
   void              SetSymbol(const string symbol)            { this.m_symbol=symbol;       }  // Symbol name
   void              SetComment(const string comment)          { this.m_comment=comment;     }  // Comment
   void              SetExternalID(const string ext_id)        { this.m_external_id=ext_id;  }  // Deal ID in an external trading system

//--- Get the properties
//--- Integer properties
   long              Ticket(void)                        const { return(this.m_ticket);      }  // Ticket
   long              Order(void)                         const { return(this.m_order);       }  // Order
   datetime          Time(void)                          const { return(this.m_time);        }  // Time
   long              TimeMsc(void)                       const { return(this.m_time_msc);    }  // Time in milliseconds
   ENUM_DEAL_TYPE    TypeDeal(void)                      const { return(this.m_type);        }  // Type
   ENUM_DEAL_ENTRY   Entry(void)                         const { return(this.m_entry);       }  // Direction
   long              Magic(void)                         const { return(this.m_magic);       }  // Magic number
   ENUM_DEAL_REASON  Reason(void)                        const { return(this.m_reason);      }  // Deal execution reason or source
   long              PositionID(void)                    const { return(this.m_position_id); }  // Position ID
   
//--- Real properties
   double            Volume(void)                        const { return(this.m_volume);      }  // Volume
   double            Price(void)                         const { return(this.m_price);       }  // Price
   double            Commission(void)                    const { return(this.m_commission);  }  // Commission
   double            Swap(void)                          const { return(this.m_swap);        }  // Accumulated swap when closing
   double            Profit(void)                        const { return(this.m_profit);      }  // Financial result
   double            Fee(void)                           const { return(this.m_fee);         }  // Deal fee
   double            SL(void)                            const { return(this.m_sl);          }  // Stop Loss level
   double            TP(void)                            const { return(this.m_tp);          }  // Take Profit level
   
   double            Bid(void)                           const { return(this.m_bid);         }  // Bid when performing a deal
   double            Ask(void)                           const { return(this.m_ask);         }  // Ask when performing a deal
   int               Spread(void)                        const { return(this.m_spread);      }  // Spread when performing a deal
   
//--- String properties
   string            Symbol(void)                        const { return(this.m_symbol);      }  // Symbol name
   string            Comment(void)                       const { return(this.m_comment);     }  // Comment
   string            ExternalID(void)                    const { return(this.m_external_id); }  // Deal ID in an external trading system
   
//--- Set the color of the deal label
   void              SetColorArrow(const color clr);

//--- (1) Hide, (2) display the deal label on a chart
   void              HideArrow(const bool chart_redraw=false);
   void              ShowArrow(const bool chart_redraw=false);

//--- Return the description of a (1) deal type, (2) position change method and (3) deal reason
   string            TypeDescription(void)   const;
   string            EntryDescription(void)  const;
   string            ReasonDescription(void) const;
   
//--- Return (1) a short description and (2) a tooltip text of a deal
   string            Description(void);
   virtual string    Tooltip(void);

//--- Print deal properties in the journal
   void              Print(void);
   
//--- Compare two objects by the property specified in 'mode'
   virtual int       Compare(const CObject *node, const int mode=0) const;
   
//--- Constructors/destructor
                     CDeal(void) { this.m_ticket=0; }
                     CDeal(const ulong ticket);
                    ~CDeal();
  };

Veamos en detalle los métodos declarados.


Constructor paramétrico:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CDeal::CDeal(const ulong ticket)
  {
//--- Store the properties
//--- Integer properties
   this.m_ticket     =  (long)ticket;                                                     // Deal ticket
   this.m_order      =  ::HistoryDealGetInteger(ticket, DEAL_ORDER);                      // Order
   this.m_time       =  (datetime)::HistoryDealGetInteger(ticket, DEAL_TIME);             // Deal execution time
   this.m_time_msc   =  ::HistoryDealGetInteger(ticket, DEAL_TIME_MSC);                   // Deal execution time in milliseconds
   this.m_type       =  (ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE);       // Type
   this.m_entry      =  (ENUM_DEAL_ENTRY)::HistoryDealGetInteger(ticket, DEAL_ENTRY);     // Direction
   this.m_magic      =  ::HistoryDealGetInteger(ticket, DEAL_MAGIC);                      // Magic number
   this.m_reason     =  (ENUM_DEAL_REASON)::HistoryDealGetInteger(ticket, DEAL_REASON);   // Deal execution reason or source
   this.m_position_id=  ::HistoryDealGetInteger(ticket, DEAL_POSITION_ID);                // Position ID
   
//--- Real properties
   this.m_volume     =  ::HistoryDealGetDouble(ticket, DEAL_VOLUME);                      // Volume
   this.m_price      =  ::HistoryDealGetDouble(ticket, DEAL_PRICE);                       // Price
   this.m_commission =  ::HistoryDealGetDouble(ticket, DEAL_COMMISSION);                  // Commission
   this.m_swap       =  ::HistoryDealGetDouble(ticket, DEAL_SWAP);                        // Accumulated swap when closing
   this.m_profit     =  ::HistoryDealGetDouble(ticket, DEAL_PROFIT);                      // Financial result
   this.m_fee        =  ::HistoryDealGetDouble(ticket, DEAL_FEE);                         // Deal fee
   this.m_sl         =  ::HistoryDealGetDouble(ticket, DEAL_SL);                          // Stop Loss level
   this.m_tp         =  ::HistoryDealGetDouble(ticket, DEAL_TP);                          // Take Profit level

//--- String properties
   this.m_symbol     =  ::HistoryDealGetString(ticket, DEAL_SYMBOL);                      // Symbol name
   this.m_comment    =  ::HistoryDealGetString(ticket, DEAL_COMMENT);                     // Comment
   this.m_external_id=  ::HistoryDealGetString(ticket, DEAL_EXTERNAL_ID);                 // Deal ID in an external trading system

//--- Graphics display parameters
   this.m_chart_id   =  ::ChartID();
   this.m_digits     =  (int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS);
   this.m_point      =  ::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT);
   this.m_width      =  19;
   this.m_height     =  19;
   this.m_name       =  "Deal#"+(string)this.m_ticket;
   this.m_color_arrow=  (this.TypeDeal()==DEAL_TYPE_BUY ? C'3,95,172' : this.TypeDeal()==DEAL_TYPE_SELL ? C'225,68,29' : C'180,180,180');
   
//--- Parameters for calculating spread
   this.m_spread     =  0;
   this.m_bid        =  0;
   this.m_ask        =  0;
   
//--- Create a graphic label
   this.CreateLabelObj();
   
//--- If the historical tick and the Point value of the symbol were obtained
   if(this.GetDealTick() && this.m_point!=0)
     {
      //--- set the Bid and Ask price values, calculate and save the spread value
      this.m_bid=this.m_tick.bid;
      this.m_ask=this.m_tick.ask;
      this.m_spread=int((this.m_ask-this.m_bid)/this.m_point);
     }
//--- If failed to obtain a historical tick, take the spread value of the minute bar the deal took place on
   else
      this.m_spread=this.GetSpreadM1();
  }

Tenga en cuenta que la operación actual ya ha sido seleccionada. Por lo tanto, aquí rellenamos inmediatamente todas las propiedades del objeto a partir de las propiedades de la operación. A continuación, configure los parámetros del panel para mostrar la etiqueta de la operación en el gráfico, cree un objeto gráfico de esta etiqueta y obtenga el tick en el momento de la operación para calcular el spread presente en el momento de la operación. Si no se consigue un tick, se toma el spread medio de la barra de minutos en la que tuvo lugar la operación.

Como resultado, al crear un objeto de operación, recibimos inmediatamente un objeto con las propiedades de una operación histórica establecidas en él según su ticket, así como una etiqueta ya creada pero oculta para visualizar la operación en un gráfico, además del valor del spread en el momento de la operación.


En el destructor de la clase, comprueba la razón de desinicialización. Si no se trata de un cambio en el marco temporal del gráfico, se destruye el recurso gráfico del objeto:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CDeal::~CDeal()
  {
   if(::UninitializeReason()!=REASON_CHARTCHANGE)
      this.m_canvas.Destroy();
  }


El método virtual para comparar dos objetos entre sí por una propiedad especifica:

//+------------------------------------------------------------------+
//| Compare two objects by the specified property                    |
//+------------------------------------------------------------------+
int CDeal::Compare(const CObject *node,const int mode=0) const
  {
   const CDeal * obj = node;
   switch(mode)
     {
      case SORT_MODE_DEAL_TICKET       :  return(this.Ticket() > obj.Ticket()          ?  1  :  this.Ticket() < obj.Ticket()           ? -1  :  0);
      case SORT_MODE_DEAL_ORDER        :  return(this.Order() > obj.Order()            ?  1  :  this.Order() < obj.Order()             ? -1  :  0);
      case SORT_MODE_DEAL_TIME         :  return(this.Time() > obj.Time()              ?  1  :  this.Time() < obj.Time()               ? -1  :  0);
      case SORT_MODE_DEAL_TIME_MSC     :  return(this.TimeMsc() > obj.TimeMsc()        ?  1  :  this.TimeMsc() < obj.TimeMsc()         ? -1  :  0);
      case SORT_MODE_DEAL_TYPE         :  return(this.TypeDeal() > obj.TypeDeal()      ?  1  :  this.TypeDeal() < obj.TypeDeal()       ? -1  :  0);
      case SORT_MODE_DEAL_ENTRY        :  return(this.Entry() > obj.Entry()            ?  1  :  this.Entry() < obj.Entry()             ? -1  :  0);
      case SORT_MODE_DEAL_MAGIC        :  return(this.Magic() > obj.Magic()            ?  1  :  this.Magic() < obj.Magic()             ? -1  :  0);
      case SORT_MODE_DEAL_REASON       :  return(this.Reason() > obj.Reason()          ?  1  :  this.Reason() < obj.Reason()           ? -1  :  0);
      case SORT_MODE_DEAL_POSITION_ID  :  return(this.PositionID() > obj.PositionID()  ?  1  :  this.PositionID() < obj.PositionID()   ? -1  :  0);
      case SORT_MODE_DEAL_VOLUME       :  return(this.Volume() > obj.Volume()          ?  1  :  this.Volume() < obj.Volume()           ? -1  :  0);
      case SORT_MODE_DEAL_PRICE        :  return(this.Price() > obj.Price()            ?  1  :  this.Price() < obj.Price()             ? -1  :  0);
      case SORT_MODE_DEAL_COMMISSION   :  return(this.Commission() > obj.Commission()  ?  1  :  this.Commission() < obj.Commission()   ? -1  :  0);
      case SORT_MODE_DEAL_SWAP         :  return(this.Swap() > obj.Swap()              ?  1  :  this.Swap() < obj.Swap()               ? -1  :  0);
      case SORT_MODE_DEAL_PROFIT       :  return(this.Profit() > obj.Profit()          ?  1  :  this.Profit() < obj.Profit()           ? -1  :  0);
      case SORT_MODE_DEAL_FEE          :  return(this.Fee() > obj.Fee()                ?  1  :  this.Fee() < obj.Fee()                 ? -1  :  0);
      case SORT_MODE_DEAL_SL           :  return(this.SL() > obj.SL()                  ?  1  :  this.SL() < obj.SL()                   ? -1  :  0);
      case SORT_MODE_DEAL_TP           :  return(this.TP() > obj.TP()                  ?  1  :  this.TP() < obj.TP()                   ? -1  :  0);
      case SORT_MODE_DEAL_SYMBOL       :  return(this.Symbol() > obj.Symbol()          ?  1  :  this.Symbol() < obj.Symbol()           ? -1  :  0);
      case SORT_MODE_DEAL_COMMENT      :  return(this.Comment() > obj.Comment()        ?  1  :  this.Comment() < obj.Comment()         ? -1  :  0);
      case SORT_MODE_DEAL_EXTERNAL_ID  :  return(this.ExternalID() > obj.ExternalID()  ?  1  :  this.ExternalID() < obj.ExternalID()   ? -1  :  0);
      default                             :  return(-1);
     }
  }

El método recibe el puntero al objeto comparado y el valor de la propiedad comparada de la enumeración ENUM_DEAL_SORT_MODE. Si el valor de la propiedad especificada del objeto actual es mayor que el valor de la propiedad del objeto comparado, se devuelve 1. Si el valor de la propiedad del objeto actual es menor que el valor de los objetos comparados, se devuelve -1. En cualquier otro caso, tenemos cero.


Método que devuelve una descripción del tipo de operación:

//+------------------------------------------------------------------+
//| Return the deal type description                                 |
//+------------------------------------------------------------------+
string CDeal::TypeDescription(void) const
  {
   switch(this.m_type)
     {
      case DEAL_TYPE_BUY                     :  return "Buy";
      case DEAL_TYPE_SELL                    :  return "Sell";
      case DEAL_TYPE_BALANCE                 :  return "Balance";
      case DEAL_TYPE_CREDIT                  :  return "Credit";
      case DEAL_TYPE_CHARGE                  :  return "Additional charge";
      case DEAL_TYPE_CORRECTION              :  return "Correction";
      case DEAL_TYPE_BONUS                   :  return "Bonus";
      case DEAL_TYPE_COMMISSION              :  return "Additional commission";
      case DEAL_TYPE_COMMISSION_DAILY        :  return "Daily commission";
      case DEAL_TYPE_COMMISSION_MONTHLY      :  return "Monthly commission";
      case DEAL_TYPE_COMMISSION_AGENT_DAILY  :  return "Daily agent commission";
      case DEAL_TYPE_COMMISSION_AGENT_MONTHLY:  return "Monthly agent commission";
      case DEAL_TYPE_INTEREST                :  return "Interest rate";
      case DEAL_TYPE_BUY_CANCELED            :  return "Canceled buy deal";
      case DEAL_TYPE_SELL_CANCELED           :  return "Canceled sell deal";
      case DEAL_DIVIDEND                     :  return "Dividend operations";
      case DEAL_DIVIDEND_FRANKED             :  return "Franked (non-taxable) dividend operations";
      case DEAL_TAX                          :  return "Tax charges";
      default                                :  return "Unknown: "+(string)this.m_type;
     }
  }

Aquí se devuelven las descripciones de todos los tipos de operaciones. La mayoría de ellos no se utilizarán en el programa. Pero el método devuelve todos los tipos, teniendo en cuenta las posibilidades de extensión y herencia de las clases.


El método devuelve una descripción del método de cambio de posición:

//+------------------------------------------------------------------+
//| Return position change method                                    |
//+------------------------------------------------------------------+
string CDeal::EntryDescription(void) const
  {
   switch(this.m_entry)
     {
      case DEAL_ENTRY_IN      :  return "Entry In";
      case DEAL_ENTRY_OUT     :  return "Entry Out";
      case DEAL_ENTRY_INOUT   :  return "Reverse";
      case DEAL_ENTRY_OUT_BY  :  return "Close a position by an opposite one";
      default                 :  return "Unknown: "+(string)this.m_entry;
     }
  }


El método devuelve una descripción del motivo (reason) del trato (deal):

//+------------------------------------------------------------------+
//| Return a deal reason description                                 |
//+------------------------------------------------------------------+
string CDeal::ReasonDescription(void) const
  {
   switch(this.m_reason)
     {
      case DEAL_REASON_CLIENT          :  return "Terminal";
      case DEAL_REASON_MOBILE          :  return "Mobile";
      case DEAL_REASON_WEB             :  return "Web";
      case DEAL_REASON_EXPERT          :  return "EA";
      case DEAL_REASON_SL              :  return "SL";
      case DEAL_REASON_TP              :  return "TP";
      case DEAL_REASON_SO              :  return "SO";
      case DEAL_REASON_ROLLOVER        :  return "Rollover";
      case DEAL_REASON_VMARGIN         :  return "Var. Margin";
      case DEAL_REASON_SPLIT           :  return "Split";
      case DEAL_REASON_CORPORATE_ACTION:  return "Corp. Action";
      default                          :  return "Unknown reason "+(string)this.m_reason;
     }
  }

Utilizaremos sólo tres valores del método: cierre por StopLoss, TakeProfit y StopOut.


El método devuelve una descripción del trato (deal):

//+------------------------------------------------------------------+
//| Return deal description                                          |
//+------------------------------------------------------------------+
string CDeal::Description(void)
  {
   return(::StringFormat("Deal: %-9s %.2f %-4s #%I64d at %s", this.EntryDescription(), this.Volume(), this.TypeDescription(), this.Ticket(), this.TimeMscToString(this.TimeMsc())));
  }

Utilice la función StringFormat() para crear y devolver la siguiente cadena string:

Deal: Entry In  0.10 Buy  #1728374638 at 2023.06.12 16:51:36.838


El método virtual devuelve un texto con un mensaje emergente de trato (deal):

//+------------------------------------------------------------------+
//| Returns a text of a deal pop-up message                          |
//+------------------------------------------------------------------+
string CDeal::Tooltip(void)
  {
   return(::StringFormat("Position ID #%I64d %s:\nDeal #%I64d %.2f %s %s\n%s [%.*f]\nProfit: %.2f, SL: %.*f, TP: %.*f",
                         this.PositionID(), this.Symbol(), this.Ticket(), this.Volume(), this.TypeDescription(),
                         this.EntryDescription(), this.TimeMscToString(this.TimeMsc()), this.m_digits, this.Price(),
                         this.Profit(), this.m_digits, this.SL(), this.m_digits, this.TP()));
  }

Similar al método discutido anteriormente, aquí se forma y devuelve una cadena de texto del siguiente tipo:

Position ID #1752955040 EURUSD:
Deal #1728430603 0.10 Sell Entry Out
2023.06.12 17:04:20.362 [1.07590]
Profit: 15.00, SL: 1.07290, TP: 1.07590

Esta cadena se utilizará más adelante para mostrar una descripción de la operación en un tooltip al pasar el puntero del ratón sobre la etiqueta de la operación en el gráfico. El método puede redefinirse en clases heredadas para obtener otra información sobre la operación.


El método que imprime las propiedades del acuerdo en el diario:

//+------------------------------------------------------------------+
//| Print deal properties in the journal                             |
//+------------------------------------------------------------------+
void CDeal::Print(void)
  {
   ::PrintFormat("  %s", this.Description());
  }

El método no imprime todas las propiedades de la operación, sino que sólo muestra la información mínima sobre la operación devuelta por el método Description() comentado anteriormente. Se añaden dos espacios antes de la cadena de descripción del trato para ordenar la información cuando se muestra una descripción de posición, en la que un encabezado de descripción de posición va seguido de datos sobre tratos de posición:

Position EURUSD 0.10 Buy #1752955040, Magic 0
-Opened 2023.06.12 16:51:36.838 [1.07440]
-Closed 2023.06.12 17:04:20.362 [1.07590]
  Deal: Entry In  0.10 Buy  #1728374638 at 2023.06.12 16:51:36.838
  Deal: Entry Out 0.10 Sell #1728430603 at 2023.06.12 17:04:20.362

Obtenga más información sobre las funciones PrintFormat() y StringFormat() en los artículos «Analizamos PrintFormat() y tomamos ejemplos listos para usar» y «StringFormat(). Panorámica, ejemplos de uso listos para aplicar.".


El método que devuelve tiempo en milisegundos:

//+------------------------------------------------------------------+
//| Return time with milliseconds                                    |
//+------------------------------------------------------------------+
string CDeal::TimeMscToString(const long time_msc, int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const
  {
   return(::TimeToString(time_msc/1000, flags) + "." + ::IntegerToString(time_msc %1000, 3, '0'));
  }

Aquí formamos una cadena string que contiene el valor del tiempo en milisegundos convertido a tiempo normal dividido por 1000. El número de milisegundos obtenido como resto de dividir el tiempo en milisegundos por 1000 se agrega a la cadena resultante después de un punto. La cadena de milisegundos se formatea como un número de tres dígitos con ceros iniciales agregados si es más corta que tres dígitos. Como resultado, obtenemos la siguiente representación del tiempo:

2023.06.12 17:04:20.362

Para entender cuál era el spread en el momento de la transacción, es necesario obtener el tick requerido para el tiempo de la transacción en milisegundos. Una tarea aparentemente trivial de copiar un tick en un momento conocido utilizando la función estándar CopyTicks() resultó en una pequeña búsqueda para encontrar una solución, ya que no era posible copiar el tick en el momento exacto especificado. Como resultado, después de buscar un poco una solución, logré encontrar el algoritmo necesario: tenemos que hacer un cierto número de solicitudes de ticks en un rango de tiempo cada vez mayor «From» (Desde) hasta el momento de la operación. Más información aquí (en ruso).


El método para obtener el tick del trato:

//+------------------------------------------------------------------+
//| Get the deal tick                                                |
//+------------------------------------------------------------------+
bool CDeal::GetDealTick(const int amount=20)
  {
   MqlTick ticks[];        // We will receive ticks here
   int attempts = amount;  // Number of attempts to get ticks
   int offset = 500;       // Initial time offset for an attempt
   int copied = 0;         // Number of ticks copied
   
//--- Until the tick is copied and the number of copy attempts is over
//--- we try to get a tick, doubling the initial time offset at each iteration (expand the "from_msc" time range)
   while(!::IsStopped() && (copied<=0) && (attempts--)!=0)
      copied = ::CopyTicksRange(this.m_symbol, ticks, COPY_TICKS_INFO, this.m_time_msc-(offset <<=1), this.m_time_msc);
    
//--- If the tick was successfully copied (it is the last one in the tick array), set it to the m_tick variable
   if(copied>0)
      this.m_tick=ticks[copied-1];

//--- Return the flag that the tick was copied
   return(copied>0);
  }

Después de recibir un tick, se toman los precios de compra y venta y el tamaño del spread se calcula como (compra - venta) / punto.

Si no se puede obtener un tick con este método, obtenga el valor promedio del spread utilizando el método para obtener el spread de la barra de minutos de negociación:

//+------------------------------------------------------------------+
//| Gets the spread of the deal minute bar                           |
//+------------------------------------------------------------------+
int CDeal::GetSpreadM1(void)
  {
   int array[1]={};
   int bar=::iBarShift(this.m_symbol, PERIOD_M1, this.Time());
   if(bar==WRONG_VALUE)
      return 0;
   return(::CopySpread(this.m_symbol, PERIOD_M1, bar, 1, array)==1 ? array[0] : 0);
  }

Aquí obtenemos la hora de apertura de la barra de minutos por la hora de negociación, que se utiliza para obtener el spread medio de la barra utilizando la función CopySpread(). Si hay un error al obtener una barra o un spread, el método devuelve cero.


El método que crea un objeto de etiqueta en un gráfico:

//+------------------------------------------------------------------+
//| Create a label object on the chart                               |
//+------------------------------------------------------------------+
bool CDeal::CreateLabelObj(void)
  {
//--- Create a graphical resource with a Bitmap object attached to it
   ::ResetLastError();
   if(!this.m_canvas.CreateBitmap(this.m_name, this.m_time, this.m_price, this.m_width, this.m_height, COLOR_FORMAT_ARGB_NORMALIZE))
     {
      ::PrintFormat("%s: When creating a graphic object, error %d occurred in the CreateBitmap method of the CCanvas class",__FUNCTION__, ::GetLastError());
      return false;
     }
//--- If the graphical resource is successfully created, set the Bitmap object, anchor point, price, time and tooltip text
   ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_ANCHOR, ANCHOR_CENTER);
   ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIME, this.Time());
   ::ObjectSetDouble(this.m_chart_id, this.m_name, OBJPROP_PRICE, this.Price());
   ::ObjectSetString(this.m_chart_id, this.m_name, OBJPROP_TOOLTIP, this.Tooltip());
   
//--- Hide the created object from the chart and draw its appearance on it
   this.HideArrow();
   this.DrawLabelView();
   return true;
  }

Cree un recurso gráfico, establezca su punto de anclaje en el centro, el precio y el tiempo de la oferta y el texto de la información sobre herramientas. A continuación, se establece una bandera para el objeto creado indicando que está oculto en todos los períodos del gráfico y su apariencia se dibuja en él. Ahora, para mostrarlo, solo necesitamos establecer el indicador de visibilidad para el objeto en todos los períodos de tiempo.


El método que dibuja la apariencia del objeto de etiqueta:

//+------------------------------------------------------------------+
//| Draw the appearance of the label object                          |
//+------------------------------------------------------------------+
void CDeal::DrawLabelView(void)
  {
   this.m_canvas.Erase(0x00FFFFFF);
   this.DrawArrow();
   this.m_canvas.Update(true);
  }

Primero, el objeto gráfico Bitmap se rellena con un color completamente transparente, luego se dibuja sobre él una flecha correspondiente al tipo de operación y luego se actualiza el lienzo mientras se vuelve a dibujar el gráfico.


El método virtual de dibujo de la flecha correspondiente al tipo de trato:

//+------------------------------------------------------------------+
//| Draw an arrow corresponding to the deal type                     |
//+------------------------------------------------------------------+
void CDeal::DrawArrow(void)
  {
   switch(this.TypeDeal())
     {
      case DEAL_TYPE_BUY   :  this.DrawArrowBuy(5, 10);  break;
      case DEAL_TYPE_SELL  :  this.DrawArrowSell(5, 0);  break;
      default              :  break;
     }
  }

Dependiendo del tipo de operación (Compra o Venta) se llama al método para dibujar la flecha correspondiente. El método es virtual, por lo que puede redefinirse en las clases heredadas. Por ejemplo, en una clase secundaria, podemos dibujar una etiqueta diferente para un tipo de trato diferente, o también podemos tener en cuenta el método para cambiar la posición, etc.


El método que dibuja la flecha Buy en el "lienzo":

//+------------------------------------------------------------------+
//| Draw Buy arrow mask on the canvas                                |
//+------------------------------------------------------------------+
void CDeal::DrawArrowMaskBuy(const int shift_x, const int shift_y)
  {
   int x[]={4+shift_x, 8+shift_x, 8+shift_x, 6+shift_x, 6+shift_x, 2+shift_x, 2+shift_x, 0+shift_x, 0+shift_x, 4+shift_x};
   int y[]={0+shift_y, 4+shift_y, 5+shift_y, 5+shift_y, 7+shift_y, 7+shift_y, 5+shift_y, 5+shift_y, 4+shift_y, 0+shift_y};
   this.m_canvas.Polygon(x, y, ::ColorToARGB(clrWhite, 220));
  }

Las flechas dibujadas como etiquetas de tratos tienen un contorno blanco (máscara) alrededor del perímetro para que destaquen sobre el fondo oscuro de la vela:

Puesto que las coordenadas de las figuras dibujadas en el lienzo se especifican siempre en relación con las coordenadas locales del lienzo, es necesario introducir desplazamientos que se añadirán a las coordenadas de los puntos de la línea quebrada a lo largo de los ejes X e Y, de modo que podamos centrar con precisión la figura que se está dibujando dentro del lienzo. A continuación, teniendo en cuenta los desplazamientos pasados al método, se establecen los valores de las coordenadas X e Y en las matrices y se llama al método Polygon() para dibujar un contorno blanco utilizando los puntos de coordenadas. A continuación, la flecha se dibujará dentro del contorno utilizando los métodos de dibujo de flechas.

El método que dibuja la flecha de compra en el lienzo:

//+------------------------------------------------------------------+
//| Draw Buy arrow on the canvas                                     |
//+------------------------------------------------------------------+
void CDeal::DrawArrowBuy(const int shift_x, const int shift_y)
  {
   this.DrawArrowMaskBuy(shift_x, shift_y);
   int x[]={4+shift_x, 7+shift_x, 5+shift_x, 5+shift_x, 3+shift_x, 3+shift_x, 1+shift_x, 4+shift_x};
   int y[]={1+shift_y, 4+shift_y, 4+shift_y, 6+shift_y, 6+shift_y, 4+shift_y, 4+shift_y, 1+shift_y};
   this.m_canvas.Polygon(x, y, ::ColorToARGB(this.m_color_arrow));
   this.m_canvas.Fill(4+shift_x, 4+shift_y, ::ColorToARGB(this.m_color_arrow));
  }

Aquí primero dibujamos la máscara de la flecha de compra, luego dibujamos la flecha de compra usando las coordenadas especificadas y rellenamos su espacio interno con color.


Se aplican métodos similares para dibujar la flecha de Venta:

//+------------------------------------------------------------------+
//| Draw Sell arrow mask on the canvas                               |
//+------------------------------------------------------------------+
void CDeal::DrawArrowMaskSell(const int shift_x, const int shift_y)
  {
   int x[]={4+shift_x, 0+shift_x, 0+shift_x, 2+shift_x, 2+shift_x, 6+shift_x, 6+shift_x, 8+shift_x, 8+shift_x, 4+shift_x};
   int y[]={8+shift_y, 4+shift_y, 3+shift_y, 3+shift_y, 1+shift_y, 1+shift_y, 3+shift_y, 3+shift_y, 4+shift_y, 8+shift_y};
   this.m_canvas.Polygon(x, y, ::ColorToARGB(clrWhite, 220));
  }
//+------------------------------------------------------------------+
//| Draw Sell arrow on the canvas                                    |
//+------------------------------------------------------------------+
void CDeal::DrawArrowSell(const int shift_x, const int shift_y)
  {
   this.DrawArrowMaskSell(shift_x, shift_y);
   int x[]={4+shift_x, 1+shift_x, 3+shift_x, 3+shift_x, 5+shift_x, 5+shift_x, 7+shift_x, 4+shift_x};
   int y[]={7+shift_y, 4+shift_y, 4+shift_y, 2+shift_y, 2+shift_y, 4+shift_y, 4+shift_y, 7+shift_y};
   this.m_canvas.Polygon(x, y, ::ColorToARGB(this.m_color_arrow));
   this.m_canvas.Fill(4+shift_x, 4+shift_y, ::ColorToARGB(this.m_color_arrow));
  }


Métodos para ocultar y mostrar la etiqueta de operaciones en el gráfico:

//+------------------------------------------------------------------+
//| Hide the deal label on the chart                                 |
//+------------------------------------------------------------------+
void CDeal::HideArrow(const bool chart_redraw=false)
  {
   ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }
//+------------------------------------------------------------------+
//| Display the deal label on the chart                              |
//+------------------------------------------------------------------+
void CDeal::ShowArrow(const bool chart_redraw=false)
  {
   ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Para ocultar un objeto en el gráfico, las propiedades de visibilidad OBJPROP_TIMEFRAMES del objeto gráfico deben establecerse en OBJ_NO_PERIODS.

En consecuencia, para mostrar el objeto, debemos establecer la propiedad OBJPROP_TIMEFRAMES en OBJ_ALL_PERIODS.


El método que establece el color de la etiqueta del trato:

//+------------------------------------------------------------------+
//| Set the deal label color                                         |
//+------------------------------------------------------------------+
void CDeal::SetColorArrow(const color clr)
  {
   this.m_color_arrow=clr;
   this.DrawLabelView();
  }

La variable m_color_arrow que almacena el color de la etiqueta dibujada se establece en el valor pasado al método, y todo el objeto gráfico se redibuja completamente. Así, es posible cambiar el color de la etiqueta de reparto en el gráfico desde el programa de control «sobre la marcha».

Hemos creado una clase deal que proporciona acceso a las propiedades de un deal histórico por su ticket, y nos permite obtener los datos necesarios sobre este deal y mostrar u ocultar su etiqueta en el gráfico.

Los objetos de clase se almacenarán en la lista de operaciones de objetos de posición, que a su vez mostrará las propiedades de la posición histórica, proporcionará acceso a sus operaciones y parámetros, y mostrará las operaciones de apertura y cierre conectadas por una línea.


Clase de posición

Crea un nuevo fichero Position.mqh de la clase CPosition en la misma carpeta donde se ha creado la clase deal.

Similar a la clase deal, implementa la enumeración de propiedades de objeto para buscar y ordenar por propiedades de posición y conecta ficheros de clase deal y CArrayObj para la clase position:

//+------------------------------------------------------------------+
//|                                                     Position.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include "Deal.mqh"
#include <Arrays\ArrayObj.mqh>

enum ENUM_POSITION_SORT_MODE
  {
   SORT_MODE_POSITION_TICKET = 0,      // Mode of comparing/sorting by a position ticket
   SORT_MODE_POSITION_TIME,            // Mode of comparing/sorting by position open time
   SORT_MODE_POSITION_TIME_MSC,        // Mode of comparing/sorting by position open time im milliseconds
   SORT_MODE_POSITION_TIME_UPDATE,     // Mode of comparing/sorting by position update time
   SORT_MODE_POSITION_TIME_UPDATE_MSC, // Mode of comparing/sorting by position update time im milliseconds
   SORT_MODE_POSITION_TYPE,            // Mode of comparing/sorting by position type
   SORT_MODE_POSITION_MAGIC,           // Mode of comparing/sorting by a position magic number
   SORT_MODE_POSITION_IDENTIFIER,      // Mode of comparing/sorting by a position ID
   SORT_MODE_POSITION_REASON,          // Mode of comparing/sorting by position open reason
   SORT_MODE_POSITION_VOLUME,          // Mode of comparing/sorting by a position volume
   SORT_MODE_POSITION_PRICE_OPEN,      // Mode of comparing/sorting by a position price
   SORT_MODE_POSITION_SL,              // Mode of comparing/sorting by Stop Loss level for an open position
   SORT_MODE_POSITION_TP,              // Mode of comparing/sorting by Take Profit level for an open position
   SORT_MODE_POSITION_PRICE_CURRENT,   // Mode of comparing/sorting by the current symbol price
   SORT_MODE_POSITION_SWAP,            // Mode of comparing/sorting by accumulated swap
   SORT_MODE_POSITION_PROFIT,          // Mode of comparing/sorting by the current profit
   SORT_MODE_POSITION_SYMBOL,          // Mode of comparing/sorting by a symbol a position is opened on
   SORT_MODE_POSITION_COMMENT,         // Mode of comparing/sorting by a position comment
   SORT_MODE_POSITION_EXTERNAL_ID,     // Mode of comparing/sorting by a position ID in an external system
   SORT_MODE_POSITION_TIME_CLOSE,      // Mode of comparing/sorting by a position open time
   SORT_MODE_POSITION_TIME_CLOSE_MSC,  // Mode of comparing/sorting by a position open time in milliseconds
   SORT_MODE_POSITION_PRICE_CLOSE,     // Mode of comparing/sorting by a position price
  };

//+------------------------------------------------------------------+
//| Position class                                                   |
//+------------------------------------------------------------------+
class CPosition : public CObject
  {
  }

En las secciones protected y public de la clase, introduce variables y métodos para manejar la clase:

//+------------------------------------------------------------------+
//| Position class                                                   |
//+------------------------------------------------------------------+
class CPosition : public CObject
  {
private:

protected:
   CArrayObj         m_list_deals;        // List of position deals
   CDeal             m_temp_deal;         // Temporary deal object for searching by property in the list
   
//--- Return time with milliseconds
   string            TimeMscToString(const long time_msc,int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const;

//--- Integer properties
   long              m_ticket;            // Position ticket
   datetime          m_time;              // Position opening time
   long              m_time_msc;          // Position opening time in milliseconds since 01.01.1970 
   datetime          m_time_update;       // Position update time
   long              m_time_update_msc;   // Position update time in milliseconds since 01.01.1970
   ENUM_POSITION_TYPE m_type;             // Position type
   long              m_magic;             // Magic number for a position (see ORDER_MAGIC)
   long              m_identifier;        // Position ID
   ENUM_POSITION_REASON m_reason;         // Position opening reason
 
//--- Real properties
   double            m_volume;            // Position volume
   double            m_price_open;        // Position price
   double            m_sl;                // Stop Loss level for an open position
   double            m_tp;                // Take Profit level for an open position
   double            m_price_current;     // Current price by symbol
   double            m_swap;              // Accumulated swap
   double            m_profit;            // Current profit

//--- String properties
   string            m_symbol;            // A symbol the position is open for
   string            m_comment;           // Position comment
   string            m_external_id;       // Position ID in an external system (on the exchange) 

//--- Additional properties
   long              m_chart_id;          // Chart ID
   int               m_profit_pt;         // Profit in points
   int               m_digits;            // Symbol digits
   double            m_point;             // One symbol point value
   double            m_contract_size;     // Symbol trade contract size
   string            m_currency_profit;   // Symbol profit currency
   string            m_account_currency;  // Deposit currency
   
   string            m_line_name;         // Line graphical object name
   color             m_line_color;        // Connecting line color

//--- Create a line connecting open-close deals
   virtual bool      CreateLine(void);

//--- Return the pointer to (1) open and (2) close deal
   CDeal            *GetDealIn(void)   const;
   CDeal            *GetDealOut(void)  const;
   
//--- (1) Hide and (2) display deal labels on the chart
   void              HideDeals(const bool chart_redraw=false);
   void              ShowDeals(const bool chart_redraw=false);
   
//--- (1) Hide and (2) display the connecting line between the deal labels
   void              HideLine(const bool chart_redraw=false);
   void              ShowLine(const bool chart_redraw=false);

public:
//--- Set the properties
//--- Integer properties
   void              SetTicket(const long ticket)                    { this.m_ticket=ticket;          }  // Position ticket
   void              SetTime(const datetime time)                    { this.m_time=time;              }  // Position open time
   void              SetTimeMsc(const long value)                    { this.m_time_msc=value;         }  // Position open time in milliseconds 01.01.1970 
   void              SetTimeUpdate(const datetime time)              { this.m_time_update=time;       }  // Position update time
   void              SetTimeUpdateMsc(const long value)              { this.m_time_update_msc=value;  }  // Position update time in milliseconds 01.01.1970
   void              SetTypePosition(const ENUM_POSITION_TYPE type)  { this.m_type=type;              }  // Position type
   void              SetMagic(const long magic)                      { this.m_magic=magic;            }  // Magic number for a position (see ORDER_MAGIC)
   void              SetID(const long id)                            { this.m_identifier=id;          }  // Position ID
   void              SetReason(const ENUM_POSITION_REASON reason)    { this.m_reason=reason;          }  // Position opening reason
 
//--- Real properties
   void              SetVolume(const double volume)                  { this.m_volume=volume;          }  // Position volume
   void              SetPriceOpen(const double price)                { this.m_price_open=price;       }  // Position price
   void              SetSL(const double value)                       { this.m_sl=value;               }  // Stop Loss level for an open position
   void              SetTP(const double value)                       { this.m_tp=value;               }  // Take Profit level for an open position
   void              SetPriceCurrent(const double price)             { this.m_price_current=price;    }  // Current price by symbol
   void              SetSwap(const double value)                     { this.m_swap=value;             }  // Accumulated swap
   void              SetProfit(const double value)                   { this.m_profit=value;           }  // Current profit

//--- String properties
   void              SetSymbol(const string symbol)                  { this.m_symbol=symbol;          }  // Symbol a position is opened for
   void              SetComment(const string comment)                { this.m_comment=comment;        }  // Position comment
   void              SetExternalID(const string ext_id)              { this.m_external_id=ext_id;     }  // Position ID in an external system (on the exchange)


//--- Get the properties
//--- Integer properties
   long              Ticket(void)                              const { return(this.m_ticket);         }  // Position ticket
   datetime          Time(void)                                const { return(this.m_time);           }  // Position open time
   long              TimeMsc(void)                             const { return(this.m_time_msc);       }  // Position open time in milliseconds since 01.01.1970 
   datetime          TimeUpdate(void)                          const { return(this.m_time_update);    }  // Position update time
   long              TimeUpdateMsc(void)                       const { return(this.m_time_update_msc);}  // Position update time in milliseconds since 01.01.1970
   ENUM_POSITION_TYPE TypePosition(void)                       const { return(this.m_type);           }  // Position type
   long              Magic(void)                               const { return(this.m_magic);          }  // Magic number for a position (see ORDER_MAGIC)
   long              ID(void)                                  const { return(this.m_identifier);     }  // Position ID
   ENUM_POSITION_REASON Reason(void)                           const { return(this.m_reason);         }  // Position opening reason
   
//--- Real properties
   double            Volume(void)                              const { return(this.m_volume);         }  // Position volume
   double            PriceOpen(void)                           const { return(this.m_price_open);     }  // Position price
   double            SL(void)                                  const { return(this.m_sl);             }  // Stop Loss level for an open position
   double            TP(void)                                  const { return(this.m_tp);             }  // Take Profit for an open position
   double            PriceCurrent(void)                        const { return(this.m_price_current);  }  // Current price by symbol
   double            Swap(void)                                const { return(this.m_swap);           }  // Accumulated swap
   double            Profit(void)                              const { return(this.m_profit);         }  // Current profit
   
//--- String properties
   string            Symbol(void)                              const { return(this.m_symbol);         }  // A symbol position is opened on
   string            Comment(void)                             const { return(this.m_comment);        }  // Position comment
   string            ExternalID(void)                          const { return(this.m_external_id);    }  // Position ID in an external system (on the exchange)
   
//--- Additional properties
   ulong             DealIn(void)                              const;                                    // Open deal ticket
   ulong             DealOut(void)                             const;                                    // Close deal ticket
   datetime          TimeClose(void)                           const;                                    // Close time
   long              TimeCloseMsc(void)                        const;                                    // Close time in milliseconds
   int               ProfitInPoints(void)                      const;                                    // Profit in points
   double            PriceClose(void)                          const;                                    // Close price

//--- Add a deal to the list of deals, return the pointer
   CDeal            *DealAdd(const long ticket);
   
//--- Set the color of the (1) connecting line, (2) Buy and Sell deals
   void              SetLineColor(const color clr=C'225,68,29');
   void              SetDealsColor(const color clr_deal_buy=C'3,95,172', const color clr_deal_sell=C'225,68,29');

//--- Return a position type description
   string            TypeDescription(void) const;
   
//--- Return position open time and price description
   string            TimePriceCloseDescription(void);

//--- Return position close time and price description
   string            TimePriceOpenDescription(void);
   
//--- Return a brief position description
   string            Description(void);

//--- Returns a text of a position popup message
   virtual string    Tooltip(void);
 
//--- Print the properties of the position and its deals in the journal
   void              Print(void);
   
//--- (1) Hide and (2) display a graphical representation of a position on a chart
   void              Hide(const bool chart_redraw=false);
   void              Show(const bool chart_redraw=false);
   
//--- Compare two objects by the property specified in 'mode'
   virtual int       Compare(const CObject *node, const int mode=0) const;
   
//--- Constructor/destructor
                     CPosition(const long position_id, const string symbol);
                     CPosition(void)   { this.m_symbol=::Symbol();   }
                    ~CPosition();
  };

Para buscar en la lista ordenada en la clase del array dinámico de punteros a las instancias de la clase CObject y sus descendientes CArrayObj, el método Search() recibe el puntero a la instancia del objeto, en la que debemos realizar una comparación por la propiedad utilizada para ordenar la lista. Para evitar crear y destruir constantemente un nuevo objeto con una propiedad determinada, lo que es bastante costoso en términos de rendimiento, se declara una instancia de un objeto de trato en la sección protegida de la clase. Para realizar una búsqueda, simplemente estableceremos el valor requerido de la propiedad requerida en este objeto y lo pasaremos al método de búsqueda como la instancia deseada.

Echemos un vistazo a los métodos declarados.


Constructor paramétrico:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CPosition::CPosition(const long position_id, const string symbol)
  {
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
   this.m_identifier       =  position_id;
   this.m_account_currency =  ::AccountInfoString(ACCOUNT_CURRENCY);
   this.m_symbol           =  (symbol==NULL ? ::Symbol() : symbol);
   this.m_digits           =  (int)::SymbolInfoInteger(this.m_symbol,SYMBOL_DIGITS);
   this.m_point            =  ::SymbolInfoDouble(this.m_symbol,SYMBOL_POINT);
   this.m_contract_size    =  ::SymbolInfoDouble(this.m_symbol,SYMBOL_TRADE_CONTRACT_SIZE);
   this.m_currency_profit  =  ::SymbolInfoString(this.m_symbol,SYMBOL_CURRENCY_PROFIT);
   this.m_chart_id         =  ::ChartID();
   this.m_line_name        =  "line#"+(string)this.m_identifier;
   this.m_line_color       =  C'225,68,29';
  }

El constructor recibe la ID de la posición y el símbolo para el que se abrió la posición. La lista de operaciones de posición recibe el indicador de ordenación por tiempo de operación en milisegundos. El ID y el símbolo se guardan en las variables de la clase, mientras que se establecen algunos parámetros de la cuenta y el símbolo, así como la ID del gráfico y las propiedades de la línea que conecta las etiquetas de apertura y cierre de la posición.


En el destructor de la clase, elimina todos los objetos gráficos cuyo prefijo sea el nombre de la línea y borra la lista de tratos:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CPosition::~CPosition()
  {
   ::ObjectDelete(this.m_chart_id, this.m_line_name);
   this.m_list_deals.Clear();
  }

Si la clase heredada debe mostrar varias líneas que conecten todos los repartos de posición, y no sólo los de apertura y cierre, entonces el nombre de cada línea puede establecerse como «nombre_línea» + «número_línea». En este caso, todas estas líneas se eliminarán en el destructor, ya que todas tienen un prefijo común: «m_line_name».


El método que compara dos objetos por una propiedad especifica:

//+------------------------------------------------------------------+
//| Compare two objects by the specified property                    |
//+------------------------------------------------------------------+
int CPosition::Compare(const CObject *node,const int mode=0) const
  {
   const CPosition *obj=node;
   switch(mode)
     {
      case SORT_MODE_POSITION_TICKET         :  return(this.Ticket() > obj.Ticket()                ?  1  :  this.Ticket() < obj.Ticket()                 ? -1  :  0);
      case SORT_MODE_POSITION_TIME           :  return(this.Time() > obj.Time()                    ?  1  :  this.Time() < obj.Time()                     ? -1  :  0);
      case SORT_MODE_POSITION_TIME_MSC       :  return(this.TimeMsc() > obj.TimeMsc()              ?  1  :  this.TimeMsc() < obj.TimeMsc()               ? -1  :  0);
      case SORT_MODE_POSITION_TIME_UPDATE    :  return(this.TimeUpdate() > obj.TimeUpdate()        ?  1  :  this.TimeUpdate() < obj.TimeUpdate()         ? -1  :  0);
      case SORT_MODE_POSITION_TIME_UPDATE_MSC:  return(this.TimeUpdateMsc() > obj.TimeUpdateMsc()  ?  1  :  this.TimeUpdateMsc() < obj.TimeUpdateMsc()   ? -1  :  0);
      case SORT_MODE_POSITION_TYPE           :  return(this.TypePosition() > obj.TypePosition()    ?  1  :  this.TypePosition() < obj.TypePosition()     ? -1  :  0);
      case SORT_MODE_POSITION_MAGIC          :  return(this.Magic() > obj.Magic()                  ?  1  :  this.Magic() < obj.Magic()                   ? -1  :  0);
      case SORT_MODE_POSITION_IDENTIFIER     :  return(this.ID() > obj.ID()                        ?  1  :  this.ID() < obj.ID()                         ? -1  :  0);
      case SORT_MODE_POSITION_REASON         :  return(this.Reason() > obj.Reason()                ?  1  :  this.Reason() < obj.Reason()                 ? -1  :  0);
      case SORT_MODE_POSITION_VOLUME         :  return(this.Volume() > obj.Volume()                ?  1  :  this.Volume() < obj.Volume()                 ? -1  :  0);
      case SORT_MODE_POSITION_PRICE_OPEN     :  return(this.PriceOpen() > obj.PriceOpen()          ?  1  :  this.PriceOpen() < obj.PriceOpen()           ? -1  :  0);
      case SORT_MODE_POSITION_SL             :  return(this.SL() > obj.SL()                        ?  1  :  this.SL() < obj.SL()                         ? -1  :  0);
      case SORT_MODE_POSITION_TP             :  return(this.TP() > obj.TP()                        ?  1  :  this.TP() < obj.TP()                         ? -1  :  0);
      case SORT_MODE_POSITION_PRICE_CURRENT  :  return(this.PriceCurrent() > obj.PriceCurrent()    ?  1  :  this.PriceCurrent() < obj.PriceCurrent()     ? -1  :  0);
      case SORT_MODE_POSITION_SWAP           :  return(this.Swap() > obj.Swap()                    ?  1  :  this.Swap() < obj.Swap()                     ? -1  :  0);
      case SORT_MODE_POSITION_PROFIT         :  return(this.Profit() > obj.Profit()                ?  1  :  this.Profit() < obj.Profit()                 ? -1  :  0);
      case SORT_MODE_POSITION_SYMBOL         :  return(this.Symbol() > obj.Symbol()                ?  1  :  this.Symbol() < obj.Symbol()                 ? -1  :  0);
      case SORT_MODE_POSITION_COMMENT        :  return(this.Comment() > obj.Comment()              ?  1  :  this.Comment() < obj.Comment()               ? -1  :  0);
      case SORT_MODE_POSITION_EXTERNAL_ID    :  return(this.ExternalID() > obj.ExternalID()        ?  1  :  this.ExternalID() < obj.ExternalID()         ? -1  :  0);
      case SORT_MODE_POSITION_TIME_CLOSE     :  return(this.TimeClose() > obj.TimeClose()          ?  1  :  this.TimeClose() < obj.TimeClose()           ? -1  :  0);
      case SORT_MODE_POSITION_TIME_CLOSE_MSC :  return(this.TimeCloseMsc() > obj.TimeCloseMsc()    ?  1  :  this.TimeCloseMsc() < obj.TimeCloseMsc()     ? -1  :  0);
      case SORT_MODE_POSITION_PRICE_CLOSE    :  return(this.PriceClose() > obj.PriceClose()        ?  1  :  this.PriceClose() < obj.PriceClose()         ? -1  :  0);
      default                                :  return -1;
     }
  }

Un método similar se consideró al desarrollar la clase de objeto «deal». Aquí todo es similar: si el valor de la propiedad del objeto actual es mayor que el que se está comparando, se devuelve 1; si es menor, obtenemos -1; si las propiedades son iguales, tenemos cero.


El método que devuelve el tiempo en milisegundos:

//+------------------------------------------------------------------+
//| Return time with milliseconds                                    |
//+------------------------------------------------------------------+
string CPosition::TimeMscToString(const long time_msc, int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const
  {
   return(::TimeToString(time_msc/1000, flags) + "." + ::IntegerToString(time_msc %1000, 3, '0'));
  }

Se trata de una copia del método del mismo nombre de la clase de objeto deal considerado en la clase deal.


El método que devuelve el puntero a la operación abierta:

//+------------------------------------------------------------------+
//| Return the pointer to the opening deal                           |
//+------------------------------------------------------------------+
CDeal *CPosition::GetDealIn(void) const
  {
   int total=this.m_list_deals.Total();
   for(int i=0; i<total; i++)
     {
      CDeal *deal=this.m_list_deals.At(i);
      if(deal==NULL)
         continue;
      if(deal.Entry()==DEAL_ENTRY_IN)
         return deal;
     }
   return NULL;
  }

Tenemos que encontrar una operación con el método de actualización de posición "Entry In" en la lista de objetos de operaciones. Este trato debería ser el primero de una lista ordenada por tiempo. Por lo tanto, comenzamos el ciclo desde el principio de la lista, desde el índice 0.

Obtenemos una operación de la lista por índice y, si se trata de una operación de entrada en el mercado, devolvemos el puntero a la operación encontrada en la lista. Al final del ciclo, devuelve NULL - trato no encontrado.


El método que devuelve el puntero a la operación de cierre:

//+------------------------------------------------------------------+
//| Return the pointer to the close deal                             |
//+------------------------------------------------------------------+
CDeal *CPosition::GetDealOut(void) const
  {
   for(int i=this.m_list_deals.Total()-1; i>=0; i--)
     {
      CDeal *deal=this.m_list_deals.At(i);
      if(deal==NULL)
         continue;
      if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY)
         return deal;
     }
   return NULL;
  }

Aquí, comparado con el método anterior, todo es al revés - la operación para salir de la posición está al final de la lista, así que empezamos el ciclo desde el final.
Tan pronto como se encuentra un trato para salir de una posición, devolver el puntero a la misma. En caso contrario, devuelve NULL.


Los métodos para obtener propiedades adicionales de una posición histórica:

//+------------------------------------------------------------------+
//| Return the open deal ticket                                      |
//+------------------------------------------------------------------+
ulong CPosition::DealIn(void) const
  {
   CDeal *deal=this.GetDealIn();
   return(deal!=NULL ? deal.Ticket() : 0);
  }
//+------------------------------------------------------------------+
//| Return the close deal ticket                                     |
//+------------------------------------------------------------------+
ulong CPosition::DealOut(void) const
  {
   CDeal *deal=this.GetDealOut();
   return(deal!=NULL ? deal.Ticket() : 0);
  }
//+------------------------------------------------------------------+
//| Return the close time                                            |
//+------------------------------------------------------------------+
datetime CPosition::TimeClose(void) const
  {
   CDeal *deal=this.GetDealOut();
   return(deal!=NULL ? deal.Time() : 0);
  }
//+------------------------------------------------------------------+
//| Return the close time in milliseconds                            |
//+------------------------------------------------------------------+
long CPosition::TimeCloseMsc(void) const
  {
   CDeal *deal=this.GetDealOut();
   return(deal!=NULL ? deal.TimeMsc() : 0);
  }
//+------------------------------------------------------------------+
//| Return the close price                                           |
//+------------------------------------------------------------------+
double CPosition::PriceClose(void) const
  {
   CDeal *deal=this.GetDealOut();
   return(deal!=NULL ? deal.Price() : 0);
  }

En todos los métodos, primero obtenemos el puntero a un trato, de la que tenemos que tomar la propiedad correspondiente, y si el puntero es válido, devolvemos el valor de la propiedad, de lo contrario, cero.


El método que devuelve el beneficio en puntos:

//+------------------------------------------------------------------+
//| Return a profit in points                                        |
//+------------------------------------------------------------------+
int CPosition::ProfitInPoints(void) const
  {
//--- If symbol Point has not been received previously, inform of that and return 0
   if(this.m_point==0)
     {
      ::Print("The Point() value could not be retrieved.");
      return 0;
     }
//--- Get position open and close prices
   double open =this.PriceOpen();
   double close=this.PriceClose();
   
//--- If failed to get the prices, return 0
   if(open==0 || close==0)
      return 0;
      
//--- Depending on the position type, return the calculated value of the position profit in points
   return int(this.TypePosition()==POSITION_TYPE_BUY ? (close-open)/this.m_point : (open-close)/this.m_point);
  }

Aquí obtenemos primero los precios de apertura y cierre de las operaciones correspondientes, y luego devolvemos el resultado del cálculo de beneficios en puntos, dependiendo de la dirección de la posición.


El método que añade una transacción a la lista de transacciones:

//+------------------------------------------------------------------+
//| Add a deal to the list of deals                                  |
//+------------------------------------------------------------------+
CDeal *CPosition::DealAdd(const long ticket)
  {
//--- A temporary object gets a ticket of the desired deal and the flag of sorting the list of deals by ticket
   this.m_temp_deal.SetTicket(ticket);
   this.m_list_deals.Sort(SORT_MODE_DEAL_TICKET);
   
//--- Set the result of checking if a deal with such a ticket is present in the list
   bool added=(this.m_list_deals.Search(&this.m_temp_deal)!=WRONG_VALUE);
   
//--- Set the flag of sorting by time in milliseconds for the list
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);

//--- If a deal with such a ticket is already in the list, return NULL
   if(added)
      return NULL;
      
//--- Create a new deal object
   CDeal *deal=new CDeal(ticket);
   if(deal==NULL)
      return NULL;
   
//--- Add the created object to the list in sorting order by time in milliseconds
//--- If failed to add the deal to the list, remove the the deal object and return NULL
   if(!this.m_list_deals.InsertSort(deal))
     {
      delete deal;
      return NULL;
     }
   
//--- If this is a position closing deal, set the profit from the deal properties to the position profit value and
//--- create a connecting line between the position open-close deal labels
   if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY)
     {
      this.SetProfit(deal.Profit());
      this.CreateLine();
     }
     
//--- Return the pointer to the created deal object
   return deal;   
  }

El método recibe el ticket de una operación seleccionada. En primer lugar, asegúrese de que no hay ningún acuerdo con dicho ticket en la lista. Para ello, se asigna al objeto deal temporal el ticket pasado al método. La lista de operaciones se ordena por el valor de los tickets de operación, y se realiza una búsqueda de una operación de la lista con dicho ticket. Inmediatamente después de buscar una operación, se devuelve a la lista el indicador de ordenación por tiempo en milisegundos. Si tal operación ya está en la lista, no es necesario añadirla, el método devuelve NULL. Al mismo tiempo, la ordenación de la lista de operaciones ya está establecida por defecto: por tiempo en milisegundos. Si dicha operación no está en la lista, se crea un nuevo objeto de operación y se añade a la lista en el orden de ordenación por tiempo en milisegundos. Si se trata de una operación de cierre de posición, su beneficio se establece en las propiedades de la posición y se crea una línea de conexión entre las operaciones de apertura y cierre de la posición. Como resultado, se devuelve el puntero al objeto deal recién creado.


El método que devuelve una descripción del tipo de posición:

//+------------------------------------------------------------------+
//| Return a position type description                               |
//+------------------------------------------------------------------+
string CPosition::TypeDescription(void) const
  {
   return(this.m_type==POSITION_TYPE_BUY ? "Buy" : this.m_type==POSITION_TYPE_SELL ? "Sell" : "Unknown::"+(string)this.m_type);
  }

Devuelve la descripción en función del tipo de una posición almacenada en la variable m_type. Si el valor de la variable no coincide con las constantes de enumeración del tipo de posición, devuelve «Desconocido:: »+valor_variable.


El método que devuelve la hora de cierre de la posición y la descripción del precio:

//+------------------------------------------------------------------+
//| Return position close time and price description                 |
//+------------------------------------------------------------------+
string CPosition::TimePriceCloseDescription(void)
  {
   if(this.TimeCloseMsc()==0)
      return "Not closed yet";
   return(::StringFormat("Closed %s [%.*f]", this.TimeMscToString(this.TimeCloseMsc()),this.m_digits, this.PriceClose()));
  }

Si aún no hay ninguna operación de cierre en el objeto posición, el método devuelve un mensaje que indica que la posición aún no está cerrada. En caso contrario, se crea y devuelve una cadena del tipo siguiente:

Closed 2023.06.12 17:04:20.362 [1.07590]


El método devuelve la descripción de la hora y el precio de apertura de la posición:

//+------------------------------------------------------------------+
//| Return position open time and price description                  |
//+------------------------------------------------------------------+
string CPosition::TimePriceOpenDescription(void)
  {
   return(::StringFormat("Opened %s [%.*f]", this.TimeMscToString(this.TimeMsc()),this.m_digits, this.PriceOpen()));
  }

Aquí se crea y devuelve una cadena del siguiente tipo:

Opened 2023.06.12 16:51:36.838 [1.07440]


Método que devuelve una breve descripción del puesto:

//+------------------------------------------------------------------+
//| Return a brief position description                              |
//+------------------------------------------------------------------+
string CPosition::Description(void)
  {
   return(::StringFormat("Position %s %.2f %s #%I64d, Magic %I64d",
                         this.Symbol(), this.Volume(), this.TypeDescription(), this.ID(), this.Magic()));
  }

Crea y devuelve una cadena del siguiente tipo:

Position EURUSD 0.10 Buy #1752955040, Magic 123


El método virtual que devuelve un texto de un mensaje emergente de posición:

//+------------------------------------------------------------------+
//| Return a text of a position pop-up message                       |
//+------------------------------------------------------------------+
string CPosition::Tooltip(void)
  {
//--- Get the pointers to the open and close deals
   CDeal *deal_in =this.GetDealIn();
   CDeal *deal_out=this.GetDealOut();
   
//--- If no deals are received, return an empty string
   if(deal_in==NULL || deal_out==NULL)
      return NULL;
      
//--- Get commission, swap and deal fee that are common for two deals
   double commission=deal_in.Commission()+deal_out.Commission();
   double swap=deal_in.Swap()+deal_out.Swap();
   double fee=deal_in.Fee()+deal_out.Fee();
   
//--- Get the final profit of the position and the spread values when opening and closing
   double profit=deal_out.Profit();
   int    spread_in=deal_in.Spread();
   int    spread_out=deal_out.Spread();
   
//--- If the reason for closing the position is StopLoss, TakeProfit or StopOut, set the reason description in the variable
   string reason=(deal_out.Reason()==DEAL_REASON_SL || deal_out.Reason()==DEAL_REASON_TP || deal_out.Reason()==DEAL_REASON_SO ? deal_out.ReasonDescription() : "");
   
//--- Create and return the tooltip string
   return(::StringFormat("%s\nCommission %.2f, Swap %.2f, Fee %.2f\nSpread In/Out %d/%d, Profit %+.2f %s (%d points)\nResult: %s %+.2f %s",
                         this.Description(), commission, swap, fee, spread_in, spread_out, profit,this.m_currency_profit, 
                         this.ProfitInPoints(), reason, profit+commission+fee+swap, this.m_currency_profit));
  }

En el método se crea y devuelve una cadena del siguiente tipo:

Position EURUSD 0.10 Buy #1752955040, Magic 0
Commission 0.00, Swap 0.00, Fee 0.00
Spread In/Out 0/0, Profit +15.00 USD (150 points)
Result: TP +15.00 USD

El texto se establece como texto de ayuda para la cadena que conecta las operaciones de apertura y cierre de una posición. El método es virtual y puede ser sobrescrito en clases heredadas si es necesario generar datos ligeramente diferentes. Al utilizar la cadena devuelta por el método como texto de información sobre herramientas, debemos tener en cuenta la longitud de la cadena, ya que está limitada a aproximadamente 160 caracteres para una información sobre herramientas, incluidos los códigos de control. Por desgracia, no puedo dar el valor exacto, ya que se descubrió empíricamente.


El método que muestra las etiquetas de reparto en el gráfico:

//+------------------------------------------------------------------+
//| Display deal labels on the chart                                 |
//+------------------------------------------------------------------+
void CPosition::ShowDeals(const bool chart_redraw=false)
  {
   for(int i=this.m_list_deals.Total()-1; i>=0; i--)
     {
      CDeal *deal=this.m_list_deals.At(i);
      if(deal==NULL)
         continue;
      deal.ShowArrow();
     }
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

En un bucle a través de la lista de operaciones, obtenga cada operación sucesiva y llame al método ShowArrow() del objeto recibido, que muestra la etiqueta de la operación en el gráfico. Al final del bucle, vuelve a dibujar el gráfico si se activa la bandera correspondiente.


El método que oculta las etiquetas de reparto en el gráfico:

//+------------------------------------------------------------------+
//| Hide deal labels on the chart                                    |
//+------------------------------------------------------------------+
void CPosition::HideDeals(const bool chart_redraw=false)
  {
   for(int i=this.m_list_deals.Total()-1; i>=0; i--)
     {
      CDeal *deal=this.m_list_deals.At(i);
      if(deal==NULL)
         continue;
      deal.HideArrow();
     }
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

En un bucle a través de la lista de operaciones, obtenemos cada operación sucesiva y llamamos al método HideArrow() del objeto recibido, que oculta la etiqueta de la operación del gráfico. Al final del bucle, vuelve a dibujar el gráfico si se activa la bandera correspondiente.


El método que crea la línea que conecta las operaciones abiertas y cerradas:

//+------------------------------------------------------------------+
//| Create a line connecting open-close deals                        |
//+------------------------------------------------------------------+
bool CPosition::CreateLine(void)
  {
//--- If the graphical line object could not be created, report this in the journal and return 'false'
   ::ResetLastError();
   if(!::ObjectCreate(this.m_chart_id, this.m_line_name, OBJ_TREND, 0, 0, 0, 0, 0))
     {
      ::Print("ObjectCreate() failed. Error ", ::GetLastError());
      return false;
     }
//--- Hide the line
   this.HideLine();

//--- Set the line to be drawn with dots, define the color and return 'true'
   ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_STYLE, STYLE_DOT);
   ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_COLOR, this.m_line_color);
   return true;
  }

Se crea una línea con las coordenadas de precio y hora en la posición cero (el precio es 0, la hora es 01.01.1970 00:00:00). Oculta la línea del gráfico y establece el estilo de dibujo en puntos y el color por defecto.
Inicialmente, las líneas de cada objeto se crean ocultas. En el momento en que es necesario visualizarlos, se fijan para ellos las coordenadas y la visualización necesarias.


Método que muestra la línea de conexión entre las etiquetas de las operaciones:

//+------------------------------------------------------------------+
//| Display the connecting line between deal labels                  |
//+------------------------------------------------------------------+
void CPosition::ShowLine(const bool chart_redraw=false)
  {
//--- Get the pointers to the open and close deals
   CDeal *deal_in= this.GetDealIn();
   CDeal *deal_out=this.GetDealOut();
   
//--- If no deals are received, leave
   if(deal_in==NULL || deal_out==NULL)
      return;
      
//--- Set a start and end time, a price from the deal properties and a tooltip text for the line
   ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIME, 0, deal_in.Time());
   ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIME, 1, deal_out.Time());
   ::ObjectSetDouble(this.m_chart_id, this.m_line_name, OBJPROP_PRICE, 0, deal_in.Price());
   ::ObjectSetDouble(this.m_chart_id, this.m_line_name, OBJPROP_PRICE, 1, deal_out.Price());
   ::ObjectSetString(this.m_chart_id, this.m_line_name, OBJPROP_TOOLTIP, this.Tooltip());
   
//--- Show the line on the chart and update it if the flag is set
   ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }


El método que oculta la línea de conexión entre las etiquetas de los tratos:

//+------------------------------------------------------------------+
//| Hide the connecting line between the deal labels                 |
//+------------------------------------------------------------------+
void CPosition::HideLine(const bool chart_redraw=false)
  {
   ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

El indicador de visibilidad del objeto se restablece en todos los periodos del gráfico y, si el indicador está activado, se actualiza el gráfico.


El método que establece el color de la línea de conexión:

//+------------------------------------------------------------------+
//| Set the color of the connecting line                             |
//+------------------------------------------------------------------+
void CPosition::SetLineColor(const color clr=C'225,68,29')
  {
   if(::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_COLOR, clr))
      this.m_line_color=clr;
  }

El valor pasado al método se establece para el objeto como la propiedad color. Si todo va bien, el color se establece en la variable m_line_color.


Configuración del métodode color de la operación de compra y venta:

//+------------------------------------------------------------------+
//| Set Buy and Sell deal color                                      |
//+------------------------------------------------------------------+
void CPosition::SetDealsColor(const color clr_deal_buy=C'3,95,172', const color clr_deal_sell=C'225,68,29')
  {
//--- In the loop by the list of deals
   int total=this.m_list_deals.Total();
   for(int i=0; i<total; i++)
     {
      //--- get the next deal object
      CDeal *deal=this.m_list_deals.At(i);
      if(deal==NULL)
         continue;
      //--- In case of Buy deal type, set a color for a Buy deal for the object
      if(deal.TypeDeal()==DEAL_TYPE_BUY)
         deal.SetColorArrow(clr_deal_buy);
      //--- In case of Sell deal type, set a color for a Sell deal for the object
      if(deal.TypeDeal()==DEAL_TYPE_SELL)
         deal.SetColorArrow(clr_deal_sell);
     }
  }

El método recibe los colores de las operaciones de Compra y Venta. En el bucle a través de la lista de tratos de posición, obtenga el puntero a cada trato sucesivo y establezca el color para la etiqueta del trato según su tipo.


El método muestra una representación gráfica de una posición en un gráfico:

//+------------------------------------------------------------------+
//| Display a graphical representation of a position on a chart      |
//+------------------------------------------------------------------+
void CPosition::Show(const bool chart_redraw=false)
  {
   this.ShowDeals(false);
   this.ShowLine(chart_redraw);
  }

El método llama secuencialmente a métodos protegidos que muestran las operaciones de entrada y salida y una línea que las conecta en el gráfico.


El método oculta una representación gráfica de una posición en un gráfico:

//+------------------------------------------------------------------+
//| Hide a graphical representation of a position on a chart         |
//+------------------------------------------------------------------+
void CPosition::Hide(const bool chart_redraw=false)
  {
   this.HideLine(false);
   this.HideDeals(chart_redraw);
  }

El método llama secuencialmente a métodos protegidos que ocultan las operaciones de entrada y salida y una línea que las conecta en el gráfico.

Los dos métodos considerados anteriormente son públicos y se utilizan para controlar la visualización de una posición en el gráfico desde el programa de control.


El método que imprime las propiedades de posición y los tratos en el diario:

//+------------------------------------------------------------------+
//| Print the position properties and deals in the journal           |
//+------------------------------------------------------------------+
void CPosition::Print(void)
  {
   ::PrintFormat("%s\n-%s\n-%s", this.Description(), this.TimePriceOpenDescription(), this.TimePriceCloseDescription());
   for(int i=0; i<this.m_list_deals.Total(); i++)
     {
      CDeal *deal=this.m_list_deals.At(i);
      if(deal==NULL)
         continue;
      deal.Print();
     }
  }

En primer lugar, se muestra una cabecera con una breve descripción de la posición y dos líneas con la hora y el precio de apertura y cierre de la posición. A continuación, se imprimen en bucle las descripciones de todos los tratos de posición de la lista.

Como resultado, obtenemos los siguientes datos en el diario:

Position EURUSD 0.10 Sell #2523224572, Magic 0
-Opened 2024.05.31 17:06:15.134 [1.08734]
-Closed 2024.05.31 17:33:17.772 [1.08639]
  Deal: Entry In  0.10 Sell #2497852906 at 2024.05.31 17:06:15.134
  Deal: Entry Out 0.10 Buy  #2497993663 at 2024.05.31 17:33:17.772

Esto no es muy informativo, pero los métodos Print() en estas clases se hicieron sólo para depuración.

Ahora ya tenemos dos clases: la clase de trato y la clase de posición, que contiene una lista de tratos que participaron en la vida de la posición. Las clases son sencillas y contienen información básica sobre las operaciones y algunos datos adicionales sobre los precios y la hora de apertura/cierre de una posición, así como su beneficio en puntos. Los métodos restantes se utilizan para obtener y mostrar esta información en un gráfico o línea de texto.

Ahora vamos a crear una clase general, en la que se recogerán todas las posiciones de las operaciones y se colocarán en una lista de posiciones históricas. La clase proporcionará acceso a las propiedades de las posiciones y sus operaciones, actualizará y completará la lista de posiciones históricas y mostrará la posición especificada en el gráfico.


Gestión de la posición histórica de la clase

En la misma carpeta donde hemos creado las dos clases anteriores, crea un nuevo fichero PositionsControl.mqh de la clase CPositionsControl.

La clase debe heredarse del objeto base CObject, mientras que el archivo de clase de posición se incluye en el archivo de nueva clase CPositionsControl:

//+------------------------------------------------------------------+
//|                                             PositionsControl.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include "Position.mqh"

//+------------------------------------------------------------------+
//| Class of historical positions                                    |
//+------------------------------------------------------------------+
class CPositionsControl : public CObject
  {
  }

Declare las variables y métodos para manejar la clase en las secciones private, protected y public:

//+------------------------------------------------------------------+
//| Class of historical positions                                    |
//+------------------------------------------------------------------+
class CPositionsControl : public CObject
  {
private:
   string            m_symbol;            // The symbol the position is open for
   long              m_current_id;        // ID of the current position displayed on the chart
   bool              m_key_ctrl;          // Flag for allowing to control the chart using the keyboard 
   
//--- Return the position type by deal type
   ENUM_POSITION_TYPE PositionTypeByDeal(const CDeal *deal);

protected:
   CPosition         m_temp_pos;          // Temporary position object for searching
   CArrayObj         m_list_pos;          // List of positions
   long              m_chart_id;          // Chart ID
   
//--- Return the position object from the list by ID
   CPosition        *GetPositionObjByID(const long id);

//--- Return the flag of the market position
   bool              IsMarketPosition(const long id);
   
//--- Return the pointer to the (1) first and the (2) last open position in the list
   CPosition        *GetFirstClosedPosition(void);
   CPosition        *GetLastClosedPosition(void);

//--- Return the pointer to the (1) previous and (2) next closed position in the list
   CPosition        *GetPrevClosedPosition(CPosition *current);
   CPosition        *GetNextClosedPosition(CPosition *current);

//--- Displays a graphical representation of the specified position on a chart
   void              Show(CPosition *pos, const bool chart_redraw=false);
   
//--- Center the chart on the currently selected position
   void              CentersChartByCurrentSelected(void);

//--- Return the ID of the current selected position
   long              CurrentSelectedID(void)    const { return this.m_current_id;         }

//--- Return the selected position (1) open and (2) close time
   datetime          TimeOpenCurrentSelected(void);
   datetime          TimeCloseCurrentSelected(void);
   
//--- Hide the graphical representation of all positions on the chart except the specified one
   void              HideAllExceptOne(const long pos_id, const bool chart_redraw=false);
   
public:
//--- Return the chart (1) symbol and (2) ID
   string            Symbol(void)               const { return this.m_symbol;             }
   long              ChartID(void)              const { return this.m_chart_id;           }

//--- Create and update the list of positions. It can be redefined in the inherited classes
   virtual bool      Refresh(void);
   
//--- Return the number of positions in the list
   int               Total(void)                const { return this.m_list_pos.Total();   }

//--- (1) Hide and (2) display the graphical representation of the first position on the chart
   void              HideFirst(const bool chart_redraw=false);
   void              ShowFirst(const bool chart_redraw=false);
   
//--- (1) Hide and (2) display the graphical representation of the last position on the chart
   void              HideLast(const bool chart_redraw=false);
   void              ShowLast(const bool chart_redraw=false);
   
//--- Display a graphical representation of the (1) current, (2) previous and (3) next position
   void              ShowCurrent(const bool chart_redraw=false);
   void              ShowPrev(const bool chart_redraw=false);
   void              ShowNext(const bool chart_redraw=false);

//--- Return the description of the currently selected position
   string            CurrentSelectedDescription(void);

//--- Print the properties of all positions and their deals in the journal
   void              Print(void);

//--- Constructor/destructor
                     CPositionsControl(const string symbol=NULL);
                    ~CPositionsControl();
  };

Veamos en detalle los métodos declarados.


Constructor de clase:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CPositionsControl::CPositionsControl(const string symbol=NULL)
  {
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
   this.m_symbol     =  (symbol==NULL ? ::Symbol() : symbol);
   this.m_chart_id   =  ::ChartID();
   this.m_current_id =  0;
   this.m_key_ctrl   =  ::ChartGetInteger(this.m_chart_id, CHART_KEYBOARD_CONTROL);
  }

Establece el indicador de ordenación por tiempo de cierre en milisegundos para la lista de posiciones históricas. Establezca el símbolo pasado al constructor en la variable. Si se trata de una cadena vacía, entonces es el símbolo actual del gráfico en el que se está ejecutando el programa. La ID del gráfico se establece como la ID del gráfico actual. Será posible establecer una ID diferente en las clases heredadas. En la variable m_key_ctrl, establece la bandera que permite controlar el gráfico mediante teclas. Después de que se ejecute el programa, este valor se restablecerá a la propiedad del gráfico para devolverlo al estado anterior al inicio del programa.


En el destructor de la clase, la lista de posiciones se destruye, mientras que la propiedad del gráfico CHART_KEYBOARD_CONTROL recupera el valor que tenía antes del lanzamiento del programa:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CPositionsControl::~CPositionsControl()
  {
   this.m_list_pos.Shutdown();
   ::ChartSetInteger(this.m_chart_id, CHART_KEYBOARD_CONTROL, this.m_key_ctrl);
  }


El método que devuelve un objeto de posición de la lista por ID:

//+------------------------------------------------------------------+
//| Return the position object from the list by ID                   |
//+------------------------------------------------------------------+
CPosition *CPositionsControl::GetPositionObjByID(const long id)
  {
//--- Set the position ID for the temporary object and set the flag of sorting by position ID for the list
   this.m_temp_pos.SetID(id);
   this.m_list_pos.Sort(SORT_MODE_POSITION_IDENTIFIER);
//--- Get the index of the position object with such an ID (or -1 if it is absent) from the list
//--- Use the obtained index to get the pointer to the position object from the list (or NULL if the index value is -1)
   int index=this.m_list_pos.Search(&this.m_temp_pos);
   CPosition *pos=this.m_list_pos.At(index);
//--- Set the flag of sorting by position close time in milliseconds for the list and
//--- return the pointer to the position object (or NULL if it is absent)
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
   return pos;
  }

La lógica del método se describe completamente en los comentarios del código.


El método que devuelve el marcador (flag) de posición de mercado:

//+------------------------------------------------------------------+
//| Return the market position flag                                  |
//+------------------------------------------------------------------+
bool CPositionsControl::IsMarketPosition(const long id)
  {
//--- In a loop by the list of current positions in the terminal
   for(int i=::PositionsTotal()-1; i>=0; i--)
     {
      //--- get the position ticket by the loop index
      ulong ticket=::PositionGetTicket(i);
      //--- If the ticket is received, the position can be selected and its ID is equal to the one passed to the method,
      //--- this is the desired market position, return 'true'
      if(ticket!=0 && ::PositionSelectByTicket(ticket) && ::PositionGetInteger(POSITION_IDENTIFIER)==id)
         return true;
     }
//--- No such market position, return 'false'
   return false;
  }

Al crear una lista de posiciones a partir de una lista de operaciones, es importante no tener en cuenta las posiciones actuales del mercado y evitar añadirlas a la lista de posiciones históricas. Para saber que una posición aún no está cerrada, necesitamos encontrarla en la lista de posiciones activas por su ID. Si tal puesto existe, no es necesario añadirlo a la lista. El método busca posiciones de la lista de posiciones del mercado por su ticket, verifica si la ID de la posición coincide con el valor pasado al método y, si dicha posición existe (se puede seleccionar), devuelve verdadero. De lo contrario, vuelve FALSO.


El método que devuelve el puntero a la primera posición cerrada de la lista:

//+------------------------------------------------------------------+
//| Return the pointer to the first closed position in the list      |
//+------------------------------------------------------------------+
CPosition *CPositionsControl::GetFirstClosedPosition(void)
  {
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
   return this.m_list_pos.At(0);
  }

La lista de posiciones se ordena por tiempo de cierre de posición en milisegundos y se devuelve un puntero a la primera posición (la más antigua) de la lista.


El método que devuelve el puntero a la última posición cerrada de la lista:

//+------------------------------------------------------------------+
//| Return the pointer to the last closed position in the list       |
//+------------------------------------------------------------------+
CPosition *CPositionsControl::GetLastClosedPosition(void)
  {
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
   return this.m_list_pos.At(this.m_list_pos.Total()-1);
  }

La lista de posiciones se ordena por tiempo de cierre de posición en milisegundos y se devuelve un puntero a la última posición de la lista.


El método que devuelve el puntero a la posición cerrada anterior en la lista:

//+------------------------------------------------------------------+
//| Return the pointer to the previous closed position in the list   |
//+------------------------------------------------------------------+
CPosition *CPositionsControl::GetPrevClosedPosition(CPosition *current)
  {
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
   int prev=this.m_list_pos.SearchLess(current);
   return this.m_list_pos.At(prev);
  }

A partir de la posición actualmente seleccionada, cuyo puntero se pasa al método, se busca la anterior en la lista, ordenada por tiempo de cierre en milisegundos. El método SearchLess() de la clase CArrayObj devuelve el puntero al primer objeto encontrado en la lista que tenga el valor más bajo, por el que se ordena la lista. En este caso, la lista se ordena por la hora de cierre en milisegundos. En consecuencia, el primer objeto encontrado con un tiempo de cierre inferior al pasado al método es la posición anterior. Si no se encuentra el objeto de posición, o se pasa al método el primer elemento de la lista (no hay otros anteriores), el método devuelve NULL.


El método que devuelve el puntero a la siguiente posición cerrada de la lista:

//+------------------------------------------------------------------+
//| Return the pointer to the next closed position in the list       |
//+------------------------------------------------------------------+
CPosition *CPositionsControl::GetNextClosedPosition(CPosition *current)
  {
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
   int next=this.m_list_pos.SearchGreat(current);
   return this.m_list_pos.At(next);
  }

En función de la posición seleccionada actualmente, cuyo puntero se pasa al método, se busca la siguiente en la lista, ordenada por tiempo de cierre en milisegundos. El método SearchGreat() de la clase CArrayObj devuelve el puntero al primer objeto encontrado en la lista que tenga el valor más alto, por el que se ordena la lista. En este caso, la lista se ordena por la hora de cierre en milisegundos. En consecuencia, el primer objeto encontrado con un tiempo de cierre mayor que el pasado al método es la siguiente posición. Si no se encuentra el objeto de posición, o se pasa al método el último elemento de la lista (no hay siguientes), el método devuelve NULL.


Método que muestra una representación gráfica de una posición determinada en el gráfico:

//+----------------------------------------------------------------------------+
//| Display a graphical representation of the specified position on the chart  |
//+----------------------------------------------------------------------------+
void CPositionsControl::Show(CPosition *pos,const bool chart_redraw=false)
  {
   if(pos!=NULL)
     {
      pos.Show(chart_redraw);
      this.m_current_id=pos.ID();
     }
  }

El método recibe el puntero de la posición cuya representación gráfica debe mostrarse en el gráfico. Si se recibe un objeto válido, llama a su método Show() y añade la ID de posición a la variable m_current_id. Podemos definir la posición seleccionada actual mediante lal ID de posición en la variable m_current_id. Una posición se considera seleccionada si su representación gráfica aparece en el gráfico.


El método oculta la representación gráfica de la primera posición del gráfico:

//+------------------------------------------------------------------------+
//| Hide the graphical representation of the first position from the chart |
//+------------------------------------------------------------------------+
void CPositionsControl::HideFirst(const bool chart_redraw=false)
  {
   CPosition *pos=this.GetFirstClosedPosition();
   if(pos!=NULL)
      pos.Hide(chart_redraw);
  }

Obtener el puntero a la primera posición de la lista y llamar a su método Hide().


El método que muestra una representación gráfica de la primera posición en el gráfico:

//+---------------------------------------------------------------------------+
//| Display the graphical representation of the first position on the chart   |
//+---------------------------------------------------------------------------+
void CPositionsControl::ShowFirst(const bool chart_redraw=false)
  {
//--- Get the pointer to the first closed position
   CPosition *pos=this.GetFirstClosedPosition();
   if(pos==NULL)
      return;
//--- Hide labels of all positions except the first one by its ID
   this.HideAllExceptOne(pos.ID());
   
//--- Display the labels of the first position on the chart and
//--- center the chart by the labels of the currently selected position
   this.Show(pos,chart_redraw);
   this.CentersChartByCurrentSelected();
  }

Utilice el método GetFirstClosedPosition() considerado anteriormente para obtener el puntero a la primera posición de la lista, ordenada por tiempo en milisegundos. Oculta todas las etiquetas de todas las posiciones excepto la primera, muestra la primera posición y centra el gráfico en las etiquetas de sus tratos en el gráfico.


El método que oculta la representación gráfica de la última posición del gráfico:

//+------------------------------------------------------------------+
//| Hide graphical representation of the last position from the chart|
//+------------------------------------------------------------------+
void CPositionsControl::HideLast(const bool chart_redraw=false)
  {
   CPosition *pos=this.GetLastClosedPosition();
   if(pos!=NULL)
      pos.Hide(chart_redraw);
  }

Obtener el puntero a la última posición de la lista y llamar a su método Hide().


El método que muestra una representación gráfica de la última posición en el gráfico:.

//+-----------------------------------------------------------------------+
//| Display the graphical representation of the last position on the chart|
//+-----------------------------------------------------------------------+
void CPositionsControl::ShowLast(const bool chart_redraw=false)
  {
//--- Get the pointer to the last closed position
   CPosition *pos=this.GetLastClosedPosition();
   if(pos==NULL)
      return;
//--- Hide labels of all positions except the last one by its ID
   this.HideAllExceptOne(pos.ID(), false);
   
//--- Display the labels of the last position on the chart and
//--- center the chart by the labels of the currently selected position
   this.Show(pos,chart_redraw);
   this.CentersChartByCurrentSelected();
  }

Utilice el método GetLastClosedPosition() considerado anteriormente para obtener el puntero a la última posición de la lista, ordenada por tiempo en milisegundos. Oculta todas las etiquetas de todas las posiciones excepto la última, muestra la última posición y centra el gráfico en las etiquetas de sus tratos en el gráfico.


El método que muestra una representación gráfica de la posición actual en el gráfico:.

//+--------------------------------------------------------------------------+
//| Display a graphical representation of the current position on the chart  |
//+--------------------------------------------------------------------------+
void CPositionsControl::ShowCurrent(const bool chart_redraw=false)
  {
//--- Get a pointer to the currently selected closed position
   CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID());
   if(curr==NULL)
      return;
//--- Display the labels of the current position on the chart and
//--- center the chart by the labels of the currently selected position
   this.Show(curr,chart_redraw);
   this.CentersChartByCurrentSelected();
  }

Obtener el puntero a una posición cuyo ID se establece en la variable m_current_id, mostrar su representación gráfica en el gráfico y centrar el gráfico por sus etiquetas de reparto en el gráfico.


El método que muestra una representación gráfica de la posición anterior en el gráfico:.

//+------------------------------------------------------------------------+
//|Display a graphical representation of the previous position on the chart|
//+------------------------------------------------------------------------+
void CPositionsControl::ShowPrev(const bool chart_redraw=false)
  {
//--- Get the pointer to the current and previous positions
   CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID());
   CPosition *prev=this.GetPrevClosedPosition(curr);
   if(curr==NULL || prev==NULL)
      return;
//--- Hide the current position, display the previous one and
//--- center the chart by the labels of the currently selected position
   curr.Hide();
   this.Show(prev,chart_redraw);
   this.CentersChartByCurrentSelected();
  }

Aquí obtenemos los punteros a las posiciones actualmente seleccionada y anterior en la lista. Oculta las etiquetas de la posición actual y muestra las de la anterior. Cuando se muestran, la ID de la posición se establece en la variable m_current_id indicando que esta posición es ahora la actual. Utilice sus etiquetas para centrar el gráfico.


El método que muestrauna representación gráfica de la siguiente posición en el gráfico:

//+---------------------------------------------------------------------+
//| Display a graphical representation of the next position on the chart|
//+---------------------------------------------------------------------+
void CPositionsControl::ShowNext(const bool chart_redraw=false)
  {
//--- Get the pointer to the current and next positions
   CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID());
   CPosition *next=this.GetNextClosedPosition(curr);
   if(curr==NULL || next==NULL)
      return;
//--- Hide the current position, display the next one and
//--- center the chart by the labels of the currently selected position
   curr.Hide();
   this.Show(next,chart_redraw);
   this.CentersChartByCurrentSelected();
  }

El método es idéntico al anterior, salvo que aquí obtenemos los punteros a las posiciones actualmente seleccionada y siguiente de la lista. Oculta las etiquetas de la posición actual y muestra los iconos de la siguiente. Cuando se muestran, la ID de la posición se establece en la variable m_current_id indicando que esta posición es ahora la actual. Utilice sus etiquetas para centrar el gráfico.


El método oculta la representación gráfica de todas las posiciones del gráfico excepto la especificada:

//+------------------------------------------------------------------+
//| Hide the graphical representation                                |
//| of all positions except the specified one                        |
//+------------------------------------------------------------------+
void CPositionsControl::HideAllExceptOne(const long pos_id,const bool chart_redraw=false)
  {
//--- In a loop by the list of positions
   int total=this.m_list_pos.Total();
   for(int i=0; i<total; i++)
     {
      //--- get the pointer to the next position and
      CPosition *pos=this.m_list_pos.At(i);
      if(pos==NULL || pos.ID()==pos_id)
         continue;
      //--- hide the graphical representation of the position
      pos.Hide();
     }
//--- After the loop, update the chart if the flag is set
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

El método recibe la ID de la posición cuyas etiquetas deben dejarse en el gráfico. Las etiquetas de las demás posiciones están ocultas.


El método que centra el gráfico en la posición actualmente seleccionada:

//+------------------------------------------------------------------+
//| Center the chart at the currently selected position              |
//+------------------------------------------------------------------+
void CPositionsControl::CentersChartByCurrentSelected(void)
  {
//--- Get the index of the first visible bar on the chart and the number of visible bars
   int bar_open=0, bar_close=0;
   int first_visible=(int)::ChartGetInteger(this.m_chart_id, CHART_FIRST_VISIBLE_BAR);
   int visible_bars =(int)::ChartGetInteger(this.m_chart_id, CHART_VISIBLE_BARS);
   
//--- Get the position opening time and use it to get the opening bar
   datetime time_open=this.TimeOpenCurrentSelected();
   if(time_open!=0)
      bar_open=::iBarShift(this.m_symbol, PERIOD_CURRENT, time_open);
   
//--- Get the position opening time and use it to get the closing bar
   datetime time_close=this.TimeCloseCurrentSelected();
   if(time_close!=0)
      bar_close=::iBarShift(this.m_symbol, PERIOD_CURRENT, time_close);
      
//--- Calculate the width of the window the deal labels are located in
   int width=bar_open-bar_close;
   
//--- Calculate the chart offset so that the window with deals is in the center of the chart
   int shift=(bar_open + visible_bars/2 - width/2);
   
//--- If the window width is greater than the chart width, the opening deal is located on the second visible bar
   if(shift-bar_open<0)
      shift=bar_open+1;
   
//--- If the deal opening bar is to the left of the first visible bar of the chart
//--- or the deal opening bar is to the right of the chart last visible bar,
//--- scroll the chart by the calculated offset
   if(bar_open>first_visible || bar_open<first_visible+visible_bars)
      ::ChartNavigate(this.m_chart_id, CHART_CURRENT_POS, first_visible-shift);
  }

La lógica completa del método se describe en los comentarios del código. El método desplaza el gráfico de símbolos para que visualmente todas las ofertas de posición con la línea de conexión se sitúen en el centro del gráfico. Si las etiquetas de todas las operaciones no caben en el ancho del gráfico, éste se desplaza de modo que la posición de apertura de la operación se encuentre en la segunda barra visible del gráfico desde la izquierda.


Método que devuelve la hora de apertura de la posición actualmente seleccionada:

//+------------------------------------------------------------------+
//| Return the opening time of the currently selected position       |
//+------------------------------------------------------------------+
datetime CPositionsControl::TimeOpenCurrentSelected(void)
  {
   CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID());
   return(pos!=NULL ? pos.Time() : 0);
  }

Obtiene el puntero a la posición seleccionada actual por su ID establecida en la variable m_current_id. Si el puntero se recibe correctamente, devuelve el tiempo de apertura de posición. En caso contrario, devuelve cero.


El método que devuelve la hora de cierre de la posición actualmente seleccionada:

//+------------------------------------------------------------------+
//| Return the current selected position closing time                |
//+------------------------------------------------------------------+
datetime CPositionsControl::TimeCloseCurrentSelected(void)
  {
   CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID());
   return(pos!=NULL ? pos.TimeClose() : 0);
  }

Obtiene el puntero a la posición seleccionada actual por su ID establecida en la variable m_current_id. Si el puntero se recibe correctamente, devuelve la hora de cierre de posición. En caso contrario, devuelve cero.


El método que devuelve un tipo de posición por un tipo de operación:

//+------------------------------------------------------------------+
//| Return position type by deal type                                |
//+------------------------------------------------------------------+
ENUM_POSITION_TYPE CPositionsControl::PositionTypeByDeal(const CDeal *deal)
  {
   if(deal==NULL)
      return WRONG_VALUE;
   switch(deal.TypeDeal())
     {
      case DEAL_TYPE_BUY   :  return POSITION_TYPE_BUY;
      case DEAL_TYPE_SELL  :  return POSITION_TYPE_SELL;
      default              :  return WRONG_VALUE;
     }
  }

El método recibe el puntero a la operación. En función del tipo de operación, devuelve el tipo de posición adecuado.


El método que crea la lista de posiciones históricas:

//+------------------------------------------------------------------+
//| Create historical position list                                  |
//+------------------------------------------------------------------+
bool CPositionsControl::Refresh(void)
  {
//--- If failed to request the history of deals and orders, return 'false'
   if(!::HistorySelect(0,::TimeCurrent()))
      return false;
      
//--- Set the flag of sorting by time in milliseconds for the position list
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_MSC);
   
//--- Declare a result variable and a pointer to the position object
   bool res=true;
   CPosition *pos=NULL;

//--- In a loop based on the number of history deals
   int total=::HistoryDealsTotal();
   for(int i=total-1; i>=0; i--)
     {
      //--- get the ticket of the next deal in the list
      ulong ticket=::HistoryDealGetTicket(i);
      
      //--- If the deal ticket is not received, or it is not a buy/sell deal, or if the deal is not for the symbol set for the class, move on
      ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE);
      if(ticket==0 || (deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL) || ::HistoryDealGetString(ticket, DEAL_SYMBOL)!=this.m_symbol)
         continue;
      
      //--- Get the value of the position ID from the deal 
      long pos_id=::HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
      
      //--- If this is a market position, move on 
      if(this.IsMarketPosition(pos_id))
         continue;
         
      //--- Get the pointer to a position object from the list
      pos=this.GetPositionObjByID(pos_id);
      
      //--- If there is no position with this ID in the list yet 
      if(pos==NULL)
        {
         //--- Create a new position object and, if the object could not be created, add 'false' to the 'res' variable and move on
         pos=new CPosition(pos_id, this.m_symbol);
         if(pos==NULL)
           {
            res &=false;
            continue;
           }
         
         //--- If failed to add the position object to the list, add 'false' to the 'res' variable, remove the position object and move on
         if(!this.m_list_pos.InsertSort(pos))
           {
            res &=false;
            delete pos;
            continue;
           }
        }
      
      //--- If the deal object could not be added to the list of deals of the position object, add 'false' to the 'res' variable and move on
      CDeal *deal=pos.DealAdd(ticket);
      if(deal==NULL)
        {
         res &=false;
         continue;
        }
      
      //--- All is successful.
      //--- Set position properties depending on the deal type
      if(deal.Entry()==DEAL_ENTRY_IN)
        {
         pos.SetTime(deal.Time());
         pos.SetTimeMsc(deal.TimeMsc());
         ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal);
         pos.SetTypePosition(type);
         pos.SetPriceOpen(deal.Price());
         pos.SetVolume(deal.Volume());
        }
      if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY)
        {
         pos.SetPriceCurrent(deal.Price());
        }
      if(deal.Entry()==DEAL_ENTRY_INOUT)
        {
         ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal);
         pos.SetTypePosition(type);
         pos.SetVolume(deal.Volume()-pos.Volume());
        }
     }
//--- Set the flag of sorting by close time in milliseconds for the position list
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
     
//--- Return the result of creating and adding a position to the list
   return res;
  }

La lógica del método se ha descrito detalladamente en los comentarios del código.

Veamos brevemente la lógica de la búsqueda de posiciones históricas: el terminal cliente sólo contiene una lista de posiciones actuales. Cada posición tiene sus propias operaciones en la lista de operaciones históricas. Cada operación contiene una ID de la posición en la que participó. Esta ID puede utilizarse para determinar la posición a la que pertenece una operación. Por consiguiente, para crear una lista de posiciones históricas, tenemos que recorrer la lista de operaciones históricas y utilizar la ID para determinar la posición a la que pertenece la operación. Asegúrese de comprobar que un puesto con esa ID no está en la lista de puestos activos. Si lo es, aún no se trata de una posición cerrada, y la operación debería saltarse. Si dicha posición ya no está en el mercado, entonces necesitamos crear un nuevo objeto de posición histórica con dicha ID y añadir esta operación a la lista de operaciones de la posición creada. Antes de crear un objeto de posición, hay que comprobar si ya se ha creado antes una posición de este tipo. En caso afirmativo, no es necesario crear un objeto de posición. En su lugar, simplemente añada este acuerdo a la lista de la posición ya creada. Por supuesto, si un trato de este tipo ya figura en la lista de posiciones, no es necesario añadirlo. Tras completar el ciclo de transacciones históricas, se creará como resultado una lista de posiciones ya cerradas, y cada posición de esta lista contendrá una lista de sus transacciones.


El método que imprime las propiedades de las posiciones y sus operaciones en el diario:

//+------------------------------------------------------------------+
//| Print the properties of positions and their deals in the journal |
//+------------------------------------------------------------------+
void CPositionsControl::Print(void)
  {
   int total=this.m_list_pos.Total();
   for(int i=0; i<total; i++)
     {
      CPosition *pos=this.m_list_pos.At(i);
      if(pos==NULL)
         continue;
      pos.Print();
     }
  }

Empezando por la posición más antigua (desde el principio de la lista), obtenemos cada posición sucesiva y enviamos su descripción al diario en un bucle.


El método devuelve la descripción de cada puesto seleccionado:

//+------------------------------------------------------------------+
//| Return the description of the currently selected position        |
//+------------------------------------------------------------------+
string CPositionsControl::CurrentSelectedDescription(void)
  {
   CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID());
   return(pos!=NULL ? pos.Tooltip() : NULL);
  }

Obtiene el puntero a la posición seleccionada actual cuyo ID se establece en la variable m_current_id y devuelve la cadena creada para mostrar la información sobre herramientas. Esta cadena puede utilizarse para mostrarla en un comentario del gráfico. Vamos a mostrarlo en el EA de prueba para una visualización de algunas propiedades de la posición cuyas etiquetas se muestran en el gráfico. El EA de prueba contendrá las siguientes funcionalidades:

  1. Al iniciarse, se crea un historial de operaciones en forma de lista de posiciones históricas. Cada posición contiene la lista de sus operaciones;
  2. Una vez completada la lista de posiciones históricas, la última posición cerrada se muestra en el gráfico como etiquetas de apertura y cierre conectadas por la línea;
  3. Puedes navegar por la lista de posiciones históricas utilizando las teclas del cursor mientras mantienes pulsada la tecla Ctrl:
    1. Tecla izquierda - muestra la posición anterior en el gráfico;
    2. Tecla derecha - muestra la siguiente posición en el gráfico; 
    3. Tecla Arriba: muestra la primera posición del gráfico; 
    4. Tecla Abajo: muestra la última posición del gráfico;
  4. Mantenga pulsada la tecla Mayús para mostrar un comentario en el gráfico que describa la posición seleccionada actualmente en la lista.


Prueba

En \MQL5\Experts\PositionsViewer\, cree un nuevo archivo EA llamado PositionViewer.mq5.

Incluir el fichero de clase de gestión de posiciones históricas, declarar macro sustituciones para los códigos clave y las variables globales del EA:

//+------------------------------------------------------------------+
//|                                               PositionViewer.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include <PositionsViewer\PositionsControl.mqh>

#define   KEY_LEFT   37
#define   KEY_RIGHT  39
#define   KEY_UP     38
#define   KEY_DOWN   40

//--- global variables
CPositionsControl ExtPositions;     // Historical position class instance 
bool              ExtChartScroll;   // Chart scrolling flag
bool              ExtChartHistory;  // Trading history display flag

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+

En el manejador OnInit() del EA, establece el código siguiente:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Save the chart auto scroll flag and disable auto scroll
   ExtChartScroll=ChartGetInteger(ChartID(), CHART_AUTOSCROLL);
   ChartSetInteger(ChartID(), CHART_AUTOSCROLL, false);
   
//--- Save the trading history display flag and disable history display
   ExtChartHistory=ChartGetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY);
   ChartSetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY, false);
   
//--- Create a list of closed positions and display the list creation time in the journal
   ulong start=GetTickCount64();
   Print("Reading trade history and creating a list of historical positions");
   ExtPositions.Refresh();
   ulong msec=GetTickCount64()-start;
   PrintFormat("List of historical positions created in %I64u msec", msec);
   //ExtPositions.Print();
   
//--- If this is a launch after changing the chart period, display the currently selected position
   if(UninitializeReason()==REASON_CHARTCHANGE)
      ExtPositions.ShowCurrent(true);
//--- otherwise, display the last closed position
   else
      ExtPositions.ShowLast(true);

//--- Successful
   return(INIT_SUCCEEDED);
  }

Las marcas (flags) de desplazamiento automático del gráfico y de visualización del historial de operaciones se guardan aquí para restaurarlas cuando se cierre el programa. El desplazamiento automático del gráfico y la visualización del historial están desactivados, se crea una lista de todas las posiciones históricas que han existido alguna vez en el símbolo actual y la hora de creación de la lista se muestra en el diario. Si descomentamos la cadena //ExtPositions.Print();, después de crear la lista de posiciones cerradas, se mostrarán en el diario todas las posiciones históricas de la lista creada. Si no se trata de un cambio del marco temporal del gráfico, las etiquetas de la última posición cerrada se dibujan en el gráfico y éste se desplaza para que las etiquetas queden en su centro. Si se trata de un cambio del periodo del gráfico, se muestra la posición seleccionada actualmente en la lista.


En el manejador OnDeinit(), restaure los valores guardados del desplazamiento automático del gráfico y de la visualización del historial de operaciones, y elimine los comentarios del gráfico:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Restore the auto scroll and trading history property initial value and remove chart comments
   ChartSetInteger(ChartID(), CHART_AUTOSCROLL, ExtChartScroll);
   ChartSetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY, ExtChartHistory);
   Comment("");
  }


En el manejador OnTradeTransaction(), llamar al método de actualización de la lista de la clase de control de posiciones históricas cada vez que se produzca una nueva operación. Si la posición está cerrada, muestra sus etiquetas en el gráfico:

//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {
//--- In case of a transaction type, add a new deal
   if(trans.type==TRADE_TRANSACTION_DEAL_ADD)
     {
      //--- update the list of positions and their deals
      ExtPositions.Refresh();
      
      //--- Get the new deal ticket
      ulong deal_ticket=trans.deal;
      
      //--- If the ticket is not received or failed to get the method for updating the position from the deal properties, leave
      long entry;
      if(deal_ticket==0 || !HistoryDealGetInteger(deal_ticket, DEAL_ENTRY, entry))
         return;

      //--- If this is an exit deal, display the last closed position on the chart
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_OUT_BY)
         ExtPositions.ShowLast(true);
     }
  }


En el manejador de eventos, rastrea eventos de pulsación de teclas y responde a las teclas de cursor pulsadas mientras mantienes presionada la tecla Ctrl para navegar por la lista de posiciones cerradas. Además, responde a la tecla Mayúsculas para mostrar una descripción de la posición en el comentario del gráfico, así como responde a cambiar la escala del gráfico para centrarlo por las etiquetas de posición actuales:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- If the event ID is pressing a key
   if(id==CHARTEVENT_KEYDOWN)
     {
      //--- If the Ctrl key is held down
      if(TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)<0)
        {
         //--- If the chart scrolling with keys is active, disable it
         if((bool)ChartGetInteger(0, CHART_KEYBOARD_CONTROL))
            ChartSetInteger(0, CHART_KEYBOARD_CONTROL, false);
            
         //--- If the left key is pressed, display the previous closed position 

         if(lparam==KEY_LEFT)
            ExtPositions.ShowPrev(true);
            
         //--- If the right key is pressed, display the next closed position
         if(lparam==KEY_RIGHT)
            ExtPositions.ShowNext(true);
            
         //--- If the up key is pressed, display the first closed position
         if(lparam==KEY_UP)
            ExtPositions.ShowFirst(true);
            
         //--- If the down key is pressed, display the last closed position
         if(lparam==KEY_DOWN)
            ExtPositions.ShowLast(true);
        }
      //--- If Ctrl is not pressed,
      else
        {
         //--- If the chart scrolling with keys is inactive, enable it
         if(!(bool)ChartGetInteger(0, CHART_KEYBOARD_CONTROL))
            ChartSetInteger(0, CHART_KEYBOARD_CONTROL, true);
        }
     }

//--- If the Shift key is held down, display a description of the current position in the chart comment
   if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT)<0)
      Comment(ExtPositions.CurrentSelectedDescription());
//--- If the Shift key is not pressed, check the comment on the chart and delete it if it is not empty
   else
     {
      if(ChartGetString(ChartID(),CHART_COMMENT)!=NULL)
         Comment("");
     }
      
//--- If the horizontal scale of the chart has changed, display the currently selected position
   static int scale=-1;
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      int scale_curr=(int)ChartGetInteger(ChartID(), CHART_SCALE);
      if(scale!=scale_curr)
        {
         ExtPositions.ShowCurrent(true);
         scale=scale_curr;
        }
     }
  }


Se trata de la disposición de las posiciones cerradas con una visualización estándar del historial de operaciones:


Esta es una buena ilustración de lo escrito al principio del artículo:

... la visualización del historial de operaciones puede resultar ineficaz y confusa debido a la sobrecarga del gráfico con etiquetas de posiciones abiertas y cerradas. Esto es especialmente cierto para los operadores que trabajan con un gran número de operaciones, donde el gráfico se llena rápidamente, lo que hace casi imposible analizar la actividad comercial y tomar decisiones informadas ...

Ahora vamos a compilar el EA y lanzarlo al gráfico:


En el momento del lanzamiento, el historial mostrado por medios estándar estaba oculto y sólo quedaba visible la última posición cerrada. Podemos navegar por el historial de posiciones manteniendo pulsada la tecla Ctrl y pulsando las teclas del cursor. Al desplazarnos por la lista de posiciones cerradas, podemos ver fácilmente una representación gráfica del historial de operaciones en el lugar donde se rellenó el gráfico con etiquetas de todas las operaciones de las posiciones cerradas aquí.

Si además mantenemos pulsada la tecla Mayús, en el comentario del gráfico aparece una descripción de la posición cerrada seleccionada en ese momento.

Ahora busquemos una posición cuyas operaciones estén situadas a una distancia suficiente entre sí, y veamos cómo se centran en el gráfico si no caben en una zona visible de la ventana del gráfico cuando se aumenta su escala horizontal:


Vemos que si ambas operaciones no caben en la ventana del gráfico, ésta se centra para que sea visible la operación que entra en la posición de la segunda barra desde la izquierda.

Si pulsamos la tecla Mayús, veremos una descripción de esta posición cerrada en el comentario del gráfico:


Esto es conveniente porque nos permite ver una descripción de la posición sin pasar el cursor del ratón sobre la línea que conecta los tratos. Si nos desplazamos por la lista de posiciones cerradas utilizando las teclas del cursor, manteniendo pulsadas las teclas Ctrl + Mayús, podremos ver claramente la descripción de la posición cerrada que se muestra actualmente en el gráfico.


Al lanzar el EA se inicia la creación de la lista de posiciones históricas. En mi caso, con un breve historial desde 2023, momento de la creación de la lista:

PositionViewer (EURUSD,M15)     Reading trade history and creating a list of historical positions
PositionViewer (EURUSD,M15)     List of historical positions created in 6422 msec

El cambio de periodo de gráfico posterior ya no requiere volver a crear la lista:

PositionViewer (EURUSD,M1)      Reading trade history and creating a list of historical positions
PositionViewer (EURUSD,M1)      List of historical positions created in 31 msec
PositionViewer (EURUSD,M5)      Reading trade history and creating a list of historical positions
PositionViewer (EURUSD,M5)      List of historical positions created in 47 msec
PositionViewer (EURUSD,M1)      Reading trade history and creating a list of historical positions
PositionViewer (EURUSD,M1)      List of historical positions created in 31 msec


Conclusión

El historial de operaciones proporcionado por el programa creado es más conveniente en caso de operaciones activas y frecuentes - el gráfico nunca se sobrecarga con etiquetas de posiciones cerradas - cada posición se muestra de forma independiente, y el cambio entre la visualización de la posición actual y la siguiente o anterior se realiza con las teclas del cursor. Cada etiqueta de operación tiene más información en su tooltip que el historial de operaciones estándar. Si pasa el ratón por encima de la línea que une las operaciones, aparecerá un tooltip con información sobre la posición cerrada.

Las clases creadas en el artículo se pueden utilizar en nuestros propios desarrollos, ampliar la funcionalidad y crear programas más complejos y funcionales con información sobre el historial de operaciones y el historial de posiciones cerradas.

Todos los archivos de clase y el test del EA se adjuntan al artículo y están disponibles para su estudio independiente. El archivo MQL5.zip contiene los archivos de tal manera que pueden ser inmediatamente descomprimidos en el directorio del terminal MQL5, y la nueva carpeta PositionsViewer\ se creará en la carpeta Experts junto con todos los archivos del proyecto: el archivo PositionViewer.mq5 (EA) y tres archivos de clase incluidos. Podemos compilar los archivos y utilizarlos.


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

Archivos adjuntos |
Deal.mqh (64.2 KB)
Position.mqh (67.96 KB)
PositionViewer.mq5 (12.39 KB)
MQL5.zip (22.77 KB)
Desarrollo de un sistema de repetición (Parte 69): Ajuste del tiempo (II) Desarrollo de un sistema de repetición (Parte 69): Ajuste del tiempo (II)
Aquí entenderemos por qué necesitamos utilizar la función iSpread. Al mismo tiempo, comprenderemos cómo el sistema nos informa del tiempo restante de la barra cuando no hay ticks disponibles para hacerlo. El contenido presentado aquí tiene como único propósito la enseñanza y la didáctica. En ningún caso debe considerarse una aplicación cuya finalidad no sea el aprendizaje y el estudio de los conceptos mostrados.
Desarrollo de Sistemas Avanzados de Trading ICT: Implementación de señales en un indicador de Order Blocks Desarrollo de Sistemas Avanzados de Trading ICT: Implementación de señales en un indicador de Order Blocks
En este artículo, aprenderás a desarrollar un indicador de Order Blocks basado en el volumen de la profundidad de mercado y a optimizarlo mediante buffers para mejorar su precisión. Concluimos esta fase del proyecto y nos preparamos para las siguientes, en las que implementaremos una clase de gestión de riesgos y un bot de trading que aprovechará las señales generadas por el indicador.
Desarrollo de un sistema de repetición (Parte 70): Ajuste del tiempo (III) Desarrollo de un sistema de repetición (Parte 70): Ajuste del tiempo (III)
En este artículo, mostraré cómo utilizar la función CustomBookAdd de manera correcta y funcional. Aunque pueda parecer sencillo, tiene muchas implicaciones. Por ejemplo, permite indicar al indicador de mouse si el símbolo personalizado está en subasta, en negociación o si el mercado está cerrado. El contenido expuesto aquí tiene como único objetivo ser didáctico. En ningún caso debe considerarse una aplicación cuya finalidad sea distinta a la de aprender y estudiar los conceptos mostrados.
Desarrollo de un sistema de repetición (Parte 68): Ajuste del tiempo (I) Desarrollo de un sistema de repetición (Parte 68): Ajuste del tiempo (I)
A continuación, continuaremos con el trabajo de lograr que el indicador del mouse nos informe sobre el tiempo restante de la barra en momentos de baja liquidez. Aunque a primera vista parece sencillo, verás que esta tarea es mucho más complicada. Esto se debe a algunos obstáculos que tendremos que superar. Por eso, es importante que sigas esta primera parte para poder comprender las siguientes.