How to Implement a Trailing Stop in MQL5: A Practical Example

Trailing stops are a crucial risk management tool in algorithmic trading, allowing EAs to lock in profits and limit potential losses as a trade moves favorably. Implementing them correctly in MQL5 requires a solid understanding of order management functions and event handling.

Introduction to Trailing Stops in MQL5

What is a Trailing Stop and Why Use It?

A trailing stop is a type of stop-loss order that follows the price of an asset at a specified distance as it moves in a profitable direction. Unlike a static stop loss, which remains fixed unless manually moved, a trailing stop automatically adjusts. This dynamic nature helps protect accumulated profits without needing constant manual intervention.

The primary purpose is to allow a trade to continue benefiting from favorable price movements while simultaneously ensuring that if the price reverses, the stop loss triggers, securing a portion of the unrealized profit.

Advantages and Disadvantages of Using Trailing Stops

Advantages:

  • Profit Protection: Automatically locks in gains as the price moves favorably.
  • Risk Management: Limits potential losses if the trend reverses.
  • Automation: Reduces the need for manual adjustment, crucial for unattended EAs.
  • Captures Trends: Allows participation in extended price runs.

Disadvantages:

  • Whipsaw Risk: In volatile or choppy markets, trailing stops can be triggered prematurely, cutting short potentially profitable trades.
  • Optimal Distance: Determining the correct trailing distance is critical and often requires significant backtesting and optimization; too tight and it’s triggered too early, too wide and it offers insufficient protection.
  • Execution Risk: Slippage can still occur, especially during high volatility or fast price movements, meaning the actual exit price might differ from the stop level.

Understanding the Core Logic Behind Trailing Stop Implementation

The fundamental logic involves monitoring open positions and, for each position:

  1. Determining if it is currently in profit (or meets a minimum profit threshold).
  2. Calculating the required stop-loss level based on the current price and the defined trailing distance.
  3. Comparing the calculated level to the position’s current stop-loss level.
  4. If the calculated level is more favorable (higher for a buy, lower for a sell) than the current stop loss, modify the position to set the new stop loss.

This logic is typically executed on every price tick (OnTick function) or periodically (e.g., on the new bar, using OnTimer), ensuring the stop loss follows the price closely.

Implementing a Basic Trailing Stop in MQL5

Implementing a basic trailing stop involves iterating through open positions, checking conditions, and modifying orders.

Setting Up the MQL5 Environment and Initializing Variables

An MQL5 Expert Advisor starts with inclusion directives, input parameters, and global variables.

//+------------------------------------------------------------------+
//| Simple Trailing Stop EA                                          |
//|                                                                  |
//+------------------------------------------------------------------+
#property copyright "Your Name"
#property link      "Your Website"
#property version   "1.00"
#property strict

// Input parameters for trailing stop
input int TrailingStopPips = 20; // Trailing distance in pips
input int MinProfitPips    = 5;  // Minimum profit required to activate trailing stop

// Global variables if needed
// double trailing_point; // Can be used for pip calculation precision

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //trailing_point = Point(); // Initialize trailing point precision
   //if(_Digits==5 || _Digits==3) trailing_point = Point()*10; // Adjust for fractional pips
   // For simplicity in this example, we'll use standard Point()

   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   // Cleanup operations here
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   // Trailing stop logic will go here
  }

Note the input variables for trailing distance and minimum profit. The OnInit function is used for initial setup, although for this simple example, it’s mostly boilerplate.

Creating the OnTick() Function and Handling Price Data

The OnTick() function is the heart of most EAs, executing on every price change. Inside OnTick, we iterate through open positions.

void OnTick()
  {
   // Get total number of open positions
   int total_positions = PositionsTotal();

   // Iterate through all open positions
   for (int i = total_positions - 1; i >= 0; i--)
     {
      // Get position ticket
      ulong position_ticket = PositionGetTicket(i);

      // Select the position by ticket
      if (PositionSelectByTicket(position_ticket))
        {
         // Check if the position is for the current symbol and magic number (optional but recommended)
         if (PositionGetString(POSITION_SYMBOL) == Symbol() //&& PositionGetInteger(POSITION_MAGIC) == YOUR_MAGIC_NUMBER)
           {
            // Get position details
            long position_type = PositionGetInteger(POSITION_TYPE);
            double current_price = (position_type == POSITION_TYPE_BUY) ? SymbolInfoDouble(Symbol(), SYMBOL_ASK) : SymbolInfoDouble(Symbol(), SYMBOL_BID);
            double open_price = PositionGetDouble(POSITION_PRICE_OPEN);
            double current_sl = PositionGetDouble(POSITION_SL);

            // Calculate current profit in pips
            double profit_pips = (current_price - open_price) / _Point; // Use _Point for current symbol
            if (position_type == POSITION_TYPE_SELL) profit_pips = (open_price - current_price) / _Point;

            // Check if minimum profit condition is met
            if (profit_pips >= MinProfitPips)
              {
               // Logic to calculate new stop loss level goes here
              }
           }
        }
     }
  }

We loop backward to handle potential position closures within the loop without skipping elements. We retrieve position details like type, open price, and current stop loss.

Calculating the Trailing Stop Level Based on Price Movement

This is where we calculate the target stop-loss level based on the current price and the TrailingStopPips input.

// Inside the if (profit_pips >= MinProfitPips) block...

               double new_sl = 0.0; // Initialize new stop loss level

               if (position_type == POSITION_TYPE_BUY)
                 {
                  // For a buy position, trailing stop is (Current Price - Trailing Distance in Points)
                  new_sl = current_price - TrailingStopPips * _Point;

                  // We only move the stop loss up (higher) for a buy position
                  if (current_sl == 0.0 || new_sl > current_sl)
                    {
                     // Modify position with the new stop loss
                     // Call OrderModify here
                    }
                 }
               else // POSITION_TYPE_SELL
                 {
                  // For a sell position, trailing stop is (Current Price + Trailing Distance in Points)
                  new_sl = current_price + TrailingStopPips * _Point;

                  // We only move the stop loss down (lower) for a sell position
                  if (current_sl == 0.0 || new_sl < current_sl)
                    {
                     // Modify position with the new stop loss
                     // Call OrderModify here
                    }
                 }

The _Point variable is crucial here. It gives the point size for the current symbol. We multiply the desired pips by _Point to get the distance in points. For buy positions, the new stop loss is Ask - TrailingDistance, and we only update if the calculated new_sl is higher than the current current_sl (or if current_sl is 0, meaning no stop loss is set). For sell positions, it’s Bid + TrailingDistance, and we update if new_sl is lower.

Modifying Order Stop Loss Levels Using OrderModify()

In MQL5, position modification is handled using PositionModify(). There is no OrderModify function for positions directly; it’s used for pending orders. For modifying an open position (like changing SL/TP), you use PositionModify.

// Inside the conditional block where new_sl is more favorable...

                     // Normalize the new stop loss level to the symbol's price digits
                     new_sl = NormalizeDouble(new_sl, _Digits);

                     // Check if the new SL is different from the current one
                     if (new_sl != current_sl)
                       {
                        MqlTradeRequest request = {0};
                        MqlTradeResult  result = {0};

                        request.action = TRADE_ACTION_SLTP; // Action type for SL/TP modification
                        request.position = position_ticket; // Position ticket
                        request.sl = new_sl;               // New Stop Loss level
                        request.tp = PositionGetDouble(POSITION_TP); // Keep current Take Profit

                        // Send the request
                        if (OrderSend(request, result))
                          {
                           // Check result status
                           if (result.retcode == TRADE_RETCODE_DONE)
                             {
                              Print("Trailing Stop updated for position ", position_ticket, " to ", new_sl);
                             }
                           else
                             {
                              Print("Failed to modify position ", position_ticket, ": ", result.retcode);
                             }
                          }
                        else
                          {
                           Print("OrderSend failed for position ", position_ticket, ", error: ", GetLastError());
                          }
                       }

Note the difference from MQL4 where OrderModify was used directly on order tickets. In MQL5, you build an MqlTradeRequest structure and send it using OrderSend with TRADE_ACTION_SLTP. We include the position ticket, the new SL, and the existing TP (if any). Normalizing the price is crucial to avoid incorrect stop levels due to floating-point precision.

Advanced Trailing Stop Implementation Techniques

Moving beyond a fixed-pip distance, other methods offer more dynamic trailing based on market volatility or percentage of the position value.

Implementing Trailing Stop Based on ATR (Average True Range)

ATR is a volatility indicator. Using an ATR-based trailing stop means the stop distance adjusts to market conditions: wider during high volatility, tighter during low volatility.

  1. Calculate ATR: Get the ATR value for a specified period (e.g., 14 bars) and shift (e.g., 1 bar ago).
  2. Determine Trailing Distance: Multiply the ATR value by a factor (e.g., ATR_Factor * iATR(Symbol(), Period(), ATR_Period, 1)).
  3. Calculate New SL: CurrentPrice - TrailingDistance for buy, CurrentPrice + TrailingDistance for sell.
  4. Compare and Modify: Same logic as the basic example, ensuring the new SL is more favorable than the current one.
// Inside OnTick, within the position loop and profit check...
input int ATR_Period = 14;
input double ATR_Factor = 2.0; // e.g., 2 * ATR

... // Get position details as before

            if (profit_pips >= MinProfitPips)
              {
               double current_atr = iATR(Symbol(), Period(), ATR_Period, 1); // ATR of the previous bar
               if (current_atr > 0) // Ensure ATR is valid
                 {
                  double trailing_distance = current_atr * ATR_Factor;
                  double new_sl = 0.0;

                  if (position_type == POSITION_TYPE_BUY)
                    {
                     new_sl = current_price - trailing_distance;
                     if (current_sl == 0.0 || new_sl > current_sl)
                       {
                        // Call PositionModify/OrderSend TRADE_ACTION_SLTP with new_sl
                       }
                    }
                  else // POSITION_TYPE_SELL
                    {
                     new_sl = current_price + trailing_distance;
                     if (current_sl == 0.0 || new_sl < current_sl)
                       {
                         // Call PositionModify/OrderSend TRADE_ACTION_SLTP with new_sl
                       }
                    }
                 }
              }

This makes the trailing stop more adaptive to current market volatility.

Using a Percentage-Based Trailing Stop

A percentage-based trailing stop trails the price by a fixed percentage of the current price.

  1. Determine Trailing Percentage: Input a percentage (e.g., 1%).
  2. Calculate Trailing Distance: CurrentPrice * (TrailingPercentage / 100.0).
  3. Calculate New SL: CurrentPrice - TrailingDistance for buy, CurrentPrice + TrailingDistance for sell.
  4. Compare and Modify: Same logic as before.
// Inside OnTick, within the position loop and profit check...
input double TrailingStopPercentage = 1.0; // Trailing percentage (e.g., 1.0 means 1%)

... // Get position details as before

            if (profit_pips >= MinProfitPips)
              {
               double trailing_distance = current_price * (TrailingStopPercentage / 100.0);
               double new_sl = 0.0;

               if (position_type == POSITION_TYPE_BUY)
                 {
                  new_sl = current_price - trailing_distance;
                  if (current_sl == 0.0 || new_sl > current_sl)
                    {
                     // Call PositionModify/OrderSend TRADE_ACTION_SLTP with new_sl
                    }
                 }
               else // POSITION_TYPE_SELL
                 {
                  new_sl = current_price + trailing_distance;
                  if (current_sl == 0.0 || new_sl < current_sl)
                    {
                     // Call PositionModify/OrderSend TRADE_ACTION_SLTP with new_sl
                    }
                 }
              }

This approach is particularly useful for instruments with significantly different price ranges or when you want the stop distance to scale with the price magnitude.

Adding a Breakeven Feature to the Trailing Stop

A breakeven feature automatically moves the stop loss to the position’s open price (or slightly beyond) once a certain profit threshold is reached. This ensures that even if the market reverses immediately after hitting the threshold, the trade will close at no loss (or a small profit covering commission/spread).

This can be integrated into the trailing stop logic:

  1. Define Breakeven Threshold: Input a profit level in pips (e.g., 10 pips).
  2. Define Breakeven Buffer: Optional input for a few extra pips beyond the open price (e.g., 2 pips).
  3. Check Breakeven Condition: If CurrentProfitPips >= BreakevenThreshold and the current stop loss is still at or below the open price (for a buy) or at or above the open price (for a sell).
  4. Calculate Breakeven SL: OpenPrice + BreakevenBuffer * _Point for buy, OpenPrice - BreakevenBuffer * _Point for sell.
  5. Modify to Breakeven: Set the stop loss to the calculated breakeven level if the condition is met.
  6. Then, Apply Trailing: After checking and potentially setting breakeven, the standard trailing stop logic checks if the price has moved further past the breakeven point such that the trailing stop calculation results in a stop loss even more favorable than the breakeven level. If so, it updates the stop loss to the trailing level.

This sequence ensures the breakeven is hit first, protecting against initial reversals, and the trailing stop takes over as profits increase further.

Practical Example: An MQL5 Expert Advisor with Trailing Stop

Combining the elements discussed, here’s a simplified structure for an EA that implements a trailing stop. This example focuses on the trailing stop logic itself, assuming positions are already open.

Complete Code Walkthrough of a Trailing Stop EA

//+------------------------------------------------------------------+
//| Trailing Stop EA Example                                         |
//|                                                                  |
//+------------------------------------------------------------------+
#property copyright "Your Name"
#property link      "Your Website"
#property version   "1.00"
#property strict

// Input parameters
input int TrailingStopPips = 20; // Trailing distance in pips
input int MinProfitPips    = 5;  // Minimum profit required to activate trailing stop
input int BreakevenPips    = 10; // Profit in pips to trigger breakeven
input int BreakevenBuffer  = 2;  // Pips beyond open price for breakeven

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   // Initialization if needed
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   // Cleanup if needed
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   // --- Trailing Stop and Breakeven Logic ---

   // Iterate through all open positions
   for (int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong position_ticket = PositionGetTicket(i);

      // Select the position by ticket
      if (PositionSelectByTicket(position_ticket))
        {
         // Check if the position is for the current symbol and magic number (replace 0 with your EA's magic number if used)
         // if (PositionGetString(POSITION_SYMBOL) == Symbol() && PositionGetInteger(POSITION_MAGIC) == 0)
         if (PositionGetString(POSITION_SYMBOL) == Symbol())
           {
            long position_type = PositionGetInteger(POSITION_TYPE);
            double open_price = PositionGetDouble(POSITION_PRICE_OPEN);
            double current_sl = PositionGetDouble(POSITION_SL);
            double current_tp = PositionGetDouble(POSITION_TP); // Keep current TP

            // Get current relevant price (Ask for buy, Bid for sell)
            double current_price = (position_type == POSITION_TYPE_BUY) ? SymbolInfoDouble(Symbol(), SYMBOL_ASK) : SymbolInfoDouble(Symbol(), SYMBOL_BID);

            // Calculate current profit in pips
            double profit_pips = 0;
            if (position_type == POSITION_TYPE_BUY) profit_pips = (current_price - open_price) / _Point;
            else profit_pips = (open_price - current_price) / _Point;

            double target_sl = 0.0; // Target SL level initially zero
            bool modify_needed = false;

            // --- 1. Check for Breakeven --- 
            if (BreakevenPips > 0 && profit_pips >= BreakevenPips)
              {
               double breakeven_sl = 0.0;
               if (position_type == POSITION_TYPE_BUY) breakeven_sl = open_price + BreakevenBuffer * _Point;
               else breakeven_sl = open_price - BreakevenBuffer * _Point;

               // Normalize breakeven SL
               breakeven_sl = NormalizeDouble(breakeven_sl, _Digits);

               // Check if breakeven SL is more favorable than current SL
               if (current_sl == 0.0 || (position_type == POSITION_TYPE_BUY && breakeven_sl > current_sl) || (position_type == POSITION_TYPE_SELL && breakeven_sl < current_sl))
                 {
                  target_sl = breakeven_sl;
                  modify_needed = true;
                 }
              }

            // --- 2. Check for Trailing Stop (only if minimum profit reached) ---
            if (MinProfitPips > 0 && profit_pips >= MinProfitPips)
              {
               double trailing_sl = 0.0;
               if (position_type == POSITION_TYPE_BUY) trailing_sl = current_price - TrailingStopPips * _Point;
               else trailing_sl = current_price + TrailingStopPips * _Point;

               // Normalize trailing SL
               trailing_sl = NormalizeDouble(trailing_sl, _Digits);

               // Compare trailing SL with current SL and potentially the breakeven SL set above
               // We only trail if the trailing SL is *more favorable* than the current SL (or breakeven SL if already set)
               if (current_sl == 0.0 || (position_type == POSITION_TYPE_BUY && trailing_sl > current_sl) || (position_type == POSITION_TYPE_SELL && trailing_sl < current_sl))
                 {
                  // Further check: ensure trailing_sl is more favorable than *current* target_sl (could be breakeven SL)
                  if (target_sl == 0.0 || (position_type == POSITION_TYPE_BUY && trailing_sl > target_sl) || (position_type == POSITION_TYPE_SELL && trailing_sl < target_sl))
                    {
                     target_sl = trailing_sl;
                     modify_needed = true;
                    }
                 }
              }

            // --- 3. Modify Position if needed ---
            if (modify_needed && target_sl != 0.0 && target_sl != current_sl)
              {
               MqlTradeRequest request = {0};
               MqlTradeResult  result = {0};

               request.action = TRADE_ACTION_SLTP; // Action type for SL/TP modification
               request.position = position_ticket; // Position ticket
               request.sl = target_sl;             // New Stop Loss level
               request.tp = current_tp;           // Keep current Take Profit

               // Send the request
               if (OrderSend(request, result))
                 {
                  if (result.retcode == TRADE_RETCODE_DONE)
                    {
                     // Print("Trailing Stop/Breakeven updated for position ", position_ticket, ": ", target_sl);
                    }
                  else
                    {
                     Print("Failed to modify position ", position_ticket, ": ", result.retcode, ". Error ", GetLastError());
                    }
                 }
               else
                 {
                  Print("OrderSend TRADE_ACTION_SLTP failed for position ", position_ticket, ", error: ", GetLastError());
                 }
              }
           }
        }
     }
  }

Explanation of Key Functions and Logic

  • PositionsTotal(): Returns the count of open positions across all symbols and accounts. We iterate from total_positions - 1 down to 0.
  • PositionGetTicket(i): Gets the ticket of the i-th position in the pool.
  • PositionSelectByTicket(ticket): Selects a specific position by its ticket for accessing its properties.
  • PositionGetString(POSITION_SYMBOL), PositionGetInteger(POSITION_TYPE), PositionGetDouble(POSITION_PRICE_OPEN), PositionGetDouble(POSITION_SL), PositionGetDouble(POSITION_TP): These functions retrieve various properties of the selected position.
  • SymbolInfoDouble(Symbol(), SYMBOL_ASK) / SYMBOL_BID: Gets the current Ask or Bid price for the chart’s symbol.
  • _Point: Provides the size of a point for the current chart symbol. Multiplying pips by _Point gives the distance in quote currency units.
  • NormalizeDouble(value, digits): Rounds a double value to a specified number of decimal places (_Digits for price levels), essential for correct price comparisons and order modifications.
  • MqlTradeRequest, MqlTradeResult, OrderSend(): The MQL5 mechanism for sending trade requests. TRADE_ACTION_SLTP specifically requests modification of Stop Loss and Take Profit.
  • GetLastError(): Returns the code of the last execution error. Useful for debugging OrderSend failures.

This example prioritizes checking for breakeven first. If the breakeven condition is met and its calculated SL is more favorable than the current SL, target_sl is updated. Then, it checks the standard trailing stop condition. If the trailing stop condition is met and its calculated SL is more favorable than the current target_sl (which might already be the breakeven level), target_sl is updated again. Finally, if target_sl was updated and is different from the original current_sl, the OrderSend call is made.

Testing and Optimizing the EA’s Trailing Stop Functionality

Backtesting is crucial for finding optimal values for TrailingStopPips, MinProfitPips, BreakevenPips, BreakevenBuffer (and ATR_Period/ATR_Factor if using ATR). Use the Strategy Tester in MetaTrader 5:

  1. Compile the EA.
  2. Open the Strategy Tester (Ctrl+R).
  3. Select the EA, symbol, time frame, and desired date range.
  4. Set the ‘Mode’ to ‘Every tick based on real ticks’ for the most accurate backtest, especially for trailing stops.
  5. In the ‘Inputs’ tab, adjust the parameters. Use the ‘Optimization’ feature (e.g., ‘Fast genetic based algorithm’) to test a range of values for your trailing stop parameters.
  6. Analyze the results: look at Gross Profit, Drawdown, Profit Factor, and also visualize trades on the chart to see how the trailing stop behaved.

Optimization helps identify parameters that performed well historically, but remember that past performance is not indicative of future results.

Troubleshooting and Best Practices for Trailing Stops in MQL5

Implementing trailing stops can lead to common pitfalls if not done carefully.

Common Errors and How to Avoid Them

  • Incorrect Pip Calculation: Using a fixed 0.00001 or 0.01 instead of _Point can cause errors, especially on symbols with different decimal places (e.g., JPY pairs). Always use _Point for the current symbol.
  • Normalization Issues: Not using NormalizeDouble() on the calculated stop loss level before modifying the position can lead to modification failures due to incorrect precision.
  • Modifying Too Frequently: Excessive OrderSend calls on every tick can put a strain on the trading server and potentially lead to processing delays or ‘Trade context is busy’ errors. Consider adding a check to only modify if the price has moved a significant distance (e.g., a few points) since the last modification attempt.
  • Ignoring OrderSend Results: Always check the result.retcode from OrderSend and GetLastError() if OrderSend returns false. This provides critical debugging information.
  • Looping Direction: Looping backwards (for (int i = PositionsTotal() - 1; i >= 0; i--)) is standard practice when modifying or closing orders/positions within a loop, as it prevents skipping indices if an item is removed from the collection.

Considerations for Different Market Conditions

  • Volatile Markets: Fixed-pip or percentage trailing stops can be hit too easily. ATR-based trailing stops or wider distances might be more suitable.
  • Trending Markets: Trailing stops excel here, allowing maximum profit capture. The challenge is setting a distance that doesn’t exit too early during minor pullbacks.
  • Ranging Markets: Trailing stops are less effective and prone to whipsaws. They might trigger repeatedly for small profits or even losses. Consider disabling trailing stops in ranging conditions or using alternative exit strategies.

Improving Efficiency and Reducing Latency in Trailing Stop Logic

  • Optimize Position Selection: If the EA manages positions with a specific magic number, filter positions by PositionGetInteger(POSITION_MAGIC) to avoid processing positions opened manually or by other EAs.
  • Minimize Calls: As mentioned, avoid unnecessary modification attempts. Check if the calculated new_sl is actually different from the current_sl before calling OrderSend.
  • Execute Less Frequently: For strategies that don’t require tick-by-tick precision, consider running the trailing stop logic only on new bars (OnTrade, checking for TRADE_EVENT_POSITION_CLOSE or on a timer via EventSetTimer/OnTimer). Tick-by-tick is necessary for tight trailing stops but might be overkill otherwise.

Implementing a robust trailing stop is an iterative process involving careful coding, thorough testing, and optimization. By understanding the MQL5 position management functions and applying sound logic, you can effectively use trailing stops to enhance your algorithmic trading strategies.


Leave a Reply