Hull MA Volatility Contraction
The Hull MA Volatility Contraction strategy is built around Hull Moving Average with volatility contraction filter.
Testing indicates an average annual return of about 76%. It performs best in the forex market.
Signals trigger when its indicators confirms volatility contraction patterns on intraday (15m) data. This makes the method suitable for active traders.
Stops rely on ATR multiples and factors like HmaPeriod, AtrPeriod. Adjust these defaults to balance risk and reward.
Details
- Entry Criteria: see implementation for indicator conditions.
- Long/Short: Both directions.
- Exit Criteria: opposite signal or stop logic.
- Stops: Yes, using indicator-based calculations.
- Default Values:
HmaPeriod = 9AtrPeriod = 14VolatilityContractionFactor = 2.0mCandleType = TimeSpan.FromMinutes(15).TimeFrame()
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: multiple indicators
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (15m)
- 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 with volatility contraction filter.
/// </summary>
public class HullMaVolatilityContractionStrategy : Strategy
{
private readonly StrategyParam<int> _hmaPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _volatilityContractionFactor;
private readonly StrategyParam<DataType> _candleType;
private HullMovingAverage _hma;
private AverageTrueRange _atr;
// Store values for analysis
private readonly SynchronizedList<decimal> _atrValues = [];
private decimal _prevHmaValue;
private decimal _currentHmaValue;
private bool _isLongPosition;
private bool _isShortPosition;
/// <summary>
/// Hull Moving Average period.
/// </summary>
public int HmaPeriod
{
get => _hmaPeriod.Value;
set => _hmaPeriod.Value = value;
}
/// <summary>
/// Average True Range period for volatility calculation.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Volatility contraction factor (standard deviation multiplier).
/// </summary>
public decimal VolatilityContractionFactor
{
get => _volatilityContractionFactor.Value;
set => _volatilityContractionFactor.Value = value;
}
/// <summary>
/// Candle type to use for the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="HullMaVolatilityContractionStrategy"/>.
/// </summary>
public HullMaVolatilityContractionStrategy()
{
_hmaPeriod = Param(nameof(HmaPeriod), 9)
.SetDisplay("Hull MA Period", "Hull Moving Average period", "Hull MA")
.SetOptimize(5, 20, 1);
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetDisplay("ATR Period", "Period for ATR volatility calculation", "Volatility")
.SetOptimize(10, 30, 2);
_volatilityContractionFactor = Param(nameof(VolatilityContractionFactor), 2.0m)
.SetDisplay("Volatility Contraction Factor", "Standard deviation multiplier for volatility contraction", "Volatility")
.SetOptimize(1.0m, 3.0m, 0.5m);
_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();
_prevHmaValue = default;
_currentHmaValue = default;
_isLongPosition = false;
_isShortPosition = false;
_atrValues.Clear();
_hma = null;
_atr = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create indicators
_hma = new HullMovingAverage
{
Length = HmaPeriod
};
_atr = new AverageTrueRange
{
Length = AtrPeriod
};
// Create subscription and bind indicators
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_hma, _atr, ProcessCandle)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _hma);
DrawIndicator(area, _atr);
DrawOwnTrades(area);
}
// Setup position protection
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(2, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle, decimal hmaValue, decimal atrValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Save previous HMA value
_prevHmaValue = _currentHmaValue;
// Extract values from indicators
_currentHmaValue = hmaValue;
decimal atr = atrValue;
// Store ATR values for volatility analysis
_atrValues.Add(atr);
// Keep only needed history
while (_atrValues.Count > AtrPeriod * 2)
_atrValues.RemoveAt(0);
// Check for volatility contraction
bool isVolatilityContracted = IsVolatilityContracted();
// Determine HMA trend direction
bool isHmaRising = _currentHmaValue > _prevHmaValue;
bool isHmaFalling = _currentHmaValue < _prevHmaValue;
if (!IsFormedAndOnlineAndAllowTrading())
return;
// Log current status
if (_atrValues.Count >= AtrPeriod)
{
decimal avgAtr = _atrValues.Skip(Math.Max(0, _atrValues.Count - AtrPeriod)).Average();
LogInfo($"HMA: {_currentHmaValue:F2} (Prev: {_prevHmaValue:F2}), ATR: {atr:F2}, Avg ATR: {avgAtr:F2}, Volatility Contracted: {isVolatilityContracted}");
}
// Trading logic
// Buy when HMA is rising and volatility is contracted
if (isHmaRising && isVolatilityContracted && Position <= 0)
{
BuyMarket(Volume);
LogInfo($"Buy Signal: HMA Rising ({_prevHmaValue:F2} -> {_currentHmaValue:F2}) with Contracted Volatility");
_isLongPosition = true;
_isShortPosition = false;
}
// Sell when HMA is falling and volatility is contracted
else if (isHmaFalling && isVolatilityContracted && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
LogInfo($"Sell Signal: HMA Falling ({_prevHmaValue:F2} -> {_currentHmaValue:F2}) with Contracted Volatility");
_isLongPosition = false;
_isShortPosition = true;
}
// Exit long position when HMA starts falling
else if (_isLongPosition && isHmaFalling)
{
SellMarket(Position);
LogInfo($"Exit Long: HMA started falling ({_prevHmaValue:F2} -> {_currentHmaValue:F2})");
_isLongPosition = false;
}
// Exit short position when HMA starts rising
else if (_isShortPosition && isHmaRising)
{
BuyMarket(Math.Abs(Position));
LogInfo($"Exit Short: HMA started rising ({_prevHmaValue:F2} -> {_currentHmaValue:F2})");
_isShortPosition = false;
}
}
private bool IsVolatilityContracted()
{
// Need enough ATR values for calculation
if (_atrValues.Count < AtrPeriod)
return false;
// Get recent ATR values for analysis
var recentAtrValues = _atrValues.Skip(Math.Max(0, _atrValues.Count - AtrPeriod)).ToList();
// Calculate mean and standard deviation
decimal mean = recentAtrValues.Average();
decimal sumSquaredDifferences = recentAtrValues.Sum(x => (x - mean) * (x - mean));
decimal standardDeviation = (decimal)Math.Sqrt((double)(sumSquaredDifferences / recentAtrValues.Count));
// Get current ATR (latest)
decimal currentAtr = _atrValues.Last();
// Check if current ATR is less than mean minus standard deviation * factor
bool isContracted = currentAtr < (mean - standardDeviation * VolatilityContractionFactor);
// Log details if contraction is detected
if (isContracted)
{
LogInfo($"Volatility Contraction Detected: Current ATR {currentAtr:F2} < Mean {mean:F2} - (StdDev {standardDeviation:F2} * Factor {VolatilityContractionFactor})");
}
return isContracted;
}
}
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_volatility_contraction_strategy(Strategy):
"""
Hull MA with volatility contraction filter. Enters when HMA trends and ATR is contracted.
"""
def __init__(self):
super(hull_ma_volatility_contraction_strategy, self).__init__()
self._hma_period = self.Param("HmaPeriod", 9) \
.SetDisplay("Hull MA Period", "Hull Moving Average period", "Hull MA") \
.SetCanOptimize(True) \
.SetOptimize(5, 20, 1)
self._atr_period = self.Param("AtrPeriod", 14) \
.SetDisplay("ATR Period", "Period for ATR volatility calculation", "Volatility") \
.SetCanOptimize(True) \
.SetOptimize(10, 30, 2)
self._volatility_contraction_factor = self.Param("VolatilityContractionFactor", 2.0) \
.SetDisplay("Volatility Contraction Factor", "Standard deviation multiplier for volatility contraction", "Volatility") \
.SetCanOptimize(True) \
.SetOptimize(1.0, 3.0, 0.5)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._prev_hma = 0.0
self._cur_hma = 0.0
self._atr_values = []
self._is_long = False
self._is_short = False
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)]
def OnReseted(self):
super(hull_ma_volatility_contraction_strategy, self).OnReseted()
self._prev_hma = 0.0
self._cur_hma = 0.0
self._atr_values = []
self._is_long = False
self._is_short = False
def OnStarted2(self, time):
super(hull_ma_volatility_contraction_strategy, self).OnStarted2(time)
hma = HullMovingAverage()
hma.Length = int(self._hma_period.Value)
atr = AverageTrueRange()
atr.Length = int(self._atr_period.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(hma, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, hma)
self.DrawIndicator(area, atr)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(2, UnitTypes.Percent)
)
def _is_volatility_contracted(self):
period = int(self._atr_period.Value)
if len(self._atr_values) < period:
return False
recent = self._atr_values[-period:]
mean = sum(recent) / len(recent)
sum_sq = sum((x - mean) ** 2 for x in recent)
std = math.sqrt(sum_sq / len(recent))
current = self._atr_values[-1]
return current < (mean - std * float(self._volatility_contraction_factor.Value))
def _process_candle(self, candle, hma_val, atr_val):
if candle.State != CandleStates.Finished:
return
self._prev_hma = self._cur_hma
self._cur_hma = float(hma_val)
atr = float(atr_val)
self._atr_values.append(atr)
max_buf = int(self._atr_period.Value) * 2
while len(self._atr_values) > max_buf:
self._atr_values.pop(0)
contracted = self._is_volatility_contracted()
rising = self._cur_hma > self._prev_hma
falling = self._cur_hma < self._prev_hma
if not self.IsFormedAndOnlineAndAllowTrading():
return
if rising and contracted and self.Position <= 0:
self.BuyMarket(self.Volume)
self._is_long = True
self._is_short = False
elif falling and contracted and self.Position >= 0:
self.SellMarket(self.Volume + Math.Abs(self.Position))
self._is_long = False
self._is_short = True
elif self._is_long and falling:
self.SellMarket(self.Position)
self._is_long = False
elif self._is_short and rising:
self.BuyMarket(Math.Abs(self.Position))
self._is_short = False
def CreateClone(self):
return hull_ma_volatility_contraction_strategy()