Auf GitHub ansehen

Elli Ichimoku ADX Strategy

Overview

The strategy is a C# port of the MetaTrader 5 expert "Elli" (barabashkakvn's edition). It combines Ichimoku Kinko Hyo structure with an Average Directional Index (+DI) breakout filter. Trades are opened only when a strong directional impulse is confirmed simultaneously by Ichimoku line alignment and a sudden surge in the positive directional index.

The StockSharp implementation keeps the original behaviour of working with two candle streams: Ichimoku analysis is performed on a higher timeframe (default 1 hour) while ADX is evaluated on a faster series (default 1 minute). Orders are entered with a fixed protective stop and target measured in price steps, identical to the original expert advisor.

Indicators and data

  • Ichimoku (Tenkan 19, Kijun 60, Senkou Span B 120 by default).
  • Average Directional Index (ADX), only the +DI line is used as in the source code.
  • Optional chart areas display the candle series, Ichimoku cloud and the ADX line.

Two independent candle subscriptions are created:

  1. IchimokuCandleType (default 1 hour) – drives Ichimoku calculations and generates trading decisions.
  2. AdxCandleType (default 1 minute) – feeds the ADX indicator and supplies current/previous +DI values.

Parameters

Parameter Default Description
TakeProfitPoints 60 Take profit distance in price steps. Set to 0 to disable.
StopLossPoints 30 Stop loss distance in price steps. Set to 0 to disable.
TenkanPeriod 19 Length of the Ichimoku Tenkan-sen (conversion line).
KijunPeriod 60 Length of the Ichimoku Kijun-sen (base line).
SenkouSpanBPeriod 120 Length of the Ichimoku Senkou Span B line.
AdxPeriod 10 Period for the ADX indicator.
PlusDiHighThreshold 13 Threshold that the current +DI value must exceed.
PlusDiLowThreshold 6 Threshold that the previous +DI value must stay below.
BaselineDistanceThreshold 20 Minimum Tenkan/Kijun spread (in price steps) required to confirm momentum.
IchimokuCandleType 1 hour candles Candle series used for Ichimoku evaluation.
AdxCandleType 1 minute candles Candle series used for ADX calculation.

Trading logic

  1. Wait for one finished Ichimoku candle.
  2. Ensure ADX has at least two finished values and the last reading produced a +DI breakout (previous +DI < PlusDiLowThreshold and current +DI > PlusDiHighThreshold).
  3. Convert the Tenkan/Kijun spread into price steps and verify it exceeds BaselineDistanceThreshold.
  4. All orders are blocked if an open position already exists.
  5. Buy when:
    • Tenkan > Kijun.
    • Kijun > Senkou Span A.
    • Senkou Span A > Senkou Span B (bullish cloud).
    • Closing price > Kijun.
  6. Sell when the reverse alignment is observed (Tenkan < Kijun < Senkou Span A < Senkou Span B and the close is below Kijun).
  7. Position exits rely on the protective stop and target configured via StartProtection. No discretionary exit is triggered; this mirrors the original EA that waited for stops/targets or manual intervention.

Risk management

StartProtection is called once on start. If either stop or target is zero the respective protection is omitted. Orders are sent with market execution (BuyMarket/SellMarket), matching the MQL implementation that used market orders with attached SL/TP.

Implementation notes

  • Only the positive directional index is used for both long and short signals, replicating the logic of the MQL5 code (the original author commented out the -DI branch).
  • The strategy does not track the Chikou span explicitly; instead, cloud alignment is validated by comparing Senkou Span A and B.
  • Internal fields store the last two +DI values without calling GetValue, in accordance with the high-level API guidelines.
  • If both candle parameters are identical, a single subscription is reused for Ichimoku and ADX to reduce overhead.

Usage tips

  • Keep AdxCandleType faster than IchimokuCandleType to emulate the MT5 version (e.g., M1 ADX vs. H1 Ichimoku).
  • Raise BaselineDistanceThreshold on high-volatility instruments to demand wider Tenkan/Kijun separation.
  • Because the expert opens only one position at a time, combine the strategy with portfolio-level risk controls when trading multiple symbols.
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>
/// Port of the MQL5 strategy "Elli" combining Ichimoku and ADX filters.
/// Focuses on impulsive moves confirmed by +DI acceleration and Ichimoku line alignment.
/// </summary>
public class ElliIchimokuAdxStrategy : Strategy
{
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<int> _tenkanPeriod;
	private readonly StrategyParam<int> _kijunPeriod;
	private readonly StrategyParam<int> _senkouSpanBPeriod;
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<decimal> _plusDiHighThreshold;
	private readonly StrategyParam<decimal> _plusDiLowThreshold;
	private readonly StrategyParam<decimal> _baselineDistanceThreshold;
	private readonly StrategyParam<DataType> _ichimokuCandleType;
	private readonly StrategyParam<DataType> _adxCandleType;

	private Ichimoku _ichimoku;

	private decimal? _previousPlusDi;
	private decimal? _currentPlusDi;
	private bool _isAdxReady;
	private decimal? _previousAdxHigh;
	private decimal? _previousAdxLow;
	private decimal? _previousAdxClose;
	private decimal _smoothedTrueRange;
	private decimal _smoothedPlusDm;
	private int _adxSamples;

	/// <summary>
	/// Take profit distance expressed in price steps.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in price steps.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Tenkan-sen (conversion line) period.
	/// </summary>
	public int TenkanPeriod
	{
		get => _tenkanPeriod.Value;
		set => _tenkanPeriod.Value = value;
	}

	/// <summary>
	/// Kijun-sen (base line) period.
	/// </summary>
	public int KijunPeriod
	{
		get => _kijunPeriod.Value;
		set => _kijunPeriod.Value = value;
	}

	/// <summary>
	/// Senkou Span B period.
	/// </summary>
	public int SenkouSpanBPeriod
	{
		get => _senkouSpanBPeriod.Value;
		set => _senkouSpanBPeriod.Value = value;
	}

	/// <summary>
	/// ADX calculation period.
	/// </summary>
	public int AdxPeriod
	{
		get => _adxPeriod.Value;
		set => _adxPeriod.Value = value;
	}

	/// <summary>
	/// Upper threshold for +DI breakout confirmation.
	/// </summary>
	public decimal PlusDiHighThreshold
	{
		get => _plusDiHighThreshold.Value;
		set => _plusDiHighThreshold.Value = value;
	}

	/// <summary>
	/// Lower threshold that previous +DI must stay below before breakout.
	/// </summary>
	public decimal PlusDiLowThreshold
	{
		get => _plusDiLowThreshold.Value;
		set => _plusDiLowThreshold.Value = value;
	}

	/// <summary>
	/// Required Tenkan/Kijun separation measured in price steps.
	/// </summary>
	public decimal BaselineDistanceThreshold
	{
		get => _baselineDistanceThreshold.Value;
		set => _baselineDistanceThreshold.Value = value;
	}

	/// <summary>
	/// Candle type used for Ichimoku evaluation and trading decisions.
	/// </summary>
	public DataType IchimokuCandleType
	{
		get => _ichimokuCandleType.Value;
		set => _ichimokuCandleType.Value = value;
	}

	/// <summary>
	/// Candle type used for ADX calculation.
	/// </summary>
	public DataType AdxCandleType
	{
		get => _adxCandleType.Value;
		set => _adxCandleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="ElliIchimokuAdxStrategy"/>.
	/// </summary>
	public ElliIchimokuAdxStrategy()
	{
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 60m)
			.SetDisplay("Take Profit", "Take profit distance in price steps", "Risk Management")
			.SetNotNegative();

		_stopLossPoints = Param(nameof(StopLossPoints), 30m)
			.SetDisplay("Stop Loss", "Stop loss distance in price steps", "Risk Management")
			.SetNotNegative();

		_tenkanPeriod = Param(nameof(TenkanPeriod), 19)
			.SetDisplay("Tenkan Period", "Tenkan-sen (conversion line) length", "Ichimoku")
			.SetGreaterThanZero();

		_kijunPeriod = Param(nameof(KijunPeriod), 60)
			.SetDisplay("Kijun Period", "Kijun-sen (base line) length", "Ichimoku")
			.SetGreaterThanZero();

		_senkouSpanBPeriod = Param(nameof(SenkouSpanBPeriod), 120)
			.SetDisplay("Senkou Span B Period", "Senkou Span B length", "Ichimoku")
			.SetGreaterThanZero();

		_adxPeriod = Param(nameof(AdxPeriod), 10)
			.SetDisplay("ADX Period", "Average Directional Index period", "ADX")
			.SetGreaterThanZero();

		_plusDiHighThreshold = Param(nameof(PlusDiHighThreshold), 10m)
			.SetDisplay("+DI High Threshold", "Level current +DI must exceed", "ADX")
			.SetGreaterThanZero();

		_plusDiLowThreshold = Param(nameof(PlusDiLowThreshold), 8m)
			.SetDisplay("+DI Low Threshold", "Level previous +DI must stay below", "ADX")
			.SetNotNegative();

		_baselineDistanceThreshold = Param(nameof(BaselineDistanceThreshold), 5m)
			.SetDisplay("Baseline Distance", "Minimum Tenkan/Kijun spread in steps", "Ichimoku")
			.SetNotNegative();

		_ichimokuCandleType = Param(nameof(IchimokuCandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Ichimoku Candle", "Candle series for Ichimoku", "General");

		_adxCandleType = Param(nameof(AdxCandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("ADX Candle", "Candle series for ADX", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, IchimokuCandleType);

		if (AdxCandleType != IchimokuCandleType)
			yield return (Security, AdxCandleType);
	}

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

		_previousPlusDi = null;
		_currentPlusDi = null;
		_isAdxReady = false;
		_previousAdxHigh = null;
		_previousAdxLow = null;
		_previousAdxClose = null;
		_smoothedTrueRange = 0m;
		_smoothedPlusDm = 0m;
		_adxSamples = 0;
	}

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

		_ichimoku = new Ichimoku
		{
			Tenkan = { Length = TenkanPeriod },
			Kijun = { Length = KijunPeriod },
			SenkouB = { Length = SenkouSpanBPeriod }
		};

		var ichimokuSubscription = SubscribeCandles(IchimokuCandleType);
		ichimokuSubscription.BindEx(_ichimoku, ProcessIchimoku);

		if (AdxCandleType == IchimokuCandleType)
		{
			ichimokuSubscription.Bind(ProcessAdxCandle);
			ichimokuSubscription.Start();
		}
		else
		{
			ichimokuSubscription.Start();

			var adxSubscription = SubscribeCandles(AdxCandleType);
			adxSubscription.Bind(ProcessAdxCandle).Start();
		}

		if (TakeProfitPoints > 0m || StopLossPoints > 0m)
		{
			StartProtection(
				StopLossPoints > 0m ? new Unit(StopLossPoints, UnitTypes.Absolute) : null,
				TakeProfitPoints > 0m ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null);
		}

		var priceArea = CreateChartArea();
		if (priceArea != null)
		{
			DrawCandles(priceArea, ichimokuSubscription);
			DrawIndicator(priceArea, _ichimoku);
			DrawOwnTrades(priceArea);
		}

	}

	private void ProcessAdxCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_previousAdxHigh is not decimal previousHigh ||
			_previousAdxLow is not decimal previousLow ||
			_previousAdxClose is not decimal previousClose)
		{
			_previousAdxHigh = candle.HighPrice;
			_previousAdxLow = candle.LowPrice;
			_previousAdxClose = candle.ClosePrice;
			return;
		}

		var upMove = candle.HighPrice - previousHigh;
		var downMove = previousLow - candle.LowPrice;
		var plusDm = upMove > downMove && upMove > 0m ? upMove : 0m;
		var trueRange = Math.Max(
			candle.HighPrice - candle.LowPrice,
			Math.Max(
				Math.Abs(candle.HighPrice - previousClose),
				Math.Abs(candle.LowPrice - previousClose)));

		if (_adxSamples < AdxPeriod)
		{
			_smoothedPlusDm += plusDm;
			_smoothedTrueRange += trueRange;
			_adxSamples++;
		}
		else
		{
			_smoothedPlusDm = _smoothedPlusDm - (_smoothedPlusDm / AdxPeriod) + plusDm;
			_smoothedTrueRange = _smoothedTrueRange - (_smoothedTrueRange / AdxPeriod) + trueRange;
		}

		if (_adxSamples >= AdxPeriod && _smoothedTrueRange > 0m)
		{
			_previousPlusDi = _currentPlusDi;
			_currentPlusDi = 100m * _smoothedPlusDm / _smoothedTrueRange;
			_isAdxReady = _previousPlusDi.HasValue;
		}

		_previousAdxHigh = candle.HighPrice;
		_previousAdxLow = candle.LowPrice;
		_previousAdxClose = candle.ClosePrice;
	}

	private void ProcessIchimoku(ICandleMessage candle, IIndicatorValue ichimokuValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_currentPlusDi is not decimal currentPlus || _previousPlusDi is not decimal previousPlus)
			return;

		if (!_isAdxReady)
			return;

		var ich = (IchimokuValue)ichimokuValue;

		if (ich.Tenkan is not decimal tenkan ||
			ich.Kijun is not decimal kijun ||
			ich.SenkouA is not decimal senkouA ||
			ich.SenkouB is not decimal senkouB)
		{
			return;
		}

		var priceStep = Security?.PriceStep ?? 1m;
		if (priceStep <= 0m)
			priceStep = 1m;

		var baselineDistance = Math.Abs(tenkan - kijun) / priceStep;
		var hasPlusDiBreakout = currentPlus > PlusDiHighThreshold && previousPlus >= PlusDiLowThreshold && currentPlus >= previousPlus;

		if (!hasPlusDiBreakout)
			return;

		if (baselineDistance < BaselineDistanceThreshold)
			return;

		if (Position != 0)
			return;

		var priceAboveCloud = senkouA > senkouB && kijun > senkouA && tenkan > kijun && candle.ClosePrice > kijun;
		var priceBelowCloud = senkouA < senkouB && kijun < senkouA && tenkan < kijun && candle.ClosePrice < kijun;

		if (priceAboveCloud)
		{
			this.LogInfo($"Bullish signal: Tenkan {tenkan:F2} > Kijun {kijun:F2}, cloud rising, +DI from {previousPlus:F2} to {currentPlus:F2}.");
			BuyMarket();
		}
		else if (priceBelowCloud)
		{
			this.LogInfo($"Bearish signal: Tenkan {tenkan:F2} < Kijun {kijun:F2}, cloud falling, +DI from {previousPlus:F2} to {currentPlus:F2}.");
			SellMarket();
		}
	}
}