Ver no GitHub

Estratégia Renko Line Break vs RSI

Esta estratégia recria o especialista MetaTrader "RenkoLineBreak vs RSI" usando a API de alto nível do StockSharp. Combina a detecção de tendência Renko com um filtro de retrocesso RSI e entra a mercado assim que uma estrutura de preço de três velas confirma a configuração. Os tijolos Renko são calculados dentro da própria estratégia a partir dos fechamentos das velas temporais, portanto uma única subscrição de velas alimenta tudo.

Detalhes

  • Critérios de entrada:
    • Comprado: A tendência Renko permanece altista e o RSI cai até 50 - RsiShift ou abaixo. A configuração é validada contra um nível de referência igual à máxima da vela de três barras atrás mais IndentFromHighLow, e uma ordem de compra a mercado é enviada no fechamento da vela de sinal.
    • Vendido: A tendência Renko permanece baixista e o RSI sobe até 50 + RsiShift ou acima. A configuração é validada contra um nível de referência igual à mínima da vela de três barras atrás menos IndentFromHighLow, e uma ordem de venda a mercado é enviada no fechamento da vela de sinal.
    • Nenhuma nova entrada é feita enquanto a tendência Renko está em um estado de transição (ToUp / ToDown); a configuração armazenada é descartada.
  • Comprado/Vendido: Ambos.
  • Critérios de saída:
    • Saídas de mercado quando a transição Renko oposta aparece (ToDown para comprados, ToUp para vendidos).
    • O RSI cruza de volta pelo ponto médio (50 ± RsiShift).
    • Intervalos de velas atingindo os níveis de stop loss ou take profit planejados.
  • Stops:
    • O stop loss está ancorado ao extremo das últimas três velas mais IndentFromHighLow.
    • O take profit está a TakeProfit unidades de preço do nível de rompimento de referência (opcional quando definido como zero).
  • Valores padrão:
    • BoxSize = 100m.
    • RsiPeriod = 4.
    • RsiShift = 10m.
    • TakeProfit = 1000m.
    • IndentFromHighLow = 50m.
    • Volume = 1m.
    • CandleType = período de 2 horas.
  • Filtros:
    • Categoria: Seguidor de tendência.
    • Direção: Ambos.
    • Indicadores: Renko, RSI.
    • Stops: Stop fixo e take profit.
    • Complexidade: Intermediário.
    • Período: Um único período (os tijolos Renko são derivados dos fechamentos das velas).
    • Sazonalidade: Não.
    • Redes neurais: Não.
    • Divergência: Não.
    • Nível de risco: Médio.

Como funciona

  1. Os tijolos Renko são construídos dentro da estratégia a partir dos fechamentos das velas temporais: um tijolo que continua a direção atual é gerado assim que o fechamento se afasta um BoxSize completo da âncora atual, enquanto um tijolo que inverte a direção exige dois BoxSize. Antes de o primeiro tijolo definir uma direção, basta um box em qualquer sentido. São gerados tantos tijolos quantos o movimento cobrir, e a âncora acompanha o movimento. Quando um tijolo muda de direção, o estado de tendência é definido como ToUp ou ToDown por um passo para imitar o comportamento do indicador original.
  2. O mesmo fluxo de velas alimenta o indicador RSI e fornece as últimas três máximas/mínimas usadas para os níveis de ruptura, de modo que a estratégia abre exatamente uma subscrição de dados de mercado.
  3. Quando as condições de tendência Renko e RSI se alinham, a estratégia envia uma ordem a mercado (compra ou venda). Os níveis planejados de stop loss e take profit são armazenados e monitorados assim que a posição é aberta.
  4. Assim que a posição é aberta, os níveis de proteção armazenados tornam-se ativos. As velas subsequentes verificam se o preço atinge os intervalos de stop ou alvo; se sim, a posição é fechada a mercado.
  5. Se o impulso diminui (RSI cruza de volta pelo ponto médio) ou a tendência Renko muda, a posição é fechada antecipadamente.

Indicadores utilizados

  • Tijolos Renko derivados dos fechamentos das velas temporais com o passo BoxSize, para inferir o viés direcional e detectar transições entre estados de alta e baixa.
  • Relative Strength Index (RSI) para qualificar entradas exigindo retrocessos contra a tendência.

Notas adicionais

  • IndentFromHighLow modela o buffer do especialista original que mantém o nível de rompimento de referência e o stop loss afastados das máximas e mínimas recentes.
  • TakeProfit pode ser definido como zero para desabilitar o alvo de lucro enquanto mantém a lógica de stop loss intacta.
  • A estratégia mantém apenas uma posição de cada vez: uma nova entrada só é considerada quando ela está fora do mercado, e a configuração armazenada é descartada assim que as condições do mercado a invalidam.
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;
	}
}