在 GitHub 上查看

Elli Ichimoku ADX 策略

概述

该策略是 MetaTrader 5 专家顾问 “Elli”(barabashkakvn 修改版)的 StockSharp 移植版本。策略结合 Ichimoku 云结构和平均趋向指标(ADX)的 +DI 突破过滤,仅在价格动量足够强、并且 Ichimoku 关键线条呈现一致方向时入场。

在 StockSharp 中保留了双时间框架的设计:Ichimoku 分析默认在 1 小时 K 线完成后触发,ADX 则使用更快的 1 分钟数据流计算。下单使用市价,并附带以价格步长表示的固定止损和止盈,与原策略保持一致。

指标与数据

  • Ichimoku:默认 Tenkan 19、Kijun 60、Senkou Span B 120。
  • 平均趋向指数(ADX):仅使用 +DI 方向指标,与原版相同。
  • 图表区域可绘制价格 K 线、Ichimoku 云以及 ADX 线条。

建立两条独立的订阅:

  1. IchimokuCandleType(默认 1 小时)— 驱动 Ichimoku 计算及交易信号。
  2. AdxCandleType(默认 1 分钟)— 供 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 计算所用 K 线类型。
AdxCandleType 1 分钟 ADX 计算所用 K 线类型。

交易逻辑

  1. 等待 Ichimoku 订阅上的最新 K 线收盘。
  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 作为动量过滤,复现了原始 MQL5 代码(原作者注释掉了 -DI 分支)。
  • 未单独读取 Chikou Span,通过 Senkou Span A、B 的相对位置来验证云层方向。
  • 通过内部字段保存 +DI 最新两次数值,避免调用 GetValue,符合高层 API 规范。
  • 当两个时间框架相同时时,Ichimoku 与 ADX 共用同一个订阅,减少资源消耗。

使用建议

  • 若希望忠实复刻 MT5 表现,请保持 ADX 时间框架更快(如 M1)而 Ichimoku 较慢(如 H1)。
  • 在波动性较大的品种上可提高 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();
		}
	}
}