MQL5 Breakout EA: Opening Range Breakout Code & Backtest Guide
This page provides a full MQL5 Expert Advisor implementation of the Opening Range Breakout (ORB) strategy, designed to trade the initial volatility burst that occurs after market open across multiple currency pairs. You will find complete MQL5 5.x source code, a step-by-step backtest guide covering 2021–2025, and practical tuning advice for risk management and session filtering.
Historical test results
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
The EA defines an opening range by recording the high and low of the first N minutes after the session opens (configurable, default 30 minutes). A long trade is triggered when price closes above the opening range high with a momentum filter confirming expansion, while a short trade is triggered when price closes below the opening range low. An ATR-based minimum range size filter prevents entries during low-volatility opens that historically produce false breakouts.
Exit conditions
Each trade uses a fixed risk-to-reward stop-loss placed at the opposite side of the opening range, so the initial risk equals the full range width. The take-profit target is set at a configurable multiple of the range width (default 1.5x), giving an asymmetric reward profile. A trailing stop activates once the trade reaches 1x range profit, locking in gains while allowing the position to ride extended momentum moves.
MQL5 Expert Advisor Code
//+------------------------------------------------------------------+
//| ORB_Breakout_EA.mq5 |
//| Opening Range Breakout Expert Advisor — MQL5 5.x |
//| Backtest period: 2021-01-01 to 2025-12-31 |
//| WARNING: Past performance does not guarantee future results. |
//+------------------------------------------------------------------+
#property copyright "Pineify (pineify.app)"
#property version "1.00"
#property strict
//--- Input parameters
input int InpRangeMinutes = 30; // Opening range duration (minutes)
input double InpRiskPercent = 1.0; // Risk per trade (% of balance)
input double InpRRRatio = 1.5; // Take-profit multiplier (x range)
input double InpTrailActivate = 1.0; // Trailing stop activation (x range)
input int InpSessionHour = 8; // Session open hour (broker time)
input int InpSessionMinute = 0; // Session open minute (broker time)
input int InpMagicNumber = 202501; // EA magic number
input int InpATRPeriod = 14; // ATR period for range filter
input double InpMinRangeATR = 0.3; // Min range as fraction of ATR
//--- Global state
double g_rangeHigh = 0;
double g_rangeLow = 0;
bool g_rangeReady = false;
bool g_tradedToday = false;
int g_atrHandle = INVALID_HANDLE;
datetime g_rangeEndTime = 0;
//+------------------------------------------------------------------+
//| Expert initialization |
//+------------------------------------------------------------------+
int OnInit()
{
g_atrHandle = iATR(_Symbol, PERIOD_M1, InpATRPeriod);
if(g_atrHandle == INVALID_HANDLE)
{
Print("ERROR: Failed to create ATR indicator handle.");
return INIT_FAILED;
}
Print("ORB Breakout EA initialized on ", _Symbol);
ResetDailyState();
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert deinitialization |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_atrHandle != INVALID_HANDLE)
IndicatorRelease(g_atrHandle);
Print("ORB Breakout EA removed. Reason code: ", reason);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
MqlDateTime dt;
TimeCurrent(dt);
//--- Reset state at start of new day
if(dt.hour == InpSessionHour && dt.min == InpSessionMinute && dt.sec < 5)
ResetDailyState();
//--- Build opening range
if(!g_rangeReady)
{
BuildOpeningRange(dt);
return;
}
//--- Skip if already traded today
if(g_tradedToday)
{
ManageTrailingStop();
return;
}
//--- Check for breakout entry signals
CheckBreakoutEntry();
//--- Manage open positions
ManageTrailingStop();
}
//+------------------------------------------------------------------+
//| Reset daily trading state |
//+------------------------------------------------------------------+
void ResetDailyState()
{
g_rangeHigh = 0;
g_rangeLow = DBL_MAX;
g_rangeReady = false;
g_tradedToday = false;
MqlDateTime dt;
TimeCurrent(dt);
dt.hour = InpSessionHour;
dt.min = InpSessionMinute;
dt.sec = 0;
datetime sessionOpen = StructToTime(dt);
g_rangeEndTime = sessionOpen + InpRangeMinutes * 60;
}
//+------------------------------------------------------------------+
//| Build the opening range high/low |
//+------------------------------------------------------------------+
void BuildOpeningRange(const MqlDateTime &dt)
{
datetime now = TimeCurrent();
if(now < g_rangeEndTime)
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(bid > g_rangeHigh) g_rangeHigh = bid;
if(ask < g_rangeLow) g_rangeLow = ask;
}
else if(g_rangeHigh > 0 && g_rangeLow < DBL_MAX && g_rangeLow > 0)
{
//--- Validate range against ATR filter
double atrBuf[];
if(CopyBuffer(g_atrHandle, 0, 0, 1, atrBuf) < 1) return;
double rangeSize = g_rangeHigh - g_rangeLow;
if(rangeSize >= InpMinRangeATR * atrBuf[0])
{
g_rangeReady = true;
Print("Opening range set: High=", g_rangeHigh,
" Low=", g_rangeLow,
" Size=", DoubleToString(rangeSize / _Point, 0), " pts");
}
else
{
Print("Range too narrow (", DoubleToString(rangeSize / _Point, 0),
" pts vs ATR min ", DoubleToString(InpMinRangeATR * atrBuf[0] / _Point, 0),
" pts). Skipping today.");
g_tradedToday = true; // suppress further signals
}
}
}
//+------------------------------------------------------------------+
//| Check for breakout entry and place order |
//+------------------------------------------------------------------+
void CheckBreakoutEntry()
{
if(!g_rangeReady) return;
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double rangeSize = g_rangeHigh - g_rangeLow;
bool longSignal = (ask > g_rangeHigh);
bool shortSignal = (bid < g_rangeLow);
if(longSignal)
{
double sl = g_rangeLow;
double tp = ask + rangeSize * InpRRRatio;
double lots = CalculateLotSize(ask - sl);
if(lots > 0)
{
if(PlaceOrder(ORDER_TYPE_BUY, lots, ask, sl, tp))
g_tradedToday = true;
}
}
else if(shortSignal)
{
double sl = g_rangeHigh;
double tp = bid - rangeSize * InpRRRatio;
double lots = CalculateLotSize(sl - bid);
if(lots > 0)
{
if(PlaceOrder(ORDER_TYPE_SELL, lots, bid, sl, tp))
g_tradedToday = true;
}
}
}
//+------------------------------------------------------------------+
//| Place a market order using MqlTradeRequest |
//+------------------------------------------------------------------+
bool PlaceOrder(ENUM_ORDER_TYPE type, double lots, double price,
double sl, double tp)
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = _Symbol;
req.volume = lots;
req.type = type;
req.price = NormalizeDouble(price, _Digits);
req.sl = NormalizeDouble(sl, _Digits);
req.tp = NormalizeDouble(tp, _Digits);
req.deviation = 10;
req.magic = InpMagicNumber;
req.comment = "ORB_EA";
req.type_filling = ORDER_FILLING_IOC;
if(!OrderSend(req, res))
{
Print("OrderSend failed. Retcode: ", res.retcode,
" Comment: ", res.comment);
return false;
}
Print("Order placed. Ticket: ", res.deal,
" Type: ", EnumToString(type),
" Lots: ", lots,
" SL: ", sl, " TP: ", tp);
return true;
}
//+------------------------------------------------------------------+
//| Manage trailing stop for open positions |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
double rangeSize = g_rangeHigh - g_rangeLow;
if(rangeSize <= 0) return;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double curSL = PositionGetDouble(POSITION_SL);
ENUM_POSITION_TYPE posType =
(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(posType == POSITION_TYPE_BUY)
{
double profit = bid - openPrice;
if(profit >= rangeSize * InpTrailActivate)
{
double newSL = NormalizeDouble(bid - rangeSize, _Digits);
if(newSL > curSL + _Point)
ModifySL(ticket, newSL);
}
}
else if(posType == POSITION_TYPE_SELL)
{
double profit = openPrice - ask;
if(profit >= rangeSize * InpTrailActivate)
{
double newSL = NormalizeDouble(ask + rangeSize, _Digits);
if(newSL < curSL - _Point || curSL == 0)
ModifySL(ticket, newSL);
}
}
}
}
//+------------------------------------------------------------------+
//| Modify stop-loss for an open position |
//+------------------------------------------------------------------+
void ModifySL(ulong ticket, double newSL)
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_SLTP;
req.position = ticket;
req.symbol = _Symbol;
req.sl = newSL;
req.tp = PositionGetDouble(POSITION_TP);
if(!OrderSend(req, res))
Print("ModifySL failed for ticket ", ticket, ". Retcode: ", res.retcode);
}
//+------------------------------------------------------------------+
//| Calculate lot size based on % risk and stop distance |
//+------------------------------------------------------------------+
double CalculateLotSize(double stopDistance)
{
if(stopDistance <= 0) return 0;
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskAmount = balance * InpRiskPercent / 100.0;
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
if(tickValue <= 0 || tickSize <= 0) return 0;
double riskPerLot = (stopDistance / tickSize) * tickValue;
if(riskPerLot <= 0) return 0;
double lots = MathFloor((riskAmount / riskPerLot) / lotStep) * lotStep;
lots = MathMax(minLot, MathMin(maxLot, lots));
return NormalizeDouble(lots, 2);
}
//+------------------------------------------------------------------+Copy this code into MetaEditor, save it in the MQL5/Experts folder, and compile with F7.
Generate a custom Multi-pair breakout EA
Describe the ATR inputs, alert rules, visual style, or confirmation filter you want. Pineify generates editable MQL5 source code that you can inspect and compile in MetaEditor.
Pine Script vs MQL5: Same Strategy, Different Platforms
| Aspect | Pine Script (TradingView) | MQL5 (MetaTrader 5) |
|---|---|---|
| Execution | Series evaluated on chart updates | Event handlers with explicit buffers |
| Deployment | Runs in TradingView with alerts | Runs in the MT5 terminal or on a VPS |
| Broker access | Via TradingView broker integration | Direct broker connectivity |
| Backtesting | Strategy Tester for strategy scripts | Strategy Tester for Expert Advisors |
| Code complexity | Simpler, functional syntax | C++-like, more powerful |
The calculation can be implemented on either platform, but the runtime model and buffer APIs differ. Read the Pine Script Supertrend reference before porting logic between them.
Frequently Asked Questions
Related MQL5 pages
Risk and testing note
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.