Ver no GitHub

EMA Estratégia Cruzada 2

Visão geral

Esta estratégia é uma porta StockSharp do MetaTrader 4 consultor especialista "EMA_CROSS_2" do repositório MQL. O EA original monitora duas médias móveis exponenciais (EMAs) e coloca ordens de mercado sempre que as médias trocam de ordem. A conversão mantém a natureza contrária do script - ela compra quando o longo EMA se move acima do curto EMA e vende quando o curto EMA sobe acima do longo EMA - enquanto envolve a lógica na infraestrutura estratégica de alto nível StockSharp.

A estratégia opera em velas completas fornecidas pelo tipo de dados de vela configurável. Os sinais são avaliados no fechamento da vela para evitar disparos repetidos dentro da mesma barra. O gerenciamento de risco imita o comportamento MetaTrader usando distâncias de take-profit, stop-loss e trailing stop expressas em pontos de corretor (etapas de preço).

Lógica de negociação

  1. Cálculo do indicador
    • Calcule os EMAs de curto e longo período em cada vela concluída.
    • Ignore a primeira atualização do indicador, correspondendo ao sinalizador first_time original que ignorou a primeira avaliação.
    • Depois, detecte uma mudança de direção quando a ordem relativa entre o EMA longo e curto mudar.
  2. Interpretação de sinal
    • Quando o EMA longo se move acima do EMA curto, o EA original abriu uma negociação de compra. A porta StockSharp mantém esta regra contrária, embora se comporte de forma oposta a um sistema de crossover clássico.
    • Quando a posição curta EMA fecha acima da posição longa EMA, a estratégia abre uma negociação de venda.
    • Novas posições só são permitidas quando nenhuma exposição estiver aberta no momento, replicando a condição OrdersTotal() < 1.
  3. Execução de pedido
    • As negociações são enviadas como ordens de mercado com volume fixo configurável.
    • Na entrada, a estratégia registra os preços de stop-loss e take-profit usando a distância do pip fornecida através dos parâmetros.
  4. Gerenciamento de riscos
    • Em cada vela finalizada, a estratégia verifica se a ação do preço tocou os níveis armazenados de stop-loss ou take-profit. A violação de qualquer um dos níveis fecha toda a posição com uma ordem de mercado.
    • Um trailing stop (também definido nos pontos do corretor) é aplicado quando o preço se move favoravelmente mais do que a distância final. Para posições longas, o batente de proteção é deslocado para cima; para posições curtas, o preço segue para baixo.
    • Quando a posição se torna plana, os níveis de proteção armazenados são apagados.

Parâmetros

Nome Descrição Padrão
CandleType Série de velas usada para cálculos de indicadores e detecção de sinal. Período de 15 minutos
OrderVolume Volume de cada ordem de mercado em lotes/contratos. 2
TakeProfitPoints Distância até o nível de lucro expresso em pontos de corretor (etapas de preço). Um valor de 0 desativa o take-profit. 20
StopLossPoints Distância até o nível de stop loss expressa em pontos da corretora. Um valor de 0 desativa o stop loss. 30
TrailingStopPoints Distância usada ao rastrear a posição aberta. 0 desativa o trailing stop. 50
ShortEmaPeriod Comprimento do EMA rápida. 5
LongEmaPeriod Comprimento da lentidão EMA. 60

Notas de implementação

  • A estratégia usa SubscribeCandles().Bind(shortEma, longEma, ProcessCandle) para conectar dados de velas com indicadores EMA, seguindo o padrão preferido de alto nível API.
  • Os valores dos indicadores são recebidos como decimais prontos para uso no retorno de chamada de ligação, portanto, nenhuma indexação manual do buffer é necessária.
  • As distâncias de proteção são convertidas de MetaTrader pontos em StockSharp preços multiplicando pelo instrumento PriceStep. Se o instrumento usar preços de pip fracionários (3 ou 5 casas decimais), o auxiliar calculará o tamanho do pip de acordo.
  • O comportamento de stop-loss, take-profit e trailing são implementados internamente com saídas de mercado porque StockSharp não expõe o mesmo fluxo de trabalho OrderModify que MetaTrader 4. O gerenciamento comercial resultante reflete a lógica original: os níveis são verificados em cada vela e as saídas ocorrem imediatamente após a violação.
  • A primeira avaliação cruzada é intencionalmente ignorada para reproduzir a proteção first_time que evitou negociações prematuras no script MQL.

Diferenças da versão MetaTrader

  • Gestão de dinheiro: o EA original sempre negociou o parâmetro Lots. A conversão expõe o mesmo conceito por meio de OrderVolume e também o atribui à propriedade de estratégia Volume para que designers e otimizadores possam reutilizá-lo.
  • Colocação de pedidos: MetaTrader aplicou stop-loss e take-profit diretamente em OrderSend. Em StockSharp esses níveis são rastreados pela estratégia e fechados com ordens de mercado quando violados.
  • Precisão do trailing stop: as paradas EA movidas usando dados de tick (Bid/Ask). A porta atualiza a lógica final no fechamento da vela, que é a granularidade mais fina disponível neste projeto de amostra. As regras de distância e ativação permanecem idênticas.
  • O tratamento de erros e o registro em log foram simplificados; O registro StockSharp fornece informações detalhadas por meio do registro de estratégia padrão.

Dicas de uso

  • Alinhe CandleType com o período usado durante os backtests do EA original para manter um comportamento comparável do indicador.
  • Ao negociar símbolos cotados com pips fracionários, certifique-se de que as distâncias dos pontos configuradas reflitam o número desejado de pips (por exemplo, em EURUSD 10 pontos são iguais a 1 pip).
  • Defina OrderVolume com o tamanho do contrato esperado pelo seu local de execução. A estratégia não realiza escalonamento automático de volume.
  • Use os alternadores de otimização integrados em cada parâmetro para explorar combinações de EMA períodos e distâncias de risco, assim como você otimizaria entradas em MetaTrader.

Arquivos

  • CS/EmaCross2Strategy.cs – StockSharp implementação da lógica de negociação.
  • README.md – Documentação em inglês (este arquivo).
  • README_zh.md – tradução chinesa.
  • README_ru.md – Tradução russa.
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>
/// Counter-trend EMA crossover strategy converted from the MetaTrader 4 expert "EMA_CROSS_2".
/// Buys when the long EMA rises above the short EMA, and sells when the short EMA climbs above the long EMA.
/// Incorporates MetaTrader-style risk management with point-based stop-loss, take-profit, and trailing stop levels.
/// </summary>
public class EmaCross2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<int> _shortEmaPeriod;
	private readonly StrategyParam<int> _longEmaPeriod;

	private ExponentialMovingAverage _shortEma;
	private ExponentialMovingAverage _longEma;

	private bool _skipFirstSignal = true;
	private int _lastDirection;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal _pointSize;
	private decimal _entryPrice;

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

	/// <summary>
	/// Order volume applied to new market orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

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

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

	/// <summary>
	/// Period of the short EMA.
	/// </summary>
	public int ShortEmaPeriod
	{
		get => _shortEmaPeriod.Value;
		set => _shortEmaPeriod.Value = value;
	}

	/// <summary>
	/// Period of the long EMA.
	/// </summary>
	public int LongEmaPeriod
	{
		get => _longEmaPeriod.Value;
		set => _longEmaPeriod.Value = value;
	}

	/// <summary>
	/// Initialize strategy parameters.
	/// </summary>
	public EmaCross2Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for EMA calculations", "General");

		_orderVolume = Param(nameof(OrderVolume), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume of each market order", "Trading")
		
		.SetOptimize(0.1m, 5m, 0.1m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Take Profit (points)", "Distance from entry to take-profit in broker points", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (points)", "Distance from entry to stop-loss in broker points", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop (points)", "Trailing distance maintained after entry", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_shortEmaPeriod = Param(nameof(ShortEmaPeriod), 5)
		.SetGreaterThanZero()
		.SetDisplay("Short EMA", "Length of the fast EMA", "Indicators")
		
		.SetOptimize(2, 40, 1);

		_longEmaPeriod = Param(nameof(LongEmaPeriod), 60)
		.SetGreaterThanZero()
		.SetDisplay("Long EMA", "Length of the slow EMA", "Indicators")
		
		.SetOptimize(10, 200, 5);
	}

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

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

		Volume = OrderVolume;
		_shortEma = null;
		_longEma = null;
		_skipFirstSignal = true;
		_lastDirection = 0;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_pointSize = 0m;
		_entryPrice = 0m;
	}

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

		Volume = OrderVolume;
		_pointSize = CalculatePointSize();

		_shortEma = new EMA { Length = ShortEmaPeriod };
		_longEma = new EMA { Length = LongEmaPeriod };

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

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

	private void ProcessCandle(ICandleMessage candle, decimal shortEmaValue, decimal longEmaValue)
	{
		// Work only with finished candles to avoid repeated signals inside the same bar.
		if (candle.State != CandleStates.Finished)
		return;

		if (_pointSize <= 0m)
		_pointSize = CalculatePointSize();

		if (CheckRisk(candle))
		return;

		if (Position != 0)
		UpdateTrailingStop(candle);
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		ResetRiskLevels();

		var signal = EvaluateCross(longEmaValue, shortEmaValue);

		if (signal == 0)
		return;

		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		if (Position != 0)
		return;

		var volume = OrderVolume;
		if (volume <= 0m)
		volume = 1m;

		if (signal == 1)
		{
			BuyMarket(volume);
			SetRiskLevels(candle.ClosePrice, true);
		}
		else if (signal == 2)
		{
			SellMarket(volume);
			SetRiskLevels(candle.ClosePrice, false);
		}
	}

	private int EvaluateCross(decimal longValue, decimal shortValue)
	{
		var currentDirection = 0;

		if (longValue > shortValue)
		currentDirection = 1;
		else if (longValue < shortValue)
		currentDirection = 2;

		if (_skipFirstSignal)
		{
			_skipFirstSignal = false;
			return 0;
		}

		if (currentDirection != 0 && currentDirection != _lastDirection)
		{
			_lastDirection = currentDirection;
			return _lastDirection;
		}

		return 0;
	}

	private bool CheckRisk(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var size = Math.Abs(Position);

			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(size);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(size);
				ResetRiskLevels();
				return true;
			}
		}
		else if (Position < 0)
		{
			var size = Math.Abs(Position);

			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(size);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(size);
				ResetRiskLevels();
				return true;
			}
		}
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		{
			ResetRiskLevels();
		}

		return false;
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m || _pointSize <= 0m)
		return;

		var distance = TrailingStopPoints * _pointSize;
		if (distance <= 0m)
		return;

		var entryPrice = _entryPrice > 0 ? _entryPrice : candle.ClosePrice;

		if (Position > 0)
		{
			var profit = candle.ClosePrice - entryPrice;
			if (profit > distance)
			{
				var candidate = candle.ClosePrice - distance;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value < candidate)
				_stopLossPrice = candidate;
			}
		}
		else if (Position < 0)
		{
			var profit = entryPrice - candle.ClosePrice;
			if (profit > distance)
			{
				var candidate = candle.ClosePrice + distance;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value > candidate)
				_stopLossPrice = candidate;
			}
		}
	}

	private void SetRiskLevels(decimal executionPrice, bool isLong)
	{
		if (_pointSize <= 0m)
		{
			ResetRiskLevels();
			return;
		}

		_stopLossPrice = StopLossPoints > 0m
		? executionPrice + (isLong ? -1m : 1m) * StopLossPoints * _pointSize
		: null;

		_takeProfitPrice = TakeProfitPoints > 0m
		? executionPrice + (isLong ? 1m : -1m) * TakeProfitPoints * _pointSize
		: null;
	}

	private void ResetRiskLevels()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;

		if (Position == 0m)
			_entryPrice = 0m;
	}

	private decimal CalculatePointSize()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}
}