Auf GitHub ansehen

Renko Line Break vs RSI-Strategie

Diese Strategie recreiert den MetaTrader-Expertenberater "RenkoLineBreak vs RSI" mit der StockSharp-High-Level-API. Sie kombiniert Renko-Trenderkennung mit einem RSI-Rücksetzerfilter und steigt zum Marktpreis ein, sobald eine Drei-Kerzen-Preisstruktur das Setup bestätigt. Die Renko-Ziegel werden in der Strategie selbst aus den Schlusskursen der Zeitkerzen berechnet, sodass ein einziges Kerzen-Abonnement alles antreibt.

Details

  • Einstiegskriterien:
    • Long: Der Renko-Trend bleibt bullisch und der RSI fällt auf 50 - RsiShift oder darunter. Das Setup wird gegen ein Referenzniveau aus dem Hoch der Kerze von drei Balken zuvor plus IndentFromHighLow geprüft, und beim Schluss der Signalkerze wird eine Kauf-Market-Order gesendet.
    • Short: Der Renko-Trend bleibt bärisch und der RSI steigt auf 50 + RsiShift oder darüber. Das Setup wird gegen ein Referenzniveau aus dem Tief der Kerze von drei Balken zuvor minus IndentFromHighLow geprüft, und beim Schluss der Signalkerze wird eine Verkauf-Market-Order gesendet.
    • Solange der Renko-Trend in einem Übergangszustand steht (ToUp / ToDown), wird kein neuer Einstieg vorgenommen; das gespeicherte Setup wird verworfen.
  • Long/Short: Beide.
  • Ausstiegskriterien:
    • Marktausstiege, wenn der entgegengesetzte Renko-Übergang erscheint (ToDown für Longs, ToUp für Shorts).
    • RSI kreuzt zurück durch den Mittelpunkt (50 ± RsiShift).
    • Kerzenbereiche, die die geplanten Stop-Loss- oder Take-Profit-Level erreichen.
  • Stops:
    • Der Stop-Loss ist am Extrempunkt der letzten drei Kerzen plus IndentFromHighLow verankert.
    • Take-Profit liegt TakeProfit Preiseinheiten vom Referenz-Ausbruchsniveau entfernt (optional wenn auf null gesetzt).
  • Standardwerte:
    • BoxSize = 100m.
    • RsiPeriod = 4.
    • RsiShift = 10m.
    • TakeProfit = 1000m.
    • IndentFromHighLow = 50m.
    • Volume = 1m.
    • CandleType = 2-Stunden-Zeitrahmen.
  • Filter:
    • Kategorie: Trendfolge.
    • Richtung: Beide.
    • Indikatoren: Renko, RSI.
    • Stops: Harter Stop & Take Profit.
    • Komplexität: Mittel.
    • Zeitrahmen: Ein einziger Zeitrahmen (Renko-Ziegel aus den Kerzenschlusskursen abgeleitet).
    • Saisonalität: Nein.
    • Neuronale Netze: Nein.
    • Divergenz: Nein.
    • Risikolevel: Moderat.

Funktionsweise

  1. Die Renko-Ziegel werden innerhalb der Strategie aus den Schlusskursen der Zeitkerzen gebildet: Ein Ziegel, der die aktuelle Richtung fortsetzt, entsteht, sobald sich der Schlusskurs um eine volle BoxSize vom aktuellen Ankerpreis entfernt, während ein Ziegel, der die Richtung umkehrt, zwei BoxSize benötigt. Bevor der erste Ziegel eine Richtung festlegt, genügt eine Box in beide Richtungen. Es entstehen so viele Ziegel, wie die Bewegung abdeckt, und der Anker wandert mit. Wenn ein Ziegel die Richtung wechselt, wird der Trendzustand für einen Schritt auf ToUp oder ToDown gesetzt, um das ursprüngliche Indikatorverhalten nachzuahmen.
  2. Derselbe Kerzenstrom speist den RSI-Indikator und liefert die letzten drei Hochs/Tiefs, die für Ausbruchniveaus verwendet werden, sodass die Strategie genau ein Marktdaten-Abonnement öffnet.
  3. Wenn sowohl Renko-Trend als auch RSI-Bedingungen übereinstimmen, sendet die Strategie eine Market-Order (Kauf oder Verkauf). Geplante Stop-Loss- und Take-Profit-Level werden gespeichert und überwacht, sobald die Position offen ist.
  4. Sobald die Position offen ist, werden die gespeicherten Schutzlevel aktiv. Nachfolgende Kerzen prüfen, ob der Preis den Stop- oder Zielbereich erreicht; wenn ja, wird die Position zum Markt geschlossen.
  5. Wenn die Dynamik nachlässt (RSI kreuzt zurück durch den Mittelpunkt) oder der Renko-Trend sich ändert, wird die Position frühzeitig geschlossen.

Verwendete Indikatoren

  • Renko-Ziegel, die mit der Schrittweite BoxSize aus den Schlusskursen der Zeitkerzen abgeleitet werden, zur Ableitung des Richtungsbiases und Erkennung von Übergängen zwischen Auf- und Abwärtszuständen.
  • Relative Strength Index (RSI) zur Qualifizierung von Einstiegen durch Forderung von Rücksetzern gegen den Trend.

Zusätzliche Hinweise

  • IndentFromHighLow modelliert den Puffer des ursprünglichen Expertenberaters, der das Referenz-Ausbruchsniveau und den Stop-Loss von aktuellen Hochs und Tiefs fernhält.
  • TakeProfit kann auf null gesetzt werden, um das Gewinnziel zu deaktivieren, während die Stop-Loss-Logik intakt bleibt.
  • Die Strategie hält jeweils nur eine Position: Ein neuer Einstieg kommt nur infrage, solange keine Position offen ist, und das gespeicherte Setup wird verworfen, sobald die Marktbedingungen es ungültig machen.
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>
/// 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 TrendStates _trendState = TrendStates.None;
	private bool _renkoHasPrev;
	private bool _renkoPrevBull;
	private decimal _renkoAnchorPrice;
	private bool _hasRenkoAnchor;

	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()
	{
		return [(Security, CandleType)];
	}

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

		_rsi = null;

		_trendState = TrendStates.None;
		_renkoHasPrev = false;
		_renkoPrevBull = false;
		_renkoAnchorPrice = 0m;
		_hasRenkoAnchor = 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
		};

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

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

		StartProtection(null, null);
	}

	private void UpdateRenkoBricks(decimal closePrice)
	{
		var boxSize = BoxSize;

		if (boxSize <= 0m)
		return;

		if (!_hasRenkoAnchor)
		{
			// The first close only anchors the brick grid, a direction needs a full box move.
			_renkoAnchorPrice = closePrice;
			_hasRenkoAnchor = true;
			return;
		}

		while (true)
		{
			// A brick continuing the current direction needs one box, a brick reversing it needs two.
			// Until the first brick sets a direction one box is enough either way.
			var upDistance = _renkoHasPrev && !_renkoPrevBull ? boxSize * 2m : boxSize;
			var downDistance = _renkoHasPrev && _renkoPrevBull ? boxSize * 2m : boxSize;

			if (closePrice - _renkoAnchorPrice >= upDistance)
			{
				_renkoAnchorPrice += upDistance;
				ProcessRenkoBrick(true);
			}
			else if (_renkoAnchorPrice - closePrice >= downDistance)
			{
				_renkoAnchorPrice -= downDistance;
				ProcessRenkoBrick(false);
			}
			else
			{
				break;
			}
		}
	}

	private void ProcessRenkoBrick(bool isBull)
	{
		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;
		}

		_trendState = isBull
		? (_renkoPrevBull ? TrendStates.Up : TrendStates.ToUp)
		: (_renkoPrevBull ? TrendStates.ToDown : TrendStates.Down);

		_renkoPrevBull = isBull;
	}

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

		UpdateRenkoBricks(candle.ClosePrice);

		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;
	}
}