MQL5 Breakout EA: Opening Range Breakout Code & Backtest Guide
This educational Opening Range Breakout EA reconstructs a configurable broker-time window from completed M1 bars, then checks live bid prices for a breakout. It includes an ATR minimum-range condition, approximate balance-based sizing, initial exits, and a range-distance trailing stop. The page supplies source for review and testing, not a documented 2021–2025 performance record. Session mapping, costs, contract specifications, and restart behavior must be tested separately.
Strategy logic
Entry conditions
After the session window ends, CopyRates requests its M1 bars. The example requires one bar for every configured minute; incomplete history or a market with missing-minute bars produces no entry until the range is available. It compares the high-low width with a fraction of the last completed M1 ATR. After acceptance, a live bid above the range high triggers a buy request; a bid below the low triggers a sell request. There is no candle-close or separate momentum confirmation. Only one successful entry is allowed per in-memory server-day state, and no new entry is sent while a matching position exists. A terminal restart resets that daily memory and can permit another trade after an earlier one has closed.
Exit conditions
The initial stop is the opposite range edge, while the target distance is a multiple of range width from entry. Because entry can occur beyond the edge, stop distance is not necessarily one range and the target multiplier is not the realized reward-to-risk ratio. Trailing activates after favorable movement reaches the configured multiple of range width and then follows price by one range. For an existing position, the example recovers the reference width from its fixed target distance and the target multiplier; keep that input and target unchanged while managing it. Price gaps, costs, failed modifications, and partial execution can prevent a break-even outcome. There is no daily close, spread limit, or portfolio loss cap.
MQL5 Expert Advisor Code
//+------------------------------------------------------------------+
//| ORB_Breakout_EA.mq5 |
//| Opening Range Breakout Expert Advisor — MQL5 5.x |
//| Educational example; no documented historical performance. |
//| 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;
datetime g_sessionOpen = 0;
datetime g_serverDay = 0;
//+------------------------------------------------------------------+
//| Expert initialization |
//+------------------------------------------------------------------+
int OnInit()
{
if(InpRangeMinutes <= 0 || InpSessionHour < 0 || InpSessionHour > 23 ||
InpSessionMinute < 0 || InpSessionMinute > 59 ||
InpSessionHour * 60 + InpSessionMinute + InpRangeMinutes >= 1440 ||
InpRiskPercent <= 0 || InpRiskPercent > 100 || InpRRRatio <= 0 ||
InpTrailActivate <= 0 || InpATRPeriod <= 0 || InpMinRangeATR < 0 || InpMagicNumber < 0)
return INIT_PARAMETERS_INCORRECT;
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);
dt.hour = 0; dt.min = 0; dt.sec = 0;
if(StructToTime(dt) != g_serverDay) ResetDailyState();
// Existing positions are managed even before today's range is available.
ManageTrailingStop();
if(g_tradedToday) return;
if(!g_rangeReady) BuildOpeningRange();
if(g_rangeReady && !g_tradedToday) CheckBreakoutEntry();
}
//+------------------------------------------------------------------+
//| 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 = 0; dt.min = 0; dt.sec = 0;
g_serverDay = StructToTime(dt);
dt.hour = InpSessionHour;
dt.min = InpSessionMinute;
dt.sec = 0;
g_sessionOpen = StructToTime(dt);
g_rangeEndTime = g_sessionOpen + InpRangeMinutes * 60;
}
//+------------------------------------------------------------------+
//| Build the opening range high/low |
//+------------------------------------------------------------------+
void BuildOpeningRange()
{
if(TimeCurrent() < g_rangeEndTime) return;
MqlRates bars[];
int copied = CopyRates(_Symbol, PERIOD_M1, g_sessionOpen, g_rangeEndTime - 1, bars);
if(copied != InpRangeMinutes) return;
for(int i = 0; i < copied; i++)
if(bars[i].time != g_sessionOpen + i * 60) return;
double atr[1];
if(CopyBuffer(g_atrHandle, 0, 1, 1, atr) != 1 || atr[0] <= 0 || atr[0] == EMPTY_VALUE) return;
g_rangeHigh = bars[0].high;
g_rangeLow = bars[0].low;
for(int i = 1; i < copied; i++)
{
g_rangeHigh = MathMax(g_rangeHigh, bars[i].high);
g_rangeLow = MathMin(g_rangeLow, bars[i].low);
}
double width = g_rangeHigh - g_rangeLow;
g_rangeReady = true;
if(width <= 0 || width < InpMinRangeATR * atr[0])
{
g_tradedToday = true;
Print("Range rejected for this server day");
}
}
//+------------------------------------------------------------------+
//| Check for breakout entry and place order |
//+------------------------------------------------------------------+
void CheckBreakoutEntry()
{
if(!g_rangeReady) return;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetTicket(i) == 0) continue;
if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
PositionGetInteger(POSITION_MAGIC) == InpMagicNumber) return;
}
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double rangeSize = g_rangeHigh - g_rangeLow;
bool longSignal = (bid > 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) ||
(res.retcode != TRADE_RETCODE_DONE && res.retcode != TRADE_RETCODE_DONE_PARTIAL))
{
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()
{
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);
double target = PositionGetDouble(POSITION_TP);
double rangeSize = MathAbs(target - openPrice) / InpRRRatio;
if(target <= 0 || rangeSize <= 0) continue;
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) ||
(res.retcode != TRADE_RETCODE_DONE && res.retcode != TRADE_RETCODE_DONE_PARTIAL))
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 || lotStep <= 0 || balance <= 0) return 0;
double riskPerLot = (stopDistance / tickSize) * tickValue;
if(riskPerLot <= 0) return 0;
double lots = MathFloor(MathMin(maxLot, riskAmount / riskPerLot) / lotStep) * lotStep;
if(lots < minLot) return 0;
return NormalizeDouble(lots, 8);
}
//+------------------------------------------------------------------+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 entry, exit, position, order, and risk rules 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 | Strategy orders are simulated; live automation needs a separate 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.
Frequently Asked Questions
Related MQL5 pages
Risk and testing note
These educational examples require independent compilation and testing. A compile pass or an illustrative setting does not establish execution correctness or profitability. Any reported performance must be supported by reproducible data and test settings; past results do not predict future returns. Trading involves significant risk of loss. This content is not financial advice.