Контроль за изменениями торгового окружения

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

Как вы знаете, свойства всех торговых объектов делятся по типам на 3 группы: целочисленные, вещественные и строковые. У каждого класса объектов эти группы свои (например, у ордеров целочисленные свойства описаны в перечислении ENUM_ORDER_PROPERTY_INTEGER, а у позиций — в ENUM_POSITION_PROPERTY_INTEGER), но суть деления общая. Поэтому введем перечисление PROP_TYPE, с помощью которого можно будет описывать для любого объекта, к какому типу относится то или иное свойство. Это обобщение напрашивается, поскольку механизмы хранения и обработки свойств одного типа должны быть одинаковыми, вне зависимости от того, принадлежит ли свойство ордеру, позиции или сделке.

enum PROP_TYPE
{
   PROP_TYPE_INTEGER,
   PROP_TYPE_DOUBLE,
   PROP_TYPE_STRING,
};

Наиболее простым способом хранения значений свойств являются массивы. Очевидно, что из-за наличия 3 базовых типов нам потребуется 3 разных массива. Опишем их внутри нового класса TradeState, вложенного в MonitorInterface (TradeBaseMonitor.mqh).

Напомним, что базовый шаблон MonitorInterface<I,D,S> составляет основу всех прикладных классов-мониторов (OrderMonitor, DealMonitor, PositionMonitor). Типы I, D, S здесь соответствуют конкретным перечислениям целых, вещественных и строковых свойств.

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

template<typename I,typename D,typename S>
class MonitorInterface
{
   ...
   class TradeState
   {
   public:
      ...
      long ulongs[];
      double doubles[];
      string strings[];
      const MonitorInterface *owner;
      
      TradeState(const MonitorInterface *ptr) : owner(ptr)
      {
         ...
      }
   };

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

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

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

В разделе Перечисления мы представили скрипт ConversionEnum.mq5, в котором реализована функция process для вывода в журнал всех элементов конкретного перечисления. В том скрипте изучалось перечисление ENUM_APPLIED_PRICE, но ничто не мешает нам сделать копию скрипта и выполнить анализ трех других перечислений, которые нас интересуют. Например, так:

void OnStart()
{
   process((ENUM_POSITION_PROPERTY_INTEGER)0);
   process((ENUM_POSITION_PROPERTY_DOUBLE)0);
   process((ENUM_POSITION_PROPERTY_STRING)0);
}

В результате его выполнения получим следующий лог. Левая колонка содержит нумерацию внутри перечислений, а значения справа (после знака '=') — встроенные константы (идентификаторы) элементов.

ENUM_POSITION_PROPERTY_INTEGER Count=9
0 POSITION_TIME=1
1 POSITION_TYPE=2
2 POSITION_MAGIC=12
3 POSITION_IDENTIFIER=13
4 POSITION_TIME_MSC=14
5 POSITION_TIME_UPDATE=15
6 POSITION_TIME_UPDATE_MSC=16
7 POSITION_TICKET=17
8 POSITION_REASON=18
ENUM_POSITION_PROPERTY_DOUBLE Count=8
0 POSITION_VOLUME=3
1 POSITION_PRICE_OPEN=4
2 POSITION_PRICE_CURRENT=5
3 POSITION_SL=6
4 POSITION_TP=7
5 POSITION_COMMISSION=8
6 POSITION_SWAP=9
7 POSITION_PROFIT=10
ENUM_POSITION_PROPERTY_STRING Count=3
0 POSITION_SYMBOL=0
1 POSITION_COMMENT=11
2 POSITION_EXTERNAL_ID=19

Например, свойство с константой 0 — это строковое POSITION_SYMBOL, с константами 1 и 2 — целочисленные POSITION_TIME и POSITION_TYPE, а с константой 3 — вещественное POSITION_VOLUME, и так далее.

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

Для каждого свойства нужно запомнить его тип (от этого зависит, в каком из трех массивов хранить значение) и порядковый номер среди свойств такого же типа (это будет индекс элемента в соответствующем массиве). Например, мы видим, что у позиций есть только 3 строковых свойства, значит массив strings в слепке одной позиции должен будет иметь такой размер, и под индексами 0, 1, 2 в него будут записываться POSITION_SYMBOL (0), POSITION_COMMENT (11), POSITION_EXTERNAL_ID (19).

Преобразование сквозных индексов свойств в их тип (один из PROP_TYPE) и в порядковый номер в массиве соответствующего типа можно сделать один раз при старте программы, так как перечисления со свойствами являются постоянными (встроены в систему). Полученную таблицу косвенной адресации запишем в статический двумерный массив indices. Его размер по первому измерению будет динамически определен как общее количество свойств (всех 3-х типов) — его запишем в статическую переменную limit. По второму измерению выделена пара ячеек: indices[i][0] — тип PROP_TYPE, indices[i][1] — индекс в одном из массивов ulongs, doubles или strings (в зависимости от indices[i][0]).

   class TradeState
   {
      ...
      static int indices[][2];
      static int jds;
   public:
      const static int limit;
      
      static PROP_TYPE type(const int i)
      {
         return (PROP_TYPE)indices[i][0];
      }
      
      static int offset(const int i)
      {
         return indices[i][1];
      }
      ...

Переменные j, d, s будут использованы для последовательной индексации свойств внутри каждого из 3-х разных типов. Вот, собственно, как это делается в статическом методе calcIndices.

      static int calcIndices()
      {
         const int size = fmax(boundary<I>(),
            fmax(boundary<D>(), boundary<S>())) + 1;
         ArrayResize(indicessize);
         j = d = s = 0;
         for(int i = 0i < size; ++i)
         {
            if(detect<I>(i))
            {
               indices[i][0] = PROP_TYPE_INTEGER;
               indices[i][1] = j++;
            }
            else if(detect<D>(i))
            {
               indices[i][0] = PROP_TYPE_DOUBLE;
               indices[i][1] = d++;
            }
            else if(detect<S>(i))
            {
               indices[i][0] = PROP_TYPE_STRING;
               indices[i][1] = s++;
            }
            else
            {
               Print("Unresolved int value as enum: "i" "typename(TradeState));
            }
         }
         return size;
      }

Метод boundary возвращает максимальную константу среди всех элементов заданного перечисления E.

   template<typename E>
   static int boundary(const E dummy = (E)NULL)
   {
      int values[];
      const int n = EnumToArray(dummyvalues01000);
      ArraySort(values);
      return values[n - 1];
   }

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

В этом помогает метод detect, который возвращает true, если целое число является элементом перечисления.

   template<typename E>
   static bool detect(const int v)
   {
      ResetLastError();
      const string s = EnumToString((E)v); // результат не используется 
      if(_LastError == 0// важно только отсутствие ошибки
      {
         return true;
      }
      return false;
   }

Последний вопрос в том, как запустить этот расчет при старте программы. Это достигается за счет статичности переменных и метода.

template<typename I,typename D,typename S>
static int MonitorInterface::TradeState::indices[][2];
template<typename I,typename D,typename S>
static int MonitorInterface::TradeState::j,
   MonitorInterface::TradeState::d,
   MonitorInterface::TradeState::s;
template<typename I,typename D,typename S>
const static int MonitorInterface::TradeState::limit =
   MonitorInterface::TradeState::calcIndices();

Обратите внимание, что limit инициализируется результатом вызова нашей функции calcIndices.

Имея таблицу с индексами, реализуем заполнение массивов значениями свойств в методе cache.

   class TradeState
   {
      ...
      TradeState(const MonitorInterface *ptr) : owner(ptr)
      {
         cache(); // при создании объекта сразу кешируем свойства
      }
      
      template<typename T>
      void _get(const int eT &valueconst // перегрузка с записью по ссылке
      {
         value = owner.get(evalue);
      }
      
      void cache()
      {
         ArrayResize(ulongsj);
         ArrayResize(doublesd);
         ArrayResize(stringss);
         for(int i = 0i < limit; ++i)
         {
            switch(indices[i][0])
            {
            case PROP_TYPE_INTEGER_get(iulongs[indices[i][1]]); break;
            case PROP_TYPE_DOUBLE_get(idoubles[indices[i][1]]); break;
            case PROP_TYPE_STRING_get(istrings[indices[i][1]]); break;
            }
         }
      }
   };

Мы проходимся в цикле по всему диапазону свойств от 0 до limit и в зависимости от типа свойства в indices[i][0] записываем его значение в элемент массива ulongs, doubles или strings под номером indices[i][1] (соответствующий элемент массива передается по ссылке в метод _get).

Вызов owner.get(e, value) обращается к одному из стандартных методов класса-монитора (здесь он виден как абстрактный указатель MonitorInterface). В частности, для позиций в классе PositionMonitor это приведет к вызовам PositionGetInteger, PositionGetDouble или PositionGetString. Нужный тип выберет компилятор. В мониторах ордеров и сделок существуют свои аналогичные реализации, которые автоматически подключаются этим базовым кодом.

Описание слепка одного торгового объекта логично унаследовать от класса-монитора. Поскольку нам предстоит кэшировать ордера, сделки и позиции, имеет смысл сделать новый класс шаблоном и собрать в нем все общие алгоритмы, подходящие для всех объектов. Назовем его TradeBaseState (файл TradeState.mqh).

template<typename M,typename I,typename D,typename S>
class TradeBaseStatepublic M
{
   M::TradeState state;
   bool cached;
   
public:
   TradeBaseState(const ulong t) : M(t), state(&this), cached(ready)
   {
   }
   
   void passthrough(const bool b)   // включение/отключение кеша по желанию
   {
      cached = b;
   }
   ...

Под буквой M здесь скрывается один из конкретных классов-мониторов, описанных ранее (OrderMonitor.mqh, PositionMonitor.mqh, DealMonitor.mqh). Основу составляет кэширующий объект state только что представленного класса M::TradeState. В зависимости от M внутри будет сформирована специфическая индексная таблица (одна для класса M) и распределены массивы свойств (собственные для каждого экземпляра M, то есть для каждого ордера, сделки, позиции).

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

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

   virtual long get(const I propertyconst override
   {
      return cached ? state.ulongs[M::TradeState::offset(property)] : M::get(property);
   }
   
   virtual double get(const D propertyconst override
   {
      return cached ? state.doubles[M::TradeState::offset(property)] : M::get(property);
   }
   
   virtual string get(const S propertyconst override
   {
      return cached ? state.strings[M::TradeState::offset(property)] : M::get(property);
   }
   ...

По умолчанию, кэширование, разумеется, включено.

Мы должны предусмотреть и метод, непосредственно выполняющий кэширование (заполнение массивов). Для этого достаточно вызвать метод cache у объекта state.

   bool update()
   {
      if(refresh())
      {
         cached = false// отключаем чтение из кэша
         state.cache();  // читаем реальные свойства и записываем в кэш
         cached = true;  // включаем обратно внешний доступ к кэшу 
         return true;
      }
      return false;
   }

Но что такое метод refresh?

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

// TradeBaseMonitor.mqh
template<typename I,typename D,typename S>
class MonitorInterface
{
   ...
   virtual bool refresh() = 0;

Он должен вернуть true при успешном выделении ордера, сделки или позиции. Если результат равен false, во встроенной переменной _LastError подразумевается наличие одной из ошибок:

  • 4753 ERR_TRADE_POSITION_NOT_FOUND;
  • 4754 ERR_TRADE_ORDER_NOT_FOUND;
  • 4755 ERR_TRADE_DEAL_NOT_FOUND;

При этом переменная-член ready, которая сигнализирует доступность объекта, должна быть сброшена в false в реализациях этого метода в производных классах.

Например, в конструкторе PositionMonitor у нас была и остается такая инициализация. В мониторах ордеров и сделок ситуация похожа.

// PositionMonitor.mqh
   const ulong ticket;
   PositionMonitor(const ulong t): ticket(t)
   {
      if(!PositionSelectByTicket(ticket))
      {
         PrintFormat("Error: PositionSelectByTicket(%lld) failed: %s"ticket,
            E2S(_LastError));
      }
      else
      {
         ready = true;
      }
   }
   ...

Теперь мы добавим во все конкретные классы метод refresh такого вида (на примере PositionMonitor):

// PositionMonitor.mqh
   virtual bool refresh() override
   {
      ready = PositionSelectByTicket(ticket);
      return ready;
   }

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

Для выявления различий и записи индексов изменившихся свойств в массив changes предназначен метод getChanges в создаваемом классе TradeBaseState. Метод возвращает true при обнаружении изменений.

template<typename M,typename I,typename D,typename S>
class TradeBaseStatepublic M
{
   ...
   bool getChanges(int &changes[])
   {
      const bool previous = ready;
      if(refresh())
      {
         // элемент выбран в торговом окружении = можно читать и сравнивать свойства
         cached = false;    // читаем напрямую
         const bool result = M::diff(statechanges);
         cached = true;     // включаем обратно кэш по умолчанию
         return result;
      }
      // перестал быть "готовым" = скорее всего, удален
      return previous != ready// если удален только что, это тоже изменение
   }

Как видно, основная работа поручается некоему методу diff в классе M. Это новый метод: нужно его написать. К счастью, благодаря ООП, можно сделать это единожды в базовом шаблоне MonitorInterface, и метод появится сразу для ордеров, сделок и позиций.

// TradeBaseMonitor.mqh
template<typename I,typename D,typename S>
class MonitorInterface
{
   ...
   bool diff(const TradeState &thatint &changes[])
   {
      ArrayResize(changes0);
      for(int i = 0i < TradeState::limit; ++i)
      {
         switch(TradeState::indices[i][0])
         {
         case PROP_TYPE_INTEGER:
            if(this.get((I)i) != that.ulongs[TradeState::offset(i)])
            {
               PUSH(changesi);
            }
            break;
         case PROP_TYPE_DOUBLE:
            if(!TU::Equal(this.get((D)i), that.doubles[TradeState::offset(i)]))
            {
               PUSH(changesi);
            }
            break;
         case PROP_TYPE_STRING:
            if(this.get((S)i) != that.strings[TradeState::offset(i)])
            {
               PUSH(changesi);
            }
            break;
         }
      }
      return ArraySize(changes) > 0;
   }

Итак, все готово для формирования конкретных кэширующих классов для ордеров, сделок и позиций. Например, позиции будут храниться в расширенном мониторе PositionState на базе PositionMonitor.

class PositionStatepublic TradeBaseState<PositionMonitor,
   ENUM_POSITION_PROPERTY_INTEGER,
   ENUM_POSITION_PROPERTY_DOUBLE,
   ENUM_POSITION_PROPERTY_STRING>
{
public:
   PositionState(const long t): TradeBaseState(t) { }
};

Аналогичным образом в файле TradeState.mqh определен кэширующий класс для сделок.

class DealStatepublic TradeBaseState<DealMonitor,
   ENUM_DEAL_PROPERTY_INTEGER,
   ENUM_DEAL_PROPERTY_DOUBLE,
   ENUM_DEAL_PROPERTY_STRING>
{
public:
   DealState(const long t): TradeBaseState(t) { }
};

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

В связи с этим в файл OrderMonitor.mqh добавлены 2 более конкретных класса: ActiveOrderMonitor и HistoryOrderMonitor.

// OrderMonitor.mqh
class ActiveOrderMonitorpublic OrderMonitor
{
public:
   ActiveOrderMonitor(const ulong t): OrderMonitor(t)
   {
      if(history// если ордер в истории, то он уже неактивен
      {
         ready = false;   // сбрасываем флаг готовности
         history = false// это объект только для активных ордеров по определению
      }
   }
   
   virtual bool refresh() override
   {
      ready = OrderSelect(ticket);
      return ready;
   }
};
   
class HistoryOrderMonitorpublic OrderMonitor
{
public:
   HistoryOrderMonitor(const ulong t): OrderMonitor(t) { }
   
   virtual bool refresh() override
   {
      history = true// работаем только с историей
      ready = historyOrderSelectWeak(ticket);
      return ready// готовность определяется наличием тикета в истории
   }
};

Каждый из них ищет тикет только в своей области. На основе этих мониторов уже можно создать кэширующие классы.

// TradeState.mqh
class OrderStatepublic TradeBaseState<ActiveOrderMonitor,
   ENUM_ORDER_PROPERTY_INTEGER,
   ENUM_ORDER_PROPERTY_DOUBLE,
   ENUM_ORDER_PROPERTY_STRING>
{
public:
   OrderState(const long t): TradeBaseState(t) { }
};
   
class HistoryOrderStatepublic TradeBaseState<HistoryOrderMonitor,
   ENUM_ORDER_PROPERTY_INTEGER,
   ENUM_ORDER_PROPERTY_DOUBLE,
   ENUM_ORDER_PROPERTY_STRING>
{
public:
   HistoryOrderState(const long t): TradeBaseState(t) { }
};

Последний штрих, который мы добавим для удобства в класс TradeBaseState — особый метод для преобразования значения свойства в строку. Хотя в мониторе есть несколько версий методов stringify, все они будут "печатать" либо значения из кэша (если переменная-член cached равна true), либо из оригинального объекта торгового окружения (если cached равно false). Нам же для визуализации отличий кэша от измененного объекта (когда эти отличия обнаружатся) нужно одновременно прочитать и значение из кэша, и минуя кэш. В связи с этим добавим метод stringifyRaw, всегда работающий со свойством напрямую (за счет того, что переменная cached временно сбрасывается и устанавливается обратно).

   // получить строковое представление свойства 'i' минуя кэш
   string stringifyRaw(const int i)
   {
      const bool previous = cached;
      cached = false;
      const string s = stringify(i);
      cached = previous;
   }

Проверим работоспособность кэширующего монитора на простом примере эксперта, отслеживающего состояние действующего ордера (OrderSnapshot.mq5). Позднее мы разовьем данную идея для кэширования любой совокупности ордеров, сделок или позиций, то есть создадим полноценный кэш.

Эксперт будет пытаться найти последний в списке действующих ордеров и создавать для него объект OrderState. Если ордеров нет, пользователь получит предложение создать ордер или открыть позицию (последнее сопряжено с постановкой и исполнением ордера по рынку). Как только ордер обнаружен, для него в обработчике OnTrade производится проверка на изменение состояния. Эксперт продолжит контролировать этот ордер, пока не будет выгружен.

int OnInit()
{
   if(OrdersTotal() == 0)
   {
      Alert("Please, create a pending order or open/close a position");
   }
   else
   {
      OnTrade(); // self-invocation
   }
   return INIT_SUCCEEDED;
}
   
void OnTrade()
{
   static int count = 0;
   // указатель на объект хранится в статическом AutoPtr
   static AutoPtr<OrderStateauto;
   // получаем "чистый" указатель (чтобы не разыменовывать auto[] везде)
   OrderState *state = auto[];
   
   PrintFormat(">>> OnTrade(%d)"count++);
   
   if(OrdersTotal() > 0 && state == NULL)
   {
      const ulong ticket = OrderGetTicket(OrdersTotal() - 1);
      auto = new OrderState(ticket);
      PrintFormat("Order picked up: %lld %s"ticket,
         auto[].isReady() ? "true" : "false");
      auto[].print(); // начальное состояние на момент "захвата" ордера
   }
   else if(state)
   {
      int changes[];
      if(state.getChanges(changes))
      {
         Print("Order properties changed:");
         ArrayPrint(changes);
         ...
      }
      if(_LastError != 0Print(E2S(_LastError));
   }
}

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

         for(int k = 0k < ArraySize(changes); ++k)
         {
            switch(OrderState::TradeState::type(changes[k]))
            {
            case PROP_TYPE_INTEGER:
               Print(EnumToString((ENUM_ORDER_PROPERTY_INTEGER)changes[k]), ": ",
                  state.stringify(changes[k]), " -> ",
                  state.stringifyRaw(changes[k]));
                  break;
            case PROP_TYPE_DOUBLE:
               Print(EnumToString((ENUM_ORDER_PROPERTY_DOUBLE)changes[k]), ": ",
                  state.stringify(changes[k]), " -> ",
                  state.stringifyRaw(changes[k]));
                  break;
            case PROP_TYPE_STRING:
               Print(EnumToString((ENUM_ORDER_PROPERTY_STRING)changes[k]), ": ",
                  state.stringify(changes[k]), " -> ",
                  state.stringifyRaw(changes[k]));
                  break;
            }
         }

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

         state.update();

Если запустить эксперт на счете без действующих ордеров и выставить новый, увидим в журнале следующие записи (в данном случае создавался buy limit для EURUSD, ниже текущей рыночной цены).

Alert: Please, create a pending order or open/close a position

>>> OnTrade(0)

Order picked up: 1311736135 true

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

  0 ORDER_TIME_SETUP=2022.04.11 11:42:39

  1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

  2 ORDER_TIME_DONE=1970.01.01 00:00:00

  3 ORDER_TYPE=ORDER_TYPE_BUY_LIMIT

  4 ORDER_TYPE_FILLING=ORDER_FILLING_RETURN

  5 ORDER_TYPE_TIME=ORDER_TIME_GTC

  6 ORDER_STATE=ORDER_STATE_STARTED

  7 ORDER_MAGIC=0

  8 ORDER_POSITION_ID=0

  9 ORDER_TIME_SETUP_MSC=2022.04.11 11:42:39'729

 10 ORDER_TIME_DONE_MSC=1970.01.01 00:00:00'000

 11 ORDER_POSITION_BY_ID=0

 12 ORDER_TICKET=1311736135

 13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

  0 ORDER_VOLUME_INITIAL=0.01

  1 ORDER_VOLUME_CURRENT=0.01

  2 ORDER_PRICE_OPEN=1.087

  3 ORDER_PRICE_CURRENT=1.087

  4 ORDER_PRICE_STOPLIMIT=0.0

  5 ORDER_SL=0.0

  6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

  0 ORDER_SYMBOL=EURUSD

  1 ORDER_COMMENT=

  2 ORDER_EXTERNAL_ID=

>>> OnTrade(1)

Order properties changed:

10 14

ORDER_PRICE_CURRENT: 1.087 -> 1.09073

ORDER_STATE: ORDER_STATE_STARTED -> ORDER_STATE_PLACED

>>> OnTrade(2)

>>> OnTrade(3)

>>> OnTrade(4)

Здесь видно, как статус ордера изменился со STARTED на PLACED. Если бы мы вместо отложенного ордера открылись по рынку мелким объемом, этих изменений могли бы не успеть получить, потому что такие ордера, как правило, очень быстро устанавливаются, и их наблюдаемый статус меняется со STARTED сразу на FILLED. А последнее уже означает, что ордер перенесен в историю. Поэтому для их отслеживания требуется параллельный мониторинг истории. Мы покажем это в следующем примере.

Обратите внимание, что событий OnTrade может быть много, но они не все связаны с нашим ордером.

Попробуем задать уровень Take Profit и посмотрим в журнал.

>>> OnTrade(5)
Order properties changed:
10 13
ORDER_PRICE_CURRENT: 1.09073 -> 1.09079
ORDER_TP: 0.0 -> 1.097
>>> OnTrade(6)
>>> OnTrade(7)

Далее поменяем срок истечения: с GTC до одного дня.

>>> OnTrade(8)
Order properties changed:
10
ORDER_PRICE_CURRENT: 1.09079 -> 1.09082
>>> OnTrade(9)
>>> OnTrade(10)
Order properties changed:
2 6
ORDER_TIME_EXPIRATION: 1970.01.01 00:00:00 -> 2022.04.11 00:00:00
ORDER_TYPE_TIME: ORDER_TIME_GTC -> ORDER_TIME_DAY
>>> OnTrade(11)

Здесь в процессе изменения нашего ордера цена успела измениться, и потому мы "зацепили" промежуточное уведомление о новом значении в ORDER_PRICE_CURRENT. И только после этого в журнал попали ожидаемые перемены в ORDER_TYPE_TIME и ORDER_TIME_EXPIRATION.

Далее мы удалили ордер.

>>> OnTrade(12)
TRADE_ORDER_NOT_FOUND

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

Кэшировать и контролировать изменения, как правило, требуется не для отдельного ордера или позиции, а для всех или их набора, отобранного по некоторым условиям. Для этих целей разработаем базовый класс-шаблон TradeCache (TradeCache.mqh) и на его основе — прикладные классы для списков ордеров, сделок и позиций.

template<typename T,typename F,typename E>
class TradeCache
{
   AutoPtr<Tdata[];
   const E property;
   const int NOT_FOUND_ERROR;
   
public:
   TradeCache(const E idconst int error): property(id), NOT_FOUND_ERROR(error) { }
   
   virtual string rtti() const
   {
      return typename(this); // будем переопределять в производных классах для наглядного вывода в журнал
   }
   ...

В данном шаблоне буквой T обозначен один из классов семейства TradeState. Как видно, массив таких объектов в виде авто-указателей зарезервирован под именем data.

Буква F описывает тип одного из классов-фильтров (OrderFilter.mqh, включая HistoryOrderFilter, DealFilter.mqh, PositionFilter.mqh), используемых для отбора кэшируемых элементов. В простейшем случае, когда в фильтре нет let-условий, будут кэшироваться все элементы (с учетом выборки истории для объектов из истории).

Буква E соответствует перечислению, в котором находится свойство property, идентифицирующее объекты. Поскольку обычно это свойство — КАКОЙ-ТО_TICKET, в качестве перечисления предполагается целочисленный ENUM_ЧТО-ТО_PROPERTY_INTEGER.

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

Главный метод класса scan принимает в качестве параметра ссылку на настроенный фильтр (его настройкой должен заняться вызывающий код).

   void scan(F &f)
   {
      const int existedBefore = ArraySize(data);
      
      ulong tickets[];
      ArrayResize(ticketsexistedBefore);
      for(int i = 0i < existedBefore; ++i)
      {
         tickets[i] = data[i][].get(property);
      }
      ...

В начале метода мы собираем идентификаторы уже кэшированных объектов в массив tickets. Очевидно, что при первом запуске он окажется пустым.

Далее заполняем другой массив objects тикетами актуальных объектов с помощью фильтра. Для каждого нового тикета создаем объект кэширующего монитора T и добавляем в массив data. Для старых объектов анализируем наличие изменений путем вызова data[j][].getChanges(changes) и затем обновляем кэш, вызвав data[j][].update().

      ulong objects[];
      f.select(objects);
      for(int i = 0ji < ArraySize(objects); ++i)
      {
         const ulong ticket = objects[i];
         for(j = 0j < existedBefore; ++j)
         {
            if(tickets[j] == ticket)
            {
               tickets[j] = 0// помечаем как найденный
               break;
            }
         }
         
         if(j == existedBefore// такого в кэше нет, надо добавить
         {
            const T *ptr = new T(ticket);
            PUSH(dataptr);
            onAdded(*ptr);
         }
         else
         {
            ResetLastError();
            int changes[];
            if(data[j][].getChanges(changes))
            {
               onUpdated(data[j][], changes);
               data[j][].update();
            }
            if(_LastErrorPrintFormat("%s: %lld (%s)"rtti(), ticketE2S(_LastError));
         }
      }
      ...

Как нетрудно заметить, в каждой фазе изменения — то есть при добавлении объекта или после его изменения — вызываются некие методы onAdded и onUpdated. Это виртуальные методы-заглушки, с помощью которых сканирование может уведомить программу о соответствующих событиях. Предполагается, что прикладной код реализует класс-наследник с переопределенными версиями этих методов. Мы коснемся этого вопроса чуть ниже, а пока продолжим рассматривать метод scan.

В вышеприведенном цикле все найденные тикеты в массиве tickets обнулены, и потому оставшиеся элементы соответствуют отсутствующим объектам торговой среды. Далее делается их проверка путем вызова getChanges и сравнением кода ошибки с NOT_FOUND_ERROR. Если это действительно так, вызывается еще один виртуальный метод onRemoved. Он возвращает логический флаг (проставляемый вашим прикладным кодом), следует ли удалить элемент из кэша.

      for(int j = 0j < existedBefore; ++j)
      {
         if(tickets[j] == 0continue// пропускаем обработанные элементы
         
         // этот тикет не нашелся, скорее всего удален
         int changes[];
         ResetLastError();
         if(data[j][].getChanges(changes))
         {
            if(_LastError == NOT_FOUND_ERROR// например, ERR_TRADE_POSITION_NOT_FOUND
            {
               if(onRemoved(data[j][]))
               {
                  data[j] = NULL;             // освобождаем объект и элемент массива
               }
               continue;
            }
            
            // NB! обычно мы не должны сюда проваливаться
            PrintFormat("Unexpected ticket: %lld (%s) %s"tickets[j],
               E2S(_LastError), rtti());
            onUpdated(data[j][], changestrue);
            data[j][].update();
         }
         else
         {
            PrintFormat("Orphaned element: %lld (%s) %s"tickets[j],
               E2S(_LastError), rtti());
         }
      }
   }

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

Базовый класс предоставляет стандартные реализации методов onAdded, onRemoved, onUpdated, которые выводят суть событий в журнал. Определив макрос PRINT_DETAILS в своем коде до включения заголовочного файла TradeCache.mqh, можно заказать распечатку всех свойств каждого нового объекта.

   virtual void onAdded(const T &state)
   {
      Print(rtti(), " added: "state.get(property));
      #ifdef PRINT_DETAILS
      state.print();
      #endif
   }
   
   virtual bool onRemoved(const T &state)
   {
      Print(rtti(), " removed: "state.get(property));
      return true// разрешаем удалить объект из кэша (false, чтобы сохранить)
   }
   
   virtual void onUpdated(T &stateconst int &changes[],
      const bool unexpected = false)
   {
      ...
   }

Метод onUpdated не будем приводить, поскольку он практически повторяет код для вывода изменений из эксперта OrderSnapshot.mq5, показанный выше.

Разумеется, в базовом классе есть средства для получения размера кэша и доступа к конкретному объекту по номеру.

   int size() const
   {
      return ArraySize(data);
   }
   
   T *operator[](int iconst
   {
      return data[i][]; // возвращаем указатель (T*) из объекта AutoPtr
   }

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

class PositionCachepublic TradeCache<PositionState,PositionFilter,
   ENUM_POSITION_PROPERTY_INTEGER>
{
public:
   PositionCache(const ENUM_POSITION_PROPERTY_INTEGER selector = POSITION_TICKET,
      const int error = ERR_TRADE_POSITION_NOT_FOUND): TradeCache(selectorerror) { }
};
   
class OrderCachepublic TradeCache<OrderState,OrderFilter,
   ENUM_ORDER_PROPERTY_INTEGER>
{
public:
   OrderCache(const ENUM_ORDER_PROPERTY_INTEGER selector = ORDER_TICKET,
      const int error = ERR_TRADE_ORDER_NOT_FOUND): TradeCache(selectorerror) { }
};
   
class HistoryOrderCachepublic TradeCache<HistoryOrderState,HistoryOrderFilter,
   ENUM_ORDER_PROPERTY_INTEGER>
{
public:
   HistoryOrderCache(const ENUM_ORDER_PROPERTY_INTEGER selector = ORDER_TICKET,
      const int error = ERR_TRADE_ORDER_NOT_FOUND): TradeCache(selectorerror) { }
};

Чтобы подвести итог процессу разработки представленного функционала приведем диаграмму основных классов. Это упрощенный вариант диаграмм UML, которые имеет смысл взять на вооружение при проектировании сложных программ на MQL5.

Диаграмма классов мониторов, фильтров и кэшей торговых объектов

Диаграмма классов мониторов, фильтров и кэшей торговых объектов

Желтым цветом обозначены шаблоны, белым оставлены абстрактные классы, цветные — конкретные реализации. Сплошные стрелки с закрашенными наконечниками обозначают наследование, пунктирные с полыми — типизацию шаблонов. Пунктирные стрелки с открытыми наконечниками — это использование классами указанных методов друг друга. Связи с ромбами — это композиция (включение одних объектов в другие).

В качестве примера использования кэша создадим эксперт TradeSnapshot.mq5, который будет реагировать на любые изменения торгового окружения из обработчика OnTrade. Для фильтрации и кэширования в коде описано 6 объектов, по 2 (фильтр и кэш) для каждого типа элементов — позиций, действующих ордеров и исторических ордеров.

PositionFilter filter0;
PositionCache positions;
   
OrderFilter filter1;
OrderCache orders;
   
HistoryOrderFilter filter2;
HistoryOrderCache history;

Фильтрам не задается каких-либо условий через вызовы метода let, так что в кэш попадут все обнаруженные онлайн объекты. Для ордеров из истории существует дополнительная настройка.

Опционально при запуске можно загрузить в кэш прошлые ордера на заданную глубину истории. Для этого предусмотрена входная переменная HistoryLookup. В ней можно выбрать последние сутки, последнюю неделю (по длительности, а не календарную), месяц (30 дней) или год (360 дней). По умолчанию прошлая история не подгружается (точнее, подгружается только за 1 секунду). Поскольку в эксперте определен макрос PRINT_DETAILS, будьте осторожны со счетами, где большая история: они могут сгенерировать объемный журнал, если не ограничить период.

enum ENUM_HISTORY_LOOKUP
{
   LOOKUP_NONE = 1,
   LOOKUP_DAY = 86400,
   LOOKUP_WEEK = 604800,
   LOOKUP_MONTH = 2419200,
   LOOKUP_YEAR = 29030400,
   LOOKUP_ALL = 0,
};
   
input ENUM_HISTORY_LOOKUP HistoryLookup = LOOKUP_NONE;
   
datetime origin;

В обработчике OnInit сбрасываем кэши (на тот случай, если эксперт перезапущен с новыми параметрами), вычисляем начальную дату истории в переменной origin и вызываем сами OnTrade первый раз.

int OnInit()
{
   positions.reset();
   orders.reset();
   history.reset();
   origin = HistoryLookup ? TimeCurrent() - HistoryLookup : 0;
   
   OnTrade(); // само-запуск
   return INIT_SUCCEEDED;
}

Обработчик OnTrade довольно минималистичен, поскольку все сложности теперь скрыты внутри классов.

void OnTrade()
{
   static int count = 0;
   
   PrintFormat(">>> OnTrade(%d)"count++);
   positions.scan(filter0);
   orders.scan(filter1);
   // делаем выборку истории непосредственно перед использованием фильтра
   // внутри метода 'scan'
   HistorySelect(originLONG_MAX);
   history.scan(filter2);
   PrintFormat(">>> positions: %d, orders: %d, history: %d",
      positions.size(), orders.size(), history.size());
}

Сразу после запуска эксперта на чистом счете увидим сообщение:

>>> OnTrade(0)
>>> positions: 0, orders: 0, history: 0

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

Сначала обнаружится активный ордер.

>>> OnTrade(1)

OrderCache added: 1311792104

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

  0 ORDER_TIME_SETUP=2022.04.11 12:34:51

  1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

  2 ORDER_TIME_DONE=1970.01.01 00:00:00

  3 ORDER_TYPE=ORDER_TYPE_BUY

  4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK

  5 ORDER_TYPE_TIME=ORDER_TIME_GTC

  6 ORDER_STATE=ORDER_STATE_STARTED

  7 ORDER_MAGIC=0

  8 ORDER_POSITION_ID=0

  9 ORDER_TIME_SETUP_MSC=2022.04.11 12:34:51'096

 10 ORDER_TIME_DONE_MSC=1970.01.01 00:00:00'000

 11 ORDER_POSITION_BY_ID=0

 12 ORDER_TICKET=1311792104

 13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

  0 ORDER_VOLUME_INITIAL=0.01

  1 ORDER_VOLUME_CURRENT=0.01

  2 ORDER_PRICE_OPEN=1.09218

  3 ORDER_PRICE_CURRENT=1.09218

  4 ORDER_PRICE_STOPLIMIT=0.0

  5 ORDER_SL=0.0

  6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

  0 ORDER_SYMBOL=EURUSD

  1 ORDER_COMMENT=

  2 ORDER_EXTERNAL_ID=

Затем этот ордер будет перемещен в историю (при этом поменяются, как минимум, статус, время исполнения и идентификатор позиции).

HistoryOrderCache added: 1311792104

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

  0 ORDER_TIME_SETUP=2022.04.11 12:34:51

  1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

  2 ORDER_TIME_DONE=2022.04.11 12:34:51

  3 ORDER_TYPE=ORDER_TYPE_BUY

  4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK

  5 ORDER_TYPE_TIME=ORDER_TIME_GTC

  6 ORDER_STATE=ORDER_STATE_FILLED

  7 ORDER_MAGIC=0

  8 ORDER_POSITION_ID=1311792104

  9 ORDER_TIME_SETUP_MSC=2022.04.11 12:34:51'096

 10 ORDER_TIME_DONE_MSC=2022.04.11 12:34:51'097

 11 ORDER_POSITION_BY_ID=0

 12 ORDER_TICKET=1311792104

 13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

  0 ORDER_VOLUME_INITIAL=0.01

  1 ORDER_VOLUME_CURRENT=0.0

  2 ORDER_PRICE_OPEN=1.09218

  3 ORDER_PRICE_CURRENT=1.09218

  4 ORDER_PRICE_STOPLIMIT=0.0

  5 ORDER_SL=0.0

  6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

  0 ORDER_SYMBOL=EURUSD

  1 ORDER_COMMENT=

  2 ORDER_EXTERNAL_ID=

>>> positions: 0, orders: 1, history: 1

Обратите внимание, что данные модификации произошли в течение одного вызова OnTrade. Иными словами, пока наша программа анализировала свойства нового ордера (с помощью вызова orders.scan), ордер параллельно был обработан терминалом, и к моменту проверки истории (с помощью вызова history.scan), уже попал в историю. Именно поэтому он числится и там, и там согласно последней строке этого фрагмента лога. Такое поведение нормально для многопоточных программ, и это следует учитывать при их разработке. Но оно не обязательно всегда будет проявляться. Здесь мы просто обращаем на это внимание. При быстром выполнении MQL-программы такую ситуацию обычно не удается застать.

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

Напомним, что MQL5 не позволяет синхронизировать торговое окружение целиком, а лишь по частям:

  • среди активных ордеров актуальна информация для ордера, для которого только что была вызвана функция OrderSelect или OrderGetTicket;
  • среди позиций актуальна информация для позиции, для которой только что была вызвана функция PositionSelect, PositionSelectByTicket или PositionGetTicket;
  • для ордеров и сделок истории доступна информация в контексте последнего вызова HistorySelect, HistorySelectByPosition, HistoryOrderSelect, HistoryDealSelect.

Кроме того, напомним, что торговые события (как и любые события MQL5) — это сообщения о произошедших изменениях, помещенные в очередь и извлеченные из очереди отложенным образом, а не непосредственно в момент совершения изменений. Тем более, событие OnTrade происходит после соответствующих событий OnTradeTransaction.

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

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

>>> OnTrade(2)

PositionCache added: 1311792104

MonitorInterface<ENUM_POSITION_PROPERTY_INTEGER,ENUM_POSITION_PROPERTY_DOUBLE,ENUM_POSITION_PROPERTY_STRING>

ENUM_POSITION_PROPERTY_INTEGER Count=9

  0 POSITION_TIME=2022.04.11 12:34:51

  1 POSITION_TYPE=POSITION_TYPE_BUY

  2 POSITION_MAGIC=0

  3 POSITION_IDENTIFIER=1311792104

  4 POSITION_TIME_MSC=2022.04.11 12:34:51'097

  5 POSITION_TIME_UPDATE=2022.04.11 12:34:51

  6 POSITION_TIME_UPDATE_MSC=2022.04.11 12:34:51'097

  7 POSITION_TICKET=1311792104

  8 POSITION_REASON=POSITION_REASON_CLIENT

ENUM_POSITION_PROPERTY_DOUBLE Count=8

  0 POSITION_VOLUME=0.01

  1 POSITION_PRICE_OPEN=1.09218

  2 POSITION_PRICE_CURRENT=1.09214

  3 POSITION_SL=0.00000

  4 POSITION_TP=0.00000

  5 POSITION_COMMISSION=0.0

  6 POSITION_SWAP=0.00

  7 POSITION_PROFIT=-0.04

ENUM_POSITION_PROPERTY_STRING Count=3

  0 POSITION_SYMBOL=EURUSD

  1 POSITION_COMMENT=

  2 POSITION_EXTERNAL_ID=

OrderCache removed: 1311792104

>>> positions: 1, orders: 0, history: 1

Спустя некоторое время закроем позицию. Поскольку у нас в коде первым проверяется кэш позиций (positions.scan), в журнал попадают изменения закрываемой позиции.

>>> OnTrade(8)
PositionCache changed: 1311792104
POSITION_PRICE_CURRENT: 1.09214 -> 1.09222
POSITION_PROFIT: -0.04 -> 0.04

Далее в этом же вызове OnTrade засекается появление ордера на закрытие и его моментальный перенос в историю (опять же за счет его быстрой параллельной обработки терминалом).

OrderCache added: 1311796883

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

  0 ORDER_TIME_SETUP=2022.04.11 12:39:55

  1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

  2 ORDER_TIME_DONE=1970.01.01 00:00:00

  3 ORDER_TYPE=ORDER_TYPE_SELL

  4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK

  5 ORDER_TYPE_TIME=ORDER_TIME_GTC

  6 ORDER_STATE=ORDER_STATE_STARTED

  7 ORDER_MAGIC=0

  8 ORDER_POSITION_ID=1311792104

  9 ORDER_TIME_SETUP_MSC=2022.04.11 12:39:55'710

 10 ORDER_TIME_DONE_MSC=1970.01.01 00:00:00'000

 11 ORDER_POSITION_BY_ID=0

 12 ORDER_TICKET=1311796883

 13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

  0 ORDER_VOLUME_INITIAL=0.01

  1 ORDER_VOLUME_CURRENT=0.01

  2 ORDER_PRICE_OPEN=1.09222

  3 ORDER_PRICE_CURRENT=1.09222

  4 ORDER_PRICE_STOPLIMIT=0.0

  5 ORDER_SL=0.0

  6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

  0 ORDER_SYMBOL=EURUSD

  1 ORDER_COMMENT=

  2 ORDER_EXTERNAL_ID=

HistoryOrderCache added: 1311796883

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

  0 ORDER_TIME_SETUP=2022.04.11 12:39:55

  1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

  2 ORDER_TIME_DONE=2022.04.11 12:39:55

  3 ORDER_TYPE=ORDER_TYPE_SELL

  4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK

  5 ORDER_TYPE_TIME=ORDER_TIME_GTC

  6 ORDER_STATE=ORDER_STATE_FILLED

  7 ORDER_MAGIC=0

  8 ORDER_POSITION_ID=1311792104

  9 ORDER_TIME_SETUP_MSC=2022.04.11 12:39:55'710

 10 ORDER_TIME_DONE_MSC=2022.04.11 12:39:55'711

 11 ORDER_POSITION_BY_ID=0

 12 ORDER_TICKET=1311796883

 13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

  0 ORDER_VOLUME_INITIAL=0.01

  1 ORDER_VOLUME_CURRENT=0.0

  2 ORDER_PRICE_OPEN=1.09222

  3 ORDER_PRICE_CURRENT=1.09222

  4 ORDER_PRICE_STOPLIMIT=0.0

  5 ORDER_SL=0.0

  6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

  0 ORDER_SYMBOL=EURUSD

  1 ORDER_COMMENT=

  2 ORDER_EXTERNAL_ID=

>>> positions: 1, orders: 1, history: 2

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

Но уже в следующем событии OnTrade мы видим, что позиция закрыта и рыночный ордер исчез.

>>> OnTrade(9)
PositionCache removed: 1311792104
OrderCache removed: 1311796883
>>> positions: 0, orders: 0, history: 2

Если мониторить кэши на каждом тике (или раз в секунду, но не только по событиям OnTrade), мы увидим изменения свойств ORDER_PRICE_CURRENT и POSITION_PRICE_CURRENT "на лету". Также будет меняться POSITION_PROFIT.

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