//+------------------------------------------------------------------+ //| Volume Profile Heatmap.mq5 | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property indicator_chart_window #property indicator_buffers 6 #property indicator_plots 6 #include //+------------------------------------------------------------------+ //| Volume source enumeration | //+------------------------------------------------------------------+ enum ENUM_VOLUME_SOURCE { VOLUME_SOURCE_TICK, // Tick Volume VOLUME_SOURCE_REAL // Real Volume }; //+------------------------------------------------------------------+ //| Candle cell mode enumeration | //+------------------------------------------------------------------+ enum ENUM_CANDLE_CELL_MODE { CANDLE_CELLS_NEUTRAL, // Neutral Gray CANDLE_CELLS_TRANSPARENT, // Transparent CANDLE_CELLS_COLORED // Colored Like Every Other Cell }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "PROFILE" input int InpProfileDepthBars = 300; // Profile Depth (bars in the rolling window) input int InpBinCount = 31; // Price Bins input ENUM_VOLUME_SOURCE InpVolumeSource = VOLUME_SOURCE_TICK; // Volume Source input bool InpDistributeAcrossRange = true; // Spread Bar Volume Across Its Range input double InpValueAreaPercent = 70.0; // Value Area Percent input int InpMaxHistoryBars = 1500; // Max History Columns Kept input group "HEATMAP" input bool InpShowHeatmap = true; // Show Heatmap input double InpHeatFloor = 0.25; // Heat Floor (0 = smooth, 0.5 = high contrast) input int InpHeatMaxOpacity = 50; // Heat Max Opacity Percent input int InpPocOpacity = 80; // Point Of Control Opacity Percent input ENUM_CANDLE_CELL_MODE InpCandleCellMode = CANDLE_CELLS_NEUTRAL; // Cells Where The Candle Body Sits input group "SIDE PROFILE" input bool InpShowSideProfile = true; // Show Side Profile input int InpNearBandBars = 10; // Near Band Width (bars from current bar) input int InpProfileOffsetBars = 25; // Profile Offset (bars right of current bar) input int InpProfileWidthBars = 10; // Profile Width (bars) input bool InpShowReadouts = true; // Show Liquidity Readouts input int InpReadoutOffsetBars = 40; // Readout Offset (bars right of current bar) input int InpFontSize = 12; // Font Size (pixels) input group "LEVELS" input bool InpShowPocLine = true; // Show Point Of Control Line input bool InpShowValueArea = true; // Show Value Area High And Low input color InpPocLineColor = clrRoyalBlue; // Point Of Control Line And Tag Color input color InpValueAreaColor = clrDarkViolet; // Value Area Lines And Tags Color input group "COLORS" input color InpUpperColor = C'189,43,43'; // Liquidity Above Price Color input color InpLowerColor = C'41,180,83'; // Liquidity Below Price Color input group "ALERTS" input bool InpAlertPocEntry = false; // Alert When A Close Enters The POC Bin input bool InpAlertPopup = true; // Popup Alert input bool InpAlertJournal = true; // Print To Journal //+------------------------------------------------------------------+ //| Heat canvas with a cached base layer and direct pixel blending | //+------------------------------------------------------------------+ class CHeatCanvas : public CCanvas { private: uint m_baseLayer[]; // Snapshot of the heatmap pixels without overlays public: bool SaveBase(); // Snapshot the pixels as the base layer bool RestoreBase(); // Put the base layer back void BlendPixel(const int x, const int y, const uint argb); // Source over blend of one pixel void BlendRectangle(int x1, int y1, int x2, int y2, const uint argb); // Source over blend of a rectangle void DrawTextClean(const int x, const int y, const string text, const uint argb, const uint alignment); // Text composited by true glyph coverage }; //+------------------------------------------------------------------+ //| Snapshot the current pixels as the base layer | //+------------------------------------------------------------------+ bool CHeatCanvas::SaveBase() { //--- Nothing to save without a live buffer int count = m_width * m_height; if(count <= 0) return(false); //--- One native bulk copy if(ArraySize(m_baseLayer) != count && ArrayResize(m_baseLayer, count) != count) return(false); return(ArrayCopy(m_baseLayer, m_pixels, 0, 0, count) == count); } //+------------------------------------------------------------------+ //| Put the base layer back over the whole canvas | //+------------------------------------------------------------------+ bool CHeatCanvas::RestoreBase() { //--- The snapshot must match the current size int count = m_width * m_height; if(count <= 0 || ArraySize(m_baseLayer) != count) return(false); //--- One native bulk copy return(ArrayCopy(m_pixels, m_baseLayer, 0, 0, count) == count); } //+------------------------------------------------------------------+ //| Source over blend of one pixel straight into the pixel buffer | //+------------------------------------------------------------------+ void CHeatCanvas::BlendPixel(const int x, const int y, const uint argb) { //--- Clip to the canvas and skip transparent paint if(x < 0 || y < 0 || x >= m_width || y >= m_height) return; int sourceAlpha = (int)(argb >> 24); if(sourceAlpha <= 0) return; //--- Destination and source channels int index = y * m_width + x; uint dest = m_pixels[index]; int destAlpha = (int)(dest >> 24); //--- Split both colors into channels int sourceRed = (int)((argb >> 16) & 0xFF); int sourceGreen = (int)((argb >> 8) & 0xFF); int sourceBlue = (int)(argb & 0xFF); int destRed = (int)((dest >> 16) & 0xFF); int destGreen = (int)((dest >> 8) & 0xFF); int destBlue = (int)(dest & 0xFF); //--- Source over compositing in 8 bit fixed point int outAlpha = sourceAlpha + destAlpha * (255 - sourceAlpha) / 255; if(outAlpha <= 0) { //--- Both transparent m_pixels[index] = 0; return; } int outRed = (sourceRed * sourceAlpha + destRed * destAlpha * (255 - sourceAlpha) / 255) / outAlpha; int outGreen = (sourceGreen * sourceAlpha + destGreen * destAlpha * (255 - sourceAlpha) / 255) / outAlpha; int outBlue = (sourceBlue * sourceAlpha + destBlue * destAlpha * (255 - sourceAlpha) / 255) / outAlpha; m_pixels[index] = ((uint)outAlpha << 24) | ((uint)outRed << 16) | ((uint)outGreen << 8) | (uint)outBlue; } //+------------------------------------------------------------------+ //| Source over blend of a rectangle straight into the pixel buffer | //+------------------------------------------------------------------+ void CHeatCanvas::BlendRectangle(int x1, int y1, int x2, int y2, const uint argb) { //--- Skip fully transparent paint if((argb >> 24) == 0) return; //--- Order the corners and clip to the canvas int left = MathMax(0, MathMin(x1, x2)); int right = MathMin(m_width - 1, MathMax(x1, x2)); int top = MathMax(0, MathMin(y1, y2)); int bottom = MathMin(m_height - 1, MathMax(y1, y2)); if(left > right || top > bottom) return; //--- Blend every pixel of the rectangle for(int y = top; y <= bottom; y++) { //--- One row for(int x = left; x <= right; x++) BlendPixel(x, y, argb); } } //+------------------------------------------------------------------+ //| Composite text by true glyph coverage, no holes at the edges | //+------------------------------------------------------------------+ void CHeatCanvas::DrawTextClean(const int x, const int y, const string text, const uint argb, const uint alignment) { //--- Measure the string in the font set by TextSetFont uint textWidth = 0; uint textHeight = 0; if(text == "" || !::TextGetSize(text, textWidth, textHeight) || textWidth == 0 || textHeight == 0) return; int width = (int)textWidth; int height = (int)textHeight; int count = width * height; //--- Render black text on a black and on a white buffer uint onBlack[]; uint onWhite[]; if(ArrayResize(onBlack, count) != count || ArrayResize(onWhite, count) != count) return; ArrayFill(onBlack, 0, count, 0xFF000000); ArrayFill(onWhite, 0, count, 0xFFFFFFFF); //--- Render the text onto the black and white bitmaps ::TextOut(text, 0, 0, TA_LEFT | TA_TOP, onBlack, width, height, 0xFF000000, COLOR_FORMAT_ARGB_NORMALIZE); ::TextOut(text, 0, 0, TA_LEFT | TA_TOP, onWhite, width, height, 0xFF000000, COLOR_FORMAT_ARGB_NORMALIZE); //--- Resolve the top left from the requested alignment int left = x; int top = y; if((alignment & TA_CENTER) == TA_CENTER) left = x - width / 2; else if((alignment & TA_RIGHT) == TA_RIGHT) left = x - width; if((alignment & TA_VCENTER) == TA_VCENTER) top = y - height / 2; else if((alignment & TA_BOTTOM) == TA_BOTTOM) top = y - height; //--- Text color and its own alpha int textAlpha = (int)(argb >> 24); uint textRgb = argb & 0x00FFFFFF; //--- Coverage is how much the white background was darkened for(int row = 0; row < height; row++) { //--- One row of glyph pixels for(int column = 0; column < width; column++) { //--- Channel deltas between the two renders int index = row * width + column; int deltaRed = (int)((onWhite[index] >> 16) & 0xFF) - (int)((onBlack[index] >> 16) & 0xFF); int deltaGreen = (int)((onWhite[index] >> 8) & 0xFF) - (int)((onBlack[index] >> 8) & 0xFF); int deltaBlue = (int)(onWhite[index] & 0xFF) - (int)(onBlack[index] & 0xFF); //--- Full coverage leaves no delta, no coverage leaves the full white to black delta int coverage = 255 - (deltaRed + deltaGreen + deltaBlue) / 3; if(coverage <= 0) continue; if(coverage > 255) coverage = 255; //--- Scale by the text alpha and blend int alpha = coverage * textAlpha / 255; if(alpha <= 0) continue; BlendPixel(left + column, top + row, ((uint)alpha << 24) | textRgb); } } } //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ #define OBJECT_PREFIX g_objectPrefix // Prefix for the canvas object, unique per instance string g_objectPrefix = "VPH_"; // Instance prefix, finalized in OnInit CHeatCanvas g_canvas; // The single full chart canvas string g_canvasName = ""; // Name of the bitmap label carrying the canvas double g_pocPriceBuffer[]; // Hidden: point of control price for the bar double g_valueAreaHighBuffer[]; // Hidden: value area high for the bar double g_valueAreaLowBuffer[]; // Hidden: value area low for the bar double g_liquidityBelowBuffer[]; // Hidden: percent of window volume below the close double g_liquidityAboveBuffer[]; // Hidden: percent of window volume above the close double g_imbalanceBuffer[]; // Hidden: volume below minus volume above int g_columnCapacity = 0; // Ring capacity in columns int g_columnCount = 0; // Columns currently stored int g_columnHead = 0; // Next ring slot to write int g_newestColumnBar = -1; // Bar index held by the newest column double g_columnLow[]; // Window low of each column double g_columnStep[]; // Bin height of each column uint g_columnColors[]; // Cell colors, capacity times bins, flat double g_liveBinVolumes[]; // Bin volumes of the newest profile double g_liveLow = 0.0; // Window low of the newest profile double g_liveHigh = 0.0; // Window high of the newest profile double g_liveStep = 0.0; // Bin height of the newest profile double g_liveMaxVolume = 0.0; // Largest bin volume of the newest profile int g_livePocIndex = -1; // Point of control bin of the newest profile bool g_liveProfileReady = false; // A profile exists to draw double g_liveValueAreaHigh = 0.0; // Value area high of the newest profile double g_liveValueAreaLow = 0.0; // Value area low of the newest profile double g_liveOpen = 0.0; // Forming bar open for the candle hide test double g_liveHighPrice = 0.0; // Forming bar high for the candle hide test double g_liveLowPrice = 0.0; // Forming bar low for the candle hide test double g_liveClose = 0.0; // Forming bar close, drives the split and readouts int g_lastLiveHalfBin = -999999; // Half bin index of the close at the last overlay draw int g_ratesTotal = 0; // Bars on the chart at the last calculation datetime g_lastProcessedBarTime = 0; // Time of the last closed bar processed bool g_wasInsidePoc = false; // Previous closed bar sat inside the POC bin int g_chartWidth = 0; // Viewport key: chart width in pixels int g_chartHeight = 0; // Viewport key: chart height in pixels int g_chartScale = -1; // Viewport key: chart scale int g_firstVisibleBar = -1; // Viewport key: series index of the first visible bar int g_visibleBars = -1; // Viewport key: number of visible bars double g_priceMin = 0.0; // Viewport key: lowest visible price double g_priceMax = 0.0; // Viewport key: highest visible price int g_barPixels = 1; // Pixel width of one bar at the current scale bool g_canvasReady = false; // Canvas created and sized color g_foregroundColor = clrBlack; // Chart foreground color for neutral text uint g_upperArgb = 0; // Precomputed opaque upper color uint g_lowerArgb = 0; // Precomputed opaque lower color //+------------------------------------------------------------------+ //| Convert the chart timeframe to a short string | //+------------------------------------------------------------------+ string TimeframeToString() { //--- Get the enum name of the current period string periodName = EnumToString((ENUM_TIMEFRAMES)Period()); //--- Strip the PERIOD_ prefix when present if(StringFind(periodName, "PERIOD_") == 0) periodName = StringSubstr(periodName, 7); //--- Return the short form return(periodName); } //+------------------------------------------------------------------+ //| Format a volume with K, M, or B suffixes | //+------------------------------------------------------------------+ string FormatVolume(const double volume) { //--- Work on the magnitude and restore the sign at the end double magnitude = MathAbs(volume); string sign = volume < 0.0 ? "-" : ""; //--- Pick the suffix by scale if(magnitude >= 1.0e9) return(sign + DoubleToString(magnitude / 1.0e9, 2) + "B"); if(magnitude >= 1.0e6) return(sign + DoubleToString(magnitude / 1.0e6, 2) + "M"); if(magnitude >= 1.0e3) return(sign + DoubleToString(magnitude / 1.0e3, 1) + "K"); //--- Small values print as integers return(sign + DoubleToString(magnitude, 0)); } //+------------------------------------------------------------------+ //| Build an ARGB value from a color and an opacity percent | //+------------------------------------------------------------------+ uint ArgbFromOpacity(const color baseColor, double opacityPercent) { //--- Clamp the opacity if(opacityPercent < 0.0) opacityPercent = 0.0; if(opacityPercent > 100.0) opacityPercent = 100.0; //--- Scale to a byte alpha uchar alpha = (uchar)(opacityPercent * 255.0 / 100.0 + 0.5); return(ColorToARGB(baseColor, alpha)); } //+------------------------------------------------------------------+ //| Interpolate between two colors by a unit weight | //+------------------------------------------------------------------+ color InterpolateColor(const color fromColor, const color toColor, double weight) { //--- Clamp the weight if(weight < 0.0) weight = 0.0; if(weight > 1.0) weight = 1.0; //--- Mix each channel, colors store blue in the high byte int red = (int)((fromColor & 0xFF) * (1.0 - weight) + (toColor & 0xFF) * weight + 0.5); int green = (int)(((fromColor >> 8) & 0xFF) * (1.0 - weight) + ((toColor >> 8) & 0xFF) * weight + 0.5); int blue = (int)(((fromColor >> 16) & 0xFF) * (1.0 - weight) + ((toColor >> 16) & 0xFF) * weight + 0.5); return((color)(red | (green << 8) | (blue << 16))); } //+------------------------------------------------------------------+ //| Dispatch an alert through the enabled channels | //+------------------------------------------------------------------+ void SendProfileAlert(const string message) { //--- Prepend the symbol and timeframe string fullMessage = _Symbol + " " + TimeframeToString() + ": " + message; //--- Show a popup when enabled if(InpAlertPopup) Alert(fullMessage); //--- Write to the journal when enabled if(InpAlertJournal) Print(fullMessage); } //+------------------------------------------------------------------+ //| Read the volume of a bar from the chosen source | //+------------------------------------------------------------------+ double ReadBarVolume(const long &tick_volume[], const long &volume[], const int bar) { //--- Return the chosen source as a double return((InpVolumeSource == VOLUME_SOURCE_TICK) ? (double)tick_volume[bar] : (double)volume[bar]); } //+------------------------------------------------------------------+ //| Compute the heat cell color for one bin of one bar | //+------------------------------------------------------------------+ uint ComputeCellColor(const double binVolume, const double maxVolume, const bool isPoc, const double binLow, const double binHigh, const double barOpen, const double barHigh, const double barLow, const double barClose) { //--- No volume in the window means no heat if(maxVolume <= 0.0) return(0); //--- Bins at or above the close carry the upper color, the rest the lower color color sideColor = (barClose <= binLow) ? InpUpperColor : InpLowerColor; //--- Cells the candle body and its midpoints sit in follow the candle cell mode if(InpCandleCellMode != CANDLE_CELLS_COLORED) { //--- Range midpoint and typical price complete the four test points double midHalf = (barHigh + barLow) / 2.0; double midThird = (barHigh + barLow + barClose) / 3.0; //--- Any of the four price points inside the bin marks it bool openInside = (barOpen >= binLow && barOpen <= binHigh); bool closeInside = (barClose >= binLow && barClose <= binHigh); bool halfInside = (midHalf >= binLow && midHalf <= binHigh); bool thirdInside = (midThird >= binLow && midThird <= binHigh); if(openInside || closeInside || halfInside || thirdInside) { //--- Transparent drops the cell, neutral swaps the side color for a quiet gray if(InpCandleCellMode == CANDLE_CELLS_TRANSPARENT) return(0); sideColor = C'68,68,68'; } } //--- The point of control paints at its own opacity if(isPoc) return(ArgbFromOpacity(sideColor, InpPocOpacity)); //--- Everything else ramps from the floor to the max opacity double value = binVolume / maxVolume; double weight = (1.0 - InpHeatFloor) > 0.0 ? (value - InpHeatFloor) / (1.0 - InpHeatFloor) : 0.0; if(weight <= 0.0) return(0); if(weight > 1.0) weight = 1.0; //--- Color ramps from a neutral gray into the side color as the alpha rises color rampColor = InterpolateColor(C'68,68,68', sideColor, weight); return(ArgbFromOpacity(rampColor, weight * InpHeatMaxOpacity)); } //+------------------------------------------------------------------+ //| Build the profile of the window ending at a closed bar | //+------------------------------------------------------------------+ bool BuildProfile(const int bar, const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[]) { //--- Resolve the window int firstBar = bar - InpProfileDepthBars + 1; if(firstBar < 0) return(false); //--- Window extremes double windowHigh = high[bar]; double windowLow = low[bar]; for(int k = firstBar; k < bar; k++) { //--- Track the extremes if(high[k] > windowHigh) windowHigh = high[k]; if(low[k] < windowLow) windowLow = low[k]; } //--- Reject a flat window double step = (windowHigh - windowLow) / InpBinCount; if(step <= 0.0) return(false); //--- Start every bin empty ArrayInitialize(g_liveBinVolumes, 0.0); //--- Accumulate every bar of the window for(int k = firstBar; k <= bar; k++) { //--- Volume of this bar double barVolume = ReadBarVolume(tick_volume, volume, k); if(barVolume <= 0.0) continue; //--- Spread the volume across the bins the bar range covers if(InpDistributeAcrossRange) { //--- Bin span of the bar, clamped to the ladder double barRange = high[k] - low[k]; int lowBin = (int)MathFloor((low[k] - windowLow) / step); int highBin = (int)MathFloor((high[k] - windowLow) / step); //--- Clamp both ends, a bar edge on the window high lands one past the ladder if(lowBin < 0) lowBin = 0; if(lowBin > InpBinCount - 1) lowBin = InpBinCount - 1; if(highBin < 0) highBin = 0; if(highBin > InpBinCount - 1) highBin = InpBinCount - 1; //--- A bar inside one bin drops everything there if(barRange <= 0.0 || lowBin == highBin) { //--- Single bin g_liveBinVolumes[lowBin] += barVolume; continue; } //--- Otherwise split by overlap length for(int i = lowBin; i <= highBin; i++) { //--- Overlap of the bar with this bin double binLow = windowLow + step * i; double binHigh = binLow + step; double overlap = MathMin(high[k], binHigh) - MathMax(low[k], binLow); if(overlap > 0.0) g_liveBinVolumes[i] += barVolume * overlap / barRange; } } //--- Or drop it at the close, split between the containing bin and its nearer neighbor else { //--- Containing bin int binIndex = (int)MathFloor((close[k] - windowLow) / step); if(binIndex < 0) binIndex = 0; if(binIndex > InpBinCount - 1) binIndex = InpBinCount - 1; //--- Nearer neighbor double binMid = windowLow + step * binIndex + step / 2.0; int neighbor = (close[k] < binMid) ? binIndex - 1 : binIndex + 1; //--- Split the volume evenly when the neighbor sits on the ladder if(neighbor >= 0 && neighbor < InpBinCount) { //--- Give each bin half the volume g_liveBinVolumes[binIndex] += barVolume * 0.5; g_liveBinVolumes[neighbor] += barVolume * 0.5; } //--- Otherwise keep the whole volume in the containing bin else g_liveBinVolumes[binIndex] += barVolume; } } //--- Locate the point of control g_liveMaxVolume = 0.0; g_livePocIndex = -1; for(int i = 0; i < InpBinCount; i++) { //--- Keep the strongest bin if(g_liveBinVolumes[i] > g_liveMaxVolume) { //--- New leader g_liveMaxVolume = g_liveBinVolumes[i]; g_livePocIndex = i; } } //--- Publish the window geometry g_liveLow = windowLow; g_liveHigh = windowHigh; g_liveStep = step; g_liveProfileReady = (g_livePocIndex >= 0); return(g_liveProfileReady); } //+------------------------------------------------------------------+ //| Compute the value area around the point of control | //+------------------------------------------------------------------+ void ComputeValueArea(double &valueAreaHigh, double &valueAreaLow) { //--- Total window volume and the target share double totalVolume = 0.0; for(int i = 0; i < InpBinCount; i++) totalVolume += g_liveBinVolumes[i]; double target = totalVolume * InpValueAreaPercent / 100.0; //--- Grow outward from the point of control, taking the larger neighbor each step int lowerBin = g_livePocIndex; int upperBin = g_livePocIndex; double accumulated = g_liveBinVolumes[g_livePocIndex]; while(accumulated < target && (lowerBin > 0 || upperBin < InpBinCount - 1)) { //--- Candidate volumes on each side double belowVolume = (lowerBin > 0) ? g_liveBinVolumes[lowerBin - 1] : -1.0; double aboveVolume = (upperBin < InpBinCount - 1) ? g_liveBinVolumes[upperBin + 1] : -1.0; if(aboveVolume >= belowVolume) { //--- Extend upward upperBin++; accumulated += aboveVolume; } else { //--- Extend downward lowerBin--; accumulated += belowVolume; } } //--- Convert the bin span to prices valueAreaLow = g_liveLow + g_liveStep * lowerBin; valueAreaHigh = g_liveLow + g_liveStep * (upperBin + 1); } //+------------------------------------------------------------------+ //| Split the window volume into below and above a price | //+------------------------------------------------------------------+ void SplitLiquidity(const double price, double &belowVolume, double &aboveVolume) { //--- Start both sides empty belowVolume = 0.0; aboveVolume = 0.0; //--- Bins whose midpoint sits under the price count as below for(int i = 0; i < InpBinCount; i++) { //--- Midpoint test double binMid = g_liveLow + g_liveStep * i + g_liveStep / 2.0; if(binMid < price) belowVolume += g_liveBinVolumes[i]; else aboveVolume += g_liveBinVolumes[i]; } } //+------------------------------------------------------------------+ //| Store the newest profile as a heat column for one bar | //+------------------------------------------------------------------+ void StoreColumn(const int bar, const double barOpen, const double barHigh, const double barLow, const double barClose) { //--- Write into the head slot int slot = g_columnHead; g_columnLow[slot] = g_liveLow; g_columnStep[slot] = g_liveStep; //--- Precompute every cell color once, scrolling then costs only a lookup int base = slot * InpBinCount; for(int i = 0; i < InpBinCount; i++) { //--- Cell bounds double binLow = g_liveLow + g_liveStep * i; double binHigh = binLow + g_liveStep; g_columnColors[base + i] = ComputeCellColor(g_liveBinVolumes[i], g_liveMaxVolume, i == g_livePocIndex, binLow, binHigh, barOpen, barHigh, barLow, barClose); } //--- Advance the ring g_columnHead = (g_columnHead + 1) % g_columnCapacity; if(g_columnCount < g_columnCapacity) g_columnCount++; g_newestColumnBar = bar; } //+------------------------------------------------------------------+ //| Resolve the ring slot holding the column of a bar | //+------------------------------------------------------------------+ int ColumnSlotForBar(const int bar) { //--- Distance back from the newest column int back = g_newestColumnBar - bar; if(back < 0 || back >= g_columnCount) return(-1); //--- Walk back from the slot before the head int slot = g_columnHead - 1 - back; while(slot < 0) slot += g_columnCapacity; return(slot); } //+------------------------------------------------------------------+ //| Read the chart viewport and report whether it changed | //+------------------------------------------------------------------+ bool ReadViewport() { //--- Read every component of the mapping int width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0); int height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0); int scale = (int)ChartGetInteger(0, CHART_SCALE, 0); //--- Horizontal window and vertical window int firstVisible = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR, 0); int visible = (int)ChartGetInteger(0, CHART_VISIBLE_BARS, 0); double priceMin = ChartGetDouble(0, CHART_PRICE_MIN, 0); double priceMax = ChartGetDouble(0, CHART_PRICE_MAX, 0); //--- Compare against the cached key bool changed = (width != g_chartWidth || height != g_chartHeight || scale != g_chartScale || firstVisible != g_firstVisibleBar || visible != g_visibleBars || priceMin != g_priceMin || priceMax != g_priceMax); if(!changed) return(false); //--- Store the new key g_chartWidth = width; g_chartHeight = height; g_chartScale = scale; //--- Store the visible window g_firstVisibleBar = firstVisible; g_visibleBars = visible; g_priceMin = priceMin; g_priceMax = priceMax; //--- One bar is two to the power of the scale pixels wide g_barPixels = (int)MathPow(2.0, scale); if(g_barPixels < 1) g_barPixels = 1; //--- Report the change return(true); } //+------------------------------------------------------------------+ //| Pixel x of the left edge of a bar given its series shift | //+------------------------------------------------------------------+ int ShiftToLeftX(const int shift) { //--- Bars step right as the shift falls return((g_firstVisibleBar - shift) * g_barPixels); } //+------------------------------------------------------------------+ //| Pixel y of a price at the current scale | //+------------------------------------------------------------------+ int PriceToY(const double price) { //--- Guard a collapsed range double span = g_priceMax - g_priceMin; if(span <= 0.0) return(0); //--- Linear map, highest price at the top return((int)MathRound((g_priceMax - price) / span * g_chartHeight)); } //+------------------------------------------------------------------+ //| Make sure the canvas covers the current chart size | //+------------------------------------------------------------------+ bool EnsureCanvasSize() { //--- Resize whenever the chart size differs, CCanvas rebinds the resource and sizes the label itself if(g_canvas.Width() != g_chartWidth || g_canvas.Height() != g_chartHeight) { //--- Reallocate to the exact chart size if(!g_canvas.Resize(g_chartWidth, g_chartHeight)) return(false); } //--- Canvas matches the chart return(true); } //+------------------------------------------------------------------+ //| Paint the historical heat columns that are on screen | //+------------------------------------------------------------------+ void DrawHeatmap() { //--- Skip when hidden or empty if(!InpShowHeatmap || g_columnCount == 0 || g_ratesTotal <= 0) return; //--- Visible shift range, clipped to real bars int oldestShift = g_firstVisibleBar; int newestShift = g_firstVisibleBar - g_visibleBars + 1; if(newestShift < 0) newestShift = 0; //--- One column per visible bar for(int shift = oldestShift; shift >= newestShift; shift--) { //--- Forward index and stored slot int bar = g_ratesTotal - 1 - shift; int slot = ColumnSlotForBar(bar); if(slot < 0) continue; //--- Column geometry double columnLow = g_columnLow[slot]; double step = g_columnStep[slot]; int xLeft = ShiftToLeftX(shift); int xRight = xLeft + g_barPixels - 1; if(xRight < 0 || xLeft >= g_chartWidth) continue; //--- Paint the cells, ladder bottom to top int base = slot * InpBinCount; int yBottom = PriceToY(columnLow); for(int i = 0; i < InpBinCount; i++) { //--- Next boundary up int yTop = PriceToY(columnLow + step * (i + 1)); uint argb = g_columnColors[base + i]; if(argb != 0) g_canvas.FillRectangle(xLeft, yTop, xRight, yBottom - 1, argb); yBottom = yTop; } } } //+------------------------------------------------------------------+ //| Paint a dashed horizontal line by blending short segments | //+------------------------------------------------------------------+ void DrawDashedHorizontal(const int x1, const int x2, const int y, const uint argb, const int dash, const int gap) { //--- Walk left to right in dash plus gap strides int left = MathMin(x1, x2); int right = MathMax(x1, x2); for(int x = left; x <= right; x += dash + gap) { //--- One dash, clipped to the end g_canvas.BlendRectangle(x, y, MathMin(x + dash - 1, right), y, argb); } } //+------------------------------------------------------------------+ //| Paint a level tag as a solid chip with white text | //+------------------------------------------------------------------+ void DrawLevelTag(const int rightX, const int centerY, const string text, const uint argb) { //--- Measure the text in the current font uint textWidth = 0; uint textHeight = 0; if(!TextGetSize(text, textWidth, textHeight)) return; //--- Chip bounds with a small padding, right edge on the requested x int padding = 3; int left = rightX - (int)textWidth - padding * 2; int top = centerY - (int)textHeight / 2 - padding; int bottom = centerY + (int)textHeight / 2 + padding; //--- Solid chip so the tag reads over the densest heat g_canvas.BlendRectangle(left, top, rightX, bottom, argb); //--- White text centered in the chip g_canvas.DrawTextClean(left + padding, centerY, text, ColorToARGB(clrWhite, 255), TA_LEFT | TA_VCENTER); } //+------------------------------------------------------------------+ //| Paint the point of control and value area lines with tags | //+------------------------------------------------------------------+ void DrawLevelLines(const int xLive) { //--- Skip when both are hidden if(!InpShowPocLine && !InpShowValueArea) return; //--- Lines span the profile window, from its first bar to the live bar int xStart = ShiftToLeftX(InpProfileDepthBars); int xEnd = xLive + g_barPixels - 1; if(xStart < 0) xStart = 0; //--- Tags sit just left of the live bar, centered on their line, as solid chips int tagX = xLive - 4; TextSetFont("Arial", InpFontSize - 1, FW_BOLD); //--- Point of control if(InpShowPocLine) { //--- Dashed line at the bin midpoint with the price double pocPrice = g_liveLow + g_liveStep * g_livePocIndex + g_liveStep / 2.0; int yPoc = PriceToY(pocPrice); uint pocArgb = ColorToARGB(InpPocLineColor, 255); DrawDashedHorizontal(xStart, xEnd, yPoc, pocArgb, 6, 4); DrawLevelTag(tagX, yPoc, "POC " + DoubleToString(pocPrice, _Digits), pocArgb); } //--- Value area if(InpShowValueArea) { //--- Dotted lines at both edges with their prices uint areaArgb = ColorToARGB(InpValueAreaColor, 255); int yHigh = PriceToY(g_liveValueAreaHigh); int yLow = PriceToY(g_liveValueAreaLow); //--- Lines then tags DrawDashedHorizontal(xStart, xEnd, yHigh, areaArgb, 2, 3); DrawDashedHorizontal(xStart, xEnd, yLow, areaArgb, 2, 3); DrawLevelTag(tagX, yHigh, "VAH " + DoubleToString(g_liveValueAreaHigh, _Digits), areaArgb); DrawLevelTag(tagX, yLow, "VAL " + DoubleToString(g_liveValueAreaLow, _Digits), areaArgb); } } //+------------------------------------------------------------------+ //| Paint the live overlay: band, profile, levels, and readouts | //+------------------------------------------------------------------+ void DrawSideProfile() { //--- Nothing to show without a profile if(!g_liveProfileReady) return; //--- Pixel anchor of the forming bar int liveShift = 0; int xLive = ShiftToLeftX(liveShift); //--- Where the close sits on the ladder int splitIndex = (int)MathFloor((g_liveClose - g_liveLow) / g_liveStep); if(splitIndex < 0) splitIndex = 0; if(splitIndex > InpBinCount - 1) splitIndex = InpBinCount - 1; int maxDistance = MathMax(splitIndex, InpBinCount - 1 - splitIndex); //--- Near band, the live gradient projected right of the current bar if(InpShowSideProfile && InpNearBandBars > 0) { //--- Band spans the forming bar to the band width int xBandLeft = xLive; int xBandRight = xLive + InpNearBandBars * g_barPixels - 1; for(int i = 0; i < InpBinCount; i++) { //--- Live cell color using the forming bar for the candle hide test double binLow = g_liveLow + g_liveStep * i; double binHigh = binLow + g_liveStep; uint argb = ComputeCellColor(g_liveBinVolumes[i], g_liveMaxVolume, i == g_livePocIndex, binLow, binHigh, g_liveOpen, g_liveHighPrice, g_liveLowPrice, g_liveClose); if(argb == 0) continue; g_canvas.BlendRectangle(xBandLeft, PriceToY(binHigh), xBandRight, PriceToY(binLow) - 1, argb); } } //--- Side profile boxes fading away from the split int xProfileLeft = xLive + InpProfileOffsetBars * g_barPixels; int xProfileRight = xProfileLeft + InpProfileWidthBars * g_barPixels - 1; for(int i = 0; InpShowSideProfile && i < InpBinCount; i++) { //--- Cell bounds and side color from the live close double binLow = g_liveLow + g_liveStep * i; double binHigh = binLow + g_liveStep; double binMid = binLow + g_liveStep / 2.0; color sideColor = (g_liveClose > binMid) ? InpLowerColor : InpUpperColor; //--- Opacity from ninety percent at the split down to five percent at the ends double fade = maxDistance > 0 ? (double)MathAbs(i - splitIndex) / maxDistance : 0.0; double opacity = 90.0 - fade * 85.0; int yTop = PriceToY(binHigh); int yBottom = PriceToY(binLow) - 1; //--- The point of control stretches back to the band and carries its volume if(i == g_livePocIndex) { //--- Full strength box from the band edge to the profile edge int xPocLeft = xLive + InpNearBandBars * g_barPixels; g_canvas.BlendRectangle(xPocLeft, yTop, xProfileRight, yBottom, ArgbFromOpacity(sideColor, 100.0)); TextSetFont("Arial", InpFontSize, FW_BOLD); g_canvas.DrawTextClean((xPocLeft + xProfileRight) / 2, (yTop + yBottom) / 2, ">" + FormatVolume(g_liveMaxVolume) + "<", ColorToARGB(clrWhite, 255), TA_CENTER | TA_VCENTER); continue; } //--- Ordinary box g_canvas.BlendRectangle(xProfileLeft, yTop, xProfileRight, yBottom, ArgbFromOpacity(sideColor, opacity)); } //--- Levels go on last so no band or box can cover them DrawLevelLines(xLive); //--- Readouts to the right of the profile if(!InpShowReadouts) return; //--- Split the window volume around the live close double belowVolume = 0.0; double aboveVolume = 0.0; SplitLiquidity(g_liveClose, belowVolume, aboveVolume); double totalVolume = belowVolume + aboveVolume; double belowPercent = totalVolume > 0.0 ? belowVolume / totalVolume * 100.0 : 0.0; double abovePercent = totalVolume > 0.0 ? aboveVolume / totalVolume * 100.0 : 0.0; //--- Text anchor and line pitch int xText = xLive + InpReadoutOffsetBars * g_barPixels; int linePitch = InpFontSize + 3; TextSetFont("Arial", InpFontSize, FW_BOLD); //--- Liquidity below price, centered on the space under the close int yBelow = PriceToY(g_liveLow + (g_liveClose - g_liveLow) / 2.0); g_canvas.DrawTextClean(xText, yBelow - linePitch, "BUY LIQ", g_lowerArgb, TA_LEFT | TA_VCENTER); g_canvas.DrawTextClean(xText, yBelow, FormatVolume(belowVolume), g_lowerArgb, TA_LEFT | TA_VCENTER); g_canvas.DrawTextClean(xText, yBelow + linePitch, DoubleToString(belowPercent, 1) + "%", g_lowerArgb, TA_LEFT | TA_VCENTER); //--- Liquidity above price, centered on the space over the close int yAbove = PriceToY(g_liveClose + (g_liveHigh - g_liveClose) / 2.0); g_canvas.DrawTextClean(xText, yAbove - linePitch, "SELL LIQ", g_upperArgb, TA_LEFT | TA_VCENTER); g_canvas.DrawTextClean(xText, yAbove, FormatVolume(aboveVolume), g_upperArgb, TA_LEFT | TA_VCENTER); g_canvas.DrawTextClean(xText, yAbove + linePitch, DoubleToString(abovePercent, 1) + "%", g_upperArgb, TA_LEFT | TA_VCENTER); //--- Imbalance above the window, signed volume plus the side that carries more double imbalance = belowVolume - aboveVolume; string heavierSide = imbalance > 0.0 ? " below heavier" : (imbalance < 0.0 ? " above heavier" : " balanced"); int yImbalance = PriceToY(g_liveHigh + g_liveStep); g_canvas.DrawTextClean(xText, yImbalance, "Imbalance: " + FormatVolume(imbalance) + heavierSide, ColorToARGB(g_foregroundColor, 255), TA_LEFT | TA_VCENTER); } //+------------------------------------------------------------------+ //| Half bin index of the live close, gates the overlay repaint | //+------------------------------------------------------------------+ int LiveHalfBin() { //--- No ladder yet if(!g_liveProfileReady || g_liveStep <= 0.0) return(-999999); //--- Index the live close at half-bin resolution return((int)MathFloor((g_liveClose - g_liveLow) / (g_liveStep / 2.0))); } //+------------------------------------------------------------------+ //| Repaint everything from scratch | //+------------------------------------------------------------------+ void FullRedraw() { //--- Need a live canvas and a usable viewport if(!g_canvasReady || g_chartWidth <= 0 || g_chartHeight <= 0) return; if(!EnsureCanvasSize()) return; //--- Clear, paint history, keep it as the base g_canvas.Erase(0); DrawHeatmap(); g_canvas.SaveBase(); //--- Overlay the live profile and publish DrawSideProfile(); g_canvas.Update(); //--- Remember which half bin the close was in for the overlay gate g_lastLiveHalfBin = LiveHalfBin(); } //+------------------------------------------------------------------+ //| Repaint only the live overlay on top of the cached base | //+------------------------------------------------------------------+ void OverlayRedraw() { //--- Need a live canvas and a saved base if(!g_canvasReady) return; if(!g_canvas.RestoreBase()) { //--- No base yet, fall back to the full path FullRedraw(); return; } //--- Overlay and publish DrawSideProfile(); g_canvas.Update(); g_lastLiveHalfBin = LiveHalfBin(); } //+------------------------------------------------------------------+ //| Process one closed bar: profile, buffers, column, alert | //+------------------------------------------------------------------+ void ProcessBar(const int bar, const bool alertable, const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[]) { //--- Blank the buffers for this bar g_pocPriceBuffer[bar] = EMPTY_VALUE; g_valueAreaHighBuffer[bar] = EMPTY_VALUE; g_valueAreaLowBuffer[bar] = EMPTY_VALUE; g_liquidityBelowBuffer[bar] = EMPTY_VALUE; g_liquidityAboveBuffer[bar] = EMPTY_VALUE; g_imbalanceBuffer[bar] = EMPTY_VALUE; //--- Build the window profile ending here if(!BuildProfile(bar, open, high, low, close, tick_volume, volume)) return; //--- Store the heat column of this bar StoreColumn(bar, open[bar], high[bar], low[bar], close[bar]); //--- Publish the point of control and value area double valueAreaHigh = 0.0; double valueAreaLow = 0.0; ComputeValueArea(valueAreaHigh, valueAreaLow); g_liveValueAreaHigh = valueAreaHigh; g_liveValueAreaLow = valueAreaLow; //--- Write the level buffers g_pocPriceBuffer[bar] = g_liveLow + g_liveStep * g_livePocIndex + g_liveStep / 2.0; g_valueAreaHighBuffer[bar] = valueAreaHigh; g_valueAreaLowBuffer[bar] = valueAreaLow; //--- Publish the liquidity split around this bar's close double belowVolume = 0.0; double aboveVolume = 0.0; SplitLiquidity(close[bar], belowVolume, aboveVolume); //--- Percentages and raw imbalance double totalVolume = belowVolume + aboveVolume; g_liquidityBelowBuffer[bar] = totalVolume > 0.0 ? belowVolume / totalVolume * 100.0 : 0.0; g_liquidityAboveBuffer[bar] = totalVolume > 0.0 ? aboveVolume / totalVolume * 100.0 : 0.0; g_imbalanceBuffer[bar] = belowVolume - aboveVolume; //--- Alert on a close entering the point of control bin double pocLow = g_liveLow + g_liveStep * g_livePocIndex; double pocHigh = pocLow + g_liveStep; bool insidePoc = (close[bar] >= pocLow && close[bar] <= pocHigh); //--- Fire once on entry, only on live bars if(InpAlertPocEntry && alertable && insidePoc && !g_wasInsidePoc) SendProfileAlert("Close " + DoubleToString(close[bar], _Digits) + " entered the point of control bin " + DoubleToString(pocLow, _Digits) + " to " + DoubleToString(pocHigh, _Digits)); g_wasInsidePoc = insidePoc; } //+------------------------------------------------------------------+ //| Reset every piece of state and clear the canvas | //+------------------------------------------------------------------+ void ResetIndicatorState() { //--- Size the column ring g_columnCapacity = InpMaxHistoryBars; ArrayResize(g_columnLow, g_columnCapacity); ArrayResize(g_columnStep, g_columnCapacity); ArrayResize(g_columnColors, g_columnCapacity * InpBinCount); ArrayInitialize(g_columnColors, 0); //--- Empty ring g_columnCount = 0; g_columnHead = 0; g_newestColumnBar = -1; //--- Size the live profile ArrayResize(g_liveBinVolumes, InpBinCount); ArrayInitialize(g_liveBinVolumes, 0.0); g_liveProfileReady = false; g_livePocIndex = -1; g_lastLiveHalfBin = -999999; //--- Reset the processing markers g_lastProcessedBarTime = 0; g_wasInsidePoc = false; //--- Force the next viewport read to count as a change g_chartWidth = 0; g_chartHeight = 0; //--- Refresh the chart dependent colors g_foregroundColor = (color)ChartGetInteger(0, CHART_COLOR_FOREGROUND); g_upperArgb = ColorToARGB(InpUpperColor, 255); g_lowerArgb = ColorToARGB(InpLowerColor, 255); //--- Clear the canvas when it exists if(g_canvasReady) { //--- Publish a transparent frame g_canvas.Erase(0); g_canvas.Update(); } } //+------------------------------------------------------------------+ //| Create the single canvas object | //+------------------------------------------------------------------+ bool CreateCanvas() { //--- Start from the current chart size, one pixel minimum int width = MathMax(1, (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0)); int height = MathMax(1, (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0)); //--- Create the bitmap label at the chart origin, no property is touched after this g_canvasName = OBJECT_PREFIX + "Canvas"; if(!g_canvas.CreateBitmapLabel(0, 0, g_canvasName, 0, 0, width, height, COLOR_FORMAT_ARGB_NORMALIZE)) return(false); //--- Publish transparent so nothing flashes before the first paint g_canvas.Erase(0); g_canvas.Update(); g_canvasReady = true; return(true); } //+------------------------------------------------------------------+ //| Validate the inputs before the indicator starts | //+------------------------------------------------------------------+ bool ValidateInputs() { //--- Require a usable window and ladder if(InpProfileDepthBars < 2 || InpBinCount < 2 || InpBinCount > 200) { //--- Report the problem Print("Profile Depth must be at least 2 and Price Bins between 2 and 200"); return(false); } //--- Require the ring to hold at least the window if(InpMaxHistoryBars < 10) { //--- Report the problem Print("Max History Columns Kept must be at least 10"); return(false); } //--- Require sane percentages if(InpHeatFloor < 0.0 || InpHeatFloor >= 1.0 || InpHeatMaxOpacity < 0 || InpHeatMaxOpacity > 100 || InpPocOpacity < 0 || InpPocOpacity > 100 || InpValueAreaPercent <= 0.0 || InpValueAreaPercent > 100.0) { //--- Report the problem Print("Heat Floor must be 0 to 0.99, opacities 0 to 100, Value Area Percent 1 to 100"); return(false); } //--- Require non negative layout distances and a readable font if(InpNearBandBars < 0 || InpProfileOffsetBars < 0 || InpProfileWidthBars < 1 || InpReadoutOffsetBars < 0 || InpFontSize < 6) { //--- Report the problem Print("Layout offsets cannot be negative, Profile Width at least 1, Font Size at least 6"); return(false); } //--- Everything is acceptable return(true); } //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Stop on invalid inputs if(!ValidateInputs()) return(INIT_PARAMETERS_INCORRECT); //--- Bind the data buffers, readable by EAs and in the Data Window, never drawn SetIndexBuffer(0, g_pocPriceBuffer, INDICATOR_DATA); SetIndexBuffer(1, g_valueAreaHighBuffer, INDICATOR_DATA); SetIndexBuffer(2, g_valueAreaLowBuffer, INDICATOR_DATA); SetIndexBuffer(3, g_liquidityBelowBuffer, INDICATOR_DATA); SetIndexBuffer(4, g_liquidityAboveBuffer, INDICATOR_DATA); SetIndexBuffer(5, g_imbalanceBuffer, INDICATOR_DATA); //--- Every plot is data only, the canvas does all drawing string plotLabels[6] = {"POC", "Value Area High", "Value Area Low", "Liquidity Below %", "Liquidity Above %", "Imbalance"}; for(int plotIndex = 0; plotIndex < 6; plotIndex++) { //--- Hidden plot with a readable label PlotIndexSetInteger(plotIndex, PLOT_DRAW_TYPE, DRAW_NONE); PlotIndexSetDouble(plotIndex, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetString(plotIndex, PLOT_LABEL, plotLabels[plotIndex]); } //--- Use forward indexing on every buffer ArraySetAsSeries(g_pocPriceBuffer, false); ArraySetAsSeries(g_valueAreaHighBuffer, false); ArraySetAsSeries(g_valueAreaLowBuffer, false); ArraySetAsSeries(g_liquidityBelowBuffer, false); ArraySetAsSeries(g_liquidityAboveBuffer, false); ArraySetAsSeries(g_imbalanceBuffer, false); //--- Give this instance its own object prefix so several copies can share a chart g_objectPrefix = "VPH_" + IntegerToString((int)(GetMicrosecondCount() % 1000000)) + "_"; //--- Create the canvas if(!CreateCanvas()) { //--- Report the problem Print(__FUNCTION__, ": failed to create the canvas"); return(INIT_FAILED); } //--- Start from a clean state ResetIndicatorState(); //--- Name the indicator and match the chart digits IndicatorSetString(INDICATOR_SHORTNAME, "Volume Profile Heatmap"); IndicatorSetInteger(INDICATOR_DIGITS, _Digits); //--- Ready return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Free the canvas and its bitmap label g_canvas.Destroy(); g_canvasReady = false; //--- Remove anything else carrying the prefix ObjectsDeleteAll(0, OBJECT_PREFIX); //--- Refresh the chart ChartRedraw(); } //+------------------------------------------------------------------+ //| 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[]) { //--- Wait for one full window plus a closed bar int minimumBars = InpProfileDepthBars + 2; if(rates_total < minimumBars) return(0); //--- Identify the closed and forming bars int lastClosedBar = rates_total - 2; int formingBar = rates_total - 1; g_ratesTotal = rates_total; //--- Snapshot the forming bar for the live overlay g_liveOpen = open[formingBar]; g_liveHighPrice = high[formingBar]; g_liveLowPrice = low[formingBar]; g_liveClose = close[formingBar]; //--- Detect a full recalculation bool fullRecompute = (prev_calculated == 0); //--- Ticks that did not close a bar repaint only what moved if(!fullRecompute && time[lastClosedBar] <= g_lastProcessedBarTime) { //--- The view is read on every call, a moved mapping repaints everything if(ReadViewport()) FullRedraw(); //--- Otherwise the overlay only, and only when the close crossed a half bin else if(LiveHalfBin() != g_lastLiveHalfBin) OverlayRedraw(); return(rates_total); } //--- Resolve where processing starts int startBar = 0; if(fullRecompute) { //--- Wipe every piece of state ResetIndicatorState(); //--- Blank the hidden buffers ArrayInitialize(g_pocPriceBuffer, EMPTY_VALUE); ArrayInitialize(g_valueAreaHighBuffer, EMPTY_VALUE); ArrayInitialize(g_valueAreaLowBuffer, EMPTY_VALUE); ArrayInitialize(g_liquidityBelowBuffer, EMPTY_VALUE); ArrayInitialize(g_liquidityAboveBuffer, EMPTY_VALUE); ArrayInitialize(g_imbalanceBuffer, EMPTY_VALUE); //--- Replay only as many columns as the ring holds startBar = InpProfileDepthBars - 1; if(rates_total - InpMaxHistoryBars > startBar) startBar = rates_total - InpMaxHistoryBars; //--- Never start past the last closed bar if(startBar > lastClosedBar) startBar = lastClosedBar; } else { //--- Continue from the bar that was forming last time startBar = prev_calculated - 1; //--- Never step before one full window if(startBar < InpProfileDepthBars - 1) startBar = InpProfileDepthBars - 1; } //--- Process every closed bar that has not been handled yet for(int bar = startBar; bar <= lastClosedBar; bar++) { //--- Skip bars already processed if(time[bar] <= g_lastProcessedBarTime) continue; //--- Allow alerts only for the newest closed bar during live updates bool alertable = (!fullRecompute && bar == lastClosedBar); //--- Run the pipeline ProcessBar(bar, alertable, open, high, low, close, tick_volume, volume); //--- Remember the bar as handled g_lastProcessedBarTime = time[bar]; } //--- Keep the forming bar clean in every buffer g_pocPriceBuffer[formingBar] = EMPTY_VALUE; g_valueAreaHighBuffer[formingBar] = EMPTY_VALUE; g_valueAreaLowBuffer[formingBar] = EMPTY_VALUE; g_liquidityBelowBuffer[formingBar] = EMPTY_VALUE; g_liquidityAboveBuffer[formingBar] = EMPTY_VALUE; g_imbalanceBuffer[formingBar] = EMPTY_VALUE; //--- A new column shifts the picture, repaint everything ReadViewport(); FullRedraw(); //--- Return value of prev_calculated for next call return(rates_total); } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Only chart change events matter here if(id != CHARTEVENT_CHART_CHANGE) return; //--- Repaint only when the mapping actually moved if(ReadViewport()) FullRedraw(); } //+------------------------------------------------------------------+