English Русский 中文 Español Deutsch 日本語
preview
Como visualizar operações diretamente no gráfico sem se perder no histórico de negociações

Como visualizar operações diretamente no gráfico sem se perder no histórico de negociações

MetaTrader 5Exemplos |
667 0
Artyom Trishkin
Artyom Trishkin

Conteúdo


Introdução

Os traders modernos frequentemente enfrentam dificuldades para analisar um grande volume de dados relacionados às operações de negociação. No terminal cliente MetaTrader 5, a exibição do histórico de negociações pode se tornar ineficiente e confusa devido à sobrecarga de ícones de posições abertas e fechadas no gráfico. Isso é especialmente relevante para traders que lidam com inúmeras operações, onde o gráfico rapidamente se torna lotado, dificultando praticamente a análise da atividade de negociação e a tomada de decisões informadas.

O objetivo deste artigo é propor uma solução que facilite a compreensão e análise do histórico de negociações. Desenvolveremos um mecanismo de exibição passo a passo de posições fechadas e informações aprimoradas sobre operações. Isso permitirá que traders se concentrem em cada operação individual e adquiram uma compreensão mais profunda de suas atividades de negociação.

Durante o desenvolvimento, implementaremos funções que permitirão:

  • Visualizar passo a passo as posições fechadas, utilizando teclas de navegação.
  • Melhorar as dicas pop-up, fornecendo informações mais detalhadas sobre cada operação.
  • Centralizar o gráfico para que os elementos mais importantes estejam sempre visíveis.

Além disso, será discutida em detalhes a estrutura de dados necessária para a implementação dessa funcionalidade, bem como os princípios básicos de registro de operações e posições no MetaTrader 5. Como resultado, os traders poderão gerenciar seu histórico de negociações de maneira mais eficiente e tomar decisões fundamentadas com base nas informações obtidas.

No terminal cliente MetaTrader 5, podemos exibir o histórico de negociações, ativando a opção nas configurações do gráfico (tecla F8) na guia "Exibir" e marcando a caixa "Mostrar histórico de negociações":


Ao marcar a caixa, permitimos que o terminal exiba no gráfico todo o histórico de negociações com ícones de abertura/fechamento de posições conectados por linhas. Assim, todas as operações realizadas no símbolo são totalmente exibidas:


Quando há inúmeras operações, o gráfico pode ficar excessivamente sobrecarregado de ícones, dificultando distinguir as informações. Além disso, as dicas pop-up que surgem ao passar o cursor sobre um ícone de operação ou linha de conexão entre operações não são particularmente informativas:


O programa que estamos criando hoje, ao ser iniciado, exibirá apenas a última posição fechada. A navegação pelas demais posições será realizada através das teclas do cursor:

  • a tecla para cima exibirá a primeira posição fechada do símbolo;
  • a tecla para baixo exibirá a última posição fechada do símbolo;
  • a tecla para a esquerda exibirá a posição fechada anterior do símbolo;
  • a tecla para a direita exibirá a próxima posição fechada do símbolo.

Nas dicas pop-up das operações e da linha de conexão, tentaremos exibir mais informações úteis e, ao segurar a tecla Shift, serão mostradas no gráfico as informações da posição fechada atualmente selecionada:


Ao exibir cada nova posição fechada, centralizaremos o gráfico de forma que o esquema desenhado com os ícones de abertura/fechamento da posição e a linha que os conecta sejam posicionados no centro do gráfico.

Caso ambos os ícones de abertura e fechamento não caibam em uma única tela do gráfico, centralizaremos o gráfico de forma que a operação de abertura esteja no segundo bar visível a partir da esquerda no gráfico.

Vamos considerar os princípios básicos de registro de operações e posições, bem como a estrutura das classes que criaremos hoje.

Como uma posição é formada? Primeiro, um pedido de negociação é enviado ao servidor, isto é, uma ordem. A ordem pode ser cancelada ou executada. Quando a ordem é executada, obtemos uma operação, que é o registro da execução do pedido de negociação. Se, no momento da execução da ordem, não houver uma posição, a operação gera uma posição na direção da operação. Se já houver uma posição, então há várias possibilidades, dependendo do tipo de registro de posição. No registro de posição por netting, é possível ter apenas uma posição por símbolo. Consequentemente, a operação gerada pelo pedido de negociação modifica a posição já existente. Ela pode ser:

  1. fechada — se uma operação de venda foi realizada para uma posição de compra com o volume igual ao volume da posição:
    Posição 1.0 Buy - Operação 1.0 Sell = volume 0 (fechamento da posição);

  2. parcialmente fechada — se uma operação de venda foi realizada para uma posição de compra com volume inferior ao da posição:
    Posição 1.0 Buy - Operação 0.5 Sell = volume 0.5
    (fechamento parcial da posição);

  3. aumentado o volume — se uma operação de compra foi realizada para uma posição de compra:
    Posição 1.0 Buy + Operação 0.5 Buy = volume 1.5
    (aumento do volume da posição);

  4. invertida — se uma operação de venda foi realizada para uma posição de compra com volume superior ao da posição:
    Posição 1.0 Buy - Operação 1.5 Sell = Posição 0.5 Sell (inversão da posição);

Para o registro de posições em uma conta com tipo de hedge, cada operação pode alterar a posição existente ou criar uma nova. Para alterar uma posição existente (fechamento ou fechamento parcial), é necessário indicar na ordem o identificador da posição a ser negociada. O identificador da posição é um número único atribuído a cada nova posição, que permanece inalterado durante toda a vida da posição:

  1. abertura de uma nova posição — se uma operação de venda ou compra foi realizada com uma posição já existente:
    Posição 1.0 Buy + Operação 1.0 Sell = 2 posições independentes 1.0 Buy e 1.0 Sell
    (abertura de posição), ou
    Posição 1.0 Buy + Operação 1.5 Buy = 2 posições independentes 1.0 Buy e 1.5 Buy (abertura de posição), e assim por diante;

  2. fechamento de uma posição existente — se uma operação de venda foi realizada com a indicação do identificador da posição de compra já existente:
    Posição 1.0 Buy com ID 123 - Operação 1.0 Sell com ID 123 = fechamento da posição 1.0 Buy com ID 123 (fechamento da posição com ID 123).

  3. fechamento parcial de uma posição existente — se uma operação de venda foi realizada com a indicação do identificador de uma posição de compra já existente, com volume inferior ao volume da posição com o identificador especificado:
    Posição 1.0 Buy com ID 123 - Operação 0.5 Sell com ID 123 = volume 0.5 Buy com ID 123 (fechamento parcial da posição com ID 123);

Mais informações sobre ordens, operações e posições podem ser encontradas no artigo "Ordens, posições e operações no MetaTrader 5".

Cada ordem enviada ao servidor de negociação permanece na lista de ordens ativas no terminal até o momento de sua execução. Quando é executada, ocorre uma operação que gera uma nova posição ou altera uma posição existente. No momento da execução da ordem, ela é transferida para a lista de ordens históricas, e a operação resultante é registrada na lista de operações históricas. Se a operação gera uma posição, então uma nova posição é criada e localizada na lista de posições ativas.

Não há uma lista de posições históricas (fechadas) no terminal. Também não existe uma lista de operações ativas no terminal, já que elas são registradas diretamente na lista histórica correspondente.
Isso se explica facilmente: uma ordem é um comando para realizar operações de negociação na conta. Ela existe até ser executada e está localizada na lista de ordens ativas. Assim que a ordem é executada, ela deixa de existir (idealmente) e é movida para a lista de ordens executadas. No momento da execução, a ordem gera uma operação. Uma operação é o registro do cumprimento de uma ordem de negociação. É um evento instantâneo — a ordem foi executada — isso é uma operação. E ponto. A operação é registrada na lista de eventos ocorridos na conta.
A operação gera uma posição, que permanece até ser fechada. Consequentemente, uma posição está sempre na lista de posições ativas. Assim que ela é fechada, é removida dessa lista.

No terminal, não há uma lista de posições fechadas. Mas há uma lista de operações realizadas. Portanto, para criar uma lista de posições fechadas, é necessário compô-la a partir das operações que pertenciam a uma posição anteriormente encerrada. A lista de operações está sempre acessível. E em cada operação, nas suas propriedades, há o identificador da posição da qual a operação fez parte ao longo da sua existência. Dessa forma, temos todas as informações necessárias para reconstruir o histórico de cada posição que existiu na conta: o identificador da posição e a lista de suas operações.

Com base no exposto, obtemos o seguinte algoritmo para reconstruir as posições anteriormente fechadas a partir da lista histórica de operações:

  • Obtemos a lista de todas as operações do símbolo desejado;
  • Percorremos a lista e obtemos cada operação sequencialmente;
  • Verificamos nas propriedades da operação o identificador da posição;
  • Se uma posição com esse identificador não estiver na lista própria de posições históricas, criamos um novo objeto de posição histórica;
  • Obtemos o objeto de posição histórica pelo identificador recebido anteriormente;
  • Adicionamos a operação atual à lista de operações do objeto de posição histórica.

Ao final do ciclo sobre a lista de operações do terminal, teremos no programa a lista completa de todas as posições fechadas na conta para o símbolo, contendo listas de operações pertencentes a cada posição.

Agora, em cada posição histórica, haverá uma lista de operações que contribuíram para suas alterações, permitindo-nos obter informações completas sobre todas as modificações da posição fechada ao longo de sua existência.

Para implementar essa lógica, precisamos criar três classes:

  1. Classe de Operação Ela conterá uma lista de todas as propriedades da operação histórica, métodos para acesso às propriedades, tanto para definir quanto para obter valores, além de métodos adicionais para exibir informações sobre a operação.
  2. Classe de Posição Ela conterá uma lista de todas as operações, métodos de acesso a elas, além de métodos adicionais para exibir informações sobre a posição e suas operações.
  3. Classe de Gerenciamento de Posições Históricas Ela conterá uma lista de todas as posições históricas, com métodos para criação e atualização, além de métodos para acessar propriedades das posições e suas operações, e métodos adicionais para exibir informações sobre essas posições e suas operações.

Vamos começar.


Classe de operações

Para obter e armazenar as propriedades de uma operação, é necessário selecionar o histórico de ordens e operações (HistorySelect()), iterar sobre o histórico e obter o ticket da operação a partir do índice do loop (HistoryDealGetTicket()). Com isso, a operação será selecionada para a obtenção de suas propriedades usando as funções HistoryDealGetInteger(), HistoryDealGetDouble() e HistoryDealGetString().

Na classe de operação, partiremos do pressuposto de que a operação já foi selecionada e é possível acessar suas propriedades diretamente. Além de registrar e acessar as propriedades da operação, a classe permitirá a criação de um marcador gráfico no gráfico que exiba a operação. O marcador será desenhado no canvas, permitindo que cada tipo de operação tenha um marcador específico, em vez de usar símbolos do conjunto pré-definido de fontes. Ao criar um objeto para cada operação, capturaremos o histórico de preços no momento em que a operação ocorreu, o que permitirá calcular o spread do momento da operação e exibi-lo na descrição de seus parâmetros.

Como cada posição pode incluir diversas operações que contribuíram para suas mudanças, todas as operações serão organizadas em uma lista de um array dinâmico de ponteiros para instâncias da classe CObject e seus derivados CArrayObj.
Assim, a classe de operação será derivada da classe de objeto base da Biblioteca Padrão CObject.

Uma operação possui um conjunto de propriedades inteiras, reais e de texto. Cada objeto-operação será incluído na lista de objetos da classe de posição histórica. Para buscar a operação desejada, será necessário escrever uma enumeração de parâmetros, permitindo a busca e a ordenação das operações na lista com base nesses valores. Para a implementação do programa, precisamos de uma busca por apenas duas propriedades, nomeadamente pelo ticket da operação e pelo tempo em milissegundos. Com o ticket, verificamos se uma operação já está na lista (ou se ainda não está), e pelo tempo em milissegundos as operações são ordenadas na lista, garantindo que estejam em ordem cronológica rigorosa. Como as classes foram projetadas para expansão, a enumeração das propriedades da operação incluirá todas as suas propriedades.

Para fazer a busca e ordenação nas listas CArrayObj da Biblioteca Padrão, a classe CObject já define o método virtual Compare(), que sempre retorna o valor 0:

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

Ou seja, este método sempre retorna igualdade. Esse método, no entanto, deve ser reescrito em cada objeto das classes herdadas de CObject de forma que, ao comparar uma propriedade específica indicada no parâmetro mode, o método retorne:

  • -1 — se o valor da propriedade do objeto atual for menor do que o do objeto comparado;
  •  1 — se o valor da propriedade do objeto atual for maior do que o do objeto comparado;
  •  0 — se os valores da propriedade indicada forem iguais em ambos os objetos.

Dessa forma, como explicado acima, precisaremos criar enumerações das propriedades para cada uma das classes que desenvolveremos hoje, além dos métodos de comparação.

Criamos na pasta \MQL5\Include\ uma nova pasta PositionsViewer\ e, dentro dela, um novo arquivo Deal.mqh para a classe CDeal:


O classe base será CObject da Biblioteca Padrão:


Assim, teremos o seguinte esboço de arquivo da classe de objetos de 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"
class CDeal : public CObject
  {
private:

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


Incluímos os arquivos da classe CObject da biblioteca padrão e da classe CCanvas no arquivo da classe de objetos de operação e escrevemos a enumeração para ordenação com base nas propriedades do objeto de 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>
#include <Canvas\Canvas.mqh>

enum ENUM_DEAL_SORT_MODE
  {
   SORT_MODE_DEAL_TICKET = 0,          // Режим сравнения/сортировки по тикету сделки
   SORT_MODE_DEAL_ORDER,               // Режим сравнения/сортировки по ордеру, на основание которого выполнена сделка
   SORT_MODE_DEAL_TIME,                // Режим сравнения/сортировки по времени совершения сделки
   SORT_MODE_DEAL_TIME_MSC,            // Режим сравнения/сортировки по времени совершения сделки в миллисекундах
   SORT_MODE_DEAL_TYPE,                // Режим сравнения/сортировки по типу сделки
   SORT_MODE_DEAL_ENTRY,               // Режим сравнения/сортировки по направлению сделки
   SORT_MODE_DEAL_MAGIC,               // Режим сравнения/сортировки по Magic number сделки
   SORT_MODE_DEAL_REASON,              // Режим сравнения/сортировки по причине или источнику проведения сделки
   SORT_MODE_DEAL_POSITION_ID,         // Режим сравнения/сортировки по идентификатору позиции
   SORT_MODE_DEAL_VOLUME,              // Режим сравнения/сортировки по объему сделки
   SORT_MODE_DEAL_PRICE,               // Режим сравнения/сортировки по цене сделки
   SORT_MODE_DEAL_COMMISSION,          // Режим сравнения/сортировки по комиссии
   SORT_MODE_DEAL_SWAP,                // Режим сравнения/сортировки по накопленному свопу при закрытии
   SORT_MODE_DEAL_PROFIT,              // Режим сравнения/сортировки по финансовому результату сделки
   SORT_MODE_DEAL_FEE,                 // Режим сравнения/сортировки по оплате за проведение сделки
   SORT_MODE_DEAL_SL,                  // Режим сравнения/сортировки по уровню Stop Loss
   SORT_MODE_DEAL_TP,                  // Режим сравнения/сортировки по уровню Take Profit
   SORT_MODE_DEAL_SYMBOL,              // Режим сравнения/сортировки по имени символа, по которому произведена сделка
   SORT_MODE_DEAL_COMMENT,             // Режим сравнения/сортировки по комментарию к сделке
   SORT_MODE_DEAL_EXTERNAL_ID,         // Режим сравнения/сортировки по идентификатору сделки во внешней торговой системе 
  };
//+------------------------------------------------------------------+
//| Класс сделки                                                     |
//+------------------------------------------------------------------+
class CDeal : public CObject
  {
  }

Nas seções privadas, protegidas e públicas, declararemos todas as variáveis e métodos necessários para o funcionamento da classe.

//+------------------------------------------------------------------+
//| Класс сделки                                                     |
//+------------------------------------------------------------------+
class CDeal : public CObject
  {
private:
   MqlTick           m_tick;           // Структура тика сделки
//--- Объект CCanvas
   CCanvas           m_canvas;         // Канвас
   long              m_chart_id;       // Идентификатор графика
   int               m_width;          // Ширина канваса
   int               m_height;         // Высота канваса
   string            m_name;           // Имя графического объекта
   
//--- Создаёт объект-метку на графике
   bool              CreateLabelObj(void);

//--- Рисует на холсте (1) маску, (2) стрелку Buy
   void              DrawArrowMaskBuy(const int shift_x, const int shift_y);
   void              DrawArrowBuy(const int shift_x, const int shift_y);
   
//--- Рисует на холсте (1) маску, (2) стрелку Sell
   void              DrawArrowMaskSell(const int shift_x, const int shift_y);
   void              DrawArrowSell(const int shift_x, const int shift_y);
   
//--- Рисует внешний вид метки
   void              DrawLabelView(void);
   
//--- Получает (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:
//--- Целочисленные свойства
   long              m_ticket;            // Тикет сделки. Уникальное число, которое присваивается каждой сделке
   long              m_order;             // Ордер, на основание которого выполнена сделка
   datetime          m_time;              // Время совершения сделки
   long              m_time_msc;          // Время совершения сделки в миллисекундах с 01.01.1970
   ENUM_DEAL_TYPE    m_type;              // Тип сделки
   ENUM_DEAL_ENTRY   m_entry;             // Направление сделки – вход в рынок, выход из рынка или разворот
   long              m_magic;             // Magic number для сделки (смотри ORDER_MAGIC)
   ENUM_DEAL_REASON  m_reason;            // Причина или источник проведения сделки
   long              m_position_id;       // Идентификатор позиции, в открытии, изменении или закрытии которой участвовала эта сделка
   
//--- Вещественные свойства
   double            m_volume;            // Объем сделки
   double            m_price;             // Цена сделки
   double            m_commission;        // Комиссия по сделке
   double            m_swap;              // Накопленный своп при закрытии
   double            m_profit;            // Финансовый результат сделки
   double            m_fee;               // Оплата за проведение сделки, начисляется сразу после совершения сделки
   double            m_sl;                // Уровень Stop Loss
   double            m_tp;                // Уровень Take Profit

//--- Строковые свойства
   string            m_symbol;            // Имя символа, по которому произведена сделка
   string            m_comment;           // Комментарий к сделке
   string            m_external_id;       // Идентификатор сделки во внешней торговой системе (на бирже)
   
//--- Дополнительные свойства
   int               m_digits;            // Digits символа
   double            m_point;             // Point символа
   double            m_bid;               // Bid при совершении сделки
   double            m_ask;               // Ask при совершении сделки
   int               m_spread;            // Spread при совершении сделки
   color             m_color_arrow;       // Цвет значка сделки
   
//--- Рисует стрелку, соответствующую типу сделки. Может быть переопределён в наследуемых классах
   virtual void      DrawArrow(void);

public:
//--- Установка свойств
//--- Целочисленные свойства
   void              SetTicket(const long ticket)              { this.m_ticket=ticket;       }  // Тикет
   void              SetOrder(const long order)                { this.m_order=order;         }  // Ордер
   void              SetTime(const datetime time)              { this.m_time=time;           }  // Время
   void              SetTimeMsc(const long value)              { this.m_time_msc=value;      }  // Время в миллисекундах
   void              SetTypeDeal(const ENUM_DEAL_TYPE type)    { this.m_type=type;           }  // Тип
   void              SetEntry(const ENUM_DEAL_ENTRY entry)     { this.m_entry=entry;         }  // Направление
   void              SetMagic(const long magic)                { this.m_magic=magic;         }  // Magic number
   void              SetReason(const ENUM_DEAL_REASON reason)  { this.m_reason=reason;       }  // Причина или источник проведения сделки
   void              SetPositionID(const long id)              { this.m_position_id=id;      }  // Идентификатор позиции
   
//--- Вещественные свойства
   void              SetVolume(const double volume)            { this.m_volume=volume;       }  // Объем
   void              SetPrice(const double price)              { this.m_price=price;         }  // Цена
   void              SetCommission(const double value)         { this.m_commission=value;    }  // Комиссия
   void              SetSwap(const double value)               { this.m_swap=value;          }  // Накопленный своп при закрытии
   void              SetProfit(const double value)             { this.m_profit=value;        }  // Финансовый результат
   void              SetFee(const double value)                { this.m_fee=value;           }  // Оплата за проведение сделки
   void              SetSL(const double value)                 { this.m_sl=value;            }  // Уровень Stop Loss
   void              SetTP(const double value)                 { this.m_tp=value;            }  // Уровень Take Profit

//--- Строковые свойства
   void              SetSymbol(const string symbol)            { this.m_symbol=symbol;       }  // Имя символа
   void              SetComment(const string comment)          { this.m_comment=comment;     }  // Комментарий
   void              SetExternalID(const string ext_id)        { this.m_external_id=ext_id;  }  // Идентификатор сделки во внешней торговой системе

//--- Получение свойств
//--- Целочисленные свойства
   long              Ticket(void)                        const { return(this.m_ticket);      }  // Тикет
   long              Order(void)                         const { return(this.m_order);       }  // Ордер
   datetime          Time(void)                          const { return(this.m_time);        }  // Время
   long              TimeMsc(void)                       const { return(this.m_time_msc);    }  // Время в миллисекундах
   ENUM_DEAL_TYPE    TypeDeal(void)                      const { return(this.m_type);        }  // Тип
   ENUM_DEAL_ENTRY   Entry(void)                         const { return(this.m_entry);       }  // Направление
   long              Magic(void)                         const { return(this.m_magic);       }  // Magic number
   ENUM_DEAL_REASON  Reason(void)                        const { return(this.m_reason);      }  // Причина или источник проведения сделки
   long              PositionID(void)                    const { return(this.m_position_id); }  // Идентификатор позиции
   
//--- Вещественные свойства
   double            Volume(void)                        const { return(this.m_volume);      }  // Объем
   double            Price(void)                         const { return(this.m_price);       }  // Цена
   double            Commission(void)                    const { return(this.m_commission);  }  // Комиссия
   double            Swap(void)                          const { return(this.m_swap);        }  // Накопленный своп при закрытии
   double            Profit(void)                        const { return(this.m_profit);      }  // Финансовый результат
   double            Fee(void)                           const { return(this.m_fee);         }  // Оплата за проведение сделки
   double            SL(void)                            const { return(this.m_sl);          }  // Уровень Stop Loss
   double            TP(void)                            const { return(this.m_tp);          }  // Уровень Take Profit
   
   double            Bid(void)                           const { return(this.m_bid);         }  // Bid при совершении сделки
   double            Ask(void)                           const { return(this.m_ask);         }  // Ask при совершении сделки
   int               Spread(void)                        const { return(this.m_spread);      }  // Spread при совершении сделки
   
//--- Строковые свойства
   string            Symbol(void)                        const { return(this.m_symbol);      }  // Имя символа
   string            Comment(void)                       const { return(this.m_comment);     }  // Комментарий
   string            ExternalID(void)                    const { return(this.m_external_id); }  // Идентификатор сделки во внешней торговой системе
   
//--- Устанавливает цвет значка сделки
   void              SetColorArrow(const color clr);

//--- (1) Скрывает, (2) отображает значок сделки на графике
   void              HideArrow(const bool chart_redraw=false);
   void              ShowArrow(const bool chart_redraw=false);

//--- Возвращает описание (1) типа сделки, (2) способа изменения позиции, (3) причины проведения сделки
   string            TypeDescription(void)   const;
   string            EntryDescription(void)  const;
   string            ReasonDescription(void) const;
   
//--- Возвращает (1) краткое описание, (2) текст всплывающего сообщения сделки
   string            Description(void);
   virtual string    Tooltip(void);

//--- Распечатывает в журнал свойства сделки
   void              Print(void);
   
//--- Сравнивает два объекта между собой по указанному в mode свойству
   virtual int       Compare(const CObject *node, const int mode=0) const;
   
//--- Конструкторы/деструктор
                     CDeal(void) { this.m_ticket=0; }
                     CDeal(const ulong ticket);
                    ~CDeal();
  };

Analisemos em detalhe os métodos declarados.


Construtor parametrizado:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CDeal::CDeal(const ulong ticket)
  {
//--- Сохранение свойств
//--- Целочисленные свойства
   this.m_ticket     =  (long)ticket;                                                     // Тикет сделки
   this.m_order      =  ::HistoryDealGetInteger(ticket, DEAL_ORDER);                      // Ордер
   this.m_time       =  (datetime)::HistoryDealGetInteger(ticket, DEAL_TIME);             // Время совершения сделки
   this.m_time_msc   =  ::HistoryDealGetInteger(ticket, DEAL_TIME_MSC);                   // Время совершения сделки в миллисекундах
   this.m_type       =  (ENUM_DEAL_TYPE)::HistoryDealGetInteger(ticket, DEAL_TYPE);       // Тип
   this.m_entry      =  (ENUM_DEAL_ENTRY)::HistoryDealGetInteger(ticket, DEAL_ENTRY);     // Направление
   this.m_magic      =  ::HistoryDealGetInteger(ticket, DEAL_MAGIC);                      // Magic number
   this.m_reason     =  (ENUM_DEAL_REASON)::HistoryDealGetInteger(ticket, DEAL_REASON);   // Причина или источник проведения сделки
   this.m_position_id=  ::HistoryDealGetInteger(ticket, DEAL_POSITION_ID);                // Идентификатор позиции
   
//--- Вещественные свойства
   this.m_volume     =  ::HistoryDealGetDouble(ticket, DEAL_VOLUME);                      // Объем
   this.m_price      =  ::HistoryDealGetDouble(ticket, DEAL_PRICE);                       // Цена
   this.m_commission =  ::HistoryDealGetDouble(ticket, DEAL_COMMISSION);                  // Комиссия
   this.m_swap       =  ::HistoryDealGetDouble(ticket, DEAL_SWAP);                        // Накопленный своп при закрытии
   this.m_profit     =  ::HistoryDealGetDouble(ticket, DEAL_PROFIT);                      // Финансовый результат
   this.m_fee        =  ::HistoryDealGetDouble(ticket, DEAL_FEE);                         // Оплата за проведение сделки
   this.m_sl         =  ::HistoryDealGetDouble(ticket, DEAL_SL);                          // Уровень Stop Loss
   this.m_tp         =  ::HistoryDealGetDouble(ticket, DEAL_TP);                          // Уровень Take Profit

//--- Строковые свойства
   this.m_symbol     =  ::HistoryDealGetString(ticket, DEAL_SYMBOL);                      // Имя символа
   this.m_comment    =  ::HistoryDealGetString(ticket, DEAL_COMMENT);                     // Комментарий
   this.m_external_id=  ::HistoryDealGetString(ticket, DEAL_EXTERNAL_ID);                 // Идентификатор сделки во внешней торговой системе

//--- Параметры отображения графики
   this.m_chart_id   =  ::ChartID();
   this.m_digits     =  (int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS);
   this.m_point      =  ::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT);
   this.m_width      =  19;
   this.m_height     =  19;
   this.m_name       =  "Deal#"+(string)this.m_ticket;
   this.m_color_arrow=  (this.TypeDeal()==DEAL_TYPE_BUY ? C'3,95,172' : this.TypeDeal()==DEAL_TYPE_SELL ? C'225,68,29' : C'180,180,180');
   
//--- Параметры для расчёта спреда
   this.m_spread     =  0;
   this.m_bid        =  0;
   this.m_ask        =  0;
   
//--- Создаём графическую метку
   this.CreateLabelObj();
   
//--- Если исторический тик и значение Point символа удалось получить
   if(this.GetDealTick() && this.m_point!=0)
     {
      //--- запишем значения цен Bid и Ask и рассчитаем и сохраним значение спреда
      this.m_bid=this.m_tick.bid;
      this.m_ask=this.m_tick.ask;
      this.m_spread=int((this.m_ask-this.m_bid)/this.m_point);
     }
//--- Если исторический тик получить не удалось, возьмём значение спреда минутного бара, на котором была сделка
   else
      this.m_spread=this.GetSpreadM1();
  }

Considerando que a operação atual já foi selecionada. Carregamos imediatamente todas as propriedades do objeto com as propriedades da operação. Em seguida, configuramos os parâmetros do canvas para exibir a marca da operação no gráfico, criamos o objeto gráfico dessa marca e obtemos o tick do momento da operação para calcular o spread que havia na ocasião. Se o tick não estiver disponível, utilizamos o spread médio da barra de minuto em que ocorreu a operação.

Assim, ao criar o objeto-operação, obtemos imediatamente um objeto com as propriedades da operação histórica registradas com base em seu ticket, além de uma marca já criada, porém oculta, para visualização da operação no gráfico, e o valor do spread no momento da operação.


No destrutor da classe verificamos o motivo da desinicialização e, caso não seja devido a uma mudança de time frame, destruímos o recurso gráfico associado à marca gráfica:

//+------------------------------------------------------------------+
//| Деструктор                                                       |
//+------------------------------------------------------------------+
CDeal::~CDeal()
  {
   if(::UninitializeReason()!=REASON_CHARTCHANGE)
      this.m_canvas.Destroy();
  }


Método virtual para comparação de objetos com base em uma propriedade específica:

//+------------------------------------------------------------------+
//| Сравнивает два объекта между собой по указанному свойству        |
//+------------------------------------------------------------------+
int CDeal::Compare(const CObject *node,const int mode=0) const
  {
   const CDeal * obj = node;
   switch(mode)
     {
      case SORT_MODE_DEAL_TICKET       :  return(this.Ticket() > obj.Ticket()          ?  1  :  this.Ticket() < obj.Ticket()           ? -1  :  0);
      case SORT_MODE_DEAL_ORDER        :  return(this.Order() > obj.Order()            ?  1  :  this.Order() < obj.Order()             ? -1  :  0);
      case SORT_MODE_DEAL_TIME         :  return(this.Time() > obj.Time()              ?  1  :  this.Time() < obj.Time()               ? -1  :  0);
      case SORT_MODE_DEAL_TIME_MSC     :  return(this.TimeMsc() > obj.TimeMsc()        ?  1  :  this.TimeMsc() < obj.TimeMsc()         ? -1  :  0);
      case SORT_MODE_DEAL_TYPE         :  return(this.TypeDeal() > obj.TypeDeal()      ?  1  :  this.TypeDeal() < obj.TypeDeal()       ? -1  :  0);
      case SORT_MODE_DEAL_ENTRY        :  return(this.Entry() > obj.Entry()            ?  1  :  this.Entry() < obj.Entry()             ? -1  :  0);
      case SORT_MODE_DEAL_MAGIC        :  return(this.Magic() > obj.Magic()            ?  1  :  this.Magic() < obj.Magic()             ? -1  :  0);
      case SORT_MODE_DEAL_REASON       :  return(this.Reason() > obj.Reason()          ?  1  :  this.Reason() < obj.Reason()           ? -1  :  0);
      case SORT_MODE_DEAL_POSITION_ID  :  return(this.PositionID() > obj.PositionID()  ?  1  :  this.PositionID() < obj.PositionID()   ? -1  :  0);
      case SORT_MODE_DEAL_VOLUME       :  return(this.Volume() > obj.Volume()          ?  1  :  this.Volume() < obj.Volume()           ? -1  :  0);
      case SORT_MODE_DEAL_PRICE        :  return(this.Price() > obj.Price()            ?  1  :  this.Price() < obj.Price()             ? -1  :  0);
      case SORT_MODE_DEAL_COMMISSION   :  return(this.Commission() > obj.Commission()  ?  1  :  this.Commission() < obj.Commission()   ? -1  :  0);
      case SORT_MODE_DEAL_SWAP         :  return(this.Swap() > obj.Swap()              ?  1  :  this.Swap() < obj.Swap()               ? -1  :  0);
      case SORT_MODE_DEAL_PROFIT       :  return(this.Profit() > obj.Profit()          ?  1  :  this.Profit() < obj.Profit()           ? -1  :  0);
      case SORT_MODE_DEAL_FEE          :  return(this.Fee() > obj.Fee()                ?  1  :  this.Fee() < obj.Fee()                 ? -1  :  0);
      case SORT_MODE_DEAL_SL           :  return(this.SL() > obj.SL()                  ?  1  :  this.SL() < obj.SL()                   ? -1  :  0);
      case SORT_MODE_DEAL_TP           :  return(this.TP() > obj.TP()                  ?  1  :  this.TP() < obj.TP()                   ? -1  :  0);
      case SORT_MODE_DEAL_SYMBOL       :  return(this.Symbol() > obj.Symbol()          ?  1  :  this.Symbol() < obj.Symbol()           ? -1  :  0);
      case SORT_MODE_DEAL_COMMENT      :  return(this.Comment() > obj.Comment()        ?  1  :  this.Comment() < obj.Comment()         ? -1  :  0);
      case SORT_MODE_DEAL_EXTERNAL_ID  :  return(this.ExternalID() > obj.ExternalID()  ?  1  :  this.ExternalID() < obj.ExternalID()   ? -1  :  0);
      default                             :  return(-1);
     }
  }

O método recebe um ponteiro para o objeto comparado e o valor da propriedade especificada na enumeração ENUM_DEAL_SORT_MODE. Retorna 1 se o valor da propriedade do objeto atual for maior que o do objeto comparado. Retorna -1 se o valor da propriedade do objeto atual for menor que o do objeto comparado, e 0 se forem iguais.


Método para retornar a descrição do tipo de operação:

//+------------------------------------------------------------------+
//| Возвращает описание типа сделки                                  |
//+------------------------------------------------------------------+
string CDeal::TypeDescription(void) const
  {
   switch(this.m_type)
     {
      case DEAL_TYPE_BUY                     :  return "Buy";
      case DEAL_TYPE_SELL                    :  return "Sell";
      case DEAL_TYPE_BALANCE                 :  return "Balance";
      case DEAL_TYPE_CREDIT                  :  return "Credit";
      case DEAL_TYPE_CHARGE                  :  return "Additional charge";
      case DEAL_TYPE_CORRECTION              :  return "Correction";
      case DEAL_TYPE_BONUS                   :  return "Bonus";
      case DEAL_TYPE_COMMISSION              :  return "Additional commission";
      case DEAL_TYPE_COMMISSION_DAILY        :  return "Daily commission";
      case DEAL_TYPE_COMMISSION_MONTHLY      :  return "Monthly commission";
      case DEAL_TYPE_COMMISSION_AGENT_DAILY  :  return "Daily agent commission";
      case DEAL_TYPE_COMMISSION_AGENT_MONTHLY:  return "Monthly agent commission";
      case DEAL_TYPE_INTEREST                :  return "Interest rate";
      case DEAL_TYPE_BUY_CANCELED            :  return "Canceled buy deal";
      case DEAL_TYPE_SELL_CANCELED           :  return "Canceled sell deal";
      case DEAL_DIVIDEND                     :  return "Dividend operations";
      case DEAL_DIVIDEND_FRANKED             :  return "Franked (non-taxable) dividend operations";
      case DEAL_TAX                          :  return "Tax charges";
      default                                :  return "Unknown: "+(string)this.m_type;
     }
  }

Este método retorna descrições para todos os tipos de operação, embora a maioria não seja utilizada no programa. Mas o método abrange todos os tipos, considerando futuras expansões e herança de classes.


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

//+------------------------------------------------------------------+
//| Возвращает описание способа изменения позиции                    |
//+------------------------------------------------------------------+
string CDeal::EntryDescription(void) const
  {
   switch(this.m_entry)
     {
      case DEAL_ENTRY_IN      :  return "Entry In";
      case DEAL_ENTRY_OUT     :  return "Entry Out";
      case DEAL_ENTRY_INOUT   :  return "Reverse";
      case DEAL_ENTRY_OUT_BY  :  return "Close a position by an opposite one";
      default                 :  return "Unknown: "+(string)this.m_entry;
     }
  }


Método que retorna a descrição do motivo da operação:

//+------------------------------------------------------------------+
//| Возвращает описание причины проведения сделки                    |
//+------------------------------------------------------------------+
string CDeal::ReasonDescription(void) const
  {
   switch(this.m_reason)
     {
      case DEAL_REASON_CLIENT          :  return "Terminal";
      case DEAL_REASON_MOBILE          :  return "Mobile";
      case DEAL_REASON_WEB             :  return "Web";
      case DEAL_REASON_EXPERT          :  return "EA";
      case DEAL_REASON_SL              :  return "SL";
      case DEAL_REASON_TP              :  return "TP";
      case DEAL_REASON_SO              :  return "SO";
      case DEAL_REASON_ROLLOVER        :  return "Rollover";
      case DEAL_REASON_VMARGIN         :  return "Var. Margin";
      case DEAL_REASON_SPLIT           :  return "Split";
      case DEAL_REASON_CORPORATE_ACTION:  return "Corp. Action";
      default                          :  return "Unknown reason "+(string)this.m_reason;
     }
  }

Neste método, utilizaremos apenas três valores: fechamento por StopLoss, TakeProfit e StopOut.


Método que retorna a descrição 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())));
  }

Com a função StringFormat(), cria e retorna uma string no formato:

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


Método virtual que retorna o texto da mensagem pop-up da operação:

//+------------------------------------------------------------------+
//| Возвращает текст всплывающего сообщения сделки                   |
//+------------------------------------------------------------------+
string CDeal::Tooltip(void)
  {
   return(::StringFormat("Position ID #%I64d %s:\nDeal #%I64d %.2f %s %s\n%s [%.*f]\nProfit: %.2f, SL: %.*f, TP: %.*f",
                         this.PositionID(), this.Symbol(), this.Ticket(), this.Volume(), this.TypeDescription(),
                         this.EntryDescription(), this.TimeMscToString(this.TimeMsc()), this.m_digits, this.Price(),
                         this.Profit(), this.m_digits, this.SL(), this.m_digits, this.TP()));
  }

De maneira semelhante ao método mencionado acima, aqui uma string de texto é formada e retornada no formato:

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

Essa string será usada para exibir a descrição da operação na dica pop-up ao passar o cursor do mouse sobre o ícone da operação no gráfico. O método pode ser sobrescrito em classes derivadas para mostrar informações diferentes sobre a operação.


Método que imprime no log as propriedades da operação:

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

O método não imprime todas as propriedades da operação, mas apenas exibe informações mínimas sobre a operação, retornadas pelo método Description(), discutido acima. Antes da linha de descrição da operação, são adicionados dois espaços para organizar a informação ao exibir a descrição da posição, onde sob o cabeçalho com a descrição da posição são impressos os dados sobre suas operações:

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

Mais informações sobre as funções PrintFormat() e StringFormat() podem ser encontradas nos artigos “Explorando PrintFormat() com exemplos prontos para uso” e “StringFormat(). Visão geral e exemplos práticos de uso”.


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

Aqui, uma string é formada a partir do valor de tempo em milissegundos, convertida para tempo normal dividindo por 1000. A essa string, é adicionado o valor de milissegundos após o ponto, obtido como o restante da divisão do tempo em milissegundos por 1000. A string dos milissegundos é formatada com três dígitos, adicionando zeros à esquerda, se tiver menos de três dígitos. O resultado é uma representação de tempo como esta:

2023.06.12 17:04:20.362

Para entender qual era o spread no momento da operação, é necessário obter o tick pelo tempo da operação em milissegundos. À primeira vista, uma tarefa trivial de copiar um tick para o tempo especificado pela função padrão CopyTicks() acabou se transformando em um pequeno desafio, pois o tick não pôde ser copiado no tempo exato especificado. Finalmente, após algumas tentativas de solução, um algoritmo adequado foi encontrado: é necessário realizar uma quantidade de consultas de ticks dentro de um intervalo de tempo progressivamente ampliado “De” até o momento da operação. Mais detalhes aqui.


Método para obter o tick da operação:

//+------------------------------------------------------------------+
//| Получает тик сделки                                              |
//+------------------------------------------------------------------+
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.m_symbol, ticks, COPY_TICKS_INFO, this.m_time_msc-(offset <<=1), this.m_time_msc);
    
//--- Если тик скопировать удалось (он последний в массиве тиков) - записываем его в переменную m_tick
   if(copied>0)
      this.m_tick=ticks[copied-1];

//--- Возвращаем флаг того, что тик скопирован
   return(copied>0);
  }

Após obter o tick, os valores de preço Ask e Bid são coletados, e o spread é calculado como (Ask - Bid) / Point.

Se, ao final, o método não conseguiu obter o tick, então um valor médio do spread é obtido usando o método de spread da barra de um minuto da operação:

//+------------------------------------------------------------------+
//| Получает спред минутного бара сделки                             |
//+------------------------------------------------------------------+
int CDeal::GetSpreadM1(void)
  {
   int array[1]={};
   int bar=::iBarShift(this.m_symbol, PERIOD_M1, this.Time());
   if(bar==WRONG_VALUE)
      return 0;
   return(::CopySpread(this.m_symbol, PERIOD_M1, bar, 1, array)==1 ? array[0] : 0);
  }

Aqui, obtemos o tempo de abertura da barra de um minuto com base no tempo da operação e, com esse tempo, obtemos o spread médio da barra usando a função CopySpread(). Em caso de erro ao obter a barra ou o spread, o método retorna zero.


Método que cria o objeto-marca no gráfico:

//+------------------------------------------------------------------+
//| Создаёт объект-метку на графике                                  |
//+------------------------------------------------------------------+
bool CDeal::CreateLabelObj(void)
  {
//--- Создаём графический ресурс с привязанным к нему объектом Bitmap
   ::ResetLastError();
   if(!this.m_canvas.CreateBitmap(this.m_name, this.m_time, this.m_price, this.m_width, this.m_height, COLOR_FORMAT_ARGB_NORMALIZE))
     {
      ::PrintFormat("%s: When creating a graphic object, error %d occurred in the CreateBitmap method of the CCanvas class",__FUNCTION__, ::GetLastError());
      return false;
     }
//--- При успешном создании графического ресурса устанавливаем для объекта Bitmap, точку привязки, цену, время и текст всплывающей подсказки
   ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_ANCHOR, ANCHOR_CENTER);
   ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIME, this.Time());
   ::ObjectSetDouble(this.m_chart_id, this.m_name, OBJPROP_PRICE, this.Price());
   ::ObjectSetString(this.m_chart_id, this.m_name, OBJPROP_TOOLTIP, this.Tooltip());
   
//--- Скрываем созданный объект с графика и рисуем на нём его внешний вид
   this.HideArrow();
   this.DrawLabelView();
   return true;
  }

Criamos um recurso gráfico, definimos seu ponto de ancoragem no centro, o preço e o horário da operação, além do texto da dica pop-up. Em seguida, o objeto criado recebe uma flag indicando que ele está oculto em todos os períodos do gráfico, e seu visual é desenhado. Agora, para exibir o objeto, basta configurar a flag de visibilidade em todos os timeframes.


Método que desenha o visual do objeto-marca:

//+------------------------------------------------------------------+
//| Рисует внешний вид объекта-метки                                 |
//+------------------------------------------------------------------+
void CDeal::DrawLabelView(void)
  {
   this.m_canvas.Erase(0x00FFFFFF);
   this.DrawArrow();
   this.m_canvas.Update(true);
  }

Primeiro, o objeto gráfico Bitmap é preenchido com uma cor totalmente transparente, depois uma seta é desenhada nele, correspondente ao tipo da operação, e então o canvas é atualizado, redesenhando o gráfico.


Método virtual que desenha a seta correspondente ao tipo de operação:

//+------------------------------------------------------------------+
//| Рисует стрелку, соответствующую типу сделки                      |
//+------------------------------------------------------------------+
void CDeal::DrawArrow(void)
  {
   switch(this.TypeDeal())
     {
      case DEAL_TYPE_BUY   :  this.DrawArrowBuy(5, 10);  break;
      case DEAL_TYPE_SELL  :  this.DrawArrowSell(5, 0);  break;
      default              :  break;
     }
  }

Dependendo do tipo de operação (Buy ou Sell), o método de desenho da seta correspondente é chamado. Esse é um método virtual, podendo ser sobrescrito em classes derivadas. Por exemplo, em uma classe filha, é possível desenhar outro ícone para outro tipo de operação ou considerar o modo de alteração da posição, etc.


Método que desenha a máscara da seta Buy no canvas:

//+------------------------------------------------------------------+
//| Рисует на холсте маску стрелки Buy                               |
//+------------------------------------------------------------------+
void CDeal::DrawArrowMaskBuy(const int shift_x, const int shift_y)
  {
   int x[]={4+shift_x, 8+shift_x, 8+shift_x, 6+shift_x, 6+shift_x, 2+shift_x, 2+shift_x, 0+shift_x, 0+shift_x, 4+shift_x};
   int y[]={0+shift_y, 4+shift_y, 5+shift_y, 5+shift_y, 7+shift_y, 7+shift_y, 5+shift_y, 5+shift_y, 4+shift_y, 0+shift_y};
   this.m_canvas.Polygon(x, y, ::ColorToARGB(clrWhite, 220));
  }

As setas usadas como ícones de operações têm uma borda branca (máscara) ao longo do perímetro para destacá-las no fundo escuro da vela:

Como as coordenadas das figuras desenhadas no canvas são sempre definidas em relação às coordenadas locais do canvas, é necessário introduzir deslocamentos que serão adicionados às coordenadas dos pontos da linha quebrada nos eixos X e Y, para centralizar a figura desenhada dentro do canvas. Em seguida, considerando os deslocamentos passados para o método, os valores das coordenadas X e Y são definidos em arrays e o método Polygon() é chamado para desenhar a borda branca nos pontos das coordenadas. Depois, dentro dessa borda, a seta é desenhada com os métodos de desenho de setas.

Método que desenha a seta Buy no canvas:

//+------------------------------------------------------------------+
//| Рисует на холсте стрелку Buy                                     |
//+------------------------------------------------------------------+
void CDeal::DrawArrowBuy(const int shift_x, const int shift_y)
  {
   this.DrawArrowMaskBuy(shift_x, shift_y);
   int x[]={4+shift_x, 7+shift_x, 5+shift_x, 5+shift_x, 3+shift_x, 3+shift_x, 1+shift_x, 4+shift_x};
   int y[]={1+shift_y, 4+shift_y, 4+shift_y, 6+shift_y, 6+shift_y, 4+shift_y, 4+shift_y, 1+shift_y};
   this.m_canvas.Polygon(x, y, ::ColorToARGB(this.m_color_arrow));
   this.m_canvas.Fill(4+shift_x, 4+shift_y, ::ColorToARGB(this.m_color_arrow));
  }

Aqui, primeiramente a máscara da seta Buy é desenhada e depois, nos pontos de coordenada especificados, é desenhada a seta Buy, e o espaço interno é preenchido com uma cor.


Métodos correspondentes para desenhar a seta Sell:

//+------------------------------------------------------------------+
//| Рисует на холсте маску стрелки Sell                              |
//+------------------------------------------------------------------+
void CDeal::DrawArrowMaskSell(const int shift_x, const int shift_y)
  {
   int x[]={4+shift_x, 0+shift_x, 0+shift_x, 2+shift_x, 2+shift_x, 6+shift_x, 6+shift_x, 8+shift_x, 8+shift_x, 4+shift_x};
   int y[]={8+shift_y, 4+shift_y, 3+shift_y, 3+shift_y, 1+shift_y, 1+shift_y, 3+shift_y, 3+shift_y, 4+shift_y, 8+shift_y};
   this.m_canvas.Polygon(x, y, ::ColorToARGB(clrWhite, 220));
  }
//+------------------------------------------------------------------+
//| Рисует на холсте стрелку Sell                                    |
//+------------------------------------------------------------------+
void CDeal::DrawArrowSell(const int shift_x, const int shift_y)
  {
   this.DrawArrowMaskSell(shift_x, shift_y);
   int x[]={4+shift_x, 1+shift_x, 3+shift_x, 3+shift_x, 5+shift_x, 5+shift_x, 7+shift_x, 4+shift_x};
   int y[]={7+shift_y, 4+shift_y, 4+shift_y, 2+shift_y, 2+shift_y, 4+shift_y, 4+shift_y, 7+shift_y};
   this.m_canvas.Polygon(x, y, ::ColorToARGB(this.m_color_arrow));
   this.m_canvas.Fill(4+shift_x, 4+shift_y, ::ColorToARGB(this.m_color_arrow));
  }


Métodos para ocultar e exibir o ícone da operação no gráfico:

//+------------------------------------------------------------------+
//| Скрывает значок сделки на графике                                |
//+------------------------------------------------------------------+
void CDeal::HideArrow(const bool chart_redraw=false)
  {
   ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }
//+------------------------------------------------------------------+
//| Отображает значок сделки на графике                              |
//+------------------------------------------------------------------+
void CDeal::ShowArrow(const bool chart_redraw=false)
  {
   ::ObjectSetInteger(this.m_chart_id, this.m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Para ocultar o objeto no gráfico, basta configurar o atributo de visibilidade do objeto gráfico OBJPROP_TIMEFRAMES para o valor OBJ_NO_PERIODS.

Para exibir o objeto no gráfico, basta definir o atributo OBJPROP_TIMEFRAMES com o valor OBJ_ALL_PERIODS.


Método que define a cor do ícone da operação:

//+------------------------------------------------------------------+
//| Устанавливает цвет значка сделки                                 |
//+------------------------------------------------------------------+
void CDeal::SetColorArrow(const color clr)
  {
   this.m_color_arrow=clr;
   this.DrawLabelView();
  }

A variável m_color_arrow, que armazena a cor do ícone desenhado, recebe o valor passado para o método, e todo o objeto gráfico é redesenhado. Assim, é possível alterar a cor da marca da operação no gráfico "em tempo real" a partir do programa de controle.

Criamos uma classe de operação que fornece acesso às propriedades de uma operação histórica por meio de seu ticket, permitindo obter os dados necessários sobre essa operação e exibir ou ocultar seu ícone no gráfico.

Os objetos desta classe serão armazenados na lista de operações do objeto posição, que, por sua vez, exibirá as propriedades da posição histórica, oferecerá acesso às suas operações e parâmetros e exibirá no gráfico as operações de abertura e fechamento da posição, conectadas por uma linha.


Classe de posições

Na mesma pasta onde criamos a classe de operação, criamos um novo arquivo Position.mqh para a classe de posição CPosition.

De maneira semelhante à classe de operação, para a classe de posição escrevemos uma enumeração das propriedades do objeto para busca e ordenação com base nas propriedades da posição, e incluímos os arquivos da classe de operação e CArrayObj:

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

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

enum ENUM_POSITION_SORT_MODE
  {
   SORT_MODE_POSITION_TICKET = 0,      // Режим сравнения/сортировки по тикету позиции
   SORT_MODE_POSITION_TIME,            // Режим сравнения/сортировки по времени открытия позиции
   SORT_MODE_POSITION_TIME_MSC,        // Режим сравнения/сортировки по времени открытия позиции в миллисекундах
   SORT_MODE_POSITION_TIME_UPDATE,     // Режим сравнения/сортировки по времени изменения позиции
   SORT_MODE_POSITION_TIME_UPDATE_MSC, // Режим сравнения/сортировки по времени изменения позиции в миллисекундах
   SORT_MODE_POSITION_TYPE,            // Режим сравнения/сортировки по типу позиции
   SORT_MODE_POSITION_MAGIC,           // Режим сравнения/сортировки по Magic number позиции
   SORT_MODE_POSITION_IDENTIFIER,      // Режим сравнения/сортировки по идентификатору позиции
   SORT_MODE_POSITION_REASON,          // Режим сравнения/сортировки по причине открытия позиции
   SORT_MODE_POSITION_VOLUME,          // Режим сравнения/сортировки по объему позиции
   SORT_MODE_POSITION_PRICE_OPEN,      // Режим сравнения/сортировки по цене позиции
   SORT_MODE_POSITION_SL,              // Режим сравнения/сортировки по уровню Stop Loss для открытой позиции
   SORT_MODE_POSITION_TP,              // Режим сравнения/сортировки по уровню Take Profit для открытой позиции
   SORT_MODE_POSITION_PRICE_CURRENT,   // Режим сравнения/сортировки по текущей цене по символу
   SORT_MODE_POSITION_SWAP,            // Режим сравнения/сортировки по накопленному свопу
   SORT_MODE_POSITION_PROFIT,          // Режим сравнения/сортировки по текущей прибыли
   SORT_MODE_POSITION_SYMBOL,          // Режим сравнения/сортировки по символу, по которому открыта позиция
   SORT_MODE_POSITION_COMMENT,         // Режим сравнения/сортировки по комментарию к позиции
   SORT_MODE_POSITION_EXTERNAL_ID,     // Режим сравнения/сортировки по идентификатору позиции во внешней системе
   SORT_MODE_POSITION_TIME_CLOSE,      // Режим сравнения/сортировки по времени открытия позиции
   SORT_MODE_POSITION_TIME_CLOSE_MSC,  // Режим сравнения/сортировки по времени открытия позиции в миллисекундах
   SORT_MODE_POSITION_PRICE_CLOSE,     // Режим сравнения/сортировки по цене позиции
  };

//+------------------------------------------------------------------+
//| Класс позиции                                                    |
//+------------------------------------------------------------------+
class CPosition : public CObject
  {
  }

Nas seções protegida e pública da classe, declaramos as variáveis e métodos para o funcionamento da classe:

//+------------------------------------------------------------------+
//| Класс позиции                                                    |
//+------------------------------------------------------------------+
class CPosition : public CObject
  {
private:

protected:
   CArrayObj         m_list_deals;        // Список сделок позиции
   CDeal             m_temp_deal;         // Временный объект-сделка для поиска по свойству в списке
   
//--- Возвращает время с миллисекундами
   string            TimeMscToString(const long time_msc,int flags=TIME_DATE|TIME_MINUTES|TIME_SECONDS) const;

//--- Целочисленные свойства
   long              m_ticket;            // Тикет позиции
   datetime          m_time;              // Время открытия позиции
   long              m_time_msc;          // Время открытия позиции в миллисекундах с 01.01.1970 
   datetime          m_time_update;       // Время изменения позиции
   long              m_time_update_msc;   // Время изменения позиции в миллисекундах с 01.01.1970
   ENUM_POSITION_TYPE m_type;             // Тип позиции
   long              m_magic;             // Magic number для позиции (смотри ORDER_MAGIC)
   long              m_identifier;        // Идентификатор позиции
   ENUM_POSITION_REASON m_reason;         // Причина открытия позиции
 
//--- Вещественные свойства
   double            m_volume;            // Объем позиции
   double            m_price_open;        // Цена позиции
   double            m_sl;                // Уровень Stop Loss для открытой позиции
   double            m_tp;                // Уровень Take Profit для открытой позиции
   double            m_price_current;     // Текущая цена по символу
   double            m_swap;              // Накопленный своп
   double            m_profit;            // Текущая прибыль

//--- Строковые свойства
   string            m_symbol;            // Символ, по которому открыта позиция
   string            m_comment;           // Комментарий к позиции
   string            m_external_id;       // Идентификатор позиции во внешней системе (на бирже)

//--- Дополнительные свойства
   long              m_chart_id;          // Идентификатор графика
   int               m_profit_pt;         // Прибыль в пунктах
   int               m_digits;            // Digits символа
   double            m_point;             // Значение одного пункта символа
   double            m_contract_size;     // Размер торгового контракта символа
   string            m_currency_profit;   // Валюта прибыли символа
   string            m_account_currency;  // Валюта депозита
   
   string            m_line_name;         // Имя графического объекта-линии
   color             m_line_color;        // Цвет соединительной линии

//---Создаёт линию, соединяющую сделки открытия-закрытия
   virtual bool      CreateLine(void);

//--- Возвращает указатель на сделку (1) открытия, (2) закрытия
   CDeal            *GetDealIn(void)   const;
   CDeal            *GetDealOut(void)  const;
   
//--- (1) Скрывает, (2) отображает значки сделок на графике
   void              HideDeals(const bool chart_redraw=false);
   void              ShowDeals(const bool chart_redraw=false);
   
//--- (1) Скрывает, (2) отображает соединительную линию между значками сделок
   void              HideLine(const bool chart_redraw=false);
   void              ShowLine(const bool chart_redraw=false);

public:
//--- Установка свойств
//--- Целочисленные свойства
   void              SetTicket(const long ticket)                    { this.m_ticket=ticket;          }  // Тикет позиции
   void              SetTime(const datetime time)                    { this.m_time=time;              }  // Время открытия позиции
   void              SetTimeMsc(const long value)                    { this.m_time_msc=value;         }  // Время открытия позиции в миллисекундах с 01.01.1970 
   void              SetTimeUpdate(const datetime time)              { this.m_time_update=time;       }  // Время изменения позиции
   void              SetTimeUpdateMsc(const long value)              { this.m_time_update_msc=value;  }  // Время изменения позиции в миллисекундах с 01.01.1970
   void              SetTypePosition(const ENUM_POSITION_TYPE type)  { this.m_type=type;              }  // Тип позиции
   void              SetMagic(const long magic)                      { this.m_magic=magic;            }  // Magic number для позиции (смотри ORDER_MAGIC)
   void              SetID(const long id)                            { this.m_identifier=id;          }  // Идентификатор позиции
   void              SetReason(const ENUM_POSITION_REASON reason)    { this.m_reason=reason;          }  // Причина открытия позиции
 
//--- Вещественные свойства
   void              SetVolume(const double volume)                  { this.m_volume=volume;          }  // Объем позиции
   void              SetPriceOpen(const double price)                { this.m_price_open=price;       }  // Цена позиции
   void              SetSL(const double value)                       { this.m_sl=value;               }  // Уровень Stop Loss для открытой позиции
   void              SetTP(const double value)                       { this.m_tp=value;               }  // Уровень Take Profit для открытой позиции
   void              SetPriceCurrent(const double price)             { this.m_price_current=price;    }  // Текущая цена по символу
   void              SetSwap(const double value)                     { this.m_swap=value;             }  // Накопленный своп
   void              SetProfit(const double value)                   { this.m_profit=value;           }  // Текущая прибыль

//--- Строковые свойства
   void              SetSymbol(const string symbol)                  { this.m_symbol=symbol;          }  // Символ, по которому открыта позиция
   void              SetComment(const string comment)                { this.m_comment=comment;        }  // Комментарий к позиции
   void              SetExternalID(const string ext_id)              { this.m_external_id=ext_id;     }  // Идентификатор позиции во внешней системе (на бирже)


//--- Получение свойств
//--- Целочисленные свойства
   long              Ticket(void)                              const { return(this.m_ticket);         }  // Тикет позиции
   datetime          Time(void)                                const { return(this.m_time);           }  // Время открытия позиции
   long              TimeMsc(void)                             const { return(this.m_time_msc);       }  // Время открытия позиции в миллисекундах с 01.01.1970 
   datetime          TimeUpdate(void)                          const { return(this.m_time_update);    }  // Время изменения позиции
   long              TimeUpdateMsc(void)                       const { return(this.m_time_update_msc);}  // Время изменения позиции в миллисекундах с 01.01.1970
   ENUM_POSITION_TYPE TypePosition(void)                       const { return(this.m_type);           }  // Тип позиции
   long              Magic(void)                               const { return(this.m_magic);          }  // Magic number для позиции (смотри ORDER_MAGIC)
   long              ID(void)                                  const { return(this.m_identifier);     }  // Идентификатор позиции
   ENUM_POSITION_REASON Reason(void)                           const { return(this.m_reason);         }  // Причина открытия позиции
   
//--- Вещественные свойства
   double            Volume(void)                              const { return(this.m_volume);         }  // Объем позиции
   double            PriceOpen(void)                           const { return(this.m_price_open);     }  // Цена позиции
   double            SL(void)                                  const { return(this.m_sl);             }  // Уровень Stop Loss для открытой позиции
   double            TP(void)                                  const { return(this.m_tp);             }  // Уровень Take Profit для открытой позиции
   double            PriceCurrent(void)                        const { return(this.m_price_current);  }  // Текущая цена по символу
   double            Swap(void)                                const { return(this.m_swap);           }  // Накопленный своп
   double            Profit(void)                              const { return(this.m_profit);         }  // Текущая прибыль
   
//--- Строковые свойства
   string            Symbol(void)                              const { return(this.m_symbol);         }  // Символ, по которому открыта позиция
   string            Comment(void)                             const { return(this.m_comment);        }  // Комментарий к позиции
   string            ExternalID(void)                          const { return(this.m_external_id);    }  // Идентификатор позиции во внешней системе (на бирже)
   
//--- Дополнительные свойства
   ulong             DealIn(void)                              const;                                    // Тикет сделки открытия
   ulong             DealOut(void)                             const;                                    // Тикет сделки закрытия
   datetime          TimeClose(void)                           const;                                    // Время закрытия
   long              TimeCloseMsc(void)                        const;                                    // Время закрытия в миллисекундах
   int               ProfitInPoints(void)                      const;                                    // Прибыль в пунктах
   double            PriceClose(void)                          const;                                    // Цена закрытия

//--- Добавляет сделку в список сделок, возвращает указатель
   CDeal            *DealAdd(const long ticket);
   
//--- Устанавливает цвет (1) соединительной линии, (2) сделок Buy и Sell
   void              SetLineColor(const color clr=C'225,68,29');
   void              SetDealsColor(const color clr_deal_buy=C'3,95,172', const color clr_deal_sell=C'225,68,29');

//--- Возвращает описание типа позиции
   string            TypeDescription(void) const;
   
//--- Возвращает описание времени и цены открытия позиции
   string            TimePriceCloseDescription(void);

//--- Возвращает описание времени и цены закрытия позиции
   string            TimePriceOpenDescription(void);
   
//--- Возвращает краткое описание позиции
   string            Description(void);

//--- Возвращает текст всплывающего сообщения позиции
   virtual string    Tooltip(void);
 
//--- Распечатывает в журнале свойства позиции и её сделок
   void              Print(void);
   
//--- (1) Скрывает, (2) отображает графическое представление позиции на графике
   void              Hide(const bool chart_redraw=false);
   void              Show(const bool chart_redraw=false);
   
//--- Сравнивает два объекта между собой по указанному в mode свойству
   virtual int       Compare(const CObject *node, const int mode=0) const;
   
//--- Конструктор/деструктор
                     CPosition(const long position_id, const string symbol);
                     CPosition(void)   { this.m_symbol=::Symbol();   }
                    ~CPosition();
  };

Para a busca em uma lista ordenada na classe de array dinâmico de ponteiros para instâncias da classe CObject e seus derivados CArrayObj, no método de busca Search() é passado um ponteiro para a instância do objeto, com o qual se deve comparar a propriedade pela qual a lista foi ordenada. Para evitar a criação e destruição constantes de um novo objeto com uma propriedade específica, o que é custoso em termos de desempenho, na seção protegida da classe é declarada uma instância de objeto-operação. Para a busca, basta definir o valor necessário da propriedade nesse objeto e passá-lo como a instância de busca no método.

Analisemos os métodos declarados com mais detalhes.


Construtor parametrizado:

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

O construtor recebe o identificador da posição e o símbolo no qual a posição foi aberta. A lista de operações da posição recebe uma flag de ordenação pelo tempo da operação em milissegundos, o identificador e o símbolo são salvos nas variáveis da classe, e alguns parâmetros da conta, do símbolo, do identificador do gráfico e das propriedades da linha que conecta os ícones de abertura e fechamento da posição são configurados.


No destrutor da classe são excluídos todos os objetos gráficos cujo prefixo seja o nome da linha, e a lista de operações é limpa:

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

A exclusão do objeto gráfico-linha é feita por prefixo com base em considerações de que, se na classe derivada for implementado o display de várias linhas conectando todas as operações da posição, e não apenas a operação de abertura com a de fechamento, o nome de cada linha pode ser estruturado como "nome_da_linha" + "número_da_linha". Dessa forma, todas essas linhas ainda serão excluídas no destrutor, pois todas elas têm um prefixo comum, que é o "nome da linha".


Método que compara dois objetos com base em uma propriedade específica:

//+------------------------------------------------------------------+
//| Сравнивает два объекта между собой по указанному свойству        |
//+------------------------------------------------------------------+
int CPosition::Compare(const CObject *node,const int mode=0) const
  {
   const CPosition *obj=node;
   switch(mode)
     {
      case SORT_MODE_POSITION_TICKET         :  return(this.Ticket() > obj.Ticket()                ?  1  :  this.Ticket() < obj.Ticket()                 ? -1  :  0);
      case SORT_MODE_POSITION_TIME           :  return(this.Time() > obj.Time()                    ?  1  :  this.Time() < obj.Time()                     ? -1  :  0);
      case SORT_MODE_POSITION_TIME_MSC       :  return(this.TimeMsc() > obj.TimeMsc()              ?  1  :  this.TimeMsc() < obj.TimeMsc()               ? -1  :  0);
      case SORT_MODE_POSITION_TIME_UPDATE    :  return(this.TimeUpdate() > obj.TimeUpdate()        ?  1  :  this.TimeUpdate() < obj.TimeUpdate()         ? -1  :  0);
      case SORT_MODE_POSITION_TIME_UPDATE_MSC:  return(this.TimeUpdateMsc() > obj.TimeUpdateMsc()  ?  1  :  this.TimeUpdateMsc() < obj.TimeUpdateMsc()   ? -1  :  0);
      case SORT_MODE_POSITION_TYPE           :  return(this.TypePosition() > obj.TypePosition()    ?  1  :  this.TypePosition() < obj.TypePosition()     ? -1  :  0);
      case SORT_MODE_POSITION_MAGIC          :  return(this.Magic() > obj.Magic()                  ?  1  :  this.Magic() < obj.Magic()                   ? -1  :  0);
      case SORT_MODE_POSITION_IDENTIFIER     :  return(this.ID() > obj.ID()                        ?  1  :  this.ID() < obj.ID()                         ? -1  :  0);
      case SORT_MODE_POSITION_REASON         :  return(this.Reason() > obj.Reason()                ?  1  :  this.Reason() < obj.Reason()                 ? -1  :  0);
      case SORT_MODE_POSITION_VOLUME         :  return(this.Volume() > obj.Volume()                ?  1  :  this.Volume() < obj.Volume()                 ? -1  :  0);
      case SORT_MODE_POSITION_PRICE_OPEN     :  return(this.PriceOpen() > obj.PriceOpen()          ?  1  :  this.PriceOpen() < obj.PriceOpen()           ? -1  :  0);
      case SORT_MODE_POSITION_SL             :  return(this.SL() > obj.SL()                        ?  1  :  this.SL() < obj.SL()                         ? -1  :  0);
      case SORT_MODE_POSITION_TP             :  return(this.TP() > obj.TP()                        ?  1  :  this.TP() < obj.TP()                         ? -1  :  0);
      case SORT_MODE_POSITION_PRICE_CURRENT  :  return(this.PriceCurrent() > obj.PriceCurrent()    ?  1  :  this.PriceCurrent() < obj.PriceCurrent()     ? -1  :  0);
      case SORT_MODE_POSITION_SWAP           :  return(this.Swap() > obj.Swap()                    ?  1  :  this.Swap() < obj.Swap()                     ? -1  :  0);
      case SORT_MODE_POSITION_PROFIT         :  return(this.Profit() > obj.Profit()                ?  1  :  this.Profit() < obj.Profit()                 ? -1  :  0);
      case SORT_MODE_POSITION_SYMBOL         :  return(this.Symbol() > obj.Symbol()                ?  1  :  this.Symbol() < obj.Symbol()                 ? -1  :  0);
      case SORT_MODE_POSITION_COMMENT        :  return(this.Comment() > obj.Comment()              ?  1  :  this.Comment() < obj.Comment()               ? -1  :  0);
      case SORT_MODE_POSITION_EXTERNAL_ID    :  return(this.ExternalID() > obj.ExternalID()        ?  1  :  this.ExternalID() < obj.ExternalID()         ? -1  :  0);
      case SORT_MODE_POSITION_TIME_CLOSE     :  return(this.TimeClose() > obj.TimeClose()          ?  1  :  this.TimeClose() < obj.TimeClose()           ? -1  :  0);
      case SORT_MODE_POSITION_TIME_CLOSE_MSC :  return(this.TimeCloseMsc() > obj.TimeCloseMsc()    ?  1  :  this.TimeCloseMsc() < obj.TimeCloseMsc()     ? -1  :  0);
      case SORT_MODE_POSITION_PRICE_CLOSE    :  return(this.PriceClose() > obj.PriceClose()        ?  1  :  this.PriceClose() < obj.PriceClose()         ? -1  :  0);
      default                                :  return -1;
     }
  }

Método semelhante foi discutido no desenvolvimento da classe de objeto-operacao. Aqui, tudo é idêntico: se o valor da propriedade do objeto atual for maior que o do objeto comparado, retorna 1; se for menor, retorna -1; se as propriedades forem iguais, retorna zero.


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'));
  }

É uma cópia do método de mesmo nome da classe de objeto-operação, discutido na classe de operação.


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

Precisamos encontrar na lista de objetos de operações a operação com o modo de alteração de posição "Entry In" — entrada na posição. Na lista ordenada por tempo, essa operação deve ser a primeira. Portanto, o loop começa do início da lista — índice 0.

Obtemos a operação pelo índice e, se for uma operação de entrada na posição, retornamos o ponteiro para a operação encontrada na lista. Ao final do loop, retornamos NULL — operação nã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;
  }

Aqui, ao contrário do método anterior, tudo é invertido, quer dizer, a operação de saída da posição está no final da lista, então o loop começa do final.
Assim que uma operação de saída é encontrada, retornamos o ponteiro para ela. Caso contrário, retornamos o valor NULL.


Métodos para obter propriedades adicionais da posição histórica:

//+------------------------------------------------------------------+
//| Возвращает тикет сделки открытия                                 |
//+------------------------------------------------------------------+
ulong CPosition::DealIn(void) const
  {
   CDeal *deal=this.GetDealIn();
   return(deal!=NULL ? deal.Ticket() : 0);
  }
//+------------------------------------------------------------------+
//| Возвращает тикет сделки закрытия                                 |
//+------------------------------------------------------------------+
ulong CPosition::DealOut(void) const
  {
   CDeal *deal=this.GetDealOut();
   return(deal!=NULL ? deal.Ticket() : 0);
  }
//+------------------------------------------------------------------+
//| Возвращает время закрытия                                        |
//+------------------------------------------------------------------+
datetime CPosition::TimeClose(void) const
  {
   CDeal *deal=this.GetDealOut();
   return(deal!=NULL ? deal.Time() : 0);
  }
//+------------------------------------------------------------------+
//| Возвращает время закрытия в миллисекундах                        |
//+------------------------------------------------------------------+
long CPosition::TimeCloseMsc(void) const
  {
   CDeal *deal=this.GetDealOut();
   return(deal!=NULL ? deal.TimeMsc() : 0);
  }
//+------------------------------------------------------------------+
//| Возвращает цену закрытия                                         |
//+------------------------------------------------------------------+
double CPosition::PriceClose(void) const
  {
   CDeal *deal=this.GetDealOut();
   return(deal!=NULL ? deal.Price() : 0);
  }

Em todos os métodos, primeiro obtemos o ponteiro para a operação da qual precisamos extrair a propriedade correspondente e, se o ponteiro for válido, retornamos o valor da propriedade; caso contrário, retornamos zero.


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(this.TypePosition()==POSITION_TYPE_BUY ? (close-open)/this.m_point : (open-close)/this.m_point);
  }

Primeiro, obtemos os preços de abertura e fechamento das operações correspondentes e depois retornamos o resultado do cálculo do lucro em pontos, dependendo da direção da posição.


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

//+------------------------------------------------------------------+
//| Добавляет сделку в список сделок                                 |
//+------------------------------------------------------------------+
CDeal *CPosition::DealAdd(const long ticket)
  {
//--- Устанавливаем временному объекту тикет искомой сделки и устанавливаем флаг сортировки списка сделок по тикету
   this.m_temp_deal.SetTicket(ticket);
   this.m_list_deals.Sort(SORT_MODE_DEAL_TICKET);
   
//--- Записываем результат проверки присутствия в списке сделки с таким тикетом
   bool added=(this.m_list_deals.Search(&this.m_temp_deal)!=WRONG_VALUE);
   
//--- Устанавливаем списку флаг сортировки по времени в миллисекундах
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);

//--- Если сделка с таким тикетом уже есть в списке - возвращаем NULL
   if(added)
      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.CreateLine();
     }
     
//--- Возвращаем указатель на созданный объект-сделку
   return deal;   
  }

O método recebe o ticket da operação selecionada. Primeiro, verifica-se se não existe uma operação com esse ticket na lista. Para isso, o ticket passado ao método é atribuído ao objeto temporário de operação, a lista de operações é ordenada pelos valores dos tickets, e a busca da operação com esse ticket é realizada na lista. Logo após a busca, a lista de operações volta a ser ordenada pelo tempo em milissegundos. Se a operação já estiver na lista, não é necessário adicioná-la — o método retorna NULL. Por padrão, a lista de operações já está ordenada por tempo em milissegundos. Se essa operação não estiver na lista, um novo objeto de operação é criado e adicionado à lista na ordem de classificação por tempo em milissegundos. Se for uma operação de fechamento, o lucro é registrado nas propriedades da posição, e uma linha conectando as operações de abertura e fechamento é criada. Por fim, o método retorna o ponteiro para o novo objeto de operação criado.


Método que retorna a descrição do tipo de posição:

//+------------------------------------------------------------------+
//| Возвращает описание типа позиции                                 |
//+------------------------------------------------------------------+
string CPosition::TypeDescription(void) const
  {
   return(this.m_type==POSITION_TYPE_BUY ? "Buy" : this.m_type==POSITION_TYPE_SELL ? "Sell" : "Unknown::"+(string)this.m_type);
  }

Dependendo do tipo de posição armazenado na variável m_type, a descrição é retornada. Se a variável contiver um valor que não coincide com as constantes de enumeração do tipo de posição, retorna-se o valor "Unknown::"+valor_da_variável.


Método que retorna descrição do horário e do preço de fechamento da posição:

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

Se ainda não houver uma operação de fechamento na posição, o método retornará a mensagem de que a posição ainda não foi fechada. Caso contrário, uma string no formato é criada e retornada:

Closed 2023.06.12 17:04:20.362 [1.07590]


Método que retorna a descrição do horário e do preço de abertura da posição:

//+------------------------------------------------------------------+
//| Возвращает описание времени и цены открытия позиции              |
//+------------------------------------------------------------------+
string CPosition::TimePriceOpenDescription(void)
  {
   return(::StringFormat("Opened %s [%.*f]", this.TimeMscToString(this.TimeMsc()),this.m_digits, this.PriceOpen()));
  }

Aqui, uma string no formato é criada e retornada:

Opened 2023.06.12 16:51:36.838 [1.07440]


Método que retorna uma descrição breve da posição:

//+------------------------------------------------------------------+
//| Возвращает краткое описание позиции                              |
//+------------------------------------------------------------------+
string CPosition::Description(void)
  {
   return(::StringFormat("Position %s %.2f %s #%I64d, Magic %I64d",
                         this.Symbol(), this.Volume(), this.TypeDescription(), this.ID(), this.Magic()));
  }

Uma string no formato é criada e retornada:

Position EURUSD 0.10 Buy #1752955040, Magic 123


Método virtual que retorna o texto da mensagem pop-up da posição:

//+------------------------------------------------------------------+
//| Возвращает текст всплывающего сообщения позиции                  |
//+------------------------------------------------------------------+
string CPosition::Tooltip(void)
  {
//--- Получаем указатели на сделки открытия и закрытия
   CDeal *deal_in =this.GetDealIn();
   CDeal *deal_out=this.GetDealOut();
   
//--- Если сделки не получены - возвращаем пустую строку
   if(deal_in==NULL || deal_out==NULL)
      return NULL;
      
//--- Получаем общие для двух сделок комиссию, своп и оплату за сделку
   double commission=deal_in.Commission()+deal_out.Commission();
   double swap=deal_in.Swap()+deal_out.Swap();
   double fee=deal_in.Fee()+deal_out.Fee();
   
//--- Получаем итоговый профит позиции и значения спреда при открытии и закрытии
   double profit=deal_out.Profit();
   int    spread_in=deal_in.Spread();
   int    spread_out=deal_out.Spread();
   
//--- Если причина закрытия позиции StopLoss, TakeProfit или StopOut - установим в переменную описание причины
   string reason=(deal_out.Reason()==DEAL_REASON_SL || deal_out.Reason()==DEAL_REASON_TP || deal_out.Reason()==DEAL_REASON_SO ? deal_out.ReasonDescription() : "");
   
//--- Создаём и возвращаем строку всплывающей подсказки
   return(::StringFormat("%s\nCommission %.2f, Swap %.2f, Fee %.2f\nSpread In/Out %d/%d, Profit %+.2f %s (%d points)\nResult: %s %+.2f %s",
                         this.Description(), commission, swap, fee, spread_in, spread_out, profit,this.m_currency_profit, 
                         this.ProfitInPoints(), reason, profit+commission+fee+swap, this.m_currency_profit));
  }

O método cria e retorna uma string no formato:

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

Este texto é definido como a dica pop-up para a linha que conecta as operações de abertura e fechamento da posição. O método é virtual e pode ser sobrescrito em classes derivadas, caso seja necessário exibir uma informação ligeiramente diferente. Ao usar a string retornada pelo método como texto de dica pop-up, é importante considerar o comprimento da string, já que há um limite de aproximadamente 160 caracteres para a dica, incluindo os códigos de controle. Infelizmente, não posso fornecer o valor exato, já que ele foi determinado empiricamente.


Método que exibe os ícones das operações no gráfico:

//+------------------------------------------------------------------+
//| Отображает значки сделок на графике                              |
//+------------------------------------------------------------------+
void CPosition::ShowDeals(const bool chart_redraw=false)
  {
   for(int i=this.m_list_deals.Total()-1; i>=0; i--)
     {
      CDeal *deal=this.m_list_deals.At(i);
      if(deal==NULL)
         continue;
      deal.ShowArrow();
     }
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

No loop pela lista de operações, obtemos cada operação e chamamos o método ShowArrow() do objeto obtido, exibindo o ícone da operação no gráfico. Ao final do loop, o gráfico é redesenhado se a flag para isso estiver definida.


Método que oculta os ícones das operações no gráfico:

//+------------------------------------------------------------------+
//| Скрывает значки сделок на графике                                |
//+------------------------------------------------------------------+
void CPosition::HideDeals(const bool chart_redraw=false)
  {
   for(int i=this.m_list_deals.Total()-1; i>=0; i--)
     {
      CDeal *deal=this.m_list_deals.At(i);
      if(deal==NULL)
         continue;
      deal.HideArrow();
     }
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

No loop pela lista de operações, obtemos cada operação e chamamos o método HideArrow() do objeto obtido, ocultando o ícone da operação no gráfico. Ao final do loop, o gráfico é redesenhado se a flag para isso estiver definida.


Método que cria a linha conectando as operações de abertura e fechamento:

//+------------------------------------------------------------------+
//| Создаёт линию, соединяющую сделки открытия-закрытия              |
//+------------------------------------------------------------------+
bool CPosition::CreateLine(void)
  {
//--- Если графический объект-линию создать не удалось - сообщаем об это в журнал и возвращаем false
   ::ResetLastError();
   if(!::ObjectCreate(this.m_chart_id, this.m_line_name, OBJ_TREND, 0, 0, 0, 0, 0))
     {
      ::Print("ObjectCreate() failed. Error ", ::GetLastError());
      return false;
     }
//--- Скрываем линию
   this.HideLine();

//--- Устанавливаем для линии рисование точками, устанавливаем цвет и возвращаем true
   ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_STYLE, STYLE_DOT);
   ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_COLOR, this.m_line_color);
   return true;
  }

Uma linha é criada com coordenadas de preço e tempo na posição zero (preço igual a 0, tempo igual a 01.01.1970 00:00:00), a linha é oculta no gráfico, e seu estilo de desenho em pontos e cor padrão são definidos.
Inicialmente, as linhas de cada objeto são criadas em modo oculto, e apenas no momento em que precisam ser exibidas, recebem as coordenadas e a exibição apropriadas usando


Método que exibe a linha conectiva entre os ícones das operações:

//+------------------------------------------------------------------+
//| Отображает соединительную линию между значками сделок            |
//+------------------------------------------------------------------+
void CPosition::ShowLine(const bool chart_redraw=false)
  {
//--- Получаем указатели на сделки открытия и закрытия
   CDeal *deal_in= this.GetDealIn();
   CDeal *deal_out=this.GetDealOut();
   
//--- Если сделки не получены - уходим
   if(deal_in==NULL || deal_out==NULL)
      return;
      
//--- Устанавливаем для линии начальные и конечные время и цену из свойств сделок и текст всплывающей подсказки
   ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIME, 0, deal_in.Time());
   ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIME, 1, deal_out.Time());
   ::ObjectSetDouble(this.m_chart_id, this.m_line_name, OBJPROP_PRICE, 0, deal_in.Price());
   ::ObjectSetDouble(this.m_chart_id, this.m_line_name, OBJPROP_PRICE, 1, deal_out.Price());
   ::ObjectSetString(this.m_chart_id, this.m_line_name, OBJPROP_TOOLTIP, this.Tooltip());
   
//--- Показываем линию на графике и, если установлен флаг, обновляем график
   ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }


Método que oculta a linha conectiva entre os ícones das operações:

//+------------------------------------------------------------------+
//| Скрывает соединительную линию между значками сделок              |
//+------------------------------------------------------------------+
void CPosition::HideLine(const bool chart_redraw=false)
  {
   ::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Para o objeto, a flag de visibilidade é desativada em todos os períodos do gráfico e, se a flag estiver definida, o gráfico é atualizado.


Método que define a cor da linha conectiva:

//+------------------------------------------------------------------+
//| Устанавливает цвет соединительной линии                          |
//+------------------------------------------------------------------+
void CPosition::SetLineColor(const color clr=C'225,68,29')
  {
   if(::ObjectSetInteger(this.m_chart_id, this.m_line_name, OBJPROP_COLOR, clr))
      this.m_line_color=clr;
  }

Para o objeto, o valor da cor passado ao método é atribuído à propriedade de cor e, se tudo for bem-sucedido, essa cor é armazenada na variável m_line_color.


Método que define a cor das operações Buy e Sell:

//+------------------------------------------------------------------+
//| Устанавливает цвет сделок Buy и Sell                             |
//+------------------------------------------------------------------+
void CPosition::SetDealsColor(const color clr_deal_buy=C'3,95,172', const color clr_deal_sell=C'225,68,29')
  {
//--- В цикле по списку сделок
   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;
      //--- Если тип сделки Buy - устанавливаем в объект цвет для сделки Buy
      if(deal.TypeDeal()==DEAL_TYPE_BUY)
         deal.SetColorArrow(clr_deal_buy);
      //--- Если тип сделки Sell - устанавливаем в объект цвет для сделки Sell
      if(deal.TypeDeal()==DEAL_TYPE_SELL)
         deal.SetColorArrow(clr_deal_sell);
     }
  }

O método recebe duas cores, uma para a operação Buy e outra para a operação Sell. No loop pela lista de operações da posição, obtemos o ponteiro para cada operação e definimos a cor do ícone conforme o tipo da operação.


Método que exibe a representação gráfica da posição no gráfico:

//+------------------------------------------------------------------+
//| Отображает графическое представление позиции на графике          |
//+------------------------------------------------------------------+
void CPosition::Show(const bool chart_redraw=false)
  {
   this.ShowDeals(false);
   this.ShowLine(chart_redraw);
  }

No método, os métodos protegidos são chamados sequencialmente, exibindo as operações de entrada e saída e a linha conectiva no gráfico.


Método que oculta a representação gráfica da posição no gráfico:

//+------------------------------------------------------------------+
//| Скрывает графическое представление позиции на графике            |
//+------------------------------------------------------------------+
void CPosition::Hide(const bool chart_redraw=false)
  {
   this.HideLine(false);
   this.HideDeals(chart_redraw);
  }

No método, os métodos protegidos são chamados sequencialmente, ocultando as operações de entrada e saída e a linha conectiva no gráfico.

Os dois métodos analisados acima são públicos e usados para controlar a exibição da posição no gráfico a partir do programa de controle.


Método que imprime no log as propriedades da posição e suas operações:

//+------------------------------------------------------------------+
//| Распечатывает в журнале свойства позиции и её сделок             |
//+------------------------------------------------------------------+
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, um cabeçalho com a descrição breve da posição e duas linhas com o horário e o preço de abertura e fechamento da posição são exibidos. Em seguida, no loop, são impressas as descrições de todas as operações da posição na lista de operações.

No final, obtemos uma impressão no log no seguinte formato:

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

Não é muito informativo, mas os métodos Print() nestas classes foram feitos apenas para depuração, e isso é suficiente.

Agora temos duas classes: a classe de operação e a classe de posição, que contém a lista de operações envolvidas na posição. As classes são simples e contêm as informações principais sobre as operações e algumas informações adicionais sobre preços e horários de abertura/fechamento da posição e seu lucro em pontos. Os demais métodos são usados para obter e exibir essa informação no gráfico ou em uma string de texto.

Agora vamos criar uma classe resumida, onde todas as posições serão reunidas a partir das operações e colocadas em uma lista de posições históricas. A classe fornecerá acesso às propriedades das posições e de suas operações, atualizará e complementará a lista de posições históricas e exibirá uma posição específica no gráfico.


Classe de gerenciamento de histórico de posições

Na mesma pasta onde criamos as duas classes anteriores, vamos criar um novo arquivo PositionsControl.mqh para a classe CPositionsControl.

A classe deve herdar do objeto base da Biblioteca Padrão CObject, e o arquivo da classe de posições é incluído no novo arquivo da classe CPositionsControl:

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

#include "Position.mqh"

//+------------------------------------------------------------------+
//| Класс исторических позиций                                       |
//+------------------------------------------------------------------+
class CPositionsControl : public CObject
  {
  }

Nas seções privada, protegida e pública, declararemos as variáveis e métodos para trabalhar com a classe:

//+------------------------------------------------------------------+
//| Класс исторических позиций                                       |
//+------------------------------------------------------------------+
class CPositionsControl : public CObject
  {
private:
   string            m_symbol;            // Инструмент, по которому открыта позиция
   long              m_current_id;        // Идентификатор текущей отображённой на графике позиции
   bool              m_key_ctrl;          // Флаг разрешение на управление графиком с помощью клавиатуры 
   
//--- Возвращает тип позиции по типу сделки
   ENUM_POSITION_TYPE PositionTypeByDeal(const CDeal *deal);

protected:
   CPosition         m_temp_pos;          // Временный объект позиции для поиска
   CArrayObj         m_list_pos;          // Список позиций
   long              m_chart_id;          // Идентификатор графика
   
//--- Возвращает объект-позицию из списка по идентификатору
   CPosition        *GetPositionObjByID(const long id);

//--- Возвращает флаг того, что позиция рыночная
   bool              IsMarketPosition(const long id);
   
//--- Возвращает указатель на (1) первую, (2) последнюю закрытую позицию в списке
   CPosition        *GetFirstClosedPosition(void);
   CPosition        *GetLastClosedPosition(void);

//--- Возвращает указатель на (1) предыдущую, (2) следующую закрытую позицию в списке
   CPosition        *GetPrevClosedPosition(CPosition *current);
   CPosition        *GetNextClosedPosition(CPosition *current);

//--- Отображает на графике графическое представление указанной позиции
   void              Show(CPosition *pos, const bool chart_redraw=false);
   
//--- Центрирует график на текущей выбранной позиции
   void              CentersChartByCurrentSelected(void);

//--- Возвращает идентификатор текущей выбранной позиции
   long              CurrentSelectedID(void)    const { return this.m_current_id;         }

//--- Возвращает время (1) открытия, (2) закрытия текущей выбранной позиции
   datetime          TimeOpenCurrentSelected(void);
   datetime          TimeCloseCurrentSelected(void);
   
//--- Скрывает на графике графическое представление всех позиций, кроме указанной
   void              HideAllExceptOne(const long pos_id, const bool chart_redraw=false);
   
public:
//--- Возвращает (1) символ, (2) идентификатор графика
   string            Symbol(void)               const { return this.m_symbol;             }
   long              ChartID(void)              const { return this.m_chart_id;           }

//--- Создаёт и обновляет список позиций. Может быть переопределён в наследуемых классах
   virtual bool      Refresh(void);
   
//--- Возвращает количество позиций в списке
   int               Total(void)                const { return this.m_list_pos.Total();   }

//--- (1) Скрывает, (2) отображает на графике графическое представление первой позиции
   void              HideFirst(const bool chart_redraw=false);
   void              ShowFirst(const bool chart_redraw=false);
   
//--- (1) Скрывает, (2) отображает на графике графическое представление последней позиции
   void              HideLast(const bool chart_redraw=false);
   void              ShowLast(const bool chart_redraw=false);
   
//--- Отображает на графике графическое представление (1) текущей, (2) предыдущей, (3) следующей позиции
   void              ShowCurrent(const bool chart_redraw=false);
   void              ShowPrev(const bool chart_redraw=false);
   void              ShowNext(const bool chart_redraw=false);

//--- Возвращает описание текущей выбранной позиции
   string            CurrentSelectedDescription(void);

//--- Распечатывает в журнале свойства всех позиций в списке и их сделок
   void              Print(void);

//--- Конструктор/деструктор
                     CPositionsControl(const string symbol=NULL);
                    ~CPositionsControl();
  };

Analisemos em detalhe os métodos declarados.


Construtor da classe:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CPositionsControl::CPositionsControl(const string symbol=NULL)
  {
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
   this.m_symbol     =  (symbol==NULL ? ::Symbol() : symbol);
   this.m_chart_id   =  ::ChartID();
   this.m_current_id =  0;
   this.m_key_ctrl   =  ::ChartGetInteger(this.m_chart_id, CHART_KEYBOARD_CONTROL);
  }

Definimos na lista de posições históricas a flag de ordenação pelo tempo de fechamento em milissegundos. Gravamos na variável o símbolo passado ao construtor. Se for uma string vazia, então o símbolo atual do gráfico no qual o programa está sendo executado. O identificador do gráfico é configurado como o identificador do gráfico atual. Nas classes herdadas, será possível definir um identificador diferente. Na variável m_key_ctrl, armazenamos a flag que permite o controle do gráfico via teclado. Após a execução do programa, esse valor será restaurado na propriedade do gráfico para retornar ao estado em que estava antes do início do programa, pois a execução altera ativamente essa configuração.


No destrutor da classe, a lista de posições é destruída e a propriedade do gráfico CHART_KEYBOARD_CONTROL é restaurada ao valor que estava antes do início do programa:

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


Método que retorna o objeto-posição da lista pelo identificador:

//+------------------------------------------------------------------+
//| Возвращает объект-позицию из списка по идентификатору            |
//+------------------------------------------------------------------+
CPosition *CPositionsControl::GetPositionObjByID(const long id)
  {
//--- Устанавливаем временному объекту идентификатор позиции, а списку - флаг сортировки по идентификатору позиции
   this.m_temp_pos.SetID(id);
   this.m_list_pos.Sort(SORT_MODE_POSITION_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(SORT_MODE_POSITION_TIME_CLOSE_MSC);
   return pos;
  }

A lógica do método está totalmente explicada nos comentários do código.


Método que retorna a flag indicando que 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;
  }

Ao criar a lista de posições a partir da lista de operações, é importante desconsiderar as posições de mercado atuais e não adicioná-las à lista de posições históricas. Para verificar se a posição ainda não foi fechada, é preciso procurá-la na lista de posições ativas pelo seu identificador. Se tal posição existir, ela não precisa ser adicionada à lista. O método busca posições na lista de mercado pelo seu ticket, verifica a correspondência do identificador da posição com o valor passado para o método e, se tal posição existir (e puder ser selecionada), retorna true. Caso contrário, retorna false.


Método que retorna o ponteiro para a primeira posição fechada na lista:

//+------------------------------------------------------------------+
//| Возвращает указатель на первую закрытую позицию в списке         |
//+------------------------------------------------------------------+
CPosition *CPositionsControl::GetFirstClosedPosition(void)
  {
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
   return this.m_list_pos.At(0);
  }

A lista de posições é ordenada pelo tempo de fechamento em milissegundos, e o ponteiro para a primeira posição na lista (a mais antiga) é retornado.


Método que retorna o ponteiro para a última posição fechada na lista:

//+------------------------------------------------------------------+
//| Возвращает указатель на последнюю закрытую позицию в списке      |
//+------------------------------------------------------------------+
CPosition *CPositionsControl::GetLastClosedPosition(void)
  {
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
   return this.m_list_pos.At(this.m_list_pos.Total()-1);
  }

A lista de posições é ordenada pelo tempo de fechamento em milissegundos, e o ponteiro para a última posição na lista é retornado.


Método que retorna o ponteiro para a posição fechada anterior na lista:

//+------------------------------------------------------------------+
//| Возвращает указатель на предыдущую закрытую позицию в списке     |
//+------------------------------------------------------------------+
CPosition *CPositionsControl::GetPrevClosedPosition(CPosition *current)
  {
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
   int prev=this.m_list_pos.SearchLess(current);
   return this.m_list_pos.At(prev);
  }

Com base na posição atual selecionada, cujo ponteiro é passado ao método, a posição anterior na lista, ordenada pelo tempo de fechamento em milissegundos, é encontrada. O método SearchLess() da classe CArrayObj retorna o ponteiro para o primeiro objeto encontrado na lista que possui um valor menor, pelo qual a lista foi ordenada. Neste caso, a lista está ordenada pelo tempo de fechamento em milissegundos. Assim, o primeiro objeto encontrado com tempo de fechamento menor que o passado ao método é a posição anterior. Se o objeto-posição não for encontrado ou se o primeiro elemento da lista (sem anteriores) for passado ao método, ele retorna NULL.


Método que retorna o ponteiro para a próxima posição fechada na lista:

//+------------------------------------------------------------------+
//| Возвращает указатель на следующую закрытую позицию в списке      |
//+------------------------------------------------------------------+
CPosition *CPositionsControl::GetNextClosedPosition(CPosition *current)
  {
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
   int next=this.m_list_pos.SearchGreat(current);
   return this.m_list_pos.At(next);
  }

Com base na posição atual selecionada, cujo ponteiro é passado ao método, a próxima posição na lista, ordenada pelo tempo de fechamento em milissegundos, é localizada. O método SearchGreat() da classe CArrayObj retorna o ponteiro para o primeiro objeto encontrado na lista que possui um valor maior, pelo qual a lista foi ordenada. Neste caso, a lista está ordenada pelo tempo de fechamento em milissegundos. Assim, o primeiro objeto encontrado com tempo de fechamento maior que o passado ao método é a próxima posição. Se o objeto-posição não for encontrado ou se o último elemento da lista (sem próximos) for passado ao método, ele retorna NULL.


Método que exibe no gráfico a representação gráfica da posição especificada:

//+------------------------------------------------------------------+
//| Выводит на график графическое представление указанной позиции    |
//+------------------------------------------------------------------+
void CPositionsControl::Show(CPosition *pos,const bool chart_redraw=false)
  {
   if(pos!=NULL)
     {
      pos.Show(chart_redraw);
      this.m_current_id=pos.ID();
     }
  }

O método recebe o ponteiro para a posição cuja representação gráfica deve ser exibida no gráfico. Se o objeto válido for recebido, chamamos seu método Show() e gravamos o identificador dessa posição na variável m_current_id. Pelo valor do identificador da posição na variável m_current_id podemos determinar a posição atualmente selecionada. Considera-se selecionada a posição cuja representação gráfica está exibida no gráfico.


Método que oculta do gráfico a representação gráfica da primeira posição:

//+------------------------------------------------------------------+
//| Скрывает с графика графическое представление первой позиции      |
//+------------------------------------------------------------------+
void CPositionsControl::HideFirst(const bool chart_redraw=false)
  {
   CPosition *pos=this.GetFirstClosedPosition();
   if(pos!=NULL)
      pos.Hide(chart_redraw);
  }

Obtemos o ponteiro para a primeira posição na lista e chamamos seu método Hide().


Método que exibe no gráfico a representação gráfica da primeira posição:

//+------------------------------------------------------------------+
//| Отображает на графике графическое представление первой позиции   |
//+------------------------------------------------------------------+
void CPositionsControl::ShowFirst(const bool chart_redraw=false)
  {
//--- Получаем указатель на первую закрытую позицию
   CPosition *pos=this.GetFirstClosedPosition();
   if(pos==NULL)
      return;
//--- Скрываем значки всех позиций, кроме первой по её идентификатору
   this.HideAllExceptOne(pos.ID());
   
//--- Отображаем значки первой позиции на графике и
//--- центрируем график по значкам текущей выбранной позиции
   this.Show(pos,chart_redraw);
   this.CentersChartByCurrentSelected();
  }

Com o método GetFirstClosedPosition(), descrito acima, obtemos o ponteiro para a primeira posição na lista, ordenada pelo tempo em milissegundos. Ocultamos todos os ícones de todas as posições, exceto a primeira, exibimos a primeira posição e centralizamos o gráfico com base nos ícones de suas operações no gráfico.


Método que oculta a representação gráfica da última posição no gráfico:

//+------------------------------------------------------------------+
//| Скрывает с графика графическое представление последней позиции   |
//+------------------------------------------------------------------+
void CPositionsControl::HideLast(const bool chart_redraw=false)
  {
   CPosition *pos=this.GetLastClosedPosition();
   if(pos!=NULL)
      pos.Hide(chart_redraw);
  }

Obtemos o ponteiro para a última posição na lista e chamamos seu método Hide().


Método que exibe a representação gráfica da última posição no gráfico:

//+------------------------------------------------------------------+
//| Отображает на графике графическое представление последней позиции|
//+------------------------------------------------------------------+
void CPositionsControl::ShowLast(const bool chart_redraw=false)
  {
//--- Получаем указатель на последнюю закрытую позицию
   CPosition *pos=this.GetLastClosedPosition();
   if(pos==NULL)
      return;
//--- Скрываем значки всех позиций, кроме последней по её идентификатору
   this.HideAllExceptOne(pos.ID(), false);
   
//--- Отображаем значки последней позиции на графике и
//--- центрируем график по значкам текущей выбранной позиции
   this.Show(pos,chart_redraw);
   this.CentersChartByCurrentSelected();
  }

Com o método GetLastClosedPosition(), descrito acima, obtemos o ponteiro para a última posição na lista, ordenada pelo tempo em milissegundos. Ocultamos todos os ícones de todas as posições, exceto a última, exibimos a última posição e centralizamos o gráfico com base nos ícones de suas operações no gráfico.


Método que exibe a representação gráfica da posição atual no gráfico:

//+------------------------------------------------------------------+
//| Отображает на графике графическое представление текущей позиции  |
//+------------------------------------------------------------------+
void CPositionsControl::ShowCurrent(const bool chart_redraw=false)
  {
//--- Получаем указатель на текущую выбранную закрытую позицию
   CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID());
   if(curr==NULL)
      return;
//--- Отображаем значки текущей позиции на графике и
//--- центрируем график по значкам текущей выбранной позиции
   this.Show(curr,chart_redraw);
   this.CentersChartByCurrentSelected();
  }

Obtemos o ponteiro para a posição cujo identificador está registrado na variável m_current_id, exibimos sua representação gráfica no gráfico e centralizamos o gráfico com base nos ícones de suas operações no gráfico.


Método que exibe a representação gráfica da posição anterior no gráfico:

//+------------------------------------------------------------------+
//|Отображает на графике графическое представление предыдущей позиции|
//+------------------------------------------------------------------+
void CPositionsControl::ShowPrev(const bool chart_redraw=false)
  {
//--- Получаем указатель на текущую и предыдущую позиции
   CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID());
   CPosition *prev=this.GetPrevClosedPosition(curr);
   if(curr==NULL || prev==NULL)
      return;
//--- Текущую позицию скрываем, предыдущую отображаем и
//--- центрируем график по значкам текущей выбранной позиции
   curr.Hide();
   this.Show(prev,chart_redraw);
   this.CentersChartByCurrentSelected();
  }

Aqui, obtemos os ponteiros para a posição atual selecionada e para a posição anterior na lista. Ocultamos os ícones da posição atual e exibimos os ícones da posição anterior. Ao exibir esses ícones, o identificador dessa posição é registrado na variável m_current_id, indicando que agora esta é a posição atual. Centralizamos o gráfico com base nos ícones dessa posição.


Método que exibe a representação gráfica da próxima posição no gráfico:

//+------------------------------------------------------------------+
//| Отображает на графике графическое представление следующей позиции|
//+------------------------------------------------------------------+
void CPositionsControl::ShowNext(const bool chart_redraw=false)
  {
//--- Получаем указатель на текущую и следующую позиции
   CPosition *curr=this.GetPositionObjByID(this.CurrentSelectedID());
   CPosition *next=this.GetNextClosedPosition(curr);
   if(curr==NULL || next==NULL)
      return;
//--- Текущую позицию скрываем, следующую отображаем и
//--- центрируем график по значкам текущей выбранной позиции
   curr.Hide();
   this.Show(next,chart_redraw);
   this.CentersChartByCurrentSelected();
  }

Este método é idêntico ao anterior, exceto pelo fato de que obtemos os ponteiros para a posição atual selecionada e para a próxima posição na lista. Ocultamos os ícones da posição atual e exibimos os ícones da próxima posição. Ao exibir esses ícones, o identificador dessa posição é registrado na variável m_current_id, indicando que agora esta é a posição atual. Centralizamos o gráfico com base nos ícones dessa posição.


Método que oculta a representação gráfica de todas as posições, exceto a especificada:

//+------------------------------------------------------------------+
//| Скрывает с графика графическое представление                     |
//| всех позиций, кроме указанной                                    |
//+------------------------------------------------------------------+
void CPositionsControl::HideAllExceptOne(const long pos_id,const bool chart_redraw=false)
  {
//--- В цикле по списку позиций
   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 || pos.ID()==pos_id)
         continue;
      //--- скрываем графическое представление позиции
      pos.Hide();
     }
//--- После цикла при установленном флаге обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

O método recebe o identificador da posição cujos ícones devem permanecer no gráfico. Os ícones de todas as outras posições são ocultados.


Método que centraliza o gráfico na posição atualmente selecionada:

//+------------------------------------------------------------------+
//| Центрирует график на текущей выбранной позиции                   |
//+------------------------------------------------------------------+
void CPositionsControl::CentersChartByCurrentSelected(void)
  {
//--- Получаем индекс первого видимого бара на графике и количество видимых баров
   int bar_open=0, bar_close=0;
   int first_visible=(int)::ChartGetInteger(this.m_chart_id, CHART_FIRST_VISIBLE_BAR);
   int visible_bars =(int)::ChartGetInteger(this.m_chart_id, CHART_VISIBLE_BARS);
   
//--- Получаем время открытия позиции, а по нему - бар открытия
   datetime time_open=this.TimeOpenCurrentSelected();
   if(time_open!=0)
      bar_open=::iBarShift(this.m_symbol, PERIOD_CURRENT, time_open);
   
//--- Получаем время закрытия позиции, а по нему - бар закрытия
   datetime time_close=this.TimeCloseCurrentSelected();
   if(time_close!=0)
      bar_close=::iBarShift(this.m_symbol, PERIOD_CURRENT, time_close);
      
//--- Рассчитаем ширину окна, в котором расположены значки сделки
   int width=bar_open-bar_close;
   
//--- Рассчитаем смещение графика так,  чтобы окно со сделками находилось по центру графика
   int shift=(bar_open + visible_bars/2 - width/2);
   
//--- Если ширина окна больше ширины графика, то сделка открытия будет располагаться на втором видимом баре
   if(shift-bar_open<0)
      shift=bar_open+1;
   
//--- Если бар сделки открытия левее первого видимого бара графика,
//--- или бар сделки открытия правее последнего видимого бара графика -
//--- прокручиваем график на рассчитанное смещение
   if(bar_open>first_visible || bar_open<first_visible+visible_bars)
      ::ChartNavigate(this.m_chart_id, CHART_CURRENT_POS, first_visible-shift);
  }

Toda a lógica do método está detalhada nos comentários do código. O método desloca o gráfico do ativo para que, visualmente, todas as operações da posição, junto com a linha conectiva, fiquem centralizadas no gráfico. Se os ícones de todas as operações não couberem na largura do gráfico, o gráfico é ajustado para que a operação de abertura da posição esteja na segunda barra visível à esquerda.


Método que retorna o horário de abertura da posição atualmente selecionada:

//+------------------------------------------------------------------+
//| Возвращает время открытия текущей выбранной позиции              |
//+------------------------------------------------------------------+
datetime CPositionsControl::TimeOpenCurrentSelected(void)
  {
   CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID());
   return(pos!=NULL ? pos.Time() : 0);
  }

Obtemos o ponteiro para a posição atual selecionada com base no identificador registrado na variável m_current_id e, ao obter o ponteiro com sucesso, retornamos o horário de abertura da posição. Caso contrário, retornamos zero.


Método que retorna o horário de fechamento da posição atualmente selecionada:

//+------------------------------------------------------------------+
//| Возвращает время закрытия текущей выбранной позиции              |
//+------------------------------------------------------------------+
datetime CPositionsControl::TimeCloseCurrentSelected(void)
  {
   CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID());
   return(pos!=NULL ? pos.TimeClose() : 0);
  }

Obtemos o ponteiro para a posição atual selecionada com base no identificador registrado na variável m_current_id e, ao obter o ponteiro com sucesso, retornamos o horário de fechamento da posição. Caso contrário, retornamos zero.


Método que retorna o tipo de posição com base no tipo de 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;
     }
  }

O método recebe o ponteiro para uma operação e, dependendo do tipo da operação, retorna o tipo correspondente da posição.


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

//+------------------------------------------------------------------+
//| Создаёт список исторических позиций                              |
//+------------------------------------------------------------------+
bool CPositionsControl::Refresh(void)
  {
//--- Если запросить историю сделок и ордеров не удалось - возвращаем false
   if(!::HistorySelect(0,::TimeCurrent()))
      return false;
      
//--- Ставим списку позиций флаг сортировки по времени в миллисекундах
   this.m_list_pos.Sort(SORT_MODE_POSITION_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) || ::HistoryDealGetString(ticket, DEAL_SYMBOL)!=this.m_symbol)
         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 и идём далее
         pos=new CPosition(pos_id, this.m_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.SetTime(deal.Time());
         pos.SetTimeMsc(deal.TimeMsc());
         ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal);
         pos.SetTypePosition(type);
         pos.SetPriceOpen(deal.Price());
         pos.SetVolume(deal.Volume());
        }
      if(deal.Entry()==DEAL_ENTRY_OUT || deal.Entry()==DEAL_ENTRY_OUT_BY)
        {
         pos.SetPriceCurrent(deal.Price());
        }
      if(deal.Entry()==DEAL_ENTRY_INOUT)
        {
         ENUM_POSITION_TYPE type=this.PositionTypeByDeal(deal);
         pos.SetTypePosition(type);
         pos.SetVolume(deal.Volume()-pos.Volume());
        }
     }
//--- Устанавливаем  списку позиций флаг сортировки по времени закрытия в миллисекундах
   this.m_list_pos.Sort(SORT_MODE_POSITION_TIME_CLOSE_MSC);
     
//--- Возвращаем результат создания и добавления позиции в список
   return res;
  }

A lógica do método está detalhadamente descrita nos comentários do código.

Em resumo sobre a lógica de busca das posições históricas: no terminal do cliente, há apenas uma lista de posições ativas. Cada posição possui suas operações, que estão na lista de operações históricas. Em cada operação, o identificador da posição à qual ela pertence está especificado. Por esse identificador, é possível determinar a que posição a operação pertence. Assim, para criar a lista de posições históricas, é necessário percorrer a lista de operações históricas e identificar, pelo identificador, a que posição a operação pertence. É essencial verificar se uma posição com esse identificador não está na lista de posições ativas. Se existir, isso significa que ainda é uma posição aberta, e essa operação deve ser ignorada. Se essa posição já não está no mercado, é necessário criar um novo objeto de posição histórica com esse identificador e adicionar essa operação à lista de operações da posição criada. Antes de criar o objeto de posição, é preciso verificar se essa posição já foi criada anteriormente. Se sim, não é necessário criar o objeto de posição; basta adicionar essa operação à lista da posição já existente. Naturalmente, se a operação já estiver na lista da posição, não é necessário adicioná-la novamente. Após concluir o loop pelas operações históricas, o resultado será uma lista de posições já fechadas, e cada posição nesta lista conterá apenas as suas próprias operações.


Método que imprime no log 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();
     }
  }

Começando pela posição mais antiga (desde o início da lista), no loop obtemos cada posição sucessiva e exibimos sua descrição no log.


Método que retorna a descrição da posição atualmente selecionada:

//+------------------------------------------------------------------+
//| Возвращает описание текущей выбранной позиции                    |
//+------------------------------------------------------------------+
string CPositionsControl::CurrentSelectedDescription(void)
  {
   CPosition *pos=this.GetPositionObjByID(this.CurrentSelectedID());
   return(pos!=NULL ? pos.Tooltip() : NULL);
  }

Obtemos o ponteiro para a posição atualmente selecionada, cujo identificador está registrado na variável m_current_id, e retornamos uma string criada para exibir como uma dica pop-up (Tooltip). Essa string pode ser usada para exibi-la como um comentário no gráfico, o que será demonstrado em um EA de teste para exibir algumas propriedades da posição cujos ícones estão no gráfico. O EA de teste terá a seguinte funcionalidade:

  1. Ao iniciar, cria-se um histórico de negociações na forma de uma lista de posições históricas, cada uma com sua lista de operações;
  2. Após a criação da lista de posições históricas, a última posição fechada é exibida no gráfico como ícones de operações de abertura e fechamento, conectados por uma linha;
  3. É possível navegar pela lista de posições históricas usando as teclas de setas em conjunto com a tecla Ctrl:
    1. Tecla Esquerda — exibe a posição anterior no gráfico;
    2. Tecla Direita — exibe a próxima posição no gráfico; 
    3. Tecla Cima — exibe a primeira posição no gráfico; 
    4. Tecla Baixo — exibe a última posição no gráfico;
  4. Segurando a tecla Shift, é exibido no gráfico um comentário com a descrição da posição atualmente selecionada na lista.


Testes

Na pasta \MQL5\Experts\PositionsViewer\ criaremos um novo arquivo de EA PositionViewer.mq5.

Conectaremos o arquivo da classe de controle de posições históricas ao novo arquivo do EA, definiremos substituições de macro para os códigos das teclas e as variáveis globais do EA:

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

#include <PositionsViewer\PositionsControl.mqh>

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

//--- global variables
CPositionsControl ExtPositions;     // Экземпляр класса исторических позиций
bool              ExtChartScroll;   // Флаг прокрутки графика
bool              ExtChartHistory;  // Флаг отображения торговой истории

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

No manipulador OnInit() do EA, incluiremos o seguinte código:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Запоминаем флаг автопрокрутки графика и отключаем автопрокрутку
   ExtChartScroll=ChartGetInteger(ChartID(), CHART_AUTOSCROLL);
   ChartSetInteger(ChartID(), CHART_AUTOSCROLL, false);
   
//--- Запоминаем флаг отображения торговой истории и отключаем показ истории
   ExtChartHistory=ChartGetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY);
   ChartSetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY, false);
   
//--- Создаём список закрытых позиций и выводим в журнал время создания списка
   ulong start=GetTickCount64();
   Print("Reading trade history and creating a list of historical positions");
   ExtPositions.Refresh();
   ulong msec=GetTickCount64()-start;
   PrintFormat("List of historical positions created in %I64u msec", msec);
   //ExtPositions.Print();
   
//--- Если это запуск после смены периода графика - отображаем текущую выбранную позицию
   if(UninitializeReason()==REASON_CHARTCHANGE)
      ExtPositions.ShowCurrent(true);
//--- иначе - отображаем последнюю закрытую позицию
   else
      ExtPositions.ShowLast(true);

//--- Успешно
   return(INIT_SUCCEEDED);
  }

Aqui, as flags de rolagem automática do gráfico e exibição do histórico de negociações são salvos para restaurá-los ao término do programa. A rolagem automática do gráfico e a exibição do histórico são desativadas, é criada a lista de todas as posições históricas já existentes para o símbolo atual, e o tempo de criação da lista é registrado no log. Se a linha "//ExtPositions.Print();" for descomentada, após a criação da lista de posições fechadas, todas as posições históricas da lista criada serão impressas no log. Se não for uma troca de período do gráfico, os ícones da última posição fechada são desenhados no gráfico, e o gráfico é centralizado para que os ícones fiquem no centro. Se houver uma mudança de período no gráfico, é exibida a posição atualmente selecionada na lista.


No manipulador OnDeinit(), restauramos os valores salvos de rolagem automática do gráfico e exibição do histórico de negociações, e removemos os comentários do gráfico:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Восстанавливаем начальное значение свойства автопрокрутки, торговой истории и удаляем комментарии на графике
   ChartSetInteger(ChartID(), CHART_AUTOSCROLL, ExtChartScroll);
   ChartSetInteger(ChartID(), CHART_SHOW_TRADE_HISTORY, ExtChartHistory);
   Comment("");
  }


No manipulador OnTradeTransaction(), ao surgir uma nova operação, é necessário chamar o método de atualização da lista da classe de controle de posições históricas; e se for um fechamento de posição, os ícones dessa posição são exibidos no gráfico:

//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {
//--- Если тип транзакции - добавление новой сделки
   if(trans.type==TRADE_TRANSACTION_DEAL_ADD)
     {
      //--- обновляем список позиций и их сделок
      ExtPositions.Refresh();
      
      //--- Получаем тикет новой сделки
      ulong deal_ticket=trans.deal;
      
      //--- Если тикет не получен, или не удалось получить из свойств сделки способ изменения позиции - уходим
      long entry;
      if(deal_ticket==0 || !HistoryDealGetInteger(deal_ticket, DEAL_ENTRY, entry))
         return;

      //--- Если это сделка выхода из позиции - отобразим на графике последнюю закрытую позицию
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_OUT_BY)
         ExtPositions.ShowLast(true);
     }
  }


No manipulador de eventos, monitoraremos os eventos de pressionamento de teclas, respondendo às teclas de seta em conjunto com Ctrl para navegar pela lista de posições fechadas, à tecla Shift para exibir a descrição da posição em um comentário no gráfico, e à alteração de escala do gráfico para centralizá-lo com base nos ícones da posição atual:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Если идентификатор события - нажатие клавиши
   if(id==CHARTEVENT_KEYDOWN)
     {
      //--- Если удерживается клавиша Ctrl
      if(TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)<0)
        {
         //--- Если прокрутка графика клавишами активна - выключим прокрутку графика клавишами
         if((bool)ChartGetInteger(0, CHART_KEYBOARD_CONTROL))
            ChartSetInteger(0, CHART_KEYBOARD_CONTROL, false);
            
         //--- Если нажата клавиша "курсор влево" - отображаем предыдущую закрытую позицию
         if(lparam==KEY_LEFT)
            ExtPositions.ShowPrev(true);
            
         //--- Если нажата клавиша "курсор вправо" - отображаем следующую закрытую позицию
         if(lparam==KEY_RIGHT)
            ExtPositions.ShowNext(true);
            
         //--- Если нажата клавиша "курсор вверх" - отображаем первую закрытую позицию
         if(lparam==KEY_UP)
            ExtPositions.ShowFirst(true);
            
         //--- Если нажата клавиша "курсор вниз" - отображаем последнюю закрытую позицию
         if(lparam==KEY_DOWN)
            ExtPositions.ShowLast(true);
        }
      //--- Если клавиша Ctrl не нажата
      else
        {
         //--- Если прокрутка графика клавишами не активна - включим прокрутку графика клавишами
         if(!(bool)ChartGetInteger(0, CHART_KEYBOARD_CONTROL))
            ChartSetInteger(0, CHART_KEYBOARD_CONTROL, true);
        }
     }

//--- Если удерживается клавиша Shift - выводим в комментарии на график описание текущей позиции
   if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT)<0)
      Comment(ExtPositions.CurrentSelectedDescription());
//--- Если клавиша Shift не нажата - проверяем комментарий на графике и стираем, если он не пустой
   else
     {
      if(ChartGetString(ChartID(),CHART_COMMENT)!=NULL)
         Comment("");
     }
      
//--- Если изменился горизонтальный масштаб графика - отобразим текущую выбранную позицию
   static int scale=-1;
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      int scale_curr=(int)ChartGetInteger(ChartID(), CHART_SCALE);
      if(scale!=scale_curr)
        {
         ExtPositions.ShowCurrent(true);
         scale=scale_curr;
        }
     }
  }


Esse arranjo das posições fechadas com a exibição padrão do histórico de negociações:


é uma boa ilustração do que foi mencionado no início do artigo:

... a exibição do histórico de negociações pode se tornar ineficaz e confusa devido ao excesso de ícones de posições abertas e fechadas no gráfico. Isso é especialmente relevante para traders que trabalham com um grande volume de operações, onde o gráfico fica rapidamente sobrecarregado, tornando quase impossível a análise das atividades de negociação e a tomada de decisões ponderadas ...

Agora compilaremos o EA e o executaremos no gráfico:


Ao iniciar, o histórico exibido pelos meios padrão foi ocultado, restando visível apenas a última posição fechada. É possível navegar pelo histórico de posições segurando a tecla Ctrl e pressionando as teclas de seta. Ao percorrer a lista de posições fechadas, é possível visualizar de forma clara e organizada no gráfico a representação gráfica do histórico de negociações no local onde o gráfico estava sobrecarregado com os ícones de todas as operações de posições fechadas ali.

Se também mantivermos pressionada a tecla Shift, a descrição da posição atualmente selecionada no histórico será exibida como um comentário no gráfico.

Agora, vamos localizar uma posição cujas operações estejam suficientemente distantes entre si e observar como elas são centralizadas no gráfico se não couberem em uma única área visível na janela ao aumentar a escala horizontal do gráfico:


Podemos ver que, se ambas as operações não cabem na janela do gráfico, ele é centralizado de forma que a operação de entrada fique visível no segundo candle à esquerda.

Se pressionarmos a tecla Shift, a descrição da posição fechada será exibida como um comentário no gráfico:


Isso é conveniente, pois permite visualizar a descrição da posição sem precisar passar o cursor sobre a linha que conecta as operações. E se navegarmos pela lista de posições fechadas usando as teclas de seta, segurando Ctrl + Shift, a descrição da posição fechada exibida no momento será imediatamente visível como um comentário no gráfico.


O início do EA dispara o processo de criação da lista de posições históricas. No meu caso, com um histórico modesto desde 2023, o tempo de criação da lista foi:

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

Mudanças subsequentes de período do gráfico já não exigem tempo adicional para recriar a lista.

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


Considerações finais

O histórico de negociações apresentado pelo programa criado é mais conveniente para operações frequentes e ativas — o gráfico nunca estará sobrecarregado com ícones de operações de posições fechadas — cada posição é exibida isoladamente, e a alternância entre a exibição da posição atual e da próxima ou anterior é feita pelas teclas de seta. Cada ícone de operação exibe mais informações nas dicas pop-up do que a exibição padrão do histórico de negociações, e ao passar o cursor sobre a linha que conecta as operações, é exibida uma dica pop-up com informações sobre a posição fechada.

As classes desenvolvidas neste artigo permitem que sejam utilizadas em projetos próprios, ampliando sua funcionalidade e criando programas mais complexos e funcionais com informações sobre o histórico de negociações e de posições fechadas.

Todos os arquivos das classes e do EA de teste estão anexados ao artigo e disponíveis para estudo independente. No arquivo MQL5.zip, os arquivos estão organizados de forma que podem ser extraídos diretamente para o diretório MQL5 do terminal. Na pasta Experts, será criada uma nova pasta PositionsViewer\ contendo todos os arquivos deste projeto: o arquivo do EA PositionViewer.mq5 e três arquivos de classe incluídos. Basta compilar o arquivo do EA e utilizar.


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

Arquivos anexados |
Deal.mqh (64.2 KB)
Position.mqh (67.96 KB)
PositionViewer.mq5 (12.39 KB)
MQL5.zip (22.77 KB)
Desenvolvendo um sistema de Replay (Parte 72): Uma comunicação inusitada (I) Desenvolvendo um sistema de Replay (Parte 72): Uma comunicação inusitada (I)
O que iremos construir será complexo de entender. Por isso, apresentarei apenas o início da construção neste artigo. Leia com calma, pois entender o conteúdo aqui é essencial para o próximo passo. O objetivo deste conteúdo é apenas didático, sem aplicação prática além do aprendizado e estudo dos conceitos apresentados.
Elementos da análise correlacional em MQL5: Critério de independência qui-quadrado de Pearson e relação de correlação Elementos da análise correlacional em MQL5: Critério de independência qui-quadrado de Pearson e relação de correlação
O artigo aborda as ferramentas clássicas da análise correlacional. São apresentadas as bases teóricas breves, bem como a implementação prática do critério de independência qui-quadrado de Pearson e o coeficiente de relação de correlação.
Do básico ao intermediário: Array (IV) Do básico ao intermediário: Array (IV)
Neste artigo iremos ver como podemos fazer algo muito parecido com o encontrado em linguagens como C, C++ e Java. Onde podemos enviar um número quase infinito de parâmetros para dentro de uma função ou procedimento. Apesar de aparentemente ser um tópico avançado. Na minha visão, o que será visto aqui, pode muito bem ser implementado por qualquer iniciante. Desde que ele tenha compreendido os conceitos vistos arteriormente. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como sendo, uma aplicação cuja finalidade não venha a ser o aprendizado e estudo dos conceitos mostrados.
Desenvolvendo um EA Multimoeda (Parte 13): Automação da segunda etapa — Seleção de grupos Desenvolvendo um EA Multimoeda (Parte 13): Automação da segunda etapa — Seleção de grupos
A primeira etapa do processo automatizado de otimização já foi implementada. Para diferentes símbolos e timeframes, realizamos a otimização com base em vários critérios e armazenamos as informações dos resultados de cada execução em um banco de dados. Agora, vamos nos dedicar à seleção dos melhores grupos de conjuntos de parâmetros encontrados na primeira etapa.