Hull MA Volatility Contraction
Hull MA Volatility Contraction 策略基于 Hull Moving Average with volatility contraction filter。
测试表明年均收益约为 76%,该策略在外汇市场表现最佳。
当 its indicators confirms volatility contraction patterns 在日内(15m)数据上得到确认时触发信号,适合积极交易者。
止损依赖于 ATR 倍数以及 HmaPeriod, AtrPeriod 等参数,可根据需要调整以平衡风险与收益。
详情
- 入场条件:参见指标条件实现.
- 多空方向:双向.
- 退出条件:反向信号或止损逻辑.
- 止损:是,基于指标计算.
- 默认值:
HmaPeriod = 9AtrPeriod = 14VolatilityContractionFactor = 2.0mCandleType = TimeSpan.FromMinutes(15).TimeFrame()
- 过滤器:
- 分类: 趋势跟随
- 方向: 双向
- 指标: multiple indicators
- 止损: 是
- 复杂度: 中等
- 时间框架: 日内 (15m)
- 季节性: 否
- 神经网络: 否
- 背离: 否
- 风险等级: 中等
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()