English 中文 Español Deutsch 日本語 Português
preview
Пошаговая инструкция для торговли по стратегии Break of Structure (BoS)

Пошаговая инструкция для торговли по стратегии Break of Structure (BoS)

MetaTrader 5Трейдинг |
3 044 9
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В этой статье мы обсудим понятие Break of Structure (BoS, прорыв структуры), которое указывает на смену рыночного тренда, изменение стратегии торговли на рынке Форекс в рамках концепции Smart Money (SMC) и создание советника (EA) на ее основе.

Мы рассмотрим определение стратегии Break of Structure, типы, применение и разработку в MetaQuotes Language 5 (MQL5) для MetaTrader 5. Break of Structure является полезным инструментом, который упрощает прогнозирование движения рынка, позволяет принимать более обоснованные решения и управлять рисками. Статья содержит следующие разделы:

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

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


Определение Break of Structure (BoS)

Break of Structure (BoS) — ключевая концепция технического анализа, которая использует концепцию Smart Money (SMC) для выявления существенных изменений в рыночных трендах. Обычно BoS происходит, когда цена решительно преодолевает минимумы или максимумы колебаний, установленные предыдущим ценовым движением. Когда цены поднимаются выше максимумов колебаний (swing highs) или опускаются ниже минимумов колебаний (swing lows), они прорывают ранее сформированную рыночную структуру, отсюда и их название. Обычно это указывает на изменение рыночных настроений и направления тренда, сигнализируя о продолжении существующего тренда или начале нового.


Описание Break of Structure (BoS)

Чтобы эффективно описать BoS, давайте сначала выделим его из других элементов концепции Smart Money, а именно: Market Structure Shift (MSS) и Change of Character (CHoCH).

  • Market Structure Shift (MSS, изменение структуры рынка)

Market Structure Shift, также известное как Market Momentum Shift (MMS, изменение импульса рынка), происходит из-за прорыва цены на самый последний максимум относительно нисходящего тренда или, наоборот, на самый последний максимум относительно восходящего тренда без предварительного прорыва самого последнего минимума или максимума колебания соответственно. Такое поведение означает разворот тренда из-за изменения структуры.

Market Structure Shift

  • Change of Character (CHoCH, изменение характера)

В свою очередь, Change of Character происходит из-за ценового прорыва самого последнего максимума в нисходящем тренде после первого пробития самого последнего минимума колебания или из-за ценового прорыва самого последнего минимума в восходящем тренде после первого пробития самого последнего максимума колебания.

Change of Character

  • Break of Structure (BoS, прорыв структуры)

Согласно приведенному выше определению, прорыв структуры означает преодоление старых максимумов или минимумов для достижения новых максимумов или минимумов соответственно. Каждый случай прорыва структуры способствует восходящему рыночному тренду, образуя новый более высокий максимум (Higher High, HH) и новый более высокий минимум (Higher Low, HL), или нисходящему тренду, образуя новый более низкий максимум (Lower High, LH) и новый более низкий минимум (Lower Low, LL), которые обычно описываются как точки максимума и минимума колебания цены.

Break of Structure

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

  • Неверное определение BoS:

Неверный BoS

  • Верное определение BoS:

Верный BoS


Типы Break of Structure

Как уже было сказано, BoS происходит на трендовых рынках, то есть либо при восходящих, либо при нисходящих трендах. Таким образом у нас есть только два типа BoS.

  • Бычий Break of Structure

Происходят при восходящих трендах, которые характеризуются более высокими максимумами (HH) и более высокими минимумами (HL). BoS происходит, когда цена пробивает недавний более высокий максимум восходящего тренда и формирует новый более высокий максимум.

Бычий BoS

  • Медвежий Break of Structure

Медвежий BoS происходит при нисходящих трендах, состоящих из более низких минимумов (LL) и более низких максимумов (LH). BoS происходит, когда цена пробивает недавний более низкий минимум в нисходящем тренде и формирует новый более низкий минимум.

Медвежий BoS


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

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

Используйте более крупные таймфреймы (higher time frames, HTF): для комплексного анализа изучите более крупные таймфреймы для выбранного актива, поскольку они дают общий обзор рыночных трендов. Это может быть четырехчасовой или дневной таймфрейм. Мелкие таймфреймы содержат много шума из-за поскольку он содержит много точек колебаний из-за манипуляций, перепадов ликвидности и зигзагов манипуляций, перепадов ликвидности и зигзагов.

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

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

Пример восходящего тренда:

Пример восходящего тренда

Пример нисходящего тренда:

Пример нисходящего тренда

На коротком таймфрейме, например, пятиминутном, вы можете использовать дополнительные подтверждения, такие как спрос и предложение, технические индикаторы, включая индекс относительной силы (RSI) и схождение-расхождение скользящих средних (MACD), а также модели японских свечей, в частности поглощение или внутренний бар.

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

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

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

Бычий Break of Structure:

Схема бычьего BoS

Медвежий Break of Structure:

Схема медвежьего BoS


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

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

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

Открываем MetaQuotes Language Editor

Теперь нажмите "Создать", выберите "Советник (шаблон)" и кликните "Далее".

Создание нового файла советника

Даем имя файлу

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

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

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

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

void OnTick(){

}

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

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

   static bool isNewBar = false;
   int currBars = iBars(_Symbol,_Period);
   static int prevBars = currBars;
   if (prevBars == currBars){isNewBar = false;}
   else if (prevBars != currBars){isNewBar = true; prevBars = currBars;}

Во-первых, объявим логическую переменную static под названием isNewBar и инициализируем ее значением false. Цель переменной — отслеживать, сформировался ли новый бар на графике. Мы объявляем локальную переменную с ключевым словом static, чтобы она могла сохранять свое значение на протяжении всего времени жизни функции. Это означает, что она не будет динамической. Обычно наша переменная всегда будет равна false, если только мы позже не изменим ее на true, а при изменении она сохранит свое значение и не будет обновляться на следующем тике, в отличие от динамической переменной, где она всегда будет обновляться до значения инициализации.

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

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

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

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

   const int length = 20;
   const int limit = 20;

Здесь мы объявляем две целочисленные переменные - length и limit. Length представляет собой диапазон баров, которые следует учитывать при определении максимумов и минимумов колебаний, тогда как limit представляет собой индекс текущего бара, который сканируется в данный конкретный момент. Предположим, что мы выбрали бар с индексом 10 для сканирования, чтобы определить, является ли он максимумом колебания. Затем мы проходим по всем соседним барам справа и слева и ищем, есть ли другой бар, который выше текущего, имеющего индекс 10. Таким образом, бар слева предшествует текущему, и, таким образом, находится по индексу (limit, равному 10, + 1) 11. То же самое происходит и при движении направо.

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

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

   int right_index, left_index;
   bool isSwingHigh = true, isSwingLow = true;
   static double swing_H = -1.0, swing_L = -1.0;
   int curr_bar = limit;

Сначала мы объявляем две целочисленные переменные right_index и left_index для отслеживания индексов соседних баров. Правый индекс представляет собой индекс бара справа от текущего, а левый - индекс бара слева от текущего (выбранного для анализа). Опять же, мы объявляем две логические переменные isSwingHigh и isSwingLow, которые служат флагами для определения того, является ли текущий бар потенциальным максимумом или минимумом колебания соответственно, и инициализируем их значением true. После анализа, если хотя бы один из флагов останется равным true, это будет указывать на наличие точки колебания. Более того, мы объявляем статические двойные переменные swing_H и swing_L, которые будут хранить уровни цен максимумов и минимумов колебаний соответственно. Мы инициализируем их значениями -1, чтобы просто указать, что пока не обнаружено ни одного максимума или минимума колебания. Они сделаны статическими, чтобы гарантировать, что после получения точек колебания они останутся неизменными, и мы сможем сохранить их для дальнейшего использования, чтобы впоследствии определить, были ли они нарушены из-за изменения структуры. Мы изменим их на -1, если произойдет прорыв структуры, или они будут заменены новыми сгенерированными точками колебания. Наконец, у нас есть переменная curr_bar, которая определяет начальную точку анализа.

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

   if (isNewBar){ ... }

Затем создадим экземпляр цикла for, чтобы найти максимумы и минимумы колебаний.

      for (int j=1; j<=length; j++){
         right_index = curr_bar - j;
         left_index = curr_bar + j;
         if ( (high(curr_bar) <= high(right_index)) || (high(curr_bar) < high(left_index)) ){
            isSwingHigh = false;
         }
         if ( (low(curr_bar) >= low(right_index)) || (low(curr_bar) > low(left_index)) ){
            isSwingLow = false;
         }
      }

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

Индекс баров

Операторы print были получены с помощью следующей встроенной функции:

         Print("Current Bar Index = ",curr_bar," ::: Right index: ",right_index,", Left index: ",left_index);

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

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

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

      if (isSwingHigh){
         swing_H = high(curr_bar);
         Print("UP @ BAR INDEX ",curr_bar," of High: ",high(curr_bar));
         drawSwingPoint(TimeToString(time(curr_bar)),time(curr_bar),high(curr_bar),77,clrBlue,-1);
      }
      if (isSwingLow){
         swing_L = low(curr_bar);
         Print("DOWN @ BAR INDEX ",curr_bar," of Low: ",low(curr_bar));
         drawSwingPoint(TimeToString(time(curr_bar)),time(curr_bar),low(curr_bar),77,clrRed,1);
      }

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

double high(int index){return (iHigh(_Symbol,_Period,index));}
double low(int index){return (iLow(_Symbol,_Period,index));}
double close(int index){return (iClose(_Symbol,_Period,index));}
datetime time(int index){return (iTime(_Symbol,_Period,index));}

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

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

void drawSwingPoint(string objName,datetime time,double price,int arrCode,
   color clr,int direction){
   
   if (ObjectFind(0,objName) < 0){
      ObjectCreate(0,objName,OBJ_ARROW,0,time,price);
      ObjectSetInteger(0,objName,OBJPROP_ARROWCODE,arrCode);
      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 txt = " BoS";
      string objNameDescr = objName + txt;
      ObjectCreate(0,objNameDescr,OBJ_TEXT,0,time,price);
      ObjectSetInteger(0,objNameDescr,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,objNameDescr,OBJPROP_FONTSIZE,10);
      if (direction > 0) {
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
         ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt);
      }
      if (direction < 0) {
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER);
         ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt);
      }
   }
   ChartRedraw(0);
}

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

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

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

Коды стрелок

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

Точка колебания без описания

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

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

      string txt = " BoS";
      string objNameDescr = objName + txt;
      ObjectCreate(0,objNameDescr,OBJ_TEXT,0,time,price);
      ObjectSetInteger(0,objNameDescr,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,objNameDescr,OBJPROP_FONTSIZE,10);
      if (direction > 0) {
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
         ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt);
      }
      if (direction < 0) {
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER);
         ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt);
      }

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

Точка колебания с описанием

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

   if (isNewBar){
      for (int j=1; j<=length; j++){
         right_index = curr_bar - j;
         left_index = curr_bar + j;

         if ( (high(curr_bar) <= high(right_index)) || (high(curr_bar) < high(left_index)) ){
            isSwingHigh = false;
         }
         if ( (low(curr_bar) >= low(right_index)) || (low(curr_bar) > low(left_index)) ){
            isSwingLow = false;
         }
      }
      
      if (isSwingHigh){
         swing_H = high(curr_bar);
         Print("UP @ BAR INDEX ",curr_bar," of High: ",high(curr_bar));
         drawSwingPoint(TimeToString(time(curr_bar)),time(curr_bar),high(curr_bar),77,clrBlue,-1);
      }
      if (isSwingLow){
         swing_L = low(curr_bar);
         Print("DOWN @ BAR INDEX ",curr_bar," of Low: ",low(curr_bar));
         drawSwingPoint(TimeToString(time(curr_bar)),time(curr_bar),low(curr_bar),77,clrRed,1);
      }
   }

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

   double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
   double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);

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

Мы используем условное утверждение, чтобы определить, произошел ли рост цены и был ли преодолен уровень максимальной точки колебания. Во-первых, мы проверяем, существует ли точка максимума колебания, исходя из логики, что она больше нуля, просто потому, что мы не можем превзойти какой-то максимум колебания, которого у нас еще нет. Затем, если у нас уже есть точка максимума колебания, мы проверяем, находится ли цена Bid выше уровня максимума колебания, чтобы гарантировать, что позиция покупки открыта по цене Ask, а уровни торговли, то есть стоп-лосс и тейк-профит, связанные с ценой Bid, правильно отображены в точке выше уровня пробоя. Наконец, проверяем, находится ли цена закрытия предыдущего бара выше уровня максимума колебания, чтобы убедиться, что у нас есть действительный прорыв, отвечающий требованиям. Если все условия выполнены, мы имеем действительный BoS и отправляем уведомление в журнал.

   if (swing_H > 0 && Bid > swing_H && close(1) > swing_H){
      Print("BREAK UP NOW");
      ...
   }

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

Координаты точки

Две координаты, которые нам нужны, — это время, обозначенное как X и представленное на оси x, и цена, обозначенная как Y и представленная на оси y. Для получения вторых координат, то есть свечи, где происходит прорыв структуры, мы используем текущий индекс бара, который обычно равен 0. Однако получить индекс бара, содержащего точку максимума колебания, не так просто. Напомним, что мы сохранили только цену свечи максимума колебания. Мы могли бы также хранить индекс бара в то же время, что и цену, но это было бы совершенно бесполезно, поскольку впоследствии генерируются новые бары. Это не означает, что мы не можем найти индекс бара, содержащего точку колебания. Мы можем просмотреть максимальные цены предыдущих баров и найти тот, который соответствует нашей точке максимума колебания. Ниже показано, как это делается.

      int swing_H_index = 0;
      for (int i=0; i<=length*2+1000; i++){
         double high_sel = high(i);
         if (high_sel == swing_H){
            swing_H_index = i;
            Print("BREAK HIGH @ BAR ",swing_H_index);
            break;
         }
      }

Сначала мы объявляем целочисленную переменную swing_H_index, которая будет содержать индекс максимума нашего колебания и инициализируем его нулем. Затем используем цикл for для обхода удвоения всех предопределенных баров плюс дополнительный диапазон баров в 1000 (произвольное количество баров, по которым может быть найдена точка колебания, это может быть любое значение) и сравним максимум выбранного бара с сохраненной точкой максимума колебания. Таким образом, если мы находим совпадение, мы сохраняем индекс и преждевременно выходим из цикла, поскольку мы уже нашли индекс бара максимума колебания. 

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

void drawBreakLevel(string objName,datetime time1,double price1,
   datetime time2,double price2,color clr,int direction){
   if (ObjectFind(0,objName) < 0){
      ObjectCreate(0,objName,OBJ_ARROWED_LINE,0,time1,price1,time2,price2);
      ObjectSetInteger(0,objName,OBJPROP_TIME,0,time1);
      ObjectSetDouble(0,objName,OBJPROP_PRICE,0,price1);
      ObjectSetInteger(0,objName,OBJPROP_TIME,1,time2);
      ObjectSetDouble(0,objName,OBJPROP_PRICE,1,price2);
      ObjectSetInteger(0,objName,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,objName,OBJPROP_WIDTH,2);
      
      string txt = " Break   ";
      string objNameDescr = objName + txt;
      ObjectCreate(0,objNameDescr,OBJ_TEXT,0,time2,price2);
      ObjectSetInteger(0,objNameDescr,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,objNameDescr,OBJPROP_FONTSIZE,10);
      if (direction > 0) {
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
         ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt);
      }
      if (direction < 0) {
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_RIGHT_LOWER);
         ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt);
      }
   }
   ChartRedraw(0);
}

Вот отличия этой функции от предыдущей.

  1. Мы объявляем имя функции как drawBreakLevel.
  2. Создаваемый нами объект представляет собой линию со стрелкой OBJ_ARROWED_LINE.
  3. Наша линия со стрелкой содержит две координаты: время 1 и цена 1 для первой координаты, а также время 2 и цена 2 для второй.
  4. Текст объединения — Break, сигнализирующий о том, что произошел прорыв структуры (BoS).

Затем используем функцию, чтобы нарисовать на графике линию со стрелкой уровня прорыва. Для времени 2 второй координаты мы просто прибавляем 1, что для точности переносит нас на бар, предшествующий текущему. Затем мы сбрасываем значение переменной максимума колебания (swing high) до -1, чтобы обозначить, что мы уже прорвали структуру и данной настройки больше нет. Это помогает избежать поиска прорыва максимума колебания на предыдущих тиках, поскольку мы уже прорвали точку максимума колебания. Таким образом, мы просто ждем, пока не сформируется еще одна точка максимума колебания, и переменная снова заполнится, а цикл продолжится.

      drawBreakLevel(TimeToString(time(0)),time(swing_H_index),high(swing_H_index),
      time(0+1),high(swing_H_index),clrBlue,-1);
      
      swing_H = -1.0;

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

      //--- Open Buy
      obj_Trade.Buy(0.01,_Symbol,Ask,Bid-500*7*_Point,Bid+500*_Point,"BoS Break Up BUY");
      
      return;

Мы используем наш объект obj_Trade и оператор-точку, чтобы получить доступ ко всем методам, содержащимся в классе. В этом случае нам просто нужно совершить покупку и использовать метод Buy, указав объем, уровни сделки и комментарий к ней. Наконец, используем return, поскольку все настроено и нам больше не нужно выполнять код. Однако если у вас есть дополнительный код, избегайте использования оператора return, поскольку он завершает выполнение текущей функции и возвращает управление вызывающей программе. Ниже приведен полный код с поиском прорывов структуры, отображением стрелок и открытием позиций на покупку:

   if (swing_H > 0 && Bid > swing_H && close(1) > swing_H){
      Print("BREAK UP NOW");
      int swing_H_index = 0;
      for (int i=0; i<=length*2+1000; i++){
         double high_sel = high(i);
         if (high_sel == swing_H){
            swing_H_index = i;
            Print("BREAK HIGH @ BAR ",swing_H_index);
            break;
         }
      }
      drawBreakLevel(TimeToString(time(0)),time(swing_H_index),high(swing_H_index),
      time(0+1),high(swing_H_index),clrBlue,-1);
      
      swing_H = -1.0;
      
      //--- Open Buy
      obj_Trade.Buy(0.01,_Symbol,Ask,Bid-500*7*_Point,Bid+500*_Point,"BoS Break Up BUY");
      
      return;
   }

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

   else if (swing_L > 0 && Ask < swing_L && close(1) < swing_L){
      Print("BREAK DOWN NOW");
      int swing_L_index = 0;
      for (int i=0; i<=length*2+1000; i++){
         double low_sel = low(i);
         if (low_sel == swing_L){
            swing_L_index = i;
            Print("BREAK LOW @ BAR ",swing_L_index);
            break;
         }
      }
      drawBreakLevel(TimeToString(time(0)),time(swing_L_index),low(swing_L_index),
      time(0+1),low(swing_L_index),clrRed,1);

      swing_L = -1.0;
      
      //--- Open Sell
      obj_Trade.Sell(0.01,_Symbol,Bid,Ask+500*7*_Point,Ask-500*_Point,"BoS Break Down SELL");

      return;
   }

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

Результат

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

//+------------------------------------------------------------------+
//|                                                          BOS.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;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){return(INIT_SUCCEEDED);}
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
   
   static bool isNewBar = false;
   int currBars = iBars(_Symbol,_Period);
   static int prevBars = currBars;
   if (prevBars == currBars){isNewBar = false;}
   else if (prevBars != currBars){isNewBar = true; prevBars = currBars;}
   
   const int length = 5;
   const int limit = 5;

   int right_index, left_index;
   bool isSwingHigh = true, isSwingLow = true;
   static double swing_H = -1.0, swing_L = -1.0;
   int curr_bar = limit;
   
   if (isNewBar){
      for (int j=1; j<=length; j++){
         right_index = curr_bar - j;
         left_index = curr_bar + j;
         //Print("Current Bar Index = ",curr_bar," ::: Right index: ",right_index,", Left index: ",left_index);
         //Print("curr_bar(",curr_bar,") right_index = ",right_index,", left_index = ",left_index);
         // If high of the current bar curr_bar is <= high of the bar at right_index (to the left),
         //or if it’s < high of the bar at left_index (to the right), then isSwingHigh is set to false
         //This means that the current bar curr_bar does not have a higher high compared
         //to its neighbors, and therefore, it’s not a swing high
         if ( (high(curr_bar) <= high(right_index)) || (high(curr_bar) < high(left_index)) ){
            isSwingHigh = false;
         }
         if ( (low(curr_bar) >= low(right_index)) || (low(curr_bar) > low(left_index)) ){
            isSwingLow = false;
         }
      }
      //By the end of the loop, if isSwingHigh is still true, it suggests that 
      //current bar curr_bar has a higher high than the surrounding bars within
      //length range, marking a potential swing high.
      
      if (isSwingHigh){
         swing_H = high(curr_bar);
         Print("UP @ BAR INDEX ",curr_bar," of High: ",high(curr_bar));
         drawSwingPoint(TimeToString(time(curr_bar)),time(curr_bar),high(curr_bar),77,clrBlue,-1);
      }
      if (isSwingLow){
         swing_L = low(curr_bar);
         Print("DOWN @ BAR INDEX ",curr_bar," of Low: ",low(curr_bar));
         drawSwingPoint(TimeToString(time(curr_bar)),time(curr_bar),low(curr_bar),77,clrRed,1);
      }
   }
   
   double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
   double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);

   if (swing_H > 0 && Bid > swing_H && close(1) > swing_H){
      Print("BREAK UP NOW");
      int swing_H_index = 0;
      for (int i=0; i<=length*2+1000; i++){
         double high_sel = high(i);
         if (high_sel == swing_H){
            swing_H_index = i;
            Print("BREAK HIGH @ BAR ",swing_H_index);
            break;
         }
      }
      drawBreakLevel(TimeToString(time(0)),time(swing_H_index),high(swing_H_index),
      time(0+1),high(swing_H_index),clrBlue,-1);
      
      swing_H = -1.0;
      
      //--- Open Buy
      obj_Trade.Buy(0.01,_Symbol,Ask,Bid-500*7*_Point,Bid+500*_Point,"BoS Break Up BUY");
      
      return;
   }
   else if (swing_L > 0 && Ask < swing_L && close(1) < swing_L){
      Print("BREAK DOWN NOW");
      int swing_L_index = 0;
      for (int i=0; i<=length*2+1000; i++){
         double low_sel = low(i);
         if (low_sel == swing_L){
            swing_L_index = i;
            Print("BREAK LOW @ BAR ",swing_L_index);
            break;
         }
      }
      drawBreakLevel(TimeToString(time(0)),time(swing_L_index),low(swing_L_index),
      time(0+1),low(swing_L_index),clrRed,1);

      swing_L = -1.0;
      
      //--- Open Sell
      obj_Trade.Sell(0.01,_Symbol,Bid,Ask+500*7*_Point,Ask-500*_Point,"BoS Break Down SELL");

      return;
   }
   
}
//+------------------------------------------------------------------+

double high(int index){return (iHigh(_Symbol,_Period,index));}
double low(int index){return (iLow(_Symbol,_Period,index));}
double close(int index){return (iClose(_Symbol,_Period,index));}
datetime time(int index){return (iTime(_Symbol,_Period,index));}

void drawSwingPoint(string objName,datetime time,double price,int arrCode,
   color clr,int direction){
   
   if (ObjectFind(0,objName) < 0){
      ObjectCreate(0,objName,OBJ_ARROW,0,time,price);
      ObjectSetInteger(0,objName,OBJPROP_ARROWCODE,arrCode);
      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 txt = " BoS";
      string objNameDescr = objName + txt;
      ObjectCreate(0,objNameDescr,OBJ_TEXT,0,time,price);
      ObjectSetInteger(0,objNameDescr,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,objNameDescr,OBJPROP_FONTSIZE,10);
      if (direction > 0) {
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
         ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt);
      }
      if (direction < 0) {
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER);
         ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt);
      }
   }
   ChartRedraw(0);
}

void drawBreakLevel(string objName,datetime time1,double price1,
   datetime time2,double price2,color clr,int direction){
   if (ObjectFind(0,objName) < 0){
      ObjectCreate(0,objName,OBJ_ARROWED_LINE,0,time1,price1,time2,price2);
      ObjectSetInteger(0,objName,OBJPROP_TIME,0,time1);
      ObjectSetDouble(0,objName,OBJPROP_PRICE,0,price1);
      ObjectSetInteger(0,objName,OBJPROP_TIME,1,time2);
      ObjectSetDouble(0,objName,OBJPROP_PRICE,1,price2);
      ObjectSetInteger(0,objName,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,objName,OBJPROP_WIDTH,2);
      
      string txt = " Break   ";
      string objNameDescr = objName + txt;
      ObjectCreate(0,objNameDescr,OBJ_TEXT,0,time2,price2);
      ObjectSetInteger(0,objNameDescr,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,objNameDescr,OBJPROP_FONTSIZE,10);
      if (direction > 0) {
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
         ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt);
      }
      if (direction < 0) {
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_RIGHT_LOWER);
         ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt);
      }
   }
   ChartRedraw(0);
}

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


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

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

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

График

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

Результаты


Заключение

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

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

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

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


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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (9)
Allan Munene Mutiiria
Allan Munene Mutiiria | 8 окт. 2024 в 21:18
Dragosh Zavadschi #:

Спасибо за предоставленную кодовую базу, она на самом деле очень хороша.

С минимальными изменениями я адаптировал/изменил.

Первоначальный результат без добавления какого-либо фильтра действительно впечатляет.


@Dragosh Zavadschi спасибо за добрый отзыв и обзор. Это очень приятно. Спасибо.
Maxim Kuznetsov
Maxim Kuznetsov | 8 нояб. 2024 в 20:08

прочёл по диагонали.

в коде бросилось в глаза:

void function() {

const int localConst = 5; // 

        // some code follows

}

так нельзя. Это маскировка "магических" констант

Vitaly Murlenko
Vitaly Murlenko | 8 нояб. 2024 в 22:03

Автор, что-то я недопонял Вас. Ваш скриншот, который показывает восходящую тенденцию, имеет уровни HH и HL. Но скрин, который идёт далее порождает недоразумения. Смотрите:

Так как же верно выбрать пробиваемый уровень?

Maxim Kuznetsov
Maxim Kuznetsov | 8 нояб. 2024 в 22:43
Vitaly Murlenko #:

Автор, что-то я недопонял Вас. Ваш скриншот, который показывает восходящую тенденцию, имеет уровни HH и HL. Но скрин, который идёт далее порождает недоразумения. Смотрите:

Так как же верно выбрать пробиваемый уровень?

все подобные стратегии работают (и то не факт) только по днёвкам.

Внутри дня с его регулярными и резкими всплесками волатильности НЕ РАБОТАЮТ. 

Но зато по внутри-дня удобно подбирать скриншоты. 

Vitaly Murlenko
Vitaly Murlenko | 8 нояб. 2024 в 22:44
Maxim Kuznetsov #:

все подобные стратегии работают (и то не факт) только по днёвкам.

Внутри дня с его регулярными и резкими всплесками волатильности НЕ РАБОТАЮТ. 

Но зато по внутри-дня удобно подбирать скриншоты. 

Вопрос был не про это

Нейросети в трейдинге: Гиперболическая модель латентной диффузии (Окончание) Нейросети в трейдинге: Гиперболическая модель латентной диффузии (Окончание)
Применение анизотропных диффузионных процессов для кодирования исходных данных в гиперболическом латентном пространстве, как это предложено в фреймворке HypDIff, способствует сохранению топологических особенностей текущей рыночной ситуации, и повышает качество её анализа. В предыдущей статье мы начали реализацию предложенных подходов средствами MQL5. И сегодня продолжим начатую работу, доведя ее до логического завершения.
Интеграция скрытых марковских моделей в MetaTrader 5 Интеграция скрытых марковских моделей в MetaTrader 5
В этой статье мы продемонстрируем, как скрытые марковские модели, обученные с использованием Python, могут быть интегрированы в приложения MetaTrader 5. Скрытые марковские модели — это мощный статистический инструмент, используемый для моделирования временных рядов данных, где моделируемая система характеризуется ненаблюдаемыми (скрытыми) состояниями. Фундаментальная предпосылка HMM заключается в том, что вероятность нахождения в заданном состоянии в определенный момент времени зависит от состояния процесса в предыдущем временном интервале.
Построение модели для ограничения диапазона сигналов по тренду (Часть 4): Настройка стиля отображения для каждой трендовой волны Построение модели для ограничения диапазона сигналов по тренду (Часть 4): Настройка стиля отображения для каждой трендовой волны
В статье показаны возможности мощного языка MQL5 для отрисовки различных стилей индикаторов в MetaTrader 5. Мы также рассмотрим скрипты и их использование в нашей модели.
Обучение многослойного персептрона с помощью алгоритма Левенберга-Марквардта Обучение многослойного персептрона с помощью алгоритма Левенберга-Марквардта
В статье представлена реализация алгоритма Левенберга-Марквардта для обучения нейронных сетей прямого распространения. Проведен сравнительный анализ результативности с алгоритмами из библиотеки scikit-learn Python. Предварительно обсуждаются более простые методы обучения такие как градиентный спуск, градиентный спуск с импульсом и стохастический градиентный спуск.