Ver no GitHub

Macd Pattern Trader v03 (StockSharp port)

Overview

Macd Pattern Trader v03 is a high-level StockSharp strategy converted from the MetaTrader 4 expert advisor MacdPatternTraderv03. The original robot searches the MACD main line for a three-peak reversal formation and applies partial profit-taking rules based on moving averages. This C# port preserves the pattern logic while using StockSharp subscriptions, indicators, and order helpers.

The strategy is designed for trend exhaustion setups on liquid FX pairs, but it can be applied to any instrument that exposes a smooth MACD curve. The default timeframe is 30-minute candles, matching the original advisor, and the default trade size is one contract (or lot equivalent in StockSharp terms).

Indicators and data flow

  • MACD (Fast EMA 5, Slow EMA 13, Signal 1) — main indicator used to detect the triple-top/triple-bottom structure. The signal line is not used; the strategy relies on the MACD main line only.
  • EMA(7) and EMA(21) — short and medium averages used during position management.
  • SMA(98) and EMA(365) — slow filters that form the scaling-out trigger.

The implementation subscribes to the configured candle type and binds the indicators through Bind / BindEx. Only finished candles are processed to avoid acting on incomplete data.

Entry rules

Short setup

  1. Arm the setup when the MACD main line rises above the Upper Activation level (default 0.0030).
  2. Register the first peak once MACD prints a local maximum above both the previous and pre-previous values and then falls below the Upper Threshold (default 0.0045).
  3. Register the second peak if MACD returns above the threshold, makes a higher local maximum, and drops back below the threshold again.
  4. Confirm the pattern when a third rollover occurs with MACD staying below the threshold for three consecutive bars and the last local maximum is lower than the previous one.
  5. If no long position exists, flatten any remaining long exposure and open a short position with the configured volume.

Long setup

  1. Arm the setup when the MACD main line drops below the Lower Activation level (default −0.0030).
  2. Register the first trough once MACD prints a local minimum below the two previous values and then rises above the Lower Threshold (default −0.0045).
  3. Register the second trough if MACD falls back under the threshold, reaches a lower minimum, and rises above the threshold again.
  4. Confirm the bullish pattern when a third upswing is observed with MACD staying above the threshold for three candles and the latest trough is higher than the previous one.
  5. Flatten any remaining short exposure and buy the configured volume.

The logic mirrors the nested stops, stops1, and aop_ok* flags in the original MQ4 file, including resets whenever MACD retraces past the activation band.

Trade management

  • Scaling out — when unrealized profit (calculated as (Close − Entry) * Position) exceeds ProfitThreshold (default 5 price units), the strategy applies two staged exits:
    • Stage 1 (long): previous candle close must stay above EMA(21). The strategy sells one-third of the initial long position. For shorts the requirement is the previous close below EMA(21) and one-third of the initial short volume is bought back.
    • Stage 2 (long): previous candle high must pierce the average of SMA(98) and EMA(365). Half of the original long position is closed. Shorts mirror this with the previous low dropping below the averaged filter.
  • Residual position — whatever remains after the scaling sequence is left unmanaged by this port, matching the source EA.
  • Risk orders — the MetaTrader version placed stop-loss and take-profit orders based on rolling highs and lows. Because StockSharp manages protective orders differently, this port does not auto-attach stops/targets. Users may combine the strategy with StartProtection() or an external risk module if required.

Parameters

Name Default Description
Volume 1 Trade size submitted on each entry.
CandleType 30-minute time frame Candle series used for indicator calculations.
FastEmaLength / SlowEmaLength 5 / 13 MACD fast and slow EMA periods.
UpperThreshold / LowerThreshold 0.0045 / −0.0045 Exhaustion band where pattern confirmations happen.
UpperActivation / LowerActivation 0.0030 / −0.0030 Outer band that arms the bearish/bullish setups.
EmaOneLength / EmaTwoLength 7 / 21 Auxiliary EMAs for visualization and scaling logic.
SmaLength 98 Slow SMA used together with EMA(365) during stage-two exits.
EmaFourLength 365 Long-term EMA used during stage-two exits.
ProfitThreshold 5 Minimum unrealized PnL (price * volume units) required before scaling out.

Practical notes

  • Ensure the broker adapter supports partial position reduction. The original EA closed 1/3 and 1/2 portions; this port replicates the same fractions using market orders.
  • Because protective orders are not attached automatically, consider enabling StartProtection() or adding custom risk rules if you need hard stops.
  • The profit threshold is expressed in raw price * volume units. Adjust it according to the instrument's pip size or tick value to match the “5 currency units” assumption from the original MQ4 code.
  • The strategy expects smooth MACD dynamics; excessive noise or illiquid instruments may prevent the three-peak logic from triggering.

Differences from the MQ4 version

  • Uses StockSharp indicator bindings instead of repeated iMACD calls.
  • Unrealized profit calculation relies on Position and PositionAvgPrice, meaning broker rounding rules might differ from MetaTrader's OrderProfit().
  • Stop-loss and take-profit orders are not auto-generated; manual risk tools must be added if needed.
  • The MQ4 parameter sum_bars_bup is not present because it was unused in the original source.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// MACD pattern strategy inspired by the MetaTrader advisor "MacdPatternTraderv03".
/// </summary>
public class MacdPatternTraderV03Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastEmaLength;
	private readonly StrategyParam<int> _slowEmaLength;
	private readonly StrategyParam<decimal> _upperThreshold;
	private readonly StrategyParam<decimal> _upperActivation;
	private readonly StrategyParam<decimal> _lowerThreshold;
	private readonly StrategyParam<decimal> _lowerActivation;
	private readonly StrategyParam<int> _emaOneLength;
	private readonly StrategyParam<int> _emaTwoLength;
	private readonly StrategyParam<int> _smaLength;
	private readonly StrategyParam<int> _emaFourLength;
	private readonly StrategyParam<decimal> _profitThreshold;

	private decimal? _previousMacd;
	private decimal? _olderMacd;
	private decimal _entryPrice;

	private bool _isAboveUpperActivation;
	private bool _firstUpperDropConfirmed;
	private bool _secondUpperDropConfirmed;
	private bool _sellReady;
	private decimal _firstUpperPeak;
	private decimal _secondUpperPeak;

	private bool _isBelowLowerActivation;
	private bool _firstLowerRiseConfirmed;
	private bool _secondLowerRiseConfirmed;
	private bool _buyReady;
	private decimal _firstLowerTrough;
	private decimal _secondLowerTrough;

	private decimal? _emaTwoValue;
	private decimal? _smaValue;
	private decimal? _emaFourValue;

	private ICandleMessage _previousCandle;

	private int _longScaleStage;
	private int _shortScaleStage;
	private decimal _initialLongPosition;
	private decimal _initialShortPosition;

	/// <summary>
	/// Initializes a new instance of the strategy.
	/// </summary>
	public MacdPatternTraderV03Strategy()
	{

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for calculations", "General");

		_fastEmaLength = Param(nameof(FastEmaLength), 5)
		.SetDisplay("Fast EMA", "Fast period used inside MACD", "MACD");

		_slowEmaLength = Param(nameof(SlowEmaLength), 13)
		.SetDisplay("Slow EMA", "Slow period used inside MACD", "MACD");

		_upperThreshold = Param(nameof(UpperThreshold), 50m)
		.SetDisplay("Upper Threshold", "Level that confirms bearish exhaustion", "MACD");

		_upperActivation = Param(nameof(UpperActivation), 30m)
		.SetDisplay("Upper Activation", "Level that arms the bearish pattern", "MACD");

		_lowerThreshold = Param(nameof(LowerThreshold), -50m)
		.SetDisplay("Lower Threshold", "Level that confirms bullish exhaustion", "MACD");

		_lowerActivation = Param(nameof(LowerActivation), -30m)
		.SetDisplay("Lower Activation", "Level that arms the bullish pattern", "MACD");

		_emaOneLength = Param(nameof(EmaOneLength), 7)
		.SetDisplay("EMA #1", "Short EMA used for scaling out", "Management");

		_emaTwoLength = Param(nameof(EmaTwoLength), 21)
		.SetDisplay("EMA #2", "Second EMA used for scaling out", "Management");

		_smaLength = Param(nameof(SmaLength), 98)
		.SetDisplay("SMA", "Simple moving average used for scaling out", "Management");

		_emaFourLength = Param(nameof(EmaFourLength), 365)
		.SetDisplay("EMA #4", "Slow EMA used for scaling out", "Management");

		_profitThreshold = Param(nameof(ProfitThreshold), 5m)
		.SetDisplay("Profit Threshold", "Unrealized PnL required before scaling out", "Management");
	}


	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Fast EMA length inside MACD.
	/// </summary>
	public int FastEmaLength
	{
		get => _fastEmaLength.Value;
		set => _fastEmaLength.Value = value;
	}

	/// <summary>
	/// Slow EMA length inside MACD.
	/// </summary>
	public int SlowEmaLength
	{
		get => _slowEmaLength.Value;
		set => _slowEmaLength.Value = value;
	}

	/// <summary>
	/// Upper threshold that marks MACD exhaustion for shorts.
	/// </summary>
	public decimal UpperThreshold
	{
		get => _upperThreshold.Value;
		set => _upperThreshold.Value = value;
	}

	/// <summary>
	/// Upper activation level that arms the short pattern.
	/// </summary>
	public decimal UpperActivation
	{
		get => _upperActivation.Value;
		set => _upperActivation.Value = value;
	}

	/// <summary>
	/// Lower threshold that marks MACD exhaustion for longs.
	/// </summary>
	public decimal LowerThreshold
	{
		get => _lowerThreshold.Value;
		set => _lowerThreshold.Value = value;
	}

	/// <summary>
	/// Lower activation level that arms the long pattern.
	/// </summary>
	public decimal LowerActivation
	{
		get => _lowerActivation.Value;
		set => _lowerActivation.Value = value;
	}

	/// <summary>
	/// Short EMA used for position management.
	/// </summary>
	public int EmaOneLength
	{
		get => _emaOneLength.Value;
		set => _emaOneLength.Value = value;
	}

	/// <summary>
	/// Second EMA used for position management.
	/// </summary>
	public int EmaTwoLength
	{
		get => _emaTwoLength.Value;
		set => _emaTwoLength.Value = value;
	}

	/// <summary>
	/// SMA length used for position management.
	/// </summary>
	public int SmaLength
	{
		get => _smaLength.Value;
		set => _smaLength.Value = value;
	}

	/// <summary>
	/// Slow EMA used for position management.
	/// </summary>
	public int EmaFourLength
	{
		get => _emaFourLength.Value;
		set => _emaFourLength.Value = value;
	}

	/// <summary>
	/// Minimum unrealized PnL before scaling out (in price units * volume).
	/// </summary>
	public decimal ProfitThreshold
	{
		get => _profitThreshold.Value;
		set => _profitThreshold.Value = value;
	}

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

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

		var macd = new MovingAverageConvergenceDivergence();
		macd.ShortMa.Length = FastEmaLength;
		macd.LongMa.Length = SlowEmaLength;

		var emaOne = new ExponentialMovingAverage { Length = EmaOneLength };
		var emaTwo = new ExponentialMovingAverage { Length = EmaTwoLength };
		var sma = new SimpleMovingAverage { Length = SmaLength };
		var emaFour = new ExponentialMovingAverage { Length = EmaFourLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(macd, emaOne, emaTwo, sma, emaFour, ProcessCandle)
		.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, macd);
			DrawIndicator(area, emaOne);
			DrawIndicator(area, emaTwo);
			DrawIndicator(area, sma);
			DrawIndicator(area, emaFour);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal macdMain, decimal emaOne, decimal emaTwo, decimal sma, decimal emaFour)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_emaTwoValue = emaTwo;
		_smaValue = sma;
		_emaFourValue = emaFour;

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			CacheMacd(macdMain);
			_previousCandle = candle;
			return;
		}

		if (_previousMacd is null || _olderMacd is null)
		{
			CacheMacd(macdMain);
			_previousCandle = candle;
			return;
		}

		var macdPrev = _previousMacd.Value;
		var macdPrev2 = _olderMacd.Value;

		EvaluateSellPattern(macdMain, macdPrev, macdPrev2);
		EvaluateBuyPattern(macdMain, macdPrev, macdPrev2);
		ManageOpenPosition(candle);

		CacheMacd(macdMain);
		_previousCandle = candle;
	}

	private void EvaluateSellPattern(decimal macdCurrent, decimal macdPrevious, decimal macdPrevious2)
	{
		if (macdCurrent > UpperActivation)
		_isAboveUpperActivation = true;

		if (_isAboveUpperActivation && macdCurrent < macdPrevious && macdPrevious > macdPrevious2 && macdPrevious > _firstUpperPeak && !_firstUpperDropConfirmed)
		_firstUpperPeak = macdPrevious;

		if (_firstUpperPeak > 0m && macdCurrent < UpperThreshold)
		_firstUpperDropConfirmed = true;

		if (macdCurrent < UpperActivation)
		{
			ResetSellPattern();
			return;
		}

		if (_firstUpperDropConfirmed && macdCurrent > UpperThreshold && macdCurrent < macdPrevious && macdPrevious > macdPrevious2 && macdPrevious > _firstUpperPeak && macdPrevious > _secondUpperPeak && !_secondUpperDropConfirmed)
		_secondUpperPeak = macdPrevious;

		if (_secondUpperPeak > 0m && macdCurrent < UpperThreshold)
		_secondUpperDropConfirmed = true;

		if (_secondUpperDropConfirmed && macdCurrent < UpperThreshold && macdPrevious < UpperThreshold && macdPrevious2 < UpperThreshold && macdCurrent < macdPrevious && macdPrevious > macdPrevious2 && macdPrevious < _secondUpperPeak)
		_sellReady = true;

		if (!_sellReady)
		return;

		EnterShort();
	}

	private void EvaluateBuyPattern(decimal macdCurrent, decimal macdPrevious, decimal macdPrevious2)
	{
		if (macdCurrent < LowerActivation)
		_isBelowLowerActivation = true;

		if (_isBelowLowerActivation && macdCurrent > macdPrevious && macdPrevious < macdPrevious2 && macdPrevious < _firstLowerTrough && !_firstLowerRiseConfirmed)
		_firstLowerTrough = macdPrevious;

		if (_firstLowerTrough < 0m && macdCurrent > LowerThreshold)
		_firstLowerRiseConfirmed = true;

		if (macdCurrent > LowerActivation)
		{
			ResetBuyPattern();
			return;
		}

		if (_firstLowerRiseConfirmed && macdCurrent < LowerThreshold && macdCurrent > macdPrevious && macdPrevious < macdPrevious2 && macdPrevious < _firstLowerTrough && macdPrevious < _secondLowerTrough && !_secondLowerRiseConfirmed)
		_secondLowerTrough = macdPrevious;

		if (_secondLowerTrough < 0m && macdCurrent > LowerThreshold)
		_secondLowerRiseConfirmed = true;

		if (_secondLowerRiseConfirmed && macdCurrent > LowerThreshold && macdPrevious > LowerThreshold && macdPrevious2 > LowerThreshold && macdCurrent > macdPrevious && macdPrevious < macdPrevious2 && macdPrevious > _secondLowerTrough)
		_buyReady = true;

		if (!_buyReady)
		return;

		EnterLong();
	}

	private void EnterShort()
	{
		var currentPosition = Position;
		var flattenVolume = currentPosition > 0m ? currentPosition : 0m;
		if (flattenVolume > 0m)
			SellMarket(flattenVolume);

		var entryVolume = Volume + Math.Max(0m, Position);
		if (entryVolume <= 0m)
		{
			ResetSellPattern();
			_sellReady = false;
			return;
		}

		SellMarket(entryVolume);
		_entryPrice = _previousCandle?.ClosePrice ?? 0m;
		_initialShortPosition = Math.Abs(Position);
		_shortScaleStage = 0;
		_longScaleStage = 0;
		_sellReady = false;
		ResetSellPattern();
		ResetBuyPattern();
	}

	private void EnterLong()
	{
		var currentPosition = Position;
		var flattenVolume = currentPosition < 0m ? -currentPosition : 0m;
		if (flattenVolume > 0m)
			BuyMarket(flattenVolume);

		var entryVolume = Volume + Math.Max(0m, -Position);
		if (entryVolume <= 0m)
		{
			ResetBuyPattern();
			_buyReady = false;
			return;
		}

		BuyMarket(entryVolume);
		_entryPrice = _previousCandle?.ClosePrice ?? 0m;
		_initialLongPosition = Math.Max(0m, Position);
		_longScaleStage = 0;
		_shortScaleStage = 0;
		_buyReady = false;
		ResetBuyPattern();
		ResetSellPattern();
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position == 0m)
		{
			_longScaleStage = 0;
			_shortScaleStage = 0;
			_initialLongPosition = 0m;
			_initialShortPosition = 0m;
			return;
		}

		var previousCandle = _previousCandle;
		if (previousCandle is null)
		return;

		var profitThreshold = ProfitThreshold;
		if (profitThreshold <= 0m)
		return;

		var unrealized = GetUnrealizedPnL(candle);
		if (unrealized < profitThreshold)
		return;

		if (Position > 0m)
		{
			if (_emaTwoValue is decimal emaTwo && previousCandle.ClosePrice > emaTwo && _longScaleStage == 0)
			{
				var volume = Math.Min(Position, _initialLongPosition / 3m);
				if (volume > 0m)
				{
					SellMarket(volume);
					_longScaleStage = 1;
				}
			}

			if (_smaValue is decimal sma && _emaFourValue is decimal emaFour && previousCandle.HighPrice > (sma + emaFour) / 2m && _longScaleStage == 1)
			{
				var volume = Math.Min(Position, _initialLongPosition / 2m);
				if (volume > 0m)
				{
					SellMarket(volume);
					_longScaleStage = 2;
				}
			}
		}
		else if (Position < 0m)
		{
			var shortPosition = -Position;
			if (_emaTwoValue is decimal emaTwo && previousCandle.ClosePrice < emaTwo && _shortScaleStage == 0)
			{
				var volume = Math.Min(shortPosition, _initialShortPosition / 3m);
				if (volume > 0m)
				{
					BuyMarket(volume);
					_shortScaleStage = 1;
				}
			}

			if (_smaValue is decimal sma && _emaFourValue is decimal emaFour && previousCandle.LowPrice < (sma + emaFour) / 2m && _shortScaleStage == 1)
			{
				var volume = Math.Min(shortPosition, _initialShortPosition / 2m);
				if (volume > 0m)
				{
					BuyMarket(volume);
					_shortScaleStage = 2;
				}
			}
		}
	}

	private void CacheMacd(decimal macdValue)
	{
		_olderMacd = _previousMacd;
		_previousMacd = macdValue;
	}

	private decimal GetUnrealizedPnL(ICandleMessage candle)
	{
		if (Position == 0m)
			return 0m;

		if (_entryPrice == 0m)
			return 0m;

		var diff = candle.ClosePrice - _entryPrice;
		return diff * Position;
	}

	private void ResetSellPattern()
	{
		_isAboveUpperActivation = false;
		_firstUpperDropConfirmed = false;
		_secondUpperDropConfirmed = false;
		_sellReady = false;
		_firstUpperPeak = 0m;
		_secondUpperPeak = 0m;
	}

	private void ResetBuyPattern()
	{
		_isBelowLowerActivation = false;
		_firstLowerRiseConfirmed = false;
		_secondLowerRiseConfirmed = false;
		_buyReady = false;
		_firstLowerTrough = 0m;
		_secondLowerTrough = 0m;
	}

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

		_previousMacd = null;
		_olderMacd = null;
		_entryPrice = 0m;

		_isAboveUpperActivation = false;
		_firstUpperDropConfirmed = false;
		_secondUpperDropConfirmed = false;
		_sellReady = false;
		_firstUpperPeak = 0m;
		_secondUpperPeak = 0m;

		_isBelowLowerActivation = false;
		_firstLowerRiseConfirmed = false;
		_secondLowerRiseConfirmed = false;
		_buyReady = false;
		_firstLowerTrough = 0m;
		_secondLowerTrough = 0m;

		_emaTwoValue = null;
		_smaValue = null;
		_emaFourValue = null;

		_previousCandle = null;

		_longScaleStage = 0;
		_shortScaleStage = 0;
		_initialLongPosition = 0m;
		_initialShortPosition = 0m;
	}
}