Hull Ma Adx Strategy
Strategy based on Hull Moving Average and ADX. Enters long when HMA increases and ADX > 25 (strong trend). Enters short when HMA decreases and ADX > 25 (strong trend). Exits when ADX < 20 (weakening trend).
Testing indicates an average annual return of about 178%. It performs best in the stocks market.
Hull MA shows the trend, while ADX confirms its intensity. Entries follow the Hull slope when ADX indicates strength.
Effective for traders who focus on smooth trends with confirmation. ATR stops keep losses under control.
Details
- Entry Criteria:
- Long:
HullMA turning up && ADX > 25 - Short:
HullMA turning down && ADX > 25
- Long:
- Long/Short: Both
- Exit Criteria: Hull MA reversal
- Stops: ATR-based using
AtrMultiplier - Default Values:
HmaPeriod= 9AdxPeriod= 14AdxThreshold= 25AdxExitThreshold= 20AtrMultiplier= 2AtrPeriod= 14CandleType= TimeSpan.FromMinutes(15).TimeFrame() The 25 and 20 in the rules are the defaults of AdxThreshold and AdxExitThreshold. The Hull average turns up when it rises after falling and turns down when it falls after rising; a long closes when it falls and a short when it rises, and either closes once ADX drops below AdxExitThreshold. The stop lies AtrMultiplier ATRs (AtrPeriod) from the entry close and is checked on candle closes. An entry signal against an open position reverses it.
- Filters:
- Category: Trend
- Direction: Both
- Indicators: Hull MA, Moving Average, ADX
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Mid-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 MA ADX strategy.
/// The Hull average turns up when it rises after falling and turns down when it falls after rising. While ADX is above AdxThreshold, a turn up
/// goes long and a turn down goes short, reversing an opposite position. A long closes when the Hull average falls, a short when it rises,
/// and either closes once ADX drops below AdxExitThreshold. The stop lies AtrMultiplier ATR from the entry close and is checked on candle closes.
/// </summary>
public class HullMaAdxStrategy : Strategy
{
private readonly StrategyParam<int> _hmaPeriod;
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<decimal> _adxThreshold;
private readonly StrategyParam<decimal> _adxExitThreshold;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<DataType> _candleType;
private decimal? _prevHull;
private decimal? _prevPrevHull;
private decimal _stopPrice;
/// <summary>
/// Period of the Hull moving average.
/// </summary>
public int HmaPeriod
{
get => _hmaPeriod.Value;
set => _hmaPeriod.Value = value;
}
/// <summary>
/// Period of ADX.
/// </summary>
public int AdxPeriod
{
get => _adxPeriod.Value;
set => _adxPeriod.Value = value;
}
/// <summary>
/// ADX level required to enter.
/// </summary>
public decimal AdxThreshold
{
get => _adxThreshold.Value;
set => _adxThreshold.Value = value;
}
/// <summary>
/// ADX level below which the position closes.
/// </summary>
public decimal AdxExitThreshold
{
get => _adxExitThreshold.Value;
set => _adxExitThreshold.Value = value;
}
/// <summary>
/// Stop distance from the entry in ATRs.
/// </summary>
public decimal AtrMultiplier
{
get => _atrMultiplier.Value;
set => _atrMultiplier.Value = value;
}
/// <summary>
/// Period of the stop ATR.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public HullMaAdxStrategy()
{
_hmaPeriod = Param(nameof(HmaPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("HMA Period", "Period of the Hull moving average", "Indicators");
_adxPeriod = Param(nameof(AdxPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ADX Period", "Period of ADX", "Indicators");
_adxThreshold = Param(nameof(AdxThreshold), 25m)
.SetDisplay("ADX Threshold", "ADX level required to enter", "Indicators");
_adxExitThreshold = Param(nameof(AdxExitThreshold), 20m)
.SetDisplay("ADX Exit Threshold", "ADX level below which the position closes", "Indicators");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m)
.SetNotNegative()
.SetDisplay("ATR Multiplier", "Stop distance from the entry in ATRs", "Risk");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "Period of the stop ATR", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).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();
_prevHull = null;
_prevPrevHull = null;
_stopPrice = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevHull = null;
_prevPrevHull = null;
_stopPrice = default;
var hull = new HullMovingAverage { Length = HmaPeriod };
var adx = new AverageDirectionalIndex { Length = AdxPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(hull, adx, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, hull);
DrawOwnTrades(area);
var oscillators = CreateChartArea();
if (oscillators != null)
{
DrawIndicator(oscillators, adx);
}
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue hullValue, IIndicatorValue adxValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!hullValue.IsFormed || !adxValue.IsFormed || !atrValue.IsFormed)
return;
if (adxValue is not AverageDirectionalIndexValue { MovingAverage: decimal strength })
return;
var hull = hullValue.GetValue<decimal>();
var prevHull = _prevHull;
var prevPrevHull = _prevPrevHull;
_prevPrevHull = prevHull;
_prevHull = hull;
if (prevHull is not decimal last || prevPrevHull is not decimal beforeLast)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var atr = atrValue.GetValue<decimal>();
var close = candle.ClosePrice;
var rising = hull > last;
var falling = hull < last;
var turnsUp = rising && last < beforeLast;
var turnsDown = falling && last > beforeLast;
var strong = strength > AdxThreshold;
var weak = strength < AdxExitThreshold;
if (turnsUp && strong && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_stopPrice = close - AtrMultiplier * atr;
}
else if (turnsDown && strong && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_stopPrice = close + AtrMultiplier * atr;
}
else if (Position > 0 && (falling || weak || (AtrMultiplier > 0 && close <= _stopPrice)))
{
SellMarket(Position);
}
else if (Position < 0 && (rising || weak || (AtrMultiplier > 0 && close >= _stopPrice)))
{
BuyMarket(-Position);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import HullMovingAverage, AverageDirectionalIndex, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class hull_ma_adx_strategy(Strategy):
"""
Hull MA ADX strategy.
The Hull average turns up when it rises after falling and turns down when it falls after rising. While ADX is above AdxThreshold, a turn up
goes long and a turn down goes short, reversing an opposite position. A long closes when the Hull average falls, a short when it rises,
and either closes once ADX drops below AdxExitThreshold. The stop lies AtrMultiplier ATR from the entry close and is checked on candle closes.
"""
def __init__(self):
super(hull_ma_adx_strategy, self).__init__()
self._hma_period = self.Param("HmaPeriod", 9).SetGreaterThanZero().SetDisplay("HMA Period", "Period of the Hull moving average", "Indicators")
self._adx_period = self.Param("AdxPeriod", 14).SetGreaterThanZero().SetDisplay("ADX Period", "Period of ADX", "Indicators")
self._adx_threshold = self.Param("AdxThreshold", 25.0).SetDisplay("ADX Threshold", "ADX level required to enter", "Indicators")
self._adx_exit_threshold = self.Param("AdxExitThreshold", 20.0).SetDisplay("ADX Exit Threshold", "ADX level below which the position closes", "Indicators")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetNotNegative().SetDisplay("ATR Multiplier", "Stop distance from the entry in ATRs", "Risk")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero().SetDisplay("ATR Period", "Period of the stop ATR", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._prev_hull = None
self._prev_prev_hull = None
self._stop_price = Decimal(0)
def OnReseted(self):
super(hull_ma_adx_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(hull_ma_adx_strategy, self).OnStarted2(time)
self._reset_state()
hull = HullMovingAverage()
hull.Length = self._hma_period.Value
adx = AverageDirectionalIndex()
adx.Length = self._adx_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(hull, adx, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, hull)
self.DrawOwnTrades(area)
oscillators = self.CreateChartArea()
if oscillators is not None:
self.DrawIndicator(oscillators, adx)
def _process_candle(self, candle, hull_value, adx_value, atr_value):
if candle.State != CandleStates.Finished:
return
if not hull_value.IsFormed or not adx_value.IsFormed or not atr_value.IsFormed or adx_value.MovingAverage is None:
return
hull = hull_value.GetValue[Decimal](None)
prev_hull = self._prev_hull
prev_prev_hull = self._prev_prev_hull
self._prev_prev_hull = prev_hull
self._prev_hull = hull
if prev_hull is None or prev_prev_hull is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
strength = adx_value.MovingAverage
atr = atr_value.GetValue[Decimal](None)
close = candle.ClosePrice
rising = hull > prev_hull
falling = hull < prev_hull
turns_up = rising and prev_hull < prev_prev_hull
turns_down = falling and prev_hull > prev_prev_hull
strong = strength > Decimal(self._adx_threshold.Value)
weak = strength < Decimal(self._adx_exit_threshold.Value)
stop_atr = Decimal(self._atr_multiplier.Value)
if turns_up and strong and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._stop_price = close - stop_atr * atr
elif turns_down and strong and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._stop_price = close + stop_atr * atr
elif self.Position > 0 and (falling or weak or (stop_atr > 0 and close <= self._stop_price)):
self.SellMarket(self.Position)
elif self.Position < 0 and (rising or weak or (stop_atr > 0 and close >= self._stop_price)):
self.BuyMarket(-self.Position)
def CreateClone(self):
return hull_ma_adx_strategy()