在 GitHub 上查看

一目均衡表与赫斯特指数

趋势策略结合转换线与基准线交叉、一目均衡表云区,以及基于对数收益率的赫斯特过滤器。信号仅使用已完成的蜡烛。

计算

转换线 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);
	}
}