Automatizando Estratégias de Trading em MQL5 (Parte 6): Dominando a Detecção de Order Blocks para Trading com Smart Money
Introdução
No artigo anterior (Parte 5 da série), desenvolvemos a Estratégia Adaptive Crossover RSI Trading Suite, combinando cruzamentos de médias móveis com filtragem por RSI para identificar oportunidades de trade de alta probabilidade. Agora, na Parte 6, focamos na análise pura de price action com um Sistema Automatizado de Detecção de Order Blocks em MetaQuotes Language 5 (MQL5), uma ferramenta poderosa utilizada no trading com smart money. Essa estratégia identifica order blocks institucionais chave — zonas onde grandes participantes acumulam ou distribuem posições — ajudando traders a antecipar possíveis reversões e continuações de tendência.
Diferentemente dos indicadores tradicionais, essa abordagem depende inteiramente da estrutura de preço, detectando dinamicamente order blocks de alta e de baixa com base no comportamento histórico do preço. O sistema visualiza essas zonas diretamente no gráfico, fornecendo aos traders um contexto claro de mercado e possíveis configurações de trade. Neste artigo, abordaremos o desenvolvimento passo a passo dessa estratégia, desde a definição de order blocks até sua implementação em MQL5, o backtest de sua eficácia e a análise de desempenho. Estruturaremos esta discussão nas seguintes seções:
- Plano Estratégico
- Implementação em MQL5
- Backtesting
- Conclusão
Ao final, você terá uma base sólida em automatizar a detecção de order blocks, permitindo integrar conceitos de smart money aos seus algoritmos de trading. Vamos começar então!
Plano Estratégico
Começaremos identificando faixas de consolidação, que ocorrem quando o preço se move dentro de um intervalo restrito sem uma direção clara de tendência. Para isso, examinaremos o mercado em busca de áreas onde a ação do preço não apresenta rompimentos significativos. Assim que detectarmos um rompimento dessa faixa, avaliaremos se um order block pode ser formado. Nosso processo de validação envolverá a verificação das três velas anteriores ao rompimento.. Se essas velas apresentarem movimento impulsivo, classificaremos o order block como de alta ou de baixa com base na direção do rompimento. Um order block de alta será identificado quando o rompimento ocorrer para cima, enquanto um order block de baixa será marcado quando o rompimento ocorrer para baixo. Uma vez validado, plotaremos o order block no gráfico para referência futura. Aqui está um exemplo.

Se as três velas anteriores não mostrarem movimento impulsivo, não validaremos um order block. Em vez disso, desenharemos apenas a faixa de consolidação, garantindo que não marquemos zonas fracas ou insignificantes. Após marcar os order blocks válidos, monitoraremos continuamente a ação do preço. Se o preço retornar a um order block previamente validado, executaremos trades alinhados com a direção inicial do rompimento, esperando que a tendência continue. No entanto, se um order block se estender além do último ponto significativo de preço, nós o removeremos do nosso array de order blocks válidos, garantindo que negociemos apenas zonas relevantes e recentes. Essa abordagem estruturada nos ajudará a focar em configurações de alta probabilidade, filtrando rompimentos fracos e garantindo que nossos trades estejam alinhados com os movimentos de smart money.
Implementação em MQL5
Para implementar a identificação dos Order Blocks em MQL5, precisaremos definir algumas variáveis globais que serão necessárias durante todo o processo.
#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;
Começamos incluindo a biblioteca "Trade.mqh" e criando um objeto "CTrade", "obj_Trade", para lidar com a execução de trades. Definimos uma struct "PriceIndex" para armazenar tanto o nível de preço quanto seu índice correspondente, o que nos ajuda a rastrear a máxima mais alta e a mínima mais baixa dentro da faixa de consolidação. As variáveis globais "highestHigh" e "lowestLow" armazenam esses níveis-chave, enquanto o sinalizador "breakoutDetected" indica se um rompimento ocorreu.
Para validar movimento impulsivo, introduzimos "impulseLow" e "impulseHigh", que ajudarão a determinar a força do rompimento. A variável "breakoutBarIndex" rastreia exatamente a barra onde o rompimento ocorreu, e "breakoutTime" armazena o timestamp correspondente. Para o gerenciamento de order blocks, mantemos três arrays globais: "totalOBs_names", "totalOBs_dates" e "totalOBs_is_signals". Esses arrays armazenam os nomes dos order blocks, seus respectivos timestamps e se são sinais de trade válidos.
Definimos o prefixo de order block como "OB_Prefix" e atribuimos códigos de cor para order blocks de alta e de baixa usando "CLR_UP" para alta (lime) e "CLR_DOWN" para baixa (vermelho). Por fim, os sinalizadores booleanos "is_OB_UP" e "is_OB_DOWN" nos ajudam a rastrear se o último order block detectado é de alta ou de baixa. Não precisamos rastrear os Order Blocks na inicialização do programa, pois queremos começar do zero, com um estado limpo. Assim, implementaremos a lógica de controle diretamente no manipulador de evento 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; //--- }
No manipulador de evento OnTick, começamos detectando a formação de uma nova barra usando "currBars" e "prevBars". Definimos "isNewBar" como "true" quando uma nova barra aparece e retornamos imediatamente se nenhuma nova barra for detectada. Em seguida, definimos "rangeCandles" como "7", que representa o número mínimo de velas que analisamos para identificar consolidação. A variável "maxDeviation" é definida como "50" pontos, limitando a diferença aceitável entre os preços mais alto e mais baixo dentro da faixa. O "startingIndex" é inicializado como "1", garantindo que comecemos a varredura a partir da barra concluída mais recente. Além disso, definimos "waitBars" como "3" para determinar quantas barras devem passar antes de validar um order block. Em seguida, precisamos verificar faixas de consolidação e obter os preços para posterior determinação de order blocks válidos.
// 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(); } }
A cada nova barra formada, verificamos consolidação ou estendemos a faixa existente se nenhum rompimento tiver sido detectado. Se "highestHigh.price" e "lowestLow.price" forem ambos zero, isso significa que nenhuma faixa de consolidação foi estabelecida ainda. Então chamamos a função "IsConsolidationEqualHighsAndLows" para verificar se as últimas "rangeCandles" formam uma consolidação dentro do "maxDeviation" permitido. Se confirmado, usamos as funções "GetHighestHigh" e "GetLowestLow" para determinar os preços máximo e mínimo exatos dentro da faixa, armazenando seus valores juntamente com seus respectivos índices de barra.
Se uma faixa já estiver estabelecida, garantimos que a barra atual permaneça dentro dos limites definidos chamando a função "ExtendRangeIfWithinLimits". Essa função ajuda a ajustar dinamicamente a faixa enquanto nenhum rompimento ocorre. Aqui está a implementação dos trechos de código das funções personalizadas.
// 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; }
Definimos uma função boolean "IsConsolidationEqualHighsAndLows", responsável por detectar consolidação verificando se as máximas e mínimas das últimas "rangeCandles" são praticamente iguais dentro de um "maxDeviation" especificado. Alcançamos isso iterando sobre cada barra, começando de "startingIndex", e comparando as máximas e mínimas de velas consecutivas.
Dentro do loop for, usamos a função MathAbs para calcular a diferença absoluta entre a máxima da barra atual ("high(i)") e a próxima máxima. Se essa diferença exceder o desvio máximo convertido em forma de ponto, Point, a função retorna imediatamente false, indicando que as máximas não são suficientemente iguais para serem consideradas uma consolidação. De forma semelhante, aplicamos a função MathAbs novamente para comparar as mínimas de barras consecutivas ("low(i)" e "low(i + 1)"), garantindo que as mínimas também estejam dentro do desvio permitido. Se qualquer verificação falhar, a função é encerrada antecipadamente com false. Se todas as máximas e mínimas permanecerem dentro do desvio aceitável, retornamos true, confirmando uma faixa de consolidação válida. As próximas funções que definimos são as responsáveis por recuperar os preços máximo e mínimo das barras.
// 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 } } }
A função "GetHighestHigh" é responsável por identificar a máxima mais alta e seu índice correspondente dentro das últimas "rangeCandles" barras, começando de "startingIndex". Inicializamos "highestHighRef.price" com a máxima da primeira vela da faixa ("high(startingIndex)") e definimos "highestHighRef.index" como "startingIndex". Em seguida, iteramos pelas velas restantes dentro da faixa especificada, verificando se alguma delas possui preço mais alto que o atual "highestHighRef.price". Se uma nova máxima mais alta for encontrada, atualizamos tanto "highestHighRef.price" quanto "highestHighRef.index". Essa função nos ajuda a determinar o limite superior de uma faixa de consolidação.
De forma semelhante, a função "GetLowestLow" encontra a mínima mais baixa e seu índice dentro da mesma faixa. Inicializamos "lowestLowRef.price" com "low(startingIndex)" e "lowestLowRef.index" com "startingIndex". À medida que percorremos as velas, verificamos se alguma possui preço mais baixo que o atual "lowestLowRef.price". À medida que percorremos as velas, verificamos se alguma possui preço mais baixo que o atual "lowestLowRef.price". Essa função determina o limite inferior de uma faixa de consolidação. Por fim, temos a função que estenderá a faixa.
// 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."); } }
Aqui, a função "ExtendRangeIfWithinLimits" garante que a faixa de consolidação previamente identificada permaneça válida se novas barras continuarem dentro de seus limites. Primeiro, recuperamos a máxima e a mínima da vela mais recentemente fechada usando as funções "high(1)" e "low(1)". Em seguida, verificamos se "currentHigh" é menor ou igual a "highestHigh.price" e se "currentLow" é maior ou igual a "lowestLow.price". Se ambas as condições forem atendidas, a faixa é estendida, e imprimimos uma mensagem de confirmação indicando que a nova vela está incluída dentro da faixa existente.
Caso contrário, se a nova vela se mover para fora da faixa estabelecida, nenhuma extensão ocorre, e imprimimos uma mensagem informando que a faixa não pode ser estendida. Essa função desempenha um papel fundamental na manutenção de zonas de consolidação válidas e evita a detecção desnecessária de rompimentos se o mercado permanecer dentro da faixa predefinida.
Também utilizamos funções predefinidas responsáveis por recuperar dados de preço das barras. Aqui estão seus trechos de código.
//--- 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); }
Essas funções de uma linha "high", "low", "open", "close" e "time" servem como wrappers simples para recuperar dados de preço e tempo de barras históricas. Cada função chama a respectiva função interna do MQL5 — iHigh, iLow, iOpen, iClose e iTime — para buscar o valor solicitado para um determinado "index". A função "high" retorna o preço máximo de uma barra específica, enquanto a função "low" retorna o preço mínimo. De forma semelhante, "open" recupera o preço de abertura, e "close" obtém o preço de fechamento. A função "time" retorna o timestamp da barra. Usamos essas funções para melhorar a legibilidade do código e permitir um acesso mais limpo e estruturado aos dados históricos em todo o programa.
Munidos dessas funções, agora podemos verificar rompimentos se uma faixa de consolidação estiver estabelecida por meio do seguinte trecho de código.
// Check for breakout if a consolidation range is established if (highestHigh.price > 0 && lowestLow.price > 0) { breakoutDetected = CheckRangeBreak(highestHigh, lowestLow); }
Aqui, se uma faixa de consolidação estiver estabelecida, verificamos o rompimento da faixa usando novamente uma função personalizada chamada "CheckRangeBreak", armazenando o resultado na variável "breakoutDetected". A implementação da função é a seguinte.
// 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 }
Para a função boolean "CheckRangeBreak", comparamos o "closingPrice" da vela atual com "highestHighRef.price" e "lowestLowRef.price". Se o "closingPrice" for maior que "highestHighRef.price", detectamos um rompimento para cima. Se for menor que "lowestLowRef.price", detectamos um rompimento para baixo. Em ambos os casos, retornamos "true" e imprimimos a direção do rompimento. Se nenhuma das condições for atendida, retornamos "false".
Agora podemos usar a variável para detectar um rompimento, onde precisamos redefinir o estado da faixa para nos preparar para uma próxima possível faixa de consolidação, conforme a seguir.
// 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; }
Após a detecção de um rompimento, redefinimos o estado para a próxima faixa. Definimos "breakoutBarIndex" como 1, referindo-se à barra concluída mais recente. Também atualizamos "breakoutTime" com o tempo atual usando a função "TimeCurrent". "impulseHigh" e "impulseLow" são definidos com base em "highestHigh.price" e "lowestLow.price" da faixa anterior. Em seguida, marcamos "breakoutDetected" como "false" e redefinimos os preços e índices de "highestHigh" e "lowestLow" para 0, preparando a detecção da próxima faixa. Agora podemos prosseguir para verificar order blocks válidos com base em movimento impulsivo.
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";} //--- }
Aqui, primeiro verificamos se "breakoutBarIndex" é maior ou igual a 0 e se o tempo atual é maior que "breakoutTime" somado a um período de espera, calculado multiplicando "waitBars" pelo período em segundos (usando a função PeriodSeconds). Se essa condição for atendida, chamamos a função "DetectImpulsiveMovement" para identificar movimentos impulsivos do mercado, passando os valores de "impulseHigh", "impulseLow", "waitBars" e um parâmetro fixo de 1.
Em seguida, validamos o order block verificando se "is_OB_DOWN" ou "is_OB_UP" é verdadeiro, armazenando o resultado em "is_OB_Valid". Recuperamos o timestamp da barra com iTime, que fornece o tempo de uma barra específica no símbolo e período, armazenando-o em "time1". O preço dessa barra é armazenado em "impulseHigh", que usamos para cálculos posteriores. Em seguida, obtemos o número de barras visíveis no gráfico usando a função ChartGetInteger com o parâmetro CHART_VISIBLE_BARS, que retorna quantas barras estão visíveis no gráfico. Calculamos então "time2", que depende de o order block ser válido. Se "is_OB_Valid" for verdadeiro, ajustamos o tempo adicionando as barras visíveis a "time1", multiplicadas pelo período em segundos. Caso contrário, usamos o tempo da próxima barra, determinado por "time(waitBars+1)". Determinamos isso usando um operador ternário.
"price2" é definido como "impulseLow". Em seguida, geramos o nome do order block usando "OB_Prefix" junto com o tempo formatado pela função TimeToString. A cor do order block é definida usando a variável "obClr", que é preta por padrão. Se o order block for válido, definimos a cor como "CLR_UP" (para um order block de alta) ou "CLR_DOWN" (para um order block de baixa). Se o order block for inválido, a cor é definida como azul.
O texto do order block, armazenado em "obText", é definido com base na direção do order block. Se o order block for válido, exibimos "Bullish Order Block" ou "Bearish Order Block" com códigos de caracteres Unicode exclusivos (0x2BED para alta, 0x2BEF para baixa), que convertemos usando a função "ShortToString". Caso contrário, rotulamos como "Range". Esses símbolos Unicode são mostrados abaixo.

A função para detectar movimentos impulsivos é a seguinte.
// 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."); }
Na função, para detectar se o preço se move impulsivamente após um rompimento, primeiro calculamos o "range" subtraindo "breakoutLow" de "breakoutHigh". O "impulseThresholdPrice" é determinado multiplicando o range pelo valor de "impulseThreshold", que define o quanto o preço deve se mover para ser considerado impulsivo. Em seguida, verificamos o movimento de preço nas próximas "impulseBars" barras usando um loop for.
Para cada barra, obtemos o "closePrice" usando a função "close(i)", que recupera o preço de fechamento da i-ésima barra. Se o preço de fechamento exceder "breakoutHigh" em pelo menos "impulseThresholdPrice", consideramos isso um movimento impulsivo de alta, definindo "is_OB_UP" como true e imprimindo o movimento detectado. De forma semelhante, se o preço de fechamento cair abaixo de "breakoutLow" em pelo menos "impulseThresholdPrice", detectamos um movimento impulsivo de baixa, definindo "is_OB_DOWN" como true e imprimindo o resultado.
Se nenhum movimento significativo de preço for detectado após verificar todas as barras, tanto "is_OB_UP" quanto "is_OB_DOWN" são definidos como false, e imprimimos que nenhum movimento impulsivo foi detectado. Agora podemos plotar as faixas no gráfico, bem como os order blocks, conforme a seguir.
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;
Aqui, verificamos se o order block ("is_OB_Valid") é válido. Se não for válido, usamos a função ObjectFind para determinar se um objeto com o nome "obNAME" já existe no gráfico. Se o objeto não for encontrado (a função retorna um valor negativo), chamamos "CreateRec" para criar o order block no gráfico usando os parâmetros fornecidos, como tempo, preço, cor e texto.
Se o order block for válido, verificamos novamente se o objeto existe. Caso não exista, nós o criamos e então gerenciamos os dados do order block redimensionando com a função ArrayResize e atualizando nossos três arrays: "totalOBs_names" para armazenar os nomes dos order blocks, "totalOBs_dates" para os timestamps e "totalOBs_is_signals" para armazenar se cada order block é um sinal válido (inicialmente definido como false). Após redimensionar os arrays, imprimimos os tamanhos antigo e novo dos arrays com ArraySize e exibimos o conteúdo dos arrays usando a função ArrayPrint. Por fim, redefinimos o estado de rompimento definindo "breakoutBarIndex" como -1, redefinindo "breakoutTime", "impulseHigh" e "impulseLow" para 0, e definindo os sinalizadores de direção do order block, "is_OB_UP" e "is_OB_DOWN", como false.
Para criar os retângulos com texto, usamos uma função personalizada "CreateRec", conforme a seguir.
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); } }
Na função "CreateRec" que definimos, verificamos se o objeto "objName" existe usando a função ObjectFind. Caso não exista, criamos um retângulo com os pontos de tempo e preço fornecidos usando a função ObjectCreate, definido por OBJ_RECTANGLE, e configuramos suas propriedades (por exemplo, cor, preenchimento, visibilidade) usando ObjectSetInteger e ObjectSetDouble. Calculamos a posição central do retângulo e criamos um rótulo no meio usando ObjectCreate para texto, definido por OBJ_TEXT, configurando suas propriedades (texto, cor, tamanho, âncora). Por fim, chamamos a função ChartRedraw para atualizar o gráfico. Se o objeto ou rótulo já existir, nenhuma ação é tomada.
Com os order blocks plotados, agora podemos avançar para determinar se os testamos novamente e abrir posições quando o preço entra e rompe para fora de suas faixas.
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); } } }
Aqui, fazemos um loop pelo array "totalOBs_names" para processar cada order block ("obNAME"). Recuperamos os preços máximo e mínimo do order block, timestamps e cor usando as funções ObjectGetDouble e ObjectGetInteger. Em seguida, verificamos se o tempo atual é anterior ao tempo final do order block. Se a condição de tempo for atendida, prosseguimos para verificar sinais de compra ou venda com base na cor do order block e nas condições de preço. Se as condições forem satisfeitas, executamos uma operação de compra ou venda usando as funções "obj_Trade.Buy" ou "obj_Trade.Sell" e atualizamos o array "totalOBs_is_signals" para marcar que o order block acionou um sinal, evitando negociá-lo novamente caso o preço retorne.
Se um order block não atender à condição de tempo, nós o removemos dos arrays "totalOBs_names", "totalOBs_dates" e "totalOBs_is_signals" usando a função ArrayRemove. Se a remoção for bem-sucedida, imprimimos os tamanhos e conteúdos atualizados dos arrays. Aqui está o marco atual que alcançamos.

Pela imagem, podemos ver que os order blocks são detectados e negociados, atingindo nosso objetivo, restando apenas realizar o backtest do programa e analisar seu desempenho. Isso é tratado na seção seguinte.
Backtesting e Otimização
Após um backtest completo, obtivemos os seguintes resultados.
Gráfico de backtest:

Relatório de backtest:

Aqui também há um vídeo demonstrando todo o backtest da estratégia em um período de 1 ano, 2024.
Conclusão
Em conclusão, demonstramos o processo de desenvolvimento de um Expert Advisor (EA) sofisticado em MQL5 que utiliza a detecção de Order Blocks para estratégias de trading com smart money. Ao incorporar ferramentas como análise dinâmica de faixa, price action e detecção de rompimentos em tempo real, criamos um programa capaz de identificar níveis-chave de suporte e resistência, gerar sinais de trade acionáveis e gerenciar ordens com alta precisão.
Aviso: Este artigo tem finalidade exclusivamente educacional. O trading envolve risco financeiro substancial, e o comportamento do mercado pode ser altamente imprevisível. As estratégias apresentadas neste artigo oferecem uma abordagem estruturada, mas não garantem lucratividade futura. Testes adequados e gestão de risco são essenciais antes da operação em ambiente real.
Ao aplicar esses métodos, você pode construir sistemas de trading mais eficazes, aprimorar sua abordagem de análise de mercado e levar seu trading algorítmico ao próximo nível. Boa sorte em sua jornada no trading!
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/17135
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Caminhe em novos trilhos: Personalize indicadores no MQL5
Desenvolvimento do Kit de Ferramentas de Análise de Ação de Preço (Parte 12): Fluxo Externo (III) Mapa de Tendências
Está chegando o novo MetaTrader 5 e MQL5
Rede neural na prática: Surgimento de C_Neuron
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso