View on GitHub

Long/Short Expert MACD Strategy

Overview

The Long/Short Expert MACD Strategy is a StockSharp conversion of the MetaTrader expert "LongShortExpertMACD". It combines the standard Moving Average Convergence Divergence (MACD) crossover logic with fixed-distance risk controls. The strategy reacts to crossovers between the MACD line and its signal line, can operate in long-only, short-only, or bidirectional modes, and automatically applies take-profit and stop-loss levels expressed in price points.

The implementation uses the high-level StockSharp API with candle subscriptions and indicator bindings. Orders are registered as market orders, making the strategy simple to connect to both real-time and historical data sources.

Indicators and Market Data

  • Candles – a single timeframe provided by the CandleType parameter (1-minute time frame by default). The strategy subscribes to this candle series via SubscribeCandles.
  • MovingAverageConvergenceDivergenceSignal – StockSharp's MACD indicator with configurable fast EMA, slow EMA, and signal EMA lengths. The histogram value is implicitly derived from the difference between the MACD and signal outputs.

Trading Logic

  1. Signal preparation

    • On every finished candle the MACD and signal values are retrieved through the indicator binding.
    • Historical state _prevIsMacdAboveSignal tracks whether MACD was above the signal line during the previous candle.
  2. Entry conditions

    • Bullish crossover: when MACD crosses above the signal line, the strategy opens a long position if the configured trade direction allows long entries.
      • If a short position is already active and reversal mode is enabled (AllowedPosition = Both), the order size includes the current short volume to close the position and flip to long in a single market order.
      • In long-only mode an existing short position is immediately closed, but no new long trade is opened until the following signal.
    • Bearish crossover: the symmetric action for short entries.
  3. Exit conditions

    • Risk management: both stop-loss and take-profit levels are recomputed from the current average entry price each time a position is detected. The distances are set in price points (i.e., Security.PriceStep * parameter), which keeps the behaviour consistent across instruments.
      • Long positions exit when the candle's low reaches the stop-loss level or the high reaches the take-profit level.
      • Short positions exit when the candle's high reaches the stop-loss level or the low touches the take-profit level.
    • Opposite crossover: if trade direction permits the opposite side, the position is flattened (and optionally reversed) whenever the indicator relationship flips.
  4. Operational safeguards

    • Trading logic is executed only when the strategy is formed, online, and trading is allowed (IsFormedAndOnlineAndAllowTrading).
    • Protection levels are reset whenever no position is held to avoid stale thresholds.

Parameters

Name Default Description
AllowedPosition Both Restricts the strategy to long-only, short-only, or bidirectional trading.
FastLength 12 Period of the fast EMA within the MACD calculation.
SlowLength 24 Period of the slow EMA within the MACD calculation.
SignalLength 9 Period of the signal EMA used for crossover detection.
TakeProfitPoints 50 Distance to the take-profit level measured in price points (PriceStep * points). Set to 0 to disable.
StopLossPoints 20 Distance to the stop-loss level measured in price points. Set to 0 to disable.
CandleType TimeFrame(1 minute) Candle series used for signal generation.
Volume 1 Number of lots/contracts sent with each market order.

All numeric parameters expose optimization ranges to simplify walk-forward testing within StockSharp Designer or the Runner.

Position Management

  • Reversal logic: when bidirectional trading is allowed the strategy uses combined order sizes to flip positions in a single market order, mirroring the behaviour of the original MetaTrader expert.
  • Long-only / short-only modes: existing positions on the disallowed side are closed immediately, but no new exposure is established until a signal aligned with the permitted direction occurs.
  • Stop/take recalculation: the strategy recalculates protection levels on each candle using the latest PositionAvgPrice, ensuring correct distances even after partial fills or scaled entries.

Usage Notes

  • Ensure the instrument provides a valid PriceStep; if the value is missing the strategy falls back to 1.0 price units, which is appropriate for equity-style instruments but may require adjustment for Forex symbols.
  • The strategy relies on completed candles. Latency-sensitive scenarios should supply appropriately granular candles to avoid delays.
  • Because orders are market orders without slippage controls, risk management should consider potential fill differences, especially on illiquid assets.
  • Visualisation is automatically created when the host application supports chart areas; MACD, candles, and own trades are drawn for quick monitoring.

Conversion Notes

  • The StockSharp implementation preserves the configurable MACD parameters, take-profit and stop-loss distances, and the position-availability switch from the MQL5 expert.
  • Trailing-stop and money-management modules used in MetaTrader are intentionally omitted because their behaviour is equivalent to the "none" variants included with the original expert.
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>
/// Long/short MACD expert strategy converted from the MetaTrader example.
/// The strategy opens positions on MACD crossovers and applies fixed stop-loss and take-profit distances.
/// Allowed trade direction can be restricted to long only, short only, or both sides.
/// </summary>
public class LongShortExpertMacdStrategy : Strategy
{
	/// <summary>
	/// Trade directions supported by the strategy.
	/// </summary>
	public enum AllowedPositionTypes
	{
		/// <summary>
		/// Long trades only.
		/// </summary>
		Long,

		/// <summary>
		/// Short trades only.
		/// </summary>
		Short,

		/// <summary>
		/// Long and short trades are allowed.
		/// </summary>
		Both
	}

	private readonly StrategyParam<AllowedPositionTypes> _allowedPosition;
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _signalLength;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<DataType> _candleType;

	private MovingAverageConvergenceDivergenceSignal _macd = null!;

	private bool? _prevIsMacdAboveSignal;
	private decimal _longStopPrice;
	private decimal _longTakePrice;
	private decimal _shortStopPrice;
	private decimal _shortTakePrice;
	private decimal? _entryPrice;

	/// <summary>
	/// Initializes a new instance of <see cref="LongShortExpertMacdStrategy"/>.
	/// </summary>
	public LongShortExpertMacdStrategy()
	{
		_allowedPosition = Param(nameof(AllowedPosition), AllowedPositionTypes.Both)
			.SetDisplay("Allowed Positions", "Permitted trade direction", "General");

		_fastLength = Param(nameof(FastLength), 12)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Fast MACD EMA length", "MACD")

			.SetOptimize(8, 16, 2);

		_slowLength = Param(nameof(SlowLength), 24)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Slow MACD EMA length", "MACD")

			.SetOptimize(20, 40, 2);

		_signalLength = Param(nameof(SignalLength), 9)
			.SetGreaterThanZero()
			.SetDisplay("Signal EMA", "MACD signal EMA length", "MACD")

			.SetOptimize(5, 15, 1);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take profit distance in price points", "Risk")

			.SetOptimize(0, 150, 10);

		_stopLossPoints = Param(nameof(StopLossPoints), 20)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop loss distance in price points", "Risk")

			.SetOptimize(0, 100, 10);

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

		Volume = 1;
	}

	/// <summary>
	/// Allowed trade direction.
	/// </summary>
	public AllowedPositionTypes AllowedPosition
	{
		get => _allowedPosition.Value;
		set => _allowedPosition.Value = value;
	}

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

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

	/// <summary>
	/// Signal EMA length used by MACD.
	/// </summary>
	public int SignalLength
	{
		get => _signalLength.Value;
		set => _signalLength.Value = value;
	}

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

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

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

	private bool CanEnterLong => AllowedPosition != AllowedPositionTypes.Short;
	private bool CanEnterShort => AllowedPosition != AllowedPositionTypes.Long;
	private bool AllowReverse => AllowedPosition == AllowedPositionTypes.Both;

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

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

		_prevIsMacdAboveSignal = null;
		_entryPrice = null;
		ResetProtection();
	}

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

		_macd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = FastLength },
				LongMa = { Length = SlowLength },
			},
			SignalMa = { Length = SignalLength }
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_macd, ProcessCandle)
			.Start();

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

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

		var macdTyped = (MovingAverageConvergenceDivergenceSignalValue)macdValue;

		if (macdTyped.Macd is not decimal macd || macdTyped.Signal is not decimal signal)
			return;

		UpdateProtectionLevels();

		var isMacdAboveSignal = macd > signal;

		if (!_macd.IsFormed)
		{
			_prevIsMacdAboveSignal = isMacdAboveSignal;
			return;
		}

		if (TryExitWithProtection(candle))
		{
			_prevIsMacdAboveSignal = isMacdAboveSignal;
			return;
		}

		if (_prevIsMacdAboveSignal is null)
		{
			_prevIsMacdAboveSignal = isMacdAboveSignal;
			return;
		}

		var crossUp = isMacdAboveSignal && _prevIsMacdAboveSignal == false;
		var crossDown = !isMacdAboveSignal && _prevIsMacdAboveSignal == true;

		if (crossUp)
		{
			if (CanEnterLong)
			{
				if (Position < 0)
				{
					if (AllowReverse)
					{
						var volume = Volume + Math.Abs(Position);

						if (volume > 0)
						{
							ResetProtection();
							BuyMarket();
							_entryPrice = candle.ClosePrice;
						}
					}
					else
					{
						var volume = Math.Abs(Position);
						if (volume > 0)
						{
							BuyMarket();
							ResetProtection();
							_entryPrice = null;
						}
					}
				}
				else if (Position == 0)
				{
					if (Volume > 0)
					{
						ResetProtection();
						BuyMarket();
						_entryPrice = candle.ClosePrice;
					}
				}
			}
			else if (Position < 0)
			{
				var volume = Math.Abs(Position);
				if (volume > 0)
				{
					BuyMarket();
					ResetProtection();
					_entryPrice = null;
				}
			}
		}
		else if (crossDown)
		{
			if (CanEnterShort)
			{
				if (Position > 0)
				{
					if (AllowReverse)
					{
						var volume = Volume + Math.Abs(Position);

						if (volume > 0)
						{
							ResetProtection();
							SellMarket();
							_entryPrice = candle.ClosePrice;
						}
					}
					else
					{
						var volume = Math.Abs(Position);
						if (volume > 0)
						{
							SellMarket();
							ResetProtection();
							_entryPrice = null;
						}
					}
				}
				else if (Position == 0)
				{
					if (Volume > 0)
					{
						ResetProtection();
						SellMarket();
						_entryPrice = candle.ClosePrice;
					}
				}
			}
			else if (Position > 0)
			{
				var volume = Math.Abs(Position);
				if (volume > 0)
				{
					SellMarket();
					ResetProtection();
					_entryPrice = null;
				}
			}
		}

		_prevIsMacdAboveSignal = isMacdAboveSignal;
	}

	private void UpdateProtectionLevels()
	{
		if (_entryPrice is not decimal entry)
		{
			ResetProtection();
			return;
		}

		if (Position > 0)
		{
			var step = GetPriceStep();
			_longStopPrice = StopLossPoints > 0 ? entry - StopLossPoints * step : 0m;
			_longTakePrice = TakeProfitPoints > 0 ? entry + TakeProfitPoints * step : 0m;
			_shortStopPrice = 0m;
			_shortTakePrice = 0m;
		}
		else if (Position < 0)
		{
			var step = GetPriceStep();
			_shortStopPrice = StopLossPoints > 0 ? entry + StopLossPoints * step : 0m;
			_shortTakePrice = TakeProfitPoints > 0 ? entry - TakeProfitPoints * step : 0m;
			_longStopPrice = 0m;
			_longTakePrice = 0m;
		}
		else
		{
			ResetProtection();
		}
	}

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

			if (volume > 0)
			{
				if (StopLossPoints > 0 && _longStopPrice > 0m && candle.LowPrice <= _longStopPrice)
				{
					SellMarket();
					ResetProtection();
					_entryPrice = null;
					return true;
				}

				if (TakeProfitPoints > 0 && _longTakePrice > 0m && candle.HighPrice >= _longTakePrice)
				{
					SellMarket();
					ResetProtection();
					_entryPrice = null;
					return true;
				}
			}
		}
		else if (Position < 0)
		{
			var volume = Math.Abs(Position);

			if (volume > 0)
			{
				if (StopLossPoints > 0 && _shortStopPrice > 0m && candle.HighPrice >= _shortStopPrice)
				{
					BuyMarket();
					ResetProtection();
					_entryPrice = null;
					return true;
				}

				if (TakeProfitPoints > 0 && _shortTakePrice > 0m && candle.LowPrice <= _shortTakePrice)
				{
					BuyMarket();
					ResetProtection();
					_entryPrice = null;
					return true;
				}
			}
		}
		return false;
	}

	private void ResetProtection()
	{
		_longStopPrice = 0m;
		_longTakePrice = 0m;
		_shortStopPrice = 0m;
		_shortTakePrice = 0m;
	}

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