Ver en GitHub

Estrategia de Cruce RSI EA

La estrategia RSI EA replica el asesor experto "RSI EA" de MetaTrader 5. Observa el Índice de Fuerza Relativa (RSI) en la serie de velas seleccionada y reacciona cuando el impulso cruza niveles configurables de sobrecompra o sobreventa. La conversión mantiene las ideas de stop-loss, take-profit, trailing-stop y gestión automática del dinero del sistema original mientras las adapta a la API de estrategia de alto nivel de StockSharp.

Lógica de la Estrategia

Indicadores

  • RSI con un período configurable aplicado al tipo de vela elegido.

Criterios de Entrada

  • Largo: el RSI cruza hacia arriba RsiBuyLevel (valor previo por debajo del umbral, valor actual por encima del umbral) y el trading largo está habilitado.
  • Corto: el RSI cruza hacia abajo RsiSellLevel (valor previo por encima del umbral, valor actual por debajo del umbral) y el trading corto está habilitado.

Solo se mantiene una posición neta. Si la estrategia ya está en el mercado, no se abren posiciones de cobertura adicionales.

Criterios de Salida

  • Salida por señal: cuando CloseBySignal está habilitado, el cruce RSI opuesto cierra inmediatamente la posición activa.
  • Stop protector: cuando StopLoss es mayor que cero, la estrategia monitorea la distancia del precio desde el precio promedio de entrada y sale una vez que la pérdida alcanza el monto especificado.
  • Take-profit: cuando TakeProfit es mayor que cero, la posición se cierra tan pronto como se alcanza la distancia objetivo.
  • Trailing stop: cuando TrailingStop es mayor que cero, el nivel del stop sigue al precio. Para posiciones largas, el stop se eleva a Close - TrailingStop una vez que el precio avanza al menos TrailingStop desde el stop actual; los cortos se comportan simétricamente.

Dimensionamiento de Posición

  • Cuando UseAutoVolume es true, el volumen se calcula a partir del patrimonio de la cuenta y el riesgo: Volume = Equity * RiskPercent / (100 * stopDistance), donde stopDistance usa StopLoss si está disponible y de lo contrario TrailingStop. Si no se establece ninguna distancia de protección, la estrategia recurre al volumen manual.
  • Cuando UseAutoVolume es false, el parámetro fijo ManualVolume se usa para cada orden.

Parámetros

  • CandleType: serie de velas usada para el cálculo del indicador (predeterminado: marco temporal de 1 minuto).
  • RsiPeriod: número de barras en la ventana de cálculo del RSI (predeterminado: 14).
  • RsiBuyLevel: límite de sobreventa que desencadena entradas largas y salidas cortas (predeterminado: 30).
  • RsiSellLevel: límite de sobrecompra que desencadena entradas cortas y salidas largas (predeterminado: 70).
  • EnableLong: habilitar o deshabilitar operaciones largas (predeterminado: true).
  • EnableShort: habilitar o deshabilitar operaciones cortas (predeterminado: true).
  • CloseBySignal: cerrar posiciones cuando el RSI cruza el umbral opuesto (predeterminado: true).
  • StopLoss: distancia del stop-loss en unidades de precio (predeterminado: 0, deshabilitado).
  • TakeProfit: distancia del take-profit en unidades de precio (predeterminado: 0, deshabilitado).
  • TrailingStop: distancia del trailing stop en unidades de precio (predeterminado: 0, deshabilitado).
  • UseAutoVolume: activar el dimensionamiento de posición basado en riesgo (predeterminado: true).
  • RiskPercent: porcentaje del patrimonio a arriesgar cuando el dimensionamiento automático está activo (predeterminado: 10).
  • ManualVolume: tamaño de orden fijo cuando el dimensionamiento automático está deshabilitado (predeterminado: 0.1).

Notas de Implementación

  • La implementación de StockSharp usa el flujo de trabajo de alto nivel SubscribeCandles(...).Bind(...), lo que permite al indicador RSI entregar valores directamente a la estrategia sin gestión manual de búferes.
  • La estrategia restablece todos los niveles de protección cuando la posición vuelve a cero para evitar valores obsoletos de stop o take-profit.
  • La lógica de trailing refleja el código MQL: el stop solo se ajusta después de que el precio viaja más del doble de la distancia de trailing más allá del nivel de stop actual, evitando el ajuste prematuro.
  • Debido a que las estrategias de StockSharp operan en un entorno de netting, no es posible mantener posiciones largas y cortas simultáneas como en el EA de cobertura original. En cambio, la estrategia espera a que la posición actual se cierre antes de abrir en la dirección opuesta.
  • El dimensionamiento automático requiere que StopLoss o TrailingStop estén definidos; de lo contrario, se usa el volumen manual porque la distancia de riesgo es desconocida.

Configuración Predeterminada

  • Marco temporal: velas de 1 minuto.
  • RSI: período 14, niveles 30/70.
  • Gestión del dinero: volumen automático habilitado, riesgo de patrimonio del 10%, volumen de respaldo manual 0.1.
  • Controles de riesgo: sin stop-loss, take-profit, ni trailing stop por defecto (deben configurarse para trading en vivo).

Consejos de Uso

  • Configure CandleType para que coincida con el instrumento y el horizonte temporal que pretende operar; la estrategia funciona en cualquier intervalo compatible con las velas de StockSharp.
  • Proporcione distancias de stop-loss o trailing-stop realistas antes de habilitar el dimensionamiento automático para que el cálculo de riesgo use valores significativos.
  • Combine la estrategia con StartProtection() (ya llamado en el código) para permitir que el framework gestione desconexiones inesperadas o posiciones huérfanas.
  • Monitoree las ejecuciones y ajuste los niveles de RSI al aplicar la estrategia a diferentes mercados, ya que los umbrales óptimos pueden 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;
	}
}