- Главное событие экспертов: OnTick
- Основные принципы и понятия: ордер, сделка, позиция
- Типы торговых операций
- Типы ордеров
- Режимы исполнения ордеров по цене и объемам
- Сроки действия отложенных ордеров
- Расчет залога для будущего ордера: OrderCalcMargin
- Оценка прибыли торговой операции: OrderCalcProfit
- Структура торгового запроса MqlTradeRequest
- Структура проверки запроса MqlTradeCheckResult
- Проверка корректности запроса: OrderCheck
- Результат отправки запроса: структура MqlTradeResult
- Отправка торгового запроса: OrderSend и OrderSendAsync
- Совершение покупки или продажи
- Модификация уровней Stop Loss и/или Take Profit позиции
- Трейлинг стоп
- Полное и частичное закрытие позиции
- Полное и частичное закрытие встречных позиций (хедж)
- Установка отложенного ордера
- Модификация отложенного ордера
- Удаление отложенного ордера
- Получение списка действующих ордеров
- Свойства ордеров (действующих и в истории)
- Функции для чтения свойств действующих ордеров
- Отбор ордеров по свойствам
- Получение списка позиций
- Свойства позиций
- Функции для чтения свойств позиций
- Свойства сделок
- Выборка ордеров и сделок из истории
- Функции для чтения свойств ордеров из истории
- Функции для чтения свойств сделок из истории
- Типы торговых транзакций
- Событие OnTradeTransaction
- Синхронные и асинхронные запросы
- Событие OnTrade
- Контроль за изменениями торгового окружения
- Особенности создания мультисимвольных экспертов
- Ограничения и преимущества экспертов
- Создание заготовки эксперта в Мастере MQL
Контроль за изменениями торгового окружения
В предыдущем разделе про событие OnTrade мы упоминали о том, что некоторые подходы программирования торговых стратегий могут потребовать делать слепки окружения и сравнивать их друг с другом с течением времени. Это частый прием при использовании OnTrade, но его также можно активировать по расписанию, на каждом баре или даже тике. В наших классах-мониторах, способных читать свойства ордеров, сделок и позиций до сих пор отсутствовала способность сохранять состояние. В данном разделе мы представим один из вариантов кэширования торгового окружения.
Как вы знаете, свойства всех торговых объектов делятся по типам на 3 группы: целочисленные, вещественные и строковые. У каждого класса объектов эти группы свои (например, у ордеров целочисленные свойства описаны в перечислении ENUM_ORDER_PROPERTY_INTEGER, а у позиций — в ENUM_POSITION_PROPERTY_INTEGER), но суть деления общая. Поэтому введем перечисление PROP_TYPE, с помощью которого можно будет описывать для любого объекта, к какому типу относится то или иное свойство. Это обобщение напрашивается, поскольку механизмы хранения и обработки свойств одного типа должны быть одинаковыми, вне зависимости от того, принадлежит ли свойство ордеру, позиции или сделке.
enum PROP_TYPE
|
Наиболее простым способом хранения значений свойств являются массивы. Очевидно, что из-за наличия 3 базовых типов нам потребуется 3 разных массива. Опишем их внутри нового класса TradeState, вложенного в MonitorInterface (TradeBaseMonitor.mqh).
Напомним, что базовый шаблон MonitorInterface<I,D,S> составляет основу всех прикладных классов-мониторов (OrderMonitor, DealMonitor, PositionMonitor). Типы I, D, S здесь соответствуют конкретным перечислениям целых, вещественных и строковых свойств.
Вполне логично включить механизм хранения именно в базовый монитор, тем более что создаваемый кэш свойств будет наполняться данными посредством чтения свойств из объекта-монитора.
template<typename I,typename D,typename S>
|
Весь класс TradeState сделан публичным, поскольку к его полям потребуется доступ из родительского объекта-монитора (который передается как указатель в конструктор), а кроме того объекты TradeState будут использованы только в защищенной части монитора (извне к ним достучаться нельзя).
Для того чтобы заполнять 3 массива значениями свойств 3-х разных типов необходимо предварительно выяснить распределение свойств по типам и индексы в каждом конкретном массиве.
Для каждого типа торговых объектов (ордеров, сделок, позиций) идентификаторы 3-х соответствующих перечислений со свойствами разных типов не пересекаются и составляют сквозную нумерацию. Продемонстрируем это.
В разделе Перечисления мы представили скрипт ConversionEnum.mq5, в котором реализована функция process для вывода в журнал всех элементов конкретного перечисления. В том скрипте изучалось перечисление ENUM_APPLIED_PRICE, но ничто не мешает нам сделать копию скрипта и выполнить анализ трех других перечислений, которые нас интересуют. Например, так:
void OnStart()
|
В результате его выполнения получим следующий лог. Левая колонка содержит нумерацию внутри перечислений, а значения справа (после знака '=') — встроенные константы (идентификаторы) элементов.
ENUM_POSITION_PROPERTY_INTEGER Count=9
|
Например, свойство с константой 0 — это строковое POSITION_SYMBOL, с константами 1 и 2 — целочисленные POSITION_TIME и POSITION_TYPE, а с константой 3 — вещественное POSITION_VOLUME, и так далее.
Таким образом, константы представляют собой систему сквозных индексов по свойствам всех типов, и мы можем использовать тот же алгоритм (основанный на EnumToArray.mqh) для их получения.
Для каждого свойства нужно запомнить его тип (от этого зависит, в каком из трех массивов хранить значение) и порядковый номер среди свойств такого же типа (это будет индекс элемента в соответствующем массиве). Например, мы видим, что у позиций есть только 3 строковых свойства, значит массив strings в слепке одной позиции должен будет иметь такой размер, и под индексами 0, 1, 2 в него будут записываться POSITION_SYMBOL (0), POSITION_COMMENT (11), POSITION_EXTERNAL_ID (19).
Преобразование сквозных индексов свойств в их тип (один из PROP_TYPE) и в порядковый номер в массиве соответствующего типа можно сделать один раз при старте программы, так как перечисления со свойствами являются постоянными (встроены в систему). Полученную таблицу косвенной адресации запишем в статический двумерный массив indices. Его размер по первому измерению будет динамически определен как общее количество свойств (всех 3-х типов) — его запишем в статическую переменную limit. По второму измерению выделена пара ячеек: indices[i][0] — тип PROP_TYPE, indices[i][1] — индекс в одном из массивов ulongs, doubles или strings (в зависимости от indices[i][0]).
class TradeState
|
Переменные j, d, s будут использованы для последовательной индексации свойств внутри каждого из 3-х разных типов. Вот, собственно, как это делается в статическом методе calcIndices.
static int calcIndices()
|
Метод boundary возвращает максимальную константу среди всех элементов заданного перечисления E.
template<typename E>
|
Наибольшее значение из всех трех типов перечислений определяет диапазон целых чисел, которые следует рассортировать по принадлежности к свойствам трех типов.
В этом помогает метод detect, который возвращает true, если целое число является элементом перечисления.
template<typename E>
|
Последний вопрос в том, как запустить этот расчет при старте программы. Это достигается за счет статичности переменных и метода.
template<typename I,typename D,typename S>
|
Обратите внимание, что limit инициализируется результатом вызова нашей функции calcIndices.
Имея таблицу с индексами, реализуем заполнение массивов значениями свойств в методе cache.
class TradeState
|
Мы проходимся в цикле по всему диапазону свойств от 0 до limit и в зависимости от типа свойства в indices[i][0] записываем его значение в элемент массива ulongs, doubles или strings под номером indices[i][1] (соответствующий элемент массива передается по ссылке в метод _get).
Вызов owner.get(e, value) обращается к одному из стандартных методов класса-монитора (здесь он виден как абстрактный указатель MonitorInterface). В частности, для позиций в классе PositionMonitor это приведет к вызовам PositionGetInteger, PositionGetDouble или PositionGetString. Нужный тип выберет компилятор. В мониторах ордеров и сделок существуют свои аналогичные реализации, которые автоматически подключаются этим базовым кодом.
Описание слепка одного торгового объекта логично унаследовать от класса-монитора. Поскольку нам предстоит кэшировать ордера, сделки и позиции, имеет смысл сделать новый класс шаблоном и собрать в нем все общие алгоритмы, подходящие для всех объектов. Назовем его TradeBaseState (файл TradeState.mqh).
template<typename M,typename I,typename D,typename S>
|
Под буквой M здесь скрывается один из конкретных классов-мониторов, описанных ранее (OrderMonitor.mqh, PositionMonitor.mqh, DealMonitor.mqh). Основу составляет кэширующий объект state только что представленного класса M::TradeState. В зависимости от M внутри будет сформирована специфическая индексная таблица (одна для класса M) и распределены массивы свойств (собственные для каждого экземпляра M, то есть для каждого ордера, сделки, позиции).
Переменная cached содержит признак того, заполнены ли уже массивы в state значениями свойств и следует ли при запросе свойств у объекта возвращать значения из кэша. Это потребуется в дальнейшем для сравнения сохраненного и актуального состояний.
Иными словами, когда cached сброшено в false, объект будет вести себя как обычный монитор, считывая свойства из торгового окружения. Когда cached равно true, объект вернет предварительно сохраненные значения из внутренних массивов.
virtual long get(const I property) const override
|
По умолчанию, кэширование, разумеется, включено.
Мы должны предусмотреть и метод, непосредственно выполняющий кэширование (заполнение массивов). Для этого достаточно вызвать метод cache у объекта state.
bool update()
|
Но что такое метод refresh?
Дело в том, что до сих пор мы использовали объекты-мониторы в простом режиме: создавали, читали свойства и удаляли. При этом чтение свойств предполагает, что соответствующий ордер, сделка или позиция были выбраны в торговом контексте (внутри конструктора). Поскольку сейчас мы совершенствуем мониторы, привнося в них поддержку внутреннего состояния, необходимо обеспечить повторное выделение нужного элемента, чтобы прочитать свойства даже спустя неопределенное время (разумеется, с проверкой на то, что элемент еще существует). В связи с этим в шаблонный класс MonitorInterface и был добавлен виртуальный метод refresh.
// TradeBaseMonitor.mqh
|
Он должен вернуть true при успешном выделении ордера, сделки или позиции. Если результат равен false, во встроенной переменной _LastError подразумевается наличие одной из ошибок:
- 4753 ERR_TRADE_POSITION_NOT_FOUND;
- 4754 ERR_TRADE_ORDER_NOT_FOUND;
- 4755 ERR_TRADE_DEAL_NOT_FOUND;
При этом переменная-член ready, которая сигнализирует доступность объекта, должна быть сброшена в false в реализациях этого метода в производных классах.
Например, в конструкторе PositionMonitor у нас была и остается такая инициализация. В мониторах ордеров и сделок ситуация похожа.
// PositionMonitor.mqh
|
Теперь мы добавим во все конкретные классы метод refresh такого вида (на примере PositionMonitor):
// PositionMonitor.mqh
|
Но заполнение массивов кэша значениями свойств еще полдела. Вторая половина заключается в сравнении этих значений с актуальным состоянием ордера, сделки или позиции.
Для выявления различий и записи индексов изменившихся свойств в массив changes предназначен метод getChanges в создаваемом классе TradeBaseState. Метод возвращает true при обнаружении изменений.
template<typename M,typename I,typename D,typename S>
|
Как видно, основная работа поручается некоему методу diff в классе M. Это новый метод: нужно его написать. К счастью, благодаря ООП, можно сделать это единожды в базовом шаблоне MonitorInterface, и метод появится сразу для ордеров, сделок и позиций.
// TradeBaseMonitor.mqh
|
Итак, все готово для формирования конкретных кэширующих классов для ордеров, сделок и позиций. Например, позиции будут храниться в расширенном мониторе PositionState на базе PositionMonitor.
class PositionState: public TradeBaseState<PositionMonitor,
|
Аналогичным образом в файле TradeState.mqh определен кэширующий класс для сделок.
class DealState: public TradeBaseState<DealMonitor,
|
С ордерами все немного сложнее, потому что они могут быть действующими и историческими. У нас до сих пор был один универсальный класс монитора для ордеров OrderMonitor. Он пытается найти переданный тикет ордера и среди активных ордеров, и в истории. Для кэширования такой подход не подойдет, потому что в экспертах требуется отслеживать переход ордера из одного состояния в другое.
В связи с этим в файл OrderMonitor.mqh добавлены 2 более конкретных класса: ActiveOrderMonitor и HistoryOrderMonitor.
// OrderMonitor.mqh
|
Каждый из них ищет тикет только в своей области. На основе этих мониторов уже можно создать кэширующие классы.
// TradeState.mqh
|
Последний штрих, который мы добавим для удобства в класс TradeBaseState — особый метод для преобразования значения свойства в строку. Хотя в мониторе есть несколько версий методов stringify, все они будут "печатать" либо значения из кэша (если переменная-член cached равна true), либо из оригинального объекта торгового окружения (если cached равно false). Нам же для визуализации отличий кэша от измененного объекта (когда эти отличия обнаружатся) нужно одновременно прочитать и значение из кэша, и минуя кэш. В связи с этим добавим метод stringifyRaw, всегда работающий со свойством напрямую (за счет того, что переменная cached временно сбрасывается и устанавливается обратно).
// получить строковое представление свойства 'i' минуя кэш
|
Проверим работоспособность кэширующего монитора на простом примере эксперта, отслеживающего состояние действующего ордера (OrderSnapshot.mq5). Позднее мы разовьем данную идея для кэширования любой совокупности ордеров, сделок или позиций, то есть создадим полноценный кэш.
Эксперт будет пытаться найти последний в списке действующих ордеров и создавать для него объект OrderState. Если ордеров нет, пользователь получит предложение создать ордер или открыть позицию (последнее сопряжено с постановкой и исполнением ордера по рынку). Как только ордер обнаружен, для него в обработчике OnTrade производится проверка на изменение состояния. Эксперт продолжит контролировать этот ордер, пока не будет выгружен.
int OnInit()
|
В дополнение к выводу массива изменившихся свойств неплохо бы отобразить сами изменения. Поэтому вместо многоточия добавим такой фрагмент (он нам пригодится и в будущих классах полноценных кэшей).
for(int k = 0; k < ArraySize(changes); ++k)
|
Здесь нам уже пригодился новый метод stringifyRaw. После отображения изменений не забываем обновить состояние кэша.
state.update(); |
Если запустить эксперт на счете без действующих ордеров и выставить новый, увидим в журнале следующие записи (в данном случае создавался buy limit для EURUSD, ниже текущей рыночной цены).
Alert: Please, create a pending order or open/close a position >>> OnTrade(0) Order picked up: 1311736135 true MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING> ENUM_ORDER_PROPERTY_INTEGER Count=14 0 ORDER_TIME_SETUP=2022.04.11 11:42:39 1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00 2 ORDER_TIME_DONE=1970.01.01 00:00:00 3 ORDER_TYPE=ORDER_TYPE_BUY_LIMIT 4 ORDER_TYPE_FILLING=ORDER_FILLING_RETURN 5 ORDER_TYPE_TIME=ORDER_TIME_GTC 6 ORDER_STATE=ORDER_STATE_STARTED 7 ORDER_MAGIC=0 8 ORDER_POSITION_ID=0 9 ORDER_TIME_SETUP_MSC=2022.04.11 11:42:39'729 10 ORDER_TIME_DONE_MSC=1970.01.01 00:00:00'000 11 ORDER_POSITION_BY_ID=0 12 ORDER_TICKET=1311736135 13 ORDER_REASON=ORDER_REASON_CLIENT ENUM_ORDER_PROPERTY_DOUBLE Count=7 0 ORDER_VOLUME_INITIAL=0.01 1 ORDER_VOLUME_CURRENT=0.01 2 ORDER_PRICE_OPEN=1.087 3 ORDER_PRICE_CURRENT=1.087 4 ORDER_PRICE_STOPLIMIT=0.0 5 ORDER_SL=0.0 6 ORDER_TP=0.0 ENUM_ORDER_PROPERTY_STRING Count=3 0 ORDER_SYMBOL=EURUSD 1 ORDER_COMMENT= 2 ORDER_EXTERNAL_ID= >>> OnTrade(1) Order properties changed: 10 14 ORDER_PRICE_CURRENT: 1.087 -> 1.09073 ORDER_STATE: ORDER_STATE_STARTED -> ORDER_STATE_PLACED >>> OnTrade(2) >>> OnTrade(3) >>> OnTrade(4) |
Здесь видно, как статус ордера изменился со STARTED на PLACED. Если бы мы вместо отложенного ордера открылись по рынку мелким объемом, этих изменений могли бы не успеть получить, потому что такие ордера, как правило, очень быстро устанавливаются, и их наблюдаемый статус меняется со STARTED сразу на FILLED. А последнее уже означает, что ордер перенесен в историю. Поэтому для их отслеживания требуется параллельный мониторинг истории. Мы покажем это в следующем примере.
Обратите внимание, что событий OnTrade может быть много, но они не все связаны с нашим ордером.
Попробуем задать уровень Take Profit и посмотрим в журнал.
>>> OnTrade(5)
|
Далее поменяем срок истечения: с GTC до одного дня.
>>> OnTrade(8)
|
Здесь в процессе изменения нашего ордера цена успела измениться, и потому мы "зацепили" промежуточное уведомление о новом значении в ORDER_PRICE_CURRENT. И только после этого в журнал попали ожидаемые перемены в ORDER_TYPE_TIME и ORDER_TIME_EXPIRATION.
Далее мы удалили ордер.
>>> OnTrade(12)
|
Теперь при любых действиях со счетом, которые приводят к событиям OnTrade, наш эксперт будет выводить TRADE_ORDER_NOT_FOUND, потому что он рассчитан на отслеживание одного единственного ордера. Если эксперт перезапустить, он "подхватит" другой ордер при его наличии. Но мы остановим эксперт и займемся приготовлениями к решению более насущной задачи.
Кэшировать и контролировать изменения, как правило, требуется не для отдельного ордера или позиции, а для всех или их набора, отобранного по некоторым условиям. Для этих целей разработаем базовый класс-шаблон TradeCache (TradeCache.mqh) и на его основе — прикладные классы для списков ордеров, сделок и позиций.
template<typename T,typename F,typename E>
|
В данном шаблоне буквой T обозначен один из классов семейства TradeState. Как видно, массив таких объектов в виде авто-указателей зарезервирован под именем data.
Буква F описывает тип одного из классов-фильтров (OrderFilter.mqh, включая HistoryOrderFilter, DealFilter.mqh, PositionFilter.mqh), используемых для отбора кэшируемых элементов. В простейшем случае, когда в фильтре нет let-условий, будут кэшироваться все элементы (с учетом выборки истории для объектов из истории).
Буква E соответствует перечислению, в котором находится свойство property, идентифицирующее объекты. Поскольку обычно это свойство — КАКОЙ-ТО_TICKET, в качестве перечисления предполагается целочисленный ENUM_ЧТО-ТО_PROPERTY_INTEGER.
Переменная NOT_FOUND_ERROR предназначена для кода ошибки, возникающей при попытке выделить для чтения не существующий объект, например, ERR_TRADE_POSITION_NOT_FOUND для позиций.
Главный метод класса scan принимает в качестве параметра ссылку на настроенный фильтр (его настройкой должен заняться вызывающий код).
void scan(F &f)
|
В начале метода мы собираем идентификаторы уже кэшированных объектов в массив tickets. Очевидно, что при первом запуске он окажется пустым.
Далее заполняем другой массив objects тикетами актуальных объектов с помощью фильтра. Для каждого нового тикета создаем объект кэширующего монитора T и добавляем в массив data. Для старых объектов анализируем наличие изменений путем вызова data[j][].getChanges(changes) и затем обновляем кэш, вызвав data[j][].update().
ulong objects[];
|
Как нетрудно заметить, в каждой фазе изменения — то есть при добавлении объекта или после его изменения — вызываются некие методы onAdded и onUpdated. Это виртуальные методы-заглушки, с помощью которых сканирование может уведомить программу о соответствующих событиях. Предполагается, что прикладной код реализует класс-наследник с переопределенными версиями этих методов. Мы коснемся этого вопроса чуть ниже, а пока продолжим рассматривать метод scan.
В вышеприведенном цикле все найденные тикеты в массиве tickets обнулены, и потому оставшиеся элементы соответствуют отсутствующим объектам торговой среды. Далее делается их проверка путем вызова getChanges и сравнением кода ошибки с NOT_FOUND_ERROR. Если это действительно так, вызывается еще один виртуальный метод onRemoved. Он возвращает логический флаг (проставляемый вашим прикладным кодом), следует ли удалить элемент из кэша.
for(int j = 0; j < existedBefore; ++j)
|
В самом конце метода scan массив data очищается от нулевых элементов, но здесь данный фрагмент опущен для краткости.
Базовый класс предоставляет стандартные реализации методов onAdded, onRemoved, onUpdated, которые выводят суть событий в журнал. Определив макрос PRINT_DETAILS в своем коде до включения заголовочного файла TradeCache.mqh, можно заказать распечатку всех свойств каждого нового объекта.
virtual void onAdded(const T &state)
|
Метод onUpdated не будем приводить, поскольку он практически повторяет код для вывода изменений из эксперта OrderSnapshot.mq5, показанный выше.
Разумеется, в базовом классе есть средства для получения размера кэша и доступа к конкретному объекту по номеру.
int size() const
|
На основе базового класса TradeCache легко создать конкретные классы для кэширования списков позиций, действующих ордеров и ордеров из истории. Кэширование сделок оставлено в качестве самостоятельного задания.
class PositionCache: public TradeCache<PositionState,PositionFilter,
|
Чтобы подвести итог процессу разработки представленного функционала приведем диаграмму основных классов. Это упрощенный вариант диаграмм UML, которые имеет смысл взять на вооружение при проектировании сложных программ на MQL5.
Диаграмма классов мониторов, фильтров и кэшей торговых объектов
Желтым цветом обозначены шаблоны, белым оставлены абстрактные классы, цветные — конкретные реализации. Сплошные стрелки с закрашенными наконечниками обозначают наследование, пунктирные с полыми — типизацию шаблонов. Пунктирные стрелки с открытыми наконечниками — это использование классами указанных методов друг друга. Связи с ромбами — это композиция (включение одних объектов в другие).
В качестве примера использования кэша создадим эксперт TradeSnapshot.mq5, который будет реагировать на любые изменения торгового окружения из обработчика OnTrade. Для фильтрации и кэширования в коде описано 6 объектов, по 2 (фильтр и кэш) для каждого типа элементов — позиций, действующих ордеров и исторических ордеров.
PositionFilter filter0;
|
Фильтрам не задается каких-либо условий через вызовы метода let, так что в кэш попадут все обнаруженные онлайн объекты. Для ордеров из истории существует дополнительная настройка.
Опционально при запуске можно загрузить в кэш прошлые ордера на заданную глубину истории. Для этого предусмотрена входная переменная HistoryLookup. В ней можно выбрать последние сутки, последнюю неделю (по длительности, а не календарную), месяц (30 дней) или год (360 дней). По умолчанию прошлая история не подгружается (точнее, подгружается только за 1 секунду). Поскольку в эксперте определен макрос PRINT_DETAILS, будьте осторожны со счетами, где большая история: они могут сгенерировать объемный журнал, если не ограничить период.
enum ENUM_HISTORY_LOOKUP
|
В обработчике OnInit сбрасываем кэши (на тот случай, если эксперт перезапущен с новыми параметрами), вычисляем начальную дату истории в переменной origin и вызываем сами OnTrade первый раз.
int OnInit()
|
Обработчик OnTrade довольно минималистичен, поскольку все сложности теперь скрыты внутри классов.
void OnTrade()
|
Сразу после запуска эксперта на чистом счете увидим сообщение:
>>> OnTrade(0)
|
Попробуем выполнить простейший тест-кейс: совершим покупку или продажу на "пустом" счете без открытых позиций и отложенных ордеров. Журнал зафиксирует следующие события (происходящие практически моментально).
Сначала обнаружится активный ордер.
>>> OnTrade(1) OrderCache added: 1311792104 MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING> ENUM_ORDER_PROPERTY_INTEGER Count=14 0 ORDER_TIME_SETUP=2022.04.11 12:34:51 1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00 2 ORDER_TIME_DONE=1970.01.01 00:00:00 3 ORDER_TYPE=ORDER_TYPE_BUY 4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK 5 ORDER_TYPE_TIME=ORDER_TIME_GTC 6 ORDER_STATE=ORDER_STATE_STARTED 7 ORDER_MAGIC=0 8 ORDER_POSITION_ID=0 9 ORDER_TIME_SETUP_MSC=2022.04.11 12:34:51'096 10 ORDER_TIME_DONE_MSC=1970.01.01 00:00:00'000 11 ORDER_POSITION_BY_ID=0 12 ORDER_TICKET=1311792104 13 ORDER_REASON=ORDER_REASON_CLIENT ENUM_ORDER_PROPERTY_DOUBLE Count=7 0 ORDER_VOLUME_INITIAL=0.01 1 ORDER_VOLUME_CURRENT=0.01 2 ORDER_PRICE_OPEN=1.09218 3 ORDER_PRICE_CURRENT=1.09218 4 ORDER_PRICE_STOPLIMIT=0.0 5 ORDER_SL=0.0 6 ORDER_TP=0.0 ENUM_ORDER_PROPERTY_STRING Count=3 0 ORDER_SYMBOL=EURUSD 1 ORDER_COMMENT= 2 ORDER_EXTERNAL_ID= |
Затем этот ордер будет перемещен в историю (при этом поменяются, как минимум, статус, время исполнения и идентификатор позиции).
HistoryOrderCache added: 1311792104 MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING> ENUM_ORDER_PROPERTY_INTEGER Count=14 0 ORDER_TIME_SETUP=2022.04.11 12:34:51 1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00 2 ORDER_TIME_DONE=2022.04.11 12:34:51 3 ORDER_TYPE=ORDER_TYPE_BUY 4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK 5 ORDER_TYPE_TIME=ORDER_TIME_GTC 6 ORDER_STATE=ORDER_STATE_FILLED 7 ORDER_MAGIC=0 8 ORDER_POSITION_ID=1311792104 9 ORDER_TIME_SETUP_MSC=2022.04.11 12:34:51'096 10 ORDER_TIME_DONE_MSC=2022.04.11 12:34:51'097 11 ORDER_POSITION_BY_ID=0 12 ORDER_TICKET=1311792104 13 ORDER_REASON=ORDER_REASON_CLIENT ENUM_ORDER_PROPERTY_DOUBLE Count=7 0 ORDER_VOLUME_INITIAL=0.01 1 ORDER_VOLUME_CURRENT=0.0 2 ORDER_PRICE_OPEN=1.09218 3 ORDER_PRICE_CURRENT=1.09218 4 ORDER_PRICE_STOPLIMIT=0.0 5 ORDER_SL=0.0 6 ORDER_TP=0.0 ENUM_ORDER_PROPERTY_STRING Count=3 0 ORDER_SYMBOL=EURUSD 1 ORDER_COMMENT= 2 ORDER_EXTERNAL_ID= >>> positions: 0, orders: 1, history: 1 |
Обратите внимание, что данные модификации произошли в течение одного вызова OnTrade. Иными словами, пока наша программа анализировала свойства нового ордера (с помощью вызова orders.scan), ордер параллельно был обработан терминалом, и к моменту проверки истории (с помощью вызова history.scan), уже попал в историю. Именно поэтому он числится и там, и там согласно последней строке этого фрагмента лога. Такое поведение нормально для многопоточных программ, и это следует учитывать при их разработке. Но оно не обязательно всегда будет проявляться. Здесь мы просто обращаем на это внимание. При быстром выполнении MQL-программы такую ситуацию обычно не удается застать.
Если бы мы выполняли сначала проверку истории, а потом онлайн-ордеров, то могли бы на первом этапе обнаружить, что ордера еще нет в истории, а на втором — что ордера уже нет онлайн. То есть он на какое-то мгновение мог бы теоретически потеряться. Или, более реальна ситуация — за счет синхронизации истории пропустить ордер в его активной фазе, то есть зафиксировать первый раз сразу в истории.
Напомним, что MQL5 не позволяет синхронизировать торговое окружение целиком, а лишь по частям:
- среди активных ордеров актуальна информация для ордера, для которого только что была вызвана функция OrderSelect или OrderGetTicket;
- среди позиций актуальна информация для позиции, для которой только что была вызвана функция PositionSelect, PositionSelectByTicket или PositionGetTicket;
- для ордеров и сделок истории доступна информация в контексте последнего вызова HistorySelect, HistorySelectByPosition, HistoryOrderSelect, HistoryDealSelect.
Кроме того, напомним, что торговые события (как и любые события MQL5) — это сообщения о произошедших изменениях, помещенные в очередь и извлеченные из очереди отложенным образом, а не непосредственно в момент совершения изменений. Тем более, событие OnTrade происходит после соответствующих событий OnTradeTransaction.
Пробуйте разные конфигурации программы, проводите отладку и формируйте подробные логи, чтобы выбрать наиболее надежный алгоритм для вашей торговой системы.
Вернемся к нашему логу. При следующем срабатывании OnTrade ситуация уже исправлена: кэш активных ордеров обнаружил удаление ордера. Попутно кэш позиций "увидел" открытую позицию.
>>> OnTrade(2) PositionCache added: 1311792104 MonitorInterface<ENUM_POSITION_PROPERTY_INTEGER,ENUM_POSITION_PROPERTY_DOUBLE,ENUM_POSITION_PROPERTY_STRING> ENUM_POSITION_PROPERTY_INTEGER Count=9 0 POSITION_TIME=2022.04.11 12:34:51 1 POSITION_TYPE=POSITION_TYPE_BUY 2 POSITION_MAGIC=0 3 POSITION_IDENTIFIER=1311792104 4 POSITION_TIME_MSC=2022.04.11 12:34:51'097 5 POSITION_TIME_UPDATE=2022.04.11 12:34:51 6 POSITION_TIME_UPDATE_MSC=2022.04.11 12:34:51'097 7 POSITION_TICKET=1311792104 8 POSITION_REASON=POSITION_REASON_CLIENT ENUM_POSITION_PROPERTY_DOUBLE Count=8 0 POSITION_VOLUME=0.01 1 POSITION_PRICE_OPEN=1.09218 2 POSITION_PRICE_CURRENT=1.09214 3 POSITION_SL=0.00000 4 POSITION_TP=0.00000 5 POSITION_COMMISSION=0.0 6 POSITION_SWAP=0.00 7 POSITION_PROFIT=-0.04 ENUM_POSITION_PROPERTY_STRING Count=3 0 POSITION_SYMBOL=EURUSD 1 POSITION_COMMENT= 2 POSITION_EXTERNAL_ID= OrderCache removed: 1311792104 >>> positions: 1, orders: 0, history: 1 |
Спустя некоторое время закроем позицию. Поскольку у нас в коде первым проверяется кэш позиций (positions.scan), в журнал попадают изменения закрываемой позиции.
>>> OnTrade(8)
|
Далее в этом же вызове OnTrade засекается появление ордера на закрытие и его моментальный перенос в историю (опять же за счет его быстрой параллельной обработки терминалом).
OrderCache added: 1311796883 MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING> ENUM_ORDER_PROPERTY_INTEGER Count=14 0 ORDER_TIME_SETUP=2022.04.11 12:39:55 1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00 2 ORDER_TIME_DONE=1970.01.01 00:00:00 3 ORDER_TYPE=ORDER_TYPE_SELL 4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK 5 ORDER_TYPE_TIME=ORDER_TIME_GTC 6 ORDER_STATE=ORDER_STATE_STARTED 7 ORDER_MAGIC=0 8 ORDER_POSITION_ID=1311792104 9 ORDER_TIME_SETUP_MSC=2022.04.11 12:39:55'710 10 ORDER_TIME_DONE_MSC=1970.01.01 00:00:00'000 11 ORDER_POSITION_BY_ID=0 12 ORDER_TICKET=1311796883 13 ORDER_REASON=ORDER_REASON_CLIENT ENUM_ORDER_PROPERTY_DOUBLE Count=7 0 ORDER_VOLUME_INITIAL=0.01 1 ORDER_VOLUME_CURRENT=0.01 2 ORDER_PRICE_OPEN=1.09222 3 ORDER_PRICE_CURRENT=1.09222 4 ORDER_PRICE_STOPLIMIT=0.0 5 ORDER_SL=0.0 6 ORDER_TP=0.0 ENUM_ORDER_PROPERTY_STRING Count=3 0 ORDER_SYMBOL=EURUSD 1 ORDER_COMMENT= 2 ORDER_EXTERNAL_ID= HistoryOrderCache added: 1311796883 MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING> ENUM_ORDER_PROPERTY_INTEGER Count=14 0 ORDER_TIME_SETUP=2022.04.11 12:39:55 1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00 2 ORDER_TIME_DONE=2022.04.11 12:39:55 3 ORDER_TYPE=ORDER_TYPE_SELL 4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK 5 ORDER_TYPE_TIME=ORDER_TIME_GTC 6 ORDER_STATE=ORDER_STATE_FILLED 7 ORDER_MAGIC=0 8 ORDER_POSITION_ID=1311792104 9 ORDER_TIME_SETUP_MSC=2022.04.11 12:39:55'710 10 ORDER_TIME_DONE_MSC=2022.04.11 12:39:55'711 11 ORDER_POSITION_BY_ID=0 12 ORDER_TICKET=1311796883 13 ORDER_REASON=ORDER_REASON_CLIENT ENUM_ORDER_PROPERTY_DOUBLE Count=7 0 ORDER_VOLUME_INITIAL=0.01 1 ORDER_VOLUME_CURRENT=0.0 2 ORDER_PRICE_OPEN=1.09222 3 ORDER_PRICE_CURRENT=1.09222 4 ORDER_PRICE_STOPLIMIT=0.0 5 ORDER_SL=0.0 6 ORDER_TP=0.0 ENUM_ORDER_PROPERTY_STRING Count=3 0 ORDER_SYMBOL=EURUSD 1 ORDER_COMMENT= 2 ORDER_EXTERNAL_ID= >>> positions: 1, orders: 1, history: 2 |
В кэше истории числятся уже 2 ордера, но кэши позиций и активных ордеров, которые анализировались раньше кэша истории, еще не применили эти изменения.
Но уже в следующем событии OnTrade мы видим, что позиция закрыта и рыночный ордер исчез.
>>> OnTrade(9)
|
Если мониторить кэши на каждом тике (или раз в секунду, но не только по событиям OnTrade), мы увидим изменения свойств ORDER_PRICE_CURRENT и POSITION_PRICE_CURRENT "на лету". Также будет меняться POSITION_PROFIT.
Наши классы не обладают персистентностью, то есть "живут" только в оперативной памяти и не умеют сохранять и восстанавливать свое состояние в каких-либо долговременных хранилищах, таких как файлы. Это означает, что программа может пропустить изменение, которое произошло между сеансами работы в терминале. Если вам необходим такой функционал, его следует реализовать самостоятельно. В будущем, в 7-й Части книги мы рассмотрим встроенную в MQL5 поддержку базы данных SQLite, которая предоставляет наиболее эффективный и удобный способ для хранения кэша торгового окружения и подобных табличных данных.