Ver en GitHub

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 = 9
    • ClusterDataLength = 50
    • RsiPeriod = 14
    • CandleType = 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;
		}
	}
}