515 lines
No EOL
18 KiB
MQL5
515 lines
No EOL
18 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| DR_BreakoutStraddle.mq5 |
|
|
//| Copyright 2026, Jim Gray |
|
|
//| /www.mql5.com/en/users/digitalrogue |
|
|
//| |
|
|
//|5-Minute Breakout Straddle EA |
|
|
//|Places Buy Stop + Sell Stop with OCO logic |
|
|
//|Configurable distance, TP/SL, time filter, spread filter, trail |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Copyright 2026, Jim Gray"
|
|
#property link "https://www.mql5.com/en/users/digitalrogue"
|
|
#property version "1.10"
|
|
#property strict
|
|
|
|
#include <Trade\Trade.mqh>
|
|
|
|
CTrade trade;
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Input parameters |
|
|
//+------------------------------------------------------------------+
|
|
input group "=== Orders ==="
|
|
input int StopDistancePips = 10; // Distance from price in pips
|
|
input int TakeProfitPips = 20; // Take profit in pips
|
|
input int StopLossPips = 10; // Stop loss in pips
|
|
input int OrderExpireMinutes = 60; // Order expiration in minutes (0=none)
|
|
input double MaxSpreadPips = 5.0; // Maximum spread to place orders
|
|
|
|
input group "=== Time Filter ==="
|
|
input bool UseTimeFilter = true; // Enable time filter
|
|
input int StartHour = 7; // Trading start hour (broker time)
|
|
input int StartMinute = 0; // Trading start minute
|
|
input int EndHour = 20; // Trading end hour (broker time)
|
|
input int EndMinute = 0; // Trading end minute
|
|
|
|
input group "=== Trade ==="
|
|
input double RiskPercent = 1.0; // Risk percent per trade
|
|
input int MagicNumber = 20260416; // Magic number
|
|
input int SlippagePoints = 10; // Max slippage
|
|
|
|
input group "=== OCO ==="
|
|
input bool EnableOCO = true; // Cancel opposite order on trigger
|
|
|
|
input group "=== Trailing Stop ==="
|
|
input bool UseTrailingStop = true; // Enable trailing stop
|
|
input int TrailPips = 10; // Trail distance in pips
|
|
input int TrailActivatePips = 10; // Activate trail after X pips profit
|
|
|
|
input group "=== Display ==="
|
|
input bool DrawOnChart = true; // Draw entry lines and HUD
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Global variables |
|
|
//+------------------------------------------------------------------+
|
|
datetime g_lastBarTime = 0;
|
|
ulong g_buyStopTicket = 0;
|
|
ulong g_sellStopTicket = 0;
|
|
bool g_ordersPlaced = false;
|
|
bool g_tradeActive = false;
|
|
double g_trailSL = 0;
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert initialization |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
trade.SetExpertMagicNumber(MagicNumber);
|
|
trade.SetDeviationInPoints(SlippagePoints);
|
|
|
|
Comment("DR Breakout Straddle v1.10\nWaiting for next bar...");
|
|
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert deinitialization |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
Comment("");
|
|
ObjectDelete(0, "DR_BuyStopLine");
|
|
ObjectDelete(0, "DR_SellStopLine");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert tick function |
|
|
//+------------------------------------------------------------------+
|
|
void OnTick()
|
|
{
|
|
// Trailing stop runs on every tick, not just new bars
|
|
if(UseTrailingStop && HasOpenPosition())
|
|
TrailStop();
|
|
|
|
// Check for new bar on M5
|
|
datetime currentBarTime = iTime(_Symbol, PERIOD_M5, 0);
|
|
if(currentBarTime == g_lastBarTime)
|
|
return;
|
|
g_lastBarTime = currentBarTime;
|
|
|
|
// Check if we have an open position for this EA
|
|
g_tradeActive = HasOpenPosition();
|
|
|
|
// If trade is active, check OCO
|
|
if(g_tradeActive && EnableOCO)
|
|
{
|
|
CancelPendingOrders();
|
|
g_ordersPlaced = false;
|
|
}
|
|
|
|
// If trade is active, just update HUD
|
|
if(g_tradeActive)
|
|
{
|
|
UpdateHUD("Trade active (trailing)");
|
|
return;
|
|
}
|
|
|
|
// Trade closed, reset trail
|
|
g_trailSL = 0;
|
|
|
|
// Check time filter
|
|
if(UseTimeFilter && !IsInTradingHours())
|
|
{
|
|
CancelPendingOrders();
|
|
g_ordersPlaced = false;
|
|
UpdateHUD("Outside trading hours");
|
|
return;
|
|
}
|
|
|
|
// Check spread
|
|
double currentSpread = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID)) / _Point;
|
|
double maxSpreadPoints = MaxSpreadPips * 10;
|
|
if(currentSpread > maxSpreadPoints)
|
|
{
|
|
UpdateHUD("Spread too wide: " + DoubleToString(currentSpread / 10, 1) + " pips");
|
|
return;
|
|
}
|
|
|
|
// Check if pending orders expired
|
|
if(g_ordersPlaced)
|
|
{
|
|
if(CheckOrderExpired())
|
|
{
|
|
CancelPendingOrders();
|
|
g_ordersPlaced = false;
|
|
UpdateHUD("Orders expired, waiting for next bar");
|
|
return;
|
|
}
|
|
// Orders still active, update HUD
|
|
UpdateHUD("Orders placed, waiting for trigger");
|
|
return;
|
|
}
|
|
|
|
// Place new orders
|
|
PlaceOrders();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Trade transaction handler |
|
|
//+------------------------------------------------------------------+
|
|
void OnTradeTransaction(const MqlTradeTransaction& trans,
|
|
const MqlTradeRequest& request,
|
|
const MqlTradeResult& result)
|
|
{
|
|
if(trans.type == TRADE_TRANSACTION_ORDER_DELETE)
|
|
{
|
|
// Order deleted - check if it's one of ours
|
|
if(trans.order == g_buyStopTicket || trans.order == g_sellStopTicket)
|
|
{
|
|
// One of our pending orders was removed
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Place Buy Stop and Sell Stop orders |
|
|
//+------------------------------------------------------------------+
|
|
void PlaceOrders()
|
|
{
|
|
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
double point = _Point;
|
|
double pipSize = point * 10; // 5-digit broker
|
|
|
|
double buyStopPrice = NormalizeDouble(ask + StopDistancePips * pipSize, _Digits);
|
|
double sellStopPrice = NormalizeDouble(bid - StopDistancePips * pipSize, _Digits);
|
|
double tp = TakeProfitPips * pipSize;
|
|
double sl = StopLossPips * pipSize;
|
|
|
|
double buyStopSL = NormalizeDouble(buyStopPrice - sl, _Digits);
|
|
double buyStopTP = NormalizeDouble(buyStopPrice + tp, _Digits);
|
|
double sellStopSL = NormalizeDouble(sellStopPrice + sl, _Digits);
|
|
double sellStopTP = NormalizeDouble(sellStopPrice - tp, _Digits);
|
|
|
|
// Calculate lot size based on risk
|
|
double lots = CalculateLotSize(sl);
|
|
if(lots <= 0)
|
|
{
|
|
UpdateHUD("Error: Cannot calculate lot size");
|
|
return;
|
|
}
|
|
|
|
// Set expiration
|
|
datetime expiration = 0;
|
|
if(OrderExpireMinutes > 0)
|
|
expiration = TimeCurrent() + OrderExpireMinutes * 60;
|
|
|
|
// Place Buy Stop
|
|
if(!trade.BuyStop(lots, buyStopPrice, _Symbol, buyStopSL, buyStopTP, ORDER_TIME_GTC, 0, "DR_Breakout_BuyStop"))
|
|
{
|
|
Print("Buy Stop failed: ", GetLastError());
|
|
UpdateHUD("Buy Stop failed: " + IntegerToString(GetLastError()));
|
|
return;
|
|
}
|
|
g_buyStopTicket = trade.ResultOrder();
|
|
|
|
// Place Sell Stop
|
|
if(!trade.SellStop(lots, sellStopPrice, _Symbol, sellStopSL, sellStopTP, ORDER_TIME_GTC, 0, "DR_Breakout_SellStop"))
|
|
{
|
|
Print("Sell Stop failed: ", GetLastError());
|
|
// Cancel the buy stop we just placed
|
|
trade.OrderDelete(g_buyStopTicket);
|
|
g_buyStopTicket = 0;
|
|
UpdateHUD("Sell Stop failed: " + IntegerToString(GetLastError()));
|
|
return;
|
|
}
|
|
g_sellStopTicket = trade.ResultOrder();
|
|
|
|
g_ordersPlaced = true;
|
|
|
|
// Draw lines
|
|
if(DrawOnChart)
|
|
{
|
|
DrawLine("DR_BuyStopLine", buyStopPrice, clrLime);
|
|
DrawLine("DR_SellStopLine", sellStopPrice, clrRed);
|
|
}
|
|
|
|
Print("Orders placed: BuyStop ", g_buyStopTicket, " @ ", buyStopPrice,
|
|
" SellStop ", g_sellStopTicket, " @ ", sellStopPrice);
|
|
|
|
UpdateHUD("Orders placed, waiting for trigger");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Cancel all pending orders for this EA |
|
|
//+------------------------------------------------------------------+
|
|
void CancelPendingOrders()
|
|
{
|
|
// Cancel our specific orders by ticket
|
|
if(g_buyStopTicket != 0)
|
|
{
|
|
if(OrderSelect(g_buyStopTicket))
|
|
{
|
|
if(OrderGetString(ORDER_COMMENT) == "DR_Breakout_BuyStop" ||
|
|
OrderGetInteger(ORDER_MAGIC) == MagicNumber)
|
|
trade.OrderDelete(g_buyStopTicket);
|
|
}
|
|
g_buyStopTicket = 0;
|
|
}
|
|
|
|
if(g_sellStopTicket != 0)
|
|
{
|
|
if(OrderSelect(g_sellStopTicket))
|
|
{
|
|
if(OrderGetString(ORDER_COMMENT) == "DR_Breakout_SellStop" ||
|
|
OrderGetInteger(ORDER_MAGIC) == MagicNumber)
|
|
trade.OrderDelete(g_sellStopTicket);
|
|
}
|
|
g_sellStopTicket = 0;
|
|
}
|
|
|
|
// Also cancel any pending orders by magic number (safety net)
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = OrderGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
|
|
if(OrderGetInteger(ORDER_MAGIC) != MagicNumber) continue;
|
|
long orderType = OrderGetInteger(ORDER_TYPE);
|
|
if(orderType == ORDER_TYPE_BUY_STOP || orderType == ORDER_TYPE_SELL_STOP ||
|
|
orderType == ORDER_TYPE_BUY_LIMIT || orderType == ORDER_TYPE_SELL_LIMIT)
|
|
{
|
|
trade.OrderDelete(ticket);
|
|
}
|
|
}
|
|
|
|
ObjectDelete(0, "DR_BuyStopLine");
|
|
ObjectDelete(0, "DR_SellStopLine");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if we have an open position for this EA |
|
|
//+------------------------------------------------------------------+
|
|
bool HasOpenPosition()
|
|
{
|
|
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) != MagicNumber) continue;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if pending orders have expired |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckOrderExpired()
|
|
{
|
|
// If no expiration set, never expire
|
|
if(OrderExpireMinutes <= 0)
|
|
return false;
|
|
|
|
// Check if our orders still exist
|
|
bool buyStopExists = false;
|
|
bool sellStopExists = false;
|
|
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = OrderGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
|
|
if(OrderGetInteger(ORDER_MAGIC) != MagicNumber) continue;
|
|
|
|
long orderType = OrderGetInteger(ORDER_TYPE);
|
|
if(orderType == ORDER_TYPE_BUY_STOP) buyStopExists = true;
|
|
if(orderType == ORDER_TYPE_SELL_STOP) sellStopExists = true;
|
|
}
|
|
|
|
// If neither order exists, they either triggered or were deleted
|
|
if(!buyStopExists && !sellStopExists)
|
|
{
|
|
g_buyStopTicket = 0;
|
|
g_sellStopTicket = 0;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if current time is within trading hours |
|
|
//+------------------------------------------------------------------+
|
|
bool IsInTradingHours()
|
|
{
|
|
MqlDateTime dt;
|
|
TimeCurrent(dt);
|
|
int currentTime = dt.hour * 60 + dt.min;
|
|
int startTime = StartHour * 60 + StartMinute;
|
|
int endTime = EndHour * 60 + EndMinute;
|
|
|
|
if(startTime < endTime)
|
|
return (currentTime >= startTime && currentTime < endTime);
|
|
else // wraps around midnight
|
|
return (currentTime >= startTime || currentTime < endTime);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate lot size based on risk percentage |
|
|
//+------------------------------------------------------------------+
|
|
double CalculateLotSize(double slDistance)
|
|
{
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
double riskAmount = balance * RiskPercent / 100.0;
|
|
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
|
|
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
|
|
if(tickValue == 0 || tickSize == 0 || slDistance == 0)
|
|
return 0;
|
|
|
|
double ticksInSL = slDistance / tickSize;
|
|
double lossPerLot = ticksInSL * tickValue;
|
|
|
|
if(lossPerLot == 0)
|
|
return 0;
|
|
|
|
double lots = riskAmount / lossPerLot;
|
|
|
|
// Normalize to lot step
|
|
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
|
|
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
|
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
|
|
|
|
lots = MathFloor(lots / lotStep) * lotStep;
|
|
lots = MathMax(lots, minLot);
|
|
lots = MathMin(lots, maxLot);
|
|
|
|
return NormalizeDouble(lots, 2);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Trail stop loss behind profitable positions |
|
|
//+------------------------------------------------------------------+
|
|
void TrailStop()
|
|
{
|
|
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) != MagicNumber) continue;
|
|
|
|
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
double currentSL = PositionGetDouble(POSITION_SL);
|
|
double currentTP = PositionGetDouble(POSITION_TP);
|
|
long posType = PositionGetInteger(POSITION_TYPE);
|
|
double pipSize = _Point * 10;
|
|
double trailDist = TrailPips * pipSize;
|
|
double activateDist = TrailActivatePips * pipSize;
|
|
|
|
if(posType == POSITION_TYPE_BUY)
|
|
{
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
double profit = bid - openPrice;
|
|
|
|
// Only trail after position is in profit by activation distance
|
|
if(profit < activateDist) continue;
|
|
|
|
double newSL = NormalizeDouble(bid - trailDist, _Digits);
|
|
|
|
// Only move SL up, never down
|
|
if(newSL > currentSL || currentSL == 0)
|
|
{
|
|
trade.PositionModify(ticket, newSL, currentTP);
|
|
}
|
|
}
|
|
else if(posType == POSITION_TYPE_SELL)
|
|
{
|
|
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
double profit = openPrice - ask;
|
|
|
|
// Only trail after position is in profit by activation distance
|
|
if(profit < activateDist) continue;
|
|
|
|
double newSL = NormalizeDouble(ask + trailDist, _Digits);
|
|
|
|
// Only move SL down, never up (for sells)
|
|
if(newSL < currentSL || currentSL == 0)
|
|
{
|
|
trade.PositionModify(ticket, newSL, currentTP);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Draw a horizontal line on the chart |
|
|
//+------------------------------------------------------------------+
|
|
void DrawLine(string name, double price, color clr)
|
|
{
|
|
ObjectDelete(0, name);
|
|
ObjectCreate(0, name, OBJ_HLINE, 0, 0, price);
|
|
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
|
|
ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DASH);
|
|
ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
|
|
ObjectSetInteger(0, name, OBJPROP_BACK, true);
|
|
ObjectSetString(0, name, OBJPROP_TEXT, name);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update HUD on chart |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateHUD(string status)
|
|
{
|
|
if(!DrawOnChart) return;
|
|
|
|
string hud = "";
|
|
hud += "══════ DR Breakout Straddle v1.10 ══════\n";
|
|
hud += "Status: " + status + "\n";
|
|
hud += "Stop Distance: " + IntegerToString(StopDistancePips) + " pips\n";
|
|
hud += "TP: " + IntegerToString(TakeProfitPips) + " | SL: " + IntegerToString(StopLossPips) + " pips\n";
|
|
hud += "Trail: " + (UseTrailingStop ? IntegerToString(TrailPips) + "pips after " + IntegerToString(TrailActivatePips) + "pips" : "OFF") + "\n";
|
|
hud += "Risk: " + DoubleToString(RiskPercent, 1) + "%\n";
|
|
|
|
if(g_tradeActive)
|
|
{
|
|
// Find our position
|
|
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) != MagicNumber) continue;
|
|
|
|
string dir = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? "BUY" : "SELL";
|
|
double profit = PositionGetDouble(POSITION_PROFIT);
|
|
hud += "══════ ACTIVE TRADE ══════\n";
|
|
hud += "Direction: " + dir + "\n";
|
|
hud += "Entry: " + DoubleToString(PositionGetDouble(POSITION_PRICE_OPEN), _Digits) + "\n";
|
|
hud += "P/L: $" + DoubleToString(profit, 2) + "\n";
|
|
break;
|
|
}
|
|
}
|
|
else if(g_ordersPlaced)
|
|
{
|
|
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
double pipSize = _Point * 10;
|
|
hud += "══════ PENDING ══════\n";
|
|
hud += "Buy Stop: " + DoubleToString(ask + StopDistancePips * pipSize, _Digits) + "\n";
|
|
hud += "Sell Stop: " + DoubleToString(bid - StopDistancePips * pipSize, _Digits) + "\n";
|
|
}
|
|
|
|
if(UseTimeFilter)
|
|
{
|
|
MqlDateTime dt;
|
|
TimeCurrent(dt);
|
|
hud += "Time: " + IntegerToString(dt.hour) + ":" + StringFormat("%02d", dt.min);
|
|
hud += (IsInTradingHours() ? " ✅" : " ⛔") + "\n";
|
|
}
|
|
|
|
Comment(hud);
|
|
}
|
|
//+------------------------------------------------------------------+ |