Volatility Cluster Breakout
The Volatility Cluster Breakout strategy is built around breakouts during high volatility clusters.
Testing indicates an average annual return of about 169%. It performs best in the crypto market.
Signals trigger when its indicators confirms breakout opportunities on intraday (5m) data. This makes the method suitable for active traders.
Stops rely on ATR multiples and factors like PriceAvgPeriod, AtrPeriod. Adjust these defaults to balance risk and reward.
Details
- Entry Criteria: see implementation for indicator conditions.
- Long/Short: Both directions.
- Exit Criteria: opposite signal or stop logic.
- Stops: Yes, using indicator-based calculations.
- Default Values:
PriceAvgPeriod = 20AtrPeriod = 14StdDevMultiplier = 2.0mStopMultiplier = 2.0mCandleType = TimeSpan.FromMinutes(5).TimeFrame()
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: multiple indicators
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (5m)
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
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;
namespace StockSharp.Samples.Strategies;
/// <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 _entryPrice;
private decimal _entryAtr;
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), 1.3m)
.SetRange(0.25m, 5m)
.SetDisplay("StdDev Multiplier", "Multiplier for breakout levels", "Signals");
_stopMultiplier = Param(nameof(StopMultiplier), 1.8m)
.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;
_entryPrice = 0m;
_entryAtr = 0m;
_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);
}
StartProtection(new Unit(0, UnitTypes.Absolute), new Unit(StopMultiplier, UnitTypes.Percent), false);
}
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;
if (ProcessState != ProcessStates.Started)
return;
if (_cooldown > 0)
{
_cooldown--;
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 (!isHighVolatility)
return;
if (price >= upperLevel)
{
_entryPrice = price;
_entryAtr = atrValue;
BuyMarket();
_cooldown = CooldownBars;
}
else if (price <= lowerLevel)
{
_entryPrice = price;
_entryAtr = atrValue;
SellMarket();
_cooldown = CooldownBars;
}
return;
}
var stopDistance = _entryAtr * StopMultiplier;
if (Position > 0)
{
if (price <= smaValue || !isHighVolatility || price <= _entryPrice - stopDistance)
{
SellMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
}
else if (Position < 0)
{
if (price >= smaValue || !isHighVolatility || price >= _entryPrice + stopDistance)
{
BuyMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import SimpleMovingAverage, StandardDeviation, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class volatility_cluster_breakout_strategy(Strategy):
"""
Breakout strategy that trades only when ATR expands into a high-volatility cluster.
"""
def __init__(self):
super(volatility_cluster_breakout_strategy, self).__init__()
self._price_avg_period = self.Param("PriceAvgPeriod", 20) \
.SetDisplay("Price Average Period", "Period for moving average and standard deviation", "Indicators")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetDisplay("ATR Period", "Period for ATR calculation", "Indicators")
self._std_dev_multiplier = self.Param("StdDevMultiplier", 1.3) \
.SetDisplay("StdDev Multiplier", "Multiplier for breakout levels", "Signals")
self._stop_multiplier = self.Param("StopMultiplier", 1.8) \
.SetDisplay("Stop ATR Multiplier", "ATR multiplier used for stop distance", "Risk")
self._cooldown_bars = self.Param("CooldownBars", 60) \
.SetDisplay("Cooldown Bars", "Bars to wait after each order", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles for the strategy", "General")
self._sma = None
self._std_dev = None
self._atr = None
self._atr_avg = None
self._entry_price = 0.0
self._entry_atr = 0.0
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(volatility_cluster_breakout_strategy, self).OnReseted()
self._sma = None
self._std_dev = None
self._atr = None
self._atr_avg = None
self._entry_price = 0.0
self._entry_atr = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(volatility_cluster_breakout_strategy, self).OnStarted2(time)
atr_period = int(self._atr_period.Value)
price_period = int(self._price_avg_period.Value)
self._sma = SimpleMovingAverage()
self._sma.Length = price_period
self._std_dev = StandardDeviation()
self._std_dev.Length = price_period
self._atr = AverageTrueRange()
self._atr.Length = atr_period
self._atr_avg = SimpleMovingAverage()
self._atr_avg.Length = max(atr_period * 2, 10)
self._cooldown = 0
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._sma, self._std_dev, self._atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._sma)
self.DrawIndicator(area, self._atr)
self.DrawOwnTrades(area)
self.StartProtection(Unit(0, UnitTypes.Absolute), Unit(self._stop_multiplier.Value, UnitTypes.Percent), False)
def _process_candle(self, candle, sma_value, std_dev_value, atr_value):
if candle.State != CandleStates.Finished:
return
av = float(atr_value)
atr_avg_value = float(process_float(self._atr_avg, Decimal(av), candle.OpenTime, True))
if not self._sma.IsFormed or not self._std_dev.IsFormed or not self._atr.IsFormed or not self._atr_avg.IsFormed:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown > 0:
self._cooldown -= 1
return
sv = float(sma_value)
sdv = float(std_dev_value)
sdm = float(self._std_dev_multiplier.Value)
upper_level = sv + sdm * sdv
lower_level = sv - sdm * sdv
is_high_volatility = av >= atr_avg_value * 1.15
price = float(candle.ClosePrice)
cd = int(self._cooldown_bars.Value)
sm = float(self._stop_multiplier.Value)
if self.Position == 0:
if not is_high_volatility:
return
if price >= upper_level:
self._entry_price = price
self._entry_atr = av
self.BuyMarket()
self._cooldown = cd
elif price <= lower_level:
self._entry_price = price
self._entry_atr = av
self.SellMarket()
self._cooldown = cd
return
stop_distance = self._entry_atr * sm
if self.Position > 0:
if price <= sv or not is_high_volatility or price <= self._entry_price - stop_distance:
self.SellMarket(Math.Abs(self.Position))
self._cooldown = cd
elif self.Position < 0:
if price >= sv or not is_high_volatility or price >= self._entry_price + stop_distance:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown = cd
def CreateClone(self):
return volatility_cluster_breakout_strategy()