Паттерн "Строитель" в MQL5: Формирование ордеров с помощью цепочек вызовов методов
Введение
Каждая отправка ордера в 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() выполняет итоговую проверку согласованности всех сохраненных полей перед формированием запроса и его отправкой. Сама цепочка вызовов описывает назначение ордера в форме, которая читается как обычный текст.

Рисунок 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). Выявление этого нарушения в строителе до отправки запроса на сервер позволяет избежать сетевого обмена, завершающегося отказом, и связанных с ним затрат на обработку.

Рисунок 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 вызывающего кода. На четвёртом этапе структура результата заполняется целиком.

Рисунок 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);

Рисунок 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.