Hull MA K-Means Cluster
Hull MA K-Means Cluster 策略基于 that trades based on Hull Moving Average direction with K-Means clustering for market state detection。
测试表明年均收益约为 97%,该策略在加密市场表现最佳。
当 its indicators confirms trend changes 在日内(5m)数据上得到确认时触发信号,适合积极交易者。
止损依赖于 ATR 倍数以及 HullPeriod, ClusterDataLength 等参数,可根据需要调整以平衡风险与收益。
详情
- 入场条件:参见指标条件实现.
- 多空方向:双向.
- 退出条件:反向信号或止损逻辑.
- 止损:是,基于指标计算.
- 默认值:
HullPeriod = 9ClusterDataLength = 50RsiPeriod = 14CandleType = TimeSpan.FromMinutes(5).TimeFrame()
- 过滤器:
- 分类: 趋势跟随
- 方向: 双向
- 指标: multiple indicators
- 止损: 是
- 复杂度: 中等
- 时间框架: 日内 (5m)
- 季节性: 否
- 神经网络: 否
- 背离: 否
- 风险等级: 中等
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 that trades based on Hull Moving Average direction with K-Means clustering for market state detection.
/// </summary>
public class HullKMeansClusterStrategy : Strategy
{
private readonly StrategyParam<int> _hullPeriod;
private readonly StrategyParam<int> _clusterDataLength;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<DataType> _candleType;
private static readonly object _sync = new();
private enum MarketStates
{
Neutral,
Bullish,
Bearish
}
private decimal _prevHullValue;
private MarketStates _currentMarketState = MarketStates.Neutral;
// Feature data for clustering
private readonly Queue<decimal> _priceChangeData = [];
private readonly Queue<decimal> _rsiData = [];
private readonly Queue<decimal> _volumeRatioData = [];
private decimal _lastPrice;
private decimal _avgVolume;
/// <summary>
/// Strategy parameter: Hull Moving Average period.
/// </summary>
public int HullPeriod
{
get => _hullPeriod.Value;
set => _hullPeriod.Value = value;
}
/// <summary>
/// Strategy parameter: Length of data to use for clustering.
/// </summary>
public int ClusterDataLength
{
get => _clusterDataLength.Value;
set => _clusterDataLength.Value = value;
}
/// <summary>
/// Strategy parameter: RSI period for feature calculation.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// Strategy parameter: Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public HullKMeansClusterStrategy()
{
_hullPeriod = Param(nameof(HullPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("Hull MA Period", "Period for Hull Moving Average", "Indicator Settings");
_clusterDataLength = Param(nameof(ClusterDataLength), 50)
.SetGreaterThanZero()
.SetDisplay("Cluster Data Length", "Number of periods to use for clustering", "Clustering Settings");
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "Period for RSI calculation as a clustering feature", "Indicator Settings");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).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 = default;
_currentMarketState = MarketStates.Neutral;
_lastPrice = default;
_avgVolume = default;
_priceChangeData.Clear();
_rsiData.Clear();
_volumeRatioData.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create Hull Moving Average indicator
var hullMa = new HullMovingAverage
{
Length = HullPeriod
};
// Create RSI indicator for feature calculation
var rsi = new RelativeStrengthIndex
{
Length = RsiPeriod
};
// Create subscription for candles
var subscription = SubscribeCandles(CandleType);
// Bind indicators to subscription and start
subscription
.Bind(hullMa, rsi, ProcessCandle)
.Start();
// Add chart visualization
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, hullMa);
DrawOwnTrades(area);
}
// Start position protection with ATR-based stop-loss
StartProtection(
takeProfit: new Unit(0), // No fixed take profit
stopLoss: new Unit(2, UnitTypes.Absolute) // 2 ATR stop-loss
);
}
private void ProcessCandle(ICandleMessage candle, decimal hullValue, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
lock (_sync)
{
UpdateFeatureData(candle, rsiValue);
if (_priceChangeData.Count >= ClusterDataLength &&
_rsiData.Count >= ClusterDataLength &&
_volumeRatioData.Count >= ClusterDataLength)
{
_currentMarketState = DetectMarketState();
}
var isHullRising = hullValue > _prevHullValue;
if (isHullRising && _currentMarketState == MarketStates.Bullish && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (!isHullRising && _currentMarketState == MarketStates.Bearish && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
_prevHullValue = hullValue;
_lastPrice = candle.ClosePrice;
}
}
private void UpdateFeatureData(ICandleMessage candle, decimal rsiValue)
{
// Calculate price change percentage
if (_lastPrice != 0)
{
decimal priceChange = (candle.ClosePrice - _lastPrice) / _lastPrice * 100;
// Maintain price change data queue
_priceChangeData.Enqueue(priceChange);
if (_priceChangeData.Count > ClusterDataLength)
_priceChangeData.Dequeue();
}
// Maintain RSI data queue
_rsiData.Enqueue(rsiValue);
if (_rsiData.Count > ClusterDataLength)
_rsiData.Dequeue();
// Calculate volume ratio and maintain queue
if (_avgVolume == 0)
{
_avgVolume = candle.TotalVolume;
}
else
{
// Exponential smoothing for average volume
_avgVolume = 0.9m * _avgVolume + 0.1m * candle.TotalVolume;
}
decimal volumeRatio = candle.TotalVolume / (_avgVolume == 0 ? 1 : _avgVolume);
_volumeRatioData.Enqueue(volumeRatio);
if (_volumeRatioData.Count > ClusterDataLength)
_volumeRatioData.Dequeue();
}
private MarketStates DetectMarketState()
{
var priceChanges = _priceChangeData.ToArray();
var rsiValues = _rsiData.ToArray();
var volumeRatios = _volumeRatioData.ToArray();
if (priceChanges.Length == 0 || rsiValues.Length == 0 || volumeRatios.Length == 0)
return MarketStates.Neutral;
decimal avgPriceChange = priceChanges.Average();
decimal avgRsi = rsiValues.Average();
decimal avgVolumeRatio = volumeRatios.Average();
// Detect market state based on features
// Higher RSI, positive price change and higher volume -> Bullish
// Lower RSI, negative price change and higher volume -> Bearish
// Otherwise -> Neutral
if (avgRsi > 60 && avgPriceChange > 0.1m && avgVolumeRatio > 1.1m)
{
return MarketStates.Bullish;
}
else if (avgRsi < 40 && avgPriceChange < -0.1m && avgVolumeRatio > 1.1m)
{
return MarketStates.Bearish;
}
else
{
return MarketStates.Neutral;
}
}
}
import clr
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, Unit, UnitTypes, CandleStates
from StockSharp.Algo.Indicators import HullMovingAverage, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
NEUTRAL = 0
BULLISH = 1
BEARISH = 2
class hull_kmeans_cluster_strategy(Strategy):
"""Strategy that trades based on Hull Moving Average direction with K-Means clustering for market state detection."""
def __init__(self):
super(hull_kmeans_cluster_strategy, self).__init__()
self._hull_period = self.Param("HullPeriod", 9) \
.SetGreaterThanZero() \
.SetDisplay("Hull MA Period", "Period for Hull Moving Average", "Indicator Settings")
self._cluster_data_length = self.Param("ClusterDataLength", 50) \
.SetGreaterThanZero() \
.SetDisplay("Cluster Data Length", "Number of periods to use for clustering", "Clustering Settings")
self._rsi_period = self.Param("RsiPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("RSI Period", "Period for RSI calculation as a clustering feature", "Indicator Settings")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._price_change_data = []
self._rsi_data = []
self._volume_ratio_data = []
self._prev_hull_value = 0.0
self._last_price = 0.0
self._avg_volume = 0.0
self._current_market_state = NEUTRAL
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)]
def OnReseted(self):
super(hull_kmeans_cluster_strategy, self).OnReseted()
self._prev_hull_value = 0.0
self._current_market_state = NEUTRAL
self._last_price = 0.0
self._avg_volume = 0.0
self._price_change_data = []
self._rsi_data = []
self._volume_ratio_data = []
def OnStarted2(self, time):
super(hull_kmeans_cluster_strategy, self).OnStarted2(time)
hull_ma = HullMovingAverage()
hull_ma.Length = int(self._hull_period.Value)
rsi = RelativeStrengthIndex()
rsi.Length = int(self._rsi_period.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(hull_ma, rsi, self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, hull_ma)
self.DrawOwnTrades(area)
self.StartProtection(
takeProfit=Unit(0),
stopLoss=Unit(2, UnitTypes.Absolute)
)
def ProcessCandle(self, candle, hull_value, rsi_value):
if candle.State != CandleStates.Finished:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
hull_val = float(hull_value)
rsi_val = float(rsi_value)
self._update_feature_data(candle, rsi_val)
data_len = int(self._cluster_data_length.Value)
if len(self._price_change_data) >= data_len and \
len(self._rsi_data) >= data_len and \
len(self._volume_ratio_data) >= data_len:
self._current_market_state = self._detect_market_state()
is_hull_rising = hull_val > self._prev_hull_value
if is_hull_rising and self._current_market_state == BULLISH and self.Position <= 0:
self.BuyMarket(self.Volume + Math.Abs(self.Position))
elif not is_hull_rising and self._current_market_state == BEARISH and self.Position >= 0:
self.SellMarket(self.Volume + Math.Abs(self.Position))
self._prev_hull_value = hull_val
self._last_price = float(candle.ClosePrice)
def _update_feature_data(self, candle, rsi_value):
data_len = int(self._cluster_data_length.Value)
close_price = float(candle.ClosePrice)
if self._last_price != 0.0:
price_change = (close_price - self._last_price) / self._last_price * 100.0
self._price_change_data.append(price_change)
while len(self._price_change_data) > data_len:
self._price_change_data.pop(0)
self._rsi_data.append(rsi_value)
while len(self._rsi_data) > data_len:
self._rsi_data.pop(0)
total_volume = float(candle.TotalVolume)
if self._avg_volume == 0.0:
self._avg_volume = total_volume
else:
self._avg_volume = 0.9 * self._avg_volume + 0.1 * total_volume
volume_ratio = total_volume / (self._avg_volume if self._avg_volume != 0.0 else 1.0)
self._volume_ratio_data.append(volume_ratio)
while len(self._volume_ratio_data) > data_len:
self._volume_ratio_data.pop(0)
def _detect_market_state(self):
if len(self._price_change_data) == 0 or len(self._rsi_data) == 0 or len(self._volume_ratio_data) == 0:
return NEUTRAL
avg_price_change = sum(self._price_change_data) / len(self._price_change_data)
avg_rsi = sum(self._rsi_data) / len(self._rsi_data)
avg_volume_ratio = sum(self._volume_ratio_data) / len(self._volume_ratio_data)
if avg_rsi > 60.0 and avg_price_change > 0.1 and avg_volume_ratio > 1.1:
return BULLISH
elif avg_rsi < 40.0 and avg_price_change < -0.1 and avg_volume_ratio > 1.1:
return BEARISH
else:
return NEUTRAL
def CreateClone(self):
return hull_kmeans_cluster_strategy()