GitHub で見る

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 = 14
    • AdxThreshold = 25m
    • AdxExitThreshold = 20m
    • CandleType = 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;
	}
}