Отбор ордеров по свойствам

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

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

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

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

enum IS // поддерживаемые условия сравнения в фильтрах
{
   EQUAL,
   GREATER,
   NOT_EQUAL,
   LESS
};
   
enum ENUM_ANY // фиктивное перечисление для приведения к нему всех перечислений
{
};
   
template<typename T,typename I,typename D,typename S>
class TradeFilter
{
protected:
   MapArray<ENUM_ANY,longlongs;
   MapArray<ENUM_ANY,doubledoubles;
   MapArray<ENUM_ANY,stringstrings;
   MapArray<ENUM_ANY,ISconditions;
   ...
   
   template<typename V>
   static bool equal(const V v1const V v2);
   
   template<typename V>
   static bool greater(const V v1const V v2);
   
   template<typename V>
   bool match(const T &mconst MapArray<ENUM_ANY,V> &dataconst;
   
public:
   // методы добавления условий в фильтр
   TradeFilter *let(const I propertyconst long valueconst IS cmp = EQUAL);
   TradeFilter *let(const D propertyconst double valueconst IS cmp = EQUAL);
   TradeFilter *let(const S propertyconst string valueconst IS cmp = EQUAL);
   // методы получения в массивы подходящих по фильтру записей   
   template<typename E,typename V>
   bool select(const E propertyulong &tickets[], V &data[],
      const bool sort = falseconst;
   template<typename E,typename V>
   bool select(const E &property[], ulong &tickets[], V &data[][],
      const bool sort = falseconst
   bool select(ulong &tickets[]) const;
   ...
}

Параметры шаблона I, D и S — это перечисления для групп свойств трех основных типов (целочисленные, вещественные, строковые): для ордеров они были описаны в предыдущих разделах, поэтому для наглядности вы можете представлять, что I=ENUM_ORDER_PROPERTY_INTEGER, D=ENUM_ORDER_PROPERTY_DOUBLE, S=ENUM_ORDER_PROPERTY_STRING.

Тип T предназначен для указания класса монитора. В данный момент у нас готов только один монитор — OrderMonitor. Позднее мы реализуем DealMonitor и PositionMonitor.

Ранее, в классе SymbolFilter мы обходились без параметров шаблона, потому что для символов неизменно известны все типы перечислений свойств и существует единственный класс SymbolMonitor.

Напомним структуру класса-фильтра. Группа методов let позволяет зарегистрировать в фильтре сочетание пар "свойство=значение", по которым будет затем осуществляться отбор объектов в методах select. Идентификатор свойства указывается в параметре property, а значение — в параметре value.

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

Сочетание свойства и значения может проверяться не только на равенство (EQUAL), но и операции больше/меньше (GREATER/LESS). Для строковых свойств допустимо указывать шаблон поиска с символом "*", обозначающим любую последовательность знаков (например, "*[tp]*" для свойства ORDER_COMMENT совпадет со всеми комментариями, в которых с любом месте встретится "[tp]", хотя это только демонстрация возможности и для поиска ордеров в результате сработавшего Take Profit следует анализировать ORDER_REASON).

Поскольку алгоритм требует организации цикла с перебором всех объектов, а объекты могут быть разного типа (пока это ордера, но затем появится поддержка сделок и позиций), в классе TradeFilter потребовалось описать два абстрактных метода: total и get:

   virtual int total() const = 0;
   virtual ulong get(const int iconst = 0;

Первый возвращает количество объектов, а второй — тикет объекта по его номеру. Это должно напомнить вам пару функций OrdersTotal и OrderGetTicket. И действительно, они применены в конкретных реализациях методов для фильтра ордеров.

Ниже показан класс OrderFilter (OrderFilter.mqh) целиком.

#include <MQL5Book/OrderMonitor.mqh>
#include <MQL5Book/TradeFilter.mqh>
   
class OrderFilterpublic TradeFilter<OrderMonitor,
   ENUM_ORDER_PROPERTY_INTEGER,
   ENUM_ORDER_PROPERTY_DOUBLE,
   ENUM_ORDER_PROPERTY_STRING>
{
protected:
   virtual int total() const override
   {
      return OrdersTotal();
   }
   virtual ulong get(const int iconst override
   {
      return OrderGetTicket(i);
   }
};

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

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

   OrderFilter filter;
   ulong tickets[];
   
   // задаем условие на ордера по текущему символу и нашему "магику"
   filter.let(ORDER_SYMBOL_Symbol).let(ORDER_MAGICMagic);
   // отбираем подходящие тикеты в массив
   if(filter.select(tickets))
   {
      ArrayPrint(tickets);
   }

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

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

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

Сетка отложенных ордеров на 4 уровня с шагом 200 пунктов

Сетка отложенных ордеров на 4 уровня с шагом 200 пунктов

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

На каждом верхнем уровне ставится лимитный ордер на продажу и stoplimit-ордер на покупку с ценой будущего лимитного ордера на один уровень ниже. На каждом нижнем уровне ставится лимитный ордер по покупку и stoplimit-ордер на продажу с ценой будущего лимитного ордера на один уровень выше.

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

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

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

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

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

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

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

Дело в том, что лимитные и стоп-лимитные ордера, стоящие на каждом уровне относятся к противоположным типам (buy/sell), а потому активируются разными типами цен.

Получается, что если рынок двигался вверх к очередному уровню в верхней половине сетки, Ask-цена может задеть уровень и активирует стоп-лимитный ордер на покупку, но Bid-цена не дойдет до уровня, и лимитный ордер на продажу останется как есть (не превратится в позицию). В нижней половине сетки — при движении рынка вниз — ситуация зеркальная. Любого уровня первой касается Bid-цена и активирует стоп-лимитный ордер на продажу, и только при дальнейшем снижении до уровня также доходит Ask-цена. Если движения не произойдет, лимитный ордер на покупку останется как есть.

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

Исходный код прилагается в файле PendingOrderGrid1.mq5. Во входных параметрах можно задать объем каждой сделки Volume (по умолчанию, если оставить его равным 0, берется минимальный лот символа графика), количество уровней сетки GridSize (должно быть четным) и шаг между уровнями в пунктах GridStep. Начальное и конечное время внутрисуточного отрезка, на котором разрешена работа стратегии, указывается в параметрах StartTime и StopTime: в обоих важно только время.

#include <MQL5Book/MqlTradeSync.mqh>
#include <MQL5Book/OrderFilter.mqh>
#include <MQL5Book/MapArray.mqh>
   
input double Volume;                                       // Volume (0 = minimal lot)
input uint GridSize = 6;                                   // GridSize (even number of price levels)
input uint GridStep = 200;                                 // GridStep (points)
input ENUM_ORDER_TYPE_TIME Expiration = ORDER_TIME_GTC;
input ENUM_ORDER_TYPE_FILLING Filling = ORDER_FILLING_FOK;
input datetime StartTime = D'1970.01.01 00:00:00';         // StartTime (hh:mm:ss)
input datetime StopTime = D'1970.01.01 09:00:00';          // StopTime (hh:mm:ss)
input ulong Magic = 1234567890;

Отрезок рабочего времени может быть как внутри суток (StartTime < StopTime), так и пересекать границу суток (StartTime > StopTime), например, с 22:00 до 09:00. Если два времени равны, предполагается круглосуточная торговля.

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

const ulong DAYLONG = 60 * 60 * 24// размер суток в секундах
   
struct MqlTradeRequestSyncLogpublic MqlTradeRequestSync
{
   MqlTradeRequestSyncLog()
   {
      magic = Magic;
      type_filling = Filling;
      type_time = Expiration;
      if(Expiration == ORDER_TIME_SPECIFIED)
      {
         expiration = (datetime)(TimeCurrent() / DAYLONG * DAYLONG
            + StopTime % DAYLONG);
         if(StartTime > StopTime)
         {
            expiration = (datetime)(expiration + DAYLONG);
         }
      }
   }
   ~MqlTradeRequestSyncLog()
   {
      Print(TU::StringOf(this));
      Print(TU::StringOf(this.result));
   }
};

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

В OnInit выполним некоторые проверки на корректность входных переменных, в частности, на четный размер сетки.

int OnInit()
{
   if(GridSize < 2 || !!(GridSize % 2))
   {
      Alert("GridSize should be 2, 4, 6+ (even number)");
      return INIT_FAILED;
   }
   return INIT_SUCCEEDED;
}

Основной точкой входа алгоритма является обработчик OnTick. В нем мы опустим для краткости тот же механизм обработки ошибок на базе TRADE_RETCODE_SEVERITY, что и в примере PendingOrderModify.mq5.

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

void OnTick()
{
   static datetime lastBar = 0;
   if(iTime(_Symbol_Period0) == lastBarreturn;
   uint retcode = 0;
   
   ... // основной алгоритм (см. далее)
   
   const TRADE_RETCODE_SEVERITY severity = TradeCodeSeverity(retcode);
   if(severity < SEVERITY_RETRY)
   {
      lastBar = iTime(_Symbol_Period0);
   }
}

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

   ...
   bool tradeScheduled = true;
   
   if(StartTime != StopTime)
   {
      const ulong now = TimeCurrent() % DAYLONG;
      
      if(StartTime < StopTime)
      {
         tradeScheduled = now >= StartTime && now < StopTime;
      }
      else
      {
         tradeScheduled = now >= StartTime || now < StopTime;
      }
   }
   ...

При разрешенной торговле сначала проверим, есть ли уже сеть ордеров, с помощью функции CheckGrid. Если сети нет, функция вернет константу GRID_EMPTY и нам следует создать сеть с помощью вызова SetupGrid. Если сеть уже построена, имеет смысл проверить, нет ли встречных позиций для закрытия: этим занимается функция CompactPositions.

   if(tradeScheduled)
   {
      retcode = CheckGrid();
      
      if(retcode == GRID_EMPTY)
      {
         retcode = SetupGrid();
      }
      else
      {
         retcode = CompactPositions();
      }
   }
   ...

Как только торговый период заканчивается, необходимо удалить ордера и закрыть все позиции (если есть) — это поручено, соответственно, функции RemoveOrders и всё той же CompactPositions, но с логическим флагом (true): этот единственный, необязательный аргумент предписывает после встречного закрытия применить простое закрытие для оставшихся позиций.

   else
   {
      retcode = CompactPositions(true);
      if(!retcoderetcode = RemoveOrders();
   }

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

#define GRID_OK    +1
#define GRID_EMPTY  0

Теперь разберем функции по отдельности.

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

uint CheckGrid()
{
   OrderFilter filter;
   ulong tickets[];
   
   filter.let(ORDER_SYMBOL_Symbol).let(ORDER_MAGICMagic)
      .let(ORDER_TYPEORDER_TYPE_SELLIS::GREATER)
      .select(tickets);
   const int n = ArraySize(tickets);
   if(!nreturn GRID_EMPTY;
   ...

Для анализа полноты сетки применяется уже знакомый класс MapArray, хранящий пары "ключ=значение". В данном случае в качестве ключа выступает уровень (цена, переведенная в пункты), а в качестве значения — битовая маска (суперпозиция) типов ордеров на данном уровне. Попутно в переменных limits и stops подсчитываются количества, соответственно, лимитных и стоп-лимитных ордеров.

   // ценовые уровни => маски типов существующих там ордеров
   MapArray<ulong,uintlevels;
 
   const double point = SymbolInfoDouble(_SymbolSYMBOL_POINT);
   int limits = 0;
   int stops = 0;
   
   for(int i = 0i < n; ++i)
   {
      if(OrderSelect(tickets[i]))
      {
         const ulong level = (ulong)MathRound(OrderGetDouble(ORDER_PRICE_OPEN) / point);
         const ulong type = OrderGetInteger(ORDER_TYPE);
         if(type == ORDER_TYPE_BUY_LIMIT || type == ORDER_TYPE_SELL_LIMIT)
         {
            ++limits;
            levels.put(levellevels[level] | (1 << type));
         }
         else if(type == ORDER_TYPE_BUY_STOP_LIMIT 
            || type == ORDER_TYPE_SELL_STOP_LIMIT)
         {
            ++stops;
            levels.put(levellevels[level] | (1 << type));
         }
      }
   }
   ...

Если количество ордеров каждого типа совпадает и равно заданному размеру сетки, значит всё в порядке.

   if(limits == stops)
   {
      if(limits == GridSizereturn GRID_OK// полная сетка
      
      Alert("Error: Order number does not match requested");
      return TRADE_RETCODE_ERROR;
   }
   ...

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

   if(limits > stops)
   {
      const uint stopmask = 
         (1 << ORDER_TYPE_BUY_STOP_LIMIT) | (1 << ORDER_TYPE_SELL_STOP_LIMIT);
      for(int i = 0i < levels.getSize(); ++i)
      {
         if((levels[i] & stopmask) == 0// на этом уровне нет стоп-лимитного ордера
         {
            // направление лимитного требуется для установки обратного стоп-лимитного
            const bool buyLimit = (levels[i] & (1 << ORDER_TYPE_BUY_LIMIT));
            // здесь опущены проверки "лишних" ордеров из-за спреда (см. исходный код)
            ...
            // создаем стоп-лимитный ордер нужного направления
            const uint retcode = RepairGridLevel(levels.getKey(i), pointbuyLimit);
            if(TradeCodeSeverity(retcode) > SEVERITY_NORMAL)
            {
               return retcode;
            }
         }
      }
      return GRID_OK;
   }
   ...

Ситуация, когда количество стоп-лимитных ордеров больше, чем лимитных, трактуется как ошибка (вероятно, сервер по какой-то причине пропустил цену).

   Alert("Error: Orphaned Stop-Limit orders found");
   return TRADE_RETCODE_ERROR;
}

Функция RepairGridLevel выполняет следующие действия.

uint RepairGridLevel(const ulong levelconst double pointconst bool buyLimit)
{
   const double price = level * point;
   const double volume = Volume == 0 ?
      SymbolInfoDouble(_SymbolSYMBOL_VOLUME_MIN) : Volume;
   
   MqlTradeRequestSyncLog request;
   
   request.comment = "repair";
   
   // если существует непарный buy-limit, устанавливаем к нему sell-stop-limit
   // если существует непарный sell-limit, устанавливаем к нему buy-stop-limit
   const ulong order = (buyLimit ?
      request.sellStopLimit(volumepriceprice + GridStep * point) :
      request.buyStopLimit(volumepriceprice - GridStep * point));
   const bool result = (order != 0) && request.completed();
   if(!resultAlert("RepairGridLevel failed");
   return request.result.retcode;
}

Обратите внимание, что нам не требуется фактически заполнять структуру (кроме комментария, который можно сделать более информативным при необходимости), поскольку часть полей заполняется автоматически конструктором, а объем и цену мы передаем непосредственно в метод sellStopLimit или buyStopLimit.

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

uint SetupGrid()
{
   const double current = SymbolInfoDouble(_SymbolSYMBOL_BID);
   const double point = SymbolInfoDouble(_SymbolSYMBOL_POINT);
   const double volume = Volume == 0 ?
      SymbolInfoDouble(_SymbolSYMBOL_VOLUME_MIN) : Volume;
   // центральная цена диапазона с округлением до шага,
   // от неё вверх и вниз отложим уровни   
   const double base = ((ulong)MathRound(current / point / GridStep) * GridStep)
      * point;
   const string comment = "G[" + DoubleToString(base,
      (int)SymbolInfoInteger(_SymbolSYMBOL_DIGITS)) + "]";
   const static string message = "SetupGrid failed: ";
   MqlTradeRequestSyncLog request[][2]; // лимитный и стоп-лимитный - одна пара
   ArrayResize(requestGridSize);      // 2 отложенных ордера на каждый уровень

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

   for(int i = 0i < (int)GridSize / 2; ++i)
   {
      const int k = i + 1;
      
      // нижняя половина сетки
      request[i][0].comment = comment;
      request[i][1].comment = comment;
      
      if(!(request[i][0].buyLimit(volumebase - k * GridStep * point)))
      {
         Alert(message + (string)i + "/BL");
         return request[i][0].result.retcode;
      }
      if(!(request[i][1].sellStopLimit(volumebase - k * GridStep * point,
         base - (k - 1) * GridStep * point)))
      {
         Alert(message + (string)i + "/SSL");
         return request[i][1].result.retcode;
      }
      
      // верхняя половина сетки
      const int m = i + (int)GridSize / 2;
      
      request[m][0].comment = comment;
      request[m][1].comment = comment;
      
      if(!(request[m][0].sellLimit(volumebase + k * GridStep * point)))
      {
         Alert(message + (string)m + "/SL");
         return request[m][0].result.retcode;
      }
      if(!(request[m][1].buyStopLimit(volumebase + k * GridStep * point,
         base + (k - 1) * GridStep * point)))
      {
         Alert(message + (string)m + "/BSL");
         return request[m][1].result.retcode;
      }
   }

Затем проверяем готовность.

   for(int i = 0i < (int)GridSize; ++i)
   {
      for(int j = 0j < 2; ++j)
      {
         if(!request[i][j].completed())
         {
            Alert(message + (string)i + "/" + (string)j + " post-check");
            return request[i][j].result.retcode;
         }
      }
   }
   return GRID_OK;
}

Хотя проверка (вызов completed) разнесена с отправкой приказов, наша структура по прежнему использует внутри синхронную форму OrderSend. На самом деле для ускорения отправки пакета приказов (как в нашем сеточном эксперте) лучше использовать асинхронную версию OrderSendAsync. Но тогда статус исполнения ордеров следует инициировать из обработчика события OnTradeTransaction. А его мы изучим позднее.

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

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

uint CompactPositions(const bool cleanup = false)

Параметр cleanup, равный по умолчанию false, означает штатную "подчистку" позиций внутри торгового периода, то есть закрытие встречных позиций (если они есть). Значение cleanup=true используется для принудительного закрытия всех позиций в конце торгового периода.

Функция заполняет массивы ticketsLong и ticketsShort тикетами длинных и коротких позиций с помощью вспомогательной функции GetMyPositions. Мы уже использовали последнюю в примере TradeCloseBy.mq5 в разделе про полное и частичное закрытие встречных позиций. Там же была показана и функция CloseByPosition. В новом эксперте она претерпела минимальные изменения: возвращает код с сервера вместо логического признака успеха или ошибки.

uint CompactPositions(const bool cleanup = false)
{
   uint retcode = 0;
   ulong ticketsLong[], ticketsShort[];
   const int n = GetMyPositions(_SymbolMagicticketsLongticketsShort);
   if(n > 0)
   {
      Print("CompactPositions, pairs: "n);
      for(int i = 0i < n; ++i)
      {
         retcode = CloseByPosition(ticketsShort[i], ticketsLong[i]);
         if(retcodereturn retcode;
      }
   }
   ...

Вторая часть CompactPositions работает только при cleanup=true. Она далека от совершенства и будет скоро переделана.

   if(cleanup)
   {
      if(ArraySize(ticketsLong) > ArraySize(ticketsShort))
      {
         retcode = CloseAllPositions(ticketsLong, ArraySize(ticketsShort));
      }
      else if(ArraySize(ticketsLong) < ArraySize(ticketsShort))
      {
         retcode = CloseAllPositions(ticketsShort, ArraySize(ticketsLong));
      }
   }
   
   return retcode;
}

Для всех найденных оставшихся позиций выполняется обычное закрытие вызовом CloseAllPositions.

uint CloseAllPositions(const ulong &tickets[], const int start = 0)
{
   const int n = ArraySize(tickets);
   Print("CloseAllPositions "n);
   for(int i = start; i < n; ++i)
   {
      MqlTradeRequestSyncLog request;
      request.comment = "close down " + (string)(i + 1 - start)
         + " of " + (string)(n - start);
      if(!(request.close(tickets[i]) && request.completed()))
      {
         Print("Error: position is not closed "tickets[i]);
         return request.result.retcode;
      }
   }
   return 0// success
}

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

uint RemoveOrders()
{
   OrderFilter filter;
   ulong tickets[];
   filter.let(ORDER_SYMBOL_Symbol).let(ORDER_MAGICMagic)
      .select(tickets);
   const int n = ArraySize(tickets);
   for(int i = 0i < n; ++i)
   {
      MqlTradeRequestSyncLog request;
      request.comment = "removal " + (string)(i + 1) + " of " + (string)n;
      if(!(request.remove(tickets[i]) && request.completed()))
      {
         Print("Error: order is not removed "tickets[i]);
         return request.result.retcode;
      }
   }
   return 0;
}

Проверим, как эксперт работает в тестере с настройками по умолчанию (торговый период с 00:00 до 09:00). Ниже представлен скриншот для запуска на EURUSD,H1.

Сеточная стратегия PendingOrderGrid1.mq5 в тестере

Сеточная стратегия PendingOrderGrid1.mq5 в тестере

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

buy stop limit 0.01 EURUSD at 1.14200 (1.14000) (1.13923 / 1.13923)
TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, »
   » @ 1.14200, X=1.14000, ORDER_TIME_GTC, M=1234567890, repair
DONE, #=159, V=0.01, Bid=1.13923, Ask=1.13923, Request executed, Req=287
CompactPositions, pairs: 1
close position #152 sell 0.01 EURUSD by position #153 buy 0.01 EURUSD (1.13923 / 1.13923)
deal #18 buy 0.01 EURUSD at 1.13996 done (based on order #160)
deal #19 sell 0.01 EURUSD at 1.14202 done (based on order #160)
Positions collapse initiated
OK CloseBy Order/Deal/Position
TRADE_ACTION_CLOSE_BY, EURUSD, ORDER_TYPE_BUY, ORDER_FILLING_FOK, P=152, b=153, »
   » M=1234567890, compacting
DONE, D=18, #=160, Request executed, Req=288

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