MH Hull Moving Average Based Trading
Hull Moving Average based breakout strategy.
The strategy compares the open price with dynamic levels derived from the Hull Moving Average. It enters long when price breaks above the upper level and short when it falls below the lower level. Existing positions are closed on opposite breakouts.
Details
- Entry Criteria: Price relation to Hull Moving Average levels.
- Long/Short: Both directions.
- Exit Criteria: Opposite breakout.
- Stops: No.
- Default Values:
HullPeriod= 210CandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Trend
- Direction: Both
- Indicators: MA
- Stops: No
- Complexity: Basic
- Timeframe: Intraday (5m)
- 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>
/// HMA cross strategy with threshold and cooldown to reduce signal noise.
/// </summary>
public class MhHullMovingAverageBasedTradingStrategy : Strategy
{
private readonly StrategyParam<int> _hullPeriod;
private readonly StrategyParam<decimal> _signalThresholdPercent;
private readonly StrategyParam<int> _signalCooldownBars;
private readonly StrategyParam<DataType> _candleType;
private HullMovingAverage _hma;
private decimal _prevDiffPercent;
private bool _hasPrevDiff;
private int _barsFromSignal;
/// <summary>
/// Period for Hull Moving Average.
/// </summary>
public int HullPeriod
{
get => _hullPeriod.Value;
set => _hullPeriod.Value = value;
}
/// <summary>
/// Minimum price to HMA distance in percent required for a signal.
/// </summary>
public decimal SignalThresholdPercent
{
get => _signalThresholdPercent.Value;
set => _signalThresholdPercent.Value = value;
}
/// <summary>
/// Minimum bars between entries.
/// </summary>
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize the strategy.
/// </summary>
public MhHullMovingAverageBasedTradingStrategy()
{
_hullPeriod = Param(nameof(HullPeriod), 120)
.SetGreaterThanZero()
.SetDisplay("Hull Period", "Period for Hull Moving Average", "Indicators");
_signalThresholdPercent = Param(nameof(SignalThresholdPercent), 0.15m)
.SetGreaterThanZero()
.SetDisplay("Signal Threshold %", "Minimum distance from HMA", "Indicators");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 10)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown Bars", "Minimum bars between entries", "Indicators");
_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();
_hma = null;
_prevDiffPercent = 0m;
_hasPrevDiff = false;
_barsFromSignal = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
StartProtection(null, null);
_hma = new HullMovingAverage { Length = HullPeriod };
_prevDiffPercent = 0m;
_hasPrevDiff = false;
_barsFromSignal = SignalCooldownBars;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_hma, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal hmaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (!_hma.IsFormed)
return;
var price = candle.ClosePrice;
if (price <= 0m)
return;
var diffPercent = (price - hmaValue) / price * 100m;
var threshold = SignalThresholdPercent;
var crossedUp = _hasPrevDiff && _prevDiffPercent <= threshold && diffPercent > threshold;
var crossedDown = _hasPrevDiff && _prevDiffPercent >= -threshold && diffPercent < -threshold;
_prevDiffPercent = diffPercent;
_hasPrevDiff = true;
_barsFromSignal++;
if (_barsFromSignal < SignalCooldownBars)
return;
if (crossedUp && Position <= 0)
{
var volume = Volume + Math.Abs(Position);
BuyMarket(volume);
_barsFromSignal = 0;
}
else if (crossedDown && Position >= 0)
{
var volume = Volume + Math.Abs(Position);
SellMarket(volume);
_barsFromSignal = 0;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import HullMovingAverage, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class mh_hull_moving_average_based_trading_strategy(Strategy):
def __init__(self):
super(mh_hull_moving_average_based_trading_strategy, self).__init__()
self._hull_period = self.Param("HullPeriod", 120) \
.SetGreaterThanZero() \
.SetDisplay("Hull Period", "Period for Hull Moving Average", "Indicators")
self._signal_threshold_percent = self.Param("SignalThresholdPercent", 0.15) \
.SetGreaterThanZero() \
.SetDisplay("Signal Threshold %", "Minimum distance from HMA", "Indicators")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 10) \
.SetGreaterThanZero() \
.SetDisplay("Signal Cooldown Bars", "Minimum bars between entries", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._prev_diff_percent = 0.0
self._has_prev_diff = False
self._bars_from_signal = 0
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(mh_hull_moving_average_based_trading_strategy, self).OnReseted()
self._prev_diff_percent = 0.0
self._has_prev_diff = False
self._bars_from_signal = 0
def OnStarted2(self, time):
super(mh_hull_moving_average_based_trading_strategy, self).OnStarted2(time)
self._prev_diff_percent = 0.0
self._has_prev_diff = False
self._bars_from_signal = self._signal_cooldown_bars.Value
self._hma = HullMovingAverage()
self._hma.Length = self._hull_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._hma, self.OnProcess).Start()
def OnProcess(self, candle, hma_value):
if candle.State != CandleStates.Finished:
return
if not self._hma.IsFormed:
return
price = float(candle.ClosePrice)
if price <= 0.0:
return
hv = float(hma_value)
diff_percent = (price - hv) / price * 100.0
threshold = float(self._signal_threshold_percent.Value)
crossed_up = self._has_prev_diff and self._prev_diff_percent <= threshold and diff_percent > threshold
crossed_down = self._has_prev_diff and self._prev_diff_percent >= -threshold and diff_percent < -threshold
self._prev_diff_percent = diff_percent
self._has_prev_diff = True
self._bars_from_signal += 1
if self._bars_from_signal < self._signal_cooldown_bars.Value:
return
if crossed_up and self.Position <= 0:
self.BuyMarket()
self._bars_from_signal = 0
elif crossed_down and self.Position >= 0:
self.SellMarket()
self._bars_from_signal = 0
def CreateClone(self):
return mh_hull_moving_average_based_trading_strategy()