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:
- Determining if it is currently in profit (or meets a minimum profit threshold).
- Calculating the required stop-loss level based on the current price and the defined trailing distance.
- Comparing the calculated level to the position’s current stop-loss level.
- 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.
- Calculate ATR: Get the ATR value for a specified period (e.g., 14 bars) and shift (e.g., 1 bar ago).
- Determine Trailing Distance: Multiply the ATR value by a factor (e.g.,
ATR_Factor * iATR(Symbol(), Period(), ATR_Period, 1)). - Calculate New SL:
CurrentPrice - TrailingDistancefor buy,CurrentPrice + TrailingDistancefor sell. - 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.
- Determine Trailing Percentage: Input a percentage (e.g., 1%).
- Calculate Trailing Distance:
CurrentPrice * (TrailingPercentage / 100.0). - Calculate New SL:
CurrentPrice - TrailingDistancefor buy,CurrentPrice + TrailingDistancefor sell. - 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:
- Define Breakeven Threshold: Input a profit level in pips (e.g., 10 pips).
- Define Breakeven Buffer: Optional input for a few extra pips beyond the open price (e.g., 2 pips).
- Check Breakeven Condition: If
CurrentProfitPips >= BreakevenThresholdand 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). - Calculate Breakeven SL:
OpenPrice + BreakevenBuffer * _Pointfor buy,OpenPrice - BreakevenBuffer * _Pointfor sell. - Modify to Breakeven: Set the stop loss to the calculated breakeven level if the condition is met.
- 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 fromtotal_positions - 1down to0.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_Pointgives the distance in quote currency units.NormalizeDouble(value, digits): Rounds a double value to a specified number of decimal places (_Digitsfor price levels), essential for correct price comparisons and order modifications.MqlTradeRequest,MqlTradeResult,OrderSend(): The MQL5 mechanism for sending trade requests.TRADE_ACTION_SLTPspecifically requests modification of Stop Loss and Take Profit.GetLastError(): Returns the code of the last execution error. Useful for debuggingOrderSendfailures.
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:
- Compile the EA.
- Open the Strategy Tester (
Ctrl+R). - Select the EA, symbol, time frame, and desired date range.
- Set the ‘Mode’ to ‘Every tick based on real ticks’ for the most accurate backtest, especially for trailing stops.
- 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.
- 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.00001or0.01instead of_Pointcan cause errors, especially on symbols with different decimal places (e.g., JPY pairs). Always use_Pointfor 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
OrderSendcalls 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
OrderSendResults: Always check theresult.retcodefromOrderSendandGetLastError()ifOrderSendreturnsfalse. 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_slis actually different from thecurrent_slbefore callingOrderSend. - 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 forTRADE_EVENT_POSITION_CLOSEor on a timer viaEventSetTimer/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.