Ver en GitHub

Estrategia ColorJjrsxTimePlus

Convertida del experto MetaTrader5 Exp_ColorJJRSX_Tm_Plus. La estrategia opera reversiones de tendencia detectadas con un oscilador RSI suavizado por Jurik e incluye salidas opcionales basadas en tiempo, imitando los toggles de gestión monetaria originales.

Descripción general

  • Idea: Rastrear la pendiente del oscilador Color JJRSX (aproximado mediante RSI suavizado por una Media Móvil Jurik). Cuando el oscilador sube, el sistema puede cerrar cortos y opcionalmente abrir largos, y viceversa para las caídas.
  • Mercado: Instrumento único definido por el Security conectado.
  • Marco temporal: Configurable; por defecto velas de 4 horas (coincidiendo con la entrada EA original).
  • Dirección: Largo y corto. Cada dirección puede deshabilitarse independientemente.
  • Tipo de orden: Órdenes de mercado mediante BuyMarket() / SellMarket().

Pila de indicadores

  1. Relative Strength Index (RSI) — oscilador de momentum base usando el parámetro RSI Length (refleja JurXPeriod).
  2. Jurik Moving Average (JMA) — suaviza la salida del RSI con Smoothing Length (refleja JMAPeriod). El parámetro de fase JMA de la versión MQL no está expuesto por StockSharp y por lo tanto se omite.
  3. Signal Shift — reproduce el parámetro SignalBar. Las señales se generan a partir del valor Signal Shift barras atrás y los dos valores anteriores para detectar cambios de pendiente.

Lógica de trading

Gestión de largos

  • Entrada: Habilitada por Enable Long Entries. Requiere que el oscilador suavizado estuviera declinando dos barras atrás (previous > older es falso), giró hacia arriba en la última barra completada (previous < older), y continúa más alto en la barra actual (current > previous). La posición debe estar plana o corta.
  • Salida: Si Exit Long on Downturn está habilitado y el oscilador se inclina hacia abajo (previous > older), cualquier largo abierto se cierra.

Gestión de cortos

  • Entrada: Habilitada por Enable Short Entries. Requiere que el oscilador gire hacia abajo (previous > older) y continúe cayendo en la barra actual (current < previous) mientras la estrategia está plana o larga.
  • Salida: Si Exit Short on Upturn está habilitado y el oscilador se inclina hacia arriba (previous < older), cualquier corto abierto se cubre.

Filtro de tiempo

  • Enable Time Exit cierra posiciones una vez que su tiempo de tenencia supera Holding Minutes. Esto refleja el temporizador del experto original que liquida posiciones después de nTime minutos.

Controles de riesgo

  • Stop Loss (pts) y Take Profit (pts) se convierten en niveles de protección de StockSharp mediante StartProtection usando UnitTypes.PriceStep.

Parámetros

Parámetro Descripción Predeterminado
Indicator Timeframe Tipo de vela para los cálculos del indicador. Velas de 4 horas
RSI Length Período para el RSI (análogo al período JurX). 8
Smoothing Length Longitud del suavizado Jurik MA (análogo al período JMA). 3
Signal Shift Número de barras completadas a saltar antes de verificar pendientes (SignalBar). 1
Enable Long Entries / Enable Short Entries Permitir abrir operaciones en cada dirección. true
Exit Long on Downturn / Exit Short on Upturn Permitir salidas impulsadas por el oscilador para posiciones existentes. true
Enable Time Exit Activar la liquidación basada en tiempo de tenencia. true
Holding Minutes Minutos máximos para mantener una posición abierta. 240
Stop Loss (pts) Distancia del stop de protección en pasos de precio. 1000
Take Profit (pts) Distancia del objetivo de ganancia en pasos de precio. 2000

Notas sobre la conversión

  • El buffer del histograma JJRSX del indicador original se emula con RSI + suavizado Jurik. Solo se usa información de pendiente, por lo que las diferencias de escala numérica no afectan las decisiones.
  • Las opciones de gestión monetaria (MM, MMMode, Deviation) no están portadas. El dimensionamiento de órdenes en StockSharp debe manejarse a través de la propiedad Strategy.Volume o configuraciones de cartera externas.
  • Las variables globales usadas en MQL para limitar la tasa de órdenes son innecesarias aquí porque la estrategia solo reacciona a velas finalizadas.
  • Todos los comentarios y documentación están en inglés según las directrices del repositorio.
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>
/// Trend-following strategy inspired by the Color JJRSX TM Plus Expert Advisor.
/// Uses a smoothed RSI oscillator to detect slope reversals and optional time-based exits.
/// </summary>
public class ColorJjrsxTimePlusStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<int> _smoothingLength;
	private readonly StrategyParam<int> _signalShift;
	private readonly StrategyParam<bool> _enableBuyEntries;
	private readonly StrategyParam<bool> _enableSellEntries;
	private readonly StrategyParam<bool> _enableBuyExit;
	private readonly StrategyParam<bool> _enableSellExit;
	private readonly StrategyParam<bool> _enableTimeExit;
	private readonly StrategyParam<int> _holdingMinutes;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private readonly Queue<decimal> _smoothedValues = new();

	private RelativeStrengthIndex _rsi;
	private JurikMovingAverage _smoother;
	private DateTimeOffset? _entryTime;

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

	/// <summary>
	/// RSI length before Jurik smoothing.
	/// </summary>
	public int RsiLength
	{
		get => _rsiLength.Value;
		set => _rsiLength.Value = value;
	}

	/// <summary>
	/// Length of the Jurik moving average.
	/// </summary>
	public int SmoothingLength
	{
		get => _smoothingLength.Value;
		set => _smoothingLength.Value = value;
	}

	/// <summary>
	/// Number of completed candles to shift before calculating signals.
	/// </summary>
	public int SignalShift
	{
		get => _signalShift.Value;
		set => _signalShift.Value = value;
	}

	/// <summary>
	/// Enable or disable long entries.
	/// </summary>
	public bool EnableBuyEntries
	{
		get => _enableBuyEntries.Value;
		set => _enableBuyEntries.Value = value;
	}

	/// <summary>
	/// Enable or disable short entries.
	/// </summary>
	public bool EnableSellEntries
	{
		get => _enableSellEntries.Value;
		set => _enableSellEntries.Value = value;
	}

	/// <summary>
	/// Allow closing long positions on oscillator downturns.
	/// </summary>
	public bool EnableBuyExit
	{
		get => _enableBuyExit.Value;
		set => _enableBuyExit.Value = value;
	}

	/// <summary>
	/// Allow closing short positions on oscillator upturns.
	/// </summary>
	public bool EnableSellExit
	{
		get => _enableSellExit.Value;
		set => _enableSellExit.Value = value;
	}

	/// <summary>
	/// Enable the maximum holding time exit.
	/// </summary>
	public bool EnableTimeExit
	{
		get => _enableTimeExit.Value;
		set => _enableTimeExit.Value = value;
	}

	/// <summary>
	/// Maximum minutes to keep an open position.
	/// </summary>
	public int HoldingMinutes
	{
		get => _holdingMinutes.Value;
		set => _holdingMinutes.Value = value;
	}

	/// <summary>
	/// Stop loss distance in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="ColorJjrsxTimePlusStrategy"/>.
	/// </summary>
	public ColorJjrsxTimePlusStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Indicator Timeframe", "Timeframe used for the JJRSX oscillator", "General");

		_rsiLength = Param(nameof(RsiLength), 14)
			.SetGreaterThanZero()
			.SetDisplay("RSI Length", "Period for the RSI calculation", "Indicator")
			
			.SetOptimize(4, 20, 1);

		_smoothingLength = Param(nameof(SmoothingLength), 3)
			.SetGreaterThanZero()
			.SetDisplay("Smoothing Length", "Jurik moving average length", "Indicator")
			
			.SetOptimize(1, 10, 1);

		_signalShift = Param(nameof(SignalShift), 1)
			.SetDisplay("Signal Shift", "Completed candles to skip before evaluating signals", "Indicator");

		_enableBuyEntries = Param(nameof(EnableBuyEntries), true)
			.SetDisplay("Enable Long Entries", "Allow opening long positions", "Execution");

		_enableSellEntries = Param(nameof(EnableSellEntries), true)
			.SetDisplay("Enable Short Entries", "Allow opening short positions", "Execution");

		_enableBuyExit = Param(nameof(EnableBuyExit), true)
			.SetDisplay("Exit Long on Downturn", "Close longs when the oscillator turns down", "Execution");

		_enableSellExit = Param(nameof(EnableSellExit), true)
			.SetDisplay("Exit Short on Upturn", "Close shorts when the oscillator turns up", "Execution");

		_enableTimeExit = Param(nameof(EnableTimeExit), true)
			.SetDisplay("Enable Time Exit", "Close positions after the holding period expires", "Risk");

		_holdingMinutes = Param(nameof(HoldingMinutes), 480)
			.SetGreaterThanZero()
			.SetDisplay("Holding Minutes", "Maximum time in minutes to keep a position", "Risk")
			
			.SetOptimize(60, 720, 60);

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
			.SetDisplay("Stop Loss (pts)", "Stop loss distance expressed in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
			.SetDisplay("Take Profit (pts)", "Take profit distance expressed in price steps", "Risk");
	}

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

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

		_smoothedValues.Clear();
		_entryTime = null;
	}

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

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiLength
		};

		_smoother = new JurikMovingAverage
		{
			Length = SmoothingLength
		};

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

		var priceStep = Security?.PriceStep ?? 0.01m;
		StartProtection(
			stopLoss: StopLossPoints > 0 ? new Unit(StopLossPoints * priceStep, UnitTypes.Absolute) : null,
			takeProfit: TakeProfitPoints > 0 ? new Unit(TakeProfitPoints * priceStep, UnitTypes.Absolute) : null);
	}

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

		if (_smoother is null)
			return;

		HandleTimeExit(candle.CloseTime);

		var smoothValue = _smoother.Process(new DecimalIndicatorValue(_smoother, rsiValue, candle.CloseTime) { IsFinal = true });

		if (!_smoother.IsFormed || smoothValue is not DecimalIndicatorValue smoothDecimal)
			return;

		_smoothedValues.Enqueue(smoothDecimal.Value);

		var required = SignalShift + 3;

		if (_smoothedValues.Count < required)
			return;

		while (_smoothedValues.Count > required)
		{
			_smoothedValues.Dequeue();
		}

		var values = _smoothedValues.ToArray();

		var currentIndex = values.Length - SignalShift - 1;
		var previousIndex = values.Length - SignalShift - 2;
		var olderIndex = values.Length - SignalShift - 3;

		if (currentIndex < 0 || previousIndex < 0 || olderIndex < 0)
			return;

		var current = values[currentIndex];
		var previous = values[previousIndex];
		var older = values[olderIndex];

		var slopeUp = previous < older;
		var slopeDown = previous > older;

		if (EnableSellExit && slopeUp && Position < 0)
		{
			BuyMarket();
			_entryTime = null;
		}

		if (EnableBuyExit && slopeDown && Position > 0)
		{
			SellMarket();
			_entryTime = null;
		}

		if (EnableBuyEntries && slopeUp && current > previous && Position <= 0)
		{
			BuyMarket();
			_entryTime = candle.CloseTime;
		}
		else if (EnableSellEntries && slopeDown && current < previous && Position >= 0)
		{
			SellMarket();
			_entryTime = candle.CloseTime;
		}
	}

	private void HandleTimeExit(DateTimeOffset candleTime)
	{
		if (!EnableTimeExit || Position == 0 || _entryTime is null)
			return;

		var minutesInPosition = (candleTime - _entryTime.Value).TotalMinutes;

		if (minutesInPosition < HoldingMinutes)
			return;

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

		_entryTime = null;
	}
}