View on GitHub

Hurst Exponent Reversion Strategy

This approach uses the Hurst exponent to detect when a market is behaving in a mean-reverting manner. Values below 0.5 suggest price tends to return toward its average, creating opportunities to fade extremes.

Testing indicates an average annual return of about 121%. It performs best in the crypto market.

A long position is opened when the Hurst exponent is below HurstThreshold and price closes under a moving average. A short position occurs when the Hurst value is below HurstThreshold and price closes above the average. Positions exit when price returns to the average line or the Hurst exponent rises above the threshold.

The strategy fits traders who favour statistical tendencies over strong trends. A protective stop-loss shields against extended moves that fail to revert.

Details

  • Entry Criteria:
    • Long: Hurst < HurstThreshold && Close < MA
    • Short: Hurst < HurstThreshold && Close > MA
  • Long/Short: Both sides.
  • Exit Criteria:
    • Long: Exit when Close >= MA or Hurst > HurstThreshold
    • Short: Exit when Close <= MA or Hurst > HurstThreshold
  • Stops: Yes, percent stop-loss.
  • Default Values:
    • HurstPeriod = 100
    • AveragePeriod = 20
    • HurstThreshold = 0.7
    • StopLossPercent = 2 The rules quote 0.5, the theoretical boundary of mean reversion, but the R/S estimate over 100 five-minute candles never fell below about 0.65 on the BTC and TON history the examples are tested on, so the rule could not trade; HurstThreshold makes the level a setting and defaults to 0.7. MA is the AveragePeriod simple moving average. The stop is a fixed StopLossPercent of the entry price, watched between candles as well; 0 disables it. An entry signal against an open position reverses it.
    • CandleType = TimeSpan.FromMinutes(5)
  • Filters:
    • Category: Mean Reversion
    • Direction: Both
    • Indicators: Hurst Exponent, MA
    • Stops: Yes
    • Complexity: Intermediate
    • Timeframe: Intraday
    • Seasonality: No
    • Neural networks: No
    • Divergence: No
    • Risk Level: Medium
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Hurst Exponent Reversion strategy.
/// A HurstPeriod Hurst exponent below HurstThreshold marks a mean-reverting market. Then a close below the AveragePeriod simple moving average
/// goes long and a close above it goes short, reversing an opposite position. A long closes once the close is back at or above the average
/// or the exponent rises above the threshold, a short mirrors it, and a percent stop limits the loss.
/// </summary>
public class HurstExponentReversionStrategy : Strategy
{
	private readonly StrategyParam<int> _hurstPeriod;
	private readonly StrategyParam<int> _averagePeriod;
	private readonly StrategyParam<decimal> _hurstThreshold;
	private readonly StrategyParam<decimal> _stopLossPercent;
	private readonly StrategyParam<DataType> _candleType;

	/// <summary>
	/// Period of the Hurst exponent.
	/// </summary>
	public int HurstPeriod
	{
		get => _hurstPeriod.Value;
		set => _hurstPeriod.Value = value;
	}

	/// <summary>
	/// Period of the simple moving average.
	/// </summary>
	public int AveragePeriod
	{
		get => _averagePeriod.Value;
		set => _averagePeriod.Value = value;
	}

	/// <summary>
	/// Hurst exponent level below which the market reverts.
	/// </summary>
	public decimal HurstThreshold
	{
		get => _hurstThreshold.Value;
		set => _hurstThreshold.Value = value;
	}

	/// <summary>
	/// Stop loss percentage from entry price.
	/// </summary>
	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

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

	/// <summary>
	/// Constructor.
	/// </summary>
	public HurstExponentReversionStrategy()
	{
		_hurstPeriod = Param(nameof(HurstPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("Hurst Period", "Period of the Hurst exponent", "Indicators");

		_averagePeriod = Param(nameof(AveragePeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Average Period", "Period of the simple moving average", "Indicators");

		_hurstThreshold = Param(nameof(HurstThreshold), 0.7m)
			.SetDisplay("Hurst Threshold", "Hurst exponent level below which the market reverts", "Indicators");

		_stopLossPercent = Param(nameof(StopLossPercent), 2m)
			.SetNotNegative()
			.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var hurst = new HurstExponent { Length = HurstPeriod };
		var sma = new SimpleMovingAverage { Length = AveragePeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(hurst, sma, ProcessCandle)
			.Start();

		StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);

		// The stop has to see prices between candles, not only at their close.
		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quotes = new Subscription(DataType.Level1, Security);
			quotes.MarketData.BuildField = field;
			SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
		}

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, sma);
			DrawOwnTrades(area);

			var oscillators = CreateChartArea();
			if (oscillators != null)
			{
				DrawIndicator(oscillators, hurst);
			}
		}
	}

	private void ObserveProtectionQuote(Level1ChangeMessage quote)
	{
		// The high-level handler activates native protection before this callback, also between signal bars.
	}

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

		if (!hurstValue.IsFormed || !smaValue.IsFormed)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var hurst = hurstValue.GetValue<decimal>();
		var sma = smaValue.GetValue<decimal>();
		var close = candle.ClosePrice;
		var reverting = hurst < HurstThreshold;

		if (reverting && close < sma && Position <= 0)
			BuyMarket(Volume + Math.Abs(Position));
		else if (reverting && close > sma && Position >= 0)
			SellMarket(Volume + Math.Abs(Position));
		else if (Position > 0 && (close >= sma || hurst > HurstThreshold))
			SellMarket(Position);
		else if (Position < 0 && (close <= sma || hurst > HurstThreshold))
			BuyMarket(-Position);
	}
}