English Русский 中文 Deutsch 日本語 Português
preview
Monitoreo de transacciones usando notificaciones push: ejemplo de un servicio en MetaTrader 5

Monitoreo de transacciones usando notificaciones push: ejemplo de un servicio en MetaTrader 5

MetaTrader 5Ejemplos | 20 febrero 2025, 12:51
291 0
Artyom Trishkin
Artyom Trishkin

Contenido


Introducción

Al negociar en los mercados financieros, un componente importante es la disponibilidad de información sobre los resultados de las transacciones realizadas durante un determinado periodo de tiempo.

Probablemente, cada tráder al menos una vez se ha enfrentado a la necesidad de monitorear los resultados comerciales del último día, semana, mes, etc., con el fin de ajustar su estrategia según los resultados de la negociación. El terminal de cliente MetaTrader 5 ofrece buenas estadísticas en forma de informes, permitiéndole evaluar los resultados de sus transacciones en un formato visual cómodo. Los informes pueden ayudarle a optimizar su portafolio, comprender cómo reducir los riesgos y aumentar la estabilidad de su negociación.

Para analizar su estrategia, deberá clicar en “Informe \ Revisar” en el menú contextual de la sección de historial comercial o en “Informes” en el menú “Ver” (o simplemente presionando Alt+E):



Podrá leer más información sobre los informes en el terminal MetaTrader 5, en el artículo "Nuevo informe en MetaTrader: los 5 indicadores comerciales más importantes" .

Si por alguna razón los informes estándar ofrecidos por el terminal de cliente no resultan suficientes, el lenguaje MQL5 ofrece amplias oportunidades para crear sus propios programas, incluso para generar informes y enviarlos al smartphone del tráder. Esta es la posibilidad que discutiremos hoy.

Nuestro programa deberá ponerse en marcha al iniciar el terminal, y monitorear el cambio de cuenta o cuenta comercial, el inicio del día y la hora de creación y el envío de informes. Para dichos fines nos conviene el tipo de programa “Servicio”.

Como se desprende de la ayuda, un Servicio es un programa que, a diferencia de los indicadores, asesores y scripts, no requiere una conexión a un gráfico para funcionar. Al igual que los scripts, los servicios no procesan ningún evento que no sea el evento de inicio. Para iniciar un servicio, su código deberá contener la función del manejador OnStart. Los servicios no aceptan ningún otro evento que no sea Start, pero pueden enviar eventos personalizados a los gráficos usando EventChartCustom. Los servicios se almacenan en el directorio <directorio_terminal>\MQL5\Services .

Cada servicio que se ejecuta en el terminal se ejecutará en su propio hilo. Esto significa que un servicio en bucle no puede influir en el funcionamiento de otros programas. Nuestro servicio deberá funcionar en un ciclo infinito, verificar el tiempo especificado, leer la historia comercial completa, crear listas de posiciones cerradas, filtrar estas listas según diferentes criterios y mostrar informes sobre ellas en el diario de registro y en Notificaciones push en el smartphone del usuario. Además, al iniciar el servicio por primera vez o modificar su configuración, el servicio deberá comprobar la posibilidad de enviar notificaciones Push desde el terminal. Y para ello deberemos organizar la correlación interactiva con el usuario mediante ventanas de mensajes con la expectativa de una respuesta y reacción por parte del usuario. Además, al enviar notificaciones Push, existen limitaciones en la frecuencia de las notificaciones por unidad de tiempo, por lo que será necesario organizar retrasos en el envío de notificaciones. Y todo esto no deberá influir de ninguna manera en el funcionamiento de otras aplicaciones que se ejecuten en el terminal del cliente. En base a lo dicho, los Servicios serán la herramienta más cómoda para crear un proyecto de este tipo.

Ya hemos decidido el tipo de programa. Ahora deberemos formarnos una idea sobre los componentes necesarios para ensamblar todo lo planeado.


Estructura del proyecto

Vamos a analizar el programa y sus componentes "de principio a fin":

  • Programa de servicio. Tiene acceso a los datos de todas las cuentas que han estado activas durante todo el periodo de funcionamiento continuo del servicio. Partiendo de los datos de todas las cuentas, el programa obtiene las listas de las posiciones cerradas y las combina en una lista general. Dependiendo de la configuración, el servicio puede usar datos sobre posiciones cerradas solo de la cuenta activa actual, o de la cuenta actual y de cada una de las cuentas utilizadas anteriormente en el trading en el terminal del cliente.
    A partir de los datos sobre las posiciones cerradas obtenidos de la lista de cuentas, se crean estadísticas comerciales para los periodos comerciales requeridos y se envían en notificaciones push al smartphone del usuario. Además, las estadísticas comerciales se muestran en forma de tabla en el diario del terminal "Expertos".
  • Colección de cuentas. Incluye una lista de cuentas a las que el terminal ha estado conectado durante el funcionamiento continuo del servicio. La colección de cuentas ofrece acceso a cualquier cuenta de la lista y a todas las posiciones cerradas de todas las cuentas. Las listas están disponibles en el programa de servicio y, usando estas como base, el servicio realiza selecciones y crea estadísticas.
  • Clase de objeto de cuenta. Almacena datos de una cuenta con una lista (colección) de todas las posiciones cerradas, cuyas transacciones se han realizado en esta cuenta durante el funcionamiento continuo del servicio. Ofrece acceso a las propiedades de la cuenta, para crear y actualizar una lista de posiciones cerradas de esta cuenta y retorna las listas de las posiciones cerradas según varios criterios de selección.
  • Colección de clase de posiciones históricas. Contiene una lista de objetos de posiciones, ofrece acceso a las propiedades de las posiciones cerradas, para crear y actualizar la lista de posiciones. Retorna una lista de posiciones cerradas.
  • Clase de objeto de posición. Almacena y ofrecese acceso a las propiedades de una posición cerrada. El objeto contiene la funcionalidad necesaria para comparar dos objetos según diferentes propiedades, lo que permite crear listas de posiciones según diferentes criterios de selección. Contiene una lista de transacciones para esta posición y ofrece acceso a ellas.
  • Clase de objeto de transacción. Almacena y ofrece acceso a las propiedades de una sola transacción. El objeto tiene la funcionalidad necesaria para comparar dos objetos según diferentes propiedades, lo que permite crear listas de transacciones según diferentes criterios de selección.


Ya discutimos el concepto de recreación de una posición cerrada a partir de una lista de transacciones históricas en el artículo "Cómo ver las transacciones directamente en el gráfico sin tener que perderse en el historial de transacciones". Usando como base la lista de transacciones, cada transacción se identifica como perteneciente a una posición particular utilizando el identificador de posición (PositionID) registrado en las propiedades de la transacción. Así, se crea un objeto de posición en el que las transacciones encontradas se colocan en la lista de transacciones. Aquí lo haremos de la misma manera. Pero para organizar la construcción de objetos de transacción y posición, usaremos un concepto completamente diferente y probado durante mucho tiempo, donde cada objeto tiene métodos idénticos de acceso a las propiedades para configurarlas y obtenerlas. Este concepto permite crear objetos en una única clave común, y almacenarlos en listas, así como filtrar y realizar la clasificación según cualquiera de las propiedades de los objetos y obtener nuevas listas en el contexto de la propiedad especificada.

Para comprender adecuadamente el concepto de construcción de clases en este proyecto, resulta muy recomendable la lectura de tres artículos que lo describen con gran detalle:

  1. La estructura de las propiedades de los objetos "(Parte I): Concepto, organización de datos y primeros resultados"
  2. La estructura de las listas de los objetos "(Parte II): Colección de órdenes y transacciones históricas" y
  3. Los métodos para filtrar los objetos en las listas según las propiedades "(Parte III): Colección de órdenes y posiciones de mercado, búsqueda y filtrado"

Tras leer los artículos enumerados, quedará claro el concepto completo de la construcción de objetos, su almacenamiento en listas y la obtención de varias listas filtradas según las propiedades requeridas. En esencia, los tres artículos describen la posibilidad de crear bases de datos para cualquier objeto en MQL5, almacenarlos en la base de datos y obtener las propiedades y valores requeridos. Esta es precisamente la funcionalidad que se necesita en este proyecto, y por ello hemos decidido construir los objetos y sus colecciones según el concepto descrito en los artículos, solo que aquí lo haremos de forma un poco más simple: sin crear clases de objetos abstractos con constructores protegidos y sin definir propiedades de objetos no compatibles en las clases. Todo será más sencillo: cada objeto tendrá su propia lista de propiedades almacenadas en tres arrays con la capacidad de escribir y recuperar estas propiedades. Y todos estos objetos se almacenarán en listas, donde será posible obtener nuevas listas solo de los objetos requeridos según las propiedades especificadas.

En resumen, cada objeto creado en el proyecto tendrá un conjunto de propiedades propias, como, de hecho, cualquier objeto o entidad en MQL5. Solo en MQL5 existen funciones estándar para obtener propiedades; los objetos de proyecto, entre tanto, tendrán métodos para obtener las propiedades enteras, reales y string escritas directamente en la clase de cada objeto. Luego todos estos objetos se almacenarán en listas : los arrays dinámicos de punteros a objetos CObject de la biblioteca estándar. Y son las clases de la Biblioteca Estándar las que nos permitirán crear proyectos complejos con el mínimo esfuerzo. En este caso, una base de datos de las posiciones cerradas de todas las cuentas donde se han realizado transacciones, con la posibilidad de obtener listas de objetos clasificados y seleccionados según cualquier propiedad requerida.

    Cualquier posición existe solo desde el momento en que se abre (ejecución de una transacción In) hasta el momento en que se cierra (ejecución de una transacción Out/OutBuy). Es decir, es un objeto que existe únicamente como objeto de mercado. Cualquier transacción, por el contrario, es solo un objeto histórico, ya que una transacción supone simplemente el hecho de la ejecución de una orden (orden comercial). Por ello, en el terminal del cliente no hay posiciones en la lista histórica; solo existen en la lista de posiciones actuales del mercado.

    Por consiguiente, para recrear una posición de mercado ya cerrada, es necesario “reunir” una posición previamente existente partiendo de transacciones históricas. Afortunadamente, para este propósito, cada transacción tiene un identificador de posición en cuya vida ha participado la transacción. Para hacer esto, se debe iterar la lista de transacciones históricas, obtener la siguiente transacción de la lista, crear un nuevo objeto de transacción, verificar el ID de la posición y crear un objeto de posición. El objeto de transacción creado se añade a la nueva posición histórica. Y seguiremos haciendo esto en lo sucesivo. Entretanto, crearemos las clases para el objeto de transacción y el objeto de posición, con los que continuaremos trabajando.


    Clase de transacción

    En el directorio del terminal \MQL5\Services\ crearemos una nueva carpeta AccountReporter\, y en ella un nuevo archivo Deal.mqh de la clase CDeal.
    La clase debe heredarse de la clase básica CObject de la biblioteca estándar, y su archivo debe estar incluido en la clase recién creada :

    //+------------------------------------------------------------------+
    //|                                                         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>
    
    //+------------------------------------------------------------------+
    //| Deal class                                                       |
    //+------------------------------------------------------------------+
    class CDeal : public CObject
      {
      }
    
    

    Ahora introduciremos las enumeraciones de propiedades enteras, reales y string de la transacción, y en las secciones privada, protegida y pública declararemos las variables de miembro de clase y los métodos para trabajar con las propiedades de la transacción:

    //+------------------------------------------------------------------+
    //|                                                         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>
    
    //--- Enumeration of integer deal properties
    enum ENUM_DEAL_PROPERTY_INT
      {
       DEAL_PROP_TICKET = 0,               // Deal ticket
       DEAL_PROP_ORDER,                    // Deal order number
       DEAL_PROP_TIME,                     // Deal execution time
       DEAL_PROP_TIME_MSC,                 // Deal execution time in milliseconds
       DEAL_PROP_TYPE,                     // Deal type
       DEAL_PROP_ENTRY,                    // Deal direction
       DEAL_PROP_MAGIC,                    // Deal magic number
       DEAL_PROP_REASON,                   // Deal execution reason or source
       DEAL_PROP_POSITION_ID,              // Position ID
       DEAL_PROP_SPREAD,                   // Spread when performing a deal
      };
      
    //--- Enumeration of real deal properties
    enum ENUM_DEAL_PROPERTY_DBL
      {
       DEAL_PROP_VOLUME = DEAL_PROP_SPREAD+1,// Deal volume
       DEAL_PROP_PRICE,                    // Deal price
       DEAL_PROP_COMMISSION,               // Commission
       DEAL_PROP_SWAP,                     // Accumulated swap when closing
       DEAL_PROP_PROFIT,                   // Deal financial result
       DEAL_PROP_FEE,                      // Deal fee
       DEAL_PROP_SL,                       // Stop Loss level
       DEAL_PROP_TP,                       // Take Profit level
      };
      
    //--- Enumeration of string deal properties
    enum ENUM_DEAL_PROPERTY_STR
      {
       DEAL_PROP_SYMBOL = DEAL_PROP_TP+1,  // Symbol the deal is executed for
       DEAL_PROP_COMMENT,                  // Deal comment
       DEAL_PROP_EXTERNAL_ID,              // Deal ID in an external trading system
      };
      
    //+------------------------------------------------------------------+
    //| Deal class                                                       |
    //+------------------------------------------------------------------+
    class CDeal : public CObject
      {
    private:
       MqlTick           m_tick;                                      // Deal tick structure
       long              m_lprop[DEAL_PROP_SPREAD+1];                 // Array for storing integer properties
       double            m_dprop[DEAL_PROP_TP-DEAL_PROP_SPREAD];      // Array for storing real properties
       string            m_sprop[DEAL_PROP_EXTERNAL_ID-DEAL_PROP_TP]; // Array for storing string properties
    
    //--- Return the index of the array the deal's (1) double and (2) string properties are located at
       int               IndexProp(ENUM_DEAL_PROPERTY_DBL property)   const { return(int)property-DEAL_PROP_SPREAD-1; }
       int               IndexProp(ENUM_DEAL_PROPERTY_STR property)   const { return(int)property-DEAL_PROP_TP-1;     }
       
    //--- 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:
    //--- 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
       
    public:
    //--- Set the properties
    //--- Set deal's (1) integer, (2) real and (3) string properties
       void              SetProperty(ENUM_DEAL_PROPERTY_INT property,long   value){ this.m_lprop[property]=value;                 }
       void              SetProperty(ENUM_DEAL_PROPERTY_DBL property,double value){ this.m_dprop[this.IndexProp(property)]=value; }
       void              SetProperty(ENUM_DEAL_PROPERTY_STR property,string value){ this.m_sprop[this.IndexProp(property)]=value; }
    
    //--- Integer properties
       void              SetTicket(const long ticket)              { this.SetProperty(DEAL_PROP_TICKET, ticket);                  }  // Ticket
       void              SetOrder(const long order)                { this.SetProperty(DEAL_PROP_ORDER, order);                    }  // Order
       void              SetTime(const datetime time)              { this.SetProperty(DEAL_PROP_TIME, time);                      }  // Time
       void              SetTimeMsc(const long value)              { this.SetProperty(DEAL_PROP_TIME_MSC, value);                 }  // Time in milliseconds
       void              SetTypeDeal(const ENUM_DEAL_TYPE type)    { this.SetProperty(DEAL_PROP_TYPE, type);                      }  // Type
       void              SetEntry(const ENUM_DEAL_ENTRY entry)     { this.SetProperty(DEAL_PROP_ENTRY, entry);                    }  // Direction
       void              SetMagic(const long magic)                { this.SetProperty(DEAL_PROP_MAGIC, magic);                    }  // Magic number
       void              SetReason(const ENUM_DEAL_REASON reason)  { this.SetProperty(DEAL_PROP_REASON, reason);                  }  // Deal execution reason or source
       void              SetPositionID(const long id)              { this.SetProperty(DEAL_PROP_POSITION_ID, id);                 }  // Position ID
    
    //--- Real properties
       void              SetVolume(const double volume)            { this.SetProperty(DEAL_PROP_VOLUME, volume);                  }  // Volume
       void              SetPrice(const double price)              { this.SetProperty(DEAL_PROP_PRICE, price);                    }  // Price
       void              SetCommission(const double value)         { this.SetProperty(DEAL_PROP_COMMISSION, value);               }  // Commission
       void              SetSwap(const double value)               { this.SetProperty(DEAL_PROP_SWAP, value);                     }  // Accumulated swap when closing
       void              SetProfit(const double value)             { this.SetProperty(DEAL_PROP_PROFIT, value);                   }  // Financial result
       void              SetFee(const double value)                { this.SetProperty(DEAL_PROP_FEE, value);                      }  // Deal fee
       void              SetSL(const double value)                 { this.SetProperty(DEAL_PROP_SL, value);                       }  // Stop Loss level
       void              SetTP(const double value)                 { this.SetProperty(DEAL_PROP_TP, value);                       }  // Take Profit level
    
    //--- String properties
       void              SetSymbol(const string symbol)            { this.SetProperty(DEAL_PROP_SYMBOL,symbol);                   }  // Symbol name
       void              SetComment(const string comment)          { this.SetProperty(DEAL_PROP_COMMENT,comment);                 }  // Comment
       void              SetExternalID(const string ext_id)        { this.SetProperty(DEAL_PROP_EXTERNAL_ID,ext_id);              }  // Deal ID in an external trading system
    
    //--- Get the properties
    //--- Return deal’s (1) integer, (2) real and (3) string property from the properties array
       long              GetProperty(ENUM_DEAL_PROPERTY_INT property) const { return this.m_lprop[property];                      }
       double            GetProperty(ENUM_DEAL_PROPERTY_DBL property) const { return this.m_dprop[this.IndexProp(property)];      }
       string            GetProperty(ENUM_DEAL_PROPERTY_STR property) const { return this.m_sprop[this.IndexProp(property)];      }
    
    //--- Integer properties
       long              Ticket(void)                        const { return this.GetProperty(DEAL_PROP_TICKET);                   }  // Ticket
       long              Order(void)                         const { return this.GetProperty(DEAL_PROP_ORDER);                    }  // Order
       datetime          Time(void)                          const { return (datetime)this.GetProperty(DEAL_PROP_TIME);           }  // Time
       long              TimeMsc(void)                       const { return this.GetProperty(DEAL_PROP_TIME_MSC);                 }  // Time in milliseconds
       ENUM_DEAL_TYPE    TypeDeal(void)                      const { return (ENUM_DEAL_TYPE)this.GetProperty(DEAL_PROP_TYPE);     }  // Type
       ENUM_DEAL_ENTRY   Entry(void)                         const { return (ENUM_DEAL_ENTRY)this.GetProperty(DEAL_PROP_ENTRY);   }  // Direction
       long              Magic(void)                         const { return this.GetProperty(DEAL_PROP_MAGIC);                    }  // Magic number
       ENUM_DEAL_REASON  Reason(void)                        const { return (ENUM_DEAL_REASON)this.GetProperty(DEAL_PROP_REASON); }  // Deal execution reason or source
       long              PositionID(void)                    const { return this.GetProperty(DEAL_PROP_POSITION_ID);              }  // Position ID
       
    //--- Real properties
       double            Volume(void)                        const { return this.GetProperty(DEAL_PROP_VOLUME);                   }  // Volume
       double            Price(void)                         const { return this.GetProperty(DEAL_PROP_PRICE);                    }  // Price
       double            Commission(void)                    const { return this.GetProperty(DEAL_PROP_COMMISSION);               }  // Commission
       double            Swap(void)                          const { return this.GetProperty(DEAL_PROP_SWAP);                     }  // Accumulated swap when closing
       double            Profit(void)                        const { return this.GetProperty(DEAL_PROP_PROFIT);                   }  // Financial result
       double            Fee(void)                           const { return this.GetProperty(DEAL_PROP_FEE);                      }  // Deal fee
       double            SL(void)                            const { return this.GetProperty(DEAL_PROP_SL);                       }  // Stop Loss level
       double            TP(void)                            const { return this.GetProperty(DEAL_PROP_TP);                       }  // Take Profit level
       
    //--- String properties
       string            Symbol(void)                        const { return this.GetProperty(DEAL_PROP_SYMBOL);                   }  // Symbol name
       string            Comment(void)                       const { return this.GetProperty(DEAL_PROP_COMMENT);                  }  // Comment
       string            ExternalID(void)                    const { return this.GetProperty(DEAL_PROP_EXTERNAL_ID);              }  // Deal ID in an external trading system
       
    //--- Additional properties
       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 (int)this.GetProperty(DEAL_PROP_SPREAD);              }  // Spread when performing a deal
       
    //--- 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 deal description
       string            Description(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){}
                         CDeal(const ulong ticket);
                        ~CDeal();
      };
    
    

    Vamos a analizar la implementación de los métodos de clase.

    En el constructor de la clase, debemos asumir que la transacción ya ha sido seleccionada y podemos obtener sus propiedades:

    //+------------------------------------------------------------------+
    //| Constructor                                                      |
    //+------------------------------------------------------------------+
    CDeal::CDeal(const ulong ticket)
      {
    //--- Store the properties
    //--- Integer properties
       this.SetTicket((long)ticket);                                                    // Deal ticket
       this.SetOrder(::HistoryDealGetInteger(ticket, DEAL_ORDER));                      // Order
       this.SetTime((datetime)::HistoryDealGetInteger(ticket, DEAL_TIME));              // Deal execution time
       this.SetTimeMsc(::HistoryDealGetInteger(ticket, DEAL_TIME_MSC));                 // Deal execution time in milliseconds
       this.SetTypeDeal((ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE));    // Type
       this.SetEntry((ENUM_DEAL_ENTRY)::HistoryDealGetInteger(ticket, DEAL_ENTRY));     // Direction
       this.SetMagic(::HistoryDealGetInteger(ticket, DEAL_MAGIC));                      // Magic number
       this.SetReason((ENUM_DEAL_REASON)::HistoryDealGetInteger(ticket, DEAL_REASON));  // Deal execution reason or source
       this.SetPositionID(::HistoryDealGetInteger(ticket, DEAL_POSITION_ID));           // Position ID
       
    //--- Real properties
       this.SetVolume(::HistoryDealGetDouble(ticket, DEAL_VOLUME));                     // Volume
       this.SetPrice(::HistoryDealGetDouble(ticket, DEAL_PRICE));                       // Price
       this.SetCommission(::HistoryDealGetDouble(ticket, DEAL_COMMISSION));             // Commission
       this.SetSwap(::HistoryDealGetDouble(ticket, DEAL_SWAP));                         // Accumulated swap when closing
       this.SetProfit(::HistoryDealGetDouble(ticket, DEAL_PROFIT));                     // Financial result
       this.SetFee(::HistoryDealGetDouble(ticket, DEAL_FEE));                           // Deal fee
       this.SetSL(::HistoryDealGetDouble(ticket, DEAL_SL));                             // Stop Loss level
       this.SetTP(::HistoryDealGetDouble(ticket, DEAL_TP));                             // Take Profit level
    
    //--- String properties
       this.SetSymbol(::HistoryDealGetString(ticket, DEAL_SYMBOL));                     // Symbol name
       this.SetComment(::HistoryDealGetString(ticket, DEAL_COMMENT));                   // Comment
       this.SetExternalID(::HistoryDealGetString(ticket, DEAL_EXTERNAL_ID));            // Deal ID in an external trading system
    
    //--- Additional parameters
       this.m_digits = (int)::SymbolInfoInteger(this.Symbol(), SYMBOL_DIGITS);
       this.m_point  = ::SymbolInfoDouble(this.Symbol(), SYMBOL_POINT);
       
    //--- Parameters for calculating spread
       this.m_bid = 0;
       this.m_ask = 0;
       this.SetProperty(DEAL_PROP_SPREAD, 0);
       
    //--- 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;
          int  spread=(int)::fabs((this.m_ask-this.m_bid)/this.m_point);
          this.SetProperty(DEAL_PROP_SPREAD, spread);
         }
    //--- If failed to obtain a historical tick, take the spread value of the minute bar the deal took place on
       else
          this.SetProperty(DEAL_PROP_SPREAD, this.GetSpreadM1());
      }
    
    

    Guardamos las propiedades de la transacción, Digits y Point del símbolo para el cual se ha realizado la transacción en los arrays de propiedades de clase para realizar los cálculos y mostrar la información sobre la transacción. A continuación obtenemos el tick histórico en el momento de la transacción. De esta manera ofreceremos acceso a los precios Bid y Ask en el momento de la transacción y, por lo tanto, la capacidad de calcular el spread.

    Método que compara dos objetos según una propiedad específica:

    //+------------------------------------------------------------------+
    //| 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 DEAL_PROP_TICKET      :  return(this.Ticket() > obj.Ticket()          ?  1  :  this.Ticket() < obj.Ticket()           ? -1  :  0);
          case DEAL_PROP_ORDER       :  return(this.Order() > obj.Order()            ?  1  :  this.Order() < obj.Order()             ? -1  :  0);
          case DEAL_PROP_TIME        :  return(this.Time() > obj.Time()              ?  1  :  this.Time() < obj.Time()               ? -1  :  0);
          case DEAL_PROP_TIME_MSC    :  return(this.TimeMsc() > obj.TimeMsc()        ?  1  :  this.TimeMsc() < obj.TimeMsc()         ? -1  :  0);
          case DEAL_PROP_TYPE        :  return(this.TypeDeal() > obj.TypeDeal()      ?  1  :  this.TypeDeal() < obj.TypeDeal()       ? -1  :  0);
          case DEAL_PROP_ENTRY       :  return(this.Entry() > obj.Entry()            ?  1  :  this.Entry() < obj.Entry()             ? -1  :  0);
          case DEAL_PROP_MAGIC       :  return(this.Magic() > obj.Magic()            ?  1  :  this.Magic() < obj.Magic()             ? -1  :  0);
          case DEAL_PROP_REASON      :  return(this.Reason() > obj.Reason()          ?  1  :  this.Reason() < obj.Reason()           ? -1  :  0);
          case DEAL_PROP_POSITION_ID :  return(this.PositionID() > obj.PositionID()  ?  1  :  this.PositionID() < obj.PositionID()   ? -1  :  0);
          case DEAL_PROP_SPREAD      :  return(this.Spread() > obj.Spread()          ?  1  :  this.Spread() < obj.Spread()           ? -1  :  0);
          case DEAL_PROP_VOLUME      :  return(this.Volume() > obj.Volume()          ?  1  :  this.Volume() < obj.Volume()           ? -1  :  0);
          case DEAL_PROP_PRICE       :  return(this.Price() > obj.Price()            ?  1  :  this.Price() < obj.Price()             ? -1  :  0);
          case DEAL_PROP_COMMISSION  :  return(this.Commission() > obj.Commission()  ?  1  :  this.Commission() < obj.Commission()   ? -1  :  0);
          case DEAL_PROP_SWAP        :  return(this.Swap() > obj.Swap()              ?  1  :  this.Swap() < obj.Swap()               ? -1  :  0);
          case DEAL_PROP_PROFIT      :  return(this.Profit() > obj.Profit()          ?  1  :  this.Profit() < obj.Profit()           ? -1  :  0);
          case DEAL_PROP_FEE         :  return(this.Fee() > obj.Fee()                ?  1  :  this.Fee() < obj.Fee()                 ? -1  :  0);
          case DEAL_PROP_SL          :  return(this.SL() > obj.SL()                  ?  1  :  this.SL() < obj.SL()                   ? -1  :  0);
          case DEAL_PROP_TP          :  return(this.TP() > obj.TP()                  ?  1  :  this.TP() < obj.TP()                   ? -1  :  0);
          case DEAL_PROP_SYMBOL      :  return(this.Symbol() > obj.Symbol()          ?  1  :  this.Symbol() < obj.Symbol()           ? -1  :  0);
          case DEAL_PROP_COMMENT     :  return(this.Comment() > obj.Comment()        ?  1  :  this.Comment() < obj.Comment()         ? -1  :  0);
          case DEAL_PROP_EXTERNAL_ID :  return(this.ExternalID() > obj.ExternalID()  ?  1  :  this.ExternalID() < obj.ExternalID()   ? -1  :  0);
          default                    :  return(-1);
         }
      }
    
    

    Este es un método virtual que redefine el método homónimo en la clase principal CObject. Dependiendo del modo de comparación (una de las propiedades del objeto de transacción), estas propiedades se compararán para el objeto actual y para el transmitido por puntero al método. El método retornará 1 si el valor de la propiedad del objeto actual es mayor que el valor de esta propiedad del objeto comparado. Si es menor, se retornará -1; si los valores son iguales, se retornará 0.

    Método que retorna la descripción del tipo de transacción:

    //+------------------------------------------------------------------+
    //| Return the deal type description                                 |
    //+------------------------------------------------------------------+
    string CDeal::TypeDescription(void) const
      {
       switch(this.TypeDeal())
         {
          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.TypeDeal();
         }
      }
    

    Dependiendo del tipo de transacción, se retornará su descripción de texto. Para nuestro proyecto, este método resulta redundante, ya que no utilizaremos todos los tipos de transacciones, sino solo aquellas relacionadas con la posición: compra o venta.

    Método que retorna la descripción del método para cambiar la posición:

    //+------------------------------------------------------------------+
    //| Return position change method                                    |
    //+------------------------------------------------------------------+
    string CDeal::EntryDescription(void) const
      {
       switch(this.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.Entry();
         }
      }
    
    

    Método que retorna la descripción del motivo de la transacción:

    //+------------------------------------------------------------------+
    //| Return a deal reason description                                 |
    //+------------------------------------------------------------------+
    string CDeal::ReasonDescription(void) const
      {
       switch(this.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.Reason();
         }
      }
    
    

    Método que retorna la descripción de una transacción:

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

    Método que imprime las propiedades de la transacción en el registro:

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

    Método que retorna el 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'));
      }
    
    

    Todos los métodos que retornan y registran descripciones de texto tienen como objetivo describir la transacción. En este proyecto en realidad no son necesarios, pero siempre debemos recordar las ampliaciones y mejoras, y por eso estos métodos estarán presentes aquí.

    Método que obtiene el tick de la transacción:

    //+------------------------------------------------------------------+
    //| Get the deal tick                                                |
    //| https://www.mql5.com/ru/forum/42122/page47#comment_37205238      |
    //+------------------------------------------------------------------+
    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.Symbol(), ticks, COPY_TICKS_INFO, this.TimeMsc()-(offset <<=1), this.TimeMsc());
        
    //--- 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);
      }
    
    

    La lógica del método se explica con detalle en los comentarios al código. Tras recibir un tick, se toman los precios Bid y Ask y el tamaño del spread se calcula como (Ask - Bid) / Point.

    Si como resultado no ha sido posible obtener un tick utilizando este método, entonces obtendremos el valor promedio del spread usando el método para obtener el spread de la barra de minutos de la transacción:

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

    La clase de transacción está lista. Los objetos de esta clase se almacenarán en la lista de transacciones en la clase de posición histórica, desde donde podremos obtener los punteros a las transacciones requeridas y procesar sus datos.


    Clase de posición histórica

    En la carpeta del terminal \MQL5\Services\AccountReporter\ crearemos un nuevo archivo Position.mqh de la clase CPosition.

    La clase deberá heredar de la clase de objeto básico de la biblioteca estándar CObject:

    //+------------------------------------------------------------------+
    //|                                                     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"
      
    //+------------------------------------------------------------------+
    //| Position class                                                   |
    //+------------------------------------------------------------------+
    class CPosition : public CObject
      {
      }
    
    

    Como la clase de posición contendrá una lista de transacciones para esta posición, deberemos conectar el archivo de clase de transacción y el archivo de clase del array dinámico de punteros a objetos CObject al archivo creado:

    //+------------------------------------------------------------------+
    //|                                                     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>
    
    //+------------------------------------------------------------------+
    //| Position class                                                   |
    //+------------------------------------------------------------------+
    class CPosition : public CObject
      {
      }
    
    

    Ahora introduciremos las enumeraciones de propiedades enteras, reales y string de la transacción, y en las secciones privada, protegida y pública declararemos las variables de miembro de clase y los métodos para trabajar con las propiedades de la posición:

    //+------------------------------------------------------------------+
    //|                                                     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>
    
    //--- Enumeration of integer position properties
    enum ENUM_POSITION_PROPERTY_INT
      {
       POSITION_PROP_TICKET = 0,        // Position ticket
       POSITION_PROP_TIME,              // Position open time
       POSITION_PROP_TIME_MSC,          // Position open time in milliseconds
       POSITION_PROP_TIME_UPDATE,       // Position change time
       POSITION_PROP_TIME_UPDATE_MSC,   // Position change time in milliseconds
       POSITION_PROP_TYPE,              // Position type
       POSITION_PROP_MAGIC,             // Position magic number
       POSITION_PROP_IDENTIFIER,        // Position ID
       POSITION_PROP_REASON,            // Position open reason
       POSITION_PROP_ACCOUNT_LOGIN,     // Account number
       POSITION_PROP_TIME_CLOSE,        // Position close time
       POSITION_PROP_TIME_CLOSE_MSC,    // Position close time in milliseconds
      };
      
    //--- Enumeration of real position properties
    enum ENUM_POSITION_PROPERTY_DBL
      {
       POSITION_PROP_VOLUME = POSITION_PROP_TIME_CLOSE_MSC+1,// Position volume
       POSITION_PROP_PRICE_OPEN,        // Position price
       POSITION_PROP_SL,                // Stop Loss for open position
       POSITION_PROP_TP,                // Take Profit for open position
       POSITION_PROP_PRICE_CURRENT,     // Symbol current price
       POSITION_PROP_SWAP,              // Accumulated swap
       POSITION_PROP_PROFIT,            // Current profit
       POSITION_PROP_CONTRACT_SIZE,     // Symbol trade contract size
       POSITION_PROP_PRICE_CLOSE,       // Position close price
       POSITION_PROP_COMMISSIONS,       // Accumulated commission
       POSITION_PROP_FEE,               // Accumulated payment for deals
      };
    
    //--- Enumeration of string position properties
    enum ENUM_POSITION_PROPERTY_STR
      {
       POSITION_PROP_SYMBOL = POSITION_PROP_FEE+1,// A symbol the position is open for
       POSITION_PROP_COMMENT,           // Comment to a position
       POSITION_PROP_EXTERNAL_ID,       // Position ID in the external system
       POSITION_PROP_CURRENCY_PROFIT,   // Position symbol profit currency
       POSITION_PROP_ACCOUNT_CURRENCY,  // Account deposit currency
       POSITION_PROP_ACCOUNT_SERVER,    // Server name
      };
      
    //+------------------------------------------------------------------+
    //| Position class                                                   |
    //+------------------------------------------------------------------+
    class CPosition : public CObject
      {
    private:
       long              m_lprop[POSITION_PROP_TIME_CLOSE_MSC+1];                    // Array for storing integer properties
       double            m_dprop[POSITION_PROP_FEE-POSITION_PROP_TIME_CLOSE_MSC];    // Array for storing real properties
       string            m_sprop[POSITION_PROP_ACCOUNT_SERVER-POSITION_PROP_FEE];    // Array for storing string properties
    
    //--- Return the index of the array the order's (1) double and (2) string properties are located at
       int               IndexProp(ENUM_POSITION_PROPERTY_DBL property)   const { return(int)property-POSITION_PROP_TIME_CLOSE_MSC-1;}
       int               IndexProp(ENUM_POSITION_PROPERTY_STR property)   const { return(int)property-POSITION_PROP_FEE-1;           }
       
    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;
    
    //--- Additional properties
       int               m_profit_pt;         // Profit in points
       int               m_digits;            // Symbol digits
       double            m_point;             // One symbol point value
       double            m_tick_value;        // Calculated tick value
       
    //--- Return the pointer to (1) open and (2) close deal
       CDeal            *GetDealIn(void)   const;
       CDeal            *GetDealOut(void)  const;
       
    public:
    //--- Return the list of deals
       CArrayObj        *GetListDeals(void)                              { return(&this.m_list_deals);                                  }
    
    //--- Set the properties
    //--- Set (1) integer, (2) real and (3) string properties
       void              SetProperty(ENUM_POSITION_PROPERTY_INT property,long   value)  { this.m_lprop[property]=value;                 }
       void              SetProperty(ENUM_POSITION_PROPERTY_DBL property,double value)  { this.m_dprop[this.IndexProp(property)]=value; }
       void              SetProperty(ENUM_POSITION_PROPERTY_STR property,string value)  { this.m_sprop[this.IndexProp(property)]=value; }
    
    //--- Integer properties
       void              SetTicket(const long ticket)                    { this.SetProperty(POSITION_PROP_TICKET, ticket);              }  // Position ticket
       void              SetTime(const datetime time)                    { this.SetProperty(POSITION_PROP_TIME, time);                  }  // Position open time
       void              SetTimeMsc(const long value)                    { this.SetProperty(POSITION_PROP_TIME_MSC, value);             }  // Position open time in milliseconds since 01.01.1970 
       void              SetTimeUpdate(const datetime time)              { this.SetProperty(POSITION_PROP_TIME_UPDATE, time);           }  // Position update time
       void              SetTimeUpdateMsc(const long value)              { this.SetProperty(POSITION_PROP_TIME_UPDATE_MSC, value);      }  // Position update time in milliseconds since 01.01.1970
       void              SetTypePosition(const ENUM_POSITION_TYPE type)  { this.SetProperty(POSITION_PROP_TYPE, type);                  }  // Position type
       void              SetMagic(const long magic)                      { this.SetProperty(POSITION_PROP_MAGIC, magic);                }  // Magic number for a position (see ORDER_MAGIC)
       void              SetID(const long id)                            { this.SetProperty(POSITION_PROP_IDENTIFIER, id);              }  // Position ID
       void              SetReason(const ENUM_POSITION_REASON reason)    { this.SetProperty(POSITION_PROP_REASON, reason);              }  // Position open reason
       void              SetTimeClose(const datetime time)               { this.SetProperty(POSITION_PROP_TIME_CLOSE, time);            }  // Close time
       void              SetTimeCloseMsc(const long value)               { this.SetProperty(POSITION_PROP_TIME_CLOSE_MSC, value);       }  // Close time in milliseconds
       void              SetAccountLogin(const long login)               { this.SetProperty(POSITION_PROP_ACCOUNT_LOGIN, login);        }  // Acount number
       
    //--- Real properties
       void              SetVolume(const double volume)                  { this.SetProperty(POSITION_PROP_VOLUME, volume);              }  // Position volume
       void              SetPriceOpen(const double price)                { this.SetProperty(POSITION_PROP_PRICE_OPEN, price);           }  // Position price
       void              SetSL(const double value)                       { this.SetProperty(POSITION_PROP_SL, value);                   }  // Stop Loss level for an open position
       void              SetTP(const double value)                       { this.SetProperty(POSITION_PROP_TP, value);                   }  // Take Profit level for an open position
       void              SetPriceCurrent(const double price)             { this.SetProperty(POSITION_PROP_PRICE_CURRENT, price);        }  // Current price by symbol
       void              SetSwap(const double value)                     { this.SetProperty(POSITION_PROP_SWAP, value);                 }  // Accumulated swap
       void              SetProfit(const double value)                   { this.SetProperty(POSITION_PROP_PROFIT, value);               }  // Current profit
       void              SetPriceClose(const double price)               { this.SetProperty(POSITION_PROP_PRICE_CLOSE, price);          }  // Close price
       void              SetContractSize(const double value)             { this.SetProperty(POSITION_PROP_CONTRACT_SIZE, value);        }  // Symbol trading contract size
       void              SetCommissions(void);                                                                                             // Total commission of all deals
       void              SetFee(void);                                                                                                     // Total deal fee
       
    //--- String properties
       void              SetSymbol(const string symbol)                  { this.SetProperty(POSITION_PROP_SYMBOL, symbol);              }  // Symbol a position is opened for
       void              SetComment(const string comment)                { this.SetProperty(POSITION_PROP_COMMENT, comment);            }  // Position comment
       void              SetExternalID(const string ext_id)              { this.SetProperty(POSITION_PROP_EXTERNAL_ID, ext_id);         }  // Position ID in an external system (on the exchange)
       void              SetAccountServer(const string server)           { this.SetProperty(POSITION_PROP_ACCOUNT_SERVER, server);      }  // Server name
       void              SetAccountCurrency(const string currency)       { this.SetProperty(POSITION_PROP_ACCOUNT_CURRENCY, currency);  }  // Account deposit currency
       void              SetCurrencyProfit(const string currency)        { this.SetProperty(POSITION_PROP_CURRENCY_PROFIT, currency);   }  // Profit currency of the position symbol
       
    //--- Get the properties
    //--- Return (1) integer, (2) real and (3) string property from the properties array
       long              GetProperty(ENUM_POSITION_PROPERTY_INT property) const { return this.m_lprop[property];                        }
       double            GetProperty(ENUM_POSITION_PROPERTY_DBL property) const { return this.m_dprop[this.IndexProp(property)];        }
       string            GetProperty(ENUM_POSITION_PROPERTY_STR property) const { return this.m_sprop[this.IndexProp(property)];        }
    
    //--- Integer properties
       long              Ticket(void)                              const { return this.GetProperty(POSITION_PROP_TICKET);               }  // Position ticket
       datetime          Time(void)                                const { return (datetime)this.GetProperty(POSITION_PROP_TIME);       }  // Position open time
       long              TimeMsc(void)                             const { return this.GetProperty(POSITION_PROP_TIME_MSC);             }  // Position open time in milliseconds since 01.01.1970 
       datetime          TimeUpdate(void)                          const { return (datetime)this.GetProperty(POSITION_PROP_TIME_UPDATE);}  // Position change time
       long              TimeUpdateMsc(void)                       const { return this.GetProperty(POSITION_PROP_TIME_UPDATE_MSC);      }  // Position update time in milliseconds since 01.01.1970
       ENUM_POSITION_TYPE TypePosition(void)                       const { return (ENUM_POSITION_TYPE)this.GetProperty(POSITION_PROP_TYPE);}// Position type
       long              Magic(void)                               const { return this.GetProperty(POSITION_PROP_MAGIC);                }  // Magic number for a position (see ORDER_MAGIC)
       long              ID(void)                                  const { return this.GetProperty(POSITION_PROP_IDENTIFIER);           }  // Position ID
       ENUM_POSITION_REASON Reason(void)                           const { return (ENUM_POSITION_REASON)this.GetProperty(POSITION_PROP_REASON);}// Position opening reason
       datetime          TimeClose(void)                           const { return (datetime)this.GetProperty(POSITION_PROP_TIME_CLOSE); }  // Close time
       long              TimeCloseMsc(void)                        const { return this.GetProperty(POSITION_PROP_TIME_CLOSE_MSC);       }  // Close time in milliseconds
       long              AccountLogin(void)                        const { return this.GetProperty(POSITION_PROP_ACCOUNT_LOGIN);        }  // Login
       
    //--- Real properties
       double            Volume(void)                              const { return this.GetProperty(POSITION_PROP_VOLUME);               }  // Position volume
       double            PriceOpen(void)                           const { return this.GetProperty(POSITION_PROP_PRICE_OPEN);           }  // Position price
       double            SL(void)                                  const { return this.GetProperty(POSITION_PROP_SL);                   }  // Stop Loss level for an open position
       double            TP(void)                                  const { return this.GetProperty(POSITION_PROP_TP);                   }  // Take Profit level for an open position
       double            PriceCurrent(void)                        const { return this.GetProperty(POSITION_PROP_PRICE_CURRENT);        }  // Current price by symbol
       double            Swap(void)                                const { return this.GetProperty(POSITION_PROP_SWAP);                 }  // Accumulated swap
       double            Profit(void)                              const { return this.GetProperty(POSITION_PROP_PROFIT);               }  // Current profit
       double            ContractSize(void)                        const { return this.GetProperty(POSITION_PROP_CONTRACT_SIZE);        }  // Symbol trading contract size
       double            PriceClose(void)                          const { return this.GetProperty(POSITION_PROP_PRICE_CLOSE);          }  // Close price
       double            Commissions(void)                         const { return this.GetProperty(POSITION_PROP_COMMISSIONS);          }  // Total commission of all deals
       double            Fee(void)                                 const { return this.GetProperty(POSITION_PROP_FEE);                  }  // Total deal fee
       
    //--- String properties
       string            Symbol(void)                              const { return this.GetProperty(POSITION_PROP_SYMBOL);               }  // A symbol position is opened on
       string            Comment(void)                             const { return this.GetProperty(POSITION_PROP_COMMENT);              }  // Position comment
       string            ExternalID(void)                          const { return this.GetProperty(POSITION_PROP_EXTERNAL_ID);          }  // Position ID in an external system (on the exchange)
       string            AccountServer(void)                       const { return this.GetProperty(POSITION_PROP_ACCOUNT_SERVER);       }  // Server name
       string            AccountCurrency(void)                     const { return this.GetProperty(POSITION_PROP_ACCOUNT_CURRENCY);     }  // Account deposit currency
       string            CurrencyProfit(void)                      const { return this.GetProperty(POSITION_PROP_CURRENCY_PROFIT);      }  // Profit currency of the position symbol
       
       
    //--- Additional properties
       ulong             DealIn(void)                              const;                                                                  // Open deal ticket
       ulong             DealOut(void)                             const;                                                                  // Close deal ticket
       int               ProfitInPoints(void)                      const;                                                                  // Profit in points
       int               SpreadIn(void)                            const;                                                                  // Spread when opening
       int               SpreadOut(void)                           const;                                                                  // Spread when closing
       double            SpreadOutCost(void)                       const;                                                                  // Spread cost when closing
       double            PriceOutAsk(void)                         const;                                                                  // Ask price when closing
       double            PriceOutBid(void)                         const;                                                                  // Bid price when closing
       
    //--- Add a deal to the list of deals, return the pointer
       CDeal            *DealAdd(const long ticket);
       
    //--- 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 position description
       string            Description(void);
    
    //--- Print the properties of the position and its deals 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;
       
    //--- Constructor/destructor
                         CPosition(const long position_id, const string symbol);
                         CPosition(void){}
                        ~CPosition();
      };
    
    

    Vamos a analizar la implementación de los métodos de clase.

    En el constructor de la clase, establecemos el identificador de posición y el símbolo a partir de los parámetros transmitidos al método, y escribimos los datos de la cuenta y el símbolo:

    //+------------------------------------------------------------------+
    //| Constructor                                                      |
    //+------------------------------------------------------------------+
    CPosition::CPosition(const long position_id, const string symbol)
      {
       this.m_list_deals.Sort(DEAL_PROP_TIME_MSC);
       this.SetID(position_id);
       this.SetSymbol(symbol);
       this.SetAccountLogin(::AccountInfoInteger(ACCOUNT_LOGIN));
       this.SetAccountServer(::AccountInfoString(ACCOUNT_SERVER));
       this.SetAccountCurrency(::AccountInfoString(ACCOUNT_CURRENCY));
       this.SetCurrencyProfit(::SymbolInfoString(this.Symbol(),SYMBOL_CURRENCY_PROFIT));
       this.SetContractSize(::SymbolInfoDouble(this.Symbol(),SYMBOL_TRADE_CONTRACT_SIZE));
       this.m_digits     = (int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS);
       this.m_point      = ::SymbolInfoDouble(this.Symbol(),SYMBOL_POINT);
       this.m_tick_value = ::SymbolInfoDouble(this.Symbol(), SYMBOL_TRADE_TICK_VALUE);
      }
    
    

    En el destructor de clase, limpiamos la lista de transacciones de la posición:

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

    Método que compara dos objetos según una propiedad específica:

    //+------------------------------------------------------------------+
    //| 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 POSITION_PROP_TICKET           :  return(this.Ticket() > obj.Ticket()                   ?  1  :  this.Ticket() < obj.Ticket()                    ? -1  :  0);
          case POSITION_PROP_TIME             :  return(this.Time() > obj.Time()                       ?  1  :  this.Time() < obj.Time()                        ? -1  :  0);
          case POSITION_PROP_TIME_MSC         :  return(this.TimeMsc() > obj.TimeMsc()                 ?  1  :  this.TimeMsc() < obj.TimeMsc()                  ? -1  :  0);
          case POSITION_PROP_TIME_UPDATE      :  return(this.TimeUpdate() > obj.TimeUpdate()           ?  1  :  this.TimeUpdate() < obj.TimeUpdate()            ? -1  :  0);
          case POSITION_PROP_TIME_UPDATE_MSC  :  return(this.TimeUpdateMsc() > obj.TimeUpdateMsc()     ?  1  :  this.TimeUpdateMsc() < obj.TimeUpdateMsc()      ? -1  :  0);
          case POSITION_PROP_TYPE             :  return(this.TypePosition() > obj.TypePosition()       ?  1  :  this.TypePosition() < obj.TypePosition()        ? -1  :  0);
          case POSITION_PROP_MAGIC            :  return(this.Magic() > obj.Magic()                     ?  1  :  this.Magic() < obj.Magic()                      ? -1  :  0);
          case POSITION_PROP_IDENTIFIER       :  return(this.ID() > obj.ID()                           ?  1  :  this.ID() < obj.ID()                            ? -1  :  0);
          case POSITION_PROP_REASON           :  return(this.Reason() > obj.Reason()                   ?  1  :  this.Reason() < obj.Reason()                    ? -1  :  0);
          case POSITION_PROP_ACCOUNT_LOGIN    :  return(this.AccountLogin() > obj.AccountLogin()       ?  1  :  this.AccountLogin() < obj.AccountLogin()        ? -1  :  0);
          case POSITION_PROP_TIME_CLOSE       :  return(this.TimeClose() > obj.TimeClose()             ?  1  :  this.TimeClose() < obj.TimeClose()              ? -1  :  0);
          case POSITION_PROP_TIME_CLOSE_MSC   :  return(this.TimeCloseMsc() > obj.TimeCloseMsc()       ?  1  :  this.TimeCloseMsc() < obj.TimeCloseMsc()        ? -1  :  0);
          case POSITION_PROP_VOLUME           :  return(this.Volume() > obj.Volume()                   ?  1  :  this.Volume() < obj.Volume()                    ? -1  :  0);
          case POSITION_PROP_PRICE_OPEN       :  return(this.PriceOpen() > obj.PriceOpen()             ?  1  :  this.PriceOpen() < obj.PriceOpen()              ? -1  :  0);
          case POSITION_PROP_SL               :  return(this.SL() > obj.SL()                           ?  1  :  this.SL() < obj.SL()                            ? -1  :  0);
          case POSITION_PROP_TP               :  return(this.TP() > obj.TP()                           ?  1  :  this.TP() < obj.TP()                            ? -1  :  0);
          case POSITION_PROP_PRICE_CURRENT    :  return(this.PriceCurrent() > obj.PriceCurrent()       ?  1  :  this.PriceCurrent() < obj.PriceCurrent()        ? -1  :  0);
          case POSITION_PROP_SWAP             :  return(this.Swap() > obj.Swap()                       ?  1  :  this.Swap() < obj.Swap()                        ? -1  :  0);
          case POSITION_PROP_PROFIT           :  return(this.Profit() > obj.Profit()                   ?  1  :  this.Profit() < obj.Profit()                    ? -1  :  0);
          case POSITION_PROP_CONTRACT_SIZE    :  return(this.ContractSize() > obj.ContractSize()       ?  1  :  this.ContractSize() < obj.ContractSize()        ? -1  :  0);
          case POSITION_PROP_PRICE_CLOSE      :  return(this.PriceClose() > obj.PriceClose()           ?  1  :  this.PriceClose() < obj.PriceClose()            ? -1  :  0);
          case POSITION_PROP_COMMISSIONS      :  return(this.Commissions() > obj.Commissions()         ?  1  :  this.Commissions() < obj.Commissions()          ? -1  :  0);
          case POSITION_PROP_FEE              :  return(this.Fee() > obj.Fee()                         ?  1  :  this.Fee() < obj.Fee()                          ? -1  :  0);
          case POSITION_PROP_SYMBOL           :  return(this.Symbol() > obj.Symbol()                   ?  1  :  this.Symbol() < obj.Symbol()                    ? -1  :  0);
          case POSITION_PROP_COMMENT          :  return(this.Comment() > obj.Comment()                 ?  1  :  this.Comment() < obj.Comment()                  ? -1  :  0);
          case POSITION_PROP_EXTERNAL_ID      :  return(this.ExternalID() > obj.ExternalID()           ?  1  :  this.ExternalID() < obj.ExternalID()            ? -1  :  0);
          case POSITION_PROP_CURRENCY_PROFIT  :  return(this.CurrencyProfit() > obj.CurrencyProfit()   ?  1  :  this.CurrencyProfit() < obj.CurrencyProfit()    ? -1  :  0);
          case POSITION_PROP_ACCOUNT_CURRENCY :  return(this.AccountCurrency() > obj.AccountCurrency() ?  1  :  this.AccountCurrency() < obj.AccountCurrency()  ? -1  :  0);
          case POSITION_PROP_ACCOUNT_SERVER   :  return(this.AccountServer() > obj.AccountServer()     ?  1  :  this.AccountServer() < obj.AccountServer()      ? -1  :  0);
          default                             :  return -1;
         }
      }
    
    

    Este es un método virtual que redefine el método homónimo en la clase principal CObject. Dependiendo del modo de comparación (una de las propiedades del objeto de posición), estas propiedades se comparan para el objeto actual y para el transmitido por puntero al método. El método retornará 1 si el valor de la propiedad del objeto actual es mayor que el valor de esta propiedad del objeto comparado. Si es menor, se retornará -1; si los valores son iguales, se retornará 0.

    Método que retorna 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'));
      }
    
    

    Método que retorna el puntero a una transacción de apertura:

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

    En el ciclo que itera la lista de transacciones de la posición, buscamos una transacción con el método de cambio de posición DEAL_ENTRY_IN (entrada al mercado) y retornamos el puntero a la transacción encontrada.

    Método que retorna el puntero a la transacció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;
      }
    
    

    En el ciclo a través de la lista de transacciones de posición, buscamos una transacción con el método de cambio de posición DEAL_ENTRY_OUT (salida del mercado) o DEAL_ENTRY_OUT_BY (cierre mediante una posición opuesta) y retornamos el puntero a la transacción encontrada.

    Método que retorna el ticket de la transacción de apertura:

    //+------------------------------------------------------------------+
    //| Return the open deal ticket                                      |
    //+------------------------------------------------------------------+
    ulong CPosition::DealIn(void) const
      {
       CDeal *deal=this.GetDealIn();
       return(deal!=NULL ? deal.Ticket() : 0);
      }
    
    

    Obtenemos el puntero a la transacción de entrada al mercado y retornamos su ticket.

    Método que retorna el ticket de la transacción cerrada:

    //+------------------------------------------------------------------+
    //| Return the close deal ticket                                     |
    //+------------------------------------------------------------------+
    ulong CPosition::DealOut(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Ticket() : 0);
      }
    
    

    Obtenemos el puntero a la transacción de salida del mercado y retornamos su ticket.

    Método que retorna el spread en la apertura:

    //+------------------------------------------------------------------+
    //| Return spread when opening                                       |
    //+------------------------------------------------------------------+
    int CPosition::SpreadIn(void) const
      {
       CDeal *deal=this.GetDealIn();
       return(deal!=NULL ? deal.Spread() : 0);
      }
    
    

    Obtenemos el puntero a la transacción de entrada al mercado y retornamos el valor del spread registrado en la transacción.

    Método que retorna el spread al cierre:

    //+------------------------------------------------------------------+
    //| Return spread when closing                                       |
    //+------------------------------------------------------------------+
    int CPosition::SpreadOut(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Spread() : 0);
      }
    
    

    Obtenemos el puntero a la transacción de salida del mercado y retornamos el valor del spread registrado en la transacción.

    Método que retorna el precio Ask al cierre:

    //+------------------------------------------------------------------+
    //| Return Ask price when closing                                    |
    //+------------------------------------------------------------------+
    double CPosition::PriceOutAsk(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Ask() : 0);
      }
    
    

    Obtenemos el puntero a la transacción de salida del mercado y retornamos el valor del precio Ask registrado en la transacción.

    Método que retorna el precio de oferta al cierre:

    //+------------------------------------------------------------------+
    //| Return the Bid price when closing                                |
    //+------------------------------------------------------------------+
    double CPosition::PriceOutBid(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Bid() : 0);
      }
    
    

    Obtenemos el puntero a la transacción de salida del mercado y retornamos el valor del precio Bid registrado en la transacción.

    Método que retorna 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)::round(this.TypePosition()==POSITION_TYPE_BUY ? (close-open)/this.m_point : (open-close)/this.m_point);
      }
    
    

    Método que retorna el valor del spread al cierre:

    //+------------------------------------------------------------------+
    //| Return the spread value when closing                             |
    //+------------------------------------------------------------------+
    double CPosition::SpreadOutCost(void) const
      {
    //--- Get close deal
       CDeal *deal=this.GetDealOut();
       if(deal==NULL)
          return 0;
    
    //--- Get position profit and position profit in points
       double profit=this.Profit();
       int profit_pt=this.ProfitInPoints();
       
    //--- If the profit is zero, return the spread value using the TickValue * Spread * Lots equation
       if(profit==0)
          return(this.m_tick_value * deal.Spread() * deal.Volume());
    
    //--- Calculate and return the spread value (proportion)
       return(profit_pt>0 ? deal.Spread() * ::fabs(profit / profit_pt) : 0);
      }
    
    

    El método usa dos métodos para calcular el valor del spread:

    1. Si el beneficio de la posición no es igual a cero, entonces el coste del spread se calculará en proporción: tamaño del spread en puntos * beneficio de la posición en dinero / beneficio de la posición en puntos.
    2. Si el beneficio de la posición es cero, entonces el coste del spread se calculará utilizando la fórmula: coste del tick calculado * tamaño del spread en puntos * volumen de la transacción.

    Método que establece la comisión total para todas las transacciones:

    //+------------------------------------------------------------------+
    //| Set the total commission for all deals                           |
    //+------------------------------------------------------------------+
    void CPosition::SetCommissions(void)
      {
       double res=0;
       int total=this.m_list_deals.Total();
       for(int i=0; i<total; i++)
         {
          CDeal *deal=this.m_list_deals.At(i);
          res+=(deal!=NULL ? deal.Commission() : 0);
         }
       this.SetProperty(POSITION_PROP_COMMISSIONS, res);
      }
    
    

    Para determinar la comisión tomada durante toda la vida de una posición, deberemos sumar las comisiones de todas las transacciones en la posición. En un ciclo a través de la lista de las transacciones de la posición, añadimos la comisión de cada transacción al valor resultante, que finalmente será retornado por el método.

    Método para establecer el pago total por la realización de transacciones:

    //+------------------------------------------------------------------+
    //| Sets the total deal fee                                          |
    //+------------------------------------------------------------------+
    void CPosition::SetFee(void)
      {
       double res=0;
       int total=this.m_list_deals.Total();
       for(int i=0; i<total; i++)
         {
          CDeal *deal=this.m_list_deals.At(i);
          res+=(deal!=NULL ? deal.Fee() : 0);
         }
       this.SetProperty(POSITION_PROP_FEE, res);
      }
    
    

    Aquí todo resulta exactamente igual que en el método anterior: retornamos la suma total de los valores Fee de cada transacción de la posición.

    Ambos métodos deberán llamarse cuando ya se hayan enumerado todas las transacciones de la posición; de lo contrario, el resultado será incompleto.

    Método que añade una transacción a la lista de transacciones en una posición:

    //+------------------------------------------------------------------+
    //| 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(DEAL_PROP_TICKET);
       
    //--- Set the result of checking if a deal with such a ticket is present in the list
       bool exist=(this.m_list_deals.Search(&this.m_temp_deal)!=WRONG_VALUE);
       
    //--- Return sorting by time in milliseconds for the list
       this.m_list_deals.Sort(DEAL_PROP_TIME_MSC);
    
    //--- If a deal with such a ticket is already in the list, return NULL
       if(exist)
          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
       if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY)
         {
          this.SetProfit(deal.Profit());
          this.SetSwap(deal.Swap());
         }
         
    //--- Return the pointer to the created deal object
       return deal;   
      }
    
    

    La lógica del método se detalla en los comentarios al código. El ticket de la transacción actualmente seleccionada se transmite al método. Si aún no hay transacciones con dicho ticket en la lista, se creará un nuevo objeto de transacción y se añadirá a la lista de transacciones de la posición.

    Métodos que retornan las descripciones de algunas propiedades de una posición:

    //+------------------------------------------------------------------+
    //| Return a position type description                               |
    //+------------------------------------------------------------------+
    string CPosition::TypeDescription(void) const
      {
       return(this.TypePosition()==POSITION_TYPE_BUY ? "Buy" : this.TypePosition()==POSITION_TYPE_SELL ? "Sell" : "Unknown::"+(string)this.TypePosition());
      }
    //+------------------------------------------------------------------+
    //| 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()));
      }
    //+------------------------------------------------------------------+
    //| 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()));
      }
    //+------------------------------------------------------------------+
    //| Return a brief position description                              |
    //+------------------------------------------------------------------+
    string CPosition::Description(void)
      {
       return(::StringFormat("%I64d (%s): %s %.2f %s #%I64d, Magic %I64d", this.AccountLogin(), this.AccountServer(),
                             this.Symbol(), this.Volume(), this.TypeDescription(), this.ID(), this.Magic()));
      }
    
    

    Estos métodos se usan, por ejemplo, para mostrar la descripción de una posición en el diario de registro.

    Podemos imprimir la descripción de una posición en el diario de registro utilizando el método Print :

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

    Primero, se imprime un encabezado con una descripción de la posición y luego, en un ciclo a través de todas las transacciones en la posición, se imprime una descripción de cada transacción utilizando su método Print().

    La clase de posición histórica está lista. Ahora crearemos una clase estática para seleccionar, buscar y filtrar transacciones y posiciones según sus propiedades.


    Clase para la búsqueda y el filtrado según las propiedades de las transacciones y posiciones

    Ya analizamos esta clase con detalle en el artículo "Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte III): Colección de órdenes y posiciones de mercado, búsqueda y filtrado" en el apartado Organización de la búsqueda.

    En la carpeta \MQL5\Services\AccountReporter\, crearemos un nuevo archivo Select.mqh de la clase CSelect:

    //+------------------------------------------------------------------+
    //|                                                       Select.mqh |
    //|                        Copyright 2024, MetaQuotes Software Corp. |
    //|                             https://mql5.com/en/users/artmedia70 |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Software Corp."
    #property link      "https://mql5.com/en/users/artmedia70"
    #property version   "1.00"
    
    //+------------------------------------------------------------------+
    //| Class for sorting objects meeting the criterion                  |
    //+------------------------------------------------------------------+
    class CSelect
      {
      }
    

    Luego escribiremos una enumeración de los modos de comparación, conectaremos los archivos de las clases de transacción y posición y declararemos una lista de almacenamiento :

    //+------------------------------------------------------------------+
    //|                                                       Select.mqh |
    //|                        Copyright 2024, MetaQuotes Software Corp. |
    //|                             https://mql5.com/en/users/artmedia70 |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Software Corp."
    #property link      "https://mql5.com/en/users/artmedia70"
    #property version   "1.00"
    
    enum ENUM_COMPARER_TYPE
      {
       EQUAL,                                                   // Equal
       MORE,                                                    // More
       LESS,                                                    // Less
       NO_EQUAL,                                                // Not equal
       EQUAL_OR_MORE,                                           // Equal or more
       EQUAL_OR_LESS                                            // Equal or less
      };
    
    //+------------------------------------------------------------------+
    //| Include files                                                    |
    //+------------------------------------------------------------------+
    #include "Deal.mqh"
    #include "Position.mqh"
    
    //+------------------------------------------------------------------+
    //| Storage list                                                     |
    //+------------------------------------------------------------------+
    CArrayObj   ListStorage; // Storage object for storing sorted collection lists
    //+------------------------------------------------------------------+
    //| Class for sorting objects meeting the criterion                  |
    //+------------------------------------------------------------------+
    class CSelect
      {
      }
    
    

    Vamos a escribir todos los métodos para seleccionar los objetos y crear listas que satisfagan los criterios de búsqueda:

    //+------------------------------------------------------------------+
    //|                                                       Select.mqh |
    //|                        Copyright 2024, MetaQuotes Software Corp. |
    //|                             https://mql5.com/en/users/artmedia70 |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Software Corp."
    #property link      "https://mql5.com/en/users/artmedia70"
    #property version   "1.00"
    
    enum ENUM_COMPARER_TYPE                                     // Comparison modes
      {
       EQUAL,                                                   // Equal
       MORE,                                                    // More
       LESS,                                                    // Less
       NO_EQUAL,                                                // Not equal
       EQUAL_OR_MORE,                                           // Equal or more
       EQUAL_OR_LESS                                            // Equal or less
      };
    
    //+------------------------------------------------------------------+
    //| Include files                                                    |
    //+------------------------------------------------------------------+
    #include "Deal.mqh"
    #include "Position.mqh"
    
    //+------------------------------------------------------------------+
    //| Storage list                                                     |
    //+------------------------------------------------------------------+
    CArrayObj   ListStorage; // Storage object for storing sorted collection lists
    //+------------------------------------------------------------------+
    //| Class for sorting objects meeting the criterion                  |
    //+------------------------------------------------------------------+
    class CSelect
      {
    private:
       //--- Method for comparing two values
       template<typename T>
       static bool       CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode);
    public:
    //+------------------------------------------------------------------+
    //| Deal handling methods                                            |
    //+------------------------------------------------------------------+
       //--- Return the list of deals with one out of (1) integer, (2) real and (3) string properties meeting a specified criterion
       static CArrayObj *ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property,long value,ENUM_COMPARER_TYPE mode);
       static CArrayObj *ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property,double value,ENUM_COMPARER_TYPE mode);
       static CArrayObj *ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property,string value,ENUM_COMPARER_TYPE mode);
       
       //--- Return the deal index with the maximum value of the (1) integer, (2) real and (3) string properties
       static int        FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property);
       static int        FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property);
       static int        FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property);
       
       //--- Return the deal index with the minimum value of the (1) integer, (2) real and (3) string properties
       static int        FindDealMin(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property);
       static int        FindDealMin(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property);
       static int        FindDealMin(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property);
       
    //+------------------------------------------------------------------+
    //| Position handling methods                                        |
    //+------------------------------------------------------------------+
       //--- Return the list of positions with one out of (1) integer, (2) real and (3) string properties meeting a specified criterion
       static CArrayObj *ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property,long value,ENUM_COMPARER_TYPE mode);
       static CArrayObj *ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property,double value,ENUM_COMPARER_TYPE mode);
       static CArrayObj *ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property,string value,ENUM_COMPARER_TYPE mode);
       
       //--- Return the position index with the maximum value of the (1) integer, (2) real and (3) string properties
       static int        FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property);
       static int        FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property);
       static int        FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property);
       
       //--- Return the position index with the minimum value of the (1) integer, (2) real and (3) string properties
       static int        FindPositionMin(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property);
       static int        FindPositionMin(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property);
       static int        FindPositionMin(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property);
      };
    //+------------------------------------------------------------------+
    //| Method for comparing two values                                  |
    //+------------------------------------------------------------------+
    template<typename T>
    bool CSelect::CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode)
      {
       switch(mode)
         {
          case EQUAL           :  return(value1==value2   ?  true  :  false);
          case NO_EQUAL        :  return(value1!=value2   ?  true  :  false);
          case MORE            :  return(value1>value2    ?  true  :  false);
          case LESS            :  return(value1<value2    ?  true  :  false);
          case EQUAL_OR_MORE   :  return(value1>=value2   ?  true  :  false);
          case EQUAL_OR_LESS   :  return(value1<=value2   ?  true  :  false);
          default              :  return false;
         }
      }
    //+------------------------------------------------------------------+
    //| Deal list handling methods                                       |
    //+------------------------------------------------------------------+
    //+------------------------------------------------------------------+
    //| Return the list of deals with one integer                        |
    //| property meeting the specified criterion                         |
    //+------------------------------------------------------------------+
    CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property,long value,ENUM_COMPARER_TYPE mode)
      {
       if(list_source==NULL)
          return NULL;
       CArrayObj *list=new CArrayObj();
       if(list==NULL)
          return NULL;
       list.FreeMode(false);
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
       int total=list_source.Total();
       for(int i=0; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          long obj_prop=obj.GetProperty(property);
          if(CompareValues(obj_prop, value, mode))
             list.Add(obj);
         }
       return list;
      }
    //+------------------------------------------------------------------+
    //| Return the list of deals with one real                           |
    //| property meeting the specified criterion                         |
    //+------------------------------------------------------------------+
    CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property,double value,ENUM_COMPARER_TYPE mode)
      {
       if(list_source==NULL)
          return NULL;
       CArrayObj *list=new CArrayObj();
       if(list==NULL)
          return NULL;
       list.FreeMode(false);
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
       for(int i=0; i<list_source.Total(); i++)
         {
          CDeal *obj=list_source.At(i);
          double obj_prop=obj.GetProperty(property);
          if(CompareValues(obj_prop,value,mode))
             list.Add(obj);
         }
       return list;
      }
    //+------------------------------------------------------------------+
    //| Return the list of deals with one string                         |
    //| property meeting the specified criterion                         |
    //+------------------------------------------------------------------+
    CArrayObj *CSelect::ByDealProperty(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property,string value,ENUM_COMPARER_TYPE mode)
      {
       if(list_source==NULL)
          return NULL;
       CArrayObj *list=new CArrayObj();
       if(list==NULL)
          return NULL;
       list.FreeMode(false);
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
       for(int i=0; i<list_source.Total(); i++)
         {
          CDeal *obj=list_source.At(i);
          string obj_prop=obj.GetProperty(property);
          if(CompareValues(obj_prop,value,mode))
             list.Add(obj);
         }
       return list;
      }
    //+------------------------------------------------------------------+
    //| Return the deal index in the list                                |
    //| with the maximum integer property value                          |
    //+------------------------------------------------------------------+
    int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_INT property)
      {
       if(list_source==NULL)
          return WRONG_VALUE;
       int index=0;
       CDeal *max_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          long obj1_prop=obj.GetProperty(property);
          max_obj=list_source.At(index);
          long obj2_prop=max_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,MORE))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Return the deal index in the list                                |
    //| with the maximum real property value                             |
    //+------------------------------------------------------------------+
    int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_DBL property)
      {
       if(list_source==NULL)
          return WRONG_VALUE;
       int index=0;
       CDeal *max_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          double obj1_prop=obj.GetProperty(property);
          max_obj=list_source.At(index);
          double obj2_prop=max_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,MORE))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Return the deal index in the list                                |
    //| with the maximum string property value                           |
    //+------------------------------------------------------------------+
    int CSelect::FindDealMax(CArrayObj *list_source,ENUM_DEAL_PROPERTY_STR property)
      {
       if(list_source==NULL)
          return WRONG_VALUE;
       int index=0;
       CDeal *max_obj=NULL;
       int total=list_source.Total();
       if(total==0) return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          string obj1_prop=obj.GetProperty(property);
          max_obj=list_source.At(index);
          string obj2_prop=max_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,MORE))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Return the deal index in the list                                |
    //| with the minimum integer property value                          |
    //+------------------------------------------------------------------+
    int CSelect::FindDealMin(CArrayObj* list_source,ENUM_DEAL_PROPERTY_INT property)
      {
       int index=0;
       CDeal *min_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          long obj1_prop=obj.GetProperty(property);
          min_obj=list_source.At(index);
          long obj2_prop=min_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,LESS))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Return the deal index in the list                                |
    //| with the minimum real property value                             |
    //+------------------------------------------------------------------+
    int CSelect::FindDealMin(CArrayObj* list_source,ENUM_DEAL_PROPERTY_DBL property)
      {
       int index=0;
       CDeal *min_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          double obj1_prop=obj.GetProperty(property);
          min_obj=list_source.At(index);
          double obj2_prop=min_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,LESS))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Return the deal index in the list                                |
    //| with the minimum string property value                           |
    //+------------------------------------------------------------------+
    int CSelect::FindDealMin(CArrayObj* list_source,ENUM_DEAL_PROPERTY_STR property)
      {
       int index=0;
       CDeal *min_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CDeal *obj=list_source.At(i);
          string obj1_prop=obj.GetProperty(property);
          min_obj=list_source.At(index);
          string obj2_prop=min_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,LESS))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Position list handling method                                    |
    //+------------------------------------------------------------------+
    //+------------------------------------------------------------------+
    //| Return the list of positions with one integer                    |
    //| property meeting the specified criterion                         |
    //+------------------------------------------------------------------+
    CArrayObj *CSelect::ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property,long value,ENUM_COMPARER_TYPE mode)
      {
       if(list_source==NULL)
          return NULL;
       CArrayObj *list=new CArrayObj();
       if(list==NULL)
          return NULL;
       list.FreeMode(false);
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
       int total=list_source.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          long obj_prop=obj.GetProperty(property);
          if(CompareValues(obj_prop, value, mode))
             list.Add(obj);
         }
       return list;
      }
    //+------------------------------------------------------------------+
    //| Return the list of positions with one real                       |
    //| property meeting the specified criterion                         |
    //+------------------------------------------------------------------+
    CArrayObj *CSelect::ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property,double value,ENUM_COMPARER_TYPE mode)
      {
       if(list_source==NULL)
          return NULL;
       CArrayObj *list=new CArrayObj();
       if(list==NULL)
          return NULL;
       list.FreeMode(false);
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
       for(int i=0; i<list_source.Total(); i++)
         {
          CPosition *obj=list_source.At(i);
          double obj_prop=obj.GetProperty(property);
          if(CompareValues(obj_prop,value,mode))
             list.Add(obj);
         }
       return list;
      }
    //+------------------------------------------------------------------+
    //| Return the list of positions with one string                     |
    //| property meeting the specified criterion                         |
    //+------------------------------------------------------------------+
    CArrayObj *CSelect::ByPositionProperty(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property,string value,ENUM_COMPARER_TYPE mode)
      {
       if(list_source==NULL)
          return NULL;
       CArrayObj *list=new CArrayObj();
       if(list==NULL)
          return NULL;
       list.FreeMode(false);
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
       for(int i=0; i<list_source.Total(); i++)
         {
          CPosition *obj=list_source.At(i);
          string obj_prop=obj.GetProperty(property);
          if(CompareValues(obj_prop,value,mode))
             list.Add(obj);
         }
       return list;
      }
    //+------------------------------------------------------------------+
    //| Return the position index in the list                            |
    //| with the maximum integer property value                          |
    //+------------------------------------------------------------------+
    int CSelect::FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_INT property)
      {
       if(list_source==NULL)
          return WRONG_VALUE;
       int index=0;
       CPosition *max_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          long obj1_prop=obj.GetProperty(property);
          max_obj=list_source.At(index);
          long obj2_prop=max_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,MORE))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Return the position index in the list                            |
    //| with the maximum real property value                             |
    //+------------------------------------------------------------------+
    int CSelect::FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_DBL property)
      {
       if(list_source==NULL)
          return WRONG_VALUE;
       int index=0;
       CPosition *max_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          double obj1_prop=obj.GetProperty(property);
          max_obj=list_source.At(index);
          double obj2_prop=max_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,MORE))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Return the position index in the list                            |
    //| with the maximum string property value                           |
    //+------------------------------------------------------------------+
    int CSelect::FindPositionMax(CArrayObj *list_source,ENUM_POSITION_PROPERTY_STR property)
      {
       if(list_source==NULL)
          return WRONG_VALUE;
       int index=0;
       CPosition *max_obj=NULL;
       int total=list_source.Total();
       if(total==0) return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          string obj1_prop=obj.GetProperty(property);
          max_obj=list_source.At(index);
          string obj2_prop=max_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,MORE))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Return the position index in the list                            |
    //| with the minimum integer property value                          |
    //+------------------------------------------------------------------+
    int CSelect::FindPositionMin(CArrayObj* list_source,ENUM_POSITION_PROPERTY_INT property)
      {
       int index=0;
       CPosition *min_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          long obj1_prop=obj.GetProperty(property);
          min_obj=list_source.At(index);
          long obj2_prop=min_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,LESS))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Return the position index in the list                            |
    //| with the minimum real property value                             |
    //+------------------------------------------------------------------+
    int CSelect::FindPositionMin(CArrayObj* list_source,ENUM_POSITION_PROPERTY_DBL property)
      {
       int index=0;
       CPosition *min_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          double obj1_prop=obj.GetProperty(property);
          min_obj=list_source.At(index);
          double obj2_prop=min_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,LESS))
             index=i;
         }
       return index;
      }
    //+------------------------------------------------------------------+
    //| Return the position index in the list                            |
    //| with the minimum string property value                           |
    //+------------------------------------------------------------------+
    int CSelect::FindPositionMin(CArrayObj* list_source,ENUM_POSITION_PROPERTY_STR property)
      {
       int index=0;
       CPosition *min_obj=NULL;
       int total=list_source.Total();
       if(total==0)
          return WRONG_VALUE;
       for(int i=1; i<total; i++)
         {
          CPosition *obj=list_source.At(i);
          string obj1_prop=obj.GetProperty(property);
          min_obj=list_source.At(index);
          string obj2_prop=min_obj.GetProperty(property);
          if(CompareValues(obj1_prop,obj2_prop,LESS))
             index=i;
         }
       return index;
      }
    
    

    Podrá encontrar una descripción completa y exhaustiva de dicha clase en el artículo anteriormente sugerido, en el apartado "Organización de la búsqueda".

    Ahora ya tenemos todo listo para crear una clase para trabajar con la lista de posiciones históricas.


    Clase de colección de posiciones históricas

    En la carpeta del terminal \MQL5\Services\AccountReporter\ crearemos un nuevo archivo PositionsControl.mqh de la clase CPositionsControl.
    La clase deberá heredarse del objeto básico de la Biblioteca Estándar CObject, mientras que los archivos de la clase para las posiciones históricas y la clase para la búsqueda y el filtrado deberán incluirse en el archivo creado :

    //+------------------------------------------------------------------+
    //|                                             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"
    #include "Select.mqh"
    
    //+------------------------------------------------------------------+
    //| Collection class of historical positions                         |
    //+------------------------------------------------------------------+
    class CPositionsControl : public CObject
      {
      }
    

    Luego declararemos los métodos privados, protegidos y públicos de la clase:

    //+------------------------------------------------------------------+
    //| Collection class of historical positions                         |
    //+------------------------------------------------------------------+
    class CPositionsControl : public CObject
      {
    private:
    //--- Return (1) position type and (2) reason for opening by deal type
       ENUM_POSITION_TYPE PositionTypeByDeal(const CDeal *deal);
       ENUM_POSITION_REASON PositionReasonByDeal(const CDeal *deal);
    
    protected:
       CPosition         m_temp_pos;          // Temporary position object for searching
       CArrayObj         m_list_pos;          // List of positions
       
    //--- 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);
       
    public:
    //--- Create and update the list of positions. It can be redefined in the inherited classes
       virtual bool      Refresh(void);
       
    //--- Return (1) the list, (2) number of positions in the list
       CArrayObj        *GetPositionsList(void)           { return &this.m_list_pos;          }
       int               PositionsTotal(void)       const { return this.m_list_pos.Total();   }
    
    //--- Print the properties of all positions and their deals in the journal
       void              Print(void);
    
    //--- Constructor/destructor
                         CPositionsControl(void);
                        ~CPositionsControl();
      };
    
    

    Vamos a analizar las implementaciones de los métodos declarados.

    En el constructor de la clase, establecemos el indicador de clasificación para la lista de posiciones históricas según el tiempo de cierre en milisegundos:

    //+------------------------------------------------------------------+
    //| Constructor                                                      |
    //+------------------------------------------------------------------+
    CPositionsControl::CPositionsControl(void)
      {
       this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC);
      }
    
    

    En el destructor de clase, destruimos la lista de posiciones históricas:

    //+------------------------------------------------------------------+
    //| Destructor                                                       |
    //+------------------------------------------------------------------+
    CPositionsControl::~CPositionsControl()
      {
       this.m_list_pos.Shutdown();
      }
    
    

    Método que retorna el puntero a un objeto de posición de la lista según el identificador:

    //+------------------------------------------------------------------+
    //| 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(POSITION_PROP_IDENTIFIER);
       
    //--- Get the index of the position object with the specified ID (or -1 if it is absent) from the list
    //--- Use the obtained index to get the pointer to the positino 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);
       
    //--- Return 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(POSITION_PROP_TIME_CLOSE_MSC);
       return pos;
      }
    
    


    Método que retorna la bandera que indica que la posición es 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;
      }
    
    


    Método que retorna el tipo de posición según tipo de transacció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;
         }
      }
    
    

    Dependiendo del tipo de transacción, retornaremos el tipo de posición correspondiente.

    Método que retorna el motivo de la apertura de una posición según tipo de transacción:

    //+------------------------------------------------------------------+
    //| Returns the reason for opening a position by deal type           |
    //+------------------------------------------------------------------+
    ENUM_POSITION_REASON CPositionsControl::PositionReasonByDeal(const CDeal *deal)
      {
       if(deal==NULL)
          return WRONG_VALUE;
       switch(deal.Reason())
         {
          case DEAL_REASON_CLIENT :  return POSITION_REASON_CLIENT;
          case DEAL_REASON_MOBILE :  return POSITION_REASON_MOBILE;
          case DEAL_REASON_WEB    :  return POSITION_REASON_WEB;
          case DEAL_REASON_EXPERT :  return POSITION_REASON_EXPERT;
          default                 :  return WRONG_VALUE;
         }
      }
    
    

    Dependiendo del motivo de la transacción, retornaremos el motivo correspondiente para la apertura de la posición.

    Método que crea o actualiza una 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(POSITION_PROP_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, 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))
             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
             string pos_symbol=HistoryDealGetString(ticket, DEAL_SYMBOL);
             pos=new CPosition(pos_id, pos_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.SetTicket(deal.Order());
             pos.SetMagic(deal.Magic());
             pos.SetTime(deal.Time());
             pos.SetTimeMsc(deal.TimeMsc());
             ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal);
             pos.SetTypePosition(type);
             ENUM_POSITION_REASON reason=this.PositionReasonByDeal(deal);
             pos.SetReason(reason);
             pos.SetPriceOpen(deal.Price());
             pos.SetVolume(deal.Volume());
            }
          if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY)
            {
             pos.SetPriceCurrent(deal.Price());
             pos.SetPriceClose(deal.Price());
             pos.SetTimeClose(deal.Time());
             pos.SetTimeCloseMsc(deal.TimeMsc());
            }
          if(deal.Entry()==DEAL_ENTRY_INOUT)
            {
             ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal);
             pos.SetTypePosition(type);
             pos.SetVolume(deal.Volume()-pos.Volume());
            }
         }
         
    //--- All historical positions are created and the corresponding deals are added to the deal lists of the position objects
    //--- Set the flag of sorting by close time in milliseconds for the position list
       this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC);
    
    //--- In the loop through the created list of closed positions, we set the Commissions and Fee values for each position
       for(int i=0; i<this.m_list_pos.Total(); i++)
         {
          CPosition *pos=this.m_list_pos.At(i);
          if(pos==NULL)
             continue;
          pos.SetCommissions();
          pos.SetFee();
         }
    
    //--- Return the result of creating and adding a position to the list
       return res;
      }
    

    En un ciclo a través de la lista de transacciones en el terminal, obtendremos la siguiente transacción y verificaremos su identificador de posición. Si se trata de una posición de mercado, nos saltaremos la transacción. Si dicha posición aún no se encuentra en la lista de posiciones históricas, crearemos un nuevo objeto de posición y lo colocaremos en la lista de posiciones históricas. Si todavía no hay transacciones con el ticket de la transacción seleccionada en el objeto de posición histórica, añadiremos la transacción a la lista de transacciones del objeto de posición.

    Al final del ciclo de creación de los objetos de posiciones históricas, asignaremos a cada posición una comisión y un pago común para realizar transacciones para todas las transacciones en la posición. El método es virtual, lo cual permite crear una lógica más óptima en la clase heredada si es necesario actualizar la lista de posiciones con mucha más frecuencia que una vez al día.

    Método que imprime las propiedades de las posiciones y sus transacciones en el diario de registro:

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

    Si necesitamos controlar la lista de posiciones históricas creadas, este método permitirá imprimir cada posición con sus transacciones en el diario de registro.

    El programa de servicio puede "recordar" todas las cuentas que se conectaron durante el funcionamiento continuo del servicio. Es decir, si no se han dado reinicios del terminal y no ha habido una conexión a diferentes cuentas y servidores comerciales, el programa recordará estas cuentas y almacenará las listas de todas las posiciones cerradas. Los informes comerciales se mostrarán según las posiciones cerradas que estaban en cada una de las cuentas conectadas. O bien, si la configuración indica que se deben generar informes solo desde la cuenta actual, entonces, en consecuencia, las listas de posiciones cerradas se filtrarán según el valor del login y el servidor de la cuenta actual.

    Partiendo de lo anterior, resulta que necesitaremos una clase de cuenta que almacene una clase de gestión para la lista de posiciones cerradas negociadas en esta cuenta. En el programa de servicio, obtendremos la cuenta requerida y, de ella, una lista de posiciones cerradas.


    Clase de cuenta

    En la carpeta de terminal \MQL5\Services\AccountReporter\, crearemos el nuevo archivo Account.mqh de la clase CAccount.
    La clase debe heredarse de la clase de objeto básico de la biblioteca estándar CObject, mientras que el archivo de clase de colección de posiciones históricas debe incluirse en el archivo que estamos creando:

    //+------------------------------------------------------------------+
    //|                                                      Account.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 "PositionsControl.mqh"
    
    //+------------------------------------------------------------------+
    //| Account class                                                    |
    //+------------------------------------------------------------------+
    class CAccount : public CObject
      {
      }
    

    En la sección protegida de la clase, declaramos un objeto para monitorear las posiciones históricas (una clase de lista de posiciones cerradas de la cuenta) y una lista de propiedades de números enteros, reales y string :

    //+------------------------------------------------------------------+
    //| Account class                                                    |
    //+------------------------------------------------------------------+
    class CAccount : public CObject
      {
    private:
       
    protected:
       CPositionsControl m_positions;                  // Historical positions control object
    //--- account integer properties
       long              m_login;                    //   Account number
       ENUM_ACCOUNT_TRADE_MODE m_trade_mode;         //   Trading account type
       long              m_leverage;                 //   Leverage
       int               m_limit_orders;             //   Maximum allowed number of active pending orders
       ENUM_ACCOUNT_STOPOUT_MODE m_margin_so_mode;   //   Mode of setting the minimum available margin level
       bool              m_trade_allowed;            //   Trading permission of the current account
       bool              m_trade_expert;             //   Trading permission of an EA
       ENUM_ACCOUNT_MARGIN_MODE m_margin_mode;       //   Margin calculation mode
       int               m_currency_digits;          //   Number of digits for an account currency necessary for accurate display of trading results
       bool              m_fifo_close;               //   The flag indicating that positions can be closed only by the FIFO rule
       bool              m_hedge_allowed;            //   Allowed opposite positions on a single symbol
        
    //--- account real properties
       double            m_balance;                  //   Account balance in a deposit currency
       double            m_credit;                   //   Credit in a deposit currency
       double            m_profit;                   //   Current profit on an account in the account currency
       double            m_equity;                   //   Equity on an account in the deposit currency
       double            m_margin;                   //   Reserved margin on an account in a deposit currency
       double            m_margin_free;              //   Free funds available for opening a position in a deposit currency
       double            m_margin_level;             //   Margin level on an account in %
       double            m_margin_so_call;           //   Margin Call level
       double            m_margin_so_so;             //   Stop Out level
       double            m_margin_initial;           //   Funds reserved on an account to ensure a guarantee amount for all pending orders 
       double            m_margin_maintenance;       //   Funds reserved on an account to ensure a minimum amount for all open positions
       double            m_assets;                   //   Current assets on an account
       double            m_liabilities;              //   Current liabilities on an account
       double            m_commission_blocked;       //   Current sum of blocked commissions on an account
      
    //--- account string properties
       string            m_name;                     //   Client name
       string            m_server;                   //   Trade server name
       string            m_currency;                 //   Deposit currency
       string            m_company;                  //   Name of a company serving account
       
    public:
    
    

    En la sección pública escribiremos los métodos para trabajar con las listas, los métodos para configurar y retornar las propiedades del objeto de cuenta y otros métodos:

    public:
    //--- Return the (1) control object, (2) the list of historical positions, (3) number of positions
       CPositionsControl*GetPositionsCtrlObj(void)        { return &this.m_positions;                  }
       CArrayObj        *GetPositionsList(void)           { return this.m_positions.GetPositionsList();}
       int               PositionsTotal(void)             { return this.m_positions.PositionsTotal();  }
    
    //--- Return the list of positions by (1) integer, (2) real and (3) string property
       CArrayObj        *GetPositionsList(ENUM_POSITION_PROPERTY_INT property, long   value, ENUM_COMPARER_TYPE mode)
                           { return CSelect::ByPositionProperty(this.GetPositionsList(), property, value, mode); }
       
       CArrayObj        *GetPositionsList(ENUM_POSITION_PROPERTY_DBL property, double value, ENUM_COMPARER_TYPE mode)
                           { return CSelect::ByPositionProperty(this.GetPositionsList(), property, value, mode); }
       
       CArrayObj        *GetPositionsList(ENUM_POSITION_PROPERTY_STR property, string value, ENUM_COMPARER_TYPE mode)
                           { return CSelect::ByPositionProperty(this.GetPositionsList(), property, value, mode); }
    
    //--- (1) Update and (2) print the list of closed positions in the journal
       bool              PositionsRefresh(void)           { return this.m_positions.Refresh();}
       void              PositionsPrint(void)             { this.m_positions.Print();         }
       
    //--- set (1) login and (2) server
       void              SetLogin(const long login)       { this.m_login=login;               }
       void              SetServer(const string server)   { this.m_server=server;             }
       
    //--- return integer account properties
       long              Login(void)                const { return this.m_login;              }  //   Account number
       ENUM_ACCOUNT_TRADE_MODE TradeMode(void)      const { return this.m_trade_mode;         }  //   Trading account type
       long              Leverage(void)             const { return this.m_leverage;           }  //   Provided leverage
       int               LimitOrders(void)          const { return this.m_limit_orders;       }  //   Maximum allowed number of active pending orders
       ENUM_ACCOUNT_STOPOUT_MODE MarginSoMode(void) const { return this.m_margin_so_mode;     }  //   Mode of setting the minimum available margin level
       bool              TradeAllowed(void)         const { return this.m_trade_allowed;      }  //   Trading permission of the current account
       bool              TradeExpert(void)          const { return this.m_trade_expert;       }  //   Trading permission for EA
       ENUM_ACCOUNT_MARGIN_MODE MarginMode(void)    const { return this.m_margin_mode;        }  //   Margin calculation mode
       int               CurrencyDigits(void)       const { return this.m_currency_digits;    }  //   Number of digits for an account currency necessary for accurate display of trading results
       bool              FIFOClose(void)            const { return this.m_fifo_close;         }  //   The flag indicating that positions can be closed only by the FIFO rule
       bool              HedgeAllowed(void)         const { return this.m_hedge_allowed;      }  //   Allowed opposite positions on a single symbol
        
    //--- return real account properties
       double            Balance(void)              const { return this.m_balance;            }  //   Account balance in a deposit currency
       double            Credit(void)               const { return this.m_credit;             }  //   Credit in deposit currency
       double            Profit(void)               const { return this.m_profit;             }  //   Current profit on an account in the account currency
       double            Equity(void)               const { return this.m_equity;             }  //   Available equity in the deposit currency
       double            Margin(void)               const { return this.m_margin;             }  //   The amount of reserved collateral funds on the account in the deposit currency
       double            MarginFree(void)           const { return this.m_margin_free;        }  //   Free funds available for opening a position in a deposit currency
       double            MarginLevel(void)          const { return this.m_margin_level;       }  //   Margin level on an account in %
       double            MarginSoCall(void)         const { return this.m_margin_so_call;     }  //   Margin Call level
       double            MarginSoSo(void)           const { return this.m_margin_so_so;       }  //   Stop Out level
       double            MarginInitial(void)        const { return this.m_margin_initial;     }  //   Funds reserved on an account to ensure a guarantee amount for all pending orders 
       double            MarginMaintenance(void)    const { return this.m_margin_maintenance; }  //   Funds reserved on an account to ensure the minimum amount for all open positions 
       double            Assets(void)               const { return this.m_assets;             }  //   Current assets on an account
       double            Liabilities(void)          const { return this.m_liabilities;        }  //   Current amount of liabilities on the account
       double            CommissionBlocked(void)    const { return this.m_commission_blocked; }  //   Current sum of blocked commissions on an account
      
    //--- return account string properties
       string            Name(void)                 const { return this.m_name;               }  //   Client name
       string            Server(void)               const { return this.m_server;             }  //   Trade server name
       string            Currency(void)             const { return this.m_currency;           }  //   Deposit currency
       string            Company(void)              const { return this.m_company;            }  //   Name of the company servicing the account
       
    //--- return (1) account description, (2) trading account type and (3) margin calculation mode
       string            Description(void)          const;
       string            TradeModeDescription(void) const;
       string            MarginModeDescription(void)const;
       
    //--- virtual method for comparing two objects
       virtual int       Compare(const CObject *node,const int mode=0) const;
    
    //--- Display the account description in the journal
       void              Print(void)                      { ::Print(this.Description());      }
    
    //--- constructors/destructor
                         CAccount(void){}
                         CAccount(const long login, const string server_name);
                        ~CAccount() {}
      };
    
    

    Vamos a ver la implementación de los métodos declarados.

    En el constructor de la clase, establecemos todas las propiedades de la cuenta actual en el objeto:

    //+------------------------------------------------------------------+
    //| Constructor                                                      |
    //+------------------------------------------------------------------+
    CAccount::CAccount(const long login, const string server_name)
      {
       this.m_login=login;
       this.m_server=server_name;
       
    //--- set account integer properties
       this.m_trade_mode          = (ENUM_ACCOUNT_TRADE_MODE)::AccountInfoInteger(ACCOUNT_TRADE_MODE);    //   Trading account type
       this.m_leverage            = ::AccountInfoInteger(ACCOUNT_LEVERAGE);                               //   Leverage
       this.m_limit_orders        = (int)::AccountInfoInteger(ACCOUNT_LIMIT_ORDERS);                      //   Maximum allowed number of active pending orders
       this.m_margin_so_mode      = (ENUM_ACCOUNT_STOPOUT_MODE)AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE);//   Mode of setting the minimum available margin level
       this.m_trade_allowed       = ::AccountInfoInteger(ACCOUNT_TRADE_ALLOWED);                          //   Trading permission of the current account
       this.m_trade_expert        = ::AccountInfoInteger(ACCOUNT_TRADE_EXPERT);                           //   Trading permission of an EA
       this.m_margin_mode         = (ENUM_ACCOUNT_MARGIN_MODE)::AccountInfoInteger(ACCOUNT_MARGIN_MODE);  //   Margin calculation mode
       this.m_currency_digits     = (int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS);                   //   Number of digits for an account currency necessary for accurate display of trading results
       this.m_fifo_close          = ::AccountInfoInteger(ACCOUNT_FIFO_CLOSE);                             //   The flag indicating that positions can be closed only by the FIFO rule
       this.m_hedge_allowed       = ::AccountInfoInteger(ACCOUNT_HEDGE_ALLOWED);                          //   Allowed opposite positions on a single symbol
        
    //--- set account real properties
       this.m_balance             = ::AccountInfoDouble(ACCOUNT_BALANCE);                                 //   Account balance in a deposit currency
       this.m_credit              = ::AccountInfoDouble(ACCOUNT_CREDIT);                                  //   Credit in a deposit currency
       this.m_profit              = ::AccountInfoDouble(ACCOUNT_PROFIT);                                  //   Current profit on an account in the account currency
       this.m_equity              = ::AccountInfoDouble(ACCOUNT_EQUITY);                                  //   Equity on an account in the deposit currency
       this.m_margin              = ::AccountInfoDouble(ACCOUNT_MARGIN);                                  //   Reserved margin on an account in a deposit currency
       this.m_margin_free         = ::AccountInfoDouble(ACCOUNT_MARGIN_FREE);                             //   Free funds available for opening a position in a deposit currency
       this.m_margin_level        = ::AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);                            //   Margin level on an account in %
       this.m_margin_so_call      = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_CALL);                          //   Margin Call level
       this.m_margin_so_so        = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_SO);                            //   Stop Out level
       this.m_margin_initial      = ::AccountInfoDouble(ACCOUNT_MARGIN_INITIAL);                          //   Funds reserved on an account to ensure a guarantee amount for all pending orders 
       this.m_margin_maintenance  = ::AccountInfoDouble(ACCOUNT_MARGIN_MAINTENANCE);                      //   Funds reserved on an account to ensure a minimum amount for all open positions
       this.m_assets              = ::AccountInfoDouble(ACCOUNT_ASSETS);                                  //   Current assets on an account
       this.m_liabilities         = ::AccountInfoDouble(ACCOUNT_LIABILITIES);                             //   Current liabilities on an account
       this.m_commission_blocked  = ::AccountInfoDouble(ACCOUNT_COMMISSION_BLOCKED);                      //   Current sum of blocked commissions on an account
      
    //--- set account string properties
       this.m_name                = ::AccountInfoString(ACCOUNT_NAME);                                    //   Client name
       this.m_currency            = ::AccountInfoString(ACCOUNT_CURRENCY);                                //   Deposit currency
       this.m_company             = ::AccountInfoString(ACCOUNT_COMPANY);                                 //   Name of a company serving account
      }
    
    

    Método de comparación de dos objetos:

    //+------------------------------------------------------------------+
    //| Method for comparing two objects                                 |
    //+------------------------------------------------------------------+
    int CAccount::Compare(const CObject *node,const int mode=0) const
      {
       const CAccount *obj=node;
       return(this.Login()>obj.Login()   ? 1 : this.Login()<obj.Login()   ? -1 :
              this.Server()>obj.Server() ? 1 : this.Server()<obj.Server() ? -1 : 0);
      }
    
    

    El método compara dos objetos de cuenta usando solo dos propiedades: el login y el nombre del servidor. Si los logins de los dos objetos comparados son iguales, se comprobará la igualdad del nombre del servidor. Si los servidores son los mismos, entonces los dos objetos serán iguales. De lo contrario, se retornará 1 o -1 dependiendo de si el valor de la propiedad que se compara entre los dos objetos es mayor o menor.

    Métodos que retornan las descripciones de algunas propiedades del objeto de cuenta:

    //+------------------------------------------------------------------+
    //| Return the description of the trading account type               |
    //+------------------------------------------------------------------+
    string CAccount::TradeModeDescription(void) const
      {
       string mode=::StringSubstr(::EnumToString(this.TradeMode()), 19);
       if(mode.Lower())
          mode.SetChar(0, ushort(mode.GetChar(0)-32));
       return mode;
      }
    //+------------------------------------------------------------------+
    //| Return the description of the margin calculation mode            |
    //+------------------------------------------------------------------+
    string CAccount::MarginModeDescription(void) const
      {
       string mode=::StringSubstr(::EnumToString(this.MarginMode()), 20);
       ::StringReplace(mode, "RETAIL_", "");
       if(mode.Lower())
          mode.SetChar(0, ushort(mode.GetChar(0)-32));
       return mode;
      }
    
    

    Estos métodos se usan para crear una descripción de la cuenta en el método Description :

    //+------------------------------------------------------------------+
    //| Return the account description                                   |
    //+------------------------------------------------------------------+
    string CAccount::Description(void) const
      {
       return(::StringFormat("%I64d: %s (%s, %s, %.2f %s, %s)",
                             this.Login(), this.Name(), this.Company(), this.TradeModeDescription(),
                             this.Balance(), this.Currency(), this.MarginModeDescription()));
      }
    
    

    El método retorna una línea en el formato

    68008618: Artem (MetaQuotes Ltd., Demo, 10779.50 USD, Hedging)
    
    

    Esta línea se puede imprimir en el registro usando el método Print() de esta clase.

    Ahora necesitamos crear una clase que almacene las listas de todas las cuentas que se han conectado durante el funcionamiento del programa de servicio.


    Clase de colección de cuentas

    En la carpeta del terminal \MetaTrader 5\MQL5\Services\AccountReporter\ crearemos un nuevo archivo Accounts.mqh de la clase CAccounts.
    La clase debe heredarse de la clase de objeto básico de la biblioteca estándar CObject, mientras que el archivo de clase de cuenta debe incluirse en el archivo que estamos creando:

    //+------------------------------------------------------------------+
    //|                                                     Accounts.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 "Account.mqh"
    
    //+------------------------------------------------------------------+
    //| Account collection class                                         |
    //+------------------------------------------------------------------+
    class CAccounts : public CObject
      {
      }
    

    En las secciones privada, protegida y pública declararemos los métodos para que la clase funcione:

    //+------------------------------------------------------------------+
    //| Account collection class                                         |
    //+------------------------------------------------------------------+
    class CAccounts : public CObject
      {
    private:
       CArrayObj         m_list;           // List of account objects
       CAccount          m_tmp;            // Temporary account object for searching
       
    protected:
    //--- Create a new account object and add it to the list
       CAccount         *Add(const long login, const string server);
       
    public:
    //--- Create a new account object
       bool              Create(const long login, const string server);
       
    //--- Return the pointer to the specified account object by (1) login and server, (2) index in the list
       CAccount         *Get(const long login, const string server);
       CAccount         *Get(const int index)                         const { return this.m_list.At(index);  }
    
    //--- Combine the lists of account positions and return the combined one
       CArrayObj        *GetCommonPositionsList(void);
    
    //--- Return the list of positions for the specified account
       CArrayObj        *GetAccountPositionsList(const long login, const string server);
    
    //--- Return the number of stored accounts
       int               Total(void)                                  const { return this.m_list.Total();    }
       
    //--- Update the lists of positions of the specified account
       bool              PositionsRefresh(const long login, const string server);
    
    //--- Constructor/destructor
                         CAccounts();
                        ~CAccounts();
      };
    

    Vamos a ver la implementación de los métodos declarados.

    En el constructor de la clase, estableceremos el indicador de lista clasificada para la lista de cuentas:

    //+------------------------------------------------------------------+
    //| Constructor                                                      |
    //+------------------------------------------------------------------+
    CAccounts::CAccounts()
      {
       this.m_list.Sort();
      }
    
    

    En el destructor de clase, eliminaremos la lista de cuentas:

    //+------------------------------------------------------------------+
    //| Destructor                                                       |
    //+------------------------------------------------------------------+
    CAccounts::~CAccounts()
      {
       this.m_list.Clear();
      }
    
    

    Método protegido que crea un nuevo objeto de cuenta y lo añade a la lista:

    //+------------------------------------------------------------------+
    //| Create a new account object and add it to the list               |
    //+------------------------------------------------------------------+
    CAccount *CAccounts::Add(const long login,const string server)
      {
    //--- Create a new account object
       CAccount *account=new CAccount(login, server);
       if(account==NULL)
          return NULL;
    //--- If the created object is not added to the list, remove it and return NULL
       if(!this.m_list.Add(account))
         {
          delete account;
          return NULL;
         }
    //--- Return the pointer to a created object
       return account;
      }
    
    

    Este es un método protegido y funciona como parte de un método público que crea un nuevo objeto de cuenta:

    //+------------------------------------------------------------------+
    //| Create a new account object                                      |
    //+------------------------------------------------------------------+
    bool CAccounts::Create(const long login,const string server)
      {
    //--- Set login and server to the temporary account object
       this.m_tmp.SetLogin(login);
       this.m_tmp.SetServer(server);
       
    //--- Set the sorted list flag for the account object list
    //--- and get the object index having the same login and server as the ones the temporary object has
       this.m_list.Sort();
       int index=this.m_list.Search(&this.m_tmp);
       
    //--- Return the flag of an object being successfully added to the list (Add method operation result) or 'false' if the object is already in the list
       return(index==WRONG_VALUE ? this.Add(login, server)!=NULL : false);
      }
    
    

    Método que retorna el puntero al objeto de cuenta especificado:

    //+------------------------------------------------------------------+
    //| Return the pointer to the specified account object               |
    //+------------------------------------------------------------------+
    CAccount *CAccounts::Get(const long login,const string server)
      {
    //--- Set login and server to the temporary account object
       this.m_tmp.SetLogin(login);
       this.m_tmp.SetServer(server);
       
    //--- Set the sorted list flag for the account object list
    //--- and get the object index having the same login and server as the ones the temporary object has
       this.m_list.Sort();
       int index=this.m_list.Search(&this.m_tmp);
    
    //--- Return the pointer to the object in the list by index or NULL if the index is -1
       return this.m_list.At(index);
      }
    
    

    Método que actualiza las listas de posiciones de la cuenta especificada:

    //+------------------------------------------------------------------+
    //| Update the lists of positions of the specified account           |
    //+------------------------------------------------------------------+
    bool CAccounts::PositionsRefresh(const long login, const string server)
      {
    //--- Get the pointer to the account object with the specified login and server
       CAccount *account=this.Get(login, server);
       if(account==NULL)
          return false;
    
    //--- If the received object is not the current account,
       if(account.Login()!=::AccountInfoInteger(ACCOUNT_LOGIN) || account.Server()!=::AccountInfoString(ACCOUNT_SERVER))
         {
          //--- inform that updating data of the non-current account will result in incorrect data and return 'false'
          ::Print("Error. Updating the list of positions for a non-current account will result in incorrect data.");
          return false;
         }
    
    //--- Return the result of updating the current account data
       return account.PositionsRefresh();
      }
    
    

    Método que combina las listas de posiciones de la cuenta y retorna una lista combinada:

    //+--------------------------------------------------------------------+
    //| Combine the lists of account positions and return the combined one |
    //+--------------------------------------------------------------------+
    CArrayObj *CAccounts::GetCommonPositionsList(void)
      {
    //--- Create a new list and reset the flag of managing memory
       CArrayObj *list=new CArrayObj();
       if(list==NULL)
          return NULL;
       list.FreeMode(false);
    
    //--- In the loop through the list of accounts,
       int total=this.m_list.Total();
       for(int i=0; i<total; i++)
         {
          //--- get another account object
          CAccount *account=this.m_list.At(i);
          if(account==NULL)
             continue;
          
          //--- Get the list of closed account positions
          CArrayObj *src=account.GetPositionsList();
          if(src==NULL)
             continue;
    
          //--- If this is the first account in the list,
          if(i==0)
            {
             //--- copy the elements from the account positions list to the new list
             if(!list.AssignArray(src))
               {
                delete list;
                return NULL;
               }
            }
          //--- If this is not the first account in the list,
          else
            {
             //--- add elements from the account position list to the end of the new list
             if(!list.AddArray(src))
                continue;
            }
         }
       
    //--- Send a new list to the storage
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
         
    //--- Return the pointer to the created and filled list
       return list;
      }
    
    

    Método que retorna una lista de posiciones para la cuenta especificada:

    //+------------------------------------------------------------------+
    //| Return the list of positions for the specified account           |
    //+------------------------------------------------------------------+
    CArrayObj *CAccounts::GetAccountPositionsList(const long login,const string server)
      {
       CAccount *account=this.Get(login, server);
       return(account!=NULL ? account.GetPositionsList() : NULL);
      }
    
    

    Obtenemos el puntero al objeto de cuenta según el login y el servidor y retornamos el puntero a su lista de posiciones históricas, o NULL si no ha sido posible obtener el objeto de cuenta.

    Todos los métodos de esta clase se describen con detalle en los comentarios. Si algo aún no le queda claro, podrá formular preguntas en la discusión del artículo.

    Ya tenemos listas todas las clases sobre cuya base crearemos el programa de servicio. Ahora comenzaremos a implementar el programa en sí.


    Programa de servicio para crear informes comerciales y enviar notificaciones

    Vamos a decidir cómo debería funcionar el programa.

    Al iniciar el servicio, se comprobará la presencia de un MetaQuotes ID en el terminal del cliente y el permiso para enviar notificaciones push al smartphone.

    Podrá encontrar estas configuraciones en el menú "Tools -- Options", en la pestaña "Notifications":


    Si no hay ningún valor en el campo de MetaQuotes ID o la casilla de verificación Enable Push notifications no está marcada, el servicio mostrará una ventana solicitándole que configure estos parámetros. Si decide no configurar estos parámetros, debería recibir una advertencia indicando que no hay MQID o que no está permitido enviar notificaciones al smartphone y que todos los mensajes quedarán solo en el registro.

    Si configura todos los parámetros, los informes se enviarán tanto al smartphone como al registro del terminal "Expertos". En el ciclo principal, el programa comprobará constantemente el estado de la configuración del envío de notificaciones en el terminal. Por consiguiente, si no ha configurado el permiso para enviar notificaciones al iniciar el servicio, siempre podrá habilitarlo cuando se haya iniciado el programa de servicio: este verá los cambios y establecerá la bandera correspondiente.

    En la configuración del servicio podrá seleccionar los parámetros de envío de mensajes y en qué periodos temporales necesita realizar informes:

    • Parámetros generales de los informes
      • ¿Qué cuentas usar para los informes? (todas o actuales),
      • crear informes según los símbolos: (sí/no) - primero se crea un informe y luego se crean informes separados a partir de él para cada uno de los símbolos involucrados en la transacción,
      • crear informes según el número mágico: (sí/no) - primero se crea un informe y luego se crean informes separados a partir de él para cada uno de los números mágicos involucrados en la transacción,
      • incluir comisiones en los informes: (sí/no) - si está habilitado, además del monto total de todos los costes, se mostrarán por separado los costes de comisiones, swaps y pagos por la realización de transacciones,
      • incluir en los informes las posibles pérdidas en los spreads al cerrar las posiciones: (sí/no) - si está habilitado, se mostrará aparte la suma del coste de todos los gastos posibles en el spread al cerrar;
    • Configuración de informes diarios
      • enviar informes de las últimas 24 horas; también se aplica a informes para periodos de tiempo específicos: (sí/no) - si está habilitado, los informes de las últimas 24 horas y para los intervalos temporales de negociación configurables (para la cantidad de días, meses y años) se enviarán diariamente a la hora especificada.
      • hora de envío del informe: (por defecto, 8),
      • minutos de envío del informe: (por defecto, 0);
    • Configuración de los informes diarios para periodos temporales personalizados
      • enviar informes para el número de días especificado: (sí/no) - si está habilitado, los informes para el número de días especificado se crearán diariamente a la hora anteriormente indicada; el número de días del informe se calculará restando el número de días especificado a la fecha actual,
      • número de días para los informes para el número de días especificado: (por defecto 7),
      • enviar informes para el número especificado de meses: (sí/no) - si está habilitado, los informes para el número especificado de meses se crearán diariamente a la hora especificada anteriormente; el número de meses del informe se calculará restando el número de meses especificado a la fecha actual,
      • número de meses para los informes para el número de meses especificado: (por defecto 3),
      • enviar informes para el número de años especificado: (sí/no) - si está habilitado, los informes para el número de años especificado se crearán diariamente a la hora especificada anteriormente; el número de años del informe se calculará restando el número de años especificado a la fecha actual,
      • número de años para los informes para el número de años especificado: (por defecto 2);
    • Configuración de informes semanales para todos los demás periodos
      • día de la semana para enviar informes semanales: (el día por defecto es sábado): cuando llegue el día especificado, se crearán y enviarán los informes indicados en la configuración a continuación,
      • hora para enviar los informes: (por defecto 8),
      • minutos para enviar los informes: (por defecto 0),
      • enviar informes para el periodo desde el comienzo de la semana actual: (sí/no) - si está habilitado, se creará un informe para el periodo desde el comienzo de la semana actual semanalmente en el día especificado,
      • enviar informes para el periodo desde el comienzo del mes actual: (sí/no) - si está habilitado, se creará un informe para el periodo desde el comienzo del mes actual semanalmente en el día especificado,
      • enviar informes para el periodo desde el comienzo del año actual: (sí/no) - si está habilitado, se creará un informe para el periodo desde el comienzo del año actual semanalmente en el día especificado,
      • enviar informes para todo el periodo comercial: (sí/no) - si está habilitado, se creará un informe para todo el periodo comercial semanalmente en el día especificado.

    Estas configuraciones bastarán para cubrir la mayoría de los periodos comerciales de interés para crear informes sobre ellos.

    En la carpeta del terminal \MQL5\Services\AccountReporter\ crearemos un nuevo archivo de programa de servicio Reporter.mq5 :


    Ahora introduciremos las macrosustituciones necesarias, conectaremos los archivos externos, escribiremos las enumeraciones, los parámetros de entrada y las variables globales para que el programa funcione:

    //+------------------------------------------------------------------+
    //|                                                     Reporter.mq5 |
    //|                                  Copyright 2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property service
    #property copyright "Copyright 2024, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #define   COUNTER_DELAY       1000                                   // Counter delay in milliseconds during the working loop
    #define   REFRESH_ATTEMPTS    5                                      // Number of attempts to obtain correct account data
    #define   REFRESH_DELAY       500                                    // Delay in milliseconds before next attempt to get data
    #define   TABLE_COLUMN_W      10                                     // Width of the statistics table column for displaying in the journal
    
    #include <Arrays\ArrayString.mqh>                                    // Dynamic array of string variables for a symbol list object
    #include <Arrays\ArrayLong.mqh>                                      // Dynamic array of long type variables for the magic number list object
    #include <Tools\DateTime.mqh>                                        // Expand the MqlDateTime structure
    #include "Accounts.mqh"                                              // Collection class of account objects
    
    //+------------------------------------------------------------------+
    //| Enumerations                                                     |
    //+------------------------------------------------------------------+
    enum ENUM_USED_ACCOUNTS                                              // Enumerate used accounts in statistics
      {
       USED_ACCOUNT_CURRENT,                                             // Current Account only
       USED_ACCOUNTS_ALL,                                                // All used accounts
      };
    
    enum ENUM_REPORT_RANGE                                               // Enumerate statistics ranges
      {
       REPORT_RANGE_DAILY,                                               // Day
       REPORT_RANGE_WEEK_BEGIN,                                          // Since the beginning of the week
       REPORT_RANGE_MONTH_BEGIN,                                         // Since the beginning of the month
       REPORT_RANGE_YEAR_BEGIN,                                          // Since the beginning of the year
       REPORT_RANGE_NUM_DAYS,                                            // Number of days
       REPORT_RANGE_NUM_MONTHS,                                          // Number of months
       REPORT_RANGE_NUM_YEARS,                                           // Number of years
       REPORT_RANGE_ALL,                                                 // Entire period
      };
      
    enum ENUM_REPORT_BY                                                  // Enumerate statistics filters
      {
       REPORT_BY_RANGE,                                                  // Date range
       REPORT_BY_SYMBOLS,                                                // By symbols
       REPORT_BY_MAGICS,                                                 // By magic numbers
      };
      
    //+------------------------------------------------------------------+
    //| Inputs                                                           |
    //+------------------------------------------------------------------+
    input group                "============== Report options =============="
    input ENUM_USED_ACCOUNTS   InpUsedAccounts      =  USED_ACCOUNT_CURRENT;// Accounts included in statistics
    input bool                 InpReportBySymbols   =  true;             // Reports by Symbol
    input bool                 InpReportByMagics    =  true;             // Reports by Magics
    input bool                 InpCommissionsInclude=  true;             // Including Comissions
    input bool                 InpSpreadInclude     =  true;             // Including Spread
    
    input group                "========== Daily reports for daily periods =========="
    input bool                 InpSendDReport       =  true;             // Send daily report (per day and specified periods)
    input uint                 InpSendDReportHour   =  8;                // Hour of sending the report (Local time)
    input uint                 InpSendDReportMin    =  0;                // Minutes of sending the report (Local time)
    
    input group                "========= Daily reports for specified periods ========="
    input bool                 InpSendSReportDays   =  true;             // Send a report for the specified num days
    input uint                 InpSendSReportDaysN  =  7;                // Number of days to report for the specified number of days
    input bool                 InpSendSReportMonths =  true;             // Send a report for the specified num months
    input uint                 InpSendSReportMonthsN=  3;                // Number of months to report for the specified number of months
    input bool                 InpSendSReportYears  =  true;             // Send a report for the specified num years
    input uint                 InpSendSReportYearN  =  2;                // Number of years to report for the specified number of years
    
    input group                "======== Weekly reports for all other periods ========"
    input ENUM_DAY_OF_WEEK     InpSendWReportDayWeek=  SATURDAY;         // Day of sending the reports (Local time)
    input uint                 InpSendWReportHour   =  8;                // Hour of sending the reports (Local time)
    input uint                 InpSendWReportMin    =  0;                // Minutes of sending the reports (Local time)
    input bool                 InpSendWReport       =  true;             // Send a report for the current week
    input bool                 InpSendMReport       =  false;            // Send a report for the current month
    input bool                 InpSendYReport       =  false;            // Send a report for the current year
    input bool                 InpSendAReport       =  false;            // Send a report for the entire trading period
    
    //+------------------------------------------------------------------+
    //| Global variables                                                 |
    //+------------------------------------------------------------------+
    CAccounts      ExtAccounts;                                          // Account management object
    long           ExtLogin;                                             // Current account login
    string         ExtServer;                                            // Current account server
    bool           ExtNotify;                                            // Push notifications enabling flag
    
    //+------------------------------------------------------------------+
    //| Service program start function                                   |
    //+------------------------------------------------------------------+
    void OnStart()
      {
      }
    
    

    Vemos que hemos conectado el archivo \MQL5\Include\Tools\DateTime.mqh. Esta es una estructura heredada del estándar MqlDateTime :

    //+------------------------------------------------------------------+
    //|                                                     DateTime.mqh |
    //|                             Copyright 2000-2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    //+------------------------------------------------------------------+
    //| Structure CDateTime.                                             |
    //| Purpose: Working with dates and time.                            |
    //|         Extends the MqlDateTime structure.                       |
    //+------------------------------------------------------------------+
    struct CDateTime : public MqlDateTime
      {
       //--- additional information
       string            MonthName(const int num) const;
       string            ShortMonthName(const int num) const;
       string            DayName(const int num) const;
       string            ShortDayName(const int num) const;
       string            MonthName(void)               const { return(MonthName(mon));            }
       string            ShortMonthName(void)          const { return(ShortMonthName(mon));       }
       string            DayName(void)                 const { return(DayName(day_of_week));      }
       string            ShortDayName(void)            const { return(ShortDayName(day_of_week)); }
       int               DaysInMonth(void) const;
       //--- data access
       datetime          DateTime(void)                      { return(StructToTime(this));        }
       void              DateTime(const datetime value)      { TimeToStruct(value,this);          }
       void              DateTime(const MqlDateTime& value)  { this=value;                        }
       void              Date(const datetime value);
       void              Date(const MqlDateTime &value);
       void              Time(const datetime value);
       void              Time(const MqlDateTime &value);
       //--- settings
       void              Sec(const int value);
       void              Min(const int value);
       void              Hour(const int value);
       void              Day(const int value);
       void              Mon(const int value);
       void              Year(const int value);
       //--- increments
       void              SecDec(int delta=1);
       void              SecInc(int delta=1);
       void              MinDec(int delta=1);
       void              MinInc(int delta=1);
       void              HourDec(int delta=1);
       void              HourInc(int delta=1);
       void              DayDec(int delta=1);
       void              DayInc(int delta=1);
       void              MonDec(int delta=1);
       void              MonInc(int delta=1);
       void              YearDec(int delta=1);
       void              YearInc(int delta=1);
       //--- check
       void              DayCheck(void);
      };
    
    

    Esta estructura contiene métodos listos para usar para trabajar con fechas y horas. Y nosotros necesitaremos calcular la hora de inicio del periodo estadístico. Entonces, para no calcular de forma independiente la exactitud de las fechas obtenidas al restar el número de días, semanas, meses y años de la fecha actual, usaremos los métodos de esta estructura. Aquí se realizan todos los cálculos, corrigiéndose además los valores incorrectos. Si, por ejemplo, a la fecha actual se restan más días que los que tiene el mes, entonces deberemos ajustar la fecha resultante: calcular cuál debería ser el mes y qué día debería ser, considerando los años bisiestos. Pero resulta más fácil simplemente tomar y utilizar los métodos de reducción de días, meses y años de una estructura dada para obtener directamente la fecha final correcta.

    El propio programa de servicio debe funcionar en un ciclo infinito. Así, organizamos en un ciclo un retraso de aproximadamente un segundo, después del final de la espera se organizarán todas las comprobaciones y cálculos. Para mayor claridad, todo el cuerpo del ciclo está dividido en bloques titulados. Veamos el cuerpo del programa en sí:

    //+------------------------------------------------------------------+
    //| Service program start function                                   |
    //+------------------------------------------------------------------+
    void OnStart()
      {
    //---
       CArrayObj  *PositionsList  =  NULL;          // List of closed account positions
       long        account_prev   =  0;             // Previous login
       double      balance_prev   =  EMPTY_VALUE;   // Previous balance
       bool        Sent           =  false;         // Flag of sent report for non-daily periods
       int         day_of_year_prev= WRONG_VALUE;   // The previous day number of the year
       
    //--- Create lists of symbols and magic numbers traded in history and a list of messages for Push notifications
       CArrayString  *SymbolsList =  new CArrayString();
       CArrayLong    *MagicsList  =  new CArrayLong();
       CArrayString  *MessageList =  new CArrayString();
       if(SymbolsList==NULL || MagicsList==NULL || MessageList==NULL)
         {
          Print("Failed to create list CArrayObj");
          return;
         }
         
    //--- Check for the presence of MetaQuotes ID and permission to send notifications to it
       ExtNotify=CheckMQID();
       if(ExtNotify)
          Print(MQLInfoString(MQL_PROGRAM_NAME)+"-Service notifications OK");
    
    //--- The main loop
       int count=0;
       while(!IsStopped())
         {
          //+------------------------------------------------------------------+
          //| Delay in the loop                                                |
          //+------------------------------------------------------------------+
          //--- Increase the loop counter. If the counter has not exceeded the specified value, repeat
          Sleep(16);
          count+=10;
          if(count<COUNTER_DELAY)
             continue;
          //--- Waiting completed. Reset the loop counter
          count=0;
          
          //+------------------------------------------------------------------+
          //| Check notification settings                                      |
          //+------------------------------------------------------------------+
          //--- If the notification flag is not set, we check the notification settings in the terminal and, if activated, we report this
          if(!ExtNotify && TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
            {
             Print("Now MetaQuotes ID is specified and sending notifications is allowed");
             SendNotification("Now MetaQuotes ID is specified and sending notifications is allowed");
             ExtNotify=true;
            }
          //--- If the notification flag is set, but the terminal does not have permission for them, we report this
          if(ExtNotify && (!TerminalInfoInteger(TERMINAL_MQID) || !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)))
            {
             string caption=MQLInfoString(MQL_PROGRAM_NAME);
             string message="The terminal has a limitation on sending notifications. Please check your notification settings";
             MessageBox(message, caption, MB_OK|MB_ICONWARNING);
             ExtNotify=false;
            }
          
          //+------------------------------------------------------------------+
          //| Change account                                                   |
          //+------------------------------------------------------------------+
          //--- If the current login is not equal to the previous one
          if(AccountInfoInteger(ACCOUNT_LOGIN)!=account_prev)
            {
             //--- if we failed to wait for the account data to be updated, repeat on the next loop iteration
             if(!DataUpdateWait(balance_prev))
                continue;
             
             //--- Received new account data
             //--- Save the current login and balance as previous ones for the next check
             account_prev=AccountInfoInteger(ACCOUNT_LOGIN);
             balance_prev=AccountInfoDouble(ACCOUNT_BALANCE);
             
             //--- Reset the sent message flag and call the account change handler
             Sent=false;
             AccountChangeHandler();
            }
          
          //+------------------------------------------------------------------+
          //| Daily reports                                                    |
          //+------------------------------------------------------------------+
          //--- Fill the structure with data about local time and date
          MqlDateTime tm={};
          TimeLocal(tm);
          
          //--- Clear the list of messages sent to MQID
          MessageList.Clear();
          
          //--- If the current day number in the year is not equal to the previous one, it is the beginning of a new day
          if(tm.day_of_year!=day_of_year_prev)
            {
             //--- If hours/minutes have reached the specified values for sending statistics
             if(tm.hour>=(int)InpSendDReportHour && tm.min>=(int)InpSendDReportMin)
               {
                //--- If sending daily statistics is allowed
                if(InpSendDReport)
                  {
                   //--- update the lists of closed positions for the day on the current account
                   ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
                   //--- if the settings are set to receive statistics from all accounts -
                   //--- get a list of closed positions of all accounts that were active when the service was running
                   if(InpUsedAccounts==USED_ACCOUNTS_ALL)
                      PositionsList=ExtAccounts.GetCommonPositionsList();
                   //--- otherwise, get the list of closed positions of the current account only
                   else
                      PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer);
                   
                   //--- Create messages about trading statistics for a daily time range,
                   //--- print the generated messages to the log and send them to MQID
                   SendReport(REPORT_RANGE_DAILY, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- If the settings allow sending trading statistics for the specified number of days,
                   //--- Create messages about trade statistics for the number of days in InpSendSReportDaysN,
                   //--- print the created messages to the log and add them to the list for sending to MQID
                   if(InpSendSReportDays)
                      SendReport(REPORT_RANGE_NUM_DAYS, InpSendSReportDaysN, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- If the settings allow sending trading statistics for the specified number of months,
                   //--- Create messages about trade statistics for the number of months in InpSendSReportMonthsN,
                   //--- print the created messages to the log and add them to the list for sending to MQID
                   if(InpSendSReportMonths)
                      SendReport(REPORT_RANGE_NUM_MONTHS, InpSendSReportMonthsN, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- If the settings allow sending trading statistics for the specified number of years,
                   //--- Create messages about trade statistics for the number of years in InpSendSReportYearN,
                   //--- print the created messages to the log and add them to the list for sending to MQID
                   if(InpSendSReportYears)
                      SendReport(REPORT_RANGE_NUM_YEARS, InpSendSReportYearN, PositionsList, SymbolsList, MagicsList, MessageList);
                  }
    
                //--- Set the current day as the previous one for subsequent verification
                day_of_year_prev=tm.day_of_year;
               }
            }
            
          //+------------------------------------------------------------------+
          //| Weekly reports                                                   |
          //+------------------------------------------------------------------+
          //--- If the day of the week is equal to the one set in the settings,
          if(tm.day_of_week==InpSendWReportDayWeek)
            {
             //--- if the message has not been sent yet and it is time to send messages
             if(!Sent && tm.hour>=(int)InpSendWReportHour && tm.min>=(int)InpSendWReportMin)
               {
                //--- update the lists of closed positions on the current account 
                ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
                
                //--- if the settings are set to receive statistics from all accounts -
                //--- get a list of closed positions of all accounts that were active when the service was running
                if(InpUsedAccounts==USED_ACCOUNTS_ALL)
                   PositionsList=ExtAccounts.GetCommonPositionsList();
                //--- otherwise, get the list of closed positions of the current account only
                else
                   PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer);
                
                //--- If the settings allow sending trading statistics for a week,
                //--- Create messages about trading statistics from the beginning of the current week,
                //--- print the created messages to the log and add them to the list for sending to MQID
                if(InpSendWReport)
                   SendReport(REPORT_RANGE_WEEK_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- If the settings allow sending trading statistics for a month,
                //--- Create messages about trading statistics from the beginning of the current month,
                //--- print the created messages to the log and add them to the list for sending to MQID
                if(InpSendMReport)
                   SendReport(REPORT_RANGE_MONTH_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- If the settings allow sending trading statistics for a year,
                //--- Create messages about trading statistics from the beginning of the current year,
                //--- print the created messages to the log and add them to the list for sending to MQID
                if(InpSendYReport)
                   SendReport(REPORT_RANGE_YEAR_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- If the settings allow sending trading statistics for the entire period,
                //--- Create messages about trading statistics from the start of the epoch (01.01.1970 00:00),
                //--- print the created messages to the log and add them to the list for sending to MQID
                if(InpSendAReport)
                   SendReport(REPORT_RANGE_ALL, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Set the flag that all messages with statistics are sent to the journal
                Sent=true;
               }
            }
          //--- If the day of the week specified in the settings for sending statistics has not yet arrived, reset the flag of sent messages
          else
             Sent=false;
    
          //--- If the list of messages to send to MQID is not empty, call the function for sending notifications to a smartphone
          if(MessageList.Total()>0)
             SendMessage(MessageList);
         }
         
       //+------------------------------------------------------------------+
       //| Service shutdown                                                 |
       //+------------------------------------------------------------------+
       //--- Clear and delete lists of messages, symbols and magic numbers
       if(MessageList!=NULL)
         {
          MessageList.Clear();
          delete MessageList;
         }
       if(SymbolsList!=NULL)
         {
          SymbolsList.Clear();
          delete SymbolsList;
         }
       if(MagicsList!=NULL)
         {
          MagicsList.Clear();
          delete MagicsList;
         }
      }
    

    Podemos ver que al iniciar el servicio se comprueba la presencia de permisos en el terminal para enviar notificaciones al smartphone. Así, se llama la función CheckMQID(), donde se comprueba cada una de las configuraciones y se realizan solicitudes para habilitar los parámetros requeridos en la configuración del terminal de cliente:

    //+------------------------------------------------------------------+
    //| Check for the presence of MetaQuotes ID                          |
    //| and permission to send notifications to the mobile terminal      |
    //+------------------------------------------------------------------+
    bool CheckMQID(void)
      {
       string caption=MQLInfoString(MQL_PROGRAM_NAME); // Message box header
       string message=caption+"-Service OK";           // Message box text
       int    mb_id=IDOK;                              // MessageBox() return code
       
    //--- If MQID is not installed in the terminal settings, we will make a request to install it with explanations on the procedure
       if(!TerminalInfoInteger(TERMINAL_MQID))
         {
          message="The client terminal does not have a MetaQuotes ID for sending Push notifications.\n"+
                  "1. Install the mobile version of the MetaTrader 5 terminal from the App Store or Google Play.\n"+
                  "2. Go to the \"Messages\" section of your mobile terminal.\n"+
                  "3. Click \"MQID\".\n"+
                  "4. In the client terminal, in the \"Tools - Settings\" menu, in the \"Notifications\" tab, in the MetaQuotes ID field, enter the received code.";
          mb_id=MessageBox(message, caption, MB_RETRYCANCEL|MB_ICONWARNING);
         }
         
    //--- If the Cancel button is pressed, inform about the refusal to use Push notifications
       if(mb_id==IDCANCEL)
         {
          message="You refused to enter your MetaQuotes ID. The service will send notifications to the “Experts” tab of the terminal";
          MessageBox(message, caption, MB_OK|MB_ICONINFORMATION);
         }
         
    //--- If the Retry button is pressed, 
       else
         {
          //--- If the terminal has MetaQuotes ID installed for sending Push notifications
          if(TerminalInfoInteger(TERMINAL_MQID))
            {
             //--- if the terminal does not have permission to send notifications to a smartphone,
             if(!TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
               {
                //--- show the message asking for permission to send notifications in the settings
                message="Please enable sending Push notifications in the terminal settings in the \"Notifications\" tab in the \"Tools - Settings\" menu.";
                mb_id=MessageBox(message, caption, MB_RETRYCANCEL|MB_ICONEXCLAMATION);
                
                //--- If the Cancel button is pressed in response to the message,
                if(mb_id==IDCANCEL)
                  {
                   //--- inform about the refusal to send notifications to a smartphone
                   string message="You have opted out of sending Push notifications. The service will send notifications to the “Experts” tab of the terminal.";
                   MessageBox(message, caption, MB_OK|MB_ICONINFORMATION);
                  }
                //--- If the Retry button is pressed in response to the message (this is expected to be done after enabling permission in the settings),
                //--- but there is still no permission to send notifications in the terminal,
                if(mb_id==IDRETRY && !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
                  {
                   //--- inform that the user has refused to send notifications to a smartphone, and messages will only be in the journal
                   string message="You have not allowed push notifications. The service will send notifications to the “Experts” tab of the terminal.";
                   MessageBox(message, caption, MB_OK|MB_ICONINFORMATION);
                  }
               }
            }
          //--- If the terminal has MetaQuotes ID installed for sending Push notifications,
          else
            {
             //--- inform that the terminal does not have MetaQuotes ID installed to send notifications to a smartphone, and messages will only be sent to the journal
             string message="You have not set your MetaQuotes ID. The service will send notifications to the “Experts” tab of the terminal";
             MessageBox(message, caption, MB_OK|MB_ICONINFORMATION);
            }
         }
    //--- Return the flag that MetaQuotes ID is set in the terminal and sending notifications is allowed
       return(TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED));
      }
    

    Tras ejecutarse la función presentada anteriormente, se inicia un ciclo, donde primero se controla la bandera de permiso para enviar notificaciones en el programa y la configuración de estos permisos en el terminal:

          //+------------------------------------------------------------------+
          //| Check notification settings                                      |
          //+------------------------------------------------------------------+
          //--- If the notification flag is not set, we check the notification settings in the terminal and, if activated, we report this
          if(!ExtNotify && TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
            {
             Print("Now MetaQuotes ID is specified and sending notifications is allowed");
             SendNotification("Now MetaQuotes ID is specified and sending notifications is allowed");
             ExtNotify=true;
            }
          //--- If the notification flag is set, but the terminal does not have permission for them, we report this
          if(ExtNotify && (!TerminalInfoInteger(TERMINAL_MQID) || !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)))
            {
             string caption=MQLInfoString(MQL_PROGRAM_NAME);
             string message="The terminal has a limitation on sending notifications. Please check your notification settings";
             MessageBox(message, caption, MB_OK|MB_ICONWARNING);
             ExtNotify=false;
            }
    
    

    Si algo cambia en el terminal, el servicio emitirá advertencias al respecto: si estaba habilitado y se ha deshabilitado, el servicio informará de que existen restricciones en el envío de notificaciones. Si, por el contrario, estaba desactivado, pero el usuario ha activado los permisos en la configuración, el servicio informará de que ahora todo está normal y enviará una notificación al respecto al smartphone.

    A continuación, en el ciclo viene una comprobación del cambio de cuenta:

          //+------------------------------------------------------------------+
          //| Change account                                                   |
          //+------------------------------------------------------------------+
          //--- If the current login is not equal to the previous one
          if(AccountInfoInteger(ACCOUNT_LOGIN)!=account_prev)
            {
             //--- if we failed to wait for the account data to be updated, repeat on the next loop iteration
             if(!DataUpdateWait(balance_prev))
                continue;
             
             //--- Received new account data
             //--- Save the current login and balance as previous ones for the next check
             account_prev=AccountInfoInteger(ACCOUNT_LOGIN);
             balance_prev=AccountInfoDouble(ACCOUNT_BALANCE);
             
             //--- Reset the sent message flag and call the account change handler
             Sent=false;
             AccountChangeHandler();
            }
    
    

    Tan pronto como el nombre de usuario cambia y se vuelve diferente al anteriormente recordado, se llamará la función para esperar la carga de los datos de la cuenta actual:

    //+------------------------------------------------------------------+
    //| Waiting for account data update                                  |
    //+------------------------------------------------------------------+
    bool DataUpdateWait(double &balance_prev)
      {
       int attempts=0;   // Number of attempts
     
    //--- Until the program stop flag is disabled and until the number of attempts is less than the number set in REFRESH_ATTEMPTS
       while(!IsStopped() && attempts<REFRESH_ATTEMPTS)
         {
          //--- If the balance of the current account differs from the balance of the previously saved balance value,
          //--- we assume that we were able to obtain the account data, return 'true'
          if(NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE)-balance_prev, 8)!=0)
             return true;
          
          //--- Wait half a second for the next attempt, increase the number of attempts and
          //--- log a message about waiting for data to be received and the number of attempts
          Sleep(500);
          attempts++;
          PrintFormat("%s::%s: Waiting for account information to update. Attempt %d", MQLInfoString(MQL_PROGRAM_NAME),__FUNCTION__, attempts);
         }
         
    //--- If failed to obtain the new account data after all attempts,
    //--- report this to the log, write an empty value to the "previous balance" and return 'false'
       PrintFormat("%s::%s: Could not wait for updated account data... Try again", MQLInfoString(MQL_PROGRAM_NAME),__FUNCTION__);
       balance_prev=EMPTY_VALUE;
       return false;
      }
    
    

    La función espera hasta que ya no se reciban datos del balance de la cuenta desde la memoria caché del terminal. Después de todo, el balance de la nueva cuenta probablemente sea diferente al de la cuenta anterior. La función realiza un número específico de intentos para obtener la diferencia entre el balance recordado de la cuenta anterior y el de la nueva. En caso de fallo (o si los balances siguen siendo iguales), la función eventualmente escribirá el valor EMPTY_VALUE en el balance anterior, mientras que la siguiente iteración del ciclo verificará si se han recibido los datos actuales de la nueva cuenta comparándolos con este nuevo valor, que muy probablemente ya no pueda estar en el balance de la cuenta.

    A continuación, en el ciclo, se organizan las comprobaciones de la fecha y la hora para crear informes diarios y semanales:

          //+------------------------------------------------------------------+
          //| Daily reports                                                    |
          //+------------------------------------------------------------------+
          //--- Fill the structure with data about local time and date
          MqlDateTime tm={};
          TimeLocal(tm);
          
          //--- Clear the list of messages sent to MQID
          MessageList.Clear();
          
          //--- If the current day number in the year is not equal to the previous one, it is the beginning of a new day
          if(tm.day_of_year!=day_of_year_prev)
            {
             //--- If hours/minutes have reached the specified values for sending statistics
             if(tm.hour>=(int)InpSendDReportHour && tm.min>=(int)InpSendDReportMin)
               {
                //--- If sending daily statistics is allowed
                if(InpSendDReport)
                  {
                   //--- update the lists of closed positions for the day on the current account
                   ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
                   //--- if the settings are set to receive statistics from all accounts -
                   //--- get a list of closed positions of all accounts that were active when the service was running
                   if(InpUsedAccounts==USED_ACCOUNTS_ALL)
                      PositionsList=ExtAccounts.GetCommonPositionsList();
                   //--- otherwise, get the list of closed positions of the current account only
                   else
                      PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer);
                   
                   //--- Create messages about trading statistics for a daily time range,
                   //--- print the generated messages to the log and send them to MQID
                   SendReport(REPORT_RANGE_DAILY, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- If the settings allow sending trading statistics for the specified number of days,
                   //--- Create messages about trade statistics for the number of days in InpSendSReportDaysN,
                   //--- print the created messages to the log and add them to the list for sending to MQID
                   if(InpSendSReportDays)
                      SendReport(REPORT_RANGE_NUM_DAYS, InpSendSReportDaysN, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- If the settings allow sending trading statistics for the specified number of months,
                   //--- Create messages about trade statistics for the number of months in InpSendSReportMonthsN,
                   //--- print the created messages to the log and add them to the list for sending to MQID
                   if(InpSendSReportMonths)
                      SendReport(REPORT_RANGE_NUM_MONTHS, InpSendSReportMonthsN, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- If the settings allow sending trading statistics for the specified number of years,
                   //--- Create messages about trade statistics for the number of years in InpSendSReportYearN,
                   //--- print the created messages to the log and add them to the list for sending to MQID
                   if(InpSendSReportYears)
                      SendReport(REPORT_RANGE_NUM_YEARS, InpSendSReportYearN, PositionsList, SymbolsList, MagicsList, MessageList);
                  }
    
                //--- Set the current day as the previous one for subsequent verification
                day_of_year_prev=tm.day_of_year;
               }
            }
            
          //+------------------------------------------------------------------+
          //| Weekly reports                                                   |
          //+------------------------------------------------------------------+
          //--- If the day of the week is equal to the one set in the settings,
          if(tm.day_of_week==InpSendWReportDayWeek)
            {
             //--- if the message has not been sent yet and it is time to send messages
             if(!Sent && tm.hour>=(int)InpSendWReportHour && tm.min>=(int)InpSendWReportMin)
               {
                //--- update the lists of closed positions on the current account 
                ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
                
                //--- if the settings are set to receive statistics from all accounts -
                //--- get a list of closed positions of all accounts that were active when the service was running
                if(InpUsedAccounts==USED_ACCOUNTS_ALL)
                   PositionsList=ExtAccounts.GetCommonPositionsList();
                //--- otherwise, get the list of closed positions of the current account only
                else
                   PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer);
                
                //--- If the settings allow sending trading statistics for a week,
                //--- Create messages about trading statistics from the beginning of the current week,
                //--- print the created messages to the log and add them to the list for sending to MQID
                if(InpSendWReport)
                   SendReport(REPORT_RANGE_WEEK_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- If the settings allow sending trading statistics for a month,
                //--- Create messages about trading statistics from the beginning of the current month,
                //--- print the created messages to the log and add them to the list for sending to MQID
                if(InpSendMReport)
                   SendReport(REPORT_RANGE_MONTH_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- If the settings allow sending trading statistics for a year,
                //--- Create messages about trading statistics from the beginning of the current year,
                //--- print the created messages to the log and add them to the list for sending to MQID
                if(InpSendYReport)
                   SendReport(REPORT_RANGE_YEAR_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- If the settings allow sending trading statistics for the entire period,
                //--- Create messages about trading statistics from the start of the epoch (01.01.1970 00:00),
                //--- print the created messages to the log and add them to the list for sending to MQID
                if(InpSendAReport)
                   SendReport(REPORT_RANGE_ALL, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Set the flag that all messages with statistics are sent to the journal
                Sent=true;
               }
            }
          //--- If the day of the week specified in the settings for sending statistics has not yet arrived, reset the flag of sent messages
          else
             Sent=false;
    
          //--- If the list of messages to send to MQID is not empty, call the function for sending notifications to a smartphone
          if(MessageList.Total()>0)
             SendMessage(MessageList);
    
    

    Aquí toda la lógica se comenta en el listado. Tenga en cuenta que para enviar mensajes a un smartphone, no será posible enviar un mensaje inmediatamente después de su creación en el ciclo. Como podemos encontrar muchos mensajes de este tipo (dependiendo de los informes seleccionados en la configuración), se establecen restricciones estrictas para las notificaciones Push: no más de dos mensajes por segundo y no más de diez mensajes por minuto. Por consiguiente, aquí se utiliza la siguiente técnica: todos los mensajes creados se escriben en la lista CArrayString de la biblioteca estándar. Una vez creados todos los informes, y si este array no está vacío, se llama a la función de envío de notificaciones al smartphone, en la que se organizan todos los retrasos de envío necesarios para no violar las restricciones establecidas.

    Veamos todas las funciones usadas en el funcionamiento del programa de servicio.

    Función que retorna una lista con un rango específico de estadísticas:

    //+------------------------------------------------------------------+
    //| Return a list with the specified statistics range                |
    //+------------------------------------------------------------------+
    CArrayObj *GetListDataRange(ENUM_REPORT_RANGE range, CArrayObj *list, datetime &time_start, const int num_periods)
      {
    //--- Current date
       CDateTime current={};
       current.Date(TimeLocal());
       
    //--- Period start date
       CDateTime begin_range=current;
       
    //--- Set the period start time to 00:00:00
       begin_range.Hour(0);
       begin_range.Min(0);
       begin_range.Sec(0);
       
    //--- Adjust the start date of the period depending on the specified period of required statistics
       switch(range)
         {
          //--- Day
          case REPORT_RANGE_DAILY       :  // decrease Day by 1
            begin_range.DayDec(1);       
            break;
            
          //--- Since the beginning of the week
          case REPORT_RANGE_WEEK_BEGIN  :  // decrease Day by (number of days passed in the week)-1
            begin_range.DayDec(begin_range.day_of_week==SUNDAY ? 6 : begin_range.day_of_week-1);
            break;
            
          //--- Since the beginning of the month
          case REPORT_RANGE_MONTH_BEGIN :  // set the first day of the month as Day
            begin_range.Day(1);
            break;
            
          //--- Since the beginning of the year
          case REPORT_RANGE_YEAR_BEGIN  :  // set Month to the first month of the year, and Day to the first day of the month
            begin_range.Mon(1);
            begin_range.Day(1);
            break;
            
          //--- Number of days
          case REPORT_RANGE_NUM_DAYS    :  // Decrease Day by the specified number of days
            begin_range.DayDec(fabs(num_periods));
            break;
            
          //--- Number of months
          case REPORT_RANGE_NUM_MONTHS  :  // Decrease Month by the specified number of months
            begin_range.MonDec(fabs(num_periods));
            break;
            
          //--- Number of years
          case REPORT_RANGE_NUM_YEARS   :  // Decrease Year by the specified number of years
            begin_range.YearDec(fabs(num_periods));
            break;
            
          //---REPORT_RANGE_ALL Entire period
          default                       :  // Set the date to 1970.01.01
            begin_range.Year(1970);
            begin_range.Mon(1);
            begin_range.Day(1);
            break;
         }
         
    //--- Write the start date of the period and return the pointer to the list of positions,
    //--- the opening time of which is greater than or equal to the start time of the requested period
       time_start=begin_range.DateTime();
       return CSelect::ByPositionProperty(list,POSITION_PROP_TIME,time_start,EQUAL_OR_MORE);
      }
    
    

    A la función se le transmite una indicación del rango de estadísticas con las que estamos trabajando (diariamente, desde el comienzo de la semana, mes, año, con un número específico de días, meses, años o el periodo comercial completo), y una lista de posiciones cerradas que deberán filtrarse según la fecha de inicio del periodo. A continuación, dependiendo del rango de las estadísticas recibidas, ajustamos la fecha de inicio del rango requerido, y luego obtenemos y retornamos una lista de posiciones cerradas desde el comienzo de la fecha calculada.

    Función de manejador de cambios de cuenta:

    //+------------------------------------------------------------------+
    //| Account change handler                                           |
    //+------------------------------------------------------------------+
    void AccountChangeHandler(void)
      {
    //--- Set the current account login and server
       long   login  = AccountInfoInteger(ACCOUNT_LOGIN);
       string server = AccountInfoString(ACCOUNT_SERVER);
       
    //--- Get the pointer to the account object based on the current account data
       CAccount *account = ExtAccounts.Get(login, server);
       
    //--- If the object is empty, create a new account object and get a pointer to it
       if(account==NULL && ExtAccounts.Create(login, server))
          account=ExtAccounts.Get(login, server);
       
    //--- If the account object is eventually not received, report this and leave
       if(account==NULL)
         {
          PrintFormat("Error getting access to account object: %I64d (%s)", login, server);
          return;
         }
       
    //--- Set the current login and server values from the account object data
       ExtLogin =account.Login();
       ExtServer=account.Server();
       
    //--- Display the account data in the journal and display a message about the start of creating the list of closed positions
       account.Print();
       Print("Beginning to create a list of closed positions...");
    
    //--- Create the list of closed positions and report the number of created positions and the time spent in the journal upon completion
       ulong start=GetTickCount();
       ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
       PrintFormat("A list of %d positions was created in %I64u ms", account.PositionsTotal(), GetTickCount()-start);
      }
    
    

    En el manejador, se crea un nuevo objeto de cuenta si no se ha utilizado antes, o se obtiene el puntero a una cuenta creada previamente si había una conexión con ella antes. Luego se inicia el proceso de creación de la lista de posiciones cerradas de esta cuenta. El registro muestra mensajes sobre el inicio de la creación de una lista de posiciones históricas, su finalización y la cantidad de milisegundos dedicados al proceso.

    Función que crea estadísticas para un rango de tiempo específico:

    //+------------------------------------------------------------------+
    //| Create statistics for the specified time range                   |
    //+------------------------------------------------------------------+
    void SendReport(ENUM_REPORT_RANGE range, int num_periods, CArrayObj *list_common, CArrayString *list_symbols, CArrayLong *list_magics, CArrayString *list_msg)
      {
       string array_msg[2] = {NULL, NULL};    // Array of messages (0) for displaying in the journal and (1) for sending to a smartphone
       datetime time_start = 0;               // Here we will store the start time of the statistics period
       CArrayObj *list_tmp = NULL;            // Temporary list for sorting by symbols and magic number
       
    //--- Get a list of positions for the 'range' period
       CArrayObj *list_range=GetListDataRange(range, list_common, time_start, num_periods);
       if(list_range==NULL)
          return;
          
    //--- If the list of positions is empty, report to the journal that there were no transactions for the given period of time
       if(list_range.Total()==0)
         {
          PrintFormat("\"%s\" no trades",ReportRangeDescription(range, num_periods));
          return;
         }
    
    //--- Create the lists of symbols and magic numbers of positions in the received list of closed positions for a period of time, while resetting them beforehand
       list_symbols.Clear();
       list_magics.Clear();
       CreateSymbolMagicLists(list_range, list_symbols, list_magics);
       
    //--- Create statistics on closed positions for the specified period,
    //--- print the generated statistics from array_msg[0] in the journal and
    //--- set the string from array_msg[1] to the list of messages for push notifications
       if(CreateStatisticsMessage(range, num_periods, REPORT_BY_RANGE, MQLInfoString(MQL_PROGRAM_NAME),time_start, list_range, list_symbols, list_magics, 0, array_msg))
         {
          Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_RANGE, time_start));       // Statistics title
          Print(StatisticsTableHeader("Symbols ", InpCommissionsInclude, InpSpreadInclude));  // Table header 
          Print(array_msg[0]);                                                                // Statistics for a period of time
          Print("");                                                                          // String indentation
          list_msg.Add(array_msg[1]);                                                         // Save the message for Push notifications to the list for later sending
         }
       
    //--- If statistics are allowed separately by symbols
       if(InpReportBySymbols)
         {
          //--- Display the statistics and table headers to the journal
          Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_SYMBOLS, time_start));
          Print(StatisticsTableHeader("Symbol ", InpCommissionsInclude, InpSpreadInclude));
          
          //--- In the loop by the list of symbols,
          for(int i=0; i<list_symbols.Total(); i++)
            {
             //--- get the name of the next symbol
             string symbol=list_symbols.At(i);
             if(symbol=="")
                continue;
             //--- sort out the list of positions leaving only positions with the received symbol
             list_tmp=CSelect::ByPositionProperty(list_range, POSITION_PROP_SYMBOL, symbol, EQUAL);
             
             //--- Create statistics on closed positions for the specified period by the current list symbol,
             //--- print the generated statistics from array_msg[0] and
             //--- set the string from array_msg[1] to the list of messages for push notifications
             if(CreateStatisticsMessage(range, num_periods, REPORT_BY_SYMBOLS, MQLInfoString(MQL_PROGRAM_NAME), time_start, list_tmp, list_symbols, list_magics, i, array_msg))
               {
                Print(array_msg[0]);
                list_msg.Add(array_msg[1]);
               }
            }
          //--- After the loop has completed for all symbols, display the separator line to the journal
          Print("");
         }
       
    //--- If statistics are allowed separately by magic numbers
       if(InpReportByMagics)
         {
          //--- Display the statistics and table headers to the journal
          Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_MAGICS, time_start));
          Print(StatisticsTableHeader("Magic ", InpCommissionsInclude, InpSpreadInclude));
          
          //--- In the loop by the list of magic numbers,
          for(int i=0; i<list_magics.Total(); i++)
            {
             //--- get the next magic number
             long magic=list_magics.At(i);
             if(magic==LONG_MAX)
                continue;
             //--- sort out the list of positions leaving only positions with the received magic number
             list_tmp=CSelect::ByPositionProperty(list_range, POSITION_PROP_MAGIC, magic, EQUAL);
             
             //--- Create statistics on closed positions for the specified period by the current list magic number,
             //--- print the generated statistics from array_msg[0] and
             //--- set the string from array_msg[1] to the list of messages for push notifications
             if(CreateStatisticsMessage(range, num_periods, REPORT_BY_MAGICS, MQLInfoString(MQL_PROGRAM_NAME), time_start, list_tmp, list_symbols, list_magics, i, array_msg))
               {
                Print(array_msg[0]);
                list_msg.Add(array_msg[1]);
               }
            }
          //--- After the loop has completed for all magic numbers, display the separator line to the journal
          Print("");
         }
      }
    
    

    En la función se llama una función para crear estadísticas para el periodo comercial especificado, y se muestra el encabezado, el título de la tabla y las estadísticas en un formato de recuadro debajo del encabezado de la tabla en el registro. Los mensajes para notificaciones push se escriben en el puntero a la lista de mensajes transmitidos al método. Si las estadísticas incluyen informes según los símbolos y números mágicos, entonces, después de que las estadísticas principales se envían al registro, se muestra el título y el encabezado de la tabla de estadísticas según los símbolos y los números mágicos. Y debajo de ellos hay un informe sobre símbolos y números mágicos en forma de tabla.

    Función que crea y retorna una fila de "encabezado" de tabla:

    //+------------------------------------------------------------------+
    //| Create and return the table header row                           |
    //+------------------------------------------------------------------+
    string StatisticsTableHeader(const string first, const bool commissions, const bool spreads)
      {
    //--- Declare and initialize the table column headers
       string h_trades="Trades ";
       string h_long="Long ";
       string h_short="Short ";
       string h_profit="Profit ";
       string h_max="Max ";
       string h_min="Min ";
       string h_avg="Avg ";
       string h_costs="Costs ";
    //--- table columns disabled in the settings
       string h_commiss=(commissions ? "Commiss " : "");
       string h_swap=(commissions    ? "Swap "    : "");
       string h_fee=(commissions     ? "Fee "     : "");
       string h_spread=(spreads      ? "Spread "  : "");
    //--- width of table columns
       int w=TABLE_COLUMN_W;
       int c=(commissions ? TABLE_COLUMN_W : 0);
       
    //--- Table column separators that can be disabled in the settings
       string sep1=(commissions ? "|" : "");
       string sep2=(spreads ? "|" : "");
       
    //--- Create a table header row
       return StringFormat("|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s%s%*s%s%*s%s%*s%s",
                           w,first,
                           w,h_trades,
                           w,h_long,
                           w,h_short,
                           w,h_profit,
                           w,h_max,
                           w,h_min,
                           w,h_avg,
                           w,h_costs,
                           c,h_commiss,sep1,
                           c,h_swap,sep1,
                           c,h_fee,sep1,
                           w,h_spread,sep2);
                           
      }
    
    

    La función crea una cadena con el formato

    |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    
    

    Las últimas cuatro columnas: su visualización dependerá de si se permite el uso de valores de comisión, swap, pago de transacción y spread en las estadísticas.

    La primera columna del encabezado contiene el nombre transmitido a la función en los parámetros, ya que las diferentes tablas deben tener encabezados distintos.

    Podrá leer más información sobre el formato de mensajes de texto en los artículos "Analizamos PrintFormat() y tomamos ejemplos listos para usar" y"StringFormat(). Visión general, ejemplos de uso ya preparados .

    Función que retorna el encabezado de la descripción del periodo estadístico solicitado:

    //+------------------------------------------------------------------+
    //| Return the description header of the requested statistics period |
    //+------------------------------------------------------------------+
    string StatisticsRangeTitle(const ENUM_REPORT_RANGE range, const int num_periods, const ENUM_REPORT_BY report_by, const datetime time_start, const string symbol=NULL, const long magic=LONG_MAX)
      {
       string report_by_str=
         (
          report_by==REPORT_BY_SYMBOLS  ?  (symbol==NULL     ?  "by symbols "  :  "by "+symbol+" ") :
          report_by==REPORT_BY_MAGICS   ?  (magic==LONG_MAX  ?  "by magics "   :  "by magic #"+(string)magic+" ") : ""
         );
       return StringFormat("Report %sfor the period \"%s\" from %s", report_by_str,ReportRangeDescription(range, num_periods), TimeToString(time_start, TIME_DATE));
      }
    
    

    Dependiendo del rango de estadísticas y de los filtros de las mismas (por símbolo, por número mágico o por fecha), se creará y retornará una cadena del siguiente tipo:

    Report for the period "3 months" from 2024.04.23 00:00
    
    

    o bien

    Report by symbols for the period "3 months" from 2024.04.23 00:00
    
    

    o bien

    Report by magics for the period "3 months" from 2024.04.23 00:00
    
    

    etc.

    Función que retorna el texto de un mensaje con las estadísticas:

    //+------------------------------------------------------------------+
    //| Return a message text with statistics                            |
    //+------------------------------------------------------------------+
    bool CreateStatisticsMessage(const ENUM_REPORT_RANGE range, const int num_periods, const ENUM_REPORT_BY report_by, const string header, const datetime time_start,
                                 CArrayObj *list, CArrayString *list_symbols, CArrayLong *list_magics, const int index, string &array_msg[])
      {
    //--- Get a symbol and a magic number by index from the passed lists
       string   symbol = list_symbols.At(index);
       long     magic  = list_magics.At(index);
    //--- If the passed lists are empty, or no data was received from them, return 'false'
       if(list==NULL || list.Total()==0 || (report_by==REPORT_BY_SYMBOLS && symbol=="") || (report_by==REPORT_BY_MAGICS && magic==LONG_MAX))
          return false;
       
       CPosition  *pos_min  =  NULL;          // Pointer to the position with the minimum property value
       CPosition  *pos_max  =  NULL;          // Pointer to the position with the maximum property value
       CArrayObj  *list_tmp =  NULL;          // Pointer to a temporary list for sorting by properties
       int         index_min=  WRONG_VALUE;   // Index of the position in the list with the minimum property value
       int         index_max=  WRONG_VALUE;   // Index of the position in the list with the maximum property value
       
    //--- Get the sum of the position properties from the list of positions
       double profit=PropertyValuesSum(list, POSITION_PROP_PROFIT);            // Total profit of positions in the list
       double commissions=PropertyValuesSum(list,POSITION_PROP_COMMISSIONS);   // Total commission of positions in the list
       double swap=PropertyValuesSum(list, POSITION_PROP_SWAP);                // General swap of positions in the list
       double fee=PropertyValuesSum(list, POSITION_PROP_FEE);                  // Total deal fee in the list 
       double costs=commissions+swap+fee;                                      // All commissions
       double spreads=PositionsCloseSpreadCostSum(list);                       // Total spread costs for all items in the list
       
    //--- Define text descriptions of all received values
       string s_0=(report_by==REPORT_BY_SYMBOLS ? symbol : report_by==REPORT_BY_MAGICS ? (string)magic : (string)list_symbols.Total())+" ";
       string s_trades=StringFormat("%d ", list.Total());
       string s_profit=StringFormat("%+.2f ", profit);
       string s_costs=StringFormat("%.2f ",costs);
       string s_commiss=(InpCommissionsInclude ? StringFormat("%.2f ",commissions) : "");
       string s_swap=(InpCommissionsInclude ? StringFormat("%.2f ",swap) : "");
       string s_fee=(InpCommissionsInclude ? StringFormat("%.2f ",fee) : "");
       string s_spread=(InpSpreadInclude ? StringFormat("%.2f ",spreads) : "");
       
    //--- Get the list of only long positions and create a description of their quantity
       list_tmp=CSelect::ByPositionProperty(list, POSITION_PROP_TYPE, POSITION_TYPE_BUY, EQUAL);
       string s_long=(list_tmp!=NULL ? (string)list_tmp.Total() : "0")+" ";
       
    //--- Get the list of only short positions and create a description of their quantity
       list_tmp=CSelect::ByPositionProperty(list, POSITION_PROP_TYPE, POSITION_TYPE_SELL, EQUAL);
       string s_short=(list_tmp!=NULL ? (string)list_tmp.Total() : "0")+" ";
       
    //--- Get the index of the position in the list with the maximum profit and create a description of the received value
       index_max=CSelect::FindPositionMax(list, POSITION_PROP_PROFIT);
       pos_max=list.At(index_max);
       double profit_max=(pos_max!=NULL ? pos_max.Profit() : EMPTY_VALUE);
       string s_max=(profit_max!=EMPTY_VALUE ? StringFormat("%+.2f ",profit_max) : "No trades ");
       
    //--- Get the index of the position in the list with the minimum profit and create a description of the received value
       index_min=CSelect::FindPositionMin(list, POSITION_PROP_PROFIT);
       pos_min=list.At(index_min);
       double profit_min=(pos_min!=NULL ? pos_min.Profit() : EMPTY_VALUE);
       string s_min=(profit_min!=EMPTY_VALUE ? StringFormat("%+.2f ",profit_min) : "No trades ");
       
    //--- Create a description of the average profit value of all positions in the list
       string s_avg=StringFormat("%.2f ", PropertyAverageValue(list, POSITION_PROP_PROFIT));
       
    //--- Table column width
       int w=TABLE_COLUMN_W;
       int c=(InpCommissionsInclude ? TABLE_COLUMN_W : 0);
    
    //--- Separators for table columns that can be disabled in the settings
       string sep1=(InpCommissionsInclude ? "|" : "");
       string sep2=(InpSpreadInclude ? "|" : "");
       
    //--- For displaying in the journal, create a string with table columns featuring the values obtained above
       array_msg[0]=StringFormat("|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s|%*s%s%*s%s%*s%s%*s%s",
                                 w,s_0,
                                 w,s_trades,
                                 w,s_long,
                                 w,s_short,
                                 w,s_profit,
                                 w,s_max,
                                 w,s_min,
                                 w,s_avg,
                                 w,s_costs,
                                 c,s_commiss,sep1,
                                 c,s_swap,sep1,
                                 c,s_fee,sep1,
                                 w,s_spread,sep2);
       
    //--- For sending MQID notifications, create a string with table columns featuring the values obtained above
       array_msg[1]=StringFormat("%s:\nTrades: %s Long: %s Short: %s\nProfit: %s Max: %s Min: %s Avg: %s\n%s%s%s%s%s",
                                 StatisticsRangeTitle(range, num_periods, report_by, time_start, (report_by==REPORT_BY_SYMBOLS ? symbol : NULL), (report_by==REPORT_BY_MAGICS ? magic : LONG_MAX)),
                                 s_trades,
                                 s_long,
                                 s_short,
                                 s_profit,
                                 s_max,
                                 s_min,
                                 s_avg,
                                 (costs!=0 ? "Costs: "+s_costs : ""),
                                 (InpCommissionsInclude && commissions!=0 ? " Commiss: "+s_commiss : ""),
                                 (InpCommissionsInclude && swap!=0        ? " Swap: "+s_swap       : ""),
                                 (InpCommissionsInclude && fee!=0         ? " Fee: "+s_fee         : ""),
                                 (InpSpreadInclude      && spreads!=0     ? " Spreads: "+s_spread  : ""));
    //--- All is successful
       return true;
      }
    
    

    La función usa el filtrado de listas y la búsqueda de índices de posiciones cerradas utilizando la clase CSelect que hemos escrito anteriormente. A partir de las listas obtenidas, se crean textos para mostrar los datos en el informe.
    Los textos para el informe se crean al final de la función en dos copias: para la muestra tabular en el registro y para una línea normal para la notificación Push.

    Función que rellena las listas con los números mágicos y los símbolos de las posiciones de la lista transmitida:

    //+--------------------------------------------------------------------------+
    //| Fill the lists of magic numbers and position symbols from the passed list|
    //+--------------------------------------------------------------------------+
    void CreateSymbolMagicLists(CArrayObj *list, CArrayString *list_symbols, CArrayLong *list_magics)
      {
    //--- If an invalid pointer to a list of positions is passed, or the list is empty, leave
       if(list==NULL || list.Total()==0)
          return;
       
       int index=WRONG_VALUE;  // Index of the necessary symbol or magic number in the list
    
    //--- In a loop by the list of positions
       for(int i=0; i<list.Total(); i++)
         {
          //--- get the pointer to the next position 
          CPosition *pos=list.At(i);
          if(pos==NULL)
             continue;
       
          //--- Get the position symbol
          string symbol=pos.Symbol();
       
          //--- Set the sorted list flag for the symbol list and get the symbol index in the symbol list
          list_symbols.Sort();
          index=list_symbols.Search(symbol);
          
          //--- If there is no such symbol in the list, add it
          if(index==WRONG_VALUE)
             list_symbols.Add(symbol);
       
          //--- Get the position magic number
          long magic=pos.Magic();
          
          //--- Set the sorted list flag for the magic number list and get the magic number index in the list of magic numbers
          list_magics.Sort();
          index=list_magics.Search(magic);
          
          //--- If there is no such magic number in the list, add it
          if(index==WRONG_VALUE)
             list_magics.Add(magic);
         }
      }
    
    

    Inicialmente, no sabemos qué símbolos y números mágicos se han utilizado para negociar en la cuenta. Para recibir informes según los símbolos y los números mágicos, debemos encontrar todos los símbolos y todos los números mágicos de las posiciones cerradas en la lista completa de todas las posiciones cerradas y anotarlos en las listas correspondientes. A esta función se le transmite una lista completa de todas las posiciones cerradas y los punteros a listas de símbolos y números mágicos. Todos los símbolos y números mágicos encontrados se registran en las listas correspondientes. Una vez ejecutada la función, tendremos dos listas completas de símbolos y números mágicos, que luego se podremos usar para compilar informes sobre símbolos y números mágicos por separado.

    Para obtener la suma de los valores de cualquier propiedad entera o real de todas las posiciones de la lista, debemos sumar los valores de esta propiedad en un ciclo. ¿Por qué necesitamos esto? Bueno, por ejemplo, para obtener el valor del spread total, o los beneficios o pérdidas totales. Luego escribiremos funciones que nos permitan sumar los valores de las propiedades especificadas de todas las posiciones de la lista.

    Función que retorna la suma de los valores de una propiedad entera especificada de todos los elementos de una lista:

    //+------------------------------------------------------------------+
    //| Return the sum of the values of the specified                    |
    //| integer property of all positions in the list                    |
    //+------------------------------------------------------------------+
    long PropertyValuesSum(CArrayObj *list, const ENUM_POSITION_PROPERTY_INT property)
      {
       long res=0;
       int total=list.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *pos=list.At(i);
          res+=(pos!=NULL ? pos.GetProperty(property) : 0);
         }
       return res;
      }
    
    

    En un ciclo a través de la lista cuyo puntero se transmite a la función, obtenemos el valor de la propiedad especificada del objeto en el índice del ciclo y lo añadimos al valor resultante. Como resultado, al final del ciclo, tendremos la suma de los valores de la propiedad especificada de todas las posiciones en la lista transmitida a la función.

    Función que retorna la suma de los valores de una propiedad real especificada de todas las posiciones en una lista:

    //+------------------------------------------------------------------+
    //| Return the sum of the values of the specified                    |
    //| real property of all positions in the list                       |
    //+------------------------------------------------------------------+
    double PropertyValuesSum(CArrayObj *list, const ENUM_POSITION_PROPERTY_DBL property)
      {
       double res=0;
       int total=list.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *pos=list.At(i);
          res+=(pos!=NULL ? pos.GetProperty(property) : 0);
         }
       return res;
      }
    
    

    Utilizando el mismo principio, crearemos funciones que retornen el valor promedio de la propiedad especificada.

    Función que retorna el valor promedio de una propiedad entera especificada de todos los elementos de una lista:

    //+------------------------------------------------------------------+
    //| Return the average value of the specified                        |
    //| integer property of all positions in the list                    |
    //+------------------------------------------------------------------+
    double PropertyAverageValue(CArrayObj *list, const ENUM_POSITION_PROPERTY_INT property)
      {
       long res=0;
       int total=list.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *pos=list.At(i);
          res+=(pos!=NULL ? pos.GetProperty(property) : 0);
         }
       return(total>0 ? (double)res/(double)total : 0);
      }
    
    

    Función que retorna el valor promedio de una propiedad real especificada de todos los elementos de una lista:

    //+------------------------------------------------------------------+
    //| Return the average value of the specified                        |
    //| real property of all positions in the list                       |
    //+------------------------------------------------------------------+
    double PropertyAverageValue(CArrayObj *list, const ENUM_POSITION_PROPERTY_DBL property)
      {
       double res=0;
       int total=list.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *pos=list.At(i);
          res+=(pos!=NULL ? pos.GetProperty(property) : 0);
         }
       return(total>0 ? res/(double)total : 0);
      }
    
    

    Función que retorna la suma del coste de los spreads de las transacciones de cierre de todas las posiciones en la lista:

    //+------------------------------------------------------------------+
    //| Returns the sum of the spread costs                              |
    //| of deals closing all positions in the list                       |
    //+------------------------------------------------------------------+
    double PositionsCloseSpreadCostSum(CArrayObj *list)
      {
       double res=0;
       if(list==NULL)
          return 0;
       int total=list.Total();
       for(int i=0; i<total; i++)
         {
          CPosition *pos=list.At(i);
          res+=(pos!=NULL ? pos.SpreadOutCost() : 0);
         }
       return res;
      }
    
    

    Como la posición no tiene la propiedad "coste de spread", aquí no podemos utilizar las funciones anteriores. Por consiguiente, aquí utilizaremos directamente el método del objeto de posición, que calcula y retorna el coste del spread al cerrar una posición. Luego sumamos todos los valores obtenidos de todas las posiciones de la lista al resultado final y retornamos el valor obtenido.

    Función que retorna una descripción del periodo del informe:

    //+------------------------------------------------------------------+
    //| Return the report period description                             |
    //+------------------------------------------------------------------+
    string ReportRangeDescription(ENUM_REPORT_RANGE range, const int num_period)
      {
       switch(range)
         {
          //--- Day
          case REPORT_RANGE_DAILY       : return("Daily");
          //--- Since the beginning of the week
          case REPORT_RANGE_WEEK_BEGIN  : return("Weekly");
          //--- Since the beginning of the month
          case REPORT_RANGE_MONTH_BEGIN : return("Month-to-date");
          //--- Since the beginning of the year
          case REPORT_RANGE_YEAR_BEGIN  : return("Year-to-date");
          //--- Number of days
          case REPORT_RANGE_NUM_DAYS    : return StringFormat("%d days", num_period);
          //--- Number of months
          case REPORT_RANGE_NUM_MONTHS  : return StringFormat("%d months", num_period);
          //--- Number of years
          case REPORT_RANGE_NUM_YEARS   : return StringFormat("%d years", num_period);
          //--- Entire period
          case REPORT_RANGE_ALL         : return("Entire period");
          //--- any other
          default                       : return("Unknown period: "+(string)range);
         }
      }
    
    

    Dependiendo del valor transmitido del periodo del informe y la cantidad de días/meses/años, se creará y retornará una línea de descripción.

    Ya hemos analizado todas las funciones del programa de servicio y el programa en sí: su ciclo principal. Vamos a compilarlo y ejecutar el servicio. Tras la compilación, el programa se ubica en la ventana del terminal “Navegador” en la sección “Servicios”.

    Después buscamos nuestro servicio y en el menú RMB seleccionamos “Añadir servicio”:


    Después de lo cual se abre la ventana de configuración del programa:


    Una vez que se inicia el servicio, se crea un informe diario que incluye

    • un informe general de tres meses y un informe de tres meses referente a los símbolos y los números mágicos,
    • un informe general de dos años y un informe de dos años referente a los símbolos y los números mágicos:
    Reporter        -Service notifications OK
    Reporter        68008618: Artem (MetaQuotes Ltd., Demo, 10779.50 USD, Hedging)
    Reporter        Beginning to create a list of closed positions...
    Reporter        A list of 155 positions was created in 8828 ms
    Reporter        "Daily" no trades
    Reporter        "7 days" no trades
    Reporter        Report for the period "3 months" from 2024.04.23 00:00
    Reporter        |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    Reporter        |        2 |       77 |       17 |       60 |  +247.00 |   +36.70 |    -0.40 |     3.20 |     0.00 |     0.00 |     0.00 |     0.00 |     5.10 |
    Reporter        
    Reporter        Report by symbols for the period "3 months" from 2024.04.23 00:00
    Reporter        |   Symbol |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    Reporter        |   EURUSD |       73 |       17 |       56 |  +241.40 |   +36.70 |    -0.40 |     3.30 |     0.00 |     0.00 |     0.00 |     0.00 |     4.30 |
    Reporter        |   GBPUSD |        4 |        0 |        4 |    +5.60 |    +2.20 |    +0.10 |     1.40 |     0.00 |     0.00 |     0.00 |     0.00 |     0.80 |
    Reporter        
    Reporter        Report by magics for the period "3 months" from 2024.04.23 00:00
    Reporter        |    Magic |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    Reporter        |        0 |       75 |       15 |       60 |  +246.60 |   +36.70 |    -0.40 |     3.28 |     0.00 |     0.00 |     0.00 |     0.00 |     4.90 |
    Reporter        | 10879099 |        1 |        1 |        0 |    +0.40 |    +0.40 |    +0.40 |     0.40 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 27394171 |        1 |        1 |        0 |    +0.00 |    +0.00 |    +0.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        
    Reporter        Report for the period "2 years" from 2022.07.23 00:00
    Reporter        |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    Reporter        |        2 |      155 |       35 |      120 |  +779.50 |  +145.00 |   -22.80 |     5.03 |     0.00 |     0.00 |     0.00 |     0.00 |    15.38 |
    Reporter        
    Reporter        Report by symbols for the period "2 years" from 2022.07.23 00:00
    Reporter        |   Symbol |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    Reporter        |   EURUSD |      138 |       30 |      108 |  +612.40 |   +36.70 |   -22.80 |     4.43 |     0.00 |     0.00 |     0.00 |     0.00 |     6.90 |
    Reporter        |   GBPUSD |       17 |        5 |       12 |  +167.10 |  +145.00 |    -7.20 |     9.83 |     0.00 |     0.00 |     0.00 |     0.00 |     8.48 |
    Reporter        
    Reporter        Report by magics for the period "2 years" from 2022.07.23 00:00
    Reporter        |    Magic |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |  Commiss |     Swap |      Fee |   Spread |
    Reporter        |        0 |      131 |       31 |      100 |  +569.10 |   +36.70 |    -8.50 |     4.34 |     0.00 |     0.00 |     0.00 |     0.00 |     8.18 |
    Reporter        |        1 |        2 |        0 |        2 |    +2.80 |    +1.80 |    +1.00 |     1.40 |     0.00 |     0.00 |     0.00 |     0.00 |     1.80 |
    Reporter        |      123 |        2 |        0 |        2 |    +0.80 |    +0.40 |    +0.40 |     0.40 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        |     1024 |        2 |        1 |        1 |    +0.10 |    +0.10 |    +0.00 |     0.05 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |
    Reporter        |   140578 |        1 |        0 |        1 |  +145.00 |  +145.00 |  +145.00 |   145.00 |     0.00 |     0.00 |     0.00 |     0.00 |     4.00 |
    Reporter        |  1114235 |        1 |        0 |        1 |    +2.30 |    +2.30 |    +2.30 |     2.30 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        |  1769595 |        1 |        0 |        1 |   +15.00 |   +15.00 |   +15.00 |    15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        |  1835131 |        1 |        0 |        1 |    +3.60 |    +3.60 |    +3.60 |     3.60 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        |  2031739 |        1 |        0 |        1 |   +15.00 |   +15.00 |   +15.00 |    15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        |  2293883 |        1 |        0 |        1 |    +1.40 |    +1.40 |    +1.40 |     1.40 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        |  2949243 |        1 |        0 |        1 |   -15.00 |   -15.00 |   -15.00 |   -15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |
    Reporter        | 10879099 |        1 |        1 |        0 |    +0.40 |    +0.40 |    +0.40 |     0.40 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 12517499 |        1 |        1 |        0 |   +15.00 |   +15.00 |   +15.00 |    15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |
    Reporter        | 12976251 |        1 |        0 |        1 |    +2.90 |    +2.90 |    +2.90 |     2.90 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 13566075 |        1 |        0 |        1 |   +15.00 |   +15.00 |   +15.00 |    15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 13959291 |        1 |        0 |        1 |   +15.10 |   +15.10 |   +15.10 |    15.10 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 15728763 |        1 |        0 |        1 |   +11.70 |   +11.70 |   +11.70 |    11.70 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 16121979 |        1 |        0 |        1 |   +15.00 |   +15.00 |   +15.00 |    15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 16318587 |        1 |        0 |        1 |   -15.00 |   -15.00 |   -15.00 |   -15.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |
    Reporter        | 16580731 |        1 |        0 |        1 |    +2.10 |    +2.10 |    +2.10 |     2.10 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        | 21299323 |        1 |        0 |        1 |   -22.80 |   -22.80 |   -22.80 |   -22.80 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |
    Reporter        | 27394171 |        1 |        1 |        0 |    +0.00 |    +0.00 |    +0.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.00 |     0.10 |
    Reporter        
    Reporter        Beginning of sending 31 notifications to MQID
    Reporter        10 out of 31 messages sent.
    Reporter        No more than 10 messages per minute! Message limit has been reached. Wait 55 seconds until a minute is up.
    Reporter        20 out of 31 messages sent.
    Reporter        No more than 10 messages per minute! Message limit has been reached. Wait 55 seconds until a minute is up.
    Reporter        30 out of 31 messages sent.
    Reporter        No more than 10 messages per minute! Message limit has been reached. Wait 55 seconds until a minute is up.
    Reporter        Sending 31 notifications completed
    
    

    Después de enviar los informes al diario de registro, el servicio comienza a enviar informes al smartphone. Se han enviado 31 mensajes en 4 lotes: 10 mensajes por minuto.

    Como no se han dado transacciones ayer ni durante los siete días anteriores a la fecha de recepción del informe, el servicio ha emitido un mensaje al respecto.

    Si deshabilitamos los informes según los símbolos y los números mágicos en la configuración, deshabilitamos las comisiones y los spreads, prohibimos los informes para una cantidad específica de días, pero permitimos los informes diarios para la semana, el mes y el año actuales,


    las estadísticas estarán en un formato diferente:

    Reporter        -Service notifications OK
    Reporter        68008618: Artem (MetaQuotes Ltd., Demo, 10779.50 USD, Hedging)
    Reporter        Beginning to create a list of closed positions...
    Reporter        A list of 155 positions was created in 8515 ms
    Reporter        "Daily" no trades
    Reporter        "Weekly" no trades
    Reporter        Report for the period "Month-to-date" from 2024.07.01 00:00
    Reporter        |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |          
    Reporter        |        2 |       22 |        3 |       19 |   +46.00 |    +5.80 |    -0.30 |     2.09 |     0.00 |          
    Reporter        
    Reporter        Report for the period "Year-to-date" from 2024.01.01 00:00
    Reporter        |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |          
    Reporter        |        2 |      107 |       31 |       76 |  +264.00 |   +36.70 |    -7.20 |     2.47 |     0.00 |          
    Reporter        
    Reporter        Report for the period "Entire period" from 1970.01.01 00:00
    Reporter        |  Symbols |   Trades |     Long |    Short |   Profit |      Max |      Min |      Avg |    Costs |          
    Reporter        |        2 |      155 |       35 |      120 |  +779.50 |  +145.00 |   -22.80 |     5.03 |     0.00 |          
    Reporter        
    Reporter        Beginning of sending 3 notifications to MQID
    Reporter        Sending 3 notifications completed
    
    

    Todas las tablas de informes anteriores se imprimen en el diario "Expertos" del terminal.

    Los informes llegan al smartphone en un formato ligeramente distinto:


    Aquí, los valores de comisión cero no se muestran en el informe, independientemente de si el permiso para dichos valores está habilitado, para ahorrar espacio en la línea del informe, cuya longitud no puede superar los 255 caracteres.


    Conclusión

    A partir de la creación del programa de servicio, hemos analizado la posibilidad de almacenar diversos datos y obtener listas de datos según todo tipo de criterios. El concepto considerado permite crear conjuntos de varios datos en listas de objetos, obtener punteros a los objetos requeridos según las propiedades especificadas y también crear listas de objetos filtrados según la propiedad requerida, lo que en conjunto permite almacenar datos en forma de base de datos y obtener la información necesaria. Podemos presentar la información obtenida en forma de, por ejemplo, informes comerciales, exportarlos a un diario y enviarlos como notificaciones al smartphone del usuario a través de MetaQuotes ID.

    Además, podemos ir más allá y perfeccionar el programa de servicio presentado hoy para ampliar los informes y mostrarlos en un gráfico aparte en forma de tablas, gráficos y diagramas, y exactamente en la forma que el usuario requiera: todo esto lo hace posible el lenguaje MQL5.

    Adjuntamos al artículo todos los archivos de este proyecto y un archivo que se puede descomprimir en la carpeta del terminal MQL5 y utilizar directamente el programa, tras compilar previamente el archivo Reporter.mq5.


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

    Archivos adjuntos |
    Deal.mqh (49.73 KB)
    Position.mqh (67.53 KB)
    Select.mqh (43.22 KB)
    Account.mqh (32.94 KB)
    Accounts.mqh (15.89 KB)
    Reporter.mq5 (101.21 KB)
    MQL5.zip (36.5 KB)
    Integración en MQL5: Python Integración en MQL5: Python
    Python es un lenguaje de programación conocido y popular con muchas características, especialmente en los campos de las finanzas, la ciencia de datos, la Inteligencia Artificial y el Aprendizaje Automático. Python es una herramienta poderosa que también puede resultar útil en el trading. MQL5 nos permite utilizar este poderoso lenguaje como una integración para lograr nuestros objetivos de manera efectiva. En este artículo, compartiremos cómo podemos usar Python como una integración en MQL5 después de aprender información básica sobre Python.
    Creación de un modelo de restricción de tendencia de velas (Parte 8): Desarrollo de un asesor experto (I) Creación de un modelo de restricción de tendencia de velas (Parte 8): Desarrollo de un asesor experto (I)
    En esta discusión, crearemos nuestro primer Asesor Experto en MQL5 basado en el indicador que creamos en el artículo anterior. Cubriremos todas las características necesarias para automatizar el proceso, incluida la gestión de riesgos. Esto beneficiará ampliamente a los usuarios para pasar de la ejecución manual de operaciones a sistemas automatizados.
    Redes neuronales en el trading: Modelos "ligeros" de pronóstico de series temporales Redes neuronales en el trading: Modelos "ligeros" de pronóstico de series temporales
    Los modelos ligeros de pronóstico de series temporales logran un alto rendimiento utilizando un número mínimo de parámetros, lo que, a su vez, reduce el consumo de recursos computacionales y agiliza la toma de decisiones. De este modo consiguen una calidad de previsión comparable a la de modelos más complejos.
    Algoritmo de optimización del comportamiento social adaptativo (ASBO): — Adaptive Social Behavior Optimization (ASBO): Evolución en dos fases Algoritmo de optimización del comportamiento social adaptativo (ASBO): — Adaptive Social Behavior Optimization (ASBO): Evolución en dos fases
    Este artículo supone una continuación del tema del comportamiento social de los organismos vivos y su impacto en el desarrollo de un nuevo modelo matemático: el ASBO (Adaptive Social Behavior Optimization). Así, nos sumergiremos en la evolución en dos fases, probaremos el algoritmo y sacaremos conclusiones. Al igual que en la naturaleza un grupo de organismos vivos une sus esfuerzos para sobrevivir, el ASBO utiliza los principios de comportamiento colectivo para resolver problemas de optimización complejos.