Ver no GitHub

Estratégia do Método de Túnel EMA

Visão geral

A Estratégia do Método de Túnel EMA replica o consultor especialista original do MetaTrader "Método de Túnel" no StockSharp API de alto nível. Ele opera com velas horárias e compara três médias móveis exponenciais (EMAs) baseadas nos preços de fechamento:

  • EMA rápida (12 períodos) captura mudanças imediatas de impulso.
  • Médio EMA (144 períodos) reflete o centro do "túnel" usado para validar sinais curtos.
  • EMA lenta (169 períodos) fornece o filtro direcional de longo prazo para negociações longas.

A estratégia mantém posições mutuamente exclusivas (longas, curtas ou planas) e gerencia dinamicamente o risco por meio de controles explícitos de stop-loss, take-profit e trailing-stop.

Lógica de Sinais

Entradas longas

  1. Aguarde a conclusão de uma vela (sem decisões intrabarras).
  2. Detecte um cruzamento de alta onde o EMA rápida (12) se move de baixo para cima do EMA lenta (169).
  3. Confirme se nenhuma posição está aberta no momento e envie uma ordem de compra a mercado para o volume configurado.

Entradas curtas

  1. Espere por uma vela completa.
  2. Detecte um cruzamento de baixa onde o EMA rápida (12) se move de cima para baixo do meio EMA (144).
  3. Confirme que nenhuma posição está aberta no momento e envie uma ordem de venda a mercado.

Gerenciamento de posição

  • Stop-Loss: Fecha a negociação quando o preço se move contra a posição em StopLossPoints (convertido em preço absoluto usando a etapa de preço do título).
  • Take-Profit: bloqueia os ganhos quando o preço avança TakeProfitPoints em relação ao preço de entrada.
  • Trailing Stop: Depois que a negociação acumula pelo menos TrailingTriggerPoints de lucro, a estratégia segue o preço usando TrailingStopPoints. Para negociações longas, segue a máxima mais alta desde a entrada; para negociações curtas, segue a mínima mais baixa desde a entrada. Uma reversão para o nível final fecha a posição.
  • Redefinição de estado: Após cada saída (manual ou protetora), o estado final interno é redefinido para evitar interferência nas negociações subsequentes.

Parâmetros padrão

Parâmetro Padrão Descrição
CandleType TimeSpan.FromHours(1).TimeFrame() Velas horárias usadas para cálculos de EMA.
FastLength 12 Duração do EMA rápido que reage à ação recente do preço.
MediumLength 144 Comprimento do centro do túnel EMA para validação curta.
SlowLength 169 Comprimento do limite do túnel EMA para validação longa.
StopLossPoints 25 Distância de parada protetora nos pontos do instrumento.
TakeProfitPoints 230 Distância alvo de lucro em pontos de instrumento.
TrailingStopPoints 35 Distância mantida pelo trailing stop quando ativo.
TrailingTriggerPoints 20 Limite de lucro necessário antes do início do rastreamento.

Filtros e características

  • Categoria: Crossover que segue tendências.
  • Instrumentos: Funciona em qualquer instrumento que forneça velas horárias e uma etapa de preço confiável.
  • Direção: Negociações longas e curtas, nunca mantendo posições simultâneas.
  • Período: velas de 1 hora por padrão (configuráveis por meio de CandleType).
  • Controles de risco: Hard stop-loss, take-profit e trailing stop implementados dentro da lógica da estratégia.
  • Requisitos de dados: Depende exclusivamente dos preços de fechamento das velas; não são necessários indicadores adicionais ou profundidade de mercado.

Notas

  • Todos os valores dos indicadores são provenientes de implementações EMA do StockSharp para garantir consistência com diretrizes API de alto nível.
  • A estratégia ignora velas inacabadas para evitar sinais de contagem dupla ou agir com base em dados parciais.
  • Os ajustes de trailing stop respeitam o PriceStep do título via ShrinkPrice, mantendo os níveis de saída alinhados com incrementos de tick válidos.
  • Os parâmetros padrão refletem as configurações originais do MQL, mas podem ser otimizados por meio das ferramentas de otimização de parâmetros do StockSharp.
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>
/// Tunnel method strategy that trades EMA crossovers on hourly candles.
/// Long trades are opened when the fast EMA crosses above the slow EMA.
/// Short trades are opened when the fast EMA crosses below the medium EMA.
/// Includes fixed stop-loss, take-profit, and a trailing stop once profit reaches a trigger.
/// </summary>
public class TunnelMethodEmaStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _mediumLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingTriggerPoints;

	private bool _hasPreviousValues;
	private decimal _previousFast;
	private decimal _previousMedium;
	private decimal _previousSlow;

	private decimal _pointValue;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;
	private decimal _trailingStopDistance;
	private decimal _trailingTriggerDistance;

	private decimal? _entryPrice;
	private decimal _highestSinceEntry;
	private decimal _lowestSinceEntry;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;

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

		_fastLength = Param(nameof(FastLength), 12)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA Length", "Period of the fast EMA", "Indicators")
			
			.SetOptimize(6, 30, 2);

		_mediumLength = Param(nameof(MediumLength), 144)
			.SetGreaterThanZero()
			.SetDisplay("Medium EMA Length", "Period of the medium EMA", "Indicators")
			
			.SetOptimize(72, 200, 8);

		_slowLength = Param(nameof(SlowLength), 169)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA Length", "Period of the slow EMA", "Indicators")
			
			.SetOptimize(120, 220, 5);

		_stopLossPoints = Param(nameof(StopLossPoints), 25m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (points)", "Protective stop distance expressed in price points", "Risk")
			
			.SetOptimize(10m, 60m, 5m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 230m)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Profit target distance expressed in price points", "Risk")
			
			.SetOptimize(100m, 400m, 20m);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 35m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (points)", "Distance maintained by the trailing stop", "Risk")
			
			.SetOptimize(10m, 80m, 5m);

		_trailingTriggerPoints = Param(nameof(TrailingTriggerPoints), 20m)
			.SetNotNegative()
			.SetDisplay("Trailing Trigger (points)", "Profit required before the trailing stop activates", "Risk")
			
			.SetOptimize(5m, 60m, 5m);
	}

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

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

	/// <summary>
	/// Medium EMA period length.
	/// </summary>
	public int MediumLength
	{
		get => _mediumLength.Value;
		set => _mediumLength.Value = value;
	}

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

	/// <summary>
	/// Stop-loss distance in price points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price points.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Trailing activation threshold in price points.
	/// </summary>
	public decimal TrailingTriggerPoints
	{
		get => _trailingTriggerPoints.Value;
		set => _trailingTriggerPoints.Value = value;
	}

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

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

		_hasPreviousValues = false;
		_previousFast = 0m;
		_previousMedium = 0m;
		_previousSlow = 0m;
		_pointValue = 0m;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
		_trailingStopDistance = 0m;
		_trailingTriggerDistance = 0m;

		_entryPrice = null;
		_highestSinceEntry = 0m;
		_lowestSinceEntry = 0m;
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

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

		_pointValue = GetPointValue();
		_stopLossDistance = StopLossPoints * _pointValue;
		_takeProfitDistance = TakeProfitPoints * _pointValue;
		_trailingStopDistance = TrailingStopPoints * _pointValue;
		_trailingTriggerDistance = TrailingTriggerPoints * _pointValue;

		var slowEma = new ExponentialMovingAverage { Length = SlowLength };
		var mediumEma = new ExponentialMovingAverage { Length = MediumLength };
		var fastEma = new ExponentialMovingAverage { Length = FastLength };

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(slowEma, mediumEma, fastEma, ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal slowValue, decimal mediumValue, decimal fastValue)
	{
		if (candle.State != CandleStates.Finished)
			// Ignore unfinished candles to work on closed data.
			return;

		if (!_hasPreviousValues)
		{
			_previousSlow = slowValue;
			_previousMedium = mediumValue;
			_previousFast = fastValue;
			_hasPreviousValues = true;
			return;
		}

		UpdateRiskDistances();
		// Refresh risk distances if the price step changes during runtime.

		if (Position == 0)
		{
			ResetPositionState();
			// Clear trailing state while flat to prepare for the next trade.
		}
		else if (Position > 0)
		{
			ManageLongPosition(candle);
		}
		else
		{
			ManageShortPosition(candle);
		}

		if (Position == 0)
		{
			var shouldOpenLong = _previousFast < _previousSlow && fastValue > slowValue;
			var shouldOpenShort = _previousFast > _previousMedium && fastValue < mediumValue;

			if (shouldOpenLong && Position <= 0)
			{
				var volume = Volume + Math.Abs(Position);
				if (volume > 0)
				{
					_entryPrice = candle.ClosePrice;
					_highestSinceEntry = candle.HighPrice;
					_longTrailingStop = null;
					// Enter long with current volume when the fast EMA crosses above the slow EMA.
					BuyMarket();
				}
			}
			else if (shouldOpenShort && Position >= 0)
			{
				var volume = Volume + Math.Abs(Position);
				if (volume > 0)
				{
					_entryPrice = candle.ClosePrice;
					_lowestSinceEntry = candle.LowPrice;
					_shortTrailingStop = null;
					// Enter short with current volume when the fast EMA crosses below the medium EMA.
					SellMarket();
				}
			}
		}

		_previousSlow = slowValue;
		_previousMedium = mediumValue;
		_previousFast = fastValue;
	}

	private void ManageLongPosition(ICandleMessage candle)
	{
		if (_entryPrice is null)
			_entryPrice = candle.ClosePrice;

		_highestSinceEntry = Math.Max(_highestSinceEntry, candle.HighPrice);
		// Track the highest price reached since the long entry.

		if (_takeProfitDistance > 0m && candle.HighPrice >= _entryPrice.Value + _takeProfitDistance)
		{
			SellMarket();
			ResetPositionState();
			return;
		}

		if (_stopLossDistance > 0m && candle.LowPrice <= _entryPrice.Value - _stopLossDistance)
		{
			SellMarket();
			ResetPositionState();
			return;
		}

		if (_trailingStopDistance <= 0m || _trailingTriggerDistance <= 0m)
			return;

		if (_highestSinceEntry - _entryPrice.Value < _trailingTriggerDistance)
			return;

		var candidate = _highestSinceEntry - _trailingStopDistance;
		// Align the trailing stop with the instrument price step.
		candidate = ShrinkPrice(candidate);

		if (!_longTrailingStop.HasValue || candidate > _longTrailingStop.Value)
			_longTrailingStop = candidate;

		if (_longTrailingStop.HasValue && candle.LowPrice <= _longTrailingStop.Value)
			// Close the long position once price falls to the trailing stop.
		{
			SellMarket();
			ResetPositionState();
		}
	}

	private void ManageShortPosition(ICandleMessage candle)
	{
		if (_entryPrice is null)
			_entryPrice = candle.ClosePrice;

		_lowestSinceEntry = _lowestSinceEntry == 0m ? candle.LowPrice : Math.Min(_lowestSinceEntry, candle.LowPrice);
		// Track the lowest price reached since the short entry.

		if (_takeProfitDistance > 0m && candle.LowPrice <= _entryPrice.Value - _takeProfitDistance)
		{
			BuyMarket();
			ResetPositionState();
			return;
		}

		if (_stopLossDistance > 0m && candle.HighPrice >= _entryPrice.Value + _stopLossDistance)
		{
			BuyMarket();
			ResetPositionState();
			return;
		}

		if (_trailingStopDistance <= 0m || _trailingTriggerDistance <= 0m)
			return;

		if (_entryPrice.Value - _lowestSinceEntry < _trailingTriggerDistance)
			return;

		var candidate = _lowestSinceEntry + _trailingStopDistance;
		// Align the trailing stop with the instrument price step.
		candidate = ShrinkPrice(candidate);

		if (!_shortTrailingStop.HasValue || candidate < _shortTrailingStop.Value)
			_shortTrailingStop = candidate;

		if (_shortTrailingStop.HasValue && candle.HighPrice >= _shortTrailingStop.Value)
			// Close the short position once price rises to the trailing stop.
		{
			BuyMarket();
			ResetPositionState();
		}
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_highestSinceEntry = 0m;
		_lowestSinceEntry = 0m;
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	private void UpdateRiskDistances()
	{
		var newPointValue = GetPointValue();
		if (newPointValue <= 0m)
			return;

		if (_pointValue != newPointValue)
		{
			_pointValue = newPointValue;
			_stopLossDistance = StopLossPoints * _pointValue;
			_takeProfitDistance = TakeProfitPoints * _pointValue;
			_trailingStopDistance = TrailingStopPoints * _pointValue;
			_trailingTriggerDistance = TrailingTriggerPoints * _pointValue;
		}
	}

	private decimal GetPointValue()
	{
		var step = Security?.PriceStep;
		if (step is > 0m)
			return step.Value;

		return 1m;
	}

	private decimal ShrinkPrice(decimal price)
	{
		if (_pointValue > 0m)
			return Math.Round(price / _pointValue) * _pointValue;
		return price;
	}
}