Every serious trader eventually hits the same wall: you're staring at a chart, price is ranging, and you have no idea where the real liquidity sits. Moving averages tell you trend direction. Bollinger Bands tell you volatility. But neither tells you where the big money actually transacted. That's what a volume profile does, and a fixed range version lets you zoom into exactly the period you care about — not just today's session or the whole chart.
The problem? Most free MQL5 volume profile indicators are either broken, draw on the wrong chart corner, or use so many objects that your terminal lags. And the trading logic around HVN (High Volume Nodes) and LVN (Low Volume Nodes) is rarely explained beyond "buy at support, sell at resistance." That's lazy. This post gives you both: a clean MQL5 implementation you can compile yourself, and a practical framework for actually trading the levels.
I've been building EAs and indicators in MetaTrader for over a decade. I've seen volume profile code that leaks memory, buffers that update on every tick when they should only update on new bars, and traders who treat a single HVN like it's a crystal ball. Let's avoid all of that.
Before we dive into code, one honest caveat: volume profile is a context tool, not a standalone signal generator. It tells you where liquidity has been, not where it's going. The edge comes from combining it with price action, structure, and a solid risk framework. Anyone promising otherwise is selling something.
What Volume Profile Fixed Range Actually Measures
A standard volume indicator plots volume bars below the chart, one bar per candle, aligned in time. A volume profile rotates that perspective 90 degrees. It aggregates volume by price level rather than by time. The result is a horizontal histogram showing how much volume transacted at each price within a defined range.
The "fixed range" part means you manually define the start and end points — either by bar index or by time. This is different from a session volume profile (which auto-selects a session) or a market profile TPO (which uses time-price opportunities). A fixed range gives you full control. You can profile last week's consolidation, a specific news day, or the entire history since a major swing high.
Why does this matter in practice? Imagine you're trading EURUSD on the H1. The last 200 bars show a clear accumulation zone between 1.0850 and 1.0900. A session profile would only show today's action, which might be entirely above that zone. A whole-chart profile would dilute the signal across months of data. A fixed range profile, set to those 200 bars, reveals exactly where the big hands were filling orders. That's actionable intel.
There's another subtlety worth mentioning. When you set a fixed range, you're making a deliberate statement about what market regime you're analyzing. A range that captures a strong downtrend will show a very different volume distribution than one that captures a sideways consolidation. This isn't a bug — it's the point. The same instrument at the same price can have completely different liquidity structures depending on which history you include. That's why I tell traders to think carefully about why they're choosing a particular range before they even open the indicator settings.
HVN and LVN: The Two Levels That Matter
Once the profile is built, two concepts drive most trading decisions:
- High Volume Node (HVN) — the price level (or narrow band) where the most volume traded. This is where price spent the most time and where both buyers and sellers agreed on value. It acts as strong support or resistance, and price tends to return to it.
- Low Volume Node (LVN) — a price level with significantly less volume than surrounding areas. These are "air pockets" or gaps. Price moves through LVNs quickly, and when it returns, it often reverses because there's little resting liquidity to hold it.
Think of HVN as a magnet and LVN as a trapdoor. Price gets pulled toward HVNs because unfilled orders sit there. Price blows through LVNs because nobody's home. The value area (typically the range containing 70% of volume) sits between the two extremes, and that's where most of your trades should originate.
One nuance most traders miss: HVNs and LVNs are not static. If you profile a range that includes a major news event, that day's volume spike will dominate everything else and hide the structure you're looking for. I usually exclude obvious news spikes or event days from my fixed range unless I specifically want to study that event's aftermath. This is where the "fixed range" flexibility pays off — you can slice history any way you want.
Another nuance: the significance of an HVN depends on its width, not just its height. A tall, narrow HVN (say, 10 pips wide on EURUSD) is a precise magnet. A shorter, wider HVN (40 pips wide) is more of a zone. When I mark HVNs on my charts, I always draw a rectangle for wide ones, not just a line. The market respects zones better than exact prices.
MQL5 Implementation: Building the Indicator from Scratch
Let's get into the code. I'm going to show you a working MQL5 volume profile indicator that uses proper buffer handling — meaning it calculates once per new bar, not on every tick, and it uses the indicator buffers correctly so you can later reference these levels in an EA.
This isn't the most feature-complete volume profile you'll ever see. It's intentionally lean. The goal is to give you a solid foundation you can extend — adding volume delta, POC lines, or even a breakout alert — without wading through thousands of lines of someone else's spaghetti code.
Prerequisites and Setup
You'll need MetaEditor (comes with MetaTrader 5). Create a new indicator: File → New → Custom Indicator. Name it VPFR_VolumeProfile. Make sure you select "Indicator in separate window" — we'll draw the histogram there, but the levels will be plotted on the main chart using objects.
Before writing code, understand the key design decision: we're not going to recalculate the entire profile on every tick. That's the #1 performance killer in volume profile indicators. Instead, we calculate once when a new bar opens, and we only re-calculate if the user changes the input range. This makes the indicator smooth even on low-end VPS hosting.
Another design choice worth mentioning: I'm using tick_volume, not real volume. On most retail forex brokers, real volume data isn't available — tick volume is the closest proxy. For futures or stocks traded via MetaTrader 5, you might have access to real volume, but the code works identically either way. Just be aware that tick volume and real volume can diverge in fast markets, especially during news events.
One more setup tip: if you're planning to use this indicator on multiple symbols or timeframes, test it first on a single chart. The object names I use include the chart ID and timeframe, so you won't get collisions, but it's still good practice to verify performance before you go wild with a multi-chart workspace.
Input Parameters
Here's the input block. Notice I've kept it minimal but functional:
| Parameter | Type | Default | Description |
|---|---|---|---|
| StartBar | int | 500 | Bar index (from right) where the fixed range begins. |
| EndBar | int | 50 | Bar index (from right) where the fixed range ends. |
| Rows | int | 50 | Number of price rows (bins) in the profile. Higher = finer detail. |
| ShowHVN | bool | true | Draws a horizontal line at the high volume node. |
| ShowLVN | bool | true | Draws horizontal lines at low volume nodes. |
| LVNThreshold | double | 0.3 | Rows below this fraction of max volume are flagged as LVN. |
The Rows parameter is crucial. Too few rows (like 20) and you'll miss the nuance of the HVN/LVN structure. Too many (like 200) and you'll get noise. I usually start with 50 and adjust based on the price range of the instrument. For a $2 stock, 50 rows gives you 4-cent bins — perfect. For Bitcoin at $60,000, 50 rows gives you $1,200 bins — probably too coarse, so bump it to 100.
There's a trade-off here that deserves attention. Finer bins (more rows) reveal more detail but also more noise. Coarser bins (fewer rows) smooth out the profile but can hide important liquidity clusters. My rule of thumb: aim for bins that are roughly 0.1% to 0.3% of the instrument's price. For EURUSD at 1.10, that's 11 to 33 pips per bin. For gold at 2,400, that's $2.40 to $7.20 per bin. Adjust Rows accordingly.
Let me give you a concrete example. On XAUUSD (gold) at $2,400, if you set StartBar to 300 and EndBar to 20 on the H1 chart, you're profiling about 280 hours of trading. The price range might be $2,350 to $2,450, which is $100. With 50 rows, each bin is $2. That's a reasonable granularity for gold. If you were profiling the M15 chart over the same period, you'd have more bars but a similar price range, so 50 rows still works. The key is to think in terms of price range, not bar count.
Buffer Setup and the Calculation Loop
Here's the heart of the indicator. Note the use of ArraySetAsSeries to ensure the buffers are indexed from the rightmost bar (bar 0 = current). This is a classic MQL5 gotcha — if you forget it, your CopyRates data will be reversed and the profile will look like a mirror image.
//+------------------------------------------------------------------+
//| VPFR_VolumeProfile.mq5 |
//| Fixed Range Volume Profile with HVN/LVN detection |
//+------------------------------------------------------------------+
#property indicator_separate_window
#property indicator_minimum 0
#property indicator_buffers 3
#property indicator_plots 1
#property indicator_label1 "Volume Profile"
#property indicator_type1 DRAW_HISTOGRAM
#property indicator_color1 C'80,160,220'
#property indicator_style1 STYLE_SOLID
#property indicator_width1 2
input int StartBar = 500;
input int EndBar = 50;
input int Rows = 50;
input bool ShowHVN = true;
input bool ShowLVN = true;
input double LVNThreshold = 0.3;
double vpBuffer[];
double hvnPrice = 0.0;
double lvnPrices[];
int OnInit()
{
SetIndexBuffer(0, vpBuffer, INDICATOR_DATA);
ArraySetAsSeries(vpBuffer, true);
ArrayResize(lvnPrices, 0);
return(INIT_SUCCEEDED);
}
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &





