Specifiche
calcVolume(void) { //--- MqlRates rates[]; if(CopyRates(_Symbol, PERIOD_CURRENT, startTime, endTime, rates) > 0) { double rangeHigh = rates[0].high, rangeLow = rates[0].low; int count = MathAbs(iBarShift(_Symbol, PERIOD_CURRENT, startTime) - iBarShift(_Symbol, PERIOD_CURRENT, endTime)) + 1; //---VERTICAL PRICE RANGE for(int b = 0; b < ArraySize(rates); b++) { if(rates[b].high > rangeHigh) rangeHigh = rates[b].high; if(rates[b].low < rangeLow) rangeLow = rates[b].low; } //--- SET ANALYSIS RANGE RECTANGLE PROPERLY ObjectSetDouble(0, _VOLUME_RANGE, OBJPROP_PRICE, 0, rangeHigh); ObjectSetDouble(0, _VOLUME_RANGE, OBJPROP_PRICE, 1, rangeLow); ChartRedraw(); //--- ADAPTIVE BIN CONSTRUCTION int numberOfBins = int(count / 4); numberOfBins = MathMax(5, numberOfBins); double step = (rangeHigh - rangeLow) / numberOfBins; st_priceVolume bins[]; ArrayResize(bins, numberOfBins); double binHigh = EMPTY_VALUE, binLow = EMPTY_VALUE; bool useRealVol = (rates[0].real_volume > 0); //--- VOLUME ACCUMULATION USING SELECTED PRICE MODEL for(int w = 0; w < numberOfBins; w++) { binLow = rangeLow + step * w; binHigh = binLow + step; for(int c = 0; c < ArraySize(rates); c++) { if(getPrice(rates[c]) >= binLow && getPrice(rates[c]) <= binHigh) { bins[w].volume += (useRealVol) ? rates[c].real_volume : rates[c].tick_volume; bins[w].price = NormalizeDouble(getPrice(rates[c]), _Digits); } } } //--- VOLUME EXTREMES long maxVol = 0, minVol = LONG_MAX; for(int b = 0; b < ArraySize(bins); b++) { if(bins[b].volume > maxVol) maxVol = bins[b].volume; if(bins[b].volume < minVol) minVol = bins[b].volume; } //--- PROFILE SCALING int extendBars = 0; int maxProfileLength = int(count * 0.4); int minProfileLength = (int)MathRound(maxProfileLength * 0.05); minProfileLength = MathMax(2, minProfileLength);// CLAMP color clr = clrRed; double dominance = EMPTY_VALUE; //--- PROFILE RENDERING bool pocDrawn = false; for(int p = 0; p < ArraySize(bins); p++) { binLow = rangeLow + step * p; binHigh = binLow + step; extendBars = minProfileLength + (int)MathRound(normalizeMinMax(bins[p].volume, maxVol, minVol) * (maxProfileLength - minProfileLength)); dominance = (double)extendBars / maxProfileLength; clr = (dominance >= 0.8) ? clrBlue : (dominance >= 0.6) ? clrPurple : (dominance >= 0.5) ? clrBrown : (dominance >= 0.4) ? clrLime : (dominance >= 0.2) ? clrTeal : (dominance >= 0.15) ? clrBlueViolet : clrGray; createRect(_PROFILE + (string)p, startTime, binHigh, startTime + (PeriodSeconds()*extendBars), binLow, clr); if(bins[p].volume == maxVol && !pocDrawn) { //--- DRAW MAXIMUM VOLUME'S POC createHLine(_PROFILE_POC + (string)p, NormalizeDouble(bins[p].price, _Digits), clr, "POC"); pocDrawn = true; } } ChartRedraw(); } }
Initialization and Cleanup
Having established the supporting helper functions, we now implement the standard MQL5 event handlers, beginning with OnInit() and OnDeinit(). During initialization, the indicator creates the ON/OFF menu button, providing users with a convenient way to enable or disable the interactive Crosshair Volume Profile. Conversely, when the indicator is removed, the cleanup routine is executed to delete all indicator-generated chart objects and restore the chart's default interaction settings, ensuring the chart is left in a clean and consistent state.//+------------------------------------------------------------------+ //| INITIALIZATION FUNCTION | //+------------------------------------------------------------------+ int OnInit() { //--- createMenuButton(); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| DEINITIALIZATION FUNCTION | //+------------------------------------------------------------------+ void OnDeinit(const int32_t reason) { //--- clearChart(); }
Iteration Engine (OnCalculate)
Unlike conventional indicators that recalculate whenever new market data arrives, this indicator is designed around user interaction rather than incoming candles. For this reason, the OnCalculate() function is intentionally left empty, simply returning rates_total to satisfy the standard MQL5 indicator interface. All Volume Profile computations are instead triggered by Crosshair-driven chart events, ensuring the indicator updates only when the user selects or modifies an analysis range. This event-driven approach aligns with the interactive nature of the indicator while avoiding unnecessary recalculations on every incoming tick or bar.
//+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int32_t rates_total, const int32_t 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 int32_t &spread[]) { //--- EMPTY return(rates_total); }
Interactive Engine
At the heart of the indicator is the OnChartEvent() function, which handles all chart events and drives the interactive behavior of the Crosshair Volume Profile. Rather than relying on incoming market data, the indicator responds to user actions such as button clicks, mouse movement, keyboard input, and right-click operations. This event-driven architecture enables users to interactively define an analysis range and generate the Volume Profile only when required.
//+------------------------------------------------------------------+ //| CHARTEVENT FUNCTION | //+------------------------------------------------------------------+ void OnChartEvent(const int32_t id, const long &lparam, const double &dparam, const string &sparam) { //--- st_priceVolume tp; static bool isDragging = false; int key = (int)lparam; static ulong lastClick = 0; switch(id) { case CHARTEVENT_OBJECT_CLICK: //--- TOGGLE MENU BUTTON if(sparam == _MENU_BUTTON) { toggleMenuButton(); } break; case CHARTEVENT_KEYDOWN: //--- PROFILE DELETION: "C" DOUBLE-CLICK if(key == 67) { ulong now = GetTickCount(); bool doubleClick = (now - lastClick <= 300); lastClick = now; if(doubleClick && ObjectFind(0, _VOLUME_RANGE) > -1) { ObjectsDeleteAll(0, _PROFILE); ObjectDelete(0, _VOLUME_RANGE); if(ObjectGetString(0, _MENU_BUTTON, OBJPROP_TEXT) == "ON") { toggleMenuButton(); } ChartRedraw(); } } break; case CHARTEVENT_MOUSE_MOVE: if(isDragging) { //--- UPDATE OBJECTS ALONG THE CURSOR POSITION USING getCursorPriceTime() tp = getCursorPriceTime((short)lparam, (short)dparam); tp.time = (tp.time >= iTime(_Symbol, PERIOD_CURRENT, 0)) ? iTime(_Symbol, PERIOD_CURRENT, 1) : tp.time; ObjectMove(0, _V_END_LINE, 0, tp.time, 0); ObjectMove(0, _H_LINE, 0, 0, tp.price); ObjectMove(0, _VOLUME_RANGE, 1, tp.time, tp.price); endTime = tp.time; ChartRedraw(); } //--- RIGHT-CLICK if(((uchar)sparam & MOUSE_RIGHT) != 0) { tp = getCursorPriceTime((short)lparam, (short)dparam); if(!isDragging) { //--- CREATE CROSSHAIR isDragging = true; color clr = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND); ObjectsDeleteAll(0, _PROFILE); ChartRedraw(); startTime = tp.time; createVLine(_V_START_LINE, tp.time); createVLine(_V_END_LINE, 0); createHLine(_H_LINE, 0, (clr == clrBlack) ? clrWhite : clrBlack); //--- POSITION RECTANGLE ANCHOR (0) AT THE HIGH OF BAR AT RIGHT-CLICK POSITION int index = iBarShift(_Symbol, PERIOD_CURRENT, tp.time); double price = iHigh(_Symbol, PERIOD_CURRENT, index); createRect(_VOLUME_RANGE, tp.time, price, 0, 0, clrOldLace, "ANALYSIS RANGE"); ChartRedraw(); } } else { //--- RIGHT-CLICK RELEASE if(isDragging) { //--- DELETE CROSSHAIR ObjectDelete(0, _V_START_LINE); ObjectDelete(0, _V_END_LINE); ObjectDelete(0, _H_LINE); ChartRedraw(); isDragging = false; //--- RENDER VOLUME PROFILE calcVolume(); } } break;