preview
Automating Trading Strategies in MQL5 (Part 22): Creating a Zone Recovery System for Envelopes Trend Trading

Automating Trading Strategies in MQL5 (Part 22): Creating a Zone Recovery System for Envelopes Trend Trading

MetaTrader 5Trading |
6 233 2
Allan Munene Mutiiria
Allan Munene Mutiiria

Introduction

In our previous article (Part 21), we explored a neural network-based trading strategy enhanced with adaptive learning rates to improve prediction accuracy for market movements in MetaQuotes Language 5 (MQL5). In Part 22, we shift focus to creating a Zone Recovery System integrated with an Envelopes trend-trading strategy, combining Relative Strength Index (RSI) and Envelopes indicators to automate trades and manage losses effectively. We will cover the following topics:

  1. Understanding the Zone Recovery Envelopes Trend Architecture
  2. Implementation in MQL5
  3. Backtesting
  4. Conclusion

By the end, you’ll have a robust MQL5 trading system designed for dynamic market conditions, ready for implementation and testing—let’s get started!


Understanding the Zone Recovery Envelopes Trend Architecture

Zone recovery is a smart trading strategy that helps us turn potential losses into wins by placing extra trades when the market moves against us, aiming to come out ahead or break even. Imagine you buy a currency pair expecting it to rise, but it drops—zone recovery steps in by setting a price range, or “zone,” where we place opposite trades to recover losses if the price bounces back. We plan to develop an automated system in MetaQuotes Language 5 (MQL5) that leverages this concept to trade forex markets while maintaining low risks and maximizing profits.

To make this work, we will utilize two technical indicators to identify the optimal times to enter trades. One indicator will check the market’s energy, ensuring we only trade when there’s a strong push in one direction, avoiding weak or messy signals. The other, called Envelopes, will draw a channel around the market’s average price, showing us when prices stretch too far up or down, signaling a likely snap-back moment to jump in. These indicators will work together to find high-chance trades where the price is ready to reverse within a trend.

Here’s how we intend to pull it all together: we will start by placing a trade when our indicators signal a reversal, like when the price hits the edge of the Envelopes channel with strong momentum. If the market moves the wrong way, we’ll activate the zone recovery by opening counter-trades within our set price zone, carefully sized to balance risk and recovery. We’ll limit the number of trades to avoid getting carried away, ensuring the system stays disciplined. This setup will let us chase trend opportunities while having a safety net for when things don’t go as planned, adaptable to both wild and calm markets. Stick with us as we turn this plan into reality and test it out! See the implementation plan below.

STRATEGY PLAN


Implementation in MQL5

To create the program in MQL5, open the MetaEditor, go to the Navigator, locate the Indicators folder, click on the "New" tab, and follow the prompts to create the file. Once it is made, in the coding environment, we will start by declaring some input variables that will help us control the key values of the program easily.

//+------------------------------------------------------------------+
//|                 Envelopes Trend Bounce with Zone Recovery EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

#include <Trade/Trade.mqh>                                             //--- Include trade library

enum TradingLotSizeOptions { FIXED_LOTSIZE, UNFIXED_LOTSIZE };         //--- Define lot size options

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    magicNumber = 123456789;                                  // Magic Number
input int    maxOrders = 1;                                            // Maximum Initial Positions
input double zoneTargetPoints = 600;                                   // Zone Target Points
input double zoneSizePoints = 300;                                     // Zone Size Points
input bool   restrictMaxOrders = true;                                 // Apply Maximum Orders Restriction

Here, we lay the foundation for our Zone Recovery System for Envelopes Trend Trading in MQL5 by setting up essential components and user-configurable settings. We begin by including the "<Trade/Trade.mqh>" library, which provides the "CTrade" class for executing trading operations like opening and closing positions. This inclusion is vital as it equips our Expert Advisor (EA) with the tools needed to interact with the market seamlessly, especially order initiations. See below how to open the file.

MQL5 TRADE OPERATIONS FILE

We then define the "TradingLotSizeOptions" enumeration with two values: "FIXED_LOTSIZE" and "UNFIXED_LOTSIZE". This allows us to offer users a choice between a constant lot size or one that adjusts dynamically based on risk parameters, providing flexibility in trade sizing to suit different trading styles. Next, we configure the input parameters under the "EA GENERAL SETTINGS" group, which users can adjust in the MetaTrader 5 platform.

The "lotOption" input, set to "UNFIXED_LOTSIZE" by default, determines whether trades use a fixed or risk-based lot size. The "initialLotSize" (0.01) sets the lot size for fixed trades, while "riskPercentage" (1.0%) and "riskPoints" (300) define the account balance percentage and stop-loss distance for dynamic lot sizing. These settings control how much risk we take per trade, ensuring the EA aligns with the user’s risk tolerance.

We assign a unique "magicNumber" (123456789) to identify our EA’s trades, allowing us to distinguish them from other trades on the same account. The "maxOrders" (1) and "restrictMaxOrders" (true) inputs limit the number of initial positions, preventing the EA from opening too many trades at once. Finally, "zoneTargetPoints" (600) and "zoneSizePoints" (300) establish the profit target and recovery zone size in points, defining the boundaries for our zone recovery strategy. Upon compilation, we get the following output.

LOADED INPUTS

With the inputs loaded, we can now begin the core logic declaration for the entire system. We will start by declaring some structures and classes that we will use since we want to apply an Object Oriented Programming (OOP) approach.

class MarketZoneTrader {
private:
   //--- Trade State Definition
   enum TradeState { INACTIVE, RUNNING, TERMINATING };                 //--- Define trade lifecycle states

   //--- Data Structures
   struct TradeMetrics {
      bool   operationSuccess;                                         //--- Track operation success
      double totalVolume;                                              //--- Sum closed trade volumes
      double netProfitLoss;                                            //--- Accumulate profit/loss
   };

   struct ZoneBoundaries {
      double zoneHigh;                                                 //--- Upper recovery zone boundary
      double zoneLow;                                                  //--- Lower recovery zone boundary
      double zoneTargetHigh;                                           //--- Upper profit target
      double zoneTargetLow;                                            //--- Lower profit target
   };

   struct TradeConfig {
      string         marketSymbol;                                     //--- Trading symbol
      double         openPrice;                                        //--- Position entry price
      double         initialVolume;                                    //--- Initial trade volume
      long           tradeIdentifier;                                  //--- Magic number
      string         tradeLabel;                                       //--- Trade comment
      ulong          activeTickets[];                                  //--- Active position tickets
      ENUM_ORDER_TYPE direction;                                       //--- Trade direction
      double         zoneProfitSpan;                                   //--- Profit target range
      double         zoneRecoverySpan;                                 //--- Recovery zone range
      double         accumulatedBuyVolume;                             //--- Total buy volume
      double         accumulatedSellVolume;                            //--- Total sell volume
      TradeState     currentState;                                     //--- Current trade state
   };

   struct LossTracker {
      double tradeLossTracker;                                         //--- Track cumulative profit/loss
   };
};

Here, we define the core structure of our system for Envelopes Trend Trading in MQL5 by implementing the "MarketZoneTrader" class, focusing on its private section with trade state definitions and data structures. This logic will help organize the critical components needed to manage trades, track recovery zones, and monitor performance. We begin by defining the "MarketZoneTrader" class, which serves as the backbone of our Expert Advisor (EA), encapsulating the logic for our trading strategy.

Within its private section, we introduce the "TradeState" enumeration with three states: "INACTIVE", "RUNNING", and "TERMINATING". These states allow us to track the lifecycle of our trading operations, ensuring we know whether the EA is idle, actively managing trades, or closing positions. This is crucial for maintaining control over the trading process, as it helps us coordinate actions like opening recovery trades or finalizing positions.

Next, we create the "TradeMetrics" structure to store key performance data for our trades. It includes "operationSuccess" to track whether trade actions (like closing positions) succeed, "totalVolume" to sum the volumes of closed trades, and "netProfitLoss" to accumulate the profit or loss from those trades. This structure helps us evaluate the outcome of our trading actions, providing a clear picture of performance during recovery or closure.

We then define the "ZoneBoundaries" structure, which holds the price levels for our zone recovery strategy. The "zoneHigh" and "zoneLow" variables mark the upper and lower boundaries of the recovery zone, where we place counter-trades to mitigate losses. The "zoneTargetHigh" and "zoneTargetLow" set the profit targets above and below the zone, defining when we exit trades profitably. These boundaries are essential for our strategy, as they guide when to trigger recovery actions or close positions. Here is what they would look like in visualization, just you have a clear picture of why we need the structure.

ZONES SAMPLE

Next, the "TradeConfig" structure is where we store the trading setup. It includes "marketSymbol" for the currency pair, "openPrice" for the entry price, and "initialVolume" for the trade size. The "tradeIdentifier" holds our unique magic number, and "tradeLabel" adds a comment for trade identification. The "activeTickets" array tracks open position tickets, while "direction" specifies whether the trade is a buy or sell. We also include "zoneProfitSpan" and "zoneRecoverySpan" to define the profit target and recovery zone sizes in price units, and "accumulatedBuyVolume" and "accumulatedSellVolume" to monitor total volumes for each trade type. The "currentState" variable, using the "TradeState" enumeration, tracks the trading state, tying everything together.

Finally, we add the "LossTracker" structure with a single "tradeLossTracker" variable to monitor cumulative profit or loss across trades. This helps us assess the financial impact of our recovery actions, ensuring we can adjust our strategy if losses grow too large. We can then define some member variables to help store the other, less impactful but necessary trade information.

//--- Member Variables
TradeConfig           m_tradeConfig;                                //--- Store trade configuration
ZoneBoundaries        m_zoneBounds;                                 //--- Store zone boundaries
LossTracker           m_lossTracker;                                //--- Track profit/loss
string                m_lastError;                                  //--- Store error message
int                   m_errorStatus;                                //--- Store error code
CTrade                m_tradeExecutor;                              //--- Manage trade execution
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
TradingLotSizeOptions m_lotOption;                                  //--- Lot size option
double                m_initialLotSize;                             //--- Fixed lot size
double                m_riskPercentage;                             //--- Risk percentage
int                   m_riskPoints;                                 //--- Risk points
int                   m_maxOrders;                                  //--- Maximum positions
bool                  m_restrictMaxOrders;                          //--- Position restriction flag
double                m_zoneTargetPoints;                           //--- Profit target points
double                m_zoneSizePoints;                             //--- Recovery zone points

We define key member variables in the "MarketZoneTrader" class’s private section to manage trade settings, recovery zones, and indicator data. We use "m_tradeConfig" ("TradeConfig" structure) to store trade details like symbol and direction, "m_zoneBounds" ("ZoneBoundaries" structure) for recovery zone and profit target prices, and "m_lossTracker" ("LossTracker" structure) to track profits or losses. For error handling, "m_lastError" (string) and "m_errorStatus" (integer) log issues, while "m_tradeExecutor" ("CTrade" class) handles trade operations.

Indicator handles—"m_handleRsi", "m_handleEnvUpper", "m_handleEnvLower"—access RSI and Envelopes data, with "m_rsiBuffer", "m_envUpperBandBuffer", and "m_envLowerBandBuffer" arrays storing their values. We store input settings in "m_lotOption" ("TradingLotSizeOptions"), "m_initialLotSize", "m_riskPercentage", "m_riskPoints", "m_maxOrders", "m_restrictMaxOrders", "m_zoneTargetPoints", and "m_zoneSizePoints" to control lot sizing, position limits, and zone sizes. These variables form the backbone for managing trades and indicators, preparing us for the trading logic ahead. We then need to define some helper functions that we will use frequently within the program.

//--- Error Handling
void logError(string message, int code) {
   //--- Error Logging Start
   m_lastError = message;                                           //--- Store error message
   m_errorStatus = code;                                            //--- Store error code
   Print("Error: ", message);                                       //--- Log error to Experts tab
   //--- Error Logging End
}

//--- Market Data Access
double getMarketVolumeStep() {
   //--- Volume Step Retrieval Start
   return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_VOLUME_STEP); //--- Retrieve broker's volume step
   //--- Volume Step Retrieval End
}

double getMarketAsk() {
   //--- Ask Price Retrieval Start
   return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_ASK); //--- Retrieve ask price
   //--- Ask Price Retrieval End
}

double getMarketBid() {
   //--- Bid Price Retrieval Start
   return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_BID); //--- Retrieve bid price
   //--- Bid Price Retrieval End
}

Here, we add critical utility functions for error handling and market data access. The "logError" function stores "message" in "m_lastError", "code" in "m_errorStatus", and logs the message via Print to the Experts tab for debugging. The "getMarketVolumeStep" function uses SymbolInfoDouble with SYMBOL_VOLUME_STEP to fetch the broker’s volume increment for "m_tradeConfig.marketSymbol", ensuring valid trade sizes. The "getMarketAsk" and "getMarketBid" functions retrieve ask and bid prices using "SymbolInfoDouble" with SYMBOL_ASK and "SYMBOL_BID", respectively, for accurate trade pricing.

We can now define the major functions for executing trade operations. Let's start with ones that will help us initialize, store trade tickets for tracking and monitoring operations, and closing of the trades, as this is the less complex logic.

//--- Trade Initialization
bool configureTrade(ulong ticket) {
   //--- Trade Configuration Start
   if (!PositionSelectByTicket(ticket)) {                               //--- Select position by ticket
      logError("Failed to select ticket " + IntegerToString(ticket), INIT_FAILED); //--- Log selection failure
      return false;                                                     //--- Return failure
   }
   m_tradeConfig.marketSymbol = PositionGetString(POSITION_SYMBOL);     //--- Set symbol
   m_tradeConfig.tradeLabel = __FILE__;                                 //--- Set trade comment
   m_tradeConfig.tradeIdentifier = PositionGetInteger(POSITION_MAGIC);  //--- Set magic number
   m_tradeConfig.direction = (ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE);   //--- Set direction
   m_tradeConfig.openPrice = PositionGetDouble(POSITION_PRICE_OPEN);    //--- Set entry price
   m_tradeConfig.initialVolume = PositionGetDouble(POSITION_VOLUME);    //--- Set initial volume
   m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); //--- Set magic number for executor
   return true;                                                         //--- Return success
   //--- Trade Configuration End
}

//--- Trade Ticket Management
void storeTradeTicket(ulong ticket) {
   //--- Ticket Storage Start
   int ticketCount = ArraySize(m_tradeConfig.activeTickets);        //--- Get ticket count
   ArrayResize(m_tradeConfig.activeTickets, ticketCount + 1);       //--- Resize ticket array
   m_tradeConfig.activeTickets[ticketCount] = ticket;               //--- Store ticket
   //--- Ticket Storage End
}

//--- Trade Execution
ulong openMarketTrade(ENUM_ORDER_TYPE tradeDirection, double tradeVolume, double price) {
   //--- Trade Opening Start
   ulong ticket = 0;                                                //--- Initialize ticket
   if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, tradeDirection, tradeVolume, price, 0, 0, m_tradeConfig.tradeLabel)) { //--- Open position
      ticket = m_tradeExecutor.ResultOrder();                       //--- Get ticket
   } else {
      Print("Failed to open trade: Direction=", EnumToString(tradeDirection), ", Volume=", tradeVolume); //--- Log failure
   }
   return ticket;                                                   //--- Return ticket
   //--- Trade Opening End
}

//--- Trade Closure
void closeActiveTrades(TradeMetrics &metrics) {
   //--- Trade Closure Start
   for (int i = ArraySize(m_tradeConfig.activeTickets) - 1; i >= 0; i--) {    //--- Iterate tickets in reverse
      if (m_tradeConfig.activeTickets[i] > 0) {                               //--- Check valid ticket
         if (m_tradeExecutor.PositionClose(m_tradeConfig.activeTickets[i])) { //--- Close position
            m_tradeConfig.activeTickets[i] = 0;                               //--- Clear ticket
            metrics.totalVolume += m_tradeExecutor.ResultVolume();            //--- Accumulate volume
            if ((ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE) == ORDER_TYPE_BUY) { //--- Check buy position
               metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (m_tradeExecutor.ResultPrice() - PositionGetDouble(POSITION_PRICE_OPEN)); //--- Calculate buy profit
            } else {                                                          //--- Handle sell position
               metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (PositionGetDouble(POSITION_PRICE_OPEN) - m_tradeExecutor.ResultPrice()); //--- Calculate sell profit
            }
         } else {
            metrics.operationSuccess = false;                                  //--- Mark failure
            Print("Failed to close ticket: ", m_tradeConfig.activeTickets[i]); //--- Log failure
         }
      }
   }
   //--- Trade Closure End
}

//--- Bar Detection
bool isNewBar() {
   //--- New Bar Detection Start
   static datetime previousTime = 0;                                      //--- Store previous bar time
   datetime currentTime = iTime(m_tradeConfig.marketSymbol, Period(), 0); //--- Get current bar time
   bool result = (currentTime != previousTime);                           //--- Check for new bar
   previousTime = currentTime;                                            //--- Update previous time
   return result;                                                         //--- Return new bar status
   //--- New Bar Detection End
}

Here, we dive into the core logic of our program, crafting functions to set up trades, track positions, execute orders, close trades, and time our actions. We start by creating the "configureTrade" function to prepare a trade for a given "ticket". First, we try selecting the position with the PositionSelectByTicket function. If it doesn’t work, we log the issue using "logError" and exit with false. When it succeeds, we fill "m_tradeConfig" with details: we grab "marketSymbol" using the PositionGetString function, set "tradeLabel" to __FILE__, and pull "tradeIdentifier" and "direction" from PositionGetInteger, casting the latter to ENUM_ORDER_TYPE. Then, we set "openPrice" and "initialVolume" with PositionGetDouble and tag "m_tradeExecutor" with "SetExpertMagicNumber", ensuring our trade is ready to roll.

Next, we create the "storeTradeTicket" function to keep our open positions organized. We check the size of "m_tradeConfig.activeTickets" with the ArraySize function, stretch the array by one slot using the ArrayResize function, and slip the new "ticket" into place, so we always know which trades are active. Moving on, we create the "openMarketTrade" function to place trades in the market. We call "m_tradeExecutor.PositionOpen" with "tradeDirection", "tradeVolume", "price", and "m_tradeConfig" details. If it goes through, we assign the "ticket" with "ResultOrder"; if not, we log the error with "Print", keeping our trade execution tight.

Then, we tackle closing positions with the "closeActiveTrades" function. We loop backward through "m_tradeConfig.activeTickets", closing each valid ticket with "m_tradeExecutor.PositionClose". When a closure works, we clear the ticket, add "ResultVolume" to "metrics.totalVolume", and calculate "metrics.netProfitLoss" using the "PositionGetInteger" and "PositionGetDouble" functions to check trade direction. If something fails, we flag "metrics.operationSuccess" as false and log it with Print, ensuring we track every outcome.

Finally, we add the "isNewBar" function to help trade once per bar, reducing resource usage. We fetch the current bar time for "m_tradeConfig.marketSymbol" with the iTime function, compare it to "previousTime", and update "previousTime" if it’s different, letting us know when a new bar arrives to check for trade signals. Finally, we will need a function to calculate the trading volume and a function to open the trades.

//--- Lot Size Calculation
double calculateLotSize(double riskPercent, int riskPips) {
   //--- Lot Size Calculation Start
   double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * riskPercent / 100;                //--- Calculate risk amount
   double tickSize = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_SIZE);   //--- Get tick size
   double tickValue = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_VALUE); //--- Get tick value
   if (tickSize == 0 || tickValue == 0) {                           //--- Validate tick data
      Print("Invalid tick size or value");                          //--- Log invalid data
      return -1;                                                    //--- Return invalid lot
   }
   double lotValue = (riskPips * _Point) / tickSize * tickValue;    //--- Calculate lot value
   if (lotValue == 0) {                                             //--- Validate lot value
      Print("Invalid lot value");                                   //--- Log invalid lot
      return -1;                                                    //--- Return invalid lot
   }
   return NormalizeDouble(riskMoney / lotValue, 2);                 //--- Return normalized lot size
   //--- Lot Size Calculation End
}

//--- Order Execution
int openOrder(ENUM_ORDER_TYPE orderType, double stopLoss, double takeProfit) {
   //--- Order Opening Start
   int ticket;                                                      //--- Initialize ticket
   double openPrice;                                                //--- Initialize open price
   
   if (orderType == ORDER_TYPE_BUY) {                               //--- Check buy order
      openPrice = NormalizeDouble(getMarketAsk(), Digits());        //--- Set buy price
   } else if (orderType == ORDER_TYPE_SELL) {                       //--- Check sell order
      openPrice = NormalizeDouble(getMarketBid(), Digits());        //--- Set sell price
   } else {
      Print("Invalid order type");                                  //--- Log invalid type
      return -1;                                                    //--- Return invalid ticket
   }
   
   double lotSize = 0;                                              //--- Initialize lot size
   
   if (m_lotOption == FIXED_LOTSIZE) {                              //--- Check fixed lot
      lotSize = m_initialLotSize;                                   //--- Use fixed lot size
   } else if (m_lotOption == UNFIXED_LOTSIZE) {                     //--- Check dynamic lot
      lotSize = calculateLotSize(m_riskPercentage, m_riskPoints);   //--- Calculate risk-based lot
   }
   
   if (lotSize <= 0) {                                              //--- Validate lot size
      Print("Invalid lot size: ", lotSize);                         //--- Log invalid lot
      return -1;                                                    //--- Return invalid ticket
   }
   
   if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, orderType, lotSize, openPrice, 0, 0, __FILE__)) { //--- Open position
      ticket = (int)m_tradeExecutor.ResultOrder();                  //--- Get ticket
      Print("New trade opened: Ticket=", ticket, ", Type=", EnumToString(orderType), ", Volume=", lotSize); //--- Log success
   } else {
      ticket = -1;                                                  //--- Set invalid ticket
      Print("Failed to open order: Type=", EnumToString(orderType), ", Volume=", lotSize); //--- Log failure
   }
   
   return ticket;                                                   //--- Return ticket
   //--- Order Opening End
}

We start with the "calculateLotSize" function to determine the trade size based on risk parameters. First, we calculate the "riskMoney" by taking a percentage of the account balance using AccountInfoDouble with ACCOUNT_BALANCE and "riskPercent". Then, we fetch "tickSize" and "tickValue" for "m_tradeConfig.marketSymbol" using SymbolInfoDouble with "SYMBOL_TRADE_TICK_SIZE" and "SYMBOL_TRADE_TICK_VALUE". If either is zero, we log an error with "Print" and return -1 to avoid invalid calculations. We compute the "lotValue" using "riskPips", _Point, "tickSize", and "tickValue", and if it’s zero, we log another error and return -1. Finally, we return the lot size with NormalizeDouble to two decimal places, ensuring it matches broker requirements.

Next, we create the "openOrder" function to place trades. We initialize "ticket" and "openPrice", then check "orderType". For ORDER_TYPE_BUY, we set "openPrice" using "getMarketAsk" and "NormalizeDouble" with Digits; for "ORDER_TYPE_SELL", we use "getMarketBid". If "orderType" is invalid, we log it with "Print" 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 with "m_tradeExecutor.PositionOpen" using "m_tradeConfig.marketSymbol", "orderType", "lotSize", "openPrice", and "FILE" as the comment. On success, we set "ticket" with "ResultOrder" and log it with "Print"; on failure, we set "ticket" to -1 and log the error. Finally, we return the ticket value.

After doing that, we need to initialize the system values. We can achieve that via a dedicated function, but to keep everything simple, we will use the constructor. It is advisable to define the constructor in a public access modifier so it is available everywhere in the program. Let us define the destructor here, too.

public:
   //--- Constructor
   MarketZoneTrader(TradingLotSizeOptions lotOpt, double initLot, double riskPct, int riskPts, int maxOrds, bool restrictOrds, double targetPts, double sizePts) {
      //--- Constructor Start
      m_tradeConfig.currentState = INACTIVE;                           //--- Set initial state
      ArrayResize(m_tradeConfig.activeTickets, 0);                     //--- Initialize ticket array
      m_tradeConfig.zoneProfitSpan = targetPts * _Point;               //--- Set profit target
      m_tradeConfig.zoneRecoverySpan = sizePts * _Point;               //--- Set recovery zone
      m_lossTracker.tradeLossTracker = 0.0;                            //--- Initialize loss tracker
      m_lotOption = lotOpt;                                            //--- Set lot size option
      m_initialLotSize = initLot;                                      //--- Set initial lot
      m_riskPercentage = riskPct;                                      //--- Set risk percentage
      m_riskPoints = riskPts;                                          //--- Set risk points
      m_maxOrders = maxOrds;                                           //--- Set max positions
      m_restrictMaxOrders = restrictOrds;                              //--- Set restriction flag
      m_zoneTargetPoints = targetPts;                                  //--- Set target points
      m_zoneSizePoints = sizePts;                                      //--- Set zone points
      m_tradeConfig.marketSymbol = _Symbol;                            //--- Set symbol
      m_tradeConfig.tradeIdentifier = magicNumber;                     //--- Set magic number
      //--- Constructor End
   }

   //--- Destructor
   ~MarketZoneTrader() {
      //--- Destructor Start
      cleanup();                                                       //--- Release resources
      //--- Destructor End
   }

We continue by defining the constructor and destructor for the "MarketZoneTrader" class in its public section. We begin with the "MarketZoneTrader" constructor, which takes parameters "lotOpt", "initLot", "riskPct", "riskPts", "maxOrds", "restrictOrds", "targetPts", and "sizePts". We initialize the trading environment by setting "m_tradeConfig.currentState" to "INACTIVE" to indicate no active trades. Next, we clear the "m_tradeConfig.activeTickets" array using ArrayResize to zero, preparing it for new tickets. We calculate "m_tradeConfig.zoneProfitSpan" and "m_tradeConfig.zoneRecoverySpan" by multiplying "targetPts" and "sizePts" with "_Point", setting the profit target and recovery zone sizes in price units. We reset "m_lossTracker.tradeLossTracker" to 0.0 to start tracking profits or losses from scratch.

Then, we assign the input parameters to member variables: "m_lotOption" to "lotOpt", "m_initialLotSize" to "initLot", "m_riskPercentage" to "riskPct", "m_riskPoints" to "riskPts", "m_maxOrders" to "maxOrds", "m_restrictMaxOrders" to "restrictOrds", "m_zoneTargetPoints" to "targetPts", and "m_zoneSizePoints" to "sizePts". We set "m_tradeConfig.marketSymbol" to _Symbol to trade the current chart’s symbol and assign "m_tradeConfig.tradeIdentifier" to "magicNumber" for unique trade identification. This setup ensures our EA reflects user settings and is ready to trade.

Next, we define the "~MarketZoneTrader" destructor to clean up resources. We call the "cleanup" function to release any allocated resources, such as indicator handles, ensuring the EA shuts down cleanly without memory leaks. It is good to note that the constructor and destructor have the same class name wording, only that the destructor has a tilde (~) before it. Just that. Here is the function for destroying the class when not needed.

//--- Cleanup
void cleanup() {
   //--- Cleanup Start
   IndicatorRelease(m_handleRsi);                                   //--- Release RSI handle
   ArrayFree(m_rsiBuffer);                                          //--- Free RSI buffer
   IndicatorRelease(m_handleEnvUpper);                              //--- Release upper Envelopes handle
   ArrayFree(m_envUpperBandBuffer);                                 //--- Free upper Envelopes buffer
   IndicatorRelease(m_handleEnvLower);                              //--- Release lower Envelopes handle
   ArrayFree(m_envLowerBandBuffer);                                 //--- Free lower Envelopes buffer
   //--- Cleanup End
}

We simply use the IndicatorRelease function to release the indicator handles and the ArrayFree function to release the storage arrays. Since we have touched the indicators, let us define an initialization function that we will call when starting the program.

//--- Getters
TradeState getCurrentState() {
   //--- Get Current State Start
   return m_tradeConfig.currentState;                               //--- Return trade state
   //--- Get Current State End
}

double getZoneTargetHigh() {
   //--- Get Target High Start
   return m_zoneBounds.zoneTargetHigh;                              //--- Return profit target high
   //--- Get Target High End
}

double getZoneTargetLow() {
   //--- Get Target Low Start
   return m_zoneBounds.zoneTargetLow;                               //--- Return profit target low
   //--- Get Target Low End
}

double getZoneHigh() {
   //--- Get Zone High Start
   return m_zoneBounds.zoneHigh;                                    //--- Return recovery zone high
   //--- Get Zone High End
}

double getZoneLow() {
   //--- Get Zone Low Start
   return m_zoneBounds.zoneLow;                                     //--- Return recovery zone low
   //--- Get Zone Low End
}

//--- 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 start by creating simple getter functions to access key trading data. The "getCurrentState" function returns "m_tradeConfig.currentState", letting us check if the system is in the "INACTIVE", "RUNNING", or "TERMINATING" state. Next, we build "getZoneTargetHigh" and "getZoneTargetLow" to retrieve "m_zoneBounds.zoneTargetHigh" and "m_zoneBounds.zoneTargetLow", providing the profit target prices for our trades. Then, we add "getZoneHigh" and "getZoneLow" to fetch "m_zoneBounds.zoneHigh" and "m_zoneBounds.zoneLow", giving us the recovery zone boundaries.

Moving on, we craft the "initialize" function to set up our Expert Advisor (EA). We begin by assigning "m_tradeConfig.tradeIdentifier" to "m_tradeExecutor" using "SetExpertMagicNumber" to tag our trades. We then check for existing positions with "PositionsTotal" and loop through them, grabbing each "ticket" with "PositionGetTicket". If PositionSelectByTicket succeeds and the position matches "m_tradeConfig.marketSymbol" and "m_tradeConfig.tradeIdentifier" (via PositionGetString and "PositionGetInteger"), we call "activateTrade" to manage it, logging success or failure with "Print".

Next, we set up our indicators. We create the RSI handle with the iRSI function for "m_tradeConfig.marketSymbol" using an 8-period setting on the current timeframe and "PRICE_CLOSE". If "m_handleRsi" is INVALID_HANDLE, we log the error with "Print" and return "INIT_FAILED". We then initialize the Envelopes indicators: "m_handleEnvUpper" with the "iEnvelopes" function using a 150-period, simple moving average, 0.1 deviation, and "PRICE_CLOSE", and "m_handleEnvLower" with a 95-period, 1.4 deviation. If either handle is "INVALID_HANDLE", we log the failure and return "INIT_FAILED". Finally, we configure "m_rsiBuffer", "m_envUpperBandBuffer", and "m_envLowerBandBuffer" as time-series arrays with ArraySetAsSeries, log success with "Print", and return INIT_SUCCEEDED. We can now call this function on the OnInit event handler, but first, we will need an instance of the class.

//--- Global Instance
MarketZoneTrader *trader = NULL;                                        //--- Declare trader instance

Here, we set up the global instance of our system by declaring a pointer to the "MarketZoneTrader" class. We create the "trader" variable as a pointer to "MarketZoneTrader" and initialize it to "NULL". This step ensures we have a single, globally accessible instance of our trading system that we can use throughout the Expert Advisor (EA) to manage all trading operations, such as initializing trades, executing orders, and handling recovery zones. By starting with "NULL", we prepare the "trader" to be properly instantiated later, preventing any premature access before the EA is fully set up. We can now proceed to call the function.

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
}

In the OnInit event handler, we start by creating a new instance of the "MarketZoneTrader" class, assigning it to the global "trader" pointer. We pass the user-defined input parameters—"lotOption", "initialLotSize", "riskPercentage", "riskPoints", "maxOrders", "restrictMaxOrders", "zoneTargetPoints", and "zoneSizePoints"—to the constructor to configure the trading system with the desired settings. Then, we call the "initialize" function on "trader" to set up the EA, including trade tagging, existing position checks, and indicator initialization, and return its result to signal whether the setup was successful. This function ensures our EA is fully prepared to start trading with the specified configurations. Upon compilation, we have the following output.

INITIALIZATION IMAGE

From the image, we can see that the program initialized successfully. However, there is an issue when we try to remove the program. See below.

OBJECTS MEMORY LEAK

From the image, we can see there are undeleted objects that lead to a memory leak. To solve this, we need to do the object cleanup. To achieve that, we use the following logic.

void OnDeinit(const int reason) {
   //--- EA Deinitialization Start
   if (trader != NULL) {                                                 //--- Check trader existence
      delete trader;                                                     //--- Delete trader
      trader = NULL;                                                     //--- Clear pointer
      Print("EA deinitialized");                                         //--- Log deinitialization
   }
   //--- EA Deinitialization End
}

To handle the cleanup, in the OnDeinit event handler, we begin by checking if the "trader" pointer is not "NULL" to ensure the "MarketZoneTrader" instance exists. If it does, we use the delete operator to free the memory allocated for "trader", preventing memory leaks. Then, we set "trader" to "NULL" to avoid accidental access to the deallocated memory. Finally, we log a message with the "Print" function to confirm the EA has been deinitialized. This function ensures our EA exits cleanly, releasing resources properly. We can now continue defining the main logic to handle signal evaluations and the management of opened trades. We will need utility functions for that.

//--- Position Management
bool activateTrade(ulong ticket) {
   //--- Position Activation Start
   m_tradeConfig.currentState = INACTIVE;                           //--- Set state to inactive
   ArrayResize(m_tradeConfig.activeTickets, 0);                     //--- Clear tickets
   m_lossTracker.tradeLossTracker = 0.0;                            //--- Reset loss tracker
   if (!configureTrade(ticket)) {                                    //--- Configure trade
      return false;                                                 //--- Return failure
   }
   storeTradeTicket(ticket);                                        //--- Store ticket
   if (m_tradeConfig.direction == ORDER_TYPE_BUY) {                 //--- Handle buy position
      m_zoneBounds.zoneHigh = m_tradeConfig.openPrice;              //--- Set zone high
      m_zoneBounds.zoneLow = m_zoneBounds.zoneHigh - m_tradeConfig.zoneRecoverySpan; //--- Set zone low
      m_tradeConfig.accumulatedBuyVolume = m_tradeConfig.initialVolume; //--- Set buy volume
      m_tradeConfig.accumulatedSellVolume = 0.0;                    //--- Reset sell volume
   } else {                                                         //--- Handle sell position
      m_zoneBounds.zoneLow = m_tradeConfig.openPrice;               //--- Set zone low
      m_zoneBounds.zoneHigh = m_zoneBounds.zoneLow + m_tradeConfig.zoneRecoverySpan; //--- Set zone high
      m_tradeConfig.accumulatedSellVolume = m_tradeConfig.initialVolume; //--- Set sell volume
      m_tradeConfig.accumulatedBuyVolume = 0.0;                     //--- Reset buy volume
   }
   m_zoneBounds.zoneTargetHigh = m_zoneBounds.zoneHigh + m_tradeConfig.zoneProfitSpan; //--- Set target high
   m_zoneBounds.zoneTargetLow = m_zoneBounds.zoneLow - m_tradeConfig.zoneProfitSpan; //--- Set target low
   m_tradeConfig.currentState = RUNNING;                            //--- Set state to running
   return true;                                                     //--- Return success
   //--- Position Activation End
}

//--- Tick Processing
void processTick() {
   //--- Tick Processing Start
   double askPrice = NormalizeDouble(getMarketAsk(), Digits());     //--- Get ask price
   double bidPrice = NormalizeDouble(getMarketBid(), Digits());     //--- Get bid price
   
   if (!isNewBar()) return;                                         //--- Exit if not new bar
   
   if (!CopyBuffer(m_handleRsi, 0, 0, 3, m_rsiBuffer)) {            //--- Load RSI data
      Print("Error loading RSI data. Reverting.");                  //--- Log RSI failure
      return;                                                       //--- Exit
   }
   
   if (!CopyBuffer(m_handleEnvUpper, 0, 0, 3, m_envUpperBandBuffer)) { //--- Load upper Envelopes
      Print("Error loading upper envelopes data. Reverting.");         //--- Log failure
      return;                                                          //--- Exit
   }
   
   if (!CopyBuffer(m_handleEnvLower, 1, 0, 3, m_envLowerBandBuffer)) { //--- Load lower Envelopes
      Print("Error loading lower envelopes data. Reverting.");         //--- Log failure
      return;                                                          //--- Exit
   }
   
   int ticket = 0;                                                     //--- Initialize ticket
   
   const int rsiOverbought = 70;                                       //--- Set RSI overbought level
   const int rsiOversold = 30;                                         //--- Set RSI oversold level
   
   if (m_rsiBuffer[1] < rsiOversold && m_rsiBuffer[2] > rsiOversold && m_rsiBuffer[0] < rsiOversold) { //--- Check buy signal
      if (askPrice > m_envUpperBandBuffer[0]) {                        //--- Confirm price above upper Envelopes
         if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) { //--- Check position limit
            ticket = openOrder(ORDER_TYPE_BUY, 0, 0);                  //--- Open buy order
         }
      }
   } else if (m_rsiBuffer[1] > rsiOverbought && m_rsiBuffer[2] < rsiOverbought && m_rsiBuffer[0] > rsiOverbought) { //--- Check sell signal
      if (bidPrice < m_envLowerBandBuffer[0]) {                        //--- Confirm price below lower Envelopes
         if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) { //--- Check position limit
            ticket = openOrder(ORDER_TYPE_SELL, 0, 0);                 //--- Open sell order
         }
      }
   }
   
   if (ticket > 0) {                                                //--- Check if trade opened
      if (activateTrade(ticket)) {                                  //--- Activate position
         Print("New position activated: Ticket=", ticket);          //--- Log activation
      } else {
         Print("Failed to activate new position: Ticket=", ticket); //--- Log failure
      }
   }
   //--- Tick Processing End
}

Here, we continue developing our program by implementing the "activateTrade" and "processTick" functions within the "MarketZoneTrader" class to manage positions and handle market ticks. We start with the "activateTrade" function to activate a trade for a given "ticket". First, we set "m_tradeConfig.currentState" to "INACTIVE" and clear "m_tradeConfig.activeTickets" using the ArrayResize function to reset the ticket list. We reset "m_lossTracker.tradeLossTracker" to 0.0, then call "configureTrade" with "ticket". If it fails, we return false. Next, we save the "ticket" with "storeTradeTicket". For a buy trade ("m_tradeConfig.direction" as ORDER_TYPE_BUY), we set "m_zoneBounds.zoneHigh" to "m_tradeConfig.openPrice", calculate "m_zoneBounds.zoneLow" by subtracting "m_tradeConfig.zoneRecoverySpan", and update "m_tradeConfig.accumulatedBuyVolume" to "m_tradeConfig.initialVolume" while resetting "m_tradeConfig.accumulatedSellVolume".

For a sell trade, we set "m_zoneBounds.zoneLow" to "m_tradeConfig.openPrice", add "m_tradeConfig.zoneRecoverySpan" for "m_zoneBounds.zoneHigh", and adjust volumes accordingly. We then set "m_zoneBounds.zoneTargetHigh" and "m_zoneBounds.zoneTargetLow" using "m_tradeConfig.zoneProfitSpan", change "m_tradeConfig.currentState" to "RUNNING", and return true.

Next, we create the "processTick" function to handle market ticks. We fetch "askPrice" and "bidPrice" using "getMarketAsk" and "getMarketBid", normalized with NormalizeDouble and "Digits". If "isNewBar" returns false, we exit to save resources. We load indicator data with CopyBuffer for "m_handleRsi" into "m_rsiBuffer", "m_handleEnvUpper" into "m_envUpperBandBuffer", and "m_handleEnvLower" into "m_envLowerBandBuffer", logging errors with "Print" and exiting if any fail. For trade signals, we set "rsiOverbought" to 70 and "rsiOversold" to 30.

If "m_rsiBuffer" indicates an oversold condition and "askPrice" exceeds "m_envUpperBandBuffer", we open a buy order with "openOrder" if "m_restrictMaxOrders" is false or PositionsTotal is below "m_maxOrders". For an overbought condition with "bidPrice" below "m_envLowerBandBuffer", we open a sell order. If a valid "ticket" is returned, we call "activateTrade" and log the outcome to the journal. We can now run the function in the OnTick event handler to process the signal evaluation and position initiation.

void OnTick() {
   //--- Tick Handling Start
   if (trader != NULL) {                                                 //--- Check trader existence
      trader.processTick();                                              //--- Process tick
   }
   //--- Tick Handling End
}

In the "OnTick" event handler, we start by checking if the "trader" pointer, our instance of the "MarketZoneTrader" class, is not "NULL" to ensure the trading system is initialized. If it exists, we call the "processTick" function on "trader" to handle each market tick, evaluating positions, checking indicator signals, and executing trades as needed. Upon compilation, we have the following outcome.

INITIAL POSITION

From the image, we can see that we have identified a signal, evaluated it, and initiated a buy position. What we now need to do is manage the open positions. We will handle that in functions for modularity.

//--- Market Tick Evaluation
void evaluateMarketTick() {
   //--- Tick Evaluation Start
   if (m_tradeConfig.currentState == INACTIVE) return;              //--- Exit if inactive
   if (m_tradeConfig.currentState == TERMINATING) {                 //--- Check terminating state
      finalizePosition();                                           //--- Finalize position
      return;                                                       //--- Exit
   }
}

Here, we implement the "evaluateMarketTick" function within the "MarketZoneTrader" class to assess market conditions for active trades. We begin by checking the "m_tradeConfig.currentState" to see if it’s "INACTIVE". If it is, we exit immediately to avoid unnecessary processing when no trades are active. Next, we check if "m_tradeConfig.currentState" is "TERMINATING". If so, we call the "finalizePosition" function to close all open positions and complete the trade cycle, then exit. Here is the function to close the trades.

//--- Position Finalization
bool finalizePosition() {
   //--- Position Finalization Start
   m_tradeConfig.currentState = TERMINATING;                        //--- Set terminating state
   TradeMetrics metrics = {true, 0.0, 0.0};                         //--- Initialize metrics
   closeActiveTrades(metrics);                                       //--- Close all trades
   if (metrics.operationSuccess) {                                  //--- Check success
      ArrayResize(m_tradeConfig.activeTickets, 0);                  //--- Clear tickets
      m_tradeConfig.currentState = INACTIVE;                        //--- Set inactive state
      Print("Position closed successfully");                        //--- Log success
   } else {
      Print("Failed to close position");                            //--- Log failure
   }
   return metrics.operationSuccess;                                 //--- Return status
   //--- Position Finalization End
}

We start by setting "m_tradeConfig.currentState" to "TERMINATING" to indicate the trade cycle is ending. This helps to prevent the management cycle when we are in the process of closing the trades. Then, we initialize a "TradeMetrics" structure named "metrics" with "operationSuccess" set to true, "totalVolume" to 0.0, and "netProfitLoss" to 0.0 to track closure outcomes. We call "closeActiveTrades" with "metrics" to close all open positions listed in "m_tradeConfig.activeTickets". If "metrics.operationSuccess" remains true, we clear "m_tradeConfig.activeTickets" using ArrayResize to reset the ticket list, set "m_tradeConfig.currentState" to "INACTIVE" to mark the system as idle, and log success with "Print".

If closure fails, we log the failure with "Print". Finally, we return "metrics.operationSuccess" to indicate whether the process completed successfully. If we did not close the trades at this point, it means that we are not in the position closure process, so we can proceed with the evaluation to see if the price hit the recovery zones or target levels. We will start with the buy instance.

double currentPrice;                                             //--- Initialize price
if (m_tradeConfig.direction == ORDER_TYPE_BUY) {                 //--- Handle buy position
   currentPrice = getMarketBid();                                //--- Get bid price
   if (currentPrice > m_zoneBounds.zoneTargetHigh) {             //--- Check profit target
      Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); //--- Log closure
      finalizePosition();                                        //--- Close position
      return;                                                    //--- Exit
   } else if (currentPrice < m_zoneBounds.zoneLow) {             //--- Check recovery trigger
      Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); //--- Log recovery
      triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice);       //--- Open sell recovery
   }
}

We continue by implementing logic within the "evaluateMarketTick" function of the "MarketZoneTrader" class to handle buy positions. We start by declaring "currentPrice" to store the market price. If "m_tradeConfig.direction" is ORDER_TYPE_BUY, we set "currentPrice" using the "getMarketBid" function to fetch the bid price, as this is the price at which we can close a buy position. Next, we check if "currentPrice" exceeds "m_zoneBounds.zoneTargetHigh". If it does, we log the closure with "Print", showing the bid price and target, then call "finalizePosition" to close the trade and exit with "return".

If "currentPrice" falls below "m_zoneBounds.zoneLow", we log a recovery trigger with "Print" and call "triggerRecoveryTrade" with ORDER_TYPE_SELL and "currentPrice" to open a sell trade to mitigate losses. This logic ensures we close profitable buy trades or initiate recovery for losing ones, keeping our strategy responsive. Here is the logic for the function responsible for opening recovery trades.

//--- Recovery Trade Handling
void triggerRecoveryTrade(ENUM_ORDER_TYPE tradeDirection, double price) {
   //--- Recovery Trade Start
   TradeMetrics metrics = {true, 0.0, 0.0};                         //--- Initialize metrics
   closeActiveTrades(metrics);                                      //--- Close existing trades
   for (int i = 0; i < 10 && !metrics.operationSuccess; i++) {      //--- Retry closure
      Sleep(1000);                                                  //--- Wait 1 second
      metrics.operationSuccess = true;                              //--- Reset success flag
      closeActiveTrades(metrics);                                   //--- Retry closure
   }
   m_lossTracker.tradeLossTracker += metrics.netProfitLoss;         //--- Update loss tracker
   if (m_lossTracker.tradeLossTracker > 0 && metrics.operationSuccess) { //--- Check positive profit
      Print("Closing position due to positive profit: ", m_lossTracker.tradeLossTracker); //--- Log closure
      finalizePosition();                                           //--- Close position
      m_lossTracker.tradeLossTracker = 0.0;                         //--- Reset loss tracker
      return;                                                       //--- Exit
   }
   double tradeSize = determineRecoverySize(tradeDirection);        //--- Calculate trade size
   ulong ticket = openMarketTrade(tradeDirection, tradeSize, price); //--- Open recovery trade
   if (ticket > 0) {                                                //--- Check if trade opened
      storeTradeTicket(ticket);                                     //--- Store ticket
      m_tradeConfig.direction = tradeDirection;                     //--- Update direction
      if (tradeDirection == ORDER_TYPE_BUY) m_tradeConfig.accumulatedBuyVolume += tradeSize; //--- Update buy volume
      else m_tradeConfig.accumulatedSellVolume += tradeSize;        //--- Update sell volume
      Print("Recovery trade opened: Ticket=", ticket, ", Direction=", EnumToString(tradeDirection), ", Volume=", tradeSize); //--- Log recovery trade
   }
   //--- Recovery Trade End
}

//--- Recovery Size Calculation
double determineRecoverySize(ENUM_ORDER_TYPE tradeDirection) {
   //--- Recovery Size Calculation Start
   double tradeSize = -m_lossTracker.tradeLossTracker / m_tradeConfig.zoneProfitSpan; //--- Calculate lot size
   tradeSize = MathCeil(tradeSize / getMarketVolumeStep()) * getMarketVolumeStep(); //--- Round to volume step
   return tradeSize;                                                //--- Return trade size
   //--- Recovery Size Calculation End
}

To handle cases where the market needs to trigger recovery instances, we start with the "triggerRecoveryTrade" function to handle recovery trades when a position moves against us. First, we initialize a "TradeMetrics" structure named "metrics" with "operationSuccess" set to true, "totalVolume" to 0.0, and "netProfitLoss" to 0.0. We call "closeActiveTrades" with "metrics" to close existing positions. If "metrics.operationSuccess" is false, we retry up to 10 times, waiting one second with Sleep and resetting "operationSuccess" before each attempt.

We update "m_lossTracker.tradeLossTracker" by adding "metrics.netProfitLoss". If "m_lossTracker.tradeLossTracker" is positive and "metrics.operationSuccess" is true, we log the closure with "Print", call "finalizePosition", reset "m_lossTracker.tradeLossTracker" to 0.0, and exit with "return". Otherwise, we calculate the recovery "tradeSize" using "determineRecoverySize" with "tradeDirection", then open a new trade with "openMarketTrade" using "tradeDirection", "tradeSize", and "price".

If the returned "ticket" is valid, we save it with "storeTradeTicket", update "m_tradeConfig.direction", adjust "m_tradeConfig.accumulatedBuyVolume" or "m_tradeConfig.accumulatedSellVolume" based on "tradeDirection", and log the trade with "Print" using EnumToString. Next, we create the "determineRecoverySize" function to calculate the lot size for recovery trades. We compute "tradeSize" by dividing the negative "m_lossTracker.tradeLossTracker" by "m_tradeConfig.zoneProfitSpan" to size the trade to cover losses. We then round "tradeSize" to the broker’s volume step using MathCeil and "getMarketVolumeStep" to ensure compliance, and return the result. This now handles the recovery instances, and we can continue with the logic for handling the sell zones. The logic is just the opposite of buy, so we will not invest much time in that. The final full function will be as follows.

//--- Market Tick Evaluation
void evaluateMarketTick() {
   //--- Tick Evaluation Start
   if (m_tradeConfig.currentState == INACTIVE) return;              //--- Exit if inactive
   if (m_tradeConfig.currentState == TERMINATING) {                 //--- Check terminating state
      finalizePosition();                                           //--- Finalize position
      return;                                                       //--- Exit
   }
   double currentPrice;                                             //--- Initialize price
   if (m_tradeConfig.direction == ORDER_TYPE_BUY) {                 //--- Handle buy position
      currentPrice = getMarketBid();                                //--- Get bid price
      if (currentPrice > m_zoneBounds.zoneTargetHigh) {             //--- Check profit target
         Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); //--- Log closure
         finalizePosition();                                        //--- Close position
         return;                                                    //--- Exit
      } else if (currentPrice < m_zoneBounds.zoneLow) {             //--- Check recovery trigger
         Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); //--- Log recovery
         triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice);       //--- Open sell recovery
      }
   } else if (m_tradeConfig.direction == ORDER_TYPE_SELL) {         //--- Handle sell position
      currentPrice = getMarketAsk();                                //--- Get ask price
      if (currentPrice < m_zoneBounds.zoneTargetLow) {              //--- Check profit target
         Print("Closing position: Ask=", currentPrice, " < TargetLow=", m_zoneBounds.zoneTargetLow); //--- Log closure
         finalizePosition();                                        //--- Close position
         return;                                                    //--- Exit
      } else if (currentPrice > m_zoneBounds.zoneHigh) {            //--- Check recovery trigger
         Print("Triggering recovery trade: Ask=", currentPrice, " > ZoneHigh=", m_zoneBounds.zoneHigh); //--- Log recovery
         triggerRecoveryTrade(ORDER_TYPE_BUY, currentPrice);        //--- Open buy recovery
      }
   }
   //--- Tick Evaluation End
}

The function now handles all the directions for recovery. Upon compilation, we have the following outcome.

FINAL OUTCOME

From the image, we can see that we successfully handle positions that are triggered due to trend bounce signals. 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:

GRAPH

Backtest report:

REPORT


Conclusion

In conclusion, we have built a robust MQL5 program that implements a Zone Recovery System for Envelopes Trend Trading, combining Relative Strength Index (RSI) and Envelopes indicators to identify trade opportunities and manage losses through structured recovery zones, using an Object Oriented Programming (OOP) approach. Using components like the "MarketZoneTrader" class, structures such as "TradeConfig" and "ZoneBoundaries", and functions like "processTick" and "triggerRecoveryTrade", we created a flexible system that you can adjust by tweaking parameters like "zoneTargetPoints" or "riskPercentage" to fit various market conditions.

Disclaimer: This article is for educational purposes only. Trading involves significant financial risks, and market volatility can lead to losses. Thorough backtesting and careful risk management are essential before using this program in live markets.

With the foundation laid in this article, you can refine this zone recovery system or adapt its logic to develop new trading strategies, fueling your progress in algorithmic trading. Happy trading!

Last comments | Go to discussion (2)
Sabrina Hellal
Sabrina Hellal | 9 Jul 2025 at 13:44
Thank so much 🙏
Allan Munene Mutiiria
Allan Munene Mutiiria | 9 Jul 2025 at 16:46
Sabrina Hellal #:
Thank so much 🙏

Very much welcomed. Thanks

From Novice to Expert: Animated News Headline Using MQL5 (IV) — Locally hosted AI model market insights From Novice to Expert: Animated News Headline Using MQL5 (IV) — Locally hosted AI model market insights
In today's discussion, we explore how to self-host open-source AI models and use them to generate market insights. This forms part of our ongoing effort to expand the News Headline EA, introducing an AI Insights Lane that transforms it into a multi-integration assistive tool. The upgraded EA aims to keep traders informed through calendar events, financial breaking news, technical indicators, and now AI-generated market perspectives—offering timely, diverse, and intelligent support to trading decisions. Join the conversation as we explore practical integration strategies and how MQL5 can collaborate with external resources to build a powerful and intelligent trading work terminal.
MQL5 Wizard Techniques you should know (Part 73): Using Patterns of Ichimoku and the ADX-Wilder MQL5 Wizard Techniques you should know (Part 73): Using Patterns of Ichimoku and the ADX-Wilder
The Ichimoku-Kinko-Hyo Indicator and the ADX-Wilder oscillator are a pairing that could be used in complimentarily within an MQL5 Expert Advisor. The Ichimoku is multi-faceted, however for this article, we are relying on it primarily for its ability to define support and resistance levels. Meanwhile, we also use the ADX to define our trend. As usual, we use the MQL5 wizard to build and test any potential these two may possess.
Neural Networks in Trading: Hyperbolic Latent Diffusion Model (HypDiff) Neural Networks in Trading: Hyperbolic Latent Diffusion Model (HypDiff)
The article considers methods of encoding initial data in hyperbolic latent space through anisotropic diffusion processes. This helps to more accurately preserve the topological characteristics of the current market situation and improves the quality of its analysis.
Statistical Arbitrage Through Cointegrated Stocks (Part 1): Engle-Granger and Johansen Cointegration Tests Statistical Arbitrage Through Cointegrated Stocks (Part 1): Engle-Granger and Johansen Cointegration Tests
This article aims to provide a trader-friendly, gentle introduction to the most common cointegration tests, along with a simple guide to understanding their results. The Engle-Granger and Johansen cointegration tests can reveal statistically significant pairs or groups of assets that share long-term dynamics. The Johansen test is especially useful for portfolios with three or more assets, as it calculates the strength of cointegrating vectors all at once.