Ver no GitHub

Estratégia True Sort Trend

A estratégia replica o clássico template "True Sort" do MetaTrader aguardando que cinco médias móveis exponenciais se alinhem em ordem estrita. Quando tanto a vela atual quanto a anterior completadas respeitam o mesmo ordenamento de alta ou baixa e o Average Directional Index (ADX) confirma o momentum, a estratégia abre uma posição na direção da tendência. O risco é controlado por distâncias opcionais absolutas de stop-loss e take-profit junto com um trailing stop que só é ativado após o preço se mover o suficiente a favor da operação.

Como funciona

  1. Construir cinco EMAs (de rápida a lenta: padrão 10, 20, 50, 100, 200 períodos) na série de velas selecionada.
  2. Calcular o ADX com um período configurável (padrão 24) para qualificar se a tendência tem força suficiente (limiar padrão 20).
  3. Apenas no momento em que uma vela fecha analisamos os indicadores. Os sinais são ignorados para velas não finalizadas para evitar decisões prematuras.
  4. Um setup comprado requer o seguinte para a vela completada atual e a anterior:
    • EMA_rápida > EMA_2 > EMA_3 > EMA_4 > EMA_lenta (empilhamento de alta perfeito).
    • ADX > limiar para garantir que a inclinação é significativa.
  5. Um setup vendido espelha o acima com todas as desigualdades invertidas.
  6. As posições são fechadas quando o empilhamento ordenado se quebra, quando os níveis de proteção são atingidos, ou quando o trailing stop devolve uma quantidade configurável de lucro.

Esta lógica mantém a estratégia estritamente em mercados com forte tendência e força o alinhamento em duas barras para reduzir o ruído.

Regras de trading

  • Entrada
    • Comprado: ADX maior que o limiar e cinco EMAs ordenadas da mais rápida para a mais lenta para tanto a vela atual quanto a anterior finalizada. Qualquer posição vendida aberta é fechada primeiro, então um novo comprado é aberto com o Volume configurado.
    • Vendido: ADX maior que o limiar e EMAs ordenadas em ordem descendente por duas velas consecutivas. Qualquer posição comprada é achatada antes que a entrada vendida seja enviada.
  • Saída
    • Se o empilhamento de EMA perder sua ordenação estrita, a posição é imediatamente fechada.
    • Saídas de proteção opcionais:
      • Distância de stop-loss em unidades de preço absoluto abaixo (comprado) ou acima (vendido) do preço de entrada.
      • Distância de take-profit em unidades de preço absoluto além do preço de entrada.
      • Trailing stop que se aciona somente após o preço avançar TrailingStopDistance + TrailingStepDistance e então segue o preço em TrailingStopDistance.
    • Fechamentos manuais ou execuções externas também resetarão o estado interno.

Parâmetros

Parâmetro Descrição Padrão
CandleType Tipo de dados das velas usadas para todos os cálculos. Período de 1 hora
FastEmaLength Período da EMA mais rápida (alinhamento de entrada). 10
SecondEmaLength Período da segunda EMA. 20
ThirdEmaLength Período da terceira EMA. 50
FourthEmaLength Período da quarta EMA. 100
SlowEmaLength Período da EMA mais lenta que representa a tendência de longo prazo. 200
AdxPeriod Comprimento de média para o indicador ADX. 24
AdxThreshold Valor mínimo de ADX necessário para permitir trades. 20
StopLossDistance Distância de preço absoluta do stop protetor (0 desabilita). 0.005
TakeProfitDistance Distância de preço absoluta da meta de lucro (0 desabilita). 0.015
TrailingStopDistance Distância entre o preço mais alto/baixo e a saída por trailing. 0.0005
TrailingStepDistance Avanço extra necessário antes que o trailing stop seja ativado ou movido. 0.0001

Todos os valores de distância são expressos em unidades de preço. Para símbolos FX cotados com quatro ou cinco decimais, valores como 0.005 correspondem aproximadamente a 50 pips. Ajuste os números para corresponder ao tamanho do tick do instrumento negociado.

Notas e dicas

  • Funciona melhor em instrumentos com tendência, como os principais pares de FX ou índices em períodos intradiários ou swing. Aumente os comprimentos de EMA para barras diárias ou encurte-os para scalping.
  • A confirmação de duas velas reduz drasticamente os whipsaws, mas pode causar entradas tardias. Considere otimizar o limiar de ADX e os comprimentos de EMA para seu mercado.
  • Os trailing stops permanecem inativos até que o preço se mova TrailingStopDistance + TrailingStepDistance a partir da entrada. Definir o passo como zero imita o comportamento do MetaTrader onde o trailing começa assim que o preço percorre a distância base.
  • A estratégia depende de ordens a mercado (BuyMarket, SellMarket). Configure a propriedade Volume da instância de estratégia para controlar o dimensionamento de posição ou integrar com o gerenciamento de dinheiro do portfólio se necessário.
  • Combine com filtros de sessão ou confirmação de período de tempo superior se precisar limitar os horários de trading.
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-following strategy that requires five exponential moving averages to be sorted in the same order for two consecutive candles.
/// Uses ADX to confirm trend strength and supports optional stop-loss, take-profit, and trailing stop distances expressed in price units.
/// </summary>
public class TrueSortTrendStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastEmaLength;
	private readonly StrategyParam<int> _secondEmaLength;
	private readonly StrategyParam<int> _thirdEmaLength;
	private readonly StrategyParam<int> _fourthEmaLength;
	private readonly StrategyParam<int> _slowEmaLength;
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<decimal> _adxThreshold;
	private readonly StrategyParam<decimal> _stopLossDistance;
	private readonly StrategyParam<decimal> _takeProfitDistance;
	private readonly StrategyParam<decimal> _trailingStopDistance;
	private readonly StrategyParam<decimal> _trailingStepDistance;

	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _secondEma;
	private ExponentialMovingAverage _thirdEma;
	private ExponentialMovingAverage _fourthEma;
	private ExponentialMovingAverage _slowEma;
	private AverageDirectionalIndex _adx;

	private decimal? _prevFast;
	private decimal? _prevSecond;
	private decimal? _prevThird;
	private decimal? _prevFourth;
	private decimal? _prevSlow;

	private decimal _entryPrice;
	private decimal _highestPrice;
	private decimal _lowestPrice;
	private int _positionDirection;

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

	/// <summary>
	/// Length of the fastest EMA.
	/// </summary>
	public int FastEmaLength
	{
		get => _fastEmaLength.Value;
		set => _fastEmaLength.Value = value;
	}

	/// <summary>
	/// Length of the second EMA.
	/// </summary>
	public int SecondEmaLength
	{
		get => _secondEmaLength.Value;
		set => _secondEmaLength.Value = value;
	}

	/// <summary>
	/// Length of the third EMA.
	/// </summary>
	public int ThirdEmaLength
	{
		get => _thirdEmaLength.Value;
		set => _thirdEmaLength.Value = value;
	}

	/// <summary>
	/// Length of the fourth EMA.
	/// </summary>
	public int FourthEmaLength
	{
		get => _fourthEmaLength.Value;
		set => _fourthEmaLength.Value = value;
	}

	/// <summary>
	/// Length of the slowest EMA.
	/// </summary>
	public int SlowEmaLength
	{
		get => _slowEmaLength.Value;
		set => _slowEmaLength.Value = value;
	}

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

	/// <summary>
	/// Minimum ADX value that confirms a strong trend.
	/// </summary>
	public decimal AdxThreshold
	{
		get => _adxThreshold.Value;
		set => _adxThreshold.Value = value;
	}

	/// <summary>
	/// Absolute stop-loss distance expressed in price units.
	/// </summary>
	public decimal StopLossDistance
	{
		get => _stopLossDistance.Value;
		set => _stopLossDistance.Value = value;
	}

	/// <summary>
	/// Absolute take-profit distance expressed in price units.
	/// </summary>
	public decimal TakeProfitDistance
	{
		get => _takeProfitDistance.Value;
		set => _takeProfitDistance.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price units.
	/// </summary>
	public decimal TrailingStopDistance
	{
		get => _trailingStopDistance.Value;
		set => _trailingStopDistance.Value = value;
	}

	/// <summary>
	/// Minimum price advance required before the trailing stop is activated or moved.
	/// </summary>
	public decimal TrailingStepDistance
	{
		get => _trailingStepDistance.Value;
		set => _trailingStepDistance.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="TrueSortTrendStrategy"/> class.
	/// </summary>
	public TrueSortTrendStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used for calculations", "General");

		_fastEmaLength = Param(nameof(FastEmaLength), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA Length", "Period of the fastest EMA", "Moving Averages")
			
			.SetOptimize(5, 20, 1);

		_secondEmaLength = Param(nameof(SecondEmaLength), 20)
			.SetGreaterThanZero()
			.SetDisplay("Second EMA Length", "Period of the second EMA", "Moving Averages")
			
			.SetOptimize(10, 40, 2);

		_thirdEmaLength = Param(nameof(ThirdEmaLength), 30)
			.SetGreaterThanZero()
			.SetDisplay("Third EMA Length", "Period of the third EMA", "Moving Averages")
			
			.SetOptimize(30, 80, 5);

		_fourthEmaLength = Param(nameof(FourthEmaLength), 40)
			.SetGreaterThanZero()
			.SetDisplay("Fourth EMA Length", "Period of the fourth EMA", "Moving Averages")
			
			.SetOptimize(60, 140, 5);

		_slowEmaLength = Param(nameof(SlowEmaLength), 50)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA Length", "Period of the slowest EMA", "Moving Averages")
			
			.SetOptimize(150, 250, 5);

		_adxPeriod = Param(nameof(AdxPeriod), 24)
			.SetGreaterThanZero()
			.SetDisplay("ADX Period", "Averaging period for the ADX indicator", "Indicators")
			
			.SetOptimize(14, 40, 2);

		_adxThreshold = Param(nameof(AdxThreshold), 10m)
			.SetGreaterThanZero()
			.SetDisplay("ADX Threshold", "Minimum ADX value to confirm trend strength", "Indicators")
			
			.SetOptimize(15m, 35m, 5m);

		_stopLossDistance = Param(nameof(StopLossDistance), 500m)
			.SetRange(0m, 1m)
			.SetDisplay("Stop Loss Distance", "Absolute distance for stop loss", "Risk")
			
			.SetOptimize(0.001m, 0.01m, 0.001m);

		_takeProfitDistance = Param(nameof(TakeProfitDistance), 1500m)
			.SetRange(0m, 1m)
			.SetDisplay("Take Profit Distance", "Absolute distance for take profit", "Risk")
			
			.SetOptimize(0.002m, 0.03m, 0.002m);

		_trailingStopDistance = Param(nameof(TrailingStopDistance), 300m)
			.SetRange(0m, 1m)
			.SetDisplay("Trailing Stop Distance", "Trailing stop distance in price units", "Risk")
			
			.SetOptimize(0.0002m, 0.002m, 0.0002m);

		_trailingStepDistance = Param(nameof(TrailingStepDistance), 100m)
			.SetRange(0m, 1m)
			.SetDisplay("Trailing Step Distance", "Additional advance before trailing stop adjustment", "Risk")
			
			.SetOptimize(0m, 0.001m, 0.0001m);
	}

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

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

		_prevFast = null;
		_prevSecond = null;
		_prevThird = null;
		_prevFourth = null;
		_prevSlow = null;

		ResetTradeState();
	}

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

		_fastEma = new ExponentialMovingAverage { Length = FastEmaLength };
		_secondEma = new ExponentialMovingAverage { Length = SecondEmaLength };
		_thirdEma = new ExponentialMovingAverage { Length = ThirdEmaLength };
		_fourthEma = new ExponentialMovingAverage { Length = FourthEmaLength };
		_slowEma = new ExponentialMovingAverage { Length = SlowEmaLength };
		_adx = new AverageDirectionalIndex { Length = AdxPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandleRaw)
			.Start();

		var priceArea = CreateChartArea();
		if (priceArea != null)
		{
			DrawCandles(priceArea, subscription);
			DrawOwnTrades(priceArea);
		}
	}

	private void ProcessCandleRaw(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var fastResult = _fastEma.Process(candle);
		var secondResult = _secondEma.Process(candle);
		var thirdResult = _thirdEma.Process(candle);
		var fourthResult = _fourthEma.Process(candle);
		var slowResult = _slowEma.Process(candle);
		var adxResult = _adx.Process(candle);

		if (!_slowEma.IsFormed || !_adx.IsFormed)
			return;

		if (fastResult.IsEmpty || secondResult.IsEmpty || thirdResult.IsEmpty || fourthResult.IsEmpty || slowResult.IsEmpty || adxResult.IsEmpty)
			return;

		var fast = fastResult.ToDecimal();
		var second = secondResult.ToDecimal();
		var third = thirdResult.ToDecimal();
		var fourth = fourthResult.ToDecimal();
		var slow = slowResult.ToDecimal();

		var adxData = (AverageDirectionalIndexValue)adxResult;
		var adxValue = adxData.MovingAverage ?? 0m;

		if (_prevFast is null || _prevSecond is null || _prevThird is null || _prevFourth is null || _prevSlow is null)
		{
			UpdatePreviousValues(fast, second, third, fourth, slow);
			return;
		}

		var ascendingCurrent = fast > second && second > third && third > fourth && fourth > slow;
		var descendingCurrent = fast < second && second < third && third < fourth && fourth < slow;
		var ascendingPrevious = _prevFast > _prevSecond && _prevSecond > _prevThird && _prevThird > _prevFourth && _prevFourth > _prevSlow;
		var descendingPrevious = _prevFast < _prevSecond && _prevSecond < _prevThird && _prevThird < _prevFourth && _prevFourth < _prevSlow;

		var openLongSignal = adxValue > AdxThreshold && ascendingCurrent && ascendingPrevious;
		var openShortSignal = adxValue > AdxThreshold && descendingCurrent && descendingPrevious;
		var breakLongStack = !ascendingCurrent;
		var breakShortStack = !descendingCurrent;

		var position = Position;

		if (position == 0 && _positionDirection != 0)
			ResetTradeState();

		if (position > 0)
		{
			_highestPrice = _highestPrice == 0m ? candle.HighPrice : Math.Max(_highestPrice, candle.HighPrice);
			_lowestPrice = candle.LowPrice;

			var exitVolume = Math.Abs(position);
			var shouldExit = false;

			if (!shouldExit && StopLossDistance > 0m && candle.LowPrice <= _entryPrice - StopLossDistance)
				shouldExit = true;

			if (!shouldExit && TakeProfitDistance > 0m && candle.HighPrice >= _entryPrice + TakeProfitDistance)
				shouldExit = true;

			if (!shouldExit && TrailingStopDistance > 0m)
			{
				var activation = TrailingStopDistance + Math.Max(0m, TrailingStepDistance);
				if (_highestPrice - _entryPrice >= activation)
				{
					var trailingLevel = _highestPrice - TrailingStopDistance;
					if (candle.ClosePrice <= trailingLevel)
						shouldExit = true;
				}
			}

			if (!shouldExit && breakLongStack)
				shouldExit = true;

			if (shouldExit && exitVolume > 0)
			{
				SellMarket(exitVolume);
				ResetTradeState();
				position = 0;
			}
		}
		else if (position < 0)
		{
			_lowestPrice = _lowestPrice == 0m ? candle.LowPrice : Math.Min(_lowestPrice, candle.LowPrice);
			_highestPrice = candle.HighPrice;

			var exitVolume = Math.Abs(position);
			var shouldExit = false;

			if (!shouldExit && StopLossDistance > 0m && candle.HighPrice >= _entryPrice + StopLossDistance)
				shouldExit = true;

			if (!shouldExit && TakeProfitDistance > 0m && candle.LowPrice <= _entryPrice - TakeProfitDistance)
				shouldExit = true;

			if (!shouldExit && TrailingStopDistance > 0m)
			{
				var activation = TrailingStopDistance + Math.Max(0m, TrailingStepDistance);
				if (_entryPrice - _lowestPrice >= activation)
				{
					var trailingLevel = _lowestPrice + TrailingStopDistance;
					if (candle.ClosePrice >= trailingLevel)
						shouldExit = true;
				}
			}

			if (!shouldExit && breakShortStack)
				shouldExit = true;

			if (shouldExit && exitVolume > 0)
			{
				BuyMarket(exitVolume);
				ResetTradeState();
				position = 0;
			}
		}

		if (position <= 0 && openLongSignal)
		{
			var volumeToBuy = Volume + Math.Max(0m, -position);
			if (volumeToBuy > 0)
			{
				BuyMarket(volumeToBuy);
				_entryPrice = candle.ClosePrice;
				_highestPrice = candle.HighPrice;
				_lowestPrice = candle.LowPrice;
				_positionDirection = 1;
			}
		}
		else if (position >= 0 && openShortSignal)
		{
			var volumeToSell = Volume + Math.Max(0m, position);
			if (volumeToSell > 0)
			{
				SellMarket(volumeToSell);
				_entryPrice = candle.ClosePrice;
				_highestPrice = candle.HighPrice;
				_lowestPrice = candle.LowPrice;
				_positionDirection = -1;
			}
		}

		if (Position == 0 && _positionDirection != 0)
			ResetTradeState();

		UpdatePreviousValues(fast, second, third, fourth, slow);
	}

	private void UpdatePreviousValues(decimal fast, decimal second, decimal third, decimal fourth, decimal slow)
	{
		_prevFast = fast;
		_prevSecond = second;
		_prevThird = third;
		_prevFourth = fourth;
		_prevSlow = slow;
	}

	private void ResetTradeState()
	{
		_entryPrice = 0m;
		_highestPrice = 0m;
		_lowestPrice = 0m;
		_positionDirection = 0;
	}
}