Hull MA Slope Mean Reversion
The Hull MA Slope Mean Reversion strategy focuses on extreme readings of the Hull to exploit reversion. Wide departures from the normal level rarely last.
Trades trigger when the indicator swings far from its mean and then begins to reverse. Both long and short setups include a protective stop.
Suited for swing traders expecting oscillations, the strategy closes out once the Hull returns toward balance. Starting parameter HullPeriod = 9.
Details
- Entry Criteria: Indicator crosses back toward mean.
- Long/Short: Both directions.
- Exit Criteria: Indicator reverts to average.
- Stops: Yes.
- Default Values:
HullPeriod= 9LookbackPeriod= 20DeviationMultiplier= 2.0mAtrPeriod= 14AtrMultiplier= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Mean Reversion
- Direction: Both
- Indicators: Hull
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Short-term
- 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>
/// Hull moving average slope mean reversion strategy.
/// Trades reversions from extreme Hull MA slopes and exits when the slope returns to its recent average.
/// </summary>
public class HullMaSlopeMeanReversionStrategy : Strategy
{
private readonly StrategyParam<int> _hullPeriod;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _deviationMultiplier;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private HullMovingAverage _hullMa;
private decimal _prevHullValue;
private decimal[] _slopeHistory;
private int _currentIndex;
private int _filledCount;
private int _cooldown;
private bool _isInitialized;
/// <summary>
/// Hull Moving Average period.
/// </summary>
public int HullPeriod
{
get => _hullPeriod.Value;
set => _hullPeriod.Value = value;
}
/// <summary>
/// Lookback period for slope statistics.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Deviation multiplier for mean reversion detection.
/// </summary>
public decimal DeviationMultiplier
{
get => _deviationMultiplier.Value;
set => _deviationMultiplier.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Cooldown bars between orders.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="HullMaSlopeMeanReversionStrategy"/>.
/// </summary>
public HullMaSlopeMeanReversionStrategy()
{
_hullPeriod = Param(nameof(HullPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("Hull MA Period", "Hull Moving Average period", "Hull MA")
.SetOptimize(5, 20, 1);
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Lookback period for slope statistics", "Strategy Parameters")
.SetOptimize(10, 50, 5);
_deviationMultiplier = Param(nameof(DeviationMultiplier), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Deviation Multiplier", "Deviation multiplier for mean reversion detection", "Strategy Parameters")
.SetOptimize(1m, 3m, 0.5m);
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management");
_cooldownBars = Param(nameof(CooldownBars), 1200)
.SetRange(1, 5000)
.SetDisplay("Cooldown Bars", "Bars to wait between orders", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle type for strategy", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_hullMa = null;
_prevHullValue = default;
_slopeHistory = new decimal[LookbackPeriod];
_currentIndex = default;
_filledCount = default;
_cooldown = default;
_isInitialized = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_hullMa = new HullMovingAverage { Length = HullPeriod };
_slopeHistory = new decimal[LookbackPeriod];
_currentIndex = 0;
_filledCount = 0;
_cooldown = 0;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_hullMa, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _hullMa);
DrawOwnTrades(area);
}
StartProtection(new(), new Unit(StopLossPercent, UnitTypes.Percent));
}
private void ProcessCandle(ICandleMessage candle, decimal hullValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_hullMa.IsFormed)
return;
if (!_isInitialized)
{
_prevHullValue = hullValue;
_isInitialized = true;
return;
}
if (_prevHullValue == 0)
return;
var slope = (hullValue - _prevHullValue) / _prevHullValue * 100m;
_prevHullValue = hullValue;
_slopeHistory[_currentIndex] = slope;
_currentIndex = (_currentIndex + 1) % LookbackPeriod;
if (_filledCount < LookbackPeriod)
_filledCount++;
if (_filledCount < LookbackPeriod)
return;
var avgSlope = 0m;
var sumSq = 0m;
for (var i = 0; i < LookbackPeriod; i++)
avgSlope += _slopeHistory[i];
avgSlope /= LookbackPeriod;
for (var i = 0; i < LookbackPeriod; i++)
{
var diff = _slopeHistory[i] - avgSlope;
sumSq += diff * diff;
}
var stdSlope = (decimal)Math.Sqrt((double)(sumSq / LookbackPeriod));
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var highThreshold = avgSlope + stdSlope * DeviationMultiplier;
var lowThreshold = avgSlope - stdSlope * DeviationMultiplier;
if (Position == 0)
{
if (slope < lowThreshold)
{
BuyMarket();
_cooldown = CooldownBars;
}
else if (slope > highThreshold)
{
SellMarket();
_cooldown = CooldownBars;
}
}
else if (Position > 0 && slope >= avgSlope)
{
SellMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
else if (Position < 0 && slope <= avgSlope)
{
BuyMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan, Math
from StockSharp.Messages import DataType, Unit, UnitTypes, CandleStates
from StockSharp.Algo.Indicators import HullMovingAverage
from StockSharp.Algo.Strategies import Strategy
class hull_ma_slope_mean_reversion_strategy(Strategy):
"""
Hull moving average slope mean reversion strategy.
Trades reversions from extreme Hull MA slopes and exits when the slope returns to its recent average.
"""
def __init__(self):
super(hull_ma_slope_mean_reversion_strategy, self).__init__()
self._hull_period = self.Param("HullPeriod", 9) \
.SetGreaterThanZero() \
.SetDisplay("Hull MA Period", "Hull Moving Average period", "Hull MA")
self._lookback_period = self.Param("LookbackPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Lookback Period", "Lookback period for slope statistics", "Strategy Parameters")
self._deviation_multiplier = self.Param("DeviationMultiplier", 1.5) \
.SetGreaterThanZero() \
.SetDisplay("Deviation Multiplier", "Deviation multiplier for mean reversion detection", "Strategy Parameters")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management")
self._cooldown_bars = self.Param("CooldownBars", 1200) \
.SetDisplay("Cooldown Bars", "Bars to wait between orders", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Candle type for strategy", "General")
self._hull_ma = None
self._prev_hull_value = 0.0
self._slope_history = None
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
self._is_initialized = False
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(hull_ma_slope_mean_reversion_strategy, self).OnReseted()
self._hull_ma = None
self._prev_hull_value = 0.0
lb = int(self._lookback_period.Value)
self._slope_history = [0.0] * lb
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
self._is_initialized = False
def OnStarted2(self, time):
super(hull_ma_slope_mean_reversion_strategy, self).OnStarted2(time)
lb = int(self._lookback_period.Value)
self._slope_history = [0.0] * lb
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
self._hull_ma = HullMovingAverage()
self._hull_ma.Length = int(self._hull_period.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._hull_ma, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._hull_ma)
self.DrawOwnTrades(area)
self.StartProtection(Unit(), Unit(self._stop_loss_percent.Value, UnitTypes.Percent))
def _process_candle(self, candle, hull_value):
if candle.State != CandleStates.Finished:
return
if not self._hull_ma.IsFormed:
return
hv = float(hull_value)
if not self._is_initialized:
self._prev_hull_value = hv
self._is_initialized = True
return
if self._prev_hull_value == 0:
return
slope = (hv - self._prev_hull_value) / self._prev_hull_value * 100.0
self._prev_hull_value = hv
lb = int(self._lookback_period.Value)
self._slope_history[self._current_index] = slope
self._current_index = (self._current_index + 1) % lb
if self._filled_count < lb:
self._filled_count += 1
if self._filled_count < lb:
return
avg_slope = 0.0
for i in range(lb):
avg_slope += self._slope_history[i]
avg_slope /= float(lb)
sum_sq = 0.0
for i in range(lb):
diff = self._slope_history[i] - avg_slope
sum_sq += diff * diff
std_slope = math.sqrt(sum_sq / float(lb))
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown > 0:
self._cooldown -= 1
return
dm = float(self._deviation_multiplier.Value)
high_threshold = avg_slope + std_slope * dm
low_threshold = avg_slope - std_slope * dm
if self.Position == 0:
if slope < low_threshold:
self.BuyMarket()
self._cooldown = int(self._cooldown_bars.Value)
elif slope > high_threshold:
self.SellMarket()
self._cooldown = int(self._cooldown_bars.Value)
elif self.Position > 0 and slope >= avg_slope:
self.SellMarket(Math.Abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
elif self.Position < 0 and slope <= avg_slope:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
def CreateClone(self):
return hull_ma_slope_mean_reversion_strategy()