View on GitHub

X2MA JFATL Crossover Strategy

This strategy is a StockSharp adaptation of the MetaTrader expert Exp_X2MA_JFatl. It combines a fast Simple Moving Average (SMA) with a slow Jurik Moving Average (JMA) and an additional Jurik filter to confirm trend direction. Trades are opened when the fast average crosses the slow one and the price is on the same side of the filter. Positions are closed when price moves against the filter or an opposite crossover occurs.

Details

  • Entry Criteria:
    • Long: SMA_fast crosses above JMA_slow and Close > JMA_filter.
    • Short: SMA_fast crosses below JMA_slow and Close < JMA_filter.
  • Exit Criteria:
    • Price moves to the opposite side of the filter.
    • Opposite crossover of the averages.
  • Long/Short: Both sides.
  • Stops: Not used by default.
  • Default Values:
    • Fast MA Length = 5.
    • Slow MA Length = 12.
    • Filter Length = 20.
  • Filters:
    • Category: Trend following
    • Direction: Both
    • Indicators: Multiple (SMA, JMA)
    • Stops: No
    • Complexity: Moderate
    • Timeframe: Short-term
    • Seasonality: No
    • Neural networks: No
    • Divergence: No
    • Risk level: Medium
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>
/// X2MA with JFATL filter strategy.
/// Opens long when the fast SMA crosses above the slow Jurik MA and price is above the filter.
/// Opens short when the fast SMA crosses below the slow Jurik MA and price is below the filter.
/// </summary>
public class X2MaJfatlStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _filterLength;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevDiff;
	private bool _isInitialized;
	private int _barsSinceTrade;

	/// <summary>
	/// Fast moving average length.
	/// </summary>
	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	/// <summary>
	/// Slow Jurik moving average length.
	/// </summary>
	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

	/// <summary>
	/// Jurik filter length.
	/// </summary>
	public int FilterLength
	{
		get => _filterLength.Value;
		set => _filterLength.Value = value;
	}

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public X2MaJfatlStrategy()
	{
		_fastLength = Param(nameof(FastLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Length", "Length of the fast moving average", "Parameters")
			
			.SetOptimize(5, 20, 1);

		_slowLength = Param(nameof(SlowLength), 13)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Length", "Length of the slow Jurik MA", "Parameters")
			
			.SetOptimize(10, 40, 2);

		_filterLength = Param(nameof(FilterLength), 21)
			.SetGreaterThanZero()
			.SetDisplay("Filter Length", "Length of the Jurik filter", "Parameters")
			
			.SetOptimize(10, 60, 5);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles for calculation", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevDiff = 0m;
		_isInitialized = false;
		_barsSinceTrade = 10;
	}

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

		_prevDiff = 0m;
		_isInitialized = false;
		_barsSinceTrade = 10;

		var fastMa = new SMA { Length = FastLength };
		var slowMa = new JurikMovingAverage { Length = SlowLength };
		var filterMa = new JurikMovingAverage { Length = FilterLength };

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(fastMa, slowMa, filterMa, Process)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, fastMa);
			DrawIndicator(area, slowMa);
			DrawIndicator(area, filterMa);
			DrawOwnTrades(area);
		}
	}

	private void Process(ICandleMessage candle, decimal fastValue, decimal slowValue, decimal filterValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_barsSinceTrade++;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (!_isInitialized)
		{
			_prevDiff = fastValue - slowValue;
			_isInitialized = true;
			return;
		}

		var diff = fastValue - slowValue;

		// Exit if price moves against the filter
		if (Position > 0 && candle.ClosePrice < filterValue)
		{
			SellMarket();
			_barsSinceTrade = 0;
		}
		else if (Position < 0 && candle.ClosePrice > filterValue)
		{
			BuyMarket();
			_barsSinceTrade = 0;
		}

		// Crossover entries
		if (_barsSinceTrade >= 5 && _prevDiff <= 0m && diff > 0m && candle.ClosePrice > filterValue && Position <= 0)
		{
			BuyMarket();
			_barsSinceTrade = 0;
		}
		else if (_barsSinceTrade >= 5 && _prevDiff >= 0m && diff < 0m && candle.ClosePrice < filterValue && Position >= 0)
		{
			SellMarket();
			_barsSinceTrade = 0;
		}

		_prevDiff = diff;
	}
}