Auf GitHub ansehen

ColorJjrsxTimePlus-Strategie

Konvertiert aus dem MetaTrader5-Experten Exp_ColorJJRSX_Tm_Plus. Die Strategie handelt Trendumkehrungen, die mit einem Jurik-geglätteten RSI-Oszillator erkannt werden, und enthält optionale zeitbasierte Exits, die die ursprünglichen Geldverwaltungs-Toggles imitieren.

Übersicht

  • Idee: Den Steigungsverlauf des Color-JJRSX-Oszillators verfolgen (über RSI geglättet mit einem Jurik Moving Average approximiert). Wenn der Oszillator nach oben dreht, kann das System Shorts schließen und optional Longs eröffnen, und umgekehrt bei Abwärtsbewegungen.
  • Markt: Einzelnes Instrument, definiert durch das verbundene Security.
  • Zeitrahmen: Konfigurierbar; Standard sind 4-Stunden-Kerzen (entspricht der ursprünglichen EA-Eingabe).
  • Richtung: Long und Short. Jede Richtung kann unabhängig deaktiviert werden.
  • Ordertyp: Marktorders über BuyMarket() / SellMarket().

Indikator-Stack

  1. Relative Strength Index (RSI) — Basis-Momentum-Oszillator mit dem Parameter RSI Length (spiegelt JurXPeriod wider).
  2. Jurik Moving Average (JMA) — Glättet die RSI-Ausgabe mit Smoothing Length (spiegelt JMAPeriod wider). Der JMA-Phasenparameter der MQL-Version ist in StockSharp nicht verfügbar und wird daher weggelassen.
  3. Signal Shift — Reproduziert den SignalBar-Parameter. Signale werden aus dem Wert Signal Shift Balken zurück und den zwei vorhergehenden Werten generiert, um Steigungsänderungen zu erkennen.

Handelslogik

Long-Management

  • Einstieg: Aktiviert durch Enable Long Entries. Erfordert, dass der geglättete Oszillator vor zwei Balken rückläufig war (previous > older ist falsch), auf dem letzten abgeschlossenen Balken nach oben drehte (previous < older) und auf dem aktuellen Balken weiter steigt (current > previous). Position muss flach oder short sein.
  • Ausstieg: Wenn Exit Long on Downturn aktiviert ist und der Oszillator nach unten neigt (previous > older), wird ein offener Long geschlossen.

Short-Management

  • Einstieg: Aktiviert durch Enable Short Entries. Erfordert, dass der Oszillator nach unten dreht (previous > older) und auf dem aktuellen Balken weiter fällt (current < previous), während die Strategie flach oder long ist.
  • Ausstieg: Wenn Exit Short on Upturn aktiviert ist und der Oszillator nach oben neigt (previous < older), wird ein offener Short gedeckt.

Zeitfilter

  • Enable Time Exit schließt Positionen, sobald ihre Haltezeit Holding Minutes überschreitet. Dies spiegelt den Timer des ursprünglichen Experten wider, der Positionen nach nTime Minuten liquidiert.

Risikokontrollen

  • Stop Loss (pts) und Take Profit (pts) werden über StartProtection mit UnitTypes.PriceStep in StockSharp-Schutzlevel umgewandelt.

Parameter

Parameter Beschreibung Standard
Indicator Timeframe Kerzentyp für die Indikatorberechnungen. 4-Stunden-Kerzen
RSI Length Periode für den RSI (analog zum JurX-Periode). 8
Smoothing Length Länge des Jurik-MA-Glättens (analog zur JMA-Periode). 3
Signal Shift Anzahl der abgeschlossenen Balken, die vor der Steigungsprüfung übersprungen werden (SignalBar). 1
Enable Long Entries / Enable Short Entries Öffnen von Trades in jede Richtung erlauben. true
Exit Long on Downturn / Exit Short on Upturn Oszillator-gesteuerte Exits für bestehende Positionen erlauben. true
Enable Time Exit Haltezeitbasierte Liquidation aktivieren. true
Holding Minutes Maximale Minuten, eine Position offen zu halten. 240
Stop Loss (pts) Abstand des Schutz-Stops in Preisschritten. 1000
Take Profit (pts) Abstand des Gewinnziels in Preisschritten. 2000

Hinweise zur Konvertierung

  • Der JJRSX-Histogramm-Puffer des ursprünglichen Indikators wird mit RSI + Jurik-Glättung emuliert. Es wird nur Steigungsinformation verwendet, sodass numerische Skalenunterschiede die Entscheidungen nicht beeinflussen.
  • Geldverwaltungsoptionen (MM, MMMode, Deviation) sind nicht portiert. Die StockSharp-Order-Sizing sollte über die Strategy.Volume-Eigenschaft oder externe Portfolio-Einstellungen gehandhabt werden.
  • Globale Variablen, die in MQL zur Ratenbegrenzung von Orders verwendet werden, sind hier unnötig, da die Strategie nur auf fertige Kerzen reagiert.
  • Alle Kommentare und Dokumentation sind gemäß Repository-Richtlinien auf Englisch.
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;
	}
}