Ver en GitHub

Estrategia de Divergencia Estacional HMA

Esta estrategia combina la Media Móvil Hull (HMA) con la agrupación estacional del interés abierto para encontrar divergencias entre el precio y el posicionamiento del mercado. Asume que cuando el precio se mueve temporalmente contra la dirección del interés abierto creciente, es probable una continuación de la tendencia. El sistema está diseñado para operar tanto en largo como en corto, utilizando la pendiente de la HMA para medir el impulso y los datos estacionales de interés abierto para medir los niveles de participación.

Las pruebas indican un rendimiento anual promedio de aproximadamente el 40%. Funciona mejor en el mercado de criptomonedas.

Una configuración de operación ocurre cuando la HMA cambia respecto a la barra anterior mientras el interés abierto estacional confirma el movimiento, pero el precio imprime en la dirección opuesta. Esta divergencia alcista o bajista entre el precio y el posicionamiento a menudo señala el fin de un retroceso de corto plazo dentro de una tendencia mayor. La estrategia espera estas condiciones antes de entrar y coloca un stop basado en volatilidad para gestionar el riesgo.

Las posiciones se cierran cuando la pendiente de la HMA se invierte, lo que indica que el impulso ha cambiado. Dado que el nivel del stop utiliza un múltiplo del Rango Verdadero Promedio (ATR), el riesgo se adapta a la volatilidad del mercado. Esto ayuda a prevenir salidas prematuras durante períodos de expansión y mantiene las pérdidas contenidas cuando la volatilidad se contrae.

Detalles

  • Criterios de entrada:
    • Largo: HMA(t) > HMA(t-1) && OI_Cluster_Seasonal(t) > OI_Cluster_Seasonal(t-1) && Price(t) < Price(t-1) (divergencia alcista).
    • Corto: HMA(t) < HMA(t-1) && OI_Cluster_Seasonal(t) < OI_Cluster_Seasonal(t-1) && Price(t) > Price(t-1) (divergencia bajista).
  • Largo/Corto: Ambos lados.
  • Criterios de salida:
    • Largo: HMA(t) < HMA(t-1) (la HMA comienza a caer).
    • Corto: HMA(t) > HMA(t-1) (la HMA comienza a subir).
  • Stops: Sí, stop-loss colocado en N * ATR desde la entrada.
  • Valores predeterminados:
    • HMA period = 9.
    • OI_Cluster_Seasonal = OI estacional en niveles de clúster durante cinco años.
    • N = 2 (stop-loss = 2 * ATR).
  • Filtros:
    • Categoría: Seguimiento de tendencia
    • Dirección: Ambos
    • Indicadores: Múltiples
    • Stops: Sí
    • Complejidad: Complejo
    • Marco temporal: Medio plazo
    • Estacionalidad: Sí
    • Redes neuronales: Sí
    • Divergencia: Sí
    • Nivel de riesgo: Alto
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Moving average crossover strategy.
/// Enters long when fast MA crosses above slow MA.
/// Enters short when fast MA crosses below slow MA.
/// Implements stop-loss as a percentage of entry price.
/// </summary>
public class MaCrossoverStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<decimal> _stopLossPercent;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _entryPrice;
	private bool _isLongPosition;

	/// <summary>
	/// Fast MA period length.
	/// </summary>
	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	/// <summary>
	/// Slow MA period length.
	/// </summary>
	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

	/// <summary>
	/// Stop-loss percentage.
	/// </summary>
	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

	/// <summary>
	/// The type of candles to use for strategy calculation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Constructor.
	/// </summary>
	public MaCrossoverStrategy()
	{
		_fastLength = Param(nameof(FastLength), 100)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Length", "Period of the fast moving average", "MA Settings")

			.SetOptimize(5, 20, 5);

		_slowLength = Param(nameof(SlowLength), 400)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Length", "Period of the slow moving average", "MA Settings")

			.SetOptimize(20, 100, 10);

		_stopLossPercent = Param(nameof(StopLossPercent), 2.0m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk Management")

			.SetOptimize(1.0m, 5.0m, 1.0m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).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();
		// Initialize variables
		_entryPrice = 0;
		_isLongPosition = false;

	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		// Create indicators
		var fastMa = new ExponentialMovingAverage { Length = FastLength };
		var slowMa = new ExponentialMovingAverage { Length = SlowLength };

		// Create and setup subscription for candles
		var subscription = SubscribeCandles(CandleType);
		
		// Previous values for crossover detection
		var previousFastValue = 0m;
		var previousSlowValue = 0m;
		var wasFastLessThanSlow = false;
		var isInitialized = false;
		
		subscription
			.Bind(fastMa, slowMa, (candle, fastValue, slowValue) =>
			{
				// Skip unfinished candles
				if (candle.State != CandleStates.Finished)
					return;

				// Check if strategy is ready to trade
				if (!IsFormedAndOnlineAndAllowTrading())
					return;
					
				// Initialize on first complete values
				if (!isInitialized && fastMa.IsFormed && slowMa.IsFormed)
				{
					previousFastValue = fastValue;
					previousSlowValue = slowValue;
					wasFastLessThanSlow = fastValue < slowValue;
					isInitialized = true;
					LogInfo($"Strategy initialized. Fast MA: {fastValue}, Slow MA: {slowValue}");
					return;
				}
				
				if (!isInitialized)
					return;

				// Current crossover state
				var isFastLessThanSlow = fastValue < slowValue;
				
				LogInfo($"Candle: {candle.OpenTime}, Close: {candle.ClosePrice}, Fast MA: {fastValue}, Slow MA: {slowValue}");
				
				// Check for crossovers
				if (wasFastLessThanSlow != isFastLessThanSlow)
				{
					// Crossover happened
					if (!isFastLessThanSlow) // Fast MA crossed above Slow MA
					{
						// Buy signal
						if (Position <= 0)
						{
							_entryPrice = candle.ClosePrice;
							_isLongPosition = true;
							BuyMarket(Volume + Math.Abs(Position));
						}
					}
					else // Fast MA crossed below Slow MA
					{
						// Sell signal
						if (Position >= 0)
						{
							_entryPrice = candle.ClosePrice;
							_isLongPosition = false;
							SellMarket(Volume + Math.Abs(Position));
						}
					}

					// Update the crossover state
					wasFastLessThanSlow = isFastLessThanSlow;
				}
				
				// Update previous values
				previousFastValue = fastValue;
				previousSlowValue = slowValue;
			})
			.Start();

		// Setup chart if available
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, fastMa);
			DrawIndicator(area, slowMa);
			DrawOwnTrades(area);
		}
	}

	private void CheckStopLoss(decimal currentPrice)
	{
		if (_entryPrice == 0)
			return;

		var stopLossThreshold = _stopLossPercent.Value / 100.0m;
		
		if (_isLongPosition && Position > 0)
		{
			// For long positions, exit if price falls below entry price - stop percentage
			var stopPrice = _entryPrice * (1.0m - stopLossThreshold);
			if (currentPrice <= stopPrice)
			{
				SellMarket(Math.Abs(Position));
				LogInfo($"Long stop-loss triggered at {currentPrice}. Entry was {_entryPrice}, Stop level: {stopPrice}");
			}
		}
		else if (!_isLongPosition && Position < 0)
		{
			// For short positions, exit if price rises above entry price + stop percentage
			var stopPrice = _entryPrice * (1.0m + stopLossThreshold);
			if (currentPrice >= stopPrice)
			{
				BuyMarket(Math.Abs(Position));
				LogInfo($"Short stop-loss triggered at {currentPrice}. Entry was {_entryPrice}, Stop level: {stopPrice}");
			}
		}
	}
}