Auf GitHub ansehen

JS Sistem 2-Strategie

Überblick

JS Sistem 2 ist ein Trendfolge-System, das ursprünglich für MetaTrader 5 geschrieben wurde. Der StockSharp-Port behält den Multi-Indikator-Bestätigungsblock des Expert Advisors bei und handelt auf geschlossenen Kerzen des ausgewählten Zeitrahmens. Orders werden mit einem festen Volumen dimensioniert und können optional blockiert werden, wenn der verbundene Portfolio-Saldo unter einen konfigurierbaren Schwellenwert fällt. Das Risiko wird durch harte Stop-Loss- und Take-Profit-Distanzen in Pips zusammen mit einem adaptiven Trailing-Stop, der Kerzenschatten folgt, kontrolliert.

Indikatoren und Filter

  • EMA(55), EMA(89), EMA(144) – bilden einen direktionalen Filter. Long-Setups erfordern den schnellen EMA über dem mittleren und den mittleren über der langsamen Linie, während der Abstand zwischen den schnellen und langsamen Kurven unter MinDifferencePips bleiben muss.
  • MACD-Histogramm (OsMA) – verwendet schnelle, langsame und Signal-EMA-Längen identisch zur MQL-Version. Ein Long-Trade erfordert ein positives Histogramm, ein Short-Trade ein negatives.
  • Relativer Vigor Index (RVI) – mit Periode RviPeriod berechnet und durch einen zusätzlichen einfachen gleitenden Durchschnitt mit RviSignalLength geglättet. Long-Trades benötigen den RVI über seiner Signallinie und über dem Schwellenwert RviMax; Short-Trades benötigen das Inverse.
  • Höchst-/Tiefst-Swing-Envelopes – verfolgen das höchste Hoch und das tiefste Tief über VolatilityPeriod Kerzen. Diese Werte steuern die Trailing-Stop-Logik und replizieren den Schatten-Trailing-Modus des ursprünglichen Expert Advisors.

Handelslogik

  1. Die Strategie verarbeitet nur abgeschlossene Kerzen des konfigurierten CandleType.
  2. Vor der Bewertung von Einstiegen aktualisiert sie den Trailing-Stop für bestehende Positionen mit den neuesten Swing-Extremen und prüft dann, ob Stop-Loss- oder Take-Profit-Level während der Kerze erreicht wurden.
  3. Long-Einstiegsbedingungen:
    • Portfolio-Saldo liegt über MinBalance.
    • EMA55 > EMA89 > EMA144 und die Differenz zwischen EMA55 und EMA144 liegt unter MinDifferencePips (in Preiseinheiten über die Pip-Größe des Instruments umgerechnet).
    • MACD-Histogramm (macdLine) ist größer als null.
    • RVI liegt über seiner Signallinie und die Signallinie liegt bei oder über RviMax.
    • Keine bestehende Long-Position (Position <= 0). Wenn eine Short-Position vorhanden ist, wird sie vor dem Öffnen der Long-Position abgeflacht.
  4. Short-Einstiegsbedingungen spiegeln die Long-Regeln mit invertierten Vergleichen wider und verwenden den Schwellenwert RviMin.
  5. Beim Einstieg speichert die Strategie den Kerzenschlusskurs als Referenz, setzt virtuelle Stop-Loss- und Take-Profit-Level durch Verschiebung dieses Preises um StopLossPips und TakeProfitPips und setzt den Trailing-Zustand zurück.

Ausstieg und Trailing-Management

  • Harter Stop-Loss / Take-Profit: Immer wenn der Kerzenbereich das gespeicherte Stop- oder Zielniveau überschneidet, schließt die Strategie die gesamte Position sofort.
  • Trailing-Stop: Wenn TrailingEnabled wahr ist, versucht die Strategie den Stop in Richtung des Gewinns zu verschieben. Für Longs wird der Stop auf das tiefste Tief der letzten VolatilityPeriod Kerzen angehoben, sobald dieses Tief sowohl über dem Einstiegspreis als auch über dem vorherigen Stop um mindestens TrailingIndentPips liegt. Shorts folgen der symmetrischen Regel mit dem höchsten Hoch. Dies reproduziert das "Schatten-Trailing" des MQL-Advisors und verhindert ein verfrühtes Anziehen der Stops.
  • Bilanzschutz: Wenn der aktuelle Portfolio-Wert unter MinBalance fällt, verzichtet die Strategie auf das Absenden neuer Orders, verwaltet aber weiterhin offene Trades und Trailing-Stops.

Parameter

Parameter Beschreibung Standard
MinBalance Mindest-Portfolio-Saldo für neue Einstiege. 100
Volume Ordervolumen mit jedem Trade. 1
StopLossPips Stop-Loss-Distanz in Pips. Auf 0 setzen, um zu deaktivieren. 35
TakeProfitPips Take-Profit-Distanz in Pips. Auf 0 setzen, um zu deaktivieren. 40
MinDifferencePips Maximale erlaubte Spreizung zwischen schnellem und langsamem EMA in Pips. 28
VolatilityPeriod Anzahl der Kerzen zur Berechnung von Swing-Hochs und -Tiefs für den Trailing-Stop. 15
TrailingEnabled Aktiviert oder deaktiviert die Trailing-Stop-Logik. true
TrailingIndentPips Mindestlücke zwischen Preis, Einstieg und Stop beim Aktualisieren des Trailing-Stops. 1
MaFastPeriod Periode für den schnellen EMA. 55
MaMediumPeriod Periode für den mittleren EMA. 89
MaSlowPeriod Periode für den langsamen EMA. 144
OsmaFastPeriod Schnelle EMA-Länge für das MACD-Histogramm. 13
OsmaSlowPeriod Langsame EMA-Länge für das MACD-Histogramm. 55
OsmaSignalPeriod Signal-Glättungslänge für das MACD-Histogramm. 21
RviPeriod Periode des Relativen Vigor Index. 44
RviSignalLength Länge des auf den RVI angewendeten SMA zur Erzeugung seiner Signallinie. 4
RviMax Obere Grenze, die das RVI-Signal vor Long-Einstiegen erreichen muss. 0.04
RviMin Untere Grenze, die das RVI-Signal vor Short-Einstiegen erreichen muss. -0.04
CandleType Zeitrahmen der für alle Berechnungen verwendeten Kerzen. 5-Minuten-Kerzen

Implementierungshinweise

  • Pip-Distanz wird aus dem Preisschritt des Instruments abgeleitet. Instrumente, die mit 3 oder 5 Dezimalstellen notiert werden, verwenden einen Pip gleich zehn Preisschritten, entsprechend der ursprünglichen MQL-Logik.
  • Stop- und Ziel-Verwaltung erfolgt innerhalb der Strategie-Schleife, da StockSharp in dieser Vorlage keine serverseitigen Orders dafür automatisch einreicht.
  • Die Strategie ruft StartProtection() beim Start auf, damit die Basisklasse unerwartete Verbindungsunterbrechungen und ausstehende Positionen überwachen kann.
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>
/// JS Sistem 2 trend-following strategy converted from MetaTrader 5.
/// Combines exponential moving averages, MACD histogram (OsMA), and Relative Vigor Index filters.
/// Includes trailing stop based on recent candle shadows and configurable stop/target distances.
/// </summary>
public class JsSistem2Strategy : Strategy
{
	private readonly StrategyParam<decimal> _minBalance;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _minDifferencePips;
	private readonly StrategyParam<int> _volatilityPeriod;
	private readonly StrategyParam<bool> _trailingEnabled;
	private readonly StrategyParam<int> _trailingIndentPips;
	private readonly StrategyParam<int> _maFastPeriod;
	private readonly StrategyParam<int> _maMediumPeriod;
	private readonly StrategyParam<int> _maSlowPeriod;
	private readonly StrategyParam<int> _osmaFastPeriod;
	private readonly StrategyParam<int> _osmaSlowPeriod;
	private readonly StrategyParam<int> _osmaSignalPeriod;
	private readonly StrategyParam<int> _rviPeriod;
	private readonly StrategyParam<int> _rviSignalLength;
	private readonly StrategyParam<decimal> _rviMax;
	private readonly StrategyParam<decimal> _rviMin;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _emaFast = null!;
	private ExponentialMovingAverage _emaMedium = null!;
	private ExponentialMovingAverage _emaSlow = null!;
	private MovingAverageConvergenceDivergence _macd = null!;
	private Highest _highest = null!;
	private Lowest _lowest = null!;
	private RelativeVigorIndex _rvi = null!;

	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _entryPrice;

	/// <summary>
	/// Minimum account balance required to allow new entries.
	/// </summary>
	public decimal MinBalance
	{
		get => _minBalance.Value;
		set => _minBalance.Value = value;
	}


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

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

	/// <summary>
	/// Maximum allowed spread between fast and slow EMA in pips.
	/// </summary>
	public int MinDifferencePips
	{
		get => _minDifferencePips.Value;
		set => _minDifferencePips.Value = value;
	}

	/// <summary>
	/// Lookback for trailing stop based on candle shadows.
	/// </summary>
	public int VolatilityPeriod
	{
		get => _volatilityPeriod.Value;
		set => _volatilityPeriod.Value = value;
	}

	/// <summary>
	/// Enables trailing stop management.
	/// </summary>
	public bool TrailingEnabled
	{
		get => _trailingEnabled.Value;
		set => _trailingEnabled.Value = value;
	}

	/// <summary>
	/// Offset applied when updating trailing stop levels.
	/// </summary>
	public int TrailingIndentPips
	{
		get => _trailingIndentPips.Value;
		set => _trailingIndentPips.Value = value;
	}

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int MaFastPeriod
	{
		get => _maFastPeriod.Value;
		set => _maFastPeriod.Value = value;
	}

	/// <summary>
	/// Medium EMA period.
	/// </summary>
	public int MaMediumPeriod
	{
		get => _maMediumPeriod.Value;
		set => _maMediumPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int MaSlowPeriod
	{
		get => _maSlowPeriod.Value;
		set => _maSlowPeriod.Value = value;
	}

	/// <summary>
	/// Fast EMA length for the MACD/OsMA filter.
	/// </summary>
	public int OsmaFastPeriod
	{
		get => _osmaFastPeriod.Value;
		set => _osmaFastPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA length for the MACD/OsMA filter.
	/// </summary>
	public int OsmaSlowPeriod
	{
		get => _osmaSlowPeriod.Value;
		set => _osmaSlowPeriod.Value = value;
	}

	/// <summary>
	/// Signal length for the MACD/OsMA filter.
	/// </summary>
	public int OsmaSignalPeriod
	{
		get => _osmaSignalPeriod.Value;
		set => _osmaSignalPeriod.Value = value;
	}

	/// <summary>
	/// Relative Vigor Index period.
	/// </summary>
	public int RviPeriod
	{
		get => _rviPeriod.Value;
		set => _rviPeriod.Value = value;
	}

	/// <summary>
	/// Smoothing length for the RVI signal line.
	/// </summary>
	public int RviSignalLength
	{
		get => _rviSignalLength.Value;
		set => _rviSignalLength.Value = value;
	}

	/// <summary>
	/// Upper threshold for the RVI signal line.
	/// </summary>
	public decimal RviMax
	{
		get => _rviMax.Value;
		set => _rviMax.Value = value;
	}

	/// <summary>
	/// Lower threshold for the RVI signal line.
	/// </summary>
	public decimal RviMin
	{
		get => _rviMin.Value;
		set => _rviMin.Value = value;
	}

	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="JsSistem2Strategy"/> class.
	/// </summary>
	public JsSistem2Strategy()
	{
		_minBalance = Param(nameof(MinBalance), 100m)
			.SetDisplay("Min Balance", "Minimum balance to allow trading", "Risk")
			;


		_stopLossPips = Param(nameof(StopLossPips), 200)
			.SetDisplay("Stop Loss", "Stop-loss distance in pips", "Risk")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 300)
			.SetDisplay("Take Profit", "Take-profit distance in pips", "Risk")
			;

		_minDifferencePips = Param(nameof(MinDifferencePips), 5000)
			.SetGreaterThanZero()
			.SetDisplay("EMA Spread", "Maximum fast-slow EMA spread", "Filters")
			;

		_volatilityPeriod = Param(nameof(VolatilityPeriod), 15)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Range", "Number of candles for trailing", "Risk")
			;

		_trailingEnabled = Param(nameof(TrailingEnabled), true)
			.SetDisplay("Trailing", "Enable trailing stop", "Risk");

		_trailingIndentPips = Param(nameof(TrailingIndentPips), 1)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Offset", "Indent from candle shadows", "Risk")
			;

		_maFastPeriod = Param(nameof(MaFastPeriod), 12)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Fast EMA period", "Indicators")
			;

		_maMediumPeriod = Param(nameof(MaMediumPeriod), 26)
			.SetGreaterThanZero()
			.SetDisplay("Medium EMA", "Medium EMA period", "Indicators")
			;

		_maSlowPeriod = Param(nameof(MaSlowPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Slow EMA period", "Indicators")
			;

		_osmaFastPeriod = Param(nameof(OsmaFastPeriod), 12)
			.SetGreaterThanZero()
			.SetDisplay("OsMA Fast", "Fast EMA for MACD", "Indicators")
			;

		_osmaSlowPeriod = Param(nameof(OsmaSlowPeriod), 26)
			.SetGreaterThanZero()
			.SetDisplay("OsMA Slow", "Slow EMA for MACD", "Indicators")
			;

		_osmaSignalPeriod = Param(nameof(OsmaSignalPeriod), 9)
			.SetGreaterThanZero()
			.SetDisplay("OsMA Signal", "Signal period for MACD", "Indicators")
			;

		_rviPeriod = Param(nameof(RviPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("RVI Period", "Relative Vigor Index period", "Indicators")
			;

		_rviSignalLength = Param(nameof(RviSignalLength), 4)
			.SetGreaterThanZero()
			.SetDisplay("RVI Signal", "Smoothing for RVI signal", "Indicators")
			;

		_rviMax = Param(nameof(RviMax), 0.02m)
			.SetDisplay("RVI Max", "Upper threshold for RVI signal", "Filters")
			;

		_rviMin = Param(nameof(RviMin), -0.02m)
			.SetDisplay("RVI Min", "Lower threshold for RVI signal", "Filters")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candles used for calculations", "General");
	}

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

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

		_stopPrice = null;
		_takePrice = null;
		_entryPrice = 0m;
	}

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

		_emaFast = new ExponentialMovingAverage { Length = MaFastPeriod };
		_emaMedium = new ExponentialMovingAverage { Length = MaMediumPeriod };
		_emaSlow = new ExponentialMovingAverage { Length = MaSlowPeriod };
		_macd = new MovingAverageConvergenceDivergence
		{
			ShortMa = { Length = OsmaFastPeriod },
			LongMa = { Length = OsmaSlowPeriod },
		};
		_highest = new Highest { Length = VolatilityPeriod };
		_lowest = new Lowest { Length = VolatilityPeriod };
		_rvi = new RelativeVigorIndex();
		_rvi.Average.Length = RviPeriod;
		_rvi.Signal.Length = RviSignalLength;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_emaFast, _emaMedium, _emaSlow, _macd, _highest, _lowest, ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal emaFast, decimal emaMedium, decimal emaSlow, decimal macdLine, decimal highestValue, decimal lowestValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_emaFast.IsFormed || !_emaMedium.IsFormed || !_emaSlow.IsFormed || !_macd.IsFormed || !_highest.IsFormed || !_lowest.IsFormed)
			return;

		var step = CalculatePipSize();
		if (step == 0m)
		{
			step = Security.PriceStep ?? 0m;
		}
		if (step == 0m)
			step = 1m;

		var stopDistance = StopLossPips > 0 ? StopLossPips * step : 0m;
		var takeDistance = TakeProfitPips > 0 ? TakeProfitPips * step : 0m;
		var minDifference = MinDifferencePips * step;
		var indent = TrailingIndentPips * step;

		UpdateTrailingStops(candle, highestValue, lowestValue, indent);
		HandleStopsAndTargets(candle);

		var canTrade = (Portfolio?.CurrentValue ?? decimal.MaxValue) >= MinBalance;

		var emaOrderLong = emaFast > emaMedium && emaMedium > emaSlow;
		var emaOrderShort = emaFast < emaMedium && emaMedium < emaSlow;
		var emaSpreadLong = Math.Abs(emaFast - emaSlow) < minDifference;
		var emaSpreadShort = Math.Abs(emaSlow - emaFast) < minDifference;

		var longCondition = canTrade && emaOrderLong && emaSpreadLong && macdLine > 0m;
		var shortCondition = canTrade && emaOrderShort && emaSpreadShort && macdLine < 0m;

		if (longCondition && Position <= 0)
		{
			if (Position < 0)
			{
				BuyMarket(Math.Abs(Position));
				ResetOrders();
			}

			if (Volume > 0m)
			{
				BuyMarket(Volume);
				_entryPrice = candle.ClosePrice;
				_stopPrice = stopDistance > 0m ? _entryPrice - stopDistance : null;
				_takePrice = takeDistance > 0m ? _entryPrice + takeDistance : null;
			}
		}
		else if (shortCondition && Position >= 0)
		{
			if (Position > 0)
			{
				SellMarket(Math.Abs(Position));
				ResetOrders();
			}

			if (Volume > 0m)
			{
				SellMarket(Volume);
				_entryPrice = candle.ClosePrice;
				_stopPrice = stopDistance > 0m ? _entryPrice + stopDistance : null;
				_takePrice = takeDistance > 0m ? _entryPrice - takeDistance : null;
			}
		}
	}

	private void UpdateTrailingStops(ICandleMessage candle, decimal highestValue, decimal lowestValue, decimal indent)
	{
		if (!TrailingEnabled)
			return;

		if (Position > 0)
		{
			var newStop = lowestValue;
			if (newStop > 0m && candle.ClosePrice - newStop > indent && newStop - _entryPrice > indent)
			{
				if (!_stopPrice.HasValue || newStop - _stopPrice.Value > indent)
				{
					_stopPrice = newStop;
				}
			}
		}
		else if (Position < 0)
		{
			var newStop = highestValue;
			if (newStop > 0m && newStop - candle.ClosePrice > indent && _entryPrice - newStop > indent)
			{
				if (!_stopPrice.HasValue || _stopPrice.Value - newStop > indent)
				{
					_stopPrice = newStop;
				}
			}
		}
	}

	private void HandleStopsAndTargets(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetOrders();
				return;
			}

			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetOrders();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetOrders();
				return;
			}

			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetOrders();
			}
		}
	}

	private void ResetOrders()
	{
		_stopPrice = null;
		_takePrice = null;
		_entryPrice = 0m;
	}

	private decimal CalculatePipSize()
	{
		var security = Security;
		if (security is null)
			return 0m;

		var step = security.PriceStep ?? 0m;
		if (step == 0m)
			return 0m;

		var decimals = security.Decimals;
		return decimals == 3 || decimals == 5 ? step * 10m : step;
	}
}