View on GitHub

Renko Line Break vs RSI Strategy

This strategy recreates the "RenkoLineBreak vs RSI" MetaTrader expert using the StockSharp high level API. It combines Renko trend detection with an RSI pullback filter and executes trades through pending stop orders located around a three-candle price structure.

Details

  • Entry Criteria:
    • Long: Renko trend stays bullish and the RSI falls to or below 50 - RsiShift. A buy stop is placed at the high of the candle from three bars ago plus IndentFromHighLow.
    • Short: Renko trend stays bearish and the RSI rises to or above 50 + RsiShift. A sell stop is placed at the low of the candle from three bars ago minus IndentFromHighLow.
    • Pending orders are cancelled whenever the Renko trend switches direction (ToUp / ToDown).
  • Long/Short: Both.
  • Exit Criteria:
    • Market exits when the opposite Renko transition appears (ToDown for longs, ToUp for shorts).
    • RSI crosses back through the midpoint (50 ± RsiShift).
    • Candle ranges hitting the planned stop-loss or take-profit levels.
  • Stops:
    • Stop-loss is anchored to the extreme of the last three candles plus IndentFromHighLow.
    • Take-profit is TakeProfit price units away from the intended entry (optional when set to zero).
  • Default Values:
    • BoxSize = 500m.
    • RsiPeriod = 4.
    • RsiShift = 20m.
    • TakeProfit = 1000m.
    • IndentFromHighLow = 50m.
    • Volume = 1m.
    • CandleType = 5-minute time frame.
  • Filters:
    • Category: Trend Following.
    • Direction: Both.
    • Indicators: Renko, RSI.
    • Stops: Hard stop & take profit.
    • Complexity: Intermediate.
    • Timeframe: Hybrid (Renko + time candles).
    • Seasonality: No.
    • Neural networks: No.
    • Divergence: No.
    • Risk level: Moderate.

How It Works

  1. A Renko subscription (RenkoCandleMessage) estimates the trend direction. When a Renko brick flips direction, the trend state is set to ToUp or ToDown for one bar to mimic the original indicator behaviour.
  2. Simultaneously, a time-based candle stream feeds the RSI indicator and provides the last three highs/lows used for breakout levels.
  3. When both Renko trend and RSI conditions align, the strategy registers a stop order (buy or sell). Planned stop-loss and take-profit levels are stored and monitored after the order triggers.
  4. Upon order execution the stored protection levels become active. Subsequent candles check if price hits the stop or target ranges; if yes, the position is closed at market.
  5. If momentum fades (RSI crosses back through the midpoint) or the Renko trend changes, the position is closed early.

Indicators Used

  • Renko bricks to infer the directional bias and detect transitions between up and down states.
  • Relative Strength Index (RSI) to qualify entries by demanding pullbacks against the trend.

Additional Notes

  • IndentFromHighLow models the original expert's buffer that keeps entry and stop orders away from recent highs and lows.
  • TakeProfit can be set to zero to disable the profit target while leaving the stop-loss logic intact.
  • The strategy keeps only one pending order at a time and automatically cancels it when market conditions invalidate the setup.
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.Candles;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that combines Renko trend detection with RSI pullbacks.
/// Uses a three-bar breakout structure for entries and attaches stop-loss and take-profit levels.
/// </summary>
public class RenkoLineBreakVsRsiStrategy : Strategy
{
	private enum TrendStates
	{
		None,
		Up,
		Down,
		ToUp,
		ToDown
	}

	private readonly StrategyParam<decimal> _boxSize;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiShift;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _indentFromHighLow;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi;
	private DataType _renkoType;

	private TrendStates _trendState = TrendStates.None;
	private bool _renkoHasPrev;
	private bool _renkoPrevBull;

	private decimal _prevHigh1;
	private decimal _prevHigh2;
	private decimal _prevHigh3;
	private decimal _prevLow1;
	private decimal _prevLow2;
	private decimal _prevLow3;
	private int _historyCount;

	private bool? _pendingIsBuy;
	private bool _plannedTakeProfitEnabled;
	private bool _hasPlannedPrices;
	private decimal _plannedEntryPrice;
	private decimal _plannedStopPrice;
	private decimal _plannedTakeProfitPrice;

	private decimal? _activeStopPrice;
	private decimal? _activeTakeProfitPrice;

	private decimal _lastPosition;

	/// <summary>
	/// Renko brick size in price units.
	/// </summary>
	public decimal BoxSize
	{
		get => _boxSize.Value;
		set => _boxSize.Value = value;
	}

	/// <summary>
	/// RSI calculation period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Distance from the RSI midpoint (50) to generate pullback signals.
	/// </summary>
	public decimal RsiShift
	{
		get => _rsiShift.Value;
		set => _rsiShift.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price units from the planned entry price.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Additional indent applied to breakout and stop-loss levels.
	/// </summary>
	public decimal IndentFromHighLow
	{
		get => _indentFromHighLow.Value;
		set => _indentFromHighLow.Value = value;
	}


	/// <summary>
	/// Time-based candle type used for RSI and breakout calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="RenkoLineBreakVsRsiStrategy"/> parameters.
	/// </summary>
	public RenkoLineBreakVsRsiStrategy()
	{
		_boxSize = Param(nameof(BoxSize), 100m)
		.SetGreaterThanZero()
		.SetDisplay("Renko Box Size", "Renko brick size in price units", "Renko")
		
		.SetOptimize(100m, 1000m, 100m);

		_rsiPeriod = Param(nameof(RsiPeriod), 4)
		.SetGreaterThanZero()
		.SetDisplay("RSI Period", "Relative Strength Index period", "Indicators")
		
		.SetOptimize(2, 20, 1);

		_rsiShift = Param(nameof(RsiShift), 10m)
		.SetGreaterThanZero()
		.SetDisplay("RSI Shift", "Distance from the 50 level to detect pullbacks", "Indicators")
		
		.SetOptimize(10m, 40m, 5m);

		_takeProfit = Param(nameof(TakeProfit), 1000m)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit", "Take profit distance in price units", "Risk Management")
		
		.SetOptimize(200m, 2000m, 200m);

		_indentFromHighLow = Param(nameof(IndentFromHighLow), 50m)
		.SetGreaterThanZero()
		.SetDisplay("Indent", "Indent applied to breakout and stop levels", "Risk Management")
		
		.SetOptimize(10m, 200m, 10m);


		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe used for RSI and breakouts", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		_renkoType ??= DataType.Create(typeof(RenkoCandleMessage), new Unit(BoxSize));

		return [(Security, CandleType), (Security, _renkoType)];
	}

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

		_rsi = null;
		_renkoType = null;

		_trendState = TrendStates.None;
		_renkoHasPrev = false;
		_renkoPrevBull = false;

		_prevHigh1 = 0m;
		_prevHigh2 = 0m;
		_prevHigh3 = 0m;
		_prevLow1 = 0m;
		_prevLow2 = 0m;
		_prevLow3 = 0m;
		_historyCount = 0;

		ResetPendingPlan();
		ResetActiveTargets();

		_lastPosition = 0m;
	}

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

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod
		};

		_renkoType ??= DataType.Create(typeof(RenkoCandleMessage), new Unit(BoxSize));

		var timeSubscription = SubscribeCandles(CandleType);
		timeSubscription
		.Bind(_rsi, ProcessTimeCandle)
		.Start();

		var renkoSubscription = SubscribeCandles(_renkoType);
		renkoSubscription
		.Bind(ProcessRenkoCandle)
		.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, timeSubscription);
			DrawIndicator(area, _rsi);
			DrawOwnTrades(area);
		}

		StartProtection(null, null);
	}

	private void ProcessRenkoCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		return;

		var isBull = candle.ClosePrice > candle.OpenPrice;
		var isBear = candle.ClosePrice < candle.OpenPrice;

		if (!_renkoHasPrev)
		{
			// Store the very first renko brick direction and wait for the next one to define a trend state.
			_renkoPrevBull = isBull;
			_renkoHasPrev = true;
			_trendState = TrendStates.None;
			return;
		}

		if (isBull)
		{
			_trendState = _renkoPrevBull ? TrendStates.Up : TrendStates.ToUp;
			_renkoPrevBull = true;
		}
		else if (isBear)
		{
			_trendState = _renkoPrevBull ? TrendStates.ToDown : TrendStates.Down;
			_renkoPrevBull = false;
		}
		else
		{
			// Flat bricks keep the previous trend state.
		}
	}

	private void ProcessTimeCandle(ICandleMessage candle, decimal rsiValue)
	{
		if (candle.State != CandleStates.Finished)
		return;

		var canTrade = true;
		var hasRsi = _rsi?.IsFormed == true && rsiValue >= 0m;

		CheckPendingActivation();

		ManagePosition(candle, rsiValue, hasRsi);

		if (canTrade && Position == 0)
		{
			TryPlaceEntry(rsiValue, hasRsi);
		}
		else if (!canTrade && Position == 0 && _pendingIsBuy != null)
		{
			// Cancel pending orders when trading is not allowed.
			// CancelActiveOrders - not available
			ResetPendingPlan();
		}

		UpdateHistory(candle);
		_lastPosition = Position;
	}

	private void ManagePosition(ICandleMessage candle, decimal rsiValue, bool hasRsi)
	{
		var position = Position;

		if (position > 0m)
		{
			// Long position management.
			if (_pendingIsBuy != null)
			ResetPendingPlan();

			if (_activeTakeProfitPrice.HasValue && candle.HighPrice >= _activeTakeProfitPrice.Value)
			{
				SellMarket();
				ResetActiveTargets();
				return;
			}

			if (_activeStopPrice.HasValue && candle.LowPrice <= _activeStopPrice.Value)
			{
				SellMarket();
				ResetActiveTargets();
				return;
			}

			if (_trendState == TrendStates.ToDown)
			{
				SellMarket();
				ResetActiveTargets();
				return;
			}

			if (hasRsi && rsiValue > 50m + RsiShift)
			{
				SellMarket();
				ResetActiveTargets();
			}
		}
		else if (position < 0m)
		{
			// Short position management.
			if (_pendingIsBuy != null)
			ResetPendingPlan();

			var absPosition = Math.Abs(position);

			if (_activeTakeProfitPrice.HasValue && candle.LowPrice <= _activeTakeProfitPrice.Value)
			{
				BuyMarket();
				ResetActiveTargets();
				return;
			}

			if (_activeStopPrice.HasValue && candle.HighPrice >= _activeStopPrice.Value)
			{
				BuyMarket();
				ResetActiveTargets();
				return;
			}

			if (_trendState == TrendStates.ToUp)
			{
				BuyMarket();
				ResetActiveTargets();
				return;
			}

			if (hasRsi && rsiValue < 50m - RsiShift)
			{
				BuyMarket();
				ResetActiveTargets();
			}
		}
		else
		{
			// No position -> clear active stop/target remnants.
			if (_activeStopPrice.HasValue || _activeTakeProfitPrice.HasValue)
			ResetActiveTargets();
		}
	}

	private void TryPlaceEntry(decimal rsiValue, bool hasRsi)
	{
		var effectiveTrend = GetEffectiveTrend();

		if (effectiveTrend == TrendStates.ToDown || effectiveTrend == TrendStates.ToUp)
		{
			if (_pendingIsBuy != null)
			{
				// CancelActiveOrders - not available
				ResetPendingPlan();
			}

			return;
		}

		if (_historyCount < 3 || !hasRsi)
		return;

		var indent = IndentFromHighLow;
		var takeProfitDistance = TakeProfit;

		if (effectiveTrend == TrendStates.Up && rsiValue <= 50m - RsiShift)
		{
			var entryPrice = _prevHigh3 + indent;
			var stopPrice = Math.Min(_prevLow1, Math.Min(_prevLow2, _prevLow3)) - indent;

			if (entryPrice > 0m && stopPrice > 0m && entryPrice > stopPrice)
			{
				var takeProfitPrice = takeProfitDistance > 0m ? entryPrice + takeProfitDistance : (decimal?)null;
				PlacePendingOrder(true, entryPrice, stopPrice, takeProfitPrice);
			}
		}
		else if (effectiveTrend == TrendStates.Down && rsiValue >= 50m + RsiShift)
		{
			var entryPrice = _prevLow3 - indent;
			var stopPrice = Math.Max(_prevHigh1, Math.Max(_prevHigh2, _prevHigh3)) + indent;

			if (entryPrice > 0m && stopPrice > 0m && entryPrice < stopPrice)
			{
				var takeProfitPrice = takeProfitDistance > 0m ? entryPrice - takeProfitDistance : (decimal?)null;
				PlacePendingOrder(false, entryPrice, stopPrice, takeProfitPrice);
			}
		}
	}

	private TrendStates GetEffectiveTrend()
	{
		if (_trendState != TrendStates.None)
			return _trendState;

		if (_historyCount < 3)
			return TrendStates.None;

		if (_prevHigh1 > _prevHigh2 && _prevHigh2 > _prevHigh3)
			return TrendStates.Up;

		if (_prevLow1 < _prevLow2 && _prevLow2 < _prevLow3)
			return TrendStates.Down;

		return TrendStates.None;
	}

	private void PlacePendingOrder(bool isBuy, decimal entryPrice, decimal stopPrice, decimal? takeProfitPrice)
	{
		// Avoid duplicate registrations if the pending order already matches the desired levels.
		if (_pendingIsBuy == isBuy && _hasPlannedPrices &&
		entryPrice == _plannedEntryPrice && stopPrice == _plannedStopPrice &&
		((takeProfitPrice == null && !_plannedTakeProfitEnabled) ||
		(takeProfitPrice != null && _plannedTakeProfitEnabled && takeProfitPrice.Value == _plannedTakeProfitPrice)))
		{
			return;
		}

		CancelActiveOrders();
		ResetPendingPlan();

		var volume = Volume;

		if (isBuy)
		{
			BuyMarket();
		}
		else
		{
			SellMarket();
		}

		_pendingIsBuy = isBuy;
		_hasPlannedPrices = true;
		_plannedEntryPrice = entryPrice;
		_plannedStopPrice = stopPrice;
		_plannedTakeProfitEnabled = takeProfitPrice != null;
		_plannedTakeProfitPrice = takeProfitPrice ?? 0m;
	}

	private void CheckPendingActivation()
	{
		if (_pendingIsBuy == null || !_hasPlannedPrices)
		return;

		if (_pendingIsBuy.Value && _lastPosition <= 0m && Position > 0m)
		{
			ActivatePlannedTargets();
		}
		else if (!_pendingIsBuy.Value && _lastPosition >= 0m && Position < 0m)
		{
			ActivatePlannedTargets();
		}
	}

	private void ActivatePlannedTargets()
	{
		_activeStopPrice = _plannedStopPrice;
		_activeTakeProfitPrice = _plannedTakeProfitEnabled ? _plannedTakeProfitPrice : null;

		ResetPendingPlan();
	}

	private void UpdateHistory(ICandleMessage candle)
	{
		_prevHigh3 = _prevHigh2;
		_prevHigh2 = _prevHigh1;
		_prevHigh1 = candle.HighPrice;

		_prevLow3 = _prevLow2;
		_prevLow2 = _prevLow1;
		_prevLow1 = candle.LowPrice;

		if (_historyCount < 3)
		{
			_historyCount++;
		}
	}

	private void ResetPendingPlan()
	{
		_pendingIsBuy = null;
		_hasPlannedPrices = false;
		_plannedEntryPrice = 0m;
		_plannedStopPrice = 0m;
		_plannedTakeProfitPrice = 0m;
		_plannedTakeProfitEnabled = false;
	}

	private void ResetActiveTargets()
	{
		_activeStopPrice = null;
		_activeTakeProfitPrice = null;
	}
}