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
CandleType = TimeSpan.FromMinutes(5)
AtrPeriod = 14
The market trades around the clock, so the session VWAP restarts each UTC day and weighs the typical price of each candle by its volume. The stop is set K times the AtrPeriod ATR from the entry close and checked on candle closes. An entry signal against an open position reverses it.
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.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// VWAP Mean Reversion strategy.
/// The market trades around the clock, so the session VWAP restarts with each UTC day and weighs each candle's typical price by its volume.
/// A close more than K times the AtrPeriod ATR below VWAP goes long and one that far above it goes short, reversing an opposite position.
/// A long closes once the close is back at or above VWAP and a short once it is back at or below it. The stop lies K ATR from the entry
/// close and is checked on candle closes.
/// </summary>
public class VwapMeanReversionStrategy : Strategy
{
private readonly StrategyParam<decimal> _k;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<DataType> _candleType;
private DateTime? _day;
private decimal _cumulativePriceVolume;
private decimal _cumulativeVolume;
private decimal _stopPrice;
/// <summary>
/// ATR multiplier for the entry distance and the stop.
/// </summary>
public decimal K
{
get => _k.Value;
set => _k.Value = value;
}
/// <summary>
/// Period of the ATR.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public VwapMeanReversionStrategy()
{
_k = Param(nameof(K), 2m)
.SetGreaterThanZero()
.SetDisplay("K", "ATR multiplier for the entry distance and the stop", "Parameters");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "Period of the ATR", "Parameters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_day = null;
_cumulativePriceVolume = 0;
_cumulativeVolume = 0;
_stopPrice = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_day = null;
_cumulativePriceVolume = 0;
_cumulativeVolume = 0;
_stopPrice = default;
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
var oscillators = CreateChartArea();
if (oscillators != null)
{
DrawIndicator(oscillators, atr);
}
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
var day = candle.OpenTime.Date;
if (_day != day)
{
_day = day;
_cumulativePriceVolume = 0;
_cumulativeVolume = 0;
}
var typicalPrice = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3;
_cumulativePriceVolume += typicalPrice * candle.TotalVolume;
_cumulativeVolume += candle.TotalVolume;
if (!atrValue.IsFormed || _cumulativeVolume <= 0)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var vwap = _cumulativePriceVolume / _cumulativeVolume;
var distance = K * atrValue.GetValue<decimal>();
var close = candle.ClosePrice;
if (close < vwap - distance && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_stopPrice = close - distance;
}
else if (close > vwap + distance && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_stopPrice = close + distance;
}
else if (Position > 0 && (close >= vwap || close <= _stopPrice))
{
SellMarket(Position);
}
else if (Position < 0 && (close <= vwap || close >= _stopPrice))
{
BuyMarket(-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, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class vwap_mean_reversion_strategy(Strategy):
"""
VWAP Mean Reversion strategy.
The market trades around the clock, so the session VWAP restarts with each UTC day and weighs each candle's typical price by its volume.
A close more than K times the AtrPeriod ATR below VWAP goes long and one that far above it goes short, reversing an opposite position.
A long closes once the close is back at or above VWAP and a short once it is back at or below it. The stop lies K ATR from the entry
close and is checked on candle closes.
"""
def __init__(self):
super(vwap_mean_reversion_strategy, self).__init__()
self._k = self.Param("K", 2.0).SetGreaterThanZero().SetDisplay("K", "ATR multiplier for the entry distance and the stop", "Parameters")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero().SetDisplay("ATR Period", "Period of the ATR", "Parameters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._day = None
self._cumulative_price_volume = Decimal(0)
self._cumulative_volume = Decimal(0)
self._stop_price = Decimal(0)
def OnReseted(self):
super(vwap_mean_reversion_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(vwap_mean_reversion_strategy, self).OnStarted2(time)
self._reset_state()
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
oscillators = self.CreateChartArea()
if oscillators is not None:
self.DrawIndicator(oscillators, atr)
def _process_candle(self, candle, atr_value):
if candle.State != CandleStates.Finished:
return
day = candle.OpenTime.Date
if self._day is None or self._day != day:
self._day = day
self._cumulative_price_volume = Decimal(0)
self._cumulative_volume = Decimal(0)
typical_price = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / Decimal(3)
self._cumulative_price_volume += typical_price * candle.TotalVolume
self._cumulative_volume += candle.TotalVolume
if not atr_value.IsFormed or self._cumulative_volume <= 0:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
vwap = self._cumulative_price_volume / self._cumulative_volume
distance = Decimal(self._k.Value) * atr_value.GetValue[Decimal](None)
close = candle.ClosePrice
if close < vwap - distance and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._stop_price = close - distance
elif close > vwap + distance and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._stop_price = close + distance
elif self.Position > 0 and (close >= vwap or close <= self._stop_price):
self.SellMarket(self.Position)
elif self.Position < 0 and (close <= vwap or close >= self._stop_price):
self.BuyMarket(-self.Position)
def CreateClone(self):
return vwap_mean_reversion_strategy()