English 中文 Español Deutsch 日本語 Português
preview
Осваиваем рыночную динамику: Создание советника на основе стратегии поддержки и сопротивления

Осваиваем рыночную динамику: Создание советника на основе стратегии поддержки и сопротивления

MetaTrader 5Трейдинг | 6 декабря 2024, 14:37
1 533 1
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В этой статье мы обсудим стратегию торговли на рынке Форекс с использованием уровней поддержки и сопротивления в контексте чистой торговли по ценовому действию, а также создание советника на ее основе. Мы рассмотрим определение стратегии, ее типы, описание и разработку на языке MetaQuotes Language 5 (MQL5) для MetaTrader 5 (MT5). Мы обсудим не только теорию, лежащую в основе стратегии, но и соответствующие концепции, анализ, идентификацию и визуализацию на графике, что сделает ее полезным инструментом для трейдеров для предсказания движений рынка и принятия более обоснованных решений. Статья содержит следующие разделы:

  1. Определение поддержки и сопротивления
  2. Описание уровней поддержки и сопротивления
  3. Типы поддержки и сопротивления
  4. Описание торговой стратегии
  5. План торговой стратегии
  6. Реализация в MetaQuotes Language 5 (MQL5)
  7. Результаты тестера стратегий
  8. Заключение

В нашей работе мы будем активно использовать язык MetaQuotes Language 5 (MQL5) в качестве нашей базовой среды разработки (IDE) и запускать файлы в торговом терминале MetaTrader 5 (MT5).


Определение поддержки и сопротивления

Уровни поддержки и сопротивления — важнейший аналитический инструмент, который используется многими трейдерами на рынке Форекс для анализа и определения ценовых уровней, на которых рынок с наибольшей вероятностью либо остановится, либо развернется. С технической точки зрения эти уровни имеют тенденцию отклоняться историческими ценами, что делает их значимыми с течением времени, поскольку цена останавливается и разворачивается, достигнув их, отсюда и их название — поддержка и сопротивление. Там, где формируются эти уровни, цена, как правило, многократно отскакивает от ключевых уровней, что указывает на сильный интерес к покупке или продаже. 


Описание уровней поддержки и сопротивления

Описание стратегии поддержки и сопротивления вращается вокруг ее применения в торговых сценариях. Уровни поддержки обычно указывают на нижнюю границу, которую цена пытается пробить, что свидетельствует о концентрации спроса, в то время как уровни сопротивления представляют собой верхнюю границу, указывающую на концентрацию предложения. Покупатели обычно входят на рынок на уровнях поддержки, и цены обычно растут, поэтому для трейдеров это хорошее время подумать о покупке или открытии длинной позиции. С другой стороны, на уровнях сопротивления в игру вступают продавцы, и цены могут упасть, что позволяет трейдерам продавать или занимать короткие позиции. Наглядное представление приведено ниже.

S & R

Вход на рынок всегда динамичен и зависит от вкуса и предпочтений, хотя существует два основных способа торговли по уровням. Некоторые трейдеры предпочитают торговать на отскоке, покупая, когда цена падает к уровням поддержки, и продавая, когда цена растет к уровням сопротивления. Другие трейдеры, наоборот, предпочитают торговать на прорыве, покупая, когда цена пробивает уровни сопротивления, и продавая, когда цена пробивает уровни поддержки. Другими словами, можно либо торговать внутри уровней (Fade the Break) или рассчитывать на прорыв (Trade the Break).


Типы поддержки и сопротивления

Существует четыре типа уровней поддержки и сопротивления.

  • Уровни поддержки и сопротивления в виде круглых чисел: Эти уровни формируются путем отскока цены от уже установленного уровня, что приводит к образованию горизонтального ценового канала. Например, минимумы колебаний рынка могут иметь уровни 0.65432, 0.65435, и 0.65437. Как правило, это одни и те же уровни с незначительным углом наклона, что свидетельствует о концентрации спроса.
  • Уровни поддержки и спроса трендового канала: Точки колебаний, образованные восходящими или нисходящими трендовыми линиями, создают зоны спроса и предложения, на которые цены, как правило, реагируют.

TRENDLINE S&R

  • Уровни поддержки и сопротивления Фибоначчи: Фибоначчи используется трейдерами для определения зон разворота цены, и эти зоны, как правило, действуют как зоны спроса и предложения для уровней поддержки и сопротивления.
  • Уровни поддержки и сопротивления индикатора: Технические индикаторы, такие как скользящие средние, определяют зоны, в которых цены, как правило, реагируют на создание опорных точек для уровней поддержки и сопротивления.

MA IND S&R


Описание торговой стратегии

Как мы увидели, на рынке Форекс существуют различные типы стратегий поддержки и сопротивления. Здесь мы будем работать с типом горизонтальных круглых чисел, а затем ту же концепцию можно будет применить и адаптировать для других типов.

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


План торговой стратегии

Чтобы легче понять изложенную идею, визуализируем ее в виде схемы.

  • Уровень сопротивления:

Алгоритм сопротивления

  • Уровень поддержки:

План поддержки


Реализация в MetaQuotes Language 5 (MQL5)

С теорией покончено. Давайте создадим советника на MQL5 для MetaTrader 5.

В терминале MetaTrader 5 выберите "Сервис" > "Редактор MetaQuotes Language" или просто нажмите F4. Кроме того, вы можете щелкнуть иконку IDE (интегрированная среда разработки) на панели инструментов. Откроется среда разработки на MQL5, которая позволяет писать торговых роботов, технические индикаторы, скрипты и библиотеки функций.

Открыть MetaEditor

На панели инструментов выберите "Файл" - "Новый файл" или нажмите CTRL + N, чтобы создать новый документ. Также вы можете нажать на иконку "Создать" в панели инструментов. Откроется окно Мастера MQL.

Новый советник

В открывшемся Мастере выберите Советник (шаблон) и нажмите Далее.

Мастер MQL

В общих свойствах укажите имя файла вашего советника. Чтобы указать или создать папку, если она не существует, используйте обратную косую черту перед именем советника. По умолчанию указана папка Experts\. Это значит, что наш советник будет создан в папке Experts. Остальные разделы довольно просты, но вы можете перейти по ссылке в нижней части Мастера, чтобы узнать детали.

Имя советника

После указания имени файла советника нажмите "Далее" > "Далее" > "Готово". Мы готовы к воплощению стратегии в коде.

Прежде всего используем #include в начале исходного кода. Он даст нам доступ к классу CTrade, который мы будем использовать для создания торгового объекта. Доступ нужен для открытия сделок.

#include <Trade/Trade.mqh>
CTrade obj_Trade;

Препроцессор заменит строку #include <Trade/Trade.mqh> с содержимым файла Trade.mqh. Угловые скобки указывают, что файл Trade.mqh будет взят из стандартного каталога (обычно это каталог_установки_терминала\MQL5\Include). Текущий каталог не включен в поиск. Строку можно разместить в любом месте программы, но обычно все включения размещаются в начале исходного кода для лучшей структуры кода и удобства ссылок. Благодаря разработчикам MQL5 объявление объекта obj_Trade класса CTrade предоставит нам легкий доступ к методам, содержащимся в этом классе.

Класс CTRADE

В глобальном масштабе нам необходимо определить массивы, которые будут содержать наши данные о самых высоких и самых низких ценах, которые мы позже будем обрабатывать и анализировать, чтобы найти уровни поддержки и сопротивления. После того, как уровни найдены, нам необходимо также сохранить их в массиве, поскольку их, конечно же, будет больше одного.

double pricesHighest[], pricesLowest[];

double resistanceLevels[2], supportLevels[2];

Здесь мы объявляем два двойных массива, которые будут содержать самые высокие и самые низкие цены для определенного объема данных, и еще два, которые будут содержать данные об определенных и отсортированных уровнях поддержки и сопротивления. Обычно это две координаты каждого уровня. Обратите внимание, что ценовые переменные являются пустыми, что делает их динамическими массивами без предопределенного размера, а значит, они могут содержать произвольное количество элементов на основе предоставленных данных. Напротив, переменные уровня имеют фиксированный размер, равный двум, что делает их статическими массивами, то есть каждый из них может содержать ровно два элемента. Если вы собираетесь использовать больше координат, вы можете увеличить их размер до определенного количества точек, которое вы считаете нужным.

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

#define resLine "RESISTANCE LEVEL"
#define colorRes clrRed
#define resline_prefix "R"

#define supLine "SUPPORT LEVEL"
#define colorSup clrBlue
#define supline_prefix "S"

Мы используем ключевое слово #define для определения макроса resLine со значением RESISTANCE LEVEL для простого сохранения имени нашего уровня сопротивления, вместо того, чтобы повторно вводить имя при каждом создании уровня, что значительно экономит наше время и снижает вероятность неправильного указания имени. По сути, макросы используются для подстановки текста во время компиляции.

Аналогично мы определяем цвет уровня сопротивления как красный и, наконец, определяем префикс R для уровней сопротивления, который мы будем использовать для обозначения линий сопротивления на графике. Мы определяем уровни поддержки, следуя тем же критериям.

После инициализации EA нам необходимо установить наши данные во временной ряд, поэтому сначала мы будем работать с последними данными и подготовим наши массивы хранения для хранения наших данных. Это делается в обработчике событий OnInit.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
//---
   
   ArraySetAsSeries(pricesHighest,true);
   ArraySetAsSeries(pricesLowest,true);
   // define the size of the arrays
   ArrayResize(pricesHighest,50);
   ArrayResize(pricesLowest,50);
//---
   return(INIT_SUCCEEDED);
}

Происходят две разные вещи. Сначала мы устанавливаем наши массивы хранения цен как временные ряды, используя встроенную MQL5-функцию ArraySetAsSeries, которая принимает два аргумента, целевой массив и логический флаг, в данном случае true, чтобы принять преобразование. Это означает, что массивы будут индексированы с самыми старыми данными по самому высокому индексу и самыми последними данными по индексу 0. Вот пример. Допустим, мы извлекаем данные с 2020 по 2024 год. Вот формат, в котором мы получаем данные.

Год Данные
2020 0
2021 1
2022 2
2023 3
2024 4

Данные в указанном выше формате неудобны в использовании, поскольку они расположены в хронологическом порядке, где самые старые данные индексируются по первому индексу, то есть они используются первыми. Удобнее сначала использовать для анализа самые последние данные, поэтому нам необходимо расположить данные в обратном хронологическом порядке, чтобы получить результаты, как показано ниже.

Год Данные
2024 4
2023 3
2022 2
2021 1
2020 0

Как объяснялось ранее, для достижения вышеуказанного формата программным путем мы используем функцию ArraySetAsSeries. Во-вторых, мы определяем размер массивов с помощью функции ArrayResize и указываем, что каждая из них содержит пятьдесят элементов. Это может быть что угодно, просто произвольное значение. Вы можете его проигнорировать. Однако для порядка нам не нужно слишком много данных в наших массивах, поскольку мы планируем отсортировать полученные данные о ценах так, чтобы остались только первые десять наиболее значимых данных. Дополнительный размер будет зарезервирован. Теперь вы понимаете, почему нет смысла увеличивать размер наших массивов.

В обработчике событий OnDeinit нам необходимо избавиться от данных хранилища, которые использовались в памяти компьютера. Это поможет сэкономить ресурсы.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
//---
   ArrayFree(pricesHighest);
   ArrayFree(pricesLowest);
   
   //ArrayFree(resistanceLevels); // cannot be used for static allocated array
   //ArrayFree(supportLevels); // cannot be used for static allocated array
   
   ArrayRemove(resistanceLevels,0,WHOLE_ARRAY);
   ArrayRemove(supportLevels,0,WHOLE_ARRAY);
}

Мы используем функцию ArrayFree, позволяющую избавиться от данных, занимающих часть памяти компьютера, поскольку советник больше не будет использоваться, а данные станут бесполезными. Функция представляет собой тип данных void, который принимает только один параметр или аргумент — динамический массив, освобождает его буфер и устанавливает размер нулевого измерения равным 0. Однако для статических массивов, где мы храним цены поддержки и сопротивления, эту функцию использовать нельзя. Это не значит, что мы не можем избавиться от данных. Мы вызываем другую функцию ArrayRemove, чтобы удалить ненужные данные. Функция представляет собой логический тип данных, который принимает три аргумента для удаления указанного количества элементов из массива. Мы указываем переменную целевого массива, индекс, с которого начинается удаление (в нашем случае это ноль, так как мы хотим удалить всё), и, наконец, количество элементов для удаления (в данном случае весь массив, чтобы избавиться от всего).

Большая часть работы будет проводиться в обработчике событий OnTick. Это будет чистое ценовое действие, и мы будем в значительной степени полагаться на этот обработчик событий. Итак, давайте рассмотрим параметры, которые принимает функция, поскольку это центральная часть всего кода.

void OnTick(){
//---

}

Как уже было видно, это простая, но важная функция, которая не принимает никаких аргументов и ничего не возвращает. It is just a void function, meaning it does not have to return anything. Функция используется в советниках и выполняется при появлении нового тика, то есть изменении котировок на конкретный товар.

Теперь, когда мы увидели, что функция OnTick генерируется при каждом изменении котировок цен, нам нужно определить некоторую логику управления, которая позволит нам запускать код, который будет выполняться один раз за бар, а не на каждом тике, по крайней мере, чтобы избежать ненужных запусков кода, тем самым экономя память устройства. Это будет необходимо при поиске уровней поддержки и сопротивления. Нам не нужно искать уровни на каждом тике, но мы всегда будем получать одни и те же результаты, при условии, что мы находимся на той же свече. Логика представлена ниже:

   int currBars = iBars(_Symbol,_Period);
   static int prevBars = currBars;
   if (prevBars == currBars) return;
   prevBars = currBars;

Сначала мы объявляем целочисленную переменную currBars, которая хранит рассчитанное количество текущих баров на графике для указанного торгового символа и таймфрейма. Это делается с помощью функции iBars, которая принимает всего два аргумента: symbol и period. 

Затем объявим еще одну статическую целочисленную переменную prevBars для хранения общего количества предыдущих баров на графике при генерации нового бара и по-прежнему инициализируем ее значением текущих баров на графике для первого запуска функции. Использовать ее для сравнения текущего количества баров с предыдущим, чтобы определить момент появления нового бара на графике.

Наконец, используем условный оператор, чтобы проверить, равно ли текущее количество баров предыдущему. Если они равны, это означает, что новый бар не сформировался, поэтому мы прекращаем дальнейшее выполнение и используем return. В противном случае, если текущее и предыдущее количество баров не равны, это указывает на то, что образовался новый бар. В этом случае мы приступаем к обновлению переменной предыдущих баров до текущих баров, так что на следующем тике она будет равна количеству баров на графике, если только мы не перейдем к новому.

Для анализа следует учитывать только видимые бары на графике. Это связано с тем, что нам не нужно учитывать самые старые данные, например, для десяти миллионов баров, поскольку это было бы бесполезно. Представьте, что у вас есть уровень поддержки, который датируется прошлым годом. Странно, правда? Поэтому сейчас мы рассмотрим только те бары, которые видны на графике, поскольку это самые последние актуальные данные о текущих рыночных условиях. Для этого мы используем следующую логику.

   int visible_bars = (int)ChartGetInteger(0,CHART_VISIBLE_BARS);

Мы объявляем целочисленную переменную visible_bars и используем функцию ChartGetInteger для получения общего количества видимых баров на графике. Это функция данных типа long, поэтому мы приводим ее в целое число, добавляя (int) перед функцией. Конечно, мы могли бы определить нашу целевую переменную как long, но нам не нужно выделять такие большие байты памяти. 

Для поиска уровней нам придется пройтись по каждому бару. Решающее значение здесь имеет цикл for.

   for (int i=1; i<=visible_bars-1; i++){
   ...
   
   }

Цикл выполняется путем первоначальной инициализации целочисленной переменной счетчика цикла i значением один, чтобы обозначить начальную точку цикла. Один означает, что мы начинаем с бара, предшествующего текущему, поскольку текущий бар находится в стадии формирования и, таким образом, еще не определен. Далее приведено условие, которое должно быть истинным для продолжения выполнения цикла. Пока i меньше или равно общему числу рассматриваемых баров за вычетом одного, цикл будет продолжать выполняться. Наконец, мы увеличиваем счетчик цикла i на единицу каждый раз, когда цикл выполняется. Проще говоря, i++ — это то же самое, что i=i+1. Мы также могли бы использовать декрементный цикл, который имел бы оператор счетчика цикла --, что привело бы к анализу с самого старого последнего бара до текущего, но мы решили использовать инкрементный цикл, чтобы анализ баров начинался с самого последнего бара и заканчивался самым старым.

В каждом цикле мы выбираем бар или свечу, и таким образом нам необходимо получить свойства бара. В этом случае для нас важны только свойства бара: открытие, максимум, минимум, закрытие и время.

      double open = iOpen(_Symbol,_Period,i);
      double high = iHigh(_Symbol,_Period,i);
      double low = iLow(_Symbol,_Period,i);
      double close = iClose(_Symbol,_Period,i);
      datetime time = iTime(_Symbol,_Period,i);

Здесь мы объявляем соответствующие переменные типа данных и инициализируем их соответствующими данными. Например, используйте функцию iOpen для получения цены открытия бара, указав имя символа, его период и индекс целевого бара.

После получения данных бара нам необходимо будет сравнить их с данными остальных предыдущих баров, чтобы найти бар с такими же данными, как у выбранного, что будет означать уровень, на который цена реагировала несколько раз в прошлом. Однако не имеет смысла иметь уровень поддержки или сопротивления, состоящий из двух последовательных уровней. Уровни должны быть, по крайней мере, далеко друг от друга. Давайте сначала сделаем следующее.

      int diff_i_j = 10;

Мы определяем целочисленную переменную, содержащую разницу в барах между текущим баром и баром, который следует учитывать для проверки соответствия того же уровня. В нашем случае это 10. Визуально это выглядит так.

Разница в барах

Теперь мы можем инициировать цикл, включающий логику.

      for (int j=i+diff_i_j; j<=visible_bars-1; j++){
      ...
      
      }

Для внутреннего цикла for мы используем целочисленную переменную-счетчик j, которая начинается с десяти баров от текущего и продолжается до предпоследнего бара. Введем визуализацию, чтобы лучшее всё понять и ознакомиться с результатами.

//Print in the outer loop
      Print(":: BAR NO: ",i);

      //Print in the inner loop
         Print("BAR CHECK NO: ",j);

Выбранные бары цикла

Вы можете видеть, что, например, для выбранного бара с индексом 15 мы инициализируем цикл на 10 баров от текущего выбранного бара. Математически это 15+10=25. Затем с 25-го бара цикл выполняется до предпоследнего бара, который в нашем случае равен 33-му.

Теперь, когда мы можем правильно выбрать интервал столбцов по мере необходимости, мы можем получить и свойства выбранных столбцов.

         double open_j = iOpen(_Symbol,_Period,j);
         double high_j = iHigh(_Symbol,_Period,j);
         double low_j = iLow(_Symbol,_Period,j);
         double close_j = iClose(_Symbol,_Period,j);
         datetime time_j = iTime(_Symbol,_Period,j);

Используется та же логика, что и при извлечении свойств внешнего цикла. Единственное отличие состоит в том, что мы определяем наши переменные с дополнительной нижней чертой _j, чтобы обозначить, что свойства предназначены для внутреннего цикла for, а целевой индекс панели изменяется на j.

Поскольку теперь у нас есть все необходимые ценовые данные, мы можем приступить к проверке уровней поддержки и сопротивления.

         // CHECK FOR RESISTANCE
         double high_diff = NormalizeDouble((MathAbs(high-high_j)/_Point),0);
         bool is_resistance = high_diff <= 10;
         
         // CHECK FOR SUPPORT
         double low_diff = NormalizeDouble((MathAbs(low-low_j)/_Point),0);
         bool is_support = low_diff <= 10;

Для проверки уровней сопротивления мы определяем двойную переменную high_diff, которая будет хранить наши данные о разнице между максимумом текущего выбранного бара во внешнем цикле и баром, который в данный момент выбран во внутреннем цикле. Функция MathAbs используется для того, чтобы гарантировать, что результат будет положительным числом независимо от того, какая цена выше, возвращая абсолютное или модульное значение входных данных. Например, у нас может быть 0,65432 - 0,05456 = -0,00024. Наш ответ содержит отрицательный знак, но функция проигнорирует минус и выведет 0,00024. Опять же, необходимо разделить результат на пункт (минимально возможное изменение цены инструмента), чтобы получить разницу в пунктах. Если мы вернемся к нашему примеру, это будет 0,00024/0,00001 = 24,0. Наконец, чтобы быть точным, мы форматируем число с плавающей точкой до указанного количества цифр с помощью функции NormalizeDouble. В этом случае у нас ноль, что означает, что на выходе мы получим целое число. Опять возвращаясь к нашему примеру, у нас было бы 24 без десятичной точки.

Затем мы проверяем, меньше ли разница или равна десяти (предопределенный диапазон, в котором разница допустима), и сохраняем результат в логической переменной is_resistance. Та же логика применяется и к проверке уровня поддержки.

Если условия наших уровней выполнены, наши уровни поддержки и сопротивления будут истинными (true), в противном случае они будут ложными (false). Чтобы проверить, сможем ли мы определить уровни, давайте распечатаем их в журнале. Для перепроверки мы печатаем цены координат уровней сопротивления вместе с их ценовыми разницами, чтобы убедиться, что они соответствуют нашим условиям.

         if (is_resistance){
            Print("RESISTANCE AT BAR ",i," (",high,") & ",j," (",high_j,"), Pts = ",high_diff);
          ...  
         }

Вот что мы получаем в результате.

Сопротивление

Мы можем определить уровни, но это всего лишь уровни. Поскольку нам нужны уровни, которые находятся в самых высоких или самых низких точках, нам понадобится дополнительная логика, чтобы гарантировать, что мы рассматриваем только самые значимые уровни. Для этого нам нужно будет скопировать максимальную и минимальную цены рассматриваемых баров, отсортировать их в порядке возрастания и убывания соответственно, а затем взять первое и последнее количество необходимых баров соответственно. Мы делаем это перед циклами for.

   ArrayFree(pricesHighest);
   ArrayFree(pricesLowest);
   
   int copiedBarsHighs = CopyHigh(_Symbol,_Period,1,visible_bars,pricesHighest);
   int copiedBarsLows = CopyLow(_Symbol,_Period,1,visible_bars,pricesLowest);

Перед сохранением мы освобождаем наши массивы от всех данных. Затем мы копируем максимумы баров в целевой массив. Это достигается за счет использования функции целочисленного типа данных CopyHigh, предоставляющей символ, период, начальный индекс копируемого столбца, количество столбцов и целевой массив хранения. Результат, представляющий собой количество скопированных баров, присваивается целочисленной переменной copyBarsHighs. То же самое касается и низких цен. Чтобы убедиться, что мы получаем данные, мы печатаем массивы в журнале, используя функцию ArrayPrint.

   ArrayPrint(pricesHighest);
   ArrayPrint(pricesLowest);

Вот какие результаты мы получаем.

Несортированные данные

Затем мы сортируем данные в порядке возрастания, используя функцию ArraySort и снова распечатаем результаты.

         // sort the array in ascending order
   ArraySort(pricesHighest);
   ArraySort(pricesLowest);

   ArrayPrint(pricesHighest);
   ArrayPrint(pricesLowest);

Вот что мы получаем.

Сортировка данных в порядке возрастания

Наконец, нам нужно получить первые десять цен: самые высокие цены и последние десять цен из самых низких цен, данные, которые сформируют наши экстремальные точки.

   ArrayRemove(pricesHighest,10,WHOLE_ARRAY);

Чтобы получить первые десять самых высоких цен самых высоких баров, мы используем функцию ArrayRemove и предоставляем целевой массив, индекс, с которого начинается удаление, в нашем случае десять, и, наконец, количество элементов, которые нужно удалить, в нашем случае - оставшиеся данные.

Чтобы получить последние десять минимальных цен самых низких баров, выполняется аналогичная операция, но более сложным и менее простым методом.

   ArrayRemove(pricesLowest,0,visible_bars-10);

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

Окончательные требуемые данные

Поскольку теперь у нас есть данные по самым высоким барам, мы можем продолжить сверку уровней и определить допустимые настройки. Запустим еще один цикл for для выполнения операции.

            for (int k=0; k<ArraySize(pricesHighest); k++){
            ...
            }

На этот раз наша переменная-счетчик k начинается с нуля, поскольку мы хотим учесть все цены в массиве. 

Поскольку мы хотим найти совпадения цен, мы объявляем логические переменные хранения, которые будут хранить флаги результатов совпадений, за пределами циклов for, и инициализируем их значением false.

   bool matchFound_high1 = false, matchFound_low1 = false;
   bool matchFound_high2 = false, matchFound_low2 = false;

Если выбранная сохраненная цена равна максимуму бара в первом цикле, мы устанавливаем флаг для первого найденного максимума на значение true и информируем об этом экземпляре. Аналогично, если выбранная сохраненная цена равна максимуму бара во втором цикле, мы устанавливаем флаг для второго найденного максимума на значение true и информируем об этом экземпляре.

               if (pricesHighest[k]==high){
                  matchFound_high1 = true;
                  Print("> RES H1(",high,") FOUND @ ",k," (",pricesHighest[k],")");
               }
               if (pricesHighest[k]==high_j){
                  matchFound_high2 = true;
                  Print("> RES H2(",high_j,") FOUND @ ",k," (",pricesHighest[k],")");
               }

Если совпадение двух координат найдено, но текущие уровни сопротивления равны ценам, то это означает, что у нас уже есть уровни. Поэтому нам нет необходимости продолжать создавать новые уровни сопротивления. Мы информируем о находке, устанавливаем флаг stop_processing в значение true и преждевременно выходим из цикла.

               if (matchFound_high1 && matchFound_high2){
                  if (resistanceLevels[0]==high || resistanceLevels[1]==high_j){
                     Print("CONFIRMED BUT This is the same resistance level, skip updating!");
                     stop_processing = true; // Set the flag to stop processing
                     break; // stop the inner loop prematurely
                  }
                  ...
                  
               }

Флаг stop_processing определен вне циклов for в верхней части и включен в первую логику выполнения цикла for, чтобы гарантировать экономию ресурсов.

   bool stop_processing = false; // Flag to control outer loop

//...
   for (int i=1; i<=visible_bars-1 && !stop_processing; i++){
      ...
   
   }

В противном случае, если совпадение двух координат найдено, но они не равны текущим ценам, это означает, что у нас есть еще один новый уровень сопротивления, и мы можем обновить его в соответствии с последними данными. 

                  else {
                     Print(" ++++++++++ RESISTANCE LEVELS CONFIRMED @ BARS ",i,
                     "(",high,") & ",j,"(",high_j,")");
                     resistanceLevels[0] = high;
                     resistanceLevels[1] = high_j;
                     ArrayPrint(resistanceLevels);
                     
                     ...
                  }

Вот визуализация полученных нами результатов.

Подтвержденные уровни сопротивления

Отобразим уровни на графике.

                     draw_S_R_Level(resLine,high,colorRes,5);
                     draw_S_R_Level_Point(resline_prefix,high,time,218,-1,colorRes,90);
                     draw_S_R_Level_Point(resline_prefix,high,time_j,218,-1,colorRes,90);

                     stop_processing = true; // Set the flag to stop processing
                     break;

Для этой работы мы используем две функции. Первая (draw_S_R_Level) принимает имя линии, которую нужно нарисовать, цену, цвет и ширину линии.

void draw_S_R_Level(string levelName,double price,color clr,int width){
   if (ObjectFind(0,levelName) < 0){
      ObjectCreate(0,levelName,OBJ_HLINE,0,TimeCurrent(),price);
      ObjectSetInteger(0,levelName,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,levelName,OBJPROP_WIDTH,width);
   }
   else {
      ObjectSetDouble(0,levelName,OBJPROP_PRICE,price);
   }
   ChartRedraw(0);
}

Функция имеет тип данных void, что означает, что она не должна ничего возвращать. Затем мы используем условный оператор для проверки существования объекта с помощью функции ObjectFind, которая возвращает отрицательное целое число в случае, если объект не найден. Если это так, то мы приступаем к созданию объекта, идентифицированного как OBJ_HLINE, с текущим временем и указанной ценой, поскольку для этого требуется всего одна координата. Затем мы задаем его цвет и ширину. Если объект найден, мы просто обновляем его цену до указанной и перерисовываем график для применения текущих изменений. Эта функция просто рисует на графике прямую линию. Вот что мы получаем.

Прямая линия сопротивления

Вторая функция draw_S_R_Level_Point принимает имя линии, которую нужно нарисовать, цену, время, код стрелки, направление, цвет и угол метки описания. Эта функция рисует точки уровня таким образом, чтобы они были более четкими на нарисованной линии сопротивления.
void draw_S_R_Level_Point(string objName,double price,datetime time,
      int arrowcode,int direction,color clr,double angle){
   //objName = " ";
   StringConcatenate(objName,objName," @ \nTime: ",time,"\nPrice: ",DoubleToString(price,_Digits));
   if (ObjectCreate(0,objName,OBJ_ARROW,0,time,price)) {
      ObjectSetInteger(0,objName,OBJPROP_ARROWCODE,arrowcode);
      ObjectSetInteger(0,objName,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,objName,OBJPROP_FONTSIZE,10);
      if (direction > 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_TOP);
      if (direction < 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_BOTTOM);
   }
   string prefix = resline_prefix;
   string txt = "\n"+prefix+"("+DoubleToString(price,_Digits)+")";
   string objNameDescription = objName + txt;
   if (ObjectCreate(0,objNameDescription,OBJ_TEXT,0,time,price)) {
     // ObjectSetString(0,objNameDescription,OBJPROP_TEXT, "" + txt);
      ObjectSetInteger(0,objNameDescription,OBJPROP_COLOR,clr);
      ObjectSetDouble(0,objNameDescription,OBJPROP_ANGLE, angle);
      ObjectSetInteger(0,objNameDescription,OBJPROP_FONTSIZE,10);
      if (direction > 0) {
         ObjectSetInteger(0,objNameDescription,OBJPROP_ANCHOR,ANCHOR_LEFT);
         ObjectSetString(0,objNameDescription,OBJPROP_TEXT, "    " + txt);
      }
      if (direction < 0) {
         ObjectSetInteger(0,objNameDescription,OBJPROP_ANCHOR,ANCHOR_BOTTOM);
         ObjectSetString(0,objNameDescription,OBJPROP_TEXT, "    " + txt);
      }
   }
   ChartRedraw(0);
}

Пользовательская функция draw_S_R_Level_Point принимает семь параметров для облегчения ее повторного использования. Функции параметров следующие:

  • objName - имя создаваемого графического объекта.
  • price - значение double, представляющее координату цены, в которой должен быть размещен объект.
  • time - дата и время размещения объекта.
  • arrowCode - целое число, указывающее код стрелки для объекта стрелки.
  • direction - целое число, указывающее направление (вверх или вниз) позиционирования текстовой метки.
  • clr - значение цвета (например, clrBlue, clrRed) для графических объектов.
  • angle - угол расположения метки описания.

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

Затем функция проверяет, существует ли уже объект с указанным objName на графике. Если нет, она переходит к созданию объектов. Создание объекта осуществляется с помощью встроенной функции ObjectCreate, которая требует указания объекта, который необходимо нарисовать, в данном случае объекта-стрелки, идентифицированного как OBJ_ARROW, а также времени и цены, которые формируют ординаты точки создания объекта. После этого задаем свойства объекта: код стрелки, цвет, размер шрифта и точку привязки. Для кода стрелки в MQL5 уже есть некоторые предопределенные символы шрифта wingdings, который можно использовать напрямую. Таблица символов:

WINGDINGS

До этого момента мы только рисовали указанную стрелку на графике следующим образом:

Сопротивление + стрелка

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

Затем создается переменная objNameDescription путем объединения исходной objName с текстом описания. Это объединенное имя гарантирует, что стрелка и соответствующая ей текстовая метка связаны между собой. Для этого используется следующий фрагмент кода.

   string prefix = resline_prefix;
   string txt = "\n"+prefix+"("+DoubleToString(price,_Digits)+")";
   string objNameDescription = objName + txt;
   if (ObjectCreate(0,objNameDescription,OBJ_TEXT,0,time,price)) {
     // ObjectSetString(0,objNameDescription,OBJPROP_TEXT, "" + txt);
      ObjectSetInteger(0,objNameDescription,OBJPROP_COLOR,clr);
      ObjectSetDouble(0,objNameDescription,OBJPROP_ANGLE, angle);
      ObjectSetInteger(0,objNameDescription,OBJPROP_FONTSIZE,10);
      if (direction > 0) {
         ObjectSetInteger(0,objNameDescription,OBJPROP_ANCHOR,ANCHOR_LEFT);
         ObjectSetString(0,objNameDescription,OBJPROP_TEXT, "    " + txt);
      }
      if (direction < 0) {
         ObjectSetInteger(0,objNameDescription,OBJPROP_ANCHOR,ANCHOR_BOTTOM);
         ObjectSetString(0,objNameDescription,OBJPROP_TEXT, "    " + txt);
      }
   }

Вот что мы получаем в результате объединения точек сопротивления с их описаниями.

Сопротивление + стрелка + описание

Для отображения уровней поддержки применяется та же логика, но с обратными условиями.

         else if (is_support){
            //Print("SUPPORT AT BAR ",i," (",low,") & ",j," (",low_j,"), Pts = ",low_diff);
            
            for (int k=0; k<ArraySize(pricesLowest); k++){
               if (pricesLowest[k]==low){
                  matchFound_low1 = true;
                  //Print("> SUP L1(",low,") FOUND @ ",k," (",pricesLowest[k],")");
               }
               if (pricesLowest[k]==low_j){
                  matchFound_low2 = true;
                  //Print("> SUP L2(",low_j,") FOUND @ ",k," (",pricesLowest[k],")");
               }
               if (matchFound_low1 && matchFound_low2){
                  if (supportLevels[0]==low || supportLevels[1]==low_j){
                     Print("CONFIRMED BUT This is the same support level, skip updating!");
                     stop_processing = true; // Set the flag to stop processing
                     break; // stop the inner loop prematurely
                  }
                  else {
                     Print(" ++++++++++ SUPPORT LEVELS CONFIRMED @ BARS ",i,
                     "(",low,") & ",j,"(",low_j,")");
                     supportLevels[0] = low;
                     supportLevels[1] = low_j;
                     ArrayPrint(supportLevels);
                     
                     draw_S_R_Level(supLine,low,colorSup,5);
                     draw_S_R_Level_Point(supline_prefix,low,time,217,1,colorSup,-90);
                     draw_S_R_Level_Point(supline_prefix,low,time_j,217,1,colorSup,-90);

                     stop_processing = true; // Set the flag to stop processing
                     break;
                  }
               }
            }
         }

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

Уровни поддержки и сопротивления

Затем мы продолжаем следить за уровнями, и если уровни выходят за пределы видимых баров, мы считаем уровень сопротивления недействительным и удаляем его. Для этого нам нужно будет найти линии уровня объекта, а когда они будут найдены, мы проверим условия на предмет их действительности.

   if (ObjectFind(0,resLine) >= 0){
      double objPrice = ObjectGetDouble(0,resLine,OBJPROP_PRICE);
      double visibleHighs[];
      ArraySetAsSeries(visibleHighs,true);
      CopyHigh(_Symbol,_Period,1,visible_bars,visibleHighs);
      //Print("Object Found & visible bars is: ",ArraySize(visibleHighs));
      //ArrayPrint(visibleHighs);
      bool matchHighFound = false;

      ...
   }

Здесь мы проверяем, найден ли объект линии сопротивления, и если да, то получаем его цену. Мы снова копируем максимумы видимых баров на графике и сохраняем их в переменной double-массива visibleHighs.

После этого мы проходим по максимальным ценам и пытаемся найти соответствие между ценой текущего выбранного бара и ценой линии сопротивления. Если совпадение есть, устанавливаем флаг matchHighFound в true и завершаем цикл.

      for (int i=0; i<ArraySize(visibleHighs); i++){
         if (visibleHighs[i] == objPrice){
            Print("> Match price for resistance found at bar # ",i+1," (",objPrice,")");
            matchHighFound = true;
            break;
         }
      }

Если совпадения нет, это означает, что уровень сопротивления слишком далеко. Мы информируем об экземпляре и используем пользовательскую функцию для удаления объекта.

      if (!matchHighFound){
         Print("(",objPrice,") > Match price for the resistance line not found. Delete!");
         deleteLevel(resLine);
      }

Пользовательская функция deleteLevel принимает только один аргумент — имя уровня, который необходимо удалить, и использует функцию ObjectDelete для удаления определенного объекта.

void deleteLevel(string levelName){
   ObjectDelete(0,levelName);
   ChartRedraw(0);
}

Та же логика применима к линии уровня поддержки, но с обратными условиями.

   if (ObjectFind(0,supLine) >= 0){
      double objPrice = ObjectGetDouble(0,supLine,OBJPROP_PRICE);
      double visibleLows[];
      ArraySetAsSeries(visibleLows,true);
      CopyLow(_Symbol,_Period,1,visible_bars,visibleLows);
      //Print("Object Found & visible bars is: ",ArraySize(visibleLows));
      //ArrayPrint(visibleLows);
      bool matchLowFound = false;
      
      for (int i=0; i<ArraySize(visibleLows); i++){
         if (visibleLows[i] == objPrice){
            Print("> Match price for support found at bar # ",i+1," (",objPrice,")");
            matchLowFound = true;
            break;
         }
      }
      if (!matchLowFound){
         Print("(",objPrice,") > Match price for the support line not found. Delete!");
         deleteLevel(supLine);
      }
   }

Наконец, если до этого момента уровни сопротивления и поддержки все еще находятся в пределах графика, это означает, что они действительны, и, таким образом, мы можем продолжить создавать логику для определения их пробития и открытия рыночной позиции. Давайте сначала рассмотрим прорыв уровня сопротивления.

Поскольку цена может пробить уровень сопротивления несколько раз, что приведет к генерации нескольких сигналов, нам нужна логика, которая гарантирует, что как только уровень сопротивления пробит и сгенерирован сигнал, он не сработает опять при пробитии того же самого уровня. Для этого мы объявляем статическую double-переменную, которая будет содержать нашу цену сигнала, сохраняющую свое значение до тех пор, пока у нас не появится другой сигнал.

   static double ResistancePriceTrade = 0;
   if (ObjectFind(0,resLine) >= 0){
      double ResistancePriceLevel = ObjectGetDouble(0,resLine,OBJPROP_PRICE);
      if (ResistancePriceTrade != ResistancePriceLevel){
      ...

   }

Затем мы проверяем существование линии сопротивления, и если она существует, то получаем ее цену. Используя условный оператор, мы проверяем, не равен ли сигнал цене линии сопротивления, что означает, что у нас еще нет сгенерированного сигнала для этого конкретного уровня и что мы можем перейти к проверке сигнала прорыва. Для проверки нам понадобятся данные предыдущего бара, а также обновленные котировки.

         double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
         double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
         double open1 = iOpen(_Symbol,_Period,1);
         double high1 = iHigh(_Symbol,_Period,1);
         double low1 = iLow(_Symbol,_Period,1);
         double close1 = iClose(_Symbol,_Period,1);

Затем мы используем условные операторы, чтобы проверить, пробила ли цена уровень сопротивления снизу вверх. Если это так, мы информируем о сигнале на продажу посредством сообщения в журнале. Затем мы используем объект trade и оператор-точку, чтобы получить доступ к методу входа на продажу и предоставить необходимые параметры. Наконец, мы обновляем значение переменной сигнала до текущего уровня сопротивления, чтобы не генерировать другой сигнал на основе того же уровня сопротивления.

         if (open1 > close1 && open1 < ResistancePriceLevel
            && high1 > ResistancePriceLevel && Bid < ResistancePriceLevel){
            Print("$$$$$$$$$$$$ SELL NOW SIGNAL!");
            obj_Trade.Sell(0.01,_Symbol,Bid,Bid+350*5*_Point,Bid-350*_Point);
            ResistancePriceTrade = ResistancePriceLevel;
         }

Та же логика применима и к логике прорыва уровня поддержки, но с обратными условиями.

   static double SupportPriceTrade = 0;
   if (ObjectFind(0,supLine) >= 0){
      double SupportPriceLevel = ObjectGetDouble(0,supLine,OBJPROP_PRICE);
      if (SupportPriceTrade != SupportPriceLevel){
         double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
         double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
         double open1 = iOpen(_Symbol,_Period,1);
         double high1 = iHigh(_Symbol,_Period,1);
         double low1 = iLow(_Symbol,_Period,1);
         double close1 = iClose(_Symbol,_Period,1);

         if (open1 < close1 && open1 > SupportPriceLevel
            && low1 < SupportPriceLevel && Ask > SupportPriceLevel){
            Print("$$$$$$$$$$$$ BUY NOW SIGNAL!");
            obj_Trade.Buy(0.01,_Symbol,Ask,Ask-350*5*_Point,Ask+350*_Point);
            SupportPriceTrade = SupportPriceLevel;
         }
         
      }
   }

Ниже представлен результат.

Торговля от уровней поддержки и сопротивления

Ниже приведен полный код, необходимый для создания алгоритма торговли по уровням поддержки и сопротивления, на языке MQL5. Алгоритм определяет уровни, отображает их на графике и соответственно открывает рыночные позиции.

//+------------------------------------------------------------------+
//|                                       RESISTANCE AND SUPPORT.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include <Trade/Trade.mqh>
CTrade obj_Trade;

//bool stop_processing = false;

double pricesHighest[], pricesLowest[];

double resistanceLevels[2], supportLevels[2];


#define resLine "RESISTANCE LEVEL"
#define colorRes clrRed
#define resline_prefix "R"

#define supLine "SUPPORT LEVEL"
#define colorSup clrBlue
#define supline_prefix "S"

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
//---
   
   ArraySetAsSeries(pricesHighest,true);
   ArraySetAsSeries(pricesLowest,true);
   // define the size of the arrays
   ArrayResize(pricesHighest,50);
   ArrayResize(pricesLowest,50);
//---
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
//---
   ArrayFree(pricesHighest);
   ArrayFree(pricesLowest);
   
   //ArrayFree(resistanceLevels); // cannot be used for static allocated array
   //ArrayFree(supportLevels); // cannot be used for static allocated array
   
   ArrayRemove(resistanceLevels,0,WHOLE_ARRAY);
   ArrayRemove(supportLevels,0,WHOLE_ARRAY);
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
//---
   
   int currBars = iBars(_Symbol,_Period);
   static int prevBars = currBars;
   if (prevBars == currBars) return;
   prevBars = currBars;
   
   int visible_bars = (int)ChartGetInteger(0,CHART_VISIBLE_BARS);
   bool stop_processing = false; // Flag to control outer loop
   bool matchFound_high1 = false, matchFound_low1 = false;
   bool matchFound_high2 = false, matchFound_low2 = false;
   
   ArrayFree(pricesHighest);
   ArrayFree(pricesLowest);
   
   int copiedBarsHighs = CopyHigh(_Symbol,_Period,1,visible_bars,pricesHighest);
   int copiedBarsLows = CopyLow(_Symbol,_Period,1,visible_bars,pricesLowest);
   
   //ArrayPrint(pricesHighest);
   //ArrayPrint(pricesLowest);
         // sort the array in ascending order
   ArraySort(pricesHighest);
   ArraySort(pricesLowest);
   //ArrayPrint(pricesHighest);
   //ArrayPrint(pricesLowest);
   ArrayRemove(pricesHighest,10,WHOLE_ARRAY);
   ArrayRemove(pricesLowest,0,visible_bars-10);
   //Print("FIRST 10 HIGHEST PRICES:");
   //ArrayPrint(pricesHighest);
   //Print("LAST 10 LOWEST PRICES:");
   //ArrayPrint(pricesLowest);
   
   for (int i=1; i<=visible_bars-1 && !stop_processing; i++){
      //Print(":: BAR NO: ",i);
      double open = iOpen(_Symbol,_Period,i);
      double high = iHigh(_Symbol,_Period,i);
      double low = iLow(_Symbol,_Period,i);
      double close = iClose(_Symbol,_Period,i);
      datetime time = iTime(_Symbol,_Period,i);
      
      int diff_i_j = 10;
      
      for (int j=i+diff_i_j; j<=visible_bars-1; j++){
         //Print("BAR CHECK NO: ",j);
         double open_j = iOpen(_Symbol,_Period,j);
         double high_j = iHigh(_Symbol,_Period,j);
         double low_j = iLow(_Symbol,_Period,j);
         double close_j = iClose(_Symbol,_Period,j);
         datetime time_j = iTime(_Symbol,_Period,j);
         
         // CHECK FOR RESISTANCE
         double high_diff = NormalizeDouble((MathAbs(high-high_j)/_Point),0);
         bool is_resistance = high_diff <= 10;
         
         // CHECK FOR SUPPORT
         double low_diff = NormalizeDouble((MathAbs(low-low_j)/_Point),0);
         bool is_support = low_diff <= 10;
         
         if (is_resistance){
            //Print("RESISTANCE AT BAR ",i," (",high,") & ",j," (",high_j,"), Pts = ",high_diff);
            
            for (int k=0; k<ArraySize(pricesHighest); k++){
               if (pricesHighest[k]==high){
                  matchFound_high1 = true;
                  //Print("> RES H1(",high,") FOUND @ ",k," (",pricesHighest[k],")");
               }
               if (pricesHighest[k]==high_j){
                  matchFound_high2 = true;
                  //Print("> RES H2(",high_j,") FOUND @ ",k," (",pricesHighest[k],")");
               }
               if (matchFound_high1 && matchFound_high2){
                  if (resistanceLevels[0]==high || resistanceLevels[1]==high_j){
                     Print("CONFIRMED BUT This is the same resistance level, skip updating!");
                     stop_processing = true; // Set the flag to stop processing
                     break; // stop the inner loop prematurily
                  }
                  else {
                     Print(" ++++++++++ RESISTANCE LEVELS CONFIRMED @ BARS ",i,
                     "(",high,") & ",j,"(",high_j,")");
                     resistanceLevels[0] = high;
                     resistanceLevels[1] = high_j;
                     ArrayPrint(resistanceLevels);
                     
                     draw_S_R_Level(resLine,high,colorRes,5);
                     draw_S_R_Level_Point(resline_prefix,high,time,218,-1,colorRes,90);
                     draw_S_R_Level_Point(resline_prefix,high,time_j,218,-1,colorRes,90);

                     stop_processing = true; // Set the flag to stop processing
                     break;
                  }
               }
            }
         }
         
         else if (is_support){
            //Print("SUPPORT AT BAR ",i," (",low,") & ",j," (",low_j,"), Pts = ",low_diff);
            
            for (int k=0; k<ArraySize(pricesLowest); k++){
               if (pricesLowest[k]==low){
                  matchFound_low1 = true;
                  //Print("> SUP L1(",low,") FOUND @ ",k," (",pricesLowest[k],")");
               }
               if (pricesLowest[k]==low_j){
                  matchFound_low2 = true;
                  //Print("> SUP L2(",low_j,") FOUND @ ",k," (",pricesLowest[k],")");
               }
               if (matchFound_low1 && matchFound_low2){
                  if (supportLevels[0]==low || supportLevels[1]==low_j){
                     Print("CONFIRMED BUT This is the same support level, skip updating!");
                     stop_processing = true; // Set the flag to stop processing
                     break; // stop the inner loop prematurely
                  }
                  else {
                     Print(" ++++++++++ SUPPORT LEVELS CONFIRMED @ BARS ",i,
                     "(",low,") & ",j,"(",low_j,")");
                     supportLevels[0] = low;
                     supportLevels[1] = low_j;
                     ArrayPrint(supportLevels);
                     
                     draw_S_R_Level(supLine,low,colorSup,5);
                     draw_S_R_Level_Point(supline_prefix,low,time,217,1,colorSup,-90);
                     draw_S_R_Level_Point(supline_prefix,low,time_j,217,1,colorSup,-90);

                     stop_processing = true; // Set the flag to stop processing
                     break;
                  }
               }
            }
         }
         
         
         
         if (stop_processing){break;}
      }
      if (stop_processing){break;}
   }
   
   if (ObjectFind(0,resLine) >= 0){
      double objPrice = ObjectGetDouble(0,resLine,OBJPROP_PRICE);
      double visibleHighs[];
      ArraySetAsSeries(visibleHighs,true);
      CopyHigh(_Symbol,_Period,1,visible_bars,visibleHighs);
      //Print("Object Found & visible bars is: ",ArraySize(visibleHighs));
      //ArrayPrint(visibleHighs);
      bool matchHighFound = false;
      
      for (int i=0; i<ArraySize(visibleHighs); i++){
         if (visibleHighs[i] == objPrice){
            Print("> Match price for resistance found at bar # ",i+1," (",objPrice,")");
            matchHighFound = true;
            break;
         }
      }
      if (!matchHighFound){
         Print("(",objPrice,") > Match price for the resistance line not found. Delete!");
         deleteLevel(resLine);
      }
   }
   
   if (ObjectFind(0,supLine) >= 0){
      double objPrice = ObjectGetDouble(0,supLine,OBJPROP_PRICE);
      double visibleLows[];
      ArraySetAsSeries(visibleLows,true);
      CopyLow(_Symbol,_Period,1,visible_bars,visibleLows);
      //Print("Object Found & visible bars is: ",ArraySize(visibleLows));
      //ArrayPrint(visibleLows);
      bool matchLowFound = false;
      
      for (int i=0; i<ArraySize(visibleLows); i++){
         if (visibleLows[i] == objPrice){
            Print("> Match price for support found at bar # ",i+1," (",objPrice,")");
            matchLowFound = true;
            break;
         }
      }
      if (!matchLowFound){
         Print("(",objPrice,") > Match price for the support line not found. Delete!");
         deleteLevel(supLine);
      }
   }
   
   static double ResistancePriceTrade = 0;
   if (ObjectFind(0,resLine) >= 0){
      double ResistancePriceLevel = ObjectGetDouble(0,resLine,OBJPROP_PRICE);
      if (ResistancePriceTrade != ResistancePriceLevel){
         double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
         double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
         double open1 = iOpen(_Symbol,_Period,1);
         double high1 = iHigh(_Symbol,_Period,1);
         double low1 = iLow(_Symbol,_Period,1);
         double close1 = iClose(_Symbol,_Period,1);

         if (open1 > close1 && open1 < ResistancePriceLevel
            && high1 > ResistancePriceLevel && Bid < ResistancePriceLevel){
            Print("$$$$$$$$$$$$ SELL NOW SIGNAL!");
            obj_Trade.Sell(0.01,_Symbol,Bid,Bid+350*5*_Point,Bid-350*_Point);
            ResistancePriceTrade = ResistancePriceLevel;
         }
         
      }
   }
   
   static double SupportPriceTrade = 0;
   if (ObjectFind(0,supLine) >= 0){
      double SupportPriceLevel = ObjectGetDouble(0,supLine,OBJPROP_PRICE);
      if (SupportPriceTrade != SupportPriceLevel){
         double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
         double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
         double open1 = iOpen(_Symbol,_Period,1);
         double high1 = iHigh(_Symbol,_Period,1);
         double low1 = iLow(_Symbol,_Period,1);
         double close1 = iClose(_Symbol,_Period,1);

         if (open1 < close1 && open1 > SupportPriceLevel
            && low1 < SupportPriceLevel && Ask > SupportPriceLevel){
            Print("$$$$$$$$$$$$ BUY NOW SIGNAL!");
            obj_Trade.Buy(0.01,_Symbol,Ask,Ask-350*5*_Point,Ask+350*_Point);
            SupportPriceTrade = SupportPriceLevel;
         }
         
      }
   }
   
}
//+------------------------------------------------------------------+

void draw_S_R_Level(string levelName,double price,color clr,int width){
   if (ObjectFind(0,levelName) < 0){
      ObjectCreate(0,levelName,OBJ_HLINE,0,TimeCurrent(),price);
      ObjectSetInteger(0,levelName,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,levelName,OBJPROP_WIDTH,width);
   }
   else {
      ObjectSetDouble(0,levelName,OBJPROP_PRICE,price);
   }
   ChartRedraw(0);
}

void deleteLevel(string levelName){
   ObjectDelete(0,levelName);
   ChartRedraw(0);
}

void draw_S_R_Level_Point(string objName,double price,datetime time,
      int arrowcode,int direction,color clr,double angle){
   //objName = " ";
   StringConcatenate(objName,objName," @ \nTime: ",time,"\nPrice: ",DoubleToString(price,_Digits));
   if (ObjectCreate(0,objName,OBJ_ARROW,0,time,price)) {
      ObjectSetInteger(0,objName,OBJPROP_ARROWCODE,arrowcode);
      ObjectSetInteger(0,objName,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,objName,OBJPROP_FONTSIZE,10);
      if (direction > 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_TOP);
      if (direction < 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_BOTTOM);
   }
   string prefix = resline_prefix;
   string txt = "\n"+prefix+"("+DoubleToString(price,_Digits)+")";
   string objNameDescription = objName + txt;
   if (ObjectCreate(0,objNameDescription,OBJ_TEXT,0,time,price)) {
     // ObjectSetString(0,objNameDescription,OBJPROP_TEXT, "" + txt);
      ObjectSetInteger(0,objNameDescription,OBJPROP_COLOR,clr);
      ObjectSetDouble(0,objNameDescription,OBJPROP_ANGLE, angle);
      ObjectSetInteger(0,objNameDescription,OBJPROP_FONTSIZE,10);
      if (direction > 0) {
         ObjectSetInteger(0,objNameDescription,OBJPROP_ANCHOR,ANCHOR_LEFT);
         ObjectSetString(0,objNameDescription,OBJPROP_TEXT, "    " + txt);
      }
      if (direction < 0) {
         ObjectSetInteger(0,objNameDescription,OBJPROP_ANCHOR,ANCHOR_BOTTOM);
         ObjectSetString(0,objNameDescription,OBJPROP_TEXT, "    " + txt);
      }
   }
   ChartRedraw(0);
}

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


Результаты тестера стратегий

Вот результаты тестирования в тестере стратегий.

  • График баланса/эквити:

График

  • Результаты тестирования на истории:

Результаты


Заключение

Как мы уже убедились, автоматизация стратегии торговли на Форекс с использованием уровней поддержки и сопротивления возможна и проста. Мы уверенно использовали мощные возможности языка MQL5 для создания точной и эффективной торговой стратегии. Анализ и создание советника показали, что автоматизация не только экономит драгоценное время, но и повышает эффективность торговли за счет снижения человеческого фактора.

Внимание: Статья предназначена только для обучения. Она демонстрирует, как создать советник на основе уровней поддержки и сопротивления. Представленный здесь советник следует использовать в качестве заготовки при разработке более совершенного решения с учетом большей оптимизации и извлечения данных. Представленная информация не гарантирует никаких торговых результатов.

Я искренне надеюсь, что статья была полезной для вас. Интеграция автоматизированных систем, несомненно, будет встречаться все чаще по мере дальнейшего развития финансовых рынков, предоставляя трейдерам самые современные инструменты для управления всеми аспектами рыночной динамики. Поскольку такие технологии, как MQL5, продолжают развиваться и открывают двери для более сложных и интеллектуальных торговых решений, будущее трейдинга выглядит светлым.


Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15107

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
davesarge1
davesarge1 | 11 апр. 2025 в 13:37
Эта стратегия выглядит очень интересно. Что касается кода, каковы критерии входа? И как рассчитываются уровни стоп-лосс и тейк-профит? Спасибо.
Нейросети в трейдинге: Ансамбль агентов с использованием механизмов внимания (MASAAT) Нейросети в трейдинге: Ансамбль агентов с использованием механизмов внимания (MASAAT)
Предлагаем познакомиться с мультиагентной адаптивной структурой оптимизации финансового портфеля (MASAAT), которая объединяет механизмы внимания и анализ временных рядов. MASAAT формирует множество агентов, которые анализируют ценовые ряды и направленные изменения, позволяя выявлять значимые колебания цен активов на различных уровнях детализации.
Нейросети в трейдинге: Мультиагентная адаптивная модель (Окончание) Нейросети в трейдинге: Мультиагентная адаптивная модель (Окончание)
В предыдущей статье мы познакомились с мультиагентным адаптивным фреймворком MASA, который объединяет подходы обучения с подкреплением и адаптивные стратегии, обеспечивая гармоничный баланс между доходностью и рисками в турбулентных рыночных условиях. Нами был построен функционал отдельных агентов данного фреймворка, и в этой статье мы продолжим начатую работу, доведя её до логического завершения.
Алгоритмическая торговля на основе 3D-паттернов разворота Алгоритмическая торговля на основе 3D-паттернов разворота
Открываем новый мир автоматической торговли на 3D-барах. Как выглядит торговый робот на многомерных барах цены, и могут ли "желтые" кластеры 3D-баров предсказывать развороты трендов? Как выглядит трейдинг в множестве измерений?
Популяционный ADAM (Adaptive Moment Estimation) Популяционный ADAM (Adaptive Moment Estimation)
В статье представлено превращение известного и популярного градиентного метода оптимизации ADAM в популяционный алгоритм и его модификация с введением гибридных особей. Новый подход позволяет создавать агентов, комбинирующих элементы успешных решений с использованием вероятностного распределения. Ключевое нововведение — формирование гибридных популяционных особей, которые адаптивно аккумулируют информацию от наиболее перспективных решений, повышая эффективность поиска в сложных многомерных пространствах.
OSZAR »