VWAP Mean Reversion Strategy
This strategy fades moves away from the volume weighted average price. ATR is used to gauge how far price must deviate from VWAP before a reversal trade is considered.
Testing indicates an average annual return of about 58%. It performs best in the stocks market.
A long position opens when price drops below VWAP by more than K times the ATR. A short is taken when price rallies above VWAP by the same amount. Trades exit as soon as price returns to the VWAP line.
The approach is designed for intraday traders who expect prices to oscillate around VWAP rather than trend strongly. Stops sized as a multiple of ATR help keep losses controlled if the move continues against the trade.
Details
- Entry Criteria:
- Long: Close < VWAP - K * ATR
- Short: Close > VWAP + K * ATR
- Long/Short: Both sides.
- Exit Criteria:
- Long: Exit when close >= VWAP
- Short: Exit when close <= VWAP
- Stops: Yes, ATR-based stop.
- Default Values:
K= 2.0mCandleType= TimeSpan.FromMinutes(5)AtrPeriod= 14
- Filters:
- Category: Mean Reversion
- Direction: Both
- Indicators: VWAP, ATR
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// VWAP Mean Reversion Strategy.
/// Enter when price deviates from VWAP by a certain ATR multiple.
/// Exit when price returns to VWAP.
/// </summary>
public class VwapMeanReversionStrategy : Strategy
{
private readonly StrategyParam<decimal> _kParam;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrPeriod;
private AverageTrueRange _atr;
private VolumeWeightedMovingAverage _vwap;
private decimal _currentAtr;
private decimal _currentVwap;
/// <summary>
/// ATR multiplier for entry.
/// </summary>
public decimal K
{
get => _kParam.Value;
set => _kParam.Value = value;
}
/// <summary>
/// Type of candles to use.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// ATR period.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="VwapMeanReversionStrategy"/>.
/// </summary>
public VwapMeanReversionStrategy()
{
_kParam = Param(nameof(K), 2.0m)
.SetGreaterThanZero()
.SetDisplay("ATR Multiplier", "ATR multiplier for entry distance from VWAP", "Strategy Parameters")
.SetOptimize(1.0m, 4.0m, 0.5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "Strategy Parameters");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "ATR indicator period", "Strategy Parameters")
.SetOptimize(10, 20, 2);
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_atr = null;
_vwap = null;
_currentAtr = default;
_currentVwap = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create indicators
_atr = new AverageTrueRange { Length = AtrPeriod };
_vwap = new VolumeWeightedMovingAverage { Length = AtrPeriod };
// Create subscription for candles
var subscription = SubscribeCandles(CandleType);
// Bind indicators to candles
subscription
.Bind(_atr, ProcessATR)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _atr);
DrawOwnTrades(area);
}
// Enable position protection
StartProtection(
takeProfit: new Unit(5, UnitTypes.Percent),
stopLoss: new Unit(2, UnitTypes.Percent)
);
}
private void ProcessATR(ICandleMessage candle, decimal atr)
{
if (candle.State != CandleStates.Finished)
return;
try
{
_currentVwap = _vwap.Process(candle).ToDecimal();
}
catch
{
return;
}
_currentAtr = atr;
ProcessStrategy(candle.ClosePrice);
}
private void ProcessStrategy(decimal currentPrice)
{
// Check if strategy is ready for trading
if (!IsFormedAndOnlineAndAllowTrading())
return;
// Skip if we don't have valid VWAP or ATR yet
if (_currentVwap <= 0 || _currentAtr <= 0)
return;
// Calculate distance to VWAP
var upperBand = _currentVwap + K * _currentAtr;
var lowerBand = _currentVwap - K * _currentAtr;
LogInfo($"Current Price: {currentPrice}, VWAP: {_currentVwap}, Upper: {upperBand}, Lower: {lowerBand}");
// Entry logic
if (Position == 0)
{
// Long Entry: Price is below lower band
if (currentPrice < lowerBand)
{
// Buy when price is too low compared to VWAP
LogInfo($"Buy Signal - Price ({currentPrice}) < Lower Band ({lowerBand})");
BuyMarket(Volume);
}
// Short Entry: Price is above upper band
else if (currentPrice > upperBand)
{
// Sell when price is too high compared to VWAP
LogInfo($"Sell Signal - Price ({currentPrice}) > Upper Band ({upperBand})");
SellMarket(Volume);
}
}
// Exit logic
else if (Position > 0 && currentPrice > _currentVwap)
{
// Exit Long: Price returned to VWAP
LogInfo($"Exit Long - Price ({currentPrice}) > VWAP ({_currentVwap})");
SellMarket(Math.Abs(Position));
}
else if (Position < 0 && currentPrice < _currentVwap)
{
// Exit Short: Price returned to VWAP
LogInfo($"Exit Short - Price ({currentPrice}) < VWAP ({_currentVwap})");
BuyMarket(Math.Abs(Position));
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import AverageTrueRange, VolumeWeightedMovingAverage
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class vwap_mean_reversion_strategy(Strategy):
"""
VWAP Mean Reversion Strategy.
Enter when price deviates from VWAP by a certain ATR multiple.
Exit when price returns to VWAP.
"""
def __init__(self):
super(vwap_mean_reversion_strategy, self).__init__()
# Initialize strategy parameters
self._k_param = self.Param("K", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("ATR Multiplier", "ATR multiplier for entry distance from VWAP", "Strategy Parameters") \
.SetCanOptimize(True) \
.SetOptimize(1.0, 4.0, 0.5)
self._candle_type = self.Param("CandleType", tf(5)) \
.SetDisplay("Candle Type", "Type of candles to use", "Strategy Parameters")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "ATR indicator period", "Strategy Parameters") \
.SetCanOptimize(True) \
.SetOptimize(10, 20, 2)
# Internal indicators
self._atr = None
self._vwap = None
self._current_atr = 0
self._current_vwap = 0
@property
def K(self):
"""ATR multiplier for entry."""
return self._k_param.Value
@K.setter
def K(self, value):
self._k_param.Value = value
@property
def CandleType(self):
"""Type of candles to use."""
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def AtrPeriod(self):
"""ATR period."""
return self._atr_period.Value
@AtrPeriod.setter
def AtrPeriod(self, value):
self._atr_period.Value = value
def GetWorkingSecurities(self):
"""!! REQUIRED!! Override to return securities used by the strategy."""
return [(self.Security, self.CandleType)]
def OnStarted2(self, time):
"""Set up indicators, subscriptions and protection."""
super(vwap_mean_reversion_strategy, self).OnStarted2(time)
# Create indicators
self._atr = AverageTrueRange()
self._atr.Length = self.AtrPeriod
self._vwap = VolumeWeightedMovingAverage()
self._vwap.Length = self.AtrPeriod
# Create subscription for candles
subscription = self.SubscribeCandles(self.CandleType)
# Bind indicators to candles
subscription.Bind(self._atr, self.ProcessATR).Start()
# Setup chart visualization if available
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._atr)
self.DrawOwnTrades(area)
# Enable position protection
self.StartProtection(
takeProfit=Unit(5, UnitTypes.Percent),
stopLoss=Unit(2, UnitTypes.Percent)
)
def OnReseted(self):
super(vwap_mean_reversion_strategy, self).OnReseted()
self._current_atr = 0
self._current_vwap = 0
def ProcessATR(self, candle, atr_value):
if candle.State != CandleStates.Finished:
return
self._current_vwap = float(process_candle(self._vwap, candle))
self._current_vwap = self._current_vwap if self._current_vwap is not None else 0
self._current_atr = atr_value
self.ProcessStrategy(candle.ClosePrice)
def ProcessStrategy(self, current_price):
# Check if strategy is ready for trading
# Skip if we don't have valid VWAP or ATR yet
if self._current_vwap <= 0 or self._current_atr <= 0:
return
# Calculate distance to VWAP
upper_band = self._current_vwap + self.K * self._current_atr
lower_band = self._current_vwap - self.K * self._current_atr
self.LogInfo(
"Current Price: {0}, VWAP: {1}, Upper: {2}, Lower: {3}".format(
current_price, self._current_vwap, upper_band, lower_band))
# Entry logic
if self.Position == 0:
# Long Entry: Price is below lower band
if current_price < lower_band:
# Buy when price is too low compared to VWAP
self.LogInfo(
"Buy Signal - Price ({0}) < Lower Band ({1})".format(current_price, lower_band))
self.BuyMarket(self.Volume)
# Short Entry: Price is above upper band
elif current_price > upper_band:
# Sell when price is too high compared to VWAP
self.LogInfo(
"Sell Signal - Price ({0}) > Upper Band ({1})".format(current_price, upper_band))
self.SellMarket(self.Volume)
# Exit logic
elif self.Position > 0 and current_price > self._current_vwap:
# Exit Long: Price returned to VWAP
self.LogInfo(
"Exit Long - Price ({0}) > VWAP ({1})".format(current_price, self._current_vwap))
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0 and current_price < self._current_vwap:
# Exit Short: Price returned to VWAP
self.LogInfo(
"Exit Short - Price ({0}) < VWAP ({1})".format(current_price, self._current_vwap))
self.BuyMarket(Math.Abs(self.Position))
def CreateClone(self):
"""!! REQUIRED!! Creates a new instance of the strategy."""
return vwap_mean_reversion_strategy()