View on GitHub

Ichimoku Hurst Exponent

Trend strategy combining Tenkan/Kijun crossings, the Ichimoku cloud and a Hurst filter calculated from logarithmic returns. Signals use finished candles.

Calculation

Tenkan and Kijun are (highest High + lowest Low) / 2 over their respective windows. The unshifted Senkou A is their average; unshifted Senkou B is the high/low midpoint over SenkouSpanBPeriod. The strategy uses the causal displaced spans produced by StockSharp Ichimoku, with KijunPeriod displacement and its initialization buffer; no future candles are read.

For the latest HurstPeriod closes, calculate n = HurstPeriod - 1 log returns r[i] = ln(C[i] / C[i-1]). Let mu be their mean, X[k] = sum(r[i] - mu), R = max(X) - min(X) and S their population standard deviation. The filter is H = ln(R / S) / ln(n), using StockSharp HurstExponent on these returns. The window starts with the first finished candle. At least eleven closes, a complete window and nonzero variance are required. A nonpositive close restarts the return window; an unavailable Hurst value never enables an entry.

Trading

  • Long entry: flat position, close strictly above both cloud spans, Tenkan crossing from <= Kijun to > Kijun, H > HurstThreshold, positive formed ATR and expired cooldown.
  • Short entry: flat position, close strictly below both spans and the opposite Tenkan/Kijun crossing, with the same Hurst, ATR and cooldown conditions.
  • Signal exit: close the entire long when the close is below both spans or Tenkan crosses down; close the entire short on the symmetric conditions. These exits remain active during cooldown and do not require the Hurst entry filter.
  • Orders: market entries of Volume and exits of the actual remaining position. An uncompleted order blocks another signal order. An exit closes the position before a later entry.
  • Cooldown: each signal order restarts SignalCooldownBars; the next entry can occur that many finished candles later.

Protection

Wilder ATR uses AtrPeriod. Each entry freezes a stop distance of StopLossAtr * ATR from its signal candle, anchored at the actual entry execution price. Local market protection processes Level1 updates and can flatten the position between signal candles. The distance does not trail subsequent ATR values. StopLossAtr = 0 disables protection for the run. There is no take-profit or trailing stop.

Defaults

Parameter Default
TenkanPeriod 9
KijunPeriod 26
SenkouSpanBPeriod 52
HurstPeriod 100 closes
HurstThreshold 0.5
SignalCooldownBars 6
AtrPeriod 14
StopLossAtr 2
CandleType 15 minutes

Tests compare every signal order and its fills with independently calculated cloud spans and log-return R/S values on the packaged BTC/TON archive. Separate runs verify Hurst warm-up, frozen ATR distances and real intrabar protective fills.

The archive protection comparison uses Tenkan/Kijun/Senkou B periods 15/40/70 and HurstThreshold 0.45 to exercise a stop at the default 2 × ATR distance; the default 9/26/52 signals are checked separately. Protection runs use an entry volume of 3.

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Ichimoku cloud crossings filtered by the rescaled range of logarithmic returns, with entry-frozen ATR protection.
/// </summary>
public class IchimokuHurstStrategy : Strategy
{
	private readonly StrategyParam<int> _tenkanPeriod;
	private readonly StrategyParam<int> _kijunPeriod;
	private readonly StrategyParam<int> _senkouSpanBPeriod;
	private readonly StrategyParam<int> _hurstPeriod;
	private readonly StrategyParam<decimal> _hurstThreshold;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _signalCooldownBars;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _stopLossAtr;

	private Ichimoku _ichimoku;
	private HurstExponent _hurst;
	private AverageTrueRange _atr;
	private decimal? _previousClose;
	private decimal? _previousTenkan;
	private decimal? _previousKijun;
	private int _cooldownRemaining;
	private Order _pendingOrder;

	/// <summary>Tenkan rolling high/low midpoint period.</summary>
	public int TenkanPeriod
	{
		get => _tenkanPeriod.Value;
		set => _tenkanPeriod.Value = value;
	}

	/// <summary>Kijun midpoint period and cloud displacement.</summary>
	public int KijunPeriod
	{
		get => _kijunPeriod.Value;
		set => _kijunPeriod.Value = value;
	}

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

	/// <summary>Number of closes supplying the Hurst log-return window.</summary>
	public int HurstPeriod
	{
		get => _hurstPeriod.Value;
		set => _hurstPeriod.Value = value;
	}

	/// <summary>Strict lower threshold for a formed Hurst estimate.</summary>
	public decimal HurstThreshold
	{
		get => _hurstThreshold.Value;
		set => _hurstThreshold.Value = value;
	}

	/// <summary>Signal candle type.</summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>Finished candles between signal orders; exits bypass this entry restriction.</summary>
	public int SignalCooldownBars
	{
		get => _signalCooldownBars.Value;
		set => _signalCooldownBars.Value = value;
	}

	/// <summary>Wilder ATR period.</summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>ATR stop distance multiplier; zero disables protection for the run.</summary>
	public decimal StopLossAtr
	{
		get => _stopLossAtr.Value;
		set => _stopLossAtr.Value = value;
	}

	/// <summary>Initializes the strategy parameters.</summary>
	public IchimokuHurstStrategy()
	{
		_tenkanPeriod = Param(nameof(TenkanPeriod), 9)
			.SetDisplay("Tenkan Period", "Tenkan rolling high/low midpoint period", "Ichimoku")
			.SetOptimize(5, 15, 1);
		_kijunPeriod = Param(nameof(KijunPeriod), 26)
			.SetDisplay("Kijun Period", "Kijun midpoint period and cloud displacement", "Ichimoku")
			.SetOptimize(20, 40, 2);
		_senkouSpanBPeriod = Param(nameof(SenkouSpanBPeriod), 52)
			.SetDisplay("Senkou Span B Period", "Senkou B rolling high/low midpoint period", "Ichimoku")
			.SetOptimize(40, 70, 5);
		_hurstPeriod = Param(nameof(HurstPeriod), 100)
			.SetDisplay("Hurst Period", "Closes in the Hurst log-return window", "Hurst Exponent")
			.SetOptimize(50, 200, 10);
		_hurstThreshold = Param(nameof(HurstThreshold), 0.5m)
			.SetDisplay("Hurst Threshold", "Strict lower threshold for the Hurst estimate", "Hurst Exponent")
			.SetOptimize(0.45m, 0.6m, 0.05m);
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Signal candle type", "General");
		_signalCooldownBars = Param(nameof(SignalCooldownBars), 6)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Finished candles between signal orders; exits remain active", "Trading");
		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "Wilder ATR period", "Risk");
		_stopLossAtr = Param(nameof(StopLossAtr), 2m)
			.SetNotNegative()
			.SetDisplay("ATR Stop", "Entry ATR multiplier; zero disables the stop for the run", "Risk");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		if (Security is not null)
			yield return (Security, CandleType);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_ichimoku = null;
		_hurst = null;
		_atr = null;
		_previousClose = null;
		_previousTenkan = null;
		_previousKijun = null;
		_cooldownRemaining = 0;
		_pendingOrder = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		if (Security is null)
			throw new InvalidOperationException("Security is not specified.");
		if (HurstPeriod < 11)
			throw new ArgumentOutOfRangeException(nameof(HurstPeriod), "At least eleven closes are required for ten log returns.");

		_ichimoku = new Ichimoku
		{
			Tenkan = { Length = TenkanPeriod },
			Kijun = { Length = KijunPeriod },
			SenkouB = { Length = SenkouSpanBPeriod },
		};
		_hurst = new HurstExponent { Length = HurstPeriod - 1 };
		_atr = new AverageTrueRange { Length = AtrPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.BindEx(_ichimoku, _atr, ProcessCandle, allowEmpty: true).Start();

		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quotes = new Subscription(DataType.Level1, Security);
			quotes.MarketData.BuildField = field;
			SubscribeLevel1(quotes).Start();
		}

		var area = CreateChartArea();
		if (area is not null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _ichimoku);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue cloudValue, IIndicatorValue atrValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		decimal? hurst = null;
		if (candle.ClosePrice > 0m)
		{
			if (_previousClose is decimal previous)
			{
				var value = _hurst.Process((decimal)Math.Log((double)(candle.ClosePrice / previous)), candle.OpenTime, true);
				if (!value.IsEmpty)
					hurst = value.GetValue<decimal>();
			}
			_previousClose = candle.ClosePrice;
		}
		else
		{
			_previousClose = null;
			_hurst.Reset();
		}

		if (!IsFormedAndOnlineAndAllowTrading()
			|| cloudValue is not IIchimokuValue { Tenkan: decimal tenkan, Kijun: decimal kijun, SenkouA: decimal spanA, SenkouB: decimal spanB })
			return;

		var crossUp = _previousTenkan is decimal oldTenkan && _previousKijun is decimal oldKijun
			&& oldTenkan <= oldKijun && tenkan > kijun;
		var crossDown = _previousTenkan is decimal oldFast && _previousKijun is decimal oldSlow
			&& oldFast >= oldSlow && tenkan < kijun;
		_previousTenkan = tenkan;
		_previousKijun = kijun;
		if (_cooldownRemaining > 0)
			_cooldownRemaining--;
		if (_pendingOrder is { State: not (OrderStates.Done or OrderStates.Failed) })
			return;

		var above = candle.ClosePrice > Math.Max(spanA, spanB);
		var below = candle.ClosePrice < Math.Min(spanA, spanB);
		if (Position > 0m && (below || crossDown))
			SubmitOrder(Sides.Sell, Position, "Ichimoku Hurst exit");
		else if (Position < 0m && (above || crossUp))
			SubmitOrder(Sides.Buy, Math.Abs(Position), "Ichimoku Hurst exit");
		else if (Position == 0m && _cooldownRemaining == 0 && hurst is decimal estimate && estimate > HurstThreshold
			&& !atrValue.IsEmpty && atrValue.GetValue<decimal>() > 0m && (above && crossUp || below && crossDown))
		{
			var distance = atrValue.GetValue<decimal>() * StopLossAtr;
			StartProtection(new Unit(), new Unit(distance, UnitTypes.Absolute), useMarketOrders: true, isLocalStop: true);
			SubmitOrder(crossUp ? Sides.Buy : Sides.Sell, Volume, "Ichimoku Hurst entry");
		}
		else
			return;

		_cooldownRemaining = SignalCooldownBars;
	}

	private void SubmitOrder(Sides side, decimal volume, string comment)
	{
		_pendingOrder = CreateOrder(side, 0m, volume);
		_pendingOrder.Type = OrderTypes.Market;
		_pendingOrder.Comment = comment;
		RegisterOrder(_pendingOrder);
	}
}