Why Chandelier Exit Beats a Fixed Trailing Stop
Most traders who code their own Expert Advisors start with a simple fixed trailing stop — move the stop 20 pips behind price every time price moves 10 pips in your favor. It works fine in a smooth trend. But throw in a volatile news spike or a low-liquidity session, and that fixed stop gets you stopped out before the move even starts. I've watched it happen on my own charts more times than I care to count.
That's where the Chandelier Exit comes in. Developed by Chuck LeBeau, this trailing stop is based on the Average True Range (ATR). It sets the stop at a multiple of ATR below the highest high (for long positions) or above the lowest low (for shorts). The idea is simple: the stop adapts to current market volatility. When volatility expands, the stop widens. When volatility contracts, it tightens. You don't get shaken out by random noise.
In this post, I'll walk you through coding a complete Chandelier Exit trailing stop MQL4 Expert Advisor. You'll get the full source, parameter explanations, and some honest caveats from my own experience running this on live accounts. I've been using variations of this code for about three years now, and it's saved me from dozens of premature exits.
One thing I want to be upfront about: this isn't a magic bullet. The Chandelier Exit can be too wide in low-volatility environments, letting winners turn into losers. You'll need to tune it for each instrument and timeframe. But when it's dialed in, it's hands-down better than anything fixed.
How the Chandelier Exit Works
The Chandelier Exit indicator was originally designed for position traders, but it translates beautifully to intraday EAs. The calculation is straightforward:
- For long positions: Stop = Highest High over N periods - (ATR × Multiplier)
- For short positions: Stop = Lowest Low over N periods + (ATR × Multiplier)
The ATR period and the multiplier are your two main inputs. A typical setup uses a 22-period ATR with a 3x multiplier for daily charts. For lower timeframes like M15 or H1, I usually reduce the ATR period to 10-14 and the multiplier to 2-2.5. You'll need to tune this based on your instrument's volatility. For EURUSD on M15, I run ATR period 12 with multiplier 2.2. For GBPJPY, which moves more, I bump the multiplier to 2.8.
What makes the Chandelier Exit different from a simple ATR trailing stop? The ATR trailing stop typically places the stop at ATR × multiplier below the current price. The Chandelier Exit uses the highest high since entry (or lowest low for shorts). This means the stop only moves up (for longs) — it never dips back down. Once price makes a new high, the stop ratchets up. If price stalls or retraces, the stop stays put until price either breaks higher or hits the stop.
That ratcheting behavior is critical. It prevents the stop from pulling back when price consolidates, which is exactly what a fixed trailing stop does (and why it fails so often). I've seen traders lose positions on consolidation zones that lasted 2-3 hours, only to watch price blast off right after they got stopped out. The Chandelier Exit keeps you in those trades.
Key Differences From Other Trailing Methods
Let me break down how Chandelier Exit stacks up against common alternatives. I've tested all of these on real accounts, so this isn't theory:
| Method | Adapts to Volatility | Ratchets Up Only | Common Failure Mode |
|---|---|---|---|
| Fixed Pip Trail | No | Yes | Gets hit by normal volatility spikes |
| Simple ATR Trail | Yes | No | Stop pulls back during consolidation |
| Chandelier Exit | Yes | Yes | Can be too wide in low-volatility periods |
| Parabolic SAR | Partial | Yes | Whipsaws in ranging markets |
I've personally lost money on the Parabolic SAR during ranging markets — it's brutal. The fixed pip trail is fine for scalping but useless for swing trades. The simple ATR trail is better than fixed, but the pullback issue kills you in consolidations. Chandelier Exit is the only one that combines volatility adaptation with ratcheting.
MQL4 Implementation: Building the EA
Let's get into the code. I'll assume you have basic MQL4 knowledge — you know how to create a new Expert Advisor in MetaEditor, and you've compiled a few EAs before. If not, open MetaTrader 4, press F4 to launch MetaEditor, then go to File → New → Expert Advisor and save it as ChandelierExitTrailingStop.mq4.
EA Structure Overview
Our EA will have three main parts:
- Input parameters — ATR period, multiplier, lookback period, trade direction, and money management.
- Entry logic — We'll use a simple trend-following entry (optional; you can replace this with your own).
- Trailing stop logic — The core of the EA: recalculate the Chandelier Exit level on every tick and move the stop.
Here's the complete code. I've commented the key sections. Copy this into your MetaEditor file and compile it (F7).
//+------------------------------------------------------------------+
//| ChandelierExitTrailing.mq4 |
//| Copyright 2025, TradingBotMaker |
//| https://tradingbotmaker.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, TradingBotMaker"
#property link "https://tradingbotmaker.com"
#property version "1.00"
#property strict
// --- Input Parameters ---
input double LotSize = 0.1; // Fixed lot size
input int ATR_Period = 14; // ATR period
input double ATR_Multiplier = 3.0; // ATR multiplier for stop distance
input int Lookback_Period = 20; // Bars for highest high / lowest low
input bool TradeLong = true; // Allow long trades
input bool TradeShort = true; // Allow short trades
input int MagicNumber = 202501; // EA magic number
input int Slippage = 3; // Slippage in points
// Global variables
double g_chandelierStopLong = 0.0;
double g_chandelierStopShort = 0.0;
datetime g_lastBarTime = 0;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
if(ATR_Period < 1 || Lookback_Period < 1 || ATR_Multiplier <= 0)
{
Print("Invalid input parameters. Check ATR_Period, Lookback_Period, ATR_Multiplier.");
return(INIT_PARAMETERS_INCORRECT);
}
g_lastBarTime = Time[0];
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// --- Check for new bar (optional but good practice) ---
if(Time[0] != g_lastBarTime)
{
g_lastBarTime = Time[0];
// On new bar, we could recalculate entry signals here
}
// --- Calculate Chandelier Exit levels ---
CalculateChandelierExit();
// --- Manage existing positions ---
ManagePositions();
// --- Check for new entry (simple trend-following logic) ---
if(CountOpenOrders(MagicNumber) == 0)
{
CheckForEntry();
}
}
//+------------------------------------------------------------------+
//| Calculate Chandelier Exit levels |
//+------------------------------------------------------------------+
void CalculateChandelierExit()
{
int bars = MathMin(Lookback_Period, Bars - 1);
if(bars < 2) return;
double highestHigh = High[iHighest(NULL, 0, MODE_HIGH, bars, 1)];
double lowestLow = Low[iLowest(NULL, 0, MODE_LOW, bars, 1)];
double atrValue = iATR(NULL, 0, ATR_Period, 1);
if(atrValue <= 0) return;
g_chandelierStopLong = highestHigh - (atrValue * ATR_Multiplier);
g_chandelierStopShort = lowestLow + (atrValue * ATR_Multiplier);
}
//+------------------------------------------------------------------+
//| Manage trailing stops for open positions |
//+------------------------------------------------------------------+
void ManagePositions()
{
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if(OrderMagicNumber() != MagicNumber) continue;
if(OrderSymbol() != Symbol()) continue;
double newStop = 0;
if(OrderType() == OP_BUY)
{
newStop = g_chandelierStopLong;
// Only move stop up, never down
if(newStop > OrderStopLoss() + (Point * 10))
{
if(!OrderModify(OrderTicket(), OrderOpenPrice(), newStop, OrderTakeProfit(), 0, clrNONE))
{
Print("Failed to modify stop for ticket ", OrderTicket(), " Error: ", GetLastError());
}
}
}
else if(OrderType() == OP_SELL)
{
newStop = g_chandelierStopShort;
// Only move stop down, never up
if(newStop < OrderStopLoss() - (Point * 10))
{
if(!OrderModify(OrderTicket(), OrderOpenPrice(), newStop, OrderTakeProfit(), 0, clrNONE))
{
Print("Failed to modify stop for ticket ", OrderTicket(), " Error: ", GetLastError());
}
}
}
}
}
//+------------------------------------------------------------------+
//| Check for new entry signal |
//+------------------------------------------------------------------+
void CheckForEntry()
{
// Simple trend-following: buy if price above 50 EMA, sell if below
double ema50 = iMA(NULL, 0, 50, 0, MODE_EMA, PRICE_CLOSE, 1);
if(ema50 <= 0) return;
double ask = Ask;
double bid = Bid;
if(TradeLong && ask > ema50)
{
int ticket = OrderSend(Symbol(), OP_BUY, LotSize, ask, Slippage, 0, 0, "Chandelier Exit", MagicNumber, 0, clrGreen);
if(ticket > 0)
{
// Immediately set initial stop loss
if(!OrderSelect(ticket, SELECT_BY_TICKET))
{
Print("Failed to select new order");
}
else
{
double initialStop = g_chandelierStopLong;
if(!OrderModify(ticket, OrderOpenPrice(), initialStop, OrderTakeProfit(), 0, clrNONE))
{
Print("Failed to set initial stop, Error: ", GetLastError());
}
}
}
}
if(TradeShort && bid < ema50)
{
int ticket = OrderSend(Symbol(), OP_SELL, LotSize, bid, Slippage, 0, 0, "Chandelier Exit", MagicNumber, 0, clrRed);
if(ticket > 0)
{
if(!OrderSelect(ticket, SELECT_BY_TICKET))
{
Print("Failed to select new order");
}
else
{
double initialStop = g_chandelierStopShort;
if(!OrderModify(ticket, OrderOpenPrice(), initialStop, OrderTakeProfit(), 0, clrNONE))
{
Print("Failed to set initial stop, Error: ", GetLastError());
}
}
}
}
}
//+------------------------------------------------------------------+
//| Count open orders with specific magic number |
//+------------------------------------------------------------------+
int CountOpenOrders(int magic)
{
int count = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderMagicNumber() == magic && OrderSymbol() == Symbol())
count++;
}





