Ichimoku Hurst Exponent
Trendstrategie aus Tenkan/Kijun-Kreuzungen, Ichimoku-Wolke und einem Hurst-Filter auf logarithmischen Renditen. Signale verwenden abgeschlossene Kerzen.
Berechnung
Tenkan und Kijun sind (höchstes High + niedrigstes Low) / 2 ihres jeweiligen Fensters. Die unverschobene Senkou A ist ihr Mittelwert, Senkou B der Mittelpunkt über SenkouSpanBPeriod. Verwendet werden die kausal verschobenen Linien von StockSharp Ichimoku mit KijunPeriod Verschiebung und dessen Initialisierungspuffer; zukünftige Kerzen werden nicht gelesen.
Aus den letzten HurstPeriod Schlusskursen entstehen n = HurstPeriod - 1 Renditen r[i] = ln(C[i] / C[i-1]). Mit Mittelwert mu, X[k] = sum(r[i] - mu), R = max(X) - min(X) und Populationsstandardabweichung S gilt H = ln(R / S) / ln(n). StockSharp HurstExponent verarbeitet diese Renditen ab der ersten abgeschlossenen Kerze. Mindestens elf Schlusskurse, ein vollständiges Fenster und positive Varianz sind erforderlich. Ein nichtpositiver Schlusskurs startet das Fenster neu; ohne gültigen Hurst-Wert ist kein Einstieg erlaubt.
Handel
- Long: keine Position, Schlusskurs strikt über beiden Wolkenlinien, Tenkan kreuzt Kijun von
<= nach >, H > HurstThreshold, gebildeter positiver ATR und abgelaufene Wartezeit.
- Short: keine Position, Schlusskurs strikt unter beiden Linien und umgekehrte Kreuzung; dieselben Hurst-, ATR- und Wartebedingungen gelten.
- Signalausstieg: den gesamten Long unter beiden Linien oder bei Abwärtskreuzung schließen; für Short gelten die umgekehrten Bedingungen. Ausstiege bleiben während der Wartezeit aktiv und benötigen den Hurst-Einstiegsfilter nicht.
- Orders: Markteinstieg mit
Volume, Ausstieg mit der tatsächlichen Restposition. Eine noch nicht abgeschlossene Order blockiert weitere Signalorders. Ein Ausstieg schließt vor einem späteren Neueinstieg.
- Wartezeit: jede Signalorder startet
SignalCooldownBars neu; nach so vielen abgeschlossenen Kerzen ist der nächste Einstieg möglich.
Schutz
Wilder-ATR verwendet AtrPeriod. Jeder Einstieg fixiert StopLossAtr * ATR der Signalkerze als Abstand zum tatsächlichen Ausführungskurs. Lokaler Schutz verarbeitet Level1 und kann die Position zwischen Signalkerzen per Marketorder schließen. Spätere ATR-Werte verändern den Abstand nicht. StopLossAtr = 0 deaktiviert den Schutz für den Lauf. Es gibt weder Take-Profit noch Trailing-Stop.
Standardwerte
| Parameter |
Standardwert |
TenkanPeriod |
9 |
KijunPeriod |
26 |
SenkouSpanBPeriod |
52 |
HurstPeriod |
100 Schlusskurse |
HurstThreshold |
0.5 |
SignalCooldownBars |
6 |
AtrPeriod |
14 |
StopLossAtr |
2 |
CandleType |
15 Minuten |
Tests vergleichen jede Signalorder und ihre Ausführungen mit unabhängig berechneten Wolkenlinien und Rendite-R/S auf dem mitgelieferten BTC/TON-Archiv. Separate Läufe prüfen Hurst-Aufwärmung, feste ATR-Abstände und tatsächliche Schutz-Ausführungen innerhalb der Kerze.
Der Schutzvergleich im Archiv verwendet Tenkan/Kijun/Senkou B 15/40/70 und HurstThreshold 0.45, damit der Standardabstand 2 × ATR tatsächlich ausgelöst wird. Die Standardsignale 9/26/52 werden separat geprüft. Schutzläufe verwenden Einstiegsvolumen 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()