Библиотека для простого и быстрого создания программ для MetaTrader (Часть IV): Торговые события

Artyom Trishkin | 18 марта, 2019

Содержание

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

Торговые события и передача их в программу

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

Подумаем какие торговые события нам необходимо идентифицировать:

  • отложенный ордер может быть установлен,
  • отложенный ордер может быть удалён,
  • отложенный ордер может быть активирован, породив при этом позицию,
  • отложенный ордер может быть частично активирован, породив при этом позицию,
  • позиция может быть открыта,
  • позиция может быть закрыта,
  • позиция может быть частично открыта,
  • позиция может быть частично закрыта,
  • позиция может быть закрыта встречной,
  • позиция может быть частично закрыта встречной,
  • счёт может быть пополнен,
  • со счёта могут быть сняты средства,
  • на счёте могут произойти ещё какие-то балансные операции
    события, которые пока не отслеживаем:
  • отложенный ордер может быть модифицирован (изменение цены установки, добавление/удаление/изменение уровней StopLoss и TakeProfit)
  • позиция может быть модифицирована (добавление/удаление/изменение уровней StopLoss и TakeProfit)

Исходя из перечисленного, нужно решить как однозначно идентифицировать то или иное событие. Лучше сразу разделить решение по типу счетов:

Хэджинг:

  1. увеличилось количество отложенных ордеров — означает добавление отложенного ордера (событие в рыночном окружении)
  2. уменьшилось количество отложенных ордеров:
    1. увеличилось количество позиций — означает срабатывание отложенного ордера (событие в рыночном и историческом окружении)
    2. не увеличилось количество позиций — означает удаление отложенного ордера (событие в рыночном окружении)
  3. не уменьшилось количество отложенных ордеров:
    1. увеличилось количество позиций — означает открытие новой позиции (событие в рыночном и историческом окружении)
    2. уменьшилось количество позиций — означает закрытие позиции (событие в рыночном и историческом окружении)
    3. не изменилось количество позиций, но уменьшился объём — означает частичное закрытие позиции (событие в историческом окружении)

Неттинг:

  1. увеличилось количество отложенных ордеров — означает добавление отложенного ордера
  2. уменьшилось количество отложенных ордеров:
    1. увеличилось количество позиций — означает срабатывание отложенного ордера
    2. не увеличилось количество позиций, но изменилось время изменения позиции и не изменился объём — означает срабатывание отложенного ордера и как следствие — наращивание позиции
    3. уменьшилось количество позиций — означает закрытие позиции
  3. не уменьшилось количество отложенных ордеров:
    1. увеличилось количество позиций — означает открытие новой позиции
    2. уменьшилось количество позиций — означает закрытие позиции
    3. не увеличилось количество позиций, но изменилось время изменения позиции и увеличился объём — означает добавление объёма к позиции
    4. не увеличилось количество позиций, но изменилось время изменения позиции и уменьшился объём — означает частичное закрытие позиции

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

//+------------------------------------------------------------------+
//| Класс-основа библиотеки                                          |
//+------------------------------------------------------------------+
class CEngine : public CObject
  {
private:
   CHistoryCollection   m_history;                       // Коллекция исторических ордеров и сделок
   CMarketCollection    m_market;                        // Коллекция рыночных ордеров и сделок
   CArrayObj            m_list_counters;                 // Список счётчиков таймера
   bool                 m_first_start;                   // Флаг первого запуска
   bool                 m_is_hedge;                      // Флаг хэдж-счёта
//--- Возвращает индекс счётчика по id
   int                  CounterIndex(const int id) const;
//--- Возвращает флаг первого запуска
   bool                 IsFirstStart(void);
public:
//--- Создаёт счётчик таймера
   void                 CreateCounter(const int id,const ulong frequency,const ulong pause);
//--- Таймер
   void                 OnTimer(void);
                        CEngine();
                       ~CEngine();
  };
//+------------------------------------------------------------------+
//| CEngine конструктор                                              |
//+------------------------------------------------------------------+
CEngine::CEngine() : m_first_start(true)
  {
   ::EventSetMillisecondTimer(TIMER_FREQUENCY);
   this.m_list_counters.Sort();
   this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE);
   this.m_is_hedge=bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
  }
//+------------------------------------------------------------------+

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

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

  1. закрыта полностью
  2. закрыта частично
  3. закрыта встречной
  4. закрыта по стоплосс
  5. закрыта по тейкпрофит
  6. и т.д.

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

//+------------------------------------------------------------------+
//| Список флагов торговых событий на счёте                          |
//+------------------------------------------------------------------+
enum ENUM_TRADE_EVENT_FLAGS
  {
   TRADE_EVENT_FLAG_NO_EVENT        =  0,                   // Нет события
   TRADE_EVENT_FLAG_ORDER_PLASED    =  1,                   // Отложенный ордер установлен
   TRADE_EVENT_FLAG_ORDER_REMOVED   =  2,                   // Отложенный ордер удалён
   TRADE_EVENT_FLAG_ORDER_ACTIVATED =  4,                   // Отложенный ордер активирован ценой
   TRADE_EVENT_FLAG_POSITION_OPENED =  8,                   // Позиция открыта
   TRADE_EVENT_FLAG_POSITION_CLOSED =  16,                  // Позиция закрыта
   TRADE_EVENT_FLAG_ACCOUNT_BALANCE =  32,                  // Балансная операция (уточнение в типе сделки)
   TRADE_EVENT_FLAG_PARTIAL         =  64,                  // Частичное исполнение
   TRADE_EVENT_FLAG_BY_POS          =  128,                 // Исполнение встречной позицией
   TRADE_EVENT_FLAG_SL              =  256,                 // Исполнение по StopLoss
   TRADE_EVENT_FLAG_TP              =  512                  // Исполнение по TakeProfit
  };
//+------------------------------------------------------------------+
//| Список возможных торговых событий на счёте                       |
//+------------------------------------------------------------------+
enum ENUM_TRADE_EVENT
  {
   TRADE_EVENT_NO_EVENT,                                    // Нет торгового события
   TRADE_EVENT_PENDING_ORDER_PLASED,                        // Отложенный ордер установлен
   TRADE_EVENT_PENDING_ORDER_REMOVED,                       // Отложенный ордер удалён
//--- члены перечисления, совпадающие с членами перечисления ENUM_DEAL_TYPE
   TRADE_EVENT_ACCOUNT_CREDIT,                              // Начисление кредита
   TRADE_EVENT_ACCOUNT_CHARGE,                              // Дополнительные сборы
   TRADE_EVENT_ACCOUNT_CORRECTION,                          // Корректирующая запись
   TRADE_EVENT_ACCOUNT_BONUS,                               // Перечисление бонусов
   TRADE_EVENT_ACCOUNT_COMISSION,                           // Дополнительные комиссии
   TRADE_EVENT_ACCOUNT_COMISSION_DAILY,                     // Комиссия, начисляемая в конце торгового дня
   TRADE_EVENT_ACCOUNT_COMISSION_MONTHLY,                   // Комиссия, начисляемая в конце месяца
   TRADE_EVENT_ACCOUNT_COMISSION_AGENT_DAILY,               // Агентская комиссия, начисляемая в конце торгового дня
   TRADE_EVENT_ACCOUNT_COMISSION_AGENT_MONTHLY,             // Агентская комиссия, начисляемая в конце месяца
   TRADE_EVENT_ACCOUNT_INTEREST,                            // Начисления процентов на свободные средства
   TRADE_EVENT_BUY_CANCELLED,                               // Отмененная сделка покупки
   TRADE_EVENT_SELL_CANCELLED,                              // Отмененная сделка продажи
   TRADE_EVENT_DIVIDENT,                                    // Начисление дивиденда
   TRADE_EVENT_DIVIDENT_FRANKED,                            // Начисление франкированного дивиденда
   TRADE_EVENT_TAX,                                         // Начисление налога
//--- члены перечисления, относящиеся к типу сделки DEAL_TYPE_BALANCE из перечисления ENUM_DEAL_TYPE
   TRADE_EVENT_ACCOUNT_BALANCE_REFILL,                      // Пополнение средств на балансе
   TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL,                  // Снятие средств с баланса
//---
   TRADE_EVENT_PENDING_ORDER_ACTIVATED,                     // Отложенный ордер активирован ценой
   TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL,             // Отложенный ордер активирован ценой частично
   TRADE_EVENT_POSITION_OPENED,                             // Позиция открыта
   TRADE_EVENT_POSITION_OPENED_PARTIAL,                     // Позиция открыта частично
   TRADE_EVENT_POSITION_CLOSED,                             // Позиция закрыта
   TRADE_EVENT_POSITION_CLOSED_PARTIAL,                     // Позиция закрыта частично
   TRADE_EVENT_POSITION_CLOSED_BY_POS,                      // Позиция закрыта встречной
   TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS,              // Позиция закрыта частично встречной
   TRADE_EVENT_POSITION_CLOSED_BY_SL,                       // Позиция закрыта по StopLoss
   TRADE_EVENT_POSITION_CLOSED_BY_TP,                       // Позиция закрыта по TakeProfit
   TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL,               // Позиция закрыта частично по StopLoss
   TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP,               // Позиция закрыта частично по TakeProfit
   TRADE_EVENT_POSITION_REVERSED,                           // Разворот позиции (неттинг)
   TRADE_EVENT_POSITION_VOLUME_ADD                          // Добавлен объём к позиции (неттинг)
  };
//+------------------------------------------------------------------+

Здесь стоит пояснить по перечислению ENUM_TRADE_EVENT.
Так как некоторые торговые события могут происходить без участия программы или трейдера, например — начисление комиссий, сборов, бонусов и пр., то в связи с этим мы в MQL5 будем брать такие данные из типа сделки — из перечисления ENUM_DEAL_TYPE. И чтобы проще было потом отслеживать событие, нам необходимо слелать так, чтобы наши события совпадали по значению его перечисления со значением перечисления из ENUM_DEAL_TYPE.
Балансную операцию мы разделим на два события: на пополнение баланса счёта и на снятие средств со счёта, остальные же события из перечисления типа сделки (кроме покупки и продажи (DEAL_TYPE_BUY и DEAL_TYPE_SELL), которые не относятся к таким балансным операциям), начиная от DEAL_TYPE_CREDIT мы сделали с такими же значениями, что и в перечислении ENUM_DEAL_TYPE.

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

//+------------------------------------------------------------------+
//| Коллекция рыночных ордеров и позиций                             |
//+------------------------------------------------------------------+
class CMarketCollection
  {
private:
   struct MqlDataCollection
     {
      long           hash_sum_acc;           // Хэш-сумма всех ордеров и позиций на счёте
      int            total_pending;          // Количество отложенных ордеров на счёте
      int            total_positions;        // Количество позиций на счёте
      double         total_volumes;          // Общий объём ордеров и позиций на счёте
     };
   MqlDataCollection m_struct_curr_market;   // Текущие данные рыночных ордеров и позиций на счёте
   MqlDataCollection m_struct_prev_market;   // Прошлые данные рыночных ордеров и позиций на счёте
   CArrayObj         m_list_all_orders;      // Список отложенных ордеров и позиций на счёте
   COrder            m_order_instance;       // Объект-ордер для поиска по свойству
   bool              m_is_trade_event;       // Флаг торгового события
   bool              m_is_change_volume;     // Флаг изменения общего объёма
   double            m_change_volume_value;  // Величина изменения общего объёма
   int               m_new_positions;        // Количество новых позиций
   int               m_new_pendings;         // Количество новых отложенных ордеров
   //--- Сохраняет текущие значения состояния данных счёта как прошлые
   void              SavePrevValues(void)                                                                { this.m_struct_prev_market=this.m_struct_curr_market;                  }
public:
   //--- Возвращает список всех отложенных ордеров и открытых позиций
   CArrayObj*        GetList(void)                                                                       { return &m_list_all_orders;                                            }
   //--- Возвращает список ордеров и позиций со временем открытия в диапазоне от begin_time до end_time
   CArrayObj*        GetListByTime(const datetime begin_time=0,const datetime end_time=0);
   //--- Возвращает список ордеров и позиций по выбранному (1) double, (2) integer и (3) string свойству, удовлетворяющему сравниваемому условию
   CArrayObj*        GetList(ENUM_ORDER_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByOrderProperty(this.GetList(),property,value,mode);  }
   CArrayObj*        GetList(ENUM_ORDER_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode=EQUAL)  { return CSelect::ByOrderProperty(this.GetList(),property,value,mode);  }
   CArrayObj*        GetList(ENUM_ORDER_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByOrderProperty(this.GetList(),property,value,mode);  }
   //--- Возвращает количество (1) новых отложенных ордеров, (2) новых позиций, (3) флаг произошедшего торгового события (4) величину изменённого объёма
   int               NewOrders(void)                                                            const    { return this.m_new_pendings;                                           }
   int               NewPosition(void)                                                          const    { return this.m_new_positions;                                          }
   bool              IsTradeEvent(void)                                                         const    { return this.m_is_trade_event;                                         }
   double            ChangedVolumeValue(void)                                                   const    { return this.m_change_volume_value;                                    }
   //--- Конструктор
                     CMarketCollection(void);
   //--- Обновляет список отложенных ордеров и позиций
   void              Refresh(void);
  };
//+------------------------------------------------------------------+

За пределами тела класса напишем реализацию метода выбора ордеров и позиций по времени:

//+------------------------------------------------------------------+
//| Выбирает рыночные ордера или позиции из коллекции со временем    |
//| в диапазоне от begin_time, до end_time                           |
//+------------------------------------------------------------------+
CArrayObj* CMarketCollection::GetListByTime(const datetime begin_time=0,const datetime end_time=0)
  {
   CArrayObj* list=new CArrayObj();
   if(list==NULL)
     {
      ::Print(DFUN,TextByLanguage("Ошибка создания временного списка","Error creating temporary list"));
      return NULL;
     }
   datetime begin=begin_time,end=(end_time==0 ? END_TIME : end_time);
   list.FreeMode(false);
   ListStorage.Add(list);
   m_order_instance.SetProperty(ORDER_PROP_TIME_OPEN,begin);
   int index_begin=m_list_all_orders.SearchGreatOrEqual(&m_order_instance);
   if(index_begin==WRONG_VALUE)
      return list;
   m_order_instance.SetProperty(ORDER_PROP_TIME_OPEN,end);
   int index_end=m_list_all_orders.SearchLessOrEqual(&m_order_instance);
   if(index_end==WRONG_VALUE)
      return list;
   for(int i=index_begin; i<=index_end; i++)
      list.Add(m_list_all_orders.At(i));
   return list;
  }
//+------------------------------------------------------------------+

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

Так же изменим конструктор класса коллекции исторических ордеров и сделок: дело в том, что в ордерной системе MQL5 у ордера нет понятия о времени закрытия — все ордера и сделки расположены в списках в соответствии со временем их установки (временем открытия в классификации ордерной системы в MQL4). Для этого изменим строку, задающую направление сортировки в списке-коллекции исторических ордеров и сделок в конструкторе класса CHistoryCollection:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CHistoryCollection::CHistoryCollection(void) : m_index_deal(0),m_delta_deal(0),m_index_order(0),m_delta_order(0),m_is_trade_event(false)
  {
   this.m_list_all_orders.Sort(#ifdef __MQL5__ SORT_BY_ORDER_TIME_OPEN #else SORT_BY_ORDER_TIME_CLOSE #endif );
   this.m_list_all_orders.Clear();
  }
//+------------------------------------------------------------------+

Теперь в MQL5 все ордера и сделки в коллекции исторических ордеров и сделок по умолчанию будут отсортированы по времени их установки, а в MQL4 — по времени закрытия, прописанном в свойствах ордера.

Теперь стоит отметить ещё одну особенность ордерной системы MQL5. При закрытии позиции встречной, выставляется специальный закрывающий ордер с типом ORDER_TYPE_CLOSE_BY, а при закрытии по стоп-приказу выставляется маркет-ордер на закрытие позиции.
Чтобы можно было учитывать рыночные маркет-ордера, пришлось добавить ещё одно свойство к методам получения и возврата целочисленных свойств базового ордера (на примере получения и возврата магического номера ордера):

//+------------------------------------------------------------------+
//| Возвращает магик                                                 |
//+------------------------------------------------------------------+
long COrder::OrderMagicNumber() const
  {
#ifdef __MQL4__
   return ::OrderMagicNumber();
#else
   long res=0;
   switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS))
     {
      case ORDER_STATUS_MARKET_POSITION   : res=::PositionGetInteger(POSITION_MAGIC);           break;
      case ORDER_STATUS_MARKET_ORDER      :
      case ORDER_STATUS_MARKET_PENDING    : res=::OrderGetInteger(ORDER_MAGIC);                 break;
      case ORDER_STATUS_DEAL              : res=::HistoryDealGetInteger(m_ticket,DEAL_MAGIC);   break;
      case ORDER_STATUS_HISTORY_PENDING   :
      case ORDER_STATUS_HISTORY_ORDER     : res=::HistoryOrderGetInteger(m_ticket,ORDER_MAGIC); break;
      default                             : res=0;                                              break;
     }
   return res;
#endif
  }
//+------------------------------------------------------------------+

Такие (или логически соответствующие методу) изменения я сделал во всех методах получения и возврата целочисленных свойств базового ордера — там, где этот статус действительно необходимо учитывать. И коль уж есть такой статус, то для хранения такого типа ордеров создадим новый класс маркет-ордера CMarketOrder в папке Objects библиотеки. Класс совершенно идентичен остальным объектам рыночных и исторических ордеров и сделок, ранее нами созданным, поэтому приведу здесь лишь его листинг:

//+------------------------------------------------------------------+
//|                                                  MarketOrder.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2018, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "Order.mqh"
//+------------------------------------------------------------------+
//| Рыночный маркет-ордер                                            |
//+------------------------------------------------------------------+
class CMarketOrder : public COrder
  {
public:
   //--- Конструктор
                     CMarketOrder(const ulong ticket=0) : COrder(ORDER_STATUS_MARKET_ORDER,ticket) {}
   //--- Поддерживаемые свойства ордера (1) вещественные, (2) целочисленные
   virtual bool      SupportProperty(ENUM_ORDER_PROP_DOUBLE property);
   virtual bool      SupportProperty(ENUM_ORDER_PROP_INTEGER property);
  };
//+------------------------------------------------------------------+
//| Возвращает истину, если ордер поддерживает переданное            |
//| целочисленное свойство, возвращает ложь в противном случае       |
//+------------------------------------------------------------------+
bool CMarketOrder::SupportProperty(ENUM_ORDER_PROP_INTEGER property)
  {
   if(property==ORDER_PROP_TIME_EXP          || 
      property==ORDER_PROP_DEAL_ENTRY        || 
      property==ORDER_PROP_TIME_UPDATE       || 
      property==ORDER_PROP_TIME_UPDATE_MSC   ||
      property==ORDER_PROP_PROFIT_PT         ||
      property==ORDER_PROP_TIME_CLOSE        ||
      property==ORDER_PROP_TIME_CLOSE_MSC    ||
      property==ORDER_PROP_TICKET_FROM       ||
      property==ORDER_PROP_TICKET_TO
     ) return false;
   return true;
  }
//+------------------------------------------------------------------+
//| Возвращает истину, если ордер поддерживает переданное            |
//| вещественное свойство, возвращает ложь в противном случае        |
//+------------------------------------------------------------------+
bool CMarketOrder::SupportProperty(ENUM_ORDER_PROP_DOUBLE property)
  {
   if(property==ORDER_PROP_PROFIT            || 
      property==ORDER_PROP_PROFIT_FULL       || 
      property==ORDER_PROP_SWAP              || 
      property==ORDER_PROP_COMMISSION        ||
      property==ORDER_PROP_PRICE_CLOSE       ||
      property==ORDER_PROP_SL                ||
      property==ORDER_PROP_TP                ||
      property==ORDER_PROP_PRICE_STOP_LIMIT
     ) return false;
   return true;
  }
//+------------------------------------------------------------------+

В файле Defines.mqh библиотеки пропишем новый статус — маркет-ордер:

//+------------------------------------------------------------------+
//| Тип (статус) абстрактного ордера                                 |
//+------------------------------------------------------------------+
enum ENUM_ORDER_STATUS
  {
   ORDER_STATUS_MARKET_PENDING,                             // Рыночный отложенный ордер
   ORDER_STATUS_MARKET_ORDER,                               // Рыночный маркет-ордер
   ORDER_STATUS_MARKET_POSITION,                            // Рыночная позиция
   ORDER_STATUS_HISTORY_ORDER,                              // Исторический маркет-ордер
   ORDER_STATUS_HISTORY_PENDING,                            // Удаленный отложенный ордер
   ORDER_STATUS_BALANCE,                                    // Балансная операция
   ORDER_STATUS_CREDIT,                                     // Кредитная операция
   ORDER_STATUS_DEAL,                                       // Сделка
   ORDER_STATUS_UNKNOWN                                     // Неизвестный статус
  };
//+------------------------------------------------------------------+

Теперь в классе CMarketCollection в методе обновления списка рыночных ордеров и позиций Refresh() в блоке добавления ордеров в список необходимо сделать проверку типа ордера и далее, в зависимости от типа, добавлять либо объект маркет-ордера, либо объект отложенного ордера в список-коллекцию:

//+------------------------------------------------------------------+
//| Обновляет список ордеров                                         |
//+------------------------------------------------------------------+
void CMarketCollection::Refresh(void)
  {
   ::ZeroMemory(this.m_struct_curr_market);
   this.m_is_trade_event=false;
   this.m_is_change_volume=false;
   this.m_new_pendings=0;
   this.m_new_positions=0;
   this.m_change_volume_value=0;
   m_list_all_orders.Clear();
#ifdef __MQL4__
   int total=::OrdersTotal();
   for(int i=0; i<total; i++)
     {
      if(!::OrderSelect(i,SELECT_BY_POS)) continue;
      long ticket=::OrderTicket();
      ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::OrderType();
      if(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL)
        {
         CMarketPosition *position=new CMarketPosition(ticket);
         if(position==NULL) continue;
         if(this.m_list_all_orders.InsertSort(position))
           {
            this.m_struct_market.hash_sum_acc+=ticket;
            this.m_struct_market.total_volumes+=::OrderLots();
            this.m_struct_market.total_positions++;
           }
         else
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить позицию в список","Failed to add position to the list"));
            delete position;
           }
        }
      else
        {
         CMarketPending *order=new CMarketPending(ticket);
         if(order==NULL) continue;
         if(this.m_list_all_orders.InsertSort(order))
           {
            this.m_struct_market.hash_sum_acc+=ticket;
            this.m_struct_market.total_volumes+=::OrderLots();
            this.m_struct_market.total_pending++;
           }
         else
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to the list"));
            delete order;
           }
        }
     }
//--- MQ5
#else 
//--- Позиции
   int total_positions=::PositionsTotal();
   for(int i=0; i<total_positions; i++)
     {
      ulong ticket=::PositionGetTicket(i);
      if(ticket==0) continue;
      CMarketPosition *position=new CMarketPosition(ticket);
      if(position==NULL) continue;
      if(this.m_list_all_orders.InsertSort(position))
        {
         this.m_struct_curr_market.hash_sum_acc+=(long)::PositionGetInteger(POSITION_TIME_UPDATE_MSC);
         this.m_struct_curr_market.total_volumes+=::PositionGetDouble(POSITION_VOLUME);
         this.m_struct_curr_market.total_positions++;
        }
      else
        {
         ::Print(DFUN,TextByLanguage("Не удалось добавить позицию в список","Failed to add position to the list"));
         delete position;
        }
     }
//--- Ордера
   int total_orders=::OrdersTotal();
   for(int i=0; i<total_orders; i++)
     {
      ulong ticket=::OrderGetTicket(i);
      if(ticket==0) continue;
      ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::OrderGetInteger(ORDER_TYPE);
      if(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL)
        {
         CMarketOrder *order=new CMarketOrder(ticket);
         if(order==NULL) continue;
         if(this.m_list_all_orders.InsertSort(order))
           {
            this.m_struct_curr_market.hash_sum_acc+=(long)ticket;
            this.m_struct_curr_market.total_market++;
           }
         else
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить маркет-ордер в список","Failed to add market-order to the list"));
            delete order;
           }
        }
      else
        {
         CMarketPending *order=new CMarketPending(ticket);
         if(order==NULL) continue;
         if(this.m_list_all_orders.InsertSort(order))
           {
            this.m_struct_curr_market.hash_sum_acc+=(long)ticket;
            this.m_struct_curr_market.total_volumes+=::OrderGetDouble(ORDER_VOLUME_INITIAL);
            this.m_struct_curr_market.total_pending++;
           }
         else
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить отложенный ордер в список","Failed to add pending order to the list"));
            delete order;
           }
        }
     }
#endif 
//--- Первый запуск
   if(this.m_struct_prev_market.hash_sum_acc==WRONG_VALUE)
     {
      this.SavePrevValues();
     }
//--- Если хэш-сумма всех ордеров и позиций изменилась
   if(this.m_struct_curr_market.hash_sum_acc!=this.m_struct_prev_market.hash_sum_acc)
     {
      this.m_new_market=this.m_struct_curr_market.total_market-this.m_struct_prev_market.total_market;
      this.m_new_pendings=this.m_struct_curr_market.total_pending-this.m_struct_prev_market.total_pending;
      this.m_new_positions=this.m_struct_curr_market.total_positions-this.m_struct_prev_market.total_positions;
      this.m_change_volume_value=::NormalizeDouble(this.m_struct_curr_market.total_volumes-this.m_struct_prev_market.total_volumes,4);
      this.m_is_change_volume=(this.m_change_volume_value!=0 ? true : false);
      this.m_is_trade_event=true;
      this.SavePrevValues();
     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Обновляет список ордеров и сделок                                |
//+------------------------------------------------------------------+
void CHistoryCollection::Refresh(void)
  {
#ifdef __MQL4__
   int total=::OrdersHistoryTotal(),i=m_index_order;
   for(; i<total; i++)
     {
      if(!::OrderSelect(i,SELECT_BY_POS,MODE_HISTORY)) continue;
      ENUM_ORDER_TYPE order_type=(ENUM_ORDER_TYPE)::OrderType();
      //--- Закрытые позиции и балансные/кредитные операции
      if(order_type<ORDER_TYPE_BUY_LIMIT || order_type>ORDER_TYPE_SELL_STOP)
        {
         CHistoryOrder *order=new CHistoryOrder(::OrderTicket());
         if(order==NULL) continue;
         if(!this.m_list_all_orders.InsertSort(order))
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to the list"));
            delete order;
           }
        }
      else
        {
         //--- Удалённые отложенные ордера
         CHistoryPending *order=new CHistoryPending(::OrderTicket());
         if(order==NULL) continue;
         if(!this.m_list_all_orders.InsertSort(order))
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to the list"));
            delete order;
           }
        }
     }
//---
   int delta_order=i-m_index_order;
   this.m_index_order=i;
   this.m_delta_order=delta_order;
   this.m_is_trade_event=(this.m_delta_order!=0 ? true : false);
//--- __MQL5__
#else 
   if(!::HistorySelect(0,END_TIME)) return;
//--- Ордера
   int total_orders=::HistoryOrdersTotal(),i=m_index_order;
   for(; i<total_orders; i++)
     {
      ulong order_ticket=::HistoryOrderGetTicket(i);
      if(order_ticket==0) continue;
      ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::HistoryOrderGetInteger(order_ticket,ORDER_TYPE);
      if(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL || type==ORDER_TYPE_CLOSE_BY)
        {
         CHistoryOrder *order=new CHistoryOrder(order_ticket);
         if(order==NULL) continue;
         if(!this.m_list_all_orders.InsertSort(order))
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to the list"));
            delete order;
           }
        }
      else
        {
         CHistoryPending *order=new CHistoryPending(order_ticket);
         if(order==NULL) continue;
         if(!this.m_list_all_orders.InsertSort(order))
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to the list"));
            delete order;
           }
        }
     }
//--- сохранение индекса последнего добавленного ордера и разницы по сравнению с прошлой проверкой
   int delta_order=i-this.m_index_order;
   this.m_index_order=i;
   this.m_delta_order=delta_order;

//--- Сделки
   int total_deals=::HistoryDealsTotal(),j=m_index_deal;
   for(; j<total_deals; j++)
     {
      ulong deal_ticket=::HistoryDealGetTicket(j);
      if(deal_ticket==0) continue;
      CHistoryDeal *deal=new CHistoryDeal(deal_ticket);
      if(deal==NULL) continue;
      this.m_list_all_orders.InsertSort(deal);
     }
//--- сохранение индекса последней добавленной сделки и разницы по сравнению с прошлой проверкой
   int delta_deal=j-this.m_index_deal;
   this.m_index_deal=j;
   this.m_delta_deal=delta_deal;
//--- Установка флага нового события в истории
   this.m_is_trade_event=(this.m_delta_order+this.m_delta_deal);
#endif 
  }
//+------------------------------------------------------------------+

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


Продолжим работу над определением событий.

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

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

//+------------------------------------------------------------------+
//| Класс-основа библиотеки                                          |
//+------------------------------------------------------------------+
class CEngine : public CObject
  {
private:
   CHistoryCollection   m_history;                       // Коллекция исторических ордеров и сделок
   CMarketCollection    m_market;                        // Коллекция рыночных ордеров и сделок
   CArrayObj            m_list_counters;                 // Список счётчиков таймера
   bool                 m_first_start;                   // Флаг первого запуска
   bool                 m_is_hedge;                      // Флаг хедж-счёта
   bool                 m_is_market_trade_event;         // Флаг торгового события на счёте
   bool                 m_is_history_trade_event;        // Флаг торгового события в истории счёта
   int                  m_trade_event_code;              // Код состояния торгового события на аккаунте
//--- Возвращает индекс счётчика по id
   int                  CounterIndex(const int id) const;
//--- Возвращает флаг первого запуска
   bool                 IsFirstStart(void);
//--- Работа с коллекциями (1) хедж, (2) неттинг
   void                 WorkWithHedgeCollections(void);
   void                 WorkWithNettoCollections(void);
//--- Возвращает последний (1) рыночный отложенный ордер, (2) маркет-отложенный ордер, (3) последнюю позицию, (4) позицию по тикету
   COrder*              GetLastMarketPending(void);                    
   COrder*              GetLastMarketOrder(void);                      
   COrder*              GetLastPosition(void);                         
   COrder*              GetPosition(const ulong ticket);               
//--- Возвращает последний (1) удалённый отложенный ордер, (2) исторический маркет-ордер, (3) исторический маркет-ордер по его тикету
   COrder*              GetLastHistoryPending(void);                   
   COrder*              GetLastHistoryOrder(void);                     
   COrder*              GetHistoryOrder(const ulong ticket);           
//--- Возвращает (1) первый и (2) последний исторический маркет-ордер из списка всех ордеров позиции, (3) последнюю сделку
   COrder*              GetFirstOrderPosition(const ulong position_id);
   COrder*              GetLastOrderPosition(const ulong position_id); 
   COrder*              GetLastDeal(void);                             
public:
   //--- Возвращает список рыночных (1) позиций, (2) отложенных ордеров и (3) маркет-ордеров
   CArrayObj*           GetListMarketPosition(void);                     
   CArrayObj*           GetListMarketPendings(void);                     
   CArrayObj*           GetListMarketOrders(void);                       
   //--- Возвращает список исторических (1) ордеров, (2) удалённых отложенных ордеров, (3) сделок, (4) всех маркет-ордеров позиции по её идентификатору
   CArrayObj*           GetListHistoryOrders(void);                      
   CArrayObj*           GetListHistoryPendings(void);                    
   CArrayObj*           GetListHistoryDeals(void);                       
   CArrayObj*           GetListAllOrdersByPosID(const ulong position_id);
//--- Возвращает (1) код торгового события, (2) флаг счёта-хедж
   int                  TradeEventCode(void)             const { return this.m_trade_event_code;   }
   bool                 IsHedge(void)                    const { return this.m_is_hedge;           }
//--- Создаёт счётчик таймера
   void                 CreateCounter(const int id,const ulong frequency,const ulong pause);
//--- Таймер
   void                 OnTimer(void);
//--- Конструктор/Деструктор
                        CEngine();
                       ~CEngine();
  };
//+------------------------------------------------------------------+

В конструкторе класса в его списке инициализации инициализируем код торгового события.

//+------------------------------------------------------------------+
//| CEngine конструктор                                              |
//+------------------------------------------------------------------+
CEngine::CEngine() : m_first_start(true),m_trade_event_code(TRADE_EVENT_FLAG_NO_EVENT)
  {
   ::EventSetMillisecondTimer(TIMER_FREQUENCY);
   this.m_list_counters.Sort();
   this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE);
   this.m_is_hedge=bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
  }
//+------------------------------------------------------------------+

За пределами тела класса напишем реализацию объявленных методов:

//+------------------------------------------------------------------+
//| Возвращает список рыночных позиций                               |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListMarketPosition(void)
  {
   CArrayObj* list=this.m_market.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_POSITION,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//| Возвращает список рыночных отложенных ордеров                    |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListMarketPendings(void)
  {
   CArrayObj* list=this.m_market.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_PENDING,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//| Возвращает список рыночных маркет-ордеров                        |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListMarketOrders(void)
  {
   CArrayObj* list=this.m_market.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_ORDER,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//| Возвращает список исторических ордеров                           |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListHistoryOrders(void)
  {
   CArrayObj* list=this.m_history.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_HISTORY_ORDER,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//| Возвращает список удалённых отложенных ордеров                   |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListHistoryPendings(void)
  {
   CArrayObj* list=this.m_history.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_HISTORY_PENDING,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//| Возвращает список сделок                                         |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListHistoryDeals(void)
  {
   CArrayObj* list=this.m_history.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_DEAL,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//|  Возвращает список всех ордеров позиции                          |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListAllOrdersByPosID(const ulong position_id)
  {
   CArrayObj* list=this.GetListHistoryOrders();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_POSITION_ID,position_id,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//| Возвращает последнюю позицию                                     |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastPosition(void)
  {
   CArrayObj* list=this.GetListMarketPosition();
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| Возвращает позицию по тикету                                     |
//+------------------------------------------------------------------+
COrder* CEngine::GetPosition(const ulong ticket)
  {
   CArrayObj* list=this.GetListMarketPosition();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,ticket,EQUAL);
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TICKET);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| Возвращает последнюю сделку                                      |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastDeal(void)
  {
   CArrayObj* list=this.GetListHistoryDeals();
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| Возвращает последний рыночный отложенный ордер                   |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastMarketPending(void)
  {
   CArrayObj* list=this.GetListMarketPendings();
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| Возвращает последний исторический отложенный ордер               |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastHistoryPending(void)
  {
   CArrayObj* list=this.GetListHistoryPendings();
   if(list==NULL) return NULL;
   list.Sort(#ifdef __MQL5__ SORT_BY_ORDER_TIME_OPEN_MSC #else SORT_BY_ORDER_TIME_CLOSE_MSC #endif);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| Возвращает последний рыночный маркет-ордер                       |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastMarketOrder(void)
  {
   CArrayObj* list=this.GetListMarketOrders();
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| Возвращает последний исторический маркет-ордер                   |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastHistoryOrder(void)
  {
   CArrayObj* list=this.GetListHistoryOrders();
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| Возвращает исторический маркет-ордер по его тикету               |
//+------------------------------------------------------------------+
COrder* CEngine::GetHistoryOrder(const ulong ticket)
  {
   CArrayObj* list=this.GetListHistoryOrders();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,(long)ticket,EQUAL);
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TICKET);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| Возвращает первый исторический маркет-ордер                      |
//| из списка всех ордеров позиции                                   |
//+------------------------------------------------------------------+
COrder* CEngine::GetFirstOrderPosition(const ulong position_id)
  {
   CArrayObj* list=this.GetListAllOrdersByPosID(position_id);
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN);
   COrder* order=list.At(0);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| Возвращает последний исторический маркет-ордер                   |
//| из списка всех ордеров позиции                                   |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastOrderPosition(const ulong position_id)
  {
   CArrayObj* list=this.GetListAllOrdersByPosID(position_id);
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+

Разберём получение списков на примере:

//+------------------------------------------------------------------+
//| Возвращает список рыночных позиций                               |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListMarketPosition(void)
  {
   CArrayObj* list=this.m_market.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_POSITION,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+

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

Получение нужного ордера разберём на примере:

//+------------------------------------------------------------------+
//| Возвращает последнюю позицию                                     |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastPosition(void)
  {
   CArrayObj* list=this.GetListMarketPosition();
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+

Сначала получаем список только позиций выше рассмотренным методом GetListMarketPosition(); если список пустой — возвращаем NULL. Далее сортируем список по времени открытия в милисекундах (так как собираемся получить последнюю открытую позицию, то необходимо, чтобы список был отсортирован по времени) и выбираем из этого списка самый последний в нём ордер. В итоге возвращаем полученный из списка ордер в вызывающую программу.
Результат поиска ордера, возвращаемый методом, может быть пустым (ордер не найден) и иметь значение NULL, поэтому нужно перед обращением к нему проверить полученный результат на NULL.

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

Теперь напишем реализацию метода проверки торговых событий.
Пока написана реализация только для счетов с типом хедж для MQL5. Затем будут написаны методы проверки торговых событий для неттинговых счётов на MQL5, а затем и для MQL4. Следует отметить, что для теста работы метода в нём прописан вывод результатов проверок и событий в журнал, что далее будет убрано — этим будет заниматься иной метод, который будет создан после отладки метода проверки торговых событий на счёте.

//+------------------------------------------------------------------+
//| Проверка торговых событий (хедж)                                 |
//+------------------------------------------------------------------+
void CEngine::WorkWithHedgeCollections(void)
  {
//--- Инициализация кода и флагов торговых событий
   this.m_trade_event_code=TRADE_EVENT_FLAG_NO_EVENT;
   this.m_is_market_trade_event=false;
   this.m_is_history_trade_event=false;
//--- Обновление списков 
   this.m_market.Refresh();
   this.m_history.Refresh();
//--- Действия при первом запуске
   if(this.IsFirstStart())
     {
      this.m_acc_trade_event=TRADE_EVENT_NO_EVENT;
      return;
     }
//--- Проверка изменения рыночного состояния и в истории счёта
   this.m_is_market_trade_event=this.m_market.IsTradeEvent();
   this.m_is_history_trade_event=this.m_history.IsTradeEvent();
//---
#ifdef __MQL4__

#else // MQL5
//--- Если событие только в рыночных ордерах и позициях
   if(this.m_is_market_trade_event && !this.m_is_history_trade_event)
     {
      Print(DFUN,TextByLanguage("Новое торговое событие на счёте","New trading event on the account"));
      //--- Если количество отложенных ордеров увеличилось
      if(this.m_market.NewPendingOrders()>0)
        {
         //--- Добавим флаг установки отложенного ордера
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_PLASED;
         string text=TextByLanguage("Установлен отложенный ордер: ","Pending order placed: ");
         //--- Возьмём последний рыночный отложенный ордер
         COrder* order=this.GetLastMarketPending();
         if(order!=NULL)
           {
            //--- впишем тикет ордера в сообщение
            text+=order.TypeDescription()+" #"+(string)order.Ticket();
           }
         //--- Выведем сообщение в журнал
         Print(DFUN,text);
        }
      //--- Если количество маркет-ордеров увеличилось
      if(this.m_market.NewMarketOrders()>0)
        {
         //--- флаг этого события не добавляем
         //--- ...
         string text=TextByLanguage("Выставлен маркет-ордер: ","Market-order placed: ");
         //--- Возьмём последний рыночный маркет-ордер
         COrder* order=this.GetLastMarketOrder();
         if(order!=NULL)
           {
            //--- впишем тикет ордера в сообщение
            text+=order.TypeDescription()+" #"+(string)order.Ticket();
           }
         //--- Выведем сообщение в журнал
         Print(DFUN,text);
        }
     }
   
//--- Если событие только в исторических ордерах и сделках
   else if(this.m_is_history_trade_event && !this.m_is_market_trade_event)
     {
      Print(DFUN,TextByLanguage("Новое торговое событие в истории счёта","New trading event in the account history"));
      //--- Если появилась новая сделка
      if(this.m_history.NewDeals()>0)
        {
         //--- Добавим флаг появления балансного события на счёте
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ACCOUNT_BALANCE;
         string text=TextByLanguage("Новая сделка: ","New deal: ");
         //--- Возьмём последнюю сделку
         COrder* deal=this.GetLastDeal();
         if(deal!=NULL)
           {
            //--- впишем в текст её описание
            text+=deal.TypeDescription();
            //--- если сделка - балансная операция
            if((ENUM_DEAL_TYPE)deal.GetProperty(ORDER_PROP_TYPE)==DEAL_TYPE_BALANCE)
              {
              //--- проверим профит сделки и впишем событие (пополнение или снятие средств) в сообщение
               text+=(deal.Profit()>0 ? TextByLanguage(": Пополнение счёта: ",": Account Recharge: ") : TextByLanguage(": Вывод средств: ",": Withdrawal: "))+::DoubleToString(deal.Profit(),(int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS));
              }
           }
         //--- Выведем сообщение в журнал
         Print(DFUN,text);
        }
     }
   
//--- Если события в рыночных и исторических ордерах и позициях
   else if(this.m_is_market_trade_event && this.m_is_history_trade_event)
     {
      Print(DFUN,TextByLanguage("Новые торговые события на счёте и в истории счёта","New trading events on the account and in the history of the account"));
      
      //--- Если количество отложенных ордеров уменьшилось и не появилось новых сделок
      if(this.m_market.NewPendingOrders()<0 && this.m_history.NewDeals()==0)
        {
         //--- Добавим флаг удаления отложенного ордера
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_REMOVED;
         string text=TextByLanguage("Удалён отложенный ордер: ","Removed pending order: ");
         //--- Возьмём последний исторический отложенный ордер
         COrder* order=this.GetLastHistoryPending();
         if(order!=NULL)
           {
            //--- впишем в сообщение его тикет
            text+=order.TypeDescription()+" #"+(string)order.Ticket();
           }
         //--- Выведем сообщение в журнал
         Print(DFUN,text);
        }
      
      //--- Если есть новая сделка и есть новый исторический ордер
      if(this.m_history.NewDeals()>0 && this.m_history.NewOrders()>0)
        {
         //--- Возьмём последнюю сделку
         COrder* deal=this.GetLastDeal();
         if(deal!=NULL)
           {
            //--- Если сделка на вход в рынок
            if(deal.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_IN)
              {
               //--- Добавим флаг открытия позиции
               this.m_trade_event_code+=TRADE_EVENT_FLAG_POSITION_OPENED;
               string text=TextByLanguage("Открыта позиция: ","Position is open: ");
               //--- Если количество отложенных ордеров уменьшилось
               if(this.m_market.NewPendingOrders()<0)
                 {
                  //--- Добавим флаг срабатывания отложенного ордера
                  this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_ACTIVATED;
                  text=TextByLanguage("Сработал отложенный ордер: ","Pending order activated: ");
                 }
               //--- Возьмём из сделки тикет её ордера
               ulong order_ticket=deal.GetProperty(ORDER_PROP_DEAL_ORDER);
               COrder* order=this.GetHistoryOrder(order_ticket);
               if(order!=NULL)
                 {
                  //--- Если текущий объём ордера больше нуля
                  if(order.VolumeCurrent()>0)
                    {
                     //--- Добавим флаг частичного исполнения
                     this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
                     text=TextByLanguage("Частично открыта позиция: ","Position partially open: ");
                    }
                  //--- добавим к сообщению направление ордера
                  text+=order.DirectionDescription();
                 }
               //--- добавим к сообщению тикет позиции из сделки и выведем сообщение в журнал
               text+=" #"+(string)deal.PositionID();
               Print(DFUN,text);
              }
            
            //--- Если сделка на выход из рынка
            if(deal.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_OUT)
              {
               //--- Добавим флаг закрытия позиции
               this.m_trade_event_code+=TRADE_EVENT_FLAG_POSITION_CLOSED;
               string text=TextByLanguage("Закрыта позиция: ","Position closed: ");
               //--- Возьмём из сделки тикет её ордера
               ulong order_ticket=deal.GetProperty(ORDER_PROP_DEAL_ORDER);
               COrder* order=this.GetHistoryOrder(order_ticket);
               if(order!=NULL)
                 {
                  //--- Если позиция сделки ещё присутствует в рынке
                  COrder* pos=this.GetPosition(deal.PositionID());
                  if(pos!=NULL)
                    {
                     //--- Добавим флаг частичного исполнения
                     this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
                     text=TextByLanguage("Частично закрыта позиция: ","Partially closed position: ");
                    }
                  //--- Иначе, если позиция закрыта полностью
                  else
                    {
                     //--- Если у ордера есть флаг закрытия по StopLoss
                     if(order.IsCloseByStopLoss())
                       {
                        //--- Добавим флаг закрытия по StopLoss
                        this.m_trade_event_code+=TRADE_EVENT_FLAG_SL;
                        text=TextByLanguage("Позиция закрыта по StopLoss: ","Position closed by StopLoss: ");
                       }
                     //--- Если у ордера есть флаг закрытия по TakeProfit
                     if(order.IsCloseByTakeProfit())
                       {
                        //--- Добавим флаг закрытия по TakeProfit
                        this.m_trade_event_code+=TRADE_EVENT_FLAG_TP;
                        text=TextByLanguage("Позиция закрыта по TakeProfit: ","Position closed by TakeProfit: ");
                       }
                    }
                  //--- добавим к сообщению обратное направление ордера:
                  //--- для позиции Sell закрывающий ордер Buy, а для позиции Buy закрывающий ордер Sell,
                  //--- поэтому реверсируем направление ордера для корректного описания закрытой позиции
                  text+=(order.DirectionDescription()=="Sell" ? "Buy " : "Sell ");
                 }
               //--- добавим к сообщению тикет позиции из сделки и выведем сообщение в журнал
               text+="#"+(string)deal.PositionID();
               Print(DFUN,text);
              }
            
            //--- Если закрытие встречной
            if(deal.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_OUT_BY)
              {
               //--- Добавим флаг закрытия позиции
               this.m_trade_event_code+=TRADE_EVENT_FLAG_POSITION_CLOSED;
               //--- Добавим флаг закрытия встречной
               this.m_trade_event_code+=TRADE_EVENT_FLAG_BY_POS;
               string text=TextByLanguage("Позиция закрыта встречной: ","Position closed by opposite position: ");
               //--- Возьмём ордер сделки
               ulong ticket_from=deal.GetProperty(ORDER_PROP_DEAL_ORDER);
               COrder* order=this.GetHistoryOrder(ticket_from);
               if(order!=NULL)
                 {
                  //--- добавим к сообщению обратное направление ордера:
                  //--- для позиции Sell закрывающий ордер Buy, а для позиции Buy закрывающий ордер Sell,
                  //--- поэтому реверсируем направление ордера для корректного описания закрытой позиции
                  text+=(order.DirectionDescription()=="Sell" ? "Buy" : "Sell");
                  text+=" #"+(string)order.PositionID()+TextByLanguage(" закрыта позицией #"," closed by position #")+(string)order.PositionByID();
                  //--- Если позиция ордера ещё присутствует в рынке
                  COrder* pos=this.GetPosition(order.PositionID());
                  if(pos!=NULL)
                    {
                     //--- Добавим флаг частичного исполнения
                     this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
                     text+=TextByLanguage(" частично"," partially");
                    }
                 }
               //--- Выведем сообщение в журнал
               Print(DFUN,text);
              }
           //--- конец блока обработки последней сделки
           }
        }
     }
#endif 
  }
//+------------------------------------------------------------------+

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

Тест обработки торговых событий

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

Набор входных параметров будет таким:

  • Magic number - магический номер
  • Lots - объём открываемых позиций
  • StopLoss in points - стоплосс в пунктах
  • TakeProfit in points - тейкпрофит в пунктах
  • Pending orders distance (points) - дистанция установки отложенных ордеров в пунктах
  • StopLimit orders distance (points) - дистанция установки Limit-ордера при достижении ценой цены установки StopLimit-ордера
    Тут стоит пояснить: StopLimit-ордер устанавлявается как стоповый ордер на дистанции от цены, задаваемой значением Pending orders distance.
    Как только цена дойдёт до установленного ордера и он сработает, то уже от этой цены будет выставлен лимитный ордер на дистанции от цены, задаваемой значением StopLimit orders distance.
  • Slippage in points - величина проскальзывания в пунктах
  • Withdrawal funds (in tester) - размер выводимых средств со счёта в тестере

Для расчётов цен установки ордеров относительно уровня StopLevel, стоп-приказов и объёма позиций нам потребуются функции расчёта корректных значений. На данном этапе, пока у нас нет торговых классов и классов символов, добавим просто функции в библиотеку сервисных функций в файле DELib.mqh:

//+------------------------------------------------------------------+
//| Возвращает минимальный лот символа                               |
//+------------------------------------------------------------------+
double MinimumLots(const string symbol_name) 
  { 
   return SymbolInfoDouble(symbol_name,SYMBOL_VOLUME_MIN);
  }
//+------------------------------------------------------------------+
//| Возвращает максимальный лот символа                              |
//+------------------------------------------------------------------+
double MaximumLots(const string symbol_name) 
  { 
   return SymbolInfoDouble(symbol_name,SYMBOL_VOLUME_MAX);
  }
//+------------------------------------------------------------------+
//| Возвращает шаг изменения лота символа                            |
//+------------------------------------------------------------------+
double StepLots(const string symbol_name) 
  { 
   return SymbolInfoDouble(symbol_name,SYMBOL_VOLUME_STEP);
  }
//+------------------------------------------------------------------+
//| Возвращает нормализованный лот                                   |
//+------------------------------------------------------------------+
double NormalizeLot(const string symbol_name, double order_lots) 
  {
   double ml=SymbolInfoDouble(symbol_name,SYMBOL_VOLUME_MIN);
   double mx=SymbolInfoDouble(symbol_name,SYMBOL_VOLUME_MAX);
   double ln=NormalizeDouble(order_lots,int(ceil(fabs(log(ml)/log(10)))));
   return(ln<ml ? ml : ln>mx ? mx : ln);
  }
//+------------------------------------------------------------------+
//| Возвращает корректный StopLoss относительно StopLevel            |
//+------------------------------------------------------------------+
double CorrectStopLoss(const string symbol_name,const ENUM_ORDER_TYPE order_type,const double price_set,const double stop_loss,const int spread_multiplier=2)
  {
   if(stop_loss==0) return 0;
   int lv=StopLevel(symbol_name,spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : order_type==ORDER_TYPE_SELL ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price_set);
   return
     (order_type==ORDER_TYPE_BUY       || 
      order_type==ORDER_TYPE_BUY_LIMIT || 
      order_type==ORDER_TYPE_BUY_STOP
      #ifdef __MQL5__                  ||
      order_type==ORDER_TYPE_BUY_STOP_LIMIT
      #endif ? 
      NormalizeDouble(fmin(price-lv*pt,stop_loss),dg) :
      NormalizeDouble(fmax(price+lv*pt,stop_loss),dg)
     );
  }
//+------------------------------------------------------------------+
//| Возвращает корректный StopLoss относительно StopLevel            |
//+------------------------------------------------------------------+
double CorrectStopLoss(const string symbol_name,const ENUM_ORDER_TYPE order_type,const double price_set,const int stop_loss,const int spread_multiplier=2)
  {
   if(stop_loss==0) return 0;
   int lv=StopLevel(symbol_name,spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : order_type==ORDER_TYPE_SELL ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price_set);
   return
     (order_type==ORDER_TYPE_BUY       || 
      order_type==ORDER_TYPE_BUY_LIMIT || 
      order_type==ORDER_TYPE_BUY_STOP
      #ifdef __MQL5__                  ||
      order_type==ORDER_TYPE_BUY_STOP_LIMIT
      #endif ?
      NormalizeDouble(fmin(price-lv*pt,price-stop_loss*pt),dg) :
      NormalizeDouble(fmax(price+lv*pt,price+stop_loss*pt),dg)
     );
  }
//+------------------------------------------------------------------+
//| Возвращает корректный TakeProfit относительно StopLevel          |
//+------------------------------------------------------------------+
double CorrectTakeProfit(const string symbol_name,const ENUM_ORDER_TYPE order_type,const double price_set,const double take_profit,const int spread_multiplier=2)
  {
   if(take_profit==0) return 0;
   int lv=StopLevel(symbol_name,spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : order_type==ORDER_TYPE_SELL ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price_set);
   return
     (order_type==ORDER_TYPE_BUY       || 
      order_type==ORDER_TYPE_BUY_LIMIT || 
      order_type==ORDER_TYPE_BUY_STOP
      #ifdef __MQL5__                  ||
      order_type==ORDER_TYPE_BUY_STOP_LIMIT
      #endif ?
      NormalizeDouble(fmax(price+lv*pt,take_profit),dg) :
      NormalizeDouble(fmin(price-lv*pt,take_profit),dg)
     );
  }
//+------------------------------------------------------------------+
//| Возвращает корректный TakeProfit относительно StopLevel          |
//+------------------------------------------------------------------+
double CorrectTakeProfit(const string symbol_name,const ENUM_ORDER_TYPE order_type,const double price_set,const int take_profit,const int spread_multiplier=2)
  {
   if(take_profit==0) return 0;
   int lv=StopLevel(symbol_name,spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : order_type==ORDER_TYPE_SELL ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price_set);
   return
     (order_type==ORDER_TYPE_BUY       || 
      order_type==ORDER_TYPE_BUY_LIMIT || 
      order_type==ORDER_TYPE_BUY_STOP
      #ifdef __MQL5__                  ||
      order_type==ORDER_TYPE_BUY_STOP_LIMIT
      #endif ?
      ::NormalizeDouble(::fmax(price+lv*pt,price+take_profit*pt),dg) :
      ::NormalizeDouble(::fmin(price-lv*pt,price-take_profit*pt),dg)
     );
  }
//+------------------------------------------------------------------+
//| Возвращает корректную цену постановки ордера                     |
//| относительно StopLevel                                           |
//+------------------------------------------------------------------+
double CorrectPricePending(const string symbol_name,const ENUM_ORDER_TYPE order_type,const double price_set,const double price=0,const int spread_multiplier=2)
  {
   double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT),pp=0;
   int lv=StopLevel(symbol_name,spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
   switch(order_type)
     {
      case ORDER_TYPE_BUY_LIMIT        :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); return NormalizeDouble(fmin(pp-lv*pt,price_set),dg);
      case ORDER_TYPE_BUY_STOP         :  
      case ORDER_TYPE_BUY_STOP_LIMIT   :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); return NormalizeDouble(fmax(pp+lv*pt,price_set),dg);
      case ORDER_TYPE_SELL_LIMIT       :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); return NormalizeDouble(fmax(pp+lv*pt,price_set),dg);
      case ORDER_TYPE_SELL_STOP        :  
      case ORDER_TYPE_SELL_STOP_LIMIT  :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); return NormalizeDouble(fmin(pp-lv*pt,price_set),dg);
      default                          :  Print(DFUN,TextByLanguage("Не правильный тип ордера: ","Invalid order type: "),EnumToString(order_type)); return 0;
     }
  }
//+------------------------------------------------------------------+
//| Возвращает корректную цену постановки ордера                     |
//| относительно StopLevel                                           |
//+------------------------------------------------------------------+
double CorrectPricePending(const string symbol_name,const ENUM_ORDER_TYPE order_type,const int distance_set,const double price=0,const int spread_multiplier=2)
  {
   double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT),pp=0;
   int lv=StopLevel(symbol_name,spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
   switch(order_type)
     {
      case ORDER_TYPE_BUY_LIMIT        :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); return NormalizeDouble(fmin(pp-lv*pt,pp-distance_set*pt),dg);
      case ORDER_TYPE_BUY_STOP         :  
      case ORDER_TYPE_BUY_STOP_LIMIT   :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); return NormalizeDouble(fmax(pp+lv*pt,pp+distance_set*pt),dg);
      case ORDER_TYPE_SELL_LIMIT       :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); return NormalizeDouble(fmax(pp+lv*pt,pp+distance_set*pt),dg);
      case ORDER_TYPE_SELL_STOP        :  
      case ORDER_TYPE_SELL_STOP_LIMIT  :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); return NormalizeDouble(fmin(pp-lv*pt,pp-distance_set*pt),dg);
      default                          :  Print(DFUN,TextByLanguage("Не правильный тип ордера: ","Invalid order type: "),EnumToString(order_type)); return 0;
     }
  }
//+------------------------------------------------------------------+
//| Проверяет размер стоп-уровня в пунктах относительно StopLevel    |
//+------------------------------------------------------------------+
bool CheckStopLevel(const string symbol_name,const int stop_in_points,const int spread_multiplier)
  {
   return(stop_in_points>=StopLevel(symbol_name,spread_multiplier));
  }
//+------------------------------------------------------------------+
//| Возвращает размер StopLevel в пунктах                            |
//+------------------------------------------------------------------+
int StopLevel(const string symbol_name,const int spread_multiplier)
  {
   int spread=(int)SymbolInfoInteger(symbol_name,SYMBOL_SPREAD);
   int stop_level=(int)SymbolInfoInteger(symbol_name,SYMBOL_TRADE_STOPS_LEVEL);
   return(stop_level==0 ? spread*spread_multiplier : stop_level);
  }
//+------------------------------------------------------------------+ 

Функции просто рассчитывают корректные величины так, чтобы они не нарушали пределы ограничений, установленные на сервере. Стоит отметить, что в функцию расчёта уровня StopLevel передаётся помимо символа ещё и множитель спреда. Это сделано потому, что если уровень StopLevel установлен на сервере равным нулю, то это означает не нулевой уровень, а плавающий, и для расчёта уровня StopLevel нужно использовать величину спреда, умноженную на некое число (обычно 2, но бывает и 3) — вот этот множитель мы и передаём в функцию, позволяя его либо жестко задать в настройках советника, либо рассчитывать.

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

Создадим в папке терминала MQL5\Experts\TestDoEasy\Part04\ новый советник под именем TestDoEasy04.mqh (при создании, в Мастере MQL отметим галочками нужные обработчики событий — OnTimer и OnChartEvent):


После создания Мастером MQL шаблона советника подключим к нему сразу же нашу библиотеку и торговый класс стандартной библиотеки, и добавим входные параметры:

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart04.mq5 |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2018, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
#include <Trade\Trade.mqh>  
//--- enums
enum ENUM_BUTTONS
  {
   BUTT_BUY,
   BUTT_BUY_LIMIT,
   BUTT_BUY_STOP,
   BUTT_BUY_STOP_LIMIT,
   BUTT_CLOSE_BUY,
   BUTT_CLOSE_BUY2,
   BUTT_CLOSE_BUY_BY_SELL,
   BUTT_SELL,
   BUTT_SELL_LIMIT,
   BUTT_SELL_STOP,
   BUTT_SELL_STOP_LIMIT,
   BUTT_CLOSE_SELL,
   BUTT_CLOSE_SELL2,
   BUTT_CLOSE_SELL_BY_BUY,
   BUTT_CLOSE_ALL,
   BUTT_PROFIT_WITHDRAWAL
  };
#define TOTAL_BUTT   (16)
//--- structures
struct SDataButt
  {
   string      name;
   string      text;
  };
//--- input variables
input ulong    InpMagic       =  123;  // Magic number
input double   InpLots        =  0.1;  // Lots
input uint     InpStopLoss    =  50;   // StopLoss in points
input uint     InpTakeProfit  =  50;   // TakeProfit in points
input uint     InpDistance    =  50;   // Pending orders distance (points)
input uint     InpDistanceSL  =  50;   // StopLimit orders distance (points)
input uint     InpSlippage    =  0;    // Slippage in points
input double   InpWithdrawal  =  10;   // Withdrawal funds (in tester)
//--- global variables
CEngine        engine;
CTrade         trade;
SDataButt      butt_data[TOTAL_BUTT];
string         prefix;
double         lot;
double         withdrawal=(InpWithdrawal<0.1 ? 0.1 : InpWithdrawal);
ulong          magic_number;
uint           stoploss;
uint           takeprofit;
uint           distance_pending;
uint           distance_stoplimit;
uint           slippage;
//+------------------------------------------------------------------+

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

Затем объявляем структуру для хранения имени графического объекта кнопки и текста, который будет написан на кнопке.
В блоке входных переменных прописываем все перечисленные выше переменные-параметры советника, а в блоке глобальных переменных советника объявляем объект библиотеки, объект торгового класса, массив структур кнопок и переменные, которым будут присвоены значения входных параметров в обработчике OnInit():

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Check account type
   if(!engine.IsHedge())
     {
      Alert(TextByLanguage("Ошибка. Счёт должен быть хеджевым","Error. Account must be hedge"));
      return INIT_FAILED;
     }
//--- set global variables
   prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_";
   for(int i=0;i<TOTAL_BUTT;i++)
     {
      butt_data[i].name=prefix+EnumToString((ENUM_BUTTONS)i);
      butt_data[i].text=EnumToButtText((ENUM_BUTTONS)i);
     }
   lot=NormalizeLot(Symbol(),fmax(InpLots,MinimumLots(Symbol())*2.0));
   magic_number=InpMagic;
   stoploss=InpStopLoss;
   takeprofit=InpTakeProfit;
   distance_pending=InpDistance;
   distance_stoplimit=InpDistanceSL;
   slippage=InpSlippage;
//--- create buttons
   if(!CreateButtons())  
      return INIT_FAILED;
//---
   trade.SetDeviationInPoints(slippage);
   trade.SetExpertMagicNumber(magic_number);
   trade.SetTypeFillingBySymbol(Symbol());
   trade.SetMarginMode();
   trade.LogLevel(LOG_LEVEL_ERRORS);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

В обработчике OnInit() первым делом проверяем тип счёта и, если он не хеджевый, то сообщаем об этом и выходим из программы с ошибкой.
Далее задаём префикс имён объектов (чтобы советник по этому префиксу мог узнать свои объекты) и в цикле по количеству кнопок заполняем массив структур с данными кнопок.
Имя объекта-кнопки задаётся как префикс+строковое представление перечисления ENUM_BUTTONS, соответствующее индексу цикла,
а текст кнопки составляется методом преобразования строкового представления перечисления, соответствующего индексу цикла, при помощи функции EnumToButtText().

Далее рассчитывается лот открываемых позиций и выставляемых ордеров. Так как у нас предусмотрено закрытие половины позиции, то необходимо учесть, что лот открываемой позиции дожен быть как минимум в два раза больше минимального лота. Поэтому берётся максимальный лот из двух:
1) введённый во входных параметрах, 2) минимальный лот, умноженный на два в строке fmax(InpLots,MinimumLots(Symbol())*2.0), значение полученного лота нормализуется и присваивается глобальной переменной lot. В итоге: если лот, введённый пользователем во входных параметрах окажется меньше двойного минимального лота, то будет использован двойной минимальный лот, в противном случае — будет использован лот, введённый пользователем.

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

В завершение проводится инициализация класса CTrade:

//---
   trade.SetDeviationInPoints(slippage);
   trade.SetExpertMagicNumber(magic_number);
   trade.SetTypeFillingBySymbol(Symbol());
   trade.SetMarginMode();
   trade.LogLevel(LOG_LEVEL_ERRORS);
//---

В обработчике OnDeinit() пропишем удаление всех кнопок по префиксу имён объектов:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- delete objects
   ObjectsDeleteAll(0,prefix);
  }
//+------------------------------------------------------------------+

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

Обработчики OnTick(), OnTimer() и OnChartEvent() советника:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(MQLInfoInteger(MQL_TESTER))
      engine.OnTimer();
   int total=ObjectsTotal(0);
   for(int i=0;i<total;i++)
     {
      string obj_name=ObjectName(0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<0)
         continue;
      PressButtonEvents(obj_name);
     }
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
   if(!MQLInfoInteger(MQL_TESTER))
      engine.OnTimer();
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   if(MQLInfoInteger(MQL_TESTER))
      return;
   if(id==CHARTEVENT_OBJECT_CLICK && StringFind(sparam,"BUTT_")>0)
     {
      PressButtonEvents(sparam);
     }  
  }
//+------------------------------------------------------------------+

В OnTick()

  • Проверяется где запущен советник, и если в тестере, то вызывается обработчик OnTimer() библиотеки.
  • Далее в цикле по всем объектам текущего чарта проверяем имя объекта, и если оно совпадает с именем какой-либо кнопки — вызывается обработчик нажатия данной кнопки.

В OnTimer()

В OnChartEvent()

Функция CreateButtons():

//+------------------------------------------------------------------+
//| Создаёт панель кнопок                                            |
//+------------------------------------------------------------------+
bool CreateButtons(void)
  {
   int h=18,w=84,offset=10;
   int cx=offset,cy=offset+(h+1)*(TOTAL_BUTT/2)+h+1;
   int x=cx,y=cy;
   int shift=0;
   for(int i=0;i<TOTAL_BUTT;i++)
     {
      x=x+(i==7 ? w+2 : 0);
      if(i==TOTAL_BUTT-2) x=cx;
      y=(cy-(i-(i>6 ? 7 : 0))*(h+1));
      if(!ButtonCreate(butt_data[i].name,x,y,(i<TOTAL_BUTT-2 ? w : w*2+2),h,butt_data[i].text,(i<4 ? clrGreen : i>6 && i<11 ? clrRed : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text);
         return false;
        }
     }
   ChartRedraw(0);
   return true;
  }
//+------------------------------------------------------------------+

В данной функции всё сводится к вычислению координат и цвета кнопки в цикле по количеству членов перечисления ENUM_BUTTONS . Координаты и цвет рассчитываются в зависимости от индекса цикла, указывающего на номер члена перечисления ENUM_BUTTONS. После расчёта координат x и y, вызывается функция создания кнопки с рассчитанными в этом цикле значениями координат и цвета.

Функция EnumToButtText():

//+------------------------------------------------------------------+
//| Преобразует перечисление в текст кнопки                          |
//+------------------------------------------------------------------+
string EnumToButtText(const ENUM_BUTTONS member)
  {
   string txt=StringSubstr(EnumToString(member),5);
   StringToLower(txt);
   StringReplace(txt,"buy","Buy");
   StringReplace(txt,"sell","Sell");
   StringReplace(txt,"_limit"," Limit");
   StringReplace(txt,"_stop"," Stop");
   StringReplace(txt,"close_","Close ");
   StringReplace(txt,"2"," 1/2");
   StringReplace(txt,"_by_"," by ");
   StringReplace(txt,"profit_","Profit ");
   return txt;
  }
//+------------------------------------------------------------------+

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

Функции создания кнопки, установки и получения её состояния:

//+------------------------------------------------------------------+
//| Создаёт кнопку                                                   |
//+------------------------------------------------------------------+
bool ButtonCreate(const string name,const int x,const int y,const int w,const int h,const string text,const color clr,const string font="Calibri",const int font_size=8)
  {
   if(ObjectFind(0,name)<0)
     {
      if(!ObjectCreate(0,name,OBJ_BUTTON,0,0,0)) 
        { 
         Print(DFUN,TextByLanguage("не удалось создать кнопку! Код ошибки=","Could not create button! Error code="),GetLastError()); 
         return false; 
        } 
      ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
      ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
      ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
      ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
      ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
      ObjectSetInteger(0,name,OBJPROP_YSIZE,h);
      ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
      ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER);
      ObjectSetInteger(0,name,OBJPROP_FONTSIZE,font_size);
      ObjectSetString(0,name,OBJPROP_FONT,font);
      ObjectSetString(0,name,OBJPROP_TEXT,text);
      ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
      ObjectSetString(0,name,OBJPROP_TOOLTIP,"\n");
      ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//| Возвращает состояние кнопки                                      |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| Устанавливает состояние кнопки                                   |
//+------------------------------------------------------------------+
void ButtonState(const string name,const bool state)
  {
   ObjectSetInteger(0,name,OBJPROP_STATE,state);
  }
//+------------------------------------------------------------------+

Здесь всё просто и наглядно, поэтому не нуждается в пояснениях.

Функция обработки нажатия кнопок:

//+------------------------------------------------------------------+
//| Обработка нажатий кнопок                                         |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
   //--- Преобразуем имя кнопки в её строковый идентификатор
   string button=StringSubstr(button_name,StringLen(prefix));
   //--- Если кнопка в нажатом состоянии
   if(ButtonState(button_name))
     {
      //--- Если нажата кнопка BUTT_BUY: Открыть позицию Buy
      if(button==EnumToString(BUTT_BUY))
        {
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY,0,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY,0,takeprofit);
         //--- Открываем позицию Buy
         trade.Buy(NormalizeLot(Symbol(),lot),Symbol(),0,sl,tp);
        }
      //--- Если нажата кнопка BUTT_BUY_LIMIT: Выставить BuyLimit
      else if(button==EnumToString(BUTT_BUY_LIMIT))
        {
         //--- Получаем корректную цену установки ордера относительно уровня StopLevel
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_pending);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,takeprofit);
         //--- Устанавливаем ордер BuyLimit
         trade.BuyLimit(lot,price_set,Symbol(),sl,tp);
        }
      //--- Если нажата кнопка BUTT_BUY_STOP: Выставить BuyStop
      else if(button==EnumToString(BUTT_BUY_STOP))
        {
         //--- Получаем корректную цену установки ордера относительно уровня StopLevel
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_STOP,distance_pending);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_STOP,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set,takeprofit);
         //--- Устанавливаем ордер BuyStop
         trade.BuyStop(lot,price_set,Symbol(),sl,tp);
        }
      //--- Если нажата кнопка BUTT_BUY_STOP_LIMIT: Выставить BuyStopLimit
      else if(button==EnumToString(BUTT_BUY_STOP_LIMIT))
        {
         //--- Получаем корректную цену установки ордера BuyStop относительно уровня StopLevel
         double price_set_stop=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_STOP,distance_pending);
         //--- Рассчитываем цену установки ордера BuyLimit относительно уровня установки BuyStop с учётом уровня StopLevel
         double price_set_limit=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_stoplimit,price_set_stop);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_STOP,price_set_limit,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set_limit,takeprofit);
         //--- Устанавливаем ордер BuyStopLimit
         trade.OrderOpen(Symbol(),ORDER_TYPE_BUY_STOP_LIMIT,lot,price_set_limit,price_set_stop,sl,tp);
        }
      //--- Если нажата кнопка BUTT_SELL: Открыть позицию Sell
      else if(button==EnumToString(BUTT_SELL))
        {
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL,0,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL,0,takeprofit);
         //--- Открываем позицию Sell
         trade.Sell(lot,Symbol(),0,sl,tp);
        }
      //--- Если нажата кнопка BUTT_SELL_LIMIT: Выставить SellLimit
      else if(button==EnumToString(BUTT_SELL_LIMIT))
        {
         //--- Получаем корректную цену установки ордера относительно уровня StopLevel
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_LIMIT,distance_pending);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_LIMIT,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_LIMIT,price_set,takeprofit);
         //--- Устанавливаем ордер SellLimit
         trade.SellLimit(lot,price_set,Symbol(),sl,tp);
        }
      //--- Если нажата кнопка BUTT_SELL_STOP: Выставить SellStop
      else if(button==EnumToString(BUTT_SELL_STOP))
        {
         //--- Получаем корректную цену установки ордера относительно уровня StopLevel
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_STOP,distance_pending);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_STOP,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_STOP,price_set,takeprofit);
         //--- Устанавливаем ордер SellStop
         trade.SellStop(lot,price_set,Symbol(),sl,tp);
        }
      //--- Если нажата кнопка BUTT_SELL_STOP_LIMIT: Выставить SellStopLimit
      else if(button==EnumToString(BUTT_SELL_STOP_LIMIT))
        {
         //--- Получаем корректную цену установки ордера SellStop относительно уровня StopLevel
         double price_set_stop=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_STOP,distance_pending);
         //--- Рассчитываем цену установки ордера SellLimit относительно уровня установки SellStop с учётом уровня StopLevel
         double price_set_limit=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_LIMIT,distance_stoplimit,price_set_stop);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_STOP,price_set_limit,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_STOP,price_set_limit,takeprofit);
         //--- Устанавливаем ордер SellStopLimit
         trade.OrderOpen(Symbol(),ORDER_TYPE_SELL_STOP_LIMIT,lot,price_set_limit,price_set_stop,sl,tp);
        }
      //--- Если нажата кнопка BUTT_CLOSE_BUY: Закрыть Buy с максимальной прибылью
      else if(button==EnumToString(BUTT_CLOSE_BUY))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Buy
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Buy с наибольшей прибылью
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- Получаем тикет позиции Buy и закрываем позицию по тикету
               trade.PositionClose(position.Ticket());
              }
           }
        }
      //--- Если нажата кнопка BUTT_CLOSE_BUY2: Закрыть половину Buy с максимальной прибылью
      else if(button==EnumToString(BUTT_CLOSE_BUY2))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Buy
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Buy с наибольшей прибылью
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- Рассчитываем закрываемый объём и закрываем половину позиции Buy по тикету
               trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.VolumeCurrent()/2.0));
              }
           }
        }
      //--- Если нажата кнопка BUTT_CLOSE_BUY_BY_SELL: Закрыть Buy с максимальной прибылью встречной Sell с максимальной прибылью
      else if(button==EnumToString(BUTT_CLOSE_BUY_BY_SELL))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list_buy=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Buy
         list_buy=CSelect::ByOrderProperty(list_buy,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Buy с наибольшей прибылью
         int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL);
         //--- Получаем список всех открытых позиций
         CArrayObj* list_sell=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Sell
         list_sell=CSelect::ByOrderProperty(list_sell,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Sell с наибольшей прибылью
         int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL);
         if(index_buy>WRONG_VALUE && index_sell>WRONG_VALUE)
           {
            //--- Выбираем позицию Buy с наибольшей прибылью
            COrder* position_buy=list_buy.At(index_buy);
            //--- Выбираем позицию Sell с наибольшей прибылью
            COrder* position_sell=list_sell.At(index_sell);
            if(position_buy!=NULL && position_sell!=NULL)
              {
               //--- Закрываем позицию Buy встречной позицией Sell
               trade.PositionCloseBy(position_buy.Ticket(),position_sell.Ticket());
              }
           }
        }
      //--- Если нажата кнопка BUTT_CLOSE_SELL: Закрыть Sell с максимальной прибылью
      else if(button==EnumToString(BUTT_CLOSE_SELL))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Sell
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Sell с наибольшей прибылью
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- Получаем тикет позиции Sell и закрываем позицию по тикету
               trade.PositionClose(position.Ticket());
              }
           }
        }
      //--- Если нажата кнопка BUTT_CLOSE_SELL2: Закрыть половину Sell с максимальной прибылью
      else if(button==EnumToString(BUTT_CLOSE_SELL2))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Sell
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Sell с наибольшей прибылью
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- Рассчитываем закрываемый объём и закрываем половину позиции Sell по тикету
               trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.VolumeCurrent()/2.0));
              }
           }
        }
      //--- Если нажата кнопка BUTT_CLOSE_SELL_BY_BUY: Закрыть Sell с максимальной прибылью встречной Buy с максимальной прибылью
      else if(button==EnumToString(BUTT_CLOSE_SELL_BY_BUY))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list_sell=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Sell
         list_sell=CSelect::ByOrderProperty(list_sell,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Sell с наибольшей прибылью
         int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL);
         //--- Получаем список всех открытых позиций
         CArrayObj* list_buy=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Buy
         list_buy=CSelect::ByOrderProperty(list_buy,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Buy с наибольшей прибылью
         int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL);
         if(index_sell>WRONG_VALUE && index_buy>WRONG_VALUE)
           {
            //--- Выбираем позицию Sell с наибольшей прибылью
            COrder* position_sell=list_sell.At(index_sell);
            //--- Выбираем позицию Buy с наибольшей прибылью
            COrder* position_buy=list_buy.At(index_buy);
            if(position_sell!=NULL && position_buy!=NULL)
              {
               //--- Закрываем позицию Sell встречной позицией Buy
               trade.PositionCloseBy(position_sell.Ticket(),position_buy.Ticket());
              }
           }
        }
      //--- Если нажата кнопка BUTT_CLOSE_ALL: Закрыть все позиции, начиная от позиции с наименьшим профитом
      else if(button==EnumToString(BUTT_CLOSE_ALL))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list=engine.GetListMarketPosition();
         if(list!=NULL)
           {
            //--- Сортируем список по прибыли с учётом комиссии и свопа
            list.Sort(SORT_BY_ORDER_PROFIT_FULL);
            int total=list.Total();
            //--- В цикле от позиции с наименьшей прибылью
            for(int i=0;i<total;i++)
              {
               COrder* position=list.At(i);
               if(position==NULL)
                  continue;
               //--- закрываем каждую позицию по её тикету
               trade.PositionClose(position.Ticket());
              }
           }
        }
      //--- Если нажата кнопка BUTT_PROFIT_WITHDRAWAL: Вывести средства со счёта
      if(button==EnumToString(BUTT_PROFIT_WITHDRAWAL))
        {
         //--- Если программа запущена в тестере
         if(MQLInfoInteger(MQL_TESTER))
           {
            //--- Эмулируем вывод средств
            TesterWithdrawal(withdrawal);
           }
        }
      //--- Подождём 1/10 секунды
      Sleep(100);
      //--- "Отожмём" кнопку и перерисуем чарт
      ButtonState(button_name,false);
      ChartRedraw();
     }
  }
//+------------------------------------------------------------------+

Функция достаточно объёмна, но там всё просто: в функцию передаётся имя объекта-кнопки, которое мы преобразуем в строковый идентификатор. Далее проверяется состояние кнопки, и если она нажата, то проверяется строковый идентификатор. Ну и исполняется соответствующее ветвление if-else, где рассчитываются все уровни с корректировкой чтобы не нарушать ограничение по уровню StolLevel и выполняется соответствующий метод торгового класса.
Все пояснения расписаны прямо в коде в комментариях к строкам.

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

Полный листинг тестового советника:

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart04.mq5 |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2018, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
#include <Trade\Trade.mqh>
//--- enums
enum ENUM_BUTTONS
  {
   BUTT_BUY,
   BUTT_BUY_LIMIT,
   BUTT_BUY_STOP,
   BUTT_BUY_STOP_LIMIT,
   BUTT_CLOSE_BUY,
   BUTT_CLOSE_BUY2,
   BUTT_CLOSE_BUY_BY_SELL,
   BUTT_SELL,
   BUTT_SELL_LIMIT,
   BUTT_SELL_STOP,
   BUTT_SELL_STOP_LIMIT,
   BUTT_CLOSE_SELL,
   BUTT_CLOSE_SELL2,
   BUTT_CLOSE_SELL_BY_BUY,
   BUTT_CLOSE_ALL,
   BUTT_PROFIT_WITHDRAWAL
  };
#define TOTAL_BUTT   (16)
//--- structures
struct SDataButt
  {
   string      name;
   string      text;
  };
//--- input variables
input ulong    InpMagic       =  123;  // Magic number
input double   InpLots        =  0.1;  // Lots
input uint     InpStopLoss    =  50;   // StopLoss in points
input uint     InpTakeProfit  =  50;   // TakeProfit in points
input uint     InpDistance    =  50;   // Pending orders distance (points)
input uint     InpDistanceSL  =  50;   // StopLimit orders distance (points)
input uint     InpSlippage    =  0;    // Slippage in points
input double   InpWithdrawal  =  10;   // Withdrawal funds (in tester)
//--- global variables
CEngine        engine;
CTrade         trade;
SDataButt      butt_data[TOTAL_BUTT];
string         prefix;
double         lot;
double         withdrawal=(InpWithdrawal<0.1 ? 0.1 : InpWithdrawal);
ulong          magic_number;
uint           stoploss;
uint           takeprofit;
uint           distance_pending;
uint           distance_stoplimit;
uint           slippage;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Check account type
   if(!engine.IsHedge())
     {
      Alert(TextByLanguage("Ошибка. Счёт должен быть хеджевым","Error. Account must be hedge"));
      return INIT_FAILED;
     }
//--- set global variables
   prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_";
   for(int i=0;i<TOTAL_BUTT;i++)
     {
      butt_data[i].name=prefix+EnumToString((ENUM_BUTTONS)i);
      butt_data[i].text=EnumToButtText((ENUM_BUTTONS)i);
     }
   lot=NormalizeLot(Symbol(),fmax(InpLots,MinimumLots(Symbol())*2.0));
   magic_number=InpMagic;
   stoploss=InpStopLoss;
   takeprofit=InpTakeProfit;
   distance_pending=InpDistance;
   distance_stoplimit=InpDistanceSL;
   slippage=InpSlippage;
//--- create buttons
   if(!CreateButtons())
      return INIT_FAILED;
//--- setting trade parameters
   trade.SetDeviationInPoints(slippage);
   trade.SetExpertMagicNumber(magic_number);
   trade.SetTypeFillingBySymbol(Symbol());
   trade.SetMarginMode();
   trade.LogLevel(LOG_LEVEL_NO);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- delete objects
   ObjectsDeleteAll(0,prefix);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(MQLInfoInteger(MQL_TESTER))
      engine.OnTimer();
   int total=ObjectsTotal(0);
   for(int i=0;i<total;i++)
     {
      string obj_name=ObjectName(0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<0)
         continue;
      PressButtonEvents(obj_name);
     }
   if(engine.TradeEventCode()!=TRADE_EVENT_FLAG_NO_EVENT)
     {
      
      Print(DFUN,EnumToString((ENUM_TRADE_EVENT_FLAGS)engine.TradeEventCode()));
     }
   engine.TradeEventCode();
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
   if(!MQLInfoInteger(MQL_TESTER))
      engine.OnTimer();
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   if(MQLInfoInteger(MQL_TESTER))
      return;
   if(id==CHARTEVENT_OBJECT_CLICK && StringFind(sparam,"BUTT_")>0)
     {
      PressButtonEvents(sparam);
     }  
  }
//+------------------------------------------------------------------+
//| Создаёт панель кнопок                                            |
//+------------------------------------------------------------------+
bool CreateButtons(void)
  {
   int h=18,w=84,offset=10;
   int cx=offset,cy=offset+(h+1)*(TOTAL_BUTT/2)+h+1;
   int x=cx,y=cy;
   int shift=0;
   for(int i=0;i<TOTAL_BUTT;i++)
     {
      x=x+(i==7 ? w+2 : 0);
      if(i==TOTAL_BUTT-2) x=cx;
      y=(cy-(i-(i>6 ? 7 : 0))*(h+1));
      if(!ButtonCreate(butt_data[i].name,x,y,(i<TOTAL_BUTT-2 ? w : w*2+2),h,butt_data[i].text,(i<4 ? clrGreen : i>6 && i<11 ? clrRed : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text);
         return false;
        }
     }
   ChartRedraw(0);
   return true;
  }
//+------------------------------------------------------------------+
//| Создаёт кнопку                                                   |
//+------------------------------------------------------------------+
bool ButtonCreate(const string name,const int x,const int y,const int w,const int h,const string text,const color clr,const string font="Calibri",const int font_size=8)
  {
   if(ObjectFind(0,name)<0)
     {
      if(!ObjectCreate(0,name,OBJ_BUTTON,0,0,0)) 
        { 
         Print(DFUN,TextByLanguage("не удалось создать кнопку! Код ошибки=","Could not create button! Error code="),GetLastError()); 
         return false; 
        } 
      ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
      ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
      ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
      ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
      ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
      ObjectSetInteger(0,name,OBJPROP_YSIZE,h);
      ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
      ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER);
      ObjectSetInteger(0,name,OBJPROP_FONTSIZE,font_size);
      ObjectSetString(0,name,OBJPROP_FONT,font);
      ObjectSetString(0,name,OBJPROP_TEXT,text);
      ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
      ObjectSetString(0,name,OBJPROP_TOOLTIP,"\n");
      ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//| Возвращает состояние кнопки                                      |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| Устанавливает состояние кнопки                                   |
//+------------------------------------------------------------------+
void ButtonState(const string name,const bool state)
  {
   ObjectSetInteger(0,name,OBJPROP_STATE,state);
  }
//+------------------------------------------------------------------+
//| Преобразует перечисление в текст кнопки                          |
//+------------------------------------------------------------------+
string EnumToButtText(const ENUM_BUTTONS member)
  {
   string txt=StringSubstr(EnumToString(member),5);
   StringToLower(txt);
   StringReplace(txt,"buy","Buy");
   StringReplace(txt,"sell","Sell");
   StringReplace(txt,"_limit"," Limit");
   StringReplace(txt,"_stop"," Stop");
   StringReplace(txt,"close_","Close ");
   StringReplace(txt,"2"," 1/2");
   StringReplace(txt,"_by_"," by ");
   StringReplace(txt,"profit_","Profit ");
   return txt;
  }
//+------------------------------------------------------------------+
//| Обработка нажатий кнопок                                         |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
   //--- Преобразуем имя кнопки в её строковый идентификатор
   string button=StringSubstr(button_name,StringLen(prefix));
   //--- Если кнопка в нажатом состоянии
   if(ButtonState(button_name))
     {
      //--- Если нажата кнопка BUTT_BUY: Открыть позицию Buy
      if(button==EnumToString(BUTT_BUY))
        {
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY,0,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY,0,takeprofit);
         //--- Открываем позицию Buy
         trade.Buy(NormalizeLot(Symbol(),lot),Symbol(),0,sl,tp);
        }
      //--- Если нажата кнопка BUTT_BUY_LIMIT: Выставить BuyLimit
      else if(button==EnumToString(BUTT_BUY_LIMIT))
        {
         //--- Получаем корректную цену установки ордера относительно уровня StopLevel
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_pending);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,takeprofit);
         //--- Устанавливаем ордер BuyLimit
         trade.BuyLimit(lot,price_set,Symbol(),sl,tp);
        }
      //--- Если нажата кнопка BUTT_BUY_STOP: Выставить BuyStop
      else if(button==EnumToString(BUTT_BUY_STOP))
        {
         //--- Получаем корректную цену установки ордера относительно уровня StopLevel
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_STOP,distance_pending);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_STOP,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set,takeprofit);
         //--- Устанавливаем ордер BuyStop
         trade.BuyStop(lot,price_set,Symbol(),sl,tp);
        }
      //--- Если нажата кнопка BUTT_BUY_STOP_LIMIT: Выставить BuyStopLimit
      else if(button==EnumToString(BUTT_BUY_STOP_LIMIT))
        {
         //--- Получаем корректную цену установки ордера BuyStop относительно уровня StopLevel
         double price_set_stop=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_STOP,distance_pending);
         //--- Рассчитываем цену установки ордера BuyLimit относительно уровня установки BuyStop с учётом уровня StopLevel
         double price_set_limit=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_stoplimit,price_set_stop);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_STOP,price_set_limit,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set_limit,takeprofit);
         //--- Устанавливаем ордер BuyStopLimit
         trade.OrderOpen(Symbol(),ORDER_TYPE_BUY_STOP_LIMIT,lot,price_set_limit,price_set_stop,sl,tp);
        }
      //--- Если нажата кнопка BUTT_SELL: Открыть позицию Sell
      else if(button==EnumToString(BUTT_SELL))
        {
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL,0,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL,0,takeprofit);
         //--- Открываем позицию Sell
         trade.Sell(lot,Symbol(),0,sl,tp);
        }
      //--- Если нажата кнопка BUTT_SELL_LIMIT: Выставить SellLimit
      else if(button==EnumToString(BUTT_SELL_LIMIT))
        {
         //--- Получаем корректную цену установки ордера относительно уровня StopLevel
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_LIMIT,distance_pending);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_LIMIT,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_LIMIT,price_set,takeprofit);
         //--- Устанавливаем ордер SellLimit
         trade.SellLimit(lot,price_set,Symbol(),sl,tp);
        }
      //--- Если нажата кнопка BUTT_SELL_STOP: Выставить SellStop
      else if(button==EnumToString(BUTT_SELL_STOP))
        {
         //--- Получаем корректную цену установки ордера относительно уровня StopLevel
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_STOP,distance_pending);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_STOP,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_STOP,price_set,takeprofit);
         //--- Устанавливаем ордер SellStop
         trade.SellStop(lot,price_set,Symbol(),sl,tp);
        }
      //--- Если нажата кнопка BUTT_SELL_STOP_LIMIT: Выставить SellStopLimit
      else if(button==EnumToString(BUTT_SELL_STOP_LIMIT))
        {
         //--- Получаем корректную цену установки ордера SellStop относительно уровня StopLevel
         double price_set_stop=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_STOP,distance_pending);
         //--- Рассчитываем цену установки ордера SellLimit относительно уровня установки SellStop с учётом уровня StopLevel
         double price_set_limit=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_LIMIT,distance_stoplimit,price_set_stop);
         //--- Получаем корректные цены StopLoss и TakeProfit относительно уровня установки ордера с учётом StopLevel
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_STOP,price_set_limit,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_STOP,price_set_limit,takeprofit);
         //--- Устанавливаем ордер SellStopLimit
         trade.OrderOpen(Symbol(),ORDER_TYPE_SELL_STOP_LIMIT,lot,price_set_limit,price_set_stop,sl,tp);
        }
      //--- Если нажата кнопка BUTT_CLOSE_BUY: Закрыть Buy с максимальной прибылью
      else if(button==EnumToString(BUTT_CLOSE_BUY))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Buy
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Buy с наибольшей прибылью
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- Получаем тикет позиции Buy и закрываем позицию по тикету
               trade.PositionClose(position.Ticket());
              }
           }
        }
      //--- Если нажата кнопка BUTT_CLOSE_BUY2: Закрыть половину Buy с максимальной прибылью
      else if(button==EnumToString(BUTT_CLOSE_BUY2))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Buy
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Buy с наибольшей прибылью
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- Рассчитываем закрываемый объём и закрываем половину позиции Buy по тикету
               trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.VolumeCurrent()/2.0));
              }
           }
        }
      //--- Если нажата кнопка BUTT_CLOSE_BUY_BY_SELL: Закрыть Buy с максимальной прибылью встречной Sell с максимальной прибылью
      else if(button==EnumToString(BUTT_CLOSE_BUY_BY_SELL))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list_buy=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Buy
         list_buy=CSelect::ByOrderProperty(list_buy,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Buy с наибольшей прибылью
         int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL);
         //--- Получаем список всех открытых позиций
         CArrayObj* list_sell=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Sell
         list_sell=CSelect::ByOrderProperty(list_sell,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Sell с наибольшей прибылью
         int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL);
         if(index_buy>WRONG_VALUE && index_sell>WRONG_VALUE)
           {
            //--- Выбираем позицию Buy с наибольшей прибылью
            COrder* position_buy=list_buy.At(index_buy);
            //--- Выбираем позицию Sell с наибольшей прибылью
            COrder* position_sell=list_sell.At(index_sell);
            if(position_buy!=NULL && position_sell!=NULL)
              {
               //--- Закрываем позицию Buy встречной позицией Sell
               trade.PositionCloseBy(position_buy.Ticket(),position_sell.Ticket());
              }
           }
        }
      //--- Если нажата кнопка BUTT_CLOSE_SELL: Закрыть Sell с максимальной прибылью
      else if(button==EnumToString(BUTT_CLOSE_SELL))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Sell
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Sell с наибольшей прибылью
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- Получаем тикет позиции Sell и закрываем позицию по тикету
               trade.PositionClose(position.Ticket());
              }
           }
        }
      //--- Если нажата кнопка BUTT_CLOSE_SELL2: Закрыть половину Sell с максимальной прибылью
      else if(button==EnumToString(BUTT_CLOSE_SELL2))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Sell
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Sell с наибольшей прибылью
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- Рассчитываем закрываемый объём и закрываем половину позиции Sell по тикету
               trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.VolumeCurrent()/2.0));
              }
           }
        }
      //--- Если нажата кнопка BUTT_CLOSE_SELL_BY_BUY: Закрыть Sell с максимальной прибылью встречной Buy с максимальной прибылью
      else if(button==EnumToString(BUTT_CLOSE_SELL_BY_BUY))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list_sell=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Sell
         list_sell=CSelect::ByOrderProperty(list_sell,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Sell с наибольшей прибылью
         int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL);
         //--- Получаем список всех открытых позиций
         CArrayObj* list_buy=engine.GetListMarketPosition();
         //--- Выбираем из списка только позиции Buy
         list_buy=CSelect::ByOrderProperty(list_buy,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- Сортируем список по прибыли с учётом комиссии и свопа
         list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- Получаем индекс позиции Buy с наибольшей прибылью
         int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL);
         if(index_sell>WRONG_VALUE && index_buy>WRONG_VALUE)
           {
            //--- Выбираем позицию Sell с наибольшей прибылью
            COrder* position_sell=list_sell.At(index_sell);
            //--- Выбираем позицию Buy с наибольшей прибылью
            COrder* position_buy=list_buy.At(index_buy);
            if(position_sell!=NULL && position_buy!=NULL)
              {
               //--- Закрываем позицию Sell встречной позицией Buy
               trade.PositionCloseBy(position_sell.Ticket(),position_buy.Ticket());
              }
           }
        }
      //--- Если нажата кнопка BUTT_CLOSE_ALL: Закрыть все позиции, начиная от позиции с наименьшим профитом
      else if(button==EnumToString(BUTT_CLOSE_ALL))
        {
         //--- Получаем список всех открытых позиций
         CArrayObj* list=engine.GetListMarketPosition();
         if(list!=NULL)
           {
            //--- Сортируем список по прибыли с учётом комиссии и свопа
            list.Sort(SORT_BY_ORDER_PROFIT_FULL);
            int total=list.Total();
            //--- В цикле от позиции с наименьшей прибылью
            for(int i=0;i<total;i++)
              {
               COrder* position=list.At(i);
               if(position==NULL)
                  continue;
               //--- закрываем каждую позицию по её тикету
               trade.PositionClose(position.Ticket());
              }
           }
        }
      //--- Если нажата кнопка BUTT_PROFIT_WITHDRAWAL: Вывести средства со счёта
      if(button==EnumToString(BUTT_PROFIT_WITHDRAWAL))
        {
         //--- Если программа запущена в тестере
         if(MQLInfoInteger(MQL_TESTER))
           {
            //--- Эмулируем вывод средств
            TesterWithdrawal(withdrawal);
           }
        }
      //--- Подождём 1/10 секунды
      Sleep(100);
      //--- "Отожмём" кнопку и перерисуем чарт
      ButtonState(button_name,false);
      ChartRedraw();
     }
  }
//+------------------------------------------------------------------+

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

Запустим советник в тестере и пощёлкаем по кнопкам:

Всё отрабатывает верно, и в журнал выводятся сообщения о происходящих событиях.

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

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

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

//+------------------------------------------------------------------+
//| Класс-основа библиотеки                                          |
//+------------------------------------------------------------------+
class CEngine : public CObject
  {
private:
   CHistoryCollection   m_history;                       // Коллекция исторических ордеров и сделок
   CMarketCollection    m_market;                        // Коллекция рыночных ордеров и сделок
   CArrayObj            m_list_counters;                 // Список счётчиков таймера
   bool                 m_first_start;                   // Флаг первого запуска
   bool                 m_is_hedge;                      // Флаг хедж-счёта
   bool                 m_is_market_trade_event;         // Флаг торгового события на счёте
   bool                 m_is_history_trade_event;        // Флаг торгового события в истории счёта
   int                  m_trade_event_code;              // Код состояния торгового события на аккаунте
   ENUM_TRADE_EVENT     m_acc_trade_event;               // Торговое событие на счёте
//--- Расшифровывает код события и устанавливает торговое событие на счёте
   void                 SetTradeEvent(void);
//--- Возвращает индекс счётчика по id
   int                  CounterIndex(const int id) const;
//--- Возвращает (1) флаг первого запуска, (2) факт наличия флага в торговом событии
   bool                 IsFirstStart(void);
   bool                 IsTradeEventFlag(const int event_code)    const { return (this.m_trade_event_code&event_code)==event_code;  }
//--- Работа с коллекциями (1) хедж, (2) неттинг
   void                 WorkWithHedgeCollections(void);
   void                 WorkWithNettoCollections(void);
//--- Возвращает последний (1) рыночный отложенный ордер, (2) маркет-отложенный ордер, (3) последнюю позицию, (4) позицию по тикету
   COrder*              GetLastMarketPending(void);
   COrder*              GetLastMarketOrder(void);
   COrder*              GetLastPosition(void);
   COrder*              GetPosition(const ulong ticket);
//--- Возвращает последний (1) удалённый отложенный ордер, (2) исторический маркет-ордер, (3) исторический маркет-ордер по его тикету
   COrder*              GetLastHistoryPending(void);
   COrder*              GetLastHistoryOrder(void);
   COrder*              GetHistoryOrder(const ulong ticket);
//--- Возвращает (1) первый и (2) последний исторический маркет-ордер из списка всех ордеров позиции, (3) последнюю сделку
   COrder*              GetFirstOrderPosition(const ulong position_id);
   COrder*              GetLastOrderPosition(const ulong position_id);
   COrder*              GetLastDeal(void);
public:
   //--- Возвращает список рыночных (1) позиций, (2) отложенных ордеров и (3) маркет-ордеров
   CArrayObj*           GetListMarketPosition(void);
   CArrayObj*           GetListMarketPendings(void);
   CArrayObj*           GetListMarketOrders(void);
   //--- Возвращает список исторических (1) ордеров, (2) удалённых отложенных ордеров, (3) сделок, (4) всех маркет-ордеров позиции по её идентификатору
   CArrayObj*           GetListHistoryOrders(void);
   CArrayObj*           GetListHistoryPendings(void);
   CArrayObj*           GetListHistoryDeals(void);
   CArrayObj*           GetListAllOrdersByPosID(const ulong position_id);
//--- Сбрасывает последнее торговое событие
   void                 ResetLastTradeEvent(void)                       { this.m_acc_trade_event=TRADE_EVENT_NO_EVENT;  }
//--- Возвращает (1) последнее торговое событие, (2) код торгового события, (3) флаг счёта-хедж
   ENUM_TRADE_EVENT     LastTradeEvent(void)                      const { return this.m_acc_trade_event;                }
   int                  TradeEventCode(void)                      const { return this.m_trade_event_code;               }
   bool                 IsHedge(void)                             const { return this.m_is_hedge;                       }
//--- Создаёт счётчик таймера
   void                 CreateCounter(const int id,const ulong frequency,const ulong pause);
//--- Таймер
   void                 OnTimer(void);
//--- Конструктор/Деструктор
                        CEngine();
                       ~CEngine();
  };
//+------------------------------------------------------------------+

За пределами тела класса напишем метод расшифровки торгового события (все пояснения впишем прямо в код):

//+------------------------------------------------------------------+
//| Расшифровывает код события и устанавливает торговое событие      |
//+------------------------------------------------------------------+
void CEngine::SetTradeEvent(void)
  {
//--- Нет торгового события - уходим
   if(this.m_trade_event_code==TRADE_EVENT_FLAG_NO_EVENT)
      return;
//--- Отложенный ордер установлен (проверяем на равенство коду события, так как здесь может быть только один флаг)
   if(this.m_trade_event_code==TRADE_EVENT_FLAG_ORDER_PLASED)
     {
      this.m_acc_trade_event=TRADE_EVENT_PENDING_ORDER_PLASED;
      Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
      return;
     }
//--- Отложенный ордер удалён (проверяем на равенство коду события, так как здесь может быть только один флаг)
   if(this.m_trade_event_code==TRADE_EVENT_FLAG_ORDER_REMOVED)
     {
      this.m_acc_trade_event=TRADE_EVENT_PENDING_ORDER_REMOVED;
      Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
      return;
     }
//--- Позиция открыта (Проверяем наличие множества флагов в коде события)
   if(this.IsTradeEventFlag(TRADE_EVENT_FLAG_POSITION_OPENED))
     {
      //--- Если это отложенный ордер активирован ценой
      if(this.IsTradeEventFlag(TRADE_EVENT_FLAG_ORDER_ACTIVATED))
        {
         this.m_acc_trade_event=(!this.IsTradeEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_PENDING_ORDER_ACTIVATED : TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL);
         Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
         return;
        }
      this.m_acc_trade_event=(!this.IsTradeEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_OPENED : TRADE_EVENT_POSITION_OPENED_PARTIAL);
      Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
      return;
     }
//--- Позиция закрыта (Проверяем наличие множества флагов в коде события)
   if(this.IsTradeEventFlag(TRADE_EVENT_FLAG_POSITION_CLOSED))
     {
      //--- если позиция закрыта по StopLoss
      if(this.IsTradeEventFlag(TRADE_EVENT_FLAG_SL))
        {
         //--- проверяем флаг частичного закрытия и устанавливаем торговое событие "Позиция закрыта по StopLoss" или "Позиция закрыта по StopLoss частично"
         this.m_acc_trade_event=(!this.IsTradeEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_SL : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL);
         Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
         return;
        }
      //--- если позиция закрыта по TakeProfit
      else if(this.IsTradeEventFlag(TRADE_EVENT_FLAG_TP))
        {
         //--- проверяем флаг частичного закрытия и устанавливаем торговое событие "Позиция закрыта по TakeProfit" или "Позиция закрыта по TakeProfit частично"
         this.m_acc_trade_event=(!this.IsTradeEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_TP : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP);
         Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
         return;
        }
      //--- если позиция закрыта встречной
      else if(this.IsTradeEventFlag(TRADE_EVENT_FLAG_BY_POS))
        {
         //--- проверяем флаг частичного закрытия и устанавливаем торговое событие "Позиция закрыта встречной" или "Позиция закрыта встречной частично"
         this.m_acc_trade_event=(!this.IsTradeEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_POS : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS);
         Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
         return;
        }
      //--- Если позиция закрыта
      else
        {
         //--- проверяем флаг частичного закрытия и устанавливаем торговое событие "Позиция закрыта" или "Позиция закрыта частично"
         this.m_acc_trade_event=(!this.IsTradeEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED : TRADE_EVENT_POSITION_CLOSED_PARTIAL);
         Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
         return;
        }
     }
//--- Балансная операция на счёте (уточняем событие по типу сделки)
   if(this.m_trade_event_code==TRADE_EVENT_FLAG_ACCOUNT_BALANCE)
     {
      //--- Инициализируем торговое событие
      this.m_acc_trade_event=TRADE_EVENT_NO_EVENT;
      //--- Возьмём последнюю сделку
      COrder* deal=this.GetLastDeal();
      if(deal!=NULL)
        {
         ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)deal.GetProperty(ORDER_PROP_TYPE);
         //--- если сделка - балансная операция
         if(deal_type==DEAL_TYPE_BALANCE)
           {
           //--- проверим профит сделки и установим событие (пополнение или снятие средств)
            this.m_acc_trade_event=(deal.Profit()>0 ? TRADE_EVENT_ACCOUNT_BALANCE_REFILL : TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL);
           }
         //--- Остальные типы балансной операции совпадают с перечислением ENUM_DEAL_TYPE начиная от DEAL_TYPE_CREDIT
         else if(deal_type>DEAL_TYPE_BALANCE)
           {
           //--- установим это событие
            this.m_acc_trade_event=(ENUM_TRADE_EVENT)deal_type;
           }
        }
      Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
      return;
     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Проверка торговых событий (хедж)                                 |
//+------------------------------------------------------------------+
void CEngine::WorkWithHedgeCollections(void)
  {
//--- Инициализация кода и флагов торговых событий
   this.m_trade_event_code=TRADE_EVENT_FLAG_NO_EVENT;
   this.m_is_market_trade_event=false;
   this.m_is_history_trade_event=false;
//--- Обновление списков 
   this.m_market.Refresh();
   this.m_history.Refresh();
//--- Действия при первом запуске
   if(this.IsFirstStart())
     {
      this.m_acc_trade_event=TRADE_EVENT_NO_EVENT;
      return;
     }
//--- Проверка изменения рыночного состояния и в истории счёта
   this.m_is_market_trade_event=this.m_market.IsTradeEvent();
   this.m_is_history_trade_event=this.m_history.IsTradeEvent();
//---
#ifdef __MQL4__

#else // MQL5
//--- Если событие только в рыночных ордерах и позициях
   if(this.m_is_market_trade_event && !this.m_is_history_trade_event)
     {
      //--- Если количество отложенных ордеров увеличилось
      if(this.m_market.NewPendingOrders()>0)
        {
         //--- Добавим флаг установки отложенного ордера
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_PLASED;
        }
      //--- Если количество маркет-ордеров увеличилось
      if(this.m_market.NewMarketOrders()>0)
        {
         //--- флаг этого события не добавляем
         //--- ...
        }
     }
   
//--- Если событие только в исторических ордерах и сделках
   else if(this.m_is_history_trade_event && !this.m_is_market_trade_event)
     {
      //--- Если появилась новая сделка
      if(this.m_history.NewDeals()>0)
        {
         //--- Добавим флаг появления балансного события на счёте
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ACCOUNT_BALANCE;
        }
     }
   
//--- Если события в рыночных и исторических ордерах и позициях
   else if(this.m_is_market_trade_event && this.m_is_history_trade_event)
     {
      //--- Если количество отложенных ордеров уменьшилось и не появилось новых сделок
      if(this.m_market.NewPendingOrders()<0 && this.m_history.NewDeals()==0)
        {
         //--- Добавим флаг удаления отложенного ордера
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_REMOVED;
        }
      
      //--- Если есть новая сделка и есть новый исторический ордер
      if(this.m_history.NewDeals()>0 && this.m_history.NewOrders()>0)
        {
         //--- Возьмём последнюю сделку
         COrder* deal=this.GetLastDeal();
         if(deal!=NULL)
           {
            //--- Если сделка на вход в рынок
            if(deal.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_IN)
              {
               //--- Добавим флаг открытия позиции
               this.m_trade_event_code+=TRADE_EVENT_FLAG_POSITION_OPENED;
               //--- Если количество отложенных ордеров уменьшилось
               if(this.m_market.NewPendingOrders()<0)
                 {
                  //--- Добавим флаг срабатывания отложенного ордера
                  this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_ACTIVATED;
                 }
               //--- Возьмём из сделки тикет её ордера
               ulong order_ticket=deal.GetProperty(ORDER_PROP_DEAL_ORDER);
               COrder* order=this.GetHistoryOrder(order_ticket);
               if(order!=NULL)
                 {
                  //--- Если текущий объём ордера больше нуля
                  if(order.VolumeCurrent()>0)
                    {
                     //--- Добавим флаг частичного исполнения
                     this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
                    }
                 }
              }
            
            //--- Если сделка на выход из рынка
            if(deal.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_OUT)
              {
               //--- Добавим флаг закрытия позиции
               this.m_trade_event_code+=TRADE_EVENT_FLAG_POSITION_CLOSED;
               //--- Возьмём из сделки тикет её ордера
               ulong order_ticket=deal.GetProperty(ORDER_PROP_DEAL_ORDER);
               COrder* order=this.GetHistoryOrder(order_ticket);
               if(order!=NULL)
                 {
                  //--- Если позиция сделки ещё присутствует в рынке
                  COrder* pos=this.GetPosition(deal.PositionID());
                  if(pos!=NULL)
                    {
                     //--- Добавим флаг частичного исполнения
                     this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
                    }
                  //--- Иначе, если позиция закрыта полностью
                  else
                    {
                     //--- Если у ордера есть флаг закрытия по StopLoss
                     if(order.IsCloseByStopLoss())
                       {
                        //--- Добавим флаг закрытия по StopLoss
                        this.m_trade_event_code+=TRADE_EVENT_FLAG_SL;
                       }
                     //--- Если у ордера есть флаг закрытия по TakeProfit
                     if(order.IsCloseByTakeProfit())
                       {
                        //--- Добавим флаг закрытия по TakeProfit
                        this.m_trade_event_code+=TRADE_EVENT_FLAG_TP;
                       }
                    }
                 }
              }
            
            //--- Если закрытие встречной
            if(deal.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_OUT_BY)
              {
               //--- Добавим флаг закрытия позиции
               this.m_trade_event_code+=TRADE_EVENT_FLAG_POSITION_CLOSED;
               //--- Добавим флаг закрытия встречной
               this.m_trade_event_code+=TRADE_EVENT_FLAG_BY_POS;
               //--- Возьмём ордер сделки
               ulong ticket_from=deal.GetProperty(ORDER_PROP_DEAL_ORDER);
               COrder* order=this.GetHistoryOrder(ticket_from);
               if(order!=NULL)
                 {
                  //--- Если позиция ордера ещё присутствует в рынке
                  COrder* pos=this.GetPosition(order.PositionID());
                  if(pos!=NULL)
                    {
                     //--- Добавим флаг частичного исполнения
                     this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
                    }
                 }
              }
           //--- конец блока обработки последней сделки
           }
        }
     }
#endif 
   this.SetTradeEvent();
  }
//+------------------------------------------------------------------+

Таким образом, после создания кода события в методе WorkWithHedgeCollections() мы тут же вызываем метод расшифровки события. Казалось бы, почему б не расшифровывать сразу? Дело в том, что метод расшифровки у нас сейчас лишь временный — для проверки корректности её выполнения, и в следующих статьях нами будет создан полноценный класс торговых событий. Поэтому пока сделали именно так.
Уже сейчас метод SetTradeEvent() определяет торговое событие, записывает его значение в переменную-член класса m_acc_trade_event, а методы LastTradeEvent() и ResetLastTradeEvent() позволяют читать в вызывающей программе значение этой переменной как последнее торговое событие на счёте и сбрасывать его (по аналогии с GetLastError()).

В тестовом советнике TestDoEasyPart04.mqh в его обработчике OnTick() допишем строки для чтения последнего торгового события и вывода его в комментарий на графике:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   static ENUM_TRADE_EVENT last_event=WRONG_VALUE;
   if(MQLInfoInteger(MQL_TESTER))
      engine.OnTimer();
   int total=ObjectsTotal(0);
   for(int i=0;i<total;i++)
     {
      string obj_name=ObjectName(0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<0)
         continue;
      PressButtonEvents(obj_name);
     }
   if(engine.LastTradeEvent()!=last_event)                                  
     {                                                                      
      Comment("\nLast trade event: ",EnumToString(engine.LastTradeEvent()));
      last_event=engine.LastTradeEvent();                                   
     }                                                                      
  }
//+------------------------------------------------------------------+

Теперь если запустить этот советник в тестере и покликать по кнопкам, то в журнал будут выводиться происходящие торговые события из метода CEngine::SetTradeEvent(), а в комментарии графика будет отображено описание последнего события, произошедшего на счёте, полученное советником из библиотеки методом engine.LastTradeEvent():


Что дальше

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

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

К содержанию

Статьи этой серии:

Часть 1
Часть 2
Часть 3