English
preview
Паттерн "Строитель" в MQL5: Формирование ордеров с помощью цепочек вызовов методов

Паттерн "Строитель" в MQL5: Формирование ордеров с помощью цепочек вызовов методов

MetaTrader 5 — Торговые системы |
26 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Введение

Каждая отправка ордера в MQL5 начинается с заполнения структуры MqlTradeRequest. Структура содержит двадцать одно поле для задания типа операции, торгового символа, объема, цены, уровней стопов, срока действия, режима исполнения заявки и идентификаторов. На практике большинство полей взаимозависимы. Рыночный ордер не должен содержать отметку времени истечения срока действия. Для лимитного ордера необходимо указать допустимую цену, отличную от текущей рыночной. Стоп-лосс, установленный выше цены входа в длинную позицию, будет отклонен брокером или, на серверах с менее строгими правилами, скорректирован таким образом, что позиция может закрыться сразу после открытия.

Стандартный подход к заполнению этой структуры предполагает последовательное прямое присваивание значений полям, при этом язык не обеспечивает логическую согласованность полей. Компилятор проверяет типы, но не семантику. Если разработчик задаст request.sl = entry_price + 500 * _Point для ордера на покупку, код останется синтаксически корректным, но при выполнении возникнет ошибка, которая может остаться без внимания. В худшем случае нормализация на стороне сервера может привести к возникновению незащищенной позиции в зависимости от правил брокера.

Рассмотрим пример заполнения структуры для формирования корректного рыночного ордера на покупку при использовании стандартного подхода:

MqlTradeRequest request = {};
MqlTradeResult  result  = {};

request.action       = TRADE_ACTION_DEAL;
request.symbol       = _Symbol;
request.volume       = 0.10;
request.type         = ORDER_TYPE_BUY;
request.price        = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
request.sl           = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - 500 * _Point;
request.tp           = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + 1000 * _Point;
request.deviation    = 10;
request.magic        = 100001;
request.comment      = "Manual buy";
request.type_filling = ORDER_FILLING_IOC;

if(!OrderSend(request, result))
   PrintFormat("Error: %d", GetLastError());

Этот блок требует от разработчика знать, помнить и правильно учитывать взаимосвязь полей action, type, price, sl и tp без каких-либо вспомогательных средств. В рабочем советнике, где ордера нескольких типов отправляются из разных ветвей кода, даже одна ошибка копирования и вставки в одном из этих присваиваний приведет к формированию структурно несогласованного запроса. Функция OrderCheck() может выявить некоторые из этих ошибок до отправки запроса на сервер, однако для её правильного вызова требуется дополнительный шаблонный код, который часто опускается из-за нехватки времени при разработке.

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

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

Рисунок 1: Сравнение взаимозависимых ручных присваиваний значений полям MqlTradeRequest (слева) и цепочки вызовов методов плавного интерфейса COrderBuilder с проверкой на каждом шаге (справа).


Архитектура строителя и модель конечного автомата

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

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

Диаграмма конечного автомата строителя

Рисунок 2: Диаграмма конечного автомата COrderBuilder, иллюстрирующая допустимые пути конфигурирования, глобальное состояние ошибки INVALID и завершающий контрольный этап перед отправкой в методе Send(), используемый для эффективного накопления и обработки ошибок MQL5.

Строитель хранит отдельную строку m_error_message, в которой сохраняется описание первой обнаруженной ошибки проверки. Эту строку можно прочитать с помощью метода доступа GetLastError(), следуя той же схеме диагностики, которая используется в собственных торговых функциях MQL5. Вызывающий код, которому достаточно признака успеха или неудачи, проверяет возвращаемое значение метода Send(); если нужна диагностическая информация для записи в журнал, он считывает строку ошибки.

Карта внутренних полей

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

Поле строителя Соответствует полю Группа зависимостей Значение по умолчанию
m_symbol request.symbol Идентификация "" (некорректно, пока не задано)
m_volume request.volume Идентификация 0.0 (некорректно, пока не задано)
m_magic request.magic Идентификация 0
m_comment request.comment Идентификация ""
m_action request.action Направление TRADE_ACTION_DEAL
m_order_type request.type Направление ORDER_TYPE_BUY
m_price request.price Ценовой уровень 0.0
m_sl request.sl Уровни SL/TP 0.0 (необязательно)
m_tp request.tp Уровни SL/TP 0.0 (необязательно)
m_deviation request.deviation Исполнение 10
m_filling request.type_filling Исполнение ORDER_FILLING_IOC
m_expiration request.expiration Только для отложенных ордеров 0
m_stoplimit_price request.stoplimit Только для отложенных ордеров
0.0

Набор флагов валидности

Флаг Устанавливается методом От чего защищает
m_symbol_valid Symbol() Пустая строка, нераспознанные торговые символы
m_volume_valid Volume() Нулевой или отрицательный объём, объём ниже минимально допустимого
m_direction_valid Buy(), Sell() Неинициализированное состояние направления
m_price_valid AtMarket(), AtPrice(), AtStop() Нулевая цена, отложенный ордер без указания цены
m_stops_consistent StopLoss(), TakeProfit() Неверное расположение уровней стоп-лосса и тейк-профита относительно направления сделки


Проектирование методов и логика проверок

Каждый метод строителя возвращает указатель на экземпляр строителя (COrderBuilder*). Именно этот тип возвращаемого значения позволяет выстраивать цепочку вызовов методов: каждый метод возвращает указатель на тот же объект, поэтому к вызову можно сразу присоединить следующий метод. В MQL5 этот паттерн реализуется возвратом *this в конце каждого метода, поддерживающего цепочку вызовов: неявный указатель this разыменовывается, в результате чего получается ссылка на текущий экземпляр.

Механизм возврата указателя

Каждый метод строителя, поддерживающий цепочку вызовов, возвращает COrderBuilder* — указатель на текущий экземпляр объекта. Именно этот тип возвращаемого значения позволяет выстраивать цепочку вызовов методов в MQL5. В конце каждого метода, поддерживающего цепочку вызовов, оператор return this; возвращает адрес текущего экземпляра в виде типизированного указателя, который компилятор корректно распознает как указатель на объект для следующего вызова метода в цепочке.

MQL5 не поддерживает ссылочные типы возвращаемых значений для методов классов. Объявление вида COrderBuilder &Buy() вызывает ошибку компиляции в MetaEditor. Возвращаемый тип COrderBuilder* — указатель — является в MQL5 корректной и единственной поддерживаемой альтернативой используемому в C++ приему возврата ссылки через *this. В MQL5 цепочка вызовов методов обычно реализуется с помощью указателей; синтаксис цепочки зависит от того, как объявлена переменная: как экземпляр объекта или как указатель.

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

//--- Stack instance: object is destroyed automatically at scope exit
COrderBuilder builder;
MqlTradeResult result = {};
bool ok = builder->Symbol(_Symbol).Volume(0.10).Buy()
                 .AtMarket().StopLoss(sl).TakeProfit(tp)
                 .Send(result);

//--- Heap instance: caller manages lifetime explicitly
COrderBuilder *builder = new COrderBuilder();
MqlTradeResult result = {};
bool ok = builder->Symbol(_Symbol).Volume(0.10).Buy()
                 .AtMarket().StopLoss(sl).TakeProfit(tp)
                 .Send(result);
delete builder;

Методы Send() и Reset() не возвращают COrderBuilder*. Метод Send() возвращает значение типа bool как итоговый результат контрольного этапа перед отправкой, а метод Reset() имеет возвращаемый тип void. Эти два метода намеренно прерывают цепочку, поскольку обозначают переходы между состояниями, после которых нельзя незаметно продолжать вызовы: после Send() вызывающий код должен проверить результат, а после Reset() строитель находится в пустом состоянии, и прежде чем любой другой метод обретет смысл, необходимо заново вызвать Symbol().

Методы выбора направления и их связь с действием

Методы Buy() и Sell() одновременно задают тип ордера и действие, поскольку в MQL5 поля action и type всегда используются в паре, а их сочетание зависит от категории ордера. Рыночный ордер на покупку — это сочетание TRADE_ACTION_DEAL и ORDER_TYPE_BUY. Отложенный лимитный ордер на покупку — это TRADE_ACTION_PENDING плюс ORDER_TYPE_BUY_LIMIT. Строитель предоставляет отдельные методы для каждой комбинации, не требуя от вызывающего кода задавать эти два поля независимо друг от друга, что полностью исключает ошибки в их сочетании.

Метод строителя Задает action Задает type Категория ордера
Buy() TRADE_ACTION_DEAL ORDER_TYPE_BUY Исполнение по рыночной цене
Sell() TRADE_ACTION_DEAL
ORDER_TYPE_SELL
Исполнение по рыночной цене
BuyLimit(double price) TRADE_ACTION_PENDING ORDER_TYPE_BUY_LIMIT Отложенный лимитный ордер
SellLimit(double price) TRADE_ACTION_PENDING ORDER_TYPE_SELL_LIMIT Отложенный лимитный ордер
BuyStop(double price) TRADE_ACTION_PENDING ORDER_TYPE_BUY_STOP Отложенный стоп-ордер
SellStop(double price) TRADE_ACTION_PENDING ORDER_TYPE_SELL_STOP Отложенный стоп-ордер
BuyStopLimit(double price, double stoplimit) TRADE_ACTION_PENDING ORDER_TYPE_BUY_STOP_LIMIT Отложенный стоп-лимитный ордер
SellStopLimit(double price, double stoplimit) TRADE_ACTION_PENDING
ORDER_TYPE_SELL_STOP_LIMIT Отложенный стоп-лимитный ордер

Проверка уровня стопов

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

Метод StopLoss() откладывает проверку согласованности, передавая её внутренней вспомогательной функции ValidateStops(), которую также вызывают методы TakeProfit() и Send(). Такое откладывание проверки необходимо, поскольку разработчик может вызвать StopLoss() до окончательного определения цены отложенного ордера или до вызова Buy() либо Sell(). Строитель сразу же сохраняет исходное значение и повторно выполняет проверку согласованности при каждом изменении связанного поля.

bool COrderBuilder::ValidateStops()
  {
//--- Skip validation if direction has not been established
   if(!m_direction_valid)
      return(true);

   double entry = (m_price > 0.0) ? m_price : SymbolInfoDouble(m_symbol, SYMBOL_ASK);
   if(m_order_type == ORDER_TYPE_SELL || m_order_type == ORDER_TYPE_SELL_LIMIT ||
      m_order_type == ORDER_TYPE_SELL_STOP || m_order_type == ORDER_TYPE_SELL_STOP_LIMIT)
      entry = (m_price > 0.0) ? m_price : SymbolInfoDouble(m_symbol, SYMBOL_BID);

   bool is_buy = (m_order_type == ORDER_TYPE_BUY || m_order_type == ORDER_TYPE_BUY_LIMIT ||
                  m_order_type == ORDER_TYPE_BUY_STOP || m_order_type == ORDER_TYPE_BUY_STOP_LIMIT);

   double point        = SymbolInfoDouble(m_symbol, SYMBOL_POINT);
   long   stops_level  = SymbolInfoInteger(m_symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double min_distance = stops_level * point;

   if(m_sl > 0.0)
     {
      if(is_buy && m_sl >= entry)
        {
         m_error_message = StringFormat(
                           "StopLoss %.5f is at or above buy entry %.5f.", m_sl, entry);
         m_stops_consistent = false;
         return(false);
        }
      if(!is_buy && m_sl <= entry)
        {
         m_error_message = StringFormat(
                           "StopLoss %.5f is at or below sell entry %.5f.", m_sl, entry);
         m_stops_consistent = false;
         return(false);
        }
      if(MathAbs(entry - m_sl) < min_distance)
        {
         m_error_message = StringFormat(
                           "StopLoss distance %.5f is below minimum stops level %ld points.",
                           MathAbs(entry - m_sl) / point, stops_level);
         m_stops_consistent = false;
         return(false);
        }
     }

   if(m_tp > 0.0)
     {
      if(is_buy && m_tp <= entry)
        {
         m_error_message = StringFormat(
                           "TakeProfit %.5f is at or below buy entry %.5f.", m_tp, entry);
         m_stops_consistent = false;
         return(false);
        }
      if(!is_buy && m_tp >= entry)
        {
         m_error_message = StringFormat(
                           "TakeProfit %.5f is at or above sell entry %.5f.", m_tp, entry);
         m_stops_consistent = false;
         return(false);
        }
      if(MathAbs(entry - m_tp) < min_distance)
        {
         m_error_message = StringFormat(
                           "TakeProfit distance %.5f is below minimum stops level %ld points.",
                           MathAbs(entry - m_tp) / point, stops_level);
         m_stops_consistent = false;
         return(false);
        }
     }

   m_stops_consistent = true;
   return(true);
  }

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

Допустимое и недопустимое размещение SL/TP относительно запретных зон, определяемых установленным брокером значением SYMBOL_TRADE_STOPS_LEVEL, для ордеров на покупку и продажу.

Рисунок 3: Допустимое и недопустимое размещение SL/TP относительно запретных зон, определяемых установленным брокером значением SYMBOL_TRADE_STOPS_LEVEL, для ордеров на покупку и продажу.


Метод Send(): контрольный этап перед отправкой

Метод Send() — конечный узел каждой цепочки вызовов методов строителя. Порядок его действий выбран намеренно: сначала он проверяет, что все обязательные поля прошли проверку, затем выполняет итоговую проверку согласованности полей, после чего вызывает OrderCheck() для предварительной проверки брокером и только затем вызывает OrderSend().

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

  • Этап 1: Проверка всех флагов валидности → Все флаги m_*_valid имеют значение true?
  • Этап 2: Согласованность полей → Проверка ValidateStops() пройдена?
  • Этап 3: Предварительная проверка брокером → Функция OrderCheck() возвращает значение true?
  • Этап 4: Отправка на сервер → Функция OrderSend() выполнилась успешно?

Если какой-либо этап завершается неудачей, метод Send() возвращает false и задаёт значение m_error_message. Выходная структура MqlTradeResult остаётся в состоянии, сформированном на этапе, завершившемся неудачей. На первом и втором этапах обмен данными с сервером не происходит. На третьем этапе result.retcode заполняется косвенно: OrderCheck() требует параметр типа MqlTradeCheckResult, а не MqlTradeResult, поэтому строитель объявляет для этого вызова локальную переменную check_result типа MqlTradeCheckResult и при неудаче вручную копирует check_result.retcode в result.retcode вызывающего кода. На четвёртом этапе структура результата заполняется целиком.

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

Рисунок 4: Блок-схема выполнения четырёхэтапного конвейера проверки Send(), показывающая различие между локальными проверками с нулевой задержкой и сетевым обменом с сервером.

Выбор реализации метода Reset()

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

Альтернативный вариант реализации, предусматривающий автоматический сброс при каждом вызове Send(), заставил бы вызывающий код заново заполнять все поля для каждого ордера, даже при отправке серии структурно идентичных ордеров, отличающихся лишь объёмом. Ручной сброс сопряжён с небольшим риском повторного использования устаревшего состояния, но повышает эффективность при многократной отправке запросов. Само документирование этого поведения служит мерой снижения риска: любой разработчик, изучающий API строителя, увидит явно предусмотренный метод Reset(), который указывает на то, что состояние сохраняется.


Таблица описания методов

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

Метод строителя Целевые поля MqlTradeRequest Условие проверки Защитное действие при ошибке
Symbol(string) → COrderBuilder* symbol Непустая строка, успешный вызов SymbolSelect() m_symbol_valid = false, сформировано сообщение об ошибке
Volume(double) → COrderBuilder* volume В пределах [SYMBOL_VOLUME_MIN, SYMBOL_VOLUME_MAX], нормализован с учётом шага объёма При выходе за допустимый диапазон флагу присваивается значение false и задается сообщение об ошибке
Magic(ulong) → COrderBuilder* magic Без ограничений; значение сохраняется напрямую Не применимо
Comment(string) → COrderBuilder* comment Без ограничений; значение сохраняется напрямую Не применимо
Deviation(ulong) → COrderBuilder* deviation Без ограничений; значение сохраняется напрямую Не применимо
Filling(ENUM_ORDER_TYPE_FILLING) → COrderBuilder* type_filling Без ограничений; значение сохраняется напрямую Не применимо
Buy() → COrderBuilder* action, type Требуется, чтобы m_symbol_valid имело значение true Задается сообщение об ошибке, флагу направления присваивается значение false
Sell() → COrderBuilder* action, type
Требуется, чтобы m_symbol_valid имело значение true Задается сообщение об ошибке, флагу направления присваивается значение false
BuyLimit(double) → COrderBuilder* action, type, price Цена ниже текущей цены Ask Задано сообщение об ошибке, флаги цены/направления имеют значение false
SellLimit(double) → COrderBuilder* action, type, price Цена выше текущей цены Bid Задано сообщение об ошибке, флаги цены/направления имеют значение false
BuyStop(double) → COrderBuilder* action, type, price Цена выше текущей цены Ask Задано сообщение об ошибке, флаги цены/направления имеют значение false
SellStop(double) → COrderBuilder* action, type, price Цена ниже текущей цены Bid Задано сообщение об ошибке, флаги цены/направления имеют значение false
BuyStopLimit(double, double) → COrderBuilder* action, type, price, stoplimit Стоп-цена выше цены Ask; лимитная цена ниже стоп-цены Задано сообщение об ошибке, флаги имеют значение false
SellStopLimit(double, double) → COrderBuilder* action, type, price, stoplimit
Стоп-цена ниже цены Bid; лимитная цена выше стоп-цены Задано сообщение об ошибке, флаги имеют значение false
AtMarket() → COrderBuilder* price (опорная), type_filling Необходимо определить направление; должна быть доступна актуальная котировка Торговый символ должен быть допустимым; при отсутствии котировки выдается ошибка
AtPrice(double) → COrderBuilder* price Ненулевое, положительное Сообщение об ошибке задано, флаг цены — false
StopLoss(double) → COrderBuilder* sl Корректность расположения относительно направления сделки, соблюдение минимального расстояния, заданного уровнем стопов m_stops_consistent = false, установлено сообщение об ошибке
TakeProfit(double) → COrderBuilder* tp Корректность расположения относительно направления сделки, соблюдение минимального расстояния, заданного уровнем стопов m_stops_consistent = false, установлено сообщение об ошибке
Expiry(datetime) → COrderBuilder* expiration, type_time Предоставляет обёртку для поля request.expiration. Метка времени в будущем; устанавливает ORDER_TIME_SPECIFIED Метки времени в прошлом отклоняются, устанавливается сообщение об ошибке
Reset() → void Все поля Сбрасывает все поля и флаги до значений по умолчанию Не применимо
Send(MqlTradeResult &) → bool Создает полный объект MqlTradeRequest Четырёхэтапный механизм проверки: флаги, проверка согласованности, OrderCheck(), OrderSend() Возвращает false на первом этапе, завершившемся ошибкой
IsValid() → bool Нет (диагностика) Возвращает true, только если все флаги валидности имеют значение true Не применимо
ErrorMessage() → string Нет (диагностика) Возвращает последнее сохраненное сообщение об ошибке Не применимо


Анализ накладных расходов и практические ограничения

Задержка при проверке: каждый вызов StopLoss() и TakeProfit() вызывает SymbolInfoDouble() и SymbolInfoInteger() для получения текущего размера пункта и уровня стопов. Это операции чтения из кэша торговых символов терминала, не связанные с сетевыми вызовами; их задержка на типичном оборудовании составляет десятки наносекунд. Вызов SymbolSelect() в методе Symbol() — самая затратная из проверок, выполняемых в отдельных методах, поскольку он может инициировать запрос на подписку на торговый символ, если тот ещё не включён в список окна "Обзор рынка". Это следует учитывать при создании строителей для торговых символов, отличных от основного инструмента советника, в условиях, чувствительных к задержкам.

Повторное использование объекта или создание нового экземпляра: строитель — это класс, в котором нет динамических членов, кроме строковых полей. Если строитель используется для однократной отправки ордера, его можно объявить как локальную переменную в стеке: он будет уничтожен при выходе из области видимости, что безопасно и не приводит к утечкам памяти. Для советников, отправляющих ордера с высокой частотой, объявление одного строителя в области видимости класса и вызов Reset() между отправками позволяют избежать многократного создания и уничтожения строковых полей объекта, память для которых в MQL5 выделяется в куче. Если строитель создаётся в куче с помощью new COrderBuilder() для использования синтаксиса цепочки вызовов методов через указатель, вызывающий код должен применить delete к этому указателю, когда строитель больше не нужен. После удаления рекомендуется присваивать указателю значение NULL, чтобы предотвратить случайное повторное освобождение памяти при длительной работе советника.

Ограничение на порядок вызова методов: у цепочки вызовов методов есть тонкость, которой нет при заполнении структуры: вызывающий код должен вызывать методы задания направления до методов задания уровней стоп-лосса и тейк-профита, поскольку проверка этих уровней зависит от направления. Если у строителя ещё не задано направление, вызов StopLoss(1.0800) перед Buy() пропустит проверку согласованности с направлением и безусловно сохранит значение, отложив проверку до следующего метода, задающего направление, или до заключительного вызова Send(). Это предусмотренное поведение, однако разработчики должны понимать, что внутренний метод строителя ValidateStops() повторно вызывается при выполнении Send() независимо от того, когда был вызван StopLoss(), поэтому даже при позднем задании направления перед отправкой будет выполнена корректная проверка.

Отсутствие потокобезопасности: каждый экземпляр советника MQL5 выполняется в одном потоке. Строитель изначально не рассчитан на потокобезопасное использование. Если будущие версии среды выполнения MetaTrader 5 будут поддерживать параллельные контексты выполнения в рамках одного советника, поля состояния строителя потребуется защищать мьютексом.

Ограничения OrderCheck(): Предварительная проверка у брокера на третьем этапе выполнения Send() проверяет торговый запрос с учётом текущих маржинальных требований и состояния счёта, но не гарантирует его принятия сервером, поскольку рыночные условия могут измениться за время сетевого обмена между OrderCheck() и OrderSend(). Строитель не может устранить это состояние гонки; он может лишь снизить вероятность неудачной отправки за счёт проверки перед отправкой.


Варианты использования

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

MqlTradeResult result = {};
COrderBuilder  builder;
bool ok = builder.Symbol(_Symbol)
                 .Volume(0.10)
                 .Magic(100001)
                 .Comment("Trend entry")
                 .Deviation(10)
                 .Buy()
                 .AtMarket()
                 .StopLoss(SymbolInfoDouble(_Symbol, SYMBOL_ASK) - 500 * _Point)
                 .TakeProfit(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + 1000 * _Point)
                 .Send(result);

if(!ok)
   PrintFormat("[COrderBuilder] Submission failed: %s", builder.ErrorMessage());
else
   PrintFormat("[COrderBuilder] Order placed. Deal: %lld", result.deal);

Отложенный лимитный ордер на покупку со сроком действия:

MqlTradeResult result = {};
COrderBuilder  builder;
double         limit_price = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - 200 * _Point;

bool ok = builder.Symbol(_Symbol)
                 .Volume(0.05)
                 .Magic(100002)
                 .BuyLimit(limit_price)
                 .StopLoss(limit_price - 300 * _Point)
                 .TakeProfit(limit_price + 600 * _Point)
                 .Expiry(TimeCurrent() + 3600)
                 .Send(result);

if(!ok)
   PrintFormat("[COrderBuilder] Pending order failed: %s", builder.ErrorMessage());

Экземпляр строителя, повторно используемый для нескольких отправок в контексте класса:

//--- In EA class header: declared as a value member, not a pointer
COrderBuilder m_builder;

//--- In OnTick() or order dispatch method
MqlTradeResult result = {};
m_builder.Reset();

bool ok = m_builder.Symbol(_Symbol)
                   .Volume(inp_lot_size)
                   .Magic(inp_magic_number)
                   .Sell()
                   .AtMarket()
                   .StopLoss(SymbolInfoDouble(_Symbol, SYMBOL_BID) + 400 * _Point)
                   .TakeProfit(SymbolInfoDouble(_Symbol, SYMBOL_BID) - 800 * _Point)
                   .Send(result);

Диаграмма последовательности UML, иллюстрирующая процесс настройки строителя с плавным интерфейсом и последовательные этапы внутренней и внешней проверки, запускаемые при выполнении завершающего метода Send().

Рисунок 5: Диаграмма последовательности UML, иллюстрирующая процесс настройки строителя с плавным интерфейсом и последовательные этапы внутренней и внешней проверки, запускаемые при выполнении завершающего метода Send().


Заключение

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

Платой за это становится небольшое увеличение числа операций, выполняемых при каждой отправке. Каждый вызов метода создаёт небольшие накладные расходы на проверку, а контрольный этап перед отправкой в методе Send(), состоящий из четырёх стадий, добавляет операции, отсутствующие при прямом заполнении структуры и отправке запроса. Для подавляющего большинства архитектур советников, в которых ордера отправляются по сигналам, возникающим с интервалами в секунды или минуты, а не микросекунды, эти накладные расходы пренебрежимо малы с точки зрения архитектуры. Для систем с жёсткими ограничениями на задержку при обработке тиков логику проверки можно выполнить заранее один раз при формировании сигнала, а Send() вызывать лишь после того, как будет установлено, что все флаги валидности имеют значение true. Это сводит затраты времени на контрольный этап перед отправкой к одной последовательности вызовов OrderCheck() и OrderSend().

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


Программы, использованные в статье:

# Название Тип Описание
1 OrderBuilder.mqh Подключаемый файл Класс COrderBuilder: строитель с плавным интерфейсом и полноценной поддержкой цепочек вызовов методов, внутренний конечный автомат, многоступенчатый контроль перед отправкой и механизм проверки согласованности уровней стопов.
2 OrderBuilderDemo.mq5 Демонстрационный советник Демонстрационный советник, использующий COrderBuilder для работы со следующими типами ордеров: рыночный ордер на покупку, рыночный ордер на продажу, отложенный лимитный ордер на покупку, отложенный стоп-ордер на продажу и отложенный стоп-лимитный ордер на покупку; включает перехват состояний ошибки и вывод диагностической информации.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22936

Прикрепленные файлы |
OrderBuilder.mqh (27.22 KB)
Преодоление проблем доступности в торговых инструментах на MQL5 (Часть V): Торговля с помощью жестов на основе компьютерного зрения Преодоление проблем доступности в торговых инструментах на MQL5 (Часть V): Торговля с помощью жестов на основе компьютерного зрения
В этой статье рассказывается, как организовать торговлю в MetaTrader 5 без использования мыши и клавиатуры, преобразуя жесты рук, отслеживаемые с помощью веб-камеры, в торговые команды MQL5. Мы рассмотрим архитектуру (MediaPipe/OpenCV на Python плюс советник MQL5), сопоставление жестов и действий, а также межпроцессное взаимодействие с помощью глобальных переменных или HTTP-опроса. Вы реализуете советник, выполните действия BUY/SELL/CLOSE и проверите задержку и надежность в режиме реального времени.
Торговые инструменты на MQL5 (Часть 35): Расширение слоя рисования на холсте — каналы, вилы, инструменты Ганна и Фибоначчи Торговые инструменты на MQL5 (Часть 35): Расширение слоя рисования на холсте — каналы, вилы, инструменты Ганна и Фибоначчи
Мы расширяем слой рисования на холсте из предыдущей части, добавляя семь новых категорий аналитических инструментов с несколькими опорными точками, которые охватывают три варианта каналов, три варианта вил, три инструмента Ганна и шесть инструментов Фибоначчи. Рассмотрим, как каждый инструмент задает свою геометрию на холсте, как вычисляемые маркеры позволяют пользователям согласованно изменять форму составных фигур и как общие вспомогательные функции выполняют отсечение лучей, построчную заливку и отрисовку дуг со сглаживанием. В итоге у нас будет полный набор инструментов для аналитического рисования, которые будут располагаться на одном интерактивном холсте наряду с базовыми инструментами для рисования линий из предыдущей части.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Машинное обучение в условиях ограничений (Часть 2): Калибровка размера позиции по оставшемуся бюджету допустимой просадки Машинное обучение в условиях ограничений (Часть 2): Калибровка размера позиции по оставшемуся бюджету допустимой просадки
Мы представляем цепочку калибровки, учитывающую набор правил и преобразующую оставшийся бюджет риска в откалиброванный масштаб сигмоиды для расчёта размера позиции. Эта цепочка вычисляет верхний предел на основе процентного значения стоп-лосса, и коэффициента запаса, находит w обратным расчётом при опорном отклонении и постепенно уменьшает размер позиции по мере сокращения бюджета. В статье также разъясняется, где в рабочей системе необходимо применять ограничения кредитного плеча: на этапе пересчёта размера позиции в лоты, поскольку сам по себе расчёт размера позиции на основе риска не обеспечивает соблюдения максимального кредитного плеча.