Открыть на GitHub

Стратегия Elli Ichimoku ADX

Описание

Порт эксперта MetaTrader 5 «Elli» (редакция barabashkakvn) на платформу StockSharp. Стратегия объединяет индикатор Ichimoku Kinko Hyo и фильтр по линии +DI индекса направленного движения (ADX). Сделки открываются только при наличии мощного импульса, подтверждённого упорядоченным расположением линий Ichimoku и резким ростом положительного направленного индекса.

В реализации для StockSharp сохранена работа с двумя потоками свечей: анализ Ichimoku выполняется на старшем таймфрейме (по умолчанию 1 час), а ADX рассчитывается на более быстром ряду (по умолчанию 1 минута). Рыночные заявки сопровождаются фиксированными стоп-лоссом и тейк-профитом в шагах цены, как в исходном советнике.

Индикаторы и данные

  • Ichimoku (Tenkan 19, Kijun 60, Senkou Span B 120 по умолчанию).
  • Average Directional Index (ADX) – используется только линия +DI, как в оригинале.
  • На графике отображаются свечи выбранного таймфрейма, облако Ichimoku и линия ADX.

Создаются две подписки на свечи:

  1. IchimokuCandleType (по умолчанию H1) – для расчёта Ichimoku и генерации сигналов.
  2. AdxCandleType (по умолчанию M1) – для расчёта ADX и хранения текущего/предыдущего значения +DI.

Параметры

Параметр Значение по умолчанию Назначение
TakeProfitPoints 60 Дистанция тейк-профита в шагах цены. 0 – отключить.
StopLossPoints 30 Дистанция стоп-лосса в шагах цены. 0 – отключить.
TenkanPeriod 19 Период линии Tenkan-sen.
KijunPeriod 60 Период линии Kijun-sen.
SenkouSpanBPeriod 120 Период линии Senkou Span B.
AdxPeriod 10 Период индикатора ADX.
PlusDiHighThreshold 13 Минимальное значение текущего +DI.
PlusDiLowThreshold 6 Максимальное значение предыдущего +DI.
BaselineDistanceThreshold 20 Минимальный зазор между Tenkan и Kijun в шагах цены.
IchimokuCandleType Свечи 1 часа Таймфрейм для расчёта Ichimoku.
AdxCandleType Свечи 1 минуты Таймфрейм для расчёта ADX.

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

  1. Дождаться закрытия свечи Ichimoku.
  2. Проверить наличие двух последних значений ADX и факта пробоя по +DI (предыдущее +DI < PlusDiLowThreshold и текущее +DI > PlusDiHighThreshold).
  3. Перевести расстояние между Tenkan и Kijun в шаги цены и убедиться, что оно превышает BaselineDistanceThreshold.
  4. Если уже есть открытая позиция, новые заявки не выставляются.
  5. Покупка выполняется, когда:
    • Tenkan > Kijun;
    • Kijun > Senkou Span A;
    • Senkou Span A > Senkou Span B (бычье облако);
    • Цена закрытия > Kijun.
  6. Продажа выполняется при зеркальной конфигурации (Tenkan < Kijun < Senkou Span A < Senkou Span B и цена закрытия ниже Kijun).
  7. Выход из позиции происходит по защитным ордерам, устанавливаемым через StartProtection. Дополнительные условия выхода не используются, что соответствует оригинальному советнику.

Управление риском

Метод StartProtection запускается один раз при старте. Если стоп или тейк установлен в 0, соответствующий ордер не активируется. Заявки размещаются рыночными ордерами (BuyMarket/SellMarket) с заданными уровнями SL/TP.

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

  • Входы в обе стороны фильтруются только по +DI, поскольку в исходном коде блок с -DI был закомментирован.
  • Линия Chikou напрямую не контролируется: для проверки структуры облака достаточно соотношения Senkou Span A и B.
  • Последние значения +DI сохраняются во внутренних переменных, без обращения к GetValue, что соответствует рекомендациям по высокоуровневому API.
  • Если таймфреймы Ichimoku и ADX совпадают, используется одна подписка на свечи.

Рекомендации по применению

  • Для воспроизведения оригинала оставьте быстрый ADX (AdxCandleType) и более медленный Ichimoku (IchimokuCandleType).
  • Увеличивайте BaselineDistanceThreshold на волатильных рынках, чтобы требовать большего отрыва Tenkan от Kijun.
  • Советник открывает только одну позицию, поэтому при работе с несколькими инструментами добавьте портфельный контроль риска.
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();
		}
	}
}