mirror of
https://github.com/Valenteeno/RangeBars.git
synced 2026-07-27 20:37:43 +00:00
c8e3e30849
Proprietary trading software. © Teenodi Ltd. All rights reserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
494 lines
37 KiB
Plaintext
494 lines
37 KiB
Plaintext
//+------------------------------------------------------------------+
|
||
//| Rangebar3.2.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 // Sets the indicator window height to match buttons
|
||
|
||
// Input parameters
|
||
input int InpBacklogMinutes = 60; // Historical data minutes
|
||
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
|
||
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 the 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
|
||
|
||
// Constants for layout
|
||
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(InpBacklogMinutes <= 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);
|
||
g_lastProcessedTickTime = (TimeCurrent() - InpBacklogMinutes * 60) * 1000;
|
||
|
||
// 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);
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
|
||
g_initialized = true;
|
||
|
||
// Set indicator window properties
|
||
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);
|
||
|
||
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);
|
||
}
|
||
|
||
//+------------------------------------------------------------------+
|
||
//| 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);
|
||
}
|
||
// Symbol exists - clear its historical data
|
||
SymbolSelect(g_customSymbol, false);
|
||
if(!CustomRatesDelete(g_customSymbol, 0, LONG_MAX))
|
||
{
|
||
Print("Error deleting custom symbol rates: ", GetLastError());
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
// 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);
|
||
// 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_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);
|
||
}
|