在 GitHub 上查看

JS Sistem 2 策略

概述

JS Sistem 2 最初是为 MetaTrader 5 编写的趋势策略。移植到 StockSharp 后,仍然保留了原始智能交易系统中的多指标确认模块,只在所选周期的收盘 K 线后进行计算。策略使用固定下单量,当投资组合净值低于阈值 MinBalance 时可以停止开仓。风险控制通过以点数表示的止损、止盈距离以及跟随 K 线影线的自适应追踪止损共同完成。

指标与过滤器

  • EMA(55)、EMA(89)、EMA(144):构成方向性过滤。做多要求快线在上、慢线在下,并且 EMA55 与 EMA144 的距离小于 MinDifferencePips 指定的阈值。
  • MACD 柱状图(OsMA):使用与 MQL 版本一致的快、慢、信号周期。做多要求柱状图为正,做空要求柱状图为负。
  • 相对活力指数 (RVI):以 RviPeriod 为周期计算,并通过长度为 RviSignalLength 的简单移动平均生成信号线。做多时 RVI 需高于信号线且信号线不低于 RviMax;做空时条件相反并使用 RviMin 阈值。
  • 最高价/最低价通道:利用 VolatilityPeriod 根 K 线的最高价与最低价来复制原策略的“影线追踪”逻辑,为追踪止损提供参考。

交易逻辑

  1. 策略仅处理 CandleType 指定类型的完整 K 线。
  2. 在评估新信号之前,会根据最新的高低点更新追踪止损,然后检测当前 K 线是否触及止损或止盈。
  3. 做多条件:
    • 投资组合净值高于 MinBalance
    • EMA55 > EMA89 > EMA144,且 EMA55 与 EMA144 的差值(按品种点值换算)小于 MinDifferencePips
    • MACD 柱状图 macdLine 大于 0。
    • RVI 位于信号线上方,且信号线达到或超过 RviMax
    • 当前无多单(Position <= 0);若存在空单,会先平仓再开多。
  4. 做空条件与做多完全对称,并使用 RviMin 作为阈值。
  5. 开仓后以 K 线收盘价为基准,根据 StopLossPipsTakeProfitPips 计算虚拟止损和止盈价格,同时重置追踪状态。

仓位管理与追踪

  • 固定止损 / 止盈:当 K 线区间触及保存的止损或止盈价位时,立即平掉全部仓位。
  • 追踪止损:当 TrailingEnabled 为 true 时,止损会顺着盈利方向移动。多单在最近 VolatilityPeriod 根 K 线的最低价高于入场价和上一止损至少 TrailingIndentPips 时,将止损提升至该最低价;空单则使用最高价并采用对称规则,从而复现原策略的“影线追踪”效果,避免过早被震荡扫出。
  • 余额保护:如果投资组合净值跌破 MinBalance,策略将暂停新的下单,但仍会管理已有仓位和追踪止损。

参数

参数 说明 默认值
MinBalance 允许开仓的最低账户净值。 100
Volume 每次下单的固定数量。 1
StopLossPips 止损距离(点数,0 表示关闭)。 35
TakeProfitPips 止盈距离(点数,0 表示关闭)。 40
MinDifferencePips 快、慢 EMA 之间允许的最大点差。 28
VolatilityPeriod 计算高低点通道的 K 线数量。 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 RVI 的计算周期。 44
RviSignalLength 对 RVI 进行平滑的 SMA 周期。 4
RviMax 做多前信号线需要达到的上限阈值。 0.04
RviMin 做空前信号线需要达到的下限阈值。 -0.04
CandleType 用于计算的 K 线周期。 5 分钟

实现说明

  • 点值根据品种的最小价格步长推导,报价保留 3 或 5 位小数的品种会将 1 点视为 10 个最小步长,完全复刻原 MQL 策略的处理方式。
  • 止损和止盈在策略内部检测,并不会在交易所创建真实挂单。
  • 策略启动时调用 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;
	}
}