Открыть на GitHub

Стратегия JS Sistem 2

Обзор

JS Sistem 2 — трендовая система, изначально написанная для MetaTrader 5. Порт на StockSharp сохраняет блок многофакторного подтверждения из исходного советника и работает только по закрытым свечам выбранного таймфрейма. Сделки открываются фиксированным объёмом, а при падении стоимости портфеля ниже заданного порога можно заблокировать новые входы. Управление риском реализовано через стоп-лосс и тейк-профит в пунктах, дополненные адаптивным трейлингом по теням свечей.

Индикаторы и фильтры

  • EMA(55), EMA(89), EMA(144) — формируют направленный фильтр. Для покупок требуется расположение EMA55 выше EMA89 и EMA89 выше EMA144, при этом разброс между EMA55 и EMA144 должен быть меньше параметра MinDifferencePips.
  • Гистограмма MACD (OsMA) — использует те же периоды быстрого, медленного и сигнального EMA, что и оригинальная версия на MQL. Для покупок гистограмма должна быть положительной, для продаж — отрицательной.
  • Relative Vigor Index (RVI) — рассчитывается с периодом RviPeriod и дополнительно сглаживается простой скользящей средней длиной RviSignalLength. Покупки разрешены, когда значение RVI выше сигнальной линии и сама сигнальная линия находится не ниже RviMax. Для продаж условия зеркальны и задействуют порог RviMin.
  • Индикаторы Highest/Lowest — отслеживают максимум и минимум за VolatilityPeriod свечей. Эти значения управляют трейлинг-стопом и повторяют механику «по теням» из исходного советника.

Логика торговли

  1. Обработка выполняется только на закрытых свечах типа CandleType.
  2. Перед поиском новых сигналов стратегия обновляет трейлинг-стоп по актуальным экстремумам и проверяет, не были ли достигнуты уровни стопа или тейка внутри свечи.
  3. Условия для входа в покупку:
    • Текущая стоимость портфеля выше MinBalance.
    • EMA55 > EMA89 > EMA144 и разница между EMA55 и EMA144 (переведённая в цену через размер пункта инструмента) меньше MinDifferencePips.
    • Значение гистограммы MACD (macdLine) положительное.
    • RVI выше сигнальной линии, а сигнальная линия достигла или превысила RviMax.
    • Отсутствует открытая длинная позиция (Position <= 0). При наличии короткой позиции она закрывается перед открытием покупки.
  4. Условия для входа в продажу симметричны и используют порог RviMin.
  5. После открытия позиции фиксируется цена закрытия свечи, рассчитываются уровни стоп-лосса и тейк-профита с учётом StopLossPips и TakeProfitPips, сбрасывается состояние трейлинга.

Управление позицией и трейлинг

  • Жёсткий стоп-лосс / тейк-профит: Если диапазон свечи достигает сохранённого уровня стопа либо цели, позиция закрывается полностью.
  • Трейлинг-стоп: При включённом параметре TrailingEnabled стоп подтягивается в сторону прибыли. Для покупок стоп переносится на минимум из последних VolatilityPeriod свечей, когда этот минимум находится выше цены входа и предыдущего стопа минимум на TrailingIndentPips. Для продаж применяется зеркальное правило через максимум. Такой подход повторяет «трейлинг по теням» оригинального алгоритма и защищает прибыль без преждевременного срабатывания.
  • Контроль баланса: Когда текущая стоимость портфеля опускается ниже MinBalance, стратегия перестаёт открывать новые сделки, но продолжает сопровождать уже открытые позиции и обновлять трейлинг-стоп.

Параметры

Параметр Описание Значение по умолчанию
MinBalance Минимальная стоимость портфеля для разрешения новых входов. 100
Volume Объём заявки при открытии позиции. 1
StopLossPips Дистанция стоп-лосса в пунктах (0 — отключить). 35
TakeProfitPips Дистанция тейк-профита в пунктах (0 — отключить). 40
MinDifferencePips Максимально допустимый разрыв между быстрой и медленной EMA в пунктах. 28
VolatilityPeriod Количество свечей для расчёта экстремумов в трейлинг-стопе. 15
TrailingEnabled Включение механизма трейлинг-стопа. true
TrailingIndentPips Минимальный зазор между ценой, входом и стопом при обновлении трейлинга. 1
MaFastPeriod Период быстрой EMA. 55
MaMediumPeriod Период средней EMA. 89
MaSlowPeriod Период медленной EMA. 144
OsmaFastPeriod Период быстрого EMA для гистограммы MACD. 13
OsmaSlowPeriod Период медленного EMA для гистограммы MACD. 55
OsmaSignalPeriod Период сигнального EMA для гистограммы MACD. 21
RviPeriod Период индикатора Relative Vigor Index. 44
RviSignalLength Длина SMA, сглаживающей сигнал RVI. 4
RviMax Верхний порог сигнальной линии RVI для разрешения покупок. 0.04
RviMin Нижний порог сигнальной линии RVI для разрешения продаж. -0.04
CandleType Таймфрейм свечей, используемых в расчётах. 5 минут

Особенности реализации

  • Размер пункта определяется по шагу цены инструмента. Для инструментов с 3 или 5 знаками после запятой пункт равен десяти шагам цены, что соответствует исходной MQL-реализации.
  • Стопы и тейки обрабатываются внутри стратегии, потому что в шаблоне StockSharp они не выставляются как серверные заявки автоматически.
  • При запуске вызывается StartProtection(), чтобы базовый класс контролировал нештатные ситуации и зависшие позиции.
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;
	}
}