
Monitoreo de transacciones usando notificaciones push: ejemplo de un servicio en MetaTrader 5
Contenido
- Introducción
- Estructura del proyecto
- Clase de transacción
- Clase de posición histórica
- Clase para la búsqueda y el filtrado según las propiedades de las transacciones y posiciones
- Clase de colección de posiciones históricas
- Clase de cuenta
- Clase de colección de cuentas
- Programa de servicio para crear informes comerciales y enviar notificaciones
- Conclusión
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:
- La estructura de las propiedades de los objetos "(Parte I): Concepto, organización de datos y primeros resultados" ,
- La estructura de las listas de los objetos "(Parte II): Colección de órdenes y transacciones históricas" y
- 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:
- 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.
- 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





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso