
Automating Trading Strategies in MQL5 (Part 22): Creating a Zone Recovery System for Envelopes Trend Trading
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:
- Understanding the Zone Recovery Envelopes Trend Architecture
- Implementation in MQL5
- Backtesting
- 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.
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.
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.
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.
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.
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.
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.
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.
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:
Backtest 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!
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
Thank so much 🙏
Very much welcomed. Thanks