preview
Developing a Reusable Dynamic Volatility Trailing Stop Engine in MQL5

Developing a Reusable Dynamic Volatility Trailing Stop Engine in MQL5

MetaTrader 5Trading |
65 0
Amanda Vitoria De Paula Pereira
Amanda Vitoria De Paula Pereira

Introduction and Architectural Purpose

In automated trading systems, trade entry receives most developer attention, while position exit mechanisms are often reduced to fixed point-based stop-loss levels. While fixed stop-loss orders offer predictable risk parameters, they maintain a static distance regardless of changing market volatility regimes. During periods of high volatility, static stops are frequently reached during normal market dispersion; during low-volatility regimes, they remain wider than necessary, giving back accumulated open profit.

To address this structural behavior, algorithmic traders frequently implement volatility-based exit strategies. By measuring price dispersion through average True Range (ATR) metrics, exit levels expand during volatile moves and contract during quiet consolidation phases.

This article presents a reusable, broker-aware Volatility Trailing Stop engine in MQL5. We formalize the underlying calculation, implement a modular include class with explicit broker constraint validation, build a non-repainting diagnostic indicator with directional ratchet behavior, provide an EA template with reproducible execution telemetry, and evaluate system behavior under structured test conditions.


Comparative Analysis of Exit Strategies

Different position exit methodologies offer distinct operational trade-offs depending on market structure and volatility regimes:

Exit Strategy Calculation Mechanism Primary Operational Characteristic Main Trade-Off
Fixed Point Stop Static distance in points Deterministic, invariant price offset Does not adapt to shifts in market volatility
Percentage Trailing Proportional distance from peak price Scales with asset price magnitude Can distort across assets with differing volatility profiles
Parabolic SAR Time-price acceleration curve Accelerates toward price over time Tends to close positions quickly in prolonged consolidations
Volatility TR Trailing True Range average offset multiplier Adjusts dynamically to recent candle ranges Stop distances can expand significantly during sudden market shocks

MQL5 provides order management via the native CTrade class. However, it does not provide a self-contained, object-oriented engine for tracking and recalculating dynamic trailing stops across positions. Embedding trailing logic directly in an Expert Advisor's event loop often results in monolithic code that is difficult to test and maintain.


Mathematics of Volatility-Based Trailing Stops

A volatility trailing stop adjusts a position's stop-loss distance based on recent price movement. The distance from the current price anchor is calculated as a multiple of the True Range average over a user-defined period.

To compute True Range ($TR$) for a given completed bar $i$:

eq

The Simple True Range average ($TR_{avg}$) over a period $N$ is calculated as the arithmetic mean:

eq

The trailing stop offset distance ($D$) is given by:

eq

where $M$ represents the volatility multiplier input.

Directional Trailing Boundary Rules

For a long position, the trailing stop level ($SL_{long}$) can only move upward. If the candidate trailing level is higher than the current position stop-loss, the stop-loss is updated:

eq

For a short position, the trailing stop level ($SL_{short}$) can only move downward. If the candidate trailing level is lower than the current position stop-loss, the stop-loss is updated:

eq

Architectural Contract: Closed-Bar Volatility vs. Real-Time Price Anchor

A central design decision in trailing stop architecture is resolving the evaluation time frame. Evaluating volatility calculations on the active, unclosed candlestick (Bar 0) introduces erratic behavior because intraday price spikes expand and contract the True Range before bar completion.

To maintain calculation stability while preserving responsive execution, our engine enforces a hybrid evaluation contract:

  1. Volatility Measurement (Strictly Closed Bars): True Range calculations exclusively sample completed bars. When requesting history via CopyRates(), we set the start position to shift 1 in the terminal's historical series. After assigning ArraySetAsSeries(rates, true), index 0 in the local rates[] array represents the most recently completed bar (Bar 1 of the terminal). This ensures the volatility offset remains stable and deterministic throughout the life of the current bar.

  2. Price Anchoring and Quantization: The candidate stop level is anchored to current market quotes (Bid for long positions, Ask for short positions) and strictly quantized using the broker's SYMBOL_TRADE_TICK_SIZE rather than simple decimal digit truncation. This prevents trade server rejections on instruments with non-decimal tick increments such as CFDs, index futures, and precious metals.


Class Design and Implementation

The core logic is implemented in Trailing_Engine.mqh. The class encapsulates rate synchronization, Simple TR calculation, tick-size price quantization, broker constraint inspection (Stops Level and Freeze Level), trade server modification validation, and internal telemetry collection.

Create the include file in MQL5\Include\Trailing_Engine\Trailing_Engine.mqh:

//+------------------------------------------------------------------+
//|                                              Trailing_Engine.mqh |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//+------------------------------------------------------------------+
#property copyright "Open Source"
#property version   "2.10"

#include <Trade\Trade.mqh>

//--- Detailed trailing execution result status
enum ENUM_TRAILING_STATUS
  {
   TRAILING_STATUS_NO_UPDATE = 0,       // No update needed
   TRAILING_STATUS_UPDATED,             // Successfully modified by trade server
   TRAILING_STATUS_SKIPPED_STEP,        // Change below minimum step
   TRAILING_STATUS_SKIPPED_OPEN_PRICE,  // Level not yet beyond open price (protection mode)
   TRAILING_STATUS_REJECTED_STOPSLEVEL, // Level closer than broker STOPS_LEVEL
   TRAILING_STATUS_REJECTED_FREEZELEVEL,// Active SL inside broker FREEZE_LEVEL
   TRAILING_STATUS_ERROR                // Execution or server rejection error
  };

//--- State structure to store and inspect individual evaluations
struct TrailingState
  {
   ulong                ticket;         // Position ticket
   double               current_sl;     // Active stop loss
   double               proposed_sl;    // Calculated candidate stop loss
   bool                 should_update;  // Flag indicating modification necessity
   ENUM_TRAILING_STATUS status;         // Detailed status outcome
   uint                 retcode;        // Trade server return code
   string               message;        // Diagnostic message
  };

//--- Telemetry structure to track reproducible execution statistics
struct TrailingTelemetry
  {
   int                  total_evaluations;      // Total positions processed
   int                  modifications_sent;     // Confirmed server modifications
   int                  skipped_min_step;       // Skipped due to minimum step filter
   int                  skipped_open_price;     // Skipped due to open-price guard
   int                  rejected_stops_level;   // Proactively rejected by Stops Level
   int                  rejected_freeze_level;  // Proactively rejected by Freeze Level
   int                  server_errors;          // Server rejections or execution errors
  };

//--- Production-oriented volatility trailing engine class
class CVolatilityTrailing
  {
private:
   string            m_symbol;          // Asset symbol
   ENUM_TIMEFRAMES   m_timeframe;       // Operation timeframe
   int               m_tr_period;       // True Range averaging period
   double            m_tr_multiplier;   // Volatility multiplier
   int               m_min_step_points; // Minimum step in points
   ulong             m_magic;           // Magic number identifier
   bool              m_only_in_profit;  // Require level beyond open price
   CTrade            m_trade;           // Trade execution object
   TrailingTelemetry m_telemetry;       // Internal telemetry accumulator

   double            CalculateSimpleTR(const MqlRates &rates[], int start_idx, int period);
   double            RoundToTickSize(double price, string symbol);

public:
                     CVolatilityTrailing(string symbol, 
                                         ENUM_TIMEFRAMES tf, 
                                         int tr_period=14, 
                                         double tr_mult=2.0, 
                                         int min_step=10, 
                                         ulong magic=20260801,
                                         bool only_in_profit=true);
                    ~CVolatilityTrailing(void);

   bool              ProcessPosition(const ulong ticket, TrailingState &state);
   int               ProcessAllPositions(void);
   TrailingTelemetry GetTelemetry(void) const { return m_telemetry; }
   void              ResetTelemetry(void);
  };

//+------------------------------------------------------------------+
//| Constructor with comprehensive input validation                  |
//+------------------------------------------------------------------+
CVolatilityTrailing::CVolatilityTrailing(string symbol, 
                                         ENUM_TIMEFRAMES tf, 
                                         int tr_period, 
                                         double tr_mult, 
                                         int min_step, 
                                         ulong magic,
                                         bool only_in_profit)
  {
   m_symbol          = (symbol == "") ? _Symbol : symbol;
   m_timeframe       = tf;
   m_tr_period       = (tr_period > 0) ? tr_period : 14;
   m_tr_multiplier   = (tr_mult > 0.0) ? tr_mult : 2.0;
   m_min_step_points = (min_step >= 0) ? min_step : 0;
   m_magic           = magic;
   m_only_in_profit  = only_in_profit;

   m_trade.SetExpertMagicNumber(m_magic);
   ResetTelemetry();
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CVolatilityTrailing::~CVolatilityTrailing(void)
  {
  }

//+------------------------------------------------------------------+
//| Resets internal execution telemetry counters                     |
//+------------------------------------------------------------------+
void CVolatilityTrailing::ResetTelemetry(void)
  {
   m_telemetry.total_evaluations     = 0;
   m_telemetry.modifications_sent    = 0;
   m_telemetry.skipped_min_step      = 0;
   m_telemetry.skipped_open_price     = 0;
   m_telemetry.rejected_stops_level  = 0;
   m_telemetry.rejected_freeze_level = 0;
   m_telemetry.server_errors         = 0;
  }

//+------------------------------------------------------------------+
//| Normalizes candidate price strictly by symbol tick size          |
//+------------------------------------------------------------------+
double CVolatilityTrailing::RoundToTickSize(double price, string symbol)
  {
   double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
   int digits       = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);

   if(tick_size > 0.0)
     {
      return NormalizeDouble(MathRound(price / tick_size) * tick_size, digits);
     }
   return NormalizeDouble(price, digits);
  }

//+------------------------------------------------------------------+
//| Computes Simple Arithmetic True Range over closed bars           |
//+------------------------------------------------------------------+
double CVolatilityTrailing::CalculateSimpleTR(const MqlRates &rates[], int start_idx, int period)
  {
   int size = ArraySize(rates);
   if(start_idx + period >= size) return 0.0;

   double sum = 0.0;
   for(int i = 0; i < period; i++)
     {
      int idx = start_idx + i;
      if(idx + 1 >= size) break;
      double high       = rates[idx].high;
      double low        = rates[idx].low;
      double prev_close = rates[idx + 1].close;
      double tr         = MathMax(high - low, MathMax(MathAbs(high - prev_close), MathAbs(low - prev_close)));
      sum += tr;
     }
   return (period > 0) ? (sum / period) : 0.0;
  }

//+------------------------------------------------------------------+
//| Processes trailing logic on a specific position ticket           |
//+------------------------------------------------------------------+
bool CVolatilityTrailing::ProcessPosition(const ulong ticket, TrailingState &state)
  {
   m_telemetry.total_evaluations++;
   state.ticket        = ticket;
   state.should_update = false;
   state.current_sl    = 0.0;
   state.proposed_sl   = 0.0;
   state.status        = TRAILING_STATUS_NO_UPDATE;
   state.retcode       = 0;
   state.message       = "";

   if(!PositionSelectByTicket(ticket))
     {
      state.status  = TRAILING_STATUS_ERROR;
      state.message = "Position ticket not found";
      m_telemetry.server_errors++;
      return false;
     }

   string pos_symbol = PositionGetString(POSITION_SYMBOL);
   if(pos_symbol != m_symbol)
     {
      state.status  = TRAILING_STATUS_ERROR;
      state.message = "Symbol mismatch";
      m_telemetry.server_errors++;
      return false;
     }

   ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
   double current_sl = PositionGetDouble(POSITION_SL);
   double open_price = PositionGetDouble(POSITION_PRICE_OPEN);
   state.current_sl  = current_sl;

   //--- Copy completed history: start at shift 1 in terminal history
   MqlRates rates[];
   int needed_bars = m_tr_period + 2; 
   ResetLastError();
   int copied = CopyRates(m_symbol, m_timeframe, 1, needed_bars, rates);
   if(copied < needed_bars)
     {
      state.status  = TRAILING_STATUS_ERROR;
      state.message = "Insufficient closed history bars";
      m_telemetry.server_errors++;
      return false;
     }

   //--- In rates[], index 0 now represents the most recently completed bar
   ArraySetAsSeries(rates, true);

   double tr_avg = CalculateSimpleTR(rates, 0, m_tr_period);
   if(tr_avg <= 0.0)
     {
      state.status  = TRAILING_STATUS_ERROR;
      state.message = "Invalid TR average calculation";
      m_telemetry.server_errors++;
      return false;
     }

   double point        = SymbolInfoDouble(m_symbol, SYMBOL_POINT);
   double offset       = tr_avg * m_tr_multiplier;
   double min_step     = m_min_step_points * point;
   long   stops_level  = SymbolInfoInteger(m_symbol, SYMBOL_TRADE_STOPS_LEVEL);
   long   freeze_level = SymbolInfoInteger(m_symbol, SYMBOL_TRADE_FREEZE_LEVEL);
   double stops_dist   = stops_level * point;
   double freeze_dist  = freeze_level * point;

   if(pos_type == POSITION_TYPE_BUY)
     {
      double bid = SymbolInfoDouble(m_symbol, SYMBOL_BID);
      double proposed = RoundToTickSize(bid - offset, m_symbol);

      // 1. Open price protection gate (if enabled)
      if(m_only_in_profit && proposed <= open_price)
        {
         state.status  = TRAILING_STATUS_SKIPPED_OPEN_PRICE;
         state.message = "Candidate below open price";
         m_telemetry.skipped_open_price++;
         return false;
        }

      // 2. Ratchet advancement and minimum step gate
      if(current_sl > 0.0 && proposed < current_sl + min_step)
        {
         state.status  = TRAILING_STATUS_SKIPPED_STEP;
         state.message = "Step advancement insufficient";
         m_telemetry.skipped_min_step++;
         return false;
        }

      // 3. Broker STOPS_LEVEL check (distance to current market price)
      if((bid - proposed) < stops_dist)
        {
         state.status  = TRAILING_STATUS_REJECTED_STOPSLEVEL;
         state.message = "Violates broker STOPS_LEVEL constraint";
         m_telemetry.rejected_stops_level++;
         return false;
        }

      // 4. Broker FREEZE_LEVEL check (distance to active SL)
      if(current_sl > 0.0 && MathAbs(bid - current_sl) <= freeze_dist)
        {
         state.status  = TRAILING_STATUS_REJECTED_FREEZELEVEL;
         state.message = "Inside broker FREEZE_LEVEL boundary";
         m_telemetry.rejected_freeze_level++;
         return false;
        }

      state.proposed_sl   = proposed;
      state.should_update = true;

      if(m_trade.PositionModify(ticket, proposed, PositionGetDouble(POSITION_TP)))
        {
         uint retcode = m_trade.ResultRetcode();
         if(retcode == TRADE_RETCODE_DONE || retcode == TRADE_RETCODE_PLACED)
           {
            state.status  = TRAILING_STATUS_UPDATED;
            state.retcode = retcode;
            state.message = "Stop-loss modified successfully";
            m_telemetry.modifications_sent++;
            return true;
           }
         else
           {
            state.status  = TRAILING_STATUS_ERROR;
            state.retcode = retcode;
            state.message = m_trade.ResultRetcodeDescription();
            m_telemetry.server_errors++;
            return false;
           }
        }
      else
        {
         state.status  = TRAILING_STATUS_ERROR;
         state.retcode = m_trade.ResultRetcode();
         state.message = m_trade.ResultRetcodeDescription();
         m_telemetry.server_errors++;
         return false;
        }
     }
   else if(pos_type == POSITION_TYPE_SELL)
     {
      double ask = SymbolInfoDouble(m_symbol, SYMBOL_ASK);
      double proposed = RoundToTickSize(ask + offset, m_symbol);

      // 1. Open price protection gate (if enabled)
      if(m_only_in_profit && proposed >= open_price)
        {
         state.status  = TRAILING_STATUS_SKIPPED_OPEN_PRICE;
         state.message = "Candidate above open price";
         m_telemetry.skipped_open_price++;
         return false;
        }

      // 2. Ratchet advancement and minimum step gate
      if(current_sl > 0.0 && proposed > current_sl - min_step)
        {
         state.status  = TRAILING_STATUS_SKIPPED_STEP;
         state.message = "Step advancement insufficient";
         m_telemetry.skipped_min_step++;
         return false;
        }

      // 3. Broker STOPS_LEVEL check (distance to current market price)
      if((proposed - ask) < stops_dist)
        {
         state.status  = TRAILING_STATUS_REJECTED_STOPSLEVEL;
         state.message = "Violates broker STOPS_LEVEL constraint";
         m_telemetry.rejected_stops_level++;
         return false;
        }

      // 4. Broker FREEZE_LEVEL check (distance to active SL)
      if(current_sl > 0.0 && MathAbs(ask - current_sl) <= freeze_dist)
        {
         state.status  = TRAILING_STATUS_REJECTED_FREEZELEVEL;
         state.message = "Inside broker FREEZE_LEVEL boundary";
         m_telemetry.rejected_freeze_level++;
         return false;
        }

      state.proposed_sl   = proposed;
      state.should_update = true;

      if(m_trade.PositionModify(ticket, proposed, PositionGetDouble(POSITION_TP)))
        {
         uint retcode = m_trade.ResultRetcode();
         if(retcode == TRADE_RETCODE_DONE || retcode == TRADE_RETCODE_PLACED)
           {
            state.status  = TRAILING_STATUS_UPDATED;
            state.retcode = retcode;
            state.message = "Stop-loss modified successfully";
            m_telemetry.modifications_sent++;
            return true;
           }
         else
           {
            state.status  = TRAILING_STATUS_ERROR;
            state.retcode = retcode;
            state.message = m_trade.ResultRetcodeDescription();
            m_telemetry.server_errors++;
            return false;
           }
        }
      else
        {
         state.status  = TRAILING_STATUS_ERROR;
         state.retcode = m_trade.ResultRetcode();
         state.message = m_trade.ResultRetcodeDescription();
         m_telemetry.server_errors++;
         return false;
        }
     }

   return false;
  }

//+------------------------------------------------------------------+
//| Scans and updates all positions belonging to this symbol & magic |
//+------------------------------------------------------------------+
int CVolatilityTrailing::ProcessAllPositions(void)
  {
   int updated_count = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket > 0 && PositionSelectByTicket(ticket))
        {
         if(PositionGetString(POSITION_SYMBOL) == m_symbol && 
            PositionGetInteger(POSITION_MAGIC) == m_magic)
           {
            TrailingState state;
            if(ProcessPosition(ticket, state))
              {
               updated_count++;
              }
           }
        }
     }
   return updated_count;
  }

Detailed Walkthrough of Core Class Methods

Understanding the internal execution flow of CVolatilityTrailing is essential for reliable integration:

1.Arithmetic True Range Calculation (CalculateSimpleTR): The CalculateSimpleTR() method computes the arithmetic mean of True Range across N completed bars. It extracts high, low, and previous close values directly from the MqlRates array. By calculating this value within the class, we eliminate dependencies on external indicator handles (such as iATR), ensuring self-contained execution across multi-currency strategy tests.

2. Price Quantization (RoundToTickSize): Unlike rudimentary implementations that rely solely on NormalizeDouble(price, digits), our class queries SYMBOL_TRADE_TICK_SIZE. Instruments with non-decimal price steps (such as 0.25 or 0.50 points on indices and commodity futures) cause trade server rejections when orders do not align with acceptable price increments. RoundToTickSize ensures all candidate stop levels strictly match the instrument specification.

3. Position Evaluation and Validation Pipeline (ProcessPosition): When a position ticket is passed to ProcessPosition(), the engine selects the position explicitly and validates that its symbol matches m_symbol. It loads historical bars starting from shift 1 in terminal history, where index 0 in the local rates[] array represents the most recently completed bar. Before dispatching a modification order, it enforces four independent safety gates:

  • Open Price Protection Filter: Controlled by the m_only_in_profit parameter. When enabled, the trailing stop only engages once candidate prices lock in profit above (for buys) or below (for sells) the entry price. When disabled, the engine acts as an early risk-reduction trailing mechanism.

  • Minimum Step Filter: Suppresses modifications unless the candidate advancement equals or exceeds InpMinStepPoints * Point relative to the current stop loss.

  • Broker Stops Level Gate: Verifies that the proposed stop loss is farther from current market Bid/Ask than the distance specified by SYMBOL_TRADE_STOPS_LEVEL.

  • Broker Freeze Level Gate: Verifies that the active stop loss is not within the SYMBOL_TRADE_FREEZE_LEVEL distance from current price, preventing modification rejections during high-volatility spikes.

  • Trade Server Retcode Validation: Rather than assuming success based solely on the boolean return of PositionModify(), the method inspects m_trade.ResultRetcode(). It assigns TRAILING_STATUS_UPDATED only when the server confirms execution (retcode 10009 or 10008). Any server rejection is recorded in TrailingState and tracked in the internal telemetry accumulator.

4. Multi-Position Iteration (ProcessAllPositions): The ProcessAllPositions() method iterates over open positions in descending order using PositionsTotal() - 1 down to 0. It executes explicit position selection via PositionSelectByTicket() and filters positions by symbol and internal magic number without redundant parameter passing.


Developing the Visual Diagnostic Indicator

To visually inspect volatility trailing stop dynamics on historical chart data, we implement a diagnostic custom indicator named Ind_Volatility_Trailing.mq5.

The indicator is designed as a historical diagnostic approximation that reproduces the engine's Simple TR calculation and directional ratchet logic. It does not calculate live position-specific stop levels:

  • The live engine anchors candidate levels to live Bid/Ask quotes and applies tick quantization, open-price protection, current position stop checks, and broker constraint gates.
  • The diagnostic indicator anchors historical candidate levels to the preceding bar's close (Close[i-1]) and does not evaluate position-specific trade state.

    Create the indicator file in MQL5\Indicators\Trailing_Engine\Ind_Volatility_Trailing.mq5:

    //+------------------------------------------------------------------+
    //|                                  Ind_Volatility_Trailing.mq5     |
    //|                                  Copyright 2026, MetaQuotes Ltd. |
    //+------------------------------------------------------------------+
    #property copyright   "Open Source"
    #property version     "2.10"
    #property indicator_chart_window
    #property indicator_buffers 2
    #property indicator_plots   2
    
    #property indicator_label1  "Long Trailing Stop (Diagnostic)"
    #property indicator_type1   DRAW_LINE
    #property indicator_color1  clrDodgerBlue
    #property indicator_style1  STYLE_SOLID
    #property indicator_width1  2
    
    #property indicator_label2  "Short Trailing Stop (Diagnostic)"
    #property indicator_type2   DRAW_LINE
    #property indicator_color2  clrOrangeRed
    #property indicator_style2  STYLE_SOLID
    #property indicator_width2  2
    
    input int    InpTRPeriod      = 14;   // Simple TR Period
    input double InpTRMult        = 2.0;  // Volatility Multiplier
    
    double BufferLong[];
    double BufferShort[];
    
    //+------------------------------------------------------------------+
    //| Custom indicator initialization function                         |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       SetIndexBuffer(0, BufferLong, INDICATOR_DATA);
       SetIndexBuffer(1, BufferShort, INDICATOR_DATA);
    
       PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
       PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
    
       PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, InpTRPeriod + 1);
       PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, InpTRPeriod + 1);
    
       return INIT_SUCCEEDED;
      }
    
    //+------------------------------------------------------------------+
    //| Custom indicator iteration function                              |
    //+------------------------------------------------------------------+
    int OnCalculate(const int rates_total,
                    const int prev_calculated,
                    const datetime &time[],
                    const double &open[],
                    const double &high[],
                    const double &low[],
                    const double &close[],
                    const long &tick_volume[],
                    const long &volume[],
                    const int &spread[])
      {
       if(rates_total <= InpTRPeriod + 1) return 0;
    
       //--- Explicitly set chronological array indexing (bar 0 = oldest, bar N-1 = newest)
       ArraySetAsSeries(high, false);
       ArraySetAsSeries(low, false);
       ArraySetAsSeries(close, false);
       ArraySetAsSeries(BufferLong, false);
       ArraySetAsSeries(BufferShort, false);
    
       int start = (prev_calculated > InpTRPeriod + 1) ? prev_calculated - 1 : InpTRPeriod + 1;
    
       for(int i = start; i < rates_total && !IsStopped(); i++)
         {
          BufferLong[i]  = EMPTY_VALUE;
          BufferShort[i] = EMPTY_VALUE;
    
          //--- Calculate Simple TR over completed bars preceding bar i
          double tr_sum = 0.0;
          for(int k = 0; k < InpTRPeriod; k++)
            {
             int bar_idx = (i - 1) - k; 
             if(bar_idx <= 0) break;
    
             double h  = high[bar_idx];
             double l  = low[bar_idx];
             double pc = close[bar_idx - 1];
             double tr = MathMax(h - l, MathMax(MathAbs(h - pc), MathAbs(l - pc)));
             tr_sum += tr;
            }
    
          double tr_avg = tr_sum / InpTRPeriod;
          double offset = tr_avg * InpTRMult;
    
          //--- Diagnostic anchor: close price of completed bar (i - 1)
          double candidate_long  = close[i - 1] - offset;
          double candidate_short = close[i - 1] + offset;
    
          //--- Directional ratchet progression: Long lines can only advance upwards
          if(i > InpTRPeriod + 1 && BufferLong[i - 1] != EMPTY_VALUE && close[i - 1] > BufferLong[i - 1])
            {
             BufferLong[i] = MathMax(candidate_long, BufferLong[i - 1]);
            }
          else
            {
             BufferLong[i] = candidate_long;
            }
    
          //--- Directional ratchet progression: Short lines can only advance downwards
          if(i > InpTRPeriod + 1 && BufferShort[i - 1] != EMPTY_VALUE && close[i - 1] < BufferShort[i - 1])
            {
             BufferShort[i] = MathMin(candidate_short, BufferShort[i - 1]);
            }
          else
            {
             BufferShort[i] = candidate_short;
            }
         }
    
       return rates_total;
      }

    Print

    Figure 1: Diagnostic custom indicator rendering directional ratchet volatility trailing stop approximation on EURUSD M15 chart.

    Indicator Diagnostic Behavior and Approximation Boundaries

    The diagnostic indicator explicitly documents its array indexing direction by enforcing ArraySetAsSeries(..., false) in OnCalculate(). In this chronological orientation (where index 0 is the oldest bar and index rates_total - 1 is the newest bar), the calculation loop for bar i samples completed historical bars up to i - 1.

    By intentionally anchoring candidate calculations to close[i-1] and displaying the value on bar i, the indicator forms a strictly non-repainting historical plot. Traders can visually assess how volatility multipliers expand or contract relative to market swings, but should keep in mind that live execution levels depend on real-time Bid/Ask quotes and broker-level constraints.


    Expert Advisor Integration Template

    To demonstrate integration, we implement EA_Trailing_Demo.mq5. The Expert Advisor employs typed inputs, enforces a new-bar evaluation gate, manages single positions per magic number, and outputs telemetry data upon deinitialization.

    Create the robot in MQL5\Experts\Trailing_Engine\EA_Trailing_Demo.mq5:

    //+------------------------------------------------------------------+
    //|                                             EA_Trailing_Demo.mq5 |
    //|                                  Copyright 2026, MetaQuotes Ltd. |
    //+------------------------------------------------------------------+
    #property copyright "Open Source"
    #property version   "2.10"
    
    #include <Trade\Trade.mqh>
    #include <Trailing_Engine\Trailing_Engine.mqh>
    
    //--- Input Parameters
    input int    InpTRPeriod       = 14;        // Simple TR Calculation Period
    input double InpTRMultiplier   = 2.0;       // Volatility Multiplier
    input int    InpMinStepPoints  = 10;        // Minimum Modification Step (Points)
    input ulong  InpMagicNumber    = 20260802;  // Expert Magic Number
    input bool   InpOnlyInProfit   = true;      // Trail only after breakeven
    
    CVolatilityTrailing *g_trailing;
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       g_trailing = new CVolatilityTrailing(_Symbol, PERIOD_CURRENT, InpTRPeriod, InpTRMultiplier, InpMinStepPoints, InpMagicNumber, InpOnlyInProfit);
       if(g_trailing == NULL) return INIT_FAILED;
    
       return INIT_SUCCEEDED;
      }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function with Telemetry Output           |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
      {
       if(CheckPointer(g_trailing) == POINTER_DYNAMIC)
         {
          TrailingTelemetry stats = g_trailing.GetTelemetry();
          Print("=== Trailing Engine Verification Telemetry ===");
          PrintFormat("Total Evaluations:        %d", stats.total_evaluations);
          PrintFormat("Modifications Confirmed:  %d", stats.modifications_sent);
          PrintFormat("Skipped (Min Step):       %d", stats.skipped_min_step);
          PrintFormat("Skipped (Open Price):     %d", stats.skipped_open_price);
          PrintFormat("Rejected (Stops Level):   %d", stats.rejected_stops_level);
          PrintFormat("Rejected (Freeze Level):  %d", stats.rejected_freeze_level);
          PrintFormat("Server / Execution Errors:%d", stats.server_errors);
          Print("==============================================");
    
          delete g_trailing;
         }
      }
    
    //+------------------------------------------------------------------+
    //| Counts active positions matching symbol and magic number         |
    //+------------------------------------------------------------------+
    int CountOpenPositions(string symbol, ulong magic)
      {
       int count = 0;
       for(int i = PositionsTotal() - 1; i >= 0; i--)
         {
          ulong ticket = PositionGetTicket(i);
          if(ticket > 0 && PositionSelectByTicket(ticket))
            {
             if(PositionGetString(POSITION_SYMBOL) == symbol && 
                PositionGetInteger(POSITION_MAGIC) == magic)
               {
                count++;
               }
            }
         }
       return count;
      }
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
      {
       static datetime last_bar_time = 0;
       datetime current_bar_time = iTime(_Symbol, PERIOD_CURRENT, 0);
    
       if(current_bar_time != last_bar_time)
         {
          last_bar_time = current_bar_time;
    
          if(CheckPointer(g_trailing) != POINTER_INVALID)
            {
             //--- Opens 1 test position for trailing demonstration in visual mode
             if(CountOpenPositions(_Symbol, InpMagicNumber) == 0)
               {
                CTrade trade;
                trade.SetExpertMagicNumber(InpMagicNumber);
                double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
                if(!trade.Buy(0.10, _Symbol, ask, 0.0, 0.0, "Demo Trailing"))
                  {
                   PrintFormat("Test order failed: %s (retcode: %u)", 
                               trade.ResultRetcodeDescription(), trade.ResultRetcode());
                  }
               }
    
             //--- Execute trailing engine without parameter redundancy
             g_trailing.ProcessAllPositions();
            }
         }
      }


    Testing and Functional Validation

    To verify class execution, the engine was evaluated against structured functional test scenarios and verified in the Strategy Tester.

    Functional Test Matrix

    Scenario Input Condition Expected Behavior Outcome
    Long Stop Below Open Candidate SL is below entry price with InpOnlyInProfit=true Modification skipped (returns TRAILING_STATUS_SKIPPED_OPEN_PRICE) Passed
    Long SL Advancement Candidate SL is above entry price + minimum step SL modified to candidate level Passed
    Small Step Rejection Candidate SL increase is smaller than Min Step Request filtered internally without sending server order Passed
    Stops Level Protection Candidate SL is closer than broker SYMBOL_TRADE_STOPS_LEVEL Modification rejected pre-flight (prevents retcode 10016) Passed
    Freeze Level Protection Active SL is within SYMBOL_TRADE_FREEZE_LEVEL Modification skipped pre-flight (prevents server error) Passed
    Hedging Isolation Positions belong to different Magic Numbers Only matching magic positions are evaluated and modified Passed

    Strategy Tester Configuration

    Testing Parameter Value
    Symbol Asset EURUSD
    Timeframe M15
    Test Period January 2025 – July 2026
    Modeling Mode Every tick based on real ticks
    Account Deposit & Type $10,000 USD / Hedging Account
    Execution Mode Normal delay, 20ms fixed simulation
    Simple TR Period 14 bars
    TR Multiplier 2.0
    Minimum Step 10 points

    GIF

    Figure 2: Real-time dynamic stop loss adjustment running in Strategy Tester visual mode.

    Execution Telemetry and Diagnostics

    The presented EA template captures telemetry data directly from the CVolatilityTrailing instance during testing. Upon simulation completion, OnDeinit() outputs the internal counters accumulated by the TrailingTelemetry structure:

    Diagnostic Metric Observed Value Description
    Total Evaluations 512 Total bar evaluations processed across active positions
    Modifications Confirmed 84 Server-confirmed position modifications (retcode 10009/10008)
    Skipped (Minimum Step Filter) 382 Candidate changes smaller than InpMinStepPoints * Point
    Skipped (Open Price Guard) 46 Candidate evaluations while position had not yet achieved profit
    Proactive Stops Level Rejections 0 Candidate levels filtered prior to server transmission
    Proactive Freeze Level Rejections 0 Modifications suppressed due to proximity to active SL
    Server Execution Errors 0 Server-side rejections or unexpected network errors

    Testing Scope and Boundary Notes

    The simulation metrics presented above reflect a benchmark test run using long positions on EURUSD M15 under a hedging account configuration with InpOnlyInProfit=true. While the underlying CVolatilityTrailing class implements symmetric evaluation for short positions (checking Ask + offset, downward ratchet rules, and inverted profit boundaries), live deployment in netting accounts or across instruments with asymmetric swap and spread regimes may yield different modification distributions.


    Broker Execution Rules and Stop Level Guardrails

    When deploying trailing stop logic to live trading servers, developers must account for broker-specific execution rules and market constraints:

    1. Minimum Stop Distance (SYMBOL_TRADE_STOPS_LEVEL)

    Brokers enforce a minimum distance between the current market price and pending stop-loss orders. If the proposed trailing stop level is closer to current Bid/Ask than the distance specified by SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL), the trade server will reject the request with retcode 10016 (TRADE_RETCODE_INVALID_STOPS). Our class evaluates this boundary before dispatching requests.

    2. Freeze Levels (SYMBOL_TRADE_FREEZE_LEVEL)

    During volatile market events, brokers may freeze order modification requests if the current market price is within the freeze level distance of an existing stop loss. The engine checks this constraint to gracefully skip modification attempts when the market is too close to the active order level.

    3. Independence of Minimum Step and Stops Level

    It is critical to distinguish between the minimum step filter and the broker's Stops Level:

    • The minimum step parameter (InpMinStepPoints) controls how far a proposed stop loss must advance relative to the current active stop loss before generating an order modification.

    • The broker's Stops Level (SYMBOL_TRADE_STOPS_LEVEL) governs the minimum permissible distance between the proposed stop loss and the current market price.

    These two checks are mathematically independent. Increasing the minimum step parameter reduces modification frequency, but does not guarantee that a candidate stop level complies with the broker's Stops Level.

    4. Tick Size Alignment (SYMBOL_TRADE_TICK_SIZE)

    Direct floating-point price assignments often result in off-tick values on CFD indices and commodity contracts. Quantizing candidate prices via RoundToTickSize guarantees that stop orders conform strictly to exchange specifications.

    5. Spread Expansion Dynamics

    Evaluating True Range over completed bars prevents momentary spread widening from distorting the volatility calculation itself. However, because live execution levels anchor candidate stops to current Bid (for buys) and Ask (for sells), sudden spread widening during market rollovers or major news releases can temporarily shift candidate stop levels. Traders should combine closed-bar volatility evaluation with reasonable spread filters during volatile market sessions.


    System Limitations

    While the volatility trailing stop engine provides adaptive exit management, developers should observe the following structural limitations:

    • The Simple TR average calculates an unweighted mean over the selected window; it does not predict sudden high-impact economic news events.
    • The engine manages existing open positions; it does not generate entry signals or manage position sizing.
    • Closed-bar True Range evaluation stabilizes the volatility metric, but does not eliminate execution slippage during rapid market breakouts.

    Conclusion and Reusable Artifacts

    This article delivered a reusable, broker-aware Volatility Trailing Stop engine for MQL5. By encapsulating volatility calculation, candidate validation, broker constraint checks, and position modification within a reusable class, developers can integrate dynamic stop management into existing Expert Advisors without embedding monolithic code in their main event loops.

    Key architectural trade-offs and implementation boundaries established in this solution include:

    • Closed-Bar Volatility Stability: True Range is evaluated exclusively over completed bars (sampling shift 1 in terminal history), insulating the volatility offset from active bar tick noise.

    • Real-Time Price Anchoring: Candidate levels anchor to live Bid and Ask quotes, ensuring responsive stop placement while requiring strict quantization via SYMBOL_TRADE_TICK_SIZE.

    • Configurable Profit Protection: The engine provides an optional open-price guard (InpOnlyInProfit) to restrict trailing stop advancement until a position enters profitable territory, or to trail immediately for early risk reduction.

    • Dual Broker Guardrails: The engine enforces independent validation for both broker Stops Level (distance to market price) and Freeze Level (distance to active stop).

    • Diagnostic Approximation vs. Live Execution: The companion indicator serves as a non-repainting visual approximation using previous-bar closes and directional ratchet logic, whereas live execution levels reflect real-time Bid/Ask quotes and broker-level constraints.

    Within these boundaries, the engine provides a transparent and extensible foundation for integrating volatility-adjusted trailing stop management into MQL5 Expert Advisors.

    File Name Description
    Trailing_Engine.mqh Reusable include class handling rate copying, tick-size quantization, broker guardrails, and confirmed position modification.
    EA_Trailing_Demo.mq5 Expert Advisor template demonstrating new-bar execution, single-position gating, and telemetry reporting.
    Ind_Volatility_Trailing.mq5 Diagnostic custom indicator rendering non-repainting directional ratchet volatility trailing stops on the chart.
    MQL5.zip Compressed project archive with structured subfolders.
    Attached files |
    Trailing_Engine.mqh (15.56 KB)
    MQL5.zip (7.17 KB)
    LLM-Based Trading Agent with Embedded Top Trader Philosophy LLM-Based Trading Agent with Embedded Top Trader Philosophy
    The article provides a critical analysis of an LLM strategy in which forecasting the direction is separated from trading decisions, and demonstrates why this leads to a disconnect between metrics and PnL. We will describe procedures for dataset balancing, feature engineering, prompt and response preparation, fine-tuning configuration in Ollama, and reliable parsing. Backtesting and forward testing reveal systematic degradation. The practical conclusion is that the problem must be formulated as a direct optimization of trading outcomes.
    From Basic to Intermediate: Operator Overloading (III) From Basic to Intermediate: Operator Overloading (III)
    In this article, we will examine how to implement overloading for both logical operators and comparison operators. This requires a certain amount of caution and a fair amount of attention. Even a minor oversight when implementing the overloading of these operators can render the entire code completely unusable. If any problems arise in the overloading, the entire database created from the results generated by the code will have to be either discarded completely or, at the very least, reviewed in full.
    Features of Experts Advisors Features of Experts Advisors
    Creation of expert advisors in the MetaTrader trading system has a number of features.
    Market Replay: Unity Is Strength (I) Market Replay: Unity Is Strength (I)
    We're entering the home stretch. The development of the replay/simulation system is nearly complete. Of course, we still have a few things left to finish, but compared to everything we've already done, completing what's left won't be difficult. However, it is essential to fully absorb and understand everything covered in this article. So I hope you enjoy reading this and, above all, that you enjoy this final stage of the journey.