DR_BreakoutStraddle/DR_BreakoutStraddle.mq5

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);
}
//+------------------------------------------------------------------+