Ver no GitHub

Estratégia HarVesteR

A estratégia HarVesteR combina o momentum do MACD com duas médias móveis simples e um filtro opcional de força de tendência ADX. Ela busca situações em que o preço se mantém próximo às médias móveis enquanto o MACD cruzou recentemente a linha zero, sinalizando um potencial rompimento da consolidação. Os stops são colocados em máximos ou mínimos de swing, metade da posição é fechada em um múltiplo fixo de recompensa, e o restante é protegido com uma saída de break-even controlada pela média móvel rápida.

Detalhes

  • Critérios de entrada:
    • Comprado: MACD > 0 && MACD history contains negative value && Close < SlowSMA && Close + Indentation > FastSMA && Close + Indentation > SlowSMA && ADX ≥ AdxBuyLevel (if enabled)
    • Vendido: 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 de swing nos StopLookback candles concluídos.
  • Saída parcial: Fecha metade da posição quando o preço se move HalfCloseRatio vezes a distância entre entrada e stop, depois move o stop para break-even.
  • Saída final:
    • Comprado: fecha o restante se o preço cair abaixo de FastSMA + Indentation após o stop estar em break-even.
    • Vendido: fecha o restante se o preço subir acima de FastSMA + Indentation após o stop estar em break-even.
  • Comprado/Vendido: Ambas as direções suportadas.
  • Filtros: Filtro opcional de força de tendência ADX; defina UseAdxFilter como false para desativá-lo.
  • Gestão de posição: Reverte a posição compensando o volume do sinal oposto mais a exposição atual.

Parâmetros

Nome Padrão Descrição
MacdFast 12 Período EMA rápido para a linha de diferença do MACD.
MacdSlow 24 Período EMA lento para a linha de diferença do MACD.
MacdSignal 9 Período EMA de sinal para suavização do MACD.
MacdLookback 6 Número de candles recentemente concluídos verificados para uma mudança de sinal do MACD.
SmaFastLength 50 Comprimento da média móvel simples rápida.
SmaSlowLength 100 Comprimento da média móvel simples lenta.
MinIndentation 10 Deslocamento em pips aplicado em torno das médias móveis antes de entrar ou sair.
StopLookback 6 Retrocesso de máximo/mínimo de swing usado para inicializar o nível de stop inicial.
UseAdxFilter false Habilita o filtro de força ADX para ambas as direções.
AdxBuyLevel 50 Nível mínimo de ADX necessário para permitir entradas compradas quando o filtro está habilitado.
AdxSellLevel 50 Nível mínimo de ADX necessário para permitir entradas vendidas quando o filtro está habilitado.
AdxPeriod 14 Período usado para o cálculo do ADX.
HalfCloseRatio 2 Multiplicador aplicado à distância entrada-stop antes de realizar lucros parciais.
Volume 1 Volume de ordem para novas entradas (compensando qualquer exposição oposta).
CandleType 1 hour Período principal usado para construir candles e indicadores.

Notas

  • MinIndentation é convertido em distância de preço usando o tamanho do tick do instrumento. Instrumentos cotados com três ou cinco decimais recebem um ajuste de dez vezes para aproximar as unidades de pip.
  • Quando UseAdxFilter está desativado, a estratégia aceita sinais em ambas as direções sem verificar o valor do ADX.
  • A realização de lucros parciais e as saídas de break-even são executadas em cada candle concluído para proteger posições abertas mesmo quando novas negociações não são permitidas.
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;
	}
}