
Monitoramento de Trading com Notificações-Push — Exemplo de Serviço no MetaTrader 5
Conteúdo
- Introdução
- Estrutura do projeto
- Classe de operação
- Classe de posição histórica
- Classe para busca e filtragem por propriedades de operações e posições
- Classe coleção de posições históricas
- Classe de conta
- Classe coleção de contas
- Programa de serviço para criação de relatórios de trading e envio de notificações
- Considerações finais
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:
- estrutura das propriedades dos objetos "(Parte I): Conceito, gerenciamento de dados e primeiros resultados",
- estrutura das listas de objetos "(Parte II): Coleção do histórico de ordens e negócios" e
- 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:
- 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.
- 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
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.





- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
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.
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ê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.