VWAP Stochastic Divergence
The VWAP Stochastic Divergence strategy is built around combining VWAP with ADX trend strength indicator.
Testing indicates an average annual return of about 79%. It performs best in the stocks market.
Signals trigger when Stochastic confirms divergence setups on intraday (5m) data. This makes the method suitable for active traders.
Stops rely on ATR multiples and factors like AdxPeriod, AdxThreshold. Adjust these defaults to balance risk and reward.
Details
- Entry Criteria: see implementation for indicator conditions.
- Long/Short: Both directions.
- Exit Criteria: opposite signal or stop logic.
- Stops: Yes, using indicator-based calculations.
- Default Values:
AdxPeriod = 14AdxThreshold = 25mAdxExitThreshold = 20mCandleType = TimeSpan.FromMinutes(5).TimeFrame()
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: Stochastic, Divergence
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (5m)
- Seasonality: No
- Neural Networks: No
- Divergence: Yes
- 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>
/// Strategy combining VWAP with ADX trend strength indicator.
/// </summary>
public class VwapAdxTrendStrategy : Strategy
{
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<decimal> _adxThreshold;
private readonly StrategyParam<decimal> _adxExitThreshold;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _signalCooldownBars;
private VolumeWeightedMovingAverage _vwap;
private AverageDirectionalIndex _adx;
private DirectionalIndex _di;
private decimal _vwapValue;
private decimal _adxValue;
private decimal _plusDiValue;
private decimal _minusDiValue;
private decimal? _prevPlusDiValue;
private decimal? _prevMinusDiValue;
private int _cooldownRemaining;
/// <summary>
/// ADX period for trend strength calculation.
/// </summary>
public int AdxPeriod
{
get => _adxPeriod.Value;
set => _adxPeriod.Value = value;
}
/// <summary>
/// ADX threshold for trend strength entry.
/// </summary>
public decimal AdxThreshold
{
get => _adxThreshold.Value;
set => _adxThreshold.Value = value;
}
/// <summary>
/// ADX threshold for trend strength exit.
/// </summary>
public decimal AdxExitThreshold
{
get => _adxExitThreshold.Value;
set => _adxExitThreshold.Value = value;
}
/// <summary>
/// Candle type to use for the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Number of closed candles to wait before entering a new trend signal.
/// </summary>
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="VwapAdxTrendStrategy"/>.
/// </summary>
public VwapAdxTrendStrategy()
{
_adxPeriod = Param(nameof(AdxPeriod), 14)
.SetDisplay("ADX Period", "Period for ADX and Directional Index calculations", "ADX")
.SetOptimize(8, 20, 2);
_adxThreshold = Param(nameof(AdxThreshold), 25m)
.SetDisplay("ADX Threshold", "ADX threshold for trend strength entry", "ADX")
.SetOptimize(20m, 40m, 5m);
_adxExitThreshold = Param(nameof(AdxExitThreshold), 20m)
.SetDisplay("ADX Exit Threshold", "ADX threshold for trend strength exit", "ADX")
.SetOptimize(10m, 25m, 5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 4)
.SetNotNegative()
.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before a new DI crossover entry", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_vwapValue = default;
_adxValue = default;
_plusDiValue = default;
_minusDiValue = default;
_prevPlusDiValue = null;
_prevMinusDiValue = null;
_cooldownRemaining = 0;
_vwap = null;
_adx = null;
_di = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create indicators
_vwap = new VolumeWeightedMovingAverage();
_adx = new AverageDirectionalIndex
{
Length = AdxPeriod
};
_di = new DirectionalIndex
{
Length = AdxPeriod
};
// Create subscription and bind indicators
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(
_vwap,
_adx,
_di,
ProcessCandle)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _vwap);
DrawIndicator(area, _adx);
DrawOwnTrades(area);
}
// Setup position protection
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(2, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue vwapValue, IIndicatorValue adxValue, IIndicatorValue diValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var adxTyped = (AverageDirectionalIndexValue)adxValue;
if (adxTyped.MovingAverage is not decimal adx)
return;
var dx = adxTyped.Dx;
if (dx.Plus is not decimal plusDi || dx.Minus is not decimal minusDi)
return;
// Extract values from indicators
_vwapValue = vwapValue.ToDecimal();
_adxValue = adx;
_plusDiValue = plusDi; // +DI
_minusDiValue = minusDi; // -DI
if (_prevPlusDiValue is null || _prevMinusDiValue is null)
{
_prevPlusDiValue = _plusDiValue;
_prevMinusDiValue = _minusDiValue;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
var bullishCross = _prevPlusDiValue.Value <= _prevMinusDiValue.Value && _plusDiValue > _minusDiValue;
var bearishCross = _prevPlusDiValue.Value >= _prevMinusDiValue.Value && _minusDiValue > _plusDiValue;
// Trading logic
if (_cooldownRemaining == 0 && bullishCross && candle.ClosePrice > _vwapValue && _adxValue > AdxThreshold && Position <= 0)
{
BuyMarket(Volume + (Position < 0 ? Math.Abs(Position) : 0m));
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 && bearishCross && candle.ClosePrice < _vwapValue && _adxValue > AdxThreshold && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
// Exit long position when ADX weakens below exit threshold or -DI crosses above +DI
else if (Position > 0 && (_adxValue < AdxExitThreshold || _minusDiValue > _plusDiValue))
{
SellMarket(Position);
_cooldownRemaining = SignalCooldownBars;
}
// Exit short position when ADX weakens below exit threshold or +DI crosses above -DI
else if (Position < 0 && (_adxValue < AdxExitThreshold || _plusDiValue > _minusDiValue))
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
_prevPlusDiValue = _plusDiValue;
_prevMinusDiValue = _minusDiValue;
}
}
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, Unit, UnitTypes, CandleStates
from StockSharp.Algo.Indicators import VolumeWeightedMovingAverage, AverageDirectionalIndex, DirectionalIndex
from StockSharp.Algo.Strategies import Strategy
class vwap_adx_trend_strategy(Strategy):
"""
Strategy combining VWAP with ADX trend strength indicator.
"""
def __init__(self):
super(vwap_adx_trend_strategy, self).__init__()
self._adx_period = self.Param("AdxPeriod", 14) \
.SetDisplay("ADX Period", "Period for ADX and Directional Index calculations", "ADX") \
.SetCanOptimize(True) \
.SetOptimize(8, 20, 2)
self._adx_threshold = self.Param("AdxThreshold", 25.0) \
.SetDisplay("ADX Threshold", "ADX threshold for trend strength entry", "ADX") \
.SetCanOptimize(True) \
.SetOptimize(20.0, 40.0, 5.0)
self._adx_exit_threshold = self.Param("AdxExitThreshold", 20.0) \
.SetDisplay("ADX Exit Threshold", "ADX threshold for trend strength exit", "ADX") \
.SetCanOptimize(True) \
.SetOptimize(10.0, 25.0, 5.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 4) \
.SetNotNegative() \
.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before a new DI crossover entry", "General")
self._vwap_value = 0.0
self._adx_value = 0.0
self._plus_di_value = 0.0
self._minus_di_value = 0.0
self._prev_plus_di = None
self._prev_minus_di = None
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)]
def OnReseted(self):
super(vwap_adx_trend_strategy, self).OnReseted()
self._vwap_value = 0.0
self._adx_value = 0.0
self._plus_di_value = 0.0
self._minus_di_value = 0.0
self._prev_plus_di = None
self._prev_minus_di = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(vwap_adx_trend_strategy, self).OnStarted2(time)
vwap = VolumeWeightedMovingAverage()
adx = AverageDirectionalIndex()
adx.Length = int(self._adx_period.Value)
di = DirectionalIndex()
di.Length = int(self._adx_period.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(vwap, adx, di, self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, vwap)
self.DrawIndicator(area, adx)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(2, UnitTypes.Percent)
)
def ProcessCandle(self, candle, vwap_value, adx_value, di_value):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
if adx_value.MovingAverage is None:
return
adx = float(adx_value.MovingAverage)
dx = adx_value.Dx
if dx.Plus is None or dx.Minus is None:
return
plus_di = float(dx.Plus)
minus_di = float(dx.Minus)
self._vwap_value = float(vwap_value)
self._adx_value = adx
self._plus_di_value = plus_di
self._minus_di_value = minus_di
if self._prev_plus_di is None or self._prev_minus_di is None:
self._prev_plus_di = self._plus_di_value
self._prev_minus_di = self._minus_di_value
return
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_plus_di = self._plus_di_value
self._prev_minus_di = self._minus_di_value
return
bullish_cross = self._prev_plus_di <= self._prev_minus_di and self._plus_di_value > self._minus_di_value
bearish_cross = self._prev_plus_di >= self._prev_minus_di and self._minus_di_value > self._plus_di_value
close_price = float(candle.ClosePrice)
adx_threshold = float(self._adx_threshold.Value)
adx_exit_threshold = float(self._adx_exit_threshold.Value)
cooldown_bars = int(self._signal_cooldown_bars.Value)
if self._cooldown_remaining == 0 and bullish_cross and close_price > self._vwap_value and self._adx_value > adx_threshold and self.Position <= 0:
vol = self.Volume
if self.Position < 0:
vol = self.Volume + Math.Abs(self.Position)
self.BuyMarket(vol)
self._cooldown_remaining = cooldown_bars
elif self._cooldown_remaining == 0 and bearish_cross and close_price < self._vwap_value and self._adx_value > adx_threshold and self.Position >= 0:
self.SellMarket(self.Volume + Math.Abs(self.Position))
self._cooldown_remaining = cooldown_bars
elif self.Position > 0 and (self._adx_value < adx_exit_threshold or self._minus_di_value > self._plus_di_value):
self.SellMarket(self.Position)
self._cooldown_remaining = cooldown_bars
elif self.Position < 0 and (self._adx_value < adx_exit_threshold or self._plus_di_value > self._minus_di_value):
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown_bars
self._prev_plus_di = self._plus_di_value
self._prev_minus_di = self._minus_di_value
def CreateClone(self):
return vwap_adx_trend_strategy()