View on GitHub

RSI Bollinger Fractal Breakout Strategy

Overview

This strategy reproduces the MetaTrader "RSI and Bollinger Bands" expert advisor in StockSharp. It applies Bollinger Bands to the RSI oscillator, waits for a recent fractal breakout level and places stop orders beyond that level with configurable offsets. A Parabolic SAR trailing filter dynamically tightens stops once a position is open.

Indicators and Signals

  • RSI (default 8 periods) – the main oscillator. Overbought and oversold thresholds are used to cancel pending orders.
  • Bollinger Bands on RSI (default 14 periods, 1.0 deviation) – entries only trigger when the RSI closes outside the upper or lower band, matching the original script behaviour where Bollinger is fed by RSI values.
  • Bill Williams Fractals – the strategy scans the last confirmed up and down fractals (5-bar pattern) and uses their prices as the base breakout levels.
  • Parabolic SAR (step 0.003, max 0.2) – delivers a trailing stop reference once a position is active.

Entry Logic

  1. Work is performed on finished candles of the selected timeframe (default 4-hour).
  2. When an up fractal appears and the RSI closes above the upper Bollinger band, while the previous close remains below the fractal, a buy stop is placed:
    • Entry price = fractal high + indent (15 pips by default).
    • Optional stop loss = entry − StopLossPips.
    • Optional take profit = entry + TakeProfitPips.
  3. Symmetrically, when a down fractal forms and RSI closes below the lower Bollinger band, while the previous close remains above the fractal, a sell stop is placed below the fractal.
  4. RSI reverting inside the channel cancels pending orders:
    • RSI < lower threshold cancels buy stops.
    • RSI > upper threshold cancels sell stops.

Exit and Risk Management

  • Fixed stop loss and take profit distances (in pips) replicate the MQL inputs. Setting any distance to 0 disables that protection.
  • The Parabolic SAR trailing logic requires the SAR to be at least SarTrailingPips away from the current price and only moves the stop in the favourable direction.
  • When the trailing stop crosses price or the price reaches the fixed take profit the position is closed with a market order.
  • Opening a position automatically clears the opposite pending order and stores the intended protective levels.

Parameters

Parameter Description Default
RsiPeriod RSI smoothing length. 8
BandsPeriod RSI Bollinger period. 14
BandsDeviation Standard deviation multiplier for Bollinger on RSI. 1.0
SarStep Parabolic SAR acceleration step. 0.003
SarMax Parabolic SAR maximum acceleration. 0.2
TakeProfitPips Take profit distance in pips. 50
StopLossPips Stop loss distance in pips. 135
IndentPips Offset beyond a fractal before placing the stop order. 15
RsiUpper RSI threshold that cancels sell stops. 70
RsiLower RSI threshold that cancels buy stops. 30
SarTrailingPips Minimum gap (in pips) between price and SAR before trailing. 10
CandleType Data type / timeframe for processing. 4-hour candles

Notes

  • Python version is intentionally omitted, as requested.
  • Use Volume in the base class to configure the lot size (default 1 if unspecified).
  • The strategy should be run on the same timeframe as the original EA configuration (EURUSD H4 according to the provided .set file).
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that combines RSI-based Bollinger Bands with fractal breakouts and Parabolic SAR trailing.
/// </summary>
public class RsiBollingerFractalBreakoutStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<int> _bandsPeriod;
	private readonly StrategyParam<decimal> _bandsDeviation;
	private readonly StrategyParam<decimal> _sarStep;
	private readonly StrategyParam<decimal> _sarMax;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _indentPips;
	private readonly StrategyParam<decimal> _rsiUpper;
	private readonly StrategyParam<decimal> _rsiLower;
	private readonly StrategyParam<decimal> _sarTrailingPips;
	private readonly StrategyParam<DataType> _candleType;
	
	private RelativeStrengthIndex _rsi = null!;
	private BollingerBands _bollinger = null!;
	private ParabolicSar _parabolicSar = null!;
	
	private Order _buyStopOrder;
	private Order _sellStopOrder;
	
	private decimal? _pendingLongEntry;
	private decimal? _pendingLongStop;
	private decimal? _pendingLongTake;
	private decimal? _pendingShortEntry;
	private decimal? _pendingShortStop;
	private decimal? _pendingShortTake;
	
	private decimal? _longStopPrice;
	private decimal? _longTakeProfit;
	private decimal? _shortStopPrice;
	private decimal? _shortTakeProfit;
	
	private decimal _pipSize;
	private decimal _previousPosition;
	
	private decimal _h1;
	private decimal _h2;
	private decimal _h3;
	private decimal _h4;
	private decimal _h5;
	private decimal _l1;
	private decimal _l2;
	private decimal _l3;
	private decimal _l4;
	private decimal _l5;
	private int _fractalCount;
	
	/// <summary>
	/// RSI averaging period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}
	
	/// <summary>
	/// Bollinger Bands period applied to RSI values.
	/// </summary>
	public int BandsPeriod
	{
		get => _bandsPeriod.Value;
		set => _bandsPeriod.Value = value;
	}
	
	/// <summary>
	/// Bollinger Bands standard deviation multiplier.
	/// </summary>
	public decimal BandsDeviation
	{
		get => _bandsDeviation.Value;
		set => _bandsDeviation.Value = value;
	}
	
	/// <summary>
	/// Parabolic SAR acceleration step.
	/// </summary>
	public decimal SarStep
	{
		get => _sarStep.Value;
		set => _sarStep.Value = value;
	}
	
	/// <summary>
	/// Parabolic SAR maximum acceleration.
	/// </summary>
	public decimal SarMax
	{
		get => _sarMax.Value;
		set => _sarMax.Value = value;
	}
	
	/// <summary>
	/// Take profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}
	
	/// <summary>
	/// Stop loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}
	
	/// <summary>
	/// Offset added to the fractal breakout level in pips.
	/// </summary>
	public decimal IndentPips
	{
		get => _indentPips.Value;
		set => _indentPips.Value = value;
	}
	
	/// <summary>
	/// RSI upper threshold used to cancel sell stops.
	/// </summary>
	public decimal RsiUpper
	{
		get => _rsiUpper.Value;
		set => _rsiUpper.Value = value;
	}
	
	/// <summary>
	/// RSI lower threshold used to cancel buy stops.
	/// </summary>
	public decimal RsiLower
	{
		get => _rsiLower.Value;
		set => _rsiLower.Value = value;
	}
	
	/// <summary>
	/// Additional distance required between Parabolic SAR and price in pips before trailing.
	/// </summary>
	public decimal SarTrailingPips
	{
		get => _sarTrailingPips.Value;
		set => _sarTrailingPips.Value = value;
	}
	
	/// <summary>
	/// Candle data type to subscribe.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}
	
	/// <summary>
	/// Initialize <see cref="RsiBollingerFractalBreakoutStrategy"/>.
	/// </summary>
	public RsiBollingerFractalBreakoutStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 8)
			.SetDisplay("RSI Period", "RSI averaging period", "RSI")
			.SetGreaterThanZero();
		
		_bandsPeriod = Param(nameof(BandsPeriod), 10)
			.SetDisplay("Bollinger Period", "RSI Bollinger period", "Bollinger")
			.SetGreaterThanZero();
		
		_bandsDeviation = Param(nameof(BandsDeviation), 1m)
			.SetDisplay("Bollinger Deviation", "Standard deviations on RSI", "Bollinger")
			.SetGreaterThanZero();
		
		_sarStep = Param(nameof(SarStep), 0.003m)
			.SetDisplay("SAR Step", "Parabolic SAR acceleration step", "Parabolic SAR")
			.SetGreaterThanZero();
		
		_sarMax = Param(nameof(SarMax), 0.2m)
			.SetDisplay("SAR Max", "Parabolic SAR maximum acceleration", "Parabolic SAR")
			.SetGreaterThanZero();
		
		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetDisplay("Take Profit (pips)", "Take profit distance", "Risk");
		
		_stopLossPips = Param(nameof(StopLossPips), 135m)
			.SetDisplay("Stop Loss (pips)", "Stop loss distance", "Risk");
		
		_indentPips = Param(nameof(IndentPips), 15m)
			.SetDisplay("Indent (pips)", "Offset from fractal breakout", "Entries");
		
		_rsiUpper = Param(nameof(RsiUpper), 75m)
			.SetDisplay("RSI Upper", "Overbought threshold", "RSI");

		_rsiLower = Param(nameof(RsiLower), 25m)
			.SetDisplay("RSI Lower", "Oversold threshold", "RSI");
		
		_sarTrailingPips = Param(nameof(SarTrailingPips), 10m)
			.SetDisplay("SAR Trailing (pips)", "Extra distance before SAR trailing", "Risk");
		
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for analysis", "General");
	}
	
	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_rsi = null!;
		_bollinger = null!;
		_parabolicSar = null!;
		_buyStopOrder = null;
		_sellStopOrder = null;
		_pendingLongEntry = null;
		_pendingLongStop = null;
		_pendingLongTake = null;
		_pendingShortEntry = null;
		_pendingShortStop = null;
		_pendingShortTake = null;
		_longStopPrice = null;
		_longTakeProfit = null;
		_shortStopPrice = null;
		_shortTakeProfit = null;
		_pipSize = 0m;
		_previousPosition = 0m;
		_h1 = 0m; _h2 = 0m; _h3 = 0m; _h4 = 0m; _h5 = 0m;
		_l1 = 0m; _l2 = 0m; _l3 = 0m; _l4 = 0m; _l5 = 0m;
		_fractalCount = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
		_bollinger = new BollingerBands { Length = BandsPeriod, Width = BandsDeviation };
		_parabolicSar = new ParabolicSar
		{
			AccelerationStep = SarStep,
			AccelerationMax = SarMax
		};

		_pipSize = GetPipSize();
		if (_pipSize <= 0m)
			_pipSize = Security?.PriceStep ?? 1m;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
		
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _rsi);
			DrawIndicator(area, _bollinger);
			DrawIndicator(area, _parabolicSar);
			DrawOwnTrades(area);
		}
	}
	
	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var rsiResult = _rsi.Process(new DecimalIndicatorValue(_rsi, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var sarResult = _parabolicSar.Process(new CandleIndicatorValue(_parabolicSar, candle));
		var sarValue = (_parabolicSar.IsFormed && !sarResult.IsEmpty) ? sarResult.ToDecimal() : candle.ClosePrice;

		if (!_rsi.IsFormed)
		{
			UpdateFractals(candle);
			UpdateTrailingAndExits(candle, sarValue);
			return;
		}

		var rsiValue = rsiResult.ToDecimal();

		UpdateFractals(candle);
		UpdateTrailingAndExits(candle, sarValue);

		// Buy when RSI is above upper threshold (bullish momentum)
		if (rsiValue > RsiUpper && Position <= 0)
		{
			var entryPrice = candle.ClosePrice;
			var stopPrice = StopLossPips > 0m ? NormalizePrice(entryPrice - StopLossPips * _pipSize) : (decimal?)null;
			var takePrice = TakeProfitPips > 0m ? NormalizePrice(entryPrice + TakeProfitPips * _pipSize) : (decimal?)null;

			if (Position < 0)
				BuyMarket();
			BuyMarket();
			_longStopPrice = stopPrice;
			_longTakeProfit = takePrice;
		}
		// Sell when RSI is below lower threshold (bearish momentum)
		else if (rsiValue < RsiLower && Position >= 0)
		{
			var entryPrice = candle.ClosePrice;
			var stopPrice = StopLossPips > 0m ? NormalizePrice(entryPrice + StopLossPips * _pipSize) : (decimal?)null;
			var takePrice = TakeProfitPips > 0m ? NormalizePrice(entryPrice - TakeProfitPips * _pipSize) : (decimal?)null;

			if (Position > 0)
				SellMarket();
			SellMarket();
			_shortStopPrice = stopPrice;
			_shortTakeProfit = takePrice;
		}
	}
	
	private void UpdateFractals(ICandleMessage candle)
	{
		_h1 = _h2;
		_h2 = _h3;
		_h3 = _h4;
		_h4 = _h5;
		_h5 = candle.HighPrice;
		
		_l1 = _l2;
		_l2 = _l3;
		_l3 = _l4;
		_l4 = _l5;
		_l5 = candle.LowPrice;
		
		if (_fractalCount < 5)
			_fractalCount++;
	}
	
	private decimal? DetectUpperFractal()
	{
		if (_fractalCount < 5)
			return null;
		
		return _h3 > _h1 && _h3 > _h2 && _h3 > _h4 && _h3 > _h5 ? _h3 : null;
	}
	
	private decimal? DetectLowerFractal()
	{
		if (_fractalCount < 5)
			return null;
		
		return _l3 < _l1 && _l3 < _l2 && _l3 < _l4 && _l3 < _l5 ? _l3 : null;
	}
	
	private void UpdateTrailingAndExits(ICandleMessage candle, decimal sarValue)
	{
		if (Position > 0)
		{
			if (_longTakeProfit is decimal tp && candle.HighPrice >= tp)
			{
				SellMarket();
				return;
			}
			
			if (_longStopPrice is decimal sl && candle.LowPrice <= sl)
			{
				SellMarket();
				return;
			}
			
			if (SarTrailingPips > 0m)
			{
				var trailingDistance = SarTrailingPips * _pipSize;
				if (sarValue < candle.ClosePrice - trailingDistance)
				{
					if (_longStopPrice is null || sarValue > _longStopPrice.Value)
					_longStopPrice = NormalizePrice(sarValue);
				}
			}
		}
		else if (Position < 0)
		{
			if (_shortTakeProfit is decimal tp && candle.LowPrice <= tp)
			{
				BuyMarket();
				return;
			}
			
			if (_shortStopPrice is decimal sl && candle.HighPrice >= sl)
			{
				BuyMarket();
				return;
			}
			
			if (SarTrailingPips > 0m)
			{
				var trailingDistance = SarTrailingPips * _pipSize;
				if (sarValue > candle.ClosePrice + trailingDistance)
				{
					if (_shortStopPrice is null || sarValue < _shortStopPrice.Value)
					_shortStopPrice = NormalizePrice(sarValue);
				}
			}
		}
	}
	
	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);
		
		var delta = Position - _previousPosition;
		_previousPosition = Position;

		if (Position == 0)
		{
			_longStopPrice = null;
			_longTakeProfit = null;
			_shortStopPrice = null;
			_shortTakeProfit = null;
			_pendingLongEntry = null;
			_pendingLongStop = null;
			_pendingLongTake = null;
			_pendingShortEntry = null;
			_pendingShortStop = null;
			_pendingShortTake = null;
			return;
		}

		if (delta > 0 && Position > 0)
		{
			if (_pendingLongEntry is decimal)
			{
				_longStopPrice = _pendingLongStop;
				_longTakeProfit = _pendingLongTake;
			}
			
			CancelSellStop();
			_buyStopOrder = null;
			_pendingLongEntry = null;
			_pendingLongStop = null;
			_pendingLongTake = null;
		}
		else if (delta < 0 && Position < 0)
		{
			if (_pendingShortEntry is decimal)
			{
				_shortStopPrice = _pendingShortStop;
				_shortTakeProfit = _pendingShortTake;
			}
			
			CancelBuyStop();
			_sellStopOrder = null;
			_pendingShortEntry = null;
			_pendingShortStop = null;
			_pendingShortTake = null;
		}
	}
	
	private void CancelBuyStop()
	{
		if (_buyStopOrder != null && _buyStopOrder.State == OrderStates.Active)
			{} // CancelOrder not available
		
		_buyStopOrder = null;
		_pendingLongEntry = null;
		_pendingLongStop = null;
		_pendingLongTake = null;
	}
	
	private void CancelSellStop()
	{
		if (_sellStopOrder != null && _sellStopOrder.State == OrderStates.Active)
			{} // CancelOrder not available
		
		_sellStopOrder = null;
		_pendingShortEntry = null;
		_pendingShortStop = null;
		_pendingShortTake = null;
	}
	
	private decimal GetPipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 0m;
		
		var temp = step;
		var decimals = 0;
		while (temp != Math.Truncate(temp) && decimals < 10)
		{
			temp *= 10m;
			decimals++;
		}
		
		return decimals == 3 || decimals == 5 ? step * 10m : step;
	}
	
	private decimal NormalizePrice(decimal price)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return price;
		
		var steps = decimal.Round(price / step, 0, MidpointRounding.AwayFromZero);
		return steps * step;
	}
}