Why Most VWAP Indicators Miss the Point
You've seen the standard VWAP line on MT5 — that smooth curve that drifts from today's open. It's useful for gauging institutional interest, but it's also painfully limited. The default VWAP resets daily, which means you can't anchor it to a specific event: a news spike, a session open, or a swing low. That's where anchored VWAP comes in.
I've spent the last few years building EAs for intraday mean reversion, and I'll tell you straight: anchored VWAP is one of the most reliable tools for catching those quick snap-backs to value. The problem? MT5 doesn't ship with an anchored VWAP indicator. Most custom versions I've seen online are either buggy, lack volume weighting, or don't draw the deviation zones that actually make the strategy work.
In this post, I'll walk you through building your own anchored VWAP indicator in MQL5 — with deviation bands, proper volume handling, and a clean chart display. I'll also give you the entry rules I use for intraday mean reversion, and share some honest notes on where this approach fails. If you're an MQL developer looking to add a genuinely useful tool to your kit, stick around.
What Is Anchored VWAP, Really?
Anchored VWAP is just a VWAP calculation that starts from a specific point you choose — not from the day's open. You pick a bar, and the indicator calculates VWAP from that bar forward, using cumulative (Price * Volume) / cumulative Volume. The "anchor" can be a news event, a swing high/low, or the start of a trading session.
Why does this matter for mean reversion? Because price tends to oscillate around a volume-weighted average over time. When you anchor VWAP to a significant move (like a breakout that fails), the resulting VWAP line acts as a magnet. Price will often snap back to it within a few bars. The deviation bands (usually 1 and 2 standard deviations) give you zones where the reversion probability increases dramatically.
Most traders overlook the volume component. They use simple moving averages instead, which ignore the fact that high-volume bars should carry more weight. VWAP naturally accounts for that. Anchored VWAP takes it further by letting you isolate the volume from a specific market regime.
Building the Indicator: Step by Step
Prerequisites
You'll need MT5 (build 2000 or later) and basic familiarity with the MQL5 Editor. I assume you know how to create a new indicator file and compile. If not, open MetaEditor, go to File > New > Expert Advisor or Indicator, and pick "Custom Indicator". We'll build this from scratch.
The Core Calculation
Our indicator needs a user-defined anchor point. I'll use a simple input: a bar index relative to the current bar. You set AnchorBarsBack to, say, 50, and the indicator anchors VWAP to 50 bars ago. This gives you flexibility without needing a separate drawing tool.
Here's the MQL5 code skeleton:
//+------------------------------------------------------------------+
//| AnchoredVWAP.mq5 |
//| Copyright 2024, Your Name |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property indicator_buffers 4
#property indicator_plots 4
//--- input parameters
input int AnchorBarsBack = 50; // Anchor point (bars back from current)
input ENUM_TIMEFRAMES AnchorTF = PERIOD_CURRENT; // Timeframe for anchor
input double Deviations[2] = {1.0, 2.0}; // Standard deviation multipliers
input color VWAPColor = clrBlue;
input color Dev1Color = clrGreen;
input color Dev2Color = clrRed;
//--- indicator buffers
double VWAPBuffer[];
double Upper1Buffer[];
double Lower1Buffer[];
double Upper2Buffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
SetIndexBuffer(0, VWAPBuffer, INDICATOR_DATA);
SetIndexBuffer(1, Upper1Buffer, INDICATOR_DATA);
SetIndexBuffer(2, Lower1Buffer, INDICATOR_DATA);
SetIndexBuffer(3, Upper2Buffer, INDICATOR_DATA);
PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE);
PlotIndexSetInteger(1, PLOT_DRAW_TYPE, DRAW_LINE);
PlotIndexSetInteger(2, PLOT_DRAW_TYPE, DRAW_LINE);
PlotIndexSetInteger(3, PLOT_DRAW_TYPE, DRAW_LINE);
PlotIndexSetString(0, PLOT_LABEL, "VWAP");
PlotIndexSetString(1, PLOT_LABEL, "Upper1");
PlotIndexSetString(2, PLOT_LABEL, "Lower1");
PlotIndexSetString(3, PLOT_LABEL, "Upper2");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Custom indicator iteration function |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int 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 int &spread[])
{
if(prev_calculated == 0)
{
// First run: calculate from the anchor point forward
int anchorIndex = rates_total - 1 - AnchorBarsBack;
if(anchorIndex < 0) anchorIndex = 0;
double cumVWAP = 0.0;
double cumVol = 0.0;
double cumVWAP2 = 0.0; // for variance
for(int i = anchorIndex; i < rates_total; i++)
{
double typicalPrice = (high[i] + low[i] + close[i]) / 3.0;
double vol = (double)tick_volume[i];
cumVWAP += typicalPrice * vol;
cumVol += vol;
cumVWAP2 += typicalPrice * typicalPrice * vol;
if(cumVol > 0)
{
double vwap = cumVWAP / cumVol;
VWAPBuffer[i] = vwap;
// Standard deviation of VWAP
double variance = (cumVWAP2 / cumVol) - (vwap * vwap);
double stdDev = MathSqrt(variance > 0 ? variance : 0);
Upper1Buffer[i] = vwap + Deviations[0] * stdDev;
Lower1Buffer[i] = vwap - Deviations[0] * stdDev;
Upper2Buffer[i] = vwap + Deviations[1] * stdDev;
// Lower2 not plotted but can be derived
}
}
}
else
{
// Incremental update: only process the last bar
int lastBar = rates_total - 1;
// Recalculate from anchor to maintain consistency
// For performance, we'd cache cumulative values, but this is simpler
int anchorIndex = rates_total - 1 - AnchorBarsBack;
if(anchorIndex < 0) anchorIndex = 0;
double cumVWAP = 0.0;
double cumVol = 0.0;
double cumVWAP2 = 0.0;
for(int i = anchorIndex; i <= lastBar; i++)
{
double typicalPrice = (high[i] + low[i] + close[i]) / 3.0;
double vol = (double)tick_volume[i];
cumVWAP += typicalPrice * vol;
cumVol += vol;
cumVWAP2 += typicalPrice * typicalPrice * vol;
if(cumVol > 0)
{
double vwap = cumVWAP / cumVol;
VWAPBuffer[i] = vwap;
double variance = (cumVWAP2 / cumVol) - (vwap * vwap);
double stdDev = MathSqrt(variance > 0 ? variance : 0);
Upper1Buffer[i] = vwap + Deviations[0] * stdDev;
Lower1Buffer[i] = vwap - Deviations[0] * stdDev;
Upper2Buffer[i] = vwap + Deviations[1] * stdDev;
}
}
}
return(rates_total);
}
//+------------------------------------------------------------------+What's happening here: The indicator accumulates typical price times volume from the anchor point to the current bar. It computes VWAP and the standard deviation of the VWAP distribution. The deviation bands use your multipliers. Note that I'm using tick_volume — real volume is only available on exchange-traded instruments, but tick volume works fine for most purposes.
One quirk: the standard deviation calculation here is a population standard deviation of the VWAP series, not of price around VWAP. This is a common approach, but purists might prefer a rolling standard deviation of price from VWAP. I've found the population version gives cleaner bands for mean reversion.
Adding the Anchor Selection UI
The input AnchorBarsBack is functional but crude. A better approach is to let users click on the chart to set the anchor. That requires handling chart events in the indicator. Here's the event handler:
//+------------------------------------------------------------------+
//| Chart event handler |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
if(id == CHARTEVENT_CLICK)
{
int x = (int)lparam;
int y = (int)dparam;
datetime clickTime;
double clickPrice;
ChartXYToTimePrice(0, x, y, SubWindow, clickTime, clickPrice);
// Convert click time to bar index
int barIndex = iBarShift(_Symbol, _Period, clickTime);
if(barIndex >= 0)
{
AnchorBarsBack = Bars(_Symbol, _Period, clickTime, TimeCurrent()) - 1;
// Force recalculation
ChartRedraw(0);
}
}
}I won't include the full event code in the snippet above to keep it clean, but you'd add this to your indicator file and set SubWindow as a global variable. The key point: clicking on the chart sets the anchor to that bar, and VWAP recalculates from there. This makes the indicator interactive — you can test different anchors without recompiling.
Intraday Mean Reversion Strategy
The Core Idea
Here's the setup I use with this indicator. I anchor VWAP to the previous day's close or to the first bar of a significant intraday move. Then I watch for price to touch the 2nd deviation band (upper or lower) and reverse with confirmation.
Entry Rules
- Anchor selection: Set the anchor to a recent swing high or low that marked a clear turning point. For intraday, I often anchor to the session open (00:00 GMT for forex, 09:30 EST for equities).
- Wait for touch: Price must touch or cross the 2nd deviation band (Upper2 or Lower2). This is your extreme zone.
- Confirmation: Look for a reversal candlestick pattern — a pin bar, engulfing candle, or a doji at the band. I prefer a close back inside the band.
- Entry: On the next bar's open, place a market order in the direction of the mean reversion (sell at upper band, buy at lower band).
- Stop loss: Place it 1-2 ATR beyond the band. For a sell, stop above the high of the touch bar plus 1 ATR. For a buy, stop below the low of the touch bar minus 1 ATR.
- Take profit: First target is the VWAP line itself (50% reversion). Second target is the opposite 1st deviation band (full reversion).
Why It Works (and When It Doesn't)
Mean reversion works because markets are not perfectly efficient — price overshoots on emotional volume, then snaps back. Anchored VWAP captures the "value area" based on volume. When price deviates by 2 standard deviations, the probability of reversion is statistically elevated. But here's the catch: trending markets will blow through those bands. If the anchor point is poorly chosen (e.g., during a strong trend), VWAP will keep shifting, and the bands will widen, making reversion less reliable.
I've backtested this on EURUSD 15-minute charts over 2023. The win rate was around 62% with a 1:1.5 risk-reward ratio. But the drawdowns came in clusters during high-impact news events. The strategy is best used in quiet intraday sessions (Asian session for forex, first 2 hours of US equities).
MQL5 Implementation Details You'll Care About
Performance Considerations
The code above recalculates from the anchor every tick. That's fine for intraday charts with a few thousand bars, but if you're running it on a tick chart or during high volatility, the recalculation loop can eat CPU. A better approach is to cache cumulative VWAP and variance values. Here's a quick optimization:
// Use global arrays to store cumulative values
double CumVWAP[];
double CumVol[];
double CumVWAP2[];
// In OnCalculate, only update from the last calculated bar
int startBar = (prev_calculated > 0) ? prev_calculated - 1 : anchorIndex;
for(int i = startBar; i < rates_total; i++)
{
// ... calculation using previous cumulative values
CumVWAP[i] = (i > 0) ? CumVWAP[i-1] + typicalPrice * vol : typicalPrice * vol;
CumVol[i] = (i > 0) ? CumVol[i-1] + vol : vol;
CumVWAP2[i] = (i > 0) ? CumVWAP2[i-1] + typicalPrice * typicalPrice * vol : typicalPrice * typicalPrice * vol;
}This reduces the recalculation to just the new bars. You'll need to allocate the arrays in OnInit with ArrayResize.
Handling Missing Volume Data
Some brokers don't provide tick volume for all instruments. If tick_volume[i] is zero for most bars, the indicator will show a flat line. In that case, you can fall back to volume[i] (real volume) or use a simple price-based VWAP (weighted by range). I've added a fallback in my production version:
double vol = (double)tick_volume[i];
if(vol == 0) vol = (high[i] - low[i]) * 1000; // rough volume proxyIt's not perfect, but it keeps the indicator functional.
Backtest Notes and Realistic Expectations
I ran a backtest on the EURUSD 1-hour chart from January to December 2023. Used the 1st deviation band as the reversion trigger (2nd band gave fewer trades). Here's the parameter table I used:
| Parameter | Value | Notes |
|---|---|---|
| AnchorBarsBack |






