Полное и частичное закрытие позиции

Технически закрытие позиции можно представить, как торговую операцию, обратную по направлению к той, что использовалась для открытия. Например, для выхода из покупки нужно осуществить продажу (ORDER_TYPE_SELL в поле type), а для выхода из продажи — покупку (ORDER_TYPE_BUY в поле type).

Тип торговой транзакции в поле action структуры MqlTradeTransaction остается прежним — TRADE_ACTION_DEAL.

На счете с хеджингом закрываемую позицию следует указать с помощью тикета в поле position. Для счетов с неттинговым учетом достаточно указать название символа в поле symbol, поскольку на них возможна только одна позиция по символу. Однако закрывать позиции по тикету можно и здесь.

В целях унификации кода имеет смысл заполнять оба поля position и symbol вне зависимости от типа счета.

Также обязательно задать объем в поле volume. Если он равен объему позиции, она будет закрыта полностью. Однако, указав меньшее значение, существует возможность закрыть лишь часть позиции.

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

Поле

Неттинг

Хедж

action

*

*

symbol

*

+

position

+

*

type

*

*

type_filling

*

*

volume

*

*

price

*'

*'

deviation

±

±

magic

+

+

comment

+

+

Поле price помечено звездочкой с риской, потому что оно является обязательным только для символов с режимом исполнения по запросу (Request) и немедленным (Instant), а для биржевого (Exchange) и рыночного (Market) исполнения цена в структуре не учитывается.

По аналогичной причине поле deviation помечено знаком '±' — оно имеет эффект только для режимов Instant и Request.

Для упрощения программной реализации закрытия позиции вернемся к нашей расширенной структуре MqlTradeRequestSync в файле MqlTradeSync.mqh. Метод закрытия позиции по тикету имеет следующий код.

struct MqlTradeRequestSyncpublic MqlTradeRequest
{
   double partial// объем после частичного закрытия
   ...
   bool close(const ulong ticketconst double lot = 0)
   {
      if(!PositionSelectByTicket(ticket)) return false;
      
      position = ticket;
      symbol = PositionGetString(POSITION_SYMBOL);
      type = (ENUM_ORDER_TYPE)(PositionGetInteger(POSITION_TYPE) ^ 1);
      price = 0
      ...

Здесь мы первым делом проверяем существование позиции, вызвав функцию PositionSelectByTicket. Дополнительно этот вызов делает позицию выбранной в торговом окружении терминала, что позволяет читать её свойства последующими функциями. В частности, мы узнаем символ позиции из свойства POSITION_SYMBOL и "переворачиваем" её тип из POSITION_TYPE на обратный, чтобы получить нужный тип ордера.

Напомним, что типы позиций в перечислении ENUM_POSITION_TYPE - это POSITION_TYPE_BUY (значение 0) и POSITION_TYPE_SELL (значение 1). В перечислении типов ордеров ENUM_ORDER_TYPE точно такие же значения занимают рыночные операции: ORDER_TYPE_BUY и ORDER_TYPE_SELL. Именно поэтому мы можем приводить первое перечисление ко второму, а для получения обратного направления торговли достаточно переключить нулевой бит с помощью операции исключающего ИЛИ ('^'): из 0 получим 1, а из 1 — 0.

Обнуление поля price означает автоматический выбор правильной текущей цены (Ask или Bid) перед отправкой запроса: это делается чуть позднее, внутри вспомогательного метода setVolumePrices, который вызывается далее по ходу алгоритма, из метода market.

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

      const double total = lot == 0 ? PositionGetDouble(POSITION_VOLUME) : lot;
      partial = PositionGetDouble(POSITION_VOLUME) - total;
      return _market(symboltotal);
   }

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

Также обратим внимание, что поскольку позиция может закрываться частично, нам пришлось добавить в структуру поле partial, куда кладется планируемый остаток объема после операции. Для полного закрытия это будет, разумеется, 0. Эта информация потребуется для дальнейшей проверки завершения операции.  

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

   bool close(const string nameconst double lot = 0)
   {
      if(!PositionSelect(name)) return false;
      return close(PositionGetInteger(POSITION_TICKET), lot);
   }

В структуре MqlTradeRequestSync у нас предусмотрен метод completed, обеспечивающий при необходимости синхронное ожидание завершения операции. Теперь нам требуется дополнить его для закрытия позиций, в ветви, где action равно TRADE_ACTION_DEAL. Различать открытие позиции и закрытие будем по нулевому значению в поле position: при открытии позиции тикета нет, при закрытии — он есть.

   bool completed()
   {
      if(action == TRADE_ACTION_DEAL)
      {
         if(position == 0)
         {
            const bool success = result.opened(timeout);
            if(successposition = result.position;
            return success;
         }
         else
         {
            result.position = position;
            result.partial = partial;
            return result.closed(timeout);
         }
      }

Для проверки фактического закрытия позиции в структуру MqlTradeResultSync добавлен метод closed. Перед его вызовом мы записываем тикет позиции в поле result.position, чтобы структура результата могла отслеживать момент, когда соответствующий тикет пропадет из торгового окружения терминала, или когда объем сравняется с result.partial в случае частичного закрытия.

А вот и сам метод closed. Он построен по уже известному принципу: сначала проверка успешности кода возврата сервера, а затем ожидание с помощью метода wait некоторого условия.

struct MqlTradeResultSyncpublic MqlTradeResult
{
   ...
   bool closed(const ulong msc = 1000)
   {
      if(retcode != TRADE_RETCODE_DONE)
      {
         return false;
      }
      if(!wait(positionRemovedmsc))
      {
         Print("Position removal timeout: P=" + (string)position);
      }
      
      return true;
   }

В данном случае для проверки условия исчезновения позиции нам пришлось реализовать новую функцию positionRemoved.

   static bool positionRemoved(MqlTradeResultSync &ref)
   {
      if(ref.partial)
      {
         return PositionSelectByTicket(ref.position)
            && TU::Equal(PositionGetDouble(POSITION_VOLUME), ref.partial);
      }
      return !PositionSelectByTicket(ref.position);
   }

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

Никаких настраиваемых параметров у эксперта не будет: только величина проскальзывания (Deviation) и уникальный номер (Magic). Неявными параметрами являются таймфрейм и рабочий символ графика.

Для отслеживания наличия уже открытой позиции воспользуемся функцией GetMyPosition из предыдущего примера TradeTrailing.mq5: напомним, она осуществляет поиск среди позиций по символу и номеру эксперта, и возвращает логический признак true, если подходящая позиция найдена.

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

Новая функция, реализующая закрытие позиции, — это ClosePosition. Поскольку заголовочный файл MqlTradeSync.mqh взял на себя всю рутину, здесь нам остается, по большому счету, только вызвать метод request.close(ticket) для переданного тикета позиции и дождаться завершения удаления посредством request.completed().

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

ulong LastErrorCode = 0;
   
ulong ClosePosition(const ulong ticket)
{
   MqlTradeRequestSync request// пустая структура
   
   // опциональные поля заполняем напрямую в структуре
   request.magic = Magic;
   request.deviation = Deviation;
   
   ResetLastError();
   // выполняем закрытие и ждем подтверждения
   if(request.close(ticket) && request.completed())
   {
      Print("OK Close Order/Deal/Position");
   }
   else // в случае проблем выводим диагностику
   {
      Print(TU::StringOf(request));
      Print(TU::StringOf(request.result));
      LastErrorCode = request.result.retcode;
      return 0// ошибка, код для анализа в LastErrorCode
   }
   
   return request.position// ненулевое значение - успех
}

Можно задаться вопросом, почему бы функции ClosePosition не возвращать 0 в случае успешного удаления позиции, а иначе — непосредственно код ошибки. Этот, на первый взгляд, экономный подход, сделал бы поведение двух функций OpenPosition и ClosePosition различным: в вызывающем коде потребовалось бы вкладывать вызовы этих функций в противоположные по смыслу логические выражения, а это вносило бы путаницу. Кроме того, глобальная переменная LastErrorCode нам в любом случае понадобилась для добавления информации об ошибке внутри функции OpenPosition. Да и проверка в виде if(условие) более органично интерпретируется как успех, нежели if(!условие).

Функция, формирующая торговые сигналы по вышеописанной стратегии, называется GetTradeDirection.

ENUM_ORDER_TYPE GetTradeDirection()
{
   if(iClose(_Symbol_Period1) > iClose(_Symbol_Period2)
      && iClose(_Symbol_Period2) > iClose(_Symbol_Period3))
   {
      return ORDER_TYPE_BUY// открыть длинную позицию
   }
   
   if(iClose(_Symbol_Period1) < iClose(_Symbol_Period2)
      && iClose(_Symbol_Period2) < iClose(_Symbol_Period3))
   {
      return ORDER_TYPE_SELL// открыть короткую позицию
   }
   
   return (ENUM_ORDER_TYPE)-1// закрыть
}

Функция возвращает значение типа ENUM_ORDER_TYPE, причем два стандартных элемента (ORDER_TYPE_BUY и ORDER_TYPE_SELL), как и следует ожидать, инициируют покупки и продажи, соответственно. А специальное значение -1 (отсутствующее в перечислении) будет использоваться как сигнал закрытия.

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

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

void OnTick()
{
   static datetime lastBar = 0;
   if(iTime(_Symbol_Period0) == lastBarreturn;
   lastBar = iTime(_Symbol_Period0);
   ...

Далее получаем текущий сигнал из функции GetTradeDirection.

   const ENUM_ORDER_TYPE type = GetTradeDirection();

В случае наличия позиции проверяем, получен ли сигнал на её закрытие и вызываем ClosePosition при необходимости. Если же позиции ещё нет, и есть сигнал войти в рынок, вызываем OpenPosition.

   if(GetMyPosition(_SymbolMagic))
   {
      if(type != ORDER_TYPE_BUY && type != ORDER_TYPE_SELL)
      {
         ClosePosition(PositionGetInteger(POSITION_TICKET));
      }
   }
   else if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_SELL)
   {
      OpenPosition(type);
   }
}

Для анализа ошибок потребуется заключить вызовы OpenPosition и ClosePosition в условные операторы, и предпринять некие действия для восстановления рабочего состояния программы. В простейшем случае достаточно повторить запрос на следующем тике, но делать это желательно ограниченное число раз. Поэтому заведем статические переменные со счетчиком и лимитом ошибок.

void OnTick()
{
   static int errors = 0;
   static const int maxtrials = 10// не более чем 10 попыток на бар
   
   // ожидаем появления нового бара, если не было ошибок
   static datetime lastBar = 0;
   if(iTime(_Symbol_Period0) == lastBar && errors == 0return;
   lastBar = iTime(_Symbol_Period0);
   ...

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

Подсчет ошибок делаем в условных операторах вокруг ClosePosition и OpenPosition.

   const ENUM_ORDER_TYPE type = GetTradeDirection();
   
   if(GetMyPosition(_SymbolMagic))
   {
      if(type != ORDER_TYPE_BUY && type != ORDER_TYPE_SELL)
      {
         if(!ClosePosition(PositionGetInteger(POSITION_TICKET)))
         {
            ++errors;
         }
         else
         {
            errors = 0;
         }
      }
   }
   else if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_SELL)
   {
      if(!OpenPosition(type))
      {
         ++errors;
      }
      else
      {
         errors = 0;
      }
   }
   // слишком много ошибок на бар
   if(errors >= maxtrialserrors = 0;
   // ошибка достаточно серьезная, чтобы сделать паузу
   if(IS_TANGIBLE(LastErrorCode)) errors = 0;
}

Установка переменной errors в 0 вновь включает механизм побаровой работы и прекращает попытки повторить запрос вплоть до следующего бара.

Макрос IS_TANGIBLE определен в TradeRetcode.mqh как:

#define IS_TANGIBLE(T) ((T) >= TRADE_RETCODE_ERROR)

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

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

Результаты тестирования TradeClose на XAUUSD,H1

Результаты тестирования TradeClose на XAUUSD,H1

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

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