Estrategia Hull MA con K-Means Cluster
La estrategia Hull MA K-Means Cluster opera basándose en la dirección de la Media Móvil Hull con agrupamiento K-Means para la detección del estado del mercado.
Las pruebas indican un rendimiento anual promedio de aproximadamente 97%. Funciona mejor en el mercado de criptomonedas.
Las señales se activan cuando los indicadores confirman cambios de tendencia en datos intradía (5m). Esto hace que el método sea adecuado para traders activos.
Los stops se basan en múltiplos de ATR y factores como HullPeriod, ClusterDataLength. Ajuste estos valores predeterminados para equilibrar el riesgo y la recompensa.
Detalles
- Criterios de entrada: consulte la implementación para conocer las condiciones de los indicadores.
- Largo/Corto: Ambos direcciones.
- Criterios de salida: señal opuesta o lógica de stop.
- Stops: Sí, utilizando cálculos basados en indicadores.
- Valores predeterminados:
HullPeriod = 9ClusterDataLength = 50RsiPeriod = 14CandleType = TimeSpan.FromMinutes(5).TimeFrame()
- Filtros:
- Categoría: Seguimiento de tendencia
- Dirección: Ambos
- Indicadores: Múltiples indicadores
- Stops: Sí
- Complejidad: Intermedio
- Marco temporal: Intradía (5m)
- Estacionalidad: No
- Redes neuronales: No
- Divergencia: No
- Nivel de riesgo: Medio
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()