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