- Создание и удаление пользовательских символов
- Свойства пользовательских символов
- Установка маржинальных коэффициентов
- Настройка котировочных и торговых сессий
- Добавление, замена и удаление котировок
- Добавление, замена и удаление тиков
- Трансляция изменений стакана заявок
- Особенности торговли с пользовательскими символами
Добавление, замена и удаление тиков
MQL5 API позволяет формировать историю пользовательского символа не только на уровне баров, но и тиков. Таким образом, можно добиться большего реализма при тестировании и оптимизации экспертов, а также эмулировать в реальном времени обновление графиков пользовательских инструментов, транслируя на них свои тики. Совокупность переданных системе тиков автоматически учитывается при формировании баров. Иными словами, нет необходимости вызывать функции из предыдущего раздела, оперирующие структурами MqlRates, если более детальная информация об изменениях цен за тот же период предоставлена в виде тиков, а именно массивов структур MqlTick. Единственное преимущество, которое дают побаровые котировки MqlRates, — быстродействие и экономия памяти, когда это в приоритете.
Для добавления тиков существует 2 функции CustomTicksAdd и CustomTicksReplace. Первая производит добавление интерактивных тиков, которые поступают в окно Обзора рынка (и оттуда они автоматически переносятся терминалом в базу тиков) и генерируют соответствующие события в MQL-программах. Вторая — записывает тики напрямую в базу тиков.
int CustomTicksAdd(const string symbol, const MqlTick &ticks[], uint count = WHOLE_ARRAY)
Функция CustomTicksAdd добавляет в ценовую историю пользовательского инструмента под именем symbol данные из массива ticks. По умолчанию, если параметр count равен WHOLE_ARRAY, добавляется весь массив, но при необходимости можно указать меньшее количество и загрузить лишь часть тиков.
При этом важно, что пользовательский символ должен быть уже выбран в окне Обзора рынка в момент вызова функции. Для символов, не выбранных в Обзор рынка, необходимо использовать функцию CustomTicksReplace (см. далее).
Массив тиковых данных должен быть упорядочен по времени в порядке возрастания, то есть требуется, чтобы выполнялось условия ticks[i].time_msc <= ticks[j].time_msc для всех i < j.
Функция возвращает количество добавленных тиков либо -1 в случае ошибки.
Функция CustomTicksAdd транслирует тики на график так же, как если бы они приходили от сервера брокера. Обычно функция применяется для одного или нескольких тиков. В этом случае они "проигрываются" в окне Обзора рынка и из него сохраняются в базе тиков.
Однако при большом объеме данных, передаваемых за один вызов, функция меняет свое поведение для экономии ресурсов. Если передается более 256 тиков, они делятся на две части. Первая часть (большая) сразу напрямую записывается в базу тиков (как это делает CustomTicksReplace). Вторая часть, состоящая из последних (наиболее актуальных) 128 тиков, передается в окно Обзор рынка и после этого сохраняется терминалом в базе.
Структура MqlTick имеет два поля со значением времени: time (время тика в секундах) и time_msc (время тика в миллисекундах). Оба значения ведут отсчет от 01 января 1970 года. Заполненное (ненулевое) поле time_msc имеет приоритет перед time. При этом time заполняется в секундах в результате пересчета по формуле time_msc / 1000. Если поле time_msc равно нулю, используется время из поля time, причем поле time_msc в свою очередь получает значение в миллисекундах из формулы time * 1000. Если оба поля равны нулю, в тик проставляется текущее время сервера (с точностью до миллисекунд).
Из двух полей, описывающих объем, volume_real имеет высший приоритет по сравнению с volume.
В зависимости от того, какие другие поля заполнены в конкретном элементе массива (структуре MqlTick), система устанавливает для сохраняемого тика флаги в поле flags:
- ticks[i].bid — TICK_FLAG_BID (тик изменил цену Bid)
- ticks[i].ask — TICK_FLAG_ASK (тик изменил цену Ask)
- ticks[i].last — TICK_FLAG_LAST (тик изменил цену последней сделки)
- ticks[i].volume или ticks[i].volume_real — TICK_FLAG_VOLUME (тик изменил объем)
Если значение какого-то поля меньше или равно нулю, соответствующий ему флаг не записываются в поле flags.
Флаги TICK_FLAG_BUY и TICK_FLAG_SELL в историю пользовательского инструмента не добавляются.
Функция CustomTicksReplace полностью заменяет ценовую историю пользовательского инструмента в указанном временном интервале данными из передаваемого массива.
int CustomTicksReplace(const string symbol, long from_msc, long to_msc,
const MqlTick &ticks[], uint count = WHOLE_ARRAY)
Интервал задается параметрами from_msc и to_msc, в миллисекундах с 01.01.1970. Оба значения входят в интервал.
Массив ticks должен быть упорядочен в хронологическом порядке прихода тиков, что соответствует возрастанию, а точнее, неубыванию времени, так как в потоке тиков нередко подряд идут тики с одним и тем же временем с точностью до миллисекунды.
Параметр count позволяет обрабатывать не весь массив, а лишь его часть.
Замена тиков производится последовательно день за днём до времени указанного в to_msc либо до возникновения ошибки в очередности тиков. Сначала обрабатывается первый день из указанного диапазона, затем следующий, и так далее. Как только обнаружится несоответствие времени тика порядку возрастания (неубывания), то процесс замены тиков прекращается на текущем дне. При этом тики за предыдущие дни будут успешно заменены, а текущий день (на момент неправильного тика) и все оставшиеся дни в указанном интервале останутся без изменения. Функция вернет -1, причем код ошибки в _LastError равен 0 ("нет ошибки").
Если в массиве ticks отсутствуют данные за какой-то период внутри общего интервала от from_msc до to_msc (включительно), то после выполнения функции в истории пользовательского инструмента образуется "дыра", соответствующая пропущенным данным.
Если в базе тиков в указанном интервале времени данные отсутствуют, то CustomTicksReplace просто добавит в нее тики из массива ticks.
Удалить все тики в указанном временном интервале позволяет функция CustomTicksDelete.
int CustomTicksDelete(const string symbol, long from_msc, long to_msc)
Имя редактируемого пользовательского инструмента задается в параметре symbol, а очищаемый интервал — параметрами from_msc и to_msc (включительно), в миллисекундах.
Функция возвращает количество удаленных тиков либо -1 в случае ошибки.
Внимание! Удаление тиков с помощью CustomTicksDelete приводит к автоматическому удалению соответствующих баров! Однако вызов CustomRatesDelete, то есть удаление баров, не удаляет тики!
Для освоения материала на практике решим с помощью новых функций несколько прикладных задач.
Для начала коснемся такой интересной задачи, как создание пользовательского инструмента на основе реального символа, но с прореживанием тиков. Это позволит добиться ускорения тестирования и оптимизации, а также снизит потребление ресурсов (в первую очередь, оперативной памяти) по сравнению с режимом по реальным тикам, оставив при этом приемлемое, близкое к идеальному, качество процесса.
Ускорение тестирования и оптимизации
Трейдеры часто задаются вопросом, каким образом можно ускорить оптимизацию и тестирование экспертов. Среди возможных решений есть очевидные, для которых достаточно изменить настройки (когда это допустимо), а есть более трудоемкие, которые требуют адаптации эксперта или тестовой среды.
Среди первого класса решений можно отметить:
· уменьшение пространства оптимизации за счет исключения некоторых параметров или уменьшения их шага;
· уменьшение срока оптимизации;
· переход на режим моделирования тиков более низкого качества (например, от реальных к OHLC M1);
· включение опции расчета прибыли в пунктах, вместо денег;
· апгрейд компьютера;
· использование MQL Cloud или дополнительных компьютеров локальной сети.
Среди второго класса решений, связанных с разработкой, упомянем:
· профилировку кода, на основе которой можно ликвидировать "узкие" места в коде;
· по возможности, использовать экономный расчет индикаторов — без директивы #property tester_everytick_calculate;
· перенос алгоритмов индикаторов (если они используются) непосредственно в код советника: вызовы индикаторов налагают определенные накладные расходы;
· исключение работы с графикой и объектами;
· кэширование расчетов, если возможно;
· уменьшение количества одновременно открытых позиций и выставленных ордеров (их обсчет на каждом тике может стать заметным при большом числе);
· полная виртуализация расчетов, ордеров, сделок и позиций: встроенный механизм денежного учета в силу своей универсальности, поддержки мультивалютности и прочих особенностей имеет свои накладные расходы, которые можно исключить, выполняя аналогичные действия в коде на MQL5 (хотя этот вариант наиболее трудоемок);
Прореживание тиков относится к промежуточному классу решений: оно требует программного создания пользовательского символа, но зато не затрагивает исходный код эксперта.
Пользовательский символ с прореженными тиками будет создаваться скриптом CustomSymbolFilterTicks.mq5. Исходным инструментом будет выступать рабочий символ графика, на котором запускается скрипт. Во входных параметрах можно указать папку для пользовательского символа и начальную дату обработки истории. По умолчанию, если дата не задана, делается расчет для последних 120 дней.
input string CustomPath = "MQL5Book\\Part7"; // Custom Symbol Folder
|
Название символа формируется из имени исходного инструмента и суффикса ".TckFltr". Позднее мы добавим к нему обозначение метода прореживания тиков.
string CustomSymbol = _Symbol + ".TckFltr";
|
Для удобства пользователя, в обработчике OnStart предусмотрена возможность удалить предыдущую копию символа, если она уже существует.
void OnStart()
|
Далее с согласия пользователя создается символ. Заполнение истории тиковыми данными производится во вспомогательной функции GenerateTickData. В случае успеха, скрипт добавляет новый символ в Обзор рынка и открывает график.
if(IDYES == MessageBox(StringFormat("Create new custom symbol '%s'?", CustomSymbol),
|
Функция GenerateTickData обрабатывает тики в цикле порциями, по суткам. Тики за сутки запрашиваются с помощью вызова CopyTicksRange. Далее их надо тем или иным способом проредить, что делегировано классу TickFilter, который мы покажем ниже. Наконец, массив тиков добавляется в историю пользовательского символа с помощью CustomTicksReplace.
bool GenerateTickData()
|
На всех этапах проводится контроль ошибок и подсчет обработанных тиков. В конце выводим в журнал количество исходных тиков и оставшихся, а также коэффициент "сжатия".
Теперь обратимся непосредственно к методике прореживания тиков. Очевидно, что подходов может быть много, и каждый лучше или хуже подойдет к конкретной торговой стратегии. Мы предложим 3 базовых варианта, объединенных в классе TickFilter (TickFilter.mqh). Также, для полноты картины, там поддержан и режим копирования тиков без прореживания.
Таким образом, в классе реализованы следующие режимы:
- без прореживания;
- пропуск последовательностей тиков с монотонным изменением цены без разворота (а-ля "зиг-заг");
- пропуск колебаний цен в пределах спреда;
- запись только тиков с фрактальной конфигурацией, когда цена Bid или Ask представляет собой экстремум между двумя соседними тиками.
Данные режимы описаны в виде элементов перечисления FILTER_MODE.
class TickFilter
|
Каждый из режимов реализован отдельным статическим методом, принимающим на вход массив тиков, который необходимо проредить. Редактирование массива производится по месту (без выделения нового выходного массива).
static int filterBySequences(MqlTick &data[]);
|
Все методы возвращают количество оставшихся тиков (уменьшенный размер массива).
Для унификации выполнения процедуры в разных режимах предусмотрен метод filter. Для режима NONE массив data просто остается без изменений.
static int filter(FILTER_MODE mode, MqlTick &data[])
|
Например, вот как реализована фильтрация по монотонным последовательностям тиков в методе filterBySequences.
static int filterBySequences(MqlTick &data[])
|
А вот как выглядит прореживание по фракталам.
static int filterByFractals(MqlTick &data[])
|
Давайте последовательно создадим пользовательский символ для EURUSD в нескольких режимах прореживания и сравним их показатели, то есть степень "сжатия", насколько ускорится тестирование и как изменятся торговые показатели эксперта.
Например, прореживание последовательностей тиков дает такие результаты (для полуторагодичной истории на MQ Demo).
Create new custom symbol 'EURUSD.TckFltr-SE'?
|
Для режимов сглаживания колебаний и по фракталам показатели другие:
EURUSD.TckFltr-FL will be updated
|
Для практических торговых экспериментов на основе прореженных тиков нам потребуется эксперт. Возьмем адаптированную версию BandOsMAticks.mq5, в которой по сравнению с оригиналом включена торговля на каждом тике (в методе SimpleStrategy::trade отключена строка if(lastBar == iTime(_Symbol, _Period, 0)) return false;), а значения сигнальных индикаторов берутся с баров 0 и 1 (раньше были только завершенные бары 1 и 2).
Запустим эксперт на диапазоне дат с начала 2021 года по 1 июня 2022. Настройки прилагаются в файле MQL5/Presets/MQL5Book/BandOsMAticks.set. Общее поведение кривой баланса во всех режимах достаточно схожее.
Совмещенные графики балансов тестов в разных режимах по тикам
Смещение эквивалентных экстремумов разных кривых по горизонтали вызвано тем, что стандартный график отчета использует для горизонтальной координаты не время, а количество трейдов, которое, разумеется, отличается из-за точности срабатывания торговых сигналов по разным базам тиков.
Различия в показателях приведены в следующей таблице (N — количество трейдов, $ — прибыль, PF — профит-фактор, RF — фактор восстановления, DD — просадка):
Режим |
Тики |
Время |
Память |
N |
$ |
PF |
RF |
DD |
---|---|---|---|---|---|---|---|---|
Реальные |
31002919 |
02:45.251 |
835 Mb |
962 |
166.24 |
1.32 |
2.88 |
54.99 |
Эмуляция |
25808139 |
01:58.131 |
687 Mb |
928 |
171.94 |
1.34 |
3.44 |
47.64 |
OHLC M1 |
2084820 |
00:11.094 |
224 Mb |
856 |
193.52 |
1.39 |
3.97 |
46.55 |
Sequence |
16310236 |
01:24.784 |
559 Mb |
860 |
168.95 |
1.34 |
2.92 |
55.16 |
Flutter |
21362616 |
01:52.172 |
623 Mb |
920 |
179.75 |
1.37 |
3.60 |
47.28 |
Fractal |
12270854 |
01:04.756 |
430 Mb |
866 |
142.19 |
1.27 |
2.47 |
54.80 |
Будем считать тест по реальным тикам наиболее достоверным и оценивать остальные по степени близости к нему. Очевидно, что наибольшую скорость и меньшие затраты ресурсов за счет существенной потери точности показал режим OHLC M1 (режим по ценам открытия не рассматривался). У него сверх-оптимистичные финансовые результаты.
Среди трех режимов с искусственно прореженными тиками наиболее близким к реальному по комплексу показателей является Sequence. По времени он в 2 раза быстрее реального, а по памяти в 1.5 раза экономичнее. Режим Flutter, судя по всему, лучше сохраняет оригинальное количество сделок. Наиболее быстрый и наименее требовательный к памяти режим по фракталам, конечно, проигрывает OHLC M1, но зато не завышает торговые оценки.
Следует иметь в виду, что алгоритмы прореживания тиков могут по-разному подходить или, наоборот, давать плохие результаты с различными торговыми стратегиями, финансовыми инструментами и даже тиковой историей конкретного брокера. Проводите исследования с вашими экспертами и в вашей рабочей среде.
В рамках второго примера работы с пользовательскими символами рассмотрим интересную возможность, которую предоставляет трансляция тиков с помощью CustomTicksAdd.
Как известно, многие трейдеры любят применять в своей практике торговые панели — программы с интерактивными элементами управления для выполнения произвольных торговых действий вручную. Отрабатывать навыки работы с ними приходится в основном в режиме онлайн, потому что тестер налагает некоторые ограничения. Прежде всего, в тестере не поддерживаются события на графике и нанесенных на него объектах. Из-за этого элементы управления перестают функционировать. Также в тестере нельзя наносить произвольные объекты для графической разметки.
Попробуем решить эти проблемы собственными силами.
Мы можем генерировать пользовательский символ по историческим тикам в замедленном режиме. Тогда график такого символа станет представлять собой аналог визуального тестера.
Данный подход имеет несколько преимуществ:
- стандартное поведение всех событий чарта;
- интерактивное нанесение и настройка индикаторов;
- интерактивное нанесение и настройка объектов;
- переключение таймфрейма на лету;
- тест на истории вплоть до текущего времени, включая сегодняшний день (стандартный тестер не позволяет тестировать сегодня).
По поводу последнего пункта отметим, что разработчики MetaTrader 5 намеренно запретили проверку торговли на последнем (текущем) дне, хотя она бывает нужна для оперативного поиска ошибок (в коде или в торговой стратегии).
Также потенциально интересна модификация цен на лету (увеличение спреда, например).
На основе графика подобного кастом-символа мы сможем позднее реализовать эмулятор ручной торговли на исторических данных.
Генератором символа будет неторгующий эксперт CustomTester.mq5. В его входных параметрах предусмотрим указание размещения нового кастом-символа в иерархии символов, начальную дату в прошлом для трансляции тиков (и построения котировок кастом-символа), а также таймфрейм для графика, который автоматически будет открыт для визуального тестирования.
input string CustomPath = "MQL5Book\\Part7"; // Custom Symbol Folder
|
Имя нового символа конструируется из имени символа текущего графика и суффикса ".Tester".
string CustomSymbol = _Symbol + ".Tester"; |
Если начальная дата в параметрах не задана, эксперт сделает отступ назад на 120 дней от текущей даты.
const uint DailySeconds = 60 * 60 * 24;
|
Тики будут считываться из истории реальных тиков рабочего символа пакетами сразу за целый день. Указатель на считываемый день хранится в переменной Cursor.
bool FirstCopy = true;
|
Подлежащие воспроизведению тики одних суток будут запрашиваться в массив Ticks, откуда мелкими порциями размером Step транслироваться на график кастом-символа.
MqlTick Ticks[]; // тики для "текущего" дня в прошлом
|
Для воспроизведения тиков с постоянной скоростью запустим таймер в OnInit.
void OnInit()
|
Генерацию тиков поручим функции GenerateData. Сразу после запуска, когда флаг InitDone сброшен, попытаемся создать новый символ или очистить прежние котировки и тики, если кастом-символ уже существует.
bool GenerateData()
|
На данном этапе мы кое-что опустим в месте (A) и вернемся к данному моменту позднее.
После создания символа, выбираем его в Обзор рынка и открываем для него чарт.
SymbolSelect(CustomSymbol, true);
|
Здесь тоже пропущена пара строк (B), связанных с будущими усовершенствованиями, но пока они не требуются.
Если символ уже создан, запускаем трансляцию тиков пакетами Step тиков, но не более 256. Это ограничение связано с особенностью функции CustomTicksAdd.
else
|
Вспомогательная функция GenerateTicks транслирует тики порциями по Step тиков (но не более 256), считывая их из суточного массива Ticks по смещению Index. Когда массив пуст или мы прочитали его до конца, запрашиваем тики следующего дня путем вызова FillTickBuffer.
bool GenerateTicks()
|
Функция FillTickBuffer использует в своей работе CopyTicksRange.
bool FillTickBuffer()
|
При остановке эксперта будем также закрывать зависимый график (чтобы он не дублировался при следующем запуске).
void OnDeinit(const int)
|
На этом эксперт можно было бы считать завершенным, однако существует одна проблема. Дело в том, что по тем или иным причинам свойства кастом-символа не копируются один в один из исходного рабочего символа, по крайней мере, в текущей реализации MQL5 API. Это касается даже очень важных свойств, таких как SYMBOL_TRADE_TICK_VALUE, SYMBOL_TRADE_TICK_SIZE. Если мы выведем на печать значения этих свойств сразу после вызова CustomSymbolCreate(CustomSymbol, CustomPath, _Symbol), то увидим там нули.
Чтобы организовать проверку свойств, их сравнение и при необходимости исправление был написан специальный класс CustomSymbolMonitor (CustomSymbolMonitor.mqh), производный от SymbolMonitor. С его внутренним устройством предлагается разобраться самостоятельно, а здесь мы лишь приведем публичный интерфейс.
Конструкторы позволяют создать монитор пользовательского символа с указанием образцового рабочего символа (по имени в строке или из объекта SymbolMonitor), который служит источником настроек.
class CustomSymbolMonitor: public SymbolMonitor
|
Поскольку кастом-символы, в отличие от стандартных символов, позволяют задавать свои свойства, в классе добавлена тройка set-методов. Они, в частности, используются для пакетного переноса свойств символа-образца и проверки успешности этих действий в других методах класса.
Теперь мы можем вернуться к генератору кастом-символа и фрагменту его исходного кода, обозначенного ранее комментарием (A).
// (A) проверяем важные свойства и устанавливаем их в "ручном" режиме
|
Сейчас эксперт CustomTester.mq5 уже можно запускать и наблюдать, как в автоматически открытом графике динамически формируются котировки, а в окне Обзора рынка пробрасываются тики из истории.
Однако это делается с постоянной скоростью 32 тика за 0.1 секунды. Желательно на лету менять скорость воспроизведения по желанию пользователя, как в большую, так и в меньшую сторону. Такое управление можно организовать, например, с клавиатуры.
Следовательно, требуется добавить обработчик OnChartEvent. Как мы знаем, для события CHARTEVENT_KEYDOWN в программу поступает код нажатой клавиши в параметре lparam, и мы передаем его в функцию CheckKeys (см. ниже). Некий фрагмент (C), тесно связанный с (B), пока пришлось отложить — мы к ним скоро вернемся.
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
|
В функции CheckKeys обрабатываем клавиши "стрелка вверх" и "стрелка вниз" для увеличения и уменьшения скорости воспроизведения. Кроме того, клавиша "пауза" позволяет и совсем приостановить процесс "тестирования" (трансляции тиков). Повторное нажатие "паузы" возобновляет работу с прежней скоростью.
void CheckKeys(const long key)
|
Новый код можно проверить в действии, предварительно убедившись, что активным является график, на котором работает эксперт. Напомним, что события клавиатуры поступают только в активное окно. В этом кроется еще одна проблема нашего тестера.
Поскольку пользователь должен выполнять торговые действия на графике кастом-символа, окно с генератором практически всегда будет находиться в фоновом режиме. Переключаться на окно генератора, чтобы на время остановить поток тиков, а потом возобновить — не практично. Поэтому требуется неким образом организовать интерактивное управление с клавиатуры непосредственно из окна кастом-символа.
Для этой цели подойдет специальный индикатор, который мы можем автоматически добавить в открываемое окно кастом-символа. Индикатор будет перехватывать события клавиатуры в своем окне (окне с кастом-символом) и посылать их в окно генератора.
Исходный код индикатора прилагается в файле KeyboardSpy.mq5. Разумеется, индикатор не имеет диаграмм. Пара входных параметров предназначена для получения идентификатора графика HostID, куда следует отправлять сообщения, и кода пользовательского события EventID, в которое будут "упаковываться" интерактивные события.
#property indicator_chart_window
|
Основная "работа" выполняется в обработчике OnChartEvent — она элементарна.
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
|
Обратите внимание, что все выбранные нами "горячие клавиши" являются простыми, то есть не используют сочетаний с клавишами состояния клавиатуры, такими как Ctrl или Shift. Это сделано вынужденно, потому что внутри индикаторов, созданных программно (в частности, через iCustom), состояние клавиатуры не считывается. Иными словами, вызов TerminalInfoInteger(TERMINAL_KEYSTATE_XYZ) всегда возвращает 0. В вышеприведенном обработчике мы добавили его просто для демонстрации, чтобы вы могли при желании убедиться в данном ограничении, выведя поступающие параметры на "принимающей стороне".
Однако одиночные нажатия "стрелок" и "паузы" будут передаваться в "родительский" чарт нормально, и нам этого достаточно. Дело осталось за малым: интегрировать индикатор с экспертом.
В пропущенном ранее фрагменте (B), во время инициализации генератора, создадим индикатор и добавим его на график кастом-символа.
#define EVENT_KEY 0xDED // пользовательское событие
|
Далее во фрагменте (C) обеспечим прием пользовательских сообщений из индикатора и их передачу в уже известную функцию CheckKeys.
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
|
Таким образом, управлять скоростью воспроизведения теперь можно как на графике с экспертом, так и на генерируемом им графике кастом-символа.
С новым инструментарием вы можете попробовать интерактивную работу с графиком, "живущим прошлой жизнью". На график выводится комментарий с текущей скоростью воспроизведения или метка паузы.
На графике с экспертом в комментарий выводится время "текущих" транслируемых тиков.
Эксперт, воспроизводящий историю тиков (и котировок) реального символа
В этом окне пользователю в принципе нечего делать (если только удалить эксперт и прекратить генерацию кастом-символа). Сам процесс трансляции тиков здесь не виден. Более того, поскольку эксперт автоматически открывает чарт кастом-символа (где и обновляются исторические котировки), то именно тот становится активным. А для получения вышеприведенного скриншота нам потребовалось специально ненадолго переключиться на исходный график.
Поэтому вернемся на график кастом-символа. То, как он плавно и поступательно обновляется в прошлом времени, — это уже здорово, но на нем нельзя проводить торговые эксперименты. Например, если набросить на него привычную торговую панель, её элементы управления хоть и будут формально работать, не приведут к сделкам — из-за отсутствия кастом-символа на сервере, получим ошибки. Как мы знаем, эта особенность проявляется у любых программ, которые специально не адаптированы для кастом-символов. Покажем пример того, как можно виртуализировать торговлю кастом-символом.
Вместо торговой панели (в целях упрощения примера, но без потери общности) возьмем за основу максимально простой эксперт CustomOrderSend.mq5, который умеет выполнять несколько торговых действий по нажатию клавиш:
- 'B' — покупка по рынку;
- 'S' — продажа по рынку;
- 'U' — установка лимитного ордера на покупку;
- 'L' — установка лимитного ордера на продажу;
- 'C' — закрыть все позиции;
- 'D' — удалить все ордера;
- 'R' — вывести в журнал торговый отчет.
Во входных параметрах эксперта зададим объем одной сделки (по умолчанию, минимальный лот) и расстояние до уровней стоп-лосс и тейк-профит в пунктах.
input double Volume; // Volume (0 = minimal lot)
|
Если Distance2SLTP оставлен равным нулю, защитные уровни в рыночных ордерах на проставляются, а отложенные ордера не формируются. Когда Distance2SLTP имеет ненулевое значение, именно оно используется как расстояние от текущей цены при установке отложенного ордера (либо вверх, либо вниз, в зависимости от команды).
С учетом ранее представленных классов из MqlTradeSync.mqh, вышеописанная логика преобразуется в следующий исходный код.
#include <MQL5Book/MqlTradeSync.mqh>
|
Как мы видим, здесь используются как стандартные функции торгового API, так и методы MqlTradeRequestSync, которые, опосредованно, также в конечном счете вызывают множество встроенных функций. Нам необходимо "заставить" этот эксперт торговать кастом-символом.
Наиболее простая, хотя и трудоемкая идея заключатся в том, чтобы подменить все стандартные функции на собственные аналоги, которые вели бы подсчет ордеров, сделок, позиций и финансовых показателей в неких структурах. Разумеется, такое возможно только в случаях, когда мы имеем исходный код эксперта, который следует адаптировать.
Экспериментальная реализация подхода продемонстрирована в прилагаемом файле CustomTrade.mqh. С полным кодом можно ознакомиться самостоятельно, а в рамках книги мы перечислим лишь основные моменты.
Прежде всего отметим, что многие расчеты сделаны в упрощенном виде, многие режимы не поддерживаются, и не делается полная проверка данных на корректность. Используйте исходный код, как отправную точку для собственных разработок.
Весь код обернут в пространство имен CustomTrade для исключения конфликтов.
Сущности ордер, сделка и позиция формализованы в виде соответствующих классов CustomOrder, CustomDeal, CustomPosition. Все они являются наследниками класса MonitorInterface<I,D,S>::TradeState. Напомним, что в этом классе уже автоматически поддержано формирование массивов целочисленных, вещественных и строковых свойств для каждого типа объектов и его специфических троек перечислений. Например, CustomOrder выглядит так:
class CustomOrder: public MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,
|
Обратите внимание, что в виртуальном окружении старого "текущего" времени нельзя использовать функцию TimeCurrent и вместо неё берется последнее известное время кастом-символа SymbolInfoInteger(symbol, SYMBOL_TIME).
В процессе виртуальной торговли текущие объекты и их история накапливается в массивах соответствующих классов.
AutoPtr<CustomOrder> orders[];
|
Метафора выделения ордеров, сделок и позиций потребовалась для имитации аналогичного подхода во встроенных функциях. Для них в пространстве имен CustomTrade созданы "двойники", которые подменяют оригиналы с помощью директив макро-подстановки.
#define HistorySelect CustomTrade::MT5HistorySelect
|
Вот, например, как реализована функция MT5HistorySelectByPosition.
bool MT5HistorySelectByPosition(long id)
|
Как нетрудно заметить, все функции этой группы имеют префикс "MT5", чтобы было сразу понятно их "двойное" назначение и легко было отличить от функций второй группы.
Вторая группа функций в пространстве CustomTrade выполняет утилитарные действия: проверяет и обновляет состояния ордеров, сделок и позиций, создает новые и удаляет старые объекты в соответствии с обстановкой. В частности, среди них есть функции CheckPositions и CheckOrders, которые можно вызывать по таймеру или в ответ на действия пользователя. Но это можно и не делать, если использовать пару других функций, предназначенных для отображения текущего и исторического состояния виртуального торгового счета:
- string ReportTradeState() — возвращает многострочный текст со списком открытых позиций и выставленных ордеров;
- void PrintTradeHistory() — выводит в журнал историю ордеров и сделок.
Эти функции самостоятельно вызывают CheckPositions и CheckOrders, чтобы предоставить вам актуальную информацию.
Кроме того имеется функция для визуализации позиций и действующих ордеров на графике в виде объектов: DisplayTrades.
Заголовочный файл CustomTrade.mqh следует подключать в эксперт до прочих заголовков, чтобы подмена макросов имела эффект во всех последующих строках исходных кодов.
#include <MQL5Book/CustomTrade.mqh>
|
Этого достаточно, чтобы приведенный ранее алгоритм CustomOrderSend.mq5 без каких-либо изменений стал "торговать" в виртуальном окружении на основе текущего кастом-символа (для которого не нужен сервер или стандартный тестер).
Для оперативного отображения состояния запустим секундный таймер и периодически будем менять комментарий, а также отображать графические объекты.
int OnInit()
|
Для построения отчета по нажатию 'R' дополним обработчик OnChartEvent.
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
|
Наконец, все готово для проверки нового программного комплекса в действии.
Запустим генератор кастом-символа CustomTester.mq5 на EURUSD. На открывшемся графике "EURUSD.Tester" запустим CustomOrderSend.mq5 и начнем "торговать". Ниже показано изображение процесса тестирования.
Виртуальная торговля на графике пользовательского символа
Здесь видны 2 открытых длинных позиции (с защитными уровнями) и отложенный лимитный ордер на продажу.
Спустя некоторое время одна из позиций закрывается (обозначена ниже пунктирной синей линией со стрелкой), а отложенный ордер на продажу срабатывает (красная линия со стрелкой), в результате чего получаем следующую картину.
Виртуальная торговля на графике пользовательского символа
После закрытия всех позиций (части — по тейк-профиту, а остальных — по команде пользователя), был заказан отчет нажатием 'R'.
History Orders: (1) #1 ORDER_TYPE_BUY 2022.02.15 01:20:50 -> 2022.02.15 01:20:50 L=0.01 @ 1.1306 (4) #2 ORDER_TYPE_SELL_LIMIT 2022.02.15 02:34:29 -> 2022.02.15 18:10:17 L=0.01 @ 1.13626 [sell limit] (2) #3 ORDER_TYPE_BUY 2022.02.15 10:08:20 -> 2022.02.15 10:08:20 L=0.01 @ 1.13189 (3) #4 ORDER_TYPE_BUY 2022.02.15 15:01:26 -> 2022.02.15 15:01:26 L=0.01 @ 1.13442 (1) #5 ORDER_TYPE_SELL 2022.02.15 15:35:43 -> 2022.02.15 15:35:43 L=0.01 @ 1.13568 (2) #6 ORDER_TYPE_SELL 2022.02.16 09:39:17 -> 2022.02.16 09:39:17 L=0.01 @ 1.13724 (4) #7 ORDER_TYPE_BUY 2022.02.16 23:31:15 -> 2022.02.16 23:31:15 L=0.01 @ 1.13748 (3) #8 ORDER_TYPE_SELL 2022.02.16 23:31:15 -> 2022.02.16 23:31:15 L=0.01 @ 1.13742 Deals: (1) #1 [#1] DEAL_TYPE_BUY DEAL_ENTRY_IN 2022.02.15 01:20:50 L=0.01 @ 1.1306 = 0.00 (2) #2 [#3] DEAL_TYPE_BUY DEAL_ENTRY_IN 2022.02.15 10:08:20 L=0.01 @ 1.13189 = 0.00 (3) #3 [#4] DEAL_TYPE_BUY DEAL_ENTRY_IN 2022.02.15 15:01:26 L=0.01 @ 1.13442 = 0.00 (1) #4 [#5] DEAL_TYPE_SELL DEAL_ENTRY_OUT 2022.02.15 15:35:43 L=0.01 @ 1.13568 = 5.08 [tp] (4) #5 [#2] DEAL_TYPE_SELL DEAL_ENTRY_IN 2022.02.15 18:10:17 L=0.01 @ 1.13626 = 0.00 (2) #6 [#6] DEAL_TYPE_SELL DEAL_ENTRY_OUT 2022.02.16 09:39:17 L=0.01 @ 1.13724 = 5.35 [tp] (4) #7 [#7] DEAL_TYPE_BUY DEAL_ENTRY_OUT 2022.02.16 23:31:15 L=0.01 @ 1.13748 = -1.22 (3) #8 [#8] DEAL_TYPE_SELL DEAL_ENTRY_OUT 2022.02.16 23:31:15 L=0.01 @ 1.13742 = 3.00 Total: 12.21, Trades: 4 |
В круглых скобках — идентификаторы позиций, в квадратных скобках — тикеты ордеров для соответствующих сделок (тикеты обоих типов предваряются "решёткой" '#').
Здесь не учитываются свопы и комиссии. Их расчет можно добавить.
Еще один пример работы с тиками пользовательских символов мы рассмотрим в разделе об особенностях реальной торговли с пользовательскими символами. Речь пойдет о создании эквиобъемных графиков.