Ver no GitHub

Boll Trade Breakout Strategy

This strategy replicates the original BollTrade expert advisor by trading Bollinger Band breakouts with a configurable pip buffer and optional balance-based position sizing. Orders are opened only on completed candles and are managed with static stop-loss and take-profit levels.

Concept

  • Subscribes to the configurable primary timeframe and calculates a Bollinger Bands envelope with the specified period and deviation.
  • Adds an extra offset (Band Offset) measured in pip units on top of the upper band and below the lower band to reduce premature entries.
  • Opens a long position when the candle close finishes below the lower band minus the offset.
  • Opens a short position when the candle close finishes above the upper band plus the offset.
  • Only one position can be active at any time. The strategy waits for the current trade to finish before evaluating new entries.

Trade Management

  • Stop-loss and take-profit levels are set immediately after an entry. They are expressed in pip multiples and evaluated on every completed candle. If price touches either level the position is closed at market.
  • If Scale Volume is enabled the traded volume grows (or shrinks) with the account balance. The scaling baseline is the starting portfolio value divided by the base lot size, mimicking the original MQL implementation. Volume is capped at 500 lots to keep risk under control just like in the source code.
  • The pip size is derived from the security price step. For very small steps (forex-style symbols) the code multiplies the step by 10 to convert fractional pip steps into standard pips, matching the behaviour of the MetaTrader version.

Parameters

Name Description Default
Candle Type Timeframe used for signal candles. 15-minute time frame
Bollinger Period Number of bars in the Bollinger Bands calculation. 4
Bollinger Deviation Width multiplier for the Bollinger Bands. 2
Band Offset Additional pip offset added outside both bands before triggering signals. 3
Take Profit (pips) Distance to the profit target in pip units. 3
Stop Loss (pips) Distance to the protective stop in pip units. 20
Base Volume Default volume in lots used when scaling is disabled. 1
Scale Volume When enabled, scales position size with the account balance. Enabled

Usage Notes

  • Works best on forex or CFD symbols where pip-based offsets provide clear breakout levels, but it can also run on futures or equities provided their PriceStep is configured.
  • The strategy processes only finished candles, so intrabar spikes that revert before the bar closes will not trigger entries.
  • Because exits are handled with fixed stops and targets, ensure those distances are appropriate for the selected timeframe and instrument volatility.
  • The original EA relied on broker-side stops. This port monitors candle extremes to emulate the same protective behaviour inside StockSharp.

Files

  • CS/BollTradeStrategy.cs – C# implementation of the strategy.
  • README.md – English documentation (this file).
  • README_ru.md – Russian documentation.
  • README_zh.md – Chinese documentation.

No Python translation is provided yet, as requested.

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 with optional balance-based position sizing.
/// </summary>
public class BollTradeStrategy : Strategy
{
	private readonly StrategyParam<decimal> _maxVolume;

	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _bandOffset;
	private readonly StrategyParam<int> _bollingerPeriod;
	private readonly StrategyParam<decimal> _bollingerDeviation;
	private readonly StrategyParam<decimal> _lots;
	private readonly StrategyParam<bool> _lotIncrease;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _lotBaseline;
	private decimal _pipSize;
	private BollingerBands _bollinger;
	private decimal? _longStop;
	private decimal? _longTarget;
	private decimal? _shortStop;
	private decimal? _shortTarget;

	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	public decimal BollingerDistance
	{
		get => _bandOffset.Value;
		set => _bandOffset.Value = value;
	}

	public int BollingerPeriod
	{
		get => _bollingerPeriod.Value;
		set => _bollingerPeriod.Value = value;
	}

	public decimal BollingerDeviation
	{
		get => _bollingerDeviation.Value;
		set => _bollingerDeviation.Value = value;
	}

	public decimal Lots
	{
		get => _lots.Value;
		set => _lots.Value = value;
	}

	public bool LotIncrease
	{
		get => _lotIncrease.Value;
		set => _lotIncrease.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	public BollTradeStrategy()
	{
		_maxVolume = Param(nameof(MaxVolume), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Max Volume", "Upper bound for scaled volume", "Money Management");

		_takeProfit = Param(nameof(TakeProfit), 3m)
		.SetNotNegative()
		.SetDisplay("Take Profit (pips)", "Distance to take profit expressed in pip units.", "Orders")
		;

		_stopLoss = Param(nameof(StopLoss), 20m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (pips)", "Distance to stop loss expressed in pip units.", "Orders")
		;

		_bandOffset = Param(nameof(BollingerDistance), 0m)
		.SetNotNegative()
		.SetDisplay("Band Offset", "Extra pip offset beyond Bollinger Bands.", "Signals")
		;

		_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("Bollinger Period", "Length of the Bollinger Bands.", "Signals")
		;

		_bollingerDeviation = Param(nameof(BollingerDeviation), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Bollinger Deviation", "Width multiplier of the Bollinger Bands.", "Signals")
		;

		_lots = Param(nameof(Lots), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Base Volume", "Default trade volume in lots.", "Money Management");

		_lotIncrease = Param(nameof(LotIncrease), true)
		.SetDisplay("Scale Volume", "Increase volume with balance growth.", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Primary timeframe for signals.", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, CandleType);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_lotBaseline = 0m;
		_pipSize = 0m;
		_bollinger = null!;
		ResetStops();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = Lots;

		_pipSize = CalculatePipSize();
		_lotBaseline = 0m;

		if (LotIncrease && Lots > 0m)
		{
			var balance = Portfolio?.CurrentValue ?? 0m;

			if (balance > 0m)
			_lotBaseline = balance / Lots;
		}

		var bollinger = new BollingerBands
		{
			Length = BollingerPeriod,
			Width = BollingerDeviation
		};

		var subscription = SubscribeCandles(CandleType);

		subscription
		.BindEx(bollinger, ProcessCandle)
		.Start();

		_bollinger = bollinger;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;

		if (step <= 0m)
		step = 1m;

		if (step < 0.01m)
		step *= 10m;

		return step;
	}

	private decimal CalculateVolume()
	{
		var baseVolume = Lots;

		if (!LotIncrease || _lotBaseline <= 0m)
		return baseVolume;

		var balance = Portfolio?.CurrentValue ?? 0m;

		if (balance <= 0m)
		return baseVolume;

		var scaled = baseVolume * (balance / _lotBaseline);

		return Math.Min(scaled, MaxVolume);
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue value)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (value is not BollingerBandsValue bollingerValue)
			return;

		if (bollingerValue.MovingAverage is not decimal middleBand ||
			bollingerValue.UpBand is not decimal upperBand ||
			bollingerValue.LowBand is not decimal lowerBand)
			return;

		var offset = _pipSize * BollingerDistance;
		var upperThreshold = upperBand + offset;
		var lowerThreshold = lowerBand - offset;

		var shouldBuy = candle.ClosePrice < lowerThreshold;
		var shouldSell = candle.ClosePrice > upperThreshold;

		if (Position == 0)
		{
			if (shouldBuy)
			{
				EnterLong(candle);
			}
			else if (shouldSell)
			{
				EnterShort(candle);
			}

			return;
		}

		if (Position > 0)
		{
			// Close long positions when stop loss or take profit levels are hit.
			if ((_longStop.HasValue && candle.LowPrice <= _longStop.Value) ||
				(_longTarget.HasValue && candle.HighPrice >= _longTarget.Value))
			{
				SellMarket();
				ResetStops();
			}
		}
		else if (Position < 0)
		{
			// Close short positions when stop loss or take profit levels are hit.
			if ((_shortStop.HasValue && candle.HighPrice >= _shortStop.Value) ||
				(_shortTarget.HasValue && candle.LowPrice <= _shortTarget.Value))
			{
				BuyMarket();
				ResetStops();
			}
		}
	}

	private void EnterLong(ICandleMessage candle)
	{
		var volume = CalculateVolume();

		if (volume <= 0m)
			return;

		BuyMarket(volume);

		// Store exit targets for the newly opened long trade.
		_longStop = StopLoss > 0m ? candle.ClosePrice - _pipSize * StopLoss : null;
		_longTarget = TakeProfit > 0m ? candle.ClosePrice + _pipSize * TakeProfit : null;
		_shortStop = null;
		_shortTarget = null;
	}

	private void EnterShort(ICandleMessage candle)
	{
		var volume = CalculateVolume();

		if (volume <= 0m)
			return;

		SellMarket(volume);

		// Store exit targets for the newly opened short trade.
		_shortStop = StopLoss > 0m ? candle.ClosePrice + _pipSize * StopLoss : null;
		_shortTarget = TakeProfit > 0m ? candle.ClosePrice - _pipSize * TakeProfit : null;
		_longStop = null;
		_longTarget = null;
	}

	private void ResetStops()
	{
		// Clear cached exit levels after a position is closed.
		_longStop = null;
		_longTarget = null;
		_shortStop = null;
		_shortTarget = null;
	}
}