Открыть на GitHub

Стратегия истощения объёма

Резкие всплески объёма часто сигнализируют об окончании движения, когда трейдеры массово закрывают или открывают позиции. Эта стратегия сравнивает текущий объём со средним, чтобы найти признаки истощения. В сочетании с направлением свечи и фильтром скользящей средней это помогает выявить точки разворота.

Тестирование показывает среднегодичную доходность около 133%. Стратегию лучше запускать на крипторынке.

Каждая свеча обновляет средний объём. Если объём новой свечи превышает средний на заданный множитель и свеча закрывается в направлении, противоположном текущему тренду, открывается сделка. Стоп‑лосс основывается на ATR.

Обычно позиция закрывается именно по стоп‑лоссу, так как ожидается быстрый разворот после всплеска объёма.

Детали

  • Условия входа: всплеск объёма выше среднего и свеча против тренда.
  • Длинные/короткие: обе стороны.
  • Условия выхода: стоп‑лосс.
  • Стопы: да, на основе ATR.
  • Значения по умолчанию:
    • VolumePeriod = 20
    • VolumeMultiplier = 2.0
    • MAPeriod = 20
    • AtrMultiplier = 2 ATR
    • CandleType = 5 минут
  • Фильтры:
    • Категория: разворот
    • Направление: оба
    • Индикаторы: объём, MA, ATR
    • Стопы: да
    • Сложность: средняя
    • Таймфрейм: внутридневной
    • Сезонность: нет
    • Нейросети: нет
    • Дивергенция: нет
    • Уровень риска: средний
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 Exhaustion strategy.
/// Looks for volume spikes (current volume much higher than previous) with directional candles.
/// High volume + bullish above SMA = buy.
/// High volume + bearish below SMA = sell.
/// Exits when price crosses SMA in opposite direction.
/// </summary>
public class VolumeExhaustionStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _cooldownBars;

	private decimal _prevVolume;
	private int _cooldown;

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

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

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

	/// <summary>
	/// Constructor.
	/// </summary>
	public VolumeExhaustionStrategy()
	{
		_maPeriod = Param(nameof(MAPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Period for SMA", "Indicators");

		_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();
		_prevVolume = default;
		_cooldown = default;
	}

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

		_prevVolume = 0;
		_cooldown = 0;

		var sma = new SimpleMovingAverage { Length = MAPeriod };

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

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

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

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_prevVolume = candle.TotalVolume;
			return;
		}

		if (_prevVolume == 0)
		{
			_prevVolume = candle.TotalVolume;
			return;
		}

		if (_cooldown > 0)
		{
			_cooldown--;
			_prevVolume = candle.TotalVolume;
			return;
		}

		// Volume spike: current volume significantly higher than previous
		var volumeSpike = _prevVolume > 0 && candle.TotalVolume > _prevVolume * 1.5m;

		var isBullish = candle.ClosePrice > candle.OpenPrice;
		var isBearish = candle.ClosePrice < candle.OpenPrice;

		if (Position == 0 && volumeSpike && isBullish && candle.ClosePrice > smaValue)
		{
			BuyMarket();
			_cooldown = CooldownBars;
		}
		else if (Position == 0 && volumeSpike && isBearish && candle.ClosePrice < smaValue)
		{
			SellMarket();
			_cooldown = CooldownBars;
		}
		else if (Position > 0 && candle.ClosePrice < smaValue)
		{
			SellMarket();
			_cooldown = CooldownBars;
		}
		else if (Position < 0 && candle.ClosePrice > smaValue)
		{
			BuyMarket();
			_cooldown = CooldownBars;
		}

		_prevVolume = candle.TotalVolume;
	}
}