Ver en GitHub

Estrategia HarVesteR

La estrategia HarVesteR combina el momentum del MACD con dos medias móviles simples y un filtro opcional de fuerza de tendencia ADX. Busca situaciones en las que el precio se adhiere a las medias móviles mientras el MACD ha cruzado recientemente la línea cero, señalando un posible rompimiento de la consolidación. Los stops se colocan en los máximos o mínimos oscilantes, se toma la mitad de la posición en un múltiplo fijo de recompensa, y el resto se protege con una salida de punto de equilibrio impulsada por la media móvil rápida.

Detalles

  • Criterios de entrada:
    • Largo: MACD > 0 && MACD history contains negative value && Close < SlowSMA && Close + Indentation > FastSMA && Close + Indentation > SlowSMA && ADX ≥ AdxBuyLevel (if enabled)
    • Corto: MACD < 0 && MACD history contains positive value && Close > SlowSMA && Close - Indentation < FastSMA && Close - Indentation < SlowSMA && ADX ≥ AdxSellLevel (if enabled)
  • Stop Loss: Último mínimo/máximo oscilante en StopLookback velas completadas.
  • Salida parcial: Cierra la mitad de la posición cuando el precio se mueve HalfCloseRatio veces la distancia entre entrada y stop, luego mueve el stop al punto de equilibrio.
  • Salida final:
    • Largo: cierra el resto si el precio cae por debajo de FastSMA + Indentation después de que el stop esté en punto de equilibrio.
    • Corto: cierra el resto si el precio sube por encima de FastSMA + Indentation después de que el stop esté en punto de equilibrio.
  • Largo/Corto: Ambas direcciones soportadas.
  • Filtros: Filtro opcional de fuerza de tendencia ADX; establezca UseAdxFilter en false para desactivarlo.
  • Gestión de posición: Revierte la posición compensando el volumen de la señal opuesta más la exposición actual.

Parámetros

Nombre Predeterminado Descripción
MacdFast 12 Período EMA rápido para la línea de diferencia del MACD.
MacdSlow 24 Período EMA lento para la línea de diferencia del MACD.
MacdSignal 9 Período EMA de señal para suavizado del MACD.
MacdLookback 6 Número de velas recientemente completadas verificadas para un cambio de signo del MACD.
SmaFastLength 50 Longitud de la media móvil simple rápida.
SmaSlowLength 100 Longitud de la media móvil simple lenta.
MinIndentation 10 Desplazamiento en pips aplicado alrededor de las medias móviles antes de entrar o salir.
StopLookback 6 Retroceso de máximo/mínimo oscilante utilizado para inicializar el nivel de stop inicial.
UseAdxFilter false Habilita el filtro de fuerza ADX para ambas direcciones.
AdxBuyLevel 50 Nivel mínimo de ADX requerido para permitir entradas largas cuando el filtro está habilitado.
AdxSellLevel 50 Nivel mínimo de ADX requerido para permitir entradas cortas cuando el filtro está habilitado.
AdxPeriod 14 Período utilizado para el cálculo del ADX.
HalfCloseRatio 2 Multiplicador aplicado a la distancia entrada-stop antes de tomar ganancias parciales.
Volume 1 Volumen de orden para nuevas entradas (compensando cualquier exposición opuesta).
CandleType 1 hour Marco temporal principal utilizado para construir velas e indicadores.

Notas

  • MinIndentation se convierte en distancia de precio usando el tamaño del tick del instrumento. Los instrumentos cotizados con tres o cinco decimales reciben un ajuste de diez veces para aproximar las unidades de pip.
  • Cuando UseAdxFilter está desactivado, la estrategia acepta señales en ambas direcciones sin verificar el valor del ADX.
  • La toma de ganancias parciales y las salidas de punto de equilibrio se ejecutan en cada vela completada para proteger las posiciones abiertas incluso cuando no se permiten nuevas operaciones.
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>
/// Trend strategy that combines MACD momentum, moving average proximity and ADX filter with partial profit taking.
/// </summary>
public class HarVesteRStrategy : Strategy
{
	private readonly StrategyParam<int> _macdFast;
	private readonly StrategyParam<int> _macdSlow;
	private readonly StrategyParam<int> _macdSignal;
	private readonly StrategyParam<int> _macdLookback;
	private readonly StrategyParam<int> _smaFastLength;
	private readonly StrategyParam<int> _smaSlowLength;
	private readonly StrategyParam<decimal> _minIndentation;
	private readonly StrategyParam<int> _stopLookback;
	private readonly StrategyParam<bool> _useAdx;
	private readonly StrategyParam<decimal> _adxBuyLevel;
	private readonly StrategyParam<decimal> _adxSellLevel;
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<int> _halfCloseRatio;
	private readonly StrategyParam<DataType> _candleType;

	private MovingAverageConvergenceDivergenceSignal _macd = null!;
	private SimpleMovingAverage _smaFast = null!;
	private SimpleMovingAverage _smaSlow = null!;
	private AverageDirectionalIndex _adx = null!;
	private Lowest _lowest = null!;
	private Highest _highest = null!;

	private readonly List<decimal> _macdHistory = new();
	private decimal? _lastLowest;
	private decimal? _lastHighest;

	private decimal? _longEntry;
	private decimal? _longStop;
	private bool _longStopMoved;

	private decimal? _shortEntry;
	private decimal? _shortStop;
	private bool _shortStopMoved;

	/// <summary>
	/// Fast period for MACD.
	/// </summary>
	public int MacdFast
	{
		get => _macdFast.Value;
		set => _macdFast.Value = value;
	}

	/// <summary>
	/// Slow period for MACD.
	/// </summary>
	public int MacdSlow
	{
		get => _macdSlow.Value;
		set => _macdSlow.Value = value;
	}

	/// <summary>
	/// Signal line period for MACD.
	/// </summary>
	public int MacdSignal
	{
		get => _macdSignal.Value;
		set => _macdSignal.Value = value;
	}

	/// <summary>
	/// Number of bars used to confirm MACD sign change.
	/// </summary>
	public int MacdLookback
	{
		get => _macdLookback.Value;
		set => _macdLookback.Value = value;
	}

	/// <summary>
	/// Fast simple moving average length.
	/// </summary>
	public int SmaFastLength
	{
		get => _smaFastLength.Value;
		set => _smaFastLength.Value = value;
	}

	/// <summary>
	/// Slow simple moving average length.
	/// </summary>
	public int SmaSlowLength
	{
		get => _smaSlowLength.Value;
		set => _smaSlowLength.Value = value;
	}

	/// <summary>
	/// Minimum indentation measured in pips.
	/// </summary>
	public decimal MinIndentation
	{
		get => _minIndentation.Value;
		set => _minIndentation.Value = value;
	}

	/// <summary>
	/// Bars used to compute stop loss levels.
	/// </summary>
	public int StopLookback
	{
		get => _stopLookback.Value;
		set => _stopLookback.Value = value;
	}

	/// <summary>
	/// Enable ADX filter for entries.
	/// </summary>
	public bool UseAdxFilter
	{
		get => _useAdx.Value;
		set => _useAdx.Value = value;
	}

	/// <summary>
	/// Minimum ADX strength required to buy.
	/// </summary>
	public decimal AdxBuyLevel
	{
		get => _adxBuyLevel.Value;
		set => _adxBuyLevel.Value = value;
	}

	/// <summary>
	/// Minimum ADX strength required to sell.
	/// </summary>
	public decimal AdxSellLevel
	{
		get => _adxSellLevel.Value;
		set => _adxSellLevel.Value = value;
	}

	/// <summary>
	/// ADX indicator period.
	/// </summary>
	public int AdxPeriod
	{
		get => _adxPeriod.Value;
		set => _adxPeriod.Value = value;
	}

	/// <summary>
	/// Ratio used to trigger half position exit.
	/// </summary>
	public int HalfCloseRatio
	{
		get => _halfCloseRatio.Value;
		set => _halfCloseRatio.Value = value;
	}


	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="HarVesteRStrategy"/>.
	/// </summary>
	public HarVesteRStrategy()
	{
		_macdFast = Param(nameof(MacdFast), 12)
			.SetGreaterThanZero()
			.SetDisplay("MACD Fast EMA", "Short EMA period for MACD", "MACD")
			;

		_macdSlow = Param(nameof(MacdSlow), 24)
			.SetGreaterThanZero()
			.SetDisplay("MACD Slow EMA", "Long EMA period for MACD", "MACD")
			;

		_macdSignal = Param(nameof(MacdSignal), 9)
			.SetGreaterThanZero()
			.SetDisplay("MACD Signal", "Signal averaging period", "MACD")
			;

		_macdLookback = Param(nameof(MacdLookback), 6)
			.SetGreaterThanZero()
			.SetDisplay("MACD Lookback", "Bars to confirm MACD sign change", "MACD")
			;

		_smaFastLength = Param(nameof(SmaFastLength), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast SMA", "First moving average length", "Moving Averages")
			;

		_smaSlowLength = Param(nameof(SmaSlowLength), 20)
			.SetGreaterThanZero()
			.SetDisplay("Slow SMA", "Second moving average length", "Moving Averages")
			;

		_minIndentation = Param(nameof(MinIndentation), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Indentation", "Distance from moving averages in pips", "Trading")
			;

		_stopLookback = Param(nameof(StopLookback), 6)
			.SetGreaterThanZero()
			.SetDisplay("Stop Lookback", "Bars for stop loss calculation", "Risk")
			;

		_useAdx = Param(nameof(UseAdxFilter), false)
			.SetDisplay("Use ADX", "Enable ADX trend filter", "ADX");

		_adxBuyLevel = Param(nameof(AdxBuyLevel), 50m)
			.SetGreaterThanZero()
			.SetDisplay("ADX Buy Level", "Minimum ADX strength for longs", "ADX");

		_adxSellLevel = Param(nameof(AdxSellLevel), 50m)
			.SetGreaterThanZero()
			.SetDisplay("ADX Sell Level", "Minimum ADX strength for shorts", "ADX");

		_adxPeriod = Param(nameof(AdxPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ADX Period", "ADX calculation length", "ADX")
			;

		_halfCloseRatio = Param(nameof(HalfCloseRatio), 2)
			.SetGreaterThanZero()
			.SetDisplay("Half Close Ratio", "Multiplier applied to stop distance", "Risk")
			;


		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_macdHistory.Clear();
		_lastLowest = null;
		_lastHighest = null;
		ResetLongState();
		ResetShortState();
	}

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

		// Configure indicators used by the strategy.
		_macd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = MacdFast },
				LongMa = { Length = MacdSlow },
			},
			SignalMa = { Length = MacdSignal }
		};

		_smaFast = new SMA { Length = SmaFastLength };
		_smaSlow = new SMA { Length = SmaSlowLength };
		_adx = new AverageDirectionalIndex { Length = AdxPeriod };
		_lowest = new Lowest { Length = StopLookback };
		_highest = new Highest { Length = StopLookback };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_macd, _smaFast, _smaSlow, _adx, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _smaFast);
			DrawIndicator(area, _smaSlow);
			DrawIndicator(area, _macd);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdValue, IIndicatorValue smaFastValue, IIndicatorValue smaSlowValue, IIndicatorValue adxValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Update trailing stop helpers from recent highs and lows.
		var lowValue = _lowest.Process(new DecimalIndicatorValue(_lowest, candle.LowPrice, candle.ServerTime) { IsFinal = true });
		if (lowValue.IsFormed)
			_lastLowest = lowValue.ToDecimal();

		var highValue = _highest.Process(new DecimalIndicatorValue(_highest, candle.HighPrice, candle.ServerTime) { IsFinal = true });
		if (highValue.IsFormed)
			_lastHighest = highValue.ToDecimal();

		if (!macdValue.IsFinal || !smaFastValue.IsFinal || !smaSlowValue.IsFinal)
			return;

		var macdTyped = (MovingAverageConvergenceDivergenceSignalValue)macdValue;
		if (macdTyped.Macd is not decimal macdMain)
			return;

		var smaFast = smaFastValue.ToDecimal();
		var smaSlow = smaSlowValue.ToDecimal();

		decimal? adxStrength = null;
		if (UseAdxFilter)
		{
			if (!adxValue.IsFinal)
				return;

			var adxTyped = (AverageDirectionalIndexValue)adxValue;
			adxStrength = adxTyped.MovingAverage;
			if (adxStrength is not decimal)
				return;
		}

		_macdHistory.Add(macdMain);
		while (_macdHistory.Count > MacdLookback)
			try { _macdHistory.RemoveAt(0); } catch { break; }

		var indentation = GetIndentation();
		var close = candle.ClosePrice;

		if (macdMain == 0m || smaFast == 0m || smaSlow == 0m || close <= 0m)
			return;

		// Manage partial exits and break-even logic for open positions.
		ManageOpenPositions(close, smaFast, indentation);

		if (!_macd.IsFormed || !_smaFast.IsFormed || !_smaSlow.IsFormed)
			return;

		if (_macdHistory.Count < MacdLookback)
			return;

		var hadNegative = HasNegativeMacd();
		var hadPositive = HasPositiveMacd();

		var adxBuyOk = !UseAdxFilter;
		var adxSellOk = !UseAdxFilter;
		if (UseAdxFilter && adxStrength is decimal adxValueDecimal)
		{
			adxBuyOk = adxValueDecimal >= AdxBuyLevel;
			adxSellOk = adxValueDecimal >= AdxSellLevel;
		}

		var okBuy = close < smaSlow;
		var okSell = close > smaSlow;

		if (macdMain > 0m && hadNegative && adxBuyOk && okBuy && close + indentation > smaFast && close + indentation > smaSlow && Position <= 0m && _lastLowest is decimal longStop)
		{
			var volume = Volume + Math.Abs(Position);
			if (volume > 0m)
			{
				BuyMarket();
				_longEntry = close;
				_longStop = longStop;
				_longStopMoved = false;
				ResetShortState();
			}
		}
		else if (macdMain < 0m && hadPositive && adxSellOk && okSell && close - indentation < smaFast && close - indentation < smaSlow && Position >= 0m && _lastHighest is decimal shortStop)
		{
			var volume = Volume + Math.Abs(Position);
			if (volume > 0m)
			{
				SellMarket();
				_shortEntry = close;
				_shortStop = shortStop;
				_shortStopMoved = false;
				ResetLongState();
			}
		}
	}

	private void ManageOpenPositions(decimal close, decimal smaFast, decimal indentation)
	{
		if (Position > 0m && _longEntry is decimal entry && _longStop is decimal stop)
		{
			var distance = Math.Abs(entry - stop);
			if (distance > 0m)
			{
				var target = entry + distance * HalfCloseRatio;
				if (!_longStopMoved && close > target)
				{
					var half = Position / 2m;
					if (half > 0m)
					{
						SellMarket();
						_longStop = entry;
						_longStopMoved = true;
					}
				}
				else if (_longStopMoved && smaFast > close - indentation)
				{
					SellMarket();
					ResetLongState();
				}
			}
		}
		else if (Position <= 0m)
		{
			ResetLongState();
		}

		if (Position < 0m && _shortEntry is decimal entryShort && _shortStop is decimal stopShort)
		{
			var distance = Math.Abs(entryShort - stopShort);
			if (distance > 0m)
			{
				var target = entryShort - distance * HalfCloseRatio;
				if (!_shortStopMoved && close < target)
				{
					var half = -Position / 2m;
					if (half > 0m)
					{
						BuyMarket();
						_shortStop = entryShort;
						_shortStopMoved = true;
					}
				}
				else if (_shortStopMoved && smaFast < close - indentation)
				{
					BuyMarket();
					ResetShortState();
				}
			}
		}
		else if (Position >= 0m)
		{
			ResetShortState();
		}
	}

	private bool HasNegativeMacd()
	{
		foreach (var value in _macdHistory)
		{
			if (value < 0m)
				return true;
		}

		return false;
	}

	private bool HasPositiveMacd()
	{
		foreach (var value in _macdHistory)
		{
			if (value > 0m)
				return true;
		}

		return false;
	}

	private decimal GetIndentation()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return MinIndentation;

		var decimals = Security?.Decimals ?? 0;
		var factor = (decimals == 3 || decimals == 5) ? 10m : 1m;
		return MinIndentation * step * factor;
	}

	private void ResetLongState()
	{
		_longEntry = null;
		_longStop = null;
		_longStopMoved = false;
	}

	private void ResetShortState()
	{
		_shortEntry = null;
		_shortStop = null;
		_shortStopMoved = false;
	}
}