301 行
无行尾
10 KiB
MQL5
301 行
无行尾
10 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| Article-22342-Liquidity-Spectrum-Volume-Profile-Indicator.mq5 |
|
|
//| Abioye Israel Pelumi |
|
|
//| https://Algoyin.com |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Abioye Israel Pelumi"
|
|
#property link "https://linktr.ee/abioyeisraelpelumi"
|
|
#property version "1.00"
|
|
#property indicator_chart_window
|
|
#property indicator_plots 0
|
|
|
|
//--- INPUT PARAMETERS
|
|
input int InpLookback = 100; // Number of bars used for calculation
|
|
input bool InpVolumeProfile = true; // Display volume profile boxes
|
|
input bool InpLiqLevels = true; // Display POC (liquidity) lines
|
|
|
|
//--- CONSTANTS
|
|
#define N_BINS 100 // Number of price levels (bins)
|
|
#define MAX_BAR_WIDTH 50 // Maximum horizontal width of profile
|
|
#define OBJ_PREFIX "VP_" // Prefix for all chart objects
|
|
#define POC_THRESHOLD 25 // Threshold for drawing POC lines
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Convert "bars ago" into actual chart time |
|
|
//| This allows drawing objects into the past or future |
|
|
//+------------------------------------------------------------------+
|
|
datetime BarsAgoToTime(const datetime ×[], int copiedBars,
|
|
int barsAgo, long barDur)
|
|
{
|
|
//--- Case 1: within available history
|
|
if(barsAgo >= 0 && barsAgo < copiedBars)
|
|
return times[barsAgo];
|
|
//--- Case 2: future projection (negative index)
|
|
if(barsAgo < 0)
|
|
return (datetime)((long)times[0] - (long)barsAgo * barDur); // barsAgo is negative
|
|
//--- Case 3: beyond copied range
|
|
return (datetime)((long)times[copiedBars-1] - (long)(barsAgo - copiedBars + 1) * barDur);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Create or update a rectangle (volume profile bar) |
|
|
//| Returns true if a new object was created |
|
|
//+------------------------------------------------------------------+
|
|
bool DrawBox(const string name,
|
|
datetime x1, double yTop,
|
|
datetime x2, double yBot,
|
|
color fillCol)
|
|
{
|
|
bool created = false;
|
|
|
|
//--- Create object only if it does not exist
|
|
if(ObjectFind(0, name) < 0)
|
|
{
|
|
ObjectCreate(0, name, OBJ_RECTANGLE, 0, x1, yTop, x2, yBot);
|
|
created = true;
|
|
}
|
|
|
|
//--- Update object properties
|
|
ObjectSetInteger(0, name, OBJPROP_COLOR, fillCol);
|
|
ObjectSetInteger(0, name, OBJPROP_BGCOLOR, fillCol);
|
|
ObjectSetInteger(0, name, OBJPROP_FILL, true);
|
|
ObjectSetInteger(0, name, OBJPROP_BACK, true);
|
|
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
|
|
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
|
|
|
|
//--- Update position
|
|
ObjectMove(0, name, 0, x1, yTop);
|
|
ObjectMove(0, name, 1, x2, yBot);
|
|
|
|
return created;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Create or update a horizontal POC (liquidity) line |
|
|
//| Returns true if a new object was created |
|
|
//+------------------------------------------------------------------+
|
|
bool DrawTrend(const string name,
|
|
datetime x1, double y,
|
|
datetime x2,
|
|
color col, int w)
|
|
{
|
|
bool created = false;
|
|
|
|
//--- Create only if missing
|
|
if(ObjectFind(0, name) < 0)
|
|
{
|
|
ObjectCreate(0, name, OBJ_TREND, 0, x1, y, x2, y);
|
|
created = true;
|
|
}
|
|
|
|
//--- Update properties
|
|
ObjectSetInteger(0, name, OBJPROP_COLOR, col);
|
|
ObjectSetInteger(0, name, OBJPROP_WIDTH, w);
|
|
ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false);
|
|
ObjectSetInteger(0, name, OBJPROP_BACK, true);
|
|
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
|
|
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
|
|
|
|
//--- Update position
|
|
ObjectMove(0, name, 0, x1, y);
|
|
ObjectMove(0, name, 1, x2, y);
|
|
|
|
return created;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Delete all indicator objects |
|
|
//+------------------------------------------------------------------+
|
|
void DeleteAllObjects()
|
|
{
|
|
for(int i = ObjectsTotal(0, 0, -1) - 1; i >= 0; i--)
|
|
{
|
|
string n = ObjectName(0, i, 0, -1);
|
|
if(StringFind(n, OBJ_PREFIX) == 0)
|
|
ObjectDelete(0, n);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Core calculation function |
|
|
//| Builds and draws the volume profile |
|
|
//+------------------------------------------------------------------+
|
|
void RecalcVolumeProfile(bool &need_redraw)
|
|
{
|
|
DeleteAllObjects();
|
|
|
|
//--- Exit early if both features are disabled
|
|
if(!InpVolumeProfile && !InpLiqLevels)
|
|
return;
|
|
int totalBars = Bars(_Symbol, _Period);
|
|
int lookback = MathMin(InpLookback, totalBars - 1);
|
|
if(lookback < 2)
|
|
return;
|
|
|
|
//--- Prepare arrays for market data
|
|
double hi[], lo[], cl[];
|
|
long vol[];
|
|
datetime tm[];
|
|
ArraySetAsSeries(hi,true);
|
|
ArraySetAsSeries(lo,true);
|
|
ArraySetAsSeries(cl,true);
|
|
ArraySetAsSeries(vol,true);
|
|
ArraySetAsSeries(tm,true);
|
|
|
|
//--- Copy required data from terminal
|
|
if(CopyHigh(_Symbol,_Period,0,lookback,hi) < lookback)
|
|
return;
|
|
if(CopyLow(_Symbol,_Period,0,lookback,lo) < lookback)
|
|
return;
|
|
if(CopyClose(_Symbol,_Period,0,lookback,cl) < lookback)
|
|
return;
|
|
if(CopyTime(_Symbol,_Period,0,lookback,tm) < lookback)
|
|
return;
|
|
//--- Prefer tick volume, fallback to real volume
|
|
if(CopyTickVolume(_Symbol,_Period,0,lookback,vol) < lookback)
|
|
if(CopyRealVolume(_Symbol,_Period,0,lookback,vol) < lookback)
|
|
return;
|
|
|
|
//--- Determine price range
|
|
double priceMax = hi[ArrayMaximum(hi, 0, lookback)];
|
|
double priceMin = lo[ArrayMinimum(lo, 0, lookback)];
|
|
if(priceMax <= priceMin)
|
|
return;
|
|
|
|
int idxHigh = ArrayMaximum(hi, 0, lookback);
|
|
int idxLow = ArrayMinimum(lo, 0, lookback);
|
|
//--- Divide price range into bins
|
|
double step = (priceMax - priceMin) / N_BINS;
|
|
|
|
//--- build bins
|
|
double bins[];
|
|
ArrayResize(bins, N_BINS);
|
|
ArrayInitialize(bins, 0.0);
|
|
|
|
//--- Assign volume to bins
|
|
for(int i = 0; i < N_BINS; i++)
|
|
{
|
|
double lower = priceMin + step * i;
|
|
double upper = lower + step;
|
|
for(int j = 0; j < lookback; j++)
|
|
{
|
|
double c = cl[j];
|
|
if(c >= lower - step && c <= upper + step)
|
|
{
|
|
bins[i] += (double)vol[j];
|
|
}
|
|
}
|
|
}
|
|
|
|
//--- Find maximum volume bin
|
|
double maxBin = 0;
|
|
for(int i = 0; i < N_BINS; i++)
|
|
if(bins[i] > maxBin)
|
|
maxBin = bins[i];
|
|
if(maxBin == 0)
|
|
return;
|
|
|
|
|
|
|
|
//--- Define drawing boundaries
|
|
int profileLeftBarsAgo = lookback + MAX_BAR_WIDTH;
|
|
long barDur = (long)PeriodSeconds(_Period);
|
|
datetime profileLeftTime = BarsAgoToTime(tm, lookback, profileLeftBarsAgo, barDur);
|
|
|
|
// POC right end = bar_index + 5 (5 bars into the future)
|
|
datetime pocRightTime = BarsAgoToTime(tm, lookback, -5, barDur);
|
|
|
|
//--- Loop through bins and draw
|
|
for(int i = 0; i < N_BINS; i++)
|
|
{
|
|
double lower = priceMin + step * i;
|
|
double upper = lower + step;
|
|
double mid = (lower + upper) * 0.5;
|
|
|
|
//--- Normalize bin width
|
|
int val = (int)(bins[i] / maxBin * (double)MAX_BAR_WIDTH);
|
|
if(val < 1)
|
|
continue;
|
|
|
|
int profileRightBarsAgo = profileLeftBarsAgo - val;
|
|
datetime profileRightTime = BarsAgoToTime(tm, lookback, profileRightBarsAgo, barDur);
|
|
|
|
color box_line_clr = (val >= 40) ? clrBlue : (val > 30) ? clrGreen : (val > 20) ? clrGray : (val > 10) ? clrOlive : clrAquamarine;
|
|
//--- Draw volume box
|
|
if(InpVolumeProfile)
|
|
{
|
|
string boxName = OBJ_PREFIX + "BOX_" + IntegerToString(i);
|
|
if(DrawBox(boxName, profileRightTime, upper, profileLeftTime, lower, box_line_clr))
|
|
need_redraw = true;
|
|
}
|
|
|
|
//--- Draw POC line for strong bins
|
|
// POC lines for high-volume bins
|
|
if(val > POC_THRESHOLD && InpLiqLevels)
|
|
{
|
|
string lineName = OBJ_PREFIX + "POC_" + IntegerToString(i);
|
|
int lineW = (val > 45) ? 3 : (val > 35) ? 2 : 1;
|
|
if(DrawTrend(lineName, pocRightTime, mid, profileLeftTime, box_line_clr, lineW))
|
|
need_redraw = true;
|
|
}
|
|
}
|
|
|
|
// Border right = bar_index + 5 (same as POC)
|
|
datetime borderRightTime = pocRightTime;
|
|
datetime borderLeftTime = BarsAgoToTime(tm, lookback,
|
|
profileLeftBarsAgo + MAX_BAR_WIDTH, barDur);
|
|
//--- outer border box
|
|
if(DrawBox(OBJ_PREFIX+"BORDER", borderRightTime, priceMax, borderLeftTime, priceMin, clrSnow))
|
|
need_redraw = true;
|
|
}
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Custom indicator initialization function |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
bool redraw = false;
|
|
//--- Initial calculation so indicator appears immediately
|
|
RecalcVolumeProfile(redraw);
|
|
|
|
if(redraw)
|
|
ChartRedraw(0);
|
|
//---
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Deinitialization |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
// ObjectsDeleteAll(0);
|
|
DeleteAllObjects();
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| 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[])
|
|
{
|
|
//---
|
|
bool need_redraw = false;
|
|
|
|
//--- Recalculate only when new data arrives
|
|
if(prev_calculated == 0 || rates_total != prev_calculated)
|
|
RecalcVolumeProfile(need_redraw);
|
|
|
|
//--- Redraw only if something changed
|
|
if(need_redraw)
|
|
ChartRedraw(0);
|
|
|
|
//--- return value of prev_calculated for next call
|
|
return(rates_total);
|
|
}
|
|
//+------------------------------------------------------------------+ |