Ver no GitHub

Estratégia ColorJjrsxTimePlus

Convertida do especialista MetaTrader5 Exp_ColorJJRSX_Tm_Plus. A estratégia opera reversões de tendência detectadas com um oscilador RSI suavizado por Jurik e inclui saídas opcionais baseadas em tempo, imitando os toggles de gestão monetária originais.

Visão geral

  • Ideia: Rastrear a inclinação do oscilador Color JJRSX (aproximado via RSI suavizado por uma Média Móvel Jurik). Quando o oscilador sobe, o sistema pode fechar vendidos e opcionalmente abrir comprados, e vice-versa para quedas.
  • Mercado: Instrumento único definido pelo Security conectado.
  • Período: Configurável; padrão são velas de 4 horas (correspondendo à entrada EA original).
  • Direção: Comprado e vendido. Cada direção pode ser desabilitada independentemente.
  • Tipo de ordem: Ordens a mercado via BuyMarket() / SellMarket().

Pilha de indicadores

  1. Relative Strength Index (RSI) — oscilador de momentum base usando o parâmetro RSI Length (reflete JurXPeriod).
  2. Jurik Moving Average (JMA) — suaviza a saída do RSI com Smoothing Length (reflete JMAPeriod). O parâmetro de fase JMA da versão MQL não está exposto pelo StockSharp e portanto é omitido.
  3. Signal Shift — reproduz o parâmetro SignalBar. Os sinais são gerados a partir do valor Signal Shift barras atrás e dos dois valores precedentes para detectar mudanças de inclinação.

Lógica de trading

Gestão de comprados

  • Entrada: Habilitada por Enable Long Entries. Requer que o oscilador suavizado estivesse declinando duas barras atrás (previous > older é falso), girou para cima na última barra completa (previous < older), e continua mais alto na barra atual (current > previous). A posição deve estar plana ou vendida.
  • Saída: Se Exit Long on Downturn estiver habilitado e o oscilador inclinar para baixo (previous > older), qualquer comprado aberto é fechado.

Gestão de vendidos

  • Entrada: Habilitada por Enable Short Entries. Requer que o oscilador gire para baixo (previous > older) e continue caindo na barra atual (current < previous) enquanto a estratégia está plana ou comprada.
  • Saída: Se Exit Short on Upturn estiver habilitado e o oscilador inclinar para cima (previous < older), qualquer vendido aberto é coberto.

Filtro de tempo

  • Enable Time Exit fecha posições assim que seu tempo de manutenção excede Holding Minutes. Isso reflete o temporizador do especialista original que liquida posições após nTime minutos.

Controles de risco

  • Stop Loss (pts) e Take Profit (pts) são convertidos em níveis de proteção do StockSharp via StartProtection usando UnitTypes.PriceStep.

Parâmetros

Parâmetro Descrição Padrão
Indicator Timeframe Tipo de vela para os cálculos do indicador. Velas de 4 horas
RSI Length Período para o RSI (análogo ao período JurX). 8
Smoothing Length Comprimento do suavizado Jurik MA (análogo ao período JMA). 3
Signal Shift Número de barras completadas a pular antes de verificar inclinações (SignalBar). 1
Enable Long Entries / Enable Short Entries Permitir abrir operações em cada direção. true
Exit Long on Downturn / Exit Short on Upturn Permitir saídas impulsionadas pelo oscilador para posições existentes. true
Enable Time Exit Ativar a liquidação baseada em tempo de manutenção. true
Holding Minutes Minutos máximos para manter uma posição aberta. 240
Stop Loss (pts) Distância do stop de proteção em passos de preço. 1000
Take Profit (pts) Distância do alvo de lucro em passos de preço. 2000

Notas sobre a conversão

  • O buffer do histograma JJRSX do indicador original é emulado com RSI + suavização Jurik. Apenas informações de inclinação são usadas, portanto as diferenças de escala numérica não afetam as decisões.
  • As opções de gestão monetária (MM, MMMode, Deviation) não estão portadas. O dimensionamento de ordens no StockSharp deve ser tratado através da propriedade Strategy.Volume ou configurações de portfólio externas.
  • As variáveis globais usadas no MQL para limitar a taxa de ordens são desnecessárias aqui porque a estratégia reage apenas a velas finalizadas.
  • Todos os comentários e documentação estão em inglês conforme as diretrizes do repositório.
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;
	}
}