Volume Profile VPVR MQL5: Code Institutional Market Tools

Build a non-repainting VPVR indicator in MQL5 that calculates Point of Control and Value Area correctly. Complete code walkthrough with real trading.

volume-profile-vpvr-mql5-code-institutional

Why Most Volume Profile Indicators on MT5 Are Useless

I've tested over a dozen Volume Profile indicators for MetaTrader 5 in the past three years. Almost all of them repaint, lag by several bars, or calculate volume over a fixed period instead of visible price range. If you've ever tried to build an EA around one of these, you know the pain: the indicator values shift after the fact, your backtest results are fiction, and live trading hits you with nasty surprises.

VPVR (Volume Profile Visible Range) is different. It shows you exactly where volume traded within whatever price range is currently visible on your chart. No hidden recalculation, no look-ahead bias — just a snapshot of where the market has been active. Institutional traders use this to identify high-volume nodes (support/resistance zones) and low-volume nodes (potential breakout areas). The problem? Most MQL5 implementations either cost a fortune or are coded so poorly they're unusable for automation.

Let's fix that. I'll walk you through building a proper VPVR indicator in MQL5 — one that doesn't repaint, calculates Point of Control (POC) and Value Area correctly, and can actually be used in an EA without driving you insane. I've been coding MQL5 indicators for six years, and this is the approach I've settled on after throwing out three failed versions.

What VPVR Actually Tells You

VPVR is a histogram plotted along the vertical price axis. Each horizontal bar represents the total volume traded at that specific price level during the visible range of bars on your chart. The key components:

  • Point of Control (POC): The price level with the highest volume. This is where most trading occurred — think of it as the market's center of gravity.
  • Value Area (VA): The range of prices where a specified percentage of total volume traded (usually 70%). This tells you where the market "values" the asset.
  • High Volume Nodes (HVN): Price levels with above-average volume. These act as support/resistance zones.
  • Low Volume Nodes (LVN): Price levels with below-average volume. Price tends to move through these quickly — they're breakout zones.

The magic of VPVR over traditional volume indicators is that it's price-specific. A standard volume bar at the bottom of your chart tells you total volume for that period. VPVR tells you exactly which prices saw activity. That's a massive difference for identifying precise entry and exit levels.

Here's a concrete example: on EURUSD M15, you might see a volume spike at the bottom of the chart. But was that volume at 1.1050 or 1.1070? Those are very different levels for placing a stop. VPVR answers that question instantly.

Building the VPVR Indicator in MQL5

Let's get into the code. I'm assuming you have basic MQL5 knowledge — you know what OnCalculate does and can add an indicator to a chart. I'll focus on the parts that matter: data collection, histogram drawing, and POC/VA calculation. Open MetaEditor, create a new Custom Indicator, and let's start.

Step 1: Setting Up Input Parameters

These are the inputs you'll expose to the user. Get them right from the start — changing them later means recompiling and reattaching.

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input int                InpBarsToScan = 100;        // Visible bars to scan
input ENUM_TIMEFRAMES    InpTimeframe  = PERIOD_CURRENT; // Timeframe
input double             InpVAPercent  = 70.0;       // Value Area percentage
input color              InpPOCColor   = clrGold;     // POC line color
input color              InpVAHighColor = clrDodgerBlue; // VA High line color
input color              InpVALowColor  = clrDodgerBlue; // VA Low line color
input bool               InpShowVolumeProfile = true; // Show histogram
input int                InpProfileWidth = 50;       // Histogram width in pixels

The InpBarsToScan parameter is critical. Unlike fixed-period volume profiles, VPVR scans only the bars visible on your chart. I default to 100 bars — that's about 4 days on a 1-hour chart. You'll want to adjust this based on your trading style. Scalpers might use 20-30 bars; swing traders might use 200+. I've seen traders use 500 on daily charts, but that can slow things down on older hardware.

One thing I learned the hard way: always use PERIOD_CURRENT as default for the timeframe. If you hardcode M15, the indicator won't adapt when you switch chart periods. Let the user override it if they want a different timeframe for the profile than the chart shows.

Step 2: Collecting Tick Volume Data

MQL5 gives us CopyTickVolume to get volume data. Here's the core function that builds our price-to-volume map:

//+------------------------------------------------------------------+
//| Build volume profile from visible range                          |
//+------------------------------------------------------------------+
bool BuildVolumeProfile(double &priceLevels[], long &volumes[], int &levelsCount)
{
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   
   int copied = CopyRates(_Symbol, InpTimeframe, 0, InpBarsToScan, rates);
   if(copied <= 0) return false;
   
   // Find price range
   double minPrice = rates[0].low;
   double maxPrice = rates[0].high;
   for(int i = 1; i < copied; i++)
   {
      if(rates[i].low < minPrice) minPrice = rates[i].low;
      if(rates[i].high > maxPrice) maxPrice = rates[i].high;
   }
   
   // Calculate tick size and number of levels
   double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   if(tickSize == 0) tickSize = _Point;
   
   int totalLevels = (int)((maxPrice - minPrice) / tickSize) + 1;
   ArrayResize(priceLevels, totalLevels);
   ArrayResize(volumes, totalLevels);
   ArrayInitialize(volumes, 0);
   
   // Fill price levels
   for(int i = 0; i < totalLevels; i++)
      priceLevels[i] = minPrice + i * tickSize;
   
   // Accumulate volume at each price level
   for(int i = 0; i < copied; i++)
   {
      double barLow = rates[i].low;
      double barHigh = rates[i].high;
      long barVolume = rates[i].tick_volume;
      
      int lowIdx = (int)((barLow - minPrice) / tickSize);
      int highIdx = (int)((barHigh - minPrice) / tickSize);
      
      if(lowIdx < 0) lowIdx = 0;
      if(highIdx >= totalLevels) highIdx = totalLevels - 1;
      
      // Distribute volume evenly across price levels within the bar
      int range = highIdx - lowIdx + 1;
      if(range > 0)
      {
         long volPerLevel = barVolume / range;
         for(int j = lowIdx; j <= highIdx; j++)
            volumes[j] += volPerLevel;
      }
   }
   
   levelsCount = totalLevels;
   return true;
}

Notice I'm distributing tick volume evenly across all price levels within each bar. This isn't perfect — we don't know exactly where within the bar the volume traded — but it's the standard approach and works well enough for most purposes. If you have tick data, you can get exact distribution, but that's overkill for 99% of traders.

Edge case to watch: when tickSize is extremely small (like 0.00001 for JPY pairs), totalLevels can blow up. On USDJPY with a 100-pip range, that's 10,000 levels. Your indicator will still run, but drawing 10,000 objects will lag. I cap levels at 2000 in production code — anything beyond that and you're just adding noise.

Step 3: Calculating POC and Value Area

This is where most custom indicators fail. Here's the correct calculation:

//+------------------------------------------------------------------+
//| Find Point of Control and Value Area boundaries                  |
//+------------------------------------------------------------------+
void CalculatePOCandVA(double &priceLevels[], long &volumes[], int levelsCount,
                       double &pocPrice, double &vaHigh, double &vaLow)
{
   // Find POC (level with maximum volume)
   long maxVol = 0;
   int pocIdx = 0;
   for(int i = 0; i < levelsCount; i++)
   {
      if(volumes[i] > maxVol)
      {
         maxVol = volumes[i];
         pocIdx = i;
      }
   }
   pocPrice = priceLevels[pocIdx];
   
   // Calculate total volume
   long totalVol = 0;
   for(int i = 0; i < levelsCount; i++)
      totalVol += volumes[i];
   
   // Calculate Value Area (default 70%)
   double targetVol = totalVol * (InpVAPercent / 100.0);
   long cumulativeVol = volumes[pocIdx];
   
   int upperIdx = pocIdx;
   int lowerIdx = pocIdx;
   
   // Expand outward from POC until we reach target volume
   while(cumulativeVol < targetVol)
   {
      long upperVol = (upperIdx + 1 < levelsCount) ? volumes[upperIdx + 1] : 0;
      long lowerVol = (lowerIdx - 1 >= 0) ? volumes[lowerIdx - 1] : 0;
      
      if(upperVol >= lowerVol && upperIdx + 1 < levelsCount)
      {
         upperIdx++;
         cumulativeVol += volumes[upperIdx];
      }
      else if(lowerIdx - 1 >= 0)
      {
         lowerIdx--;
         cumulativeVol += volumes[lowerIdx];
      }
      else
         break;
   }
   
   vaHigh = priceLevels[upperIdx];
   vaLow = priceLevels[lowerIdx];
}

The key insight: we start at the POC and expand outward, always adding the next highest-volume level. This ensures we capture the densest volume area first. Some implementations just expand evenly in both directions — that's wrong and will give you a skewed value area. I've seen indicators that expand 5 levels up, 5 levels down regardless of volume. That's not a value area, it's a fixed range.

One nuance: if two adjacent levels have identical volume, we pick the upper one first. That's arbitrary but consistent. In practice, it rarely matters because volume distributions are rarely perfectly symmetric.

Step 4: Drawing the Histogram

For drawing, I use OBJ_LABEL objects positioned in the chart's price scale. Here's the drawing function:

//+------------------------------------------------------------------+
//| Draw volume profile histogram                                    |
//+------------------------------------------------------------------+
void DrawProfile(double &priceLevels[], long &volumes[], int levelsCount,
                 double pocPrice, double vaHigh, double vaLow)
{
   // Clear previous objects
   ObjectsDeleteAll(0, "VPVR_");
   
   if(!InpShowVolumeProfile) return;
   
   // Normalize volumes for display
   long maxVol = 0;
   for(int i = 0; i < levelsCount; i++)
      if(volumes[i] > maxVol) maxVol = volumes[i];
   
   double scaleFactor = (double)InpProfileWidth / maxVol;
   
   // Draw each level as a horizontal line
   for(int i = 0; i < levelsCount; i++)
   {
      if(volumes[i] == 0) continue;
      
      string objName = "VPVR_Level_" + IntegerToString(i);
      double barWidth = volumes[i] * scaleFactor;
      
      // Use trendline for each level (horizontal)
      ObjectCreate(0, objName, OBJ_TREND, 0, 0, 0);
      ObjectSetDouble(0, objName, OBJPROP_PRICE, priceLevels[i]);
      
      // Set line width proportional to volume
      ObjectSetInteger(0, objName, OBJPROP_WIDTH, (int)MathMax(1, barWidth / 5));
      
      // Color based on value area
      color lineColor = clrGray;
      if(priceLevels[i] >= vaLow && priceLevels[i] <= vaHigh)
         lineColor = clrLightBlue;
      if(priceLevels[i] == pocPrice)
         lineColor = InpPOCColor;
         
      ObjectSetInteger(0, objName, OBJPROP_COLOR, lineColor);
      ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_SOLID);
      ObjectSetInteger(0, objName, OBJPROP_BACK, true);
      ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
   }
   
   // Draw POC and VA lines as horizontal rays
   DrawHLine("VPVR_POC", pocPrice, InpPOCColor, 2, STYLE_SOLID);
   DrawHLine("VPVR_VA_High", vaHigh, InpVAHighColor, 1, STYLE_DASH);
   DrawHLine("VPVR_VA_Low", vaLow, InpVALowColor, 1, STYLE_DASH);
}

I use OBJ_TREND instead of OBJ_HLINE for the histogram bars because it gives me more control over width. The POC gets a solid gold line, value area boundaries get dashed blue lines, and levels inside the value area are shaded lighter.

You'll need a helper function DrawHLine for the POC and VA lines. Here's a quick one:

void DrawHLine(string name, double price, color clr, int width, ENUM_LINE_STYLE style)
{
   ObjectCreate(0, name, OBJ_HLINE, 0, 0, price);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
   ObjectSetInteger(0, name, OBJPROP_STYLE, style);
   ObjectSetInteger(0, name, OBJPROP_BACK, false);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
}

Step 5: Handling Chart Changes

VPVR must recalculate when the visible range changes. Hook into OnChartEvent:

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if(id == CHARTEVENT_CHART_CHANGE || id == CHARTEVENT_OBJECT_DELETE)
   {
      // Recalculate on zoom/scroll
      EventSetTimer(1); // Debounce with 1-second timer
   }
}

Don't recalculate on every chart event — that'll kill performance. Use a timer to debounce. I set a 1-second delay so rapid scrolling doesn't trigger constant recalculations. Some traders prefer 0.5 seconds for faster updates, but I find 1 second smooth enough.

One gotcha: CHARTEVENT_OBJECT_DELETE fires when you delete objects manually. If you're cleaning up old VPVR objects in the draw function, this event can trigger itself. I add a flag to prevent re-entrancy:

bool recalculating = false;

void OnChartEvent(...)
{
   if(recalculating) return;
   if(id == CHARTEVENT_CHART_CHANGE)
   {
      recalculating = true;
      EventSetTimer(1);
   }
}

Then reset the flag after the timer fires.

Using VPVR in Your Trading

Once the indicator is on your chart, here's how to read it. I'll give you specific setups I've used, not generic advice.

High Volume Node (HVN) Trading

When price approaches a HVN from above, expect resistance. From below, expect support. I look for price to test the value area edge, then enter on a rejection candle. For example, if price touches the VA High and forms a bearish engulfing, I take a short with a stop above the HVN.

Want to build your own version?

Recreate similar entry logic, risk rules, and filters in TradingBotMaker—no MQL coding. Start free.

Community

Clap for the article and open comments only when you want to read them.

0 claps0 comments

Related articles