The Volatility Breakout strategy seeks strong directional moves when price escapes from its average range. By measuring the distance from a simple moving average using the Average True Range, the algorithm defines breakout thresholds that scale with volatility.
Testing indicates an average annual return of about 97%. It performs best in the crypto market.
A buy order is triggered when the close rises above the SMA by more than Multiplier times the ATR. A sell signal appears when the close falls below the SMA by the same distance. Positions remain open until an opposite breakout occurs or a protective stop is hit.
This technique caters to intraday traders who thrive on momentum surges. Using ATR-based thresholds helps filter out noise so only significant moves generate trades.
Details
Entry Criteria:
Long: Close > SMA + Multiplier * ATR
Short: Close < SMA - Multiplier * ATR
Long/Short: Both sides.
Exit Criteria:
Long: Exit when an opposite breakout triggers or stop-loss hits
Short: Exit when an opposite breakout triggers or stop-loss hits
Stops: Yes, stop-loss at Multiplier * ATR from entry.
Default Values:
Period = 20
Multiplier = 2
The stop is set Multiplier times the Period ATR from the entry close and checked on candle closes. An entry signal against an open position reverses it.
CandleType = TimeSpan.FromMinutes(5)
Filters:
Category: Breakout
Direction: Both
Indicators: SMA, ATR
Stops: Yes
Complexity: Intermediate
Timeframe: Intraday
Seasonality: No
Neural networks: No
Divergence: No
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>
/// Volatility Breakout strategy.
/// A close more than Multiplier times the Period ATR above the Period simple moving average goes long and one that far below it goes short,
/// reversing an opposite position. The position stays open until the opposite breakout, or until the stop Multiplier ATR from the entry
/// close is hit, checked on candle closes.
/// </summary>
public class VolatilityBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _period;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<DataType> _candleType;
private decimal _stopPrice;
/// <summary>
/// Period for SMA and ATR.
/// </summary>
public int Period
{
get => _period.Value;
set => _period.Value = value;
}
/// <summary>
/// ATR multiplier for the breakout threshold and the stop.
/// </summary>
public decimal Multiplier
{
get => _multiplier.Value;
set => _multiplier.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public VolatilityBreakoutStrategy()
{
_period = Param(nameof(Period), 20)
.SetGreaterThanZero()
.SetDisplay("Period", "Period for SMA and ATR", "Parameters");
_multiplier = Param(nameof(Multiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Multiplier", "ATR multiplier for the breakout threshold and the stop", "Parameters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_stopPrice = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_stopPrice = default;
var sma = new SimpleMovingAverage { Length = Period };
var atr = new AverageTrueRange { Length = Period };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(sma, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
var oscillators = CreateChartArea();
if (oscillators != null)
{
DrawIndicator(oscillators, atr);
}
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue smaValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!smaValue.IsFormed || !atrValue.IsFormed)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var sma = smaValue.GetValue<decimal>();
var distance = Multiplier * atrValue.GetValue<decimal>();
var close = candle.ClosePrice;
if (close > sma + distance && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_stopPrice = close - distance;
}
else if (close < sma - distance && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_stopPrice = close + distance;
}
else if (Position > 0 && close <= _stopPrice)
{
SellMarket(Position);
}
else if (Position < 0 && close >= _stopPrice)
{
BuyMarket(-Position);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class volatility_breakout_strategy(Strategy):
"""
Volatility Breakout strategy.
A close more than Multiplier times the Period ATR above the Period simple moving average goes long and one that far below it goes short,
reversing an opposite position. The position stays open until the opposite breakout, or until the stop Multiplier ATR from the entry
close is hit, checked on candle closes.
"""
def __init__(self):
super(volatility_breakout_strategy, self).__init__()
self._period = self.Param("Period", 20).SetGreaterThanZero().SetDisplay("Period", "Period for SMA and ATR", "Parameters")
self._multiplier = self.Param("Multiplier", 2.0).SetGreaterThanZero().SetDisplay("Multiplier", "ATR multiplier for the breakout threshold and the stop", "Parameters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._stop_price = Decimal(0)
def OnReseted(self):
super(volatility_breakout_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(volatility_breakout_strategy, self).OnStarted2(time)
self._reset_state()
sma = SimpleMovingAverage()
sma.Length = self._period.Value
atr = AverageTrueRange()
atr.Length = self._period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(sma, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
oscillators = self.CreateChartArea()
if oscillators is not None:
self.DrawIndicator(oscillators, atr)
def _process_candle(self, candle, sma_value, atr_value):
if candle.State != CandleStates.Finished:
return
if not sma_value.IsFormed or not atr_value.IsFormed:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
sma = sma_value.GetValue[Decimal](None)
distance = Decimal(self._multiplier.Value) * atr_value.GetValue[Decimal](None)
close = candle.ClosePrice
if close > sma + distance and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._stop_price = close - distance
elif close < sma - distance and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._stop_price = close + distance
elif self.Position > 0 and close <= self._stop_price:
self.SellMarket(self.Position)
elif self.Position < 0 and close >= self._stop_price:
self.BuyMarket(-self.Position)
def CreateClone(self):
return volatility_breakout_strategy()