Пошаговая реализация локальной системы стоп-лоссов на MQL5
Введение
Стоп-лосс — это ордер более сложного типа, закрывающий открытую позицию по заранее определенной цене, чтобы ограничить потери и защитить средства. Эта заранее определенная цена называется стоп-ценой (или ценой срабатывания стоп-ордера). Когда рынок достигает этого уровня, стоп-ордер становится рыночным ордером и исполняется по наилучшей доступной цене. Уровни стоп-лосса служат важным инструментом управления рисками, особенно в торговле на рынке Форекс и CFD, поскольку высокое кредитное плечо этих производных инструментов может в противном случае привести к быстрой потере капитала. Они представляют собой ключевую стратегию управления рисками в розничной торговле, широко применяемую трейдерами, использующими внутридневную торговлю, свинг-трейдерами и трейдерами, ведущими долгосрочную торговлю. Уровни стоп-лосса также можно использовать для фиксации прибыли. По мере того, как позиция становится прибыльной, уровень стоп-лосса можно перемещать, не используя частичное закрытие позиций. Таким способом трейдер может сохранять полный объем открытой позиции, оставаясь при этом защищенным от убытков.
Однако главное значение стоп-лосса для розничной торговли связано с его влиянием на психологию трейдера. Стоп-лоссы полностью автоматизированы и не требуют ручного исполнения или постоянного мониторинга рынка; это значительно снижает стресс, связанный с управлением несколькими позициями одновременно. Предварительное определение точек выхода из сделки с помощью стоп-лосса устраняет эмоциональную предвзятость и импульсивные решения, которые могут закрадываться, когда позиция оказывается неблагоприятной для трейдера.
Стоп-лосс — важнейший инструмент управления рисками. Однако традиционный стоп-ордер размещается на бирже и виден участникам рынка, включая брокеров-дилеров и маркет-мейкеров. Это публичное заявление, выражающее ваше видение позиции. У традиционного стоп-ордера есть серьезные недостатки, например:
- Охота на стоп-лоссы (Stop Hunting)
Поиск стоп-лоссов (известный также как «стоп-ран» или «очищение ликвидности») происходит, когда рыночная цена движется в направлении стоп-ордеров, главным образом, когда они группируются вокруг определенного уровня. Нередко трейдеры видят, как цена достигает уровня стоп-лосса, а затем продолжает движение в первоначальном направлении, как будто ничего не произошло. Наличие стоп-ордеров сигнализирует крупным игрокам о наличии ликвидности, что крайне важно для выполнения ими требований к размеру позиции. Как говорит доктор Дэвид Пол: «Я хочу размещать свои ордера на вход там, где массы размещают свои стоп-ордера».
- Минимальные расстояния до стоп-ордеров (Minimum Stop Distances)
Многие брокеры часто устанавливают требования к минимальному расстоянию между текущей рыночной ценой и ценой срабатывания стоп-лосса. Это означает, что трейдер не имеет права устанавливать стоп-лосс именно там, где ему хочется; любой стоп-ордер, размещенный ближе установленного брокером минимального расстояния к текущей рыночной цене, будет отклонен. На рынке с кредитным плечом, таком как CFD, подобные расстояния могут иметь реальные торговые последствия, поскольку трейдеру придется рассчитывать размер своей позиции с учетом убытков, которые он мог бы понести из-за этого обязательного расстояния, теряя при этом возможность максимально использовать торговый потенциал.
- Расширение спреда (Quote Widening)
На рынках CFD цены основаны на том, что для каждого торгового инструмента в каждый момент времени существуют котировки bid и ask, и ожидается, что эти цены будут несколько расширяться во время выхода новостей из-за снижения торговой активности. Однако даже при обычных обстоятельствах недобросовестный брокер-дилер может в любой момент увеличить спред по отношению к вашим отложенным ордерам, поскольку брокер будет знать вашу стоп-цену в момент размещения вами традиционного стоп-ордера. Этот тип манипуляций со спредом отличается от охоты на стоп-лоссы, поскольку рынок продолжает функционировать в обычном режиме, но трейдер будет вынужден закрыть свою позицию.
Существует ли способ устранить недостатки традиционного стоп-ордера, сохранив при этом преимущества стоп-лосса?
Несомненно! Решение заключается в локальной системе ограничения убытков. Ключевой особенностью локальной системы стоп-лоссов является тот факт, что стоп-цена устанавливается и поддерживается локально на терминале трейдера и не раскрывается ни одному брокеру или бирже. Локальный стоп-лосс выглядит и функционирует как традиционный стоп-ордер, но без недостатков, связанных с раскрытием его цены. Его можно устанавливать, перемещать и перетаскивать, чтобы уменьшить потери и сохранить прибыль. Когда рыночная цена достигает этой локальной стоп-цены, терминал исполняет такой ордер, как обычный рыночный ордер для закрытия позиции. Это предотвращает попытки поиска стоп-лосса и расширение котировок, поскольку цена стоп-лосса никогда никому не раскрывается — ни брокеру-дилеру, ни маркет-мейкеру, ни бирже. Локальный стоп-лосс также можно разместить произвольно близко к цене открытия позиции, что дает трейдеру полную свободу в выборе торговых стратегий.
Для реализации локальной системы стоп-лоссов необходимо хранить цену срабатывания стоп-ордера для каждой позиции локально и автоматизировать мониторинг условий срабатывания стоп-лосса. Инструменты, предоставляемые языком MQL5 делают его идеальным кандидатом для использования в качестве советника, далее иногда называемого сокращенно EA. В этой статье представлено пошаговое руководство по реализации локальной системы стоп-лоссов с использованием советника на языке MQL5, а также объяснение процесса проектирования. Наше учебное пособие следует современным методам объектно-ориентированного программирования. Для создания объекта «стоп-лосс» будем использовать объектные функции. Исполнение сделок будет осуществляться посредством CTrade, а доступ к открытым позициям — посредством CPositionInfo. Кроме того, мы увидим, выбор того или иного типа структур данных, таких как CHashMap, может обеспечить значительное повышение эффективности отслеживания и мониторинга наших позиций. Никаких предварительных знаний не требуется: необходимые понятия будут объяснены. Кроме того, будут представлены ссылки на дополнительную литературу. Учебный материал начнется с понимания принципа исполнения стоп-лосса, а затем перейдет к определению и тестированию основного алгоритма. Затем усовершенствуем наш код, чтобы упростить процедуру одновременной обработки нескольких позиций с визуальной маркировкой. В целом, это послужит руководством по разработке советника, начиная с определения входных параметров и заканчивая готовым к использованию практическим инструментом для торговли. Приступим!
Понимание исполнения стоп-лосса
На рынках Форекс и CFD цена формируется на основе котировок, то есть в каждый момент времени для каждого инструмента цены определяются с помощью значений bid и ask, предоставляемых поставщиком котировок. В зависимости от условий брокерского соглашения, поставщиками котировок могут быть банки, маркет-мейкеры, провайдеры ликвидности, специалисты или даже сам брокер, выступающий в качестве дилера. Эти участники рынка реализуют противоположную сторону вашей сделки, используя свои собственные запасы, и, таким образом, котируют следующее:
- Ask — это минимальная цена, по которой участник готов продать актив трейдеру.
- Bid — это максимальная цена, по которой участник готов купить актив у трейдера.
Ключевая взаимосвязь между этими двумя ценами выглядит следующим образом:
Ask ≥ Bid
На рынках с нулевым спредом цены в обоих случаях равны. В наших целях целесообразно рассматривать цены bid и ask как цены исполнения рыночного ордера на покупку и продажу соответственно. Таким образом, в любой момент времени для рыночного ордера цена ask является ценой открытия позиции на покупку, а цена bid — ценой открытия позиции на продажу, как показано на рисунке ниже. Разница между этими двумя ценами — это спред, то есть стоимость, которую берет на себя трейдер при открытии позиции.

Спред между ценами bid и ask при типичной ценовой котировке.
Стоп-лосс — это ордер на закрытие существующей позиции по заранее определенной цене. Вопрос в том, за какой из этих двух цен нам следует следить. Позиция закрывается путем совершения сделки, противоположной уже существующей, на тот же объем. Таким образом, позиция на покупку в размере 0.5 лота закрывается посредством открытия позиции на продажу в размере 0.5 лота. Торговый терминал автоматически сопоставляет два ордера для закрытия позиции, особенно в хеджирующих счетах, где открытие новой позиции на продажу не приводит к автоматическому закрытию существующей позиции на покупку. Поскольку стоп-лосс исполняется как рыночный ордер, то для закрытия позиции на покупку необходимо исполнить рыночный ордер на продажу аналогичного объема, а поскольку цена bid является ценой открытия рыночной позиции на продажу, стоп-условие выглядит следующим образом:
Bid ≤ цене SL
Аналогично, для позиций на продажу мы можем сделать вывод, что цена ask будет равна цене открытия соответствующей позиции на покупку; следовательно, мы получаем следующее условие стоп-лосса:
Ask ≥ цене SL
Приведенные ниже рисунки иллюстрируют условия стоп-лосса для позиций двух типов. Класс CTrade обрабатывает для нас все параметры сопоставления ордеров, и все, что нам нужно сделать, это вызвать функцию PositionClose для соответствующей позиции.

Механизм исполнения стоп-ордеров для позиций на покупку.

Механизм исполнения стоп-ордеров для позиций на продажу.
Простой способ определить, за какой ценой нам нужно следить, — это понаблюдать, какая из двух цен оказывается ближе всего к нашей цене стоп-лосса. Как видно из приведенных выше рисунков, для позиции на покупку цена bid находится ближе всего к уровню стоп-лосса, поэтому данное условие применяется к цене bid; аналогично, для позиции на продажу цена ask находится ближе всего к линии стоп-лосса, а это значит, что наш код должен отслеживать именно эту цену для выхода из позиции. Теперь, когда мы понимаем механику исполнения стоп-лосса, давайте рассмотрим основной алгоритм нашего локального стоп-лосса.
Основной алгоритм
Разобравшись в механике стоп-лосса, можно выделить следующий основной алгоритм нашей системы стоп-лоссов:
- Просканируйте текущие открытые позиции по символу графика и поочередно пройдите по ним в цикле.
- Если обнаружена новая позиция, обработайте ее, создав локальный стоп-лосс на заданном пользователем расстоянии и отметьте эту позицию как обработанную
- Если позиция уже отмечена как обработанная, проверьте, выполняется ли условие стоп-лосса, получив текущее значение локального стоп-лосса и сравнив его с соответствующей ценой котировки на основе условий, описанных в разделе Понимание исполнения стоп-лосса
- Если условие стоп-лосса выполнено, закройте позицию, удалите соответствующие локальные объекты стоп-лосса и снимите отметку в списке обработанных позиций.
- Повторяйте шаги с 1 по 4 на каждом тике символа.
HashMap
HashMap — это структура данных, которая хранит информацию в парах «ключ-значение», используя хеш-функцию для сопоставления ключей с определенными индексами в базовых сегментах с целью эффективного извлечения. Главная особенность хэш-карты заключается в том, что она обеспечивает среднюю временную сложность O(1) (постоянное время) для вставки, удаления и, что наиболее важно, поиска. То есть мы можем мгновенно проверить, принадлежит уникальный ключ хэш-карте или нет. MQL5 предоставляет нам CHashMap, обобщенную коллекцию данных, которую можно использовать для хранения пар «ключ-значение». В частности, сохраняя тикет позиции в качестве ключа и цену стоп-лосса в качестве значения, мы можем использовать свойства хэш-карты для эффективной проверки того, была позиция прежде обработана или нет.
#include <Generic\HashMap.mqh> CHashMap<ulong, double> OpenPositions;
Как вариант, тикеты можно сохранить в массиве или списком. Однако каждый поиск занял бы O(n) времени и повторялся бы для n позиций. В этом случае общая сложность составила бы O(n^2). В таком основном инструменте управления рисками, как локальный стоп-лосс, мы стремимся свести к минимуму любые ненужные вычисления, а выбрав хэш-карту, мы можем уменьшить общую временную сложность с O(n^2) до O(n), что значительно повысит эффективность. В документации CHashMap приведено подробное описание доступных методов; для нас актуальны следующие: Add — для добавления пар «ключ-значение«»; Remove — для удаления пары «ключ-значение»; ContainsKey — для определения того, содержит ли хэш-карта указанный ключ; TryGetValue — для сопоставления значения с указанным ключом; TrySetValue —для изменения значения, присвоенного указанному ключу; Count — для получения количества элементов в хэш-карте; CopyTo — для перебора ключей и значений.
Начало работы
Теперь у нас есть все необходимые элементы, чтобы начать процесс разработки. Начнем с открытия MetaEditor, чтобы создать новый документ, как показано ниже.

Создайте новый документ.
Выберите «Советник (шаблон)», что автоматически сгенерирует основные функции советника, и нажмите «Далее».

Выбор шаблона советника в Мастере MQL5.
Должен появиться Мастер MQL для настройки общих свойств. Дайте своему файлу описательное имя; в этой инструкции мы будем называть его Local Stop Loss. Файлы с исходным кодом MQL5 сохраняются с расширением файла .mq5. Свойства в MQL5 — это специфические параметры, определяющие конфигурацию, внешний вид и поведение программ посредством таких атрибутов, как автор, авторские права, описание, версия и так далее. В Мастере MQL укажите имя автора и ссылки (если есть), затем нажмите «Далее».

Указание общих свойств
Шаблон советника
Шаблон советника должен выглядеть следующим образом. Строки, начинающиеся с //, — это комментарии в MQL5, которые компилятор игнорирует. Комментарии можно использовать для краткого описания функций, пометок о том, что нужно сделать, и предоставления дополнительных пояснений относительно кода или выбранного дизайна. Язык MQL5, основанный на C++, также поддерживает многострочные комментарии, начинающиеся и заканчивающиеся символом /* и */ соответственно. Они полезны для длинных комментариев или для выборочной блокировки кода во время разработки. Кроме того, комментарии можно сочетать со свойствами, чтобы сделать поля ввода более информативными для пользователей, как мы вскоре увидим.
#property copyright "Your Name" #property link "https://www.mql5.com/" #property version "1.00" //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+
Шаблон советника состоит из функций OnInit, OnDeinit и OnTick, соответствующих жизненному циклу советника. Жизненный цикл советника начинается с момента его подключения к графику и заканчивается при удалении — внешним способом или с помощью кода. Между этими событиями советник обрабатывает мельчайшую единицу обновления цены, известную как тик. Понимание схемы выполнения этих трех функций позволяет нам распределить обязанности между ними.
- OnInit
OnInit — это функция инициализации, которая выполняется один раз в начале каждого жизненного цикла советника. Для наших целей нам необходимо знать, что советник будет однократно запускаться каждый раз, когда мы подключаем советника к графику, компилируем код и (или) изменяем какие-либо входные параметры. Эта функция обеспечивает нас возможностью проводить операции инициализации, которые необходимо выполнить при запуске нашего советника. Кроме того, в документации OnInit описаны значения, которые мы можем вернуть для обозначения статуса инициализации. Мы будем использовать функцию OnInit для проверки своих входных данных, установки значений наших глобальных переменных и установки локального стоп-лосса для позиций, открытых до подключения советника.
- OnDeinit
OnDeinit является функцией деинициализации, которая тоже запускается один раз — в конце жизненного цикла советника. В документации OnDeinit подробно описаны причины прекращения работы советника, но для нас достаточно знать, что он запустится один раз после удаления советника с графика. Эта функция в основном используется для задач очистки, таких как освобождение дескрипторов и высвобождение ресурсов. Мы будем использовать ее для очистки наших локальных объектов стоп-лосса при завершении действия советника.
- OnTick
Функция OnTick — именно здесь находится наша алгоритмическая логика. Она запускается всякий раз, когда цена нашего символа обновляется, и это называется тиком. В документации NewTick содержится дополнительная информация о событии, связанном с тиковым событием. Внутри OnTick мы реализуем основной алгоритм локального стоп-лосса и его основные функции.
Свойства программы
Мы уже сталкивались со свойствами и их применением в разделе Начало работы. MQL5 позволяет указывать дополнительные свойства программы для повышения читаемости исходного кода и улучшения процесса разработки. Свойства относятся к особому классу программ, известному как препроцессор и предназначенному для подготовки исходного кода программы перед компиляцией. Мы можем включать свойства в исходный код с помощью директивы препроцессора, начинающейся с #. Из полного списка свойств программы в документации мы включим description—, чтобы предоставить краткое описание советника, которое можно просмотреть в мастере ввода данных; script_show_inputs—, чтобы дать описательные названия нашим входным данным. В дополнение к сгенерированным выше свойствам полностью наш раздел свойств должен выглядеть следующим образом:
#property copyright "Your Name" #property link "https://www.mql5.com/" #property version "1.00" #property description "Local Stop Loss EA" #property script_show_inputs
Заголовки
Для построения нашей локальной системы стоп-лоссов мы используем объектно-ориентированную парадигму. Объектно-ориентированное программирование предлагает подробный анализ объектно-ориентированного программирования на языке MQL5 и его преимуществ. Короче говоря, парадигма объектно-ориентированного программирования основана на создании объектов для моделирования сущностей реального мира. Каждый объект является экземпляром класса, который представляет собой самодостаточный шаблон, объединяющий данные (в виде атрибутов или полей) и поведение (в виде методов или функций), относящиеся к сущности. MQL5 предоставляет нам предопределенные классы, которые помогают в разработке. Мы будем использовать:
- CTrade — для удобного доступа к торговым функциям
- CPositionInfo — для удобного доступа к свойствам открытых позиций
- CHashSet — для работы с наборами данных и обработки дополнительных случаев, требующих очистки
- CHashMap — для хранения пар ключ-значение в динамической хэш-карте
Эти классы объявлены в заголовочных файлах, которые нам сначала необходимо импортировать в исходный код советника, чтобы иметь возможность создавать экземпляры их объектов. Используем директиву #include, препроцессора, за которой следует имя заголовочного файла в угловых скобках. В качестве альтернативы вы можете скопировать соответствующую строку включения заголовочного файла со страницы документации класса. Необходимые заголовочные файлы включены следующим образом:
#include <Trade\Trade.mqh> #include <Trade\PositionInfo.mqh> #include <Generic\HashSet.mqh> #include <Generic\HashMap.mqh>
Определение входных данных
Теперь мы готовы определить свои входные данные. Для локальной системы стоп-лоссов нам необходимо знать от пользователя начальное расстояние от линии открытия, чтобы построить линию стоп-лосса — расстояние до стоп-лосса. Хотелось бы предоставить пользователю возможность указать расстояние до стоп-уровня в выбранном им типе. Enumerations («Перечисления») — идеальный инструмент для этого; они позволяют ограничивать объем данных определенным набором. Мы можем определить наше перечисление StoplossType следующим образом:
enum StoplossType { stoploss_pips, // Pips - non Yen pairs stoploss_pips_yen, // Pips - Yen pairs stoploss_points // Points };
Теперь пользователь может выбрать предпочтительный тип стоп-лосса. Для определения входных данных используем модификатор input , за которым следует его тип данных. В языке MQL5 каждая переменная имеет связанный с ней тип данных. Документация Data Types предоставляет полный список доступных типов данных на языке MQL5 и способы их выбора. Мы можем сгруппировать связанные входные данные с помощью идентификатора group. Теперь определим наши входные данные следующим образом:
//--- Inputs input group "Stoploss Settings" input StoplossType InpSlType = stoploss_points; // Stoploss Type input double InpPriceRange = 0; // Stop distance input group "Colors" input color InpLongColor = clrOrange; // Buy Positions input color InpShortColor = clrPink; // Sell Positions input group "EA Identification Parameters" input ulong InpMagicNumber = 12345; // Magic Number
Наше определенное выше перечисление, сгруппированное вместе со значением double, дает нам расстояние до входного стоп-уровня. Используем параметр Color Type, чтобы дать пользователю возможность выбрать цвета для длинных и коротких позиций. Кроме того, для обеспечения возможности расширения функционала предусмотрена возможность ввода «магического числа», позволяющего однозначно идентифицировать сделки, совершаемые этим советником. К каждому входному данному добавляем префикс Inp, чтобы сделать их самодокументируемыми. Значения, присвоенные указанным выше входным параметрам, на момент инициализации становятся значениями по умолчанию.
Комментарии в конце каждой строки выполняют свою функцию. Свойство script_show_inputs, которое мы добавили, отображает в диалоговом окне ввода вместо имени исходного кода следующее описание комментария: «Тип стоп-лосса» вместо «InpSlType». Эти описательные названия делают наш советник более доступным. Нажмите кнопку «Компилировать» в верхней части. Компиляция берет наш файл .mq5 с исходным кодом и создает исполняемый двоичный файл .ex5. Статья «Редактирование, компиляция и запуск программ» предоставляет подробный анализ процедуры компиляции.

Скомпилируйте советник
Теперь эксперт должен появиться в списке «Навигатор» вашего терминала MetaTrader 5. Кликните и перетащите, чтобы применить советник к графику. Мы можем просмотреть описание в наших свойствах на вкладке «Общие» диалогового окна ввода, а также имя автора и ссылку. Включите алгоритмическую торговлю.

Общее описание советника
На вкладке «Ввод данных» диалогового окна аккуратно сгруппированы наши входные данные вместе с их описательными названиями из комментариев.

Входные параметры эксперта
Проверка входных данных
Определенные выше входные данные доступны на протяжении всего программного кода. Мы можем обращаться к ним по именам в любом месте программы; считается, что они являются глобальными относительно всей программы. Кроме того, мы можем аналогичным образом определять переменные, к которым можно получить доступ из любой точки программы; мы называем эти значения глобальными переменными. Чтобы объявить глобальную переменную, нам нужно определить ее вне области видимости любой функции, аналогично нашим входным данным.
У нас есть тип входного стоп-лосса и соответствующее расстояние до уровня входного стоп-лосса; давайте определим нашу первую пользовательскую функцию для расчета фактического расстояния до линии стоп-лосса от этих входных данных. Функции — это многократно используемые блоки кода, выполняющие определенную задачу. Присвоив этим функциям полезные имена, мы можем сделать наш код декларативным, упрощая при этом подробности реализации. Функции могут опционально принимать аргументы и возвращать значения. В документации Функции приведены различные примеры функций.
Наша функция должна возвращать расстояние до уровня стоп-лосса, которое, согласно документации Тип данных представляет собой значение типа double. Сначала указываем в коде тип возвращаемого значения, а затем имя функции GetStopDistance(). Поскольку входные данные уже доступны как глобальные значения, оставляем скобки пустыми, указывая, что наша функция не принимает никаких аргументов. Мы используем оператор switch, чтобы добиться соответствия InpSlType каждому случаю нашего перечисления StopLossType. В зависимости от конкретного случая, он умножает InpPriceRange на соответствующее значение в пипсах и возвращает результат.
//+------------------------------------------------------------------+ //| Returns the stop distance for the input stop loss type | //+------------------------------------------------------------------+ double GetStopDistance() { switch(InpSlType) { case stoploss_pips: return InpPriceRange * 0.0001; case stoploss_pips_yen: return InpPriceRange * 0.01; case stoploss_points: return InpPriceRange; } return InpPriceRange; }
Поскольку мы рассмотрели все случаи перечисления, последний оператор return за пределами switch технически недостижим; однако компилятор ожидает, что каждый участок кода будет возвращать значение, и без этой строки компиляция завершится неудачей.
Мы хотим, чтобы рассчитанное нами значение расстояния до линии стоп-лосса было доступно на протяжении всей программы без необходимости многократно вызывать функцию GetStopDistance(). То есть хотим, чтобы это значение было глобальной переменной, значение которой мы могли бы установить при инициализации. Кроме того, для нашего советника необходимо установить «магическое число», которое будет отображаться на протяжении всей программы, при помощи класса CTrade, уже импортированный нами ранее. Мы создадим первый экземпляр нашего объекта, также в глобальной области видимости.
//--- Global variables CTrade Trade; double StoplossDistance;
Как можно видеть, в MQL5 нет большой разницы между объявлением переменной и объявлением объекта.
Проверка входных данных подразумевает проверку того, соответствуют ли наши входные данные допустимому состоянию в функции OnInit. InpSlType, InpLongColor и InpShortColor может принимать лишь ограниченный набор предопределенных значений; у нас нет необходимости проверять их. Аналогичным образом, значение InpMagicNumber относится к типу ulong, то есть является беззнаковым длинным целым числом с минимальным значением 0; при нулевом значении ручные сделки и сделки советника не различаются по Magic Number. Однако значение InpPriceRange не может быть равным 0 или отрицательным числом; отрицательное или нулевое значение расстояния до линии стоп-лосса не является допустимым состоянием. Поэтому используем оператор if для проверки на 0 или отрицательные значения с помощью операций отношения и уведомления пользователя с помощью функции MessageBox. Из доступных возвращаемых значений, доступных в документации OnInit, возвращаем INIT_PARAMETERS_INCORRECT, когда входные данные не проходят проверку, и INIT_SUCCEEDED для подтверждения успешной инициализации.
После проверки входных данных можно заполнить наши глобальные переменные внутри OnInit. Для установки «магического числа» советника (Expert Magic Number) получаем доступ к методу SetExpertMagicNumber нашего объекта CTrade с помощью оператора «.» и передадим InpMagicNumber в качестве аргумента.
Trade.SetExpertMagicNumber(InpMagicNumber);
Для установки значения StopLossDistance присвоим ему значение функции GetStopDistance().
StoplossDistance = GetStopDistance();
В итоге получаем нашу функцию OnInit.
//--- Global variables CTrade Trade; double StoplossDistance; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(InpPriceRange <= 0) { MessageBox("Stop Distance value cannot be 0 or negative"); return INIT_PARAMETERS_INCORRECT; } Trade.SetExpertMagicNumber(InpMagicNumber); StoplossDistance = GetStopDistance(); return(INIT_SUCCEEDED); }
Цикл по позициям
В этом разделе определим нашу функцию PositionsCheck. Эта функция будет перебирать в цикле все открытые позиции, проверять, соответствует ли символ нашему символу на графике, и определять дальнейшие действия в зависимости от того, была ли позиция обработана ранее. Она не возвращает каких-либо значений, которые будут обозначены типом возврата void в объявлении функции.
Начнем с того, что объявим CHashMap в качестве глобального объекта для отслеживания обработанных нами открытых позиций. Первый тип данных в объявлении в угловых скобках указывает на тип данных ключа, а второй — на тип данных значения, которое мы хотим сопоставить с нашим ключом. Мы будем использовать тикет позиции, который является уникальным значением ulong для каждой позиции, и сопоставить его со значением double локального уровня стоп-лосса для данной позиции.
//--- Global variable CHashMap<ulong, double> gblOpenPositions;
Чтобы оставаться верными нашей концепции самодокументируемого кода, используем префикс gbl для наших глобальных переменных, значение которых, как ожидается, изменится после OnInit. В отличие от StoplossDistance, значение которого остается постоянным после инициализации,префикс gbl будет указывать на то, что эти переменные участвуют в управлении состояниями.
Цикл for позволяет многократно исполнять блок кода, используя три оператора:
- Initialization — однократная инициализация нашей переменной counter.
- Condition — условие, определяющее, следует ли повторять код.
- Update Expression — выражение для обновления переменной counter в конце каждой итерации.
Будем использовать цикл for, чтобы пройтись по нашим открытым позициям. Общая структура будет выглядеть следующим образом:
for(int i = PositionsTotal()-1; i >= 0; i--) { }
Функция PositionsTotal, которая возвращает общее количество открытых позиций, инициализирует переменную счетчика i. Наш код будет выполняться до тех пор, пока выполняется условие i ≥ 0, и мы используем оператор уменьшения i--, чтобы обновлять переменную счетчика, уменьшая ее значение на 1 в конце каждой итерации.
Язык MQL5, основанный на C++, использует систему нулевой индексации — позиции индексируются с 0. Следовательно, мы пишем
int i = PositionsTotal()-1;
при инициализации и используем 0 вместо 1 в условии,
i >= 0; Наконец, вместо соответствующего прямого цикла мы выполняем обратный цикл.
for(int i = 0; i < PositionsTotal(); i++)
Это позволяет избежать повторных вычислений функции PositionsTotal() при условии i<PositionsTotal() в конце каждой итерации. Обратный цикл вычисляет функцию PositionsTotal() только один раз — при инициализации, что более эффективно.
После создания структуры цикла будем использовать объект CPositionInfo, posInfo, для доступа к свойствам открытых позиций. Метод SelectByIndex выбирает позицию по индексу i переменной счетчика. Этот метод возвращает тип bool, обозначающий успех или неудачу операции, что мы можем проверить с помощью оператора if и логических операций. В случае неудачного выбора, мы можем пропустить эту конкретную итерацию, используя оператор continue.
//--- Local variable CPositionInfo posInfo; if(!posInfo.SelectByIndex(i)) continue;
После успешного выбора позиции все дальнейшие методы, вызываемые по объекту posInfo, будут соответствовать этой конкретной позиции. Нам хочется работать только с позициями, которые соответствуют символу нашего графика. Мы можем вызвать метод Symbol нашего объекта posInfo и сравнить его с функцией Symbol, которая возвращает символ текущего графика. Если они не совпадают, пропускаем эту итерацию, используя оператор continue.
if(posInfo.Symbol() != Symbol()) continue;
Две указанные выше проверки также известны как защитные условия — они позволяют пропускать пути, которые для нас не актуальны. MQL5 автоматически обрабатывает уничтожение локального объекта posInfo всякий раз, когда он выходит из области видимости, следуя принципам RAII языка C++. В справочнике по C++ RAII приводится описание этого метода программирования, но в нашем случае мы просто объявляем и используем объект; MQL5 позаботится обо всем остальном.
Теперь у нас открыта позиция, соответствующая символу на графике, выбранному в объекте posInfo. Чтобы уточнить, обрабатывали ли мы эту позицию ранее, проверяем, содержит ли наша хэш-карта gblOpenPositions соответствующий тикет позиции в качестве ключа. Для этого вызываем метод ContainsKey и передаем тикет позиции в качестве аргумента.
- Если значение равно true, передаем posInfo в CheckProcessedPosition.
- Если оно равно false, обрабатываем позицию путем передачи posInfo в ProcessPosition.
if(gblOpenPositions.ContainsKey(posInfo.Ticket())) { CheckProcessedPosition(posInfo); // Not yet implemented } else { ProcessPosition(posInfo); // Not yet implemented }
Мы будем реализовывать ProcessPosition и CheckProcessedPosition в следующих разделах. Наконец, мы объявим posCount переменной int для ведения подсчета количества позиций, соответствующих нашему символу.
int posCount = 0;
В конце увеличим значение этого счетчика, posCount += 1, внутри нашего цикла. Мы будем использовать эту переменную позже, когда будем управлять несколькими позициями. Собрав все воедино, получаем свою функцию PositionsCheck.
//+------------------------------------------------------------------+ //| Scans and processes new positions, manages already processed ones| //+------------------------------------------------------------------+ void PositionsCheck() { int posCount = 0; for(int i = PositionsTotal()-1; i >= 0; i--) { CPositionInfo posInfo; if(!posInfo.SelectByIndex(i)) continue; if(posInfo.Symbol() != Symbol()) continue; if(gblOpenPositions.ContainsKey(posInfo.Ticket())) { CheckProcessedPosition(posInfo); } else { ProcessPosition(posInfo); } posCount += 1; } }
Создание объектов стоп-лосса
В этом разделе мы дадим определение функции ProcessPosition. Эта функция содержит логику для создания локального уровня стоп-лосса. Он принимает объект posInfo CPositionInfo, содержащий свойства открытой позиции, которую мы хотим обработать, и не возвращает никакого значения. После обработки позиции, соответствующий тикет добавляется в хэш-карту gblOpenPositions для обозначения позиции как обработанной. Сигнатура функции:
void ProcessPosition(CPositionInfo &posInfo) Обратите внимание на & перед параметром posInfo. В MQL5 объекты можно передавать только по ссылке. Значок & обозначает, что параметр является ссылкой на объект CPositionInfo. В справочнике по C++ представлено подробное объяснение ссылок на объекты и параметры функций. Для наших целей, поскольку мы не вносим изменений в объект posInfo, для работы с объектами достаточно знать, что ссылки — это эффективный способ передачи объектов функциям в качестве аргументов.
MQL5 предоставляет объектные функции для работы с графическими объектами (отличающимися от объектов объектно-ориентированного программирования). Будем использовать их, чтобы создавать линии открытия и стоп-лосса для наших позиций.
Каждый графический объект должен иметь уникальное имя в пределах графика. Кроме того, мы также отвечаем за очистку создаваемых нами графических объектов. Все имена относятся к типу string. Определим общий префикс для имен всех объектов, которые будем создавать.
//--- Global variable string CslPrefix = "csl_"; // Csl here stands for custom stop loss
Этот префикс позволяет нам легко идентифицировать графические объекты и помогает в процессе очистки.
Определив префикс, мы можем использовать тикет позиции для генерации уникальных имен с помощью IntegerToString и оператора объединения строк +. Будем добавлять _open для линии открытия и _line для линии стоп-лосса. Это дает нам две вспомогательные функции для генерации имен.
//+------------------------------------------------------------------+ //| Returns the name of the open line for the input position ticket | //+------------------------------------------------------------------+ string GetOpenName(ulong ticket) { return CslPrefix + IntegerToString(ticket) + "_open"; } //+------------------------------------------------------------------+ //| Returns the name for the local stop loss line | //+------------------------------------------------------------------+ string GetCslName(ulong ticket) { return CslPrefix + IntegerToString(ticket) + "_line"; }
Аналогичным образом, мы генерируем метку для линии открытия с помощью DoubleToString, чтобы конвертировать такие значения double, как объем и цена, в тип string. Будем использовать тернарный оператор на ENUM_POSITION_TYPE, чтобы добавлять префикс BUY или SELL в зависимости от направления сделки (того же можно было бы добиться с помощью оператора if или switch ).
//+------------------------------------------------------------------+ //| Returns the display label for the open line | //+------------------------------------------------------------------+ string GetOpenLabel(ENUM_POSITION_TYPE posType, double volume, double price) { string priceString = DoubleToString(volume,2) + " at " +DoubleToString(price, Digits()); // Volume normalized to 2 decimal places, price normalized to chart symbol Digits() return posType == POSITION_TYPE_BUY? " BUY " + priceString : " SELL " + priceString; }
Создадим еще одну вспомогательную функцию для расчета цены стоп-лосса на основе цены открытия. Использование тернарного оператора аналогично использованию описанной выше функции; на этот раз мы будем вычитать StopLossDistance из цены открытия позиции на покупку и прибавлять StopLossDistance к цене открытия позиции на продажу, чтобы получить цену стоп-лосса и вернуть результат.
//+------------------------------------------------------------------+ //| Returns the price at which to create the local stop loss | //+------------------------------------------------------------------+ double GetCslPrice(double openPrice, ENUM_POSITION_TYPE posType) { return posType == POSITION_TYPE_BUY? openPrice - StoplossDistance : openPrice + StoplossDistance; // For buy: stop is below open price; for sell: stop is above open price }
Процесс создания наших линий открытия и стоп-лосса состоит из двух этапов: на первом ObjectCreate используется, чтобы создать объект OBJ_HLINE, а на втором устанавливаются значения свойств объекта. Будем использовать функцию ChartID для идентификации нашего графика при работе с объектами. Давайте получим необходимые имена и значения от наших вспомогательных функций.
double openPrice = posInfo.PriceOpen(); double cslPrice = GetCslPrice(openPrice, posInfo.PositionType()); color lineColor = posInfo.PositionType() == POSITION_TYPE_BUY? InpLongColor : InpShortColor; string openName = GetOpenName(posInfo.Ticket()); string openLabel = GetOpenLabel(posInfo.PositionType(), posInfo.Volume(), openPrice); string cslName = GetCslName(posInfo.Ticket()); int subWindow = 0; // Main chart datetime time = 0; // OBJ_HLINE has no time coordinate
В документации Свойства объекта представлен полный список свойств объектов и их значений. Для нашей линии открытия мы будем использовать ObjectSetInteger, чтобы установить стиль линии OBJPROP_COLOR и OBJPROP_STYLE в значение STYLE_DASHDOT. Для метки будем использовать функцию ObjectSetString, чтобы установить свойство OBJPROP_TEXT.
//--- Create the open line ObjectCreate(ChartID(), openName, OBJ_HLINE, subWindow, time, openPrice); ObjectSetInteger(ChartID(), openName, OBJPROP_COLOR, lineColor); ObjectSetInteger(ChartID(), openName, OBJPROP_STYLE, STYLE_DASHDOT); ObjectSetString(ChartID(), openName, OBJPROP_TEXT, openLabel);
Для нашей линии стоп-лосса должна быть предусмотрена возможность выбора и перемещения; это можно сделать, установив значения свойств OBJPROP_SELECTABLE и OBJPROP_SELECTED в true с помощью ObjectSetInteger. Аналогично и для OBJPROP_COLOR.
//--- Create the stop loss line ObjectCreate(ChartID(), cslName, OBJ_HLINE, subWindow, time, cslPrice); ObjectSetInteger(ChartID(), cslName, OBJPROP_COLOR, lineColor); ObjectSetInteger(ChartID(), cslName, OBJPROP_SELECTABLE, true); ObjectSetInteger(ChartID(), cslName, OBJPROP_SELECTED, true);
В завершение вызовем метод Add объекта gblOpenPositions, чтобы добавить тикет позиции и цену стоп-лосса в нашу хэш-карту и отметить позицию как обработанную.
//--- Add position ticket to hashmap along with the stop price to mark as processed
gblOpenPositions.Add(posInfo.Ticket(), cslPrice);
В итоге получаем нашу функцию ProcessPosition.
//+------------------------------------------------------------------+ //| Creates the objects for the open and local stop loss line | //+------------------------------------------------------------------+ void ProcessPosition(CPositionInfo &posInfo) { double openPrice = posInfo.PriceOpen(); double cslPrice = GetCslPrice(openPrice, posInfo.PositionType()); color lineColor = posInfo.PositionType() == POSITION_TYPE_BUY? InpLongColor : InpShortColor; string openName = GetOpenName(posInfo.Ticket()); string openLabel = GetOpenLabel(posInfo.PositionType(), posInfo.Volume(), openPrice); string cslName = GetCslName(posInfo.Ticket()); int subWindow = 0; // Main chart datetime time = 0; // OBJ_HLINE has no time coordinate //--- Create the open line ObjectCreate(ChartID(), openName, OBJ_HLINE, subWindow, time, openPrice); ObjectSetInteger(ChartID(), openName, OBJPROP_COLOR, lineColor); ObjectSetInteger(ChartID(), openName, OBJPROP_STYLE, STYLE_DASHDOT); ObjectSetString(ChartID(), openName, OBJPROP_TEXT, openLabel); //--- Create the stop loss line ObjectCreate(ChartID(), cslName, OBJ_HLINE, subWindow, time, cslPrice); ObjectSetInteger(ChartID(), cslName, OBJPROP_COLOR, lineColor); ObjectSetInteger(ChartID(), cslName, OBJPROP_SELECTABLE, true); ObjectSetInteger(ChartID(), cslName, OBJPROP_SELECTED, true); //--- Add position ticket to hashmap along with the stop price to mark as processed gblOpenPositions.Add(posInfo.Ticket(), cslPrice); }
Очистка
Создание и очистка объектов идут рука об руку. Существует два случая, когда нам необходимо очистить свои объекты: во-первых, удалить объекты, связанные с конкретной позицией; во-вторых, когда советник удаляется с графика или прекращает свою работу. Для удаления объектов определенной позиции можно передать имя объекта в качестве параметра функции ObjectDelete. Используя вспомогательные функции, определенные в Creating Stop Loss Objects, мы можем удалить объекты позиции, используя соответствующий тикет.
//+------------------------------------------------------------------+ //| Deletes objects related to the input ticket | //+------------------------------------------------------------------+ void CleanupPositionObjects(ulong ticket) { ObjectDelete(ChartID(), GetOpenName(ticket)); ObjectDelete(ChartID(), GetCslName(ticket)); }
При деинициализации нам нужно удалить все созданные нашим советником объекты, чтобы не загромождать график. Для достижения этой цели мы можем передать CslPrefix (что характерно для всех созданных нами объектов) в функцию ObjectsDeleteAll.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { ObjectsDeleteAll(ChartID(), CslPrefix); }
Проверка условия срабатывания стоп-лосса
В этом разделе определим функцию CheckProcessedPosition. Она проверяет, достигла ли цена уровня локального стоп-лосса для ранее обработанной позиции. Если условие стоп-лосса выполнено, функция закроет данную позицию, удалит связанные с ней объекты и удалит соответствующий тикет из объекта gblOpenPositions. В противном случае произойдет обновление хэш-карты gblOpenPositions самым последним значением локального стоп-лосса.
Функция принимает ссылку на объект posInfo в CPositionInfo, содержащий свойства открытой позиции, которую мы хотим проверить, и не возвращает никакого значения. Сигнатура функции:
void CheckProcessedPosition(CPositionInfo &posInfo) При закрытии позиции необходимо удалить связанные с ней объекты, а затем вызвать метод Remove объекта gblOpenPositions, чтобы удалить его тикет. Мы уже определили функцию очистки объекта в разделе Cleanup; теперь напишем вспомогательную функцию, которая позволит достичь обеих целей.
//+------------------------------------------------------------------+ //| Deletes objects related to the input ticket and updates hashmap | //+------------------------------------------------------------------+ void CleanupPosition(ulong ticket) { CleanupPositionObjects(ticket); gblOpenPositions.Remove(ticket); }
Это позволяет собрать всю логику очистки позиций в одном месте, и мы можем вызывать ее многократно.
Линию локального стоп-лосса можно перетаскивать, а ее значение могло измениться с момента последнего сохранения. Нам нужно использоватьобъектные функции, чтобы получить самое последнее значение стоп-лосса непосредственно из объекта графика. Чтобы избежать ошибок при расчете значения стоп-лосса, сначала проверим наличие линии стоп-лосса. Кто-то из пользователей мог случайно удалить эту линию. Функция ObjectFind выполняет поиск объекта с указанным именем. Она возвращает значение int вместо bool, поэтому мы проверяем вместо этого наличие отрицательного значения.
//--- Checking if the stop loss line object exists string cslName = GetCslName(posInfo.Ticket()); if(ObjectFind(ChartID(), cslName) < 0) { CleanupPosition(posInfo.Ticket()); ProcessPosition(posInfo); return; }
Если мы не сможем найти линию локального стоп-лосса на графике, следует перерисовать ее, рассматривая эту позицию как необработанную. Сначала вызовем функцию CleanupPosition, чтобы начать с чистого листа, затем вызовем ProcessPosition на позиции.
Подтвердив наличие линии стоп-лосса, мы можем использовать ObjectGetDouble, чтобы получить свойство OBJPROP_PRICE линии стоп-лосса. Это позволит нам определить текущее значение стоп-лосса.
double cslPrice = ObjectGetDouble(ChartID(), cslName, OBJPROP_PRICE);
Мы будем использовать структуру MqlTick для получения котировок bid и ask текущего тика. Структура представляет собой тип данных, который группирует логически связанные переменные разных типов данных. Структура MqlTick объединяет все существенные переменные тика в единую структуру данных. Она предназначена для быстрого поиска информации с использованием функции SymbolInfoTick.
Согласно документации, SymbolInfoTick принимает ссылку на структуру MqlTick. Эта ссылка позволяет функции непосредственно изменять структуру входных данных, заполняя её значениями, относящимися к самому последнему тику. Ссылки не только обеспечивают эффективный способ передачи данных в функции, но и позволяют быстро вносить в них изменения. Справочник по C++ содержит более подробную информацию о ссылках.
MqlTick curTick; SymbolInfoTick(Symbol(), curTick);
Мы можем получить доступ к полям структуры curTick с помощью оператора ., аналогичного нашим объектам — curTick.bid и curTick.ask. Это позволит нам получить актуальные цены bid и ask соответственно.
Теперь мы можем выразить условие стоп-ордера, используя ENUM_POSITION_TYPE и логические операции, следующим образом:
(posInfo.PositionType() == POSITION_TYPE_BUY && curTick.bid <= cslPrice) || (posInfo.PositionType() == POSITION_TYPE_SELL && curTick.ask >= cslPrice)
- Для позиций на покупку проверяем, соблюдается ли неравенство curTick.bid ≤ cslPrice.
- Для позиций на продажу проверяем, соблюдается ли неравенство curTick.ask ≥ cslPrice.
Объединив оба условия с помощью ||, мы можем объединить проверку условия, закрытие позиции и очистку связанных объектов в одном операторе if. Для закрытия позиции мы передаем ее тикет в метод PositionClose нашего объекта Trade, а затем вызываем CleanupPosition и return для выхода из функции.
//--- Evaluate stop condition if((posInfo.PositionType() == POSITION_TYPE_BUY && curTick.bid <= cslPrice) || (posInfo.PositionType() == POSITION_TYPE_SELL && curTick.ask >= cslPrice)) { Trade.PositionClose(posInfo.Ticket()); CleanupPosition(posInfo.Ticket()); return; }
Если условие стоп-ордера не выполняется, обновляем gblOpenPositions с учетом последнего значения стоп-лосса. Метод TryGetValue извлекает значение, в данный момент сопоставленное с тикетом позиции. Если это значение отличается от цены стоп-лосса, полученной из графика, то мы будем использовать TrySetValue для его обновления.
//--- Updating the value of the local stop loss in the hashmap double lastCsl; gblOpenPositions.TryGetValue(posInfo.Ticket(), lastCsl); if(lastCsl != cslPrice) { gblOpenPositions.TrySetValue(posInfo.Ticket(), cslPrice); }
В итоге мы получаем функцию CheckProcessedPosition.
//+------------------------------------------------------------------+ //| Checks for stop condition and closes the position if met | //+------------------------------------------------------------------+ void CheckProcessedPosition(CPositionInfo &posInfo) { //--- Checking if the stop loss line object exists string cslName = GetCslName(posInfo.Ticket()); if(ObjectFind(ChartID(), cslName) < 0) { CleanupPosition(posInfo.Ticket()); ProcessPosition(posInfo); return; } //--- Evaluating the stop condition MqlTick curTick; SymbolInfoTick(Symbol(), curTick); double cslPrice = ObjectGetDouble(ChartID(), cslName, OBJPROP_PRICE); if((posInfo.PositionType() == POSITION_TYPE_BUY && curTick.bid <= cslPrice) || (posInfo.PositionType() == POSITION_TYPE_SELL && curTick.ask >= cslPrice)) { Trade.PositionClose(posInfo.Ticket()); CleanupPosition(posInfo.Ticket()); return; } //--- Updating the value of the local stop loss in the hashmap double lastCsl; gblOpenPositions.TryGetValue(posInfo.Ticket(), lastCsl); if(lastCsl != cslPrice) { gblOpenPositions.TrySetValue(posInfo.Ticket(), cslPrice); } }
Проверка функциональности стоп-ордера
Теперь мы определили все основные функции локального алгоритма стоп-лосса. Чтобы проверить нашу логику и функциональность советника, добавьте функцию PositionsCheck и для OnInit, и для OnTick.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Input validation if(InpPriceRange <= 0) { MessageBox("Stop Distance value cannot be 0 or negative"); return INIT_PARAMETERS_INCORRECT; } Trade.SetExpertMagicNumber(InpMagicNumber); StoplossDistance = GetStopDistance(); //--- Running PositionsCheck() for the existing open positions PositionsCheck(); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { PositionsCheck(); }
Мы однократно запускаем PositionsCheck внутри OnInit для обработки всех существующих открытых позиций. Поскольку функция PositionsCheck вызывает ProcessPosition и CheckProcessedPosition изнутри, тело нашей функции OnTick состоит из единственного вызова функции PositionsCheck.
Скомпилируйте программу. Если компиляция пройдет успешно и без ошибок, подключите советник к графику и понаблюдайте за его поведением. Нажмите на GIF-анимацию, чтобы увидеть систему стоп-лосса в действии.

Тестирование функциональности локального стоп-лосса
Дополнительная очистка
Теперь у нас есть работающая система стоп-лосса. В оставшихся разделах добавим уточнения, которые упростят работу с несколькими позициями и маркировкой сделок.
Но сначала нам нужно рассмотреть частный случай очистки объектов. При выполнении условия стоп-ордера функция CheckProcessedPosition вызывает CleanupPosition. Что произойдет, если позиция будет закрыта внешними средствами (пользователем или брокером)? Эта позиция не появится в функции PositionsCheck, а впоследствии функция CleanupPosition никогда не будет вызываться к ее объектам до деинициализации. Чтобы исправить это, воспользуемся CHashSet.
Хэш-множество — это структура данных, которая хранит уникальные элементы в неупорядоченном виде, используя алгоритм хэширования для сопоставления каждого элемента с определенным индексом (bucket) для быстрого доступа. Они очень похожи на хэш-карты тем, что обеспечивают среднюю временную сложность O(1) (постоянное время) для таких операций, как добавление, удаление и поиск. Ключевое различие между ними заключается в том, что хэш-множество не сопоставляет ключи со значениями — оно хранит только уникальные ключи.
Изменим функцию PositionsCheck, чтобы удалять объекты, относящиеся к закрытым извне позициям. Используя хэш-множество, мы будем собирать тикеты позиций, которые встречаются при обходе списка в цикле for. Затем мы можем пройтись по тикетам, хранящимся в объекте gblOpenPositions и удалить позиции, отсутствующие в хэш-множестве.
Начните с объявления объекта CHashSet внутри функции PositionsCheck, чтобы хранить тикеты позиций типа ulong.
//--- Declare inside PositionsCheck CHashSet<ulong> curPositions;
Внутри цикла вызовите метод Add объекта curPositions, чтобы добавить тикет в хэш-множество.
for(int i = PositionsTotal()-1; i >= 0; i--) { CPositionInfo posInfo; if(!posInfo.SelectByIndex(i)) continue; if(posInfo.Symbol() != Symbol()) continue; if(gblOpenPositions.ContainsKey(posInfo.Ticket())) { CheckProcessedPosition(posInfo); } else { ProcessPosition(posInfo); } //--- Add the ticket of the current open position to hash set curPositions.Add(posInfo.Ticket()); posCount += 1; }
Для перебора ключей в объекте gblOpenPositions нам сначала понадобится скопировать ее ключи в массив с помощью метода CopyTo объекта gblOpenPositions. CopyTo принимает два массива для копирования как ключей, так и значений хэш-карты.
Сначала мы объявляем два пустых массива, используя[], которые соответствуют типам данных ключей и значений объекта gblOpenPositions, соответственно, затем применяем ArrayResize, чтобы изменить размер массивов в соответствии с количеством пар ключ/значение, полученным с помощью метода Count функции gblOpenPositions.
ulong openPos[]; // Array to store keys double csl[]; // Array to store values ArrayResize(openPos, gblOpenPositions.Count()); ArrayResize(csl, gblOpenPositions.Count());
CopyTo возвращает количество успешно скопированных пар ключ/значение. Мы можем использовать это для инициализации нашей переменной-счетчика в цикле for. Доступ к отдельным значениям массива можно получить с помощью [] и переменной-счетчика.
Мы будем перебирать ключи (openPos) и проверять для каждой позиции, присутствует ли она в хэш-множестве curPositions, использующем ее метод Contains. Если мы не сможем найти тикет позиции в множестве curPositions, значит, она в данный момент не открыта и требует очистки с помощью CleanupPosition.
int numPos = gblOpenPositions.CopyTo(openPos, csl); for(int i = numPos-1; i >= 0; i--) { if(!curPositions.Contains(openPos[i])) { CleanupPosition(openPos[i]); } }
Теперь у нашей функции PositionsCheck есть более надежная очистка.
//+------------------------------------------------------------------+ //| Scans and processes new positions, manages already processed ones| //+------------------------------------------------------------------+ void PositionsCheck() { CHashSet<ulong> curPositions; int posCount = 0; for(int i = PositionsTotal()-1; i >= 0; i--) { CPositionInfo posInfo; if(!posInfo.SelectByIndex(i)) continue; if(posInfo.Symbol() != Symbol()) continue; if(gblOpenPositions.ContainsKey(posInfo.Ticket())) { CheckProcessedPosition(posInfo); } else { ProcessPosition(posInfo); } curPositions.Add(posInfo.Ticket()); posCount += 1; } //--- Closed positions object cleanup ulong openPos[]; double csl[]; ArrayResize(openPos, gblOpenPositions.Count()); ArrayResize(csl, gblOpenPositions.Count()); int numPos = gblOpenPositions.CopyTo(openPos, csl); for(int i = numPos-1; i >= 0; i--) { if(!curPositions.Contains(openPos[i])) { CleanupPosition(openPos[i]); } } }
Управление несколькими позициями
В этом разделе применим линии-разделители (spacers), которые помогут визуально различать стоп-лоссы нескольких позиций. Логика нашего советника обеспечивает бесперебойное исполнение стоп-лосс ордеров для нескольких позиций. Однако при открытии нескольких позиций в непосредственной близости друг от друга, может быть трудно определить, к какой позиции относится та или иная линия стоп-лосса. Рисунок ниже иллюстрирует эту проблему.

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

Разделители решают эту проблему
Добавим их в наш советник.
Для построения разделителей получим свойство графика CHART_FIRST_VISIBLE_BAR с помощью функции ChartGetInteger. Эта функция возвращает индекс (начиная с правого края) самого левого видимого бара на графике. Кроме того, нам нужно сместить наш разделитель от самого левого бара для лучшего визуального размещения. Для смещения создадим глобальную переменную.
//--- Global variable int LeftOffset = 1; // Shift from the leftmost bar to draw the spacer
Индекс самого левого бара можно получить, используя ChartGetInteger. По умолчанию, функция ChartGetInteger возвращает значение ulong. Преобразуем его в тип int.
int curLeftBar = (int) ChartGetInteger(ChartID(), CHART_FIRST_VISIBLE_BAR);
Включим его в нашу функцию PositionsCheck, как только определим логику построения разделителей. Начнем с написания вспомогательной функции для получения названия линии разделителя по позиции.
//+------------------------------------------------------------------+ //| Returns the name of spacer line for the input position ticket | //+------------------------------------------------------------------+ string GetSpacerName(ulong ticket) { return CslPrefix + IntegerToString(ticket) + "_spacer"; }
Переменная posCount функции PositionsCheck ведет текущий подсчет открытых позиций по символу графика. Мы можем использовать ее в качестве индекса для определения общего смещения линии разделителя по каждой позиции.
Линия разделителя — это объект OBJ_TREND, для создания с помощью ObjectCreate требуются две опорные точки. Поскольку линия вертикальная, временные координаты этих двух точек будут одинаковыми. Ценовыми координатами являются цена линии открытия и цена линии стоп-лосса.
Чтобы получить временную координату бара, измеренную относительно самого левого бара для заданного индекса idx, будем использовать функцию iTime вместе с текущим периодом графика.
datetime time = iTime(Symbol(), Period(), curLeftBar-idx-LeftOffset); // idx is the index of the current open position obtained from posCount, curLeftBar is the index of the leftmost bar
Последний параметр iTime представляет собой смещение от текущего формирующегося бара. Поскольку индексы баров также начинаются с текущего формирующегося бара, по мере движения вправо вычитаем индекс позиции idx и LeftOffset из curLeftBar.
Мы создадим две перегрузки функций для функции DrawSpacer. Это функции с одинаковым именем, но разным количеством параметров или разными типами возвращаемых значений. Компилятор выберет соответствующий вызов функции, который согласуется с переданными аргументами.
Первая перегрузка создает объект-разделитель для новой позиции. Эта функция требует в качестве входных данных для отрисовки разделителя и значения openPrice , и значения cslPrice. Обратите внимание, что для двух точек привязки, которых требует ObjectCreate, точка привязки цены открытия имеет индекс 0, а точка привязки цены стоп-лосса — индекс 1. Эти индексы понадобятся нам при обновлении разделителей для существующих позиций.
//+------------------------------------------------------------------+ //| Draws the spacer line for a newly detected position | //+------------------------------------------------------------------+ void DrawSpacer(ulong ticket, double openPrice, double cslPrice, color clr, int curLeftBar, int idx) { string spacerName = GetSpacerName(ticket); datetime time = iTime(Symbol(), Period(), curLeftBar-idx-LeftOffset); int subWindow = 0; ObjectCreate(ChartID(), spacerName, OBJ_TREND, subWindow, time, openPrice, time, cslPrice); ObjectSetInteger(ChartID(), spacerName, OBJPROP_COLOR, clr); }
Вторая перегрузка отрисовывает разделитель для существующей открытой позиции. В этой функции нам не нужна цена открытия, поскольку она никогда не меняется. Мы используем ObjectSetInteger для установки временной координаты точки привязки цены открытия (индекс 0) и ObjectMove для изменения как временных, так и ценовых координат точки привязки цены стоп-лосса (индекс 1).
//+------------------------------------------------------------------+ //| Draws the spacer line for an existing open position | //+------------------------------------------------------------------+ void DrawSpacer(ulong ticket, double cslPrice, int curLeftBar, int idx) { string spacerName = GetSpacerName(ticket); datetime newTime = iTime(Symbol(), Period(), curLeftBar-idx-LeftOffset); ObjectSetInteger(ChartID(), spacerName, OBJPROP_TIME, newTime); ObjectMove(ChartID(), spacerName, 1, newTime, cslPrice); }
В соответствии с нашей концепцией объединения создания и удаления объектов, обновим CleanupPositionObjects, чтобы включить в нее удаление объекта-разделителя.
//+------------------------------------------------------------------+ //| Deletes objects related to the input ticket | //+------------------------------------------------------------------+ void CleanupPositionObjects(ulong ticket) { ObjectDelete(ChartID(), GetOpenName(ticket)); ObjectDelete(ChartID(), GetCslName(ticket)); ObjectDelete(ChartID(), GetSpacerName(ticket)); }
Для вызова DrawSpacer из ProcessPosition нам потребуется расширить сигнатуру функции, чтобы она принимала следующие параметры: curLeftBar и idx.
void ProcessPosition(CPositionInfo &posInfo, int curLeftBar, int idx)
Затем эти аргументы можно передать в вызов функции DrawSpacer. Поскольку ProcessPosition обрабатывает новую позицию, мы вызовем первую перегрузку DrawSpacer. Обновленная ProcessPosition с разделителями.
//+------------------------------------------------------------------+ //| Creates the objects for the open and local stop loss line | //+------------------------------------------------------------------+ void ProcessPosition(CPositionInfo &posInfo, int curLeftBar, int idx) { double openPrice = posInfo.PriceOpen(); double cslPrice = GetCslPrice(openPrice, posInfo.PositionType()); color lineColor = posInfo.PositionType() == POSITION_TYPE_BUY? InpLongColor : InpShortColor; string openName = GetOpenName(posInfo.Ticket()); string openLabel = GetOpenLabel(posInfo.PositionType(), posInfo.Volume(), openPrice); string cslName = GetCslName(posInfo.Ticket()); int subWindow = 0; // Main chart datetime time = 0; // OBJ_HLINE has no time coordinate //--- Create the open line ObjectCreate(ChartID(), openName, OBJ_HLINE, subWindow, time, openPrice); ObjectSetInteger(ChartID(), openName, OBJPROP_COLOR, lineColor); ObjectSetInteger(ChartID(), openName, OBJPROP_STYLE, STYLE_DASHDOT); ObjectSetString(ChartID(), openName, OBJPROP_TEXT, openLabel); //--- Create the stop loss line ObjectCreate(ChartID(), cslName, OBJ_HLINE, subWindow, time, cslPrice); ObjectSetInteger(ChartID(), cslName, OBJPROP_COLOR, lineColor); ObjectSetInteger(ChartID(), cslName, OBJPROP_SELECTABLE, true); ObjectSetInteger(ChartID(), cslName, OBJPROP_SELECTED, true); //--- Draw the spacer line for the new position DrawSpacer(posInfo.Ticket(), openPrice, cslPrice, lineColor, curLeftBar, idx); //--- Add position ticket to hashmap along with the stop price to mark as processed gblOpenPositions.Add(posInfo.Ticket(), cslPrice); }
Аналогичным образом, сигнатура функции CheckProcessedPosition расширяется, чтобы принимать параметры curLeftBar и idx.
void CheckProcessedPosition(CPositionInfo &posInfo, int curLeftBar, int idx)
Для обработанных позиций мы вызываем вторую перегрузку DrawSpacer. Кроме того, мы обновляем вызов функции ProcessPosition для обработки дополнительных аргументов в случае, если нам не удается найти линию стоп-лосса.
Обновленная версия CheckProcessedPosition с разделителями,
//+------------------------------------------------------------------+ //| Checks for stop condition and closes the position if met | //+------------------------------------------------------------------+ void CheckProcessedPosition(CPositionInfo &posInfo, int curLeftBar, int idx) { //--- Check if the stop loss line object exists string cslName = GetCslName(posInfo.Ticket()); if(ObjectFind(ChartID(), cslName) < 0) { CleanupPosition(posInfo.Ticket()); ProcessPosition(posInfo, curLeftBar, idx); return; } //--- Evaluate the stop condition MqlTick curTick; SymbolInfoTick(Symbol(), curTick); double cslPrice = ObjectGetDouble(ChartID(), cslName, OBJPROP_PRICE); if((posInfo.PositionType() == POSITION_TYPE_BUY && curTick.bid <= cslPrice) || (posInfo.PositionType() == POSITION_TYPE_SELL && curTick.ask >= cslPrice)) { Trade.PositionClose(posInfo.Ticket()); CleanupPosition(posInfo.Ticket()); return; } //--- Draw the spacer line for an existing position DrawSpacer(posInfo.Ticket(), cslPrice, curLeftBar, idx); //--- Update the value of the local stop loss in the hashmap double lastCsl; gblOpenPositions.TryGetValue(posInfo.Ticket(), lastCsl); if(lastCsl != cslPrice) { gblOpenPositions.TrySetValue(posInfo.Ticket(), cslPrice); } }
Наконец, обновляем PositionsCheck , чтобы получить значение самого левого столбца и передать его вместе с posCount в виде idx в ProcessPosition и CheckProcessedPosition. На этом завершается реализация разделителя. Обновленная версия PositionsCheck с разделителями.
//+------------------------------------------------------------------+ //| Scans and processes new positions, manages already processed ones| //+------------------------------------------------------------------+ void PositionsCheck() { int curLeftBar = (int) ChartGetInteger(ChartID(), CHART_FIRST_VISIBLE_BAR); CHashSet<ulong> curPositions; int posCount = 0; for(int i = PositionsTotal()-1; i >= 0; i--) { CPositionInfo posInfo; if(!posInfo.SelectByIndex(i)) continue; if(posInfo.Symbol() != Symbol()) continue; if(gblOpenPositions.ContainsKey(posInfo.Ticket())) { CheckProcessedPosition(posInfo, curLeftBar, posCount); } else { ProcessPosition(posInfo, curLeftBar, posCount); } curPositions.Add(posInfo.Ticket()); posCount += 1; } //--- Closed positions object cleanup ulong openPos[]; double csl[]; ArrayResize(openPos, gblOpenPositions.Count()); ArrayResize(csl, gblOpenPositions.Count()); int numPos = gblOpenPositions.CopyTo(openPos, csl); for(int i = numPos-1; i >= 0; i--) { if(!curPositions.Contains(openPos[i])) { CleanupPosition(openPos[i]); } } }
Маркировка сделки
В этом разделе внедрим единообразную маркировку сделок для наших позиций. Мы уже установили метку для открытой линии, используя свойство OBJPROP_TEXT объектов. Однако свойство графика, которое называется CHART_SHOW_OBJECT_DESCR, отображающее эту метку, по умолчанию отключено. Метки, отображающие направление сделки и объем позиции на данный момент, являются частью свойства графика, которое называется CHART_SHOW_TRADE_LEVELS. Пользователи могут включить или отключить это свойство из основного интерфейса графика.
Вполне логично предположить, что пользователь локальной системы стоп-лоссов предпочтет отключить свойство CHART_SHOW_TRADE_LEVELS во избежание помех графическим объектам системы стоп-лосса. При отключенных свойствах CHART_SHOW_TRADE_LEVELS и CHART_SHOW_OBJECT_DESCR позиция будет отображаться без каких-либо меток, что затруднит определение объема и направления сделки.

Метка сделки необходима, чтобы указать ее направление, когда уровни сделок на графике отключены.
Кроме того, нам хотелось бы избежать перезаписи, вызванной одновременным включением обеих меток. Следовательно, мы должны включить свойство CHART_SHOW_OBJECT_DESCR, когда свойство CHART_SHOW_TRADE_LEVELS отключено, и наоборот.
Поскольку пользователь мог в любой момент изменить свойство CHART_SHOW_TRADE_LEVELS, его состояние будет поддерживаться в глобальной переменной gblLastShowLevels для отслеживания изменений.
//--- Global variable bool gblLastShowLevels;
Мы можем получить ее значение, используя ChartGetInteger.
//--- Fetching the show trade levels chart property gblLastShowLevels = ChartGetInteger(ChartID(), CHART_SHOW_TRADE_LEVELS);
Чтобы установить значение свойства CHART_SHOW_OBJECT_DESCR, используйте ChartSetInteger.
ChartSetInteger(ChartID(), CHART_SHOW_OBJECT_DESCR, true); // To enable object description ChartSetInteger(ChartID(), CHART_SHOW_OBJECT_DESCR, false); // To disable object description
Определим функцию, которая устанавливает свойство CHART_SHOW_OBJECT_DESCR, исходя из текущего состояния свойства CHART_SHOW_TRADE_LEVELS.
//+------------------------------------------------------------------+ //| Sets the value of the object show description property | //+------------------------------------------------------------------+ void SetTradeLabels(bool isChartShowLevels) { if(!isChartShowLevels) { ChartSetInteger(ChartID(),CHART_SHOW_OBJECT_DESCR,true); } else { ChartSetInteger(ChartID(),CHART_SHOW_OBJECT_DESCR,false); } }
Мы можем инициализировать переменную gblLastShowLevels в OnInit и однократно запустить SetTradeLabels при инициализации.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Input validation if(InpPriceRange <= 0) { MessageBox("Stop Distance value cannot be 0 or negative"); return INIT_PARAMETERS_INCORRECT; } Trade.SetExpertMagicNumber(InpMagicNumber); StoplossDistance = GetStopDistance(); //--- Fetching the show trade levels chart property gblLastShowLevels = ChartGetInteger(ChartID(), CHART_SHOW_TRADE_LEVELS); //--- Setting the value of the object description property SetTradeLabels(gblLastShowLevels); //--- Running PositionsCheck() for the existing open positions PositionsCheck(); return(INIT_SUCCEEDED); }
В OnTick проверьте наличие изменений и обновите значение gblLastShowLevels, вызвав функцию SetTradeLabels , как только изменится значение свойства CHART_SHOW_TRADE_LEVELS.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { bool curShowLevels = ChartGetInteger(ChartID(), CHART_SHOW_TRADE_LEVELS); if(gblLastShowLevels != curShowLevels) { SetTradeLabels(curShowLevels); //--- Updating the show trade levels chart property gblLastShowLevels = curShowLevels; } PositionsCheck(); }
На этом мы завершаем разработку советника. После компиляции прикрепите его к графику. Наши позиции должны быть аккуратно помечены, как показано ниже, независимо от того, включены уровни сделки или нет.

Управление разрывами соединения
Локальный стоп-лосс работает на терминале MQL5 и для успешной работы требует активного сеанса с постоянным подключением. Для управления разрывами соединения мы можем использовать встроенный механизм обработки событий языка MQL5.
В документации OnDeinit перечислены причины, по которым срабатывает деинициализация. Следует отметить, что отключение не приводит к срабатыванию OnDeinit. Это означает, что наши объекты не исчезнут из-за потери соединения. Советник просто будет ждать следующего тика, как обычно, и возобновит работу, когда соединение будет восстановлено. То же самое относится и к нашим графическим объектам. Они сохранят то состояние, в котором находились на момент отключения. В это время линию стоп-лосса все еще можно перемещать, но ее значение будет обработано при следующем вызове OnTick. Следовательно, нет необходимости реализовывать какой-либо дополнительный код для обработки разрывов соединения. Для обеспечения бесперебойной работы локального стоп-лосса необходимо поддерживать непрерывное интернет-соединение всякий раз, когда советник подключен к графику.
Соберем все вместе
Мы подошли к концу нашего учебного пособия. Полный код локальной системы стоп-лоссов, включая наши доработки, прилагается ниже вместе с файлом .mq5.
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2026, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Your Name" #property link "https://www.mql5.com/" #property version "1.00" #property description "Local Stop Loss EA" #property script_show_inputs #include <Trade\Trade.mqh> #include <Trade\PositionInfo.mqh> #include <Generic\HashSet.mqh> #include <Generic\HashMap.mqh> //--- Enums enum StoplossType { stoploss_pips, // Pips - non Yen pairs stoploss_pips_yen, // Pips - Yen pairs stoploss_points // Points }; //--- Inputs input group "Stoploss Settings" input StoplossType InpSlType = stoploss_points; // Stoploss Type input double InpPriceRange = 0; // Stop distance input group "Colors" input color InpLongColor = clrOrange; // Buy Positions input color InpShortColor = clrPink; // Sell Positions input group "EA Identification Parameters" input ulong InpMagicNumber = 12345; // Magic Number //--- Global variables CTrade Trade; double StoplossDistance; CHashMap<ulong, double> gblOpenPositions; int LeftOffset = 1; bool gblLastShowLevels; string CslPrefix = "csl_"; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Input validation if(InpPriceRange <= 0) { MessageBox("Stop Distance value cannot be 0 or negative"); return INIT_PARAMETERS_INCORRECT; } Trade.SetExpertMagicNumber(InpMagicNumber); StoplossDistance = GetStopDistance(); //--- Fetching the show trade levels chart property gblLastShowLevels = ChartGetInteger(ChartID(), CHART_SHOW_TRADE_LEVELS); //--- Setting the value of the object description property SetTradeLabels(gblLastShowLevels); //--- Running PositionsCheck() for the existing open positions PositionsCheck(); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { ObjectsDeleteAll(ChartID(), CslPrefix); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { bool curShowLevels = ChartGetInteger(ChartID(), CHART_SHOW_TRADE_LEVELS); if(gblLastShowLevels != curShowLevels) { SetTradeLabels(curShowLevels); //--- Updating the show trade levels chart property gblLastShowLevels = curShowLevels; } PositionsCheck(); } //+------------------------------------------------------------------+ //| Scans and processes new positions, manages already processed ones| //+------------------------------------------------------------------+ void PositionsCheck() { int curLeftBar = (int) ChartGetInteger(ChartID(), CHART_FIRST_VISIBLE_BAR); CHashSet<ulong> curPositions; int posCount = 0; for(int i = PositionsTotal()-1; i >= 0; i--) { CPositionInfo posInfo; if(!posInfo.SelectByIndex(i)) continue; if(posInfo.Symbol() != Symbol()) continue; if(gblOpenPositions.ContainsKey(posInfo.Ticket())) { CheckProcessedPosition(posInfo, curLeftBar, posCount); } else { ProcessPosition(posInfo, curLeftBar, posCount); } curPositions.Add(posInfo.Ticket()); posCount += 1; } //--- Closed positions object cleanup ulong openPos[]; double csl[]; ArrayResize(openPos, gblOpenPositions.Count()); ArrayResize(csl, gblOpenPositions.Count()); int numPos = gblOpenPositions.CopyTo(openPos, csl); for(int i = numPos-1; i >= 0; i--) { if(!curPositions.Contains(openPos[i])) { CleanupPosition(openPos[i]); } } } //+------------------------------------------------------------------+ //| Creates the objects for the open and local stop loss line | //+------------------------------------------------------------------+ void ProcessPosition(CPositionInfo &posInfo, int curLeftBar, int idx) { double openPrice = posInfo.PriceOpen(); double cslPrice = GetCslPrice(openPrice, posInfo.PositionType()); color lineColor = posInfo.PositionType() == POSITION_TYPE_BUY? InpLongColor : InpShortColor; string openName = GetOpenName(posInfo.Ticket()); string openLabel = GetOpenLabel(posInfo.PositionType(), posInfo.Volume(), openPrice); string cslName = GetCslName(posInfo.Ticket()); int subWindow = 0; // Main chart datetime time = 0; // OBJ_HLINE has no time coordinate //--- Create the open line ObjectCreate(ChartID(), openName, OBJ_HLINE, subWindow, time, openPrice); ObjectSetInteger(ChartID(), openName, OBJPROP_COLOR, lineColor); ObjectSetInteger(ChartID(), openName, OBJPROP_STYLE, STYLE_DASHDOT); ObjectSetString(ChartID(), openName, OBJPROP_TEXT, openLabel); //--- Create the stop loss line ObjectCreate(ChartID(), cslName, OBJ_HLINE, subWindow, time, cslPrice); ObjectSetInteger(ChartID(), cslName, OBJPROP_COLOR, lineColor); ObjectSetInteger(ChartID(), cslName, OBJPROP_SELECTABLE, true); ObjectSetInteger(ChartID(), cslName, OBJPROP_SELECTED, true); //--- Draw the spacer line for the new position DrawSpacer(posInfo.Ticket(), openPrice, cslPrice, lineColor, curLeftBar, idx); //--- Add position ticket to hashmap along with the stop price to mark as processed gblOpenPositions.Add(posInfo.Ticket(), cslPrice); } //+------------------------------------------------------------------+ //| Checks for stop condition and closes the position if met | //+------------------------------------------------------------------+ void CheckProcessedPosition(CPositionInfo &posInfo, int curLeftBar, int idx) { //--- Check if the stop loss line object exists string cslName = GetCslName(posInfo.Ticket()); if(ObjectFind(ChartID(), cslName) < 0) { CleanupPosition(posInfo.Ticket()); ProcessPosition(posInfo, curLeftBar, idx); return; } //--- Evaluate the stop condition MqlTick curTick; SymbolInfoTick(Symbol(), curTick); double cslPrice = ObjectGetDouble(ChartID(), cslName, OBJPROP_PRICE); if((posInfo.PositionType() == POSITION_TYPE_BUY && curTick.bid <= cslPrice) || (posInfo.PositionType() == POSITION_TYPE_SELL && curTick.ask >= cslPrice)) { Trade.PositionClose(posInfo.Ticket()); CleanupPosition(posInfo.Ticket()); return; } //--- Draw the spacer line for an existing position DrawSpacer(posInfo.Ticket(), cslPrice, curLeftBar, idx); //--- Update the value of the local stop loss in the hashmap double lastCsl; gblOpenPositions.TryGetValue(posInfo.Ticket(), lastCsl); if(lastCsl != cslPrice) { gblOpenPositions.TrySetValue(posInfo.Ticket(), cslPrice); } } //+------------------------------------------------------------------+ //| Returns the stop distance for the input stop loss type | //+------------------------------------------------------------------+ double GetStopDistance() { switch(InpSlType) { case stoploss_pips: return InpPriceRange * 0.0001; case stoploss_pips_yen: return InpPriceRange * 0.01; case stoploss_points: return InpPriceRange; } return InpPriceRange; // This line is not really necessary as we have covered all the cases of the enumeration } //+------------------------------------------------------------------+ //| Draws the spacer line for a newly detected position | //+------------------------------------------------------------------+ void DrawSpacer(ulong ticket, double openPrice, double cslPrice, color clr, int curLeftBar, int idx) { string spacerName = GetSpacerName(ticket); datetime time = iTime(Symbol(), Period(), curLeftBar-idx-LeftOffset); int subWindow = 0; ObjectCreate(ChartID(), spacerName, OBJ_TREND, subWindow, time, openPrice, time, cslPrice); ObjectSetInteger(ChartID(), spacerName, OBJPROP_COLOR, clr); } //+------------------------------------------------------------------+ //| Draws the spacer line for an existing open position | //+------------------------------------------------------------------+ void DrawSpacer(ulong ticket, double cslPrice, int curLeftBar, int idx) { string spacerName = GetSpacerName(ticket); datetime newTime = iTime(Symbol(), Period(), curLeftBar-idx-LeftOffset); ObjectSetInteger(ChartID(), spacerName, OBJPROP_TIME, newTime); ObjectMove(ChartID(), spacerName, 1, newTime, cslPrice); } //+------------------------------------------------------------------+ //| Deletes objects related to the input ticket | //+------------------------------------------------------------------+ void CleanupPositionObjects(ulong ticket) { ObjectDelete(ChartID(), GetOpenName(ticket)); ObjectDelete(ChartID(), GetCslName(ticket)); ObjectDelete(ChartID(), GetSpacerName(ticket)); } //+------------------------------------------------------------------+ //| Deletes objects related to the input ticket and updates hashmap | //+------------------------------------------------------------------+ void CleanupPosition(ulong ticket) { CleanupPositionObjects(ticket); gblOpenPositions.Remove(ticket); } //+------------------------------------------------------------------+ //| Sets the value of the object show description property | //+------------------------------------------------------------------+ void SetTradeLabels(bool isChartShowLevels) { if(!isChartShowLevels) { ChartSetInteger(ChartID(),CHART_SHOW_OBJECT_DESCR,true); } else { ChartSetInteger(ChartID(),CHART_SHOW_OBJECT_DESCR,false); } } //+------------------------------------------------------------------+ //| Returns the price at which to create the local stop loss | //+------------------------------------------------------------------+ double GetCslPrice(double openPrice, ENUM_POSITION_TYPE posType) { return posType == POSITION_TYPE_BUY? openPrice - StoplossDistance : openPrice + StoplossDistance; // For buy: stop is below open price; for sell: stop is above open price } //+------------------------------------------------------------------+ //| Returns the name of the open line for the input position ticket | //+------------------------------------------------------------------+ string GetOpenName(ulong ticket) { return CslPrefix + IntegerToString(ticket) + "_open"; } //+------------------------------------------------------------------+ //| Returns the display label for the open line | //+------------------------------------------------------------------+ string GetOpenLabel(ENUM_POSITION_TYPE posType, double volume, double price) { string priceString = DoubleToString(volume,2) + " at " +DoubleToString(price, Digits()); // Volume normalized to 2 decimal places, price normalized to chart symbol Digits() return posType == POSITION_TYPE_BUY? " BUY " + priceString : " SELL " + priceString; } //+------------------------------------------------------------------+ //| Returns the name for the local stop loss line | //+------------------------------------------------------------------+ string GetCslName(ulong ticket) { return CslPrefix + IntegerToString(ticket) + "_line"; } //+------------------------------------------------------------------+ //| Returns the name of spacer line for the input position ticket | //+------------------------------------------------------------------+ string GetSpacerName(ulong ticket) { return CslPrefix + IntegerToString(ticket) + "_spacer"; } //+------------------------------------------------------------------+
Заключение
После краткого анализа исполнения стоп-лоссов по позициям bid/ask и проблемы публичного стоп-лосса в этом руководстве был разработан ориентированный на реальную разработку советник с локальным стоп-лоссом для MetaTrader 5. В статье была рассмотрена вся последовательность обработки данных: как триггеры стоп-ордеров сопоставляются с ценами bid/ask для каждой стороны, основанный на тиковых данных алгоритм мониторинга и выполнения, эффективное управление состоянием со сложностью O(n) при помощи CHashMap/CHashSet, графическое управление объектами (линия открытия, перетаскиваемая линия SL, вертикальные разделители), надежная очистка для закрытых извне позиций и деинициализация EA. Исполнение сделок осуществляется с помощью CTrade, а данные о позиции — с помощью CPositionInfo; а для удобства были добавлены маркировка графика и согласованная настройка свойств графика CHART_SHOW_OBJECT_DESCR/CHART_SHOW_TRADE_LEVELS. Также рассмотрено поведение советника при потере соединения: он возобновляет работу с поступлением следующего тика, а графические объекты сохраняются.
Результат: готовый к использованию советник на языке MQL5, который
Используйте этот советник в качестве безопасного и точного локального уровня управления рисками; при необходимости расширьте ее функциональность для ведения журналов, тестирования на исторических данных или интеграции с централизованными службами управления рисками.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22691
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Память между перезапусками: Сохранение состояния советника с помощью двоичных файлов в MQL5
Пакетный подход к разработке на языке MQL5 с помощью KnitPkg
От новичка до эксперта: Усиление стратегий ликвидности за счет структурного подтверждения на нескольких таймфреймах
От новичка до эксперта: Расширение стратегии на основе ликвидности с помощью трендовых фильтров
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования