Ver en GitHub

Volume Divergence

Volume Divergence looks for discrepancies between price movement and trading volume. If price falls but volume increases, it may signal accumulation; if price rises with strong volume, it may signal distribution.

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

The strategy enters long when falling prices are accompanied by rising volume, and enters short when rising prices pair with heavy volume. Exits rely on a moving average crossover.

This approach attempts to trade against unsustainable moves.

Details

  • Entry Criteria: Price and volume moving in opposite directions.
  • Long/Short: Both directions.
  • Exit Criteria: Price crosses MA or stop.
  • Stops: Yes.
  • Default Values:
    • MAPeriod = 20
    • ATRPeriod = 14
    • CandleType = TimeSpan.FromMinutes(5)
  • Filters:
    • Category: Divergence
    • Direction: Both
    • Indicators: Volume, MA
    • Stops: Yes
    • Complexity: Intermediate
    • Timeframe: Intraday
    • Seasonality: No
    • Neural Networks: No
    • Divergence: Yes
    • Risk Level: Medium
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Volume Divergence strategy.
/// Long entry: Price falls but volume increases (possible accumulation).
/// Short entry: Price rises but volume increases (possible distribution).
/// Exit: Price crosses MA.
/// </summary>
public class VolumeDivergenceStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _cooldownBars;

	private decimal _previousClose;
	private decimal _previousVolume;
	private int _cooldown;

	/// <summary>
	/// MA Period.
	/// </summary>
	public int MAPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

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

	/// <summary>
	/// Cooldown bars between trades.
	/// </summary>
	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="VolumeDivergenceStrategy"/>.
	/// </summary>
	public VolumeDivergenceStrategy()
	{
		_maPeriod = Param(nameof(MAPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Period for Moving Average", "Indicators")
			.SetOptimize(10, 50, 10);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");

		_cooldownBars = Param(nameof(CooldownBars), 500)
			.SetRange(1, 1000)
			.SetDisplay("Cooldown Bars", "Bars to wait between trades", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_previousClose = default;
		_previousVolume = default;
		_cooldown = default;
	}

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

		_previousClose = 0;
		_previousVolume = 0;
		_cooldown = 0;

		var ma = new SimpleMovingAverage { Length = MAPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ma, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, ma);
			DrawOwnTrades(area);
		}
	}

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (_previousClose == 0)
		{
			_previousClose = candle.ClosePrice;
			_previousVolume = candle.TotalVolume;
			return;
		}

		if (_cooldown > 0)
		{
			_cooldown--;
			_previousClose = candle.ClosePrice;
			_previousVolume = candle.TotalVolume;
			return;
		}

		var priceDown = candle.ClosePrice < _previousClose;
		var priceUp = candle.ClosePrice > _previousClose;
		var volumeUp = candle.TotalVolume > _previousVolume;

		var bullishDivergence = priceDown && volumeUp;
		var bearishDivergence = priceUp && volumeUp;

		if (Position == 0)
		{
			if (bullishDivergence && candle.ClosePrice < maValue)
			{
				BuyMarket();
				_cooldown = CooldownBars;
			}
			else if (bearishDivergence && candle.ClosePrice > maValue)
			{
				SellMarket();
				_cooldown = CooldownBars;
			}
		}
		else if (Position > 0 && candle.ClosePrice < maValue && !bullishDivergence)
		{
			SellMarket();
			_cooldown = CooldownBars;
		}
		else if (Position < 0 && candle.ClosePrice > maValue && !bearishDivergence)
		{
			BuyMarket();
			_cooldown = CooldownBars;
		}

		_previousClose = candle.ClosePrice;
		_previousVolume = candle.TotalVolume;
	}
}