English Русский 中文 Español Deutsch 日本語
preview
Monitoramento de Trading com Notificações-Push — Exemplo de Serviço no MetaTrader 5

Monitoramento de Trading com Notificações-Push — Exemplo de Serviço no MetaTrader 5

MetaTrader 5Exemplos |
173 4
Artyom Trishkin
Artyom Trishkin

Conteúdo


Introdução

No trading em mercados financeiros, é essencial ter acesso a informações sobre os resultados das operações realizadas dentro de um determinado período de tempo.

Provavelmente, todos os traders já se depararam com a necessidade de verificar os resultados da sua atividade no dia anterior, na semana, no mês, etc., para ajustar a sua estratégia com base nos resultados. O terminal de cliente MetaTrader 5 fornece estatísticas detalhadas sob a forma de relatórios, permitindo avaliar os resultados da atividade de negociação de maneira visualmente conveniente. O relatório pode ajudar a otimizar o portfólio, a compreender como reduzir riscos e a aumentar a estabilidade do trading.

Para analisar a estratégia, basta clicar em "Relatório \ Resumo" no menu de contexto da seção do histórico de trading ou acessar "Relatórios" no menu "Exibir" (ou simplesmente usar o atalho Alt+E):




Para obter mais informações sobre relatórios no MetaTrader 5, consulte o artigo New report in MetaTrader: os 5 indicadores de trading mais importantes".

Se, por algum motivo, os relatórios padrão fornecidos pelo terminal do cliente não forem suficientes, a linguagem MQL5 oferece amplas possibilidades para criar programas personalizados, incluindo a geração de relatórios e o envio para o telemóvel do trader. Essa é precisamente a funcionalidade que iremos discutir hoje.

Nosso programa deve ser iniciado junto com o terminal, monitorar a troca de conta ou de número da conta, a virada do dia e o horário de criação e envio dos relatórios. Para esses fins, o tipo de programa ideal é um "Serviço".

Conforme a documentação, um Serviço é um programa que, ao contrário de indicadores, EAs e scripts, não precisa ser anexado a um gráfico para funcionar. Assim como os scripts, os serviços não processam eventos, exceto o evento de inicialização. Para executar um serviço, seu código deve obrigatoriamente conter a função de tratamento OnStart. Os serviços não aceitam eventos além do Start, mas podem enviar eventos personalizados para os gráficos por meio da função EventChartCustom. Os serviços são armazenados no diretório <diretório_do_terminal>\MQL5\Services.

Cada serviço iniciado no terminal opera em sua própria thread. Isso significa que um serviço em laço contínuo não pode afetar o funcionamento de outros programas. Nosso serviço deve operar em um laço infinito, verificar a ocorrência do horário definido, ler todo o histórico de negociações, criar listas de posições fechadas, filtrar essas listas com base em diferentes critérios e gerar relatórios tanto no diário quanto por notificações push para o smartphone do usuário. Além disso, no primeiro lançamento do serviço ou ao alterar suas configurações, o serviço deve verificar a possibilidade de envio de notificações push a partir do terminal. Para isso, deve haver uma interação com o usuário por meio de janelas de mensagem que aguardam uma resposta ou reação. Outro ponto importante é que o envio de notificações Push tem restrições quanto à frequência dentro de um determinado período de tempo. Portanto, é necessário implementar atrasos no envio das notificações. E tudo isso não pode, de forma alguma, afetar o funcionamento de outros aplicativos executados no terminal cliente. Com base nesses requisitos, os Serviços são a ferramenta mais conveniente para a criação desse projeto.

Já definimos o tipo de programa. Agora, precisamos estruturar os componentes necessários para reunir tudo o que planejamos.


Estrutura do projeto

Vamos analisar o programa e seus componentes "do fim para o começo":

  • O programa de serviço tem acesso aos dados de todas as contas que estiveram ativas durante todo o tempo de execução ininterrupta. A partir desses dados, o programa obtém listas de posições fechadas e as combina em uma lista geral. Dependendo das configurações, o serviço pode utilizar dados de posições fechadas apenas da conta atualmente ativa ou incluir também dados de todas as contas utilizadas anteriormente no terminal cliente para operações de trading.
    Com base nos dados das posições fechadas extraídas da lista de contas, é gerada a estatística de trading para os períodos exigidos, que é então enviada via notificações Push para o smartphone do usuário. Além disso, a estatística de trading também é exibida em formato de tabela no diário do terminal na aba "Especialistas".
  • A coleção de contas inclui uma lista de todas as contas às quais o terminal esteve conectado durante a execução ininterrupta do serviço. Essa coleção permite o acesso a qualquer conta da lista e a todas as posições fechadas associadas a essas contas. As listas estão disponíveis no programa de serviço, sendo utilizadas para gerar amostras e criar estatísticas.
  • A classe do objeto de conta armazena os dados de uma conta específica, junto com uma lista (coleção) de todas as posições fechadas cujas transações foram realizadas nessa conta durante o período de execução ininterrupta do serviço. Essa classe permite acesso às propriedades da conta, possibilita a criação e atualização da lista de posições fechadas dessa conta e retorna listas de posições fechadas com base em diferentes critérios de seleção.
  • A classe coleção de posições históricas contém uma lista de objetos de posições, fornece acesso às propriedades das posições fechadas, bem como permite a criação e atualização da lista de posições. Também retorna listas de posições fechadas conforme necessário.
  • A classe do objeto de posição armazena as propriedades de uma posição fechada e oferece acesso a elas. A classe inclui funcionalidades para comparar dois objetos com base em diferentes propriedades, permitindo a criação de listas de posições filtradas por diversos critérios. Além disso, a classe contém uma lista de operações pertencentes a essa posição e fornece acesso a elas.
  • A classe do objeto de operação armazena as propriedades de uma única operação e fornece acesso a essas propriedades. A classe também inclui funcionalidades para comparar dois objetos com base em diferentes propriedades, permitindo a criação de listas de operações filtradas por diversos critérios.


A ideia de reconstruir uma posição fechada a partir de uma lista de operações históricas foi abordada no artigo "Como visualizar operações diretamente no gráfico sem se perder no histórico de trading". A partir da lista de operações, é possível determinar a qual posição cada operação pertence por meio do identificador de posição (PositionID), registrado nas propriedades da operação. Então, um objeto de posição é criado e as operações encontradas são adicionadas à sua lista. Faremos o mesmo aqui. No entanto, para estruturar a criação dos objetos de operações e posições, utilizaremos uma abordagem bem diferente, mas já amplamente testada. Nesse modelo, cada objeto possui métodos de acesso às suas propriedades para definição e recuperação de valores de maneira padronizada. Essa abordagem permite criar objetos seguindo um formato unificado, armazená-los em listas, filtrá-los e ordená-los conforme qualquer propriedade, além de gerar novas listas segmentadas por propriedades específicas.

Para compreender corretamente a estruturação das classes deste projeto, recomenda-se a leitura de três artigos que detalham minuciosamente:

  1. estrutura das propriedades dos objetos "(Parte I): Conceito, gerenciamento de dados e primeiros resultados"
  2. estrutura das listas de objetos "(Parte II): Coleção do histórico de ordens e negócios" e
  3. métodos para filtrar objetos em listas por propriedades "(Parte III): Coleção de ordens e posições de mercado, busca e filtragem"

Após a leitura dos artigos mencionados, ficará clara toda a concepção da estruturação dos objetos, seu armazenamento em listas e a obtenção de diferentes listas filtradas por propriedades específicas. Essencialmente, os três artigos descrevem a possibilidade de criar bancos de dados para quaisquer objetos em MQL5, armazená-los e recuperar seus atributos e valores conforme necessário. Esse é exatamente o tipo de funcionalidade requerida neste projeto, e por essa razão, optamos por estruturar os objetos e suas coleções seguindo a abordagem descrita nos artigos. No entanto, aqui isso será feito de maneira um pouco mais simplificada — sem a criação de classes de objetos abstratos com construtores protegidos e sem a definição de propriedades não suportadas nos objetos. A estrutura será mais direta: cada objeto terá sua própria lista de propriedades, armazenada em três arrays, permitindo tanto a gravação quanto a recuperação dessas propriedades. Todos esses objetos serão mantidos em listas, com a possibilidade de gerar novas listas contendo apenas os objetos necessários, de acordo com as propriedades especificadas.

Resumindo, cada objeto criado no projeto terá um conjunto de propriedades próprias, assim como qualquer objeto ou entidade em MQL5. A diferença é que, em MQL5, existem funções padrão para recuperar propriedades, enquanto no projeto essas propriedades serão acessadas por meio de métodos específicos para valores inteiros, de ponto flutuante e strings, definidos diretamente na classe de cada objeto. Posteriormente, todos esses objetos serão armazenados em listas — arrays dinâmicos de ponteiros para objetos CObject da Biblioteca Padrão. São justamente as classes da Biblioteca Padrão que nos permitem desenvolver projetos complexos com o mínimo de esforço. Neste caso, a estrutura será utilizada como um banco de dados de posições fechadas de todas as contas onde ocorreu trading, permitindo a obtenção de listas de objetos filtrados e ordenados por qualquer propriedade desejada.

    Qualquer posição só existe a partir do momento de sua abertura, ou seja, quando uma operação In é realizada, até o momento de seu fechamento, quando ocorre uma operação Out/OutBuy. Isso significa que uma posição é um objeto que existe apenas enquanto está no mercado. Já uma operação, ao contrário, é sempre um objeto histórico, pois representa apenas o registro da execução de uma ordem (ordem de trading). Por essa razão, no terminal cliente, não há posições no histórico — elas existem apenas na lista de posições ativas do mercado.

    Portanto, para reconstruir uma posição de mercado que já foi fechada, é necessário "reunir" a posição que existiu a partir das operações históricas. Felizmente, cada operação contém o identificador da posição à qual pertenceu. Para isso, será necessário percorrer a lista de operações históricas, recuperar cada operação, criar um novo objeto de operação, verificar o identificador da posição e, então, criar um objeto de posição. O objeto da nova posição histórica será preenchido com o objeto de operação criado. Implementaremos esse processo adiante. Mas, por ora, vamos criar as classes do objeto de operação e do objeto de posição, que serão a base para o nosso trabalho a seguir.


    Classe de operação

    No diretório do terminal \MQL5\Services, criaremos uma nova pasta chamada AccountReporter e, dentro dela, um novo arquivo Deal.mqh, que conterá a classe CDeal.
    A classe deve ser derivada da classe base CObject da Biblioteca Padrão, e seu arquivo deve ser incluído na nova classe que estamos criando:

    //+------------------------------------------------------------------+
    //|                                                         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>
    
    //+------------------------------------------------------------------+
    //| Класс сделки                                                     |
    //+------------------------------------------------------------------+
    class CDeal : public CObject
      {
      }
    


    Agora, definimos as enumerações para as propriedades inteiras, de ponto flutuante e de string da operação, e nas seções privada, protegida e pública, declaramos as variáveis-membro da classe e os métodos para manipular as propriedades da operação:

    //+------------------------------------------------------------------+
    //|                                                         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>
    
    //--- Перечисление целочисленных свойств сделки
    enum ENUM_DEAL_PROPERTY_INT
      {
       DEAL_PROP_TICKET = 0,               // Тикет сделки
       DEAL_PROP_ORDER,                    // Ордер, на основание которого выполнена сделка
       DEAL_PROP_TIME,                     // Время совершения сделки
       DEAL_PROP_TIME_MSC,                 // Время совершения сделки в миллисекундах
       DEAL_PROP_TYPE,                     // Тип сделки
       DEAL_PROP_ENTRY,                    // Направление сделки
       DEAL_PROP_MAGIC,                    // Magic number сделки
       DEAL_PROP_REASON,                   // Причина или источник проведения сделки
       DEAL_PROP_POSITION_ID,              // Идентификатор позиции
       DEAL_PROP_SPREAD,                   // Spread при совершении сделки
      };
      
    //--- Перечисление вещественных свойств сделки
    enum ENUM_DEAL_PROPERTY_DBL
      {
       DEAL_PROP_VOLUME = DEAL_PROP_SPREAD+1,// Объем сделки
       DEAL_PROP_PRICE,                    // Цена сделки
       DEAL_PROP_COMMISSION,               // Комиссия
       DEAL_PROP_SWAP,                     // Накопленный своп при закрытии
       DEAL_PROP_PROFIT,                   // Финансовый результат сделки
       DEAL_PROP_FEE,                      // Оплата за проведение сделки
       DEAL_PROP_SL,                       // Уровень Stop Loss
       DEAL_PROP_TP,                       // Уровень Take Profit
      };
      
    //--- Перечисление строковых свойств сделки
    enum ENUM_DEAL_PROPERTY_STR
      {
       DEAL_PROP_SYMBOL = DEAL_PROP_TP+1,  // Символ, по которому произведена сделка
       DEAL_PROP_COMMENT,                  // Комментарий к сделке
       DEAL_PROP_EXTERNAL_ID,              // Идентификатор сделки во внешней торговой системе 
      };
      
    //+------------------------------------------------------------------+
    //| Класс сделки                                                     |
    //+------------------------------------------------------------------+
    class CDeal : public CObject
      {
    private:
       MqlTick           m_tick;                                      // Структура тика сделки
       long              m_lprop[DEAL_PROP_SPREAD+1];                 // Массив для хранения целочисленных свойств
       double            m_dprop[DEAL_PROP_TP-DEAL_PROP_SPREAD];      // Массив для хранения вещественных свойств
       string            m_sprop[DEAL_PROP_EXTERNAL_ID-DEAL_PROP_TP]; // Массив для хранения строковых свойств
    
    //--- Возвращает индекс массива, по которому фактически расположено (1) double-свойство и (2) string-свойство сделки
       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;     }
       
    //--- Получает (1) тик сделки, (2) спред минутного бара сделки
       bool              GetDealTick(const int amount=20);
       int               GetSpreadM1(void);
    
    //--- Возвращает время с миллисекундами
       string            TimeMscToString(const long time_msc,int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const;
       
    protected:
    //--- Дополнительные свойства
       int               m_digits;                                    // Digits символа
       double            m_point;                                     // Point символа
       double            m_bid;                                       // Bid при совершении сделки
       double            m_ask;                                       // Ask при совершении сделки
       
    public:
    //--- Установка свойств
    //--- Устанавливает (1) целочисленное, (2) вещественное и (3) строковое свойство сделки
       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; }
    
    //--- Целочисленные свойства
       void              SetTicket(const long ticket)              { this.SetProperty(DEAL_PROP_TICKET, ticket);                  }  // Тикет
       void              SetOrder(const long order)                { this.SetProperty(DEAL_PROP_ORDER, order);                    }  // Ордер
       void              SetTime(const datetime time)              { this.SetProperty(DEAL_PROP_TIME, time);                      }  // Время
       void              SetTimeMsc(const long value)              { this.SetProperty(DEAL_PROP_TIME_MSC, value);                 }  // Время в миллисекундах
       void              SetTypeDeal(const ENUM_DEAL_TYPE type)    { this.SetProperty(DEAL_PROP_TYPE, type);                      }  // Тип
       void              SetEntry(const ENUM_DEAL_ENTRY entry)     { this.SetProperty(DEAL_PROP_ENTRY, entry);                    }  // Направление
       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);                  }  // Причина или источник проведения сделки
       void              SetPositionID(const long id)              { this.SetProperty(DEAL_PROP_POSITION_ID, id);                 }  // Идентификатор позиции
    
    //--- Вещественные свойства
       void              SetVolume(const double volume)            { this.SetProperty(DEAL_PROP_VOLUME, volume);                  }  // Объем
       void              SetPrice(const double price)              { this.SetProperty(DEAL_PROP_PRICE, price);                    }  // Цена
       void              SetCommission(const double value)         { this.SetProperty(DEAL_PROP_COMMISSION, value);               }  // Комиссия
       void              SetSwap(const double value)               { this.SetProperty(DEAL_PROP_SWAP, value);                     }  // Накопленный своп при закрытии
       void              SetProfit(const double value)             { this.SetProperty(DEAL_PROP_PROFIT, value);                   }  // Финансовый результат
       void              SetFee(const double value)                { this.SetProperty(DEAL_PROP_FEE, value);                      }  // Оплата за проведение сделки
       void              SetSL(const double value)                 { this.SetProperty(DEAL_PROP_SL, value);                       }  // Уровень Stop Loss
       void              SetTP(const double value)                 { this.SetProperty(DEAL_PROP_TP, value);                       }  // Уровень Take Profit
    
    //--- Строковые свойства
       void              SetSymbol(const string symbol)            { this.SetProperty(DEAL_PROP_SYMBOL,symbol);                   }  // Имя символа
       void              SetComment(const string comment)          { this.SetProperty(DEAL_PROP_COMMENT,comment);                 }  // Комментарий
       void              SetExternalID(const string ext_id)        { this.SetProperty(DEAL_PROP_EXTERNAL_ID,ext_id);              }  // Идентификатор сделки во внешней торговой системе
    
    //--- Получение свойств
    //--- Возвращает из массива свойств (1) целочисленное, (2) вещественное и (3) строковое свойство сделки
       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)];      }
    
    //--- Целочисленные свойства
       long              Ticket(void)                        const { return this.GetProperty(DEAL_PROP_TICKET);                   }  // Тикет
       long              Order(void)                         const { return this.GetProperty(DEAL_PROP_ORDER);                    }  // Ордер
       datetime          Time(void)                          const { return (datetime)this.GetProperty(DEAL_PROP_TIME);           }  // Время
       long              TimeMsc(void)                       const { return this.GetProperty(DEAL_PROP_TIME_MSC);                 }  // Время в миллисекундах
       ENUM_DEAL_TYPE    TypeDeal(void)                      const { return (ENUM_DEAL_TYPE)this.GetProperty(DEAL_PROP_TYPE);     }  // Тип
       ENUM_DEAL_ENTRY   Entry(void)                         const { return (ENUM_DEAL_ENTRY)this.GetProperty(DEAL_PROP_ENTRY);   }  // Направление
       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); }  // Причина или источник проведения сделки
       long              PositionID(void)                    const { return this.GetProperty(DEAL_PROP_POSITION_ID);              }  // Идентификатор позиции
       
    //--- Вещественные свойства
       double            Volume(void)                        const { return this.GetProperty(DEAL_PROP_VOLUME);                   }  // Объем
       double            Price(void)                         const { return this.GetProperty(DEAL_PROP_PRICE);                    }  // Цена
       double            Commission(void)                    const { return this.GetProperty(DEAL_PROP_COMMISSION);               }  // Комиссия
       double            Swap(void)                          const { return this.GetProperty(DEAL_PROP_SWAP);                     }  // Накопленный своп при закрытии
       double            Profit(void)                        const { return this.GetProperty(DEAL_PROP_PROFIT);                   }  // Финансовый результат
       double            Fee(void)                           const { return this.GetProperty(DEAL_PROP_FEE);                      }  // Оплата за проведение сделки
       double            SL(void)                            const { return this.GetProperty(DEAL_PROP_SL);                       }  // Уровень Stop Loss
       double            TP(void)                            const { return this.GetProperty(DEAL_PROP_TP);                       }  // Уровень Take Profit
       
    //--- Строковые свойства
       string            Symbol(void)                        const { return this.GetProperty(DEAL_PROP_SYMBOL);                   }  // Имя символа
       string            Comment(void)                       const { return this.GetProperty(DEAL_PROP_COMMENT);                  }  // Комментарий
       string            ExternalID(void)                    const { return this.GetProperty(DEAL_PROP_EXTERNAL_ID);              }  // Идентификатор сделки во внешней торговой системе
       
    //--- Дополнительные свойства
       double            Bid(void)                           const { return this.m_bid;                                           }  // Bid при совершении сделки
       double            Ask(void)                           const { return this.m_ask;                                           }  // Ask при совершении сделки
       int               Spread(void)                        const { return (int)this.GetProperty(DEAL_PROP_SPREAD);              }  // Spread при совершении сделки
       
    //--- Возвращает описание (1) типа сделки, (2) способа изменения позиции, (3) причины проведения сделки
       string            TypeDescription(void)   const;
       string            EntryDescription(void)  const;
       string            ReasonDescription(void) const;
       
    //--- Возвращает описание сделки
       string            Description(void);
    
    //--- Распечатывает в журнал свойства сделки
       void              Print(void);
       
    //--- Сравнивает два объекта между собой по указанному в mode свойству
       virtual int       Compare(const CObject *node, const int mode=0) const;
       
    //--- Конструкторы/деструктор
                         CDeal(void){}
                         CDeal(const ulong ticket);
                        ~CDeal();
      };
    

    Vamos analisar a implementação dos métodos da classe.

    No construtor da classe, assumimos que a operação já foi selecionada e, portanto, podemos acessar suas propriedades:

    //+------------------------------------------------------------------+
    //| Конструктор                                                      |
    //+------------------------------------------------------------------+
    CDeal::CDeal(const ulong ticket)
      {
    //--- Сохранение свойств
    //--- Целочисленные свойства
       this.SetTicket((long)ticket);                                                    // Тикет сделки
       this.SetOrder(::HistoryDealGetInteger(ticket, DEAL_ORDER));                      // Ордер
       this.SetTime((datetime)::HistoryDealGetInteger(ticket, DEAL_TIME));              // Время совершения сделки
       this.SetTimeMsc(::HistoryDealGetInteger(ticket, DEAL_TIME_MSC));                 // Время совершения сделки в миллисекундах
       this.SetTypeDeal((ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE));    // Тип
       this.SetEntry((ENUM_DEAL_ENTRY)::HistoryDealGetInteger(ticket, DEAL_ENTRY));     // Направление
       this.SetMagic(::HistoryDealGetInteger(ticket, DEAL_MAGIC));                      // Magic number
       this.SetReason((ENUM_DEAL_REASON)::HistoryDealGetInteger(ticket, DEAL_REASON));  // Причина или источник проведения сделки
       this.SetPositionID(::HistoryDealGetInteger(ticket, DEAL_POSITION_ID));           // Идентификатор позиции
       
    //--- Вещественные свойства
       this.SetVolume(::HistoryDealGetDouble(ticket, DEAL_VOLUME));                     // Объем
       this.SetPrice(::HistoryDealGetDouble(ticket, DEAL_PRICE));                       // Цена
       this.SetCommission(::HistoryDealGetDouble(ticket, DEAL_COMMISSION));             // Комиссия
       this.SetSwap(::HistoryDealGetDouble(ticket, DEAL_SWAP));                         // Накопленный своп при закрытии
       this.SetProfit(::HistoryDealGetDouble(ticket, DEAL_PROFIT));                     // Финансовый результат
       this.SetFee(::HistoryDealGetDouble(ticket, DEAL_FEE));                           // Оплата за проведение сделки
       this.SetSL(::HistoryDealGetDouble(ticket, DEAL_SL));                             // Уровень Stop Loss
       this.SetTP(::HistoryDealGetDouble(ticket, DEAL_TP));                             // Уровень Take Profit
    
    //--- Строковые свойства
       this.SetSymbol(::HistoryDealGetString(ticket, DEAL_SYMBOL));                     // Имя символа
       this.SetComment(::HistoryDealGetString(ticket, DEAL_COMMENT));                   // Комментарий
       this.SetExternalID(::HistoryDealGetString(ticket, DEAL_EXTERNAL_ID));            // Идентификатор сделки во внешней торговой системе
    
    //--- Дополнительные параметры
       this.m_digits = (int)::SymbolInfoInteger(this.Symbol(), SYMBOL_DIGITS);
       this.m_point  = ::SymbolInfoDouble(this.Symbol(), SYMBOL_POINT);
       
    //--- Параметры для расчёта спреда
       this.m_bid = 0;
       this.m_ask = 0;
       this.SetProperty(DEAL_PROP_SPREAD, 0);
       
    //--- Если исторический тик и значение Point символа удалось получить
       if(this.GetDealTick() && this.m_point!=0)
         {
          //--- запишем значения цен Bid и Ask и рассчитаем и сохраним значение спреда
          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);
         }
    //--- Если исторический тик получить не удалось, возьмём значение спреда минутного бара, на котором была сделка
       else
          this.SetProperty(DEAL_PROP_SPREAD, this.GetSpreadM1());
      }
    

    Armazenamos nos arrays de propriedades da classe os atributos da operação, bem como Digits e Point do símbolo no qual a operação foi executada. Esses valores são essenciais para cálculos e para a exibição das informações da operação. Em seguida, obtemos o tick histórico correspondente ao horário da operação. Isso nos permite acessar os preços Bid e Ask no momento da execução da operação, o que possibilita calcular o spread.


    O método de comparação entre dois objetos com base em uma propriedade especificada:

    //+------------------------------------------------------------------+
    //| Сравнивает два объекта между собой по указанному свойству        |
    //+------------------------------------------------------------------+
    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);
         }
      }
    

    Esse é um método virtual, que sobrescreve um método de mesmo nome na classe base CObject. Dependendo do critério de comparação (uma das propriedades da operação), esse método compara a propriedade do objeto atual com a do objeto passado como argumento. Ele retorna 1 se a propriedade do objeto atual for maior que a do objeto comparado, -1 se for menor e 0 se forem iguais.


    O método que retorna a descrição do tipo de operação:

    //+------------------------------------------------------------------+
    //| Возвращает описание типа сделки                                  |
    //+------------------------------------------------------------------+
    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();
         }
      }
    

    Dependendo do tipo da operação, o método retorna sua descrição em formato de texto. Para este projeto, esse método é redundante, pois utilizaremos apenas os tipos de operação relacionados a posições — compra e venda.


    O método que retorna a descrição da forma de modificação da posição:

    //+------------------------------------------------------------------+
    //| Возвращает описание способа изменения позиции                    |
    //+------------------------------------------------------------------+
    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();
         }
      }
    


    O método que retorna a descrição da razão da operação:

    //+------------------------------------------------------------------+
    //| Возвращает описание причины проведения сделки                    |
    //+------------------------------------------------------------------+
    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();
         }
      }
    


    O método que retorna a descrição completa da operação:

    //+------------------------------------------------------------------+
    //| Возвращает описание сделки                                       |
    //+------------------------------------------------------------------+
    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())));
      }
    


    O método que imprime no diário do terminal as propriedades da operação:

    //+------------------------------------------------------------------+
    //| Распечатывает в журнал свойства сделки                           |
    //+------------------------------------------------------------------+
    void CDeal::Print(void)
      {
       ::Print(this.Description());
      }
    


    O método que retorna o horário com milissegundos:

    //+------------------------------------------------------------------+
    //| Возвращает время с миллисекундами                                |
    //+------------------------------------------------------------------+
    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 os métodos que retornam ou imprimem no diário as descrições textuais das operações têm o objetivo de fornecer informações detalhadas sobre cada operação. No contexto deste projeto, esses métodos não são essenciais, mas sempre devemos considerar futuras expansões e melhorias. Por essa razão, esses métodos estão incluídos na implementação.

    O método que obtém o tick da operação:

    //+------------------------------------------------------------------+
    //| Получает тик сделки                                              |
    //| https://www.mql5.com/ru/forum/42122/page47#comment_37205238      |
    //+------------------------------------------------------------------+
    bool CDeal::GetDealTick(const int amount=20)
      {
       MqlTick ticks[];        // Сюда будем получать тики
       int attempts = amount;  // Количество попыток получения тиков
       int offset = 500;       // Начальное смещение времени для попытки
       int copied = 0;         // Количество скопированных тиков
       
    //--- До тех пор, пока не скопирован тик и не закончилось количество попыток копирования
    //--- пытаемся получить тик, на каждой итерации увеличивая вдвое начальное смещение времени (расширяем диапазон времени "from_msc")
       while(!::IsStopped() && (copied<=0) && (attempts--)!=0)
          copied = ::CopyTicksRange(this.Symbol(), ticks, COPY_TICKS_INFO, this.TimeMsc()-(offset <<=1), this.TimeMsc());
        
    //--- Если тик скопировать удалось (он последний в массиве тиков) - записываем его в переменную m_tick
       if(copied>0)
          this.m_tick=ticks[copied-1];
    
    //--- Возвращаем флаг того, что тик скопирован
       return(copied>0);
      }
    

    A lógica desse método está explicada nos comentários do código. Após obter o tick, são extraídos os preços Ask e Bid, e o spread é calculado como (Ask - Bid) / Point.

    Se, no final, esse método não conseguir obter o tick, então o valor médio do spread será calculado utilizando o método que obtém o spread da barra de minuto da operação:

    //+------------------------------------------------------------------+
    //| Получает спред минутного бара сделки                             |
    //+------------------------------------------------------------------+
    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);
      }
    

    A classe de operação está pronta. Os objetos dessa classe serão armazenados na lista de operações dentro da classe posição histórica, de onde poderão ser acessados e processados conforme necessário.


    Classe de posição histórica

    No diretório do terminal \MQL5\Services\AccountReporter, criaremos um novo arquivo Position.mqh, que conterá a classe CPosition.

    A classe deve ser derivada da classe base CObject da Biblioteca Padrão:

    //+------------------------------------------------------------------+
    //|                                                     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"
      
    //+------------------------------------------------------------------+
    //| Класс позиции                                                    |
    //+------------------------------------------------------------------+
    class CPosition : public CObject
      {
      }
    


    Como a classe de posição conterá a lista de operações pertencentes a essa posição, é necessário incluir no arquivo recém-criado o arquivo da classe de operações e o arquivo da classe de array dinâmico de ponteiros para objetos 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"
    
    #include "Deal.mqh"
    #include <Arrays\ArrayObj.mqh>
    
    //+------------------------------------------------------------------+
    //| Класс позиции                                                    |
    //+------------------------------------------------------------------+
    class CPosition : public CObject
      {
      }
    


    Agora, definimos as enumerações para as propriedades inteiras, de ponto flutuante e de string da posição e, nas seções privada, protegida e pública, declaramos as variáveis-membro da classe e os métodos para manipular as propriedades da posição:

    //+------------------------------------------------------------------+
    //|                                                     Position.mqh |
    //|                                  Copyright 2024, MetaQuotes Ltd. |
    //|                                             https://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Ltd."
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #include "Deal.mqh"
    #include <Arrays\ArrayObj.mqh>
    
    //--- Перечисление целочисленных свойств позиции
    enum ENUM_POSITION_PROPERTY_INT
      {
       POSITION_PROP_TICKET = 0,        // Тикет позиции
       POSITION_PROP_TIME,              // Время открытия позиции
       POSITION_PROP_TIME_MSC,          // Время открытия позиции в миллисекундах
       POSITION_PROP_TIME_UPDATE,       // Время изменения позиции
       POSITION_PROP_TIME_UPDATE_MSC,   // Время изменения позиции в миллисекундах
       POSITION_PROP_TYPE,              // Тип позиции
       POSITION_PROP_MAGIC,             // Magic number позиции
       POSITION_PROP_IDENTIFIER,        // Идентификатор позиции
       POSITION_PROP_REASON,            // Причина открытия позиции
       POSITION_PROP_ACCOUNT_LOGIN,     // Номер счёта
       POSITION_PROP_TIME_CLOSE,        // Время закрытия позиции
       POSITION_PROP_TIME_CLOSE_MSC,    // Время закрытия позиции в миллисекундах
      };
      
    //--- Перечисление вещественных свойств позиции
    enum ENUM_POSITION_PROPERTY_DBL
      {
       POSITION_PROP_VOLUME = POSITION_PROP_TIME_CLOSE_MSC+1,// Объем позиции
       POSITION_PROP_PRICE_OPEN,        // Цена позиции
       POSITION_PROP_SL,                // Stop Loss для открытой позиции
       POSITION_PROP_TP,                // Take Profit для открытой позиции
       POSITION_PROP_PRICE_CURRENT,     // Текущая цена по символу
       POSITION_PROP_SWAP,              // Накопленный своп
       POSITION_PROP_PROFIT,            // Текущая прибыль
       POSITION_PROP_CONTRACT_SIZE,     // Размер торгового контракта символа
       POSITION_PROP_PRICE_CLOSE,       // Цена закрытия позиции
       POSITION_PROP_COMMISSIONS,       // Накопленная комиссия
       POSITION_PROP_FEE,               // Накопленная оплата за сделки
      };
    
    //--- Перечисление строковых свойств позиции
    enum ENUM_POSITION_PROPERTY_STR
      {
       POSITION_PROP_SYMBOL = POSITION_PROP_FEE+1,// Символ, по которому открыта позиция
       POSITION_PROP_COMMENT,           // Комментарий к позиции
       POSITION_PROP_EXTERNAL_ID,       // Идентификатор позиции во внешней системе
       POSITION_PROP_CURRENCY_PROFIT,   // Валюта прибыли символа позиции
       POSITION_PROP_ACCOUNT_CURRENCY,  // Валюта депозита аккаунта
       POSITION_PROP_ACCOUNT_SERVER,    // Имя сервера
      };
      
    //+------------------------------------------------------------------+
    //| Класс позиции                                                    |
    //+------------------------------------------------------------------+
    class CPosition : public CObject
      {
    private:
       long              m_lprop[POSITION_PROP_TIME_CLOSE_MSC+1];                    // Массив для хранения целочисленных свойств
       double            m_dprop[POSITION_PROP_FEE-POSITION_PROP_TIME_CLOSE_MSC];    // Массив для хранения вещественных свойств
       string            m_sprop[POSITION_PROP_ACCOUNT_SERVER-POSITION_PROP_FEE];    // Массив для хранения строковых свойств
    
    //--- Возвращает индекс массива, по которому фактически расположено (1) double-свойство и (2) string-свойство ордера
       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;        // Список сделок позиции
       CDeal             m_temp_deal;         // Временный объект-сделка для поиска по свойству в списке
       
    //--- Возвращает время с миллисекундами
       string            TimeMscToString(const long time_msc,int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const;
    
    //--- Дополнительные свойства
       int               m_profit_pt;         // Прибыль в пунктах
       int               m_digits;            // Digits символа
       double            m_point;             // Значение одного пункта символа
       double            m_tick_value;        // Рассчитанная стоимость тика
       
    //--- Возвращает указатель на сделку (1) открытия, (2) закрытия
       CDeal            *GetDealIn(void)   const;
       CDeal            *GetDealOut(void)  const;
       
    public:
    //--- Возвращает список сделок
       CArrayObj        *GetListDeals(void)                              { return(&this.m_list_deals);                                  }
    
    //--- Установка свойств
    //--- Устанавливает (1) целочисленное, (2) вещественное и (3) строковое свойство
       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; }
    
    //--- Целочисленные свойства
       void              SetTicket(const long ticket)                    { this.SetProperty(POSITION_PROP_TICKET, ticket);              }  // Тикет позиции
       void              SetTime(const datetime time)                    { this.SetProperty(POSITION_PROP_TIME, time);                  }  // Время открытия позиции
       void              SetTimeMsc(const long value)                    { this.SetProperty(POSITION_PROP_TIME_MSC, value);             }  // Время открытия позиции в миллисекундах с 01.01.1970 
       void              SetTimeUpdate(const datetime time)              { this.SetProperty(POSITION_PROP_TIME_UPDATE, time);           }  // Время изменения позиции
       void              SetTimeUpdateMsc(const long value)              { this.SetProperty(POSITION_PROP_TIME_UPDATE_MSC, value);      }  // Время изменения позиции в миллисекундах с 01.01.1970
       void              SetTypePosition(const ENUM_POSITION_TYPE type)  { this.SetProperty(POSITION_PROP_TYPE, type);                  }  // Тип позиции
       void              SetMagic(const long magic)                      { this.SetProperty(POSITION_PROP_MAGIC, magic);                }  // Magic number для позиции (смотри ORDER_MAGIC)
       void              SetID(const long id)                            { this.SetProperty(POSITION_PROP_IDENTIFIER, id);              }  // Идентификатор позиции
       void              SetReason(const ENUM_POSITION_REASON reason)    { this.SetProperty(POSITION_PROP_REASON, reason);              }  // Причина открытия позиции
       void              SetTimeClose(const datetime time)               { this.SetProperty(POSITION_PROP_TIME_CLOSE, time);            }  // Время закрытия
       void              SetTimeCloseMsc(const long value)               { this.SetProperty(POSITION_PROP_TIME_CLOSE_MSC, value);       }  // Время закрытия в миллисекундах
       void              SetAccountLogin(const long login)               { this.SetProperty(POSITION_PROP_ACCOUNT_LOGIN, login);        }  // Номер счёта
       
    //--- Вещественные свойства
       void              SetVolume(const double volume)                  { this.SetProperty(POSITION_PROP_VOLUME, volume);              }  // Объем позиции
       void              SetPriceOpen(const double price)                { this.SetProperty(POSITION_PROP_PRICE_OPEN, price);           }  // Цена позиции
       void              SetSL(const double value)                       { this.SetProperty(POSITION_PROP_SL, value);                   }  // Уровень Stop Loss для открытой позиции
       void              SetTP(const double value)                       { this.SetProperty(POSITION_PROP_TP, value);                   }  // Уровень Take Profit для открытой позиции
       void              SetPriceCurrent(const double price)             { this.SetProperty(POSITION_PROP_PRICE_CURRENT, price);        }  // Текущая цена по символу
       void              SetSwap(const double value)                     { this.SetProperty(POSITION_PROP_SWAP, value);                 }  // Накопленный своп
       void              SetProfit(const double value)                   { this.SetProperty(POSITION_PROP_PROFIT, value);               }  // Текущая прибыль
       void              SetPriceClose(const double price)               { this.SetProperty(POSITION_PROP_PRICE_CLOSE, price);          }  // Цена закрытия
       void              SetContractSize(const double value)             { this.SetProperty(POSITION_PROP_CONTRACT_SIZE, value);        }  // Размер торгового контракта символа
       void              SetCommissions(void);                                                                                             // Совокупная комиссия всех сделок
       void              SetFee(void);                                                                                                     // Совокупная оплату за проведение сделок
       
    //--- Строковые свойства
       void              SetSymbol(const string symbol)                  { this.SetProperty(POSITION_PROP_SYMBOL, symbol);              }  // Символ, по которому открыта позиция
       void              SetComment(const string comment)                { this.SetProperty(POSITION_PROP_COMMENT, comment);            }  // Комментарий к позиции
       void              SetExternalID(const string ext_id)              { this.SetProperty(POSITION_PROP_EXTERNAL_ID, ext_id);         }  // Идентификатор позиции во внешней системе (на бирже)
       void              SetAccountServer(const string server)           { this.SetProperty(POSITION_PROP_ACCOUNT_SERVER, server);      }  // Имя сервера
       void              SetAccountCurrency(const string currency)       { this.SetProperty(POSITION_PROP_ACCOUNT_CURRENCY, currency);  }  // Валюта депозита аккаунта
       void              SetCurrencyProfit(const string currency)        { this.SetProperty(POSITION_PROP_CURRENCY_PROFIT, currency);   }  // Валюта прибыли символа позиции
       
    //--- Получение свойств
    //--- Возвращает из массива свойств (1) целочисленное, (2) вещественное и (3) строковое свойство
       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)];        }
    
    //--- Целочисленные свойства
       long              Ticket(void)                              const { return this.GetProperty(POSITION_PROP_TICKET);               }  // Тикет позиции
       datetime          Time(void)                                const { return (datetime)this.GetProperty(POSITION_PROP_TIME);       }  // Время открытия позиции
       long              TimeMsc(void)                             const { return this.GetProperty(POSITION_PROP_TIME_MSC);             }  // Время открытия позиции в миллисекундах с 01.01.1970 
       datetime          TimeUpdate(void)                          const { return (datetime)this.GetProperty(POSITION_PROP_TIME_UPDATE);}  // Время изменения позиции
       long              TimeUpdateMsc(void)                       const { return this.GetProperty(POSITION_PROP_TIME_UPDATE_MSC);      }  // Время изменения позиции в миллисекундах с 01.01.1970
       ENUM_POSITION_TYPE TypePosition(void)                       const { return (ENUM_POSITION_TYPE)this.GetProperty(POSITION_PROP_TYPE);}// Тип позиции
       long              Magic(void)                               const { return this.GetProperty(POSITION_PROP_MAGIC);                }  // Magic number для позиции (смотри ORDER_MAGIC)
       long              ID(void)                                  const { return this.GetProperty(POSITION_PROP_IDENTIFIER);           }  // Идентификатор позиции
       ENUM_POSITION_REASON Reason(void)                           const { return (ENUM_POSITION_REASON)this.GetProperty(POSITION_PROP_REASON);}// Причина открытия позиции
       datetime          TimeClose(void)                           const { return (datetime)this.GetProperty(POSITION_PROP_TIME_CLOSE); }  // Время закрытия
       long              TimeCloseMsc(void)                        const { return this.GetProperty(POSITION_PROP_TIME_CLOSE_MSC);       }  // Время закрытия в миллисекундах
       long              AccountLogin(void)                        const { return this.GetProperty(POSITION_PROP_ACCOUNT_LOGIN);        }  // Логин
       
    //--- Вещественные свойства
       double            Volume(void)                              const { return this.GetProperty(POSITION_PROP_VOLUME);               }  // Объем позиции
       double            PriceOpen(void)                           const { return this.GetProperty(POSITION_PROP_PRICE_OPEN);           }  // Цена позиции
       double            SL(void)                                  const { return this.GetProperty(POSITION_PROP_SL);                   }  // Уровень Stop Loss для открытой позиции
       double            TP(void)                                  const { return this.GetProperty(POSITION_PROP_TP);                   }  // Уровень Take Profit для открытой позиции
       double            PriceCurrent(void)                        const { return this.GetProperty(POSITION_PROP_PRICE_CURRENT);        }  // Текущая цена по символу
       double            Swap(void)                                const { return this.GetProperty(POSITION_PROP_SWAP);                 }  // Накопленный своп
       double            Profit(void)                              const { return this.GetProperty(POSITION_PROP_PROFIT);               }  // Текущая прибыль
       double            ContractSize(void)                        const { return this.GetProperty(POSITION_PROP_CONTRACT_SIZE);        }  // Размер торгового контракта символа
       double            PriceClose(void)                          const { return this.GetProperty(POSITION_PROP_PRICE_CLOSE);          }  // Цена закрытия
       double            Commissions(void)                         const { return this.GetProperty(POSITION_PROP_COMMISSIONS);          }  // Совокупная комиссия всех сделок
       double            Fee(void)                                 const { return this.GetProperty(POSITION_PROP_FEE);                  }  // Совокупная оплата за проведение сделок
       
    //--- Строковые свойства
       string            Symbol(void)                              const { return this.GetProperty(POSITION_PROP_SYMBOL);               }  // Символ, по которому открыта позиция
       string            Comment(void)                             const { return this.GetProperty(POSITION_PROP_COMMENT);              }  // Комментарий к позиции
       string            ExternalID(void)                          const { return this.GetProperty(POSITION_PROP_EXTERNAL_ID);          }  // Идентификатор позиции во внешней системе (на бирже)
       string            AccountServer(void)                       const { return this.GetProperty(POSITION_PROP_ACCOUNT_SERVER);       }  // Имя сервера
       string            AccountCurrency(void)                     const { return this.GetProperty(POSITION_PROP_ACCOUNT_CURRENCY);     }  // Валюта депозита аккаунта
       string            CurrencyProfit(void)                      const { return this.GetProperty(POSITION_PROP_CURRENCY_PROFIT);      }  // Валюта прибыли символа позиции
       
       
    //--- Дополнительные свойства
       ulong             DealIn(void)                              const;                                                                  // Тикет сделки открытия
       ulong             DealOut(void)                             const;                                                                  // Тикет сделки закрытия
       int               ProfitInPoints(void)                      const;                                                                  // Прибыль в пунктах
       int               SpreadIn(void)                            const;                                                                  // Спред при открытии
       int               SpreadOut(void)                           const;                                                                  // Спред при закрытии
       double            SpreadOutCost(void)                       const;                                                                  // Стоимость спреда при закрытии
       double            PriceOutAsk(void)                         const;                                                                  // Цена Ask при закрытии
       double            PriceOutBid(void)                         const;                                                                  // Цена Bid при закрытии
       
    //--- Добавляет сделку в список сделок, возвращает указатель
       CDeal            *DealAdd(const long ticket);
       
    //--- Возвращает описание типа позиции
       string            TypeDescription(void) const;
       
    //--- Возвращает описание времени и цены открытия позиции
       string            TimePriceCloseDescription(void);
    
    //--- Возвращает описание времени и цены закрытия позиции
       string            TimePriceOpenDescription(void);
       
    //--- Возвращает описание позиции
       string            Description(void);
    
    //--- Распечатывает в журнале свойства позиции и её сделок
       void              Print(void);
       
    //--- Сравнивает два объекта между собой по указанному в mode свойству
       virtual int       Compare(const CObject *node, const int mode=0) const;
       
    //--- Конструктор/деструктор
                         CPosition(const long position_id, const string symbol);
                         CPosition(void){}
                        ~CPosition();
      };
    

    Vamos analisar a implementação dos métodos da classe.

    No construtor da classe, atribuímos o identificador da posição e o símbolo a partir dos parâmetros passados ao método e registramos os dados da conta e do símbolo.

    //+------------------------------------------------------------------+
    //| Конструктор                                                      |
    //+------------------------------------------------------------------+
    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);
      }
    


    No destrutor da classe, limpamos a lista de operações da posição:

    //+------------------------------------------------------------------+
    //| Деструктор                                                       |
    //+------------------------------------------------------------------+
    CPosition::~CPosition()
      {
       this.m_list_deals.Clear();
      }
    


    O método de comparação entre dois objetos com base em uma propriedade especificada:

    //+------------------------------------------------------------------+
    //| Сравнивает два объекта между собой по указанному свойству        |
    //+------------------------------------------------------------------+
    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;
         }
      }
    

    Esse é um método virtual, que sobrescreve um método de mesmo nome na classe base CObject. Dependendo do critério de comparação (uma das propriedades da posição), esse método compara a propriedade do objeto atual com a do objeto passado como argumento. Ele retorna 1 se a propriedade do objeto atual for maior que a do objeto comparado, -1 se for menor e 0 se forem iguais.


    Método que retorna o tempo com milissegundos:

    //+------------------------------------------------------------------+
    //| Возвращает время с миллисекундами                                |
    //+------------------------------------------------------------------+
    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 o ponteiro para a operação de abertura:

    //+------------------------------------------------------------------+
    //| Возвращает указатель на сделку открытия                          |
    //+------------------------------------------------------------------+
    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;
      }
    

    Percorre a lista de operações da posição e busca aquela cujo modo de modificação da posição seja DEAL_ENTRY_IN (entrada no mercado). O método retorna o ponteiro para a operação encontrada.


    Método que retorna o ponteiro para a operação de fechamento:

    //+------------------------------------------------------------------+
    //| Возвращает указатель на сделку закрытия                          |
    //+------------------------------------------------------------------+
    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;
      }
    

    Percorre a lista de operações da posição e busca aquela cujo modo de modificação da posição seja DEAL_ENTRY_OUT (saída do mercado) ou DEAL_ENTRY_OUT_BY (fechamento por posição oposta). O método retorna o ponteiro para a operação encontrada.


    Método que retorna o ticket da operação de abertura:

    //+------------------------------------------------------------------+
    //| Возвращает тикет сделки открытия                                 |
    //+------------------------------------------------------------------+
    ulong CPosition::DealIn(void) const
      {
       CDeal *deal=this.GetDealIn();
       return(deal!=NULL ? deal.Ticket() : 0);
      }
    

    Obtém o ponteiro para a operação de entrada no mercado e retorna seu ticket.


    Método que retorna o ticket da operação de fechamento:

    //+------------------------------------------------------------------+
    //| Возвращает тикет сделки закрытия                                 |
    //+------------------------------------------------------------------+
    ulong CPosition::DealOut(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Ticket() : 0);
      }
    

    Obtém o ponteiro para a operação de saída do mercado e retorna seu ticket.


    Método que retorna o spread no momento da abertura:

    //+------------------------------------------------------------------+
    //| Возвращает спред при открытии                                    |
    //+------------------------------------------------------------------+
    int CPosition::SpreadIn(void) const
      {
       CDeal *deal=this.GetDealIn();
       return(deal!=NULL ? deal.Spread() : 0);
      }
    

    Obtemos o ponteiro para a operação de entrada no mercado e retornamos o valor do spread registrado na operação.


    Método que retorna o spread no fechamento:

    //+------------------------------------------------------------------+
    //| Возвращает спред при закрытии                                    |
    //+------------------------------------------------------------------+
    int CPosition::SpreadOut(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Spread() : 0);
      }
    

    Obtemos o ponteiro para a operação de saída do mercado e retornamos o valor do spread registrado na operação.


    Método que retorna o preço Ask no fechamento:

    //+------------------------------------------------------------------+
    //| Возвращает цену Ask при закрытии                                 |
    //+------------------------------------------------------------------+
    double CPosition::PriceOutAsk(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Ask() : 0);
      }
    

    Obtemos o ponteiro para a operação de saída do mercado e retornamos o valor do preço Ask registrado na operação.


    Método que retorna o preço Bid no fechamento:

    //+------------------------------------------------------------------+
    //| Возвращает цену Bid при закрытии                                 |
    //+------------------------------------------------------------------+
    double CPosition::PriceOutBid(void) const
      {
       CDeal *deal=this.GetDealOut();
       return(deal!=NULL ? deal.Bid() : 0);
      }
    

    Obtemos o ponteiro para a operação de saída do mercado e retornamos o valor do preço Bid registrado na operação.


    Método que retorna o lucro em pontos:

    //+------------------------------------------------------------------+
    //| Возвращает прибыль в пунктах                                     |
    //+------------------------------------------------------------------+
    int CPosition::ProfitInPoints(void) const
      {
    //--- Если Point символа не получен ранее, сообщаем об этом и возвращаем 0
       if(this.m_point==0)
         {
          ::Print("The Point() value could not be retrieved.");
          return 0;
         }
    //--- Получаем цены открытия и закрытия позиции
       double open =this.PriceOpen();
       double close=this.PriceClose();
       
    //--- Если цены получить не удалось - возвращаем 0
       if(open==0 || close==0)
          return 0;
       
    //--- В зависимости от типа позиции возвращаем рассчитанное значение прибыли позиции в пунктах
       return (int)::round(this.TypePosition()==POSITION_TYPE_BUY ? (close-open)/this.m_point : (open-close)/this.m_point);
      }
    


    Método que retorna o custo do spread no fechamento:

    //+------------------------------------------------------------------+
    //| Возвращает стоимость спреда при закрытии                         |
    //+------------------------------------------------------------------+
    double CPosition::SpreadOutCost(void) const
      {
    //--- Получаем сделку закрытия
       CDeal *deal=this.GetDealOut();
       if(deal==NULL)
          return 0;
    
    //--- Получаем профит позиции и профит позиции в пунктах
       double profit=this.Profit();
       int profit_pt=this.ProfitInPoints();
       
    //--- Если профит нулевой - возвращаем стоимость спреда по формуле TickValue * Spread * Lots
       if(profit==0)
          return(this.m_tick_value * deal.Spread() * deal.Volume());
    
    //--- Рассчитываем и возвращаем стоимость спреда (пропорция)
       return(profit_pt>0 ? deal.Spread() * ::fabs(profit / profit_pt) : 0);
      }
    

    Este método usa duas formas de cálculo do custo do spread:

    1. Se o lucro da posição for diferente de zero, o custo do spread é calculado por proporção: tamanho do spread em pontos * lucro da posição em dinheiro / lucro da posição em pontos.
    2. Se o lucro da posição for zero, o custo do spread é calculado pela fórmula: custo do tick calculado * tamanho do spread em pontos × volume da operação.


    Método que define a comissão total de todas as operações:

    //+------------------------------------------------------------------+
    //| Устанавливает совокупную комиссию всех сделок                    |
    //+------------------------------------------------------------------+
    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 a comissão total cobrada durante todo o período de existência da posição, somamos as comissões de todas as operações da posição. No laço que percorre a lista de operações da posição, somamos a comissão de cada operação ao valor total, que no final é retornado pelo método.


    Método que define o custo total das operações:

    //+------------------------------------------------------------------+
    //| Устанавливает совокупную оплату за проведение сделок             |
    //+------------------------------------------------------------------+
    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);
      }
    

    Aqui, o processo é idêntico ao do método anterior — retornamos a soma total dos valores Fee de todas as operações da posição.

    Ambos os métodos devem ser chamados somente após todas as operações da posição terem sido adicionadas à lista, caso contrário, o resultado será incompleto.


    Método que adiciona uma operação à lista de operações da posição:

    //+------------------------------------------------------------------+
    //| Добавляет сделку в список сделок                                 |
    //+------------------------------------------------------------------+
    CDeal *CPosition::DealAdd(const long ticket)
      {
    //--- Устанавливаем временному объекту тикет искомой сделки и устанавливаем флаг сортировки списка сделок по тикету
       this.m_temp_deal.SetTicket(ticket);
       this.m_list_deals.Sort(DEAL_PROP_TICKET);
       
    //--- Записываем результат проверки присутствия в списке сделки с таким тикетом
       bool exist=(this.m_list_deals.Search(&this.m_temp_deal)!=WRONG_VALUE);
       
    //--- Возвращаем для списка сортировку по времени в миллисекундах
       this.m_list_deals.Sort(DEAL_PROP_TIME_MSC);
    
    //--- Если сделка с таким тикетом уже есть в списке - возвращаем NULL
       if(exist)
          return NULL;
          
    //--- Создаём новый объект-сделку
       CDeal *deal=new CDeal(ticket);
       if(deal==NULL)
          return NULL;
       
    //--- Добавляем созданный объект в список в порядке сортировки по времени в миллисекундах
    //--- Если сделку добавить в список не удалось - удаляем объект сделки и возвращаем NULL
       if(!this.m_list_deals.InsertSort(deal))
         {
          delete deal;
          return NULL;
         }
       
    //--- Если это сделка закрытия позиции - записываем в значение прибыли позиции прибыль и своп из свойств сделки
       if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY)
         {
          this.SetProfit(deal.Profit());
          this.SetSwap(deal.Swap());
         }
         
    //--- Возвращаем указатель на созданный объект-сделку
       return deal;   
      }
    

    A lógica do método está completamente explicada nos comentários do código. O método recebe o ticket da operação atualmente selecionada. Se a operação com esse ticket ainda não estiver na lista, um novo objeto de operação é criado e adicionado à lista de operações da posição.


    Os métodos que retornam descrições de algumas propriedades da posição:

    //+------------------------------------------------------------------+
    //| Возвращает описание типа позиции                                 |
    //+------------------------------------------------------------------+
    string CPosition::TypeDescription(void) const
      {
       return(this.TypePosition()==POSITION_TYPE_BUY ? "Buy" : this.TypePosition()==POSITION_TYPE_SELL ? "Sell" : "Unknown::"+(string)this.TypePosition());
      }
    //+------------------------------------------------------------------+
    //| Возвращает описание времени и цены открытия позиции              |
    //+------------------------------------------------------------------+
    string CPosition::TimePriceOpenDescription(void)
      {
       return(::StringFormat("Opened %s [%.*f]", this.TimeMscToString(this.TimeMsc()),this.m_digits, this.PriceOpen()));
      }
    //+------------------------------------------------------------------+
    //| Возвращает описание времени и цены закрытия позиции              |
    //+------------------------------------------------------------------+
    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()));
      }
    //+------------------------------------------------------------------+
    //| Возвращает краткое описание позиции                              |
    //+------------------------------------------------------------------+
    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()));
      }
    

    Esses métodos são usados, por exemplo, para exibir a descrição da posição no diário do terminal.

    A impressão da descrição da posição no diário pode ser feita utilizando o método Print:

    //+------------------------------------------------------------------+
    //| Распечатывает в журнале свойства позиции и её сделок             |
    //+------------------------------------------------------------------+
    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();
         }
      }
    

    Primeiro, o cabeçalho com a descrição da posição é impresso. Em seguida, o código percorre todas as operações da posição e imprime a descrição de cada operação utilizando o método Print() da operação correspondente.

    A classe de posição histórica está pronta. Agora, vamos criar uma classe estática para a seleção, busca e filtragem de operações e posições com base em suas propriedades.


    Classe para busca e filtragem por propriedades de operações e posições

    Esse tipo de classe foi detalhadamente analisado no artigo "Biblioteca para a criação simples e rápida de programas para MetaTrader (Parte III): Coleção de ordens de mercado e posições, busca e filtragem", na seção Organização da busca.

    No diretório \MQL5\Services\AccountReporter, criaremos um novo arquivo Select.mqh, que conterá a classe CSelect:

    //+------------------------------------------------------------------+
    //|                                                       Select.mqh |
    //|                        Copyright 2024, MetaQuotes Software Corp. |
    //|                             https://mql5.com/ru/users/artmedia70 |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Software Corp."
    #property link      "https://mql5.com/ru/users/artmedia70"
    #property version   "1.00"
    
    //+------------------------------------------------------------------+
    //| Класс для выборки объектов, удовлетворяющих критерию             |
    //+------------------------------------------------------------------+
    class CSelect
      {
      }


    Definiremos as enumerações dos modos de comparação, incluiremos os arquivos das classes de operações e posições e declararemos a lista de armazenamento:

    //+------------------------------------------------------------------+
    //|                                                       Select.mqh |
    //|                        Copyright 2024, MetaQuotes Software Corp. |
    //|                             https://mql5.com/ru/users/artmedia70 |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Software Corp."
    #property link      "https://mql5.com/ru/users/artmedia70"
    #property version   "1.00"
    
    enum ENUM_COMPARER_TYPE
      {
       EQUAL,                                                   // Равно
       MORE,                                                    // Больше
       LESS,                                                    // Меньше
       NO_EQUAL,                                                // Не равно
       EQUAL_OR_MORE,                                           // Больше или равно
       EQUAL_OR_LESS                                            // Меньше или равно
      };
    
    //+------------------------------------------------------------------+
    //| Включаемые файлы                                                 |
    //+------------------------------------------------------------------+
    #include "Deal.mqh"
    #include "Position.mqh"
    
    //+------------------------------------------------------------------+
    //| Список-хранилище                                                 |
    //+------------------------------------------------------------------+
    CArrayObj   ListStorage; // Объект-хранилище для хранения сортированных списков коллекций
    //+------------------------------------------------------------------+
    //| Класс для выборки объектов, удовлетворяющих критерию             |
    //+------------------------------------------------------------------+
    class CSelect
      {
      }
    


    Implementaremos todos os métodos para a seleção de objetos e criação de listas que atendam aos critérios de busca:

    //+------------------------------------------------------------------+
    //|                                                       Select.mqh |
    //|                        Copyright 2024, MetaQuotes Software Corp. |
    //|                             https://mql5.com/ru/users/artmedia70 |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024, MetaQuotes Software Corp."
    #property link      "https://mql5.com/ru/users/artmedia70"
    #property version   "1.00"
    
    enum ENUM_COMPARER_TYPE                                     // Режимы сравнения
      {
       EQUAL,                                                   // Равно
       MORE,                                                    // Больше
       LESS,                                                    // Меньше
       NO_EQUAL,                                                // Не равно
       EQUAL_OR_MORE,                                           // Больше или равно
       EQUAL_OR_LESS                                            // Меньше или равно
      };
    
    //+------------------------------------------------------------------+
    //| Включаемые файлы                                                 |
    //+------------------------------------------------------------------+
    #include "Deal.mqh"
    #include "Position.mqh"
    
    //+------------------------------------------------------------------+
    //| Список-хранилище                                                 |
    //+------------------------------------------------------------------+
    CArrayObj   ListStorage; // Объект-хранилище для хранения сортированных списков коллекций
    //+------------------------------------------------------------------+
    //| Класс для выборки объектов, удовлетворяющих критерию             |
    //+------------------------------------------------------------------+
    class CSelect
      {
    private:
       //--- Метод сравнения двух величин
       template<typename T>
       static bool       CompareValues(T value1,T value2,ENUM_COMPARER_TYPE mode);
    public:
    //+------------------------------------------------------------------+
    //| Методы работы со сделками                                        |
    //+------------------------------------------------------------------+
       //--- Возвращает список сделок, у которых одно из (1) целочисленных, (2) вещественных и (3) строковых свойств удовлетворяет заданному критерию
       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);
       
       //--- Возвращает индекс сделки в списке с максимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства
       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);
       
       //--- Возвращает индекс сделки в списке с минимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства
       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);
       
    //+------------------------------------------------------------------+
    //| Методы работы с позициями                                        |
    //+------------------------------------------------------------------+
       //--- Возвращает список позиций, у которых одно из (1) целочисленных, (2) вещественных и (3) строковых свойств удовлетворяет заданному критерию
       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);
       
       //--- Возвращает индекс позиции в списке с максимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства
       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);
       
       //--- Возвращает индекс позиции в списке с минимальным значением (1) целочисленного, (2) вещественного и (3) строкового свойства
       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);
      };
    //+------------------------------------------------------------------+
    //| Метод сравнения двух величин                                     |
    //+------------------------------------------------------------------+
    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;
         }
      }
    //+------------------------------------------------------------------+
    //| Методы работы со списками сделок                                 |
    //+------------------------------------------------------------------+
    //+------------------------------------------------------------------+
    //| Возвращает список сделок, у которых одно из целочисленных        |
    //| свойств удовлетворяет заданному критерию                         |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает список сделок, у которых одно из вещественных         |
    //| свойств удовлетворяет заданному критерию                         |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает список сделок, у которых одно из строковых            |
    //| свойств удовлетворяет заданному критерию                         |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс сделки в списке                                |
    //| с максимальным значением целочисленного свойства                 |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс сделки в списке                                |
    //| с максимальным значением вещественного свойства                  |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс сделки в списке                                |
    //| с максимальным значением строкового свойства                     |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс сделки в списке                                |
    //| с минимальным значением целочисленного свойства                  |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс сделки в списке                                |
    //| с минимальным значением вещественного свойства                   |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс сделки в списке                                |
    //| с минимальным значением строкового свойства                      |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Методы работы со списками позиций                                |
    //+------------------------------------------------------------------+
    //+------------------------------------------------------------------+
    //| Возвращает список позиций, у которых одно из целочисленных       |
    //| свойств удовлетворяет заданному критерию                         |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает список позиций, у которых одно из вещественных        |
    //| свойств удовлетворяет заданному критерию                         |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает список позиций, у которых одно из строковых           |
    //| свойств удовлетворяет заданному критерию                         |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс позиции в списке                               |
    //| с максимальным значением целочисленного свойства                 |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс позиции в списке                               |
    //| с максимальным значением вещественного свойства                  |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс позиции в списке                               |
    //| с максимальным значением строкового свойства                     |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс позиции в списке                               |
    //| с минимальным значением целочисленного свойства                  |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс позиции в списке                               |
    //| с минимальным значением вещественного свойства                   |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает индекс позиции в списке                               |
    //| с минимальным значением строкового свойства                      |
    //+------------------------------------------------------------------+
    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;
      }
    

    Para uma explicação detalhada e completa desse tipo de classe, consulte o artigo mencionado anteriormente na seção "Organização da busca".

    Agora, temos tudo pronto para criar a classe que trabalhará com a lista de posições históricas.


    Classe coleção de posições históricas

    A classe coleção de posições históricas No diretório do terminal \MQL5\Services\AccountReporter, criaremos um novo arquivo PositionsControl.mqh, que conterá a classe CPositionsControl.
    A classe deve ser derivada da classe base CObject da Biblioteca Padrão, e os arquivos da classe de posições históricas e da classe de busca e filtragem devem ser incluídos no novo arquivo:

    //+------------------------------------------------------------------+
    //|                                             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"
    
    //+------------------------------------------------------------------+
    //| Класс-коллекция исторических позиций                             |
    //+------------------------------------------------------------------+
    class CPositionsControl : public CObject
      {
      }


    Declaramos os métodos privados, protegidos e públicos da classe:

    //+------------------------------------------------------------------+
    //| Класс-коллекция исторических позиций                             |
    //+------------------------------------------------------------------+
    class CPositionsControl : public CObject
      {
    private:
    //--- Возвращает (1) тип позиции, (2) причину открытия по типу сделки
       ENUM_POSITION_TYPE PositionTypeByDeal(const CDeal *deal);
       ENUM_POSITION_REASON PositionReasonByDeal(const CDeal *deal);
    
    protected:
       CPosition         m_temp_pos;          // Временный объект позиции для поиска
       CArrayObj         m_list_pos;          // Список позиций
       
    //--- Возвращает объект-позицию из списка по идентификатору
       CPosition        *GetPositionObjByID(const long id);
    
    //--- Возвращает флаг того, что позиция рыночная
       bool              IsMarketPosition(const long id);
       
    public:
    //--- Создаёт и обновляет список позиций. Может быть переопределён в наследуемых классах
       virtual bool      Refresh(void);
       
    //--- Возвращает (1) список, (2) количество позиций в списке
       CArrayObj        *GetPositionsList(void)           { return &this.m_list_pos;          }
       int               PositionsTotal(void)       const { return this.m_list_pos.Total();   }
    
    //--- Распечатывает в журнале свойства всех позиций в списке и их сделок
       void              Print(void);
    
    //--- Конструктор/деструктор
                         CPositionsControl(void);
                        ~CPositionsControl();
      };
    


    Agora, analisamos a implementação dos métodos declarados.

    No construtor da classe, definimos o sinalizador de ordenação da lista de posições históricas, organizando as posições pelo horário de fechamento em milissegundos:

    //+------------------------------------------------------------------+
    //| Конструктор                                                      |
    //+------------------------------------------------------------------+
    CPositionsControl::CPositionsControl(void)
      {
       this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC);
      }
    


    No destrutor da classe, eliminamos a lista de posições históricas:

    //+------------------------------------------------------------------+
    //| Деструктор                                                       |
    //+------------------------------------------------------------------+
    CPositionsControl::~CPositionsControl()
      {
       this.m_list_pos.Shutdown();
      }
    


    Método que retorna um ponteiro para o objeto de posição da lista com base no identificador:

    //+------------------------------------------------------------------+
    //| Возвращает объект-позицию из списка по идентификатору            |
    //+------------------------------------------------------------------+
    CPosition *CPositionsControl::GetPositionObjByID(const long id)
      {
    //--- Устанавливаем временному объекту идентификатор позиции, а списку - флаг сортировки по идентификатору позиции
       this.m_temp_pos.SetID(id);
       this.m_list_pos.Sort(POSITION_PROP_IDENTIFIER);
       
    //--- Получаем из списка индекс объекта-позиции с указанным идентификатором (либо -1 при его отсутствии)
    //--- По полученному индексу получаем указатель на объект позицию из списка (либо NULL при значении индекса -1)
       int index=this.m_list_pos.Search(&this.m_temp_pos);
       CPosition *pos=this.m_list_pos.At(index);
       
    //--- Возвращаем списку флаг сортировки по времени закрытия позиции в миллисекундах и
    //--- возвращаем указатель на объект-позицию (либо NULL при его отсутствии)
       this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC);
       return pos;
      }
    


    Método que retorna um sinalizador indicando se a posição é de mercado:

    //+------------------------------------------------------------------+
    //| Возвращает флаг того, что позиция рыночная                       |
    //+------------------------------------------------------------------+
    bool CPositionsControl::IsMarketPosition(const long id)
      {
    //--- В цикле по списку действующих позиций в терминале
       for(int i=::PositionsTotal()-1; i>=0; i--)
         {
          //--- получаем тикет позиции по индексу цикла
          ulong ticket=::PositionGetTicket(i);
          //--- Если тикет получен и позицию можно выбрать и её идентификатор равен переданному в метод -
          //--- это искомая рыночная позиция, возвращаем true
          if(ticket!=0 && ::PositionSelectByTicket(ticket) && ::PositionGetInteger(POSITION_IDENTIFIER)==id)
             return true;
         }
    //--- Нет такой рыночной позиции - возвращаем false
       return false;
      }
    


    Método que retorna o tipo da posição com base no tipo da operação:

    //+------------------------------------------------------------------+
    //| Возвращает тип позиции по типу сделки                            |
    //+------------------------------------------------------------------+
    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;
         }
      }
    

    Dependendo do tipo da operação, retorna o tipo correspondente da posição.


    Método que retorna a razão da abertura da posição com base no tipo da operação:

    //+------------------------------------------------------------------+
    //| Возвращает причину открытия позиции по типу сделки               |
    //+------------------------------------------------------------------+
    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;
         }
      }
    

    Dependendo da razão da execução da operação, retorna a razão correspondente para a abertura da posição.


    Método que cria ou atualiza a lista de posições históricas:

    //+------------------------------------------------------------------+
    //| Создаёт список исторических позиций                              |
    //+------------------------------------------------------------------+
    bool CPositionsControl::Refresh(void)
      {
    //--- Если запросить историю сделок и ордеров не удалось - возвращаем false
       if(!::HistorySelect(0,::TimeCurrent()))
          return false;
          
    //--- Ставим списку позиций флаг сортировки по времени в миллисекундах
       this.m_list_pos.Sort(POSITION_PROP_TIME_MSC);
       
    //--- Объявляем переменную результата и указатель на объект позиции
       bool res=true;
       CPosition *pos=NULL;
    
    //--- В цикле по количеству сделок истории
       int total=::HistoryDealsTotal();
       for(int i=total-1; i>=0; i--)
         {
          //--- получаем тикет очередной сделки в списке
          ulong ticket=::HistoryDealGetTicket(i);
          
          //--- Если тикет сделки не получен, или это не операция покупки/продажи - идём дальше
          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;
          
          //--- Получаем из сделки значение идентификатора позиции
          long pos_id=::HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
          
          //--- Если это рыночная позиция - идём далее
          if(this.IsMarketPosition(pos_id))
             continue;
             
          //--- Получаем указатель на объект-позицию из списка
          pos=this.GetPositionObjByID(pos_id);
          
          //--- Если позиции с таким идентификатором в списке ещё нет
          if(pos==NULL)
            {
             //--- Создаём новый объект позиции и, если объект создать не удалось, добавляем к переменной res значение false и идём далее
             string pos_symbol=HistoryDealGetString(ticket, DEAL_SYMBOL);
             pos=new CPosition(pos_id, pos_symbol);
             if(pos==NULL)
               {
                res &=false;
                continue;
               }
             
             //--- Если объект позиции не удалось добавить в список - добавляем к переменной res значение false, удаляем объект позиции и идём далее
             if(!this.m_list_pos.InsertSort(pos))
               {
                res &=false;
                delete pos;
                continue;
               }
            }
          
          //--- Если объект сделки не удалось добавить в список сделок объекта позиции - добавляем к переменной res значение false и идём далее
          CDeal *deal=pos.DealAdd(ticket);
          if(deal==NULL)
            {
             res &=false;
             continue;
            }
          
          //--- Всё успешно.
          //--- В зависимости от типа сделки устанавливаем свойства позиции
          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());
            }
         }
         
    //--- Все исторические позиции созданы, а соответствующие сделки добавлены в списки сделок объектов-позиций
    //--- Устанавливаем  списку позиций флаг сортировки по времени закрытия в миллисекундах
       this.m_list_pos.Sort(POSITION_PROP_TIME_CLOSE_MSC);
    
    //--- В цикле по созданному списку закрытых позиций устанавливаем каждой позиции значения Commissions и Fee
       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 res;
      }
    

    No laço que percorre a lista de operações no terminal, obtemos a próxima operação e verificamos seu identificador de posição. Se for uma posição de mercado, a operação é ignorada. Se a posição ainda não existir na lista de posições históricas, criamos um novo objeto de posição e o adicionamos à lista de posições históricas. Se a operação com o ticket da operação selecionada ainda não estiver no objeto de posição histórica, adicionamos a operação à lista de operações do objeto de posição. Ao final do ciclo de criação dos objetos de posições históricas, definimos para cada posição a comissão total de todas as operações da posição e o custo total das operações. O método é virtual, permitindo que, em uma classe derivada, uma lógica mais otimizada seja implementada caso a atualização da lista de posições seja necessária com maior frequência do que o mínimo de uma vez por dia.


    Método que imprime no diário do terminal as propriedades das posições e suas operações:

    //+------------------------------------------------------------------+
    //| Распечатывает в журнале свойства позиций и их сделок             |
    //+------------------------------------------------------------------+
    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();
         }
      }
    

    Caso seja necessário verificar a lista de posições históricas criadas, este método permite imprimir no diário do terminal cada posição juntamente com suas operações.

    O programa de serviço pode "lembrar" todos os contas às quais esteve conectado durante sua execução contínua. Ou seja, se o terminal não foi reiniciado e houve conexões com diferentes contas e servidores de trading, o programa armazenará essas contas, e cada uma delas conterá a lista de todas as posições fechadas. Os relatórios de trading serão gerados com base nas posições fechadas de cada uma das contas conectadas. Se, nas configurações, estiver definido que os relatórios devem ser gerados apenas para a conta atual, então as listas de posições fechadas serão filtradas pelo login e servidor da conta ativa.

    Com base nisso, fica claro que precisamos de uma classe de conta, que armazenará a classe de gerenciamento da lista de posições fechadas que foram negociadas nessa conta. No programa de serviço, obteremos a conta necessária e, a partir dela, a lista de posições fechadas.


    Classe de conta

    No diretório do terminal \MQL5\Services\AccountReporter, criaremos um novo arquivo Account.mqh, que conterá a classe CAccount.
    A classe deve ser derivada da classe base CObject da Biblioteca Padrão, e o arquivo da classe de coleção de posições históricas deve ser incluído no novo arquivo:

    //+------------------------------------------------------------------+
    //|                                                      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"
    
    //+------------------------------------------------------------------+
    //| Класс аккаунта                                                   |
    //+------------------------------------------------------------------+
    class CAccount : public CObject
      {
      }


    Na seção protegida da classe, declaramos o objeto de controle de posições históricas (a classe que gerencia a lista de posições fechadas da conta) e as listas de propriedades inteiras, de ponto flutuante e de string:

    //+------------------------------------------------------------------+
    //| Класс аккаунта                                                   |
    //+------------------------------------------------------------------+
    class CAccount : public CObject
      {
    private:
       
    protected:
       CPositionsControl m_positions;                  // Объект контроля исторических позиций
    //--- целочисленные свойства аккаунта
       long              m_login;                    //   Номер счета
       ENUM_ACCOUNT_TRADE_MODE m_trade_mode;         //   Тип торгового счета
       long              m_leverage;                 //   Размер предоставленного плеча
       int               m_limit_orders;             //   Максимально допустимое количество действующих отложенных ордеров
       ENUM_ACCOUNT_STOPOUT_MODE m_margin_so_mode;   //   Режим задания минимально допустимого уровня залоговых средств
       bool              m_trade_allowed;            //   Разрешенность торговли для текущего счета
       bool              m_trade_expert;             //   Разрешенность торговли для эксперта
       ENUM_ACCOUNT_MARGIN_MODE m_margin_mode;       //   Режим расчета маржи
       int               m_currency_digits;          //   Количество знаков после запятой для валюты счета, необходимых для точного отображения торговых результатов
       bool              m_fifo_close;               //   Признак того, что позиции можно закрывать только по правилу FIFO
       bool              m_hedge_allowed;            //   Признак того, что разрешены встречные позиции по одному символу
        
    //--- вещественные свойства аккаунта
       double            m_balance;                  //   Баланс счета в валюте депозита
       double            m_credit;                   //   Размер предоставленного кредита в валюте депозита
       double            m_profit;                   //   Размер текущей прибыли на счете в валюте депозита
       double            m_equity;                   //   Значение собственных средств на счете в валюте депозита
       double            m_margin;                   //   Размер зарезервированных залоговых средств на счете  в валюте депозита
       double            m_margin_free;              //   Размер свободных средств на счете  в валюте депозита, доступных для открытия позиции
       double            m_margin_level;             //   Уровень залоговых средств на счете в процентах
       double            m_margin_so_call;           //   Уровень залоговых средств, при котором требуется пополнение счета (Margin Call)
       double            m_margin_so_so;             //   Уровень залоговых средств, при достижении которого происходит принудительное закрытие самой убыточной позиции (Stop Out)
       double            m_margin_initial;           //   Размер средств, зарезервированных на счёте, для обеспечения гарантийной суммы по всем отложенным ордерам 
       double            m_margin_maintenance;       //   Размер средств, зарезервированных на счёте, для обеспечения минимальной суммы по всем открытым позициям
       double            m_assets;                   //   Текущий размер активов на счёте
       double            m_liabilities;              //   Текущий размер обязательств на счёте
       double            m_commission_blocked;       //   Текущая сумма заблокированных комиссий по счёту
      
    //--- строковые свойства аккаунта
       string            m_name;                     //   Имя клиента
       string            m_server;                   //   Имя торгового сервера
       string            m_currency;                 //   Валюта депозита
       string            m_company;                  //   Имя компании, обслуживающей счет
       
    public:
    


    Na seção pública, definimos os métodos para manipular as listas, métodos para definir e recuperar as propriedades do objeto de conta e outros métodos auxiliares:

    public:
    //--- Возвращает (1) объект контроля, (2) список исторических позиций, (3) количество позиций
       CPositionsControl*GetPositionsCtrlObj(void)        { return &this.m_positions;                  }
       CArrayObj        *GetPositionsList(void)           { return this.m_positions.GetPositionsList();}
       int               PositionsTotal(void)             { return this.m_positions.PositionsTotal();  }
    
    //--- Возвращает список позиций по фильтру (1) целочисленного, (2) вещественного, (3) строкового свойства
       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) Обновляет, (2) распечатывает в журнал список закрытых позиций
       bool              PositionsRefresh(void)           { return this.m_positions.Refresh();}
       void              PositionsPrint(void)             { this.m_positions.Print();         }
       
    //--- устанавливает (1) логин, (2) сервер
       void              SetLogin(const long login)       { this.m_login=login;               }
       void              SetServer(const string server)   { this.m_server=server;             }
       
    //--- возврат целочисленных свойств аккаунта
       long              Login(void)                const { return this.m_login;              }  //   Номер счета
       ENUM_ACCOUNT_TRADE_MODE TradeMode(void)      const { return this.m_trade_mode;         }  //   Тип торгового счета
       long              Leverage(void)             const { return this.m_leverage;           }  //   Размер предоставленного плеча
       int               LimitOrders(void)          const { return this.m_limit_orders;       }  //   Максимально допустимое количество действующих отложенных ордеров
       ENUM_ACCOUNT_STOPOUT_MODE MarginSoMode(void) const { return this.m_margin_so_mode;     }  //   Режим задания минимально допустимого уровня залоговых средств
       bool              TradeAllowed(void)         const { return this.m_trade_allowed;      }  //   Разрешенность торговли для текущего счета
       bool              TradeExpert(void)          const { return this.m_trade_expert;       }  //   Разрешенность торговли для эксперта
       ENUM_ACCOUNT_MARGIN_MODE MarginMode(void)    const { return this.m_margin_mode;        }  //   Режим расчета маржи
       int               CurrencyDigits(void)       const { return this.m_currency_digits;    }  //   Количество знаков после запятой для валюты счета, необходимых для точного отображения торговых результатов
       bool              FIFOClose(void)            const { return this.m_fifo_close;         }  //   Признак того, что позиции можно закрывать только по правилу FIFO
       bool              HedgeAllowed(void)         const { return this.m_hedge_allowed;      }  //   Признак того, что разрешены встречные позиции по одному символу
        
    //--- возврат вещественных свойств аккаунта
       double            Balance(void)              const { return this.m_balance;            }  //   Баланс счета в валюте депозита
       double            Credit(void)               const { return this.m_credit;             }  //   Размер предоставленного кредита в валюте депозита
       double            Profit(void)               const { return this.m_profit;             }  //   Размер текущей прибыли на счете в валюте депозита
       double            Equity(void)               const { return this.m_equity;             }  //   Значение собственных средств на счете в валюте депозита
       double            Margin(void)               const { return this.m_margin;             }  //   Размер зарезервированных залоговых средств на счете  в валюте депозита
       double            MarginFree(void)           const { return this.m_margin_free;        }  //   Размер свободных средств на счете  в валюте депозита, доступных для открытия позиции
       double            MarginLevel(void)          const { return this.m_margin_level;       }  //   Уровень залоговых средств на счете в процентах
       double            MarginSoCall(void)         const { return this.m_margin_so_call;     }  //   Уровень залоговых средств, при котором требуется пополнение счета (Margin Call)
       double            MarginSoSo(void)           const { return this.m_margin_so_so;       }  //   Уровень залоговых средств, при достижении которого происходит принудительное закрытие самой убыточной позиции (Stop Out)
       double            MarginInitial(void)        const { return this.m_margin_initial;     }  //   Размер средств, зарезервированных на счёте, для обеспечения гарантийной суммы по всем отложенным ордерам 
       double            MarginMaintenance(void)    const { return this.m_margin_maintenance; }  //   Размер средств, зарезервированных на счёте, для обеспечения минимальной суммы по всем открытым позициям
       double            Assets(void)               const { return this.m_assets;             }  //   Текущий размер активов на счёте
       double            Liabilities(void)          const { return this.m_liabilities;        }  //   Текущий размер обязательств на счёте
       double            CommissionBlocked(void)    const { return this.m_commission_blocked; }  //   Текущая сумма заблокированных комиссий по счёту
      
    //--- возврат строковых свойств аккаунта
       string            Name(void)                 const { return this.m_name;               }  //   Имя клиента
       string            Server(void)               const { return this.m_server;             }  //   Имя торгового сервера
       string            Currency(void)             const { return this.m_currency;           }  //   Валюта депозита
       string            Company(void)              const { return this.m_company;            }  //   Имя компании, обслуживающей счет
       
    //--- возвращает описание (1) аккаунта, (2) типа торгового счёта, (3) режима расчёта маржи
       string            Description(void)          const;
       string            TradeModeDescription(void) const;
       string            MarginModeDescription(void)const;
       
    //--- виртуальный метод сравнения двух объектов
       virtual int       Compare(const CObject *node,const int mode=0) const;
    
    //--- Выводит в журнал описание аккаунта
       void              Print(void)                      { ::Print(this.Description());      }
    
    //--- конструкторы/деструктор
                         CAccount(void){}
                         CAccount(const long login, const string server_name);
                        ~CAccount() {}
      };
    


    Vejamos a implementação dos métodos declarados.

    No construtor da classe, atribuiremos ao objeto todas as propriedades da conta atual:

    //+------------------------------------------------------------------+
    //| Конструктор                                                      |
    //+------------------------------------------------------------------+
    CAccount::CAccount(const long login, const string server_name)
      {
       this.m_login=login;
       this.m_server=server_name;
       
    //--- устанавливаем целочисленные свойства аккаунта
       this.m_trade_mode          = (ENUM_ACCOUNT_TRADE_MODE)::AccountInfoInteger(ACCOUNT_TRADE_MODE);    //   Тип торгового счета
       this.m_leverage            = ::AccountInfoInteger(ACCOUNT_LEVERAGE);                               //   Размер предоставленного плеча
       this.m_limit_orders        = (int)::AccountInfoInteger(ACCOUNT_LIMIT_ORDERS);                      //   Максимально допустимое количество действующих отложенных ордеров
       this.m_margin_so_mode      = (ENUM_ACCOUNT_STOPOUT_MODE)AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE);//   Режим задания минимально допустимого уровня залоговых средств
       this.m_trade_allowed       = ::AccountInfoInteger(ACCOUNT_TRADE_ALLOWED);                          //   Разрешенность торговли для текущего счета
       this.m_trade_expert        = ::AccountInfoInteger(ACCOUNT_TRADE_EXPERT);                           //   Разрешенность торговли для эксперта
       this.m_margin_mode         = (ENUM_ACCOUNT_MARGIN_MODE)::AccountInfoInteger(ACCOUNT_MARGIN_MODE);  //   Режим расчета маржи
       this.m_currency_digits     = (int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS);                   //   Количество знаков после запятой для валюты счета, необходимых для точного отображения торговых результатов
       this.m_fifo_close          = ::AccountInfoInteger(ACCOUNT_FIFO_CLOSE);                             //   Признак того, что позиции можно закрывать только по правилу FIFO
       this.m_hedge_allowed       = ::AccountInfoInteger(ACCOUNT_HEDGE_ALLOWED);                          //   Признак того, что разрешены встречные позиции по одному символу
        
    //--- устанавливаем вещественные свойства аккаунта
       this.m_balance             = ::AccountInfoDouble(ACCOUNT_BALANCE);                                 //   Баланс счета в валюте депозита
       this.m_credit              = ::AccountInfoDouble(ACCOUNT_CREDIT);                                  //   Размер предоставленного кредита в валюте депозита
       this.m_profit              = ::AccountInfoDouble(ACCOUNT_PROFIT);                                  //   Размер текущей прибыли на счете в валюте депозита
       this.m_equity              = ::AccountInfoDouble(ACCOUNT_EQUITY);                                  //   Значение собственных средств на счете в валюте депозита
       this.m_margin              = ::AccountInfoDouble(ACCOUNT_MARGIN);                                  //   Размер зарезервированных залоговых средств на счете  в валюте депозита
       this.m_margin_free         = ::AccountInfoDouble(ACCOUNT_MARGIN_FREE);                             //   Размер свободных средств на счете  в валюте депозита, доступных для открытия позиции
       this.m_margin_level        = ::AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);                            //   Уровень залоговых средств на счете в процентах
       this.m_margin_so_call      = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_CALL);                          //   Уровень залоговых средств, при котором требуется пополнение счета (Margin Call)
       this.m_margin_so_so        = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_SO);                            //   Уровень залоговых средств, при достижении которого происходит принудительное закрытие самой убыточной позиции (Stop Out)
       this.m_margin_initial      = ::AccountInfoDouble(ACCOUNT_MARGIN_INITIAL);                          //   Размер средств, зарезервированных на счёте, для обеспечения гарантийной суммы по всем отложенным ордерам 
       this.m_margin_maintenance  = ::AccountInfoDouble(ACCOUNT_MARGIN_MAINTENANCE);                      //   Размер средств, зарезервированных на счёте, для обеспечения минимальной суммы по всем открытым позициям
       this.m_assets              = ::AccountInfoDouble(ACCOUNT_ASSETS);                                  //   Текущий размер активов на счёте
       this.m_liabilities         = ::AccountInfoDouble(ACCOUNT_LIABILITIES);                             //   Текущий размер обязательств на счёте
       this.m_commission_blocked  = ::AccountInfoDouble(ACCOUNT_COMMISSION_BLOCKED);                      //   Текущая сумма заблокированных комиссий по счёту
      
    //--- устанавливаем строковые свойства аккаунта
       this.m_name                = ::AccountInfoString(ACCOUNT_NAME);                                    //   Имя клиента
       this.m_currency            = ::AccountInfoString(ACCOUNT_CURRENCY);                                //   Валюта депозита
       this.m_company             = ::AccountInfoString(ACCOUNT_COMPANY);                                 //   Имя компании, обслуживающей счет
      }
    


    O método de comparação entre dois objetos:

    //+------------------------------------------------------------------+
    //| Метод сравнения двух объектов                                    |
    //+------------------------------------------------------------------+
    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);
      }
    

    Esse método compara dois objetos de conta com base em duas propriedades — o login e o nome do servidor. Se os logins dos dois objetos comparados forem iguais, então o método verifica a igualdade do nome do servidor. Se ambos forem idênticos, significa que os dois objetos representam a mesma conta. Caso contrário, o método retorna 1 ou -1, dependendo da ordem dos valores da propriedade comparada.


    Os métodos que retornam descrições de algumas propriedades da conta:

    //+------------------------------------------------------------------+
    //| Возвращает описание типа торгового счёта                         |
    //+------------------------------------------------------------------+
    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;
      }
    //+------------------------------------------------------------------+
    //| Возвращает описание режима расчёта маржи                         |
    //+------------------------------------------------------------------+
    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;
      }
    

    Esses métodos são utilizados para compor a descrição da conta no método 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()));
      }
    

    O método retorna uma string no formato:

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

    Essa string pode ser impressa no diário do terminal utilizando o método Print() desta classe.

    Agora, precisamos criar a classe que armazenará as listas de todas as contas às quais houve conexão durante a execução do programa de serviço.


    Classe coleção de contas

    No diretório do terminal \MT5\MQL5\Services\AccountReporter, criaremos um novo arquivo Accounts.mqh, que conterá a classe CAccounts.
    A classe deve ser derivada da classe base CObject da Biblioteca Padrão, e o arquivo da classe de conta deve ser incluído no novo arquivo:

    //+------------------------------------------------------------------+
    //|                                                     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"
    
    //+------------------------------------------------------------------+
    //| Класс-коллекция аккаунтов                                        |
    //+------------------------------------------------------------------+
    class CAccounts : public CObject
      {
      }


    Nas seções privada, protegida e pública, declaramos os métodos para o funcionamento da classe:

    //+------------------------------------------------------------------+
    //| Класс-коллекция аккаунтов                                        |
    //+------------------------------------------------------------------+
    class CAccounts : public CObject
      {
    private:
       CArrayObj         m_list;           // Список объектов-аккаунтов
       CAccount          m_tmp;            // Временный объект-аккаунт для поиска
       
    protected:
    //--- Создаёт новый объект-аккаунт и добавляет его в список
       CAccount         *Add(const long login, const string server);
       
    public:
    //--- Создаёт новый объект-аккаунт
       bool              Create(const long login, const string server);
       
    //--- Возвращает указатель на указанный объект-аккаунт по (1) логину и серверу, (2) индексу в списке
       CAccount         *Get(const long login, const string server);
       CAccount         *Get(const int index)                         const { return this.m_list.At(index);  }
    
    //--- Объединяет списки позиций аккаунтов и возвращает общий
       CArrayObj        *GetCommonPositionsList(void);
    
    //--- Возвращает список позиций указанного аккаунта
       CArrayObj        *GetAccountPositionsList(const long login, const string server);
    
    //--- Возвращает количество хранимых аккаунтов
       int               Total(void)                                  const { return this.m_list.Total();    }
       
    //--- Обновляет списки позиций указанного аккаунта
       bool              PositionsRefresh(const long login, const string server);
    
    //--- Конструктор/деструктор
                         CAccounts();
                        ~CAccounts();
      };
    


    Vejamos a implementação dos métodos declarados.

    No construtor da classe, definimos o sinalizador de lista ordenada para a lista de contas:

    //+------------------------------------------------------------------+
    //| Конструктор                                                      |
    //+------------------------------------------------------------------+
    CAccounts::CAccounts()
      {
       this.m_list.Sort();
      }
    


    No destrutor da classe, limpamos a lista de contas:

    //+------------------------------------------------------------------+
    //| Деструктор                                                       |
    //+------------------------------------------------------------------+
    CAccounts::~CAccounts()
      {
       this.m_list.Clear();
      }
    


    O método protegido que cria um novo objeto de conta e o adiciona à lista:

    //+------------------------------------------------------------------+
    //| Создаёт новый объект-аккаунт и добавляет его в список            |
    //+------------------------------------------------------------------+
    CAccount *CAccounts::Add(const long login,const string server)
      {
    //--- Создаём новый объект-аккаунт
       CAccount *account=new CAccount(login, server);
       if(account==NULL)
          return NULL;
    //--- Если созданный объект не добавлен в список - удаляем его и возвращаем NULL
       if(!this.m_list.Add(account))
         {
          delete account;
          return NULL;
         }
    //--- Возвращаем указатель на созданный объект
       return account;
      }
    

    Este é um método protegido, que é chamado dentro do método público responsável por criar um novo objeto de conta:

    //+------------------------------------------------------------------+
    //| Создаёт новый объект-аккаунт                                     |
    //+------------------------------------------------------------------+
    bool CAccounts::Create(const long login,const string server)
      {
    //--- Во временный объект-аккаунт устанавливаем логин и сервер
       this.m_tmp.SetLogin(login);
       this.m_tmp.SetServer(server);
       
    //--- Списку объектов-аккаунтов устанавливаем флаг сортированного списка и
    //--- получаем индекс объекта в списке, имеющего те же логин и сервер, что и у временного объекта
       this.m_list.Sort();
       int index=this.m_list.Search(&this.m_tmp);
       
    //--- Возвращаем флаг успешного добавления объекта в список (результат работы метода Add), либо false, если объект в списке уже есть
       return(index==WRONG_VALUE ? this.Add(login, server)!=NULL : false);
      }
    


    Método que retorna um ponteiro para um objeto de conta específico:

    //+------------------------------------------------------------------+
    //| Возвращает указатель на указанный объект-аккаунт                 |
    //+------------------------------------------------------------------+
    CAccount *CAccounts::Get(const long login,const string server)
      {
    //--- Во временный объект-аккаунт устанавливаем логин и сервер
       this.m_tmp.SetLogin(login);
       this.m_tmp.SetServer(server);
       
    //--- Списку объектов-аккаунтов устанавливаем флаг сортированного списка и
    //--- получаем индекс объекта в списке, имеющего те же логин и сервер, что и у временного объекта
       this.m_list.Sort();
       int index=this.m_list.Search(&this.m_tmp);
    
    //--- Возвращаем указатель на объект в списке по индексу, либо NULL, если индекс равен -1
       return this.m_list.At(index);
      }
    


    Método que atualiza as listas de posições da conta especificada:

    //+------------------------------------------------------------------+
    //| Обновляет списки позиций указанного аккаунта                     |
    //+------------------------------------------------------------------+
    bool CAccounts::PositionsRefresh(const long login, const string server)
      {
    //--- Получаем указатель на объект-аккаунт с указанными логином и сервером
       CAccount *account=this.Get(login, server);
       if(account==NULL)
          return false;
    
    //--- Если полученный объект - не текущий аккаунт,
       if(account.Login()!=::AccountInfoInteger(ACCOUNT_LOGIN) || account.Server()!=::AccountInfoString(ACCOUNT_SERVER))
         {
          //--- сообщаем, что обновление данных не текущего аккаунта приведёт к некорректным данным и возвращаем false
          ::Print("Error. Updating the list of positions for a non-current account will result in incorrect data.");
          return false;
         }
    
    //--- Возвращаем результат обновления данных текущего аккаунта
       return account.PositionsRefresh();
      }
    


    Método que combina as listas de posições das contas e retorna uma lista geral:

    //+------------------------------------------------------------------+
    //| Объединяет списки позиций аккаунтов и возвращает общий           |
    //+------------------------------------------------------------------+
    CArrayObj *CAccounts::GetCommonPositionsList(void)
      {
    //--- Создаём новый список и сбрасываем для него флаг управления памятью
       CArrayObj *list=new CArrayObj();
       if(list==NULL)
          return NULL;
       list.FreeMode(false);
    
    //--- В цикле по списку аккаунтов
       int total=this.m_list.Total();
       for(int i=0; i<total; i++)
         {
          //--- получаем очередной объект-аккаунт
          CAccount *account=this.m_list.At(i);
          if(account==NULL)
             continue;
          
          //--- Получаем список закрытых позиций аккаунта
          CArrayObj *src=account.GetPositionsList();
          if(src==NULL)
             continue;
    
          //--- Если это первый аккаунт в списке
          if(i==0)
            {
             //--- копируем в новый список элементы из списка позиций аккаунта
             if(!list.AssignArray(src))
               {
                delete list;
                return NULL;
               }
            }
          //--- Если это не первый аккаунт в списке
          else
            {
             //--- добавляем в конец нового списка элементы из списка позиций аккаунта
             if(!list.AddArray(src))
                continue;
            }
         }
       
    //--- Отправляем новый список в хранилище
       if(!ListStorage.Add(list))
         {
          delete list;
          return NULL;
         }
         
    //--- Возвращаем указатель на созданный и заполненный список
       return list;
      }
    


    Método que retorna a lista de posições da conta especificada:

    //+------------------------------------------------------------------+
    //| Возвращает список позиций указанного аккаунта                    |
    //+------------------------------------------------------------------+
    CArrayObj *CAccounts::GetAccountPositionsList(const long login,const string server)
      {
       CAccount *account=this.Get(login, server);
       return(account!=NULL ? account.GetPositionsList() : NULL);
      }
    

    Obtemos o ponteiro para o objeto da conta com base no login e no servidor, retornamos o ponteiro para sua lista de posições históricas, ou NULL caso a conta não seja encontrada.

    Todos os métodos dessa classe estão detalhadamente explicados nos comentários do código. Se houver dúvidas, elas podem ser esclarecidas na seção de discussão do artigo.

    Agora, todas as classes fundamentais para a construção do programa de serviço estão prontas. Vamos começar a implementação do próprio programa.


    Programa de serviço para criação de relatórios de trading e envio de notificações

    Definimos o comportamento do programa.

    Ao iniciar o serviço, será verificada a presença do MetaQuotes ID no terminal cliente e a permissão para envio de notificações Push para o smartphone.

    Essas configurações podem ser encontradas no menu "Tools → Options", na aba "Notifications":


    Se o campo MetaQuotes ID estiver vazio ou a opção Enable Push notifications não estiver ativada, o serviço exibirá uma janela solicitando a ativação dessas configurações. Caso o usuário recuse a configuração desses parâmetros, será exibido um aviso informando que o MetaQuotes ID está ausente ou que o envio de notificações Push não está permitido, e que todas as mensagens serão registradas apenas no diário do terminal. Se todas as configurações forem ativadas corretamente, os relatórios serão enviados tanto para o smartphone quanto para o diário do terminal na aba "Especialistas". No laço principal, o programa verificará constantemente o estado das configurações de envio de notificações no terminal. Portanto, se no momento da inicialização do serviço a permissão para envio de notificações não estiver ativada, será possível ativá-la posteriormente durante a execução do programa. O serviço detectará a alteração e atualizará automaticamente seu sinalizador interno.

    Nas configurações do serviço, o usuário poderá definir os parâmetros de envio de mensagens e escolher os períodos de tempo para os quais os relatórios devem ser gerados.

    • Parâmetros gerais dos relatórios
      • Quais contas usar para os relatórios: (todas ou apenas a atual).
      • Criar relatórios separados por símbolos: (sim/não) — primeiro, um relatório geral é gerado e, a partir dele, são criados relatórios individuais para cada símbolo negociado.
      • Criar relatórios separados por Magic Number: (sim/não) — primeiro, um relatório geral é gerado e, a partir dele, são criados relatórios individuais para cada Magic Number utilizado no trading.
      • Incluir comissões nos relatórios: (sim/não) — se ativado, além do total de custos, serão exibidos separadamente os valores referentes às comissões, swaps e taxas de execução de operações.
      • Incluir possíveis perdas com spread no fechamento das posições: (sim/não) — se ativado, será exibido separadamente o custo total estimado das perdas de spread no fechamento das posições.
    • Configurações dos relatórios diários
      • Enviar relatórios das últimas 24 horas: (sim/não) — essa opção também se aplica aos relatórios de períodos personalizados. Se ativado, todos os dias, no horário configurado, serão enviados relatórios das últimas 24 horas e para os períodos de tempo especificados (dias, meses e anos).
      • Horário de envio do relatório: (padrão: 08:00).
      • Minutos de envio do relatório: (padrão: 00).
    • Configurações dos relatórios diários para períodos personalizados
      • Enviar relatórios de um número específico de dias: (sim/não) — se ativado, todos os dias, no horário configurado, serão gerados relatórios para um período de X dias. O período é calculado subtraindo o número de dias especificado da data atual.
      • Número de dias para os relatórios de período personalizado: (padrão: 7).
      • Enviar relatórios de um número específico de meses: (sim/não) — se ativado, todos os dias, no horário configurado, serão gerados relatórios para um período de X meses. O período é calculado subtraindo o número de meses especificado da data atual.
      • Número de meses para os relatórios de período personalizado: (padrão: 3).
      • Enviar relatórios de um número específico de anos: (sim/não) — se ativado, todos os dias, no horário configurado, serão gerados relatórios para um período de X anos. O período é calculado subtraindo o número de anos especificado da data atual.
      • Número de anos para os relatórios de período personalizado: (padrão: 2).
    • Configurações dos relatórios semanais para todos os períodos adicionais
      • Dia da semana para o envio dos relatórios semanais: (padrão: sábado) — quando o dia especificado chegar, serão gerados e enviados os relatórios configurados abaixo.
      • Hora de envio dos relatórios: (padrão: 08:00).
      • Minutos de envio dos relatórios: (padrão: 00).
      • Enviar relatório do período desde o início da semana atual: (sim/não) — se ativado, semanalmente, no dia especificado, será gerado um relatório para o período desde o início da semana atual.
      • Enviar relatório do período desde o início do mês atual: (sim/não) — se ativado, semanalmente, no dia especificado, será gerado um relatório para o período desde o início do mês atual.
      • Enviar relatório do período desde o início do ano atual: (sim/não) — se ativado, semanalmente, no dia especificado, será gerado um relatório para o período desde o início do ano atual.
      • Enviar relatório para todo o período de trading: (sim/não) — se ativado, semanalmente, no dia especificado, será gerado um relatório para todo o período de trading registrado.


    Essas configurações são suficientes para abranger a maioria dos períodos relevantes para a geração de relatórios detalhados.

    No diretório do terminal \MQL5\Services\AccountReporter, criaremos um novo arquivo Reporter.mq5, que conterá o código do programa de serviço.



    Agora, definimos as macros necessárias, incluímos os arquivos externos, criamos as enumerações, os parâmetros de entrada e as variáveis globais que serão utilizadas no funcionamento do programa:

    //+------------------------------------------------------------------+
    //|                                                     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                                   // Задержка счётчика в миллисекундах в рабочем цикле
    #define   REFRESH_ATTEMPTS    5                                      // Количество попыток получения корректных данных аккаунта 
    #define   REFRESH_DELAY       500                                    // Задержка в миллисекундах перед очередной попыткой получения данных
    #define   TABLE_COLUMN_W      10                                     // Ширина колонки таблицы статистики для вывода в журнал
    
    #include <Arrays\ArrayString.mqh>                                    // Динамический массив переменных типа string для объекта списка символов
    #include <Arrays\ArrayLong.mqh>                                      // Динамический массив переменных типа long для объекта списка магиков
    #include <Tools\DateTime.mqh>                                        // Расширение структуры MqlDateTime
    #include "Accounts.mqh"                                              // Класс-коллекция объектов-аккаунтов
    
    //+------------------------------------------------------------------+
    //| Перечисления                                                     |
    //+------------------------------------------------------------------+
    enum ENUM_USED_ACCOUNTS                                              // Перечисление используемых аккаунтов в статистике
      {
       USED_ACCOUNT_CURRENT,                                             // Current Account only
       USED_ACCOUNTS_ALL,                                                // All used accounts
      };
    
    enum ENUM_REPORT_RANGE                                               // Перечисление диапазонов статистики
      {
       REPORT_RANGE_DAILY,                                               // Сутки
       REPORT_RANGE_WEEK_BEGIN,                                          // С начала недели
       REPORT_RANGE_MONTH_BEGIN,                                         // С начала месяца
       REPORT_RANGE_YEAR_BEGIN,                                          // С начала года
       REPORT_RANGE_NUM_DAYS,                                            // Количество дней
       REPORT_RANGE_NUM_MONTHS,                                          // Количество месяцев
       REPORT_RANGE_NUM_YEARS,                                           // Количество лет
       REPORT_RANGE_ALL,                                                 // Весь период
      };
      
    enum ENUM_REPORT_BY                                                  // Перечисление фильтров статистики
      {
       REPORT_BY_RANGE,                                                  // Диапазон дат
       REPORT_BY_SYMBOLS,                                                // По символам
       REPORT_BY_MAGICS,                                                 // По магикам
      };
      
    //+------------------------------------------------------------------+
    //| Входные параметры                                                |
    //+------------------------------------------------------------------+
    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
    
    //+------------------------------------------------------------------+
    //| Глобальные переменные                                            |
    //+------------------------------------------------------------------+
    CAccounts      ExtAccounts;                                          // Объект управления аккаунтами
    long           ExtLogin;                                             // Логин текущего аккаунта
    string         ExtServer;                                            // Сервер текущего аккаунта
    bool           ExtNotify;                                            // Флаг разрешения Push-уведомлений
    
    //+------------------------------------------------------------------+
    //| Service program start function                                   |
    //+------------------------------------------------------------------+
    void OnStart()
      {
      }
    


    Vemos que o arquivo \MQL5\Include\ToolsDateTime.mqh está incluído. Esse arquivo contém uma estrutura herdada de 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);
      };
    

    Essa estrutura já contém métodos prontos para manipulação de datas e horários. Precisamos calcular o início do período estatístico, e para evitar erros manuais no cálculo de datas ao subtrair um número de dias, semanas, meses ou anos da data atual, utilizamos os métodos dessa estrutura. Esses cálculos já incluem a correção automática de valores inválidos. Por exemplo, se subtrairmos mais dias do que existem no mês atual, a data resultante precisará ser corrigida automaticamente — será necessário calcular qual deve ser o mês correto e qual deve ser o dia correspondente, levando em consideração anos bissextos. Em vez de lidar com esses ajustes manualmente, utilizamos os métodos dessa estrutura, que já fornecem diretamente a data final correta.

    O programa de serviço deve operar em um laço infinito. Dentro do laço, implementamos um atraso de aproximadamente um segundo e, após essa espera, todas as verificações e cálculos são processados. Para tornar o código mais legível e organizado, todo o corpo do laço é dividido em blocos nomeados. Agora, analisamos a estrutura principal do programa:

    //+------------------------------------------------------------------+
    //| Service program start function                                   |
    //+------------------------------------------------------------------+
    void OnStart()
      {
    //---
       CArrayObj  *PositionsList  =  NULL;          // Список закрытых позиций аккаунтов
       long        account_prev   =  0;             // Прошлый логин
       double      balance_prev   =  EMPTY_VALUE;   // Прошлый баланс
       bool        Sent           =  false;         // Флаг отправленного отчёта за не дневные периоды
       int         day_of_year_prev= WRONG_VALUE;   // Прошлый номер дня в году
       
    //--- Создаём списки торгуемых в истории символов и магиков и список сообщений для Push-уведомлений
       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;
         }
         
    //--- Проверяем наличие MetaQuotes ID и разрешение отправки на него уведомлений
       ExtNotify=CheckMQID();
       if(ExtNotify)
          Print(MQLInfoString(MQL_PROGRAM_NAME)+"-Service notifications OK");
    
    //--- Основной цикл
       int count=0;
       while(!IsStopped())
         {
          //+------------------------------------------------------------------+
          //| Задержка в цикле                                                 |
          //+------------------------------------------------------------------+
          //--- Увеличиваем счётчик цикла. Если счётчик не превысил заданного значения - повторяем
          Sleep(16);
          count+=10;
          if(count<COUNTER_DELAY)
             continue;
          //--- Ожидание завершено. Сбрасываем счётчик цикла
          count=0;
          
          //+------------------------------------------------------------------+
          //| Проверка настроек уведомлений                                    |
          //+------------------------------------------------------------------+
          //--- Если флаг уведомлений не установлен - проверяем настройки уведомлений в терминале и, если активированы - сообщаем об этом
          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(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;
            }
          
          //+------------------------------------------------------------------+
          //| Смена аккаунта                                                   |
          //+------------------------------------------------------------------+
          //--- Если текущий логин не равен предыдущему
          if(AccountInfoInteger(ACCOUNT_LOGIN)!=account_prev)
            {
             //--- если не дождались обновления данных аккаунта - повторим на следующей итерации цикла
             if(!DataUpdateWait(balance_prev))
                continue;
             
             //--- Получены данные нового аккаунта
             //--- Сохраним текущие логин и баланс как предыдущие для следующей проверки
             account_prev=AccountInfoInteger(ACCOUNT_LOGIN);
             balance_prev=AccountInfoDouble(ACCOUNT_BALANCE);
             
             //--- Сбросим флаг отправленного сообщения и вызовем обработчик смены аккаунта
             Sent=false;
             AccountChangeHandler();
            }
          
          //+------------------------------------------------------------------+
          //| Ежедневные отчёты                                                |
          //+------------------------------------------------------------------+
          //--- Заполним структуру данными о локальном времени и дате
          MqlDateTime tm={};
          TimeLocal(tm);
          
          //--- Очистим список сообщений, отправляемых на MQID
          MessageList.Clear();
          
          //--- Если текущий номер дня в году не равен прошлому - это начало нового дня
          if(tm.day_of_year!=day_of_year_prev)
            {
             //--- Если часы/минуты достигли заданных значений для отправки статистики
             if(tm.hour>=(int)InpSendDReportHour && tm.min>=(int)InpSendDReportMin)
               {
                //--- Если разрешена отправка ежедневной статистики
                if(InpSendDReport)
                  {
                   //--- обновляем списки закрытых позиций за сутки на текущем аккаунте
                   ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
                   //--- если в настройках задано получение статистики со всех аккаунтов -
                   //--- получаем список закрытых позиций всех аккаунтов, бывших активными при работе сервиса
                   if(InpUsedAccounts==USED_ACCOUNTS_ALL)
                      PositionsList=ExtAccounts.GetCommonPositionsList();
                   //--- иначе - получаем список закрытых позиций только текущего на данный момент аккаунта
                   else
                      PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer);
                   
                   //--- Создаём сообщения о торговой статистике за дневной диапазон времени,
                   //--- распечатываем созданные сообщения в журнал и отправляем их на MQID
                   SendReport(REPORT_RANGE_DAILY, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- Если в настройках разрешена отправка торговой статистики за указанное количество дней,
                   //--- Создаём сообщения о торговой статистике за количество дней в InpSendSReportDaysN,
                   //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                   if(InpSendSReportDays)
                      SendReport(REPORT_RANGE_NUM_DAYS, InpSendSReportDaysN, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- Если в настройках разрешена отправка торговой статистики за указанное количество месяцев,
                   //--- Создаём сообщения о торговой статистике за количество месяцев в InpSendSReportMonthsN,
                   //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                   if(InpSendSReportMonths)
                      SendReport(REPORT_RANGE_NUM_MONTHS, InpSendSReportMonthsN, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- Если в настройках разрешена отправка торговой статистики за указанное количество лет,
                   //--- Создаём сообщения о торговой статистике за количество лет в InpSendSReportYearN,
                   //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                   if(InpSendSReportYears)
                      SendReport(REPORT_RANGE_NUM_YEARS, InpSendSReportYearN, PositionsList, SymbolsList, MagicsList, MessageList);
                  }
    
                //--- Записываем текущий день как прошлый для последующей проверки
                day_of_year_prev=tm.day_of_year;
               }
            }
            
          //+------------------------------------------------------------------+
          //| Еженедельные отчёты                                              |
          //+------------------------------------------------------------------+
          //--- Если день недели равен устанорвленному в настройках,
          if(tm.day_of_week==InpSendWReportDayWeek)
            {
             //--- если сообщение ещё не отправлено и наступило время отправки сообщений
             if(!Sent && tm.hour>=(int)InpSendWReportHour && tm.min>=(int)InpSendWReportMin)
               {
                //--- обновляем списки закрытых позиций на текущем аккаунте
                ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
                
                //--- если в настройках задано получение статистики со всех аккаунтов -
                //--- получаем список закрытых позиций всех аккаунтов, бывших активными при работе сервиса
                if(InpUsedAccounts==USED_ACCOUNTS_ALL)
                   PositionsList=ExtAccounts.GetCommonPositionsList();
                //--- иначе  -получаем список закрытых позиций только текущего на данный момент аккаунта
                else
                   PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer);
                
                //--- Если в настройках разрешена отправка торговой статистики за неделю,
                //--- Создаём сообщения о торговой статистике с начала текущей недели,
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendWReport)
                   SendReport(REPORT_RANGE_WEEK_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Если в настройках разрешена отправка торговой статистики за месяц,
                //--- Создаём сообщения о торговой статистике с начала текущего месяца,
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendMReport)
                   SendReport(REPORT_RANGE_MONTH_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Если в настройках разрешена отправка торговой статистики за год,
                //--- Создаём сообщения о торговой статистике с начала текущго года,
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendYReport)
                   SendReport(REPORT_RANGE_YEAR_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Если в настройках разрешена отправка торговой статистики за весь период,
                //--- Создаём сообщения о торговой статистике с начала эпохи (01.01.1970 00:00),
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendAReport)
                   SendReport(REPORT_RANGE_ALL, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Устанавливаем флаг, что все сообщения со статистикой в журнал распечатаны
                Sent=true;
               }
            }
          //--- Если ещё не наступил указанный в настройках день недели для отправки статистики - сбрасываем флаг отправленных сообщений
          else
             Sent=false;
    
          //--- Если список сообщений для отправки на MQID не пустой - вызываем функцию отправки уведомлений на смартфон
          if(MessageList.Total()>0)
             SendMessage(MessageList);
         }
         
       //+------------------------------------------------------------------+
       //| Завершение работы сервиса                                        |
       //+------------------------------------------------------------------+
       //--- Очищаем и удаляем списки сообщений, символов и магиков
       if(MessageList!=NULL)
         {
          MessageList.Clear();
          delete MessageList;
         }
       if(SymbolsList!=NULL)
         {
          SymbolsList.Clear();
          delete SymbolsList;
         }
       if(MagicsList!=NULL)
         {
          MagicsList.Clear();
          delete MagicsList;
         }
      }
    

    Podemos ver que, ao iniciar o serviço, o programa verifica se as permissões para envio de notificações para o smartphone estão ativadas no terminal. Para isso, ele chama a função CheckMQID(), que verifica cada uma das configurações necessárias e solicita ao usuário a ativação dos parâmetros obrigatórios dentro das configurações do terminal cliente:

    //+------------------------------------------------------------------+
    //| Проверяет наличие в терминале MetaQuotes ID                      |
    //| и разрешение отправки уведомлений на мобильный терминал          |
    //+------------------------------------------------------------------+
    bool CheckMQID(void)
      {
       string caption=MQLInfoString(MQL_PROGRAM_NAME); // Заголовок окна сообщений
       string message=caption+"-Service OK";           // Текст окна сообщений
       int    mb_id=IDOK;                              // Код возврата MessageBox()
       
    //--- Если в настройках терминала не установлен MQID - сделаем запрос на его установку с пояснениями о порядке действий
       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);
         }
         
    //--- Если нажата кнопка "Cancel" - сообщим об отказе от использования Push-уведомлений
       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);
         }
         
    //--- Если нажата кнопка "Retry" - 
       else
         {
          //--- Если в терминале установлен MetaQuotes ID для отправки Push-уведомлений
          if(TerminalInfoInteger(TERMINAL_MQID))
            {
             //--- если в терминале отсутствует разрешение на отправку уведомлений на смартфон
             if(!TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
               {
                //--- показываем сообщение с просьбой дать разрешение на отправку уведомлений в настройках
                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);
                
                //--- Если в ответ на сообщение нажата кнопка Cancel
                if(mb_id==IDCANCEL)
                  {
                   //--- сообщаем об отказе от отправки уведомлений на смартфон
                   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);
                  }
                //--- Если в ответ на сообщение нажата кнопка Retry (ожидается, что сделано это будет после включения разрешения в настройках),
                //--- но разрешения на отправку уведомлений в терминале так и нет,
                if(mb_id==IDRETRY && !TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
                  {
                   //--- сообщаем, что пользователь отказался от отправки уведомлений на смартфон, и сообщения будут только в журнале
                   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);
                  }
               }
            }
          //--- Если в терминале не установлен MetaQuotes ID для отправки Push-уведомлений
          else
            {
             //--- сообщаем, что в терминале не установлен MetaQuotes ID для отправки уведомлений на смартфон, и сообщения будут только в журнале
             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);
            }
         }
    //--- Возвращаем флаг, что MetaQuotes ID в терминале установлен и отправка уведомлений разрешена
       return(TerminalInfoInteger(TERMINAL_MQID) && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED));
      }
    


    Após a execução da função apresentada anteriormente, o laço principal do serviço é iniciado. A primeira verificação feita no laço é o status da permissão de envio de notificações, tanto no próprio programa quanto nas configurações do terminal:

          //+------------------------------------------------------------------+
          //| Проверка настроек уведомлений                                    |
          //+------------------------------------------------------------------+
          //--- Если флаг уведомлений не установлен - проверяем настройки уведомлений в терминале и, если активированы - сообщаем об этом
          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(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;
            }
    

    Se houver alterações nas configurações do terminal, o serviço emitirá avisos apropriados. Se o envio de notificações estava ativado e foi desativado, o serviço informará que há restrições no envio de notificações. Se, ao contrário, a opção estava desativada e o usuário a ativou, o serviço confirmará a ativação da função e enviará uma notificação ao smartphone.

    Em seguida, o laço verifica se houve mudança de conta:

          //+------------------------------------------------------------------+
          //| Смена аккаунта                                                   |
          //+------------------------------------------------------------------+
          //--- Если текущий логин не равен предыдущему
          if(AccountInfoInteger(ACCOUNT_LOGIN)!=account_prev)
            {
             //--- если не дождались обновления данных аккаунта - повторим на следующей итерации цикла
             if(!DataUpdateWait(balance_prev))
                continue;
             
             //--- Получены данные нового аккаунта
             //--- Сохраним текущие логин и баланс как предыдущие для следующей проверки
             account_prev=AccountInfoInteger(ACCOUNT_LOGIN);
             balance_prev=AccountInfoDouble(ACCOUNT_BALANCE);
             
             //--- Сбросим флаг отправленного сообщения и вызовем обработчик смены аккаунта
             Sent=false;
             AccountChangeHandler();
            }
    

    Assim que o login for alterado e se tornar diferente do valor anteriormente armazenado, o serviço chama uma função de espera para o carregamento dos dados atualizados da conta:

    //+------------------------------------------------------------------+
    //| Ожидает обновления данных аккаунта                               |
    //+------------------------------------------------------------------+
    bool DataUpdateWait(double &balance_prev)
      {
       int attempts=0;   // Количество попыток
     
    //--- До тех пор пока снят флаг остановки программы и пока количество попыток меньше установленного в REFRESH_ATTEMPTS
       while(!IsStopped() && attempts<REFRESH_ATTEMPTS)
         {
          //--- Если баланс текущего аккаунта отличается от баланса ранее сохранённого значения баланса,
          //--- считаем, что данные аккаунта получить удалось - возвращаем true
          if(NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE)-balance_prev, 8)!=0)
             return true;
          
          //--- Ожидаем полсекунды для следующей попытки, увеличиваем количество попыток и
          //--- выводим в журнал сообщение об ожидании получения данных и количестве попыток
          Sleep(500);
          attempts++;
          PrintFormat("%s::%s: Waiting for account information to update. Attempt %d", MQLInfoString(MQL_PROGRAM_NAME),__FUNCTION__, attempts);
         }
         
    //--- Если по истечении всех попыток получить данные нового аккаунта не удалось,
    //--- сообщаем об этом в журнал, записываем в "прошлый баланс" пустое значение и возвращаем false
       PrintFormat("%s::%s: Could not wait for updated account data... Try again", MQLInfoString(MQL_PROGRAM_NAME),__FUNCTION__);
       balance_prev=EMPTY_VALUE;
       return false;
      }
    

    Essa função aguarda até que os dados de saldo da conta sejam atualizados, pois, provavelmente, a nova conta possui um saldo diferente da anterior. A função realiza um número predefinido de tentativas para verificar a diferença entre o saldo da conta anterior e o saldo da nova conta. Se a tentativa falhar (ou os saldos forem iguais), o serviço atribuirá o valor EMPTY_VALUE ao saldo anterior. Na próxima iteração do laço, o programa verificará se os novos dados da conta foram corretamente atualizados, comparando-os com esse novo valor, que certamente não pode existir como saldo real.

    Após essa verificação, o laço realiza o controle de data e horário para a geração de relatórios diários e semanais:

          //+------------------------------------------------------------------+
          //| Ежедневные отчёты                                                |
          //+------------------------------------------------------------------+
          //--- Заполним структуру данными о локальном времени и дате
          MqlDateTime tm={};
          TimeLocal(tm);
          
          //--- Очистим список сообщений, отправляемых на MQID
          MessageList.Clear();
          
          //--- Если текущий номер дня в году не равен прошлому - это начало нового дня
          if(tm.day_of_year!=day_of_year_prev)
            {
             //--- Если часы/минуты достигли заданных значений для отправки статистики
             if(tm.hour>=(int)InpSendDReportHour && tm.min>=(int)InpSendDReportMin)
               {
                //--- Если разрешена отправка ежедневной статистики
                if(InpSendDReport)
                  {
                   //--- обновляем списки закрытых позиций за сутки на текущем аккаунте
                   ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
                   //--- если в настройках задано получение статистики со всех аккаунтов -
                   //--- получаем список закрытых позиций всех аккаунтов, бывших активными при работе сервиса
                   if(InpUsedAccounts==USED_ACCOUNTS_ALL)
                      PositionsList=ExtAccounts.GetCommonPositionsList();
                   //--- иначе - получаем список закрытых позиций только текущего на данный момент аккаунта
                   else
                      PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer);
                   
                   //--- Создаём сообщения о торговой статистике за дневной диапазон времени,
                   //--- распечатываем созданные сообщения в журнал и отправляем их на MQID
                   SendReport(REPORT_RANGE_DAILY, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- Если в настройках разрешена отправка торговой статистики за указанное количество дней,
                   //--- Создаём сообщения о торговой статистике за количество дней в InpSendSReportDaysN,
                   //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                   if(InpSendSReportDays)
                      SendReport(REPORT_RANGE_NUM_DAYS, InpSendSReportDaysN, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- Если в настройках разрешена отправка торговой статистики за указанное количество месяцев,
                   //--- Создаём сообщения о торговой статистике за количество месяцев в InpSendSReportMonthsN,
                   //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                   if(InpSendSReportMonths)
                      SendReport(REPORT_RANGE_NUM_MONTHS, InpSendSReportMonthsN, PositionsList, SymbolsList, MagicsList, MessageList);
                   
                   //--- Если в настройках разрешена отправка торговой статистики за указанное количество лет,
                   //--- Создаём сообщения о торговой статистике за количество лет в InpSendSReportYearN,
                   //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                   if(InpSendSReportYears)
                      SendReport(REPORT_RANGE_NUM_YEARS, InpSendSReportYearN, PositionsList, SymbolsList, MagicsList, MessageList);
                  }
    
                //--- Записываем текущий день как прошлый для последующей проверки
                day_of_year_prev=tm.day_of_year;
               }
            }
            
          //+------------------------------------------------------------------+
          //| Еженедельные отчёты                                              |
          //+------------------------------------------------------------------+
          //--- Если день недели равен устанорвленному в настройках,
          if(tm.day_of_week==InpSendWReportDayWeek)
            {
             //--- если сообщение ещё не отправлено и наступило время отправки сообщений
             if(!Sent && tm.hour>=(int)InpSendWReportHour && tm.min>=(int)InpSendWReportMin)
               {
                //--- обновляем списки закрытых позиций на текущем аккаунте
                ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
                
                //--- если в настройках задано получение статистики со всех аккаунтов -
                //--- получаем список закрытых позиций всех аккаунтов, бывших активными при работе сервиса
                if(InpUsedAccounts==USED_ACCOUNTS_ALL)
                   PositionsList=ExtAccounts.GetCommonPositionsList();
                //--- иначе  -получаем список закрытых позиций только текущего на данный момент аккаунта
                else
                   PositionsList=ExtAccounts.GetAccountPositionsList(ExtLogin, ExtServer);
                
                //--- Если в настройках разрешена отправка торговой статистики за неделю,
                //--- Создаём сообщения о торговой статистике с начала текущей недели,
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendWReport)
                   SendReport(REPORT_RANGE_WEEK_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Если в настройках разрешена отправка торговой статистики за месяц,
                //--- Создаём сообщения о торговой статистике с начала текущего месяца,
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendMReport)
                   SendReport(REPORT_RANGE_MONTH_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Если в настройках разрешена отправка торговой статистики за год,
                //--- Создаём сообщения о торговой статистике с начала текущго года,
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendYReport)
                   SendReport(REPORT_RANGE_YEAR_BEGIN, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Если в настройках разрешена отправка торговой статистики за весь период,
                //--- Создаём сообщения о торговой статистике с начала эпохи (01.01.1970 00:00),
                //--- распечатываем созданные сообщения в журнал и добавляем их в список для отправки на MQID
                if(InpSendAReport)
                   SendReport(REPORT_RANGE_ALL, 0, PositionsList, SymbolsList, MagicsList, MessageList);
                
                //--- Устанавливаем флаг, что все сообщения со статистикой в журнал распечатаны
                Sent=true;
               }
            }
          //--- Если ещё не наступил указанный в настройках день недели для отправки статистики - сбрасываем флаг отправленных сообщений
          else
             Sent=false;
    
          //--- Если список сообщений для отправки на MQID не пустой - вызываем функцию отправки уведомлений на смартфон
          if(MessageList.Total()>0)
             SendMessage(MessageList);
    

    Toda essa lógica está comentada no código-fonte. É importante observar que não é possível enviar notificações Push imediatamente após sua criação no laço. Isso ocorre porque, dependendo das configurações do serviço, vários relatórios podem ser gerados em um curto período de tempo, mas há restrições rigorosas para o envio de notificações Push: No máximo 2 mensagens por segundo. No máximo 10 mensagens por minuto. Para contornar essa limitação, todas as mensagens geradas são armazenadas em uma lista CArrayString da Biblioteca Padrão. Após a criação de todos os relatórios, o serviço verifica se essa lista contém mensagens pendentes. Se sim, ele chama uma função de envio de notificações, que aplica os atrasos necessários para garantir que as notificações sejam enviadas sem violação das restrições estabelecidas.

    Agora, analisamos todas as funções utilizadas no programa de serviço.

    Função que retorna uma lista com o intervalo de estatísticas especificado:

    //+------------------------------------------------------------------+
    //| Возвращает список с указанным диапазоном статистики              |
    //+------------------------------------------------------------------+
    CArrayObj *GetListDataRange(ENUM_REPORT_RANGE range, CArrayObj *list, datetime &time_start, const int num_periods)
      {
    //--- Текущая дата
       CDateTime current={};
       current.Date(TimeLocal());
       
    //--- Дата начала периода
       CDateTime begin_range=current;
       
    //--- Устанавливаем время начала периода в значение 00:00:00
       begin_range.Hour(0);
       begin_range.Min(0);
       begin_range.Sec(0);
       
    //--- В зависимости от указанного периода требуемой статистики, корректируем дату начала периода
       switch(range)
         {
          //--- Сутки
          case REPORT_RANGE_DAILY       :  // уменьшаем значение День на 1
            begin_range.DayDec(1);       
            break;
            
          //--- С начала недели
          case REPORT_RANGE_WEEK_BEGIN  :  // уменьшаем значение День на (количество прошедших дней в неделе)-1
            begin_range.DayDec(begin_range.day_of_week==SUNDAY ? 6 : begin_range.day_of_week-1);
            break;
            
          //--- С начала месяца
          case REPORT_RANGE_MONTH_BEGIN :  // устанавливаем в значение День первое число месяца
            begin_range.Day(1);
            break;
            
          //--- С начала года
          case REPORT_RANGE_YEAR_BEGIN  :  // устанавливаем в значение Месяц первый месяц в году, а в значение День первое число месяца
            begin_range.Mon(1);
            begin_range.Day(1);
            break;
            
          //--- Количество дней
          case REPORT_RANGE_NUM_DAYS    :  // Уменьшаем значение День на указанное количество дней
            begin_range.DayDec(fabs(num_periods));
            break;
            
          //--- Количество месяцев
          case REPORT_RANGE_NUM_MONTHS  :  // Уменьшаем значение Месяц на указанное количество месяцев
            begin_range.MonDec(fabs(num_periods));
            break;
            
          //--- Количество лет
          case REPORT_RANGE_NUM_YEARS   :  // Уменьшаем значение Год на указанное количество лет
            begin_range.YearDec(fabs(num_periods));
            break;
            
          //---REPORT_RANGE_ALL Весь период
          default                       :  // Устанавливаем дату 1970.01.01
            begin_range.Year(1970);
            begin_range.Mon(1);
            begin_range.Day(1);
            break;
         }
         
    //--- Записываем дату начала периода и возвращаем указатель на список позиций,
    //--- время открытия которых больше, либо равно времени начала запрошенного периода
       time_start=begin_range.DateTime();
       return CSelect::ByPositionProperty(list,POSITION_PROP_TIME,time_start,EQUAL_OR_MORE);
      }
    

    A função recebe como parâmetro a indicação do intervalo estatístico que será utilizado (últimas 24 horas, desde o início da semana, mês, ano, período personalizado de dias, meses, anos ou todo o período de trading) e a lista de posições fechadas que será filtrada pela data inicial do intervalo. Dependendo do intervalo de estatísticas selecionado, a data inicial do período é ajustada e o programa retorna a lista de posições fechadas a partir dessa data calculada.


    A função de manipulação da troca de conta:

    //+------------------------------------------------------------------+
    //| Обработчик смены аккаунта                                        |
    //+------------------------------------------------------------------+
    void AccountChangeHandler(void)
      {
    //--- Записываем логин и сервер текущего аккаунта
       long   login  = AccountInfoInteger(ACCOUNT_LOGIN);
       string server = AccountInfoString(ACCOUNT_SERVER);
       
    //--- Получаем указатель на объект-аккаунт по данным текущего аккаунта
       CAccount *account = ExtAccounts.Get(login, server);
       
    //--- Если объект пустой - создаём новый объект-аккаунт и получаем указатель на него
       if(account==NULL && ExtAccounts.Create(login, server))
          account=ExtAccounts.Get(login, server);
       
    //--- Если в итоге объект-аккаунт не получен - сообщаем об этом и уходим
       if(account==NULL)
         {
          PrintFormat("Error getting access to account object: %I64d (%s)", login, server);
          return;
         }
       
    //--- Записываем текущие значения логина и сервера из данных объекта-аккаунта
       ExtLogin =account.Login();
       ExtServer=account.Server();
       
    //--- Распечатываем данные аккаунта в журнал и выводим сообщение о начале создания списка закрытых позиций
       account.Print();
       Print("Beginning to create a list of closed positions...");
    
    //--- Создаём список закрытых позиций и по завершении процесса сообщаем в журнал количество созданных позиций и затраченное время
       ulong start=GetTickCount();
       ExtAccounts.PositionsRefresh(ExtLogin, ExtServer);
       PrintFormat("A list of %d positions was created in %I64u ms", account.PositionsTotal(), GetTickCount()-start);
      }
    

    O manipulador cria um novo objeto de conta, caso essa conta ainda não tenha sido utilizada. Se a conta já foi acessada anteriormente, o programa obtém o ponteiro para o objeto de conta existente. Em seguida, é iniciado o processo de criação da lista de posições fechadas dessa conta. Mensagens são registradas no diário do terminal para indicar o início da criação da lista de posições históricas, seu conclusão e o tempo em milissegundos que o processo levou.


    Função que gera a estatística para o período especificado:

    //+------------------------------------------------------------------+
    //| Создаёт статистику за указанный диапазон времени                 |
    //+------------------------------------------------------------------+
    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};    // Массив сообщений (0) для выводла в журнал, (1) для отправки на смартфон
       datetime time_start = 0;               // Здесь будем хранить время начала периода статистики
       CArrayObj *list_tmp = NULL;            // Временный список для фильтрации по символам и магикам
       
    //--- Получаем список позиций за период range
       CArrayObj *list_range=GetListDataRange(range, list_common, time_start, num_periods);
       if(list_range==NULL)
          return;
          
    //--- Если список позиций пуст - сообщаем в журнал, что за данный период времени не было торговых транзакций
       if(list_range.Total()==0)
         {
          PrintFormat("\"%s\" no trades",ReportRangeDescription(range, num_periods));
          return;
         }
    
    //--- Предварительно обнулив, создаём списки символов и магиков позиций в полученном списке закрытых позиций за период времени
       list_symbols.Clear();
       list_magics.Clear();
       CreateSymbolMagicLists(list_range, list_symbols, list_magics);
       
    //--- Создаём статистику о закрытых позициях за указанный период,
    //--- распечатываем в журнале созданную статистику из array_msg[0] и
    //--- записываем в список сообщений для Push-уведомлений строку из array_msg[1]
       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));       // Заголовок статистики
          Print(StatisticsTableHeader("Symbols ", InpCommissionsInclude, InpSpreadInclude));  // "Шапка" таблицы
          Print(array_msg[0]);                                                                // Статистика за период времени
          Print("");                                                                          // Отступ строки
          list_msg.Add(array_msg[1]);                                                         // Сохраняем сообщение для Push-уведомлений в список для последующей отправки
         }
       
    //--- Если разрешена статистика раздельно по символам
       if(InpReportBySymbols)
         {
          //--- Выводим в журнал заголовок статистики и "шапку" таблицы
          Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_SYMBOLS, time_start));
          Print(StatisticsTableHeader("Symbol ", InpCommissionsInclude, InpSpreadInclude));
          
          //--- В цикле по списку символов
          for(int i=0; i<list_symbols.Total(); i++)
            {
             //--- получаем наименование очередного символа
             string symbol=list_symbols.At(i);
             if(symbol=="")
                continue;
             //--- фильтруем список позиций, оставляя в нём только позиции с полученным символом
             list_tmp=CSelect::ByPositionProperty(list_range, POSITION_PROP_SYMBOL, symbol, EQUAL);
             
             //--- Создаём статистику о закрытых позициях за указанный период по текущему символу списка,
             //--- распечатываем в журнале созданную статистику из array_msg[0] и
             //--- записываем в список сообщений для Push-уведомлений строку из array_msg[1]
             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]);
               }
            }
          //--- По окончании цикла по всем символам выводим в журнал разделительную строку
          Print("");
         }
       
    //--- Если разрешена статистика раздельно по магикам
       if(InpReportByMagics)
         {
          //--- Выводим в журнал заголовок статистики и "шапку" таблицы
          Print(StatisticsRangeTitle(range, num_periods, REPORT_BY_MAGICS, time_start));
          Print(StatisticsTableHeader("Magic ", InpCommissionsInclude, InpSpreadInclude));
          
          //--- В цикле по списку магиков
          for(int i=0; i<list_magics.Total(); i++)
            {
             //--- получаем номер очередного магика
             long magic=list_magics.At(i);
             if(magic==LONG_MAX)
                continue;
             //--- фильтруем список позиций, оставляя в нём только позиции с полученным магиком
             list_tmp=CSelect::ByPositionProperty(list_range, POSITION_PROP_MAGIC, magic, EQUAL);
             
             //--- Создаём статистику о закрытых позициях за указанный период по текущему магику списка,
             //--- распечатываем в журнале созданную статистику из array_msg[0] и
             //--- записываем в список сообщений для Push-уведомлений строку из array_msg[1]
             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]);
               }
            }
          //--- По окончании цикла по всем магикам выводим в журнал разделительную строку
          Print("");
         }
      }
    

    Dentro dessa função, o programa chama a função que gera a estatística para o intervalo de trading escolhido, exibe no diário do terminal o título, o cabeçalho da tabela e a estatística em formato tabular logo abaixo do cabeçalho. As mensagens para notificações Push são adicionadas à lista de mensagens passada como parâmetro para a função. Se os relatórios estiverem separados por símbolos e Magic Numbers, após exibir a estatística principal no diário do terminal, o programa imprime: Um cabeçalho e uma tabela com a estatística segmentada por símbolos e Magic Numbers.


    Os relatórios detalhados organizados em formato tabular para facilitar a análise:

    //+------------------------------------------------------------------+
    //| Создаёт и возвращает строку "шапки" таблицы                      |
    //+------------------------------------------------------------------+
    string StatisticsTableHeader(const string first, const bool commissions, const bool spreads)
      {
    //--- Объявим и инициализируем заголовки столбцов таблицы
       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 ";
    //--- столбцы таблицы, отключаемые в настройках
       string h_commiss=(commissions ? "Commiss " : "");
       string h_swap=(commissions    ? "Swap "    : "");
       string h_fee=(commissions     ? "Fee "     : "");
       string h_spread=(spreads      ? "Spread "  : "");
    //--- ширина столбцов таблицы
       int w=TABLE_COLUMN_W;
       int c=(commissions ? TABLE_COLUMN_W : 0);
       
    //--- Разделители столбцов таблицы, отключаемых в настройках
       string sep1=(commissions ? "|" : "");
       string sep2=(spreads ? "|" : "");
       
    //--- Создаём строку "шапки" таблицы
       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);
                           
      }
    

    A função cria uma string no formato adequado,

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

    onde os últimos quatro campos exibidos dependem da permissão de uso de valores de comissão, swap, taxas de operação e spread na estatística.

    No primeiro campo do cabeçalho, é inserido o nome passado como parâmetro para a função, pois diferentes tabelas exigem diferentes títulos

    Mais detalhes sobre a formatação de mensagens de texto podem ser encontrados nos artigos "Explorando PrintFormat() com exemplos prontos para uso" e "StringFormat(). Visão geral e exemplos prontos para uso".


    Função que retorna o título da descrição do período estatístico solicitado:

    //+------------------------------------------------------------------+
    //| Возвращает заголовок описания запрашиваемого периода статистики  |
    //+------------------------------------------------------------------+
    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));
      }
    

    Dependendo do intervalo estatístico e dos filtros aplicados (por símbolo, por Magic Number ou por data), a função cria e retorna uma string no seguinte formato:

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

    Ou deste tipo

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

    Ou deste outro tipo

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

    E assim por diante

    Função que retorna o texto da mensagem com a estatística:

    //+------------------------------------------------------------------+
    //| Возвращает текст сообщения со статистикой                        |
    //+------------------------------------------------------------------+
    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[])
      {
    //--- Получаем из переданных списков по индексу символ и магик
       string   symbol = list_symbols.At(index);
       long     magic  = list_magics.At(index);
    //--- Если переданные списки пусты, или не получены данные из них - возвращаем 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;          // Указатель на позицию с минимальным значением свойства
       CPosition  *pos_max  =  NULL;          // Указатель на позицию с максимальным значением свойства
       CArrayObj  *list_tmp =  NULL;          // Указатель на временный список для фильтрации по свойствам
       int         index_min=  WRONG_VALUE;   // Индекс позиции в списке с минимальным значением свойства
       int         index_max=  WRONG_VALUE;   // Индекс позиции в списке с максимальным значением свойства
       
    //--- Получаем из списка позиций суммы свойств позиций
       double profit=PropertyValuesSum(list, POSITION_PROP_PROFIT);            // Общий профит позиций в списке
       double commissions=PropertyValuesSum(list,POSITION_PROP_COMMISSIONS);   // Общая комиссия позиций в списке
       double swap=PropertyValuesSum(list, POSITION_PROP_SWAP);                // Общий своп позиций в списке
       double fee=PropertyValuesSum(list, POSITION_PROP_FEE);                  // Общая оплата за проведение сделок позиций в списке
       double costs=commissions+swap+fee;                                      // Издержки: общая сумма значений всех комиссий
       double spreads=PositionsCloseSpreadCostSum(list);                       // Общие затраты на спред всех позиций в списке
       
    //--- Определяем текстовые описания всех полученных значений
       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) : "");
       
    //--- Получаем список только длинных позиций и создаём описание их количества
       list_tmp=CSelect::ByPositionProperty(list, POSITION_PROP_TYPE, POSITION_TYPE_BUY, EQUAL);
       string s_long=(list_tmp!=NULL ? (string)list_tmp.Total() : "0")+" ";
       
    //--- Получаем список только коротких позиций и создаём описание их количества
       list_tmp=CSelect::ByPositionProperty(list, POSITION_PROP_TYPE, POSITION_TYPE_SELL, EQUAL);
       string s_short=(list_tmp!=NULL ? (string)list_tmp.Total() : "0")+" ";
       
    //--- Получаем индекс позиции в списке с максимальным профитом и создаём описание полученного значения
       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 ");
       
    //--- Получаем индекс позиции в списке с минимальным профитом и создаём описание полученного значения
       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 ");
       
    //--- Создаём описание среднего значения профита всех позиций в списке
       string s_avg=StringFormat("%.2f ", PropertyAverageValue(list, POSITION_PROP_PROFIT));
       
    //--- Ширина столбцов таблицы
       int w=TABLE_COLUMN_W;
       int c=(InpCommissionsInclude ? TABLE_COLUMN_W : 0);
    
    //--- Разделители отключаемых в настройках столбцов таблицы
       string sep1=(InpCommissionsInclude ? "|" : "");
       string sep2=(InpSpreadInclude ? "|" : "");
       
    //--- Для вывода в журнал создаём строку со столбцами таблицы, внутри которых расположены полученных выше значения
       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);
       
    //--- Для отправки уведомления на MQID создаём строку со столбцами таблицы, внутри которых расположены полученных выше значения
       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  : ""));
    //--- Всё успешно
       return true;
      }
    

    Na função, é utilizada a filtragem da lista e a busca pelos índices das posições fechadas através da classe CSelect previamente implementada . Com as listas obtidas, são gerados textos para exibição dos dados no relatório.
    Os textos do relatório são criados no final da função em dois formatos — um para saída tabular no diário do terminal e outro para uma string simples para envio via Push Notification.


    Função que preenche as listas de Magic Numbers e símbolos das posições a partir da lista fornecida:

    //+------------------------------------------------------------------+
    //| Заполняет списки магиков и символов позиций из переданного списка|
    //+------------------------------------------------------------------+
    void CreateSymbolMagicLists(CArrayObj *list, CArrayString *list_symbols, CArrayLong *list_magics)
      {
    //--- Если передан невалидный указатель на список позиций, либо список пустой - уходим
       if(list==NULL || list.Total()==0)
          return;
       
       int index=WRONG_VALUE;  // Индекс искомого символа или магика в списке
    
    //--- В цикле по списку позиций
       for(int i=0; i<list.Total(); i++)
         {
          //--- получаем указатель на очередную позицию
          CPosition *pos=list.At(i);
          if(pos==NULL)
             continue;
       
          //--- Получаем символ позиции
          string symbol=pos.Symbol();
       
          //--- Списку символов устанавливаем флаг сортированн6ого списка и получаем индекс символа в списке символов
          list_symbols.Sort();
          index=list_symbols.Search(symbol);
          
          //--- Если такого символа в списке нет - добавляем его в список
          if(index==WRONG_VALUE)
             list_symbols.Add(symbol);
       
          //--- Получаем магик позиции
          long magic=pos.Magic();
          
          //--- Списку магиков устанавливаем флаг сортированного списка и получаем индекс магика в списке магиков
          list_magics.Sort();
          index=list_magics.Search(magic);
          
          //--- Если такого магика в списке нет - добавляем его в список
          if(index==WRONG_VALUE)
             list_magics.Add(magic);
         }
      }
    

    Inicialmente, não sabemos quais símbolos e Magic Numbers foram utilizados nas operações da conta. Para gerar relatórios segmentados por símbolo e Magic Number, é necessário extrair da lista completa de todas as posições fechadas todos os símbolos e Magic Numbers e armazená-los em listas apropriadas. Essa função recebe a lista completa de todas as posições fechadas e ponteiros para as listas de símbolos e Magic Numbers. Todos os símbolos e Magic Numbers encontrados são adicionados às listas correspondentes. Após a execução da função, teremos duas listas preenchidas — uma com símbolos e outra com Magic Numbers — que poderão ser utilizadas para gerar relatórios segmentados.

    Para obter a soma de um determinado atributo inteiro ou de ponto flutuante de todas as posições em uma lista, basta percorrer a lista e somar os valores desse atributo. Por que isso é necessário? Por exemplo, para calcular o spread total, o lucro ou o prejuízo geral. Vamos agora escrever funções que permitam somar os valores de atributos específicos de todas as posições na lista.

    Função que retorna a soma de um atributo inteiro especificado de todas as posições na lista:

    //+------------------------------------------------------------------+
    //| Возвращает сумму величин указанного                              |
    //| целочисленного свойства всех позиций в списке                    |
    //+------------------------------------------------------------------+
    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;
      }
    

    Percorremos a lista de posições recebida como parâmetro. Para cada posição, obtemos o valor da propriedade especificada e acumulamos a soma no resultado final. Ao final do laço, teremos a soma total da propriedade para todas as posições da lista fornecida.

    Função que retorna a soma de um atributo de ponto flutuante especificado de todas as posições na lista:

    //+------------------------------------------------------------------+
    //| Возвращает сумму величин указанного                              |
    //| вещественного свойства всех позиций в списке                     |
    //+------------------------------------------------------------------+
    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;
      }
    


    O princípio é o mesmo da função anterior, criamos funções para calcular a média dos valores de uma determinada propriedade.

    Função que retorna a média de um atributo inteiro especificado de todas as posições na lista:

    //+------------------------------------------------------------------+
    //| Возвращает среднюю величину указанного                           |
    //| целочисленного свойства всех позиций в списке                    |
    //+------------------------------------------------------------------+
    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);
      }
    


    Função que retorna a média de um atributo de ponto flutuante especificado de todas as posições na lista:

    //+------------------------------------------------------------------+
    //| Возвращает среднюю величину указанного                           |
    //| вещественного свойства всех позиций в списке                     |
    //+------------------------------------------------------------------+
    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);
      }
    


    Função que retorna a soma do custo dos spreads das operações de fechamento de todas as posições da lista:

    //+------------------------------------------------------------------+
    //| Возвращает сумму стоимости спредов                               |
    //| сделок закрытия всех позиций в списке                            |
    //+------------------------------------------------------------------+
    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 a posição não possui um atributo direto chamado "custo do spread", não podemos utilizar as funções genéricas de soma mencionadas anteriormente. Portanto, utilizamos diretamente o método da classe de posição, que calcula e retorna o custo do spread no fechamento da posição. Os valores de todas as posições da lista são acumulados no resultado final, que é então retornado pela função.


    Função que retorna a descrição do período do relatório:

    //+------------------------------------------------------------------+
    //| Возвращает описание периода отчёта                               |
    //+------------------------------------------------------------------+
    string ReportRangeDescription(ENUM_REPORT_RANGE range, const int num_period)
      {
       switch(range)
         {
          //--- Сутки
          case REPORT_RANGE_DAILY       : return("Daily");
          //---С начала недели
          case REPORT_RANGE_WEEK_BEGIN  : return("Weekly");
          //--- С начала месяца
          case REPORT_RANGE_MONTH_BEGIN : return("Month-to-date");
          //--- С начала года
          case REPORT_RANGE_YEAR_BEGIN  : return("Year-to-date");
          //--- Количество дней
          case REPORT_RANGE_NUM_DAYS    : return StringFormat("%d days", num_period);
          //--- Количество месяцев
          case REPORT_RANGE_NUM_MONTHS  : return StringFormat("%d months", num_period);
          //--- Количество лет
          case REPORT_RANGE_NUM_YEARS   : return StringFormat("%d years", num_period);
          //--- Весь период
          case REPORT_RANGE_ALL         : return("Entire period");
          //--- any other
          default                       : return("Unknown period: "+(string)range);
         }
      }
    

    Dependendo do período do relatório especificado (e do número de dias, meses ou anos), a função gera e retorna uma string de descrição correspondente ao período analisado.


    Analisamos todas as funções do programa de serviço, bem como seu laço principal. Agora, vamos compilá-lo e executá-lo. Após a compilação, o programa aparecerá no "Navegador" do terminal, dentro da seção "Serviços".

    Localizamos nosso serviço e, no menu de clique direito (PCM), selecionamos "Adicionar serviço":


    Em seguida, será exibida a janela de configurações do programa:



    Assim que o serviço for iniciado, ele gerará automaticamente um relatório diário, contendo:

    • Um relatório geral para três meses, além do relatório segmentado por símbolos e Magic Numbers.
    • Um relatório geral para dois anos, além do relatório segmentado por símbolos e Magic Numbers:
    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
    

    Após a geração dos relatórios no diário do terminal, o serviço iniciará o envio das notificações para o smartphone. No total, 31 mensagens foram enviadas em quatro lotes, respeitando a limitação de 10 mensagens por minuto.

    Se nenhuma negociação foi realizada no dia anterior ou nos sete dias anteriores à geração do relatório, o serviço exibirá uma mensagem informando isso.

    Caso as configurações do serviço sejam modificadas para desativar relatórios segmentados por símbolos e Magic Numbers, desative a inclusão de comissões e spreads.


    então as estatísticas terão um 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 as abas de relatório acima são impressas no registro de especialistas do terminal.

    O smartphone, por outro lado, recebe relatórios em um formato ligeiramente diferente:


    Isso economiza espaço na linha do relatório, garantindo que o tamanho da mensagem não ultrapasse o limite de 255 caracteres.


    Considerações finais

    Com base no desenvolvimento desse programa de serviço, exploramos a capacidade de armazenar diferentes tipos de dados e recuperá-los com base em múltiplos critérios. O modelo apresentado permite criar coleções de objetos, obter ponteiros para os objetos desejados com base em propriedades especificadas e gerar listas filtradas conforme critérios escolhidos. Isso, por sua vez, possibilita organizar dados em um formato semelhante a um banco de dados e acessar as informações de maneira eficiente. Esses dados podem ser apresentados em diferentes formatos, como relatórios de trading, exibidos no diário do terminal ou enviados via notificações Push para o smartphone do usuário por meio do MetaQuotes ID.

    Além disso, é possível expandir ainda mais a funcionalidade do programa de serviço apresentado, adicionando novas opções para visualização dos relatórios. Isso inclui a exibição dos dados em um gráfico separado, utilizando tabelas, gráficos e diagramas no formato que o usuário preferir. O MQL5 oferece todas as ferramentas necessárias para implementar essas melhorias.

    Os arquivos do projeto estão anexados ao artigo, incluindo um arquivo compactado que pode ser extraído diretamente na pasta MQL5 do terminal. Para começar a usar o programa imediatamente, basta compilar o arquivo Reporter.mq5 e executar o serviço.


    Traduzido do russo pela MetaQuotes Ltd.
    Artigo original: https://www.mql5.com/ru/articles/15346

    Arquivos anexados |
    Deal.mqh (49.73 KB)
    Position.mqh (67.53 KB)
    Select.mqh (43.22 KB)
    Account.mqh (32.94 KB)
    Accounts.mqh (15.89 KB)
    Reporter.mq5 (101.21 KB)
    MQL5.zip (36.5 KB)
    Últimos Comentários | Ir para discussão (4)
    theonementor
    theonementor | 31 jul. 2024 em 14:29
    Corrija-me se eu estiver errado, por que você está se refazendo? As classes Account, Position e Select são muito parecidas com o que você já tem na biblioteca do easy! Por que reimplementar o que você já tem pronto? Se você precisasse de uma funcionalidade adicional, não seria melhor adicionar a funcionalidade à biblioteca já existente e usá-la?
    Artyom Trishkin
    Artyom Trishkin | 31 jul. 2024 em 14:50
    theonementor # :
    Corrija-me se eu estiver errado, por que você está se remodelando? As classes Account, Position e Select são muito semelhantes às que você já tem em sua biblioteca. Por que reimplementar algo que já está pronto? Se você precisa de uma funcionalidade adicional, não seria melhor adicioná-la a uma biblioteca existente e usá-la?

    Este artigo não está relacionado a artigos de biblioteca. Mas o conceito de criação de objetos foi retirado da biblioteca. Leia novamente o artigo para entender melhor.

    theonementor
    theonementor | 1 ago. 2024 em 12:38
    Artyom Trishkin # :

    Este artigo não está relacionado aos artigos da biblioteca. Mas o conceito de construção de objetos foi retirado da biblioteca. Por favor, releia o artigo para entender.

    Você adicionará essa funcionalidade à biblioteca mais tarde?
    Artyom Trishkin
    Artyom Trishkin | 1 ago. 2024 em 12:45
    theonementor #:
    Vocês adicionarão essa funcionalidade à biblioteca mais tarde?

    Mais tarde, sim.

    Mas não será uma repetição do que está escrito aqui. Será possível projetar algo para você mesmo a partir de um conjunto de métodos.

    Reimaginando Estratégias Clássicas (Parte IV): SP500 e Notas do Tesouro dos EUA Reimaginando Estratégias Clássicas (Parte IV): SP500 e Notas do Tesouro dos EUA
    Nesta série de artigos, analisamos estratégias clássicas de negociação usando algoritmos modernos para determinar se podemos melhorar a estratégia utilizando IA. No artigo de hoje, revisamos uma abordagem clássica para negociar o SP500 usando a relação que ele tem com as Notas do Tesouro dos EUA.
    MQL5 Trading Toolkit (Parte 2): Expansão e Aplicação da Biblioteca EX5 para Gerenciamento de Posições MQL5 Trading Toolkit (Parte 2): Expansão e Aplicação da Biblioteca EX5 para Gerenciamento de Posições
    Aqui, você aprenderá a importar e utilizar bibliotecas EX5 em seu código ou projetos MQL5. Neste artigo, expandiremos a biblioteca EX5 criada anteriormente, adicionando mais funções de gerenciamento de posições e criando dois Expert Advisors (EA). No primeiro exemplo, usaremos o indicador técnico Variable Index Dynamic Average para desenvolver um EA baseado em uma estratégia de trailing stop. No segundo, implementaremos um painel de negociação para monitorar, abrir, fechar e modificar posições. Esses dois exemplos demonstrarão como utilizar a biblioteca EX5 aprimorada para o gerenciamento de posições.
    Funcionalidades do Assistente MQL5 que você precisa conhecer (Parte 29): Taxas de aprendizado e perceptrons multicamadas Funcionalidades do Assistente MQL5 que você precisa conhecer (Parte 29): Taxas de aprendizado e perceptrons multicamadas
    Estamos concluindo a análise da sensibilidade da taxa de aprendizado ao desempenho do EA, estudando taxas de aprendizado adaptáveis Essas taxas devem ser ajustadas para cada parâmetro da camada durante o treinamento, por isso precisamos avaliar os potenciais benefícios em relação às perdas esperadas no desempenho.
    Negociação de Notícias Facilitada (Parte 3): Realizando Negócios Negociação de Notícias Facilitada (Parte 3): Realizando Negócios
    Neste artigo, nosso especialista em negociação de notícias começará a abrir negociações com base no calendário econômico armazenado em nosso banco de dados. Além disso, melhoraremos os gráficos do especialista para exibir informações mais relevantes sobre os próximos eventos do calendário econômico.