Problem Description
Solution
Here is a complete, fully functional Expert Advisor (trading robot) written in MQL4.
This robot implements a "Breakout" strategy. It calculates the highest high and lowest low of the previous 20 closed bars. It then places a Buy Stop order above the high and a Sell Stop order below the low. It includes logic to cancel the opposite pending order once a trade is triggered (One-Cancels-Other logic).
MQL4 Trading Robot Code
//+------------------------------------------------------------------+
//| 20BarBreakout_EA.mq4 |
//| Copyright 2023, Quantitative|
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, Quantitative"
#property link "https://www.mql5.com"
#property version "1.00"
#property strict
//--- Input Parameters
input int LookbackPeriod = 20; // Number of bars to check for High/Low
input double LotSize = 0.1; // Fixed Lot Size
input int BufferPoints = 10; // Distance from High/Low to place order (in points)
input int StopLoss = 500; // Stop Loss in points (0 = no SL)
input int TakeProfit = 1000; // Take Profit in points (0 = no TP)
input int MagicNumber = 123456; // Unique Identifier for this EA
input int ExpirationBars = 1; // Expiration of pending orders in bars (0 = no expiration)
//--- Global Variables
double pPoint; // Adjusted point value for 3/5 digit brokers
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// Adjust point for 5-digit brokers (0.00001) vs 4-digit (0.0001)
if(Digits == 3 || Digits == 5) pPoint = Point * 10;
else pPoint = Point;
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// Optional: Clean up all pending orders on removal
DeleteAllPendingOrders();
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// 1. Check if we have open positions or pending orders managed by this EA
int buyOrders = 0;
int sellOrders = 0;
int buyStops = 0;
int sellStops = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
if(OrderType() == OP_BUY) buyOrders++;
if(OrderType() == OP_SELL) sellOrders++;
if(OrderType() == OP_BUYSTOP) buyStops++;
if(OrderType() == OP_SELLSTOP) sellStops++;
}
}
}
// 2. OCO Logic (One Cancels Other)
// If a market order is active, delete remaining pending orders
if(buyOrders > 0 || sellOrders > 0)
{
if(buyStops > 0 || sellStops > 0) DeleteAllPendingOrders();
return; // Exit OnTick, do not place new orders while in a trade
}
// 3. If pending orders already exist, do nothing (wait for expiration or trigger)
if(buyStops > 0 || sellStops > 0) return;
// 4. Calculate High and Low of the last 'LookbackPeriod' bars
// We start at index 1 to look at closed bars, ignoring the currently forming bar (index 0)
int highestIndex = iHighest(NULL, 0, MODE_HIGH, LookbackPeriod, 1);
int lowestIndex = iLowest(NULL, 0, MODE_LOW, LookbackPeriod, 1);
double highPrice = iHigh(NULL, 0, highestIndex);
double lowPrice = iLow(NULL, 0, lowestIndex);
// 5. Calculate Entry Prices
double buyStopPrice = NormalizeDouble(highPrice + BufferPoints * Point, Digits);
double sellStopPrice = NormalizeDouble(lowPrice - BufferPoints * Point, Digits);
// 6. Calculate Expiration Time
datetime expirationTime = 0;
if(ExpirationBars > 0)
{
expirationTime = TimeCurrent() + (PeriodSeconds() * ExpirationBars);
}
// 7. Check Minimum Stop Level (Distance from current price)
double stopLevel = MarketInfo(Symbol(), MODE_STOPLEVEL) * Point;
// Ensure Buy Stop is above Ask + Stoplevel
if(buyStopPrice > Ask + stopLevel)
{
double sl = (StopLoss > 0) ? buyStopPrice - StopLoss * Point : 0;
double tp = (TakeProfit > 0) ? buyStopPrice + TakeProfit * Point : 0;
// Normalize SL/TP
if(sl > 0) sl = NormalizeDouble(sl, Digits);
if(tp > 0) tp = NormalizeDouble(tp, Digits);
int ticket = OrderSend(Symbol(), OP_BUYSTOP, LotSize, buyStopPrice, 3, sl, tp, "Buy Breakout", MagicNumber, expirationTime, clrGreen);
if(ticket < 0) Print("Error sending Buy Stop: ", GetLastError());
}
// Ensure Sell Stop is below Bid - Stoplevel
if(sellStopPrice < Bid - stopLevel)
{
double sl = (StopLoss > 0) ? sellStopPrice + StopLoss * Point : 0;
double tp = (TakeProfit > 0) ? sellStopPrice - TakeProfit * Point : 0;
// Normalize SL/TP
if(sl > 0) sl = NormalizeDouble(sl, Digits);
if(tp > 0) tp = NormalizeDouble(tp, Digits);
int ticket = OrderSend(Symbol(), OP_SELLSTOP, LotSize, sellStopPrice, 3, sl, tp, "Sell Breakout", MagicNumber, expirationTime, clrRed);
if(ticket < 0) Print("Error sending Sell Stop: ", GetLastError());
}
}
//+------------------------------------------------------------------+
//| Helper: Delete all pending orders for this EA |
//+------------------------------------------------------------------+
void DeleteAllPendingOrders()
{
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
if(OrderType() == OP_BUYSTOP || OrderType() == OP_SELLSTOP)
{
bool res = OrderDelete(OrderTicket());
if(!res) Print("Failed to delete order: ", GetLastError());
}
}
}
}
}
//+------------------------------------------------------------------+
Strategy Logic and Features
- High/Low Calculation:
- The robot uses
iHighestandiLoweststarting from index1. - Index 1 represents the last closed bar. We do not use index 0 because the current bar is still forming, and its high/low can change every tick, which would cause the pending orders to jump around constantly.
- The robot uses
- Buffer:
- It adds a
BufferPointsvalue to the High and subtracts it from the Low. This ensures you don't buy exactly at the high, but slightly above it (filtering out false breakouts).
- It adds a
- One-Cancels-Other (OCO):
- The code checks if a market position (Buy or Sell) is active. If a trade has been triggered, it automatically deletes the remaining pending order to prevent being hedged unintentionally.
- Expiration:
- The
ExpirationBarsinput allows the pending orders to expire automatically if price does not break out within a certain time (default is 1 bar). This ensures the orders follow the moving 20-bar window dynamically.
- The
- Risk Management:
- Includes inputs for
StopLossandTakeProfitin points. - Includes a
MagicNumberto ensure the EA only manages its own trades and doesn't interfere with other manual trades or robots.
- Includes inputs for
How to Install and Run
- Open MetaTrader 4.
- Press F4 to open the MetaEditor.
- In MetaEditor, click New -> Expert Advisor (template) -> Next.
- Name it
20BarBreakoutand click Finish. - Delete everything in the new file and paste the code provided above.
- Press F7 or click Compile. Ensure there are no errors in the "Errors" tab at the bottom.
- Return to the main MetaTrader 4 window.
- Find
20BarBreakoutin the Navigator panel (under Expert Advisors). - Drag it onto a chart (e.g., EURUSD H1).
- In the settings window, adjust
LotSize,StopLoss, orLookbackPeriodif desired. - Ensure the AutoTrading button at the top of MT4 is green (Enabled).
Q&A: Common Issues
Q: Why are my orders not being placed?
A: Check the "Journal" tab in the Terminal window. Common errors include:
- Error 130 (Invalid Stops): Your Stop Loss or Take Profit is too close to the entry price. Increase the SL/TP values.
- Error 134 (Not Enough Money): You do not have enough free margin for the
LotSizeselected. - Stop Level: If the market is very volatile, the
BufferPointsmight place the order too close to the current market price (inside the broker's freeze level).
Q: Does this work on 5-digit brokers?
A: Yes. The code includes a pPoint calculation in OnInit (though standard Point is used for the OrderSend math for simplicity, the inputs are treated as raw points). If you input StopLoss = 500, on a 5-digit broker, this is 50.0 pips.
Q: Why do the pending orders disappear after one bar?
A: This is due to the ExpirationBars setting (default 1). This is intentional. As the 20-bar window moves forward, the High/Low changes. The old order expires, and a new one is placed at the new High/Low. If you want them to stay forever, set ExpirationBars = 0.