2026-10-06 09:10:16 +00:00 | | | //+------------------------------------------------------------------+
|
2026-10-06 09:55:32 +00:00 | | | //| RangeDetectorEA.mq5 |
|
| | | //| Copyright 2026, AI Assistant |
|
| | | //| https://mql5.com |
|
2026-10-06 09:10:16 +00:00 | | | //+------------------------------------------------------------------+
|
2026-10-06 09:55:32 +00:00 | | | #property copyright "Copyright 2026, AI Assistant"
|
| | | #property link "https://mql5.com"
|
| | | #property version "1.05"
|
2026-10-06 09:10:16 +00:00 | | |
|
2026-10-06 09:55:32 +00:00 | | | //--- Include the Standard Trade Class
|
2026-10-06 09:10:16 +00:00 | | | #include <Trade\Trade.mqh>
|
2026-10-06 09:55:32 +00:00 | | | CTrade trade;
|
2026-10-06 09:36:20 +00:00 | | |
|
2026-10-06 09:55:32 +00:00 | | | //--- 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
|
2026-10-06 09:36:20 +00:00 | | | {
|
2026-10-06 09:55:32 +00:00 | | | 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)
|
2026-10-06 09:36:20 +00:00 | | | };
|
| | |
|
2026-10-06 09:55:32 +00:00 | | | //--- 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()
|
2026-10-06 09:36:20 +00:00 | | | {
|
2026-10-06 09:55:32 +00:00 | | | ArraySetAsSeries(g_daily_ranges, false);
|
| | | trade.SetExpertMagicNumber(EXPERT_MAGIC); // Unique identifier for order tracking
|
| | | return(INIT_SUCCEEDED);
|
| | | }
|
2026-10-06 09:36:20 +00:00 | | |
|
2026-10-06 09:55:32 +00:00 | | | //+------------------------------------------------------------------+
|
| | | //| Expert deinitialization function |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnDeinit(const int reason)
|
2026-10-06 09:36:20 +00:00 | | | {
|
2026-10-06 09:55:32 +00:00 | | | ObjectsDeleteAll(0, "RangeBox_");
|
| | | }
|
2026-10-06 09:36:20 +00:00 | | |
|
2026-10-06 09:55:32 +00:00 | | | //+------------------------------------------------------------------+
|
| | | //| Expert tick function |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnTick()
|
2026-10-06 09:36:20 +00:00 | | | {
|
2026-10-06 09:55:32 +00:00 | | | // 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;
|
| | | }
|
2026-10-06 09:36:20 +00:00 | | |
|
2026-10-06 09:55:32 +00:00 | | | // Manage active trade trailing/breakeven states on every tick
|
| | | if(InpUseBreakeven)
|
| | | {
|
| | | ManageBreakeven();
|
| | | }
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Logic executed at the start of a new day |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnNewDay()
|
2026-10-06 09:36:20 +00:00 | | | {
|
2026-10-06 09:55:32 +00:00 | | | 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);
|
| | | }
|
2026-10-06 09:36:20 +00:00 | | |
|
2026-10-06 09:55:32 +00:00 | | | //+------------------------------------------------------------------+
|
| | | //| Logic executed at the start of a new H1 candle |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnNewH1Candle()
|
2026-10-06 09:36:20 +00:00 | | | {
|
2026-10-06 09:55:32 +00:00 | | | // 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);
|
| | | }
|
2026-10-06 09:36:20 +00:00 | | |
|
2026-10-06 09:55:32 +00:00 | | | 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()
|
2026-10-06 09:36:20 +00:00 | | | {
|
2026-10-06 09:55:32 +00:00 | | | 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;
|
2026-10-06 09:36:20 +00:00 | | |
|
2026-10-06 09:55:32 +00:00 | | | 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
|
2026-10-06 09:36:20 +00:00 | | | 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
|
2026-10-06 09:10:16 +00:00 | | | struct SRangeData
|
| | | {
|
2026-10-06 09:36:20 +00:00 | | | bool isActive;
|
| | | double highLevel;
|
| | | double lowLevel;
|
| | | double rangeSize;
|
2026-10-06 09:10:16 +00:00 | | | };
|
| | |
|
2026-10-06 09:36:20 +00:00 | | | //====================================================================
|
| | | //--- 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;
|
2026-10-06 09:26:49 +00:00 | | |
|
2026-10-06 09:36:20 +00:00 | | | 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)
|
2026-10-06 09:26:49 +00:00 | | | {
|
2026-10-06 09:36:20 +00:00 | | | 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);
|
2026-10-06 09:26:49 +00:00 | | | }
|
| | |
|
2026-10-06 09:36:20 +00:00 | | | void CRangeBreakoutEngine::ResetSavedRange(void)
|
2026-10-06 09:26:49 +00:00 | | | {
|
2026-10-06 09:36:20 +00:00 | | | m_saved_range.isActive = false;
|
| | | m_saved_range.highLevel = 0.0;
|
| | | m_saved_range.lowLevel = 0.0;
|
| | | m_saved_range.rangeSize = 0.0;
|
2026-10-06 09:26:49 +00:00 | | | }
|
| | |
|
2026-10-06 09:36:20 +00:00 | | | bool CRangeBreakoutEngine::HasActivePosition(void)
|
2026-10-06 09:26:49 +00:00 | | | {
|
2026-10-06 09:36:20 +00:00 | | | for(int p = PositionsTotal() - 1; p >= 0; p--)
|
2026-10-06 09:26:49 +00:00 | | | {
|
2026-10-06 09:36:20 +00:00 | | | if(PositionGetTicket(p) > 0 && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber && PositionGetString(POSITION_SYMBOL) == _Symbol)
|
| | | return true;
|
2026-10-06 09:26:49 +00:00 | | | }
|
2026-10-06 09:36:20 +00:00 | | | return false;
|
| | | }
|
2026-10-06 09:26:49 +00:00 | | |
|
2026-10-06 09:36:20 +00:00 | | | void CRangeBreakoutEngine::ProcessScanPipeline(CRiskManager &risk)
|
| | | {
|
2026-10-06 09:26:49 +00:00 | | | datetime current_day = iTime(_Symbol, PERIOD_D1, 0);
|
2026-10-06 09:36:20 +00:00 | | | if(current_day != m_last_day)
|
2026-10-06 09:26:49 +00:00 | | | {
|
| | | OnNewDay();
|
2026-10-06 09:36:20 +00:00 | | | m_last_day = current_day;
|
2026-10-06 09:26:49 +00:00 | | | }
|
| | |
|
| | | datetime current_h1 = iTime(_Symbol, PERIOD_H1, 0);
|
2026-10-06 09:36:20 +00:00 | | | 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)
|
2026-10-06 09:26:49 +00:00 | | | {
|
2026-10-06 09:36:20 +00:00 | | | 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);
|
2026-10-06 09:26:49 +00:00 | | | }
|
2026-10-06 09:36:20 +00:00 | | | ChartRedraw(0);
|
| | | }
|
2026-10-06 09:26:49 +00:00 | | |
|
2026-10-06 09:36:20 +00:00 | | | void CRangeBreakoutEngine::CheckFridayTeardown(void)
|
| | | {
|
| | | if(!InpCloseFlag) return;
|
| | | MqlDateTime dt;
|
| | | TimeToStruct(TimeCurrent(), dt);
|
| | | if(dt.day_of_week == (int)InpDayOfWeek)
|
2026-10-06 09:26:49 +00:00 | | | {
|
2026-10-06 09:36:20 +00:00 | | | 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);
|
| | | }
|
| | | }
|
| | | }
|
2026-10-06 09:26:49 +00:00 | | | }
|
| | | }
|
| | |
|
2026-10-06 09:36:20 +00:00 | | | //====================================================================
|
| | | //--- RUNTIME OBJECT GLOBAL INITIALIZATIONS
|
| | | //====================================================================
|
| | | CRiskManager RiskEngine;
|
| | | CPositionManager PositionTemplateManager;
|
| | | CRangeBreakoutEngine StrategyEngine;
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Expert initialization function |
|
2026-10-06 09:26:49 +00:00 | | | //+------------------------------------------------------------------+
|
2026-10-06 09:36:20 +00:00 | | | int OnInit()
|
| | | {
|
| | | return(INIT_SUCCEEDED);
|
| | | }
|
| | |
|
2026-10-06 09:26:49 +00:00 | | | //+------------------------------------------------------------------+
|
2026-10-06 09:36:20 +00:00 | | | //| 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()
|
2026-10-06 09:26:49 +00:00 | | | {
|
| | | 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()
|
2026-10-06 09:10:16 +00:00 | | | {
|
2026-10-06 09:26:49 +00:00 | | | 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.");
|
| | | }
|
| | | }
|
| | | }
|
| | | }
|
| | | }
|
| | | }
|
2026-10-06 09:10:16 +00:00 | | |
|
2026-10-06 09:26:49 +00:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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);
|
| | | }
|
2026-10-06 09:10:16 +00:00 | | | 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);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|