Ver no GitHub

Estratégia T3 MA de Mudança de Direção

Visão geral

Esta estratégia reproduz o comportamento do consultor especialista original T3MA(barabashkakvn's edition). O Consultor Especialista depende do indicador "T3MA-ALARM" que aplica suavização exponencial duas vezes e gera um sinal quando a linha suavizada muda de direção. O port em StockSharp mantém o mesmo conceito: cria uma média móvel exponencial de suavização dupla (EMA de EMA) e opera sempre que a inclinação dessa curva muda de descendente para ascendente ou vice-versa.

A estratégia opera apenas em velas finalizadas. Os sinais podem ser atrasados por um número configurável de barras para imitar a opção original InpBarNumber (atraso padrão: uma barra). As ordens são colocadas usando execução a mercado para que o portfólio alterne entre exposição comprada e vendida sem acumular múltiplas posições hedgeadas simultâneas.

Regras de trading

  1. Assinar a série de velas configurada e calcular uma EMA dos preços de fechamento. Executar uma segunda EMA sobre a saída da primeira EMA, produzindo a série suavizada usada pelo indicador.
  2. Comparar o valor atual da série suavizada (opcionalmente deslocado para frente por EMA Shift) com o valor anterior. A inclinação é considerada de alta quando a série aumenta e de baixa quando diminui.
  3. Quando a inclinação muda de baixa para alta, enfileirar um sinal de compra. Quando muda de alta para baixa, enfileirar um sinal de venda. Velas neutras inserem um sinal zero na fila para que o contador de atraso permaneça preciso.
  4. Após o número configurado de velas completas (Signal Delay) passar, executar o sinal na fila. Uma compra atrasada fecha qualquer posição vendida aberta e entra comprado com o Trade Volume base. Da mesma forma, uma venda atrasada fecha uma posição comprada e entra vendido.
  5. As ordens protetoras de stop-loss e take-profit são inicializadas via StartProtection. Ambas as distâncias são expressas em passos de preço para que se adaptem automaticamente ao tamanho do tick do instrumento selecionado.

Parâmetros

Nome Descrição
EMA Length Comprimento da EMA usado para ambas as passagens de suavização. Corresponde ao parâmetro MAPeriod na implementação do MetaTrader.
EMA Shift Número de barras pelo qual a EMA suavizada é deslocada antes de comparar as inclinações. Equivalente ao MAShift do indicador.
Signal Delay Número de velas completadas a esperar antes de executar um sinal. Reflete InpBarNumber, portanto um valor de 1 opera o sinal da barra anterior.
Stop Loss (steps) Distância do stop-loss medida em passos de preço. Definir como zero para desativar.
Take Profit (steps) Distância do take-profit medida em passos de preço. Definir como zero para desativar.
Trade Volume Tamanho de ordem base usado para novas entradas. Ao reverter uma posição, a estratégia adiciona o tamanho absoluto da posição atual a esse valor.
Candle Type Tipo de dados de vela usado para cálculos (padrão: período de 5 minutos).

Gestão de risco

  • StartProtection registra automaticamente níveis de stop-loss e take-profit quando a estratégia começa. Ambos os níveis seguem o tamanho do tick do instrumento e permanecem ativos durante toda a vida da estratégia.
  • Os giros de posição são executados usando ordens a mercado. Quando a direção do sinal coincide com a exposição atual, nenhuma operação adicional é emitida, evitando pirâmide involuntária.
  • Registros são emitidos em cada operação para rastrear o motivo e o preço de referência tomado da vela fonte.

Diferenças em relação à versão MQL5

  • O MetaTrader 5 exigia uma conta de hedge e podia acumular múltiplas posições. A versão StockSharp mantém uma única posição líquida e a reverte quando o sinal oposto é acionado.
  • O processamento de sinais é baseado em velas e ocorre uma vez por vela finalizada em vez de em cada tick, o que é mais natural dentro da API de alto nível do StockSharp.
  • O gerenciamento de stop-loss e take-profit é tratado via StartProtection em vez de enviar manualmente preços SL/TP com cada ordem.
  • Comentários em inglês, parâmetros estruturados e assistentes de gráficos foram adicionados para melhor legibilidade no ambiente StockSharp.

Notas de uso

  1. Anexar a estratégia ao instrumento desejado e garantir que o tipo de vela corresponda ao período que foi usado ao otimizar o Consultor Especialista original.
  2. Ajustar EMA Length e os parâmetros de risco para se adequar à volatilidade do instrumento. Atrasos maiores (Signal Delay) desaceleram as respostas e podem filtrar ruído.
  3. Como a estratégia trabalha com passos de preço, verificar que a propriedade PriceStep do instrumento esteja configurada corretamente para que as ordens protetoras sejam colocadas em distâncias significativas.
namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Strategy that trades when a double-smoothed EMA changes its slope direction.
/// </summary>
public class T3MaDirectionChangeStrategy : Strategy
{
	private readonly StrategyParam<int> _maLength;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<int> _signalBarOffset;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _signalCooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _recentSmoothed = new();
	private readonly Queue<SignalInfo> _pendingSignals = new();
	private ExponentialMovingAverage _emaPrice;
	private ExponentialMovingAverage _emaSmooth;
	private int _previousDirection;
	private int _cooldownRemaining;

	public int MaLength { get => _maLength.Value; set => _maLength.Value = value; }
	public int MaShift { get => _maShift.Value; set => _maShift.Value = value; }
	public int SignalBarOffset { get => _signalBarOffset.Value; set => _signalBarOffset.Value = value; }
	public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public decimal TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
	public decimal TradeVolume { get => _tradeVolume.Value; set => _tradeVolume.Value = value; }
	public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public T3MaDirectionChangeStrategy()
	{
		_maLength = Param(nameof(MaLength), 4)
			.SetGreaterThanZero()
			.SetDisplay("EMA Length", "Length of the EMA used for the double smoothing", "Indicator");

		_maShift = Param(nameof(MaShift), 0)
			.SetNotNegative()
			.SetDisplay("EMA Shift", "Shift applied to the smoothed EMA when evaluating slope changes", "Indicator");

		_signalBarOffset = Param(nameof(SignalBarOffset), 1)
			.SetNotNegative()
			.SetDisplay("Signal Delay", "How many completed candles to wait before acting on a signal", "Trading rules");

		_stopLossPoints = Param(nameof(StopLossPoints), 20m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (steps)", "Stop loss distance expressed in price steps", "Risk management");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 125m)
			.SetNotNegative()
			.SetDisplay("Take Profit (steps)", "Take profit distance expressed in price steps", "Risk management");

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Base volume used for entries", "Trading rules");

		_signalCooldownBars = Param(nameof(SignalCooldownBars), 12)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait after entries and exits", "Trading rules");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_emaPrice = null;
		_emaSmooth = null;
		_recentSmoothed.Clear();
		_pendingSignals.Clear();
		_previousDirection = 0;
		_cooldownRemaining = 0;
		Volume = TradeVolume;
	}

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

		Volume = TradeVolume;
		_emaPrice = new EMA { Length = MaLength };
		_emaSmooth = new EMA { Length = MaLength };
		_recentSmoothed.Clear();
		_pendingSignals.Clear();
		_previousDirection = 0;
		_cooldownRemaining = 0;

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

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

		var slUnit = StopLossPoints > 0m ? new Unit(StopLossPoints, UnitTypes.Absolute) : null;
		var tpUnit = TakeProfitPoints > 0m ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null;
		StartProtection(slUnit, tpUnit);
	}

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

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		var emaPriceValue = _emaPrice.Process(new DecimalIndicatorValue(_emaPrice, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var emaSmoothValue = _emaSmooth.Process(emaPriceValue);
		if (!emaSmoothValue.IsFormed)
			return;

		AddSmoothedValue(emaSmoothValue.ToDecimal(), MaShift + 2);
		if (_recentSmoothed.Count < MaShift + 2)
		{
			EnqueueSignal(new SignalInfo(0));
			return;
		}

		var currentIndex = _recentSmoothed.Count - 1 - MaShift;
		var previousIndex = _recentSmoothed.Count - 2 - MaShift;
		var current = _recentSmoothed[currentIndex];
		var previous = _recentSmoothed[previousIndex];
		var direction = _previousDirection;

		if (current > previous)
			direction = 1;
		else if (current < previous)
			direction = -1;

		var signal = 0;
		if (_previousDirection == -1 && direction == 1)
			signal = 1;
		else if (_previousDirection == 1 && direction == -1)
			signal = -1;

		_previousDirection = direction;
		EnqueueSignal(new SignalInfo(signal));
	}

	private void AddSmoothedValue(decimal value, int limit)
	{
		_recentSmoothed.Add(value);
		if (_recentSmoothed.Count > limit)
			_recentSmoothed.RemoveAt(0);
	}

	private void EnqueueSignal(SignalInfo signal)
	{
		_pendingSignals.Enqueue(signal);

		while (_pendingSignals.Count > SignalBarOffset)
		{
			var readySignal = _pendingSignals.Dequeue();
			ExecuteSignal(readySignal);
		}
	}

	private void ExecuteSignal(SignalInfo signal)
	{
		if (signal.Direction == 0 || _cooldownRemaining > 0)
			return;

		if (signal.Direction > 0 && Position <= 0)
		{
			var volume = Volume + Math.Abs(Position);
			BuyMarket(volume);
			_cooldownRemaining = SignalCooldownBars;
		}
		else if (signal.Direction < 0 && Position >= 0)
		{
			var volume = Volume + Math.Abs(Position);
			SellMarket(volume);
			_cooldownRemaining = SignalCooldownBars;
		}
	}

	private readonly record struct SignalInfo(int Direction);
}