Ver no GitHub

Estratégia de Cruzamento RSI EA

A estratégia RSI EA replica o consultor especializado "RSI EA" do MetaTrader 5. Ela observa o Índice de Força Relativa (RSI) na série de velas selecionada e reage quando o momentum cruza níveis configuráveis de sobrecompra ou sobrevenda. A conversão mantém as ideias de stop-loss, take-profit, trailing-stop e gestão automática do dinheiro do sistema original enquanto as adapta à API de estratégia de alto nível do StockSharp.

Lógica da Estratégia

Indicadores

  • RSI com um período configurável aplicado ao tipo de vela escolhido.

Critérios de Entrada

  • Comprado: o RSI cruza acima de RsiBuyLevel (valor anterior abaixo do limiar, valor atual acima do limiar) e o trading comprado está habilitado.
  • Vendido: o RSI cruza abaixo de RsiSellLevel (valor anterior acima do limiar, valor atual abaixo do limiar) e o trading vendido está habilitado.

Apenas uma posição líquida é mantida. Se a estratégia já estiver no mercado, nenhuma posição de hedge adicional é aberta.

Critérios de Saída

  • Saída por sinal: quando CloseBySignal está habilitado, o cruzamento RSI oposto fecha imediatamente a posição ativa.
  • Stop protetor: quando StopLoss é maior que zero, a estratégia monitora a distância do preço a partir do preço médio de entrada e sai assim que a perda atingir o valor especificado.
  • Take-profit: quando TakeProfit é maior que zero, a posição é fechada assim que a distância alvo é atingida.
  • Trailing stop: quando TrailingStop é maior que zero, o nível do stop segue o preço. Para posições compradas, o stop é elevado para Close - TrailingStop assim que o preço avança pelo menos TrailingStop a partir do stop atual; vendidos se comportam simetricamente.

Dimensionamento de Posição

  • Quando UseAutoVolume é true, o volume é calculado a partir do patrimônio da conta e risco: Volume = Equity * RiskPercent / (100 * stopDistance), onde stopDistance usa StopLoss se disponível e caso contrário TrailingStop. Se nenhuma distância de proteção for definida, a estratégia recorre ao volume manual.
  • Quando UseAutoVolume é false, o parâmetro fixo ManualVolume é usado para cada ordem.

Parâmetros

  • CandleType: série de velas usada para cálculo do indicador (padrão: período de 1 minuto).
  • RsiPeriod: número de barras na janela de cálculo do RSI (padrão: 14).
  • RsiBuyLevel: limite de sobrevenda que aciona entradas compradas e saídas vendidas (padrão: 30).
  • RsiSellLevel: limite de sobrecompra que aciona entradas vendidas e saídas compradas (padrão: 70).
  • EnableLong: habilitar ou desabilitar trades comprados (padrão: true).
  • EnableShort: habilitar ou desabilitar trades vendidos (padrão: true).
  • CloseBySignal: fechar posições quando o RSI cruzar o limiar oposto (padrão: true).
  • StopLoss: distância do stop-loss em unidades de preço (padrão: 0, desabilitado).
  • TakeProfit: distância do take-profit em unidades de preço (padrão: 0, desabilitado).
  • TrailingStop: distância do trailing stop em unidades de preço (padrão: 0, desabilitado).
  • UseAutoVolume: ativar dimensionamento de posição baseado em risco (padrão: true).
  • RiskPercent: porcentagem do patrimônio a arriscar quando o dimensionamento automático está ativo (padrão: 10).
  • ManualVolume: tamanho de ordem fixo quando o dimensionamento automático está desabilitado (padrão: 0.1).

Notas de Implementação

  • A implementação do StockSharp usa o fluxo de trabalho de alto nível SubscribeCandles(...).Bind(...), permitindo que o indicador RSI entregue valores diretamente à estratégia sem gerenciamento manual de buffers.
  • A estratégia redefine todos os níveis de proteção quando a posição retorna a zero para evitar valores obsoletos de stop ou take-profit.
  • A lógica de trailing espelha o código MQL: o stop só é ajustado após o preço percorrer mais do dobro da distância de trailing além do nível de stop atual, evitando o aperto prematuro.
  • Como as estratégias do StockSharp operam em um ambiente de netting, não é possível manter posições compradas e vendidas simultâneas como no EA de hedge original. Em vez disso, a estratégia aguarda o fechamento da posição atual antes de abrir na direção oposta.
  • O dimensionamento automático requer que StopLoss ou TrailingStop sejam definidos; caso contrário, o volume manual é usado porque a distância de risco é desconhecida.

Configuração Padrão

  • Período: velas de 1 minuto.
  • RSI: período 14, níveis 30/70.
  • Gestão do dinheiro: volume automático habilitado, risco de patrimônio de 10%, volume de fallback manual 0.1.
  • Controles de risco: sem stop-loss, take-profit ou trailing stop por padrão (devem ser configurados para trading ao vivo).

Dicas de Uso

  • Configure CandleType para corresponder ao instrumento e ao horizonte temporal que pretende operar; a estratégia funciona em qualquer intervalo suportado pelas velas do StockSharp.
  • Forneça distâncias de stop-loss ou trailing-stop realistas antes de habilitar o dimensionamento automático para que o cálculo de risco use valores significativos.
  • Combine a estratégia com StartProtection() (já chamado no código) para permitir que o framework gerencie desconexões inesperadas ou posições órfãs.
  • Monitore execuções e ajuste os níveis de RSI ao aplicar a estratégia a diferentes mercados, pois os limiares ideais podem variar.
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>
/// Relative Strength Index crossover strategy translated from the MetaTrader RSI EA.
/// </summary>
public class RsiCrossoverEaStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiBuyLevel;
	private readonly StrategyParam<decimal> _rsiSellLevel;
	private readonly StrategyParam<bool> _enableLong;
	private readonly StrategyParam<bool> _enableShort;
	private readonly StrategyParam<bool> _closeBySignal;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<bool> _useAutoVolume;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _manualVolume;

	private RelativeStrengthIndex _rsi;
	private decimal? _previousRsi;
	private decimal? _longStop;
	private decimal? _shortStop;
	private decimal? _longTakeProfit;
	private decimal? _shortTakeProfit;

	/// <summary>
	/// Candle type used to calculate RSI.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Period of the RSI indicator.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Oversold level that triggers long entries.
	/// </summary>
	public decimal RsiBuyLevel
	{
		get => _rsiBuyLevel.Value;
		set => _rsiBuyLevel.Value = value;
	}

	/// <summary>
	/// Overbought level that triggers short entries.
	/// </summary>
	public decimal RsiSellLevel
	{
		get => _rsiSellLevel.Value;
		set => _rsiSellLevel.Value = value;
	}

	/// <summary>
	/// Enable or disable long trades.
	/// </summary>
	public bool EnableLong
	{
		get => _enableLong.Value;
		set => _enableLong.Value = value;
	}

	/// <summary>
	/// Enable or disable short trades.
	/// </summary>
	public bool EnableShort
	{
		get => _enableShort.Value;
		set => _enableShort.Value = value;
	}

	/// <summary>
	/// Close positions when the RSI crosses the opposite level.
	/// </summary>
	public bool CloseBySignal
	{
		get => _closeBySignal.Value;
		set => _closeBySignal.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

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

	/// <summary>
	/// Trailing stop distance in price units.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStop.Value;
		set => _trailingStop.Value = value;
	}

	/// <summary>
	/// Automatically size orders by account risk.
	/// </summary>
	public bool UseAutoVolume
	{
		get => _useAutoVolume.Value;
		set => _useAutoVolume.Value = value;
	}

	/// <summary>
	/// Risk percentage applied when auto volume is enabled.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Fixed order volume used when auto sizing is disabled.
	/// </summary>
	public decimal ManualVolume
	{
		get => _manualVolume.Value;
		set => _manualVolume.Value = value;
	}

	/// <summary>
    /// Initializes a new instance of the <see cref="RsiCrossoverEaStrategy"/> class.
    /// </summary>
    public RsiCrossoverEaStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for RSI", "General");

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Number of bars used for RSI", "RSI")
			
			.SetOptimize(8, 28, 2);

		_rsiBuyLevel = Param(nameof(RsiBuyLevel), 30m)
			.SetRange(0m, 100m)
			.SetDisplay("RSI Buy Level", "Cross above this level opens longs", "RSI")
			
			.SetOptimize(20m, 40m, 5m);

		_rsiSellLevel = Param(nameof(RsiSellLevel), 70m)
			.SetRange(0m, 100m)
			.SetDisplay("RSI Sell Level", "Cross below this level opens shorts", "RSI")
			
			.SetOptimize(60m, 80m, 5m);

		_enableLong = Param(nameof(EnableLong), true)
			.SetDisplay("Enable Long", "Allow bullish trades", "Trading");

		_enableShort = Param(nameof(EnableShort), true)
			.SetDisplay("Enable Short", "Allow bearish trades", "Trading");

		_closeBySignal = Param(nameof(CloseBySignal), true)
			.SetDisplay("Close By Signal", "Exit when RSI flips", "Trading");

		_stopLoss = Param(nameof(StopLoss), 0m)
			.SetRange(0m, 1000m)
			.SetDisplay("Stop Loss", "Distance from entry for stop loss", "Risk")
			
			.SetOptimize(0m, 200m, 20m);

		_takeProfit = Param(nameof(TakeProfit), 0m)
			.SetRange(0m, 1000m)
			.SetDisplay("Take Profit", "Distance from entry for take profit", "Risk")
			
			.SetOptimize(0m, 200m, 20m);

		_trailingStop = Param(nameof(TrailingStop), 0m)
			.SetRange(0m, 1000m)
			.SetDisplay("Trailing Stop", "Trailing distance after price moves", "Risk")
			
			.SetOptimize(0m, 200m, 20m);

		_useAutoVolume = Param(nameof(UseAutoVolume), true)
			.SetDisplay("Auto Volume", "Size positions by risk percent", "Money Management");

		_riskPercent = Param(nameof(RiskPercent), 10m)
			.SetRange(0m, 100m)
			.SetDisplay("Risk Percent", "Percentage of equity risked per trade", "Money Management");

		_manualVolume = Param(nameof(ManualVolume), 0.1m)
			.SetRange(0.01m, 100m)
			.SetDisplay("Manual Volume", "Fixed volume when auto sizing is off", "Money Management");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_previousRsi = null;
		_longStop = null;
		_shortStop = null;
		_longTakeProfit = null;
		_shortTakeProfit = null;
	}

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

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_rsi, Process)
			.Start();

		StartProtection(null, null);
	}

	private void Process(ICandleMessage candle, decimal rsiValue)
	{
		if (candle.State != CandleStates.Finished)
			return; // Wait for completed candles only.

		if (!_rsi.IsFormed)
		{
			_previousRsi = rsiValue;
			return; // Indicator still gathering enough data.
		}

		var previous = _previousRsi;
		_previousRsi = rsiValue;

		if (ManageOpenPosition(candle))
			return; // Exit orders were submitted, wait for fills before new decisions.

		var crossAboveBuy = previous.HasValue && previous.Value < RsiBuyLevel && rsiValue > RsiBuyLevel;
		var crossBelowSell = previous.HasValue && previous.Value > RsiSellLevel && rsiValue < RsiSellLevel;

		if (CloseBySignal)
		{
			if (Position > 0 && crossBelowSell)
			{
				SellMarket();
				ResetProtection();
				return; // Close long trades when RSI drops below the sell level.
			}

			if (Position < 0 && crossAboveBuy)
			{
				BuyMarket();
				ResetProtection();
				return; // Close short trades when RSI rises above the buy level.
			}
		}

		if (Position != 0)
			return; // Do not add hedged positions in the netted environment.

		if (EnableShort && crossBelowSell)
		{
			var volume = CalculateVolume();
			if (volume > 0m)
				SellMarket();
			return;
		}

		if (EnableLong && crossAboveBuy)
		{
			var volume = CalculateVolume();
			if (volume > 0m)
				BuyMarket();
		}
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0)
			ResetProtection();
	}

	private bool ManageOpenPosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var entryPrice = candle.ClosePrice;

			if (_longStop is null && StopLoss > 0m)
				_longStop = entryPrice - StopLoss; // Initial protective stop below entry.

			if (_longTakeProfit is null && TakeProfit > 0m)
				_longTakeProfit = entryPrice + TakeProfit; // Profit target above entry.

			if (TrailingStop > 0m && candle.ClosePrice > entryPrice)
			{
				var candidate = candle.ClosePrice - TrailingStop;
				if (!_longStop.HasValue || candle.ClosePrice - 2m * TrailingStop > _longStop.Value)
					_longStop = candidate; // Trail only when price advances enough.
			}

			if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
			{
				SellMarket();
				ResetProtection();
				return true;
			}

			if (_longTakeProfit.HasValue && candle.HighPrice >= _longTakeProfit.Value)
			{
				SellMarket();
				ResetProtection();
				return true;
			}
		}
		else if (Position < 0)
		{
			var entryPrice = candle.ClosePrice;

			if (_shortStop is null && StopLoss > 0m)
				_shortStop = entryPrice + StopLoss; // Protective stop above entry.

			if (_shortTakeProfit is null && TakeProfit > 0m)
				_shortTakeProfit = entryPrice - TakeProfit; // Profit target below entry.

			if (TrailingStop > 0m && candle.ClosePrice < entryPrice)
			{
				var candidate = candle.ClosePrice + TrailingStop;
				if (!_shortStop.HasValue || candle.ClosePrice + 2m * TrailingStop < _shortStop.Value)
					_shortStop = candidate; // Trail short stops only after favorable move.
			}

			if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
			{
				BuyMarket();
				ResetProtection();
				return true;
			}

			if (_shortTakeProfit.HasValue && candle.LowPrice <= _shortTakeProfit.Value)
			{
				BuyMarket();
				ResetProtection();
				return true;
			}
		}
		else
		{
			ResetProtection(); // Ensure cached levels are cleared when flat.
		}

		return false;
	}

	private decimal CalculateVolume()
	{
		if (!UseAutoVolume)
			return ManualVolume; // Use fixed size when auto sizing is disabled.

		var equity = Portfolio?.CurrentValue ?? 0m;
		if (equity <= 0m)
			return ManualVolume; // Fallback if equity information is unavailable.

		var stopDistance = StopLoss > 0m ? StopLoss : TrailingStop;
		if (stopDistance <= 0m)
			return ManualVolume; // Cannot compute risk-based size without a stop.

		var riskAmount = equity * RiskPercent / 100m;
		if (riskAmount <= 0m)
			return ManualVolume;

		var volume = riskAmount / stopDistance;
		return volume > 0m ? volume : ManualVolume;
	}

	private void ResetProtection()
	{
		_longStop = null;
		_shortStop = null;
		_longTakeProfit = null;
		_shortTakeProfit = null;
	}
}