Открыть на GitHub

Macd Pattern Trader v03 (порт на StockSharp)

Общее описание

Macd Pattern Trader v03 — это стратегия StockSharp, перенесённая с советника MetaTrader 4 MacdPatternTraderv03. Исходный алгоритм ищет на основной линии MACD тройное формирование «пик/впадина» и частично фиксирует прибыль по сигналам скользящих средних. Версия на C# полностью повторяет эту идею, используя подписки StockSharp на свечи, индикаторы и рыночные заявки.

Стратегия ориентирована на поиск истощения тренда на ликвидных валютных парах. По умолчанию используется таймфрейм 30 минут (как и в оригинале) и объём 1 контракт/лот.

Индикаторы и данные

  • MACD (EMA 5/13, сигнал 1) — основа паттерна, анализируется только главная линия, без сигнальной.
  • EMA(7) и EMA(21) — быстрые средние для менеджмента позиции.
  • SMA(98) и EMA(365) — медленные фильтры, формирующие условия второй ступени фиксации.

Подписка на свечи выполняется через SubscribeCandles, а индикаторы связываются с обработчиками методом Bind/BindEx. Расчёт проводится только по закрывшимся свечам.

Правила входа

Короткая позиция

  1. Активация происходит, когда MACD поднимается выше уровня Upper Activation (0.0030 по умолчанию).
  2. Первый пик фиксируется, если MACD формирует локальный максимум выше двух предыдущих значений и затем падает ниже Upper Threshold (0.0045).
  3. Второй пик запоминается при повторном выходе MACD выше порога, образовании более высокого максимума и обратном возврате под порог.
  4. Паттерн подтверждён, когда третье снижение проходит при MACD ниже порога три свечи подряд, а последний максимум ниже предыдущего.
  5. Любые длинные позиции закрываются, после чего открывается шорт заданного объёма.

Длинная позиция

  1. Активация — MACD опускается ниже Lower Activation (−0.0030).
  2. Первый минимум фиксируется после локальной впадины ниже двух предыдущих значений и возврата выше Lower Threshold (−0.0045).
  3. Второй минимум — повторный уход под порог с более глубокой впадиной и возвращение выше порога.
  4. Паттерн подтверждён, если третье повышение проходит при MACD выше порога три свечи подряд и последняя впадина выше предыдущей.
  5. Шорт закрывается, открывается лонг объёмом Volume.

Логика полностью воспроизводит флаги stops, stops1, aop_ok* из MQ4 и сбрасывает состояния при возврате MACD в активирующую область.

Управление позицией

  • Поэтапная фиксация — при достижении незафиксированной прибыли (Close − Entry) * Position выше ProfitThreshold (по умолчанию 5 ценовых единиц) выполняются два шага:
    • Шаг 1 (лонг): закрытие предыдущей свечи выше EMA(21) — продаётся треть первоначального объёма. Для шорта условие зеркально: закрытие ниже EMA(21), покупается треть начального шорта.
    • Шаг 2 (лонг): максимум предыдущей свечи должен пробить среднее значение SMA(98) и EMA(365) — закрывается половина исходного объёма. Для шорта используется минимум свечи и покупка половины объёма.
  • Оставшаяся позиция остаётся в рынке без автоматического сопровождения, как и в оригинальном советнике.
  • Защитные заявки — MQ4-версия ставила стоп-лосс и тейк-профит по экстремумам. В StockSharp такая логика не реализована, поэтому рекомендуется дополнительно подключить StartProtection() или внешние модули риск-контроля.

Параметры

Параметр Значение по умолчанию Назначение
Volume 1 Объём входа при каждом сигнале.
CandleType 30-минутные свечи Таймфрейм для индикаторов.
FastEmaLength / SlowEmaLength 5 / 13 Периоды EMA в MACD.
UpperThreshold / LowerThreshold 0.0045 / −0.0045 Порог подтверждения паттерна.
UpperActivation / LowerActivation 0.0030 / −0.0030 Порог активации паттерна.
EmaOneLength / EmaTwoLength 7 / 21 Дополнительные EMA для визуализации и менеджмента.
SmaLength 98 Период SMA для второго шага фиксации.
EmaFourLength 365 Медленная EMA для второго шага фиксации.
ProfitThreshold 5 Минимальная незафиксированная прибыль (цена × объём) перед частичным закрытием.

Практические рекомендации

  • Убедитесь, что брокер поддерживает частичное закрытие. Стратегия повторяет шаги 1/3 и 1/2 из оригинала.
  • Стопы/тейки не создаются автоматически. Используйте StartProtection() или внешний риск-менеджер, если нужны жёсткие ограничения.
  • Порог ProfitThreshold задан в ценовых единицах умноженных на объём. Подберите значение в зависимости от тикового размера инструмента, чтобы приблизить его к «5 денежным единицам» из MQ4.
  • Паттерн эффективен на плавных трендах. На шумных инструментах количество сигналов может резко сократиться.

Отличия от MQ4

  • Используются индикаторные биндинги StockSharp вместо постоянных вызовов iMACD.
  • Нереализованная прибыль рассчитывается через Position и PositionAvgPrice, что может отличаться от OrderProfit() в MetaTrader.
  • Защитные ордера не выставляются автоматически.
  • Параметр sum_bars_bup, присутствовавший в исходнике, не использовался и здесь исключён.
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;
	}
}