Test/RangeDetectorEA.mq5
2026-10-06 09:55:32 +00:00
1397 lines
50 KiB
MQL5
//+------------------------------------------------------------------+
//| RangeDetectorEA.mq5 |
//| Copyright 2026, AI Assistant |
//| https://mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, AI Assistant"
#property link "https://mql5.com"
#property version "1.05"
//--- Include the Standard Trade Class
#include <Trade\Trade.mqh>
CTrade trade;
//--- Input parameters
input int InpDailyLookback = 60; // Number of daily candles to rank
input int InpH1Lookback = 24; // Period lookback in hours (24)
input double InpPercentile = 18.0; // Percentile threshold for RANGING status
input double InpLotSize = 0.1; // Order Volume size
input bool InpUseDynamicSLTP = true; // Enable Dynamic SL & TP
input double InpSLPercent = 60.0; // Stop Loss (% of Range Size)
input double InpTPRatio = 5.0; // Take Profit Ratio (1:X RRR)
input bool InpUseBreakeven = true; // Enable Breakeven at 1:1 RR
//--- Friday Close Settings
input bool InpFridayClose = true; // Enable Friday Close
input int InpFridayHour = 21; // Friday Close Hour (0-23)
input int InpFridayMinute = 30; // Friday Close Minute (0-59)
//--- Magic Number
#define EXPERT_MAGIC 123456
//--- Structure to hold active range tracking variables
struct SRangeData
{
bool isActive; // True if a valid range is currently being monitored
double highLevel; // Upper breakout ceiling boundary
double lowLevel; // Lower breakout floor boundary
double rangeSize; // Absolute size of the range (High - Low)
};
//--- Global variables
datetime g_last_day = 0;
datetime g_last_h1 = 0;
double g_daily_ranges[]; // Sorted array of past 60 daily sizes
SRangeData g_saved_range = {false, 0.0, 0.0, 0.0}; // Active range data storage struct
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
ArraySetAsSeries(g_daily_ranges, false);
trade.SetExpertMagicNumber(EXPERT_MAGIC); // Unique identifier for order tracking
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
ObjectsDeleteAll(0, "RangeBox_");
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Check for Friday Close Condition first
if(InpFridayClose && IsFridayCloseTime())
{
CloseAllPositions();
ResetSavedRange(); // Wipe out range memory so no trades open before market close
return;
}
// Check for New Day
datetime current_day = iTime(_Symbol, PERIOD_D1, 0);
if(current_day != g_last_day)
{
OnNewDay();
g_last_day = current_day;
}
// Check for New H1 Candle
datetime current_h1 = iTime(_Symbol, PERIOD_H1, 0);
if(current_h1 != g_last_h1)
{
OnNewH1Candle();
g_last_h1 = current_h1;
}
// Manage active trade trailing/breakeven states on every tick
if(InpUseBreakeven)
{
ManageBreakeven();
}
}
//+------------------------------------------------------------------+
//| Logic executed at the start of a new day |
//+------------------------------------------------------------------+
void OnNewDay()
{
MqlRates rates[];
int copied = CopyRates(_Symbol, PERIOD_D1, 1, InpDailyLookback, rates);
if(copied < InpDailyLookback)
{
Print("Not enough D1 data to rank.");
return;
}
ArrayResize(g_daily_ranges, InpDailyLookback);
for(int i = 0; i < InpDailyLookback; i++)
{
g_daily_ranges[i] = rates[i].high - rates[i].low;
}
ArraySort(g_daily_ranges);
}
//+------------------------------------------------------------------+
//| Logic executed at the start of a new H1 candle |
//+------------------------------------------------------------------+
void OnNewH1Candle()
{
// Check execution/invalidation rules if range data exists inside the struct
if(g_saved_range.isActive)
{
MqlRates candles[];
// Copy the current forming candle (0) and the just closed candle (1)
if(CopyRates(_Symbol, PERIOD_H1, 0, 2, candles) == 2)
{
// Force index 0 to be the most recent data
ArraySetAsSeries(candles, true);
double last_close = candles[1].close; // Close of the candle that just finished
double curr_open = candles[0].open; // Open of the new candle that just started
double gap_high = MathMax(last_close, curr_open);
double gap_low = MathMin(last_close, curr_open);
// Rule: If range high or low price is trapped between last close and current open, delete struct
if((g_saved_range.highLevel > gap_low && g_saved_range.highLevel < gap_high) ||
(g_saved_range.lowLevel > gap_low && g_saved_range.lowLevel < gap_high))
{
Print("Range invalidated due to price gap crossing boundaries. Clearing struct.");
ResetSavedRange();
}
else
{
// Evaluate breakout execution rules on the candle that JUST closed (index 1)
double open_p = candles[1].open;
double close_p = candles[1].close;
double sl = 0.0, tp = 0.0;
// Check Buy Breakout
if(open_p <= g_saved_range.highLevel && open_p >= g_saved_range.lowLevel && close_p > g_saved_range.highLevel)
{
if(InpUseDynamicSLTP)
{
sl = close_p - (g_saved_range.rangeSize * (InpSLPercent / 100.0));
tp = close_p + ((close_p - sl) * InpTPRatio);
}
if(trade.Buy(InpLotSize, _Symbol, 0, sl, tp, "Breakout Buy"))
{
Print("Breakout BUY executed! SL: ", sl, " TP: ", tp);
ResetSavedRange();
}
}
// Check Sell Breakout
else
if(open_p <= g_saved_range.highLevel && open_p >= g_saved_range.lowLevel && close_p < g_saved_range.lowLevel)
{
if(InpUseDynamicSLTP)
{
sl = close_p + (g_saved_range.rangeSize * (InpSLPercent / 100.0));
tp = close_p - ((sl - close_p) * InpTPRatio);
}
if(trade.Sell(InpLotSize, _Symbol, 0, sl, tp, "Breakout Sell"))
{
Print("Breakout SELL executed! SL: ", sl, " TP: ", tp);
ResetSavedRange();
}
}
}
}
}
if(ArraySize(g_daily_ranges) < InpDailyLookback)
return;
MqlRates h1_rates[];
int copied = CopyRates(_Symbol, PERIOD_H1, 1, InpH1Lookback, h1_rates);
if(copied < InpH1Lookback)
return;
double period_high = h1_rates[0].high;
double period_low = h1_rates[0].low;
for(int i = 1; i < InpH1Lookback; i++)
{
if(h1_rates[i].high > period_high)
period_high = h1_rates[i].high;
if(h1_rates[i].low < period_low)
period_low = h1_rates[i].low;
}
double period_range = period_high - period_low;
int rank_count = 0;
for(int i = 0; i < InpDailyLookback; i++)
{
if(period_range > g_daily_ranges[i])
rank_count++;
}
double current_percentile = ((double)rank_count / InpDailyLookback) * 100.0;
if(current_percentile < InpPercentile)
{
g_saved_range.isActive = true;
g_saved_range.highLevel = period_high;
g_saved_range.lowLevel = period_low;
g_saved_range.rangeSize = period_range;
datetime start_time = h1_rates[0].time;
datetime end_time = h1_rates[InpH1Lookback - 1].time;
DrawRangeRectangle(start_time, end_time, period_high, period_low);
}
}
//+------------------------------------------------------------------+
//| Manage Breakeven conditions at 1:1 Risk Reward Ratio |
//+------------------------------------------------------------------+
void ManageBreakeven()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == EXPERT_MAGIC)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
double open_p = PositionGetDouble(POSITION_PRICE_OPEN);
double current_sl = PositionGetDouble(POSITION_SL);
double current_p = PositionGetDouble(POSITION_PRICE_CURRENT);
long pos_type = PositionGetInteger(POSITION_TYPE);
if(current_sl == 0.0)
continue;
if(pos_type == POSITION_TYPE_BUY)
{
double initial_risk = open_p - current_sl;
if(initial_risk > 0 && current_p >= (open_p + initial_risk))
{
if(current_sl < open_p)
{
trade.PositionModify(ticket, open_p, PositionGetDouble(POSITION_TP));
Print("Buy position ticket #", ticket, " modified to Breakeven.");
}
}
}
else
if(pos_type == POSITION_TYPE_SELL)
{
double initial_risk = current_sl - open_p;
if(initial_risk > 0 && current_p <= (open_p - initial_risk))
{
if(current_sl > open_p)
{
trade.PositionModify(ticket, open_p, PositionGetDouble(POSITION_TP));
Print("Sell position ticket #", ticket, " modified to Breakeven.");
}
}
}
}
}
}
//+------------------------------------------------------------------+
//| Check if it is currently past the designated Friday close time |
//+------------------------------------------------------------------+
bool IsFridayCloseTime()
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if(dt.day_of_week == 5)
{
if(dt.hour > InpFridayHour || (dt.hour == InpFridayHour && dt.min >= InpFridayMinute))
{
return(true);
}
}
return(false);
}
//+------------------------------------------------------------------+
//| Close all positions managed by this EA |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == EXPERT_MAGIC)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
if(trade.PositionClose(ticket))
{
Print("Friday close forced: Successfully closed position #", ticket);
}
}
}
}
//+------------------------------------------------------------------+
//| Clear out structural data cache fields |
//+------------------------------------------------------------------+
void ResetSavedRange()
{
g_saved_range.isActive = false;
g_saved_range.highLevel = 0.0;
g_saved_range.lowLevel = 0.0;
g_saved_range.rangeSize = 0.0;
}
//+------------------------------------------------------------------+
//| Draw visual visual bounding box on chart |
//+------------------------------------------------------------------+
void DrawRangeRectangle(datetime t1, datetime t2, double price1, double price2)
{
string name = "RangeBox_" + TimeToString(t1, TIME_DATE|TIME_MINUTES);
if(ObjectCreate(0, name, OBJ_RECTANGLE, 0, t1, price1, t2, price2))
{
ObjectSetInteger(0, name, OBJPROP_COLOR, clrDodgerBlue);
ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, name, OBJPROP_BACK, true);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
}
else
{
ObjectSetInteger(0, name, OBJPROP_TIME, 0, t1);
ObjectSetDouble(0, name, OBJPROP_PRICE, 0, price1);
ObjectSetInteger(0, name, OBJPROP_TIME, 1, t2);
ObjectSetDouble(0, name, OBJPROP_PRICE, 1, price2);
}
ChartRedraw(0);
}
//+------------------------------------------------------------------+input ENUM_DAY_OF_WEEK_BT InpDayOfWeek = BT_FRIDAY; // Target Close Day
input int InpFridayHour = 21; // Friday Close Hour (0-23)
input int InpFridayMinute = 30; // Friday Close Minute (0-59)
//====================================================================
//--- BLUE TICK FRAMEWORK INTERFACES AND IMPLEMENTATION STUBS
//====================================================================
class CRiskManager
{
public:
double LotCalc(ENUM_RISK_TYPE type, double pct, double cash, double fixedLot, double slPoints)
{
if(type == RISK_FIXED_LOT) return fixedLot;
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(slPoints <= 0 || tickValue <= 0) return fixedLot;
double riskCash = 0;
if(type == RISK_PERCENT_BALANCE) riskCash = AccountInfoDouble(ACCOUNT_BALANCE) * (pct / 100.0);
else if(type == RISK_CASH_AMOUNT) riskCash = cash;
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double calculatedLot = (riskCash) / ((slPoints * (tickValue / tickSize) * point));
calculatedLot = MathFloor(calculatedLot / lotStep) * lotStep;
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
if(calculatedLot < minLot) calculatedLot = minLot;
if(calculatedLot > maxLot) calculatedLot = maxLot;
return calculatedLot;
}
};
class CPositionManager
{
public:
void ProcessActivePositions()
{
// Central template engine execution stub for handling trailing stops, manual partials, breakevens
}
};
//--- Internal Strategy Structure
struct SRangeData
{
bool isActive;
double highLevel;
double lowLevel;
double rangeSize;
};
//====================================================================
//--- STRATEGY BREAKOUT CLASS ENGINE DEFINITION
//====================================================================
class CRangeBreakoutEngine
{
private:
CTrade m_trade;
datetime m_last_day;
datetime m_last_h1;
double m_daily_ranges[];
SRangeData m_saved_range;
void OnNewDay(void);
void OnNewH1Candle(CRiskManager &risk);
void DrawRangeRectangle(datetime t1, datetime t2, double price1, double price2);
bool HasActivePosition(void);
public:
CRangeBreakoutEngine(void);
~CRangeBreakoutEngine(void) {}
void ProcessScanPipeline(CRiskManager &risk);
void ResetSavedRange(void);
void CheckFridayTeardown(void);
};
//--- Constructor definition inside compilation rules
CRangeBreakoutEngine::CRangeBreakoutEngine(void)
{
m_last_day = 0;
m_last_h1 = 0;
m_saved_range.isActive = false;
m_saved_range.highLevel = 0.0;
m_saved_range.lowLevel = 0.0;
m_saved_range.rangeSize = 0.0;
ArraySetAsSeries(m_daily_ranges, false);
}
void CRangeBreakoutEngine::ResetSavedRange(void)
{
m_saved_range.isActive = false;
m_saved_range.highLevel = 0.0;
m_saved_range.lowLevel = 0.0;
m_saved_range.rangeSize = 0.0;
}
bool CRangeBreakoutEngine::HasActivePosition(void)
{
for(int p = PositionsTotal() - 1; p >= 0; p--)
{
if(PositionGetTicket(p) > 0 && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber && PositionGetString(POSITION_SYMBOL) == _Symbol)
return true;
}
return false;
}
void CRangeBreakoutEngine::ProcessScanPipeline(CRiskManager &risk)
{
datetime current_day = iTime(_Symbol, PERIOD_D1, 0);
if(current_day != m_last_day)
{
OnNewDay();
m_last_day = current_day;
}
datetime current_h1 = iTime(_Symbol, PERIOD_H1, 0);
if(current_h1 != m_last_h1)
{
OnNewH1Candle(risk);
m_last_h1 = current_h1;
}
}
void CRangeBreakoutEngine::OnNewDay(void)
{
MqlRates rates[];
int copied = CopyRates(_Symbol, PERIOD_D1, 1, InpDailyLookback, rates);
if(copied < InpDailyLookback) return;
ArrayResize(m_daily_ranges, InpDailyLookback);
for(int i = 0; i < InpDailyLookback; i++)
{
m_daily_ranges[i] = rates[i].high - rates[i].low;
}
ArraySort(m_daily_ranges);
}
void CRangeBreakoutEngine::OnNewH1Candle(CRiskManager &risk)
{
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(m_saved_range.isActive)
{
MqlRates candles[];
if(CopyRates(_Symbol, PERIOD_H1, 0, 2, candles) == 2)
{
ArraySetAsSeries(candles, true);
double last_close = candles[1].close;
double curr_open = candles[0].open;
double gap_high = MathMax(last_close, curr_open);
double gap_low = MathMin(last_close, curr_open);
if((m_saved_range.highLevel > gap_low && m_saved_range.highLevel < gap_high) ||
(m_saved_range.lowLevel > gap_low && m_saved_range.lowLevel < gap_high))
{
Print("[Framework Alert] Range boundary invalidation gap encountered. Wiping local data structures.");
ResetSavedRange();
}
else
{
double open_p = candles[1].open;
double close_p = candles[1].close;
double sl = 0.0;
// --- BUY CONDITIONS MET
if(open_p <= m_saved_range.highLevel && open_p >= m_saved_range.lowLevel && close_p > m_saved_range.highLevel)
{
if(!HasActivePosition())
{
sl = close_p - (m_saved_range.rangeSize * (InpSLPercent / 100.0));
double sl_points = (close_p - sl) / point;
if(sl_points > 0)
{
double lot = risk.LotCalc(InpRiskType, InpPctBalanceVal, InpCashAmountVal, InpFixedLotVal, sl_points);
string tracking_comment = StringFormat("ISL:%.5f %s", sl, InpComment);
m_trade.SetExpertMagicNumber(InpMagicNumber);
if(m_trade.Buy(lot, _Symbol, 0, sl, 0.0, tracking_comment)) ResetSavedRange();
}
}
}
// --- SELL CONDITIONS MET
else if(open_p <= m_saved_range.highLevel && open_p >= m_saved_range.lowLevel && close_p < m_saved_range.lowLevel)
{
if(!HasActivePosition())
{
sl = close_p + (m_saved_range.rangeSize * (InpSLPercent / 100.0));
double sl_points = (sl - close_p) / point;
if(sl_points > 0)
{
double lot = risk.LotCalc(InpRiskType, InpPctBalanceVal, InpCashAmountVal, InpFixedLotVal, sl_points);
string tracking_comment = StringFormat("ISL:%.5f %s", sl, InpComment);
m_trade.SetExpertMagicNumber(InpMagicNumber);
if(m_trade.Sell(lot, _Symbol, 0, sl, 0.0, tracking_comment)) ResetSavedRange();
}
}
}
}
}
}
if(ArraySize(m_daily_ranges) < InpDailyLookback) return;
MqlRates h1_rates[];
int copied = CopyRates(_Symbol, PERIOD_H1, 1, InpH1Lookback, h1_rates);
if(copied < InpH1Lookback) return;
double period_high = h1_rates[0].high;
double period_low = h1_rates[0].low;
for(int i = 1; i < InpH1Lookback; i++)
{
if(h1_rates[i].high > period_high) period_high = h1_rates[i].high;
if(h1_rates[i].low < period_low) period_low = h1_rates[i].low;
}
double period_range = period_high - period_low;
int rank_count = 0;
for(int i = 0; i < InpDailyLookback; i++)
{
if(period_range > m_daily_ranges[i]) rank_count++;
}
double current_percentile = ((double)rank_count / InpDailyLookback) * 100.0;
if(current_percentile < InpPercentile)
{
m_saved_range.isActive = true;
m_saved_range.highLevel = period_high;
m_saved_range.lowLevel = period_low;
m_saved_range.rangeSize = period_range;
datetime start_time = h1_rates[0].time;
datetime end_time = h1_rates[InpH1Lookback - 1].time;
DrawRangeRectangle(start_time, end_time, period_high, period_low);
}
}
void CRangeBreakoutEngine::DrawRangeRectangle(datetime t1, datetime t2, double price1, double price2)
{
string name = "RangeBox_" + TimeToString(t1, TIME_DATE|TIME_MINUTES);
if(ObjectCreate(0, name, OBJ_RECTANGLE, 0, t1, price1, t2, price2))
{
ObjectSetInteger(0, name, OBJPROP_COLOR, clrDodgerBlue);
ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, name, OBJPROP_BACK, true);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
}
ChartRedraw(0);
}
void CRangeBreakoutEngine::CheckFridayTeardown(void)
{
if(!InpCloseFlag) return;
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if(dt.day_of_week == (int)InpDayOfWeek)
{
if(dt.hour > InpFridayHour || (dt.hour == InpFridayHour && dt.min >= InpFridayMinute))
{
ResetSavedRange();
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
m_trade.SetExpertMagicNumber(InpMagicNumber);
m_trade.PositionClose(ticket);
}
}
}
}
}
//====================================================================
//--- RUNTIME OBJECT GLOBAL INITIALIZATIONS
//====================================================================
CRiskManager RiskEngine;
CPositionManager PositionTemplateManager;
CRangeBreakoutEngine StrategyEngine;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
ObjectsDeleteAll(0, "RangeBox_");
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// 1. Process trade exit rules during Friday timings first
StrategyEngine.CheckFridayTeardown();
// 2. Continuous strategic scanner execution pipelines
StrategyEngine.ProcessScanPipeline(RiskEngine);
// 3. Centralized framework trailing stop / breakeven automation execution loop
PositionTemplateManager.ProcessActivePositions();
}
//+------------------------------------------------------------------+void OnNewDay()
{
MqlRates rates[];
int copied = CopyRates(_Symbol, PERIOD_D1, 1, InpDailyLookback, rates);
if(copied < InpDailyLookback)
{
Print("Not enough D1 data to rank.");
return;
}
ArrayResize(g_daily_ranges, InpDailyLookback);
for(int i = 0; i < InpDailyLookback; i++)
{
g_daily_ranges[i] = rates[i].high - rates[i].low;
}
ArraySort(g_daily_ranges);
}
//+------------------------------------------------------------------+
//| Logic executed at the start of a new H1 candle |
//+------------------------------------------------------------------+
void OnNewH1Candle()
{
// Check execution/invalidation rules if range data exists inside the struct
if(g_saved_range.isActive)
{
MqlRates candles[];
// Copy the current forming candle (0) and the just closed candle (1)
if(CopyRates(_Symbol, PERIOD_H1, 0, 2, candles) == 2)
{
// Force index 0 to be the most recent data
ArraySetAsSeries(candles, true);
double last_close = candles[1].close; // Close of the candle that just finished
double curr_open = candles[0].open; // Open of the new candle that just started
double gap_high = MathMax(last_close, curr_open);
double gap_low = MathMin(last_close, curr_open);
// Rule: If range high or low price is trapped between last close and current open, delete struct
if((g_saved_range.highLevel > gap_low && g_saved_range.highLevel < gap_high) ||
(g_saved_range.lowLevel > gap_low && g_saved_range.lowLevel < gap_high))
{
Print("Range invalidated due to price gap crossing boundaries. Clearing struct.");
ResetSavedRange();
}
else
{
// Evaluate breakout execution rules on the candle that JUST closed (index 1)
double open_p = candles[1].open;
double close_p = candles[1].close;
double sl = 0.0, tp = 0.0;
// Check Buy Breakout
if(open_p <= g_saved_range.highLevel && open_p >= g_saved_range.lowLevel && close_p > g_saved_range.highLevel)
{
if(InpUseDynamicSLTP)
{
sl = close_p - (g_saved_range.rangeSize * (InpSLPercent / 100.0));
tp = close_p + ((close_p - sl) * InpTPRatio);
}
if(trade.Buy(InpLotSize, _Symbol, 0, sl, tp, "Breakout Buy"))
{
Print("Breakout BUY executed! SL: ", sl, " TP: ", tp);
ResetSavedRange();
}
}
// Check Sell Breakout
else if(open_p <= g_saved_range.highLevel && open_p >= g_saved_range.lowLevel && close_p < g_saved_range.lowLevel)
{
if(InpUseDynamicSLTP)
{
sl = close_p + (g_saved_range.rangeSize * (InpSLPercent / 100.0));
tp = close_p - ((sl - close_p) * InpTPRatio);
}
if(trade.Sell(InpLotSize, _Symbol, 0, sl, tp, "Breakout Sell"))
{
Print("Breakout SELL executed! SL: ", sl, " TP: ", tp);
ResetSavedRange();
}
}
}
}
}
if(ArraySize(g_daily_ranges) < InpDailyLookback)
return;
MqlRates h1_rates[];
int copied = CopyRates(_Symbol, PERIOD_H1, 1, InpH1Lookback, h1_rates);
if(copied < InpH1Lookback)
return;
double period_high = h1_rates[0].high;
double period_low = h1_rates[0].low;
for(int i = 1; i < InpH1Lookback; i++)
{
if(h1_rates[i].high > period_high)
period_high = h1_rates[i].high;
if(h1_rates[i].low < period_low)
period_low = h1_rates[i].low;
}
double period_range = period_high - period_low;
int rank_count = 0;
for(int i = 0; i < InpDailyLookback; i++)
{
if(period_range > g_daily_ranges[i])
rank_count++;
}
double current_percentile = ((double)rank_count / InpDailyLookback) * 100.0;
if(current_percentile < InpPercentile)
{
g_saved_range.isActive = true;
g_saved_range.highLevel = period_high;
g_saved_range.lowLevel = period_low;
g_saved_range.rangeSize = period_range;
datetime start_time = h1_rates[0].time;
datetime end_time = h1_rates[InpH1Lookback - 1].time;
DrawRangeRectangle(start_time, end_time, period_high, period_low);
}
}
//+------------------------------------------------------------------+
//| Manage Breakeven conditions at 1:1 Risk Reward Ratio |
//+------------------------------------------------------------------+
void ManageBreakeven()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == EXPERT_MAGIC)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
double open_p = PositionGetDouble(POSITION_PRICE_OPEN);
double current_sl = PositionGetDouble(POSITION_SL);
double current_p = PositionGetDouble(POSITION_PRICE_CURRENT);
long pos_type = PositionGetInteger(POSITION_TYPE);
if(current_sl == 0.0)
continue;
if(pos_type == POSITION_TYPE_BUY)
{
double initial_risk = open_p - current_sl;
if(initial_risk > 0 && current_p >= (open_p + initial_risk))
{
if(current_sl < open_p)
{
trade.PositionModify(ticket, open_p, PositionGetDouble(POSITION_TP));
Print("Buy position ticket #", ticket, " modified to Breakeven.");
}
}
}
else if(pos_type == POSITION_TYPE_SELL)
{
double initial_risk = current_sl - open_p;
if(initial_risk > 0 && current_p <= (open_p - initial_risk))
{
if(current_sl > open_p)
{
trade.PositionModify(ticket, open_p, PositionGetDouble(POSITION_TP));
Print("Sell position ticket #", ticket, " modified to Breakeven.");
}
}
}
}
}
}
//+------------------------------------------------------------------+
//| Check if it is currently past the designated Friday close time |
//+------------------------------------------------------------------+
bool IsFridayCloseTime()
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if(dt.day_of_week == 5)
{
if(dt.hour > InpFridayHour || (dt.hour == InpFridayHour && dt.min >= InpFridayMinute))
{
return(true);
}
}
return(false);
}
//+------------------------------------------------------------------+
//| Close all positions managed by this EA |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == EXPERT_MAGIC)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
if(trade.PositionClose(ticket))
{
Print("Friday close forced: Successfully closed position #", ticket);
}
}
}
}
//+------------------------------------------------------------------+
//| Clear out structural data cache fields |
//+------------------------------------------------------------------+
void ResetSavedRange()
{
g_saved_range.isActive = false;
g_saved_range.highLevel = 0.0;
g_saved_range.lowLevel = 0.0;
g_saved_range.rangeSize = 0.0;
}
//+------------------------------------------------------------------+
//| Draw visual visual bounding box on chart |
//+------------------------------------------------------------------+
void DrawRangeRectangle(datetime t1, datetime t2, double price1, double price2)
{
string name = "RangeBox_" + TimeToString(t1, TIME_DATE|TIME_MINUTES);
if(ObjectCreate(0, name, OBJ_RECTANGLE, 0, t1, price1, t2, price2))
{
ObjectSetInteger(0, name, OBJPROP_COLOR, clrDodgerBlue);
ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, name, OBJPROP_BACK, true);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
}
else
{
ObjectSetInteger(0, name, OBJPROP_TIME, 0, t1);
ObjectSetDouble(0, name, OBJPROP_PRICE, 0, price1);
ObjectSetInteger(0, name, OBJPROP_TIME, 1, t2);
ObjectSetDouble(0, name, OBJPROP_PRICE, 1, price2);
}
ChartRedraw(0);
}
void ProcessScanPipeline(CRiskManager &risk);
void ResetSavedRange(void);
};
void CRangeBreakoutEngine::ResetSavedRange(void)
{
m_saved_range.isActive = false;
m_saved_range.highLevel = 0.0;
m_saved_range.lowLevel = 0.0;
m_saved_range.rangeSize = 0.0;
}
bool CRangeBreakoutEngine::HasActivePosition(void)
{
for(int p = PositionsTotal() - 1; p >= 0; p--)
{
if(PositionGetTicket(p) > 0 && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber && PositionGetString(POSITION_SYMBOL) == _Symbol)
return true;
}
return false;
}
void CRangeBreakoutEngine::ProcessScanPipeline(CRiskManager &risk)
{
// Check for New Day tracking metrics
datetime current_day = iTime(_Symbol, PERIOD_D1, 0);
if(current_day != m_last_day)
{
OnNewDay();
m_last_day = current_day;
}
// Check for New H1 structural candle shift transitions
datetime current_h1 = iTime(_Symbol, PERIOD_H1, 0);
if(current_h1 != m_last_h1)
{
OnNewH1Candle(risk);
m_last_h1 = current_h1;
}
}
void CRangeBreakoutEngine::OnNewDay(void)
{
MqlRates rates[];
int copied = CopyRates(_Symbol, PERIOD_D1, 1, InpDailyLookback, rates);
if(copied < InpDailyLookback) return;
ArrayResize(m_daily_ranges, InpDailyLookback);
for(int i = 0; i < InpDailyLookback; i++)
{
m_daily_ranges[i] = rates[i].high - rates[i].low;
}
ArraySort(m_daily_ranges);
}
void CRangeBreakoutEngine::OnNewH1Candle(void, CRiskManager &risk)
{
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(m_saved_range.isActive)
{
MqlRates candles[];
if(CopyRates(_Symbol, PERIOD_H1, 0, 2, candles) == 2)
{
ArraySetAsSeries(candles, true);
double last_close = candles[1].close;
double curr_open = candles[0].open;
double gap_high = MathMax(last_close, curr_open);
double gap_low = MathMin(last_close, curr_open);
if((m_saved_range.highLevel > gap_low && m_saved_range.highLevel < gap_high) ||
(m_saved_range.lowLevel > gap_low && m_saved_range.lowLevel < gap_high))
{
Print("[Framework Alert] Range boundary gap invalidation encountered. Wiping structure data cache.");
ResetSavedRange();
}
else
{
double open_p = candles[1].open;
double close_p = candles[1].close;
double sl = 0.0;
// --- BUY BREAKOUT SIGNAL DETERMINED
if(open_p <= m_saved_range.highLevel && open_p >= m_saved_range.lowLevel && close_p > m_saved_range.highLevel)
{
if(!HasActivePosition())
{
sl = close_p - (m_saved_range.rangeSize * (InpSLPercent / 100.0));
double sl_points = (close_p - sl) / point;
if(sl_points > 0)
{
double lot = risk.LotCalc(InpRiskType, InpPctBalanceVal, InpCashAmountVal, InpFixedLotVal, sl_points);
string tracking_comment = StringFormat("ISL:%.5f %s", sl, InpComment);
m_trade.SetExpertMagicNumber(InpMagicNumber);
if(m_trade.Buy(lot, _Symbol, 0, sl, 0.0, tracking_comment)) ResetSavedRange();
}
}
}
// --- SELL BREAKOUT SIGNAL DETERMINED
else if(open_p <= m_saved_range.highLevel && open_p >= m_saved_range.lowLevel && close_p < m_saved_range.lowLevel)
{
if(!HasActivePosition())
{
sl = close_p + (m_saved_range.rangeSize * (InpSLPercent / 100.0));
double sl_points = (sl - close_p) / point;
if(sl_points > 0)
{
double lot = risk.LotCalc(InpRiskType, InpPctBalanceVal, InpCashAmountVal, InpFixedLotVal, sl_points);
string tracking_comment = StringFormat("ISL:%.5f %s", sl, InpComment);
m_trade.SetExpertMagicNumber(InpMagicNumber);
if(m_trade.Sell(lot, _Symbol, 0, sl, 0.0, tracking_comment)) ResetSavedRange();
}
}
}
}
}
}
if(ArraySize(m_daily_ranges) < InpDailyLookback) return;
MqlRates h1_rates[];
int copied = CopyRates(_Symbol, PERIOD_H1, 1, InpH1Lookback, h1_rates);
if(copied < InpH1Lookback) return;
double period_high = h1_rates[0].high;
double period_low = h1_rates[0].low;
for(int i = 1; i < InpH1Lookback; i++)
{
if(h1_rates[i].high > period_high) period_high = h1_rates[i].high;
if(h1_rates[i].low < period_low) period_low = h1_rates[i].low;
}
double period_range = period_high - period_low;
int rank_count = 0;
for(int i = 0; i < InpDailyLookback; i++)
{
if(period_range > m_daily_ranges[i]) rank_count++;
}
double current_percentile = ((double)rank_count / InpDailyLookback) * 100.0;
if(current_percentile < InpPercentile)
{
m_saved_range.isActive = true;
m_saved_range.highLevel = period_high;
m_saved_range.lowLevel = period_low;
m_saved_range.rangeSize = period_range;
datetime start_time = h1_rates[0].time;
datetime end_time = h1_rates[InpH1Lookback - 1].time;
DrawRangeRectangle(start_time, end_time, period_high, period_low);
}
}
void CRangeBreakoutEngine::DrawRangeRectangle(datetime t1, datetime t2, double price1, double price2)
{
string name = "RangeBox_" + TimeToString(t1, TIME_DATE|TIME_MINUTES);
if(ObjectCreate(0, name, OBJ_RECTANGLE, 0, t1, price1, t2, price2))
{
ObjectSetInteger(0, name, OBJPROP_COLOR, clrDodgerBlue);
ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, name, OBJPROP_BACK, true);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
}
ChartRedraw(0);
}
//====================================================================
//--- RUNTIME GLOBAL OBJECT INITIALIZATIONS
//====================================================================
CRiskManager RiskEngine;
CPositionManager PositionTemplateManager;
CRangeBreakoutEngine StrategyEngine;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
Print("== [BlueTick Framework Integrated Range Breakout EA Suit Online] ==");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
ObjectsDeleteAll(0, "RangeBox_");
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// 1. Evaluate strategy signal processing scans (Calculates entries & sets initial risk levels)
StrategyEngine.ProcessScanPipeline(RiskEngine);
// 2. Continuous template processing module pipelines (Handles Partials, BEs, Trails, and Friday Close)
PositionTemplateManager.ProcessActivePositions(
InpBEFlag, InpBETrigger, InpBERRRVal, InpBEPointsVal, InpBETimeVal,
// Triple partial profit parameters handled by template array structures
InpPCFlag, InpPCTrigger,
InpPCRRRVal1, InpPCPointsVal1, InpPCTimeVal1, InpPCFixedLot1, InpPCPercent1,
InpPCRRRVal2, InpPCPointsVal2, InpPCTimeVal2, InpPCFixedLot2, InpPCPercent2,
InpPCRRRVal3, InpPCPointsVal3, InpPCTimeVal3, InpPCFixedLot3, InpPCPercent3,
InpTSFlag, InpTSTrigger, InpTSRRRVal, InpTSPointsVal, InpTSTimeVal, InpTSAmtType, InpTSPtsAway, InpTSPctReward,
InpCloseFlag, InpCloseTrigger, InpDailyTime, InpSpecificDT, InpDayOfWeek, InpDayTime
);
// 3. Intercept active execution variables to clear strategy workspace data structures on Friday close signals
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if(InpCloseFlag && dt.day_of_week == (int)InpDayOfWeek)
{
string time_parts[];
if(StringSplit(InpDayTime, ':', time_parts) == 2)
{
int close_hour = (int)StringToInteger(time_parts[0]);
int close_min = (int)StringToInteger(time_parts[1]);
if(dt.hour > close_hour || (dt.hour == close_hour && dt.min >= close_min))
{
StrategyEngine.ResetSavedRange();
}
}
}
}
//+------------------------------------------------------------------+ }
ArrayResize(g_daily_ranges, InpDailyLookback);
for(int i = 0; i < InpDailyLookback; i++)
{
g_daily_ranges[i] = rates[i].high - rates[i].low;
}
ArraySort(g_daily_ranges);
}
//+------------------------------------------------------------------+
//| Logic executed at the start of a new H1 candle |
//+------------------------------------------------------------------+
void OnNewH1Candle()
{
// Check execution/invalidation rules if range data exists inside the struct
if(g_saved_range.isActive)
{
MqlRates candles[];
// Copy the current forming candle (0) and the just closed candle (1)
if(CopyRates(_Symbol, PERIOD_H1, 0, 2, candles) == 2)
{
// Force index 0 to be the most recent data
ArraySetAsSeries(candles, true);
double last_close = candles[1].close; // Close of the candle that just finished
double curr_open = candles[0].open; // Open of the new candle that just started
double gap_high = MathMax(last_close, curr_open);
double gap_low = MathMin(last_close, curr_open);
// Rule: If range high or low price is trapped between last close and current open, delete struct
if((g_saved_range.highLevel > gap_low && g_saved_range.highLevel < gap_high) ||
(g_saved_range.lowLevel > gap_low && g_saved_range.lowLevel < gap_high))
{
Print("Range invalidated due to price gap crossing boundaries. Clearing struct.");
ResetSavedRange();
}
else
{
// Evaluate breakout execution rules on the candle that JUST closed (index 1)
double open_p = candles[1].open;
double close_p = candles[1].close;
double sl = 0.0, tp = 0.0;
// Check Buy Breakout
if(open_p <= g_saved_range.highLevel && open_p >= g_saved_range.lowLevel && close_p > g_saved_range.highLevel)
{
if(InpUseDynamicSLTP)
{
sl = close_p - (g_saved_range.rangeSize * (InpSLPercent / 100.0));
tp = close_p + ((close_p - sl) * InpTPRatio);
}
if(trade.Buy(InpLotSize, _Symbol, 0, sl, tp, "Breakout Buy"))
{
Print("Breakout BUY executed! SL: ", sl, " TP: ", tp);
ResetSavedRange();
}
}
// Check Sell Breakout
else
if(open_p <= g_saved_range.highLevel && open_p >= g_saved_range.lowLevel && close_p < g_saved_range.lowLevel)
{
if(InpUseDynamicSLTP)
{
sl = close_p + (g_saved_range.rangeSize * (InpSLPercent / 100.0));
tp = close_p - ((sl - close_p) * InpTPRatio);
}
if(trade.Sell(InpLotSize, _Symbol, 0, sl, tp, "Breakout Sell"))
{
Print("Breakout SELL executed! SL: ", sl, " TP: ", tp);
ResetSavedRange();
}
}
}
}
}
if(ArraySize(g_daily_ranges) < InpDailyLookback)
return;
MqlRates h1_rates[];
int copied = CopyRates(_Symbol, PERIOD_H1, 1, InpH1Lookback, h1_rates);
if(copied < InpH1Lookback)
return;
double period_high = h1_rates[0].high;
double period_low = h1_rates[0].low;
for(int i = 1; i < InpH1Lookback; i++)
{
if(h1_rates[i].high > period_high)
period_high = h1_rates[i].high;
if(h1_rates[i].low < period_low)
period_low = h1_rates[i].low;
}
double period_range = period_high - period_low;
int rank_count = 0;
for(int i = 0; i < InpDailyLookback; i++)
{
if(period_range > g_daily_ranges[i])
rank_count++;
}
double current_percentile = ((double)rank_count / InpDailyLookback) * 100.0;
if(current_percentile < InpPercentile)
{
g_saved_range.isActive = true;
g_saved_range.highLevel = period_high;
g_saved_range.lowLevel = period_low;
g_saved_range.rangeSize = period_range;
datetime start_time = h1_rates[0].time;
datetime end_time = h1_rates[InpH1Lookback - 1].time;
DrawRangeRectangle(start_time, end_time, period_high, period_low);
}
}
//+------------------------------------------------------------------+
//| Manage Breakeven conditions at 1:1 Risk Reward Ratio |
//+------------------------------------------------------------------+
void ManageBreakeven()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == EXPERT_MAGIC)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
double open_p = PositionGetDouble(POSITION_PRICE_OPEN);
double current_sl = PositionGetDouble(POSITION_SL);
double current_p = PositionGetDouble(POSITION_PRICE_CURRENT);
long pos_type = PositionGetInteger(POSITION_TYPE);
if(current_sl == 0.0)
continue;
if(pos_type == POSITION_TYPE_BUY)
{
double initial_risk = open_p - current_sl;
if(initial_risk > 0 && current_p >= (open_p + initial_risk))
{
if(current_sl < open_p)
{
trade.PositionModify(ticket, open_p, PositionGetDouble(POSITION_TP));
Print("Buy position ticket #", ticket, " modified to Breakeven.");
}
}
}
else
if(pos_type == POSITION_TYPE_SELL)
{
double initial_risk = current_sl - open_p;
if(initial_risk > 0 && current_p <= (open_p - initial_risk))
{
if(current_sl > open_p)
{
trade.PositionModify(ticket, open_p, PositionGetDouble(POSITION_TP));
Print("Sell position ticket #", ticket, " modified to Breakeven.");
}
}
}
}
}
}
//+------------------------------------------------------------------+
//| Check if it is currently past the designated Friday close time |
//+------------------------------------------------------------------+
bool IsFridayCloseTime()
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if(dt.day_of_week == 5)
{
if(dt.hour > InpFridayHour || (dt.hour == InpFridayHour && dt.min >= InpFridayMinute))
{
return(true);
}
}
return(false);
}
//+------------------------------------------------------------------+
//| Close all positions managed by this EA |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == EXPERT_MAGIC)
{
ulong ticket = PositionGetInteger(POSITION_TICKET);
if(trade.PositionClose(ticket))
{
Print("Friday close forced: Successfully closed position #", ticket);
}
}
}
}
//+------------------------------------------------------------------+
//| Clear out structural data cache fields |
//+------------------------------------------------------------------+
void ResetSavedRange()
{
g_saved_range.isActive = false;
g_saved_range.highLevel = 0.0;
g_saved_range.lowLevel = 0.0;
g_saved_range.rangeSize = 0.0;
}
//+------------------------------------------------------------------+
//| Draw visual visual bounding box on chart |
//+------------------------------------------------------------------+
void DrawRangeRectangle(datetime t1, datetime t2, double price1, double price2)
{
string name = "RangeBox_" + TimeToString(t1, TIME_DATE|TIME_MINUTES);
if(ObjectCreate(0, name, OBJ_RECTANGLE, 0, t1, price1, t2, price2))
{
ObjectSetInteger(0, name, OBJPROP_COLOR, clrDodgerBlue);
ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, name, OBJPROP_BACK, true);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
}
else
{
ObjectSetInteger(0, name, OBJPROP_TIME, 0, t1);
ObjectSetDouble(0, name, OBJPROP_PRICE, 0, price1);
ObjectSetInteger(0, name, OBJPROP_TIME, 1, t2);
ObjectSetDouble(0, name, OBJPROP_PRICE, 1, price2);
}
ChartRedraw(0);
}
//+------------------------------------------------------------------+