MQL5 Mean Reversion Strategy EA: Code, Logic & Backtest Results
This page covers how to build a multi-pair mean reversion Expert Advisor in MQL5, combining Bollinger Bands and RSI to detect price dislocations and fade overextended moves. You will find the complete EA source code, a breakdown of entry and exit logic, and backtested performance results across a 2021–2025 sample spanning major and minor forex pairs.
Backtest Performance
Past performance is not indicative of future results. Backtest statistics are based on historical data and do not guarantee future profits. Trading involves significant risk of loss. This content is for educational purposes only and does not constitute financial advice.
Strategy Logic
Entry Conditions
A long entry is triggered when the closing price touches or breaks below the lower Bollinger Band (20-period, 2.0 std dev) while the RSI(14) reading is below 35, confirming oversold momentum. A short entry fires when price closes above the upper Bollinger Band with RSI above 65, signalling an overbought extension. Position sizing is set to a fixed fractional risk of equity, and only one trade per symbol direction is allowed at a time.
Exit Conditions
Trades target the middle Bollinger Band (20-period SMA) as the primary take-profit level, representing mean reversion back to equilibrium. A hard stop-loss is placed 1.5x the Average True Range beyond the entry candle high/low to absorb normal volatility without premature exits. Positions are also closed if RSI crosses back through the 50 mid-line before reaching the band midpoint, acting as an early momentum-reversal exit signal.
MQL5 Expert Advisor Code
//+------------------------------------------------------------------+
//| MeanReversionEA.mq5 |
//| Bollinger Band + RSI Mean Reversion Expert Advisor |
//| For educational and research purposes only. |
//| Past performance does not guarantee future results. |
//+------------------------------------------------------------------+
#property copyright "Pineify Research"
#property version "1.00"
#property strict
//--- Input parameters
input int BB_Period = 20; // Bollinger Band period
input double BB_Deviation = 2.0; // Bollinger Band std deviation
input int RSI_Period = 14; // RSI period
input double RSI_OversoldLevel = 35.0; // RSI oversold threshold
input double RSI_OverboughtLevel = 65.0;// RSI overbought threshold
input double RiskPercent = 1.0; // Risk per trade (% of equity)
input double ATR_Multiplier = 1.5; // ATR multiplier for stop loss
input int ATR_Period = 14; // ATR period
input int MagicNumber = 202401; // Unique EA identifier
input string TradeComment = "MeanReversionEA";
//--- Indicator handles
int hBB = INVALID_HANDLE;
int hRSI = INVALID_HANDLE;
int hATR = INVALID_HANDLE;
//--- Buffers
double bbUpper[], bbMiddle[], bbLower[];
double rsiBuffer[];
double atrBuffer[];
//+------------------------------------------------------------------+
//| Expert initialisation |
//+------------------------------------------------------------------+
int OnInit()
{
hBB = iBands(_Symbol, PERIOD_CURRENT, BB_Period, 0, BB_Deviation, PRICE_CLOSE);
hRSI = iRSI(_Symbol, PERIOD_CURRENT, RSI_Period, PRICE_CLOSE);
hATR = iATR(_Symbol, PERIOD_CURRENT, ATR_Period);
if(hBB == INVALID_HANDLE || hRSI == INVALID_HANDLE || hATR == INVALID_HANDLE)
{
Print("Failed to create indicator handles. EA will not run.");
return INIT_FAILED;
}
ArraySetAsSeries(bbUpper, true);
ArraySetAsSeries(bbMiddle, true);
ArraySetAsSeries(bbLower, true);
ArraySetAsSeries(rsiBuffer, true);
ArraySetAsSeries(atrBuffer, true);
Print("MeanReversionEA initialised on ", _Symbol);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert deinitialization |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(hBB != INVALID_HANDLE) IndicatorRelease(hBB);
if(hRSI != INVALID_HANDLE) IndicatorRelease(hRSI);
if(hATR != INVALID_HANDLE) IndicatorRelease(hATR);
Print("MeanReversionEA removed. Reason: ", reason);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//--- Only act on a new bar
static datetime lastBarTime = 0;
datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
if(currentBarTime == lastBarTime) return;
lastBarTime = currentBarTime;
//--- Copy indicator values (use bar index 1 = last closed bar)
if(CopyBuffer(hBB, UPPER_BAND, 0, 3, bbUpper) < 3) return;
if(CopyBuffer(hBB, BASE_LINE, 0, 3, bbMiddle) < 3) return;
if(CopyBuffer(hBB, LOWER_BAND, 0, 3, bbLower) < 3) return;
if(CopyBuffer(hRSI, 0, 0, 3, rsiBuffer) < 3) return;
if(CopyBuffer(hATR, 0, 0, 3, atrBuffer) < 3) return;
double closePrice = iClose(_Symbol, PERIOD_CURRENT, 1);
double upperBand = bbUpper[1];
double midBand = bbMiddle[1];
double lowerBand = bbLower[1];
double rsi = rsiBuffer[1];
double atr = atrBuffer[1];
//--- Manage open positions (early RSI mid-cross exit)
ManageOpenPositions(midBand, rsi);
//--- Check if we already have an open position
if(PositionSelect(_Symbol) && PositionGetInteger(POSITION_MAGIC) == MagicNumber)
return;
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
//--- Long signal: price closed below lower BB and RSI oversold
if(closePrice <= lowerBand && rsi < RSI_OversoldLevel)
{
double sl = ask - ATR_Multiplier * atr;
double tp = midBand;
double lots = CalculateLotSize(ask - sl);
if(lots > 0.0)
OpenPosition(ORDER_TYPE_BUY, lots, ask, sl, tp);
}
//--- Short signal: price closed above upper BB and RSI overbought
else if(closePrice >= upperBand && rsi > RSI_OverboughtLevel)
{
double sl = bid + ATR_Multiplier * atr;
double tp = midBand;
double lots = CalculateLotSize(sl - bid);
if(lots > 0.0)
OpenPosition(ORDER_TYPE_SELL, lots, bid, sl, tp);
}
}
//+------------------------------------------------------------------+
//| Open a new market order |
//+------------------------------------------------------------------+
void OpenPosition(ENUM_ORDER_TYPE type, double lots, double price,
double sl, double tp)
{
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = lots;
request.type = type;
request.price = price;
request.sl = NormalizeDouble(sl, _Digits);
request.tp = NormalizeDouble(tp, _Digits);
request.deviation = 10;
request.magic = MagicNumber;
request.comment = TradeComment;
request.type_filling = ORDER_FILLING_FOK;
if(!OrderSend(request, result))
Print("OrderSend failed: ", result.retcode, " ", result.comment);
else
Print("Position opened: ", EnumToString(type), " lots=", lots,
" price=", price, " sl=", sl, " tp=", tp);
}
//+------------------------------------------------------------------+
//| Early exit: close if RSI crosses back through 50 |
//+------------------------------------------------------------------+
void ManageOpenPositions(double midBand, double rsi)
{
if(!PositionSelect(_Symbol)) return;
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber) return;
ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
bool closeEarly = false;
if(posType == POSITION_TYPE_BUY && rsi >= 50.0) closeEarly = true;
if(posType == POSITION_TYPE_SELL && rsi <= 50.0) closeEarly = true;
if(closeEarly)
{
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = PositionGetDouble(POSITION_VOLUME);
request.type = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
request.price = (posType == POSITION_TYPE_BUY)
? SymbolInfoDouble(_Symbol, SYMBOL_BID)
: SymbolInfoDouble(_Symbol, SYMBOL_ASK);
request.deviation = 10;
request.magic = MagicNumber;
request.comment = "MeanRev-EarlyExit";
request.type_filling = ORDER_FILLING_FOK;
if(!OrderSend(request, result))
Print("Early exit failed: ", result.retcode, " ", result.comment);
else
Print("Early exit triggered (RSI mid-cross) at rsi=", rsi);
}
}
//+------------------------------------------------------------------+
//| Calculate lot size based on fixed fractional risk |
//+------------------------------------------------------------------+
double CalculateLotSize(double slDistance)
{
if(slDistance <= 0.0) return 0.0;
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double riskAmount = AccountInfoDouble(ACCOUNT_EQUITY) * RiskPercent / 100.0;
double valuePerLot = (slDistance / tickSize) * tickValue;
if(valuePerLot <= 0.0) return 0.0;
double lots = riskAmount / valuePerLot;
lots = MathFloor(lots / lotStep) * lotStep;
lots = MathMax(minLot, MathMin(maxLot, lots));
return lots;
}
//+------------------------------------------------------------------+Copy this code into MetaEditor (F4 in MT5), save in the Experts folder, and compile with F7.
Generate a Custom Multi-pair Mean-reversion EA →
Pineify AI generates syntactically validated MQL5 Expert Advisors from plain English descriptions. Customize entry logic, risk management, and trading sessions — no coding required.
Pine Script vs MQL5: Same Strategy, Different Platforms
| Aspect | Pine Script (TradingView) | MQL5 (MetaTrader 5) |
|---|---|---|
| Execution | Bar-based, backtesting only | Tick-based, live trading |
| Deployment | TradingView alerts | Runs 24/5 on VPS/MT5 |
| Broker access | Via TradingView broker integration | Direct broker connectivity |
| Backtesting | Built-in, no data download needed | Strategy Tester, tick data required |
| Code complexity | Simpler, functional syntax | C++-like, more powerful |
Pineify supports both platforms. Prototype your strategy visually in TradingView Pine Script, then generate the equivalent MQL5 EA for live MT5 trading.