Auf GitHub ansehen

Example of MACD Automated Strategy

Overview

The strategy replicates the "Example of MACD Automated" MetaTrader 4 expert advisor using the StockSharp high-level API. It monitors the MACD main line on two timeframes and opens a single position when both trend filters agree. Protective stop-loss and take-profit distances are applied in price steps, and the position size follows the original AdvancedMM logic that accumulates the volume of recent losing trades.

Trading Logic

  1. Higher timeframe filter – a MACD (12, 26, 9) computed on the higher timeframe (default: daily candles) must have a positive main line for long signals or a negative main line for short signals.
  2. Entry timeframe confirmation – the same MACD settings on the entry timeframe (default: 15-minute candles) must point in the same direction as the higher timeframe filter.
  3. Single position – the strategy trades one position at a time. New entries are skipped until the existing position is closed by protective levels.
  4. Protective orders – stop-loss and take-profit levels are measured in multiples of the instrument price step, mirroring the original MT4 StopLoss and TakeProfit inputs. A value of 0 disables the corresponding protection.
  5. Advanced money management – the trade volume increases after consecutive losing trades by summing the lot size of the losses, and reverts to the base volume after profitable trades, emulating the AdvancedMM() function from the source EA.

Parameters

Name Description Default
BaseVolume Base order volume used by the AdvancedMM logic. 0.01
StopLossPoints Stop-loss distance expressed in price steps. 0 disables the stop. 50
TakeProfitPoints Take-profit distance expressed in price steps. 0 disables the target. 30
MacdFastLength Fast EMA period of the MACD on both timeframes. 12
MacdSlowLength Slow EMA period of the MACD on both timeframes. 26
MacdSignalLength Signal line EMA period. 9
EntryCandleType Timeframe for trade execution. 15m candles
FilterCandleType Higher timeframe used as trend filter. 1d candles

Position Management

  • Stop-loss and take-profit prices are recalculated on every new position based on the instrument price step.
  • When either protective level is touched inside a bar, the strategy assumes the order is filled at that level and records the realized profit or loss.
  • After each closed trade the AdvancedMM logic updates the next order size:
    • Less than two historical trades → use the base volume.
    • The most recent trade was a loss → repeat its volume.
    • Consecutive losses before the last win → sum their volumes to recover.
    • Otherwise → revert to the base volume.

Notes

  • The conversion keeps the original behaviour of holding a position until a protective level is hit; there is no exit on MACD crossovers.
  • Ensure the instrument has valid PriceStep information so that point-based stop and target distances are calculated correctly.
  • The strategy relies on completed candles and should be used with historical data or live feeds that provide finished candle updates.
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>
/// Conversion of the "Example of MACD Automated" MQL4 expert advisor.
/// The strategy waits for MACD agreement on two timeframes and uses AdvancedMM sizing.
/// </summary>
public class ExampleOfMacdAutomatedStrategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<int> _macdFastLength;
	private readonly StrategyParam<int> _macdSlowLength;
	private readonly StrategyParam<int> _macdSignalLength;
	private readonly StrategyParam<DataType> _entryCandleType;
	private readonly StrategyParam<DataType> _filterCandleType;

	private MovingAverageConvergenceDivergenceSignal _entryMacd = null!;
	private MovingAverageConvergenceDivergenceSignal _filterMacd = null!;

	private decimal? _lastEntryMacd;
	private decimal? _lastFilterMacd;

	private readonly List<TradeInfo> _tradeHistory = new();

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private decimal _entryVolume;
	private int _entryDirection;

	/// <summary>
	/// Initializes a new instance of the <see cref="ExampleOfMacdAutomatedStrategy"/> class.
	/// </summary>
	public ExampleOfMacdAutomatedStrategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Starting order volume for AdvancedMM", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 50m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (steps)", "Stop-loss distance in price steps", "Risk")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 30m)
			.SetNotNegative()
			.SetDisplay("Take Profit (steps)", "Take-profit distance in price steps", "Risk")
			;

		_macdFastLength = Param(nameof(MacdFastLength), 12)
			.SetGreaterThanZero()
			.SetDisplay("MACD Fast", "Fast EMA length", "Indicators")
			;

		_macdSlowLength = Param(nameof(MacdSlowLength), 26)
			.SetGreaterThanZero()
			.SetDisplay("MACD Slow", "Slow EMA length", "Indicators")
			;

		_macdSignalLength = Param(nameof(MacdSignalLength), 9)
			.SetGreaterThanZero()
			.SetDisplay("MACD Signal", "Signal EMA length", "Indicators")
			;

		_entryCandleType = Param(nameof(EntryCandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Entry Timeframe", "Working timeframe for entries", "General");

		_filterCandleType = Param(nameof(FilterCandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Filter Timeframe", "Higher timeframe used as trend filter", "General");
	}

	/// <summary>
	/// Base volume parameter.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

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

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

	/// <summary>
	/// MACD fast EMA length.
	/// </summary>
	public int MacdFastLength
	{
		get => _macdFastLength.Value;
		set => _macdFastLength.Value = value;
	}

	/// <summary>
	/// MACD slow EMA length.
	/// </summary>
	public int MacdSlowLength
	{
		get => _macdSlowLength.Value;
		set => _macdSlowLength.Value = value;
	}

	/// <summary>
	/// MACD signal EMA length.
	/// </summary>
	public int MacdSignalLength
	{
		get => _macdSignalLength.Value;
		set => _macdSignalLength.Value = value;
	}

	/// <summary>
	/// Timeframe used for entries.
	/// </summary>
	public DataType EntryCandleType
	{
		get => _entryCandleType.Value;
		set => _entryCandleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe used as a trend filter.
	/// </summary>
	public DataType FilterCandleType
	{
		get => _filterCandleType.Value;
		set => _filterCandleType.Value = value;
	}

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

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

		_lastEntryMacd = null;
		_lastFilterMacd = null;
		_tradeHistory.Clear();
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_entryVolume = 0m;
		_entryDirection = 0;

		_entryMacd?.Reset();
		_filterMacd?.Reset();
	}

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

		// Create MACD indicators for entry and filter timeframes.
		_entryMacd = CreateMacd();
		_filterMacd = CreateMacd();

		var entrySubscription = SubscribeCandles(EntryCandleType);
		entrySubscription
			.BindEx(_entryMacd, ProcessEntryCandle)
			.Start();

		var filterSubscription = SubscribeCandles(FilterCandleType);
		filterSubscription
			.BindEx(_filterMacd, ProcessFilterCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, entrySubscription);
			DrawIndicator(area, _entryMacd);
			DrawIndicator(area, _filterMacd);
			DrawOwnTrades(area);
		}
	}

	private MovingAverageConvergenceDivergenceSignal CreateMacd()
	{
		// Instantiate MACD with shared parameters for both timeframes.
		return new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = MacdFastLength },
				LongMa = { Length = MacdSlowLength },
			},
			SignalMa = { Length = MacdSignalLength }
		};
	}

	private void ProcessFilterCandle(ICandleMessage candle, IIndicatorValue macdValue)
	{
		// Process only completed candles on the filter timeframe.
		if (candle.State != CandleStates.Finished)
		return;

		var macd = (IMovingAverageConvergenceDivergenceSignalValue)macdValue;
		_lastFilterMacd = macd.Macd;
	}

	private void ProcessEntryCandle(ICandleMessage candle, IIndicatorValue macdValue)
	{
		// Ensure that we operate on final candle values only.
		if (candle.State != CandleStates.Finished)
		return;

		var macd = (IMovingAverageConvergenceDivergenceSignalValue)macdValue;
		var currentEntryMacd = macd.Macd;

		// Manage protective exits before searching for new entries.
		if (HandleProtection(candle))
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		// Skip further processing if there is still an open position.
		if (Position != 0)
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		if (!_entryMacd.IsFormed || !_filterMacd.IsFormed)
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		if (_lastEntryMacd is not decimal previousEntryMacd || _lastFilterMacd is not decimal filterMacdValue)
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		// Enter only on a zero-line crossover aligned with the higher timeframe filter.
		if (previousEntryMacd <= 0m && currentEntryMacd > 0m && filterMacdValue > 0m)
		{
			EnterPosition(candle.ClosePrice, true);
		}
		else if (previousEntryMacd >= 0m && currentEntryMacd < 0m && filterMacdValue < 0m)
		{
			EnterPosition(candle.ClosePrice, false);
		}

		_lastEntryMacd = currentEntryMacd;
	}

	private void EnterPosition(decimal price, bool isLong)
	{
		var volume = CalculateTradeVolume();
		if (volume <= 0m)
		return;

		if (isLong)
		{
			BuyMarket(volume);
			RegisterEntry(price, volume, 1);
		}
		else
		{
			SellMarket(volume);
			RegisterEntry(price, volume, -1);
		}
	}

	private void RegisterEntry(decimal price, decimal volume, int direction)
	{
		// Store entry information for later profit calculation.
		_entryPrice = price;
		_entryVolume = volume;
		_entryDirection = direction;

		UpdateProtectionLevels(price, direction > 0);
	}

	private void UpdateProtectionLevels(decimal price, bool isLong)
	{
		var point = GetPointValue();

		if (point <= 0m)
		{
			_stopPrice = null;
			_takeProfitPrice = null;
			return;
		}

		if (isLong)
		{
			_stopPrice = StopLossPoints > 0m ? price - StopLossPoints * point : null;
			_takeProfitPrice = TakeProfitPoints > 0m ? price + TakeProfitPoints * point : null;
		}
		else
		{
			_stopPrice = StopLossPoints > 0m ? price + StopLossPoints * point : null;
			_takeProfitPrice = TakeProfitPoints > 0m ? price - TakeProfitPoints * point : null;
		}
	}

	private bool HandleProtection(ICandleMessage candle)
	{
		if (Position == 0 || _entryDirection == 0)
		return false;

		if (_entryDirection > 0)
		{
			if (TryGetLongExitPrice(candle, out var exitPrice))
			{
				SellMarket(Math.Abs(Position));
				RegisterClosedTrade(exitPrice);
				return true;
			}
		}
		else
		{
			if (TryGetShortExitPrice(candle, out var exitPrice))
			{
				BuyMarket(Math.Abs(Position));
				RegisterClosedTrade(exitPrice);
				return true;
			}
		}

		return false;
	}

	private bool TryGetLongExitPrice(ICandleMessage candle, out decimal exitPrice)
	{
		exitPrice = 0m;

		if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
		{
			exitPrice = _stopPrice.Value;
			return true;
		}

		if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
		{
			exitPrice = _takeProfitPrice.Value;
			return true;
		}

		return false;
	}

	private bool TryGetShortExitPrice(ICandleMessage candle, out decimal exitPrice)
	{
		exitPrice = 0m;

		if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
		{
			exitPrice = _stopPrice.Value;
			return true;
		}

		if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
		{
			exitPrice = _takeProfitPrice.Value;
			return true;
		}

		return false;
	}

	private void RegisterClosedTrade(decimal exitPrice)
	{
		if (!_entryPrice.HasValue || _entryVolume <= 0m || _entryDirection == 0)
		return;

		var entryPrice = _entryPrice.Value;
		var volume = _entryVolume;
		var direction = _entryDirection;

		var profit = (exitPrice - entryPrice) * direction * volume;

		_tradeHistory.Add(new TradeInfo(volume, profit));
		if (_tradeHistory.Count > 200)
		_tradeHistory.RemoveAt(0);

		_entryPrice = null;
		_entryVolume = 0m;
		_entryDirection = 0;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	private decimal CalculateTradeVolume()
	{
		var baseVolume = BaseVolume;
		if (baseVolume <= 0m)
		return 0m;

		if (_tradeHistory.Count < 2)
		return baseVolume;

		var advancedLots = 0m;
		var profit1 = false;
		var profit2 = false;
		var firstIteration = true;

		for (var i = _tradeHistory.Count - 1; i >= 0; i--)
		{
			var trade = _tradeHistory[i];
			var isProfit = trade.Profit >= 0m;

			if (isProfit && profit1)
			return baseVolume;

			if (firstIteration)
			{
				if (isProfit)
				{
					profit1 = true;
				}
				else
				{
					return trade.Volume;
				}

				firstIteration = false;
			}

			if (isProfit && profit2)
			return advancedLots > 0m ? advancedLots : baseVolume;

			if (isProfit)
			{
				profit2 = true;
			}
			else
			{
				profit1 = false;
				profit2 = false;
				advancedLots += trade.Volume;
			}
		}

		return advancedLots > 0m ? advancedLots : baseVolume;
	}

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

	private readonly struct TradeInfo
	{
		public TradeInfo(decimal volume, decimal profit)
		{
			Volume = volume;
			Profit = profit;
		}

		public decimal Volume { get; }
		public decimal Profit { get; }
	}
}