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);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import ArgumentOutOfRangeException, Decimal, InvalidOperationException, Math, TimeSpan
from StockSharp.BusinessEntities import Subscription
from StockSharp.Messages import CandleStates, DataType, Level1Fields, OrderStates, OrderTypes, Sides, Unit, UnitTypes
from StockSharp.Algo.Indicators import AverageTrueRange, HurstExponent, Ichimoku, IndicatorHelper
from StockSharp.Algo.Strategies import Strategy
class ichimoku_hurst_exponent_strategy(Strategy):
"""Ichimoku cloud crossings, formed log-return Hurst filtering and frozen ATR protection."""
def __init__(self):
super(ichimoku_hurst_exponent_strategy, self).__init__()
self._tenkan_period = self.Param("TenkanPeriod", 9) \
.SetDisplay("Tenkan Period", "Tenkan rolling high/low midpoint period", "Ichimoku")
self._kijun_period = self.Param("KijunPeriod", 26) \
.SetDisplay("Kijun Period", "Kijun midpoint period and cloud displacement", "Ichimoku")
self._senkou_spanb_period = self.Param("SenkouSpanBPeriod", 52) \
.SetDisplay("Senkou Span B Period", "Senkou B rolling high/low midpoint period", "Ichimoku")
self._hurst_period = self.Param("HurstPeriod", 100) \
.SetDisplay("Hurst Period", "Closes in the Hurst log-return window", "Hurst Exponent")
self._hurst_threshold = self.Param("HurstThreshold", 0.5) \
.SetDisplay("Hurst Threshold", "Strict lower threshold for the Hurst estimate", "Hurst Exponent")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Signal candle type", "General")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 6) \
.SetGreaterThanZero() \
.SetDisplay("Signal Cooldown", "Finished candles between signal orders; exits remain active", "Trading")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "Wilder ATR period", "Risk")
self._stop_loss_atr = self.Param("StopLossAtr", 2.0) \
.SetNotNegative() \
.SetDisplay("ATR Stop", "Entry ATR multiplier; zero disables the stop for the run", "Risk")
self._ichimoku = None
self._hurst = None
self._atr = None
self._previous_close = None
self._previous_tenkan = None
self._previous_kijun = None
self._cooldown_remaining = 0
self._pending_order = None
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)] if self.Security is not None else []
def OnReseted(self):
super(ichimoku_hurst_exponent_strategy, self).OnReseted()
self._ichimoku = None
self._hurst = None
self._atr = None
self._previous_close = None
self._previous_tenkan = None
self._previous_kijun = None
self._cooldown_remaining = 0
self._pending_order = None
def OnStarted2(self, time):
super(ichimoku_hurst_exponent_strategy, self).OnStarted2(time)
if self.Security is None:
raise InvalidOperationException("Security is not specified.")
period = int(self._hurst_period.Value)
if period < 11:
raise ArgumentOutOfRangeException("HurstPeriod", "At least eleven closes are required for ten log returns.")
self._ichimoku = Ichimoku()
self._ichimoku.Tenkan.Length = int(self._tenkan_period.Value)
self._ichimoku.Kijun.Length = int(self._kijun_period.Value)
self._ichimoku.SenkouB.Length = int(self._senkou_spanb_period.Value)
self._hurst = HurstExponent()
self._hurst.Length = period - 1
self._atr = AverageTrueRange()
self._atr.Length = int(self._atr_period.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(self._ichimoku, self._atr, self._process_candle, True).Start()
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._ichimoku)
self.DrawOwnTrades(area)
def _process_candle(self, candle, cloud, atr_value):
if candle.State != CandleStates.Finished:
return
close = candle.ClosePrice
hurst = None
if close > 0:
if self._previous_close is not None:
log_return = Decimal(Math.Log(float(close / self._previous_close)))
value = IndicatorHelper.Process(self._hurst, log_return, candle.OpenTime, True)
if not value.IsEmpty:
hurst = value.GetValue[Decimal](None)
self._previous_close = close
else:
self._previous_close = None
self._hurst.Reset()
if not self.IsFormedAndOnlineAndAllowTrading():
return
tenkan, kijun, span_a, span_b = cloud.Tenkan, cloud.Kijun, cloud.SenkouA, cloud.SenkouB
if any(value is None for value in (tenkan, kijun, span_a, span_b)):
return
cross_up = self._previous_tenkan is not None and self._previous_kijun is not None \
and self._previous_tenkan <= self._previous_kijun and tenkan > kijun
cross_down = self._previous_tenkan is not None and self._previous_kijun is not None \
and self._previous_tenkan >= self._previous_kijun and tenkan < kijun
self._previous_tenkan, self._previous_kijun = tenkan, kijun
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
above = close > max(span_a, span_b)
below = close < min(span_a, span_b)
if self.Position > 0 and (below or cross_down):
self._submit_order(Sides.Sell, self.Position, "Ichimoku Hurst exit")
elif self.Position < 0 and (above or cross_up):
self._submit_order(Sides.Buy, Math.Abs(self.Position), "Ichimoku Hurst exit")
elif self.Position == 0 and self._cooldown_remaining == 0 \
and hurst is not None and hurst > Decimal(self._hurst_threshold.Value) \
and not atr_value.IsEmpty and atr_value.GetValue[Decimal](None) > 0 \
and ((above and cross_up) or (below and cross_down)):
distance = atr_value.GetValue[Decimal](None) * Decimal(self._stop_loss_atr.Value)
self.StartProtection(Unit(), Unit(distance, UnitTypes.Absolute), useMarketOrders=True, isLocalStop=True)
self._submit_order(Sides.Buy if cross_up else Sides.Sell, self.Volume, "Ichimoku Hurst entry")
else:
return
self._cooldown_remaining = int(self._signal_cooldown_bars.Value)
def _submit_order(self, side, volume, comment):
self._pending_order = self.CreateOrder(side, Decimal(0), volume)
self._pending_order.Type = OrderTypes.Market
self._pending_order.Comment = comment
self.RegisterOrder(self._pending_order)
def CreateClone(self):
return ichimoku_hurst_exponent_strategy()