
Automating Trading Strategies in MQL5 (Part 24): London Session Breakout System with Risk Management and Trailing Stops
Introduction
In our previous article (Part 23), we enhanced the Zone Recovery System for Envelopes Trend Trading in MetaQuotes Language 5 (MQL5) with trailing stops and multi-basket trading for better profit protection and signal handling. In Part 24, we develop a London Session Breakout System that identifies pre-session ranges, places pending orders, and incorporates risk management tools, including risk-to-reward ratios, drawdown limits, and a control panel for real-time monitoring. We will cover the following topics:
By the end, you’ll have a complete MQL5 breakout program with advanced risk controls, ready for testing and refinement—let’s dive in!
Understanding the London Session Breakout Strategy
The London Session Breakout Strategy targets the increased volatility during the London market open by identifying the price range formed in the pre-London hours and placing pending orders to capture breakouts from that range. This strategy is important because the London session often experiences high liquidity and price movements, offering reliable opportunities for profit; however, it requires careful risk management to avoid false breakouts and drawdowns.
We will achieve this by calculating the pre-London high and low to set buy and sell stop orders with offsets, incorporating risk-to-reward ratios for take-profits, trailing stops for profit locking, and limits on open trades and daily drawdown to protect capital. We plan to use a control panel for real-time monitoring and session-specific checks to ensure trades only occur within defined ranges, making the system adaptable to varying market conditions. In a nutshell, here is a representation of the system we want to achieve.
Implementation in MQL5
To create the program in MQL5, open the MetaEditor, go to the Navigator, locate the Indicators folder, click on the "New" tab, and follow the prompts to create the file. Once it is made, in the coding environment, we will start by declaring some inputs and structures that will make the program more dynamic.
//+------------------------------------------------------------------+ //| 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 };
We begin implementing our London Session Breakout System by including the "<Trade\Trade.mqh>" library and defining key enumerations, inputs, and a structure for position tracking. We include "<Trade\Trade.mqh>" to access the CTrade class for executing trading operations like placing orders and modifying positions. We define the "ENUM_TRADE_TYPE" enumeration with options "TRADE_ALL" for both buy and sell trades, "TRADE_BUY_ONLY" for buys only, and "TRADE_SELL_ONLY" for sells only, allowing us to restrict trade directions.
We then set up input parameters in groups: under "General EA Settings", "inpTradeLotsize" at 0.01 for lot size, "TradeType" using the enumeration with default "TRADE_ALL", "MagicNumber" at 12345 to identify EA trades, "RRRatio" at 1.0 for risk-reward ratio, "StopLossPoints" at 500 for stop-loss distance, "OrderOffsetPoints" at 10 for entry offsets, "DeleteOppositeOrder" as true to remove opposite pending orders, "UseTrailing" as false to enable trailing stops, "TrailingPoints" at 50 for trailing distance, and "MinProfitPoints" at 100 to start trailing.
Under "London Session Settings", "LondonStartHour" at 9 and "LondonStartMinute" at 0 for session start, "LondonEndHour" at 8 and "LondonEndMinute" at 0 for end, "MinRangePoints" at 100 and "MaxRangePoints" at 300 for pre-London range validation. Under "Risk Management", "MaxOpenTrades" is set at 2 to limit simultaneous positions, and "MaxDailyDrawdownPercent" at 5.0 to halt trading on excessive drawdown. We define the "PositionInfo" structure to track open trades, with "ticket" for the position ticket, "openPrice" for entry price, "londonRange" for the pre-London range, "sessionID" for the day identifier, and "trailingActive" as a boolean for trailing status. Upon compilation, we have the following output.
With the inputs set in that structured manner, we can now define some extra global variables that we will use throughout the program.
//--- 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
Here, we define global variables for our program: "obj_Trade" as CTrade for trading, "PreLondonHigh" and "PreLondonLow" as doubles for ranges, "PreLondonHighTime" and "PreLondonLowTime" as datetimes for timings, "buyOrderTicket" and "sellOrderTicket" as ulongs for orders, "panelVisible" as true for the panel, "LondonRangePoints" as 0.0 for current range, "positionList" as "PositionInfo" array for positions, "lastCheckedDay" as 0 for daily tracking, "noTradeToday" and "sessionChecksDone" as false for trading flags, "analysisTime" as 0 for session timing, "dailyDrawdown" as 0.0 for risk, "isTrailing" as false for trailing, and constants "PreLondonStartHour" as 3 and "PreLondonStartMinute" as 0.
With that done, we will proceed to create the panel, which is the easiest step, then advance to the more complex trading logic. Let's start with the necessary creation functions.
//+------------------------------------------------------------------+ //| 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 }
Here, we implement utility functions to create the control panel User Interface (UI) elements using rectangle labels for backgrounds and text labels for display. We start with the "createRecLabel" function to generate rectangle labels for panel backgrounds, taking some parameters. We reset errors with the ResetLastError function and create the object with ObjectCreate as OBJ_RECTANGLE_LABEL, logging failures with Print, and returning false if unsuccessful. We set properties using ObjectSetInteger for OBJPROP_XDISTANCE and the same case to all the other necessary integer properties, then redraw the chart with ChartRedraw and return true.
Next, we create the "createLabel" function for text labels in the panel, taking "objName", "xD", "yD", "txt", "clrTxt", "fontSize", and "font" as parameters. We reset errors with "ResetLastError" and create the object with "ObjectCreate" as OBJ_LABEL, logging failures with "Print", and returning false if unsuccessful. We set properties using the "ObjectSetInteger" function just as we did with the rectangle label function, but here, we use the additional ObjectSetString function for "OBJPROP_TEXT" and OBJPROP_FONT properties, then redraw and return true. These functions will enable us to build a dynamic control panel for monitoring session data and program status. We can now use the functions to create and update the panel.
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 }
Here, we define the "panelPrefix" string as "LondonPanel_" to prefix all panel object names, ensuring organized identification for the control panel. We create the "CreatePanel" function to build the information panel UI. We call "createRecLabel" for "panelPrefix + "Background" to make the panel background at position 10,10 with size 270x200, "clrMidnightBlue" background, width 1, and a silver border. We use "createLabel" to add the title "London Breakout Control Center" at 20,15 with gold color and size 12, and labels for range, high price, low price, buy level, sell level, balance, equity, and drawdown at respective positions with white color and size 10.
For the hide button, we call "createRecLabel" for "panelPrefix + 'Hide' at 250,10 with size 30x22 and "clrCrimson" background, and "createLabel" for "panelPrefix + 'HideText' with CharToString(251) from Wingdings at 258,12 with "clrWhite" color and size 13. We set OBJPROP_SELECTABLE and "OBJPROP_STATE" to true for the hide button with ObjectSetInteger to make it interactive. The choice of the Wingdings code to use is dependent on your aesthetic feel. Here is a list of code you can choose from.
We then implement the "UpdatePanel" function to refresh the panel with the current data. We format "rangeText" with "LondonRangePoints" using DoubleToString or "Calculating..." if zero, and update "panelPrefix + "RangePoints" text with the ObjectSetString function. We similarly format and update texts for high price, low price, buy level (adding "OrderOffsetPoints * _Point" to "PreLondonHigh"), sell level (subtracting "OrderOffsetPoints * _Point" from "PreLondonLow"), balance with AccountInfoDouble(ACCOUNT_BALANCE), equity with "AccountInfoDouble(ACCOUNT_EQUITY)", and drawdown with "dailyDrawdown".
We set the drawdown color with ObjectSetInteger to yellow if daily drawdown exceeds "MaxDailyDrawdownPercent / 2", else white. To make the functions usable, we call them in the initialization function as follows.
//+------------------------------------------------------------------+ //| 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 }
On the OnInit event handler, we set the magic number using the trade object, use the ArrayFree function to free the position list, call the "CreatePanel" function to create the panel, and set the panel visibility flag to true after its creation. Then, on the OnDeinit event handler, we use the ObjectsDeleteAll function to delete all the objects with the specified prefix and free the position list array since we no longer need it. When we compile, we get the following outcome.
Since we have created the panel, let us now add some life to it by making the cancel button responsive, where we will destroy the panel once it is clicked. We will achieve this in the OnChartEvent event handler.
//+------------------------------------------------------------------+ //| 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 } }
In the OnChartEvent event handler, we check if the event ID is an object click and the object is the hide or cancel button, and then set the visibility flag to false. We then delete the panel objects and redraw the chart for changes to take effect. Upon compilation, we get the following outcome.
From the visualization, we can see that the panel is now all set. Let us update it so that it initializes all parts. We will need some functions to set the daily ranges and to calculate the drawdown features.
//+------------------------------------------------------------------+ //| 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 }
Here, we implement the "IsNewDay" function to check for a new trading day. We create a MqlDateTime structure "barTime" and convert "currentBarTime" to it with the TimeToStruct function. We calculate "currentDay" using StringToTime and StringFormat from "barTime.year", "barTime.mon", and "barTime.day". If "currentDay" differs from "lastCheckedDay", we update "lastCheckedDay", reset "sessionChecksDone" and "noTradeToday" to false, clear "buyOrderTicket" and "sellOrderTicket" to 0, set "LondonRangePoints" to 0.0, and return true; otherwise, return false. This function ensures daily resets for session analysis and trading flags.
Next, we implement the "UpdateDailyDrawdown" function to monitor daily risk. We use a static "maxEquity" initialized to 0.0 to track peak equity. We get "equity" with AccountInfoDouble using "ACCOUNT_EQUITY", update "maxEquity" if "equity" is higher, and calculate "dailyDrawdown" as the percentage drop from "maxEquity". If "dailyDrawdown" meets or exceeds "MaxDailyDrawdownPercent", we set "noTradeToday" to true to halt trading, providing essential drawdown protection. To make the functions usable, we call them in the OnTick event handler to populate the panel with updated data.
//+------------------------------------------------------------------+ //| 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 }
Here, we just call the new day function to set the range parameters, update the panel, and daily drawdown levels, respectively. When we run the program now, we have the following outcome.
From the visualization, we can see the panel is now populated with recent data and shows the status. Let us now move on to the more complex logic of defining the session ranges. Let's first check the trading conditions and place the orders if the trading conditions are met, and then manage the positions later. So we will need some functions to first let us define the range, visualize the ranges, and then place the orders. Here is the logic we use to achieve that.
//+------------------------------------------------------------------+ //| 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 }
To enable range calculation, order placement, and chart drawing, we start with the "CalculateLotSize" function to compute fixed lot sizes, taking "entryPrice" and "stopLossPrice" as parameters (unused for fixed sizing). We return "inpTradeLotsize" normalized to 2 decimals with NormalizeDouble, ensuring consistent lot sizes for all trades. You can use another decimal depending on your account type.
Next, we create the "GetRange" function to calculate the pre-London session range. We get "startBar" and "endBar" with the iBarShift function using "startTime" and "endTime", returning -1 if invalid or "startBar" < "endBar". We find "highestBar" with iHighest on MODE_HIGH and "lowestBar" with "iLowest" on MODE_LOW over the bar range. We set "highVal" with "iHigh" on "highestBar", "lowVal" with "iLow" on "lowestBar", "highTime" with "iTime" on "highestBar", and "lowTime" with "iTime" on "lowestBar". We return the range as the "(highVal-lowVal) /_Point" value.
We then define the "PlacePendingOrders" function to set up buy and sell stops. We calculate "buyPrice" as "preLondonHigh + OrderOffsetPoints * _Point" and "sellPrice" as "preLondonLow - OrderOffsetPoints * _Point". We set "slPoints" to "StopLossPoints", "buySL" as "buyPrice - slPoints * _Point", "sellSL" as "sellPrice + slPoints * _Point", "tpPoints" as "slPoints * RRRatio", "buyTP" as "buyPrice + tpPoints * _Point", and "sellTP" as "sellPrice - tpPoints * _Point". We compute "lotSizeBuy" and "lotSizeSell" with "CalculateLotSize".
If "TradeType" is "TRADE_ALL" or "TRADE_BUY_ONLY", we place a buy stop with "obj_Trade.BuyStop" using "lotSizeBuy", "buyPrice", "buySL", "buyTP", and label "Buy Stop - London", storing the ticket in "buyOrderTicket" from "ResultOrder". Similarly, for sell if "TRADE_ALL" or "TRADE_SELL_ONLY".
Finally, we implement the "DrawSessionRanges" function to visualize the session on the chart. We create "sessionID" as "Sess_" plus "lastCheckedDay" with the IntegerToString function. For the rectangle "preRectName" as "PreRect_" plus "sessionID", we use ObjectCreate as OBJ_RECTANGLE from "PreLondonHighTime", "PreLondonHigh" to "PreLondonLowTime", "PreLondonLow", setting OBJPROP_COLOR to "clrTeal", "OBJPROP_FILL" to true, and "OBJPROP_BACK" to true.
For the top line "preTopLineName" as "PreTopLine_" plus "sessionID", we create OBJ_TREND from "preLondonStart", "PreLondonHigh" to "londonEnd", "PreLondonHigh", setting "OBJPROP_COLOR" to "clrBlack", "OBJPROP_WIDTH" to 1, "OBJPROP_RAY_RIGHT" to false, and "OBJPROP_BACK" to true. Similarly, for the bottom line "preBotLineName" as "PreBottomLine_" plus "sessionID" from "preLondonStart", "PreLondonLow" to "londonEnd", "PreLondonLow", with red color. We can now define a function to check for the trading conditions using these functions.
//+------------------------------------------------------------------+ //| 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 }
We implement the "CheckTradingConditions" function to evaluate session conditions and place orders in our London Session Breakout System. We create a MqlDateTime structure, "timeStruct," and convert the current time to it with the TimeToStruct function. We calculate "today" using "StringToTime" and "StringFormat" from "timeStruct.year", "timeStruct.mon", and "timeStruct.day". We set "preLondonStart" as "today" plus "PreLondonStartHour" and "PreLondonStartMinute" in seconds, "londonStart" as "today" plus "LondonStartHour" and "LondonStartMinute", and "londonEnd" as "today" plus "LondonEndHour" and "LondonEndMinute". We assign "analysisTime" to "londonStart" and exit if the current time is before it.
We get "preLondonRange" with the "GetRange" function, passing "preLondonStart", "currentTime", and references for "PreLondonHigh", "PreLondonLow", "PreLondonHighTime", and "PreLondonLowTime". If "preLondonRange" is below "MinRangePoints" or above "MaxRangePoints", we set "noTradeToday" and "sessionChecksDone" to true, call "DrawSessionRanges" with "preLondonStart" and "londonEnd", and exit. Otherwise, we set "LondonRangePoints" to "preLondonRange", call "PlacePendingOrders" with "PreLondonHigh", "PreLondonLow", and "today", set "noTradeToday" and "sessionChecksDone" to true, and call "DrawSessionRanges", ensuring trades occur only within valid session ranges. We can call the function now in the OnTick event handler for the signals.
if (!noTradeToday && !sessionChecksDone) { //--- Check trading conditions CheckTradingConditions(TimeCurrent()); //--- Check conditions }
If there are no trades done for today and no session checks done already, we call the function to check for the conditions for the current time. When we run the program, we get the following outcome.
We can see that we set the ranges and placed the pending orders. The range is 100 points, which meets our trading conditions. We can now move on to managing the trading trades, but first, let us check and delete the pending orders when one is activated, and add the positions to the position list for management.
//+------------------------------------------------------------------+ //| 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 }
We start with the "CheckAndDeleteOppositeOrder" function to remove the opposite pending order when one is triggered. We exit early if "DeleteOppositeOrder" is false or "TradeType" is not "TRADE_ALL". We initialize "buyOrderExists" and "sellOrderExists" as false. We loop backward through orders with OrdersTotal and OrderGetTicket, selecting each with "OrderSelect". If the order matches "_Symbol" and "MagicNumber" via "OrderGetString" and OrderGetInteger, we set "buyOrderExists" or "sellOrderExists" if the ticket matches "buyOrderTicket" or "sellOrderTicket".
If the buy order is gone and the sell order exists, we delete "sellOrderTicket" with "obj_Trade.OrderDelete"; similarly, if the sell order is gone and the buy order exists. This function will ensure only one direction of trade after activation.
Next, we create the "AddPositionToList" function to track opened positions. We exit if "londonRange" is <= 0 because the range is not set yet. We get the current "index" with ArraySize of "positionList", resize it with ArrayResize to add one slot, and set "positionList[index]. ticket", "openPrice", "londonRange", "sessionID", and "trailingActive" to false, helping to maintain a list for managing trailing stops and session-specific data. We can now implement this logic on the tick event handler.
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 } } }
Here, we call the "CheckAndDeleteOppositeOrder" function to manage pending orders, ensuring that if one direction's order is filled, the opposite is deleted as per the "DeleteOppositeOrder" input, preventing conflicting trades.
Next, we add untracked positions to the "positionList" to ensure all relevant open trades are monitored for trailing stops. We loop through all positions with PositionsTotal and PositionGetTicket to get each "ticket". If PositionSelectByTicket succeeds and the position matches _Symbol and "MagicNumber" using "PositionGetString" and "PositionGetInteger", we set a "tracked" flag to false and check the "positionList" array with "ArraySize" and an inner loop to see if the "ticket" is already in "positionList[j].ticket". If not "tracked", we get "openPrice" with PositionGetDouble using POSITION_PRICE_OPEN and call "AddPositionToList" with "ticket", "openPrice", "LondonRangePoints", and "lastCheckedDay". This ensures every matching position is added to the list for management without duplicates. Here is the outcome.
Since everything is perfect up to this point, we can now define the function to manage these positions as follows.
//+------------------------------------------------------------------+ //| 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 } } } } }
Here, we implement functions to remove closed positions from the tracking list and manage trailing stops. We start with the "RemovePositionFromList" function to clean up the "positionList" array when a position closes, taking "ticket" as a parameter. We loop through "positionList" with ArraySize, and if "positionList[i].ticket" matches "ticket", we shift subsequent elements with an inner loop copying "positionList[j + 1]" to "positionList[j]", then resize the array with ArrayResize to reduce its size by one, and break the loop. This function will ensure the list remains current, avoiding unnecessary checks on closed positions, especially when we trail and close them.
Next, we create the "ManagePositions" function to handle trailing stops for open trades. We exit early if PositionsTotal is 0 or "UseTrailing" is false. We reset "isTrailing" to false, get "currentBid" and "currentAsk" with SymbolInfoDouble using "SYMBOL_BID" and "SYMBOL_ASK", and "point" as "_Point". We loop through "positionList" with "ArraySize", getting "ticket" and selecting it with the PositionSelectByTicket function. If selection fails, we call "RemovePositionFromList" and continue. If the position doesn't match "_Symbol" or "MagicNumber" with "PositionGetString" and "PositionGetInteger", we skip. We get "openPrice" from "positionList[i]", "positionType" with PositionGetInteger, "currentPrice" based on type, and "profitPoints" as the difference divided by point.
If "profitPoints" meets or exceeds "MinProfitPoints + TrailingPoints", we calculate "newSL" as "currentPrice - TrailingPoints * point" for buys or plus for sells. We get "currentSL" with PositionGetDouble, and if "newSL" is better than "currentSL" by at least "point", we modify the position with "obj_Trade.PositionModify" using normalized "newSL" and current TP from "PositionGetDouble". On success, we set "positionList[i].trailingActive" and "isTrailing" to true, allowing us to dynamically adjust stops to lock in profits while allowing winning trades to run. Now, just call the function to manage the positions on every tick. Upon compilation, we have the following outcome.
From the image, we can see that we check for trading conditions, place orders, and manage them as per the trading strategy, hence achieving our objectives. The thing that remains is backtesting the program, and that is handled in the next section.
Backtesting
After thorough backtesting, we have the following results.
Backtest graph:
Backtest report:
Conclusion
In conclusion, we have developed a London Session Breakout System in MQL5 that analyzes pre-session ranges to place pending orders with customizable risk-to-reward ratios, trailing stops, and multi-trade limits, complemented by a control panel for real-time monitoring of ranges, levels, and drawdown. Through modular components like the "PositionInfo" structure, this program offers a disciplined approach to breakout trading that you can refine by adjusting session times or risk parameters.
Disclaimer: This article is for educational purposes only. Trading carries significant financial risks, and market volatility may result in losses. Thorough backtesting and careful risk management are crucial before deploying this program in live markets.
By leveraging the concepts and implementation presented, you can adapt this breakout system to your trading style, empowering your algorithmic strategies. Happy trading!





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use