Функции для чтения свойств сделок из истории

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

Для каждого типа свойств существует две формы: с непосредственным возвратом значения и путем записи в переменную по ссылке. Вторая форма возвращает true для индикации успеха. Первая форма просто вернет 0 в случае ошибки. Код ошибки — в переменной _LastError.

Целочисленные и совместимые типы свойств (datetime, перечисления) можно получить функцией HistoryDealGetInteger.

long HistoryDealGetInteger(ulong ticket, ENUM_DEAL_PROPERTY_INTEGER property)

bool HistoryDealGetInteger(ulong ticket, ENUM_DEAL_PROPERTY_INTEGER property,
  long &value)

Вещественные свойства читаются функцией HistoryDealGetDouble.

double HistoryDealGetDouble(ulong ticket, ENUM_DEAL_PROPERTY_DOUBLE property)

bool HistoryDealGetDouble(ulong ticket, ENUM_DEAL_PROPERTY_DOUBLE property,
  double &value)

Для строковых свойств предназначены функции HistoryDealGetString.

string HistoryDealGetString(ulong ticket, ENUM_DEAL_PROPERTY_STRING property)

bool HistoryDealGetString(ulong ticket, ENUM_DEAL_PROPERTY_STRING property,
  string &value)

Унифицированное чтение свойств сделки обеспечит класс DealMonitor (DealMonitor.mqh), организованный точно также, как OrderMonitor и PositionMonitor. В качестве базового класса выступает DealMonitorInterface, унаследованный от шаблона MonitorInterface (мы его описывали в разделе Функции для чтения свойств действующих ордеров). Именно на этом уровне задаются конкретные типы ENUM_DEAL_PROPERTY-перечислений в качестве параметров шаблона и специфическая реализация метода stringify.

#include <MQL5Book/TradeBaseMonitor.mqh>
   
class DealMonitorInterface:
   public MonitorInterface<ENUM_DEAL_PROPERTY_INTEGER,
   ENUM_DEAL_PROPERTY_DOUBLE,ENUM_DEAL_PROPERTY_STRING>
{
public:
   // описания свойств с учетом подтипов целого
   virtual string stringify(const long v,
      const ENUM_DEAL_PROPERTY_INTEGER propertyconst override
   {
      switch(property)
      {
         case DEAL_TYPE:
            return enumstr<ENUM_DEAL_TYPE>(v);
         case DEAL_ENTRY:
            return enumstr<ENUM_DEAL_ENTRY>(v);
         case DEAL_REASON:
            return enumstr<ENUM_DEAL_REASON>(v);
         
         case DEAL_TIME:
            return TimeToString(vTIME_DATE TIME_SECONDS);
         
         case DEAL_TIME_MSC:
            return STR_TIME_MSC(v);
      }
      
      return (string)v;
   }
};

Нижеприведенный рабочий класс DealMonitor должен напомнить недавно модифицированный для работы с историей OrderMonitor. Помимо применения HistoryDeal-функций вместо HistoryOrder-функций следует отметить, что для сделок нет необходимости проверять тикет в онлайн-окружении, потому что сделки существуют только в истории.

class DealMonitorpublic DealMonitorInterface
{
   bool historyDealSelectWeak(const ulong tconst
   {
      return ((HistoryDealGetInteger(tDEAL_TICKET) == t) ||
         (HistorySelect(0LONG_MAX) && (HistoryDealGetInteger(tDEAL_TICKET) == t)));
   }
public:
   const ulong ticket;
   DealMonitor(const long t): ticket(t)
   {
      if(!historyDealSelectWeak(ticket))
      {
         PrintFormat("Error: HistoryDealSelect(%lld) failed"ticket);
      }
      else
      {
         ready = true;
      }
   }
   
   virtual long get(const ENUM_DEAL_PROPERTY_INTEGER propertyconst override
   {
      return HistoryDealGetInteger(ticketproperty);
   }
   
   virtual double get(const ENUM_DEAL_PROPERTY_DOUBLE propertyconst override
   {
      return HistoryDealGetDouble(ticketproperty);
   }
   
   virtual string get(const ENUM_DEAL_PROPERTY_STRING propertyconst override
   {
      return HistoryDealGetString(ticketproperty);
   }
   ...
};

На основе DealMonitor и TradeFilter легко создать фильтр сделок (DealFilter.mqh). Напомним, что TradeFilter, как базовый класс для многих сущностей, был описан в разделе Отбор ордеров по свойствам.

#include <MQL5Book/DealMonitor.mqh>
#include <MQL5Book/TradeFilter.mqh>
   
class DealFilterpublic TradeFilter<DealMonitor,
   ENUM_DEAL_PROPERTY_INTEGER,
   ENUM_DEAL_PROPERTY_DOUBLE,
   ENUM_DEAL_PROPERTY_STRING>
{
protected:
   virtual int total() const override
   {
      return HistoryDealsTotal();
   }
   virtual ulong get(const int iconst override
   {
      return HistoryDealGetTicket(i);
   }
};

В качестве обобщенного примера работы с историй рассмотрим скрипт восстановления истории позиций TradeHistoryPrint.mq5.

TradeHistoryPrint

Скрипт будет строить историю для текущего символа графика.

Нам для начала потребуются фильтры сделок и ордеров.

#include <MQL5Book/OrderFilter.mqh>
#include <MQL5Book/DealFilter.mqh>

Из сделок мы извлечем идентификаторы позиций и на их основе запросим подробности об ордерах.

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

enum SELECTOR_TYPE
{
   TOTAL,    // Whole history
   POSITION// Position ID
};
   
input SELECTOR_TYPE Type = TOTAL;
input ulong PositionID = 0// Position ID

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

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

  • первое поле должно иметь название _1 — оно опционально используется в алгоритме для сортировки;
  • в структуре должна быть описана функция size, возвращающая количество полей в ней;
  • в структуре должен быть шаблонный метод assign для заполнения полей из свойств переданного объекта-монитора, производного от MonitorInterface.

В стандартных кортежах метод assign описан так:

   template<typename M
   void assign(const int &properties[], M &m);

Первым параметром он принимает массив с идентификаторами свойств, соответствующих полям, которые нас интересуют. Фактически этот тот массив, который передается вызывающим кодом в метод select фильтра (TradeFilter::select) и затем по ссылке попадает в assign. Но поскольку мы сейчас создадим не стандартные кортежи, а свои собственные структуры, "знающие" о прикладной сущности своих полей, мы можем оставить массив с идентификаторами свойств внутри самой структуры и не "гонять" его внутрь фильтра и обратно в метод assign той же структуры.

В частности, для запроса сделок опишем структуру DealTuple с 8-ю полями. Их идентификаторы укажем в статическом массиве fields.

struct DealTuple
{
   datetime _1;   // время сделки
   ulong deal;    // тикет сделки
   ulong order;   // тикет ордера
   string type;   // ENUM_DEAL_TYPE как строка
   string in_out// ENUM_DEAL_ENTRY как строка
   double volume;
   double price;
   double profit;
   
   static int size() { return 8; }; // количество свойств
   static const int fields[]; // идентификаторы запрашиваемых свойств сделок
   ...
};
   
static const int DealTuple::fields[] =
{
   DEAL_TIMEDEAL_TICKETDEAL_ORDERDEAL_TYPE,
   DEAL_ENTRYDEAL_VOLUMEDEAL_PRICEDEAL_PROFIT
};

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

Для заполнения полей значениями свойств потребуется слегка модифицированная (упрощенная) версия метода assign — она берет идентификаторы из массива fields, а не из входного параметра.

struct DealTuple
{
   ...
   template<typename M// M производный от MonitorInterface<>
   void assign(M &m)
   {
      static const int DEAL_TYPE_ = StringLen("DEAL_TYPE_");
      static const int DEAL_ENTRY_ = StringLen("DEAL_ENTRY_");
      static const ulong L = 0// декларация типа по умолчанию (пустышка)
      
      _1 = (datetime)m.get(fields[0], L);
      deal = m.get(fields[1], deal);
      order = m.get(fields[2], order);
      const ENUM_DEAL_TYPE t = (ENUM_DEAL_TYPE)m.get(fields[3], L);
      type = StringSubstr(EnumToString(t), DEAL_TYPE_);
      const ENUM_DEAL_ENTRY e = (ENUM_DEAL_ENTRY)m.get(fields[4], L);
      in_out = StringSubstr(EnumToString(e), DEAL_ENTRY_);
      volume = m.get(fields[5], volume);
      price = m.get(fields[6], price);
      profit = m.get(fields[7], profit);
   }
};

Заодно мы преобразуем числовые элементы перечислений ENUM_DEAL_TYPE и ENUM_DEAL_ENTRY в понятные для пользователя строки. Разумеется, это нужно только для вывода в журнал. Для программного анализа следовало бы оставить типы как есть.

Поскольку мы изобрели новый вариант метода assign в своих кортежах, требуется добавить для него новый вариант метода select в классе TradeFilter. Новшество наверняка будет полезно и для других программ, а потому внесем его непосредственно в TradeFilter, а не в некий новый производный класс.

template<typename T,typename I,typename D,typename S>
class TradeFilter
{
   ...
   template<typename U// U должен иметь первое поле _1 и метод assign(T)
   bool select(U &data[], const bool sort = falseconst
   {
      const int n = total();
      // цикл по элементам
      for(int i = 0i < n; ++i)
      {
         const ulong t = get(i);
         // читаем свойства через объект-монитор
         T m(t);
         // проверяем все условия фильтрации
         if(match(mlongs)
         && match(mdoubles)
         && match(mstrings))
         {
            // для подходящего объекта складываем его свойства в массив
            const int k = EXPAND(data);
            data[k].assign(m);
         }
      }
      
      if(sort)
      {
         static const U u;
         sortTuple(datau._1);
      }
      
      return true;
   }

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

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

#include <MQL5Book/Tuples.mqh>
...
DealFilter filter;
int properties[] =
{
   DEAL_TIMEDEAL_TICKETDEAL_ORDERDEAL_TYPE,
   DEAL_ENTRYDEAL_VOLUMEDEAL_PRICEDEAL_PROFIT
};
Tuple8<ulong,ulong,ulong,ulong,ulong,double,double,doubletuples[];
filter.let(DEAL_SYMBOL_Symbol).select(propertiestuples);

То с кастомизированной структурой все намного проще:

DealFilter filter;
DealTuple tuples[];
filter.let(DEAL_SYMBOL_Symbol).select(tuples);

Аналогично структуре DealTuple опишем структуру для ордеров OrderTuple с 10-ю полями.

struct OrderTuple
{
   ulong _1;       // тикет (также используется как прототип 'ulong')
   datetime setup;
   datetime done;
   string type;
   double volume;
   double open;
   double current;
   double sl;
   double tp;
   string comment;
   
   static int size() { return 10; }; // количество свойств
   static const int fields[]; // идентификаторы запрашиваемых свойств ордеров
   
   template<typename M// M производный от MonitorInterface<>
   void assign(M &m)
   {
      static const int ORDER_TYPE_ = StringLen("ORDER_TYPE_");
      
      _1 = m.get(fields[0], _1);
      setup = (datetime)m.get(fields[1], _1);
      done = (datetime)m.get(fields[2], _1);
      const ENUM_ORDER_TYPE t = (ENUM_ORDER_TYPE)m.get(fields[3], _1);
      type = StringSubstr(EnumToString(t), ORDER_TYPE_);
      volume = m.get(fields[4], volume);
      open = m.get(fields[5], open);
      current = m.get(fields[6], current);
      sl = m.get(fields[7], sl);
      tp = m.get(fields[8], tp);
      comment = m.get(fields[9], comment);
   }
};
   
static const int OrderTuple::fields[] =
{
   ORDER_TICKETORDER_TIME_SETUPORDER_TIME_DONEORDER_TYPEORDER_VOLUME_INITIAL,
   ORDER_PRICE_OPENORDER_PRICE_CURRENTORDER_SLORDER_TPORDER_COMMENT
};

Теперь все готово для реализации главной функции скрипта — OnStart. В самом начале опишем объекты фильтров сделок и ордеров.

void OnStart()
{
   DealFilter filter;
   HistoryOrderFilter subfilter;
   ...

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

   if(PositionID == 0 || Type == TOTAL)
   {
      HistorySelect(0LONG_MAX);
   }
   else if(Type == POSITION)
   {
      HistorySelectByPosition(PositionID);
   }
   ...

Далее соберем в массив все идентификаторы позиций, или оставим одну заданную пользователем.

   ulong positions[];
   if(PositionID == 0)
   {
      ulong tickets[];
      filter.let(DEAL_SYMBOL_Symbol)
         .select(DEAL_POSITION_IDticketspositionstrue); // true - сортировка
      ArrayUnique(positions);
   }
   else
   {
      PUSH(positionsPositionID);
   }
   
   const int n = ArraySize(positions);
   Print("Positions total: "n);
   if(n == 0return;
   ...

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

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

   for(int i = 0i < n; ++i)
   {
      DealTuple deals[];
      filter.let(DEAL_POSITION_IDpositions[i]).select(dealstrue);
      const int m = ArraySize(deals);
      if(m == 0)
      {
         Print("Wrong position ID: "positions[i]);
         break// неверный идентификатор задан пользователем
      }
      double profit = 0// TODO: нужно учесть комиссии, свопы и сборы
      for(int j = 0j < m; ++jprofit += deals[j].profit;
      PrintFormat("Position: % 8d %16lld Profit:%f"i + 1positions[i], (profit));
      ArrayPrint(deals);
      
      Print("Order details:");
      OrderTuple orders[];
      subfilter.let(ORDER_POSITION_IDpositions[i], IS::OR_EQUAL)
         .let(ORDER_POSITION_BY_IDpositions[i], IS::OR_EQUAL)
         .select(orders);
      ArrayPrint(orders);
   }
}

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

Вы можете сравнить результаты работы скрипта с таблицей на вкладке "История" в терминале: там в колонке "Прибыль" показывается чистая прибыль по каждой позиции (свопы, комиссии и сборы — в соседних столбцах, но их нужно включить).

Важно отметить, что ордер типа ORDER_TYPE_CLOSE_BY будет отображаться в обеих позициях только в том случае, если в настройках выбрана вся история. Если же была выбрана конкретная позиция, система включит такой ордер только в одну из них (ту, что была указана в торговом запросе первой, в поле position), но не во вторую (которая была указана в position_by).

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

Positions total: 3

Position:        1       1253500309 Profit:238.150000

                   [_1]     [deal]    [order] [type] [in_out] [volume]  [price]  [profit]

[0] 2022.02.04 17:34:57 1236049891 1253500309 "BUY"  "IN"      1.00000 76.23900   0.00000

[1] 2022.02.14 16:28:41 1242295527 1259788704 "SELL" "OUT"     1.00000 76.42100 238.15000

Order details:

          [_1]             [setup]              [done] [type] [volume]   [open] [current] »

   » [sl] [tp] [comment]

[0] 1253500309 2022.02.04 17:34:57 2022.02.04 17:34:57 "BUY"   1.00000 76.23900  76.23900 »

   » 0.00 0.00 ""       

[1] 1259788704 2022.02.14 16:28:41 2022.02.14 16:28:41 "SELL"  1.00000 76.42100  76.42100 »

   » 0.00 0.00 ""       

Position:        2       1253526613 Profit:878.030000

                   [_1]     [deal]    [order] [type] [in_out] [volume]  [price]  [profit]

[0] 2022.02.07 10:00:00 1236611994 1253526613 "BUY"  "IN"      1.00000 75.75000   0.00000

[1] 2022.02.14 16:28:40 1242295517 1259788693 "SELL" "OUT"     1.00000 76.42100 878.03000

Order details:

          [_1]             [setup]              [done]      [type] [volume]   [open] [current] »

   » [sl] [tp] [comment]

[0] 1253526613 2022.02.04 17:55:18 2022.02.07 10:00:00 "BUY_LIMIT"  1.00000 75.75000  75.67000 »

   » 0.00 0.00 ""       

[1] 1259788693 2022.02.14 16:28:40 2022.02.14 16:28:40 "SELL"       1.00000 76.42100  76.42100 »

   » 0.00 0.00 ""       

Position:        3       1256280710 Profit:4449.040000

                   [_1]     [deal]    [order] [type] [in_out] [volume]  [price]   [profit]

[0] 2022.02.09 13:17:52 1238797056 1256280710 "BUY"  "IN"      2.00000 74.72100    0.00000

[1] 2022.02.14 16:28:39 1242295509 1259788685 "SELL" "OUT"     2.00000 76.42100 4449.04000

Order details:

          [_1]             [setup]              [done] [type] [volume]   [open] [current] »

   » [sl] [tp] [comment]

[0] 1256280710 2022.02.09 13:17:52 2022.02.09 13:17:52 "BUY"   2.00000 74.72100  74.72100 »

   » 0.00 0.00 ""       

[1] 1259788685 2022.02.14 16:28:39 2022.02.14 16:28:39 "SELL"  2.00000 76.42100  76.42100 »

   » 0.00 0.00 ""       

Случай с доливкой позиции (две сделки "IN") и её переворота (сделка "INOUT" большего объема) на неттинговом счете показан в следующем фрагменте.

Position:        5        219087383 Profit:0.170000

                   [_1]    [deal]   [order] [type] [in_out] [volume] [price] [profit]

[0] 2022.03.29 08:03:33 215612450 219087383 "BUY"  "IN"      0.01000 1.10011  0.00000

[1] 2022.03.29 08:04:05 215612451 219087393 "BUY"  "IN"      0.01000 1.10009  0.00000

[2] 2022.03.29 08:04:29 215612457 219087400 "SELL" "INOUT"   0.03000 1.10018  0.16000

[3] 2022.03.29 08:04:34 215612460 219087403 "BUY"  "OUT"     0.01000 1.10017  0.01000

Order details:

         [_1]             [setup]              [done] [type] [volume] [open] [current] »

   » [sl] [tp] [comment]

[0] 219087383 2022.03.29 08:03:33 2022.03.29 08:03:33 "BUY"   0.01000 0.0000   1.10011 »

   » 0.00 0.00 ""       

[1] 219087393 2022.03.29 08:04:05 2022.03.29 08:04:05 "BUY"   0.01000 0.0000   1.10009 »

   » 0.00 0.00 ""       

[2] 219087400 2022.03.29 08:04:29 2022.03.29 08:04:29 "SELL"  0.03000 0.0000   1.10018 »

   » 0.00 0.00 ""       

[3] 219087403 2022.03.29 08:04:34 2022.03.29 08:04:34 "BUY"   0.01000 0.0000   1.10017 »

   » 0.00 0.00 ""       

Частичную историю на примере конкретных позиций рассмотрим для случая встречного закрытия на счете с хеджированием. Сперва можно просмотреть отдельно первую позицию: PositionID=1276109280. Она будет показана полностью вне зависимости от входного параметра Type.

Positions total: 1

Position:        1       1276109280 Profit:-0.040000

                   [_1]     [deal]    [order] [type] [in_out] [volume] [price] [profit]

[0] 2022.03.07 12:20:53 1258725455 1276109280 "BUY"  "IN"      0.01000 1.08344  0.00000

[1] 2022.03.07 12:20:58 1258725503 1276109328 "SELL" "OUT_BY"  0.01000 1.08340 -0.04000

Order details:

          [_1]             [setup]              [done]     [type] [volume]  [open] [current] »

   » [sl] [tp]                    [comment]

[0] 1276109280 2022.03.07 12:20:53 2022.03.07 12:20:53 "BUY"       0.01000 1.08344   1.08344 »

   » 0.00 0.00 ""                          

[1] 1276109328 2022.03.07 12:20:58 2022.03.07 12:20:58 "CLOSE_BY"  0.01000 1.08340   1.08340 »

   » 0.00 0.00 "#1276109280 by #1276109283"

Также можно посмотреть вторую: PositionID=1276109283. Однако если Type равен "Position", для выделения фрагмента истории используется функция HistorySelectByPosition, и в результате выходной ордер будет только один (несмотря на то, что сделок — две).

Positions total: 1

Position:        1       1276109283 Profit:0.000000

                   [_1]     [deal]    [order] [type] [in_out] [volume] [price] [profit]

[0] 2022.03.07 12:20:53 1258725458 1276109283 "SELL" "IN"      0.01000 1.08340  0.00000

[1] 2022.03.07 12:20:58 1258725504 1276109328 "BUY"  "OUT_BY"  0.01000 1.08344  0.00000

Order details:

          [_1]             [setup]              [done] [type] [volume]  [open] [current] »

   » [sl] [tp] [comment]

[0] 1276109283 2022.03.07 12:20:53 2022.03.07 12:20:53 "SELL"  0.01000 1.08340   1.08340 »

   » 0.00 0.00 ""       

Если поменять Type на "всю историю", ордер "CLOSE_BY" появится.

Positions total: 1

Position:        1       1276109283 Profit:0.000000

                   [_1]     [deal]    [order] [type] [in_out] [volume] [price] [profit]

[0] 2022.03.07 12:20:53 1258725458 1276109283 "SELL" "IN"      0.01000 1.08340  0.00000

[1] 2022.03.07 12:20:58 1258725504 1276109328 "BUY"  "OUT_BY"  0.01000 1.08344  0.00000

Order details:

          [_1]             [setup]              [done]     [type] [volume]  [open] [current] »

   » [sl] [tp]                    [comment]

[0] 1276109283 2022.03.07 12:20:53 2022.03.07 12:20:53 "SELL"      0.01000 1.08340   1.08340 »

   » 0.00 0.00 ""                          

[1] 1276109328 2022.03.07 12:20:58 2022.03.07 12:20:58 "CLOSE_BY"  0.01000 1.08340   1.08340 »

   » 0.00 0.00 "#1276109280 by #1276109283"

При таких настройках история выбирается полностью, но фильтр оставляет из неё только ордера, у которых в свойства ORDER_POSITION_ID или ORDER_POSITION_BY_ID встречается идентификатор заданной позиции. Для составления условий с логическим ИЛИ в классе TradeFilter добавлен элемент IS::OR_EQUAL — с ним предлагается разобраться самостоятельно.