Files
Valentine Chibuike Ozoigboanugo c8e3e30849 Initial commit: RangeBars
Proprietary trading software. © Teenodi Ltd. All rights reserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:32:09 +02:00

581 lines
42 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//+------------------------------------------------------------------+
//| 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);
}