//+------------------------------------------------------------------+ //| RangeBar3.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright Valenteeno 2025" #property link "" #property version "1.00" #property indicator_separate_window #property indicator_buffers 0 #property indicator_plots 0 #property indicator_height 25 // Input parameters input int InpStartingBars = 100; // Number of bars to start with input double InpRangeSizePoints = 10.0; // Range size in points input string InpCustomSymbol = "RB"; // Custom symbol prefix input int InpTickTolerance = 5; // Tolerance in seconds for tick processing // Global variables int InpBacklogMinutes = 525600; // One year in minutes for historical data string g_customSymbol; // Full custom symbol name long g_customChartID = 0; // Custom chart ID datetime g_startTime; // Starting time for processing MqlRates g_currentBar; // Current bar being formed bool g_isNewBar = true; // Flag for new bar initialization int g_digits; // Symbol digits double g_point; // Symbol point size bool g_initialized = false; // Initialization flag int g_barCount = 0; // Counter for bar numbering MqlRates g_ratesBuffer[]; // Buffer for collecting rates before update datetime g_lastBarTime = 0; // Time of last completed bar long g_lastProcessedTickTime = 0; // Time of last processed tick in milliseconds double g_currentHigh; // Current bar's high price double g_currentLow; // Current bar's low price double g_upperThreshold; // Current bar's upper threshold double g_lowerThreshold; // Current bar's lower threshold double InpRangeSize = InpRangeSizePoints; // Range size in points MqlRates g_historicalBars[]; // Buffer for historical bars calculation // Layout constants const int PADDING = 5; const int BUTTON_HEIGHT = 20; const int VALUE_EDIT_WIDTH = 50; const int M61_BUTTON_WIDTH = 40; const int CLOSE_BUTTON_WIDTH = 20; const int LABEL_WIDTH = 70; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { // Validate inputs if(InpStartingBars <= 0 || InpRangeSize <= 0 || InpTickTolerance <= 0 || StringLen(InpCustomSymbol) == 0) { Print("Error: Invalid input parameters"); return(INIT_PARAMETERS_INCORRECT); } // Initialize symbol-related variables g_digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); g_point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); // Create custom symbol name g_customSymbol = StringFormat("%s%s_%d", InpCustomSymbol, _Symbol); // Clean up any existing instance CleanupResources(); // Create new custom symbol if(!CreateCustomSymbol()) { Print("Failed to create custom symbol"); return(INIT_FAILED); } // Initialize historical data if(!InitializeHistoricalData()) { Print("Failed to initialize historical data"); return(INIT_FAILED); } // Open chart for custom symbol SymbolSelect(g_customSymbol, true); g_customChartID = ChartOpen(g_customSymbol, PERIOD_M1); if(g_customChartID == 0) { Print("Failed to open custom symbol chart: ", GetLastError()); CleanupResources(); return(INIT_FAILED); } IndicatorSetString(INDICATOR_SHORTNAME, "Range Bar NoGap"); // Create the background ObjectCreate(0, "Background", OBJ_RECTANGLE_LABEL, 1, 0, 0); ObjectSetInteger(0, "Background", OBJPROP_XDISTANCE, 0); ObjectSetInteger(0, "Background", OBJPROP_YDISTANCE, 0); ObjectSetInteger(0, "Background", OBJPROP_XSIZE, ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)); ObjectSetInteger(0, "Background", OBJPROP_YSIZE, 25); ObjectSetInteger(0, "Background", OBJPROP_BGCOLOR, clrBlack); ObjectSetInteger(0, "Background", OBJPROP_BORDER_TYPE, BORDER_FLAT); ObjectSetInteger(0, "Background", OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, "Background", OBJPROP_HIDDEN, true); ObjectSetInteger(0, "Background", OBJPROP_SELECTABLE, false); // Create "Range Bar" label ObjectCreate(0, "RangeBarLabel", OBJ_LABEL, 1, 0, 0); ObjectSetString(0, "RangeBarLabel", OBJPROP_TEXT, "Range Bars"); ObjectSetInteger(0, "RangeBarLabel", OBJPROP_XDISTANCE, PADDING); ObjectSetInteger(0, "RangeBarLabel", OBJPROP_YDISTANCE, 5); ObjectSetInteger(0, "RangeBarLabel", OBJPROP_COLOR, clrWhiteSmoke); ObjectSetInteger(0, "RangeBarLabel", OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, "RangeBarLabel", OBJPROP_FONTSIZE, 8); ObjectSetInteger(0, "RangeBarLabel", OBJPROP_HIDDEN, true); ObjectSetInteger(0, "RangeBarLabel", OBJPROP_SELECTABLE, false); // Create edit box (immediately after label) ObjectCreate(0, "ValueEdit", OBJ_EDIT, 1, 0, 0); ObjectSetString(0, "ValueEdit", OBJPROP_TEXT, "100"); ObjectSetInteger(0, "ValueEdit", OBJPROP_XDISTANCE, LABEL_WIDTH + PADDING); ObjectSetInteger(0, "ValueEdit", OBJPROP_YDISTANCE, 3); ObjectSetInteger(0, "ValueEdit", OBJPROP_XSIZE, VALUE_EDIT_WIDTH); ObjectSetInteger(0, "ValueEdit", OBJPROP_YSIZE, BUTTON_HEIGHT); ObjectSetInteger(0, "ValueEdit", OBJPROP_BGCOLOR, clrBlack); ObjectSetInteger(0, "ValueEdit", OBJPROP_COLOR, clrLimeGreen); ObjectSetInteger(0, "ValueEdit", OBJPROP_BORDER_COLOR, clrDarkGray); ObjectSetInteger(0, "ValueEdit", OBJPROP_ALIGN, ALIGN_CENTER); ObjectSetInteger(0, "ValueEdit", OBJPROP_HIDDEN, true); ObjectSetInteger(0, "ValueEdit", OBJPROP_SELECTABLE, false); // Create M61 button (immediately after edit box) ObjectCreate(0, "OpenChartButton", OBJ_BUTTON, 1, 0, 0); ObjectSetString(0, "OpenChartButton", OBJPROP_TEXT, "GTC"); ObjectSetInteger(0, "OpenChartButton", OBJPROP_XDISTANCE, LABEL_WIDTH + VALUE_EDIT_WIDTH + PADDING * 2); ObjectSetInteger(0, "OpenChartButton", OBJPROP_YDISTANCE, 3); ObjectSetInteger(0, "OpenChartButton", OBJPROP_XSIZE, M61_BUTTON_WIDTH); ObjectSetInteger(0, "OpenChartButton", OBJPROP_YSIZE, BUTTON_HEIGHT); ObjectSetInteger(0, "OpenChartButton", OBJPROP_COLOR, clrLimeGreen); ObjectSetInteger(0, "OpenChartButton", OBJPROP_BGCOLOR, clrBlack); ObjectSetInteger(0, "OpenChartButton", OBJPROP_BORDER_COLOR, clrDarkGray); ObjectSetInteger(0, "OpenChartButton", OBJPROP_STATE, false); ObjectSetInteger(0, "OpenChartButton", OBJPROP_HIDDEN, true); ObjectSetInteger(0, "OpenChartButton", OBJPROP_SELECTABLE, false); // Create close button CreateCloseButton(); ChartRedraw(); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(!g_initialized || rates_total <= 0) return(0); // Calculate start time for tick copying long startTime = g_lastProcessedTickTime - (InpTickTolerance * 1000); // Copy ticks MqlTick ticks[]; int copied = CopyTicksRange(_Symbol, ticks, COPY_TICKS_ALL, startTime, 0); if(copied <= 0) { Print("Error copying ticks: ", GetLastError()); return(0); } // Check if we have complete tick data if(ticks[0].time_msc > g_lastProcessedTickTime) { Print("Incomplete tick data"); return(0); } // Find first unprocessed tick int firstUnprocessedIndex = 0; for(int i = 0; i < copied; i++) { if(ticks[i].time_msc > g_lastProcessedTickTime) { firstUnprocessedIndex = i; break; } } // Process ticks for(int i = firstUnprocessedIndex; i < copied; i++) { ProcessTick(ticks[i]); g_lastProcessedTickTime = ticks[i].time_msc; } // Update chart //if chart is not found, no need. We'd need a push button to goto chart again ChartRedraw(g_customChartID); // Update close button position on each calculation //CreateCloseButton(); return(rates_total); } //+------------------------------------------------------------------+ //| Initialize historical data | //+------------------------------------------------------------------+ bool InitializeHistoricalData() { datetime startTime = TimeCurrent() - InpBacklogMinutes * 60; MqlTick ticks[]; int copied = CopyTicksRange(_Symbol, ticks, COPY_TICKS_ALL, startTime * 1000, TimeCurrent() * 1000); if(copied <= 0) { Print("Error copying historical ticks: ", GetLastError()); return false; } // Calculate historical bars without writing to custom symbol ArrayResize(g_historicalBars, 0); MqlRates tempBar; bool isNewBar = true; for(int i = 0; i < copied; i++) { double price = ticks[i].bid; if(isNewBar) { // Initialize new bar tempBar.time = (datetime)(MathFloor((double)ticks[i].time / 60.0) * 60); tempBar.open = price; tempBar.high = price; tempBar.low = price; tempBar.close = price; tempBar.tick_volume = 1; tempBar.real_volume = 1; tempBar.spread = 0; g_currentHigh = price; g_currentLow = price; g_lowerThreshold = g_currentHigh - InpRangeSize * g_point; g_upperThreshold = g_currentLow + InpRangeSize * g_point; isNewBar = false; } // Update bar values if(price > g_currentHigh) { g_currentHigh = price; g_lowerThreshold = g_currentHigh - InpRangeSize * g_point; } if(price < g_currentLow) { g_currentLow = price; g_upperThreshold = g_currentLow + InpRangeSize * g_point; } tempBar.high = g_currentHigh; tempBar.low = g_currentLow; tempBar.close = price; tempBar.tick_volume++; // Check if bar should be completed if(price >= g_upperThreshold || price <= g_lowerThreshold) { // Add completed bar to historical buffer int size = ArraySize(g_historicalBars); ArrayResize(g_historicalBars, size + 1); g_historicalBars[size] = tempBar; // Check if we have enough bars if(size + 1 >= InpStartingBars) { // Write historical bars to custom symbol CustomRatesUpdate(g_customSymbol, g_historicalBars); g_initialized = true; g_lastProcessedTickTime = ticks[i].time_msc; return true; } isNewBar = true; } } return false; } //+------------------------------------------------------------------+ //| Process individual tick | //+------------------------------------------------------------------+ bool ProcessTick(const MqlTick &tick) { double price = tick.bid; if(g_isNewBar) { // Initialize new bar InitializeNewBar(tick, price); } // Update current bar values UpdateCurrentBar(price); // Check thresholds if(price >= g_upperThreshold) { CompleteCurrentBar(); InitializeNewBar(tick, g_upperThreshold); return ProcessTick(tick); } else if(price <= g_lowerThreshold) { CompleteCurrentBar(); InitializeNewBar(tick, g_lowerThreshold); return ProcessTick(tick); } // Update custom symbol//???? MqlRates tempBuffer[1]; tempBuffer[0] = g_currentBar; CustomRatesUpdate(g_customSymbol, tempBuffer); return true; } //+------------------------------------------------------------------+ //| Initialize new bar | //+------------------------------------------------------------------+ void InitializeNewBar(const MqlTick &tick, double price) { datetime tickTime = tick.time; if(g_barCount == 0) { g_currentBar.time = (datetime)(MathFloor((double)tickTime / 60.0) * 60); } else { g_currentBar.time = g_lastBarTime + 60; } g_currentBar.open = price; g_currentBar.high = price; g_currentBar.low = price; g_currentBar.close = price; g_currentBar.tick_volume = 1; g_currentBar.real_volume = 1; g_currentBar.spread = 0; g_currentHigh = price; g_currentLow = price; g_lowerThreshold = g_currentHigh - InpRangeSize * g_point; g_upperThreshold = g_currentLow + InpRangeSize * g_point; g_isNewBar = false; g_barCount++; ArrayResize(g_ratesBuffer, ArraySize(g_ratesBuffer) + 1); g_ratesBuffer[ArraySize(g_ratesBuffer) - 1] = g_currentBar; } //+------------------------------------------------------------------+ //| Update current bar values | //+------------------------------------------------------------------+ void UpdateCurrentBar(double price) { if(price > g_currentHigh) { g_currentHigh = price; g_lowerThreshold = g_currentHigh - InpRangeSize * g_point; } if(price < g_currentLow) { g_currentLow = price; g_upperThreshold = g_currentLow + InpRangeSize * g_point; } g_currentBar.high = g_currentHigh; g_currentBar.low = g_currentLow; g_currentBar.close = price; g_currentBar.tick_volume++; } //+------------------------------------------------------------------+ //| Complete current bar | //+------------------------------------------------------------------+ void CompleteCurrentBar() { g_lastBarTime = g_currentBar.time; if(ArraySize(g_ratesBuffer) > 0) { g_ratesBuffer[ArraySize(g_ratesBuffer) - 1] = g_currentBar; CustomRatesUpdate(g_customSymbol, g_ratesBuffer); } g_isNewBar = true; } //+------------------------------------------------------------------+ //| Create custom symbol | //+------------------------------------------------------------------+ bool CreateCustomSymbol() { bool custom = true; if(SymbolExist(g_customSymbol, custom)) { // Close all charts with this symbol long chartId = ChartFirst(); while(chartId >= 0) { if(ChartSymbol(chartId) == g_customSymbol) ChartClose(chartId); chartId = ChartNext(chartId); } // Clear historical data before deleting datetime startDate = 0; datetime endDate = TimeCurrent() + 365 * 24 * 60 * 60; CustomRatesDelete(g_customSymbol, startDate, endDate); // Remove symbol and recreate SymbolSelect(g_customSymbol, false); CustomSymbolDelete(g_customSymbol); } // Create new custom symbol if(!CustomSymbolCreate(g_customSymbol, "RangeBars", _Symbol)) { Print("Error creating custom symbol: ", GetLastError()); return false; } return true; } //+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { CleanupResources(); ObjectDelete(0, "Background"); ObjectDelete(0, "OpenChartButton"); ObjectDelete(0, "ValueEdit"); ObjectDelete(0, "CloseButton"); ChartRedraw(); } //+------------------------------------------------------------------+ //| Cleanup resources | //+------------------------------------------------------------------+ void CleanupResources() { // Close all charts with this symbol long chartId = ChartFirst(); while(chartId >= 0) { if(ChartSymbol(chartId) == g_customSymbol) ChartClose(chartId); chartId = ChartNext(chartId); } // Remove symbol from Market Watch and delete bool custom = true; if(SymbolExist(g_customSymbol, custom)) { SymbolSelect(g_customSymbol, false); CustomRatesDelete(g_customSymbol, 0, LONG_MAX); CustomSymbolDelete(g_customSymbol); } // Reset variables g_customChartID = 0; g_initialized = false; g_barCount = 0; g_lastBarTime = 0; ArrayFree(g_ratesBuffer); } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { // Handle button clicks if(id == CHARTEVENT_OBJECT_CLICK) { if(sparam == "OpenChartButton") { ChartSetInteger(g_customChartID, CHART_BRING_TO_TOP, true); // Find chart with this symbol bool charton = false; long chartId = ChartFirst(); while(chartId >= 0) { if(ChartSymbol(chartId) == g_customSymbol){ charton = true; break; } chartId = ChartNext(chartId); } if(!charton){ g_customChartID = ChartOpen(g_customSymbol, PERIOD_M1); } ChartSetInteger(g_customChartID, CHART_BRING_TO_TOP, true); // ObjectSetInteger(0, "OpenChartButton", OBJPROP_STATE, false); ChartRedraw(); } else if(sparam == "CloseButton") { // Remove the indicator int window = ChartWindowFind(0,"Range Bar NoGap"); IndicatorRelease(ChartIndicatorGet(0,window, "Range Bar NoGap")); ChartIndicatorDelete(0,window,"Range Bar NoGap"); } } // Handle edit box input if(id == CHARTEVENT_OBJECT_ENDEDIT && sparam == "ValueEdit") { string text = ObjectGetString(0, "ValueEdit", OBJPROP_TEXT); double oldValue = InpRangeSize; double newValue = StringToDouble(text); if(newValue > 0) { InpRangeSize = newValue; } } if(id == CHARTEVENT_CHART_CHANGE) { CreateCloseButton(); } } //+------------------------------------------------------------------+ //| Create or update close button | //+------------------------------------------------------------------+ void CreateCloseButton() { int chartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); // Always recreate the close button to ensure proper positioning if(ObjectFind(0, "CloseButton") >= 0) ObjectDelete(0, "CloseButton"); ObjectCreate(0, "CloseButton", OBJ_BUTTON, 1, 0, 0); ObjectSetString(0, "CloseButton", OBJPROP_TEXT, "×"); ObjectSetInteger(0, "CloseButton", OBJPROP_XDISTANCE, chartWidth - CLOSE_BUTTON_WIDTH - PADDING); ObjectSetInteger(0, "CloseButton", OBJPROP_YDISTANCE, 3); ObjectSetInteger(0, "CloseButton", OBJPROP_XSIZE, CLOSE_BUTTON_WIDTH); ObjectSetInteger(0, "CloseButton", OBJPROP_YSIZE, BUTTON_HEIGHT); ObjectSetInteger(0, "CloseButton", OBJPROP_COLOR, clrWhite); ObjectSetInteger(0, "CloseButton", OBJPROP_BGCOLOR, clrRed); ObjectSetInteger(0, "CloseButton", OBJPROP_BORDER_COLOR, clrDarkGray); ObjectSetInteger(0, "CloseButton", OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, "CloseButton", OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER); ObjectSetInteger(0, "CloseButton", OBJPROP_HIDDEN, false); ObjectSetInteger(0, "CloseButton", OBJPROP_SELECTABLE, false); // Update background width ObjectSetInteger(0, "Background", OBJPROP_XSIZE, chartWidth); ChartRedraw(0); }