Bollinger Percent B Reversion
This approach fades price extremes beyond the Bollinger Bands using the Percent B indicator. Moves above the upper band or below the lower band suggest overextension.
Testing indicates an average annual return of about 142%. It performs best in the stocks market.
When percent B is less than zero or greater than one, the system bets on a return to the middle of the band. An exit threshold closes trades once momentum normalizes.
Stops are placed at a fixed percentage from entry.
Details
- Entry Criteria: Percent B outside the 0–1 range.
- Long/Short: Both directions.
- Exit Criteria: Percent B crosses
ExitValueor stop. - Stops: Yes.
- Default Values:
BollingerPeriod= 20BollingerDeviation= 2.0mExitValue= 0.5mStopLossPercent= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Mean Reversion
- Direction: Both
- Indicators: Bollinger Bands
- 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 Bollinger %B indicator.
/// Bollinger %B shows where price is relative to the Bollinger Bands.
/// Values below 0 or above 1 indicate price outside the bands.
/// </summary>
public class BollingerPercentBStrategy : Strategy
{
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<decimal> _exitValue;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private Order _pendingOrder;
private decimal? _previousPercentB;
/// <summary>
/// Period for Bollinger Bands calculation.
/// </summary>
public int BollingerPeriod
{
get => _bollingerPeriod.Value;
set => _bollingerPeriod.Value = value;
}
/// <summary>
/// Deviation for Bollinger Bands calculation.
/// </summary>
public decimal BollingerDeviation
{
get => _bollingerDeviation.Value;
set => _bollingerDeviation.Value = value;
}
/// <summary>
/// Exit threshold for %B.
/// </summary>
public decimal ExitValue
{
get => _exitValue.Value;
set => _exitValue.Value = value;
}
/// <summary>
/// Type of candles used for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize the Bollinger %B Reversion strategy.
/// </summary>
public BollingerPercentBStrategy()
{
_bollingerPeriod = Param(nameof(BollingerPeriod), 20).SetGreaterThanZero()
.SetDisplay("Bollinger Period", "Period for Bollinger Bands calculation", "Indicators")
.SetOptimize(10, 30, 5);
_bollingerDeviation = Param(nameof(BollingerDeviation), 2.0m).SetGreaterThanZero()
.SetDisplay("Bollinger Deviation", "Deviation for Bollinger Bands calculation", "Indicators")
.SetOptimize(1.5m, 2.5m, 0.25m);
_exitValue = Param(nameof(ExitValue), 0.5m).SetRange(0m, 1m)
.SetDisplay("Exit %B Value", "Exit threshold for %B", "Exit")
.SetOptimize(0.3m, 0.7m, 0.1m);
_stopLossPercent = Param(nameof(StopLossPercent), 2m).SetNotNegative()
.SetDisplay("Stop Loss (%)", "Actual-fill percent stop; 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 StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pendingOrder = null;
_previousPercentB = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
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 percentB = new BollingerPercentB
{
Length = BollingerPeriod,
StdDevMultiplier = BollingerDeviation
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(percentB, ProcessCandle, true)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, percentB);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection runs before this callback, including between finished candles.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue value)
{
if (candle.State != CandleStates.Finished || !value.Indicator.IsFormed || !IsFormedAndOnlineAndAllowTrading())
return;
// Native %B is in percent units and empty for collapsed bands.
// Explicitly normalize to the README's scale; collapsed bands are neutral, not directional zero.
var percentB = value.IsEmpty ? 0.5m : value.GetValue<decimal>() / 100m;
var previous = _previousPercentB;
_previousPercentB = percentB;
if (_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
var upwardCross = previous is decimal prevUp && prevUp < ExitValue && percentB >= ExitValue;
var downwardCross = previous is decimal prevDown && prevDown > ExitValue && percentB <= ExitValue;
if (Position > 0m && upwardCross)
SellMarket(Position);
else if (Position < 0m && downwardCross)
BuyMarket(Math.Abs(Position));
else if (Position == 0m && percentB < 0m)
BuyMarket(Volume);
else if (Position == 0m && percentB > 1m)
SellMarket(Volume);
}
}
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, UnitTypes, OrderStates, Level1Fields
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Indicators import BollingerPercentB
from StockSharp.Algo.Strategies import Strategy
class bollinger_percent_b_strategy(Strategy):
"""
Bollinger %B strategy.
Buys when %B < 0 (below lower band), sells when %B > 1 (above upper band).
"""
def __init__(self):
super(bollinger_percent_b_strategy, self).__init__()
self._bb_period = self.Param("BollingerPeriod", 20).SetGreaterThanZero().SetDisplay("Bollinger Period", "Period for Bollinger Bands calculation", "Indicators")
self._bb_deviation = self.Param("BollingerDeviation", 2.0).SetGreaterThanZero().SetDisplay("Bollinger Deviation", "Deviation for Bollinger Bands calculation", "Indicators")
self._exit_value = self.Param("ExitValue", 0.5).SetRange(0.0, 1.0).SetDisplay("Exit %B Value", "Exit threshold for %B", "Exit")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop Loss (%)", "Actual-fill percent stop; 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._pending_order = None
self._previous_percent_b = None
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(bollinger_percent_b_strategy, self).OnReseted()
self._pending_order = None
self._previous_percent_b = None
def OnStarted2(self, time):
super(bollinger_percent_b_strategy, self).OnStarted2(time)
self._pending_order = None
self._previous_percent_b = None
self.StartProtection(Unit(), Unit(Decimal(self._stop_loss_percent.Value), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
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()
bb = BollingerPercentB()
bb.Length = self._bb_period.Value
bb.StdDevMultiplier = Decimal(self._bb_deviation.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(bb, self._process_candle, True).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, bb)
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, value):
if candle.State != CandleStates.Finished or not value.Indicator.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
return
# Native %B uses percent units and is empty for collapsed bands.
# Use an explicitly neutral 0.5 for collapsed bands, not directional zero.
pct = Decimal(0.5) if value.IsEmpty else value.GetValue[Decimal](None) / Decimal(100)
previous = self._previous_percent_b
self._previous_percent_b = pct
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
level = Decimal(self._exit_value.Value)
upward_cross = previous is not None and previous < level and pct >= level
downward_cross = previous is not None and previous > level and pct <= level
if self.Position > 0 and upward_cross:
self.SellMarket(self.Position)
elif self.Position < 0 and downward_cross:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and pct < 0:
self.BuyMarket(self.Volume)
elif self.Position == 0 and pct > 1:
self.SellMarket(self.Volume)
def CreateClone(self):
return bollinger_percent_b_strategy()