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 线条。
建立两条独立的订阅:
IchimokuCandleType(默认 1 小时)— 驱动 Ichimoku 计算及交易信号。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 线类型。 |
交易逻辑
- 等待 Ichimoku 订阅上的最新 K 线收盘。
- 确认 ADX 已提供最近两条数据,并出现 +DI 突破:
上一条 +DI < PlusDiLowThreshold且当前 +DI > PlusDiHighThreshold。 - 将 Tenkan 与 Kijun 的差值换算为价格步长,确保大于
BaselineDistanceThreshold。 - 如果当前已有持仓,则跳过所有新信号。
- 做多条件:Tenkan > Kijun,Kijun > Senkou Span A,Senkou Span A > Senkou Span B(云层上升),且收盘价 > Kijun。
- 做空条件:上述条件完全反向(Tenkan < Kijun < Senkou Span A < Senkou Span B,且收盘价 < Kijun)。
- 平仓依赖
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();
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import Ichimoku
from StockSharp.Algo.Strategies import Strategy
class elli_ichimoku_adx_strategy(Strategy):
def __init__(self):
super(elli_ichimoku_adx_strategy, self).__init__()
self._take_profit_points = self.Param("TakeProfitPoints", 60.0)
self._stop_loss_points = self.Param("StopLossPoints", 30.0)
self._tenkan_period = self.Param("TenkanPeriod", 19)
self._kijun_period = self.Param("KijunPeriod", 60)
self._senkou_span_b_period = self.Param("SenkouSpanBPeriod", 120)
self._adx_period = self.Param("AdxPeriod", 10)
self._plus_di_high_threshold = self.Param("PlusDiHighThreshold", 10.0)
self._plus_di_low_threshold = self.Param("PlusDiLowThreshold", 8.0)
self._baseline_distance_threshold = self.Param("BaselineDistanceThreshold", 5.0)
self._ichimoku_candle_type = self.Param("IchimokuCandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30)))
self._adx_candle_type = self.Param("AdxCandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._previous_plus_di = None
self._current_plus_di = None
self._is_adx_ready = False
self._previous_adx_high = None
self._previous_adx_low = None
self._previous_adx_close = None
self._smoothed_true_range = 0.0
self._smoothed_plus_dm = 0.0
self._adx_samples = 0
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def TenkanPeriod(self):
return self._tenkan_period.Value
@TenkanPeriod.setter
def TenkanPeriod(self, value):
self._tenkan_period.Value = value
@property
def KijunPeriod(self):
return self._kijun_period.Value
@KijunPeriod.setter
def KijunPeriod(self, value):
self._kijun_period.Value = value
@property
def SenkouSpanBPeriod(self):
return self._senkou_span_b_period.Value
@SenkouSpanBPeriod.setter
def SenkouSpanBPeriod(self, value):
self._senkou_span_b_period.Value = value
@property
def AdxPeriod(self):
return self._adx_period.Value
@AdxPeriod.setter
def AdxPeriod(self, value):
self._adx_period.Value = value
@property
def PlusDiHighThreshold(self):
return self._plus_di_high_threshold.Value
@PlusDiHighThreshold.setter
def PlusDiHighThreshold(self, value):
self._plus_di_high_threshold.Value = value
@property
def PlusDiLowThreshold(self):
return self._plus_di_low_threshold.Value
@PlusDiLowThreshold.setter
def PlusDiLowThreshold(self, value):
self._plus_di_low_threshold.Value = value
@property
def BaselineDistanceThreshold(self):
return self._baseline_distance_threshold.Value
@BaselineDistanceThreshold.setter
def BaselineDistanceThreshold(self, value):
self._baseline_distance_threshold.Value = value
@property
def IchimokuCandleType(self):
return self._ichimoku_candle_type.Value
@IchimokuCandleType.setter
def IchimokuCandleType(self, value):
self._ichimoku_candle_type.Value = value
@property
def AdxCandleType(self):
return self._adx_candle_type.Value
@AdxCandleType.setter
def AdxCandleType(self, value):
self._adx_candle_type.Value = value
def OnStarted2(self, time):
super(elli_ichimoku_adx_strategy, self).OnStarted2(time)
self._ichimoku = Ichimoku()
self._ichimoku.Tenkan.Length = self.TenkanPeriod
self._ichimoku.Kijun.Length = self.KijunPeriod
self._ichimoku.SenkouB.Length = self.SenkouSpanBPeriod
self._previous_plus_di = None
self._current_plus_di = None
self._is_adx_ready = False
self._previous_adx_high = None
self._previous_adx_low = None
self._previous_adx_close = None
self._smoothed_true_range = 0.0
self._smoothed_plus_dm = 0.0
self._adx_samples = 0
ichi_sub = self.SubscribeCandles(self.IchimokuCandleType)
ichi_sub.BindEx(self._ichimoku, self._process_ichimoku)
if str(self.AdxCandleType) == str(self.IchimokuCandleType):
ichi_sub.Bind(self._process_adx_candle)
ichi_sub.Start()
else:
ichi_sub.Start()
adx_sub = self.SubscribeCandles(self.AdxCandleType)
adx_sub.Bind(self._process_adx_candle).Start()
tp = float(self.TakeProfitPoints)
sl = float(self.StopLossPoints)
sl_unit = Unit(sl, UnitTypes.Absolute) if sl > 0.0 else None
tp_unit = Unit(tp, UnitTypes.Absolute) if tp > 0.0 else None
if tp > 0.0 or sl > 0.0:
self.StartProtection(sl_unit, tp_unit)
def _process_adx_candle(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self._previous_adx_high is None or self._previous_adx_low is None or self._previous_adx_close is None:
self._previous_adx_high = high
self._previous_adx_low = low
self._previous_adx_close = close
return
up_move = high - self._previous_adx_high
down_move = self._previous_adx_low - low
plus_dm = up_move if (up_move > down_move and up_move > 0.0) else 0.0
true_range = max(high - low, max(abs(high - self._previous_adx_close), abs(low - self._previous_adx_close)))
adx_period = int(self.AdxPeriod)
if self._adx_samples < adx_period:
self._smoothed_plus_dm += plus_dm
self._smoothed_true_range += true_range
self._adx_samples += 1
else:
self._smoothed_plus_dm = self._smoothed_plus_dm - (self._smoothed_plus_dm / adx_period) + plus_dm
self._smoothed_true_range = self._smoothed_true_range - (self._smoothed_true_range / adx_period) + true_range
if self._adx_samples >= adx_period and self._smoothed_true_range > 0.0:
self._previous_plus_di = self._current_plus_di
self._current_plus_di = 100.0 * self._smoothed_plus_dm / self._smoothed_true_range
self._is_adx_ready = self._previous_plus_di is not None
self._previous_adx_high = high
self._previous_adx_low = low
self._previous_adx_close = close
def _process_ichimoku(self, candle, ichimoku_value):
if candle.State != CandleStates.Finished:
return
if self._current_plus_di is None or self._previous_plus_di is None:
return
if not self._is_adx_ready:
return
tenkan = ichimoku_value.Tenkan
kijun = ichimoku_value.Kijun
senkou_a = ichimoku_value.SenkouA
senkou_b = ichimoku_value.SenkouB
if tenkan is None or kijun is None or senkou_a is None or senkou_b is None:
return
tenkan_val = float(tenkan)
kijun_val = float(kijun)
senkou_a_val = float(senkou_a)
senkou_b_val = float(senkou_b)
close = float(candle.ClosePrice)
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if price_step <= 0.0:
price_step = 1.0
baseline_distance = abs(tenkan_val - kijun_val) / price_step
di_high = float(self.PlusDiHighThreshold)
di_low = float(self.PlusDiLowThreshold)
has_plus_di_breakout = (self._current_plus_di > di_high and
self._previous_plus_di >= di_low and
self._current_plus_di >= self._previous_plus_di)
if not has_plus_di_breakout:
return
if baseline_distance < float(self.BaselineDistanceThreshold):
return
if self.Position != 0:
return
price_above_cloud = (senkou_a_val > senkou_b_val and kijun_val > senkou_a_val and
tenkan_val > kijun_val and close > kijun_val)
price_below_cloud = (senkou_a_val < senkou_b_val and kijun_val < senkou_a_val and
tenkan_val < kijun_val and close < kijun_val)
if price_above_cloud:
self.BuyMarket()
elif price_below_cloud:
self.SellMarket()
def OnReseted(self):
super(elli_ichimoku_adx_strategy, self).OnReseted()
self._previous_plus_di = None
self._current_plus_di = None
self._is_adx_ready = False
self._previous_adx_high = None
self._previous_adx_low = None
self._previous_adx_close = None
self._smoothed_true_range = 0.0
self._smoothed_plus_dm = 0.0
self._adx_samples = 0
def CreateClone(self):
return elli_ichimoku_adx_strategy()