mirror of
https://github.com/Valenteeno/RangeBars.git
synced 2026-08-02 07:07:43 +00:00
c8e3e30849
Proprietary trading software. © Teenodi Ltd. All rights reserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1058 lines
80 KiB
Plaintext
1058 lines
80 KiB
Plaintext
//+------------------------------------------------------------------+
|
||
//| 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, "");
|
||
}
|
||
//+------------------------------------------------------------------+ |