
Automating Trading Strategies in MQL5 (Part 4): Building a Multi-Level Zone Recovery System
Introduction
In the previous article (Part 3 of the series), we explored the Zone Recovery RSI System, demonstrating how to integrate RSI-based signal generation with a dynamic Zone Recovery mechanism to manage trades and recover from unfavorable market movements using MetaQuotes Language 5 (MQL5). In this article (Part 4), we build upon those foundations by introducing a Multi-Level Zone Recovery System, a sophisticated trade management approach capable of handling multiple independent signals simultaneously.
This system leverages the Relative Strength Indicator (RSI) indicator to generate trading signals and dynamically incorporates each signal into an array structure, enabling seamless integration with the Zone Recovery logic. The primary objective is to scale the recovery mechanism to manage multiple trade setups efficiently, reducing overall draw downs and improving trading outcomes.
We guide you through designing the strategy blueprint, coding the system in MQL5, and backtesting its performance. For easier understanding and organization, we have divided the steps into topics as follows:
- Strategy Blueprint
- Implementation in MQL5
- Backtesting
- Conclusion
By the end, you’ll have a practical understanding of how to construct and optimize a Multi-Level Zone Recovery System for dynamic and robust trade management.
Strategy Blueprint
The Multi-Level Zone Recovery System will utilize a well-organized structure to manage multiple trading signals effectively. To achieve this, we will define a structure (struct) that acts as the blueprint for creating individual trade baskets. Each trading signal generated by the RSI indicator will correspond to its unique basket, stored as an element within an array. For instance, when the system generates Signal 1, we will create Basket 1, which will not only store the initial trade details but also manage all recovery positions associated with that signal. Similarly, Signal 2 will initiate Basket 2, and this basket will independently track and execute all recovery trades based on the parameters of Signal 2. Here is a visualization of the basket and signal properties.
Each basket will include essential data such as the signal direction (buy or sell), the entry price, recovery levels, dynamically calculated lot sizes, and other trade-specific parameters. As new signals are identified by the RSI, we will add them to the array, ensuring the system can handle multiple signals simultaneously. Recovery trades will be dynamically calculated and executed within their respective baskets, ensuring that each setup is managed independently and without interference from others. Here is an example of the signals being handled separately.
By structuring the system in this manner, we will ensure a high degree of scalability and flexibility. Each basket will act as a self-contained unit, allowing the system to respond dynamically to market conditions for every signal. This design will simplify the tracking and management of complex trade setups, as every signal and its associated recovery trades will be neatly organized. The array-based basket system will serve as the foundation for building a robust and adaptable Multi-Level Zone Recovery System, capable of handling diverse trading scenarios while maintaining efficiency and clarity. Let's get started.
Implementation in MQL5
After learning all the theories about the Multi-Level Zone Recovery trading strategy, let us then automate the theory and craft an Expert Advisor (EA) in MetaQuotes Language 5 (MQL5) for MetaTrader 5.
To create an expert advisor (EA), on your MetaTrader 5 terminal, click the Tools tab and check MetaQuotes Language Editor, or simply press F4 on your keyboard. Alternatively, you can click the IDE (Integrated Development Environment) icon on the tools bar. This will open the MetaQuotes Language Editor environment, which allows the writing of trading robots, technical indicators, scripts, and libraries of functions. Once the MetaEditor is opened, on the tools bar, navigate to the File tab and check New File, or simply press CTRL + N, to create a new document. Alternatively, you can click on the New icon on the tools tab. This will result in a MQL Wizard pop-up.
On the Wizard that pops, check Expert Advisor (template) and click Next. On the general properties of the Expert Advisor, under the name section, provide your expert's file name. Note that to specify or create a folder if it doesn't exist, you use the backslash before the name of the EA. For example, here we have "Experts\" by default. That means that our EA will be created in the Experts folder and we can find it there. The other sections are pretty much straightforward, but you can follow the link at the bottom of the Wizard to know how to precisely undertake the process.
After providing your desired Expert Advisor file name, click on Next, click Next, and then click Finish. After doing all that, we are now ready to code and program our strategy.
First, we start by defining some metadata about the Expert Advisor (EA). This includes the name of the EA, the copyright information, and a link to the MetaQuotes website. We also specify the version of the EA, which is set to "1.00".
//+------------------------------------------------------------------+ //| 1. Zone Recovery RSI EA Multi-Zone.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00"
This will display the system metadata when loading the program. We can then move on to adding some input variables that will be displayed on the user interface as follows.
sinput group "General EA Settings" input double inputlot = 0.01; input double inputzonesizepts = 200; input double inputzonetragetpts = 400; input double inputlotmultiplier = 2.0; input double inputTrailingStopPts = 50; // Trailing stop distance in points input double inputMinimumProfitPts = 50; // Minimum profit points before trailing stop starts input bool inputTrailingStopEnabled = true; // Enable or disable trailing stop
We define a group of input parameters under the category "General EA Settings", allowing users to configure essential settings for the Expert Advisor (EA) before running it. These inputs are declared using the input data type in MQL5, making them adjustable directly from the EA's input settings panel without modifying the code. Each input parameter is specific in controlling the EA's behavior and risk management.
The "inputlot" parameter defines the initial lot size for opening trades, with a default value of 0.01, enabling precise control over trade volume. The "inputzonesizepts" parameter specifies the size of the recovery zone in points, set to 200 by default, which determines the distance between recovery trades. The "inputzonetragetpts" parameter, with a default value of 400, sets the target profit distance in points, guiding the EA on when to close positions profitably.
To handle recovery trades, we use the "inputlotmultiplier" parameter, set to 2.0 by default, allowing the EA to calculate dynamically increasing lot sizes for recovery trades based on the multiplier. Additionally, trailing stop functionality is introduced through three parameters. The "inputTrailingStopPts" defines the trailing stop distance in points, set to 50, which adjusts the stop loss as the market moves in the trade's favor. The "inputMinimumProfitPts" parameter, also set to 50, ensures that the trailing stop is only activated after the trade reaches a minimum profit threshold.
Finally, the "inputTrailingStopEnabled" parameter, a bool data type, allows users to enable or disable the trailing stop feature as needed. This flexibility ensures that the EA can adapt to different trading strategies, risk profiles, and market conditions, providing a customizable framework for efficient trade and risk management. Next, since we will be opening trades, we need to include some extra files so that we include a trade instance by using #include. This gives us access to the "CTrade class", which we will use to create a trade object. This is crucial as we need it to open trades.
#include <Trade/Trade.mqh> //--- Includes the MQL5 Trade library for handling trading operations.
The preprocessor will replace the line #include <Trade/Trade.mqh> with the content of the file Trade.mqh. Angle brackets indicate that the Trade.mqh file will be taken from the standard directory (usually it is terminal_installation_directory\MQL5\Include). The current directory is not included in the search. The line can be placed anywhere in the program, but usually, all inclusions are placed at the beginning of the source code, for a better code structure and easier reference. Here is the specific file as from the navigator section.
After that, we need to declare several important global variables that we will use in the trading system.
//--- Global variables for RSI logic int rsiPeriod = 14; //--- The period used for calculating the RSI indicator. int rsiHandle; //--- Handle for the RSI indicator, used to retrieve RSI values. double rsiBuffer[]; //--- Array to store the RSI values retrieved from the indicator. datetime lastBarTime = 0; //--- Holds the time of the last processed bar to prevent duplicate signals.
Here, we define a set of global variables to manage the RSI indicator logic, which will drive the EA’s trading signals. These variables are designed to handle the calculation, retrieval, and processing of RSI values efficiently. By declaring them as global, we ensure they are accessible throughout the EA, enabling consistent and effective signal generation. We set the "rsiPeriod" variable, an int data type, to 14, specifying the lookback period for calculating the RSI indicator. This value determines the number of bars the EA will analyze to compute RSI values, giving us control over the sensitivity of the indicator. Next, we declare the "rsiHandle", another int variable, which we will use to store the handle for the RSI indicator. This handle is obtained when we initialize the RSI using the iRSI function, allowing us to retrieve RSI values directly from the terminal's indicator buffer.
To store these RSI values, we create the "rsiBuffer[]", a dynamic array of the double data type. This array will hold the calculated RSI values for each bar, which we will use to identify overbought or oversold conditions in the market. Additionally, we define the "lastBarTime" variable, a "datetime" type, to store the time of the last processed bar. By tracking this value, we ensure the EA only processes a new signal when a fresh bar appears, preventing duplicate signals for the same bar. We can now define the common "Basket" parameters, in a structure that we will link to every generated signal. To achieve this, we use the struct logic, whose general syntax is as follows:
//--- Struct to track individual position recovery states struct PositionRecovery { //--- Member 1 //--- Member 2 //--- Member 3 //--- Method 1 //... };
To group related data variables together, we need a structure, and here is the general prototype of it. We define a struct named "PositionRecovery", which serves as a blueprint for organizing and managing data related to individual position recovery states within the EA. The struct acts as a custom data type, allowing us to group related variables (members) and functions (methods) into a single entity.
Syntax Explanation:
"struct" "PositionRecovery { ... };"
This declares a structure named "PositionRecovery". The keyword struct is used to define the structure, and the { ... } braces enclose the members and methods of the structure. The semicolon (;) at the end of the definition is mandatory in MQL5.
- Members
Members are variables defined within the structure that store data specific to each instance of "PositionRecovery".
"//--- Member 1": Placeholder for a variable, such as the trade's initial lot size or entry price.
"//--- Member 2": Could represent parameters like the recovery zone size or current trade state.
"//--- Member 3": Additional data like the number of recovery trades executed or a flag indicating recovery completion.
These members allow us to encapsulate all the information needed to track and manage an individual recovery process.
- Methods
Methods are functions defined inside the structure that operate on its members.
"//--- Method 1": A placeholder for a method, such as calculating the next recovery trade lot size or checking if the recovery target has been met.
By combining both data (members) and logic (methods), the structure becomes more versatile and self-contained. After understanding that, we can now begin defining the structure members.
//--- Struct to track individual position recovery states struct PositionRecovery { CTrade trade; //--- Object to handle trading operations. double initialLotSize; //--- Initial lot size for this position. double currentLotSize; //--- Current lot size in the recovery sequence. double zoneSize; //--- Distance in points defining the recovery zone size. double targetSize; //--- Distance in points defining the profit target range. double multiplier; //--- Lot size multiplier for recovery trades. string symbol; //--- Trading symbol. ENUM_ORDER_TYPE lastOrderType; //--- Type of the last order (BUY or SELL). double lastOrderPrice; //--- Price of the last executed order. double zoneHigh; //--- Upper boundary of the recovery zone. double zoneLow; //--- Lower boundary of the recovery zone. double zoneTargetHigh; //--- Upper boundary of the target range. double zoneTargetLow; //--- Lower boundary of the target range. bool isRecovery; //--- Whether the recovery is active. ulong tickets[]; //--- Array to store tickets of positions associated with this recovery. double trailingStop; //--- Trailing stop level double initialEntryPrice; //--- Initial entry price for trailing stop calculation //--- };
Here, we create a struct named "PositionRecovery" to organize and manage all the data necessary for tracking individual position recovery states. By using this structure, we ensure that each recovery process is handled independently, allowing us to manage multiple signals effectively.
We define the "CTrade trade" object, which we will use to execute trading operations like opening, modifying, and closing orders related to this recovery. We set up "initialLotSize" to store the size of the first trade in the sequence, while "currentLotSize" helps us track the lot size of the most recent trade in the recovery process. To control the recovery strategy, we specify the recovery zone distance in points using "zoneSize" and define the profit target range with "targetSize".
To handle dynamic lot sizing, we include a "multiplier", which we will use to calculate the lot size for each subsequent recovery trade. We add "symbol" to identify the trading instrument for this recovery, ensuring that the EA executes trades on the correct symbol. We use an enumeration data type ENUM_ORDER_TYPE to declare a variable "lastOrderType" to store the type of the last executed order (e.g., BUY or SELL) and "lastOrderPrice" to record its execution price, helping us track the current state of the recovery. For monitoring recovery zones, we define "zoneHigh" and "zoneLow" as the upper and lower boundaries of the recovery zone, while "zoneTargetHigh" and "zoneTargetLow" mark the profit target range.
To determine if a recovery is active, we use "isRecovery", a flag that we set to true or false as needed. We also include "tickets[]", an array where we store the ticket numbers of all trades in the recovery sequence, allowing us to track and manage them individually. Finally, we include "trailingStop" to specify the trailing stop distance and "initialEntryPrice" to record the entry price of the first trade, which we will use for calculating the trailing stop. These components enable us to dynamically protect profits during recovery.
After defining the member variables, we need to initialize them on every instance that is created, or rather on every basket. To do this, we can create a method to handle the initialization smoothly.
//--- Initialize position recovery void Initialize(double lot, double zonePts, double targetPts, double lotMultiplier, string _symbol, ENUM_ORDER_TYPE type, double price) { initialLotSize = lot; //--- Assign initial lot size. currentLotSize = lot; //--- Set current lot size equal to initial lot size. zoneSize = zonePts * _Point; //--- Calculate zone size in points. targetSize = targetPts * _Point; //--- Calculate target size in points. multiplier = lotMultiplier; //--- Assign lot size multiplier. symbol = _symbol; //--- Assign the trading symbol. lastOrderType = type; //--- Set the type of the last order. lastOrderPrice = price; //--- Record the price of the last executed order. isRecovery = false; //--- Set recovery as inactive initially. ArrayResize(tickets, 0); //--- Initialize the tickets array. trailingStop = 0; //--- Initialize trailing stop initialEntryPrice = price; //--- Set initial entry price CalculateZones(); //--- Calculate recovery and target zones. }
We define the "Initialize" method, which is responsible for setting up all the necessary parameters and initializing the state for an individual position recovery. This method ensures that each recovery instance is configured correctly and ready to manage trades dynamically based on the provided input values. We start by assigning the "lot" value to "initialLotSize", which specifies the size of the first trade in the recovery sequence. At the same time, we set "currentLotSize" equal to "initialLotSize", as the first trade uses the same lot size. Next, we calculate the recovery zone size and profit target range in points using the "zonePts" and "targetPts" inputs, respectively, multiplying them by the "_Point" constant to account for the symbol's point value. These calculations define the distance thresholds for managing recovery trades and their targets.
We assign the "lotMultiplier" to the "multiplier" variable, which determines how the lot size will increase in subsequent recovery trades. The trading symbol is assigned to "symbol" to ensure that all trades in this recovery instance are executed on the correct market instrument. We set the "lastOrderType" to the provided "type" parameter and the "lastOrderPrice" to "price", recording the details of the most recent order. These values help us track the state of the current recovery. Additionally, we initialize "isRecovery" to false, indicating that the recovery process is not active when first created.
We resize the "tickets" array to zero using the "ArrayResize" function, clearing any existing data and preparing it to store the ticket numbers of trades associated with this recovery instance. For added flexibility, we initialize "trailingStop" to 0 and set "initialEntryPrice" to "price", providing a baseline for trailing stop calculations. Finally, we call the "CalculateZones" method, which computes the upper and lower boundaries for the recovery zone and target range. This step ensures that the EA has all the required data to manage trades efficiently. By using the "Initialize" method, we will establish a complete and well-defined starting point for each recovery process, ensuring all relevant parameters are set correctly for effective trade management. We can then proceed to define the "CalculateZones" function, which is responsible for calculating the recovery range levels.
//--- Calculate dynamic zones and targets void CalculateZones() { if (lastOrderType == ORDER_TYPE_BUY) { //--- If the last order was a BUY... zoneHigh = lastOrderPrice; //--- Set upper boundary at the last order price. zoneLow = zoneHigh - zoneSize; //--- Set lower boundary below the last order price. zoneTargetHigh = zoneHigh + targetSize; //--- Define target range above recovery zone. zoneTargetLow = zoneLow - targetSize; //--- Define target range below recovery zone. } else if (lastOrderType == ORDER_TYPE_SELL) { //--- If the last order was a SELL... zoneLow = lastOrderPrice; //--- Set lower boundary at the last order price. zoneHigh = zoneLow + zoneSize; //--- Set upper boundary above the last order price. zoneTargetLow = zoneLow - targetSize; //--- Define target range below recovery zone. zoneTargetHigh = zoneHigh + targetSize; //--- Define target range above recovery zone. } }
Here, we define the "CalculateZones" method, which dynamically calculates the recovery zone boundaries and profit target ranges based on the type of the last executed order and its price. This method ensures that each recovery process has clearly defined levels to guide subsequent trading decisions, allowing the system to react appropriately to market movements.
We begin by checking the "lastOrderType" to determine whether the most recent order was a BUY or a SELL. If the "lastOrderType" is ORDER_TYPE_BUY, we assign "zoneHigh" to the "lastOrderPrice", setting the upper boundary of the recovery zone at the entry price of the last BUY order. The lower boundary, "zoneLow", is then calculated by subtracting the "zoneSize" (converted to points) from "zoneHigh". Additionally, we define the profit target range: "zoneTargetHigh" is calculated by adding "targetSize" to "zoneHigh", while "zoneTargetLow" is calculated by subtracting "targetSize" from "zoneLow". These calculations ensure that the recovery zone and the profit target range are positioned relative to the BUY order price.
If the "lastOrderType" is ORDER_TYPE_SELL, we reverse the logic. In this case, we assign "zoneLow" to the "lastOrderPrice", setting the lower boundary of the recovery zone at the entry price of the last SELL order. The upper boundary, "zoneHigh", is calculated by adding "zoneSize" to "zoneLow". For the profit target range, we calculate "zoneTargetLow" by subtracting "targetSize" from "zoneLow" and "zoneTargetHigh" by adding "targetSize" to "zoneHigh". These boundaries are set relative to the SELL order price. These level definitions would depict the image visualized below:
After defining the zone levels, we can proceed to open the positions. We will encapsulate the position opening logic in a method for easier usage within the code structure.
//--- Open a trade with comments for position type bool OpenTrade(ENUM_ORDER_TYPE type, string comment) { if (type == ORDER_TYPE_BUY) { //--- For a BUY order... if (trade.Buy(currentLotSize, symbol, 0, 0, 0, comment)) { //--- Attempt to place a BUY trade. lastOrderType = ORDER_TYPE_BUY; //--- Update the last order type. lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Record the current price. ArrayResize(tickets, ArraySize(tickets) + 1); //--- Resize the tickets array. tickets[ArraySize(tickets) - 1] = trade.ResultOrder(); //--- Store the new ticket. CalculateZones(); //--- Recalculate zones. isRecovery = false; //--- Ensure recovery is inactive for initial trade. Print("Opened BUY Position, Ticket: ", tickets[ArraySize(tickets) - 1]); return true; //--- Return success. } } else if (type == ORDER_TYPE_SELL) { //--- For a SELL order... if (trade.Sell(currentLotSize, symbol, 0, 0, 0, comment)) { //--- Attempt to place a SELL trade. lastOrderType = ORDER_TYPE_SELL; //--- Update the last order type. lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Record the current price. ArrayResize(tickets, ArraySize(tickets) + 1); //--- Resize the tickets array. tickets[ArraySize(tickets) - 1] = trade.ResultOrder(); //--- Store the new ticket. CalculateZones(); //--- Recalculate zones. isRecovery = false; //--- Ensure recovery is inactive for initial trade. Print("Opened SELL Position, Ticket: ", tickets[ArraySize(tickets) - 1]); return true; //--- Return success. } } return false; //--- If the trade was not placed, return false. }
In this boolean "OpenTrade" function, we handle the logic to open a new trade of a specified type (BUY or SELL) and manage the necessary updates to the recovery system. This function ensures that trades are opened correctly and that all related data is updated to maintain synchronization with the recovery process. When the "type" parameter is ORDER_TYPE_BUY, we attempt to open a BUY trade using the "trade.Buy" method. The method uses the "currentLotSize", "symbol", and "comment" parameters to execute the trade, leaving stop loss and take profit levels as zero (not specified), as we define these as per the zone target ranges dynamically. If the BUY trade is successfully placed, we update "lastOrderType" to ORDER_TYPE_BUY, indicating the type of the last trade, and "lastOrderPrice" is set to the current market price retrieved using the SymbolInfoDouble function with the "SYMBOL_BID" parameter.
Next, we resize the "tickets" array using the ArrayResize function to make room for the new trade, and we store the ticket number of the successfully placed trade using "trade.ResultOrder()". This ensures that all trades related to this recovery instance are tracked and stored efficiently. We then call the "CalculateZones" function to recompute the recovery and target zones based on the latest trade. Finally, we set "isRecovery" to false, as this is the initial trade and not part of a recovery process. A success message is printed to the log, and the function returns true to indicate that the trade was successfully opened.
If the "type" parameter is "ORDER_TYPE_SELL", we follow a similar logic. The "trade.Sell" method is called to place a SELL trade with the specified parameters. Upon success, we update "lastOrderType" to ORDER_TYPE_SELL and record the "lastOrderPrice" as the current market price. The "tickets" array is resized, and the new ticket is stored, just as we did for a BUY order. The zones are recalculated using "CalculateZones", and "isRecovery" is set to false. A success message is printed, and the function returns true.
If the trade fails for either order type, the function returns false, signaling that the operation was unsuccessful. This structure ensures that trades are managed systematically and that all recovery-related data is updated correctly for seamless trade management. After the positions are opened and zone levels calculated, we can then continue to manage those zones on every tick to open the recovery positions when either of the levels is hit.
//--- Manage zone recovery void ManageZones() { double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get the current price. if (lastOrderType == ORDER_TYPE_BUY && currentPrice <= zoneLow) { //--- If price drops below the recovery zone for a BUY... double previousLotSize = currentLotSize; //--- Store the current lot size temporarily. currentLotSize *= multiplier; //--- Tentatively increase lot size. if (OpenTrade(ORDER_TYPE_SELL, "Recovery Position")) { //--- Attempt to open a SELL recovery trade. isRecovery = true; //--- Mark recovery as active if trade is successful. } else { currentLotSize = previousLotSize; //--- Revert the lot size if the trade fails. } } else if (lastOrderType == ORDER_TYPE_SELL && currentPrice >= zoneHigh) { //--- If price rises above the recovery zone for a SELL... double previousLotSize = currentLotSize; //--- Store the current lot size temporarily. currentLotSize *= multiplier; //--- Tentatively increase lot size. if (OpenTrade(ORDER_TYPE_BUY, "Recovery Position")) { //--- Attempt to open a BUY recovery trade. isRecovery = true; //--- Mark recovery as active if trade is successful. } else { currentLotSize = previousLotSize; //--- Revert the lot size if the trade fails. } } }
We declare a "ManageZones" function, to monitor the market price of the recovery zones and take action if the price moves against the initial trade. First, we retrieve the current market price using the SymbolInfoDouble function to get the latest bid price. We then check if the price has moved outside the boundaries of the recovery zone, which is defined by "zoneLow" for a BUY order and "zoneHigh" for a SELL order.
If the last order was a BUY (indicated by "lastOrderType" == ORDER_TYPE_BUY) and the current price drops below "zoneLow", we increase the lot size for the recovery trade. We store the current lot size in "previousLotSize", then multiply "currentLotSize" by the "multiplier" to increase it. After that, we attempt to open a SELL recovery trade using the "OpenTrade" function. If the recovery trade is successfully placed, we set "isRecovery" to true to mark that recovery is active. If the trade fails, we revert the lot size to the original value stored in "previousLotSize".
Similarly, if the last order was a SELL (indicated by "lastOrderType" == ORDER_TYPE_SELL) and the price rises above "zoneHigh", we apply the same logic to open a BUY recovery trade. The lot size has increased, and we are attempting to open the BUY trade. If successful, "isRecovery" is set to true, but if the trade fails, the lot size is reverted. This ensures the system manages recovery trades effectively, adjusting position sizes and taking corrective action based on market conditions. Finally, we need to close the positions when the price hits the defined target levels, and so we will need a function to handle that logic.
//--- Check and close trades at targets void CheckCloseAtTargets() { double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get the current price. if (lastOrderType == ORDER_TYPE_BUY && currentPrice >= zoneTargetHigh) { //--- If price reaches the target for a BUY... ClosePositionsAtTarget(); //--- Close positions that meet the target criteria. } else if (lastOrderType == ORDER_TYPE_SELL && currentPrice <= zoneTargetLow) { //--- If price reaches the target for a SELL... ClosePositionsAtTarget(); //--- Close positions that meet the target criteria. } }
Here, we define a void "CheckCloseAtTargets" function, to check if the market price has reached the predefined target levels and close the positions that meet the target criteria. First, we retrieve the current market bid price using "SymbolInfoDouble (symbol, SYMBOL_BID)". Then, we compare this price against the target levels defined by "zoneTargetHigh" for a BUY order and "zoneTargetLow" for a SELL order.
If the last trade was a BUY (indicated by "lastOrderType" == ORDER_TYPE_BUY) and the current price rises to or above "zoneTargetHigh", we consider the position to have reached the desired profit target. In this case, we call the "ClosePositionsAtTarget" function to close any positions that meet the target criteria. Similarly, if the last order was a SELL (indicated by "lastOrderType" == ORDER_TYPE_SELL) and the price drops to or below "zoneTargetLow", the system again calls "ClosePositionsAtTarget" to close the positions. This function ensures that trades are closed when the market reaches the designated profit target, locking in gains and finalizing the recovery process.
To close the positions, we used a function called "ClosePositionsAtTarget" so we could reuse it. Here is the function snippet.
//--- Close positions that have reached the target void ClosePositionsAtTarget() { for (int i = ArraySize(tickets) - 1; i >= 0; i--) { //--- Iterate through all tickets. ulong ticket = tickets[i]; //--- Get the position ticket. int retries = 10; //--- Set retry count. while (retries > 0) { //--- Retry until successful or retries exhausted. if (trade.PositionClose(ticket)) { //--- Attempt to close the position. Print("CLOSED # ", ticket, " Trailed and closed: ", (trailingStop != 0)); ArrayRemove(tickets, i); //--- Remove the ticket from the array on success. retries = 0; //--- Exit the loop on success. } else { retries--; //--- Decrement retries on failure. Sleep(100); //--- Wait before retrying. } } } if (ArraySize(tickets) == 0) { //--- If all tickets are closed... Reset(); //--- Reset recovery state after closing the target positions. } }
In the "ClosePositionsAtTarget" function, we iterate through all open positions stored in the "tickets" array and attempt to close those that have reached the target levels. We start by looping through the "tickets" array in reverse order to ensure we don't skip any positions while removing them after closing. For each ticket, we set a retry count of "retries" to ensure that if a position fails to close on the first attempt, the system will try again.
For each position, we attempt to close it using the "trade.PositionClose(ticket)" function. If the position is successfully closed, we print a message indicating the ticket was closed and whether it was trailing or not, using "trailingStop != 0" to check if a trailing stop was applied. Once the position is closed, we remove the ticket from the "tickets" array using the ArrayRemove function and exit the retry loop by setting "retries" to 0. If the position fails to close, we decrement the "retries" counter, wait for a brief period using the Sleep function, and then attempt to close the position again, making sure we don't overwhelm the function.
After attempting to close all positions, we check if the "tickets" array is empty using the ArraySize function. If all positions are closed, we call the "Reset" function to reset the recovery state, clearing any remaining recovery-related data and preparing for future trades. That is all. However, since we are not mostly certain that the market will hit our target levels, we can improve the system by trailing the positions that hit our minimum profit instead of having to wait till the levels are hit. We have this logic in a method again.
//--- Apply trailing stop logic to initial positions void ApplyTrailingStop() { if (inputTrailingStopEnabled && ArraySize(tickets) == 1) { // Ensure trailing stop is enabled and there is only one position (initial position) ulong ticket = tickets[0]; // Get the ticket of the initial position double entryPrice = GetPositionEntryPrice(ticket); // Get the entry price of the position by ticket double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); // Get the current price double newTrailingStop; if (lastOrderType == ORDER_TYPE_BUY) { if (currentPrice > entryPrice + (inputMinimumProfitPts + inputTrailingStopPts) * _Point) { newTrailingStop = currentPrice - inputTrailingStopPts * _Point; // Calculate new trailing stop for BUY if (newTrailingStop > trailingStop) { trailingStop = newTrailingStop; // Update trailing stop if the new one is higher Print("Trailing BUY Position, Ticket: ", ticket, " New Trailing Stop: ", trailingStop); } } if (trailingStop != 0 && currentPrice <= trailingStop) { Print("Trailed and closing BUY Position, Ticket: ", ticket); ClosePositionsAtTarget(); // Close position if the price falls below the trailing stop } } else if (lastOrderType == ORDER_TYPE_SELL) { if (currentPrice < entryPrice - (inputMinimumProfitPts + inputTrailingStopPts) * _Point) { newTrailingStop = currentPrice + inputTrailingStopPts * _Point; // Calculate new trailing stop for SELL if (newTrailingStop < trailingStop) { trailingStop = newTrailingStop; // Update trailing stop if the new one is lower Print("Trailing SELL Position, Ticket: ", ticket, " New Trailing Stop: ", trailingStop); } } if (trailingStop != 0 && currentPrice >= trailingStop) { Print("Trailed and closing SELL Position, Ticket: ", ticket); ClosePositionsAtTarget(); // Close position if the price rises above the trailing stop } } } }
In the void "ApplyTrailingStop" method that we define, we implement the trailing stop logic for the initial position based on whether trailing stops are enabled and if there's only one active position. First, we check if the trailing stop feature is enabled using "inputTrailingStopEnabled" and if there is only one open position (ensured by "ArraySize(tickets) == 1"). We then retrieve the ticket of the initial position and use it to get the entry price through the "GetPositionEntryPrice" function. We also fetch the current market price using the SymbolInfoDouble function.
For a BUY position, we check if the current price has moved above the entry price by a specific amount (considering both the minimum profit and the trailing stop distance, calculated with "inputMinimumProfitPts + inputTrailingStopPts" and set the new trailing stop accordingly. If the calculated trailing stop is higher than the current "trailingStop", we update the trailing stop value and print a message indicating the new trailing stop level. If the current price drops to or below the trailing stop level, we close the position using the "ClosePositionsAtTarget" function.
For a SELL position, we follow a similar process, but in reverse. We check if the current price is below the entry price by a certain amount and set the trailing stop lower if necessary. If the calculated trailing stop is lower than the current "trailingStop", we update the trailing stop and print a message indicating the new level. If the current price rises to or above the trailing stop, the position is closed. This function ensures that the trailing stop is applied dynamically based on market conditions, allowing for the lock-in of profits while protecting the position from significant losses. If the price moves favorably, the trailing stop is adjusted; if the price reverses and hits the trailing stop, the position is closed.
You might have noticed that we use a custom function to get the entry prices. Here is the function's logic.
//--- Get the entry price of a position by ticket double GetPositionEntryPrice(ulong ticket) { if (PositionSelectByTicket(ticket)) { return PositionGetDouble(POSITION_PRICE_OPEN); } else { Print("Failed to select position by ticket: ", ticket); return 0.0; } }
Here, we define the "GetPositionEntryPrice" function, we retrieve the entry price of a position using the provided ticket number. First, we attempt to select the position associated with the given ticket using the PositionSelectByTicket function. If the position is successfully selected, we retrieve the entry price of the position by calling "PositionGetDouble (POSITION_PRICE_OPEN)", which gives us the price at which the position was opened. If the position cannot be selected (for example, if the ticket is invalid or the position no longer exists), we print an error message indicating the failure and return a value of 0.0 to signify that the entry price could not be retrieved.
Now after opening and closing the positions, we need to reset the system and remove the associated trade basket as a cleanup method. Here is how we handle that cleanup logic, in a "Reset" function.
//--- Reset recovery state void Reset() { currentLotSize = inputlot; //--- Reset lot size to initial value. lastOrderType = -1; //--- Clear the last order type. lastOrderPrice = 0.0; //--- Reset the last order price. isRecovery = false; //--- Mark recovery as inactive. ArrayResize(tickets, 0); //--- Clear the tickets array. trailingStop = 0; //--- Reset trailing stop initialEntryPrice = 0.0; //--- Reset initial entry price Print("Strategy BASKET reset after closing trades."); }
In the "Reset" function, we reset the recovery state to prepare for a new trading cycle. First, we set the "currentLotSize" back to the initial value defined by "inputlot", ensuring that the lot size is reset to the user-defined starting amount. We also clear the last order details by setting "lastOrderType" to -1 (which indicates no active order type) and reset "lastOrderPrice" to 0.0, effectively removing any previous order price information.
Next, we mark recovery as inactive by setting "isRecovery" to false, which ensures no recovery logic will be applied when the reset occurs. We then clear the "tickets" array using the ArrayResize function, removing all stored position tickets that were part of the previous recovery process. Additionally, we reset the "trailingStop" to 0 and the "initialEntryPrice" to 0.0, clearing any trailing stop settings and entry price values from previous trades. Finally, we print a message "Strategy BASKET reset after closing trades" to notify that the reset has been completed and the recovery state has been cleared. This function ensures that the system is in a clean state, and ready for the next trade cycle.
After defining the structure properties, we are now clear to generate signals and add them to the defined structure. However, since we will be required to manage many dynamic signals, we will need to define an array structure, which will act as a whole basket in which we will define the sub-baskets for every signal generated. Here is how we achieve that.
//--- Dynamic list to track multiple positions PositionRecovery recoveryArray[]; //--- Dynamic array for recovery instances.
Here, we declare a dynamic array named "recoveryArray", which is designed to track and manage multiple position recovery instances. The array is based on the "PositionRecovery" structure, allowing it to store individual recovery states for multiple trades independently. Each element of the array represents a distinct recovery setup, complete with all relevant attributes such as lot size, zone boundaries, and associated trade tickets.
By making the array dynamic, we can expand or shrink it as needed during runtime using functions like ArrayResize. This allows us to dynamically add new recovery instances for new trading signals or remove completed recoveries, ensuring efficient memory usage and adaptability to varying trading scenarios. This approach is essential for managing multiple trades simultaneously, as it enables each trade's recovery logic to operate independently within its own "basket" of data.
After defining the array, we can now begin signal generation logic. We will need to initialize the indicator handle on the OnInit event handler, which is called whenever the program is initialized.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { rsiHandle = iRSI(_Symbol, PERIOD_CURRENT, rsiPeriod, PRICE_CLOSE); //--- Create RSI indicator handle. if (rsiHandle == INVALID_HANDLE) { //--- Check if handle creation failed. Print("Failed to create RSI handle. Error: ", GetLastError()); //--- Print error message. return(INIT_FAILED); //--- Return initialization failure. } ArraySetAsSeries(rsiBuffer, true); //--- Set RSI buffer as a time series. Print("Multi-Zone Recovery Strategy initialized."); //--- Log initialization success. return(INIT_SUCCEEDED); //--- Return initialization success. }
In the OnInit event handler function, we initialize the essential components required for the Expert Advisor (EA) to function. We start by creating an RSI indicator handle using the iRSI function, which calculates the Relative Strength Index for the current symbol and period. This handle enables the EA to access RSI values dynamically. If the handle creation fails, indicated by the value INVALID_HANDLE, we log an error message with details using the Print function and return INIT_FAILED to terminate the initialization process. We then configure the "rsiBuffer" array as a time series using ArraySetAsSeries, ensuring the data is organized chronologically for accurate processing. Upon successful initialization, we print a confirmation message indicating the strategy is ready and return INIT_SUCCEEDED to signal the EA's readiness for operation. Then on the OnDeinit event handler, we destroy the handle to save on resources.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if (rsiHandle != INVALID_HANDLE) //--- Check if RSI handle is valid. IndicatorRelease(rsiHandle); //--- Release the RSI handle. Print("Multi-Zone Recovery Strategy deinitialized."); //--- Log deinitialization. }
Here, we handle the cleanup and resource management for the Expert Advisor (EA) when it is removed or deactivated. We first check if the "rsiHandle" for the RSI indicator is valid by ensuring it is not equal to "INVALID_HANDLE". If the handle is valid, we release it using the IndicatorRelease function to free up resources and avoid memory leaks. Finally, we log a message using Print to indicate that the Multi-Zone Recovery Strategy has been successfully deinitialized. This function ensures a clean and orderly shutdown of the program, leaving no lingering resources or processes.
Afterward, we can now graduate to the OnTick event handler, which will handle all the main system's logic by utilizing the structure defined earlier. We first will need to retrieve the indicator's data so we can use it for further analysis.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { if (CopyBuffer(rsiHandle, 0, 1, 2, rsiBuffer) <= 0) { //--- Copy the RSI buffer values. Print("Failed to copy RSI buffer. Error: ", GetLastError()); //--- Print error on failure. return; //--- Exit on failure. } //--- }
Here, in the OnTick function, we handle the logic that is executed on every new tick, which represents a price update for the trading symbol. The first step involves copying the RSI indicator values into the "rsiBuffer" array using the CopyBuffer function. We specify the "rsiHandle" to identify the RSI indicator, set the buffer index to 0, and request two values starting from the most recent bar. If the operation fails (i.e., the returned value is less than or equal to 0), we print an error message using Print to notify the user of the issue and include the error details obtained from the GetLastError function. After logging the error, we immediately exit the function using return. This ensures the rest of the logic is not executed if the RSI data retrieval fails, maintaining the integrity and stability of the Expert Advisor.
If we successfully retrieve the data, we can then use it for signal generation logic as follows.
datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); //--- Get the time of the current bar. if (currentBarTime != lastBarTime) { //--- Check if a new bar has formed. lastBarTime = currentBarTime; //--- Update the last processed bar time. if (rsiBuffer[1] > 30 && rsiBuffer[0] <= 30) { //--- Check for oversold RSI crossing up. Print("BUY SIGNAL"); PositionRecovery newRecovery; //--- Create a new recovery instance. newRecovery.Initialize(inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, _Symbol, ORDER_TYPE_BUY, SymbolInfoDouble(_Symbol, SYMBOL_BID)); //--- Initialize the recovery. newRecovery.OpenTrade(ORDER_TYPE_BUY, "Initial Position"); //--- Open an initial BUY position. ArrayResize(recoveryArray, ArraySize(recoveryArray) + 1); //--- Resize the recovery array. recoveryArray[ArraySize(recoveryArray) - 1] = newRecovery; //--- Add the new recovery to the array. } else if (rsiBuffer[1] < 70 && rsiBuffer[0] >= 70) { //--- Check for overbought RSI crossing down. Print("SELL SIGNAL"); PositionRecovery newRecovery; //--- Create a new recovery instance. newRecovery.Initialize(inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, _Symbol, ORDER_TYPE_SELL, SymbolInfoDouble(_Symbol, SYMBOL_BID)); //--- Initialize the recovery. newRecovery.OpenTrade(ORDER_TYPE_SELL, "Initial Position"); //--- Open an initial SELL position. ArrayResize(recoveryArray, ArraySize(recoveryArray) + 1); //--- Resize the recovery array. recoveryArray[ArraySize(recoveryArray) - 1] = newRecovery; //--- Add the new recovery to the array. } }
Here, we focus on detecting new bars and generating trading signals based on the indicator crossing certain levels. First, we retrieve the time of the current bar using the iTime function, which is stored in the "currentBarTime" variable. We then compare "currentBarTime" with "lastBarTime" to check if a new bar has formed. If the two values differ, it indicates a new bar has formed, so we update "lastBarTime" to the value of "currentBarTime" to prevent processing the same bar multiple times.
Next, we evaluate conditions for RSI-based signals. If the RSI value in the "rsiBuffer[1]" (previous bar) is greater than 30 and the current value in "rsiBuffer[0]" (current bar) is less than or equal to 30, it signifies an oversold condition with a crossing up. In this case, we print a "BUY SIGNAL" message and initiate a new "PositionRecovery" instance named "newRecovery". We then call the "Initialize" method of "newRecovery" to set up the recovery parameters, including the "inputlot", "inputzonesizepts", "inputzonetragetpts", "inputlotmultiplier", symbol, order type as ORDER_TYPE_BUY, and the current bid price from "SymbolInfoDouble" function. Following the initialization, we open an initial "BUY" position using the "OpenTrade" method, passing "ORDER_TYPE_BUY" and a descriptive comment.
Similarly, if the "RSI" value in "rsiBuffer[1]" is less than 70 and the current value in "rsiBuffer[0]" is greater than or equal to 70, it indicates an overbought condition with a crossing down. In this scenario, we print a "SELL SIGNAL" message and create a new "PositionRecovery" instance. After initializing it with the same parameters but setting the order type to ORDER_TYPE_SELL, we open an initial "SELL" position using the "OpenTrade" method.
Finally, for both "BUY" and "SELL" signals, we add the initialized "PositionRecovery" instance to the "recoveryArray". The array is resized using the ArrayResize function, and the new instance is assigned to the last position of the array, ensuring that each recovery is tracked independently. This logic is now responsible for initiating the positions baskets with initial positions and conditions. To handle the position management, we will need to loop via the baskets in the main basket and apply the management logic as in the main structure on every tick. Here is the logic.
for (int i = 0; i < ArraySize(recoveryArray); i++) { //--- Iterate through all recovery instances. recoveryArray[i].ManageZones(); //--- Manage zones for each recovery instance. recoveryArray[i].CheckCloseAtTargets(); //--- Check and close positions at targets. recoveryArray[i].ApplyTrailingStop(); //--- Apply trailing stop logic to initial positions. }
To handle the positions management independently, we use a for loop to iterate through all recovery instances stored in the "recoveryArray". This loop ensures that each recovery instance is managed separately, allowing the system to maintain independent control over multiple recovery scenarios. The loop begins with the index "i" set to 0 and continues until all elements in the "recoveryArray" have been processed, as determined by the ArraySize function.
Within the loop, three essential methods are called on each recovery instance. First, the "ManageZones" method is invoked by using the dot operator, which monitors price movements relative to the defined recovery zones. If the price exits the zone boundaries, this method takes action by attempting to open a recovery position, dynamically adjusting the lot size according to the specified multiplier.
Next, the "CheckCloseAtTargets" method is executed to evaluate whether the price has reached the target levels for the recovery instance. If the target conditions are met, this method closes all associated positions and resets the recovery instance, ensuring that profits are secured and the instance is ready for a new cycle.
Finally, the "ApplyTrailingStop" method is applied, which enforces trailing stop logic for the initial position of the recovery instance. This method adjusts the trailing stop level dynamically as the price moves favorably, locking in profits. If the price reverses and hits the trailing stop, the method ensures the position is closed, protecting against potential losses.
By processing each recovery instance in this manner, the system effectively manages multiple independent positions, ensuring that all recovery scenarios are handled dynamically and in alignment with predefined strategies. To ensure that the program is working correctly, we run it, and here is the outcome.
From the image, we can see that the strategy reset after closing a recovery instance, which is one of the main objectives of the system. However, the closure does not interfere with the other running instances, which means that the instance is handled independently of the other instances in the array. To confirm this, we shift to the trade tab and can see that there are active instances.
From the image, we can see that there are still recovery instances that are in existence, and two are in recovery mode already. They are perfectly distinguished by the comments added to them in the right section, indicating whether they are initial or recovery positions. This verifies that we have successfully achieved our objective, and what remains is to backtest the program and analyze its performance. This is handled in the next section.
Backtesting
To evaluate the program's performance and robustness, we must first simulate historical market conditions. By doing so, we can determine how well the program handles recovery scenarios, adjusts to price movements, and manages trades. The backtest gives us important information about the strategy's profitability, drawdown levels, and risk management. The program generates buy and sell signals based on the established thresholds (e.g., oversold at 30 and overbought at 70; just as the user likes) by processing historical price data tick by tick to replicate real-market conditions. When a signal is generated, the EA initializes a new recovery instance, executes the first trade, and tracks price movements within the designated recovery zones.
We rigorously test the system's dynamic recovery mechanism, which adjusts lot sizes using the multiplier and opens hedging positions when necessary, in various market conditions. The program evaluates recovery scenarios independently for each signal, ensuring that all trades are managed in isolation, as reflected in the handling of the "recoveryArray". This ensures that even with multiple active recovery instances, the strategy remains organized and adaptable. We tested the program for the previous 5 months using the following settings:
Upon completion, we have the following results:
Strategy tester graph:
Strategy tester report:
From the above images, we can see that the graphs are smooth, though bumpy when there is a correlation between the balance and equity, caused by the continued increase in the number of recovery levels the program executes per instance and the number of signals generated. Thus, we can limit the number of recovery positions by enabling the trailing stop feature. Here are the results we get.
From the image, we can see that the number of trades reduces and the win rate increases when the trailing stop feature is enabled. We can further limit the number of positions by incorporating a trade count restriction logic where when there are already several open positions, don't consider opening more orders based on generated signals. To achieve this, we define extra input variables as follows:
input bool inputEnablePositionsRestriction = true; // Enable Maximum positions restriction input int inputMaximumPositions = 11; // Maximum number of positions
These input variables contain a flag to enable or disable the restriction option and the second one contains the maximum number of positions that can be initiated in the system when the restriction is enabled. We then adopt the logic on the OnTick event handler when a signal is confirmed, adding the extra layer of trading restriction.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { if (CopyBuffer(rsiHandle, 0, 1, 2, rsiBuffer) <= 0) { //--- Copy the RSI buffer values. Print("Failed to copy RSI buffer. Error: ", GetLastError()); //--- Print error on failure. return; //--- Exit on failure. } datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); //--- Get the time of the current bar. if (currentBarTime != lastBarTime) { //--- Check if a new bar has formed. lastBarTime = currentBarTime; //--- Update the last processed bar time. if (rsiBuffer[1] > 30 && rsiBuffer[0] <= 30) { //--- Check for oversold RSI crossing up. Print("BUY SIGNAL"); if (inputEnablePositionsRestriction == false || inputMaximumPositions > PositionsTotal()){ PositionRecovery newRecovery; //--- Create a new recovery instance. newRecovery.Initialize(inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, _Symbol, ORDER_TYPE_BUY, SymbolInfoDouble(_Symbol, SYMBOL_BID)); //--- Initialize the recovery. newRecovery.OpenTrade(ORDER_TYPE_BUY, "Initial Position"); //--- Open an initial BUY position. ArrayResize(recoveryArray, ArraySize(recoveryArray) + 1); //--- Resize the recovery array. recoveryArray[ArraySize(recoveryArray) - 1] = newRecovery; //--- Add the new recovery to the array. } else { Print("FAILED: Maximum positions threshold hit!"); } } else if (rsiBuffer[1] < 70 && rsiBuffer[0] >= 70) { //--- Check for overbought RSI crossing down. Print("SELL SIGNAL"); if (inputEnablePositionsRestriction == false || inputMaximumPositions > PositionsTotal()){ PositionRecovery newRecovery; //--- Create a new recovery instance. newRecovery.Initialize(inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, _Symbol, ORDER_TYPE_SELL, SymbolInfoDouble(_Symbol, SYMBOL_BID)); //--- Initialize the recovery. newRecovery.OpenTrade(ORDER_TYPE_SELL, "Initial Position"); //--- Open an initial SELL position. ArrayResize(recoveryArray, ArraySize(recoveryArray) + 1); //--- Resize the recovery array. recoveryArray[ArraySize(recoveryArray) - 1] = newRecovery; //--- Add the new recovery to the array. } else { Print("FAILED: Maximum positions threshold hit!"); } } } for (int i = 0; i < ArraySize(recoveryArray); i++) { //--- Iterate through all recovery instances. recoveryArray[i].ManageZones(); //--- Manage zones for each recovery instance. recoveryArray[i].CheckCloseAtTargets(); //--- Check and close positions at targets. recoveryArray[i].ApplyTrailingStop(); //--- Apply trailing stop logic to initial positions. } }
Here, we implement a mechanism to manage position restrictions and control the number of trades the Expert Advisor (EA) can open at any given time. The logic begins by evaluating whether position restrictions are disabled ("inputEnablePositionsRestriction" == false) or if the total number of currently open positions (PositionsTotal) is below the user-defined maximum ("inputMaximumPositions"). If either condition is met, the EA proceeds to open a new trade, ensuring it aligns with the user's preferences for unrestricted or limited trading.
However, if both conditions fail—indicating that position restrictions are enabled and the maximum allowable number of positions has been reached—the EA will not open a new trade. Instead, it logs a failure message to the terminal: "FAILED: Maximum positions threshold hit!". This message serves as an informative feedback mechanism, helping the user understand why additional trades were not executed. We have highlighted the changes in light yellow color for clarity. Upon testing, we get the following results.
From the image, we can see that the number of trades reduces further, and the win rate increases further. This verifies that we achieved our objective of creating a multi-zone recovery system. In a Graphic Interchange Format (GIF) visualization, we have the following simulation, confirming the achievement of our objective.
Conclusion
In conclusion, this article has illustrated the process of constructing a robust MQL5 Expert Advisor based on a multi-level Zone Recovery strategy. By leveraging core concepts such as automated signal detection, dynamic recovery management, and profit-securing mechanisms like trailing stops, we have created a flexible system capable of handling multiple independent recovery instances. Key components of this implementation include trade signal generation, position restriction logic, and efficient handling of both recovery and exit strategies.
Disclaimer: This article is intended as an educational resource for MQL5 programming. While the presented multi-level Zone Recovery system provides a structured framework for trade management, market behavior is inherently uncertain. Trading involves financial risk, and historical success does not guarantee future outcomes. Comprehensive testing and effective risk management are imperative before deploying any strategy in live markets.
By following the methodologies discussed in this guide, you can expand your expertise in algorithmic trading and apply these principles to create even more sophisticated trading systems. Happy coding, and may your trading endeavors be successful!





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