Auf GitHub ansehen

ADX Volume Strategie

Implementierung der Strategie ADX + Volume. Trades werden eröffnet, wenn der ADX über dem Schwellenwert liegt und das Volumen überdurchschnittlich ist. Die Richtung wird durch den Vergleich von DI+ und DI- bestimmt.

Tests zeigen eine durchschnittliche jährliche Rendite von etwa 67%. Am besten geeignet für den Aktienmarkt.

Ein hoher ADX weist auf einen starken Trend hin, und Volumenspitzen bestätigen das Engagement. Einstiege erfolgen, wenn beide Indikatoren gleichzeitig Stärke zeigen.

Ideal zum Erfassen energischer Ausbrüche. Ein ATR-basierter Stop hält das Risiko unter Kontrolle.

Details

  • Einstiegskriterien:
    • Long: ADX > AdxThreshold && Volume > AvgVolume
    • Short: ADX > AdxThreshold && Volume > AvgVolume
  • Long/Short: Beide
  • Ausstiegskriterien: Trend schwächt sich unter den Schwellenwert ab
  • Stops: ATR-basiert mit StopLoss
  • Standardwerte:
    • AdxPeriod = 14
    • AdxThreshold = 25m
    • VolumeAvgPeriod = 20
    • StopLoss = new Unit(2, UnitTypes.Absolute)
    • CandleType = TimeSpan.FromMinutes(5).TimeFrame()
  • Filter:
    • Kategorie: Ausbruch
    • Richtung: Beide
    • Indikatoren: ADX, Volumen
    • Stops: Ja
    • Komplexität: Mittel
    • Zeitrahmen: Mittelfristig
    • Saisonalität: Nein
    • Neuronale Netze: Nein
    • Divergenz: Nein
    • Risikolevel: Mittel
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}");
		}
	}
}