Português
preview
Как просматривать сделки прямо на графике и не утонуть в торговой истории

Как просматривать сделки прямо на графике и не утонуть в торговой истории

MetaTrader 5Примеры | 11 июня 2024, 10:49
941 0
Artyom Trishkin
Artyom Trishkin

Содержание


Введение

Современные трейдеры часто сталкиваются с проблемой анализа большого объема данных, связанных с торговыми операциями. В клиентском терминале MetaTrader 5 отображение торговой истории может стать неэффективным и запутанным из-за перегруженности графика значками открытых и закрытых позиций. Это особенно актуально для трейдеров, работающих с большим количеством сделок, где график быстро заполняется, делая практически невозможным анализ торговой деятельности и принятие взвешенных решений.

Целью данной статьи является предложить решение, которое облегчит восприятие и анализ торговой истории. Мы разработаем механизм пошагового отображения закрытых позиций и улучшенной информации о сделках. Это позволит трейдерам сосредоточиться на каждой отдельной сделке и получить более глубокое понимание их торговых операций.

В ходе работы мы реализуем функции, которые позволят:

  • Пошагово просматривать закрытые позиции, используя клавиши навигации.
  • Улучшить всплывающие подсказки, предоставляя более детальную информацию о каждой сделке.
  • Центрировать график таким образом, чтобы наиболее важные элементы всегда находились в зоне видимости.

Кроме того, будет подробно рассмотрена структура данных, необходимая для реализации данного функционала, и предложены основные принципы учета сделок и позиций в MetaTrader 5. В результате, трейдеры смогут более эффективно управлять своей торговой историей и принимать обоснованные решения на основе полученной информации.

В клиентском терминале MetaTrader 5 мы можем отобразить торговую историю, установив в настройках графика (клавиша F8) на вкладке "Показывать" флажок на пункте "Показывать торговую историю":


Установив флажок, мы позволяем терминалу отобразить на графике всю историю торговли в виде значков открытия/закрытия позиций, соединённых линиями. При этом отображаются полностью все сделки, проведённые на символе:


При наличии большого количества сделок график может быть сильно перегружен значками, где сложно что-либо разобрать. Да и всплывающие подсказки, появляющиеся при наведении курсора на значок сделки или на соединяющую сделки линию, не особо информативны:


Создаваемая сегодня программа при запуске отобразит лишь последнюю закрытую позицию. Перемещение по остальным позициям будет осуществляться при нажатии курсорных клавиш:

  • клавиша вверх отобразит первую закрытую позицию по символу;
  • клавиша вниз отобразит последнюю закрытую позицию по символу;
  • клавиша влево отобразит предыдущую закрытую позицию по символу;
  • клавиша вправо отобразит следующую закрытую позицию по символу.

Во всплывающих подсказках сделок и соединяющей линии постараемся отобразить больше полезной информации, плюс при удержании клавиши Shift, на график будет выведена информация по текущей выбранной закрытой позиции:


При отображении каждой последующей закрытой позиции, график будем центрировать так, чтобы нарисованная схема со значками открытия/закрытия позиции и соединяющая их линия размещались по центру графика.

В случае, если оба значка открытия и закрытия не умещаются внутри одного экрана графика, то график будем центрировать так, чтобы сделка открытия располагалась на втором слева видимом баре графика.

Рассмотрим основные принципы учёта сделок и позиций и структуру классов, которые сегодня создадим.

Как формируется позиция? Сначала на сервер отправляется торговый запрос — ордер (торговый приказ). Ордер может быть либо отменён, либо исполнен. При исполнении ордера получаем сделку — факт исполнения торгового запроса. Если на момент исполнения ордера не было позиций, то сделка порождает позицию в направлении сделки. Если позиция уже была, то здесь есть несколько вариантов, в зависимости от типа учёта позиций. При неттинговом учёте на одном символе возможна только одна позиция. Соответственно, сделка, порождённая торговым приказом, изменяет уже существующую позицию. Она может быть:

  1. закрыта — если была проведена сделка продажи для длинной позиции с объёмом, равным объёму позиции:
    Позиция 1.0 Buy - Сделка 1.0 Sell = объём 0 (закрытие позиции);

  2. частично закрыта — если была проведена сделка продажи для длинной позиции с объёмом, меньшим объёма позиции:
    Позиция 1.0 Buy - Сделка 0.5 Sell = объём 0.5
    (частичное закрытие позиции);

  3. добавлен объём — если была проведена сделка покупки для длинной позиции:
    Позиция 1.0 Buy + Сделка 0.5 Buy = объём 1.5
    (увеличение объёма позиции);

  4. развёрнута — если была проведена сделка продажи для длинной позиции с объёмом, большим объёма позиции:
    Позиция 1.0 Buy - Сделка 1.5 Sell = Позиция 0.5 Sell (переворот позиции);

Для учёта позиций на счёте с типом хеджинг каждая сделка может либо изменять уже существующую позицию, либо порождать новую. Для изменения уже существующей позиции (закрытие, либо частичное закрытие), необходимо указывать в торговом приказе идентификатор позиции, с которой требуется провести торговую операцию. Идентификатор позиции — это уникальное число, присваиваемое каждой новой позиции, и неизменяемое на протяжении всей жизни позиции:

  1. открытие новой позиции — если была проведена сделка продажи или покупки при уже существующей позиции:
    Позиция 1.0 Buy + Сделка 1.0 Sell = 2 самостоятельные позиции 1.0 Buy и 1.0 Sell
    (открытие позиции), или
    Позиция 1.0 Buy + Сделка 1.5 Buy = 2 самостоятельные позиции 1.0 Buy и 1.5 Buy (открытие позиции), и т.п.;

  2. закрытие существующей позиции — если была проведена сделка продажи с указанием идентификатора уже существующей длинной позиции:
    Позиция 1.0 Buy с ID 123 - Сделка 1.0 Sell с ID 123 = закрытие позиции 1.0 Buy с ID 123 (закрытие позиции с ID 123);

  3. частичное закрытие существующей позиции — если была проведена сделка продажи с указанием идентификатора уже существующей длинной позиции с объёмом, меньшим объёма существующей позиции с указанным идентификатором:
    Позиция 1.0 Buy с ID 123 - Сделка 0.5 Sell с ID 123 = объём 0.5 Buy с ID 123 (частичное закрытие позиции с ID 123);

Подробнее об ордерах, сделках и позициях можно почитать в статье "Ордерa, позиции и сделки в MetaTrader 5".

Каждый отправленный на торговый сервер ордер до момента его исполнения находится в списке действующих ордеров в терминале. При его срабатывании появляется сделка, порождающая новую, либо изменяющая существующую позицию. В момент исполнения ордера, он помещается в список исторических ордеров, и создаётся сделка, которая тоже размещается в списке исторических сделок. Если сделка порождает позицию, то создаётся новая позиция, располагающаяся в списке действующих позиций.

Списка исторических (закрытых) позиций в терминале нет. Списка действующих сделок тоже нет в терминале — они сразу же являются историческими и расположены в соответствующем списке.
Объясняется это просто: ордер — это приказ на совершение торговых операций на счёте. Он существует до тех пор, пока не будет исполнен. Расположен в списке существующих приказов. Как только ордер исполняется — он перестаёт существовать (в идеальном случае) и помещается в список исполненных торговых приказов. При этом он порождает сделку. Сделка — это факт исполнения торгового приказа. Это одномоментное событие — ордер исполнен — это сделка. Всё. Сделку поместили в список свершившихся на счёте фактов.
Сделка порождает позицию, которая существует до той поры, пока не будет закрыта. Соответственно, позиция всегда находится в списке действующих позиций. Как только она закрывается, она удаляется из этого списка.

В терминале нет списка закрытых позиций. Но есть список проведённых сделок. Поэтому, для того чтобы создать список закрытых позиций, необходимо их составить из сделок, принадлежащих ранее закрытой позиции. Список сделок нам доступен всегда. И в каждой сделке, в её свойствах, есть идентификатор той позиции, в истории жизни которой сделка участвовала. Таким образом, нам доступно всё необходимое для воссоздания истории жизни каждой из ранее существовавших на счёте позиций — идентификатор позиции и список её сделок.

Исходя из написанного выше, получаем такой алгоритм действий для воссоздания ранее закрытых позиций из имеющегося исторического списка сделок:

  • Получаем список всех сделок по нужному символу;
  • Идём по списку и получаем каждую очередную сделку из списка;
  • Проверяем в свойствах сделки идентификатор позиции;
  • Если позиции с таким идентификатором нет в собственном списке исторических позиций, создаём новый объект исторической позиции;
  • Получаем объект исторической позиции по полученному ранее идентификатору;
  • В список сделок объекта исторической позиции добавляем текущую сделку.

По завершении цикла по списку сделок терминала, будем иметь в программе полный список всех закрытых позиций на счёте по символу, содержащих списки сделок, принадлежащих каждой позиции.

Теперь в каждой исторической позиции будет список сделок, участвующих в изменении этой позиции, и мы сможем получить полную информацию обо всех изменениях закрытой позиции за время её существования.

Для реализации озвученной логики нам необходимо создать три класса:

  1. Класс сделки. Будет содержать список всех свойств исторической сделки, методы доступа к свойствам — установки и получения, плюс дополнительные методы для вывода информации о сделке;
  2. Класс позиции. Будет содержать список всех сделок, методы доступа к ним и дополнительные методы для вывода информации о позиции и её сделках;
  3. Класс управления историческими позициями. Будет содержать список всех исторических позиций, создавать и обновлять его, а также методы для доступа к свойствам позиций и их сделок, плюс дополнительные методы для вывода информации о позициях и их сделках.

Итак, начнём.


Класс сделки

Чтобы получить и сохранить свойства сделки, нужно выбрать историю ордеров и сделок (HistorySelect()), пройтись в цикле по истории сделок и получить из списка исторических сделок тикет сделки по индексу цикла (HistoryDealGetTicket()). При этом сделка будет выбрана для получения её свойств при помощи функций HistoryDealGetInteger(), HistoryDealGetDouble() и HistoryDealGetString().

В классе сделки будем исходить из того, что сделка уже выбрана и можно сразу получать её свойства. Помимо записи и получения свойств сделки, класс будет позволять создать графическую метку на графике, отображающую сделку. Метка будет рисованной на канвасе, чтобы можно было для каждого типа сделки самостоятельно нарисовать нужную метку, а не использовать значки из предопределённого набора символов шрифта. При создании объекта каждой сделки, будем получать тиковую историю цен в моменте появления сделки, что позволит рассчитать спред, бывший в момент проведения сделки, и отобразить его в описании её параметров.

Так как у каждой позиции может быть много сделок, участвовавших в её изменении, то все сделки в итоге будут располагаться в списке класса динамического массива указателей на экземпляры класса CObject и его наследников CArrayObj.
Соответственно, класс сделки будет унаследован от класса базового объекта Стандартной Библиотеки CObject.

Сделка имеет набор целочисленных, вещественных и строковых свойств. Каждый объект-сделка будет находиться в списке объектов в классе исторической позиции. Для реализации поиска нужной сделки необходимо будет написать перечисление параметров, и по значениям этого перечисления можно будет искать сделки в списке и сортировать список по этим же значениям. Для использования в создаваемой программе нам потребуется поиск всего по двум свойствам — по тикету сделки и по времени в миллисекундах. По тикету будем определять, что такая сделка уже есть в списке (либо её в нём ещё нет), а по времени в миллисекундах сделки будут отсортированы в списке — чтобы расположить их в строго хронологическом порядке. Но так как классы подразумевают расширение, то в перечислении свойств сделки будут находиться все её свойства.

Для организации поиска и сортировки в списках CArrayObj Стандартной Библиотеки в классе CObject уже определён виртуальный метод Compare(), возвращающий значение 0:

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

То есть, это метод всегда возвращает равенство. В каждом из объектов классов, унаследованных от CObject этот метод нужно переопределять таким образом, чтобы при сравнении одного, указанного в параметре mode, свойства двух сравниваемых объектов, метод возвращал:

  • -1 — если значение свойства текущего объекта меньше значения этого свойства у сравниваемого объекта;
  •  1 — если значение свойства текущего объекта больше значения этого свойства у сравниваемого объекта;
  •  0 — если значения указанного свойства у обоих сравниваемых объектов равны.

Таким образом, исходя из пояснений выше, нам дополнительно для каждого из создаваемых сегодня классов, необходимо будет создать перечисления свойств объектов классов и методы их сравнения.

Создадим в папке \MQL5\Include\ папку PositionsViewer\, а в ней новый файл Deal.mqh класса CDeal:


Базовым классом должен быть класс CObject Стандартной библиотеки:


В итоге получим такую заготовку файла класса объекта-сделки:

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


Подключим файлы классов стандартного объекта библиотеки CObject и класса CCanvas к созданному файлу класса объекта-сделки и напишем перечисление для сортировки по свойствам объекта-сделки:

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

В приватной, защищённой и публичных секциях объявим все необходимые для работы класса переменные и методы:

//+------------------------------------------------------------------+
//| Класс сделки                                                     |
//+------------------------------------------------------------------+
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();
  };

Рассмотрим подробнее объявленные методы.


Параметрический конструктор:

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

Помним, что текущая сделка уже выбрана. Поэтому здесь сразу заполняем все свойства объекта из свойств сделки. Далее устанавливаем параметры канваса для отображения метки сделки на графике, создаём графический объект этой метки и получаем тик по времени сделки для расчёта спреда, бывшего в момент совершения сделки. Если тик не удаётся получить, то берём средний спред минутного бара, на котором была сделка.

В итоге, при создании объекта-сделки, сразу получаем объект с записанными в него свойствами исторической сделки по её тикету, и уже созданный, но скрытый значок для визуализации сделки на графике, плюс величину спреда на момент совершения этой сделки.


В деструкторе класса проверяем причину деинициализации и, если это не смена таймфрейма графика, уничтожаем графический ресурс объекта графической метки:

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


Виртуальный метод сравнения двух объектов между собой по указанному свойству:

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

В метод передаётся указатель на сравниваемый объект и значение сравниваемого свойства из перечисления ENUM_DEAL_SORT_MODE. Если значение указанного свойства текущего объекта больше значения свойства сравниваемого объекта, то возвращается 1. Если значение свойства текущего объекта меньше значения сравниваемого — возвращается -1, в любом ином случае возвращается ноль.


Метод, возвращающий описание типа сделки:

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

Здесь возвращаются описания всех типов сделок, большинство из которых не будет использоваться в программе. Но метод возвращает все типы, с учётом возможностей расширения и наследования классов.


Метод, возвращающий описание способа изменения позиции:

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


Метод, возвращающий описание причины проведения сделки:

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

Из этого метода будем использовать лишь три значения: закрытие по StopLoss, TakeProfit и StopOut.


Метод, возвращающий описание сделки:

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

При помощи функции StringFormat() создаёт и возвращает строку вида:

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


Виртуальный метод, возвращающий текст всплывающего сообщения сделки:

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

Аналогично вышерассмотренному методу, здесь формируется и возвращается строка текста вида:

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

Эта строка далее будет использоваться для вывода описания сделки во всплывающей подсказке, при наведении указателя мышки на значок сделки на графике. Метод может быть переопределён в наследуемых классах для вывода иной информации по сделке.


Метод, распечатывающий в журнале свойства сделки:

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

Метод не распечатывает все свойства сделки, а лишь выводит минимальную информацию о сделке, возвращаемую методом Description(), рассмотренным выше. Перед строкой описания сделки добавляются два пробела — чтобы упорядочить информацию при выводе описания позиции, где под заголовком с описанием позиции распечатываются данные о её сделках:

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

Подробнее о функциях PrintFormat() и StringFormat() можно почитать в статьях "Изучаем PrintFormat() и берем готовые к использованию примеры" и "StringFormat(). Обзор, готовые примеры использования".


Метод, возвращающий время с миллисекундами:

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

Здесь формируется строка из значения времени в миллисекундах, сконвертированное в обычное время делением на 1000. К полученной строке через точку добавляется количество миллисекунд, полученных как остаток от деления времени в миллисекундах на 1000. Строка миллисекунд форматируется в трёхзначный вид с добавлением лидирующих нулей, если она короче трёх знаков. В итоге получаем такое представление времени:

2023.06.12 17:04:20.362

Для того, чтобы понять каким был спред в момент совершения сделки, необходимо получить нужный тик по времени сделки в миллисекундах. Казалось бы, тривиальная задача копирования одного тика по известному времени стандартной функцией CopyTicks() вылилась в небольшой квест по поиску решения, так как по точно указанному времени не получилось скопировать тик. В итоге, после некоторых поисков решения, удалось найти нужный алгоритм: необходимо делать некоторое количество запросов тиков в постоянно расширяющемся диапазоне времени "От" до времени совершения сделки. Подробнее здесь.


Метод для получения тика сделки:

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

После получения тика, из него берутся значения цен Ask и Bid и рассчитывается размер спреда как (Ask - Bid) / Point.

Если в итоге данным методом тик получить не удалось, то получаем усреднённое значение спреда при помощи метода для получения спреда минутного бара сделки:

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

Здесь: получаем время открытия минутного бара по времени сделки, и по этому времени получаем средний спред бара при помощи функции CopySpread(). При ошибке получения бара или спреда, метод возвращает ноль.


Метод, создающий объект-метку на графике:

//+------------------------------------------------------------------+
//| Создаёт объект-метку на графике                                  |
//+------------------------------------------------------------------+
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;
  }

Создаём графический ресурс, устанавливаем его точку привязки по центру, цену и время сделки, и текст всплывающей подсказки. Далее созданному объекту устанавливается флаг, что он скрыт на всех периодах графика, и рисуется на нём его внешний вид. Теперь для его отображения достаточно лишь установить для объекта флаг видимости на всех таймфреймах.


Метод, рисующий внешний вид объекта-метки:

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

Сначала графический объект Bitmap заливается полностью прозрачным цветом, затем на нём рисуется стрелка, соответствующая типу сделки, и далее канвас обновляется с перерисовкой графика.


Виртуальный метод, рисующий стрелку, соответствующую типу сделки:

//+------------------------------------------------------------------+
//| Рисует стрелку, соответствующую типу сделки                      |
//+------------------------------------------------------------------+
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;
     }
  }

В зависимости от типа сделки (Buy или Sell) вызывается метод рисования соответствующей стрелки. Метод виртуальный, поэтому он может переопределяться в наследуемых классах. Например, в дочернем классе можно для иного типа сделки рисовать другой значок, либо можно дополнительно учитывать способ изменения позиции, и т.д.


Метод, рисующий на холсте маску стрелки Buy:

//+------------------------------------------------------------------+
//| Рисует на холсте маску стрелки 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));
  }

Стрелки, рисуемые в качестве значков сделок, по периметру имеют обводку (маску) белого цвета — чтобы выделяться на тёмном фоне свечи:

Так как координаты фигур, рисуемых на канвасе, всегда задаются относительно локальных координат канваса (холста), то необходимо ввести смещения, которые будут добавляться к координатам точек ломанной линии по осям X и Y — чтобы можно было точно центрировать рисуемую фигуру внутри холста. Далее, с учётом переданных в метод смещений, задаются значения координат X и Y в массивах и вызывается метод Polygon() для рисования белой обводки по координатным точкам. Далее, уже внутри этой обводки будет нарисована стрелка методами рисования стрелок.

Метод, рисующий на холсте стрелку Buy:

//+------------------------------------------------------------------+
//| Рисует на холсте стрелку 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));
  }

Здесь сначала рисуется маска стрелки Buy, а затем, по указанным координатным точкам рисуется стрелка Buy, и внутреннее её пространство заливается цветом.


Аналогичные методы для рисования стрелки 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));
  }


Методы для скрытия и отображения значка сделки на графике:

//+------------------------------------------------------------------+
//| Скрывает значок сделки на графике                                |
//+------------------------------------------------------------------+
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);
  }

Для того, чтобы скрыть объект на графике, достаточно для свойства видимости графического объекта OBJPROP_TIMEFRAMES установить значение OBJ_NO_PERIODS.

Соответственно, чтобы отобразить объект на графике, нужно свойству OBJPROP_TIMEFRAMES установить значение OBJ_ALL_PERIODS.


Метод, устанавливающий цвет значка сделки:

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

Переменной m_color_arrow, хранящей цвет рисуемого значка, устанавливается переданное в метод значение, и весь графический объект полностью перерисовывается. Таким образом, можно "на лету" из управляющей программы поменять цвет метки сделки на графике.

Мы создали класс сделки, дающий доступ к свойствам исторической сделки по её тикету, и позволяющий получить необходимые данные по этой сделке и отобразить или скрыть её значок на графике.

Объекты этого класса будут храниться в списке сделок объекта позиции, который в свою очередь будет отображать свойства исторической позиции, давать доступ к её сделкам и параметрам и отображать на графике сделки открытия и закрытия позиции, соединённые линией.


Класс позиции

В той же папке, где создавали класс сделки, создадим новый файл Position.mqh класса позиции CPosition.

Аналогично классу сделки, для класса позиции напишем перечисление свойств объекта для поиска и сортировки по свойствам позиции, и подключим файлы класса сделки и 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
  {
  }

В защищённой и публичной секциях класса впишем переменные и методы для работы с классом:

//+------------------------------------------------------------------+
//| Класс позиции                                                    |
//+------------------------------------------------------------------+
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();
  };

Для поиска в сортированном списке в классе динамического массива указателей на экземпляры класса CObject и его наследников CArrayObj, в метод поиска Search() передаётся указатель на экземпляр объекта, с которым нужно провести сравнение по тому свойству, по которому отсортирован список. Чтобы постоянно не создавать и уничтожать новый объект с заданным свойством, что достаточно накладно в плане производительности, в защищённой секции класса объявлен экземпляр объекта-сделки. Для поиска просто будем в этот объект устанавливать нужное значение нужного свойства и передавать его в метод поиска в качестве искомого экземпляра.

Рассмотрим объявленные методы подробнее.


Параметрический конструктор:

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

В конструктор передаётся идентификатор позиции и символ, по которому была открыта позиция. Списку сделок позиции устанавливается флаг сортировки по времени сделки в миллисекундах, сохраняется идентификатор и символ в переменные класса и устанавливаются некоторые параметры аккаунта, символа, идентификатор графика и свойства линии, соединяющей значки сделок открытия-закрытия позиции.


В деструкторе класса удаляются все графические объекты, префиксом у которых выступает наименование линии, и очищается список сделок:

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

Удаление графического объекта-линии сделано по префиксу из таких соображений: если в наследуемом классе будет организовано отображение нескольких линий, соединяющих все сделки позиции, а не только сделку открытия со сделкой закрытия, то наименование каждой линии может быть организовано в виде "имя_линии" + "номер_линии". И тогда, в этом случае, всё равно все такие линии будут удалены в деструкторе, так как все они имеют один общий префикс — "имя линии".


Метод, сравнивающий два объекта между собой по указанному свойству:

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

Аналогичный метод рассматривали при разработке класса объекта-сделки. Здесь всё идентично: если у текущего объекта значение свойства больше, чем у сравниваемого — возвращается 1, если меньше — возвращается -1, если свойства равны — возвращается ноль.


Метод, возвращающий время с миллисекундами:

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

Это копия одноимённого метода класса объекта-сделки, рассмотренного в классе сделки.


Метод, возвращающий указатель на сделку открытия:

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

Нам нужно найти в списке объектов-сделок сделку со способом изменения позиции Entry In — вход в позицию. В отсортированном по времени списке эта сделка должна быть первой. Поэтому цикл начинаем с начала списка — с индекса 0.

Получаем из списка сделку по индексу и, если это сделка входа в позицию — возвращаем указатель на найденную сделку в списке. По окончании цикла возвращаем NULL — сделка не найдена.


Метод, возвращающий указатель на сделку закрытия:

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

Здесь, по сравнению с предыдущим методом, всё наоборот — сделка выхода из позиции находится в самом конце списка, поэтому и цикл начинаем с конца.
Как только найдена сделка выхода из позиции — возвращаем на неё указатель. Иначе — возвращаем значение NULL.


Методы для получения дополнительных свойств исторической позиции:

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

Во всех методах сначала получаем указатель на сделку, из которой нужно взять соответствующее свойство, и, если указатель валидный, — возвращаем значение свойства, иначе — ноль.


Метод, возвращающий прибыль в пунктах:

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

Здесь сначала получаем цены открытия и закрытия из соответствующих сделок, и далее возвращаем результат расчёта прибыли в пунктах, в зависимости от направления позиции.


Метод, добавляющий сделку в список сделок:

//+------------------------------------------------------------------+
//| Добавляет сделку в список сделок                                 |
//+------------------------------------------------------------------+
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;   
  }

В метод передаётся тикет выбранной сделки. Сначала проверяется, что в списке нет сделки с таким тикетом. Для этого временному объекту-сделке устанавливается переданный в метод тикет, список сделок сортируется по значению тикетов сделок, и выполняется поиск сделки в списке с таким тикетом. Сразу же после поиска сделки, списку возвращается флаг сортировки по времени в миллисекундах. Если такая сделка уже есть в списке, то добавлять её уже не нужно — метод возвращает NULL. При этом сортировка списка сделок уже установлена по умолчанию — по времени в миллисекундах. Если такой сделки нет в списке — создаётся новый объект-сделка и добавляется в список в порядке сортировки по времени в миллисекундах. Если это сделка закрытия позиции, то в свойства позиции записывается её профит и создаётся соединительная линия между сделками открытия и закрытия позиции. В итоге возвращается указатель на созданный новый объект сделки.


Метод, возвращающий описание типа позиции:

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

В зависимости от типа позиции, сохранённого в переменной m_type, возвращается описание. Если в переменную записано значение, не совпадающее с константами перечисления типа позиции, то возвращается значение "Unknown::"+значение_переменной.


Метод, возвращающий описание времени и цены закрытия позиции:

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

Если в объекте позиции ещё нет сделки закрытия, то метод вернёт сообщение, что позиция ещё не закрыта. В противном случае создаётся и возвращается строка вида:

Closed 2023.06.12 17:04:20.362 [1.07590]


Метод, возвращающий описание времени и цены открытия позиции:

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

Здесь создаётся и возвращается строка вида:

Opened 2023.06.12 16:51:36.838 [1.07440]


Метод, возвращающий краткое описание позиции:

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

Создаётся и возвращается строка вида:

Position EURUSD 0.10 Buy #1752955040, Magic 123


Виртуальный метод, возвращающий текст всплывающего сообщения позиции:

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

В методе создаётся и возвращается строка вида:

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

Этот текст устанавливается как текст всплывающей подсказки для линии, соединяющей сделки открытия и закрытия позиции. Метод виртуальный, и может быть переопределён в наследуемых классах в случае, если необходимо выводить несколько иную информацию. При использовании строки, возвращаемой методом как текст всплывающей подсказки, необходимо учитывать длину строки, так как она ограничена для всплывающей подсказки примерно 160-ю символами, включая управляющие коды. Точного значения, к сожалению, назвать не могу — значение было выявлено эмпирическим путём.


Метод, отображающий значки сделок на графике:

//+------------------------------------------------------------------+
//| Отображает значки сделок на графике                              |
//+------------------------------------------------------------------+
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);
  }

В цикле по списку сделок получаем каждую очередную сделку и вызываем метод полученного объекта ShowArrow(), отображающий значок сделки на графике. По окончании цикла перерисовываем график, если для этого установлен флаг.


Метод, скрывающий значки сделок на графике:

//+------------------------------------------------------------------+
//| Скрывает значки сделок на графике                                |
//+------------------------------------------------------------------+
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);
  }

В цикле по списку сделок получаем каждую очередную сделку и вызываем метод полученного объекта HideArrow(), скрывающий значок сделки с графика. По окончании цикла перерисовываем график, если для этого установлен флаг.


Метод, создающий линию, соединяющую сделки открытия-закрытия:

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

Создаётся линия с координатами цены и времени в нулевом расположении (цена равна 0, время равно 01.01.1970 00:00:00), скрываем линию с графика и устанавливаем стиль рисования точками и заданный по умолчанию цвет.
Изначально линии каждого объекта создаются в скрытом виде, и лишь в момент, когда их нужно отобразить, им устанавливаются нужные координаты и отображение при помощи


метода, отображающего соединительную линию между значками сделок:

//+------------------------------------------------------------------+
//| Отображает соединительную линию между значками сделок            |
//+------------------------------------------------------------------+
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);
  }


Метод, скрывающий соединительную линию между значками сделок:

//+------------------------------------------------------------------+
//| Скрывает соединительную линию между значками сделок              |
//+------------------------------------------------------------------+
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);
  }

Для объекта сбрасывается флаг видимости на всех периодах графика и, при установленном флаге, график обновляется.


Метод, устанавливающий цвет соединительной линии:

//+------------------------------------------------------------------+
//| Устанавливает цвет соединительной линии                          |
//+------------------------------------------------------------------+
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;
  }

Для объекта устанавливается в свойство цвета переданное в метод значение и, если всё успешно, этот цвет записывается в переменную m_line_color.


Метод, устанавливающий цвет сделок Buy и 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);
     }
  }

В метод передаются два цвета — цвет для сделки Buy и цвет для сделки Sell. В цикле по списку сделок позиции получаем указатель на каждую очередную сделку и устанавливаем цвет для значка сделки в соответствии с её типом.


Метод, отображающий графическое представление позиции на графике:

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

В методе последовательно вызываются защищённые методы, отображающие сделки входа и выхода и соединяющую их линию на графике.


Метод, скрывающий графическое представление позиции на графике:

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

В методе последовательно вызываются защищённые методы, скрывающие сделки входа и выхода и соединяющую их линию на графике.

Два вышерассмотренных метода публичные, и используются для управления отображением позиции на графике из управляющей программы.


Метод, распечатывающий в журнале свойства позиции и её сделок:

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

Сначала выводится заголовок с кратким описанием позиции и двумя строками с временем и ценой открытия и закрытия позиции. Далее в цикле распечатываются описания всех сделок позиции из списка сделок.

В итоге получаем такую распечатку в журнале:

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

Не очень информативно, но методы Print() в этих классах делались только для отладки, и этого достаточно.

Теперь мы имеем уже два класса: класс сделки, и класс позиции, содержащий в себе список сделок, участвовавших в жизни позиции. Классы несложные, и содержат в себе основную информацию по сделкам и некоторую дополнительную информацию по ценам и времени открытия/закрытия позиции и её профита в пунктах. Остальные методы используются для получения и вывода этой информации на график или в текстовую строку.

Теперь создадим обобщающий класс, в котором все позиции будут собираться из сделок и размещаться в список исторических позиций. Класс будет предоставлять доступ к свойствам позиций и их сделок, обновлять и дополнять список исторических позиций и отображать указанную позицию на графике.


Класс управления историческими позициями

В той же папке, в которой создавали два предыдущих класса, создадим новый файл PositionsControl.mqh класса CPositionsControl.

Класс должен быть унаследован от базового объекта Стандартной Библиотеки CObject, а файл класса позиций подключен к новому файлу класса 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
  {
  }

В приватной, защищённой и публичной секциях объявим переменные и методы для работы с классом:

//+------------------------------------------------------------------+
//| Класс исторических позиций                                       |
//+------------------------------------------------------------------+
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();
  };

Рассмотрим подробнее объявленные методы.


Конструктор класса:

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

Устанавливаем списку исторических позиций флаг сортировки по времени закрытия в миллисекундах. Записываем в переменную переданный в конструктор символ. Если это пустая строка, то — текущий символ графика, на котором запущена программа. Идентификатор графика устанавливается как идентификатор текущего графика. В наследуемых классах можно будет установить иной идентификатор. В переменную m_key_ctrl запишем флаг разрешения управления графиком клавишами. После работы программы это значение будет записано обратно в свойство графика — чтобы вернуть его в то состояние, которым оно было до запуска программы, так как она активно его изменяет.


В деструкторе класса список позиций уничтожается, а свойству графика CHART_KEYBOARD_CONTROL возвращается значение, бывшее перед запуском программы:

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


Метод, возвращающий объект-позицию из списка по идентификатору:

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

Логика метода полностью расписана в комментариях к коду.


Метод, возвращающий флаг того, что позиция рыночная:

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

При создании списка позиций по списку сделок, важно не учитывать текущие рыночные позиции и не добавлять их в список исторических позиций. Чтобы понять, что позиция ещё не закрыта, нужно найти её в списке действующих позиций по её идентификатору. Если такая позиция существует, то её добавлять в список не нужно. Метод ищет позиции из списка рыночных по её тикету, проверяет соответствие идентификатора позиции переданному в метод значению и, если такая позиция есть (её возможно выбрать) — возвращает true. В противном случае — возвращает false.


Метод, возвращающий указатель на первую закрытую позицию в списке:

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

Список позиций сортируется по времени закрытия позиций в миллисекундах и возвращается указатель на первую позицию в списке (самую старую).


Метод, возвращающий указатель на последнюю закрытую позицию в списке:

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

Список позиций сортируется по времени закрытия позиций в миллисекундах и возвращается указатель на последнюю позицию в списке.


Метод, возвращающий указатель на предыдущую закрытую позицию в списке:

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

На основе текущей выбранной позиции, указатель на которую передаётся в метод, ищется предыдущая в списке, отсортированном по времени закрытия в миллисекундах. Метод SearchLess() класса CArrayObj возвращает указатель на первый встреченный в списке объект, имеющий меньшее значение, по которому отсортирован список. В данном случае список отсортирован по времени закрытия в миллисекундах. Соответственно — первый найденный объект с временем закрытия меньшим, чем у переданного в метод — предыдущая позиция. Если объект-позиция не найден, либо в метод передан самый первый элемент списка (предыдущих нет), то метод возвращает NULL.


Метод, возвращающий указатель на следующую закрытую позицию в списке:

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

На основе текущей выбранной позиции, указатель на которую передаётся в метод, ищется следующая в списке, отсортированном по времени закрытия в миллисекундах. Метод SearchGreat() класса CArrayObj возвращает указатель на первый встреченный в списке объект, имеющий большее значение, по которому отсортирован список. В данном случае список отсортирован по времени закрытия в миллисекундах. Соответственно — первый найденный объект с временем закрытия большим, чем у переданного в метод — следующая позиция. Если объект-позиция не найден, либо в метод передан самый последний элемент списка (следующих нет), то метод возвращает NULL.


Метод, выводящий на график графическое представление указанной позиции:

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

В метод передаётся указатель на позицию, графическое представление которой необходимо отобразить на графике. Если получен валидный объект, вызываем его метод Show() и в переменную m_current_id записываем идентификатор этой позиции. По значению идентификатора позиции в переменной m_current_id мы можем определить текущую выбранную позицию. Выбранной считается такая позиция, графическое представление которой отображено на графике.


Метод, скрывающий с графика графическое представление первой позиции:

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

Получаем указатель на первую позицию в списке и вызываем её метод Hide().


Метод, отображающий на графике графическое представление первой позиции:

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

Методом GetFirstClosedPosition(), рассмотренным выше, получаем указатель на первую позицию в списке, отсортированном по времени в миллисекундах. Скрываем все значки всех позиций, кроме первой, отображаем первую позицию и центрируем график по значкам её сделок на графике.


Метод, скрывающий с графика графическое представление последней позиции:

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

Получаем указатель на последнюю позицию в списке и вызываем её метод Hide().


Метод, отображающий на графике графическое представление последней позиции:

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

Методом GetLastClosedPosition(), рассмотренным выше, получаем указатель на последнюю позицию в списке, отсортированном по времени в миллисекундах. Скрываем все значки всех позиций, кроме последней, отображаем последнюю позицию и центрируем график по значкам её сделок на графике.


Метод, отображающий на графике графическое представление текущей позиции:

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

Получаем указатель на позицию, идентификатор которой записан в переменной m_current_id, отображаем её графическое представление на графике и центрируем график по значкам её сделок на графике.


Метод, отображающий на графике графическое представление предыдущей позиции:

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

Здесь получаем указатели на текущую выбранную и предыдущую позиции в списке. Значки текущей позиции скрываем, а значки предыдущей отображаем. При их отображении, идентификатор этой позиции записывается в переменную m_current_id, указывая на то, что теперь эта позиция является текущей. По её значкам центрируем график.


Метод, отображающий на графике графическое представление следующей позиции:

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

Метод идентичен предыдущему, за исключением того, что здесь получаем указатели на текущую выбранную и следующую позиции в списке. Значки текущей позиции скрываем, а значки следующей отображаем. При их отображении, идентификатор этой позиции записывается в переменную m_current_id, указывая на то, что теперь эта позиция является текущей. По её значкам центрируем график.


Метод, скрывающий с графика графическое представление всех позиций, кроме указанной:

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

В метод передаётся идентификатор позиции, значки которой нужно оставить на графике. Значки всех остальных позиций скрываются.


Метод, центрирующий график на текущей выбранной позиции:

//+------------------------------------------------------------------+
//| Центрирует график на текущей выбранной позиции                   |
//+------------------------------------------------------------------+
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);
  }

Вся логика метода полностью расписана в комментариях к коду. Метод смещает график инструмента так, чтобы визуально все сделки позиции с соединяющей их линией располагались по центру графика. Если значки всех сделок не умещаются в ширину графика, то график смещается таким образом, чтобы сделка открытия позиции находилась на втором слева видимом баре графика.


Метод, возвращающий время открытия текущей выбранной позиции:

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

Получаем указатель на текущую выбранную позицию по её идентификатору, записанному в переменной m_current_id, и при успешном получении указателя возвращаем время открытия позиции. В противном случае — ноль.


Метод, возвращающий время закрытия текущей выбранной позиции:

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

Получаем указатель на текущую выбранную позицию по её идентификатору, записанному в переменной m_current_id, и при успешном получении указателя возвращаем время закрытия позиции. В противном случае — ноль.


Метод, возвращающий тип позиции по типу сделки:

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

В метод передаётся указатель на сделку и, в зависимости от типа сделки, возвращается соответствующий тип позиции.


Метод, создающий список исторических позиций:

//+------------------------------------------------------------------+
//| Создаёт список исторических позиций                              |
//+------------------------------------------------------------------+
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;
  }

Логика метода подробно расписана в комментариях к коду.

Вкратце о логике поиска исторических позиций: в клиентском терминале есть только список действующих позиций. У каждой позиции есть свои сделки, которые находятся в списке исторических сделок. В каждой сделке прописан идентификатор позиции, в жизни которой она участвовала. По этому идентификатору можно однозначно определить, к какой позиции относится сделка. Соответственно, для создания списка исторических позиций необходимо пройтись в цикле по списку исторических сделок и определить по идентификатору, к какой позиции относится сделка. Обязательно проверить, что такой позиции с таким идентификатором нет в списке действующих позиций. Если есть — это ещё не закрытая позиция, и эту сделку нужно пропустить. Если такой позиции уже нет в рынке, то нужно создать новый объект исторической позиции с таким идентификатором и добавить эту сделку в список сделок созданной позиции. Перед созданием объекта-позиции нужно проверить, создавалась ли такая позиция ранее. Если да, то объект-позицию создавать не нужно, а просто добавить эту сделку в список уже созданной позиции. Естественно, если такая сделка уже есть в списке позиции, то и сделку уже добавлять не нужно. После завершения цикла по историческим сделкам, в результате будет создан список уже закрытых позиций, и каждая позиция в этом списке будет содержать список только своих сделок.


Метод, распечатывающий в журнале свойства позиций и их сделок:

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

Начиная от самой ранней позиции (с самого начала списка), в цикле получаем каждую очередную позицию и выводим её описание в журнал.


Метод, возвращающий описание текущей выбранной позиции:

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

Получаем указатель на текущую выбранную позицию, идентификатор которой записан в переменной m_current_id, и возвращаем строку, создаваемую для вывода всплывающей подсказки (Tooltip). Эту строку можно использовать для вывода её на график в комментарии, что и покажем в тестовом советнике для наглядного отображения некоторых свойств позиции, значки которой отображены на графике. В тестовом советнике будет такой функционал:

  1. При запуске создаётся торговая история в виде списка исторических позиций, каждая со списком своих сделок;
  2. По завершении создания списка исторических позиций, последняя закрытая позиция отображается на графике в виде значков сделок открытия и закрытия, соединённых линией;
  3. Перемещаться по списку исторических позиций можно курсорными клавишами с удержанием клавиши Ctrl:
    1. Клавиша Влево — отображает на графике предыдущую позицию;
    2. Клавиша Вправо — отображает на графике следующую позицию; 
    3. Клавиша Вверх — отображает на графике первую позицию; 
    4. Клавиша Вниз — отображает на графике последнюю позицию;
  4. Удержание клавиши Shift отображает на графике комментарий с описанием текущей выбранной в списке позиции.


Тестирование

В папке \MQL5\Experts\PositionsViewer\ создадим новый файл советника PositionViewer.mq5.

Подключим к созданному файлу советника файл класса управления историческими позициями, объявим макроподстановки для кодов клавиш и глобальные переменные советника:

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

В обработчике OnInit() советника пропишем такой код:

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

Здесь запоминаются флаги автопрокрутки графика и отображения торговой истории — чтобы восстановить их при завершении работы программы. Автопрокрутка графика и показ истории отключаются, создаётся список всех исторических позиций, когда-либо существовавших на текущем символе, и в журнал выводится время создания списка. Если раскомментировать строку "//ExtPositions.Print();", то после создания списка закрытых позиций, в журнале будут распечатаны все исторические позиции из созданного списка. Если это не смена периода графика, то на графике рисуются значки последней закрытой позиции, и график прокручивается так, чтобы значки были по его центру. Если это смена периода графика, то отображается текущая выбранная в списке позиция.


В обработчике OnDeinit() восстановим запомненные значения автопрокрутки графика и показа торговой истории и удалим комментарии с графика:

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


В обработчике OnTradeTransaction() при каждом появлении новой сделки необходимо вызвать метод обновления списка класса контроля исторических позиций, а если это закрытие позиции, то отобразить её значки на графике:

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


В обработчике событий будем отслеживать события нажатия кнопок клавиатуры, и реагировать на курсорные клавиши совместно с Ctrl – для навигации по списку закрытых позиций, на клавишу Shift – для отображения описания позиции в комментарии на графике и на изменение масштаба графика – для центрирования графика по значкам текущей позиции:

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


Такое расположение закрытых позиций со стандартным отображением истории торговли:


это хорошая иллюстрация к написанному в самом начале статьи:

... отображение торговой истории может стать неэффективным и запутанным из-за перегруженности графика значками открытых и закрытых позиций. Это особенно актуально для трейдеров, работающих с большим количеством сделок, где график быстро заполняется, делая практически невозможным анализ торговой деятельности и принятие взвешенных решений ...

Теперь скомпилируем советник и запустим его на графике:


При запуске, отображённая стандартными средствами история, была скрыта, но осталась видимой только последняя закрытая позиция. Перемещаться по истории позиций можно, удерживая клавишу Ctrl и нажимая курсорные клавиши. Перемещаясь по списку закрытых позиций, можно легко и без путаницы на графике видеть графическое представление торговой истории в том месте, где график был заполнен значками всех сделок закрытых здесь позиций.

Если удерживать ещё и Shift, то на графике в комментарии будет показано описание текущей выбранной закрытой позиции.

Теперь найдём позицию, сделки которой находятся на достаточном удалении друг от друга, и посмотрим как они центрируются на графике в случае, если не умещаются в одной видимой области окна графика при увеличении его горизонтального масштаба:


Видим, что если обе сделки не помещаются в окно графика, то он центрируется таким образом, чтобы была видима сделка входа в позицию на втором баре слева.

Если нажать клавишу Shift, то увидим описание этой закрытой позиции в комментарии на графике:


Это удобно, так как позволяет увидеть описание позиции без наведения курсора мышки на соединяющую сделки линию. А если перемещаться по списку закрытых позиций курсорными клавишами, удерживая Ctrl + Shift, то можно наглядно в комментарии сразу же видеть описание закрытой позиции, отображённой в текущий момент на графике.


Старт советника запускает процесс создания списка исторических позиций. В моём случае при небольшой истории с 2023-го года время создания списка:

PositionViewer (EURUSD,M15)     Reading trade history and creating a list of historical positions
PositionViewer (EURUSD,M15)     List of historical positions created in 6422 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
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


Заключение

Торговая история, представляемая созданной программой, удобнее в случае активной и частой торговли — график никогда не будет перегружен значками сделок закрытых позиций — каждая позиция отображается самостоятельно, а переключение между показом текущей и следующей, либо прошлой позицией, осуществляется курсорными клавишами. У каждого значка сделки во всплывающих подсказках есть больше информации, чем у стандартного отображения торговой истории, а при наведении курсора на соединяющую сделки линию, показывается всплывающая подсказка с информацией по закрытой позиции.

Созданные в статье классы позволяют использовать их в собственных разработках, расширять свой функционал и создавать более сложные и функциональные программы с информацией по торговой истории и истории закрытых позиций.

Все файлы классов и тестового советника прикреплены к статье и доступны для самостоятельного изучения. В архиве MQL5.zip расположены файлы таким образом, чтобы можно было сразу разархивировать их в каталог терминала MQL5, и в папке Experts будет создана новая папка PositionsViewer\ со всеми файлами этого проекта: файл эксперта PositionViewer.mq5 и там же три включаемых файла классов. Достаточно просто скомпилировать файл советника и использовать.


Прикрепленные файлы |
Deal.mqh (65.41 KB)
Position.mqh (68.55 KB)
PositionViewer.mq5 (12.59 KB)
MQL5.zip (23.93 KB)
Нейросети — это просто (Часть 94): Оптимизация последовательности исходных данных Нейросети — это просто (Часть 94): Оптимизация последовательности исходных данных
При работе с временными рядами мы всегда используем исходные данные в их исторической последовательности. Но является ли это оптимальным вариантом? Существует мнение, что изменение последовательности исходных данных позволит повысить эффективность обучаемых моделей. В данной статье я предлагаю вам познакомиться с одним из таких методов.
Машинное обучение и Data Science (Часть 19): Совершенствуем AI-модели с помощью AdaBoost Машинное обучение и Data Science (Часть 19): Совершенствуем AI-модели с помощью AdaBoost
Алгоритм AdaBoost используется для повышения производительности моделей искусственного интеллекта. AdaBoost (Adaptive Boosting, адаптивный бустинг) представляет собой сложную методику ансамблевого обучения, которая легко объединяет слабых учащихся, повышая их коллективную способность прогнозирования.
Разрабатываем мультивалютный советник (Часть 13): Автоматизация второго этапа — отбор в группы Разрабатываем мультивалютный советник (Часть 13): Автоматизация второго этапа — отбор в группы
Первый этап автоматизированного процесса оптимизации у нас уже реализован. Для разных символов и таймфреймов мы проводим оптимизацию по нескольким критериям и сохраняем информацию о результатах каждого прохода в базе данных. Теперь займёмся отбором лучших групп наборов параметров из найденных на первом этапе.
Элементы корреляционного анализа в MQL5: Критерий независимости хи-квадрат Пирсона и корреляционное отношение Элементы корреляционного анализа в MQL5: Критерий независимости хи-квадрат Пирсона и корреляционное отношение
В статье рассматриваются классические инструменты корреляционного анализа. Даются краткие теоретические основы, а также практическая реализация критерия независимости хи-квадрат Пирсона и коэффициента корреляционного отношения.