Hull MA Trend
Strategy based on Hull Moving Average trend
Testing indicates an average annual return of about 61%. It performs best in the crypto market.
The Hull MA Trend strategy monitors the slope of the Hull Moving Average. Rising slopes prompt longs and falling slopes prompt shorts, with an ATR trailing stop protecting each trade.
Its responsive calculation reduces lag compared with traditional moving averages, allowing the system to react quickly to new momentum. The ATR stop helps avoid large drawdowns if the slope changes abruptly.
Details
- Entry Criteria: Signals based on MA, ATR.
- Long/Short: Both directions.
- Exit Criteria: Opposite signal or stop.
- Stops: Yes.
- Default Values:
HmaPeriod= 9AtrPeriod= 14AtrMultiplier= 2mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Trend
- Direction: Both
- Indicators: MA, ATR
- Stops: Yes
- Complexity: Basic
- Timeframe: Intraday (5m)
- 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 trend.
/// It turns long when the Hull MA starts rising and short when it starts falling,
/// and protects the position with a stop that trails the close by AtrMultiplier ATRs.
/// </summary>
public class HullMaTrendStrategy : Strategy
{
private readonly StrategyParam<int> _hmaPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<DataType> _candleType;
private decimal? _prevHmaValue;
// Direction of the last slope change: 1 rising, -1 falling, 0 none yet.
private int _slope;
private decimal _stopPrice;
/// <summary>
/// Period for Hull Moving Average.
/// </summary>
public int HmaPeriod
{
get => _hmaPeriod.Value;
set => _hmaPeriod.Value = value;
}
/// <summary>
/// Period for ATR calculation (stop-loss).
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Multiplier for ATR to determine stop-loss distance.
/// </summary>
public decimal AtrMultiplier
{
get => _atrMultiplier.Value;
set => _atrMultiplier.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize the Hull MA Trend strategy.
/// </summary>
public HullMaTrendStrategy()
{
_hmaPeriod = Param(nameof(HmaPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("HMA Period", "Period for Hull Moving Average", "Indicators")
.SetOptimize(5, 15, 2);
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "Period for Average True Range (stop-loss)", "Risk parameters")
.SetOptimize(10, 20, 2);
_atrMultiplier = Param(nameof(AtrMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("ATR Multiplier", "Distance of the trailing stop in ATR multiples", "Risk parameters")
.SetOptimize(1, 3, 0.5m);
_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();
_prevHmaValue = null;
_slope = 0;
_stopPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create indicators
var hma = new HullMovingAverage { Length = HmaPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
// Create subscription and bind indicators
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(hma, atr, ProcessCandle)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, hma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue hmaIndicatorValue, IIndicatorValue atrIndicatorValue)
{
if (candle.State != CandleStates.Finished || !hmaIndicatorValue.IsFormed || !atrIndicatorValue.IsFormed)
return;
var hmaValue = hmaIndicatorValue.GetValue<decimal>();
var atrValue = atrIndicatorValue.GetValue<decimal>();
var previous = _prevHmaValue;
_prevHmaValue = hmaValue;
if (previous is not decimal prevHma || !IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
var distance = AtrMultiplier * atrValue;
var slope = hmaValue > prevHma ? 1 : hmaValue < prevHma ? -1 : 0;
// Only a change of the slope direction is a new signal, so a stopped trade is not reopened at once.
if (slope != 0 && slope != _slope)
{
_slope = slope;
if (slope > 0 && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_stopPrice = close - distance;
return;
}
if (slope < 0 && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_stopPrice = close + distance;
return;
}
}
if (Position > 0)
{
if (close <= _stopPrice)
SellMarket(Position);
else
_stopPrice = Math.Max(_stopPrice, close - distance);
}
else if (Position < 0)
{
if (close >= _stopPrice)
BuyMarket(-Position);
else
_stopPrice = Math.Min(_stopPrice, close + distance);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import HullMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class hull_ma_trend_strategy(Strategy):
"""
Strategy based on Hull Moving Average trend.
It turns long when the Hull MA starts rising and short when it starts falling,
and protects the position with a stop that trails the close by AtrMultiplier ATRs.
"""
def __init__(self):
super(hull_ma_trend_strategy, self).__init__()
self._hma_period = self.Param("HmaPeriod", 9) \
.SetGreaterThanZero() \
.SetDisplay("HMA Period", "Period for Hull Moving Average", "Indicators")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "Period for Average True Range (stop-loss)", "Risk parameters")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("ATR Multiplier", "Distance of the trailing stop in ATR multiples", "Risk parameters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._prev_hma_value = None
# Direction of the last slope change: 1 rising, -1 falling, 0 none yet.
self._slope = 0
self._stop_price = Decimal(0)
@property
def HmaPeriod(self):
return self._hma_period.Value
@HmaPeriod.setter
def HmaPeriod(self, value):
self._hma_period.Value = value
@property
def AtrPeriod(self):
return self._atr_period.Value
@AtrPeriod.setter
def AtrPeriod(self, value):
self._atr_period.Value = value
@property
def AtrMultiplier(self):
return self._atr_multiplier.Value
@AtrMultiplier.setter
def AtrMultiplier(self, value):
self._atr_multiplier.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(hull_ma_trend_strategy, self).OnStarted2(time)
hma = HullMovingAverage()
hma.Length = self.HmaPeriod
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
self.SubscribeCandles(self.CandleType) \
.BindEx(hma, atr, self.ProcessCandle) \
.Start()
def ProcessCandle(self, candle, hma_indicator_value, atr_indicator_value):
if candle.State != CandleStates.Finished or not hma_indicator_value.IsFormed or not atr_indicator_value.IsFormed:
return
hma_value = hma_indicator_value.GetValue[Decimal](None)
atr_value = atr_indicator_value.GetValue[Decimal](None)
previous = self._prev_hma_value
self._prev_hma_value = hma_value
if previous is None or not self.IsFormedAndOnlineAndAllowTrading():
return
close = candle.ClosePrice
distance = Decimal(self.AtrMultiplier) * atr_value
slope = 1 if hma_value > previous else (-1 if hma_value < previous else 0)
# Only a change of the slope direction is a new signal, so a stopped trade is not reopened at once.
if slope != 0 and slope != self._slope:
self._slope = slope
if slope > 0 and self.Position <= 0:
self.BuyMarket(self.Volume + Math.Abs(self.Position))
self._stop_price = close - distance
return
if slope < 0 and self.Position >= 0:
self.SellMarket(self.Volume + Math.Abs(self.Position))
self._stop_price = close + distance
return
if self.Position > 0:
if close <= self._stop_price:
self.SellMarket(self.Position)
else:
self._stop_price = Math.Max(self._stop_price, close - distance)
elif self.Position < 0:
if close >= self._stop_price:
self.BuyMarket(-self.Position)
else:
self._stop_price = Math.Min(self._stop_price, close + distance)
def OnReseted(self):
super(hull_ma_trend_strategy, self).OnReseted()
self._prev_hma_value = None
self._slope = 0
self._stop_price = Decimal(0)
def CreateClone(self):
return hull_ma_trend_strategy()