Vwap Volume Strategy
Strategy combining VWAP and Volume indicators. Buys/sells on VWAP breakouts confirmed by above-average volume.
Testing indicates an average annual return of about 52%. It performs best in the crypto market.
This strategy references VWAP to gauge value and requires volume confirmation before trades. The idea is to join moves backed by strong participation.
Intraday traders focused on volume metrics can employ this method. Losses are trimmed via a percent stop.
Details
- Entry Criteria:
- Long:
Close < VWAP && Volume > AvgVolume * VolumeThreshold - Short:
Close > VWAP && Volume > AvgVolume * VolumeThreshold
- Long:
- Long/Short: Both
- Exit Criteria:
- Price crosses back through VWAP
- Stops: Percent-based using
StopLossPercent - Default Values:
VolumePeriod= 20VolumeThreshold= 1.5mStopLossPercent= 2.0mCandleType= TimeSpan.FromMinutes(5).TimeFrame() 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. AvgVolume is the average volume of the previous VolumePeriod candles. An entry signal against an open position reverses it.
- Filters:
- Category: Mean reversion
- Direction: Both
- Indicators: VWAP, Volume
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Mid-term
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// VWAP Volume 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 below VWAP on volume above VolumeThreshold times the average of the previous VolumePeriod candles goes long and a close above
/// VWAP on such volume goes short, reversing an opposite position. The position closes once price crosses back through VWAP,
/// and a percent stop limits the loss.
/// </summary>
public class VwapVolumeStrategy : Strategy
{
private readonly StrategyParam<int> _volumePeriod;
private readonly StrategyParam<decimal> _volumeThreshold;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _volumes = [];
private DateTime? _day;
private decimal _cumulativePriceVolume;
private decimal _cumulativeVolume;
/// <summary>
/// Previous candles the volume is averaged over.
/// </summary>
public int VolumePeriod
{
get => _volumePeriod.Value;
set => _volumePeriod.Value = value;
}
/// <summary>
/// How many times the average volume a candle must exceed.
/// </summary>
public decimal VolumeThreshold
{
get => _volumeThreshold.Value;
set => _volumeThreshold.Value = value;
}
/// <summary>
/// Stop loss percentage from entry price.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public VwapVolumeStrategy()
{
_volumePeriod = Param(nameof(VolumePeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Volume Period", "Previous candles the volume is averaged over", "Volume");
_volumeThreshold = Param(nameof(VolumeThreshold), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Volume Threshold", "How many times the average volume a candle must exceed", "Volume");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk");
_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();
_volumes.Clear();
_day = null;
_cumulativePriceVolume = 0;
_cumulativeVolume = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_volumes.Clear();
_day = null;
_cumulativePriceVolume = 0;
_cumulativeVolume = 0;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
// The stop has to see prices between candles, not only at their close.
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// The high-level handler activates native protection before this callback, also between signal bars.
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// Volume is compared with the candles before this one.
var average = _volumes.Count == VolumePeriod ? _volumes.Average() : (decimal?)null;
_volumes.Add(candle.TotalVolume);
if (_volumes.Count > VolumePeriod)
_volumes.RemoveAt(0);
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 (average is not decimal avgVolume || _cumulativeVolume <= 0)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var vwap = _cumulativePriceVolume / _cumulativeVolume;
var close = candle.ClosePrice;
var surge = candle.TotalVolume > avgVolume * VolumeThreshold;
if (close < vwap && surge && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (close > vwap && surge && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
else if (Position > 0 && close > vwap)
SellMarket(Position);
else if (Position < 0 && close < vwap)
BuyMarket(-Position);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes, Level1Fields
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Strategies import Strategy
class vwap_volume_strategy(Strategy):
"""
VWAP Volume 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 below VWAP on volume above VolumeThreshold times the average of the previous VolumePeriod candles goes long and a close above
VWAP on such volume goes short, reversing an opposite position. The position closes once price crosses back through VWAP,
and a percent stop limits the loss.
"""
def __init__(self):
super(vwap_volume_strategy, self).__init__()
self._volume_period = self.Param("VolumePeriod", 20).SetGreaterThanZero().SetDisplay("Volume Period", "Previous candles the volume is averaged over", "Volume")
self._volume_threshold = self.Param("VolumeThreshold", 1.5).SetGreaterThanZero().SetDisplay("Volume Threshold", "How many times the average volume a candle must exceed", "Volume")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk")
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._volumes = []
self._day = None
self._cumulative_price_volume = Decimal(0)
self._cumulative_volume = Decimal(0)
def OnReseted(self):
super(vwap_volume_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(vwap_volume_strategy, self).OnStarted2(time)
self._reset_state()
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
self.StartProtection(Unit(), Unit(Decimal(self._stop_loss_percent.Value), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
# The stop has to see prices between candles, not only at their close.
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# The high-level handler activates native protection before the callback, including between bars.
pass
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
# Volume is compared with the candles before this one.
period = self._volume_period.Value
average = None
if len(self._volumes) == period:
total = Decimal(0)
for volume in self._volumes:
total += volume
average = total / Decimal(period)
self._volumes.append(candle.TotalVolume)
if len(self._volumes) > period:
self._volumes.pop(0)
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 average is None or self._cumulative_volume <= 0:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
vwap = self._cumulative_price_volume / self._cumulative_volume
close = candle.ClosePrice
surge = candle.TotalVolume > average * Decimal(self._volume_threshold.Value)
if close < vwap and surge and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif close > vwap and surge and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
elif self.Position > 0 and close > vwap:
self.SellMarket(self.Position)
elif self.Position < 0 and close < vwap:
self.BuyMarket(-self.Position)
def CreateClone(self):
return vwap_volume_strategy()