Why the Elder-Ray Index Belongs in Your EA Toolkit
Most traders who build EAs gravitate toward the usual suspects: RSI, MACD, Bollinger Bands. Nothing wrong with that — they work. But if you've spent any time in the Strategy Tester staring at equity curves that look like a dying heartbeat, you know the market has a way of punishing lazy indicator combos. You need something that confirms both direction and conviction.
That's where the Elder-Ray Index comes in. Developed by Dr. Alexander Elder, it breaks price action into two components: Bull Power and Bear Power. These measure how far buyers or sellers have pushed price relative to a smoothed average (typically a 13-period EMA). When both align with the trend, you get entries with higher probability. When they diverge, you get early warnings.
In this post, I'll walk you through building a complete MQL5 EA that uses the Elder-Ray Index for trend and momentum confirmation, plus a dynamic stop-loss that adapts to market volatility. No fluff, no fake promises — just code you can compile and test today.
Understanding the Elder-Ray Index
Before we write a single line of MQL5, let's make sure we're on the same page about what this indicator actually tells us. The Elder-Ray Index consists of three components:
- EMA (13-period): The trend filter. Price above = uptrend, below = downtrend.
- Bull Power: High price minus the 13 EMA. Positive values mean bulls are in control.
- Bear Power: Low price minus the 13 EMA. Negative values mean bears are in control.
The logic is simple but powerful. In a strong uptrend, Bull Power should be positive and rising, while Bear Power stays negative but shallow (or turns positive briefly). When both Bull Power and Bear Power are positive, that's extreme bullishness — often a blow-off top. When both are negative, you're looking at capitulation.
For our EA, we'll use three conditions:
- Trend filter: Price above the 13 EMA for long trades, below for short trades.
- Momentum confirmation: Bull Power positive and rising (current > previous) for longs; Bear Power negative and falling for shorts.
- Dynamic stop-loss: Based on the current Average True Range (ATR) multiplied by a user-defined factor, placed below the recent swing low (longs) or above the recent swing high (shorts).
This gives us a system that doesn't just chase price — it waits for evidence that the move has conviction.
Building the MQL5 EA Step by Step
Prerequisites and Setup
You'll need MetaTrader 5 and the MetaEditor IDE. If you haven't already, create a new Expert Advisor: File → New → Expert Advisor. Name it ElderRayTrendEA. We'll write everything from scratch — no external dependencies beyond the built-in iMA, iATR, and custom indicator functions.
Indicator Input Parameters
Let's define the inputs a trader can tweak without touching code. Open the EA file and add this block at the top:
//+------------------------------------------------------------------+
//| Input parameters |
//+------------------------------------------------------------------+
input double LotSize = 0.1; // Fixed lot size
input int MAPeriod = 13; // EMA period for trend
input int ATRPeriod = 14; // ATR period for dynamic SL
input double ATRMultiplier = 1.5; // ATR multiplier for SL
input bool UseTrailingStop = true; // Enable trailing stop
input int MagicNumber = 202410; // EA magic number
These are straightforward. The ATRMultiplier controls how aggressive your stop is — 1.5 means the stop is placed 1.5 times the current ATR value away from entry. I've found 1.5 to 2.0 works well on H1, but you'll want to optimize this for your timeframe.
Calculating Bull Power and Bear Power
MQL5 doesn't have a built-in Elder-Ray Index function, so we'll compute it manually. In the OnTick() handler, we need the EMA value and the high/low prices for the current and previous bars:
//+------------------------------------------------------------------+
//| Get Bull Power and Bear Power values |
//+------------------------------------------------------------------+
double GetBullPower(int shift)
{
double ema = iMA(_Symbol, _Period, MAPeriod, 0, MODE_EMA, PRICE_CLOSE, shift);
if(ema == 0) return 0;
return iHigh(_Symbol, _Period, shift) - ema;
}
double GetBearPower(int shift)
{
double ema = iMA(_Symbol, _Period, MAPeriod, 0, MODE_EMA, PRICE_CLOSE, shift);
if(ema == 0) return 0;
return iLow(_Symbol, _Period, shift) - ema;
}
Notice I'm using iHigh and iLow for the current bar, not the close. This is critical — Bull Power measures the maximum strength of buyers during the bar, not where the bar ended. Using close would give you a lagging signal.
Entry Logic
Now the core decision logic. We'll check conditions on every tick, but only act on new bars to avoid repainting:
//+------------------------------------------------------------------+
//| Check for new bar |
//+------------------------------------------------------------------+
bool IsNewBar()
{
static datetime lastBarTime = 0;
datetime currentBarTime = iTime(_Symbol, _Period, 0);
if(currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
return true;
}
return false;
}
Inside OnTick(), we call IsNewBar() first. If it's a new bar, we evaluate entries:
void OnTick()
{
if(!IsNewBar()) return;
double ema = iMA(_Symbol, _Period, MAPeriod, 0, MODE_EMA, PRICE_CLOSE, 1);
double close = iClose(_Symbol, _Period, 1);
double bullPower = GetBullPower(1);
double bullPowerPrev = GetBullPower(2);
double bearPower = GetBearPower(1);
double bearPowerPrev = GetBearPower(2);
// Long entry conditions
if(close > ema && bullPower > 0 && bullPower > bullPowerPrev && bearPower < 0)
{
if(!PositionExists(MagicNumber, POSITION_TYPE_BUY))
OpenBuy();
}
// Short entry conditions
if(close < ema && bearPower < 0 && bearPower < bearPowerPrev && bullPower > 0)
{
if(!PositionExists(MagicNumber, POSITION_TYPE_SELL))
OpenSell();
}
}
Let's break down the long condition: price above EMA (trend up), Bull Power positive and rising (bulls gaining strength), Bear Power negative (bears still present but not dominant). The short condition is the mirror image. Notice I'm using PositionExists() — you'll need to write that helper to check for open positions by magic number.
Dynamic Stop-Loss Calculation
This is where the EA gets interesting. Instead of a fixed pip stop, we calculate the stop based on ATR and the recent swing structure:
double CalculateStopLoss(double entryPrice, ENUM_ORDER_TYPE orderType)
{
double atr = iATR(_Symbol, _Period, ATRPeriod, 1);
double slDistance = atr * ATRMultiplier;
if(orderType == ORDER_TYPE_BUY)
{
// Find the lowest low of the last 5 bars
double lowestLow = iLow(_Symbol, _Period, iLowest(_Symbol, _Period, MODE_LOW, 5, 1));
double sl = MathMin(entryPrice - slDistance, lowestLow - _Point * 10);
return sl;
}
else // Sell
{
double highestHigh = iHigh(_Symbol, _Period, iHighest(_Symbol, _Period, MODE_HIGH, 5, 1));
double sl = MathMax(entryPrice + slDistance, highestHigh + _Point * 10);
return sl;
}
}
The logic: we take the larger of (ATR-based distance) or (recent swing extreme + a small buffer). This ensures the stop is wide enough to survive normal volatility but tight enough to cap losses if the trend fails. I use 5 bars for the swing lookback — you can make this an input parameter.
Order Placement and Trailing
Here's the OpenBuy() function:
void OpenBuy()
{
MqlTradeRequest request = {};
MqlTradeResult result = {};
double entry = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = CalculateStopLoss(entry, ORDER_TYPE_BUY);
double tp = 0; // No fixed take profit — we'll trail
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = LotSize;
request.price = entry;
request.sl = sl;
request.tp = tp;
request.deviation = 10;
request.magic = MagicNumber;
request.type = ORDER_TYPE_BUY;
OrderSend(request, result);
}
For trailing, I prefer a simple approach: on each new bar, if the price has moved in our favor by at least 1 ATR, we move the stop to break even + a small buffer. If it moves further, we tighten the stop using a fixed ATR offset. Here's a minimal trailing implementation:
void TrailStop(ulong ticket)
{
PositionSelectByTicket(ticket);
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentSL = PositionGetDouble(POSITION_SL);
double atr = iATR(_Symbol, _Period, ATRPeriod, 1);
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
double newSL = openPrice + atr * 0.5; // Move to break even + half ATR
double distanceFromEntry = SymbolInfoDouble(_Symbol, SYMBOL_BID) - openPrice;
if(distanceFromEntry > atr * 1.5 && currentSL < newSL)
{
ModifyPosition(ticket, newSL, 0);
}
}
}
I call TrailStop() on every new bar for each open position. You can make the multiplier an input — I've used 0.5 here to give price some room after break-even.
Full Code Structure and Compilation Notes
Here's the skeleton of the EA. You'll need to fill in the helper functions (PositionExists, ModifyPosition) — they're standard MQL5 boilerplate:
//+------------------------------------------------------------------+
//| ElderRayTrendEA.mq5 |
//| Trend momentum with dynamic stop-loss |
//+------------------------------------------------------------------+
#property copyright "Your Name"
#property version "1.00"
#property description "Elder-Ray Index EA with dynamic ATR stop"
input double LotSize = 0.1;
input int MAPeriod = 13;
input int ATRPeriod = 14;
input double ATRMultiplier = 1.5;
input bool UseTrailingStop = true;
input int MagicNumber = 202410;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Your logic here
}
//+------------------------------------------------------------------+
//| Helper functions |
//+------------------------------------------------------------------+
double GetBullPower(int shift) { /* as above */ }
double GetBearPower(int shift) { /* as above */ }
bool IsNewBar() { /* as above */ }
double CalculateStopLoss(double entryPrice, ENUM_ORDER_TYPE orderType) { /* as above */ }
void OpenBuy() { /* as above */ }
void OpenSell() { /* as above */ }
void TrailStop(ulong ticket) { /* as above */ }
bool PositionExists(int magic, ENUM_POSITION_TYPE type) { /* iterate positions */ }
void ModifyPosition(ulong ticket, double sl, double tp) { /* use OrderSend with MODIFY */ }
Compilation note: If you get error "function returns reference to local variable", check your CalculateStopLoss — make sure you're returning a double, not a reference. Also, MQL5 requires explicit #property declarations for inputs; don't skip them.
Pros, Cons, and Risks
Let's be honest about what this EA can and cannot do.
<td style="border:1px solid #c8d8e4;padding:7px 10px;font-size:12px;vertical-align| Aspect | Strength | Weakness |
|---|---|---|
| Trend Filter | EMA provides clear directional bias, reducing whipsaws | Lagging in fast markets — you'll miss the first 2-3 bars of a move |
| Momentum Confirmation | Bull/Bear Power divergence catches trend exhaustion early | Can give false signals in choppy sideways markets |
| Dynamic Stop-Loss | ATR-based stops adapt to volatility, reducing random stop-outs | Wider stops in high volatility can lead to larger losses per trade |






