Bands Strategy
Overview
This strategy ports the MetaTrader 5 expert advisor Bands.mq5 to the StockSharp high-level API. It waits for a finished candle that pierces the Bollinger Bands from the outside back into the channel and only opens a position when the Donchian Channel conf irms that the band slope has been stable for a configurable number of bars. Average True Range (ATR) multiples reproduce the ori ginal stop-loss and take-profit distances, while an optional regression tracker prints the equity curve determination coefficient (R-squared) every 100 trades, mirroring the diagnostic output of the MQL version.
Trading logic
- Subscribe to a single candle stream and compute Bollinger Bands, a Donchian Channel and ATR with the same periods as the MetaT rader robot.
- When no position is open, inspect the previous completed candle:
- Enter long if that candle opened below the lower Bollinger Band and closed above it, and the Donchian lower band has not decl
ined for more than
ConfirmationPeriodbars. - Enter short if the candle opened above the upper Bollinger Band and closed below it, and the Donchian upper band has not ris
en for more than
ConfirmationPeriodbars.
- Enter long if that candle opened below the lower Bollinger Band and closed above it, and the Donchian lower band has not decl
ined for more than
- When a position exists, exit if either the trailing Donchian boundary is crossed (using the previous close) or if the ATR-base d protective levels are violated intrabar.
- Every executed trade stores the current portfolio equity and prints the linear-regression R-squared metric after each block of 100 trades. A negative slope produces a negative R-squared just like the original expert advisor.
Risk management
- Entry orders are always sent at market with the user-defined
TradeVolume. - Protective levels are recreated in code (instead of using pending orders) by comparing candle highs and lows against the ATR mu tiples.
- When the stop-loss or take-profit triggers, the strategy closes the entire position with a market order and resets the protecti on levels.
Parameters
| Parameter | Description |
|---|---|
TradeVolume |
Net volume (in lots) for each market order. |
CandleType |
Candle data type / timeframe used for all indicators. |
BollingerPeriod |
Number of candles used by the Bollinger Bands. |
BollingerDeviation |
Standard deviation multiplier applied to the Bollinger Bands. |
DonchianPeriod |
Length of the Donchian Channel used as trend filter. |
ConfirmationPeriod |
Minimum count of consecutive bars that must keep the Donchian slope non-decreasing (long) or non-increasing (short). |
AtrPeriod |
Period of the Average True Range used for risk management. |
StopAtrMultiplier |
ATR multiple that defines the stop-loss distance. |
TakeAtrMultiplier |
ATR multiple that defines the take-profit distance. |
Notes
- The Donchian slope check is implemented as a rolling counter instead of copying indicator buffers, which keeps the StockSharp version efficient while matching the behaviour of the original EA.
- All comments and diagnostics are provided in English as required by the project guidelines.
- Money-management helpers from the MetaTrader code are not reproduced; the StockSharp implementation relies on the
TradeVolumeparameter for position sizing.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Bollinger Bands breakout strategy confirmed by Donchian channel slope and ATR-based risk management.
/// </summary>
public class BandsStrategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<int> _donchianPeriod;
private readonly StrategyParam<int> _confirmationPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _stopAtrMultiplier;
private readonly StrategyParam<decimal> _takeAtrMultiplier;
private decimal? _prevOpen;
private decimal? _prevClose;
private decimal? _prevLowerBand;
private decimal? _prevUpperBand;
private decimal? _prevDonchLower;
private decimal? _prevDonchUpper;
private decimal? _prevAtr;
private int _lowerTrendLength;
private int _upperTrendLength;
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
private int _equitySamples;
private decimal _sumIndices;
private decimal _sumEquity;
private decimal _sumIndexEquity;
private decimal _sumIndexSquared;
private decimal _sumEquitySquared;
/// <summary>
/// Initializes a new instance of <see cref="BandsStrategy"/>.
/// </summary>
public BandsStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Volume", "Net volume in lots sent with every order", "Trading")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Time frame used for indicator calculations", "Market Data");
_bollingerPeriod = Param(nameof(BollingerPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Bollinger Period", "Number of candles used for the Bollinger Bands", "Indicators")
;
_bollingerDeviation = Param(nameof(BollingerDeviation), 1m)
.SetGreaterThanZero()
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for the Bollinger Bands", "Indicators")
;
_donchianPeriod = Param(nameof(DonchianPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Donchian Period", "Donchian Channel length used as trend filter", "Indicators")
;
_confirmationPeriod = Param(nameof(ConfirmationPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Slope Confirmation", "Minimum number of bars that must keep the Donchian slope intact", "Indicators")
;
_atrPeriod = Param(nameof(AtrPeriod), 21)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "Length of the Average True Range used for stops", "Indicators")
;
_stopAtrMultiplier = Param(nameof(StopAtrMultiplier), 4m)
.SetGreaterThanZero()
.SetDisplay("Stop ATR Multiplier", "How many ATRs below/above the entry to place the stop", "Risk")
;
_takeAtrMultiplier = Param(nameof(TakeAtrMultiplier), 4m)
.SetGreaterThanZero()
.SetDisplay("Take ATR Multiplier", "How many ATRs below/above the entry to place the target", "Risk")
;
}
/// <summary>
/// Trade volume in lots.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Candle type used for analysis.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Period of the Bollinger Bands.
/// </summary>
public int BollingerPeriod
{
get => _bollingerPeriod.Value;
set => _bollingerPeriod.Value = value;
}
/// <summary>
/// Deviation multiplier of the Bollinger Bands.
/// </summary>
public decimal BollingerDeviation
{
get => _bollingerDeviation.Value;
set => _bollingerDeviation.Value = value;
}
/// <summary>
/// Period of the Donchian Channel.
/// </summary>
public int DonchianPeriod
{
get => _donchianPeriod.Value;
set => _donchianPeriod.Value = value;
}
/// <summary>
/// Number of consecutive bars required to confirm the Donchian slope.
/// </summary>
public int ConfirmationPeriod
{
get => _confirmationPeriod.Value;
set => _confirmationPeriod.Value = value;
}
/// <summary>
/// Period of the Average True Range indicator.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in ATR multiples.
/// </summary>
public decimal StopAtrMultiplier
{
get => _stopAtrMultiplier.Value;
set => _stopAtrMultiplier.Value = value;
}
/// <summary>
/// Take-profit distance expressed in ATR multiples.
/// </summary>
public decimal TakeAtrMultiplier
{
get => _takeAtrMultiplier.Value;
set => _takeAtrMultiplier.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevOpen = null;
_prevClose = null;
_prevLowerBand = null;
_prevUpperBand = null;
_prevDonchLower = null;
_prevDonchUpper = null;
_prevAtr = null;
_lowerTrendLength = 0;
_upperTrendLength = 0;
_stopLossPrice = null;
_takeProfitPrice = null;
_equitySamples = 0;
_sumIndices = 0m;
_sumEquity = 0m;
_sumIndexEquity = 0m;
_sumIndexSquared = 0m;
_sumEquitySquared = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var bollinger = new BollingerBands
{
Length = BollingerPeriod,
Width = BollingerDeviation
};
var atr = new AverageTrueRange
{
Length = AtrPeriod
};
var donchian = new DonchianChannels
{
Length = DonchianPeriod
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(bollinger, atr, donchian, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue bollingerVal, IIndicatorValue atrVal, IIndicatorValue donchianVal)
{
if (candle.State != CandleStates.Finished)
return;
if (!bollingerVal.IsFormed || !atrVal.IsFormed || !donchianVal.IsFormed)
return;
var bollingerComplex = (ComplexIndicatorValue<BollingerBands>)bollingerVal;
var middle = bollingerComplex.InnerValues.ElementAt(0).Value.GetValue<decimal>();
var upper = bollingerComplex.InnerValues.ElementAt(1).Value.GetValue<decimal>();
var lower = bollingerComplex.InnerValues.ElementAt(2).Value.GetValue<decimal>();
var atrValue = atrVal.GetValue<decimal>();
var donchianComplex = (ComplexIndicatorValue<DonchianChannels>)donchianVal;
var donchUpper = donchianComplex.InnerValues.ElementAt(0).Value.GetValue<decimal>();
var donchLower = donchianComplex.InnerValues.ElementAt(1).Value.GetValue<decimal>();
var lowerTrendLength = CalculateLowerTrendLength(donchLower);
var upperTrendLength = CalculateUpperTrendLength(donchUpper);
if (!_prevOpen.HasValue)
{
CachePreviousValues(candle, lower, upper, donchLower, donchUpper, atrValue, lowerTrendLength, upperTrendLength);
return;
}
var previousOpen = _prevOpen.Value;
var previousClose = _prevClose!.Value;
var previousLowerBand = _prevLowerBand!.Value;
var previousUpperBand = _prevUpperBand!.Value;
var previousDonchLower = _prevDonchLower!.Value;
var previousDonchUpper = _prevDonchUpper!.Value;
var atrForStops = _prevAtr ?? atrValue;
if (Position == 0m)
{
if (previousOpen < previousLowerBand && previousClose > previousLowerBand && lowerTrendLength > ConfirmationPeriod)
{
OpenLong(candle.ClosePrice, atrForStops);
}
else if (previousOpen > previousUpperBand && previousClose < previousUpperBand && upperTrendLength > ConfirmationPeriod)
{
OpenShort(candle.ClosePrice, atrForStops);
}
}
else if (Position > 0m)
{
var exitVolume = Position;
var stopTriggered = _stopLossPrice is decimal stop && candle.LowPrice <= stop;
var takeTriggered = _takeProfitPrice is decimal take && candle.HighPrice >= take;
if (stopTriggered || takeTriggered || previousClose > previousDonchUpper || previousClose < previousDonchLower)
{
SellMarket(exitVolume);
ClearProtection();
}
}
else if (Position < 0m)
{
var exitVolume = Math.Abs(Position);
var stopTriggered = _stopLossPrice is decimal stop && candle.HighPrice >= stop;
var takeTriggered = _takeProfitPrice is decimal take && candle.LowPrice <= take;
if (stopTriggered || takeTriggered || previousClose < previousDonchLower || previousClose > previousDonchUpper)
{
BuyMarket(exitVolume);
ClearProtection();
}
}
CachePreviousValues(candle, lower, upper, donchLower, donchUpper, atrValue, lowerTrendLength, upperTrendLength);
}
private int CalculateLowerTrendLength(decimal currentLower)
{
if (_prevDonchLower is decimal prevLower)
{
return currentLower >= prevLower ? _lowerTrendLength + 1 : 1;
}
return 1;
}
private int CalculateUpperTrendLength(decimal currentUpper)
{
if (_prevDonchUpper is decimal prevUpper)
{
return currentUpper <= prevUpper ? _upperTrendLength + 1 : 1;
}
return 1;
}
private void CachePreviousValues(ICandleMessage candle, decimal lower, decimal upper, decimal donchLower, decimal donchUpper, decimal atrValue, int lowerTrendLength, int upperTrendLength)
{
_prevOpen = candle.OpenPrice;
_prevClose = candle.ClosePrice;
_prevLowerBand = lower;
_prevUpperBand = upper;
_prevDonchLower = donchLower;
_prevDonchUpper = donchUpper;
_prevAtr = atrValue;
_lowerTrendLength = lowerTrendLength;
_upperTrendLength = upperTrendLength;
}
private void OpenLong(decimal entryPrice, decimal atrValue)
{
var volume = TradeVolume;
if (volume <= 0m)
return;
BuyMarket(volume);
AssignProtection(entryPrice, atrValue, true);
}
private void OpenShort(decimal entryPrice, decimal atrValue)
{
var volume = TradeVolume;
if (volume <= 0m)
return;
SellMarket(volume);
AssignProtection(entryPrice, atrValue, false);
}
private void AssignProtection(decimal entryPrice, decimal atrValue, bool isLong)
{
if (atrValue <= 0m)
{
ClearProtection();
return;
}
var stopDistance = atrValue * StopAtrMultiplier;
var takeDistance = atrValue * TakeAtrMultiplier;
if (isLong)
{
_stopLossPrice = entryPrice - stopDistance;
_takeProfitPrice = entryPrice + takeDistance;
}
else
{
_stopLossPrice = entryPrice + stopDistance;
_takeProfitPrice = entryPrice - takeDistance;
}
}
private void ClearProtection()
{
_stopLossPrice = null;
_takeProfitPrice = null;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
var portfolio = Portfolio;
if (portfolio == null)
return;
UpdateEquityStatistics(portfolio.CurrentValue ?? 0m);
}
private void UpdateEquityStatistics(decimal equity)
{
var index = (decimal)_equitySamples;
_sumIndices += index;
_sumEquity += equity;
_sumIndexEquity += index * equity;
_sumIndexSquared += index * index;
_sumEquitySquared += equity * equity;
_equitySamples++;
if (_equitySamples % 100 != 0)
return;
var n = (decimal)_equitySamples;
if (n <= 1m)
return;
var denominator = n * _sumIndexSquared - _sumIndices * _sumIndices;
if (denominator == 0m)
return;
var slope = (n * _sumIndexEquity - _sumIndices * _sumEquity) / denominator;
var mean = _sumEquity / n;
var ssTotal = _sumEquitySquared - n * mean * mean;
if (ssTotal == 0m)
{
LogInfo("Equity R-squared: 1.0000");
return;
}
var regressionComponent = slope * (_sumIndexEquity - (_sumIndices / n) * _sumEquity);
var rSquared = regressionComponent / ssTotal;
if (slope < 0m)
rSquared = -rSquared;
LogInfo($"Equity R-squared: {rSquared:F4}");
}
}
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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import BollingerBands, AverageTrueRange, DonchianChannels
from StockSharp.Algo.Strategies import Strategy
class bands_strategy(Strategy):
"""Bollinger Bands breakout confirmed by Donchian channel slope and ATR-based stops."""
def __init__(self):
super(bands_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 0.1) \
.SetGreaterThanZero() \
.SetDisplay("Volume", "Net volume in lots sent with every order", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Time frame used for indicator calculations", "Market Data")
self._bollinger_period = self.Param("BollingerPeriod", 100) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Period", "Number of candles used for Bollinger Bands", "Indicators")
self._bollinger_deviation = self.Param("BollingerDeviation", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier", "Indicators")
self._donchian_period = self.Param("DonchianPeriod", 100) \
.SetGreaterThanZero() \
.SetDisplay("Donchian Period", "Donchian Channel length", "Indicators")
self._confirmation_period = self.Param("ConfirmationPeriod", 100) \
.SetGreaterThanZero() \
.SetDisplay("Slope Confirmation", "Min bars for Donchian slope confirmation", "Indicators")
self._atr_period = self.Param("AtrPeriod", 21) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "Length of Average True Range", "Indicators")
self._stop_atr_multiplier = self.Param("StopAtrMultiplier", 4.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop ATR Multiplier", "ATRs for stop placement", "Risk")
self._take_atr_multiplier = self.Param("TakeAtrMultiplier", 4.0) \
.SetGreaterThanZero() \
.SetDisplay("Take ATR Multiplier", "ATRs for target placement", "Risk")
self._prev_open = None
self._prev_close = None
self._prev_lower_band = None
self._prev_upper_band = None
self._prev_donch_lower = None
self._prev_donch_upper = None
self._prev_atr = None
self._lower_trend_length = 0
self._upper_trend_length = 0
self._stop_loss_price = None
self._take_profit_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def TradeVolume(self):
return self._trade_volume.Value
@property
def BollingerPeriod(self):
return self._bollinger_period.Value
@property
def BollingerDeviation(self):
return self._bollinger_deviation.Value
@property
def DonchianPeriod(self):
return self._donchian_period.Value
@property
def ConfirmationPeriod(self):
return self._confirmation_period.Value
@property
def AtrPeriod(self):
return self._atr_period.Value
@property
def StopAtrMultiplier(self):
return self._stop_atr_multiplier.Value
@property
def TakeAtrMultiplier(self):
return self._take_atr_multiplier.Value
def OnReseted(self):
super(bands_strategy, self).OnReseted()
self._prev_open = None
self._prev_close = None
self._prev_lower_band = None
self._prev_upper_band = None
self._prev_donch_lower = None
self._prev_donch_upper = None
self._prev_atr = None
self._lower_trend_length = 0
self._upper_trend_length = 0
self._stop_loss_price = None
self._take_profit_price = None
def OnStarted2(self, time):
super(bands_strategy, self).OnStarted2(time)
bollinger = BollingerBands()
bollinger.Length = self.BollingerPeriod
bollinger.Width = self.BollingerDeviation
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
donchian = DonchianChannels()
donchian.Length = self.DonchianPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(bollinger, atr, donchian, self._process_candle).Start()
def _process_candle(self, candle, bb_val, atr_val, donch_val):
if candle.State != CandleStates.Finished:
return
if not bb_val.IsFormed or not atr_val.IsFormed or not donch_val.IsFormed:
return
upper_bb = bb_val.UpBand
lower_bb = bb_val.LowBand
atr_v = float(atr_val)
donch_upper = donch_val.UpperBand
donch_lower = donch_val.LowerBand
if upper_bb is None or lower_bb is None or donch_upper is None or donch_lower is None:
return
upper = float(upper_bb)
lower = float(lower_bb)
d_upper = float(donch_upper)
d_lower = float(donch_lower)
lower_trend = self._calc_lower_trend(d_lower)
upper_trend = self._calc_upper_trend(d_upper)
if self._prev_open is None:
self._cache_values(candle, lower, upper, d_lower, d_upper, atr_v, lower_trend, upper_trend)
return
prev_open = self._prev_open
prev_close = self._prev_close
prev_lower_band = self._prev_lower_band
prev_upper_band = self._prev_upper_band
prev_donch_lower = self._prev_donch_lower
prev_donch_upper = self._prev_donch_upper
atr_for_stops = self._prev_atr if self._prev_atr is not None else atr_v
if self.Position == 0:
if prev_open < prev_lower_band and prev_close > prev_lower_band and lower_trend > self.ConfirmationPeriod:
self._open_long(float(candle.ClosePrice), atr_for_stops)
elif prev_open > prev_upper_band and prev_close < prev_upper_band and upper_trend > self.ConfirmationPeriod:
self._open_short(float(candle.ClosePrice), atr_for_stops)
elif self.Position > 0:
stop_hit = self._stop_loss_price is not None and float(candle.LowPrice) <= self._stop_loss_price
take_hit = self._take_profit_price is not None and float(candle.HighPrice) >= self._take_profit_price
if stop_hit or take_hit or prev_close > prev_donch_upper or prev_close < prev_donch_lower:
self.SellMarket(self.Position)
self._clear_protection()
elif self.Position < 0:
stop_hit = self._stop_loss_price is not None and float(candle.HighPrice) >= self._stop_loss_price
take_hit = self._take_profit_price is not None and float(candle.LowPrice) <= self._take_profit_price
if stop_hit or take_hit or prev_close < prev_donch_lower or prev_close > prev_donch_upper:
self.BuyMarket(abs(self.Position))
self._clear_protection()
self._cache_values(candle, lower, upper, d_lower, d_upper, atr_v, lower_trend, upper_trend)
def _calc_lower_trend(self, current_lower):
if self._prev_donch_lower is not None:
return self._lower_trend_length + 1 if current_lower >= self._prev_donch_lower else 1
return 1
def _calc_upper_trend(self, current_upper):
if self._prev_donch_upper is not None:
return self._upper_trend_length + 1 if current_upper <= self._prev_donch_upper else 1
return 1
def _cache_values(self, candle, lower, upper, d_lower, d_upper, atr_v, lower_trend, upper_trend):
self._prev_open = float(candle.OpenPrice)
self._prev_close = float(candle.ClosePrice)
self._prev_lower_band = lower
self._prev_upper_band = upper
self._prev_donch_lower = d_lower
self._prev_donch_upper = d_upper
self._prev_atr = atr_v
self._lower_trend_length = lower_trend
self._upper_trend_length = upper_trend
def _open_long(self, entry_price, atr_v):
vol = float(self.TradeVolume)
if vol <= 0:
return
self.BuyMarket(vol)
self._assign_protection(entry_price, atr_v, True)
def _open_short(self, entry_price, atr_v):
vol = float(self.TradeVolume)
if vol <= 0:
return
self.SellMarket(vol)
self._assign_protection(entry_price, atr_v, False)
def _assign_protection(self, entry_price, atr_v, is_long):
if atr_v <= 0:
self._clear_protection()
return
stop_dist = atr_v * float(self.StopAtrMultiplier)
take_dist = atr_v * float(self.TakeAtrMultiplier)
if is_long:
self._stop_loss_price = entry_price - stop_dist
self._take_profit_price = entry_price + take_dist
else:
self._stop_loss_price = entry_price + stop_dist
self._take_profit_price = entry_price - take_dist
def _clear_protection(self):
self._stop_loss_price = None
self._take_profit_price = None
def CreateClone(self):
return bands_strategy()