Hull MA 斜率突破策略
本策略跟踪 Hull 均线斜率的变化。当斜率异常陡峭时,往往预示着新趋势的形成。
测试表明年均收益约为 121%,该策略在加密市场表现最佳。
当斜率超过常态水平若干个标准差时,沿加速方向开仓并设置保护性止损。斜率回归正常后平仓。默认 HullLength = 9。
该方法适合积极交易者在趋势初期参与。
详细信息
- 入场条件: Indicator exceeds average by deviation multiplier.
- Long/Short: 双向 directions.
- 退出条件: Indicator reverts to average.
- 止损: 是
- 默认值:
HullLength= 9LookbackPeriod= 20DeviationMultiplier= 2mStopLoss= new Unit(2CandleType= TimeSpan.FromMinutes(5)
- 筛选条件:
- 类别: 突破
- 方向: 双向
- 指标: Hull
- 止损: 是
- 复杂度: 中等
- 时间框架: 短期
- 季节性: 否
- 神经网络: 否
- 背离: 否
- 风险等级: 中等
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 Slope breakout
/// Enters positions when the slope of Hull MA exceeds average slope plus a multiple of standard deviation
/// </summary>
public class HullMaSlopeBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _hullLength;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _deviationMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<Unit> _stopLoss;
private HullMovingAverage _hullMa;
private AverageTrueRange _atr;
private decimal _prevHullValue;
private decimal _currentSlope;
private decimal _avgSlope;
private decimal _stdDevSlope;
private decimal[] _slopes;
private int _currentIndex;
private bool _isInitialized;
/// <summary>
/// Hull Moving Average length
/// </summary>
public int HullLength
{
get => _hullLength.Value;
set => _hullLength.Value = value;
}
/// <summary>
/// Lookback period for slope statistics calculation
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Standard deviation multiplier for breakout detection
/// </summary>
public decimal DeviationMultiplier
{
get => _deviationMultiplier.Value;
set => _deviationMultiplier.Value = value;
}
/// <summary>
/// Candle type
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Stop loss value
/// </summary>
public Unit StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Constructor
/// </summary>
public HullMaSlopeBreakoutStrategy()
{
_hullLength = Param(nameof(HullLength), 9)
.SetGreaterThanZero()
.SetDisplay("Hull MA Length", "Period for Hull Moving Average", "Indicator Parameters")
.SetOptimize(5, 20, 1);
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Period for slope statistics calculation", "Strategy Parameters")
.SetOptimize(10, 50, 5);
_deviationMultiplier = Param(nameof(DeviationMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Deviation Multiplier", "Standard deviation multiplier for breakout detection", "Strategy Parameters")
.SetOptimize(1m, 3m, 0.5m);
_stopLoss = Param(nameof(StopLoss), new Unit(2, UnitTypes.Absolute))
.SetDisplay("Stop Loss", "Stop loss value in ATRs", "Risk Management");
_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();
_prevHullValue = 0;
_currentSlope = 0;
_avgSlope = 0;
_stdDevSlope = 0;
_currentIndex = 0;
_isInitialized = false;
_slopes = new decimal[LookbackPeriod];
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
_slopes = new decimal[LookbackPeriod];
_hullMa = new HullMovingAverage { Length = HullLength };
_atr = new AverageTrueRange { Length = 14 }; // ATR for stop-loss
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_hullMa, _atr, ProcessCandle)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _hullMa);
DrawOwnTrades(area);
}
// Set up position protection
StartProtection(
takeProfit: null, // We'll handle exits via strategy logic
stopLoss: StopLoss,
isStopTrailing: false
);
base.OnStarted2(time);
}
private void ProcessCandle(ICandleMessage candle, decimal hullValue, decimal atrValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Check if indicator is formed
if (!_hullMa.IsFormed)
return;
// Extract Hull MA value
decimal currentHullValue = hullValue;
// Calculate the slope
if (!_isInitialized)
{
_prevHullValue = currentHullValue;
_isInitialized = true;
return;
}
// Calculate current slope (simple difference for now)
_currentSlope = currentHullValue - _prevHullValue;
if (_slopes is null || _slopes.Length != LookbackPeriod)
{
_slopes = new decimal[LookbackPeriod];
_currentIndex = 0;
_prevHullValue = currentHullValue;
return;
}
// Store slope in array and update index
_slopes[_currentIndex] = _currentSlope;
_currentIndex = (_currentIndex + 1) % _slopes.Length;
// Calculate statistics once we have enough data
if (!IsFormedAndOnlineAndAllowTrading())
return;
CalculateStatistics();
// Trading logic
if (Math.Abs(_avgSlope) > 0) // Avoid division by zero
{
// Long signal: slope exceeds average + k*stddev (slope is positive and we don't have a long position)
if (_currentSlope > 0 &&
_currentSlope > _avgSlope + DeviationMultiplier * _stdDevSlope &&
Position <= 0)
{
// Cancel existing orders
CancelActiveOrders();
// Enter long position
var volume = Volume + Math.Abs(Position);
BuyMarket(volume);
LogInfo($"Long signal: Slope {_currentSlope} > Avg {_avgSlope} + {DeviationMultiplier}*StdDev {_stdDevSlope}");
}
// Short signal: slope exceeds average + k*stddev in negative direction (slope is negative and we don't have a short position)
else if (_currentSlope < 0 &&
_currentSlope < _avgSlope - DeviationMultiplier * _stdDevSlope &&
Position >= 0)
{
// Cancel existing orders
CancelActiveOrders();
// Enter short position
var volume = Volume + Math.Abs(Position);
SellMarket(volume);
LogInfo($"Short signal: Slope {_currentSlope} < Avg {_avgSlope} - {DeviationMultiplier}*StdDev {_stdDevSlope}");
}
// Exit conditions - when slope returns to average
if (Position > 0 && _currentSlope < _avgSlope)
{
// Exit long position
SellMarket(Math.Abs(Position));
LogInfo($"Exit long: Slope {_currentSlope} < Avg {_avgSlope}");
}
else if (Position < 0 && _currentSlope > _avgSlope)
{
// Exit short position
BuyMarket(Math.Abs(Position));
LogInfo($"Exit short: Slope {_currentSlope} > Avg {_avgSlope}");
}
}
// Store current Hull MA value for next slope calculation
_prevHullValue = currentHullValue;
}
private void CalculateStatistics()
{
var period = _slopes?.Length ?? 0;
if (period <= 0)
return;
// Reset statistics
_avgSlope = 0;
decimal sumSquaredDiffs = 0;
// Calculate average
for (int i = 0; i < period; i++)
{
_avgSlope += _slopes[i];
}
_avgSlope /= period;
// Calculate standard deviation
for (int i = 0; i < period; i++)
{
decimal diff = _slopes[i] - _avgSlope;
sumSquaredDiffs += diff * diff;
}
_stdDevSlope = (decimal)Math.Sqrt((double)(sumSquaredDiffs / period));
}
}
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_slope_breakout_strategy(Strategy):
"""
Hull MA slope breakout. Enters when slope exceeds avg + k*stddev.
"""
def __init__(self):
super(hull_ma_slope_breakout_strategy, self).__init__()
self._hull_length = self.Param("HullLength", 9).SetDisplay("Hull Length", "Hull MA period", "Indicators")
self._lookback = self.Param("LookbackPeriod", 20).SetDisplay("Lookback", "Slope stats period", "Strategy")
self._dev_mult = self.Param("DeviationMultiplier", 2.0).SetDisplay("Dev Mult", "Stddev multiplier", "Strategy")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
self._prev_hull = 0.0
self._slopes = None
self._current_index = 0
self._is_init = False
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(hull_ma_slope_breakout_strategy, self).OnReseted()
self._prev_hull = 0.0
self._slopes = [0.0] * int(self._lookback.Value)
self._current_index = 0
self._is_init = False
def OnStarted2(self, time):
super(hull_ma_slope_breakout_strategy, self).OnStarted2(time)
lb = int(self._lookback.Value)
self._slopes = [0.0] * lb
self._current_index = 0
hma = HullMovingAverage()
hma.Length = self._hull_length.Value
atr = AverageTrueRange()
atr.Length = 14
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(hma, atr, self._process_candle).Start()
self.StartProtection(None, Unit(2, UnitTypes.Absolute))
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, hma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, hull_val, atr_val):
if candle.State != CandleStates.Finished:
return
hull = float(hull_val)
if not self._is_init:
self._prev_hull = hull
self._is_init = True
return
current_slope = hull - self._prev_hull
lb = int(self._lookback.Value)
if self._slopes is None or len(self._slopes) != lb:
self._slopes = [0.0] * lb
self._current_index = 0
self._prev_hull = hull
return
# Store slope in circular array
self._slopes[self._current_index] = current_slope
self._current_index = (self._current_index + 1) % lb
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_hull = hull
return
# Calculate statistics
avg_slope = sum(self._slopes) / lb
sum_sq = sum((s - avg_slope) ** 2 for s in self._slopes)
std_slope = math.sqrt(sum_sq / lb)
dm = float(self._dev_mult.Value)
if abs(avg_slope) > 0:
# Long signal
if current_slope > 0 and current_slope > avg_slope + dm * std_slope and self.Position <= 0:
self.CancelActiveOrders()
vol = self.Volume + Math.Abs(self.Position)
self.BuyMarket(vol)
# Short signal
elif current_slope < 0 and current_slope < avg_slope - dm * std_slope and self.Position >= 0:
self.CancelActiveOrders()
vol = self.Volume + Math.Abs(self.Position)
self.SellMarket(vol)
# Exit conditions
if self.Position > 0 and current_slope < avg_slope:
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0 and current_slope > avg_slope:
self.BuyMarket(Math.Abs(self.Position))
self._prev_hull = hull
def CreateClone(self):
return hull_ma_slope_breakout_strategy()