1263 lines
No EOL
58 KiB
MQL5
1263 lines
No EOL
58 KiB
MQL5
//+------------------------------------------------------------------+
| |
//| 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();
| |
}
| |
| |
//+------------------------------------------------------------------+ |