View on GitHub

Volatility Cluster Breakout

Uses finished 5-minute candles and waits for formed indicators and permission to trade. All moving statistics include the signal candle. C is its close, M = SMA(C, 20), D = population standard deviation(C, 20), U = M + 2 × D, L = M − 2 × D, and A = ATR(14) using Wilder smoothing of true range.

The high-volatility condition is A >= 1.15 × SMA(A, 28).

From a flat position, buy when high volatility is confirmed and C >= U; sell when it is confirmed and C <= L. The entry volume is Volume. A new entry is allowed only after the entry cooldown expires and while no order is pending. Entry requires A > 0.

k = StopMultiplier. Freeze the signal close and ATR at entry: the long stop is C_entry − k × A_entry, the short stop is C_entry + k × A_entry. On each finished candle, close the entire actual position with a market order if the close reaches the stored stop, a long has C <= M, a short has C >= M, or high volatility is no longer confirmed.

The cooldown blocks new entries for the next 60 finished candles after an order; exits remain active throughout. Stop checks occur at candle close, and market fills use the available execution price. The stop distance stays fixed while the position is held. There is no take profit or trailing stop.

Default parameters

PriceAvgPeriod = 20
StdDevMultiplier = 2
AtrPeriod = 14
StopMultiplier = 2
CooldownBars = 60
CandleType = TimeSpan.FromMinutes(5).TimeFrame()
namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using Ecng.Common;

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

/// <summary>
/// Breakout strategy that trades only when ATR expands into a high-volatility cluster.
/// </summary>
public class VolatilityClusterBreakoutStrategy : Strategy
{
	private readonly StrategyParam<int> _priceAvgPeriod;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _stdDevMultiplier;
	private readonly StrategyParam<decimal> _stopMultiplier;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _sma;
	private StandardDeviation _stdDev;
	private AverageTrueRange _atr;
	private SimpleMovingAverage _atrAvg;
	private decimal _stopPrice;
	private Order _pendingOrder;
	private int _cooldown;

	/// <summary>
	/// Period for the moving average and standard deviation.
	/// </summary>
	public int PriceAvgPeriod
	{
		get => _priceAvgPeriod.Value;
		set => _priceAvgPeriod.Value = value;
	}

	/// <summary>
	/// Period for ATR calculation.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Standard deviation multiplier used for breakout levels.
	/// </summary>
	public decimal StdDevMultiplier
	{
		get => _stdDevMultiplier.Value;
		set => _stdDevMultiplier.Value = value;
	}

	/// <summary>
	/// ATR multiplier used for stop distance.
	/// </summary>
	public decimal StopMultiplier
	{
		get => _stopMultiplier.Value;
		set => _stopMultiplier.Value = value;
	}

	/// <summary>
	/// Bars to wait after each order.
	/// </summary>
	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public VolatilityClusterBreakoutStrategy()
	{
		_priceAvgPeriod = Param(nameof(PriceAvgPeriod), 20)
			.SetRange(5, 100)
			.SetDisplay("Price Average Period", "Period for moving average and standard deviation", "Indicators");

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetRange(5, 50)
			.SetDisplay("ATR Period", "Period for ATR calculation", "Indicators");

		_stdDevMultiplier = Param(nameof(StdDevMultiplier), 2m)
			.SetRange(0.25m, 5m)
			.SetDisplay("StdDev Multiplier", "Multiplier for breakout levels", "Signals");

		_stopMultiplier = Param(nameof(StopMultiplier), 2m)
			.SetRange(0.5m, 10m)
			.SetDisplay("Stop ATR Multiplier", "ATR multiplier used for stop distance", "Risk");

		_cooldownBars = Param(nameof(CooldownBars), 60)
			.SetRange(1, 500)
			.SetDisplay("Cooldown Bars", "Bars to wait after each order", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles for the strategy", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		if (Security != null)
			yield return (Security, CandleType);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_sma = null;
		_stdDev = null;
		_atr = null;
		_atrAvg = null;
		_stopPrice = 0m;
		_pendingOrder = null;
		_cooldown = 0;
	}

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

		if (Security == null)
			throw new InvalidOperationException("Security is not specified.");

		_sma = new SimpleMovingAverage { Length = PriceAvgPeriod };
		_stdDev = new StandardDeviation { Length = PriceAvgPeriod };
		_atr = new AverageTrueRange { Length = AtrPeriod };
		_atrAvg = new SimpleMovingAverage { Length = Math.Max(AtrPeriod * 2, 10) };
		_cooldown = 0;

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(_sma, _stdDev, _atr, ProcessCandle)
			.Start();

		var area = CreateChartArea();

		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _sma);
			DrawIndicator(area, _atr);
			DrawOwnTrades(area);
		}
	}

	private bool IsOrderPending => _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);

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

		var atrAvgValue = _atrAvg.Process(atrValue, candle.OpenTime, true).ToDecimal();

		if (!_sma.IsFormed || !_stdDev.IsFormed || !_atr.IsFormed || !_atrAvg.IsFormed)
			return;

		var isCoolingDown = _cooldown > 0;
		if (isCoolingDown)
			_cooldown--;

		if (!IsFormedAndOnlineAndAllowTrading() || IsOrderPending)
			return;

		var upperLevel = smaValue + StdDevMultiplier * stdDevValue;
		var lowerLevel = smaValue - StdDevMultiplier * stdDevValue;
		var isHighVolatility = atrValue >= atrAvgValue * 1.15m;
		var price = candle.ClosePrice;

		if (Position == 0)
		{
			if (isCoolingDown || !isHighVolatility || atrValue <= 0m)
				return;

			if (price >= upperLevel)
			{
				_stopPrice = price - atrValue * StopMultiplier;
				_pendingOrder = BuyMarket();
				_cooldown = CooldownBars;
			}
			else if (price <= lowerLevel)
			{
				_stopPrice = price + atrValue * StopMultiplier;
				_pendingOrder = SellMarket();
				_cooldown = CooldownBars;
			}

			return;
		}

		if (Position > 0)
		{
			if (price <= smaValue || !isHighVolatility || price <= _stopPrice)
			{
				_pendingOrder = SellMarket(Math.Abs(Position));
				_cooldown = CooldownBars;
			}
		}
		else if (Position < 0)
		{
			if (price >= smaValue || !isHighVolatility || price >= _stopPrice)
			{
				_pendingOrder = BuyMarket(Math.Abs(Position));
				_cooldown = CooldownBars;
			}
		}
	}
}