//+------------------------------------------------------------------+ //| Range_Bars.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright © 2025, ValenTech Trading Solutions" #property version "1.10" #property description "Range Bar NoGap Precision Indicator - Creates gap-free range bars based on tick data" #property icon "\\Images\\Range_Bars.ico" #property indicator_separate_window #property indicator_buffers 0 #property indicator_plots 0 #property indicator_height 25 // Sets the indicator window height to match buttons #define KEY_ENTER 13 // Input parameters input group "Range Bars' Settings" input int InpStartingBarsCount = 500; // Number of bars to start with int InpStartingBars = InpStartingBarsCount; // Number of bars to start with input double InpRangeSizePoints = 100.0; // Range size in points double InpRangeSize = InpRangeSizePoints; // Range size in points input string InpCustomSymbol = "RB"; // Custom symbol prefix int InpBacklogMinutes = 7000000; // Number of minutes for historical tick data int InpTickTolerance = 5; // Tolerance in seconds for tick processing // Add enum for mode selection enum ENUM_VERTICAL_LINES_MODE { HOURS_PERIOD_SEPARATOR = 1, BARS_PERIOD_SEPARATOR }; input group "Vertical Lines Settings" input ENUM_VERTICAL_LINES_MODE InpVerticalLinesMode = BARS_PERIOD_SEPARATOR; // Vertical Lines Mode input int InpVerticalLinesInterval = 360; // Interval Count // Global variables string g_customSymbol; // Full custom symbol name long g_customChartID = 0; // Custom chart ID 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 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 int max_range = 3000; // Maximum point range // Global variables for vertical lines datetime g_lastLineTime = 0; datetime g_trueTime = 0; string g_linePrefix = "VertLine_"; // Global variable to store the previous background color color g_previousBgColor = CLR_NONE; // Global variables for bid line string g_bidLineName = "CustomRangeBidLine"; datetime g_lastBidLineTime = 0; bool InpDrawBidLine = true; // Draw Bid Line color InpBidLineColor = clrRed; // Bid Line Color ENUM_LINE_STYLE InpBidLineStyle = STYLE_SOLID; // Bid Line Style int InpBidLineWidth = 1; // Bid Line Width // 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); } if(InpRangeSize > max_range){ InpRangeSize = max_range; Print("Range Size Adjusted to maximum value of ", max_range, " points"); } if(InpStartingBars > 1000){ InpStartingBars = 1000; Alert("Starting Bars Adjusted to maximum value of 1000 bars"); } // Initialize with dummy tick to trigger sync MqlTick dummy; SymbolInfoTick(_Symbol, dummy); // Calculate start time for historical data datetime currentTime = TimeCurrent(); datetime startTime = currentTime - InpBacklogMinutes * 60; g_lastProcessedTickTime = startTime * 1000; // Copy historical ticks MqlTick historical_ticks[]; ArrayResize(historical_ticks, 0); int copied = CopyTicksRange(_Symbol, historical_ticks, COPY_TICKS_ALL, g_lastProcessedTickTime, currentTime * 1000); // Initialize symbol-related variables g_digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); g_point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); //g_lastProcessedTickTime = (TimeCurrent() - InpBacklogMinutes * 60) * 1000; // Create custom symbol name g_customSymbol = StringFormat("%s%s%s_%d", InpCustomSymbol, _Symbol, DoubleToString(InpRangeSize,2)); // Clean up any existing instance CleanupResources(); // Create custom symbol with cleaned data if(!CreateCustomSymbol()) { Print("Failed to create custom symbol"); //return(INIT_FAILED); } // Open chart for custom symbol SymbolSelect(g_customSymbol, true); g_customChartID = ChartOpen(g_customSymbol, PERIOD_M1); ChartSetInteger(g_customChartID,CHART_AUTOSCROLL,true); ChartSetInteger(g_customChartID,CHART_SHIFT,true); 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(); // Calculate historical data on first run if(!g_initialized) { // Copy historical ticks ArrayFree(historical_ticks); copied = CopyTicksRange(_Symbol, historical_ticks, COPY_TICKS_ALL, g_lastProcessedTickTime, currentTime * 1000); if(copied <= 0) { Print("Error copying historical ticks: ", GetLastError()); RemoveIndicator(); //return(INIT_FAILED); } // Process historical ticks without writing to custom symbol ArrayFree(g_ratesBuffer); // clear previous bars g_barCount = 0; for(int i = 0; i < copied; i++) { if(historical_ticks[i].time_msc > g_lastProcessedTickTime) { if(ProcessHistoricalTick(historical_ticks[i])){ g_lastProcessedTickTime = historical_ticks[i].time_msc; } } } int size = ArraySize(g_ratesBuffer); if(size >= InpStartingBars) { // Delete excess bars ArrayRemove(g_ratesBuffer, 0, size-InpStartingBars); // Write remnant processed bars to custom symbol if(CustomRatesUpdate(g_customSymbol, g_ratesBuffer)) { g_initialized = true; } } else if(size > 0 && size < InpStartingBars){ Alert("Failed to initialize required number of bars! Tick data available not sufficient for chosen range size"); Print("Continuing with ", size, " Bars"); // Write remnant processed bars to custom symbol if(CustomRatesUpdate(g_customSymbol, g_ratesBuffer)) { g_initialized = true; } } if(!g_initialized) { Alert("Failed to initialize required number of bars! Sufficient tick data unavailable. Else, reduce the value of InpStartingBarsCount &/or InpRangeSizePoints"); RemoveIndicator(); //return(INIT_FAILED); } } ChartRedraw(g_customChartID); 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(rates_total <= 0) return(0); // Calculate historical data on first run if(g_initialized) { // Process regular ticks MqlTick current_ticks[]; ArrayResize(current_ticks, 0); int copied = CopyTicksRange(_Symbol, current_ticks, COPY_TICKS_ALL, (g_lastProcessedTickTime - InpTickTolerance * 1000), TimeCurrent() * 1000); if(copied > 0) { for(int i = 0; i < copied; i++) { if(current_ticks[i].time_msc > g_lastProcessedTickTime) { if(ProcessTick(current_ticks[i])){ g_lastProcessedTickTime = current_ticks[i].time_msc; } } } } } // Calculate historical data on first run if(!g_initialized) { // Calculate start time for historical data datetime currentTime = TimeCurrent(); datetime startTime = currentTime - InpBacklogMinutes * 60; g_lastProcessedTickTime = startTime * 1000; // Copy historical ticks MqlTick historical_ticks[]; ArrayResize(historical_ticks, 0); int copied = CopyTicksRange(_Symbol, historical_ticks, COPY_TICKS_ALL, g_lastProcessedTickTime, currentTime * 1000); if(copied <= 0) { Print("Error copying historical ticks: ", GetLastError()); RemoveIndicator(); } // Process historical ticks without writing to custom symbol ArrayFree(g_ratesBuffer); // clear previous bars g_barCount = 0; for(int i = 0; i < copied; i++) { if(historical_ticks[i].time_msc > g_lastProcessedTickTime) { if(ProcessHistoricalTick(historical_ticks[i])){ g_lastProcessedTickTime = historical_ticks[i].time_msc; } } } int size = ArraySize(g_ratesBuffer); if(size >= InpStartingBars) { // Delete excess bars ArrayRemove(g_ratesBuffer, 0, size-InpStartingBars); // Write remnant processed bars to custom symbol if(CustomRatesUpdate(g_customSymbol, g_ratesBuffer)) { g_initialized = true; } } else if(size > 0 && size < InpStartingBars){ Alert("Failed to initialize required number of bars! Tick data available not sufficient for chosen range size"); Print("Continuing with ", size, " Bars"); // Write remnant processed bars to custom symbol if(CustomRatesUpdate(g_customSymbol, g_ratesBuffer)) { g_initialized = true; } } if(!g_initialized) { Alert("Failed to initialize required number of bars! Tick data available not sufficient for chosen range size"); RemoveIndicator(); } } ChartRedraw(g_customChartID); return(rates_total); } //==================================================================// //HISTORICAL TICK //==================================================================// //+------------------------------------------------------------------+ //| Process historical tick | //+------------------------------------------------------------------+ bool ProcessHistoricalTick(const MqlTick &tick) { double price = tick.bid; if(g_isNewBar) { // Initialize new bar InitializeNewHistoricalBar(tick, price); } // Update current bar values UpdateCurrentHistoricalBar(price); // Write true time for period separator g_trueTime = tick.time; // Save for the first time, the last line time for hour mode calculations if(g_lastLineTime == 0) { MqlDateTime time; TimeToStruct(g_trueTime, time); if(time.hour == 0){ g_lastLineTime = g_trueTime; } } // Check thresholds if(price > g_upperThreshold) { g_currentBar.close = g_upperThreshold; g_currentBar.high = g_upperThreshold; CompleteCurrentHistoricalBar(); InitializeNewHistoricalBar(tick, g_upperThreshold); return ProcessHistoricalTick(tick); } else if(price < g_lowerThreshold) { g_currentBar.close = g_lowerThreshold; g_currentBar.low = g_lowerThreshold; CompleteCurrentHistoricalBar(); InitializeNewHistoricalBar(tick, g_lowerThreshold); return ProcessHistoricalTick(tick); } /*/ Update custom symbol//???? MqlRates tempBuffer[1]; tempBuffer[0] = g_currentBar; CustomRatesUpdate(g_customSymbol, tempBuffer);///???*/ return true; } //+------------------------------------------------------------------+ //| Initialize new bar | //+------------------------------------------------------------------+ void InitializeNewHistoricalBar(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 UpdateCurrentHistoricalBar(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 CompleteCurrentHistoricalBar() { g_lastBarTime = g_currentBar.time; if(ArraySize(g_ratesBuffer) > 0) { g_ratesBuffer[ArraySize(g_ratesBuffer) - 1] = g_currentBar; } // Check if we need to draw a vertical line CheckAndDrawVerticalLine(g_trueTime, g_currentBar.time); g_isNewBar = true; } //==================================================================// //INDIVIDUAL TICK //==================================================================// //+------------------------------------------------------------------+ //| Process individual tick | //+------------------------------------------------------------------+ bool ProcessTick(const MqlTick &tick) { double price = tick.bid; // Draw bid line if enabled if(InpDrawBidLine) { // Only redraw if time has changed to avoid excessive drawing if(tick.time > g_lastBidLineTime) { DrawBidLine(price); g_lastBidLineTime = tick.time; } } if(g_isNewBar) { // Initialize new bar InitializeNewBar(tick, price); } // Update current bar values UpdateCurrentBar(price); // Write true time for period separator g_trueTime = tick.time; // Save for the first time, the last line time for hour mode calculations if(g_lastLineTime == 0) { MqlDateTime time; TimeToStruct(g_trueTime, time); if(time.hour == 0){ g_lastLineTime = g_trueTime; } } // Check thresholds if(price > g_upperThreshold) { g_currentBar.close = g_upperThreshold; g_currentBar.high = g_upperThreshold; CompleteCurrentBar(); InitializeNewBar(tick, g_upperThreshold); return ProcessTick(tick); } else if(price < g_lowerThreshold) { g_currentBar.close = g_lowerThreshold; g_currentBar.low = 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); } // Check if we need to draw a vertical line CheckAndDrawVerticalLine(g_trueTime, g_currentBar.time); g_isNewBar = true; } //+------------------------------------------------------------------+ //| Create custom symbol with clean data | //+------------------------------------------------------------------+ 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 OHLC data MqlRates empty_rates[]; //CustomRatesUpdate(g_customSymbol, empty_rates); // Delete rates CustomRatesDelete(g_customSymbol, 0, LONG_MAX); } else { // Create new custom symbol if(!CustomSymbolCreate(g_customSymbol, "RangeBars", _Symbol)) { Print("Error creating custom symbol: ", GetLastError()); return false; } } SymbolSelect(g_customSymbol, true); return true; } //+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { RemoveIndicator(); } //+------------------------------------------------------------------+ //| 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); // Clear OHLC data MqlRates empty_rates[]; CustomRatesUpdate(g_customSymbol, empty_rates); 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_KEYDOWN && (int)lparam == KEY_ENTER) || (id == CHARTEVENT_OBJECT_ENDEDIT && sparam == "ValueEdit")) { string text = ObjectGetString(0, "ValueEdit", OBJPROP_TEXT); double oldValue = InpRangeSize; double newValue = StringToDouble(text); if(newValue > 0 && newValue <= max_range) { InpRangeSize = newValue; } else{ Alert("Ensure value change is greater than 0 and not greater than max_range"); } ResetCustom(); } if(id == CHARTEVENT_CHART_CHANGE) { CreateCloseButton(); } // Check for chart background colour change to set up of inverse colour for vertical lines long bgColorValue = 0; ChartGetInteger(g_customChartID, CHART_COLOR_BACKGROUND, 0, bgColorValue); color currentBgColor = (color)bgColorValue; // If this is the first check, initialize previous color and return false if(g_previousBgColor == CLR_NONE) { g_previousBgColor = currentBgColor; return; } if(currentBgColor != g_previousBgColor){ g_previousBgColor = currentBgColor; ResetCustom(); } } //+------------------------------------------------------------------+ //| 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);// Hide the line's label on the horizontal axis // Update background width ObjectSetInteger(0, "Background", OBJPROP_XSIZE, chartWidth); ChartRedraw(0); } //+------------------------------------------------------------------+ //| Reset Custom Symbol & params | //+------------------------------------------------------------------+ void ResetCustom() { g_customChartID = 0; // Custom chart ID ZeroMemory(g_currentBar); // Current bar being formed g_isNewBar = true; // Flag for new bar initialization g_initialized = false; // Initialization flag g_barCount = 0; // Counter for bar numbering ZeroMemory(g_ratesBuffer); // Buffer for collecting rates before update g_lastBarTime = 0; // Time of last completed bar g_lastProcessedTickTime = 0; // Time of last processed tick g_lastLineTime = 0; g_trueTime = 0; // Clean up any existing instance CleanupResources(); // Create custom symbol with cleaned data if(!CreateCustomSymbol()) { Print("Failed to create custom symbol"); } // 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()); ResetCustom(); } if(!(g_previousBgColor == CLR_NONE)){ ChartSetInteger(g_customChartID, CHART_COLOR_BACKGROUND, g_previousBgColor); } // Calculate historical data on first run if(g_initialized) { // Process regular ticks MqlTick current_ticks[]; ArrayResize(current_ticks, 0); int copied = CopyTicksRange(_Symbol, current_ticks, COPY_TICKS_ALL, (g_lastProcessedTickTime - InpTickTolerance * 1000), TimeCurrent() * 1000); if(copied > 0) { for(int i = 0; i < copied; i++) { if(current_ticks[i].time_msc > g_lastProcessedTickTime) { if(ProcessTick(current_ticks[i])){ g_lastProcessedTickTime = current_ticks[i].time_msc; } } } } } // Calculate historical data on first run if(!g_initialized) { // Calculate start time for historical data datetime currentTime = TimeCurrent(); datetime startTime = currentTime - InpBacklogMinutes * 60; g_lastProcessedTickTime = startTime * 1000; // Copy historical ticks MqlTick historical_ticks[]; ArrayResize(historical_ticks, 0); int copied = CopyTicksRange(_Symbol, historical_ticks, COPY_TICKS_ALL, g_lastProcessedTickTime, currentTime * 1000); if(copied <= 0) { Print("Error copying historical ticks: ", GetLastError()); RemoveIndicator(); } // Process historical ticks without writing to custom symbol ArrayFree(g_ratesBuffer); // clear previous bars g_barCount = 0; for(int i = 0; i < copied; i++) { if(historical_ticks[i].time_msc > g_lastProcessedTickTime) { if(ProcessHistoricalTick(historical_ticks[i])){ g_lastProcessedTickTime = historical_ticks[i].time_msc; } } } int size = ArraySize(g_ratesBuffer); if(size >= InpStartingBars) { // Delete excess bars ArrayRemove(g_ratesBuffer, 0, size-InpStartingBars); // Write remnant processed bars to custom symbol if(CustomRatesUpdate(g_customSymbol, g_ratesBuffer)) { g_initialized = true; } } else if(size > 0 && size < InpStartingBars){ Alert("Failed to initialize required number of bars! Tick data available not sufficient for chosen range size"); Print("Continuing with ", size, " Bars"); // Write remnant processed bars to custom symbol if(CustomRatesUpdate(g_customSymbol, g_ratesBuffer)) { g_initialized = true; } } if(!g_initialized) { Alert("Failed to initialize required number of bars! Tick data available not sufficient for chosen range size"); RemoveIndicator(); } } ChartRedraw(g_customChartID); } //+------------------------------------------------------------------+ //| Remove indicator completely | //+------------------------------------------------------------------+ void RemoveIndicator() { //CleanupResources(); ObjectDelete(0, "Background"); ObjectDelete(0, "OpenChartButton"); ObjectDelete(0, "ValueEdit"); ObjectDelete(0, "CloseButton"); // Remove the indicator int window = ChartWindowFind(0,"Range Bar NoGap"); IndicatorRelease(ChartIndicatorGet(0,window, "Range Bar NoGap")); ChartIndicatorDelete(0,window,"Range Bar NoGap"); ChartRedraw(); } //+------------------------------------------------------------------+ //| Check if vertical line should be drawn and draw it if needed | //+------------------------------------------------------------------+ void CheckAndDrawVerticalLine(datetime trueTime, datetime barTime) { bool shouldDrawLine = false; switch(InpVerticalLinesMode) { case HOURS_PERIOD_SEPARATOR: // Calculate hours since first bar shouldDrawLine = (g_lastLineTime > 0) && ((trueTime-g_lastLineTime) > InpVerticalLinesInterval*3600); break; case BARS_PERIOD_SEPARATOR: shouldDrawLine = (g_barCount % InpVerticalLinesInterval == 0); break; } if(shouldDrawLine) { g_lastLineTime = trueTime; CustomRatesUpdate(g_customSymbol, g_ratesBuffer); DrawVerticalLine(barTime); ChartRedraw(g_customChartID); } } //+------------------------------------------------------------------+ //| Draw a vertical line on the custom chart | //+------------------------------------------------------------------+ void DrawVerticalLine(datetime lineTime) { if(g_customChartID == 0) return; // Create a unique name for the line string lineName = g_linePrefix + TimeToString(lineTime); // Get line color (inverse of background if requested) color lineColor = GetInverseColor(g_customChartID); // Draw the vertical line if(!ObjectCreate(g_customChartID, lineName, OBJ_VLINE, 0, lineTime, 0)) { Print("Failed to create vertical line: ", GetLastError()); return; } // Set line properties ObjectSetInteger(g_customChartID, lineName, OBJPROP_COLOR, lineColor); ObjectSetInteger(g_customChartID, lineName, OBJPROP_STYLE, STYLE_DASH); ObjectSetInteger(g_customChartID, lineName, OBJPROP_WIDTH, 1); ObjectSetInteger(g_customChartID, lineName, OBJPROP_BACK, false); ObjectSetInteger(g_customChartID, lineName, OBJPROP_SELECTABLE, false); ObjectSetInteger(g_customChartID, lineName, OBJPROP_SELECTED, false); ObjectSetInteger(g_customChartID, lineName, OBJPROP_HIDDEN, true); ObjectSetInteger(g_customChartID, lineName, OBJPROP_ZORDER, 0); } //+------------------------------------------------------------------+ //| Get inverse color of chart background | //+------------------------------------------------------------------+ color GetInverseColor(long chartID) { // Get chart background color long bgColorValue = 0; ChartGetInteger(chartID, CHART_COLOR_BACKGROUND, 0, bgColorValue); color bgColor = (color)bgColorValue; // Extract RGB components int red = bgColor & 0xFF; int green = (bgColor >> 8) & 0xFF; int blue = (bgColor >> 16) & 0xFF; // Invert RGB components red = 255 - red; green = 255 - green; blue = 255 - blue; // Combine inverted components color inverseColor = (color)((blue << 16) | (green << 8) | red); return inverseColor; } //+------------------------------------------------------------------+ //| Draw a bid line on the custom chart | //+------------------------------------------------------------------+ void DrawBidLine(double price) { if(g_customChartID == 0) return; // Delete existing line ObjectDelete(g_customChartID, g_bidLineName); // Create a new horizontal line if(!ObjectCreate(g_customChartID, g_bidLineName, OBJ_HLINE, 0, 0, price)) { Print("Failed to create bid line: ", GetLastError()); return; } // Set line properties ObjectSetInteger(g_customChartID, g_bidLineName, OBJPROP_COLOR, InpBidLineColor); ObjectSetInteger(g_customChartID, g_bidLineName, OBJPROP_STYLE, InpBidLineStyle); ObjectSetInteger(g_customChartID, g_bidLineName, OBJPROP_WIDTH, InpBidLineWidth); ObjectSetInteger(g_customChartID, g_bidLineName, OBJPROP_BACK, false); ObjectSetInteger(g_customChartID, g_bidLineName, OBJPROP_SELECTABLE, false); ObjectSetInteger(g_customChartID, g_bidLineName, OBJPROP_SELECTED, false); ObjectSetInteger(g_customChartID, g_bidLineName, OBJPROP_HIDDEN, false); // Hide the line's label on the price axis ObjectSetString(g_customChartID, g_bidLineName, OBJPROP_TEXT, ""); } //+------------------------------------------------------------------+