GitHub で見る

JS Sistem 2戦略

概要

JS Sistem 2はMetaTrader 5向けに元々書かれたトレンドフォロー戦略です。StockSharpへのポートはエキスパートアドバイザーのマルチインジケーター確認ブロックを維持し、選択した時間軸のクローズした足で取引します。注文は固定ボリュームでサイジングされ、接続されたポートフォリオ残高が設定可能な閾値を下回った場合、オプションでブロックできます。リスクはピップスで表現されたハードなストップロスとテイクプロフィット距離と、足の影を追跡する適応的なトレーリングストップで制御されます。

インジケーターとフィルター

  • EMA(55)、EMA(89)、EMA(144) – 方向性フィルターを形成します。ロング設定では高速EMAが中間の上、中間が低速ラインの上にある必要があり、高速と低速の曲線間の距離はMinDifferencePips以下に留まる必要があります。
  • MACDヒストグラム(OsMA) – MQLバージョンと同じ高速、低速、シグナルEMAの長さを使用します。ロング取引はヒストグラムが正であることを、ショート取引は負であることを要求します。
  • 相対活力指数(RVI) – 期間RviPeriodで計算され、RviSignalLengthを持つ追加の単純移動平均で平滑化されます。ロング取引にはRVIがシグナルラインより上、かつRviMax閾値より上にある必要があります。ショートは逆が必要です。
  • スイング最高値/最安値エンベロープVolatilityPeriod本の足にわたって最高の高値と最低の安値を追跡します。これらの値はトレーリングストップロジックを駆動し、元のエキスパートアドバイザーの影トレーリングモードを再現します。

トレードロジック

  1. 戦略は設定されたCandleTypeの完成した足のみを処理します。
  2. エントリーを評価する前に、最新のスイング極値を使用して既存ポジションのトレーリングストップを更新し、足の間にストップロスまたはテイクプロフィットレベルに達したかどうかを確認します。
  3. ロングエントリー条件:
    • ポートフォリオ残高がMinBalanceより上。
    • EMA55 > EMA89 > EMA144で、EMA55とEMA144の差がMinDifferencePips以下(インストゥルメントのpipサイズを通じて価格単位に変換)。
    • MACDヒストグラム(macdLine)がゼロより大きい。
    • RVIがシグナルラインより上で、シグナルラインがRviMax以上。
    • 既存のロングポジションがない(Position <= 0)。ショートポジションが存在する場合、ロングを開く前にフラット化されます。
  4. ショートエントリー条件はロングルールを反転した比較で反映し、RviMin閾値を使用します。
  5. エントリー時、戦略は参照として足のクローズ価格を保存し、StopLossPipsTakeProfitPipsでその価格をシフトしてバーチャルなストップロスとテイクプロフィットレベルを設定し、トレーリング状態をリセットします。

エグジットとトレーリング管理

  • ハードストップロス / テイクプロフィット: 足のレンジが保存されたストップまたはターゲットレベルと重複するたびに、戦略はすぐにポジション全体をクローズします。
  • トレーリングストップ: TrailingEnabledが真の場合、戦略は利益の方向にストップを動かそうとします。ロングの場合、その安値がエントリー価格と前のストップの両方より少なくともTrailingIndentPips以上上にあれば、ストップは最後のVolatilityPeriod本の足の最低の安値に引き上げられます。ショートは最高の高値を使用した対称ルールに従います。これはMQLアドバイザーの「影トレーリング」を再現し、ストップが早期に締まることを防ぎます。
  • 残高保護: 現在のポートフォリオ価値が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 MACDヒストグラムの高速EMA長さ。 13
OsmaSlowPeriod MACDヒストグラムの低速EMA長さ。 55
OsmaSignalPeriod MACDヒストグラムのシグナル平滑化長さ。 21
RviPeriod 相対活力指数の期間。 44
RviSignalLength シグナルラインを取得するためにRVIに適用するSMAの長さ。 4
RviMax ロングエントリーが許可される前にRVIシグナルが達する必要がある上限。 0.04
RviMin ショートエントリーが許可される前にRVIシグナルが達する必要がある下限。 -0.04
CandleType すべての計算に使用する足の時間軸。 5分足

実装メモ

  • pip距離はインストゥルメントの価格ステップから導出されます。3桁または5桁で見積もられたインストゥルメントは10の価格ステップに等しいpipを使用し、元の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;
	}
}