Ver en GitHub

Estrategia de VWAP Stochastic Divergence

La estrategia VWAP Stochastic Divergence se basa en la combinación del VWAP con el indicador de fuerza de tendencia ADX.

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

Las señales se activan cuando Stochastic confirma configuraciones de divergencia en datos intradía (5m). Este método es adecuado para operadores activos.

Los stops se basan en múltiplos de ATR y factores como AdxPeriod, AdxThreshold. Ajuste estos valores predeterminados para equilibrar el riesgo y la recompensa.

Detalles

  • Criterios de entrada: ver implementación para condiciones de indicadores.
  • Largo/Corto: Ambos direcciones.
  • Criterios de salida: señal opuesta o lógica de stop.
  • Stops: Sí, usando cálculos basados en indicadores.
  • Valores predeterminados:
    • AdxPeriod = 14
    • AdxThreshold = 25m
    • AdxExitThreshold = 20m
    • CandleType = TimeSpan.FromMinutes(5).TimeFrame()
  • Filtros:
    • Categoría: Seguimiento de tendencia
    • Dirección: Ambos
    • Indicadores: Stochastic, Divergence
    • Stops: Sí
    • Complejidad: Intermedio
    • Marco temporal: Intradía (5m)
    • Estacionalidad: No
    • Redes neuronales: No
    • Divergencia: Sí
    • 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;

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;
	}
}