Ver no GitHub

Estratégia EMA Crossover com Trailing

Visão geral

Esta estratégia é um port do StockSharp do assessor especializado MQL5 "Intersection 2 iMA". Opera em duas médias móveis exponenciais (EMAs) e reage a cruzamentos que ocorrem em velas completamente formadas. O especialista original foi projetado para MetaTrader 5 e gerenciava o volume de negociação dinamicamente; nesta conversão o tamanho da ordem é controlado por um parâmetro configurável enquanto se preserva a lógica de cruzamento e trailing.

Lógica de trading

  1. Geração de sinais
    • Calcular EMAs rápida e lenta na série de velas selecionada.
    • Um cruzamento altista (EMA rápida cruzando acima da EMA lenta) aciona um sinal de compra quando a vela anterior fechou com a EMA rápida abaixo ou igual à EMA lenta e os valores atuais mostram a EMA rápida acima da EMA lenta.
    • Um cruzamento baixista (EMA rápida cruzando abaixo da EMA lenta) espelha a regra acima e produz um sinal de venda.
  2. Execução de ordens
    • Quando um sinal de compra é produzido e não existe posição comprada, a estratégia envia uma ordem de compra a mercado.
    • Quando um sinal de venda é produzido e não existe posição vendida, a estratégia envia uma ordem de venda a mercado.
    • Se houver uma posição oposta, o volume da ordem é aumentado para fechar a posição existente antes de estabelecer a nova, correspondendo ao comportamento do EA fonte que primeiro fechava operações opostas.
  3. Gestão do trailing stop
    • Um trailing stop escalonado mantém uma distância fixa (em passos de preço) do preço mais favorável.
    • O stop só se move quando o preço avançou um passo definido pelo usuário, prevenindo modificações constantes de ordens.
    • Se o preço violar o nível de trailing, a posição é fechada com uma ordem de mercado.

Parâmetros

Nome Descrição Padrão
FastPeriod Comprimento da EMA rápida. 4
SlowPeriod Comprimento da EMA lenta. 18
TrailingStopPoints Distância entre preço de mercado e trailing stop em passos de preço (pontos). Um valor de 0 desabilita o trailing. 20
TrailingStepPoints Progresso mínimo em passos de preço antes de o trailing stop avançar. 5
CandleType Série de dados de velas usada para cálculos (período). Velas de 15 minutos
TradeVolume Tamanho da ordem para entradas a mercado. 1

Notas de implementação

  • A estratégia usa a API de alto nível SubscribeCandles().Bind(...) para conectar dados de velas com indicadores EMA, garantindo que nenhum gerenciamento manual de buffer seja necessário.
  • As distâncias de trailing são calculadas multiplicando o número configurado de pontos pelo PriceStep do instrumento, replicando a lógica de ajuste de dígitos encontrada na versão MQL.
  • Os trailing stops são implementados internamente usando saídas de mercado, porque o StockSharp não expõe o mesmo helper PositionModify usado no MetaTrader. O comportamento permanece equivalente: uma vez que o nível de trailing é violado, a posição é encerrada imediatamente.
  • Os parâmetros são expostos através de StrategyParam<T> para que possam ser otimizados no designer ou ajustados a partir da UI.

Dicas de uso

  • Alinhar o CandleType com o período usado em backtests ou trading ao vivo para manter os valores do indicador consistentes.
  • Ao negociar instrumentos com tamanhos de tick pequenos, ajustar TrailingStopPoints e TrailingStepPoints de acordo; a distância de preço efetiva equivale a pontos × PriceStep.
  • Definir TradeVolume para corresponder ao contrato ou tamanho de lote desejado. A estratégia aumenta automaticamente o valor da ordem para fechar uma posição oposta quando um novo sinal aparece.

Diferenças em relação ao Assessor Especializado original

  • O gerenciamento de capital no MetaTrader usava MoneyFixedMargin; a versão StockSharp expõe um parâmetro de volume de ordem fixo, deixando o dimensionamento avançado de posições para configuração externa.
  • O EA oferecia uma entrada InpCloseHalf não utilizada. Não tinha efeito no código-fonte e foi omitida.
  • O trailing stop é gerenciado internamente em vez de modificar ordens de stop-loss, pois isso simplifica a execução dentro do StockSharp mantendo a lógica de saída idêntica.
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>
/// EMA crossover strategy with trailing stop logic converted from the MQL5 "Intersection 2 iMA" expert advisor.
/// The strategy opens trades on moving average crossovers and maintains a stepped trailing stop.
/// </summary>
public class EmaCrossoverTrailingStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _tradeVolume;

	private ExponentialMovingAverage _fastEma = null!;
	private ExponentialMovingAverage _slowEma = null!;

	private decimal? _previousFastValue;
	private decimal? _previousSlowValue;

	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;

	private decimal _stopDistance;
	private decimal _stepDistance;

	/// <summary>
	/// Initializes <see cref="EmaCrossoverTrailingStrategy"/>.
	/// </summary>
	public EmaCrossoverTrailingStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 4)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Period of the fast exponential moving average", "Moving Averages")
			
			.SetOptimize(2, 20, 1);

		_slowPeriod = Param(nameof(SlowPeriod), 18)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Period of the slow exponential moving average", "Moving Averages")
			
			.SetOptimize(10, 60, 2);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 20m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (points)", "Distance from price to trailing stop expressed in price steps", "Risk Management")
			
			.SetOptimize(5m, 40m, 5m);

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (points)", "Minimum price advancement before the trailing stop is moved", "Risk Management")
			
			.SetOptimize(1m, 10m, 1m);

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

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume used for entries", "General");
	}

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

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

	/// <summary>
	/// Minimum move required before shifting the trailing stop.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

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

	/// <summary>
	/// Volume used for market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

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

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

		Volume = TradeVolume;
		_previousFastValue = null;
		_previousSlowValue = null;
		_longStopPrice = null;
		_shortStopPrice = null;
		_fastEma = null!;
		_slowEma = null!;
		_stopDistance = 0m;
		_stepDistance = 0m;
	}

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

		Volume = TradeVolume;

		_fastEma = new EMA { Length = FastPeriod };
		_slowEma = new EMA { Length = SlowPeriod };

		_stopDistance = CalculateDistance(TrailingStopPoints);
		_stepDistance = CalculateDistance(TrailingStepPoints);

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

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

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

		_stopDistance = CalculateDistance(TrailingStopPoints);
		_stepDistance = CalculateDistance(TrailingStepPoints);

		UpdateTrailingStops(candle);

		if (!_fastEma.IsFormed || !_slowEma.IsFormed)
		{
			_previousFastValue = fastValue;
			_previousSlowValue = slowValue;
			return;
		}

		// indicators bound via .Bind()

		if (_previousFastValue is null || _previousSlowValue is null)
		{
			_previousFastValue = fastValue;
			_previousSlowValue = slowValue;
			return;
		}

		var fastPrev = _previousFastValue.Value;
		var slowPrev = _previousSlowValue.Value;

		var crossedUp = fastPrev <= slowPrev && fastValue > slowValue;
		var crossedDown = fastPrev >= slowPrev && fastValue < slowValue;

		if (crossedUp && Position <= 0)
		{
			var volumeToBuy = TradeVolume;

			if (Position < 0)
				volumeToBuy += Math.Abs(Position);

			if (volumeToBuy > 0)
			{
				BuyMarket();
				InitializeLongTrailing(candle.ClosePrice);
			}
		}
		else if (crossedDown && Position >= 0)
		{
			var volumeToSell = TradeVolume;

			if (Position > 0)
				volumeToSell += Math.Abs(Position);

			if (volumeToSell > 0)
			{
				SellMarket();
				InitializeShortTrailing(candle.ClosePrice);
			}
		}

		_previousFastValue = fastValue;
		_previousSlowValue = slowValue;
	}

	private decimal CalculateDistance(decimal points)
	{
		if (points <= 0m)
			return 0m;

		var priceStep = Security?.PriceStep ?? 0m;

		if (priceStep <= 0m)
			priceStep = 1m;

		return points * priceStep;
	}

	private void InitializeLongTrailing(decimal price)
	{
		if (_stopDistance <= 0m)
		{
			_longStopPrice = null;
			return;
		}

		_longStopPrice = price - _stopDistance;
		_shortStopPrice = null;
	}

	private void InitializeShortTrailing(decimal price)
	{
		if (_stopDistance <= 0m)
		{
			_shortStopPrice = null;
			return;
		}

		_shortStopPrice = price + _stopDistance;
		_longStopPrice = null;
	}

	private void UpdateTrailingStops(ICandleMessage candle)
	{
		if (_stopDistance <= 0m)
		{
			_longStopPrice = null;
			_shortStopPrice = null;
			return;
		}

		if (Position > 0)
		{
			if (_longStopPrice is null)
			{
				InitializeLongTrailing(candle.ClosePrice);
			}
			else
			{
				var newStop = candle.ClosePrice - _stopDistance;

				if (newStop - _longStopPrice.Value >= _stepDistance)
					_longStopPrice = newStop;

				if (candle.LowPrice <= _longStopPrice.Value)
				{
					SellMarket();
					_longStopPrice = null;
				}
			}
		}
		else if (Position < 0)
		{
			if (_shortStopPrice is null)
			{
				InitializeShortTrailing(candle.ClosePrice);
			}
			else
			{
				var newStop = candle.ClosePrice + _stopDistance;

				if (_shortStopPrice.Value - newStop >= _stepDistance)
					_shortStopPrice = newStop;

				if (candle.HighPrice >= _shortStopPrice.Value)
				{
					BuyMarket();
					_shortStopPrice = null;
				}
			}
		}
		else
		{
			_longStopPrice = null;
			_shortStopPrice = null;
		}
	}
}