Совершение покупки или продажи

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

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

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

Как мы видели в разделе о типах торговых операций, "моментальной" покупке/продаже соответствует элемент TRADE_ACTION_DEAL в перечислении ENUM_TRADE_REQUEST_ACTIONS. Поэтому при заполнении структуры MqlTradeRequest в поле action следует записывать TRADE_ACTION_DEAL.

Направление торговли задается с помощью поля type, где должен быть один из типов ордеров: ORDER_TYPE_BUY или ORDER_TYPE_SELL.

Разумеется, для выполнения покупки или продажи требуется указать имя символа в поле symbol и его желаемый объем в поле volume.

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

Опционально программа может заполнить поля с защитными ценовыми уровнями (sl и tp), комментарий (comment) и идентификатор эксперта (magic).

Содержимое других полей устанавливается по-разному в зависимости от режима исполнения по цене для выбранного символа. В некоторых режимах некоторые поля не имеют эффекта. Например, в режимах по запросу цен (Request Execution) и немедленного исполнения (Instant Execution) поле с ценой price должно быть обязательно заполнено подходящей ценой (последний известный Ask для покупки, Bid для продажи), а поле deviation может содержать максимальное допустимое отклонение цены от заданной для успешного заключения сделки. В прочих режимах исполнения — биржевом (Exchange Execution) и рыночном (Market Execution) — эти поля игнорируются. В целях упрощения исходного кода вы можете заполнять цену и проскальзывание единообразно во всех режимах, но в двух последних вариантах цена все равно будет выбрана и подставлена торговым сервером по правилам режимов.

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

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

Поле

Request

Instant

Exchange

Market

action

*

*

*

*

symbol

*

*

*

*

volume

*

*

*

*

type

*

*

*

*

type_filling

*

*

*

*

price

*

*

 

 

sl

+

+

+

+

tp

+

+

+

+

deviation

+

+

 

 

magic

+

+

+

+

comment

+

+

+

+

При некоторых настройках сервера может быть запрещено заполнять поля с защитными уровнями sl и tp в момент открытия позиции. Как правило, это характерно для режимов биржевого исполнения или исполнения по рынку, но в MQL5 API не предусмотрено свойств для выяснения этого обстоятельства заранее. В таких случаях Stop Loss и Take Profit следует устанавливать путем модификации уже открытой позиции. Кстати говоря, такой способ может быть рекомендован и для всех режимов исполнения, поскольку только он позволяет точно отложить защитные уровни от реальной цены открытия позиции. С другой стороны, создание и настройка позиции в два хода может привести к ситуации, когда позиция открыта, а второй запрос на установку защитных уровней не удалось выполнить по тем или иным причинам.

Вне зависимости от направления торговли (покупка/продажа) Stop Loss ордер всегда выставляется как стоп-ордер (ORDER_TYPE_BUY_STOP или ORDER_TYPE_SELL_STOP), а Take Profit ордер — как лимитный (ORDER_TYPE_BUY_LIMIT или ORDER_TYPE_SELL_LIMIT). Причем, стоп-ордера всегда контролируются сервером MetaTrader 5 и только по достижении ценой указанного уровня, отправляются во внешнюю торговую систему. В отличие от этого, лимитные ордера могут выводиться напрямую во внешнюю торговую систему. В частности, это, как правило, так для биржевых инструментов.

В целях упрощения кодирования торговых операций — не только покупки и продажи, но и всех других — мы начнем с данного раздела разрабатывать классы, а точнее структуры, обеспечивающие автоматическое и корректное заполнение полей для торговых запросов, а также по-настоящему синхронное ожидание результата. Последнее особенно важно, учитывая, что функции OrderSend и OrderSendAsync возвращают управление в вызывающий код до того, как торговое действие будет полностью завершено. В частности, при покупке и продаже по рынку алгоритму, как правило, требуется "знать" не номер тикета созданного на сервере ордера, а открыта ли позиция или нет. В зависимости от этого её можно, например, модифицировать путем установки Stop Loss и Take Profit, если она открыта, или повторять попытки её открыть, если ордер был отклонен.

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

В принципе, асинхронная парадигма программирования не уступает синхронной ни в быстродействии, ни в легкости кодирования. Однако способы её реализации могут быть разными, например, на основе прямых указателей на функции обратного вызова (базовый прием в Java, JavaScript и множестве других языков) или событий (как в MQL5), что предопределяет некоторые особенности, о которых речь пойдет в разделе про OnTradeTransaction. Асинхронный режим позволяет ускорить отправку запросов за счет отложенного контроля за их исполнением. Но этот контроль все равно потребуется рано или поздно осуществить в том же потоке, поэтому средняя производительность схем одинакова.

Все новые структуры расположим в файле MqlTradeSync.mqh. Чтобы не "изобретать велосипед", возьмем в качестве отправной точки встроенные структуры MQL5 и опишем свои структуры дочерними. Например, для получения результатов запросов определим MqlTradeResultSync, производную от MqlTradeResult. Сюда будем добавлять полезные поля и методы, в частности, поле position для хранения тикета открытой позиции в результате рыночной покупки или продажи.

struct MqlTradeResultSyncpublic MqlTradeResult
{
   ulong position;
   ...
};

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

   MqlTradeResultSync()
   {
      ZeroMemory(this);
   }

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

Он основывается на специальном типе функции обратного вызова condition. Функция такого типа должна принимать параметр-структуру MqlTradeResultSync и возвращать true в случае успеха: результат операции получен.

   typedef bool (*condition)(MqlTradeResultSync &ref);

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

   bool wait(condition pconst ulong msc = 1000)
   {
      const ulong start = GetTickCount64();
      bool success;
      while(!(success = p(this)) && GetTickCount64() - start < msc);
      return success;
   }

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

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

   static bool orderExist(MqlTradeResultSync &ref)
   {
      return OrderSelect(ref.order) || HistoryOrderSelect(ref.order);
   }

Здесь применены две встроенных функции MQL5 API OrderSelect и HistoryOrderSelect: они производят поиск и логическое выделение ордера по его тикету во внутреннем торговом окружении терминала. Это, во-первых, подтверждает наличие ордера (если одна из функций вернула true), а во-вторых, позволяет читать его свойства с помощью других функций, что для нас пока не важно. Мы рассмотрим все эти функции в отдельных разделах. Две функции записаны в связке, потому что рыночный ордер может исполниться настолько быстро, что его активная фаза (попадающая в OrderSelect) сразу перетечет в историю (HistoryOrderSelect).

Обратите внимание, что метод объявлен статическим. Это связано с тем, что MQL5 не поддерживает (по крайней мере, пока) указатели на методы объектов. Если бы это было так, мы могли бы объявить метод нестатическим, а сам прототип указателя на функции обратного вызова condition сделать без параметра-ссылки на MqlTradeResultSync (поскольку все поля присутствуют внутри самого объекта this).

Для запуска механизма ожидания (после вызова OrderSend) будет достаточно написать так:

   if(wait(orderExist))
   {
      // есть ордер
   }
   else
   {
      // таймаут
   }

Разумеется, этот фрагмент должен выполняться после того, как мы получим от сервера результат со статусами TRADE_RETCODE_DONE или TRADE_RETCODE_DONE_PARTIAL, и поле order в структуре MqlTradeResultSync гарантированно содержит тикет ордера. Правда, в силу распределенности системы ордер с сервера может не сразу отобразиться в окружении терминала. Поэтому и нужно ожидание.

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

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

   static bool positionExist(MqlTradeResultSync &ref)
   {
      ulong posidticket;
      if(HistoryOrderGetInteger(ref.orderORDER_POSITION_IDposid))
      {
         // в большинстве случаев идентификатор позиции равен тикету,
         // но не всегда: в полном коде реализовано получение тикета по идентификатору,
         // для чего нет встроенных средств MQL5
         ticket = posid;
         
         if(HistorySelectByPosition(posid))
         {
            ref.position = ticket;
            ...
            return true;
         }
      }
      return false;
   }

С помощью встроенных функций HistoryOrderGetInteger и HistorySelectByPosition мы получаем идентификатор и тикет позиции на основании ордера.

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

struct MqlTradeRequestSyncpublic MqlTradeRequest
{
   MqlTradeResultSync result;
   ulong timeout;
   ...

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

В конструкторе обнулим все поля и установим символ в значение по умолчанию, если параметр опущен.

   MqlTradeRequestSync(const string s = NULLconst ulong t = 1000): timeout(t)
   {
      ZeroMemory(this);
      symbol = s == NULL ? _Symbol : s;
   }

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

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

   bool setSymbol(const string s)
   {
      if(s == NULL)
      {
         if(symbol == NULL)
         {
            Print("symbol is NULL, defaults to " + _Symbol);
            symbol = _Symbol;
            setFilling();
         }
         else
         {
            Print("new symbol is NULL, current used " + symbol);
         }
      }
      else
      {
         if(SymbolInfoDouble(sSYMBOL_POINT) == 0)
         {
            Print("incorrect symbol " + s);
            return false;
         }
         if(symbol != s)
         {
            symbol = s;
            setFilling();
         }
      }
      return true;
   }

Таким образом, изменение символа с помощью setSymbol автоматически подберет правильный режим заливки через вложенный вызов setFilling.

Метод setFilling обеспечивает автоматическое заполнение способа заливки объемов на основе свойств символа SYMBOL_FILLING_MODE и SYMBOL_TRADE_EXEMODE (см. раздел Торговые условия и режимы исполнения приказов).

private:
   void setFilling()
   {
      const int filling = (int)SymbolInfoInteger(symbolSYMBOL_FILLING_MODE);
      const bool market = SymbolInfoInteger(symbolSYMBOL_TRADE_EXEMODE)
         == SYMBOL_TRADE_EXECUTION_MARKET;
      
      // поле может быть уже заполнено
      // и совпадение битов означает допустимый режим
      if(((type_filling + 1) & filling) != 0
         || (type_filling == ORDER_FILLING_RETURN && !market)) return;
      
      if((filling & SYMBOL_FILLING_FOK) != 0)
      {
         type_filling = ORDER_FILLING_FOK;
      }
      else if((filling & SYMBOL_FILLING_IOC) != 0)
      {
         type_filling = ORDER_FILLING_IOC;
      }
      else
      {
         type_filling = ORDER_FILLING_RETURN;
      }
   }

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

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

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

Для операции покупки и продажи нам необходимы поля price и volume — оба значения следует нормализовать и проверить на допустимый диапазон. Это поручено методу setVolumePrices.

   bool setVolumePrices(const double vconst double p,
      const double stopconst double take)
   {
      TU::SymbolMetrics sm(symbol);
      volume = sm.volume(v);
      
      if(p != 0price = sm.price(p);
      else price = sm.price(TU::GetCurrentPrice(typesymbol));
      
      return setSLTP(stoptake);
   }

Если цена операции не задана (p == 0), программа автоматически возьмет актуальную цену правильного типа, в зависимости от направления, которое легко прочитать в поле type.

Хотя уровни Stop Loss и Take Profit не являются обязательными, их также следует нормализовать при наличии, поэтому они добавлены в параметры этого метода.

Аббревиатура TU уже известна нам — она обозначает пространство имен в файле TradeUtilits.mqh с массой полезных функций, в том числе для нормализации цен и объемов.

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

   bool setSLTP(const double stopconst double take)
   {
      TU::SymbolMetrics sm(symbol);
      TU::TradeDirection dir(type);
  
      if(stop != 0)
      {
         sl = sm.price(stop);
         if(!dir.worse(slprice))
         {
            PrintFormat("wrong SL (%s) against price (%s)",
               TU::StringOf(sl), TU::StringOf(price));
            return false;
         }
      }
      else
      {
         sl = 0// удаляем SL
      }
      
      if(take != 0)
      {
         tp = sm.price(take);
         if(!dir.better(tpprice))
         {
            PrintFormat("wrong TP (%s) against price (%s)",
               TU::StringOf(tp), TU::StringOf(price));
            return false;
         }
      }
      else
      {
         tp = 0// удаляем TP
      }
      return true;
   }

Помимо нормализации и присваивания величин полям sl и tp данный метод проверяет взаимное правильное расположение уровней относительно цены price. Для этой цели в пространстве TU описан класс TradeDirection.

Его конструкторы позволяют указать анализируемое направление торговли: покупки или продажи, в контексте чего легко выявить прибыльное или убыточное взаимное расположение двух цен. Благодаря классу анализ выполняется унифицированным образом, и проверки в коде сокращаются в 2 раза, так как нет необходимости раздельно обрабатывать покупки и продажи. В частности метод worse имеет два ценовых параметра p1, p2 и возвращает true, если цена p1 расположена хуже, то есть убыточно, по отношению к цене p2. Аналогичный метод better представляет обратную логику: он вернет true, если цена p1 лучше цены p2. Например, для продажи лучшая цена находится ниже, поскольку Take Profit ниже текущей цены.

TU::TradeDirection dir(ORDER_TYPE_SELL);
Print(dir.better(100200)); // true

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

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

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

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

public:
   ulong buy(const string name, const double lot, const double p = 0,
      const double stop = 0const double take = 0)
   {
      type = ORDER_TYPE_BUY;
      return _market(name, lot, pstoptake);
   }
   ulong sell(const string name, const double lot, const double p = 0,
      const double stop = 0const double take = 0)
   {
      type = ORDER_TYPE_SELL;
      return _market(name, lot, pstoptake);
   }

Как уже было сказано, для установки опциональных полей вроде deviation, comment, magic следует делать прямое присваивание перед вызовом buy/sell. Это тем более удобно, что deviation и magic в большинстве случаев задаются единожды и используются в последующих запросах.

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

Методы buy и sell отличаются только значением поля type, а все остальное в них одинаково. Поэтому общая часть оформлена в виде отдельного метода _market. Именно здесь мы устанавливаем action в TRADE_ACTION_DEAL, вызываем setSymbol и setVolumePrices.

private:
   ulong _market(const string name, const double lot, const double p = 0,
      const double stop = 0const double take = 0)
   {
      action = TRADE_ACTION_DEAL;
      if(!setSymbol(name)) return 0;
      if(!setVolumePrices(lotpstoptake)) return 0;
      ...

Далее мы могли бы просто вызвать OrderSend, но учитывая возможность реквот (обновлений цены на сервере за время отправки приказа), заключим вызов в цикл. За счет этого метод сможет несколько раз повторить попытку, но не более чем предустановленное число раз MAX_REQUOTES (макрос выбран равным 10 в коде).

      int count = 0;
      do
      {
         ZeroMemory(result);
         if(OrderSend(thisresult)) return result.order;
         // автоматический подбор цены означает автоматическую обработку реквот
         if(result.retcode == TRADE_RETCODE_REQUOTE)
         {
            Print("Requote N" + (string)++count);
            if(p == 0)
            {
               price = TU::GetCurrentPrice(typesymbol);
            }
         }
      }
      while(p == 0 && result.retcode == TRADE_RETCODE_REQUOTE 
         && ++count < MAX_REQUOTES);
      return 0;
   }

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

public:
   ulong buy(const double lot, const double p = 0,
      const double stop = 0const double take = 0)
   {
      return buy(symbol, lot, pstoptake);
   }
   
   ulong sell(const double lot, const double p = 0,
      const double stop = 0const double take = 0)
   {
      return sell(symbol, lot, pstoptake);
   }

Таким образом, в минимальной конфигурации программе достаточно будет вызвать request.buy(1.0), чтобы совершить покупку одним лотом.

Теперь вернемся к проблеме получения окончательного результата запроса, что для случая операции TRADE_ACTION_DEAL означает тикет позиции. В структуре MqlTradeRequestSync эту задачу решает метод completed: для каждого типа операции он должен "попросить" вложенную структуру MqlTradeResultSync дождаться своего заполнения в соответствии с типом операции.

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

Открытие позиции контролирует метод opened. Внутри мы найдем пару вызовов описанного выше метода wait: первый — для orderExist, второй — для positionExist.

   bool opened(const ulong msc = 1000)
   {
      if(retcode != TRADE_RETCODE_DONE
         && retcode != TRADE_RETCODE_DONE_PARTIAL)
      {
         return false;
      }
      
      if(!wait(orderExistmsc))
      {
         Print("Waiting for order: #" + (string)order);
      }
      
      if(deal != 0)
      {
         if(HistoryDealGetInteger(dealDEAL_POSITION_IDposition))
         {
            return true;
         }
         Print("Waiting for position for deal D=" + (string)deal);
      }
      
      if(!wait(positionExistmsc))
      {
         Print("Timeout");
         return false;
      }
      position = result.position;
      
      return true;
   }

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

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

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

Подведем промежуточный итог. Созданные структуры позволяют написать следующий код для покупки 1 лота текущего символа:

   MqlTradeRequestSync request;
   if(request.buy(1.0) && request.completed())
   {
      Print("OK Position: P="request.result.position);
   }

Внутрь блока условного оператора мы попадаем только с гарантированно открытой позицией и нам известен её тикет. Если бы мы использовали методы buy/sell сами по себе, то получили бы на их выходе тикет ордера и должны были бы сами проверять исполнение. В случае ошибки мы не попадем внутрь блока if, а код сервера будет содержаться в request.result.retcode.

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

  if(request.adjust(SLTP) && request.completed())
  {
     Print("OK Adjust")
  }

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

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

enum ENUM_ORDER_TYPE_MARKET
{
   MARKET_BUY = ORDER_TYPE_BUY,  // ORDER_TYPE_BUY
   MARKET_SELL = ORDER_TYPE_SELL // ORDER_TYPE_SELL
};
   
input string Symbol;         // Symbol (empty = current _Symbol)
input double Volume;         // Volume (0 = minimal lot)
input double Price;          // Price (0 = current Ask)
input ENUM_ORDER_TYPE_MARKET Type;
input string Comment;
input ulong Magic;
input ulong Deviation;

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

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

void OnInit()
{
   // планируем отложенный запуск
   EventSetTimer(1);
}

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

void OnTimer()
{
   EventKillTimer();
   ...

Опишем переменную типа MqlTradeRequestSync и подготовим значения для основных полей.

   const bool wantToBuy = Type == MARKET_BUY;
   const string symbol = StringLen(Symbol) == 0 ? _Symbol : Symbol;
   const double volume = Volume == 0 ?
      SymbolInfoDouble(symbolSYMBOL_VOLUME_MIN) : Volume;
 
   MqlTradeRequestSync request(symbol);
   ...

Опциональные поля заполним напрямую.

   request.magic = Magic;
   request.deviation = Deviation;
   request.comment = Comment;
   ...

Среди опциональных полей можно выбрать режим заливки (type_filling). По умолчанию, MqlTradeRequestSync автоматически записывает в это поле первый из разрешенных режимов ENUM_ORDER_TYPE_FILLING. Напомним, в структуре для этого есть специальный метод setFilling.

Далее вызываем метод buy или sell с параметрами, и если он возвращает тикет ордера, ждем появления открытой позиции.

   ResetLastError();
   const ulong order = (wantToBuy ?
      request.buy(volumePrice) :
      request.sell(volumePrice));
   if(order != 0)
   {
      Print("OK Order: #="order);
      if(request.completed()) // ждем открытой позиции
      {
         Print("OK Position: P="request.result.position);
      }
   }
   Print(TU::StringOf(request));
   Print(TU::StringOf(request.result));
}

В конце функции для справки в журнал выводятся структуры запроса и результата.

Если запустить эксперт с параметрами по умолчанию (покупка минимальным лотом текущего символа), можем получить следующий результат на "XTIUSD".

OK Order: #=218966930

Waiting for position for deal D=215494463

OK Position: P=218966930

TRADE_ACTION_DEAL, XTIUSD, ORDER_TYPE_BUY, V=0.01, ORDER_FILLING_FOK, @ 109.340, P=218966930

DONE, D=215494463, #=218966930, V=0.01, @ 109.35, Request executed, Req=8

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

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

OK Order: #=218966932

Waiting for position for deal D=215494468

Position ticket <> id: 218966932, 218966930

OK Position: P=218966932

TRADE_ACTION_DEAL, XTIUSD, ORDER_TYPE_SELL, V=0.02, ORDER_FILLING_FOK, @ 109.390, P=218966932

DONE, D=215494468, #=218966932, V=0.02, @ 109.39, Request executed, Req=9

Важно отметить, что после переворота тикет позиции перестает быть равным идентификатору позиции: идентификатор остался от первого ордера, а тикет — от второго. Мы намеренно обошли стороной задачу выяснения тикета позиции по её идентификатору в целях упрощения изложения. В большинстве случаев тикет и идентификатор совпадают, но для точного контроля используйте функцию TU::PositionSelectById. Желающие могут изучить прилагаемый исходный код.

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

При совершении покупки или продажи наши методы buy/sell позволяют сразу задать уровни Stop Loss и/или Take Profit. Для этого достаточно передать их дополнительными параметрами, полученными из входных переменных или рассчитанными по каким-либо формулам. Например,

input double SL;
input double TP;
...
void OnTimer()
{
   ...
   const ulong order = (wantToBuy ?
      request.buy(symbolvolumePriceSLTP) :
      request.sell(symbolvolumePriceSLTP));
   ...

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

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

В следующем разделе мы дополним текущий пример отложенной установкой sl и tp вторым запросом после успешного открытия позиции.