GitHub で見る

Color JJRSX Time Plus Strategy

Converted from the MetaTrader5 expert Exp_ColorJJRSX_Tm_Plus. The strategy trades trend reversals detected with a Jurik-smoothed RSI oscillator and includes optional time-based exits, mimicking the original money-management toggles.

Overview

  • Idea: Track the slope of the Color JJRSX oscillator (approximated via RSI smoothed by a Jurik Moving Average). When the oscillator turns up the system can close shorts and optionally open longs, and vice versa for downturns.
  • Market: Single instrument defined by the connected Security.
  • Timeframe: Configurable; default is 4-hour candles (matching the original EA input).
  • Direction: Long and short. Each direction can be disabled independently.
  • Order Type: Market orders through BuyMarket() / SellMarket().

Indicator Stack

  1. Relative Strength Index (RSI) — base momentum oscillator using the RSI Length parameter (mirrors JurXPeriod).
  2. Jurik Moving Average (JMA) — smooths the RSI output with Smoothing Length (mirrors JMAPeriod). The JMA phase parameter of the MQL version is not exposed by StockSharp and is therefore omitted.
  3. Signal Shift — reproduces the SignalBar parameter. Signals are generated from the value Signal Shift bars back and the two preceding values to detect slope changes.

Trading Logic

Long Management

  • Entry: Enabled by Enable Long Entries. Requires that the smoothed oscillator was declining two bars ago (previous > older is false), turned upward on the last completed bar (previous < older), and continues higher on the current bar (current > previous). Position must be flat or short.
  • Exit: If Exit Long on Downturn is enabled and the oscillator slopes down (previous > older), any open long is closed.

Short Management

  • Entry: Enabled by Enable Short Entries. Requires the oscillator to turn down (previous > older) and continue falling on the current bar (current < previous) while the strategy is flat or long.
  • Exit: If Exit Short on Upturn is enabled and the oscillator slopes up (previous < older), any open short is covered.

Time Filter

  • Enable Time Exit closes positions once their holding time exceeds Holding Minutes. This mirrors the original expert's timer that liquidates positions after nTime minutes.

Risk Controls

  • Stop Loss (pts) and Take Profit (pts) are converted into StockSharp protective levels via StartProtection using UnitTypes.PriceStep.

Parameters

Parameter Description Default
Indicator Timeframe Candle type for the indicator calculations. 4-hour candles
RSI Length Period for the RSI (analogous to JurX period). 8
Smoothing Length Length of the Jurik MA smoothing (analogous to JMA period). 3
Signal Shift Number of completed bars to skip before checking slopes (SignalBar). 1
Enable Long Entries / Enable Short Entries Allow opening trades in each direction. true
Exit Long on Downturn / Exit Short on Upturn Allow oscillator-driven exits for existing positions. true
Enable Time Exit Activate the holding-time based liquidation. true
Holding Minutes Maximum minutes to keep a position open. 240
Stop Loss (pts) Distance of the protective stop in price steps. 1000
Take Profit (pts) Distance of the profit target in price steps. 2000

Notes on Conversion

  • The JJRSX histogram buffer from the original indicator is emulated with RSI + Jurik smoothing. Only slope information is used, so the numerical scale differences do not affect decisions.
  • Money-management options (MM, MMMode, Deviation) are not ported. StockSharp order sizing should be handled through the Strategy.Volume property or external portfolio settings.
  • Global variables used in MQL to rate-limit orders are unnecessary here because the strategy reacts only to finished candles.
  • All comments and documentation are in English per repository guidelines.
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;
	}
}