View on GitHub

Adx Volume Strategy

Implementation of strategy - ADX + Volume. Enter trades when ADX is above threshold with above average volume. Direction determined by DI+ and DI- comparison.

Testing indicates an average annual return of about 67%. It performs best in the stocks market.

High ADX denotes a strong trend and volume spikes confirm commitment. Entries are made when both indicators show strength together.

Great for catching energetic breakouts. A stop based on ATR keeps exposure in check.

Details

  • Entry Criteria:
    • Long: ADX > AdxThreshold && Volume > AvgVolume
    • Short: ADX > AdxThreshold && Volume > AvgVolume
  • Long/Short: Both
  • Exit Criteria: Trend weakens below threshold
  • Stops: ATR-based using StopLoss
  • Default Values:
    • AdxPeriod = 14
    • AdxThreshold = 25m
    • VolumeAvgPeriod = 20
    • StopLoss = new Unit(2, UnitTypes.Absolute)
    • CandleType = TimeSpan.FromMinutes(5).TimeFrame()
  • Filters:
    • Category: Breakout
    • Direction: Both
    • Indicators: ADX, Volume
    • Stops: Yes
    • Complexity: Intermediate
    • Timeframe: Mid-term
    • 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;

using StockSharp.Algo;
using StockSharp.Algo.Candles;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Implementation of strategy - ADX + Volume.
/// Enter trades when ADX is above threshold with above average volume.
/// Direction determined by DI+ and DI- comparison.
/// </summary>
public class AdxVolumeStrategy : Strategy
{
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<decimal> _adxThreshold;
	private readonly StrategyParam<int> _volumeAvgPeriod;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<Unit> _stopLoss;
	private readonly StrategyParam<DataType> _candleType;

	// For volume tracking
	private decimal _averageVolume;
	private int _volumeCounter;
	private int _cooldown;

	/// <summary>
	/// ADX period.
	/// </summary>
	public int AdxPeriod
	{
		get => _adxPeriod.Value;
		set => _adxPeriod.Value = value;
	}

	/// <summary>
	/// ADX threshold value to determine strong trend.
	/// </summary>
	public decimal AdxThreshold
	{
		get => _adxThreshold.Value;
		set => _adxThreshold.Value = value;
	}

	/// <summary>
	/// Volume average period.
	/// </summary>
	public int VolumeAvgPeriod
	{
		get => _volumeAvgPeriod.Value;
		set => _volumeAvgPeriod.Value = value;
	}

	/// <summary>
	/// Volume multiplier above average.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	/// <summary>
	/// Bars to wait between trades.
	/// </summary>
	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

	/// <summary>
	/// Stop-loss value.
	/// </summary>
	public Unit StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Candle type used for strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="AdxVolumeStrategy"/>.
	/// </summary>
	public AdxVolumeStrategy()
	{
		_adxPeriod = Param(nameof(AdxPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ADX Period", "Period for ADX indicator", "ADX Parameters");

		_adxThreshold = Param(nameof(AdxThreshold), 25m)
			.SetRange(10, 50)
			.SetDisplay("ADX Threshold", "Threshold above which trend is considered strong", "ADX Parameters");

		_volumeAvgPeriod = Param(nameof(VolumeAvgPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Volume Average Period", "Period for volume moving average", "Volume Parameters");

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 1.4m)
			.SetRange(1.0m, 3.0m)
			.SetDisplay("Volume Multiplier", "Multiplier over average volume", "Volume Parameters");

		_cooldownBars = Param(nameof(CooldownBars), 160)
			.SetRange(5, 500)
			.SetDisplay("Cooldown Bars", "Bars between trades", "General");

		_stopLoss = Param(nameof(StopLoss), new Unit(2, UnitTypes.Absolute))
			.SetDisplay("Stop Loss", "Stop loss in ATR or value", "Risk Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle type for strategy", "General");

		_averageVolume = 0;
		_volumeCounter = 0;
		_cooldown = 0;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_averageVolume = 0;
		_volumeCounter = 0;
		_cooldown = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		// Create ADX indicator
		var adx = new AverageDirectionalIndex { Length = AdxPeriod };

		// Setup candle subscription
		var subscription = SubscribeCandles(CandleType);

		// Bind ADX indicator to candles
		subscription
			.BindEx(adx, ProcessCandle)
			.Start();

		// Setup chart visualization if available
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, adx);
			DrawOwnTrades(area);
		}

	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue adxValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (!adxValue.IsFormed)
			return;

		// Update average volume calculation
		var currentVolume = candle.TotalVolume;
		
		if (_volumeCounter < VolumeAvgPeriod)
		{
			_volumeCounter++;
			_averageVolume = ((_averageVolume * (_volumeCounter - 1)) + currentVolume) / _volumeCounter;
		}
		else
		{
			_averageVolume = (_averageVolume * (VolumeAvgPeriod - 1) + currentVolume) / VolumeAvgPeriod;
		}

		if (_volumeCounter < VolumeAvgPeriod)
		{
			if (_cooldown > 0)
				_cooldown--;
			return;
		}

		var adxTyped = (AverageDirectionalIndexValue)adxValue;
		var diPlusValue = adxTyped.Dx.Plus;
		var diMinusValue = adxTyped.Dx.Minus;
		var adxMa = adxTyped.MovingAverage;

		// Check if volume is above average
		var isVolumeAboveAverage = currentVolume > _averageVolume * VolumeMultiplier;

		LogInfo($"Candle: {candle.OpenTime}, Close: {candle.ClosePrice}, " +
			   $"ADX: {adxMa}, DI+: {diPlusValue}, DI-: {diMinusValue}, " +
			   $"Volume: {currentVolume}, Avg Volume: {_averageVolume}");

		if (_cooldown > 0)
		{
			_cooldown--;
			return;
		}

		// Trading rules
		if (adxMa > AdxThreshold && isVolumeAboveAverage)
		{
			// Strong trend detected with above average volume
			
			if (diPlusValue > diMinusValue && Position == 0)
			{
				// Bullish trend - DI+ > DI-
				BuyMarket();
				_cooldown = CooldownBars;
				
				LogInfo($"Buy signal: Strong trend (ADX: {adxMa}) with DI+ > DI- and high volume.");
			}
			else if (diMinusValue > diPlusValue && Position == 0)
			{
				// Bearish trend - DI- > DI+
				SellMarket();
				_cooldown = CooldownBars;
				
				LogInfo($"Sell signal: Strong trend (ADX: {adxMa}) with DI- > DI+ and high volume.");
			}
		}
		// Exit conditions
		else if (adxMa < AdxThreshold * 0.8m)
		{
			// Trend weakening - exit all positions
			if (Position > 0)
			{
				SellMarket();
				_cooldown = CooldownBars;
				LogInfo($"Exit long: ADX weakening below {AdxThreshold * 0.8m}. Position: {Position}");
			}
			else if (Position < 0)
			{
				BuyMarket();
				_cooldown = CooldownBars;
				LogInfo($"Exit short: ADX weakening below {AdxThreshold * 0.8m}. Position: {Position}");
			}
		}
		// Check if DI+/DI- cross to exit positions
		else if (diPlusValue < diMinusValue && Position > 0)
		{
			// DI+ crosses below DI- while in long position
			SellMarket();
			_cooldown = CooldownBars;
			LogInfo($"Exit long: DI+ crossed below DI-. Position: {Position}");
		}
		else if (diPlusValue > diMinusValue && Position < 0)
		{
			// DI+ crosses above DI- while in short position
			BuyMarket();
			_cooldown = CooldownBars;
			LogInfo($"Exit short: DI+ crossed above DI-. Position: {Position}");
		}
	}
}