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:52 +03:00 | | |
|
2026-10-22 16:05:24 +03:00 | | | //+------------------------------------------------------------------+
|