Ver en GitHub

Estrategia RSI Expert con Filtro de Tendencia

Descripción general

  • Conversión del asesor experto de MetaTrader 5 RSI_Expert_v2.0 a la API de estrategia de alto nivel de StockSharp.
  • Genera señales en el CandleType configurado (predeterminado 1 hora) y ejecuta operaciones al cierre de la vela.
  • Diseñado para posiciones netas: la estrategia mantiene una única posición agregada en lugar de cubrir múltiples tickets.

Lógica de entrada

  1. Cruce de RSI – una configuración larga aparece cuando el último valor del RSI sube por encima de RsiLevelDown mientras la vela finalizada anterior estaba por debajo del nivel. Una configuración corta se activa cuando el RSI cae por debajo de RsiLevelUp después de estar por encima.
  2. Filtro de media móvil – el experto original permite operar con o en contra de un cruce de media móvil. El parámetro MaMode reproduce las opciones:
    • Off: ignorar las medias móviles y operar solo con disparadores de RSI.
    • Forward: permitir largos solo cuando la MA rápida está por encima de la MA lenta, cortos solo cuando está por debajo.
    • Reverse: invertir el filtro para que los largos requieran la MA rápida por debajo de la MA lenta, coincidiendo con el modo "inverso" del EA.

Ambas condiciones deben coincidir antes de que la estrategia abra una nueva orden de mercado. Si ya hay una posición abierta o una orden en espera, se ignoran las nuevas señales hasta que termine.

Gestión de operaciones

  • El stop-loss inicial y el take-profit se expresan en pips usando el PriceStep del instrumento. Ambos son opcionales; establecer un valor de cero deshabilita la salida respectiva.
  • Cuando TrailingStopPips es mayor que cero, el stop seguirá el precio una vez que el beneficio supere TrailingStopPips + TrailingStepPips. El valor del paso debe ser estrictamente positivo cuando el trailing está habilitado (la estrategia lanza una excepción en caso contrario).
  • Si UseMartingale está habilitado, el siguiente volumen de la orden se duplica después de que la posición anterior cerrara con una pérdida (detectada a través del PnL realizado). Las operaciones ganadoras reinician el multiplicador.

Gestión del capital

  • MoneyMode = FixedVolume mantiene el mismo VolumeOrRiskValue para cada entrada.
  • MoneyMode = RiskPercent trata VolumeOrRiskValue como un porcentaje del capital de la cartera y deriva la cantidad a partir de la distancia del stop-loss configurado. Cuando no se especifica stop-loss, la estrategia recurre al valor bruto.
  • Los volúmenes se normalizan a las reglas de la bolsa usando Security.MinVolume y Security.VolumeStep para evitar tamaños de orden inválidos.

Notas adicionales de implementación

  • La lógica de trailing y las comprobaciones de stop/objetivo se evalúan en velas finalizadas para replicar el comportamiento de "nueva barra" de la versión MQL.
  • La marca de martingala usa cambios de PnL realizados cuando se cierra una posición externamente, por lo que los cierres manuales también se rastrean.
  • Dado que StockSharp usa posiciones agregadas, las operaciones largas y cortas simultáneas (modo de cobertura MT5) no son compatibles.

Parámetros

Nombre Descripción
CandleType Marco temporal usado para actualizaciones de indicadores y generación de señales.
StopLossPips Distancia inicial del stop-loss en pips; cero deshabilita el stop.
TakeProfitPips Distancia inicial del take-profit en pips; cero deshabilita el objetivo.
TrailingStopPips Distancia del trailing stop. Requiere un TrailingStepPips positivo.
TrailingStepPips Pips adicionales necesarios antes de que el trailing stop se mueva de nuevo.
MoneyMode Selecciona dimensionamiento de lote fijo o cálculo por porcentaje de riesgo.
VolumeOrRiskValue Tamaño de lote en modo fijo o porcentaje de riesgo en modo de riesgo.
UseMartingale Duplica el siguiente volumen de orden después de una operación perdedora.
FastMaPeriod Período de la media móvil rápida usada por el filtro de tendencia.
SlowMaPeriod Período de la media móvil lenta usada por el filtro de tendencia.
RsiPeriod Longitud de promediado para el indicador RSI.
RsiLevelUp Umbral superior del RSI que activa configuraciones cortas.
RsiLevelDown Umbral inferior del RSI que activa configuraciones largas.
MaMode Habilita o invierte el filtro de confirmación de media móvil.
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>
/// RSI crossover expert advisor converted from the "RSI_Expert_v2.0" MetaTrader 5 strategy.
/// Combines RSI threshold crosses with an optional moving average trend filter, fixed/percentage risk sizing, and martingale recovery.
/// </summary>
public class RsiExpertTrendFilterStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<MoneyManagementModes> _moneyMode;
	private readonly StrategyParam<decimal> _volumeOrRiskValue;
	private readonly StrategyParam<bool> _useMartingale;
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiLevelUp;
	private readonly StrategyParam<decimal> _rsiLevelDown;
	private readonly StrategyParam<MaTradeModes> _maMode;

	private RelativeStrengthIndex _rsi = null!;
	private SimpleMovingAverage _fastMa = null!;
	private SimpleMovingAverage _slowMa = null!;

	private decimal? _previousRsi;
	private decimal? _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal _pipSize;
	private bool _closeRequested;
	private bool _closeByStop;
	private bool _lastTradeWasLoss;
	private decimal _prevRealizedPnL;

	/// <summary>
	/// Money management modes supported by the strategy.
	/// </summary>
	public enum MoneyManagementModes
	{
		/// <summary>
		/// Use a fixed volume for every trade.
		/// </summary>
		FixedVolume,

		/// <summary>
		/// Calculate volume from risk percent and stop-loss distance.
		/// </summary>
		RiskPercent
	}

	/// <summary>
	/// Moving average filter configuration copied from the original EA.
	/// </summary>
	public enum MaTradeModes
	{
		/// <summary>
		/// Ignore the moving average filter.
		/// </summary>
		Off,

		/// <summary>
		/// Trade in the direction of the fast and slow moving average crossover.
		/// </summary>
		Forward,

		/// <summary>
		/// Trade in the opposite direction of the moving average crossover.
		/// </summary>
		Reverse
	}

	/// <summary>
/// Initializes a new instance of the <see cref="RsiExpertTrendFilterStrategy"/> class.
/// </summary>
public RsiExpertTrendFilterStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for generating signals", "General");

		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Distance of the protective stop in pips", "Risk Management")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Distance of the profit target in pips", "Risk Management")
			;

		_trailingStopPips = Param(nameof(TrailingStopPips), 5)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (pips)", "Trailing distance applied after activation", "Risk Management")
			;

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Additional pips required before trailing moves again", "Risk Management")
			;

		_moneyMode = Param(nameof(MoneyMode), MoneyManagementModes.FixedVolume)
			.SetDisplay("Money Mode", "Choose fixed volume or percent risk sizing", "Money Management");

		_volumeOrRiskValue = Param(nameof(VolumeOrRiskValue), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume / Risk", "Lot size for fixed mode or percent risk when using risk mode", "Money Management")
			;

		_useMartingale = Param(nameof(UseMartingale), true)
			.SetDisplay("Use Martingale", "Double the next volume after a losing trade", "Money Management");

		_fastMaPeriod = Param(nameof(FastMaPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Period of the fast moving average", "Indicators")
			;

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 200)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Period of the slow moving average", "Indicators")
			;

		_rsiPeriod = Param(nameof(RsiPeriod), 21)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Averaging period for RSI", "Indicators")
			;

		_rsiLevelUp = Param(nameof(RsiLevelUp), 70m)
			.SetRange(1m, 99m)
			.SetDisplay("RSI Level Up", "Upper RSI threshold for shorts", "Indicators")
			;

		_rsiLevelDown = Param(nameof(RsiLevelDown), 30m)
			.SetRange(1m, 99m)
			.SetDisplay("RSI Level Down", "Lower RSI threshold for longs", "Indicators")
			;

		_maMode = Param(nameof(MaMode), MaTradeModes.Forward)
			.SetDisplay("MA Trade Mode", "Direction of the moving average confirmation", "Indicators");
	}

	/// <summary>
	/// Candle type that drives indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Extra pips required before the trailing stop is tightened again.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Selected money management approach.
	/// </summary>
	public MoneyManagementModes MoneyMode
	{
		get => _moneyMode.Value;
		set => _moneyMode.Value = value;
	}

	/// <summary>
	/// Lot size in fixed mode or risk percent when money mode equals <see cref="MoneyManagementModes.RiskPercent"/>.
	/// </summary>
	public decimal VolumeOrRiskValue
	{
		get => _volumeOrRiskValue.Value;
		set => _volumeOrRiskValue.Value = value;
	}

	/// <summary>
	/// Enables martingale doubling after losing positions.
	/// </summary>
	public bool UseMartingale
	{
		get => _useMartingale.Value;
		set => _useMartingale.Value = value;
	}

	/// <summary>
	/// Period for the fast moving average.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	/// <summary>
	/// Period for the slow moving average.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

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

	/// <summary>
	/// Upper RSI threshold used to detect short entries.
	/// </summary>
	public decimal RsiLevelUp
	{
		get => _rsiLevelUp.Value;
		set => _rsiLevelUp.Value = value;
	}

	/// <summary>
	/// Lower RSI threshold used to detect long entries.
	/// </summary>
	public decimal RsiLevelDown
	{
		get => _rsiLevelDown.Value;
		set => _rsiLevelDown.Value = value;
	}

	/// <summary>
	/// Moving average confirmation mode.
	/// </summary>
	public MaTradeModes MaMode
	{
		get => _maMode.Value;
		set => _maMode.Value = value;
	}

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

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

		_previousRsi = null;
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_pipSize = 0m;
		_closeRequested = false;
		_closeByStop = false;
		_lastTradeWasLoss = false;
		_prevRealizedPnL = 0m;
	}

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

		if (TrailingStopPips > 0 && TrailingStepPips == 0)
			throw new InvalidOperationException("Trailing is not possible when the trailing step is zero.");

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod
		};

		_fastMa = new SMA
		{
			Length = FastMaPeriod
		};

		_slowMa = new SMA
		{
			Length = SlowMaPeriod
		};

		_pipSize = CalculatePipSize();
		_prevRealizedPnL = PnL;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_rsi, _fastMa, _slowMa, ProcessCandle)
			.Start();

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent));

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

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

		ManageActivePosition(candle);

		var currentRsi = rsiValue;
		var previousRsi = _previousRsi;

		if (Position != 0m)
		{
			_previousRsi = currentRsi;
			return;
		}

		var rsiSignal = 0;
		if (previousRsi.HasValue)
		{
			if (currentRsi > RsiLevelDown && previousRsi.Value < RsiLevelDown)
				rsiSignal = 1;
			else if (currentRsi < RsiLevelUp && previousRsi.Value > RsiLevelUp)
				rsiSignal = -1;
		}

		var maSignal = 0;
		switch (MaMode)
		{
			case MaTradeModes.Forward:
				if (fastValue > slowValue)
					maSignal = 1;
				else if (fastValue < slowValue)
					maSignal = -1;
				break;
			case MaTradeModes.Reverse:
				if (fastValue < slowValue)
					maSignal = 1;
				else if (fastValue > slowValue)
					maSignal = -1;
				break;
		}

		var finalSignal = 0;
		if (rsiSignal == 1 && (MaMode == MaTradeModes.Off || maSignal == 1))
			finalSignal = 1;
		else if (rsiSignal == -1 && (MaMode == MaTradeModes.Off || maSignal == -1))
			finalSignal = -1;

		if (finalSignal > 0)
		{
			BuyMarket();
		}
		else if (finalSignal < 0)
		{
			SellMarket();
		}

		_previousRsi = currentRsi;
	}

	private void ManageActivePosition(ICandleMessage candle)
	{
		if (Position == 0m)
			return;

		if (_entryPrice == null)
			return;

		var isLong = Position > 0m;
		var absPosition = Math.Abs(Position);
		var stepDistance = TrailingStepPips > 0 ? TrailingStepPips * _pipSize : 0m;
		var trailingDistance = TrailingStopPips > 0 ? TrailingStopPips * _pipSize : 0m;

		if (TrailingStopPips > 0 && trailingDistance > 0m)
		{
			if (isLong)
			{
				var profitDistance = candle.ClosePrice - _entryPrice.Value;
				if (profitDistance > trailingDistance + stepDistance)
				{
					var candidate = candle.ClosePrice - trailingDistance;
					if (!_stopLossPrice.HasValue || _stopLossPrice.Value < candidate)
						_stopLossPrice = candidate;
				}
			}
			else
			{
				var profitDistance = _entryPrice.Value - candle.ClosePrice;
				if (profitDistance > trailingDistance + stepDistance)
				{
					var candidate = candle.ClosePrice + trailingDistance;
					if (!_stopLossPrice.HasValue || _stopLossPrice.Value > candidate)
						_stopLossPrice = candidate;
				}
			}
		}

		if (!_closeRequested && _takeProfitPrice.HasValue)
		{
			if (isLong && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Position);
				_closeRequested = true;
				_closeByStop = false;
				return;
			}

			if (!isLong && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(absPosition);
				_closeRequested = true;
				_closeByStop = false;
				return;
			}
		}

		if (!_closeRequested && _stopLossPrice.HasValue)
		{
			if (isLong && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(Position);
				_closeRequested = true;
				_closeByStop = true;
				return;
			}

			if (!isLong && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(absPosition);
				_closeRequested = true;
				_closeByStop = true;
			}
		}
	}

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

		if (Position == 0m)
		{
			if (_closeRequested)
			{
				_lastTradeWasLoss = _closeByStop;
				_closeRequested = false;
				_closeByStop = false;
				_prevRealizedPnL = PnL;
			}
			else
			{
				var realizedPnL = PnL;
				if (realizedPnL > _prevRealizedPnL)
					_lastTradeWasLoss = false;
				else if (realizedPnL < _prevRealizedPnL)
					_lastTradeWasLoss = true;

				_prevRealizedPnL = realizedPnL;
			}

			ResetPositionState();
		}
		else
		{
			_entryPrice ??= (_entryPrice ?? 0m);

			InitializePositionState(Position > 0m, (_entryPrice ?? 0m));
			_closeRequested = false;
			_closeByStop = false;
		}
	}

	private decimal GetOrderVolume()
	{
		var volume = MoneyMode == MoneyManagementModes.FixedVolume
			? VolumeOrRiskValue
			: CalculateRiskVolume();

		if (UseMartingale && _lastTradeWasLoss)
			volume *= 2m;

		return NormalizeVolume(volume);
	}

	private decimal CalculateRiskVolume()
	{
		if (StopLossPips <= 0)
			return VolumeOrRiskValue;

		var equity = Portfolio?.CurrentValue ?? 0m;
		if (equity <= 0m)
			return VolumeOrRiskValue;

		var riskAmount = equity * VolumeOrRiskValue / 100m;
		var stopDistance = StopLossPips * _pipSize;

		return stopDistance > 0m ? riskAmount / stopDistance : VolumeOrRiskValue;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var minVolume = Security?.MinVolume ?? 0m;
		if (minVolume > 0m && volume < minVolume)
			volume = minVolume;

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			var steps = Math.Max(1m, Math.Round(volume / step, MidpointRounding.AwayFromZero));
			volume = steps * step;
		}

		return volume > 0m ? volume : 0m;
	}

	private void InitializePositionState(bool isLong, decimal entryPrice)
	{
		_entryPrice = entryPrice;

		_stopLossPrice = StopLossPips > 0
			? entryPrice + (isLong ? -1m : 1m) * StopLossPips * _pipSize
			: null;

		_takeProfitPrice = TakeProfitPips > 0
			? entryPrice + (isLong ? 1m : -1m) * TakeProfitPips * _pipSize
			: null;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step > 0m)
			return step;

		var decimals = Security?.Decimals ?? 4;
		decimal pip = 1m;
		for (var i = 0; i < decimals; i++)
			pip /= 10m;

		return pip;
	}

	private bool HasActiveOrders()
	{
		return Orders.Any(o => o.State == OrderStates.Active || o.State == OrderStates.Pending);
	}
}