Ver en GitHub

Estrategia SAR RSI MTS

Descripción general

La Estrategia SAR RSI MTS es una traducción directa del asesor experto original de MetaTrader 5 "SAR RSI MTS" a la API de alto nivel de StockSharp. El sistema sigue la dirección del indicador Parabolic SAR y confirma las entradas con el Índice de Fuerza Relativa (RSI). Trabaja únicamente sobre velas completadas (marco temporal predeterminado de 1 hora) y respeta un límite configurable sobre el tamaño neto de la posición.

Indicadores y datos

  • Parabolic SAR (Acceleration = SarStep, AccelerationStep = SarStep, AccelerationMax = SarMax).
  • Índice de Fuerza Relativa con período personalizable y nivel neutro (por defecto 50).
  • Velas suministradas por CandleType, que por defecto es datos de marco temporal horario.

Internamente la estrategia calcula un valor de pip a partir de los metadatos del instrumento. Si el símbolo tiene 3 o 5 decimales, multiplica el paso de precio por 10, lo que coincide con el manejo de pips del programa MQL original.

Lógica de entrada

Una nueva operación se evalúa al cierre de cada vela terminada una vez que ambos indicadores han producido valores válidos:

  • Configuración larga

    1. El valor del Parabolic SAR de la barra anterior está por debajo del cierre actual y el SAR actual ha aumentado respecto al valor anterior.
    2. El RSI está por encima del umbral neutro y está subiendo en comparación con su lectura anterior.
    3. Si la cuenta ya está neta corta, la estrategia primero compra suficiente volumen para voltear la posición y luego abre un nuevo largo dimensionado según el parámetro Volume, respetando el límite MaxPosition.
  • Configuración corta

    1. El valor anterior del Parabolic SAR está por encima del cierre actual y el SAR actual ha disminuido.
    2. El RSI está por debajo del umbral neutro y está cayendo en comparación con su valor anterior.
    3. La exposición larga existente se elimina antes de establecer el nuevo corto. Se permiten cortos adicionales hasta que la posición absoluta alcance MaxPosition.

Todas las comparaciones utilizan la precisión del instrumento para que las pruebas de igualdad coincidan con el helper CompareDoubles original de MQL.

Salida y gestión de riesgos

Los controles de riesgo se evalúan antes de verificar nuevas entradas en cada vela terminada:

  • Stop-loss fijo en pips convertidos a unidades de precio y aplicado al precio promedio de entrada de la posición neta actual.
  • Take-profit fijo en pips, manejado simétricamente al stop-loss.
  • Stop trailing que se activa solo después de que el beneficio no realizado supera TrailingStop + TrailingStep. El stop se mueve en pasos discretos, imitando la rutina "Trailing" de la estrategia MQL.
  • Si ninguno de los anteriores aplica, el estado del trailing se reinicia cada vez que la posición se aplana.

Todas las salidas cierran la posición neta completa (larga o corta). Cuando se activa una regla de protección, la estrategia omite la evaluación de señales para la misma barra, reflejando el comportamiento de las órdenes stop del lado del broker en la implementación original.

Parámetros

Parámetro Descripción
StopLossPips Distancia del stop-loss expresada en pips. Un valor de 0 deshabilita el stop de protección.
TakeProfitPips Distancia del take-profit en pips. Deshabilitado cuando se establece en 0.
TrailingStopPips Distancia del trailing stop. Deshabilitado cuando se establece en 0.
TrailingStepPips Mejora mínima de precio requerida antes de avanzar el trailing stop.
SarStep Paso de aceleración para Parabolic SAR; también se usa como factor de aceleración inicial.
SarMax Factor de aceleración máximo para Parabolic SAR.
RsiPeriod Período de lookback para el indicador RSI.
RsiNeutralLevel Umbral RSI que separa el sesgo alcista y bajista (por defecto 50).
CandleType Suscripción de velas usada para los cálculos (por defecto 1 hora).
MaxPosition Máxima posición neta absoluta permitida por la estrategia.

Notas adicionales

  • La configuración predeterminada reproduce las entradas originales del EA: stop de 10 pips, objetivo de 40 pips, trailing stop de 15/5 pips, Parabolic SAR 0.05/0.5 y período RSI 14.
  • El volumen se controla mediante la propiedad base Strategy.Volume. El escalado de posición respeta MaxPosition y gestiona automáticamente las reversiones.
  • Los enlaces de indicadores y el enrutamiento de órdenes se basan íntegramente en la API de alto nivel de StockSharp sin acceso manual a series, garantizando el cumplimiento de las directrices del proyecto.
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>
/// Parabolic SAR and RSI strategy translated from the original MQL implementation.
/// </summary>
public class SarRsiMtsStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _sarStep;
	private readonly StrategyParam<decimal> _sarMax;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiNeutralLevel;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _maxPosition;

	private decimal? _previousSar;
	private decimal? _previousRsi;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private decimal _pipSize;
	private decimal _entryPrice;
	private DateTimeOffset _lastTradeTime;

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

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

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

	/// <summary>
	/// Trailing step distance expressed in pips.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Parabolic SAR acceleration step.
	/// </summary>
	public decimal SarStep
	{
		get => _sarStep.Value;
		set => _sarStep.Value = value;
	}

	/// <summary>
	/// Parabolic SAR maximum acceleration.
	/// </summary>
	public decimal SarMax
	{
		get => _sarMax.Value;
		set => _sarMax.Value = value;
	}

	/// <summary>
	/// RSI lookback period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// RSI neutral level used for bullish or bearish confirmation.
	/// </summary>
	public decimal RsiNeutralLevel
	{
		get => _rsiNeutralLevel.Value;
		set => _rsiNeutralLevel.Value = value;
	}

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

	/// <summary>
	/// Maximum absolute net position allowed by the strategy.
	/// </summary>
	public decimal MaxPosition
	{
		get => _maxPosition.Value;
		set => _maxPosition.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="SarRsiMtsStrategy"/> class.
	/// </summary>
	public SarRsiMtsStrategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 10m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 40m)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 15m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Trailing step distance in pips", "Risk");

		_sarStep = Param(nameof(SarStep), 0.05m)
			.SetGreaterThanZero()
			.SetDisplay("SAR Step", "Parabolic SAR acceleration step", "Indicators");

		_sarMax = Param(nameof(SarMax), 0.5m)
			.SetGreaterThanZero()
			.SetDisplay("SAR Maximum", "Parabolic SAR maximum acceleration", "Indicators");

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

		_rsiNeutralLevel = Param(nameof(RsiNeutralLevel), 50m)
			.SetDisplay("RSI Neutral", "Neutral RSI threshold separating bullish and bearish bias", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle type for indicator calculations", "General");

		_maxPosition = Param(nameof(MaxPosition), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Max Position", "Maximum absolute net position allowed", "Risk");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_previousSar = null;
		_previousRsi = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_pipSize = 0;
		_entryPrice = 0;
		_lastTradeTime = default;
	}

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

		_pipSize = CalculatePipSize();

		var parabolicSar = new ParabolicSar
		{
			Acceleration = SarStep,
			AccelerationStep = SarStep,
			AccelerationMax = SarMax
		};

		var rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(parabolicSar, rsi, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, parabolicSar);
			DrawIndicator(area, rsi);
			DrawOwnTrades(area);
		}
	}

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

		if (ManageRisk(candle))
			return;

		if (sarValue == 0m || rsiValue == 0m)
			return;

		if (!_previousSar.HasValue || !_previousRsi.HasValue)
		{
			_previousSar = sarValue;
			_previousRsi = rsiValue;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_previousSar = sarValue;
			_previousRsi = rsiValue;
			return;
		}

		// Cooldown: skip if a trade was placed within the last ~240 candles (5-min candles = ~1200 min)
		if (_lastTradeTime != default && (candle.OpenTime - _lastTradeTime) < TimeSpan.FromMinutes(1200))
		{
			_previousSar = sarValue;
			_previousRsi = rsiValue;
			return;
		}

		var sarPrev = _previousSar.Value;
		var rsiPrev = _previousRsi.Value;

		var price = candle.ClosePrice;
		var buySignal = sarPrev < price
			&& !AreClose(sarPrev, price)
			&& sarValue > sarPrev
			&& rsiValue > RsiNeutralLevel
			&& rsiValue > rsiPrev
			&& !AreClose(rsiValue, rsiPrev);

		if (buySignal)
		{
			EnterLong(candle);
		}
		else
		{
			var sellSignal = sarPrev > price
				&& !AreClose(sarPrev, price)
				&& sarValue < sarPrev
				&& rsiValue < RsiNeutralLevel
				&& rsiValue < rsiPrev
				&& !AreClose(rsiValue, rsiPrev);

			if (sellSignal)
				EnterShort(candle);
		}

		_previousSar = sarValue;
		_previousRsi = rsiValue;
	}

	private void EnterLong(ICandleMessage candle)
	{
		var tradeVolume = Volume;
		if (tradeVolume <= 0m)
			return;

		var maxPosition = MaxPosition;
		if (maxPosition <= 0m)
			return;

		var current = Position;
		var target = current < 0 ? Math.Min(maxPosition, tradeVolume) : Math.Min(maxPosition, current + tradeVolume);
		var required = target - current;
		if (required <= 0m)
			return;

		BuyMarket(required);
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_lastTradeTime = candle.OpenTime;
	}

	private void EnterShort(ICandleMessage candle)
	{
		var tradeVolume = Volume;
		if (tradeVolume <= 0m)
			return;

		var maxPosition = MaxPosition;
		if (maxPosition <= 0m)
			return;

		var current = Position;
		var target = current > 0 ? -Math.Min(maxPosition, tradeVolume) : Math.Max(-maxPosition, current - tradeVolume);
		var required = current - target;
		if (required <= 0m)
			return;

		SellMarket(required);
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_lastTradeTime = candle.OpenTime;
	}

	private bool ManageRisk(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			var entryPrice = _entryPrice;
			if (entryPrice <= 0m)
				return false;

			var trailingTriggered = UpdateLongTrailing(candle, entryPrice);
			if (trailingTriggered)
				return true;

			var stopDistance = GetPriceOffset(StopLossPips);
			if (stopDistance > 0m)
			{
				var stopPrice = entryPrice - stopDistance;
				if (candle.LowPrice <= stopPrice)
				{
					SellMarket(Position);
					ResetTrailing();
					return true;
				}
			}

			var takeDistance = GetPriceOffset(TakeProfitPips);
			if (takeDistance > 0m)
			{
				var takePrice = entryPrice + takeDistance;
				if (candle.HighPrice >= takePrice)
				{
					SellMarket(Position);
					ResetTrailing();
					return true;
				}
			}
		}
		else if (Position < 0m)
		{
			var entryPrice = _entryPrice;
			if (entryPrice <= 0m)
				return false;

			var trailingTriggered = UpdateShortTrailing(candle, entryPrice);
			if (trailingTriggered)
				return true;

			var stopDistance = GetPriceOffset(StopLossPips);
			if (stopDistance > 0m)
			{
				var stopPrice = entryPrice + stopDistance;
				if (candle.HighPrice >= stopPrice)
				{
					BuyMarket(Math.Abs(Position));
					ResetTrailing();
					return true;
				}
			}

			var takeDistance = GetPriceOffset(TakeProfitPips);
			if (takeDistance > 0m)
			{
				var takePrice = entryPrice - takeDistance;
				if (candle.LowPrice <= takePrice)
				{
					BuyMarket(Math.Abs(Position));
					ResetTrailing();
					return true;
				}
			}
		}
		else
		{
			ResetTrailing();
		}

		return false;
	}

	private bool UpdateLongTrailing(ICandleMessage candle, decimal entryPrice)
	{
		var trailingDistance = GetPriceOffset(TrailingStopPips);
		if (trailingDistance <= 0m)
		{
			_longTrailingStop = null;
			return false;
		}

		var trailingStep = GetPriceOffset(TrailingStepPips);
		var profit = candle.ClosePrice - entryPrice;
		if (profit >= trailingDistance + trailingStep)
		{
			var candidate = candle.ClosePrice - trailingDistance;
			var threshold = candle.ClosePrice - (trailingDistance + trailingStep);
			if (!_longTrailingStop.HasValue || _longTrailingStop.Value < threshold)
				_longTrailingStop = candidate;
		}

		if (_longTrailingStop.HasValue && candle.LowPrice <= _longTrailingStop.Value)
		{
			SellMarket(Position);
			ResetTrailing();
			return true;
		}

		return false;
	}

	private bool UpdateShortTrailing(ICandleMessage candle, decimal entryPrice)
	{
		var trailingDistance = GetPriceOffset(TrailingStopPips);
		if (trailingDistance <= 0m)
		{
			_shortTrailingStop = null;
			return false;
		}

		var trailingStep = GetPriceOffset(TrailingStepPips);
		var profit = entryPrice - candle.ClosePrice;
		if (profit >= trailingDistance + trailingStep)
		{
			var candidate = candle.ClosePrice + trailingDistance;
			var threshold = candle.ClosePrice + (trailingDistance + trailingStep);
			if (!_shortTrailingStop.HasValue || _shortTrailingStop.Value > threshold)
				_shortTrailingStop = candidate;
		}

		if (_shortTrailingStop.HasValue && candle.HighPrice >= _shortTrailingStop.Value)
		{
			BuyMarket(Math.Abs(Position));
			ResetTrailing();
			return true;
		}

		return false;
	}

	private decimal GetPriceOffset(decimal pips)
	{
		if (pips <= 0m)
			return 0m;

		var pip = _pipSize;
		if (pip <= 0m)
			pip = Security?.PriceStep ?? 1m;

		return pip * pips;
	}

	private decimal CalculatePipSize()
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			priceStep = 1m;

		var decimals = Security?.Decimals;
		var adjust = decimals == 3 || decimals == 5 ? 10m : 1m;

		return priceStep * adjust;
	}

	private void ResetTrailing()
	{
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Trade == null) return;
		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;
		if (Position == 0)
			_entryPrice = 0m;
	}

	private bool AreClose(decimal value1, decimal value2)
	{
		var decimals = Security?.Decimals ?? 4;
		return Math.Round(value1 - value2, decimals) == 0m;
	}
}