ATR Expansion Breakout
This strategy follows bursts of volatility using the Average True Range. When ATR is rising compared to the previous bar and price trades relative to a moving average, it looks to ride the breakout.
Testing indicates an average annual return of about 145%. It performs best in the crypto market.
Expansion of ATR implies a strong move is underway. Entries align with the direction of price relative to the moving average, while contractions in volatility trigger exits.
Stops are set using an ATR multiple to let trades breathe during high volatility.
Details
- Entry Criteria: ATR increasing and price above/below MA.
- Long/Short: Both directions.
- Exit Criteria: ATR contracts or stop is hit.
- Stops: Yes.
- Default Values:
AtrPeriod= 14MAPeriod= 20AtrMultiplier= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Breakout
- Direction: Both
- Indicators: ATR, MA
- Stops: Yes
- Complexity: Basic
- 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>
/// Strategy that trades on volatility expansion as measured by ATR.
/// Enters on any one-bar increase in formed ATR in the price/SMA direction,
/// exits when volatility contracts.
/// </summary>
public class AtrExpansionStrategy : Strategy
{
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevAtr;
private bool _hasPrev;
private Order _pendingOrder;
private Unit _stopDistance;
private bool _protectionStarted;
/// <summary>
/// Period for ATR calculation.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Period for Moving Average calculation.
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Type of candles used for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize the ATR Expansion strategy.
/// </summary>
public AtrExpansionStrategy()
{
_atrPeriod = Param(nameof(AtrPeriod), 14).SetGreaterThanZero()
.SetDisplay("ATR Period", "Period for ATR calculation", "Indicators")
.SetOptimize(7, 21, 7);
_maPeriod = Param(nameof(MAPeriod), 20).SetGreaterThanZero()
.SetDisplay("MA Period", "Period for MA calculation", "Indicators")
.SetOptimize(10, 50, 5);
_atrMultiplier = Param(nameof(AtrMultiplier), 2m).SetNotNegative()
.SetDisplay("ATR Stop Multiplier", "Frozen entry ATR stop distance; zero disables it", "Protection");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
OrderRegistering += order => _pendingOrder = order;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (Security, DataType.Level1)];
}
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevAtr = default;
_hasPrev = default;
_pendingOrder = null;
_stopDistance = null;
_protectionStarted = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevAtr = 0;
_hasPrev = false;
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var atr = new AverageTrueRange { Length = AtrPeriod };
var sma = new SimpleMovingAverage { Length = MAPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(atr, sma, ProcessCandle, false)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection runs before this callback, including between finished candles.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue atrValue, IIndicatorValue smaValue)
{
if (candle.State != CandleStates.Finished || !atrValue.Indicator.IsFormed || !smaValue.Indicator.IsFormed
|| !IsFormedAndOnlineAndAllowTrading())
return;
var atr = atrValue.GetValue<decimal>();
var mean = smaValue.GetValue<decimal>();
if (!_hasPrev)
{
_hasPrev = true;
_prevAtr = atr;
return;
}
var previous = _prevAtr;
_prevAtr = atr;
if (_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
// Any contraction closes, including a zero ATR; the MA direction is entry-only.
if (Position != 0m && atr < previous)
{
if (Position > 0m) SellMarket(Position);
else BuyMarket(Math.Abs(Position));
}
else if (Position == 0m && atr > previous)
{
if (candle.ClosePrice > mean) Enter(Sides.Buy, atr);
else if (candle.ClosePrice < mean) Enter(Sides.Sell, atr);
}
}
private void Enter(Sides side, decimal atr)
{
var distance = atr * AtrMultiplier;
_stopDistance ??= new Unit(distance);
// Keep the Unit reference held by native cached protection controllers.
_stopDistance.Value = distance;
if (!_protectionStarted && distance > 0m)
{
StartProtection(new Unit(), _stopDistance, useMarketOrders: true, isLocalStop: true);
_protectionStarted = true;
}
RegisterOrder(new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = Volume,
Comment = "ATR expansion entry",
});
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Unit, OrderStates, OrderTypes, Sides, Level1Fields
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Indicators import AverageTrueRange, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class atr_expansion_strategy(Strategy):
"""
Strategy that trades on volatility expansion as measured by ATR.
Enters on any one-bar increase in formed ATR in the price/SMA direction,
exits when volatility contracts.
"""
def __init__(self):
super(atr_expansion_strategy, self).__init__()
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero().SetDisplay("ATR Period", "Period for ATR calculation", "Indicators")
self._ma_period = self.Param("MAPeriod", 20).SetGreaterThanZero().SetDisplay("MA Period", "Period for MA calculation", "Indicators")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetNotNegative().SetDisplay("ATR Stop Multiplier", "Frozen entry ATR stop distance; zero disables it", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._prev_atr = Decimal.Zero
self._has_prev = False
self._pending_order = None
self._stop_distance = None
self._protection_started = False
self.OrderRegistering += self._track_pending
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(atr_expansion_strategy, self).OnReseted()
self._prev_atr = Decimal.Zero
self._has_prev = False
self._pending_order = None
self._stop_distance = None
self._protection_started = False
def OnStarted2(self, time):
super(atr_expansion_strategy, self).OnStarted2(time)
self._prev_atr = Decimal.Zero
self._has_prev = False
self._pending_order = None
self._stop_distance = None
self._protection_started = False
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(atr, sma, self._process_candle, False).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native protection runs before this callback, including between finished candles.
pass
def _process_candle(self, candle, atr_value, sma_value):
if candle.State != CandleStates.Finished or not atr_value.Indicator.IsFormed or not sma_value.Indicator.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
return
atr = atr_value.GetValue[Decimal](None)
mean = sma_value.GetValue[Decimal](None)
if not self._has_prev:
self._has_prev = True
self._prev_atr = atr
return
previous = self._prev_atr
self._prev_atr = atr
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
# Any contraction closes, including zero ATR; price/MA is only an entry filter.
if self.Position != 0 and atr < previous:
if self.Position > 0:
self.SellMarket(self.Position)
else:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and atr > previous:
if candle.ClosePrice > mean:
self._enter(Sides.Buy, atr)
elif candle.ClosePrice < mean:
self._enter(Sides.Sell, atr)
def _enter(self, side, atr):
distance = atr * Decimal(self._atr_multiplier.Value)
if self._stop_distance is None:
self._stop_distance = Unit(distance)
# Keep the same Unit held by native cached protection controllers.
self._stop_distance.Value = distance
if not self._protection_started and distance > 0:
self.StartProtection(Unit(), self._stop_distance, useMarketOrders=True, isLocalStop=True)
self._protection_started = True
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = self.Volume
order.Comment = "ATR expansion entry"
self.RegisterOrder(order)
def CreateClone(self):
return atr_expansion_strategy()