English Русский 中文 Español Deutsch 日本語
preview
Automatização de estratégias de negociação em MQL5 (Parte 24): Sistema de negociação por rompimento da sessão de Londres com gerenciamento de risco e trailing stops

Automatização de estratégias de negociação em MQL5 (Parte 24): Sistema de negociação por rompimento da sessão de Londres com gerenciamento de risco e trailing stops

MetaTrader 5 — Negociação |
29 14
Allan Munene Mutiiria
Allan Munene Mutiiria

Introdução

No artigo anterior (Parte 23), aprimoramos o sistema Zone Recovery para negociação a favor da tendência, desenvolvido em MQL5 com o indicador Envelopes, adicionando trailing stops e múltiplas cestas de operações para melhorar a proteção dos lucros e o processamento dos sinais. Na Parte 24, desenvolveremos um sistema de rompimento da sessão de Londres que identifica a faixa de preços formada antes da abertura, posiciona ordens pendentes e incorpora ferramentas de gerenciamento de risco, como relação risco/retorno, limites de drawdown e um painel de controle para monitoramento em tempo real. Abordaremos os seguintes tópicos:

  1. Entendendo a estratégia de rompimento da sessão de Londres
  2. Implementação em MQL5
  3. BACKTESTING
  4. Conclusão

Ao final, você terá um programa completo em MQL5, com mecanismos avançados de controle de risco, pronto para testes e aprimoramentos. Vamos começar!


Entendendo a estratégia de rompimento da sessão de Londres

A estratégia de rompimento da sessão de Londres aproveita o aumento da volatilidade na abertura do mercado londrino. Ela identifica a faixa de preços formada nas horas que antecedem a abertura e posiciona ordens pendentes para operar seu rompimento. A sessão de Londres costuma apresentar alta liquidez e movimentos expressivos de preços, criando boas oportunidades de lucro. No entanto, a estratégia exige um gerenciamento de risco rigoroso para proteger o capital contra falsos rompimentos e drawdowns.

Para isso, calcularemos a máxima e a mínima do período anterior à sessão de Londres e utilizaremos esses níveis para posicionar ordens Buy Stop e Sell Stop com distâncias configuráveis em relação a eles. Também configuraremos a relação risco/retorno para determinar os níveis de take profit, trailing stops para assegurar os lucros e limites para o número de operações abertas e o drawdown diário, visando preservar o capital. Além disso, utilizaremos um painel de controle para monitoramento em tempo real e verificações do horário da sessão, garantindo que as operações sejam executadas apenas nos períodos definidos. O sistema também será capaz de se adaptar a diferentes condições de mercado. A seguir, apresentamos a estrutura conceitual do sistema que implementaremos.

ESTRUTURA DA ESTRATÉGIA


Implementação em MQL5

Para criar o programa em MQL5, abra o MetaEditor, acesse o Navigator, localize a pasta Indicators, clique na aba New e siga as instruções para criar o arquivo. Após essa etapa, começaremos declarando os parâmetros de entrada e as estruturas no ambiente de desenvolvimento.

//+------------------------------------------------------------------+
//|                                        London Breakout EA.mq5    |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh> //--- Include Trade library for trading operations

//--- Enumerations
enum ENUM_TRADE_TYPE {     //--- Enumeration for trade types
   TRADE_ALL,              // All Trades (Buy and Sell)
   TRADE_BUY_ONLY,         // Buy Trades Only
   TRADE_SELL_ONLY         // Sell Trades Only
};

//--- Input parameters
sinput group "General EA Settings"
input double inpTradeLotsize = 0.01; // Lotsize
input ENUM_TRADE_TYPE TradeType = TRADE_ALL; // Trade Type Selection
sinput int MagicNumber = 12345;     // Magic Number
input double RRRatio = 1.0;        // Risk to Reward Ratio
input int StopLossPoints = 500;    // Stop loss in points
input int OrderOffsetPoints = 10;   // Points offset for Orders
input bool DeleteOppositeOrder = true; // Delete opposite order when one is activated?
input bool UseTrailing = false;    // Use Trailing Stop?
input int TrailingPoints = 50;     // Trailing Points (distance)
input int MinProfitPoints = 100;   // Minimum Profit Points to start trailing

sinput group "London Session Settings"
input int LondonStartHour = 9;        // London Start Hour
input int LondonStartMinute = 0;      // London Start Minute
input int LondonEndHour = 8;          // London End Hour
input int LondonEndMinute = 0;        // London End Minute
input int MinRangePoints = 100;       // Min Pre-London Range in points
input int MaxRangePoints = 300;       // Max Pre-London Range in points

sinput group "Risk Management"
input int MaxOpenTrades = 2;       // Maximum simultaneous open trades
input double MaxDailyDrawdownPercent = 5.0; // Max daily drawdown % to stop trading

//--- Structures
struct PositionInfo {      //--- Structure for position information
   ulong ticket;           // Position ticket
   double openPrice;      // Entry price
   double londonRange;    // Pre-London range in points for this position
   datetime sessionID;    // Session identifier (day)
   bool trailingActive;   // Trailing active flag
};

Começamos a implementação do sistema de rompimento da sessão de Londres incluindo a biblioteca <Trade\Trade.mqh> e definindo as enumerações, os parâmetros de entrada e a estrutura para monitoramento das posições. A biblioteca <Trade\Trade.mqh> fornece acesso à classe CTrade, que permite executar operações de negociação, como posicionar ordens e modificar posições. Definimos a enumeração ENUM_TRADE_TYPE com as opções TRADE_ALL para negociar em ambas as direções, TRADE_BUY_ONLY para permitir apenas compras e TRADE_SELL_ONLY para permitir apenas vendas, possibilitando restringir as direções das operações.

Em seguida, organizamos os parâmetros de entrada por grupos. Nas configurações gerais, inpTradeLotsize é definido como 0.01 para o tamanho do lote; TradeType utiliza a enumeração, com TRADE_ALL como valor padrão; MagicNumber recebe 12345 para identificar as operações do EA; RRRatio é definido como 1.0 para a relação risco/retorno; StopLossPoints recebe 500 para a distância do stop loss; OrderOffsetPoints = 10 define a distância de entrada em relação ao nível de referência; DeleteOppositeOrder é definido como true para excluir a ordem pendente oposta; UseTrailing é definido como false, deixando o trailing stop desativado por padrão; TrailingPoints recebe 50 para definir a distância do trailing stop em pontos; e MinProfitPoints recebe 100 para definir o lucro mínimo, em pontos, necessário à ativação do trailing stop.

Na seção London Session Settings, definimos o início da sessão com LondonStartHour = 9 e LondonStartMinute = 0, e o término com LondonEndHour = 8 e LondonEndMinute = 0. Também utilizamos MinRangePoints = 100 e MaxRangePoints = 300 para validar a amplitude da faixa de preços. Na seção Risk Management, MaxOpenTrades é definido como 2 para limitar o número de posições abertas simultaneamente, enquanto MaxDailyDrawdownPercent recebe 5.0 para interromper as operações quando o drawdown ultrapassar o limite estabelecido. Definimos a estrutura PositionInfo para monitorar as posições abertas, na qual ticket armazena o identificador da posição, openPrice representa o preço de abertura, londonRange corresponde à faixa de preços anterior à sessão de Londres, sessionID identifica o dia da sessão e trailingActive é um valor booleano que indica se o trailing stop está ativo. Após a compilação, obtemos o seguinte resultado.

Conjunto de parâmetros de entrada

Agora podemos definir as variáveis globais adicionais que serão utilizadas no programa.

//--- Global variables
CTrade obj_Trade;                 //--- Trade object
double PreLondonHigh = 0.0;       //--- Pre-London session high
double PreLondonLow = 0.0;        //--- Pre-London session low
datetime PreLondonHighTime = 0;   //--- Time of Pre-London high
datetime PreLondonLowTime = 0;    //--- Time of Pre-London low
ulong buyOrderTicket = 0;         //--- Buy stop order ticket
ulong sellOrderTicket = 0;        //--- Sell stop order ticket
bool panelVisible = true;         //--- Panel visibility flag
double LondonRangePoints = 0.0;   //--- Current session's Pre-London range
PositionInfo positionList[];      //--- Array to store position info
datetime lastCheckedDay = 0;      //--- Last checked day
bool noTradeToday = false;        //--- Flag to prevent trading today
bool sessionChecksDone = false;   //--- Flag for session checks completion
datetime analysisTime = 0;        //--- Time for London analysis
double dailyDrawdown = 0.0;       //--- Current daily drawdown
bool isTrailing = false;          //--- Global flag for any trailing active
const int PreLondonStartHour = 3; //--- Fixed Pre-London Start Hour
const int PreLondonStartMinute = 0; //--- Fixed Pre-London Start Minute

Declaramos as seguintes variáveis globais: obj_Trade, um objeto da classe CTrade para executar operações de negociação; PreLondonHigh e PreLondonLow, do tipo double, para armazenar os valores máximo e mínimo da faixa de preços anterior à sessão de Londres; PreLondonHighTime e PreLondonLowTime, do tipo datetime, para registrar os horários em que esses extremos foram formados; buyOrderTicket e sellOrderTicket, do tipo ulong, para armazenar os identificadores das ordens; panelVisible, inicializada como true, para controlar a exibição do painel; LondonRangePoints, inicializada como 0.0, para armazenar a amplitude atual da faixa de preços; positionList, um array de estruturas PositionInfo para monitorar as posições; lastCheckedDay, inicializada como 0, para controlar a verificação diária; noTradeToday e sessionChecksDone, inicializadas como false, para controlar as condições de negociação e as verificações da sessão; analysisTime, inicializada como 0, para registrar o horário da análise; dailyDrawdown, inicializada como 0.0, para acompanhar o drawdown diário; isTrailing, inicializada como false, para controlar o trailing stop; além das constantes PreLondonStartHour = 3 e PreLondonStartMinute - 0.

Com essas definições concluídas, começaremos pela criação do painel, que é a etapa mais simples, antes de avançarmos para a lógica de negociação, mais complexa. Primeiro, implementaremos as funções necessárias para criar o painel.

//+------------------------------------------------------------------+
//| Create a rectangle label for the panel background                |
//+------------------------------------------------------------------+
bool createRecLabel(string objName, int xD, int yD, int xS, int yS,
                    color clrBg, int widthBorder, color clrBorder = clrNONE,
                    ENUM_BORDER_TYPE borderType = BORDER_FLAT, ENUM_LINE_STYLE borderStyle = STYLE_SOLID) {
    ResetLastError();              //--- Reset last error
    if (!ObjectCreate(0, objName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { //--- Create rectangle label
        Print(__FUNCTION__, ": failed to create rec label! Error code = ", _LastError); //--- Log creation failure
        return false;              //--- Return failure
    }
    ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD); //--- Set x-distance
    ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD); //--- Set y-distance
    ObjectSetInteger(0, objName, OBJPROP_XSIZE, xS); //--- Set x-size
    ObjectSetInteger(0, objName, OBJPROP_YSIZE, yS); //--- Set y-size
    ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
    ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clrBg); //--- Set background color
    ObjectSetInteger(0, objName, OBJPROP_BORDER_TYPE, borderType); //--- Set border type
    ObjectSetInteger(0, objName, OBJPROP_STYLE, borderStyle); //--- Set border style
    ObjectSetInteger(0, objName, OBJPROP_WIDTH, widthBorder); //--- Set border width
    ObjectSetInteger(0, objName, OBJPROP_COLOR, clrBorder); //--- Set border color
    ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Set foreground
    ObjectSetInteger(0, objName, OBJPROP_STATE, false); //--- Set state
    ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); //--- Disable selectable
    ObjectSetInteger(0, objName, OBJPROP_SELECTED, false); //--- Disable selected
    ChartRedraw(0);                //--- Redraw chart
    return true;                   //--- Return success
}

//+------------------------------------------------------------------+
//| Create a text label for panel elements                           |
//+------------------------------------------------------------------+
bool createLabel(string objName, int xD, int yD,
                 string txt, color clrTxt = clrBlack, int fontSize = 10,
                 string font = "Arial") {
    ResetLastError();              //--- Reset last error
    if (!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) { //--- Create label
        Print(__FUNCTION__, ": failed to create the label! Error code = ", _LastError); //--- Log creation failure
        return false;              //--- Return failure
    }
    ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD); //--- Set x-distance
    ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD); //--- Set y-distance
    ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
    ObjectSetString(0, objName, OBJPROP_TEXT, txt); //--- Set text
    ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt); //--- Set color
    ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize); //--- Set font size
    ObjectSetString(0, objName, OBJPROP_FONT, font); //--- Set font
    ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Set foreground
    ObjectSetInteger(0, objName, OBJPROP_STATE, false); //--- Set state
    ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); //--- Disable selectable
    ObjectSetInteger(0, objName, OBJPROP_SELECTED, false); //--- Disable selected
    ChartRedraw(0);                //--- Redraw chart
    return true;                   //--- Return success
}

Nesta etapa, implementaremos funções auxiliares para criar os elementos da interface do painel de controle, utilizando rótulos retangulares para o fundo e rótulos de texto para exibir as informações. Primeiro, criamos a função createRecLabel, responsável pelos rótulos retangulares que compõem o fundo do painel, recebendo os parâmetros necessários. Redefinimos o código do último erro com ResetLastError e utilizamos ObjectCreate para criar um objeto do tipo OBJ_RECTANGLE_LABEL. Se a criação falhar, exibimos o erro com Print e retornamos false. Em seguida, configuramos OBJPROP_XDISTANCE e as demais propriedades de valores inteiros com ObjectSetInteger, forçamos o redesenho do gráfico com ChartRedraw e retornamos true.

Depois, criamos a função createLabel para os rótulos de texto do painel, com os parâmetros objName, xD, yD, txt, clrTxt, fontSize e font. Redefinimos o código do último erro com ResetLastError e utilizamos ObjectCreate para criar um objeto do tipo OBJ_LABEL. Em caso de falha, exibimos o erro com Print e retornamos false. As propriedades de valores inteiros são configuradas com ObjectSetInteger, como na função dos rótulos retangulares, mas também utilizamos ObjectSetString para definir OBJPROP_TEXT e OBJPROP_FONT. Por fim, forçamos o redesenho do gráfico com ChartRedraw e retornamos true. Essas funções permitirão criar um painel de controle dinâmico para monitorar os dados da sessão e o estado do programa. Agora podemos utilizá-las para criar e atualizar o painel.

string panelPrefix = "LondonPanel_"; //--- Prefix for panel objects

//+------------------------------------------------------------------+
//| Create the information panel                                     |
//+------------------------------------------------------------------+
void CreatePanel() {
   createRecLabel(panelPrefix + "Background", 10, 10, 270, 200, clrMidnightBlue, 1, clrSilver); //--- Create background
   createLabel(panelPrefix + "Title", 20, 15, "London Breakout Control Center", clrGold, 12); //--- Create title
   createLabel(panelPrefix + "RangePoints", 20, 40, "Range (points): ", clrWhite, 10); //--- Create range label
   createLabel(panelPrefix + "HighPrice", 20, 60, "High Price: ", clrWhite); //--- Create high price label
   createLabel(panelPrefix + "LowPrice", 20, 80, "Low Price: ", clrWhite); //--- Create low price label
   createLabel(panelPrefix + "BuyLevel", 20, 100, "Buy Level: ", clrWhite); //--- Create buy level label
   createLabel(panelPrefix + "SellLevel", 20, 120, "Sell Level: ", clrWhite); //--- Create sell level label
   createLabel(panelPrefix + "AccountBalance", 20, 140, "Balance: ", clrWhite); //--- Create balance label
   createLabel(panelPrefix + "AccountEquity", 20, 160, "Equity: ", clrWhite); //--- Create equity label
   createLabel(panelPrefix + "CurrentDrawdown", 20, 180, "Drawdown (%): ", clrWhite); //--- Create drawdown label
   createRecLabel(panelPrefix + "Hide", 250, 10, 30, 22, clrCrimson, 1, clrNONE); //--- Create hide button
   createLabel(panelPrefix + "HideText", 258, 12, CharToString(251), clrWhite, 13, "Wingdings"); //--- Create hide text
   ObjectSetInteger(0, panelPrefix + "Hide", OBJPROP_SELECTABLE, true); //--- Make hide selectable
   ObjectSetInteger(0, panelPrefix + "Hide", OBJPROP_STATE, true); //--- Set hide state
}

//+------------------------------------------------------------------+
//| Update panel with current data                                   |
//+------------------------------------------------------------------+
void UpdatePanel() {
   string rangeText = "Range (points): " + (LondonRangePoints > 0 ? DoubleToString(LondonRangePoints, 0) : "Calculating..."); //--- Format range text
   ObjectSetString(0, panelPrefix + "RangePoints", OBJPROP_TEXT, rangeText); //--- Update range text
   
   string highText = "High Price: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonHigh, _Digits) : "N/A"); //--- Format high text
   ObjectSetString(0, panelPrefix + "HighPrice", OBJPROP_TEXT, highText); //--- Update high text
   
   string lowText = "Low Price: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonLow, _Digits) : "N/A"); //--- Format low text
   ObjectSetString(0, panelPrefix + "LowPrice", OBJPROP_TEXT, lowText); //--- Update low text
   
   string buyText = "Buy Level: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonHigh + OrderOffsetPoints * _Point, _Digits) : "N/A"); //--- Format buy text
   ObjectSetString(0, panelPrefix + "BuyLevel", OBJPROP_TEXT, buyText); //--- Update buy text
   
   string sellText = "Sell Level: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonLow - OrderOffsetPoints * _Point, _Digits) : "N/A"); //--- Format sell text
   ObjectSetString(0, panelPrefix + "SellLevel", OBJPROP_TEXT, sellText); //--- Update sell text
   
   string balanceText = "Balance: " + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2); //--- Format balance text
   ObjectSetString(0, panelPrefix + "AccountBalance", OBJPROP_TEXT, balanceText); //--- Update balance text
   
   string equityText = "Equity: " + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2); //--- Format equity text
   ObjectSetString(0, panelPrefix + "AccountEquity", OBJPROP_TEXT, equityText); //--- Update equity text
   
   string ddText = "Drawdown (%): " + DoubleToString(dailyDrawdown, 2); //--- Format drawdown text
   ObjectSetString(0, panelPrefix + "CurrentDrawdown", OBJPROP_TEXT, ddText); //--- Update drawdown text
   ObjectSetInteger(0, panelPrefix + "CurrentDrawdown", OBJPROP_COLOR, dailyDrawdown > MaxDailyDrawdownPercent / 2 ? clrYellow : clrWhite); //--- Set drawdown color
}

Definimos a string panelPrefix com o valor LondonPanel_ para adicionar um prefixo aos nomes de todos os objetos do painel, facilitando a identificação de seus elementos. Criamos a função CreatePanel para montar a interface do painel informativo. Chamamos createRecLabel com panelPrefix + Background para criar o fundo do painel na posição 10,10, com dimensões de 270x200, cor clrMidnightBlue, borda prateada e espessura de 1. Em seguida, utilizamos createLabel para adicionar o título London Breakout Control Center na posição 20,15, em dourado e com tamanho de fonte 12. Também adicionamos rótulos para a amplitude da faixa de preços, o preço máximo, o preço mínimo, o nível de compra, o nível de venda, o saldo, o patrimônio líquido (equity) e o drawdown, nas respectivas posições, em branco e com tamanho de fonte 10.

Para criar o botão de ocultação, chamamos createRecLabel com panelPrefix + Hide na posição 250,10, com dimensões de 30x22 e fundo clrCrimson. Depois, utilizamos createLabel com panelPrefix + HideText, passando CharToString(251) da fonte Wingdings, na posição 258,12, com cor clrWhite e tamanho de fonte 13. Definimos OBJPROP_SELECTABLE e OBJPROP_STATE como true por meio de ObjectSetInteger para tornar o botão interativo. A escolha do código Wingdings depende de suas preferências visuais. A seguir, apresentamos a lista de códigos disponíveis.

CÓDIGOS WINGDINGS

Em seguida, implementamos a função UpdatePanel para atualizar as informações exibidas no painel. Formatamos rangeText com o valor de LondonRangePoints por meio de DoubleToString ou exibimos Calculating... quando o valor é zero. Depois, atualizamos o texto do objeto panelPrefix + RangePoints com ObjectSetString. Da mesma forma, formatamos e atualizamos os valores do preço máximo, do preço mínimo, do nível de compra (somando a PreLondonHigh o produto de OrderOffsetPoints por _Point), do nível de venda (subtraindo de PreLondonLow o produto de OrderOffsetPoints por _Point), do saldo obtido por AccountInfoDouble(ACCOUNT_BALANCE), do patrimônio líquido obtido por AccountInfoDouble(ACCOUNT_EQUITY) e do drawdown registrado em dailyDrawdown.

A cor do drawdown é definida com ObjectSetInteger: amarelo quando o drawdown diário ultrapassa MaxDailyDrawdownPercent / 2 e branco nos demais casos. Para colocar essas funções em funcionamento, chamamos cada uma delas na função de inicialização, conforme mostrado a seguir.

//+------------------------------------------------------------------+
//| Initialize EA                                                    |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(MagicNumber); //--- Set magic number
   ArrayFree(positionList);           //--- Free position list
   CreatePanel();                     //--- Create panel
   panelVisible = true;               //--- Set panel visible
   return(INIT_SUCCEEDED);            //--- Return success
}

//+------------------------------------------------------------------+
//| Deinitialize EA                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ObjectsDeleteAll(0, "LondonPanel_"); //--- Delete panel objects
   ArrayFree(positionList);           //--- Free position list
}

No manipulador de eventos OnInit, configuramos o magic number por meio do objeto de negociação, utilizamos ArrayFree para limpar o array de posições, chamamos CreatePanel para criar o painel e definimos o flag de visibilidade como true após sua criação. Em seguida, no manipulador de eventos OnDeinit, utilizamos ObjectsDeleteAll para excluir todos os objetos com o prefixo especificado e liberamos o array de posições, pois ele não será mais necessário. Após a compilação, obtemos o seguinte resultado.

PAINEL INICIAL

Com o painel criado, adicionaremos interatividade ao botão de fechamento para que, ao ser clicado, ele remova o painel do gráfico. Implementaremos esse comportamento no manipulador de eventos OnChartEvent.

//+------------------------------------------------------------------+
//| Handle chart events (e.g., panel close)                          |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if (id == CHARTEVENT_OBJECT_CLICK && sparam == panelPrefix + "Hide") { //--- Check hide click
      panelVisible = false;           //--- Set panel hidden
      ObjectsDeleteAll(0, "LondonPanel_"); //--- Delete panel objects
      ChartRedraw(0);                 //--- Redraw chart
   }
}

No manipulador OnChartEvent, verificamos se o identificador do evento corresponde a um clique em objeto e se o objeto clicado é o botão de ocultação ou fechamento. Em caso afirmativo, definimos o flag de visibilidade como false, excluímos os objetos do painel e forçamos o redesenho do gráfico para aplicar as alterações. Após a compilação, obtemos o seguinte resultado.

Fechamento do painel

A animação mostra que o painel está completamente pronto. Agora vamos atualizar o painel para inicializar todos os seus elementos. Para isso, precisaremos de funções que definam as faixas de preços diárias e calculem o drawdown.

//+------------------------------------------------------------------+
//| Check if it's a new trading day                                  |
//+------------------------------------------------------------------+
bool IsNewDay(datetime currentBarTime) {
   MqlDateTime barTime;               //--- Bar time structure
   TimeToStruct(currentBarTime, barTime); //--- Convert time
   datetime currentDay = StringToTime(StringFormat("%04d.%02d.%02d", barTime.year, barTime.mon, barTime.day)); //--- Get current day
   if (currentDay != lastCheckedDay) { //--- Check new day
      lastCheckedDay = currentDay;    //--- Update last day
      sessionChecksDone = false;      //--- Reset checks
      noTradeToday = false;           //--- Reset no trade
      buyOrderTicket = 0;             //--- Reset buy ticket
      sellOrderTicket = 0;            //--- Reset sell ticket
      LondonRangePoints = 0.0;        //--- Reset range
      return true;                    //--- Return new day
   }
   return false;                      //--- Return not new day
}

//+------------------------------------------------------------------+
//| Update daily drawdown                                            |
//+------------------------------------------------------------------+
void UpdateDailyDrawdown() {
   static double maxEquity = 0.0;     //--- Max equity tracker
   double equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Get equity
   if (equity > maxEquity) maxEquity = equity; //--- Update max equity
   dailyDrawdown = (maxEquity - equity) / maxEquity * 100; //--- Calculate drawdown
   if (dailyDrawdown >= MaxDailyDrawdownPercent) noTradeToday = true; //--- Set no trade if exceeded
}

Implementamos a função IsNewDay para detectar o início de um novo dia de negociação. Criamos a estrutura MqlDateTime barTime e utilizamos TimeToStruct para converter currentBarTime nessa estrutura. Em seguida, calculamos currentDay com StringToTime e StringFormat, utilizando os campos barTime.year, barTime.mon e barTime.day. Se currentDay for diferente de lastCheckedDay, atualizamos lastCheckedDay, redefinimos sessionChecksDone e noTradeToday como false, zeramos buyOrderTicket e sellOrderTicket, definimos LondonRangePoints como 0.0 e retornamos true. Caso contrário, retornamos false. Essa função permite reinicializar diariamente os parâmetros de análise da sessão e os flags de controle da negociação.

Em seguida, implementamos a função UpdateDailyDrawdown para monitorar o risco diário. Utilizamos uma variável estática maxEquity, inicializada como 0.0, para acompanhar o maior valor registrado do patrimônio líquido (equity). Obtemos o valor atual com AccountInfoDouble(ACCOUNT_EQUITY) e atualizamos maxEquity sempre que o patrimônio líquido ultrapassa o máximo anterior. Depois, calculamos dailyDrawdown como a queda percentual em relação a maxEquity. Se dailyDrawdown atingir ou ultrapassar MaxDailyDrawdownPercent, definimos noTradeToday como true, impedindo novas operações para proteger o capital contra o aumento do drawdown. Para utilizar essas funções, nós as chamamos no manipulador OnTick, juntamente com a atualização dos dados do painel.

//+------------------------------------------------------------------+
//| Main tick handler                                                |
//+------------------------------------------------------------------+
void OnTick() {
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   IsNewDay(currentBarTime);          //--- Check new day
   
   UpdatePanel();                     //--- Update panel
   UpdateDailyDrawdown();             //--- Update drawdown
}

Aqui, apenas chamamos as funções responsáveis por detectar um novo dia, reinicializar os parâmetros da faixa de preços, atualizar o painel e calcular o drawdown diário. Ao executar o programa, obtemos o seguinte resultado.

PAINEL INICIALIZADO

A imagem mostra que o painel agora está preenchido com dados atualizados e exibe o estado atual do sistema. Vamos avançar para uma lógica mais complexa: a identificação das faixas de preços da sessão. Primeiro, verificaremos as condições de negociação e posicionaremos as ordens quando forem atendidas. Depois, implementaremos o gerenciamento das posições. Para isso, precisaremos de funções para calcular a faixa de preços, representá-la no gráfico e posicionar as ordens. A seguir, apresentamos a lógica de implementação.

//+------------------------------------------------------------------+
//| Fixed lot size                                                   |
//+------------------------------------------------------------------+
double CalculateLotSize(double entryPrice, double stopLossPrice) {
   return NormalizeDouble(inpTradeLotsize, 2); //--- Normalize lot size
}

//+------------------------------------------------------------------+
//| Calculate session range (high-low) in points                     |
//+------------------------------------------------------------------+
double GetRange(datetime startTime, datetime endTime, double &highVal, double &lowVal, datetime &highTime, datetime &lowTime) {
   int startBar = iBarShift(_Symbol, _Period, startTime, true); //--- Get start bar
   int endBar = iBarShift(_Symbol, _Period, endTime, true); //--- Get end bar
   if (startBar == -1 || endBar == -1 || startBar < endBar) return -1; //--- Invalid bars

   int highestBar = iHighest(_Symbol, _Period, MODE_HIGH, startBar - endBar + 1, endBar); //--- Get highest bar
   int lowestBar = iLowest(_Symbol, _Period, MODE_LOW, startBar - endBar + 1, endBar); //--- Get lowest bar
   highVal = iHigh(_Symbol, _Period, highestBar); //--- Set high value
   lowVal = iLow(_Symbol, _Period, lowestBar); //--- Set low value
   highTime = iTime(_Symbol, _Period, highestBar); //--- Set high time
   lowTime = iTime(_Symbol, _Period, lowestBar); //--- Set low time
   return (highVal - lowVal) / _Point; //--- Return range in points
}

//+------------------------------------------------------------------+
//| Place pending buy/sell stop orders                               |
//+------------------------------------------------------------------+
void PlacePendingOrders(double preLondonHigh, double preLondonLow, datetime sessionID) {
   double buyPrice = preLondonHigh + OrderOffsetPoints * _Point; //--- Calculate buy price
   double sellPrice = preLondonLow - OrderOffsetPoints * _Point; //--- Calculate sell price
   double slPoints = StopLossPoints; //--- Set SL points
   double buySL = buyPrice - slPoints * _Point; //--- Calculate buy SL
   double sellSL = sellPrice + slPoints * _Point; //--- Calculate sell SL
   double tpPoints = slPoints * RRRatio; //--- Calculate TP points
   double buyTP = buyPrice + tpPoints * _Point; //--- Calculate buy TP
   double sellTP = sellPrice - tpPoints * _Point; //--- Calculate sell TP
   double lotSizeBuy = CalculateLotSize(buyPrice, buySL); //--- Calculate buy lot
   double lotSizeSell = CalculateLotSize(sellPrice, sellSL); //--- Calculate sell lot

   if (TradeType == TRADE_ALL || TradeType == TRADE_BUY_ONLY) { //--- Check buy trade
      obj_Trade.BuyStop(lotSizeBuy, buyPrice, _Symbol, buySL, buyTP, 0, 0, "Buy Stop - London"); //--- Place buy stop
      buyOrderTicket = obj_Trade.ResultOrder(); //--- Get buy ticket
   }

   if (TradeType == TRADE_ALL || TradeType == TRADE_SELL_ONLY) { //--- Check sell trade
      obj_Trade.SellStop(lotSizeSell, sellPrice, _Symbol, sellSL, sellTP, 0, 0, "Sell Stop - London"); //--- Place sell stop
      sellOrderTicket = obj_Trade.ResultOrder(); //--- Get sell ticket
   }
}

//+------------------------------------------------------------------+
//| Draw session ranges on the chart                                 |
//+------------------------------------------------------------------+
void DrawSessionRanges(datetime preLondonStart, datetime londonEnd) {
   string sessionID = "Sess_" + IntegerToString(lastCheckedDay); //--- Session ID

   string preRectName = "PreRect_" + sessionID; //--- Rectangle name
   ObjectCreate(0, preRectName, OBJ_RECTANGLE, 0, PreLondonHighTime, PreLondonHigh, PreLondonLowTime, PreLondonLow); //--- Create rectangle
   ObjectSetInteger(0, preRectName, OBJPROP_COLOR, clrTeal); //--- Set color
   ObjectSetInteger(0, preRectName, OBJPROP_FILL, true); //--- Enable fill
   ObjectSetInteger(0, preRectName, OBJPROP_BACK, true); //--- Set background

   string preTopLineName = "PreTopLine_" + sessionID; //--- Top line name
   ObjectCreate(0, preTopLineName, OBJ_TREND, 0, preLondonStart, PreLondonHigh, londonEnd, PreLondonHigh); //--- Create top line
   ObjectSetInteger(0, preTopLineName, OBJPROP_COLOR, clrBlack); //--- Set color
   ObjectSetInteger(0, preTopLineName, OBJPROP_WIDTH, 1); //--- Set width
   ObjectSetInteger(0, preTopLineName, OBJPROP_RAY_RIGHT, false); //--- Disable ray
   ObjectSetInteger(0, preTopLineName, OBJPROP_BACK, true); //--- Set background

   string preBotLineName = "PreBottomLine_" + sessionID; //--- Bottom line name
   ObjectCreate(0, preBotLineName, OBJ_TREND, 0, preLondonStart, PreLondonLow, londonEnd, PreLondonLow); //--- Create bottom line
   ObjectSetInteger(0, preBotLineName, OBJPROP_COLOR, clrRed); //--- Set color
   ObjectSetInteger(0, preBotLineName, OBJPROP_WIDTH, 1); //--- Set width
   ObjectSetInteger(0, preBotLineName, OBJPROP_RAY_RIGHT, false); //--- Disable ray
   ObjectSetInteger(0, preBotLineName, OBJPROP_BACK, true); //--- Set background
}

Para calcular a faixa de preços, posicionar as ordens e desenhar os elementos no gráfico, começamos pela função CalculateLotSize, que determina um tamanho de lote fixo. Passamos os parâmetros entryPrice e stopLossPrice, que não são utilizados no cálculo com lote fixo. A função retorna inpTradeLotsize, normalizado com NormalizeDouble para duas casas decimais, garantindo o mesmo tamanho de lote em todas as operações. Você pode ajustar o número de casas decimais de acordo com o tipo de conta.

Em seguida, criamos a função GetRange para calcular a faixa de preços anterior à sessão de Londres. Obtemos com iBarShift os índices startBar e endBar correspondentes a startTime e endTime e retornamos -1 se os valores forem inválidos ou se startBar < endBar. Identificamos a barra de máxima highestBar no intervalo especificado com iHighest e MODE_HIGH, e a barra de mínima lowestBar com iLowest e MODE_LOW. Obtemos highVal com iHigh para highestBar, lowVal com iLow para lowestBar, highTime com iTime para highestBar e lowTime com iTime para lowestBar. Por fim, retornamos a amplitude da faixa de preços, em pontos, calculada como (highVal - lowVal) / _Point.

Depois, definimos a função PlacePendingOrders para posicionar as ordens pendentes Buy Stop e Sell Stop. Calculamos o preço de compra buyPrice somando a preLondonHigh o produto de OrderOffsetPoints por _Point, e o preço de venda sellPrice subtraindo de preLondonLow esse mesmo deslocamento. Definimos slPoints como StopLossPoints, buySL como buyPrice menos slPoints multiplicado por _Point e sellSL como sellPrice mais slPoints multiplicado por _Point. Em seguida, calculamos tpPoints como slPoints multiplicado por RRRatio, buyTP como buyPrice mais tpPoints multiplicado por _Point e sellTP como sellPrice menos tpPoints multiplicado por _Point. Calculamos os tamanhos de lote lotSizeBuy e lotSizeSell com CalculateLotSize.

Se TradeType for TRADE_ALL ou TRADE_BUY_ONLY, posicionamos uma ordem Buy Stop por meio de obj_Trade.BuyStop, passando lotSizeBuy, buyPrice, buySL, buyTP e o comentário "Buy Stop - London". Armazenamos o ticket da ordem em buyOrderTicket, obtido por ResultOrder. Da mesma forma, posicionamos uma ordem Sell Stop se TradeType for TRADE_ALL ou TRADE_SELL_ONLY.

Por fim, implementamos a função DrawSessionRanges para representar visualmente a sessão no gráfico. Criamos sessionID concatenando o prefixo "Sess_" ao valor de lastCheckedDay, convertido em string com IntegerToString. Para o retângulo identificado por preRectName = "PreRect_" + sessionID, utilizamos ObjectCreate com o tipo OBJ_RECTANGLE, definindo os pontos de referência de PreLondonHighTime, PreLondonHigh até PreLondonLowTime, PreLondonLow. Configuramos OBJPROP_COLOR como clrTeal, OBJPROP_FILL como true para preencher o retângulo e OBJPROP_BACK como true para exibi-lo ao fundo do gráfico.

Para a linha superior, cujo nome é armazenado em preTopLineName e formado pela concatenação de "PreTopLine_" com sessionID, criamos um objeto OBJ_TREND que vai de preLondonStart, PreLondonHigh até londonEnd, PreLondonHigh. Configuramos OBJPROP_COLOR como clrBlack, OBJPROP_WIDTH como 1, OBJPROP_RAY_RIGHT como false e OBJPROP_BACK como true. Da mesma forma, criamos a linha inferior preBotLineName, concatenando "PreBottomLine_" com sessionID, de preLondonStart, PreLondonLow até londonEnd, PreLondonLow, utilizando a cor vermelha. Com essas funções implementadas, podemos definir a função responsável por verificar as condições de negociação.

//+------------------------------------------------------------------+
//| Check trading conditions and place orders                        |
//+------------------------------------------------------------------+
void CheckTradingConditions(datetime currentTime) {
   MqlDateTime timeStruct;            //--- Time structure
   TimeToStruct(currentTime, timeStruct); //--- Convert time
   datetime today = StringToTime(StringFormat("%04d.%02d.%02d", timeStruct.year, timeStruct.mon, timeStruct.day)); //--- Get today

   datetime preLondonStart = today + PreLondonStartHour * 3600 + PreLondonStartMinute * 60; //--- Pre-London start
   datetime londonStart = today + LondonStartHour * 3600 + LondonStartMinute * 60; //--- London start
   datetime londonEnd = today + LondonEndHour * 3600 + LondonEndMinute * 60; //--- London end
   analysisTime = londonStart;        //--- Set analysis time

   if (currentTime < analysisTime) return; //--- Exit if before analysis

   double preLondonRange = GetRange(preLondonStart, currentTime, PreLondonHigh, PreLondonLow, PreLondonHighTime, PreLondonLowTime); //--- Get range
   if (preLondonRange < MinRangePoints || preLondonRange > MaxRangePoints) { //--- Check range limits
      noTradeToday = true;            //--- Set no trade
      sessionChecksDone = true;       //--- Set checks done
      DrawSessionRanges(preLondonStart, londonEnd); //--- Draw ranges
      return;                         //--- Exit
   }

   LondonRangePoints = preLondonRange; //--- Set range points
   PlacePendingOrders(PreLondonHigh, PreLondonLow, today); //--- Place orders
   noTradeToday = true;               //--- Set no trade
   sessionChecksDone = true;          //--- Set checks done
   DrawSessionRanges(preLondonStart, londonEnd); //--- Draw ranges
}

Implementamos a função CheckTradingConditions para avaliar as condições da sessão e posicionar as ordens em nosso sistema de rompimento da sessão de Londres. Criamos a estrutura MqlDateTime timeStruct e utilizamos TimeToStruct para converter o horário atual nessa estrutura. Em seguida, calculamos today com StringToTime e StringFormat, utilizando os campos timeStruct.year, timeStruct.mon e timeStruct.day. Definimos preLondonStart somando a today os valores de PreLondonStartHour e PreLondonStartMinute convertidos em segundos. Calculamos londonStart da mesma forma, utilizando LondonStartHour e LondonStartMinute, e londonEnd com LondonEndHour e LondonEndMinute. Atribuímos londonStart a analysisTime e encerramos a execução da função se o horário atual for anterior a esse valor.

Obtemos preLondonRange com a função GetRange, passando preLondonStart, currentTime e as referências a PreLondonHigh, PreLondonLow, PreLondonHighTime e PreLondonLowTime. Se preLondonRange for menor que MinRangePoints ou maior que MaxRangePoints, definimos os flags noTradeToday e sessionChecksDone como true, chamamos DrawSessionRanges com preLondonStart e londonEnd e encerramos a execução da função. Caso contrário, atribuímos preLondonRange a LondonRangePoints, chamamos PlacePendingOrders com PreLondonHigh, PreLondonLow e today, definimos noTradeToday e sessionChecksDone como true e chamamos DrawSessionRanges. Dessa forma, as ordens são posicionadas apenas quando a amplitude da faixa de preços atende aos limites estabelecidos. Agora podemos chamar essa função no manipulador OnTick para gerar os sinais de negociação.

if (!noTradeToday && !sessionChecksDone) { //--- Check trading conditions
   CheckTradingConditions(TimeCurrent()); //--- Check conditions
}

Se ainda não houver operações no dia atual e a verificação da sessão não tiver sido realizada, chamamos a função de verificação das condições, passando o horário atual. Ao executar o programa, obtemos o seguinte resultado.

FAIXAS DE PREÇOS DEFINIDAS

Podemos observar que as faixas de preços foram definidas e as ordens pendentes foram posicionadas. A amplitude da faixa de preços é de 100 pontos, atendendo às nossas condições de negociação. Agora podemos avançar para o gerenciamento das operações, mas primeiro adicionaremos uma verificação para excluir a ordem pendente oposta quando uma delas for ativada. Também incluiremos as posições na lista para gerenciá-las posteriormente.

//+------------------------------------------------------------------+
//| Delete opposite pending order when one is filled                 |
//+------------------------------------------------------------------+
void CheckAndDeleteOppositeOrder() {
   if (!DeleteOppositeOrder || TradeType != TRADE_ALL) return; //--- Exit if not applicable

   bool buyOrderExists = false;       //--- Buy exists flag
   bool sellOrderExists = false;      //--- Sell exists flag

   for (int i = OrdersTotal() - 1; i >= 0; i--) { //--- Iterate through orders
      ulong orderTicket = OrderGetTicket(i); //--- Get ticket
      if (OrderSelect(orderTicket)) { //--- Select order
         if (OrderGetString(ORDER_SYMBOL) == _Symbol && OrderGetInteger(ORDER_MAGIC) == MagicNumber) { //--- Check symbol and magic
            if (orderTicket == buyOrderTicket) buyOrderExists = true; //--- Set buy exists
            if (orderTicket == sellOrderTicket) sellOrderExists = true; //--- Set sell exists
         }
      }
   }

   if (!buyOrderExists && sellOrderExists && sellOrderTicket != 0) { //--- Check delete sell
      obj_Trade.OrderDelete(sellOrderTicket); //--- Delete sell order
   } else if (!sellOrderExists && buyOrderExists && buyOrderTicket != 0) { //--- Check delete buy
      obj_Trade.OrderDelete(buyOrderTicket); //--- Delete buy order
   }
}

//+------------------------------------------------------------------+
//| Add position to tracking list when opened                        |
//+------------------------------------------------------------------+
void AddPositionToList(ulong ticket, double openPrice, double londonRange, datetime sessionID) {
   if (londonRange <= 0) return;      //--- Exit if invalid range
   int index = ArraySize(positionList); //--- Get current size
   ArrayResize(positionList, index + 1); //--- Resize array
   positionList[index].ticket = ticket; //--- Set ticket
   positionList[index].openPrice = openPrice; //--- Set open price
   positionList[index].londonRange = londonRange; //--- Set range
   positionList[index].sessionID = sessionID; //--- Set session ID
   positionList[index].trailingActive = false; //--- Set trailing inactive
}

Começamos pela função CheckAndDeleteOppositeOrder, responsável por excluir a ordem pendente oposta quando uma das ordens for ativada. Encerramos imediatamente a execução da função se DeleteOppositeOrder for false ou se TradeType for diferente de TRADE_ALL. Inicializamos as variáveis buyOrderExists e sellOrderExists como false. Em seguida, percorremos as ordens em ordem inversa, utilizando OrdersTotal e OrderGetTicket, e selecionamos cada uma com OrderSelect. Verificamos se a ordem pertence ao símbolo _Symbol e possui o MagicNumber correspondente, utilizando OrderGetString e OrderGetInteger. Se essas condições forem atendidas, definimos buyOrderExists ou sellOrderExists como true, conforme o ticket da ordem corresponda a buyOrderTicket ou sellOrderTicket.

Se a ordem de compra não existir, mas a ordem de venda ainda estiver pendente, excluímos sellOrderTicket com obj_Trade.OrderDelete. Da mesma forma, se a ordem de venda não existir e a ordem de compra ainda estiver pendente, excluímos a ordem correspondente. Essa função garante que, após a ativação de uma das ordens, apenas uma direção de negociação permaneça ativa.

Em seguida, criamos a função AddPositionToList para monitorar as posições abertas. Encerramos a execução da função se londonRange <= 0, pois a faixa de preços ainda não foi definida. Obtemos o índice do próximo elemento com ArraySize aplicado ao array positionList, aumentamos o tamanho do array com ArrayResize para incluir um novo elemento e preenchemos os campos ticket, openPrice, londonRange e sessionID de positionList[index], definindo trailingActive como false. Dessa forma, mantemos uma lista de posições para gerenciar os trailing stops e acompanhar os dados da sessão. Agora podemos integrar essa lógica ao manipulador de eventos OnTick.

CheckAndDeleteOppositeOrder();     //--- Delete opposite order

// Add untracked positions
for (int i = 0; i < PositionsTotal(); i++) { //--- Iterate through positions
   ulong ticket = PositionGetTicket(i); //--- Get ticket
   if (PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) { //--- Check position
      bool tracked = false;        //--- Tracked flag
      for (int j = 0; j < ArraySize(positionList); j++) { //--- Check list
         if (positionList[j].ticket == ticket) tracked = true; //--- Set tracked
      }
      if (!tracked) {              //--- If not tracked
         double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get open price
         AddPositionToList(ticket, openPrice, LondonRangePoints, lastCheckedDay); //--- Add to list
      }
   }
}

Aqui, chamamos a função CheckAndDeleteOppositeOrder para gerenciar as ordens pendentes. Quando uma ordem é executada, a ordem oposta é excluída conforme a configuração de DeleteOppositeOrder, evitando que operações em direções opostas sejam abertas.

Em seguida, adicionamos ao array positionList as posições que ainda não estão sendo monitoradas, garantindo que todas as posições abertas relevantes sejam consideradas no gerenciamento dos trailing stops. Percorremos todas as posições com PositionsTotal e PositionGetTicket, obtendo o ticket de cada uma. Se PositionSelectByTicket for executada com sucesso e a posição corresponder ao símbolo _Symbol e ao MagicNumber configurado, conforme os valores obtidos com PositionGetString e PositionGetInteger, inicializamos o flag tracked como false. Depois, percorremos o array positionList com ArraySize e um laço interno para verificar se o ticket já está registrado em positionList[j].ticket. Se a posição ainda não estiver sendo monitorada, obtemos openPrice com PositionGetDouble(POSITION_PRICE_OPEN) e chamamos AddPositionToList, passando ticket, openPrice, LondonRangePoints e lastCheckedDay. Isso garante que cada posição que atende aos critérios seja adicionada à lista sem duplicações. A seguir, apresentamos o resultado.

EXCLUSÃO DA ORDEM PENDENTE

Como tudo está funcionando corretamente nesta etapa, podemos definir a função de gerenciamento das posições.

//+------------------------------------------------------------------+
//| Remove position from tracking list when closed                   |
//+------------------------------------------------------------------+
void RemovePositionFromList(ulong ticket) {
   for (int i = 0; i < ArraySize(positionList); i++) { //--- Iterate through list
      if (positionList[i].ticket == ticket) { //--- Match ticket
         for (int j = i; j < ArraySize(positionList) - 1; j++) { //--- Shift elements
            positionList[j] = positionList[j + 1]; //--- Copy next
         }
         ArrayResize(positionList, ArraySize(positionList) - 1); //--- Resize array
         break;                   //--- Exit loop
      }
   }
}

//+------------------------------------------------------------------+
//| Manage trailing stops                                            |
//+------------------------------------------------------------------+
void ManagePositions() {
   if (PositionsTotal() == 0 || !UseTrailing) return; //--- Exit if no positions or no trailing
   isTrailing = false;                //--- Reset trailing flag

   double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get bid
   double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Get ask
   double point = _Point;             //--- Get point value

   for (int i = 0; i < ArraySize(positionList); i++) { //--- Iterate through positions
      ulong ticket = positionList[i].ticket; //--- Get ticket
      if (!PositionSelectByTicket(ticket)) { //--- Select position
         RemovePositionFromList(ticket); //--- Remove if not selected
         continue;                    //--- Skip
      }

      if (PositionGetString(POSITION_SYMBOL) != _Symbol || PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip if not matching

      double openPrice = positionList[i].openPrice; //--- Get open price
      long positionType = PositionGetInteger(POSITION_TYPE); //--- Get type
      double currentPrice = (positionType == POSITION_TYPE_BUY) ? currentBid : currentAsk; //--- Get current price
      double profitPoints = (positionType == POSITION_TYPE_BUY) ? (currentPrice - openPrice) / point : (openPrice - currentPrice) / point; //--- Calculate profit points

      if (profitPoints >= MinProfitPoints + TrailingPoints) { //--- Check for trailing
         double newSL = 0.0;          //--- New SL variable
         if (positionType == POSITION_TYPE_BUY) { //--- Buy position
            newSL = currentPrice - TrailingPoints * point; //--- Calculate new SL
         } else {                     //--- Sell position
            newSL = currentPrice + TrailingPoints * point; //--- Calculate new SL
         }
         double currentSL = PositionGetDouble(POSITION_SL); //--- Get current SL
         if ((positionType == POSITION_TYPE_BUY && newSL > currentSL + point) || (positionType == POSITION_TYPE_SELL && newSL < currentSL - point)) { //--- Check move condition
            if (obj_Trade.PositionModify(ticket, NormalizeDouble(newSL, _Digits), PositionGetDouble(POSITION_TP))) { //--- Modify position
               positionList[i].trailingActive = true; //--- Set trailing active
               isTrailing = true;        //--- Set global trailing
            }
         }
      }
   }
}

Aqui, implementamos as funções para remover as posições fechadas da lista de monitoramento e gerenciar os trailing stops. Começamos pela função RemovePositionFromList, que recebe o parâmetro ticket e remove do array positionList a posição correspondente quando ela é fechada. Percorremos positionList utilizando ArraySize e, se positionList[i].ticket corresponder a ticket, deslocamos os elementos seguintes uma posição para trás por meio de um laço interno, copiando positionList[j + 1] para positionList[j]. Em seguida, reduzimos o tamanho do array com ArrayResize e encerramos o laço. Essa função mantém a lista atualizada e evita verificações desnecessárias de posições já fechadas, especialmente durante o gerenciamento dos trailing stops e após o fechamento das posições.

Em seguida, criamos a função ManagePositions para gerenciar os trailing stops das posições abertas. Encerramos imediatamente a execução da função se PositionsTotal for igual a 0 ou UseTrailing for false. Redefinimos isTrailing como false, obtemos currentBid e currentAsk com SymbolInfoDouble, utilizando SYMBOL_BID e SYMBOL_ASK, respectivamente, e atribuímos o valor de _Point à variável point. Percorremos o array positionList com ArraySize, obtemos o ticket de cada posição e tentamos selecioná-la com PositionSelectByTicket. Se a seleção falhar, chamamos RemovePositionFromList e continuamos para a próxima iteração. Se a posição não corresponder ao símbolo _Symbol ou ao MagicNumber configurado, conforme os valores obtidos com PositionGetString e PositionGetInteger, nós a ignoramos. Obtemos openPrice de positionList[i], identificamos o tipo da posição positionType com PositionGetInteger, determinamos currentPrice conforme o tipo de posição e calculamos profitPoints dividindo a diferença entre os preços por point.

Se profitPoints for maior ou igual a MinProfitPoints + TrailingPoints, calculamos o novo nível de stop loss newSL subtraindo de currentPrice o produto de TrailingPoints por point, no caso das posições de compra, ou somando esse valor a currentPrice, no caso das posições de venda. Obtemos o stop loss atual currentSL com PositionGetDouble e, se newSL representar uma melhoria de pelo menos um ponto em relação ao nível atual, modificamos a posição com obj_Trade.PositionModify, passando o valor normalizado de newSL e o take profit atual obtido com PositionGetDouble. Se a modificação for bem-sucedida, definimos positionList[i].trailingActive e isTrailing como true. Dessa forma, o stop loss acompanha dinamicamente os movimentos favoráveis do preço, protegendo os lucros e permitindo que as operações lucrativas continuem acumulando ganhos. Agora basta chamar essa função a cada tick para gerenciar as posições. Após a compilação, obtemos o seguinte resultado.

GERENCIAMENTO DAS POSIÇÕES

A imagem mostra que o sistema verifica as condições de negociação, posiciona as ordens e as gerencia de acordo com a estratégia. Agora resta testar o programa. Para isso, passaremos à próxima seção.


BACKTESTING

Após um backtesting detalhado, obtivemos os seguintes resultados.

Gráfico do backtesting:

Gráfico

Relatório do backtesting:

RELATÓRIO


Conclusão

Neste artigo, desenvolvemos um sistema de negociação por rompimento da sessão de Londres em MQL5. O sistema analisa a faixa de preços formada antes da abertura da sessão e posiciona ordens pendentes com relação risco/retorno configurável, trailing stops e limites para o número de operações. Também implementamos um painel de controle para monitorar as faixas de preços, os níveis de negociação e o drawdown em tempo real. Graças à sua estrutura modular, que inclui a estrutura PositionInfo, o programa oferece uma abordagem eficiente para negociar rompimentos, com possibilidade de otimização adicional por meio do ajuste dos parâmetros da sessão e de risco.

Atenção: todo o conteúdo deste artigo destina-se exclusivamente a fins educacionais e não deve ser utilizado para outras finalidades. A negociação nos mercados financeiros envolve riscos significativos, e a volatilidade pode provocar perdas. Antes de utilizar o programa em uma conta real, é fundamental realizar testes rigorosos e adotar práticas adequadas de gerenciamento de risco.

Você pode utilizar livremente os conceitos e a implementação apresentados para adaptar o sistema ao seu estilo de negociação ou aprimorar suas próprias estratégias algorítmicas. Boas operações!

Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/18867

Arquivos anexados |
Últimos Comentários | Ir para discussão (14)
Allan Munene Mutiiria
Allan Munene Mutiiria | 10 ago. 2025 em 22:20
Kyle Young Sangster Strategy Tester. Ele identifica a variação diária e desenha a caixa no gráfico. No entanto, ele não realiza uma operação todos os dias (embora se espere que o faça). Ao longo de um mês e meio, ele realizou apenas três operações.



O segundo problema é que os níveis de alta/baixa e os níveis de compra/venda no painel de controle não são atualizados.


Os níveis de alta e baixa estão claramente sendo identificados no gráfico, então imagino que os níveis de compra e venda também devam ser exibidos no gráfico e atualizados no painel de controle, já que são derivados diretamente dos níveis de alta e baixa.

Quais sugestões vocês têm para que isso funcione corretamente?

Agradeço desde já.

Quanto à sua segunda questão, o artigo explica isso, mas, supondo que o problema se deva a dados de teste insuficientes e para dar uma dica: quando a faixa está sendo calculada, você sempre verá o status “Calculando...” até que haja dados suficientes para definir a sessão da faixa de Londres ou qualquer outra sessão que você definir nas configurações. Supondo que você esteja usando as configurações padrão, com 3 horas pré-Londres, e que a data na captura de tela compartilhada seja 13 de fevereiro, duas barras após as 22h, o que equivale a 2 × 15 minutos = 30, o que resulta em 22h30, está fora do horário de cálculo do intervalo; portanto, os dados no painel devem continuar visíveis, já que o intervalo definido anteriormente ainda está em vigor, a menos que a primeira sessão ainda não tenha sido encontrada, e serão apagados assim que o cálculo do intervalo for concluído a partir da meia-noite. Veja abaixo:

const int PreLondonStartHour = 3; //--- Horário fixo de início antes de Londres
const int PreLondonStartMinute = 0; //--- Minuto fixo antes do início da corrida em Londres

Talvez seja necessário conferir a lógica abaixo para identificar o intervalo

//+------------------------------------------------------------------+
//| Verificar as condições de negociação e inserir ordens                        |
//+------------------------------------------------------------------+
void CheckTradingConditions(datetime currentTime) {
   MqlDateTime timeStruct;            //--- Estrutura temporal
   TimeToStruct(currentTime, timeStruct); //--- Converter a hora
   datetime today = StringToTime(StringFormat("%04d.%02d.%02d", timeStruct.year, timeStruct.mon, timeStruct.day)); //--- Obter a data de hoje

   datetime preLondonStart = today + PreLondonStartHour * 3600 + PreLondonStartMinute * 60; //--- Início antes de Londres
   datetime londonStart = today + LondonStartHour * 3600 + LondonStartMinute * 60; //--- Início em Londres
   datetime londonEnd = today + LondonEndHour * 3600 + LondonEndMinute * 60; //--- Fim da parte de Londres
   analysisTime = londonStart;        //--- Definir o tempo de análise

   if (currentTime < analysisTime) return; //--- Sair antes da análise

   double preLondonRange = GetRange(preLondonStart, currentTime, PreLondonHigh, PreLondonLow, PreLondonHighTime, PreLondonLowTime); //--- Obter intervalo
   if (preLondonRange < MinRangePoints || preLondonRange > MaxRangePoints) { //--- Verificar os limites do intervalo
      noTradeToday = true;            //--- Definir como “sem negociação”
      sessionChecksDone = true;       //--- Verificações concluídas
      DrawSessionRanges(preLondonStart, londonEnd); //--- Desenhar intervalos
      return;                         //--- Sair
   }

   LondonRangePoints = preLondonRange; //--- Definir pontos do intervalo
   PlacePendingOrders(PreLondonHigh, PreLondonLow, today); //--- Fazer pedidos
   noTradeToday = true;               //--- Definir como “sem negociação”
   sessionChecksDone = true;          //--- Verificações concluídas
   DrawSessionRanges(preLondonStart, londonEnd); //--- Desenhar intervalos
}

e como ele é definido.

//+------------------------------------------------------------------+
//| Atualizar o painel com os dados atuais                                   |
//+------------------------------------------------------------------+
void UpdatePanel() {
   string rangeText = "Range (points): " + (LondonRangePoints > 0 ? DoubleToString(LondonRangePoints, 0) : "Calculating..."); //--- Formatar o texto do intervalo
   ObjectSetString(0, panelPrefix + "RangePoints", OBJPROP_TEXT, rangeText); //--- Atualizar o texto do intervalo

   //---

}

Veja a imagem abaixo; embora não saibamos o ano do seu teste, vamos considerar 2025. Se for 2020, como no seu caso, não temos dados de qualidade para esse ano; de qualquer forma, usamos 2025 e, portanto, o cálculo do intervalo deve começar à meia-noite.

23h55

Na imagem, você pode ver que os dados às 23h55 ainda estão intactos. No entanto, quando chega a meia-noite, devemos reiniciar. Veja abaixo.

DADOS DA MEIA-NOITE 00:00

Você pode ver que reiniciamos os dados à meia-noite para o cálculo do outro intervalo. Na verdade, quando o cálculo do intervalo é concluído, a visualização pode ajudar você a entender o que realmente aconteceu. Por exemplo, no seu caso, usando as configurações padrão, veremos as barras de intervalo das 03h00 às 08h00, pois foi isso que definimos. Veja abaixo:

HORÁRIO DE FUNCIONAMENTO DO CAMPO DE TIRO

Espero que isso esclareça as coisas novamente. Você pode ajustar tudo de acordo com seu estilo de negociação. Para evitar os problemas que você está enfrentando, é recomendável usar dados de teste confiáveis. Obrigado.

Kyle Young Sangster
Kyle Young Sangster | 10 ago. 2025 em 22:34
Em que parte do código você pretendia usar a variável “MaxOpenTrades”? Ela está definida, mas nunca é referenciada.
Kyle Young Sangster
Kyle Young Sangster | 10 ago. 2025 em 22:52
Allan Munene Mutiiria #:

Quanto à sua segunda dúvida, o artigo explica isso, mas, supondo que o problema se deva a dados de teste insuficientes e para dar uma dica: quando o intervalo está sendo calculado, você sempre verá o status “Calculando...” até que haja dados suficientes para definir o intervalo da sessão de Londres ou qualquer outra sessão que você definir nas entradas. Supondo que você esteja usando as configurações padrão, com 3 horas antes da sessão de Londres, e que a data na captura de tela compartilhada seja 13 de fevereiro, duas barras após as 22h, o que equivale a 2 × 15 minutos = 30, o que resulta em 22h30, está fora do horário de cálculo do intervalo; portanto, os dados no painel devem continuar visíveis, já que o intervalo definido anteriormente ainda está em vigor, a menos que a primeira sessão ainda não tenha sido encontrada, e serão apagados assim que o cálculo do intervalo for concluído a partir da meia-noite. Veja abaixo:

Talvez seja necessário consultar a lógica abaixo para determinar o intervalo

e como ele é definido.

Veja a imagem abaixo; embora não saibamos o ano do seu teste, vamos considerar 2025. Se for 2020, como no seu caso, não temos dados de qualidade para esse ano; portanto, de qualquer forma, usaremos 2025 e, assim, o cálculo do intervalo deve começar à meia-noite.


Pela imagem, você pode ver que os dados às 23h55 ainda estão intactos. No entanto, quando chegar à meia-noite, devemos reiniciar. Veja abaixo.

Você pode ver que reiniciamos os dados à meia-noite para o cálculo do outro intervalo. Na verdade, quando o cálculo do intervalo é concluído, a visualização pode ajudar a entender o que realmente aconteceu. Por exemplo, no seu caso, usando as configurações padrão, veremos as barras de intervalo das 03h00 às 08h00, pois foi isso que definimos. Veja abaixo:

Espero que isso tenha esclarecido as coisas novamente. Você pode ajustar tudo de acordo com seu estilo de negociação. Para evitar os problemas que você está enfrentando, é recomendável usar dados de teste confiáveis. Obrigado.

Muito obrigado pela resposta completa.

Sim, eu li o artigo e segui as instruções para codificar minha própria versão até encontrar os problemas que descrevi. O que observei foi que o painel não atualizou, mesmo durante os horários padrão. Minha captura de tela tinha o objetivo de mostrar que, embora a caixa tivesse sido desenhada no gráfico e os dados tivessem sido coletados, o painel não havia sido atualizado. Além disso, não havia mensagens de erro nos logs relacionadas a preços ou níveis inválidos.

Adicionei mensagens de log à minha versão; a partir delas, percebo que o painel não é atualizado quando o intervalo é muito grande ou muito pequeno; então, isso pode ser parte do motivo.

Vou verificar novamente a qualidade dos dados de teste. E obrigado por indicar em qual par você fez o teste; certamente farei ajustes para os pares que escolhi.

Muito obrigado pela sua ajuda.

Allan Munene Mutiiria
Allan Munene Mutiiria | 10 ago. 2025 em 23:49
Kyle Young Sangster #:

Muito obrigado pela resposta detalhada.

Sim, eu li o artigo e segui as instruções para codificar minha própria versão até encontrar os problemas que descrevi. O que observei foi que o painel não atualizou, mesmo nos intervalos padrão. Minha captura de tela tinha o objetivo de mostrar que, embora a caixa tivesse sido desenhada no gráfico e os dados tivessem sido coletados, o painel não havia sido atualizado. Além disso, não havia mensagens de erro nos logs relacionadas a preços ou níveis inválidos.

Adicionei mensagens de log à minha versão; a partir delas, percebo que o painel não é atualizado quando o intervalo é muito grande ou muito pequeno; então, isso pode ser parte do motivo.

Vou verificar novamente a qualidade dos dados de teste. E obrigado por indicar em qual par você fez o teste; certamente farei ajustes para os pares que escolhi.

Muito obrigado pela sua ajuda.

Claro. De nada.

Torsten Busch
Torsten Busch | 11 nov. 2025 em 21:01

Obrigado por compartilhar seu código conosco.

Como eu mesmo já escrevi EAs dependentes de sessão, posso te dizer que o código só funciona se a sua corretora estiver sempre no fuso horário GMT+1 e também adotar o horário de verão britânico.

Em todos os outros casos, infelizmente, sua hora de início não está correta. Por quê? Porque a sessão de Londres começa às 8h, horário do Reino Unido. No inverno, são 8h GMT, e no verão, 7h GMT.

A função `TimeCurrent()` não retorna sua hora local, mas sempre a hora do servidor de negociação.

Introdução ao MQL5 (Parte 19): Automação da detecção de ondas de Wolfe Introdução ao MQL5 (Parte 19): Automação da detecção de ondas de Wolfe
Este artigo apresenta como identificar programaticamente padrões de ondas de Wolfe de alta e de baixa e negociar com base neles usando a linguagem MQL5. Veremos como detectar essas estruturas e executar operações a partir dos sinais identificados. Isso inclui localizar os principais pontos de swing, validar as regras do padrão e preparar o EA para operar de acordo com os sinais encontrados.
Recursos do Assistente MQL5 que você precisa conhecer (Parte 76): Uso de padrões do Awesome Oscillator e canais de envelopes com aprendizado supervisionado Recursos do Assistente MQL5 que você precisa conhecer (Parte 76): Uso de padrões do Awesome Oscillator e canais de envelopes com aprendizado supervisionado
Dando continuidade ao nosso artigo anterior sobre a combinação do indicador Awesome Oscillator com os canais de envelopes (Envelope Channels), vamos analisar como o aprendizado supervisionado pode aprimorar essa combinação. O Awesome Oscillator e os canais de envelopes são ferramentas complementares que permitem identificar tendências e estabelecer níveis de suporte e resistência. Nossa abordagem de aprendizado supervisionado utiliza uma rede neural convolucional (CNN), que emprega um kernel de produto escalar (Dot Product Kernel) com mecanismo de atenção temporal cruzada (Cross-Time-Attention) para determinar as dimensões de seus kernels e canais. Como de costume, essa implementação é feita em um arquivo personalizado que contém a classe de sinais (signal class), que se integra ao Assistente MQL5 para criar um EA.
Ferramentas de trading em MQL5 (Parte 6): Painel holográfico dinâmico com animação pulsante e controles Ferramentas de trading em MQL5 (Parte 6): Painel holográfico dinâmico com animação pulsante e controles
Neste artigo, vamos criar um painel holográfico dinâmico em MQL5 para monitorar ativos e períodos gráficos por meio do RSI, com alertas de volatilidade e opções de ordenação. Também vamos adicionar animações pulsantes, botões interativos e efeitos holográficos para tornar a ferramenta visualmente atraente e responsiva.
Machine Learning e Data Science (Parte 42): Previsão dos mercados acionários com N-BEATS em Python Machine Learning e Data Science (Parte 42): Previsão dos mercados acionários com N-BEATS em Python
N-BEATS é um modelo revolucionário de aprendizado profundo desenvolvido para a previsão de séries temporais. Ele foi criado com o objetivo de superar o desempenho dos modelos clássicos de previsão de séries temporais, como ARIMA, PROPHET, VAR, entre outros. Vamos conhecer esse modelo e explorar suas possibilidades de aplicação na previsão do mercado de ações.