- Главное событие экспертов: OnTick
- Основные принципы и понятия: ордер, сделка, позиция
- Типы торговых операций
- Типы ордеров
- Режимы исполнения ордеров по цене и объемам
- Сроки действия отложенных ордеров
- Расчет залога для будущего ордера: OrderCalcMargin
- Оценка прибыли торговой операции: OrderCalcProfit
- Структура торгового запроса MqlTradeRequest
- Структура проверки запроса MqlTradeCheckResult
- Проверка корректности запроса: OrderCheck
- Результат отправки запроса: структура MqlTradeResult
- Отправка торгового запроса: OrderSend и OrderSendAsync
- Совершение покупки или продажи
- Модификация уровней Stop Loss и/или Take Profit позиции
- Трейлинг стоп
- Полное и частичное закрытие позиции
- Полное и частичное закрытие встречных позиций (хедж)
- Установка отложенного ордера
- Модификация отложенного ордера
- Удаление отложенного ордера
- Получение списка действующих ордеров
- Свойства ордеров (действующих и в истории)
- Функции для чтения свойств действующих ордеров
- Отбор ордеров по свойствам
- Получение списка позиций
- Свойства позиций
- Функции для чтения свойств позиций
- Свойства сделок
- Выборка ордеров и сделок из истории
- Функции для чтения свойств ордеров из истории
- Функции для чтения свойств сделок из истории
- Типы торговых транзакций
- Событие OnTradeTransaction
- Синхронные и асинхронные запросы
- Событие OnTrade
- Контроль за изменениями торгового окружения
- Особенности создания мультисимвольных экспертов
- Ограничения и преимущества экспертов
- Создание заготовки эксперта в Мастере MQL
Отбор ордеров по свойствам
В одном из разделов о свойствах символов мы представили класс SymbolFilter для отбора финансовых инструментов с заданными характеристиками. Теперь мы применим такой же подход для ордеров.
Поскольку нам предстоит похожим образом анализировать не только ордера, но также сделки и позиции, выделим общую часть алгоритма фильтрации в базовый класс TradeFilter (TradeFilter.mqh). Он практически один в один повторяет исходный код SymbolFilter. Поэтому мы не станем его здесь пояснять еще раз.
Желающие могут выполнить контекстное сравнение файлов SymbolFilter.mqh и TradeFilter.mqh, чтобы убедиться, насколько они похожи, и локализовать незначительные правки.
Основное отличие заключается в том, что класс TradeFilter является шаблоном, так как ему предстоит иметь дело со свойствами разных объектов: ордеров, сделок и позиций.
enum IS // поддерживаемые условия сравнения в фильтрах
|
Параметры шаблона I, D и S — это перечисления для групп свойств трех основных типов (целочисленные, вещественные, строковые): для ордеров они были описаны в предыдущих разделах, поэтому для наглядности вы можете представлять, что I=ENUM_ORDER_PROPERTY_INTEGER, D=ENUM_ORDER_PROPERTY_DOUBLE, S=ENUM_ORDER_PROPERTY_STRING.
Тип T предназначен для указания класса монитора. В данный момент у нас готов только один монитор — OrderMonitor. Позднее мы реализуем DealMonitor и PositionMonitor.
Ранее, в классе SymbolFilter мы обходились без параметров шаблона, потому что для символов неизменно известны все типы перечислений свойств и существует единственный класс SymbolMonitor.
Напомним структуру класса-фильтра. Группа методов let позволяет зарегистрировать в фильтре сочетание пар "свойство=значение", по которым будет затем осуществляться отбор объектов в методах select. Идентификатор свойства указывается в параметре property, а значение — в параметре value.
Самих методов select также несколько. Они позволяют заполнить для вызывающего кода массив с отобранными тикетами, а также при необходимости, дополнительные массивы со значениями запрошенных свойств объектов. Конкретные идентификаторы запрашиваемых свойств задаются в первом параметре метода select — это может быть одно свойство или несколько. В зависимости от этого приемный массив должен быть одномерным или двумерным.
Сочетание свойства и значения может проверяться не только на равенство (EQUAL), но и операции больше/меньше (GREATER/LESS). Для строковых свойств допустимо указывать шаблон поиска с символом "*", обозначающим любую последовательность знаков (например, "*[tp]*" для свойства ORDER_COMMENT совпадет со всеми комментариями, в которых с любом месте встретится "[tp]", хотя это только демонстрация возможности и для поиска ордеров в результате сработавшего Take Profit следует анализировать ORDER_REASON).
Поскольку алгоритм требует организации цикла с перебором всех объектов, а объекты могут быть разного типа (пока это ордера, но затем появится поддержка сделок и позиций), в классе TradeFilter потребовалось описать два абстрактных метода: total и get:
virtual int total() const = 0;
|
Первый возвращает количество объектов, а второй — тикет объекта по его номеру. Это должно напомнить вам пару функций OrdersTotal и OrderGetTicket. И действительно, они применены в конкретных реализациях методов для фильтра ордеров.
Ниже показан класс OrderFilter (OrderFilter.mqh) целиком.
#include <MQL5Book/OrderMonitor.mqh>
|
Эта простота особенно важна, учитывая, что аналогичные фильтры будут созданы для сделок и позиций без усилий.
С помощью нового класса мы можем гораздо проще проверять наличие ордеров, принадлежащих нашему эксперту, то есть заменить любые самописные варианты функции GetMyOrder, использовавшейся в примере PendingOrderModify.mq5.
OrderFilter filter;
|
Под "любыми вариантами" здесь имеется в виду, что благодаря классу фильтра мы можем составлять произвольные условия для отбора ордеров и менять их "на ходу" (например, по указанию пользователя, а не программиста).
В качестве примера применения фильтра возьмем эксперт, создающий сетку отложенных ордеров для торговли на отбой от уровней внутри некоторого диапазона цен, то есть рассчитанный на флуктуирующий рынок. Начиная с этого раздела и в течение нескольких следующих, мы будем видоизменять эксперт в контексте изучаемого материала.
Первая версия эксперта PendingOrderGrid1.mq5 строит сетку заданного размера из лимитных и стоп-лимитных ордеров. Параметрами будет количество ценовых уровней и шаг в пунктах между ними. Принцип действия иллюстрируется следующей схемой.
Сетка отложенных ордеров на 4 уровня с шагом 200 пунктов
В некий начальный момент времени, который может определяться внутрисуточным расписанием и соответствовать, например, "ночному флету", текущая цена округляется до размера шага сетки, и от этого уровня вверх вниз откладывается заданное количество уровней.
На каждом верхнем уровне ставится лимитный ордер на продажу и stoplimit-ордер на покупку с ценой будущего лимитного ордера на один уровень ниже. На каждом нижнем уровне ставится лимитный ордер по покупку и stoplimit-ордер на продажу с ценой будущего лимитного ордера на один уровень выше.
Когда цена касается одного из уровней, стоящий там лимитный ордер превращается в покупку или продажу (позицию). Одновременно с этим стоп-лимитный ордер того же уровня автоматически преобразуется системой в лимитный ордер противоположного направления на соседнем уровне.
Например, если цена при движении вверх пробила уровень, мы получим короткую позицию, а на дистанции шага ниже неё создастся лимитный ордер на покупку.
Эксперт будет отслеживать, чтобы на каждом уровне в паре с лимитным ордером существовал стоп-лимитный. Поэтому после обнаружения нового лимитного ордера на покупку программа добавит к нему на тот же уровень стоп-лимитный на продажу, причем целевой ценой будущего лимитного ордера назначается соседний сверху уровень, то есть тот, где открыта позиция.
Допустим, цена развернулась вниз и активировала лимитный ордер на уровень ниже — мы получим длинную позицию. Одновременно с этим стоп-лимитный ордер преобразуется в лимитный ордер на продажу на соседнем уровне сверху. Теперь эксперт снова обнаружит "голый" лимитный ордер и создаст ему в пару на том же уровне стоп-лимитный ордер на покупку с ценой будущего лимитного ордера на уровень ниже.
При наличии встречных позиций будем закрывать их. Также предусмотрим настройку внутрисуточного периода, когда торговая система включена, а на остальное время все ордера и позиции будут удаляться. Это, в частности, пригодится для "ночного флета", когда особенно выражены возвратные колебания рынка.
Разумеется, это лишь одна из множества потенциально возможных реализаций сеточной стратегии, и в ней отсутствуют многие настройки, присущие сеткам, но мы не станем усложнять пример.
Анализ ситуации эксперт будет проводить на каждом баре (предположительно, таймфрейма H1 или меньше). В принципе, такую логику работы советника можно и нужно усовершенствовать за счет оперативного реагирования на торговые события, но мы их еще не изучили. Поэтому вместо постоянного отслеживания и моментального "ручного" восстановления лимитных ордеров на вакантных уровнях сетки мы поручили эту работу серверу за счет применения стоп-лимитных ордеров. Однако здесь есть нюанс.
Дело в том, что лимитные и стоп-лимитные ордера, стоящие на каждом уровне относятся к противоположным типам (buy/sell), а потому активируются разными типами цен.
Получается, что если рынок двигался вверх к очередному уровню в верхней половине сетки, Ask-цена может задеть уровень и активирует стоп-лимитный ордер на покупку, но Bid-цена не дойдет до уровня, и лимитный ордер на продажу останется как есть (не превратится в позицию). В нижней половине сетки — при движении рынка вниз — ситуация зеркальная. Любого уровня первой касается Bid-цена и активирует стоп-лимитный ордер на продажу, и только при дальнейшем снижении до уровня также доходит Ask-цена. Если движения не произойдет, лимитный ордер на покупку останется как есть.
Данная проблема становится критичной с увеличением спреда. Поэтому в эксперте потребуется дополнительный контроль за "лишними" лимитными ордерами. Иными словами, эксперт не станет генерировать отсутствующий на уровне стоп-лимитный ордер, если на его предполагаемой целевой цене (соседний уровень) уже стоит лимитный ордер.
Исходный код прилагается в файле PendingOrderGrid1.mq5. Во входных параметрах можно задать объем каждой сделки Volume (по умолчанию, если оставить его равным 0, берется минимальный лот символа графика), количество уровней сетки GridSize (должно быть четным) и шаг между уровнями в пунктах GridStep. Начальное и конечное время внутрисуточного отрезка, на котором разрешена работа стратегии, указывается в параметрах StartTime и StopTime: в обоих важно только время.
#include <MQL5Book/MqlTradeSync.mqh>
|
Отрезок рабочего времени может быть как внутри суток (StartTime < StopTime), так и пересекать границу суток (StartTime > StopTime), например, с 22:00 до 09:00. Если два времени равны, предполагается круглосуточная торговля.
Прежде чем приступать к реализации торговой идеи упростим себе задачу по настройке запросов и выводу диагностической информации в журнал. Для этого опишем собственную структуру MqlTradeRequestSyncLog, производную от MqlTradeRequestSync.
const ulong DAYLONG = 60 * 60 * 24; // размер суток в секундах
|
В конструкторе мы заполняем все поля с неизменными значениями. В деструкторе выводим в журнал значащие поля запроса и результата. Очевидно, что деструктор автоматических объектов будет вызываться всегда в момент выхода из блока кода, где производилось формирование и отправка приказа, то есть в печать попадут отправленные и полученные данные.
В OnInit выполним некоторые проверки на корректность входных переменных, в частности, на четный размер сетки.
int OnInit()
|
Основной точкой входа алгоритма является обработчик OnTick. В нем мы опустим для краткости тот же механизм обработки ошибок на базе TRADE_RETCODE_SEVERITY, что и в примере PendingOrderModify.mq5.
Для побаровой работы в функции заведена статическая переменная lastBar, в которую мы сохраняем время последнего успешно обработанного бара. Все последующие тики на том же баре пропускаются.
void OnTick()
|
Вместо многоточия последует основной алгоритм, разделенный на несколько вспомогательных функций в целях систематизации. Первым делом определим, задан ли рабочий отрезок суток и если да, то включена ли в данный момент стратегия. Этот признак хранится в переменной tradeScheduled.
...
|
При разрешенной торговле сначала проверим, есть ли уже сеть ордеров, с помощью функции CheckGrid. Если сети нет, функция вернет константу GRID_EMPTY и нам следует создать сеть с помощью вызова SetupGrid. Если сеть уже построена, имеет смысл проверить, нет ли встречных позиций для закрытия: этим занимается функция CompactPositions.
if(tradeScheduled)
|
Как только торговый период заканчивается, необходимо удалить ордера и закрыть все позиции (если есть) — это поручено, соответственно, функции RemoveOrders и всё той же CompactPositions, но с логическим флагом (true): этот единственный, необязательный аргумент предписывает после встречного закрытия применить простое закрытие для оставшихся позиций.
else
|
Все функции возвращают код сервера, который анализируется на успех или ошибку с помощью TradeCodeSeverity. Специальные прикладные коды GRID_EMPTY и GRID_OK также расцениваются штатными согласно TRADE_RETCODE_SEVERITY.
#define GRID_OK +1
|
Теперь разберем функции по отдельности.
В функции CheckGrid как раз используется класс OrderFilter, представленный в начале этого раздела. С помощью фильтра запрашиваются все отложенные ордера по текущему символу и с "нашим" идентификационным номером, и в массиве сохраняются тикеты найденных ордеров.
uint CheckGrid()
|
Для анализа полноты сетки применяется уже знакомый класс MapArray, хранящий пары "ключ=значение". В данном случае в качестве ключа выступает уровень (цена, переведенная в пункты), а в качестве значения — битовая маска (суперпозиция) типов ордеров на данном уровне. Попутно в переменных limits и stops подсчитываются количества, соответственно, лимитных и стоп-лимитных ордеров.
// ценовые уровни => маски типов существующих там ордеров
|
Если количество ордеров каждого типа совпадает и равно заданному размеру сетки, значит всё в порядке.
if(limits == stops)
|
Ситуация, когда количество лимитных ордеров больше стоп-лимитных является штатной: она означает, что за счет движения цены один или несколько стоп-лимитных ордеров превратились в лимитные. Программа должна в таком случае добавить стоп-лимитные ордера на уровни, где их не хватает. Отдельный ордер конкретного типа для конкретного уровня умеет выставлять функция RepairGridLevel.
if(limits > stops)
|
Ситуация, когда количество стоп-лимитных ордеров больше, чем лимитных, трактуется как ошибка (вероятно, сервер по какой-то причине пропустил цену).
Alert("Error: Orphaned Stop-Limit orders found");
|
Функция RepairGridLevel выполняет следующие действия.
uint RepairGridLevel(const ulong level, const double point, const bool buyLimit)
|
Обратите внимание, что нам не требуется фактически заполнять структуру (кроме комментария, который можно сделать более информативным при необходимости), поскольку часть полей заполняется автоматически конструктором, а объем и цену мы передаем непосредственно в метод sellStopLimit или buyStopLimit.
Похожий подход используется и в функции SetupGrid, устанавливающий новую полную сеть ордеров. В начале функции подготавливаем переменные для расчетов и описываем массив структур MqlTradeRequestSyncLog.
uint SetupGrid()
|
Далее генерируем ордера для нижней и верхней половины сетки, расходясь от центра в стороны.
for(int i = 0; i < (int)GridSize / 2; ++i)
|
Затем проверяем готовность.
for(int i = 0; i < (int)GridSize; ++i)
|
Хотя проверка (вызов completed) разнесена с отправкой приказов, наша структура по прежнему использует внутри синхронную форму OrderSend. На самом деле для ускорения отправки пакета приказов (как в нашем сеточном эксперте) лучше использовать асинхронную версию OrderSendAsync. Но тогда статус исполнения ордеров следует инициировать из обработчика события OnTradeTransaction. А его мы изучим позднее.
Ошибка при отправке любого приказа приводит к досрочному выходу из цикла и возврату кода с сервера. Данный тестовый эксперт просто прекратит свою дальнейшую работу в случае ошибки. Для реального робота желательно предусмотреть интеллектуальный анализ сути ошибки и, при необходимости, удалить все ордера и закрыть позиции.
Закрытием позиций, которые будут порождаться отложенными ордерами, занимается функция CompactPositions.
uint CompactPositions(const bool cleanup = false) |
Параметр cleanup, равный по умолчанию false, означает штатную "подчистку" позиций внутри торгового периода, то есть закрытие встречных позиций (если они есть). Значение cleanup=true используется для принудительного закрытия всех позиций в конце торгового периода.
Функция заполняет массивы ticketsLong и ticketsShort тикетами длинных и коротких позиций с помощью вспомогательной функции GetMyPositions. Мы уже использовали последнюю в примере TradeCloseBy.mq5 в разделе про полное и частичное закрытие встречных позиций. Там же была показана и функция CloseByPosition. В новом эксперте она претерпела минимальные изменения: возвращает код с сервера вместо логического признака успеха или ошибки.
uint CompactPositions(const bool cleanup = false)
|
Вторая часть CompactPositions работает только при cleanup=true. Она далека от совершенства и будет скоро переделана.
if(cleanup)
|
Для всех найденных оставшихся позиций выполняется обычное закрытие вызовом CloseAllPositions.
uint CloseAllPositions(const ulong &tickets[], const int start = 0)
|
Нам осталось рассмотреть функцию RemoveOrders. Здесь также используется фильтр ордеров для получения их списка, и затем вызов метода remove в цикле.
uint RemoveOrders()
|
Проверим, как эксперт работает в тестере с настройками по умолчанию (торговый период с 00:00 до 09:00). Ниже представлен скриншот для запуска на EURUSD,H1.
Сеточная стратегия PendingOrderGrid1.mq5 в тестере
В журнале помимо периодических записей о пакетном создании нескольких ордеров (в начале суток) и их удалении под утро, мы будем регулярно видеть восстановление сети (добавление ордеров вместо сработавших) и закрытие позиций.
buy stop limit 0.01 EURUSD at 1.14200 (1.14000) (1.13923 / 1.13923)
|
Теперь настало время изучить функции MQL5 для работы с позициями и усовершенствовать их выборку и анализ в нашем эксперте. Этому посвящены следующие разделы.