Ver en GitHub

Estrategia ADX Volume

Implementación de la estrategia ADX + Volume. Entrar en operaciones cuando el ADX está por encima del umbral con volumen superior al promedio. La dirección se determina por la comparación de DI+ y DI-.

Las pruebas indican un retorno anual promedio de aproximadamente el 67%. Funciona mejor en el mercado de acciones.

Un ADX alto denota una tendencia fuerte y los picos de volumen confirman el compromiso. Las entradas se realizan cuando ambos indicadores muestran fuerza simultáneamente.

Excelente para capturar rupturas enérgicas. Un stop basado en ATR mantiene la exposición bajo control.

Detalles

  • Criterios de entrada:
    • Largo: ADX > AdxThreshold && Volume > AvgVolume
    • Corto: ADX > AdxThreshold && Volume > AvgVolume
  • Largo/Corto: Ambos
  • Criterios de salida: La tendencia se debilita por debajo del umbral
  • Stops: Basados en ATR usando StopLoss
  • Valores predeterminados:
    • AdxPeriod = 14
    • AdxThreshold = 25m
    • VolumeAvgPeriod = 20
    • StopLoss = new Unit(2, UnitTypes.Absolute)
    • CandleType = TimeSpan.FromMinutes(5).TimeFrame()
  • Filtros:
    • Categoría: Ruptura
    • Dirección: Ambos
    • Indicadores: ADX, Volume
    • Stops: Sí
    • Complejidad: Intermedio
    • Marco temporal: Medio plazo
    • Estacionalidad: No
    • Redes neuronales: No
    • Divergencia: No
    • Nivel de riesgo: Medio
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}");
		}
	}
}