Hull MA Slope Breakout
The Hull MA Slope Breakout strategy tracks the rate of change of the Hull. An unusually steep slope hints that a new trend is forming.
Testing indicates an average annual return of about 121%. It performs best in the crypto market.
Entries occur when slope exceeds its typical level by a multiple of standard deviation, taking trades in the direction of acceleration with a protective stop.
It appeals to active traders eager for early trend exposure. Positions exit when the slope drifts back toward normal readings. Default HullLength = 9.
Details
- Entry Criteria: Indicator exceeds average by deviation multiplier.
- Long/Short: Both directions.
- Exit Criteria: Indicator reverts to average.
- Stops: Yes.
- Default Values:
HullLength= 9LookbackPeriod= 20DeviationMultiplier= 2mStopLoss= new Unit(2CandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Breakout
- 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.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy based on Hull Moving Average Slope breakout
/// Enters positions when the slope of Hull MA exceeds average slope plus a multiple of standard deviation
/// </summary>
public class HullMaSlopeBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _hullLength;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _deviationMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<Unit> _stopLoss;
private HullMovingAverage _hullMa;
private AverageTrueRange _atr;
private decimal _prevHullValue;
private decimal _currentSlope;
private decimal _avgSlope;
private decimal _stdDevSlope;
private decimal[] _slopes;
private int _currentIndex;
private bool _isInitialized;
/// <summary>
/// Hull Moving Average length
/// </summary>
public int HullLength
{
get => _hullLength.Value;
set => _hullLength.Value = value;
}
/// <summary>
/// Lookback period for slope statistics calculation
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Standard deviation multiplier for breakout detection
/// </summary>
public decimal DeviationMultiplier
{
get => _deviationMultiplier.Value;
set => _deviationMultiplier.Value = value;
}
/// <summary>
/// Candle type
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Stop loss value
/// </summary>
public Unit StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Constructor
/// </summary>
public HullMaSlopeBreakoutStrategy()
{
_hullLength = Param(nameof(HullLength), 9)
.SetGreaterThanZero()
.SetDisplay("Hull MA Length", "Period for Hull Moving Average", "Indicator Parameters")
.SetOptimize(5, 20, 1);
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Period for slope statistics calculation", "Strategy Parameters")
.SetOptimize(10, 50, 5);
_deviationMultiplier = Param(nameof(DeviationMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Deviation Multiplier", "Standard deviation multiplier for breakout detection", "Strategy Parameters")
.SetOptimize(1m, 3m, 0.5m);
_stopLoss = Param(nameof(StopLoss), new Unit(2, UnitTypes.Absolute))
.SetDisplay("Stop Loss", "Stop loss value in ATRs", "Risk Management");
_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 OnReseted()
{
base.OnReseted();
_prevHullValue = 0;
_currentSlope = 0;
_avgSlope = 0;
_stdDevSlope = 0;
_currentIndex = 0;
_isInitialized = false;
_slopes = new decimal[LookbackPeriod];
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
_slopes = new decimal[LookbackPeriod];
_hullMa = new HullMovingAverage { Length = HullLength };
_atr = new AverageTrueRange { Length = 14 }; // ATR for stop-loss
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_hullMa, _atr, ProcessCandle)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _hullMa);
DrawOwnTrades(area);
}
// Set up position protection
StartProtection(
takeProfit: null, // We'll handle exits via strategy logic
stopLoss: StopLoss,
isStopTrailing: false
);
base.OnStarted2(time);
}
private void ProcessCandle(ICandleMessage candle, decimal hullValue, decimal atrValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Check if indicator is formed
if (!_hullMa.IsFormed)
return;
// Extract Hull MA value
decimal currentHullValue = hullValue;
// Calculate the slope
if (!_isInitialized)
{
_prevHullValue = currentHullValue;
_isInitialized = true;
return;
}
// Calculate current slope (simple difference for now)
_currentSlope = currentHullValue - _prevHullValue;
if (_slopes is null || _slopes.Length != LookbackPeriod)
{
_slopes = new decimal[LookbackPeriod];
_currentIndex = 0;
_prevHullValue = currentHullValue;
return;
}
// Store slope in array and update index
_slopes[_currentIndex] = _currentSlope;
_currentIndex = (_currentIndex + 1) % _slopes.Length;
// Calculate statistics once we have enough data
if (!IsFormedAndOnlineAndAllowTrading())
return;
CalculateStatistics();
// Trading logic
if (Math.Abs(_avgSlope) > 0) // Avoid division by zero
{
// Long signal: slope exceeds average + k*stddev (slope is positive and we don't have a long position)
if (_currentSlope > 0 &&
_currentSlope > _avgSlope + DeviationMultiplier * _stdDevSlope &&
Position <= 0)
{
// Cancel existing orders
CancelActiveOrders();
// Enter long position
var volume = Volume + Math.Abs(Position);
BuyMarket(volume);
LogInfo($"Long signal: Slope {_currentSlope} > Avg {_avgSlope} + {DeviationMultiplier}*StdDev {_stdDevSlope}");
}
// Short signal: slope exceeds average + k*stddev in negative direction (slope is negative and we don't have a short position)
else if (_currentSlope < 0 &&
_currentSlope < _avgSlope - DeviationMultiplier * _stdDevSlope &&
Position >= 0)
{
// Cancel existing orders
CancelActiveOrders();
// Enter short position
var volume = Volume + Math.Abs(Position);
SellMarket(volume);
LogInfo($"Short signal: Slope {_currentSlope} < Avg {_avgSlope} - {DeviationMultiplier}*StdDev {_stdDevSlope}");
}
// Exit conditions - when slope returns to average
if (Position > 0 && _currentSlope < _avgSlope)
{
// Exit long position
SellMarket(Math.Abs(Position));
LogInfo($"Exit long: Slope {_currentSlope} < Avg {_avgSlope}");
}
else if (Position < 0 && _currentSlope > _avgSlope)
{
// Exit short position
BuyMarket(Math.Abs(Position));
LogInfo($"Exit short: Slope {_currentSlope} > Avg {_avgSlope}");
}
}
// Store current Hull MA value for next slope calculation
_prevHullValue = currentHullValue;
}
private void CalculateStatistics()
{
var period = _slopes?.Length ?? 0;
if (period <= 0)
return;
// Reset statistics
_avgSlope = 0;
decimal sumSquaredDiffs = 0;
// Calculate average
for (int i = 0; i < period; i++)
{
_avgSlope += _slopes[i];
}
_avgSlope /= period;
// Calculate standard deviation
for (int i = 0; i < period; i++)
{
decimal diff = _slopes[i] - _avgSlope;
sumSquaredDiffs += diff * diff;
}
_stdDevSlope = (decimal)Math.Sqrt((double)(sumSquaredDiffs / period));
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import HullMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class hull_ma_slope_breakout_strategy(Strategy):
"""
Hull MA slope breakout. Enters when slope exceeds avg + k*stddev.
"""
def __init__(self):
super(hull_ma_slope_breakout_strategy, self).__init__()
self._hull_length = self.Param("HullLength", 9).SetDisplay("Hull Length", "Hull MA period", "Indicators")
self._lookback = self.Param("LookbackPeriod", 20).SetDisplay("Lookback", "Slope stats period", "Strategy")
self._dev_mult = self.Param("DeviationMultiplier", 2.0).SetDisplay("Dev Mult", "Stddev multiplier", "Strategy")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
self._prev_hull = 0.0
self._slopes = None
self._current_index = 0
self._is_init = False
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(hull_ma_slope_breakout_strategy, self).OnReseted()
self._prev_hull = 0.0
self._slopes = [0.0] * int(self._lookback.Value)
self._current_index = 0
self._is_init = False
def OnStarted2(self, time):
super(hull_ma_slope_breakout_strategy, self).OnStarted2(time)
lb = int(self._lookback.Value)
self._slopes = [0.0] * lb
self._current_index = 0
hma = HullMovingAverage()
hma.Length = self._hull_length.Value
atr = AverageTrueRange()
atr.Length = 14
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(hma, atr, self._process_candle).Start()
self.StartProtection(None, Unit(2, UnitTypes.Absolute))
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, hma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, hull_val, atr_val):
if candle.State != CandleStates.Finished:
return
hull = float(hull_val)
if not self._is_init:
self._prev_hull = hull
self._is_init = True
return
current_slope = hull - self._prev_hull
lb = int(self._lookback.Value)
if self._slopes is None or len(self._slopes) != lb:
self._slopes = [0.0] * lb
self._current_index = 0
self._prev_hull = hull
return
# Store slope in circular array
self._slopes[self._current_index] = current_slope
self._current_index = (self._current_index + 1) % lb
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_hull = hull
return
# Calculate statistics
avg_slope = sum(self._slopes) / lb
sum_sq = sum((s - avg_slope) ** 2 for s in self._slopes)
std_slope = math.sqrt(sum_sq / lb)
dm = float(self._dev_mult.Value)
if abs(avg_slope) > 0:
# Long signal
if current_slope > 0 and current_slope > avg_slope + dm * std_slope and self.Position <= 0:
self.CancelActiveOrders()
vol = self.Volume + Math.Abs(self.Position)
self.BuyMarket(vol)
# Short signal
elif current_slope < 0 and current_slope < avg_slope - dm * std_slope and self.Position >= 0:
self.CancelActiveOrders()
vol = self.Volume + Math.Abs(self.Position)
self.SellMarket(vol)
# Exit conditions
if self.Position > 0 and current_slope < avg_slope:
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0 and current_slope > avg_slope:
self.BuyMarket(Math.Abs(self.Position))
self._prev_hull = hull
def CreateClone(self):
return hull_ma_slope_breakout_strategy()