Ver no GitHub

EMA Cross 2 Strategy

Overview

This strategy is a StockSharp port of the MetaTrader 4 expert advisor "EMA_CROSS_2" from the MQL repository. The original EA monitors two exponential moving averages (EMAs) and places market orders whenever the averages swap order. The conversion keeps the contrarian nature of the script – it buys when the long EMA moves above the short EMA and sells when the short EMA rises above the long EMA – while wrapping the logic into the high-level StockSharp strategy infrastructure.

The strategy operates on completed candles supplied by the configurable candle data type. Signals are evaluated on candle close to avoid repeated triggers inside the same bar. Risk management mimics the MetaTrader behaviour by using take-profit, stop-loss, and trailing stop distances expressed in broker points (price steps).

Trading Logic

  1. Indicator calculation
    • Calculate the short-period and long-period EMAs on every completed candle.
    • Skip the first indicator update, matching the original first_time flag that ignored the very first evaluation.
    • Afterwards, detect a direction change when the relative ordering between the long and short EMA flips.
  2. Signal interpretation
    • When the long EMA moves above the short EMA the original EA opened a buy trade. The StockSharp port keeps this contrarian rule even though it behaves opposite to a classic crossover system.
    • When the short EMA closes above the long EMA the strategy opens a sell trade.
    • New positions are only allowed when no exposure is currently open, replicating the OrdersTotal() < 1 condition.
  3. Order execution
    • Trades are sent as market orders with a fixed configurable volume.
    • On entry the strategy records stop-loss and take-profit prices using the pip distance provided through parameters.
  4. Risk management
    • On every finished candle the strategy checks whether price action touched the stored stop-loss or take-profit levels. Breaching either level closes the entire position with a market order.
    • A trailing stop (also defined in broker points) is applied once price moves favourably by more than the trailing distance. For long positions the protective stop is shifted upward; for short positions it trails price downward.
    • When the position becomes flat, the stored protective levels are cleared.

Parameters

Name Description Default
CandleType Candle series used for indicator calculations and signal detection. 15-minute time frame
OrderVolume Volume of each market order in lots/contracts. 2
TakeProfitPoints Distance to the take-profit level expressed in broker points (price steps). A value of 0 disables the take-profit. 20
StopLossPoints Distance to the stop-loss level expressed in broker points. A value of 0 disables the stop-loss. 30
TrailingStopPoints Distance used when trailing the open position. 0 disables the trailing stop. 50
ShortEmaPeriod Length of the fast EMA. 5
LongEmaPeriod Length of the slow EMA. 60

Implementation Notes

  • The strategy uses SubscribeCandles().Bind(shortEma, longEma, ProcessCandle) to connect candle data with EMA indicators, following the preferred high-level API pattern.
  • Indicator values are received as ready-to-use decimals in the binding callback, so no manual buffer indexing is necessary.
  • Protective distances are converted from MetaTrader points to StockSharp prices by multiplying by the instrument PriceStep. If the instrument uses fractional pip pricing (3 or 5 decimals) the helper computes the pip size accordingly.
  • Stop-loss, take-profit, and trailing behaviour are implemented internally with market exits because StockSharp does not expose the same OrderModify workflow as MetaTrader 4. The resulting trade management mirrors the original logic: levels are checked on every candle and exits occur immediately once breached.
  • The first crossover evaluation is intentionally ignored to reproduce the first_time safeguard that prevented premature trades in the MQL script.

Differences from the MetaTrader Version

  • Money management: the original EA always traded the Lots parameter. The conversion exposes the same concept through OrderVolume and also assigns it to the strategy Volume property so designers and optimisers can reuse it.
  • Order placement: MetaTrader applied stop-loss and take-profit directly within OrderSend. In StockSharp these levels are tracked by the strategy and closed with market orders when breached.
  • Trailing stop precision: the EA moved stops using tick data (Bid/Ask). The port updates the trailing logic on candle close, which is the finest granularity available inside this sample project. The distance and activation rules remain identical.
  • Error handling and logging were simplified; StockSharp logging provides detailed information through the standard strategy log.

Usage Tips

  • Align CandleType with the timeframe used during backtests of the original EA to maintain comparable indicator behaviour.
  • When trading symbols quoted with fractional pips ensure that the configured point distances reflect the desired number of pips (for example, on EURUSD 10 points equal 1 pip).
  • Set OrderVolume to the contract size expected by your execution venue. The strategy does not perform automatic volume scaling.
  • Use the built-in optimisation toggles on each parameter to explore combinations of EMA periods and risk distances just like you would optimise inputs in MetaTrader.

Files

  • CS/EmaCross2Strategy.cs – StockSharp implementation of the trading logic.
  • README.md – English documentation (this file).
  • README_zh.md – Chinese translation.
  • README_ru.md – Russian translation.
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;
	}
}