Article-24683-Creating-Cust.../Volume Profile Heatmap.mq5

1263 lines
58 KiB
MQL5

2026-10-22 16:05:24 +03:00
//+------------------------------------------------------------------+
//| 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 <Canvas/Canvas.mqh>
//+------------------------------------------------------------------+
//| 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();
}
2026-10-22 16:05:24 +03:00
//+------------------------------------------------------------------+