Ver en GitHub

Estrategia Renko Line Break vs RSI

Esta estrategia recrea el experto MetaTrader "RenkoLineBreak vs RSI" usando la API de alto nivel de StockSharp. Combina la detección de tendencia Renko con un filtro de retroceso RSI y entra a mercado en cuanto una estructura de precio de tres velas confirma la configuración. Los ladrillos Renko se calculan dentro de la propia estrategia a partir de los cierres de las velas temporales, por lo que una única suscripción de velas alimenta todo.

Detalles

  • Criterios de entrada:
    • Largo: La tendencia Renko permanece alcista y el RSI cae hasta 50 - RsiShift o por debajo. La configuración se valida contra un nivel de referencia igual al máximo de la vela de tres barras atrás más IndentFromHighLow, y se envía una orden de compra a mercado al cierre de la vela de señal.
    • Corto: La tendencia Renko permanece bajista y el RSI sube hasta 50 + RsiShift o por encima. La configuración se valida contra un nivel de referencia igual al mínimo de la vela de tres barras atrás menos IndentFromHighLow, y se envía una orden de venta a mercado al cierre de la vela de señal.
    • No se abre ninguna posición nueva mientras la tendencia Renko está en un estado de transición (ToUp / ToDown); la configuración almacenada se descarta.
  • Largo/Corto: Ambos.
  • Criterios de salida:
    • Salidas de mercado cuando aparece la transición Renko opuesta (ToDown para largos, ToUp para cortos).
    • El RSI cruza de vuelta a través del punto medio (50 ± RsiShift).
    • Los rangos de velas alcanzando los niveles de stop-loss o take-profit planificados.
  • Stops:
    • El stop-loss está anclado al extremo de las últimas tres velas más IndentFromHighLow.
    • El take-profit está a TakeProfit unidades de precio desde el nivel de ruptura de referencia (opcional cuando se establece en cero).
  • Valores predeterminados:
    • BoxSize = 100m.
    • RsiPeriod = 4.
    • RsiShift = 10m.
    • TakeProfit = 1000m.
    • IndentFromHighLow = 50m.
    • Volume = 1m.
    • CandleType = marco temporal de 2 horas.
  • Filtros:
    • Categoría: Seguimiento de tendencia.
    • Dirección: Ambos.
    • Indicadores: Renko, RSI.
    • Stops: Stop fijo y take profit.
    • Complejidad: Intermedio.
    • Marco temporal: Un solo marco temporal (los ladrillos Renko se derivan de los cierres de las velas).
    • Estacionalidad: No.
    • Redes neuronales: No.
    • Divergencia: No.
    • Nivel de riesgo: Medio.

Cómo funciona

  1. Los ladrillos Renko se construyen dentro de la estrategia a partir de los cierres de las velas temporales: un ladrillo que continúa la dirección actual se genera cuando el cierre se aleja un BoxSize completo del ancla actual, mientras que un ladrillo que invierte la dirección exige dos BoxSize. Antes de que el primer ladrillo fije una dirección, basta con un box en cualquier sentido. Se generan tantos ladrillos como abarque el movimiento y el ancla se desplaza con ellos. Cuando un ladrillo cambia de dirección, el estado de tendencia se establece en ToUp o ToDown por un paso para imitar el comportamiento del indicador original.
  2. El mismo flujo de velas alimenta el indicador RSI y proporciona los últimos tres máximos/mínimos usados para los niveles de ruptura, por lo que la estrategia abre exactamente una suscripción de datos de mercado.
  3. Cuando ambas condiciones de tendencia Renko y RSI se alinean, la estrategia envía una orden a mercado (compra o venta). Los niveles planificados de stop-loss y take-profit se almacenan y se monitorean una vez que la posición está abierta.
  4. Una vez abierta la posición, los niveles de protección almacenados se activan. Las velas posteriores verifican si el precio alcanza los rangos de stop o objetivo; si es así, la posición se cierra a mercado.
  5. Si el impulso se desvanece (RSI cruza de vuelta a través del punto medio) o la tendencia Renko cambia, la posición se cierra anticipadamente.

Indicadores utilizados

  • Ladrillos Renko derivados de los cierres de las velas temporales con el paso BoxSize, para inferir el sesgo direccional y detectar transiciones entre estados alcistas y bajistas.
  • Relative Strength Index (RSI) para calificar entradas exigiendo retrocesos contra la tendencia.

Notas adicionales

  • IndentFromHighLow modela el buffer del experto original que mantiene el nivel de ruptura de referencia y el stop-loss alejados de los máximos y mínimos recientes.
  • TakeProfit puede establecerse en cero para deshabilitar el objetivo de ganancia mientras deja la lógica de stop-loss intacta.
  • La estrategia mantiene una sola posición a la vez: solo considera una nueva entrada cuando está fuera del mercado y descarta la configuración almacenada en cuanto las condiciones del mercado la invalidan.
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;
	}
}