View on GitHub

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

  1. Subscribe to a single candle stream and compute Bollinger Bands, a Donchian Channel and ATR with the same periods as the MetaT rader robot.
  2. 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 ConfirmationPeriod bars.
    • 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 ConfirmationPeriod bars.
  3. 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.
  4. 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 TradeVolume parameter 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}");
	}
}