
Automating Trading Strategies in MQL5 (Part 23): Zone Recovery with Trailing and Basket Logic
Introduction
In our previous article (Part 22), we developed a Zone Recovery System for Envelopes Trend Trading in MetaQuotes Language 5 (MQL5), using Relative Strength Index (RSI) and Envelopes indicators to automate trades and manage losses through structured recovery zones. In Part 23, we refine this strategy by incorporating trailing stops to dynamically secure profits and a multi-basket system to efficiently handle multiple trade signals, thereby enhancing adaptability in volatile markets. We will cover the following topics:
- Understanding the Enhanced Trailing Stop and Multi-Basket Architecture
- Implementation in MQL5
- Backtesting
- Conclusion
By the end, you’ll have a refined MQL5 trading system with advanced features, ready for testing and further customization—let’s dive in!
Understanding the Enhanced Trailing Stop and Multi-Basket Architecture
The zone recovery strategy we’re enhancing is designed to turn potential losses into wins by placing counter-trades within a defined price range when the market moves against us. We’re now strengthening it with two key improvements: trailing stops and multi-basket trading. Trailing stops are necessary because they allow us to lock in profits as the market moves in our favor, protecting gains without closing trades too early, which is critical in trending markets where prices can run significantly. Multi-basket trading is equally important, as it lets us manage multiple independent trade signals simultaneously, increasing our ability to capture more opportunities while keeping risk organized across separate trade groups. See below.
We will achieve these enhancements by integrating a trailing stop mechanism that adjusts the stop-loss level dynamically based on market movement, ensuring we secure profits while giving trades room to grow. For multi-basket trading, we will introduce a system to handle multiple trade instances, each with its unique identifier, allowing us to track and manage several zone recovery cycles at once without overlap. We plan to combine these features with the existing Relative Strength Indicator (RSI) and Envelopes indicators to maintain precise trade entries, while the trailing stops and basket system work together to optimize profit protection and trade capacity, making the strategy more robust and adaptable to various market conditions. Stay with us as we bring these improvements to life!
Implementation in MQL5
To implement the enhancements in MQL5, we will add some extra user inputs for the trailing stop feature, and rename the maximum cap order limit since we are now dealing with multiple recovery instances.
input group "======= EA GENERAL SETTINGS =======" input TradingLotSizeOptions lotOption = UNFIXED_LOTSIZE; // Lot Size Option input double initialLotSize = 0.01; // Initial Lot Size input double riskPercentage = 1.0; // Risk Percentage (%) input int riskPoints = 300; // Risk Points input int baseMagicNumber = 123456789; // Base Magic Number input int maxInitialPositions = 1; // Maximum Initial Positions (Baskets/Signals) input double zoneTargetPoints = 600; // Zone Target Points input double zoneSizePoints = 300; // Zone Size Points input bool enableInitialTrailing = true; // Enable Trailing Stop for Initial Positions input int trailingStopPoints = 50; // Trailing Stop Points input int minProfitPoints = 50; // Minimum Profit Points to Start Trailing
We start the enhancement of our Zone Recovery System for Envelopes Trend Trading in MQL5 by updating the input parameters under the "EA GENERAL SETTINGS" group to support trailing stops and multi-basket trading. We make four key changes to the inputs. First, we rename "magicNumber" to "baseMagicNumber", set to 123456789, to serve as a starting point for generating unique magic numbers for multiple trade baskets, ensuring each basket is tracked separately for our multi-basket system. Second, we replace "maxOrders" with "maxInitialPositions", set to 1, to limit the number of initial trade baskets, allowing us to manage multiple trade signals efficiently.
Third, we add "enableInitialTrailing", a boolean set to true, to let us enable or disable trailing stops for initial positions, providing control over our new profit-locking feature. Fourth, we introduce "trailingStopPoints" set to 50 and "minProfitPoints" set to 50, defining the trailing stop distance and the minimum profit needed to activate it, respectively, to implement dynamic profit protection. These changes will enable our system to handle multiple trade baskets and protect profits effectively, setting the stage for further enhancements. We will be highlighting changes to enable easier tracking of the changes and avoid confusion. Upon compilation, we have the following input set.
After adding the inputs, we can now forward declare the "MarketZoneTrader" class so it can be accessed by the base class, since we now want to handle multiple trade instances.
//--- Forward Declaration of MarketZoneTrader class MarketZoneTrader;
Here, we introduce a forward declaration of the "MarketZoneTrader" class. We add it before the "BasketManager" class definition, which we will define just after this class, to allow it to reference "MarketZoneTrader" without requiring its full definition yet. This change is necessary because our new multi-basket system, managed by "BasketManager", will need to create and handle multiple instances of "MarketZoneTrader" for different trade baskets. By declaring "MarketZoneTrader" first, we ensure the compiler recognizes it when used in the new class, enabling our system to support multiple simultaneous trade cycles efficiently. We can then define the manager class.
//--- Basket Manager Class to Handle Multiple Traders class BasketManager { private: MarketZoneTrader* m_traders[]; //--- Array of trader instances int m_handleRsi; //--- RSI indicator handle int m_handleEnvUpper; //--- Upper Envelopes handle int m_handleEnvLower; //--- Lower Envelopes handle double m_rsiBuffer[]; //--- RSI data buffer double m_envUpperBandBuffer[]; //--- Upper Envelopes buffer double m_envLowerBandBuffer[]; //--- Lower Envelopes buffer string m_symbol; //--- Trading symbol int m_baseMagicNumber; //--- Base magic number int m_maxInitialPositions; //--- Maximum baskets (signals) //--- Initialize Indicators bool initializeIndicators() { m_handleRsi = iRSI(m_symbol, PERIOD_CURRENT, 8, PRICE_CLOSE); if (m_handleRsi == INVALID_HANDLE) { Print("Failed to initialize RSI indicator"); return false; } m_handleEnvUpper = iEnvelopes(m_symbol, PERIOD_CURRENT, 150, 0, MODE_SMA, PRICE_CLOSE, 0.1); if (m_handleEnvUpper == INVALID_HANDLE) { Print("Failed to initialize upper Envelopes indicator"); return false; } m_handleEnvLower = iEnvelopes(m_symbol, PERIOD_CURRENT, 95, 0, MODE_SMA, PRICE_CLOSE, 1.4); if (m_handleEnvLower == INVALID_HANDLE) { Print("Failed to initialize lower Envelopes indicator"); return false; } ArraySetAsSeries(m_rsiBuffer, true); ArraySetAsSeries(m_envUpperBandBuffer, true); ArraySetAsSeries(m_envLowerBandBuffer, true); return true; } }
To help manage basket trades, we define the "BasketManager" class with private members to manage multiple instances of the "MarketZoneTrader" class and indicator data. We create "m_traders", an array of "MarketZoneTrader" pointers, to store individual trade baskets, each representing a separate zone recovery cycle. This change is critical as it allows us to manage multiple trade signals simultaneously, unlike the single-instance approach in the prior version. We also declare "m_handleRsi", "m_handleEnvUpper", and "m_handleEnvLower" to hold indicator handles, and "m_rsiBuffer", "m_envUpperBandBuffer", and "m_envLowerBandBuffer" arrays to store RSI and Envelopes data, moving indicator management from "MarketZoneTrader" to "BasketManager" for centralized control across baskets.
Additionally, we add "m_symbol" to store the trading symbol, "m_baseMagicNumber" for generating unique magic numbers per basket, and "m_maxInitialPositions" to limit the number of active baskets, aligning with the new "maxInitialPositions" input. In the "initializeIndicators" function, we set up the RSI indicator with iRSI using an 8-period setting and Envelopes indicators with iEnvelopes (150-period with 0.1 deviation and 95-period with 1.4 deviation), checking for "INVALID_HANDLE" and logging failures with Print. We configure "m_rsiBuffer", "m_envUpperBandBuffer", and "m_envLowerBandBuffer" as time-series arrays using ArraySetAsSeries. This new class structure will enable us to coordinate multiple trade baskets efficiently, centralizing indicator data for consistent signal generation across all baskets. We then need to have a logic to count all the individual basket positions for easier tracking, and clean the baskets.
//--- Count Active Baskets int countActiveBaskets() { int count = 0; for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL && m_traders[i].getCurrentState() != MarketZoneTrader::INACTIVE) { count++; } } return count; } //--- Cleanup Terminated Baskets void cleanupTerminatedBaskets() { int newSize = 0; for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL && m_traders[i].getCurrentState() == MarketZoneTrader::INACTIVE) { delete m_traders[i]; m_traders[i] = NULL; } if (m_traders[i] != NULL) newSize++; } MarketZoneTrader* temp[]; ArrayResize(temp, newSize); int index = 0; for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) { temp[index] = m_traders[i]; index++; } } ArrayFree(m_traders); ArrayResize(m_traders, newSize); for (int i = 0; i < newSize; i++) { m_traders[i] = temp[i]; } ArrayFree(temp); }
Here, we add two new functions to the "BasketManager" class, "countActiveBaskets" and "cleanupTerminatedBaskets". We start with the "countActiveBaskets" function to track the number of active trade baskets. We initialize a "count" variable to 0 and loop through the "m_traders" array using the ArraySize function. For each non-null "m_traders" entry, we check if its state, obtained via "getCurrentState", is not "MarketZoneTrader::INACTIVE". If active, we increment "count". We return "count" to monitor how many baskets are currently running, which is crucial for ensuring we stay within the "m_maxInitialPositions" limit when opening new baskets.
Next, we create the "cleanupTerminatedBaskets" function to remove inactive baskets and optimize memory. We first count non-null entries in "m_traders" by looping through the array. If a trader is not null and its "getCurrentState" returns "MarketZoneTrader::INACTIVE", we use "delete" to free its memory and set the entry to "NULL". We track the number of remaining non-null traders in "newSize". Then, we create a temporary "temp" array, resize it to "newSize" with ArrayResize, and copy non-null traders from "m_traders" to "temp" using an "index" counter. We clear "m_traders" with "ArrayFree", resize it to "newSize", and transfer the traders back from "temp". Finally, we free "temp" with ArrayFree. This cleanup ensures we remove terminated baskets, keeping our system efficient and ready for new trades. We then move to the public access modifier, where we will change how we handle the constructor and destructor in initializing and destroying the class members and elements.
public: BasketManager(string symbol, int baseMagic, int maxInitPos) { m_symbol = symbol; m_baseMagicNumber = baseMagic; m_maxInitialPositions = maxInitPos; ArrayResize(m_traders, 0); m_handleRsi = INVALID_HANDLE; m_handleEnvUpper = INVALID_HANDLE; m_handleEnvLower = INVALID_HANDLE; } ~BasketManager() { for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) delete m_traders[i]; } ArrayFree(m_traders); cleanupIndicators(); }
We start with the "BasketManager" constructor, which takes "symbol", "baseMagic", and "maxInitPos" as parameters. We assign these to "m_symbol", "m_baseMagicNumber", and "m_maxInitialPositions", respectively, to set the trading symbol, base magic number for unique basket identification, and the maximum number of active baskets. We initialize the "m_traders" array to zero size using the ArrayResize function and set indicator handles—"m_handleRsi", "m_handleEnvUpper", and "m_handleEnvLower"—to "INVALID_HANDLE" to prepare for indicator setup later. This constructor is crucial for configuring the multi-basket system.
Next, we create the "~BasketManager" destructor to clean up resources. Typically, destructors have the tilde sign as the prefix, just as a reminder. We loop through the "m_traders" array using ArraySize and delete any non-null "MarketZoneTrader" instances with delete to free their memory. We then clear the "m_traders" array with ArrayFree and call "cleanupIndicators" to release indicator handles and buffers. This ensures our system shuts down cleanly, preventing memory leaks when the EA stops. In the prior version, we had to add the deletion logic in the OnDeinit event handler directly after realizing that there was a memory leak, but here, we can add it early since we already know we need to take care of memory leaks. We then need to alter the initialization logic so that it can load existing positions into respective baskets. Here is the logic we implement to achieve that.
bool initialize() { if (!initializeIndicators()) return false; //--- Load existing positions into baskets int totalPositions = PositionsTotal(); for (int i = 0; i < totalPositions; i++) { ulong ticket = PositionGetTicket(i); if (PositionSelectByTicket(ticket)) { if (PositionGetString(POSITION_SYMBOL) == m_symbol) { long magic = PositionGetInteger(POSITION_MAGIC); if (magic >= m_baseMagicNumber && magic < m_baseMagicNumber + m_maxInitialPositions) { //--- Check if basket already exists for this magic bool exists = false; for (int j = 0; j < ArraySize(m_traders); j++) { if (m_traders[j] != NULL && m_traders[j].getMagicNumber() == magic) { exists = true; break; } } if (!exists && countActiveBaskets() < m_maxInitialPositions) { createNewBasket(magic, ticket); } } } } } Print("BasketManager initialized with ", ArraySize(m_traders), " existing baskets"); return true; } /* //--- PREVIOUS INITIALIZATION int initialize() { //--- Initialization Start m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); //--- Set magic number int totalPositions = PositionsTotal(); //--- Get total positions for (int i = 0; i < totalPositions; i++) { //--- Iterate positions ulong ticket = PositionGetTicket(i); //--- Get ticket if (PositionSelectByTicket(ticket)) { //--- Select position if (PositionGetString(POSITION_SYMBOL) == m_tradeConfig.marketSymbol && PositionGetInteger(POSITION_MAGIC) == m_tradeConfig.tradeIdentifier) { //--- Check symbol and magic if (activateTrade(ticket)) { //--- Activate position Print("Existing position activated: Ticket=", ticket); //--- Log activation } else { Print("Failed to activate existing position: Ticket=", ticket); //--- Log failure } } } } m_handleRsi = iRSI(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 8, PRICE_CLOSE); //--- Initialize RSI if (m_handleRsi == INVALID_HANDLE) { //--- Check RSI Print("Failed to initialize RSI indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } m_handleEnvUpper = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 150, 0, MODE_SMA, PRICE_CLOSE, 0.1); //--- Initialize upper Envelopes if (m_handleEnvUpper == INVALID_HANDLE) { //--- Check upper Envelopes Print("Failed to initialize upper Envelopes indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } m_handleEnvLower = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 95, 0, MODE_SMA, PRICE_CLOSE, 1.4); //--- Initialize lower Envelopes if (m_handleEnvLower == INVALID_HANDLE) { //--- Check lower Envelopes Print("Failed to initialize lower Envelopes indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } ArraySetAsSeries(m_rsiBuffer, true); //--- Set RSI buffer ArraySetAsSeries(m_envUpperBandBuffer, true); //--- Set upper Envelopes buffer ArraySetAsSeries(m_envLowerBandBuffer, true); //--- Set lower Envelopes buffer Print("EA initialized successfully"); //--- Log success return INIT_SUCCEEDED; //--- Return success //--- Initialization End } */
Here, we implement the updated "initialize" function in the "BasketManager" class to support our multi-basket trading improvement by initializing indicators and loading existing positions into separate baskets. We start by calling "initializeIndicators" to set up RSI and Envelopes indicators, returning false if it fails, ensuring our system has the necessary market data. Unlike the previous version, where we handled indicator setup directly in "MarketZoneTrader"’s "initialize" function, we now centralize this in "BasketManager" to share indicator data across multiple baskets. Next, we check for existing positions using the PositionsTotal function and loop through each position, grabbing its "ticket" with the PositionGetTicket function.
If PositionSelectByTicket succeeds and the position’s symbol matches "m_symbol" (via PositionGetString), we verify its magic number, obtained with "PositionGetInteger", falls within the range of "m_baseMagicNumber" to "m_baseMagicNumber + m_maxInitialPositions". We then check if a basket already exists for this magic number by looping through "m_traders" and calling "getMagicNumber" on non-null entries. If no basket exists and "countActiveBaskets" is below "m_maxInitialPositions", we call "createNewBasket" with the magic number and "ticket" to load the position into a new basket. Finally, we log the number of initialized baskets with "Print" using ArraySize of "m_traders" and return true. When we run the program, we get the following result.
We can now move on to processing ticks, where we need to process the existing baskets on every tick and create new baskets when new signals are confirmed in the "processTick" function, unlike in the previous version, where we only needed to initiate trades based on confirmed signals.
void processTick() { //--- Process existing baskets for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) { m_traders[i].processTick(m_rsiBuffer, m_envUpperBandBuffer, m_envLowerBandBuffer); } } cleanupTerminatedBaskets(); //--- Check for new signals on new bar if (!isNewBar()) return; if (!CopyBuffer(m_handleRsi, 0, 0, 3, m_rsiBuffer)) { Print("Error loading RSI data. Reverting."); return; } if (!CopyBuffer(m_handleEnvUpper, 0, 0, 3, m_envUpperBandBuffer)) { Print("Error loading upper envelopes data. Reverting."); return; } if (!CopyBuffer(m_handleEnvLower, 1, 0, 3, m_envLowerBandBuffer)) { Print("Error loading lower envelopes data. Reverting."); return; } const int rsiOverbought = 70; const int rsiOversold = 30; int ticket = -1; ENUM_ORDER_TYPE signalType = (ENUM_ORDER_TYPE)-1; double askPrice = NormalizeDouble(SymbolInfoDouble(m_symbol, SYMBOL_ASK), Digits()); double bidPrice = NormalizeDouble(SymbolInfoDouble(m_symbol, SYMBOL_BID), Digits()); if (m_rsiBuffer[1] < rsiOversold && m_rsiBuffer[2] > rsiOversold && m_rsiBuffer[0] < rsiOversold) { if (askPrice > m_envUpperBandBuffer[0]) { if (countActiveBaskets() < m_maxInitialPositions) { signalType = ORDER_TYPE_BUY; } } } else if (m_rsiBuffer[1] > rsiOverbought && m_rsiBuffer[2] < rsiOverbought && m_rsiBuffer[0] > rsiOverbought) { if (bidPrice < m_envLowerBandBuffer[0]) { if (countActiveBaskets() < m_maxInitialPositions) { signalType = ORDER_TYPE_SELL; } } } if (signalType != (ENUM_ORDER_TYPE)-1) { //--- Create new basket with unique magic number int newMagic = m_baseMagicNumber + ArraySize(m_traders); if (newMagic < m_baseMagicNumber + m_maxInitialPositions) { MarketZoneTrader* newTrader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints, newMagic); ticket = newTrader.openInitialOrder(signalType); //--- Open INITIAL position if (ticket > 0 && newTrader.activateTrade(ticket)) { int size = ArraySize(m_traders); ArrayResize(m_traders, size + 1); m_traders[size] = newTrader; Print("New basket created: Magic=", newMagic, ", Ticket=", ticket, ", Type=", EnumToString(signalType)); } else { delete newTrader; Print("Failed to create new basket: Ticket=", ticket); } } else { Print("Maximum initial positions (baskets) reached: ", m_maxInitialPositions); } } }
In the function, we start by looping through the "m_traders" array using the ArraySize function and, for each non-null "MarketZoneTrader" instance, we call its "processTick" function, passing "m_rsiBuffer", "m_envUpperBandBuffer", and "m_envLowerBandBuffer" to handle individual basket logic. This differs from the previous version, where "processTick" directly managed a single trade cycle. We then call "cleanupTerminatedBaskets" to remove inactive baskets, ensuring efficient resource use. Next, we check for new trade signals only on a new bar using "isNewBar", exiting if false to save resources.
We load indicator data with CopyBuffer for "m_handleRsi", "m_handleEnvUpper", and "m_handleEnvLower" into their respective buffers, logging errors with "Print" and exiting if any fail, unlike the previous version, where this was done in "MarketZoneTrader". We set "rsiOverbought" to 70 and "rsiOversold" to 30, and initialize "ticket" and "signalType". We fetch "askPrice" and "bidPrice" using SymbolInfoDouble with "SYMBOL_ASK" and SYMBOL_BID, normalized with the NormalizeDouble function.
For a buy signal, if "m_rsiBuffer" indicates oversold conditions and "askPrice" exceeds "m_envUpperBandBuffer", we set "signalType" to ORDER_TYPE_BUY if "countActiveBaskets" is below "m_maxInitialPositions". For a sell signal, if "m_rsiBuffer" shows overbought conditions and "bidPrice" is below "m_envLowerBandBuffer", we set "signalType" to ORDER_TYPE_SELL. If a valid "signalType" exists, we create a unique magic number with "m_baseMagicNumber" plus "ArraySize(m_traders)", and if within "m_maxInitialPositions", we instantiate a new "MarketZoneTrader" with input parameters and the new magic number.
We call "openInitialOrder" with "signalType", and if the returned "ticket" is valid and "activateTrade" succeeds, we add the new trader to "m_traders" using ArrayResize and log success with "Print" and the EnumToString function. Otherwise, we delete the trader and log the failure, or note if the basket limit is reached. Once the new trades are opened, we will need to create new baskets for them. Here is the logic we use to achieve that.
private: void createNewBasket(long magic, ulong ticket) { MarketZoneTrader* newTrader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints, magic); if (newTrader.activateTrade(ticket)) { int size = ArraySize(m_traders); ArrayResize(m_traders, size + 1); m_traders[size] = newTrader; Print("Existing position loaded into basket: Magic=", magic, ", Ticket=", ticket); } else { delete newTrader; Print("Failed to load existing position into basket: Ticket=", ticket); } }
We implement the "createNewBasket" function in the private section of the "BasketManager" class, a new addition to support our multi-basket trading improvement by creating and managing new trade baskets for existing positions. We start by creating a new "MarketZoneTrader" instance, named "newTrader", using the input parameters "lotOption", "initialLotSize", "riskPercentage", "riskPoints", "zoneTargetPoints", "zoneSizePoints", and the provided "magic" number to configure a unique trade basket. Recall that we had this user input in the initialization stage in the prior version because we just needed one instance of the zone, so it did apply to all new positions, but in this case, we organize it in new class instances. Here is the code for that for quicker comparison.
//--- PREVIOUS VERSION OF NEW CLASS INSTANCE //--- Global Instance MarketZoneTrader *trader = NULL; //--- Declare trader instance int OnInit() { //--- EA Initialization Start trader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, maxOrders, restrictMaxOrders, zoneTargetPoints, zoneSizePoints); //--- Create trader instance return trader.initialize(); //--- Initialize EA //--- EA Initialization End }
We then call "activateTrade" on "newTrader" with the given "ticket" to load the existing position into the basket. If successful, we get the current size of the "m_traders" array using ArraySize, expand it by one with ArrayResize, and add "newTrader" to the new slot. We log the success with "Print", including the "magic" and "ticket" values. If "activateTrade" fails, we delete "newTrader" to free memory and log the failure with "Print". The function will now enable us to organize existing positions into separate baskets, a key feature of our multi-basket system, unlike the single-instance approach in the previous version. That class will now enable us to manage the trade baskets effectively. Let us then graduate to modifying the base class so that it can contain the new multiple baskets and trailing stop features. Let us start with its members.
//--- Modified MarketZoneTrader Class class MarketZoneTrader { private: enum TradeState { INACTIVE, RUNNING, TERMINATING }; struct TradeMetrics { bool operationSuccess; double totalVolume; double netProfitLoss; }; struct ZoneBoundaries { double zoneHigh; double zoneLow; double zoneTargetHigh; double zoneTargetLow; }; struct TradeConfig { string marketSymbol; double openPrice; double initialVolume; long tradeIdentifier; string initialTradeLabel; //--- Label for initial positions string recoveryTradeLabel; //--- Label for recovery positions ulong activeTickets[]; ENUM_ORDER_TYPE direction; double zoneProfitSpan; double zoneRecoverySpan; double accumulatedBuyVolume; double accumulatedSellVolume; TradeState currentState; bool hasRecoveryTrades; //--- Flag to track recovery trades double trailingStopLevel; //--- Virtual trailing stop level }; struct LossTracker { double tradeLossTracker; }; TradeConfig m_tradeConfig; ZoneBoundaries m_zoneBounds; LossTracker m_lossTracker; string m_lastError; int m_errorStatus; CTrade m_tradeExecutor; TradingLotSizeOptions m_lotOption; double m_initialLotSize; double m_riskPercentage; int m_riskPoints; double m_zoneTargetPoints; double m_zoneSizePoints; }
Here, we enhance our program by modifying the "MarketZoneTrader" class, specifically its private section, to include new features supporting trailing stops and improved trade labeling. We retain the core structure but introduce key changes to the "TradeConfig" structure to align with our enhanced strategy. We keep the "TradeState" enumeration with "INACTIVE", "RUNNING", and "TERMINATING" states, and the "TradeMetrics", "ZoneBoundaries", and "LossTracker" structures unchanged from the previous version, as they continue to manage trade states, performance metrics, zone boundaries, and loss tracking.
In the "TradeConfig" structure, we add two new string variables: "initialTradeLabel" and "recoveryTradeLabel". These labels allow us to tag initial and recovery trades separately, improving trade identification and tracking within each basket, especially useful for managing multiple baskets in our new system. We also introduce "hasRecoveryTrades", a boolean to track whether a basket includes recovery trades, which is critical for enabling or disabling trailing stops appropriately. Additionally, we add "trailingStopLevel", a double to store the virtual trailing stop level for each basket, enabling dynamic profit protection for initial trades.
Among the member variables, we retain "m_tradeConfig", "m_zoneBounds", "m_lossTracker", "m_lastError", "m_errorStatus", "m_tradeExecutor", "m_lotOption", "m_initialLotSize", "m_riskPercentage", "m_riskPoints", "m_zoneTargetPoints", and "m_zoneSizePoints" as they were, but their roles now support the new trailing stop and multi-basket functionality within each "MarketZoneTrader" instance. Notably, we remove the indicator-related variables like "m_handleRsi" and "m_rsiBuffer" from the class, as these are now managed centrally by the "BasketManager" class, streamlining each trader’s focus on individual basket operations. In the constructor and destructor, we will need to slightly change some variables so that they handle the new features.
public: MarketZoneTrader(TradingLotSizeOptions lotOpt, double initLot, double riskPct, int riskPts, double targetPts, double sizePts, long magic) { m_tradeConfig.currentState = INACTIVE; ArrayResize(m_tradeConfig.activeTickets, 0); m_tradeConfig.zoneProfitSpan = targetPts * _Point; m_tradeConfig.zoneRecoverySpan = sizePts * _Point; m_lossTracker.tradeLossTracker = 0.0; m_lotOption = lotOpt; m_initialLotSize = initLot; m_riskPercentage = riskPct; m_riskPoints = riskPts; m_zoneTargetPoints = targetPts; m_zoneSizePoints = sizePts; m_tradeConfig.marketSymbol = _Symbol; m_tradeConfig.tradeIdentifier = magic; m_tradeConfig.initialTradeLabel = "EA_INITIAL_" + IntegerToString(magic); //--- Label for initial positions m_tradeConfig.recoveryTradeLabel = "EA_RECOVERY_" + IntegerToString(magic); //--- Label for recovery positions m_tradeConfig.hasRecoveryTrades = false; //--- Initialize recovery flag m_tradeConfig.trailingStopLevel = 0.0; //--- Initialize trailing stop m_tradeExecutor.SetExpertMagicNumber(magic); } ~MarketZoneTrader() { ArrayFree(m_tradeConfig.activeTickets); }
We start with the "MarketZoneTrader" constructor, now accepting an additional "magic" parameter to assign a unique magic number for each trade basket, unlike the previous version that used a fixed magic number. To support improved trade labeling, we add "m_tradeConfig.initialTradeLabel" as "EA_INITIAL" plus "magic" (via IntegerToString) and "m_tradeConfig.recoveryTradeLabel" as "EA_RECOVERY" plus "magic", enabling distinct identification of initial and recovery trades within a basket. We initialize "m_tradeConfig.hasRecoveryTrades" to false to track recovery trade status and set "m_tradeConfig.trailingStopLevel" to 0.0 for the virtual trailing stop, both new features. Finally, we configure "m_tradeExecutor" with "SetExpertMagicNumber" using "magic". We have highlighted the major changes for quick identification.
Next, we simplify the "~MarketZoneTrader" destructor compared to the previous version, which was called "cleanup". We now only clear "m_tradeConfig.activeTickets" with ArrayFree, as indicator cleanup is handled by "BasketManager", reducing the destructor’s scope to focus on basket-specific resources. We can then update the function responsible for activating trades so that it can initialize the trailing stop level and recovery state for initial trades.
bool activateTrade(ulong ticket) { m_tradeConfig.hasRecoveryTrades = false; m_tradeConfig.trailingStopLevel = 0.0; //--- THE REST OF THE LOGIC REMAINS return true; }
Here, we just add the logic to initialize the first trade's trailing stop level to 0 and recovery state to false to indicate it is the first position in the basket. Finally, we can add a function to open the initial position.
int openInitialOrder(ENUM_ORDER_TYPE orderType) { //--- Open INITIAL position based on signal int ticket; double openPrice; if (orderType == ORDER_TYPE_BUY) { openPrice = NormalizeDouble(getMarketAsk(), Digits()); } else if (orderType == ORDER_TYPE_SELL) { openPrice = NormalizeDouble(getMarketBid(), Digits()); } else { Print("Invalid order type [Magic=", m_tradeConfig.tradeIdentifier, "]"); return -1; } double lotSize = 0; if (m_lotOption == FIXED_LOTSIZE) { lotSize = m_initialLotSize; } else if (m_lotOption == UNFIXED_LOTSIZE) { lotSize = calculateLotSize(m_riskPercentage, m_riskPoints); } if (lotSize <= 0) { Print("Invalid lot size [Magic=", m_tradeConfig.tradeIdentifier, "]: ", lotSize); return -1; } if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, orderType, lotSize, openPrice, 0, 0, m_tradeConfig.initialTradeLabel)) { ticket = (int)m_tradeExecutor.ResultOrder(); Print("INITIAL trade opened [Magic=", m_tradeConfig.tradeIdentifier, "]: Ticket=", ticket, ", Type=", EnumToString(orderType), ", Volume=", lotSize); } else { ticket = -1; Print("Failed to open INITIAL order [Magic=", m_tradeConfig.tradeIdentifier, "]: Type=", EnumToString(orderType), ", Volume=", lotSize); } return ticket; }
We implement a new "openInitialOrder" function in the public section of the "MarketZoneTrader" class to support our multi-basket and improved trade labeling enhancements by opening initial positions for a specific trade basket with distinct identification. We start by initializing "ticket" and "openPrice". For "orderType" set to ORDER_TYPE_BUY, we set "openPrice" using "getMarketAsk" and normalize it with NormalizeDouble and "Digits". For "ORDER_TYPE_SELL", we use "getMarketBid". If "orderType" is invalid, we log an error with "Print", including "m_tradeConfig.tradeIdentifier", and return -1.
We determine "lotSize" based on "m_lotOption": for "FIXED_LOTSIZE", we use "m_initialLotSize"; for "UNFIXED_LOTSIZE", we call "calculateLotSize" with "m_riskPercentage" and "m_riskPoints". If "lotSize" is invalid, we log the error with "Print" and return -1. We then open the position using "m_tradeExecutor.PositionOpen" with "m_tradeConfig.marketSymbol", "orderType", "lotSize", "openPrice", and "m_tradeConfig.initialTradeLabel" for clear labeling of initial trades. On success, we set "ticket" with "ResultOrder" and log the trade with "Print", including "m_tradeConfig.tradeIdentifier" and the EnumToString function. On failure, we set "ticket" to -1 and log the error. Finally, we return the "ticket". Unlike the previous version’s "openOrder" function, this function uses the new "initialTradeLabel" and focuses solely on initial positions, aligning with our multi-basket system. Upon compilation, we get the following outcome.
From the image, we can see that we can open the initial trade and create a new basket instance for it. We now need to have trailing logic so that we can manage the trailing stop feature for the positions.
void evaluateMarketTick() { if (m_tradeConfig.currentState == INACTIVE) return; if (m_tradeConfig.currentState == TERMINATING) { finalizePosition(); return; } double currentPrice; double profitPoints = 0.0; //--- Handle BUY initial position if (m_tradeConfig.direction == ORDER_TYPE_BUY) { currentPrice = getMarketBid(); profitPoints = (currentPrice - m_tradeConfig.openPrice) / _Point; //--- Trailing Stop Logic for Initial Position if (enableInitialTrailing && !m_tradeConfig.hasRecoveryTrades && profitPoints >= minProfitPoints) { //--- Calculate desired trailing stop level double newTrailingStop = currentPrice - trailingStopPoints * _Point; //--- Start or update trailing stop if profit exceeds minProfitPoints + trailingStopPoints if (profitPoints >= minProfitPoints + trailingStopPoints) { if (m_tradeConfig.trailingStopLevel == 0.0 || newTrailingStop > m_tradeConfig.trailingStopLevel) { m_tradeConfig.trailingStopLevel = newTrailingStop; Print("Trailing stop updated [Magic=", m_tradeConfig.tradeIdentifier, "]: Level=", m_tradeConfig.trailingStopLevel, ", Profit=", profitPoints, " points"); } } //--- Check if price has hit trailing stop if (m_tradeConfig.trailingStopLevel > 0.0 && currentPrice <= m_tradeConfig.trailingStopLevel) { Print("Trailing stop triggered [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " <= TrailingStop=", m_tradeConfig.trailingStopLevel); finalizePosition(); return; } } //--- Zone Recovery Logic if (currentPrice > m_zoneBounds.zoneTargetHigh) { Print("Closing position [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); finalizePosition(); return; } else if (currentPrice < m_zoneBounds.zoneLow) { Print("Triggering RECOVERY trade [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice); } } //--- Handle SELL initial position else if (m_tradeConfig.direction == ORDER_TYPE_SELL) { currentPrice = getMarketAsk(); profitPoints = (m_tradeConfig.openPrice - currentPrice) / _Point; //--- Trailing Stop Logic for Initial Position if (enableInitialTrailing && !m_tradeConfig.hasRecoveryTrades && profitPoints >= minProfitPoints) { //--- Calculate desired trailing stop level double newTrailingStop = currentPrice + trailingStopPoints * _Point; //--- Start or update trailing stop if profit exceeds minProfitPoints + trailingStopPoints if (profitPoints >= minProfitPoints + trailingStopPoints) { if (m_tradeConfig.trailingStopLevel == 0.0 || newTrailingStop < m_tradeConfig.trailingStopLevel) { m_tradeConfig.trailingStopLevel = newTrailingStop; Print("Trailing stop updated [Magic=", m_tradeConfig.tradeIdentifier, "]: Level=", m_tradeConfig.trailingStopLevel, ", Profit=", profitPoints, " points"); } } //--- Check if price has hit trailing stop if (m_tradeConfig.trailingStopLevel > 0.0 && currentPrice >= m_tradeConfig.trailingStopLevel) { Print("Trailing stop triggered [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " >= TrailingStop=", m_tradeConfig.trailingStopLevel); finalizePosition(); return; } } //--- Zone Recovery Logic if (currentPrice < m_zoneBounds.zoneTargetLow) { Print("Closing position [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " < TargetLow=", m_zoneBounds.zoneTargetLow); finalizePosition(); return; } else if (currentPrice > m_zoneBounds.zoneHigh) { Print("Triggering RECOVERY trade [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " > ZoneHigh=", m_zoneBounds.zoneHigh); triggerRecoveryTrade(ORDER_TYPE_BUY, currentPrice); } } }
Here, we enhance the program by updating the "evaluateMarketTick" function to incorporate trailing stop logic while maintaining the existing zone recovery logic. We start by checking if "m_tradeConfig.currentState" is "INACTIVE" or "TERMINATING", exiting or calling "finalizePosition" as before. For a buy position ("m_tradeConfig.direction" as ORDER_TYPE_BUY), we get "currentPrice" with "getMarketBid" and calculate "profitPoints" as the difference between "currentPrice" and "m_tradeConfig.openPrice" divided by "_Point". The new trailing stop logic checks if "enableInitialTrailing" is true, "m_tradeConfig.hasRecoveryTrades" is false, and "profitPoints" meets or exceeds "minProfitPoints". If so, we calculate "newTrailingStop" by subtracting "trailingStopPoints" times "_Point" from "currentPrice". If "profitPoints" also exceeds "minProfitPoints" plus "trailingStopPoints" and "m_tradeConfig.trailingStopLevel" is either 0.0 or less than "newTrailingStop", we update "m_tradeConfig.trailingStopLevel" and log it with "Print".
If "m_tradeConfig.trailingStopLevel" is set and "currentPrice" falls below it, we log the trigger and call "finalizePosition" to close the trade. The zone recovery logic remains unchanged, closing the position if "currentPrice" exceeds "m_zoneBounds.zoneTargetHigh" or triggering a sell recovery trade with "triggerRecoveryTrade" if it falls below "m_zoneBounds.zoneLow".
For a sell position ("m_tradeConfig.direction" as ORDER_TYPE_SELL), we fetch "currentPrice" with "getMarketAsk" and calculate "profitPoints" inversely. The trailing stop logic mirrors the buy case, setting "newTrailingStop" by adding "trailingStopPoints" times _Point to "currentPrice", updating "m_tradeConfig.trailingStopLevel" if conditions are met, and closing the position if "currentPrice" exceeds it. The zone recovery logic closes the position if "currentPrice" is below "m_zoneBounds.zoneTargetLow" or triggers a buy recovery trade if above "m_zoneBounds.zoneHigh". We don't include a physical trailing stop because we want to have full control of the system. That way, we are able to keep all instances monitored and managed. Here is the output after running the program for the trailing stop feature.
From the image, we can see that we can trail the position and close it when the price falls back the trailing level. Finally, we just create an instance of the basket manager and then use it for the management globally.
//--- Global Instance BasketManager *manager = NULL; int OnInit() { manager = new BasketManager(_Symbol, baseMagicNumber, maxInitialPositions); if (!manager.initialize()) { delete manager; manager = NULL; return INIT_FAILED; } return INIT_SUCCEEDED; } void OnDeinit(const int reason) { if (manager != NULL) { delete manager; manager = NULL; Print("EA deinitialized"); } } void OnTick() { if (manager != NULL) { manager.processTick(); } }
We update the global instance and event handlers to use the new "BasketManager" class, replacing the previous version’s use of the "MarketZoneTrader" class to support our multi-basket trading improvement by centralizing the management of multiple trade baskets. We start by declaring a global "manager" pointer to the "BasketManager" class, initialized to "NULL", instead of the previous "trader" pointer to "MarketZoneTrader". This shift is crucial as it allows us to manage multiple trade baskets through a single manager, unlike the single-instance approach in the prior version.
In the OnInit event handler, we create a new "BasketManager" instance for "manager", passing "_Symbol", "baseMagicNumber", and "maxInitialPositions" to configure it for the current chart, unique basket identification, and the maximum number of baskets. We call "manager.initialize" to set up indicators and load existing positions, and if it fails, we delete "manager", set it to "NULL", and return INIT_FAILED. On success, we return "INIT_SUCCEEDED".
In the "OnDeinit" event handler, we check if "manager" is not "NULL", then delete it with "delete", set it to "NULL", and log the deinitialization with "Print". In the OnTick, we check if "manager" is not "NULL" and call "manager.processTick" to handle market ticks across all baskets, replacing the previous call to "trader.processTick". This centralizes tick processing for multiple baskets, enhancing the system’s ability to manage concurrent trade signals. Upon compilation, we have the following outcome.
From the image, we can see that we can create separate signal baskets and manage them, with different labels constructed from the magic number provided. 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 enhanced our Zone Recovery System for Envelopes Trend Trading in MQL5 by introducing trailing stops and a multi-basket trading system, building on the foundation from Part 22 with new components like the "BasketManager" class and updated "MarketZoneTrader" functions. These improvements offer a more flexible and robust trading framework that you can customize further by adjusting parameters like "trailingStopPoints" or "maxInitialPositions".
Disclaimer: This article is for educational purposes only. Trading carries significant financial risks, and market volatility may lead to losses. Thorough backtesting and careful risk management are essential before deploying this program in live markets.
With these enhancements, you can refine this system or adapt its architecture to create new strategies, advancing your algorithmic trading journey. 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