English Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 6): Поиск ордер-блоков для торговли по концепции Smart Money

Автоматизация торговых стратегий на MQL5 (Часть 6): Поиск ордер-блоков для торговли по концепции Smart Money

MetaTrader 5Трейдинг |
495 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В предыдущей статье (Часть 5 серии) мы разработали Стратегию торгового набора «Кроссоверы адаптивных скользящих средних RSI» (Adaptive Crossover RSI Trading Suite), сочетающую пересечения скользящих средних с фильтрацией RSI для выявления высоковероятных торговых возможностей. Теперь, в Части 6, мы сосредоточимся на чистом анализе движения цены с помощью автоматизированной Системы обнаружения блоков ордеров на языке MetaQuotes Language 5 (MQL5), мощном инструменте, используемом в торговле на основе интеллектуальных денег. Эта стратегия определяет ключевые институциональные блоки ордеров — зоны, где крупные игроки накапливают или распределяют позиции, — помогая трейдерам предвидеть потенциальные развороты и продолжения трендов.

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

  1. План стратегии
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

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


План стратегии

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

ORDER BLOCK SAMPLE

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


Реализация средствами MQL5

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

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

// Struct to hold both the price and the index of the high or low
struct PriceIndex {
    double price;
    int index;
};

// Global variables to track the range and breakout state
PriceIndex highestHigh = {0, 0}; // Stores the highest high of the range
PriceIndex lowestLow = {0, 0};  // Stores the lowest low of the range
bool breakoutDetected = false;  // Tracks if a breakout has occurred

double impulseLow = 0.0;
double impulseHigh = 0.0;
int breakoutBarIndex = -1; // To track the bar at which breakout occurred
datetime breakoutTime = 0; // To store the breakout time

string totalOBs_names[];
datetime totalOBs_dates[];
bool totalOBs_is_signals[];

#define OB_Prefix "OB REC "
#define CLR_UP clrLime
#define CLR_DOWN clrRed

bool is_OB_UP = false;
bool is_OB_DOWN = false;

Мы начинаем с включения библиотеки "Trade.mqh" и создания объекта "CTrade", "obj_Trade", для управления исполнением сделок. Мы определяем структуру "PriceIndex" для хранения как уровня цен, так и соответствующего ему индекса, который помогает нам отслеживать самый высокий максимум и самый низкий минимум в диапазоне консолидации. В глобальных переменных "highestHigh" и "lowestLow" хранятся эти ключевые уровни, в то время как флаг "breakoutDetected" указывает, произошел ли пробой.

Для подтверждения импульсивного движения мы вводим "impulseLow" и "impulseHigh", которые помогут определить силу пробоя. Переменная "breakoutBarIndex" отслеживает точный бар, на котором произошел пробой, а "breakoutTime" сохраняет соответствующую временную метку. Для управления блоками ордеров мы используем три глобальных массива: "totalOBs_names", "totalOBs_dates" и "totalOBs_is_signals". В этих массивах хранятся названия блоков ордеров, соответствующие им временные метки и информация о том, являются ли они действительными торговыми сигналами.

Мы определяем префикс блока ордеров как "OB_Prefix" и назначаем цветовые коды для блоков бычьих и медвежьих ордеров, используя "CLR_UP" для бычьего (лайм) и "CLR_DOWN" для медвежьего (красный). Наконец, логические флаги "is_OB_UP" и "is_OB_DOWN" помогают нам отслеживать, является ли последний обнаруженный блок ордеров бычьим или медвежьим. Нам не нужно отслеживать блоки ордеров при инициализации программы, так как мы хотим начать все с чистого листа. Таким образом, мы будем реализовывать логику управления непосредственно в обработчике событий OnTick.

//+------------------------------------------------------------------+
//| Expert ontick function                                           |
//+------------------------------------------------------------------+
void OnTick() {
    static bool isNewBar = false;
    int currBars = iBars(_Symbol, _Period);
    static int prevBars = currBars;

    // Detect a new bar
    if (prevBars == currBars) {
        isNewBar = false;
    } else if (prevBars != currBars) {
        isNewBar = true;
        prevBars = currBars;
    }

    if (!isNewBar)
        return; // Process only on a new bar

    int rangeCandles = 7;         // Initial number of candles to check
    double maxDeviation = 50;     // Max deviation between highs and lows in points
    int startingIndex = 1;        // Starting index for the scan
    int waitBars = 3;

   //---

}

В обработчике событий OnTick мы начинаем с обнаружения формирования нового бара, используя "currBars" и "prevBars". Мы устанавливаем для параметра "isNewBar" значение "true" при появлении нового бара и возвращаем его раньше, если новый бар не обнаружен. Затем мы определяем "rangeCandles" как "7", что представляет собой минимальное количество свечей, которые мы анализируем для выявления консолидации. Переменная "maxDeviation" установлена на "50" пунктов, что ограничивает допустимую разницу между самой высокой и самой низкой ценами в пределах диапазона. "startingIndex" инициализируется значением "1", гарантируя, что мы начнем сканирование с самого последнего заполненного бара. Кроме того, мы установили для параметра "waitBars" значение "3", чтобы определить, сколько баров должно пройти перед проверкой блока ордеров. Далее нам нужно проверить диапазоны консолидации и получить цены для дальнейшего определения допустимых блоков ордеров.

// Check for consolidation or extend the range
if (!breakoutDetected) {
  if (highestHigh.price == 0 && lowestLow.price == 0) {
      // If range is not yet established, look for consolidation
      if (IsConsolidationEqualHighsAndLows(rangeCandles, maxDeviation, startingIndex)) {
          GetHighestHigh(rangeCandles, startingIndex, highestHigh);
          GetLowestLow(rangeCandles, startingIndex, lowestLow);

          Print("Consolidation range established: Highest High = ", highestHigh.price, 
                " at index ", highestHigh.index, 
                " and Lowest Low = ", lowestLow.price, 
                " at index ", lowestLow.index);
          
      }
  } else {
      // Extend the range if the current bar's prices remain within the range
      ExtendRangeIfWithinLimits();
  }
}

На каждом формирующемся новом баре мы проверяем наличие консолидации или расширяем существующий диапазон, если не обнаружено пробоя. Если значения "highestHigh.price" и "lowestLow.price" равны нулю, это означает, что диапазон консолидации еще не установлен. Затем вызываем функцию "IsConsolidationEqualHighsAndLows", чтобы проверить, образуют ли последние "rangeCandles" консолидацию в пределах допустимого "maxDeviation". В случае подтверждения мы используем функции "GetHighestHigh" и "GetLowestLow", чтобы точно определить самую высокую и самую низкую цены в пределах диапазона, сохраняя их значения вместе с соответствующими индексами баров.

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

// Function to detect consolidation where both highs and lows are nearly equal
bool IsConsolidationEqualHighsAndLows(int rangeCandles, double maxDeviation, int startingIndex) {
    // Loop through the last `rangeCandles` to check if highs and lows are nearly equal
    for (int i = startingIndex; i < startingIndex + rangeCandles - 1; i++) {
        // Compare the high of the current candle with the next one
        if (MathAbs(high(i) - high(i + 1)) > maxDeviation * Point()) {
            return false; // If the high difference is greater than allowed, it's not a consolidation
        }
        
        // Compare the low of the current candle with the next one
        if (MathAbs(low(i) - low(i + 1)) > maxDeviation * Point()) {
            return false; // If the low difference is greater than allowed, it's not a consolidation
        }
    }

    // If both highs and lows are nearly equal, it's a consolidation range
    return true;
}

Мы определяем логическую функцию "IsConsolidationEqualHighsAndLows", которая отвечает за обнаружение консолидации путем проверки того, являются ли максимумы и минимумы последних "rangeCandles" почти одинаковыми в пределах указанного "maxDeviation". Мы достигаем этого, выполняя перебор каждого бара, начиная с "startingIndex", и сравнивая максимумы и минимумы последовательных свечей.

Внутри цикла "for" мы используем функцию MathAbs для вычисления абсолютной разницы между максимумом текущего бара ("high(i)") и следующим максимумом. Если эта разница превышает максимальное отклонение, преобразованное в точечную форму, Point, функция немедленно возвращает значение «false», указывая, что максимумы недостаточно равны, чтобы считаться консолидацией. Аналогично, мы снова применяем функцию MathAbs для сравнения минимумов последовательных баров ("low(i)" и "low(i + 1)"), чтобы убедиться, что минимумы также находятся в пределах допустимого отклонения. Если какая-либо проверка завершается неудачей, функция завершает работу досрочно с значением «false». Если все максимумы и минимумы остаются в пределах допустимого отклонения, мы возвращаем значение «true», подтверждая допустимый диапазон консолидации. Следующие определяемые функции это функции, отвечающие за получение самых высоких и самых низких цен бара.

// Function to get the highest high and its index in the last `rangeCandles` candles, starting from `startingIndex`
void GetHighestHigh(int rangeCandles, int startingIndex, PriceIndex &highestHighRef) {
    highestHighRef.price = high(startingIndex); // Start by assuming the first candle's high is the highest
    highestHighRef.index = startingIndex;       // The index of the highest high (starting with the `startingIndex`)

    // Loop through the candles and find the highest high and its index
    for (int i = startingIndex + 1; i < startingIndex + rangeCandles; i++) {
        if (high(i) > highestHighRef.price) {
            highestHighRef.price = high(i); // Update highest high
            highestHighRef.index = i;       // Update index of highest high
        }
    }
}

// Function to get the lowest low and its index in the last `rangeCandles` candles, starting from `startingIndex`
void GetLowestLow(int rangeCandles, int startingIndex, PriceIndex &lowestLowRef) {
    lowestLowRef.price = low(startingIndex); // Start by assuming the first candle's low is the lowest
    lowestLowRef.index = startingIndex;      // The index of the lowest low (starting with the `startingIndex`)

    // Loop through the candles and find the lowest low and its index
    for (int i = startingIndex + 1; i < startingIndex + rangeCandles; i++) {
        if (low(i) < lowestLowRef.price) {
            lowestLowRef.price = low(i); // Update lowest low
            lowestLowRef.index = i;      // Update index of lowest low
        }
    }
}

Функция "GetHighestHigh" отвечает за определение наивысшего максимума и соответствующего ему индекса в последних барах "rangeCandles", начиная с "startingIndex". Мы инициализируем "highestHighRef.price" максимумом первой свечи в диапазоне ("high(startingIndex)") и устанавливаем для "highestHighRef.index" значение "startingIndex". Затем мы перебираем оставшиеся свечи в указанном диапазоне, проверяя, имеет ли какая-либо из них цену выше текущей "highestHighRef.price". Если найден новый наивысший уровень, обновляем как "highestHighRef.price", так и "highestHighRef.index". Эта функция помогает нам определить верхнюю границу диапазона консолидации.

Аналогично, функция "GetLowestLow" находит самый низкий минимум и его индекс в пределах того же диапазона. Мы инициализируем "lowestLowRef.price" с помощью "low(startingIndex)", а "lowestLowRef.index" - с помощью "startingIndex". По мере того, как мы перебираем свечи, мы проверяем, есть ли у них цена ниже текущей "lowestLowRef.price". Если есть, обновляем как "lowestLowRef.price», так и "lowestLowRef.index". Эта функция определяет нижнюю границу диапазона консолидации. Наконец, у нас есть функция, которая расширит диапазон.

// Function to extend the range if the latest bar remains within the range limits
void ExtendRangeIfWithinLimits() {
    double currentHigh = high(1); // Get the high of the latest closed bar
    double currentLow = low(1);   // Get the low of the latest closed bar

    if (currentHigh <= highestHigh.price && currentLow >= lowestLow.price) {
        // Extend the range if the current bar is within the established range
        Print("Range extended: Including candle with High = ", currentHigh, " and Low = ", currentLow);
    } else {
        Print("No extension possible. The current bar is outside the range.");
    }
}

Здесь функция "ExtendRangeIfWithinLimits" обеспечивает, что ранее определенный диапазон консолидации остается в силе, если новые бары продолжают попадать в его границы. Сначала мы получаем максимум и минимум последней закрытой свечи, используя функции "high(1)" и "low(1)". Затем проверяем, является ли значение "currentHigh" меньшим или равным "highestHigh.price", а также является ли значение "currentLow" большим или равным "lowestLow.price". Если оба условия выполнены, диапазон расширяется, и мы выводим сообщение с подтверждением, указывающее на то, что новая свеча включена в существующий диапазон.

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

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

//--- One-line functions to access price data
double high(int index) { return iHigh(_Symbol, _Period, index); }
double low(int index) { return iLow(_Symbol, _Period, index); }
double open(int index) { return iOpen(_Symbol, _Period, index); }
double close(int index) { return iClose(_Symbol, _Period, index); }
datetime time(int index) { return iTime(_Symbol, _Period, index); }

Эти однострочные функции "high", "low", "open", "close" и "time" служат простыми оболочками для получения ценовых и временных данных исторических баров. Каждая функция вызывает соответствующую встроенную функцию на MQL5 —iHigh, iLow, iOpen, iClose, а также iTime, чтобы получить требуемое значение для данного "индекса". Функция "high" возвращает высокую цену определенного бара, в то время как функция "low" возвращает низкую цену. Аналогично, "open" возвращает цену открытия, а "close" получает цену закрытия. Функция "time" возвращает временную метку бара. Мы используем их для улучшения читаемости кода и обеспечения более чистого и структурированного доступа к историческим данным по всей нашей программе.

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

// Check for breakout if a consolidation range is established
if (highestHigh.price > 0 && lowestLow.price > 0) {
  breakoutDetected = CheckRangeBreak(highestHigh, lowestLow);
}

Здесь, если установлен диапазон консолидации, мы проверяем пробой диапазона, используя пользовательскую функцию, которая снова называется "CheckRangeBreak", и сохраняем результат в переменной "breakoutDetected". Реализация функции выглядит так:

// Function to check for range breaks
bool CheckRangeBreak(PriceIndex &highestHighRef, PriceIndex &lowestLowRef) {
    double closingPrice = close(1); // Get the closing price of the current candle

    if (closingPrice > highestHighRef.price) {
        Print("Range break upwards detected. Closing price ", closingPrice, " is above the highest high: ", highestHighRef.price);
        return true; // Breakout detected
    } else if (closingPrice < lowestLowRef.price) {
        Print("Range break downwards detected. Closing price ", closingPrice, " is below the lowest low: ", lowestLowRef.price);
        return true; // Breakout detected
    }
    return false; // No breakout
}

Для логической функции "CheckRangeBreak" мы сравниваем "closingPrice" текущей свечи с "highestHighRef.price" и "lowestLowRef.price". Если "closingPrice" выше, чем "highestHighRef.price", мы обнаруживаем пробой вверх. Если она ниже, чем "lowestLowRef.price", мы обнаруживаем пробой вниз. В обоих случаях мы возвращаем значение "true" и выводим направление пробоя. Если ни одно из условий не выполняется, возвращаем "false".

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

// Reset state after breakout
if (breakoutDetected) {
  Print("Breakout detected. Resetting for the next range.");
  
  breakoutBarIndex = 1; // Use the current bar's index (index 1 refers to the most recent completed bar)
  breakoutTime = TimeCurrent();
  impulseHigh = highestHigh.price;
  impulseLow = lowestLow.price;
  
  breakoutDetected = false;
  highestHigh.price = 0; 
  highestHigh.index = 0;

  lowestLow.price = 0; 
  lowestLow.index = 0;
}

После обнаружения пробоя мы сбрасываем состояние для следующего диапазона. Мы устанавливаем "breakoutBarIndex" равным 1, ссылаясь на самый последний заполненный бар. Мы также обновляем "breakoutTime" на текущее время, используя функцию "TimeCurrent". Значения "impulseHigh" и "impulseLow" устанавливаются на значения "highestHigh.price" и "lowestLow.price" предыдущего диапазона. Затем мы отмечаем "breakoutDetected" как "false" и сбрасываем значения цен и индексов "highestHigh" и "lowestLow" на 0, готовясь к следующему определению диапазона. Теперь мы можем перейти к проверке действительности блоков ордеров на основании импульсивного движения.

if (breakoutBarIndex >= 0 && TimeCurrent() > breakoutTime + waitBars * PeriodSeconds()) {
  DetectImpulsiveMovement(impulseHigh,impulseLow,waitBars,1);
   
   bool is_OB_Valid = is_OB_DOWN || is_OB_UP;
            
   datetime time1 = iTime(_Symbol,_Period,rangeCandles+waitBars+1);
   double price1 = impulseHigh;
   
   int visibleBars = (int)ChartGetInteger(0,CHART_VISIBLE_BARS);
   datetime time2 = is_OB_Valid ? time1 + (visibleBars/1)*PeriodSeconds() : time(waitBars+1);
   
   double price2 = impulseLow;
   string obNAME = OB_Prefix+"("+TimeToString(time1)+")";
   color obClr = clrBlack;
   
   if (is_OB_Valid){obClr = is_OB_UP ? CLR_UP : CLR_DOWN;}
   else if (!is_OB_Valid){obClr = clrBlue;}
   
   string obText = "";
   
   if (is_OB_Valid){obText = is_OB_UP ? "Bullish Order Block"+ShortToString(0x2BED) : "Bearish Order Block"+ShortToString(0x2BEF);}
   else if (!is_OB_Valid){obText = "Range";}

   //---

}

Здесь мы сначала проверяем больше или равно 0 значение "breakoutBarIndex", и превышает ли текущее время значение "breakoutTime" плюс период ожидания, рассчитанный путем умножения "waitBars" на период в секундах (с использованием функции PeriodSeconds ). Если это условие выполнено, мы вызываем функцию "DetectImpulsiveMovement" для определения импульсных движений рынка, передавая значения "impulseHigh", "impulseLow", «waitBars», а также фиксированный параметр, равный 1.

Затем мы проверяем блок ордеров, проверяя, является ли значение "is_OB_DOWN" или "is_OB_UP" истинным, сохраняя результат в "is_OB_Valid". Мы извлекаем временную метку бара с помощью iTime, которая показывает время для определенного бара по символу и периоду, и сохраняем ее в "time1". Цена этого бара хранится в "impulseHigh", которую мы используем для дальнейших расчетов. Далее мы получаем количество видимых баров на графике, используя функцию ChartGetInteger  с параметром CHART_VISIBLE_BARS, возвращающим количество баров, видимых на графике. Затем вычисляем "time2", которое зависит от того, является ли блок ордеров действительным. Если значение "is_OB_Valid" равно true, мы корректируем время, добавляя видимые бары к значению "time1", умноженному на период в секундах. В противном случае мы используем время следующего бара, определяемое параметром "time(waitBars+1)". Мы определяем это с помощью Ternary operator.

Значение "price2" устанавливается на "impulseLow". Затем генерируем имя ордер-блока, используя "OB_Prefix", вместе с отформатированным временем, используя функцию TimeToString . Цвет ордер-блока задается с помощью переменной "obClr", которая по умолчанию является черной. Если блок ордеров действителен, мы устанавливаем цвет либо "CLR_UP" (для восходящего блока ордеров), либо "CLR_DOWN" (для нисходящего блока ордеров). Если блок ордеров недействителен, цвет устанавливается на синий.

Текст блока ордеров, сохраненный в "obText", устанавливается в зависимости от направления блока ордеров. Если блок ордеров действителен, мы отображаем "Блок бычьих ордеров" или "Блок медвежьих ордеров" с уникальными кодами символов в Unicode (0x2BED для бычьих, 0x2BEF для медвежьих), которые мы преобразуем с помощью функции "ShortToString". Если нет, обозначаем его как "Range". Эти символы в Юникоде приведены ниже.

UNICODE SYMBOLS

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

// Function to detect impulsive movement after breakout
void DetectImpulsiveMovement(double breakoutHigh, double breakoutLow, int impulseBars, double impulseThreshold) {
    double range = breakoutHigh - breakoutLow;         // Calculate the breakout range
    double impulseThresholdPrice = range * impulseThreshold; // Threshold for impulsive move

    // Check for the price movement in the next `impulseBars` bars after breakout
    for (int i = 1; i <= impulseBars; i++) {
        double closePrice = close(i); // Get the close price of the bar

        // Check if the price moves significantly beyond the breakout high
        if (closePrice >= breakoutHigh + impulseThresholdPrice) {
            is_OB_UP = true;
            Print("Impulsive upward movement detected: Close Price = ", closePrice, 
                  ", Threshold = ", breakoutHigh + impulseThresholdPrice);
            return;
        }
        // Check if the price moves significantly below the breakout low
        else if (closePrice <= breakoutLow - impulseThresholdPrice) {
            is_OB_DOWN = true;
            Print("Impulsive downward movement detected: Close Price = ", closePrice, 
                  ", Threshold = ", breakoutLow - impulseThresholdPrice);
            return;
        }
    }

    // If no impulsive movement is detected
    is_OB_UP = false;
    is_OB_DOWN = false;
    Print("No impulsive movement detected after breakout.");
}

В этой функции, чтобы определить, движется ли цена импульсивно после пробоя, мы сначала вычисляем "диапазон", вычитая "breakoutLow" из "breakoutHigh". "impulseThresholdPrice" определяется путем умножения диапазона на значение "impulseThreshold", которое определяет, насколько далеко должна продвинуться цена, чтобы считаться импульсивной. Затем мы проверяем движение цены в следующих барах "impulseBars» с помощью оператора for loop.

Для каждого бара мы получаем "closePrice", используя функцию "close(i)", которая извлекает цену закрытия i-го бара. Если цена закрытия превышает "breakoutHigh" как минимум на значение "impulseThresholdPrice", мы считаем это импульсивным движением вверх, устанавливаем для параметра "is_OB_UP" значение true и графически выводим обнаруженное движение. Аналогично, если цена закрытия падает ниже "breakoutLow" по крайней мере на "impulseThresholdPrice", мы обнаруживаем импульсивное движение вниз, устанавливая для параметра "is_OB_DOWN" значение true и выводя результат на график.

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

if (!is_OB_Valid){
   if (ObjectFind(0,obNAME) < 0){
      CreateRec(obNAME,time1,price1,time2,price2,obClr,obText);
   }
}
else if (is_OB_Valid){
   if (ObjectFind(0,obNAME) < 0){
      CreateRec(obNAME,time1,price1,time2,price2,obClr,obText);
      
      Print("Old ArraySize = ",ArraySize(totalOBs_names));
      ArrayResize(totalOBs_names,ArraySize(totalOBs_names)+1);
      Print("New ArraySize = ",ArraySize(totalOBs_names));
      totalOBs_names[ArraySize(totalOBs_names)-1] = obNAME;
      ArrayPrint(totalOBs_names);
      
      Print("Old ArraySize = ",ArraySize(totalOBs_dates));
      ArrayResize(totalOBs_dates,ArraySize(totalOBs_dates)+1);
      Print("New ArraySize = ",ArraySize(totalOBs_dates));
      totalOBs_dates[ArraySize(totalOBs_dates)-1] = time2;
      ArrayPrint(totalOBs_dates);
      
      Print("Old ArraySize = ",ArraySize(totalOBs_is_signals));
      ArrayResize(totalOBs_is_signals,ArraySize(totalOBs_is_signals)+1);
      Print("New ArraySize = ",ArraySize(totalOBs_is_signals));
      totalOBs_is_signals[ArraySize(totalOBs_is_signals)-1] = false;
      ArrayPrint(totalOBs_is_signals);
      
   }
}

breakoutBarIndex = -1; // Use the current bar's index (index 1 refers to the most recent completed bar)
breakoutTime = 0;
impulseHigh = 0;
impulseLow = 0;
is_OB_UP = false;
is_OB_DOWN = false;

Здесь мы проверяем, действителен ли блок ордеров ("is_OB_Valid"). Если он недействителен, мы используем функцию ObjectFind , чтобы определить, существует ли на графике объект с именем "obNAME". Если объект не найден (функция возвращает отрицательное значение), мы вызываем "CreateRec", чтобы создать блок ордеров на графике, используя такие предоставленные параметры, как время, цена, цвет и текст.

Если блок ордеров действителен, мы снова проверяем, существует ли объект. Если нет, мы создаем его, а затем управляем данными блока ордеров, изменяя размер с помощью функции ArrayResize  и обновляя наши три массива: "totalOBs_names" для хранения имен блоков ордеров, "totalOBs_dates" для временных меток и "totalOBs_is_signals" для хранения того, является ли каждый блок ордеров допустимым сигналом (изначально установлено значение «false»). После изменения размера массивов мы выводим старый и новый размеры массивов с помощью ArraySize  и отображаем содержимое массива с помощью функции ArrayPrint . Наконец, мы сбрасываем состояние пробоя, установив для "breakoutBarIndex" значение -1, для "breakoutTime", "impulseHigh" и "impulseLow" - значение 0, а для флагов направления блока ордеров "is_OB_UP" и "is_OB_DOWN" значение «false».

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

void CreateRec(string objName,datetime time1,double price1,
               datetime time2,double price2,color clr,string txt){
   if (ObjectFind(0,objName) < 0){
      ObjectCreate(0,objName,OBJ_RECTANGLE,0,time1,price1,time2,price2);
      
      Print("SUCCESS CREATING OBJECT >",objName,"< WITH"," T1: ",time1,", P1: ",price1,
            ", T2: ",time2,", P2: ",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_FILL,true);
      ObjectSetInteger(0,objName,OBJPROP_COLOR,clr);
      ObjectSetInteger(0,objName,OBJPROP_BACK,false);

    // Calculate the center position of the rectangle
    datetime midTime = time1 + (time2 - time1) / 2;
    double midPrice = (price1 + price2) / 2;

    // Create a descriptive text label centered in the rectangle
    string description = txt;
    string textObjName = objName + description; // Unique name for the text object
    if (ObjectFind(0, textObjName) < 0) {
        ObjectCreate(0, textObjName, OBJ_TEXT, 0, midTime, midPrice);
        ObjectSetString(0, textObjName, OBJPROP_TEXT, description);
        ObjectSetInteger(0, textObjName, OBJPROP_COLOR, clrBlack);
        ObjectSetInteger(0, textObjName, OBJPROP_FONTSIZE, 15);
        ObjectSetInteger(0, textObjName, OBJPROP_ANCHOR, ANCHOR_CENTER);

        Print("SUCCESS CREATING LABEL >", textObjName, "< WITH TEXT: ", description);
    }

      ChartRedraw(0);
   }
}

В определяемой нами функции "CreateRec" мы проверяем, существует ли объект "objName", используя функцию ObjectFind. Если нет, то мы создаем прямоугольник с заданными временными и ценовыми точками, используя функцию ObjectCreate , определяемый параметром OBJ_RECTANGLE, а также устанавливаем его свойства (например, цвет, заливка, видимость) с помощью ObjectSetInteger и ObjectSetDouble. Мы вычисляем положение центра прямоугольника и создаем надпись посередине с помощью ObjectCreate для текста, определенную в OBJ_TEXT, задавая ее свойства (текст, цвет, размер, привязку). Наконец, вызываем функцию ChartRedraw, чтобы обновить график. Если объект или метка уже существуют, никакие действия не предпринимаются.

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

for (int j=ArraySize(totalOBs_names)-1; j>=0; j--){
   string obNAME = totalOBs_names[j];
   bool obExist = false;
   //Print("name = ",fvgNAME," >",ArraySize(totalFVGs)," >",j);
   //ArrayPrint(totalFVGs);
   //ArrayPrint(barTIMES);
   double obHigh = ObjectGetDouble(0,obNAME,OBJPROP_PRICE,0);
   double obLow = ObjectGetDouble(0,obNAME,OBJPROP_PRICE,1);
   datetime objTime1 = (datetime)ObjectGetInteger(0,obNAME,OBJPROP_TIME,0);
   datetime objTime2 = (datetime)ObjectGetInteger(0,obNAME,OBJPROP_TIME,1);
   color obColor = (color)ObjectGetInteger(0,obNAME,OBJPROP_COLOR);
   
   if (time(1) < objTime2){
      //Print("FOUND: ",obNAME," @ bar ",j,", H: ",obHigh,", L: ",obLow);
      obExist = true;
   }    
   
   double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
   double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
   
   if (obColor == CLR_UP && Ask > obHigh && close(1) > obHigh && open(1) < obHigh && !totalOBs_is_signals[j]){
      Print("BUY SIGNAL For (",obNAME,") Now @ ",Ask);
      double sl = Bid - 1500*_Point;
      double tp = Bid + 1500*_Point;
      obj_Trade.Buy(0.01,_Symbol,Ask,sl,tp);
      totalOBs_is_signals[j] = true;
      ArrayPrint(totalOBs_names,_Digits," [< >] ");
      ArrayPrint(totalOBs_is_signals,_Digits," [< >] ");
   }
   else if (obColor == CLR_DOWN && Bid < obLow && close(1) < obLow && open(1) > obLow && !totalOBs_is_signals[j]){
      Print("SELL SIGNAL For (",obNAME,") Now @ ",Bid);
      double sl = Ask  + 1500*_Point;
      double tp = Ask - 1500*_Point;
      obj_Trade.Sell(0.01,_Symbol,Bid,sl,tp);
      totalOBs_is_signals[j] = true;
      ArrayPrint(totalOBs_names,_Digits," [< >] ");
      ArrayPrint(totalOBs_is_signals,_Digits," [< >] ");
   }
   
   if (obExist == false){
      bool removeName = ArrayRemove(totalOBs_names,0,1);
      bool removeTime = ArrayRemove(totalOBs_dates,0,1);
      bool remove_isSignal = ArrayRemove(totalOBs_is_signals,0,1);
      if (removeName && removeTime && remove_isSignal){
         Print("Success removing the OB DATA from arrays. New Data as below:");
         Print("Total Sizes => OBs: ",ArraySize(totalOBs_names),", TIMEs: ",ArraySize(totalOBs_dates),", SIGNALs: ",ArraySize(totalOBs_is_signals));
         ArrayPrint(totalOBs_names);
         ArrayPrint(totalOBs_dates);
         ArrayPrint(totalOBs_is_signals);
      }
   }   
}

Здесь мы перебираем массив "totalOBs_names" для обработки каждого блока ордеров ("obNAME"). Мы получаем высокие и низкие цены блока ордеров, временные метки и цвет с помощью функций ObjectGetDouble и ObjectGetInteger. Затем мы проверяем, является ли текущее время более ранним, чем время окончания блока ордеров. Если соблюдено условие по времени, переходим к проверке сигналов на покупку или продажу на основе цвета блока ордеров и ценовых условий. Если условия выполнены, мы заключаем сделку на покупку или продажу с помощью функций "obj_Trade.Buy" или "obj_Trade.Sell» и обновляем массив "totalOBs_is_signals", чтобы отметить блок ордеров как пославший сигнал, чтобы мы не торговали им снова в случае отката цены.

Если блок ордеров не соответствует условию времени, мы удаляем его из массивов "totalOBs_names", "totalOBs_dates" и "totalOBs_is_signals" с помощью функции  ArrayRemove. Если удаление прошло успешно, мы выводим обновленные размеры массивов и их содержимое. Это текущий этап, которого нам удалось достичь.

ORDER BLOCKS VALIDATED

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


Тестирование на истории и оптимизация

После тщательного тестирования на истории мы получили следующие результаты.

График бэк-тестирования:

График

Отчет о бэк-тестировании:

REPORT

Здесь также представлен видеоформат, демонстрирующий бэк-тест всей стратегии в течение 1 года, в 2024 году.


Заключение

В заключение мы продемонстрировали процесс разработки сложного советника (EA) на MQL5, использующего функцию обнаружения блоков ордеров для стратегий smart money в трейдинге. Используя такие инструменты, как динамический анализ диапазона, ценовое движение и обнаружение пробоев в режиме реального времени, мы создали программу, которая может определять ключевые уровни поддержки и сопротивления, генерировать действенные торговые сигналы и управлять ордерами с высокой точностью.

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

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

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

Прикрепленные файлы |
ORDER_BLOCKS_EA.mq5 (33.68 KB)
Пользовательские символы MQL5: Создаем символ 3D-баров Пользовательские символы MQL5: Создаем символ 3D-баров
В данной статье представлено детальное руководство по созданию инновационного индикатора 3DBarCustomSymbol.mq5, который генерирует пользовательские символы в MetaTrader 5, объединяющие цену, время, объем и волатильность в единое трехмерное представление. Рассматриваются математические основы, архитектура системы, практические аспекты реализации и применения в торговых стратегиях.
Знакомство с кривыми рабочих характеристик приемника (ROC-кривыми) Знакомство с кривыми рабочих характеристик приемника (ROC-кривыми)
ROC-кривые — графические представления, используемые для оценки эффективности классификаторов. Хотя графики ROC относительно просты, на практике при их использовании существуют распространенные заблуждения и подводные камни. Цель данной статьи — познакомить читателя с графиками ROC как инструментом для практикующих специалистов, стремящихся разобраться в оценке эффективности классификаторов.
Трейдинг с экономическим календарем MQL5 (Часть 3): Добавление сортировки по валюте, важности и времени Трейдинг с экономическим календарем MQL5 (Часть 3): Добавление сортировки по валюте, важности и времени
В этой статье мы реализуем фильтры на панели инструментов экономического календаря MQL5 для лучшего отображения новостей по валюте, важности и времени. Сначала мы установим критерии сортировки для каждой категории, а затем интегрируем их в панель управления, чтобы отображать только релевантные события. Наконец, мы обеспечим динамическое обновление каждого фильтра, чтобы предоставлять трейдерам необходимую экономическую информацию в реальном времени.
Экстремальная оптимизация — Extremal Optimization (EO) Экстремальная оптимизация — Extremal Optimization (EO)
В данной статье рассматривается алгоритм Extremal Optimization (EO) — метод оптимизации, вдохновленный моделью самоорганизованной критичности Бака-Снеппена, где эволюция происходит через устранение наихудших компонентов системы. Модифицированная популяционная версия алгоритма демонстрирует отход от теоретических принципов в пользу практической эффективности, что приводит к созданию мощных вычислительных инструментов.