View on GitHub

MACD Multi-Timeframe Expert Strategy

Overview

This strategy replicates the original "MACD Expert" MetaTrader robot inside the StockSharp framework. It synchronizes MACD trends across four timeframes—5 minutes, 15 minutes, 1 hour, and 4 hours—and only allows a new position when every timeframe points in the same direction. The goal is to capture multi-timeframe momentum alignment while filtering out periods of high spread.

Data & Indicators

  • Candles: 5m (execution), 15m, 1h and 4h confirmations. All candles use close prices and finished bars only.
  • Indicator: MovingAverageConvergenceDivergenceSignal with defaults 12/26/9. Each timeframe has its own MACD instance so that signals do not interfere.
  • Level 1 Quotes: Best bid/ask quotes are consumed to monitor the live spread before opening trades.

Trading Logic

  1. Wait for all four MACD instances to emit a completed value.
  2. Compute the relationship between the MACD line and signal line on every timeframe.
  3. Enforce a maximum spread filter measured in price points (price steps).
  4. Open at most one position at a time; existing positions must finish via stop-loss or take-profit before a new order is allowed.

Long Setup

  • MACD signal line is above the MACD line on all monitored timeframes.
  • Spread does not exceed MaxSpreadPoints.
  • A long position is opened with OrderVolume lots at the close of the latest 5-minute candle.

Short Setup

  • MACD signal line is below the MACD line on all monitored timeframes.
  • Spread does not exceed MaxSpreadPoints.
  • A short position is opened with OrderVolume lots at the close of the latest 5-minute candle.

Position Management

  • Long trades place logical targets at TakeProfitPoints above the entry and stops StopLossPoints below it.
  • Short trades place logical targets at TakeProfitPoints below the entry and stops StopLossPoints above it.
  • Exits trigger when the intrabar high/low of a finished 5-minute candle touches the respective target or stop level.
  • While in position the strategy ignores opposite signals; it waits until the trade is closed by stop or take-profit before reacting again, matching the original MQL logic.

Parameters

Name Default Description
OrderVolume 0.1 Position size in lots (mirrors the Lots input of the MQL version).
StopLossPoints 200 Distance to the protective stop in price points.
TakeProfitPoints 400 Distance to the profit target in price points.
MaxSpreadPoints 20 Maximum allowed spread in price points before entries are skipped.
FastPeriod 12 Fast EMA length inside each MACD instance.
SlowPeriod 26 Slow EMA length inside each MACD instance.
SignalPeriod 9 Signal EMA length inside each MACD instance.
FiveMinuteCandleType 5-minute candles Primary execution timeframe.
FifteenMinuteCandleType 15-minute candles First confirmation timeframe.
HourCandleType 1-hour candles Second confirmation timeframe.
FourHourCandleType 4-hour candles Third confirmation timeframe.

Implementation Notes

  • Uses BindEx to read strongly typed MACD values without calling GetValue, following the project guidelines.
  • A shared helper converts the MACD/signal relationship into {-1, 0, 1} flags to simplify confirmation checks.
  • Spread validation divides the best ask minus best bid by Security.PriceStep so the threshold matches MetaTrader "points" behavior.
  • Trade events are logged with LogInfo to aid debugging when testing in Designer or Runner.
  • No Python translation is provided, per the task requirements; only the C# version is included.
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>
/// Multi-timeframe MACD confirmation strategy that aligns primary and confirmation timeframe trends.
/// </summary>
public class MacdMultiTimeframeExpertStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _signalPeriod;
	private readonly StrategyParam<DataType> _primaryType;
	private readonly StrategyParam<DataType> _confirmType;

	private MovingAverageConvergenceDivergenceSignal _macdPrimary;
	private MovingAverageConvergenceDivergenceSignal _macdConfirm;

	private int? _relationPrimary;
	private int? _relationConfirm;
	private int _lastTradeDirection;
	private int _candlesSinceEntry;

	private decimal _entryPrice;

	/// <summary>
	/// Order volume in lots.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

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

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

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

	/// <summary>
	/// Signal line period used by MACD.
	/// </summary>
	public int SignalPeriod
	{
		get => _signalPeriod.Value;
		set => _signalPeriod.Value = value;
	}

	/// <summary>
	/// Candle type for the primary execution timeframe.
	/// </summary>
	public DataType PrimaryCandleType
	{
		get => _primaryType.Value;
		set => _primaryType.Value = value;
	}

	/// <summary>
	/// Candle type for the confirmation timeframe.
	/// </summary>
	public DataType ConfirmCandleType
	{
		get => _confirmType.Value;
		set => _confirmType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="MacdMultiTimeframeExpertStrategy"/> class.
	/// </summary>
	public MacdMultiTimeframeExpertStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Position size in lots", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 1500m)
			.SetNotNegative()
			.SetDisplay("Stop Loss Points", "Stop-loss distance in points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2500m)
			.SetNotNegative()
			.SetDisplay("Take Profit Points", "Take-profit distance in points", "Risk");

		_fastPeriod = Param(nameof(FastPeriod), 12)
			.SetGreaterThanZero()
			.SetDisplay("MACD Fast", "Fast EMA period", "MACD");

		_slowPeriod = Param(nameof(SlowPeriod), 26)
			.SetGreaterThanZero()
			.SetDisplay("MACD Slow", "Slow EMA period", "MACD");

		_signalPeriod = Param(nameof(SignalPeriod), 9)
			.SetGreaterThanZero()
			.SetDisplay("MACD Signal", "Signal EMA period", "MACD");

		_primaryType = Param(nameof(PrimaryCandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Primary", "Primary execution timeframe", "Timeframes");

		_confirmType = Param(nameof(ConfirmCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Confirm", "Confirmation timeframe", "Timeframes");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, PrimaryCandleType);
		yield return (Security, ConfirmCandleType);
	}

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

		_macdPrimary = null;
		_macdConfirm = null;
		_relationPrimary = null;
		_relationConfirm = null;
		_lastTradeDirection = 0;
		_candlesSinceEntry = 0;
		_entryPrice = 0m;
	}

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

		_macdPrimary = CreateMacd();
		_macdConfirm = CreateMacd();

		var primarySubscription = SubscribeCandles(PrimaryCandleType);
		primarySubscription
			.Bind(ProcessPrimaryCandle)
			.Start();

		SubscribeCandles(ConfirmCandleType)
			.Bind(ProcessConfirmCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, primarySubscription);
			DrawOwnTrades(area);
		}
	}

	private MovingAverageConvergenceDivergenceSignal CreateMacd()
	{
		return new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = FastPeriod },
				LongMa = { Length = SlowPeriod }
			},
			SignalMa = { Length = SignalPeriod }
		};
	}

	private void ProcessPrimaryCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var macdValue = _macdPrimary.Process(candle);
		if (!TryUpdateRelation(macdValue, out var relation))
			return;

		_relationPrimary = relation;
		_candlesSinceEntry++;

		// Manage protective exits whenever a position is open.
		if (Position != 0)
		{
			ManageOpenPosition(candle);

			// If position was closed by SL/TP, allow new entry below
			if (Position != 0)
				return;
		}

		if (!_relationConfirm.HasValue)
			return;

		if (OrderVolume <= 0)
			return;

		// Cooldown: require at least 6 candles between trades.
		if (_candlesSinceEntry < 6)
			return;

		// Determine aligned direction: both timeframes must agree.
		var alignedDirection = 0;

		if (_relationPrimary == 1 && _relationConfirm == 1)
			alignedDirection = 1;
		else if (_relationPrimary == -1 && _relationConfirm == -1)
			alignedDirection = -1;

		if (alignedDirection == 0)
			return;

		// Avoid repeated entries in the same direction.
		if (_lastTradeDirection == alignedDirection)
			return;

		_lastTradeDirection = alignedDirection;
		_candlesSinceEntry = 0;

		if (alignedDirection > 0)
		{
			BuyMarket(OrderVolume);
			_entryPrice = candle.ClosePrice;
		}
		else
		{
			SellMarket(OrderVolume);
			_entryPrice = candle.ClosePrice;
		}
	}

	private void ProcessConfirmCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var macdValue = _macdConfirm.Process(candle);
		if (TryUpdateRelation(macdValue, out var relation))
			_relationConfirm = relation;
	}

	private bool TryUpdateRelation(IIndicatorValue macdValue, out int relation)
	{
		relation = 0;

		if (!macdValue.IsFinal)
			return false;

		var typed = (MovingAverageConvergenceDivergenceSignalValue)macdValue;

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

		// Standard MACD: macd > signal = bullish, macd < signal = bearish.
		if (macd > signal)
			relation = 1;
		else if (macd < signal)
			relation = -1;
		else
			relation = 0;

		return true;
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		// Derive the point value. Fall back to 1 if the security lacks a price step.
		var point = Security?.PriceStep ?? 0m;
		if (point <= 0)
			point = 1m;

		if (Position > 0)
		{
			if (TakeProfitPoints > 0 && candle.HighPrice >= _entryPrice + TakeProfitPoints * point)
			{
				SellMarket(Position);
				_entryPrice = 0m;
				_lastTradeDirection = 0;
				return;
			}

			if (StopLossPoints > 0 && candle.LowPrice <= _entryPrice - StopLossPoints * point)
			{
				SellMarket(Position);
				_entryPrice = 0m;
				_lastTradeDirection = 0;
			}
		}
		else if (Position < 0)
		{
			var volume = Position.Abs();

			if (TakeProfitPoints > 0 && candle.LowPrice <= _entryPrice - TakeProfitPoints * point)
			{
				BuyMarket(volume);
				_entryPrice = 0m;
				_lastTradeDirection = 0;
				return;
			}

			if (StopLossPoints > 0 && candle.HighPrice >= _entryPrice + StopLossPoints * point)
			{
				BuyMarket(volume);
				_entryPrice = 0m;
				_lastTradeDirection = 0;
			}
		}
		else
		{
			_entryPrice = 0m;
		}
	}
}