一目均衡表与赫斯特指数
趋势策略结合转换线与基准线交叉、一目均衡表云区,以及基于对数收益率的赫斯特过滤器。信号仅使用已完成的蜡烛。
计算
转换线 Tenkan 和基准线 Kijun 分别为各自窗口的 (最高 High + 最低 Low) / 2。未位移的先行带 A 是两线的平均值,先行带 B 是 SenkouSpanBPeriod 窗口的高低点中值。策略使用 StockSharp Ichimoku 生成的因果位移云线,位移为 KijunPeriod,并遵循其初始化缓冲规则;不读取未来蜡烛。
最近 HurstPeriod 个收盘价产生 n = HurstPeriod - 1 个收益率 r[i] = ln(C[i] / C[i-1])。令 mu 为均值、X[k] = sum(r[i] - mu)、R = max(X) - min(X)、S 为总体标准差,则 H = ln(R / S) / ln(n)。StockSharp HurstExponent 对这些收益率进行计算,从第一根完成的蜡烛开始积累。至少需要十一個收盘价、完整窗口和非零方差。非正收盘价会重启窗口;没有有效赫斯特值时禁止开仓。
交易规则
- 做多: 当前空仓,收盘价严格高于两条云线,Tenkan 从
<= Kijun上穿为> Kijun,H > HurstThreshold,ATR 已形成且为正,冷却结束。 - 做空: 当前空仓,收盘价严格低于两条云线且发生反向交叉;赫斯特、ATR 和冷却条件相同。
- 信号平仓: 多头在收盘价低于两条云线或 Tenkan 下穿时全部平仓;空头按对称条件全部平仓。冷却期间仍允许退出,退出不依赖入场的赫斯特过滤器。
- 委托: 按
Volume市价开仓,按实际剩余持仓量退出。未完成的委托会阻止下一笔信号委托。先完成平仓,之后才允许重新入场。 - 冷却: 每笔信号委托重置
SignalCooldownBars;经过相应数量的已完成蜡烛后允许再次开仓。
保护
Wilder ATR 使用 AtrPeriod。每次开仓固定信号蜡烛的 StopLossAtr * ATR 距离,以实际开仓成交价为基准。本地保护处理 Level1 更新,可在信号蜡烛之间通过市价委托平仓。后续 ATR 不改变该距离。StopLossAtr = 0 表示本次运行禁用保护。不设置止盈或移动止损。
默认参数
| 参数 | 默认值 |
|---|---|
TenkanPeriod |
9 |
KijunPeriod |
26 |
SenkouSpanBPeriod |
52 |
HurstPeriod |
100 个收盘价 |
HurstThreshold |
0.5 |
SignalCooldownBars |
6 |
AtrPeriod |
14 |
StopLossAtr |
2 |
CandleType |
15 分钟 |
测试在附带的 BTC/TON 历史数据上,用独立计算的云线和收益率 R/S 核对每笔信号委托及成交;另行验证赫斯特预热、固定 ATR 距离以及蜡烛内部的真实保护成交。
历史数据中的保护对比采用 Tenkan/Kijun/Senkou B 15/40/70 和 HurstThreshold 0.45,以实际触发默认的 2 × ATR 止损距离;默认 9/26/52 信号另行核对。保护测试的开仓量为 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()