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

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

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

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

long PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER property)

bool PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER property, long &value)

В случае неудачного выполнения функция возвращает 0 или false.

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

double PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE property)

bool PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE property, double &value)

Наконец, строковые свойства возвращает функция PositionGetString.

string PositionGetString(ENUM_POSITION_PROPERTY_STRING property)

bool PositionGetString(ENUM_POSITION_PROPERTY_STRING property, string &value)

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

Для чтения свойств позиций у нас уже готов абстрактный интерфейс MonitorInterface (TradeBaseMonitor.mqh) — мы его использовали для написания монитора ордеров. Теперь будет легко реализовать аналог и для позиций. Результат прилагается в файле PositionMonitor.mqh.

Класс PositionMonitorInterface наследуется от MonitorInterface с назначением шаблонным типам I, D, S рассмотренных ENUM_POSITION_PROPERTY-перечислений и переопределяет пару методов stringify с учетом специфики свойств позиций.

class PositionMonitorInterface:
   public MonitorInterface<ENUM_POSITION_PROPERTY_INTEGER,
   ENUM_POSITION_PROPERTY_DOUBLE,ENUM_POSITION_PROPERTY_STRING>
{
public:
   virtual string stringify(const long v,
      const ENUM_POSITION_PROPERTY_INTEGER propertyconst override
   {
      switch(property)
      {
         case POSITION_TYPE:
            return enumstr<ENUM_POSITION_TYPE>(v);
         case POSITION_REASON:
            return enumstr<ENUM_POSITION_REASON>(v);
         
         case POSITION_TIME:
         case POSITION_TIME_UPDATE:
            return TimeToString(vTIME_DATE TIME_SECONDS);
         
         case POSITION_TIME_MSC:
         case POSITION_TIME_UPDATE_MSC:
            return STR_TIME_MSC(v);
      }
      
      return (string)v;
   }
   
   virtual string stringify(const ENUM_POSITION_PROPERTY_DOUBLE property,
      const string format = NULLconst override
   {
      if(format == NULL &&
         (property == POSITION_PRICE_OPEN || property == POSITION_PRICE_CURRENT
         || property == POSITION_SL || property == POSITION_TP))
      {
         const int digits = (int)SymbolInfoInteger(PositionGetString(POSITION_SYMBOL),
            SYMBOL_DIGITS);
         return DoubleToString(PositionGetDouble(property), digits);
      }
      return MonitorInterface<ENUM_POSITION_PROPERTY_INTEGER,
         ENUM_POSITION_PROPERTY_DOUBLE,ENUM_POSITION_PROPERTY_STRING>
         ::stringify(propertyformat);
   }

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

class PositionMonitorpublic PositionMonitorInterface
{
public:
   const ulong ticket;
   PositionMonitor(const ulong t): ticket(t)
   {
      if(!PositionSelectByTicket(ticket))
      {
         PrintFormat("Error: PositionSelectByTicket(%lld) failed: %s",
            ticketE2S(_LastError));
      }
      else
      {
         ready = true;
      }
   }
   
   virtual long get(const ENUM_POSITION_PROPERTY_INTEGER propertyconst override
   {
      return PositionGetInteger(property);
   }
   
   virtual double get(const ENUM_POSITION_PROPERTY_DOUBLE propertyconst override
   {
      return PositionGetDouble(property);
   }
   
   virtual string get(const ENUM_POSITION_PROPERTY_STRING propertyconst override
   {
      return PositionGetString(property);
   }
   ...
};

Простой скрипт позволит вывести в журнал все характеристики первой позиции (если хотя бы одна имеется).

void OnStart()
{
   PositionMonitor pm(PositionGetTicket(0));
   pm.print();
}

В журнале мы должны получить что-то вроде этого.

MonitorInterface<ENUM_POSITION_PROPERTY_INTEGER, »
   » ENUM_POSITION_PROPERTY_DOUBLE,ENUM_POSITION_PROPERTY_STRING>
ENUM_POSITION_PROPERTY_INTEGER Count=9
  0 POSITION_TIME=2022.03.24 23:09:45
  1 POSITION_TYPE=POSITION_TYPE_BUY
  2 POSITION_MAGIC=0
  3 POSITION_IDENTIFIER=1291755067
  4 POSITION_TIME_MSC=2022.03.24 23:09:45'261
  5 POSITION_TIME_UPDATE=2022.03.24 23:09:45
  6 POSITION_TIME_UPDATE_MSC=2022.03.24 23:09:45'261
  7 POSITION_TICKET=1291755067
  8 POSITION_REASON=POSITION_REASON_EXPERT
ENUM_POSITION_PROPERTY_DOUBLE Count=8
  0 POSITION_VOLUME=0.01
  1 POSITION_PRICE_OPEN=1.09977
  2 POSITION_PRICE_CURRENT=1.09965
  3 POSITION_SL=0.00000
  4 POSITION_TP=1.10500
  5 POSITION_COMMISSION=0.0
  6 POSITION_SWAP=0.0
  7 POSITION_PROFIT=-0.12
ENUM_POSITION_PROPERTY_STRING Count=3
  0 POSITION_SYMBOL=EURUSD
  1 POSITION_COMMENT=
  2 POSITION_EXTERNAL_ID=

Если открытых позиций в данный момент нет, увидим сообщение об ошибке.

Error: PositionSelectByTicket(0) failed: TRADE_POSITION_NOT_FOUND

Однако монитор полезен не только и не столько выводом свойств в журнал. На основе PositionMonitor создадим класс для выборки позиций по условиям, аналогичный тому, что мы сделали для ордеров (OrderFilter). Конечной целью является усовершенствование нашего эксперта-сеточника.

Благодаря ООП, создание класса нового фильтра практически не требует усилий. Ниже приведен исходный код целиком (файл PositionFilter.mqh).

class PositionFilterpublic TradeFilter<PositionMonitor,
   ENUM_POSITION_PROPERTY_INTEGER,
   ENUM_POSITION_PROPERTY_DOUBLE,
   ENUM_POSITION_PROPERTY_STRING>
{
protected:
   virtual int total() const override
   {
      return PositionsTotal();
   }
   virtual ulong get(const int iconst override
   {
      return PositionGetTicket(i);
   }
};

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

input ulong Magic;
   
void OnStart()
{
   PositionFilter filter;
   
   ENUM_POSITION_PROPERTY_DOUBLE properties[] =
      {POSITION_PROFITPOSITION_VOLUME};
   
   double profits[][2];
   ulong tickets[];
   string symbols[];
   
   filter.let(POSITION_MAGICMagic).select(propertiesticketsprofits);
   filter.select(POSITION_SYMBOLticketssymbols);
   
   for(int i = 0i < ArraySize(symbols); ++i)
   {
      PrintFormat("%s[%lld]=%f",
         symbols[i], tickets[i], profits[i][0] / profits[i][1]);
   }
}

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

Сами кортежи описаны в файле Tuples.mqh. Все структуры в нем имеют название TupleN<T1,...>, где N — число от 2 до 8, и оно соответствует количеству параметров шаблона (типов Ti). Например, Tuple2:

template<typename T1,typename T2>
struct Tuple2
{
   T1 _1;
   T2 _2;
   
   static int size() { return 2; };
   
   // M – класс монитора ордеров, позиций, сделок, любой MonitorInterface<>
   template<typename M>
   void assign(const int &properties[], M &m)
   {
      if(ArraySize(properties) != size()) return;
      _1 = m.get(properties[0], _1);
      _2 = m.get(properties[1], _2);
   }
};

В классе TradeFilter (TradeFilter.mqh) добавим вариант функции select с кортежами.

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

Массив кортежей можно опционально сортировать по первому полю _1, но вспомогательный метод sortTuple оставлен для самостоятельного изучения.

Благодаря кортежам можно запросить у объекта-фильтра свойства трех разных типов за один вызов select.

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

 input ulong Magic;
   
   void OnStart()
   {
      int props[] = {POSITION_PROFITPOSITION_SYMBOLPOSITION_TICKET};
      Tuple3<double,string,ulongtuples[];
      PositionFilter filter;
      filter.let(POSITION_MAGICMagic).select(propstuplestrue);
      ArrayPrint(tuples);
   }

Разумеется, типы-параметры в описании массива кортежей (в данном случае, Tuple3<double,string,ulong>) должны соответствовать типам перечислений запрашиваемых свойств (POSITION_PROFIT, POSITION_SYMBOL, POSITION_TICKET).

Теперь мы можем слегка упростить сеточный эксперт (имеется в виду не просто более короткий, но и более понятный код). Новая версия имеет название PendingOrderGrid2.mq5. Изменения коснутся всех функций, связанных с управлением позициями.

Функция GetMyPositions заполняет переданный по ссылке массив кортежей types4tickets. В каждом кортеже Tuple2 предполагается хранить тип и тикет позиции. В принципе, в данном конкретном случае мы могли бы обойтись и двумерным массивом ulong вместо кортежей, потому что оба свойства — одного базового типа. Однако мы используем кортежи для демонстрации работы с ними в вызывающем коде.

#include <MQL5Book/Tuples.mqh>
#include <MQL5Book/PositionFilter.mqh>
   
int GetMyPositions(const string sconst ulong m,
   Tuple2<ulong,ulong> &types4tickets[])
{
   int props[] = {POSITION_TYPEPOSITION_TICKET};
   PositionFilter filter;
   filter.let(POSITION_SYMBOLs).let(POSITION_MAGICm)
      .select(propstypes4ticketstrue);
   return ArraySize(types4tickets);
}

Обратите внимание, что последний, третий параметр метода select равен true, что предписывает отсортировать массив по первому полю, то есть типу позиций. Таким образом, покупки у нас будут в начале, а продажи — в конце. Это потребуется для встречного закрытия.

Реинкарнация метода CompactPositions выглядит следующим образом.

uint CompactPositions(const bool cleanup = false)
{
   uint retcode = 0;
   Tuple2<ulong,ulongtypes4tickets[];
   int i = 0j = 0;
   int n = GetMyPositions(_SymbolMagictypes4tickets);
   if(n > 0)
   {
      Print("CompactPositions: "n);
      for(i = 0j = n - 1i < j; ++i, --j)
      {
         if(types4tickets[i]._1 != types4tickets[j]._1// пока типы отличаются
         {
            retcode = CloseByPosition(types4tickets[i]._2types4tickets[j]._2);
            if(retcodereturn retcode// ошибка
         }
         else
         {
            break;
         }
      }
   }
   
   if(cleanup && j < n)
   {
      retcode = CloseAllPositions(types4ticketsij + 1);
   }
   
   return retcode;
}

Функция CloseAllPositions почти не изменилась.

uint CloseAllPositions(const Tuple2<ulong,ulong> &types4tickets[],
   const int start = 0const int end = 0)
{
   const int n = end == 0 ? ArraySize(types4tickets) : end;
   Print("CloseAllPositions "n - start);
   for(int i = starti < n; ++i)
   {
      MqlTradeRequestSyncLog request;
      request.comment = "close down " + (string)(i + 1 - start)
         + " of " + (string)(n - start);
      const ulong ticket = types4tickets[i]._2;
      if(!(request.close(ticket) && request.completed()))
      {
         Print("Error: position is not closed "ticket);
         return request.result.retcode// ошибка
      }
   }
   return 0// успех
}

Вы можете сравнить работу экспертов PendingOrderGrid1.mq5 и PendingOrderGrid2.mq5 в тестере.

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