Bollinger Width Mean Reversion
The Bollinger Width Mean Reversion strategy focuses on extreme readings of the Bollinger to exploit reversion. Wide departures from the average level rarely last.
Testing indicates an average annual return of about 157%. It performs best in the crypto market.
Trades trigger when the indicator swings far from its mean and then begins to reverse. Both long and short setups include a protective stop.
Suited for swing traders expecting oscillations, the strategy closes out once the Bollinger returns toward balance. Starting parameter BollingerLength = 20.
Details
- Entry Criteria: Indicator crosses back toward mean.
- Long/Short: Both directions.
- Exit Criteria: Indicator reverts to average.
- Stops: Yes.
- Default Values:
BollingerLength= 20BollingerDeviation= 2.0mWidthLookbackPeriod= 20WidthDeviationMultiplier= 2.0mAtrPeriod= 14AtrMultiplier= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Mean Reversion
- Direction: Both
- Indicators: Bollinger
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Short-term
- 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>
/// Bollinger width mean reversion strategy.
/// Trades contractions and expansions of normalized Bollinger Bands width around its recent average.
/// </summary>
public class BollingerWidthMeanReversionStrategy : Strategy
{
private readonly StrategyParam<int> _bollingerLength;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<int> _widthLookbackPeriod;
private readonly StrategyParam<decimal> _widthDeviationMultiplier;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _cooldownBars;
private BollingerBands _bollinger;
private decimal[] _widthHistory;
private int _currentIndex;
private int _filledCount;
private int _cooldown;
/// <summary>
/// Period for Bollinger Bands calculation.
/// </summary>
public int BollingerLength
{
get => _bollingerLength.Value;
set => _bollingerLength.Value = value;
}
/// <summary>
/// Deviation multiplier for Bollinger Bands.
/// </summary>
public decimal BollingerDeviation
{
get => _bollingerDeviation.Value;
set => _bollingerDeviation.Value = value;
}
/// <summary>
/// Lookback period for width statistics.
/// </summary>
public int WidthLookbackPeriod
{
get => _widthLookbackPeriod.Value;
set => _widthLookbackPeriod.Value = value;
}
/// <summary>
/// Multiplier for width standard deviation thresholds.
/// </summary>
public decimal WidthDeviationMultiplier
{
get => _widthDeviationMultiplier.Value;
set => _widthDeviationMultiplier.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Cooldown bars between orders.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="BollingerWidthMeanReversionStrategy"/>.
/// </summary>
public BollingerWidthMeanReversionStrategy()
{
_bollingerLength = Param(nameof(BollingerLength), 20)
.SetGreaterThanZero()
.SetDisplay("Bollinger Length", "Period for Bollinger Bands calculation", "Indicators")
.SetOptimize(10, 50, 5);
_bollingerDeviation = Param(nameof(BollingerDeviation), 2m)
.SetGreaterThanZero()
.SetDisplay("Bollinger Deviation", "Deviation multiplier for Bollinger Bands", "Indicators")
.SetOptimize(1m, 3m, 0.5m);
_widthLookbackPeriod = Param(nameof(WidthLookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Width Lookback", "Lookback for width mean", "Strategy Parameters")
.SetOptimize(10, 50, 5);
_widthDeviationMultiplier = Param(nameof(WidthDeviationMultiplier), 1m)
.SetGreaterThanZero()
.SetDisplay("Width Dev Mult", "Multiplier for width standard deviation threshold", "Strategy Parameters")
.SetOptimize(0.5m, 3m, 0.5m);
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management");
_cooldownBars = Param(nameof(CooldownBars), 1200)
.SetRange(1, 5000)
.SetDisplay("Cooldown Bars", "Bars to wait between orders", "Risk Management");
_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();
_bollinger = null;
_currentIndex = default;
_filledCount = default;
_cooldown = default;
_widthHistory = new decimal[WidthLookbackPeriod];
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_bollinger = new BollingerBands
{
Length = BollingerLength,
Width = BollingerDeviation,
};
_widthHistory = new decimal[WidthLookbackPeriod];
_currentIndex = 0;
_filledCount = 0;
_cooldown = 0;
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_bollinger, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _bollinger);
DrawOwnTrades(area);
}
StartProtection(new(), new Unit(StopLossPercent, UnitTypes.Percent));
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue bollingerValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_bollinger.IsFormed)
return;
var bb = (BollingerBandsValue)bollingerValue;
if (bb.UpBand is not decimal upperBand ||
bb.LowBand is not decimal lowerBand ||
bb.MovingAverage is not decimal middleBand)
return;
if (middleBand <= 0)
return;
var lastWidth = (upperBand - lowerBand) / middleBand;
_widthHistory[_currentIndex] = lastWidth;
_currentIndex = (_currentIndex + 1) % WidthLookbackPeriod;
if (_filledCount < WidthLookbackPeriod)
_filledCount++;
if (_filledCount < WidthLookbackPeriod)
return;
var avgWidth = 0m;
var sumSq = 0m;
for (var i = 0; i < WidthLookbackPeriod; i++)
avgWidth += _widthHistory[i];
avgWidth /= WidthLookbackPeriod;
if (avgWidth <= 0)
return;
for (var i = 0; i < WidthLookbackPeriod; i++)
{
var diff = _widthHistory[i] - avgWidth;
sumSq += diff * diff;
}
var stdWidth = (decimal)Math.Sqrt((double)(sumSq / WidthLookbackPeriod));
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var lowerThreshold = avgWidth - WidthDeviationMultiplier * stdWidth;
var upperThreshold = avgWidth + WidthDeviationMultiplier * stdWidth;
if (Position == 0)
{
if (lastWidth < lowerThreshold)
{
BuyMarket();
_cooldown = CooldownBars;
}
else if (lastWidth > upperThreshold)
{
SellMarket();
_cooldown = CooldownBars;
}
}
else if (Position > 0 && lastWidth >= avgWidth)
{
SellMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
else if (Position < 0 && lastWidth <= avgWidth)
{
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")
import math
from System import TimeSpan, Math
from StockSharp.Messages import DataType, Unit, UnitTypes, CandleStates
from StockSharp.Algo.Indicators import BollingerBands
from StockSharp.Algo.Strategies import Strategy
class bollinger_width_mean_reversion_strategy(Strategy):
"""
Bollinger width mean reversion strategy.
Trades contractions and expansions of normalized Bollinger Bands width around its recent average.
"""
def __init__(self):
super(bollinger_width_mean_reversion_strategy, self).__init__()
self._bollinger_length = self.Param("BollingerLength", 20) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Length", "Period for Bollinger Bands calculation", "Indicators") \
.SetOptimize(10, 50, 5)
self._bollinger_deviation = self.Param("BollingerDeviation", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Deviation", "Deviation multiplier for Bollinger Bands", "Indicators") \
.SetOptimize(1.0, 3.0, 0.5)
self._width_lookback = self.Param("WidthLookbackPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Width Lookback", "Lookback for width mean", "Strategy Parameters") \
.SetOptimize(10, 50, 5)
self._width_dev_mult = self.Param("WidthDeviationMultiplier", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Width Dev Mult", "Multiplier for width standard deviation threshold", "Strategy Parameters") \
.SetOptimize(0.5, 3.0, 0.5)
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management")
self._cooldown_bars = self.Param("CooldownBars", 1200) \
.SetDisplay("Cooldown Bars", "Bars to wait between orders", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._bollinger = None
self._width_history = None
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(bollinger_width_mean_reversion_strategy, self).OnReseted()
self._bollinger = None
lb = int(self._width_lookback.Value)
self._width_history = [0.0] * lb
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
def OnStarted2(self, time):
super(bollinger_width_mean_reversion_strategy, self).OnStarted2(time)
lb = int(self._width_lookback.Value)
self._width_history = [0.0] * lb
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
self._bollinger = BollingerBands()
self._bollinger.Length = int(self._bollinger_length.Value)
self._bollinger.Width = self._bollinger_deviation.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(self._bollinger, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._bollinger)
self.DrawOwnTrades(area)
self.StartProtection(Unit(), Unit(self._stop_loss_percent.Value, UnitTypes.Percent))
def _process_candle(self, candle, bollinger_value):
if candle.State != CandleStates.Finished:
return
if not self._bollinger.IsFormed:
return
upper_band = bollinger_value.UpBand
lower_band = bollinger_value.LowBand
middle_band = bollinger_value.MovingAverage
if upper_band is None or lower_band is None or middle_band is None:
return
upper_val = float(upper_band)
lower_val = float(lower_band)
middle_val = float(middle_band)
if middle_val <= 0:
return
last_width = (upper_val - lower_val) / middle_val
lb = int(self._width_lookback.Value)
self._width_history[self._current_index] = last_width
self._current_index = (self._current_index + 1) % lb
if self._filled_count < lb:
self._filled_count += 1
if self._filled_count < lb:
return
# Calculate statistics
avg_width = 0.0
for i in range(lb):
avg_width += self._width_history[i]
avg_width /= float(lb)
if avg_width <= 0:
return
sum_sq = 0.0
for i in range(lb):
diff = self._width_history[i] - avg_width
sum_sq += diff * diff
std_width = math.sqrt(sum_sq / float(lb))
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown > 0:
self._cooldown -= 1
return
wdm = float(self._width_dev_mult.Value)
lower_threshold = avg_width - wdm * std_width
upper_threshold = avg_width + wdm * std_width
if self.Position == 0:
if last_width < lower_threshold:
self.BuyMarket()
self._cooldown = int(self._cooldown_bars.Value)
elif last_width > upper_threshold:
self.SellMarket()
self._cooldown = int(self._cooldown_bars.Value)
elif self.Position > 0 and last_width >= avg_width:
self.SellMarket(Math.Abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
elif self.Position < 0 and last_width <= avg_width:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
def CreateClone(self):
return bollinger_width_mean_reversion_strategy()