Открыть на GitHub

Стратегия MAMACD

Общая информация

Стратегия представляет собой прямую конвертацию эксперта MetaTrader 5 MAMACD (редакция barabashkakvn) из каталога MQL/19334 на высокоуровневый API StockSharp. Логика использует две линейно-взвешенные скользящие средние (LWMA) по минимумам свечей, быструю экспоненциальную среднюю (EMA) по закрытиям и фильтр по основной линии MACD. Обработка выполняется только на закрытых свечах, а флаги готовности повторяют механику оригинального советника: для нового входа быстрая EMA сначала должна выйти за пределы канала из двух LWMA.

Индикаторы

  • LWMA #1 (минимумы, по умолчанию 85) — медленный фильтр направления тренда.
  • LWMA #2 (минимумы, по умолчанию 75) — немного быстрее, подтверждает канал.
  • EMA-триггер (закрытия, по умолчанию 5) — короткая EMA, которая должна пересечь обе LWMA для активации сигнала.
  • Основная линия MACD (быстрая 15, медленная 26) — фильтр импульса; для покупок требуется положительный или растущий MACD, для продаж — отрицательный или снижающийся.

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

  1. Стратегия игнорирует незавершённые свечи (CandleStates.Finished).
  2. Когда EMA-триггер опускается ниже обеих LWMA, устанавливается флаг готовности к покупке. Сделка на покупку открывается после возвращения EMA выше обеих LWMA и подтверждения по MACD (значение > 0 либо выше предыдущего). Одновременно может существовать только одна длинная позиция.
  3. Когда EMA-триггер поднимается выше обеих LWMA, устанавливается флаг готовности к продаже. Короткая позиция открывается после возврата EMA ниже LWMA при условии, что MACD < 0 или меньше предыдущего значения. Одновременно разрешена только одна короткая позиция.
  4. Объём заявок берётся из свойства Volume. При смене направления алгоритм сначала закрывает противоположную позицию.

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

  • Дополнительных правил выхода в оригинальном советнике нет. Защитные ордера реализованы через StartProtection со стоп-лоссом и тейк-профитом в пунктах. Достижение любого из уровней автоматически закрывает позицию.

Параметры

Имя Описание
FirstLowMaLength Период первой LWMA по минимумам (значение по умолчанию 85).
SecondLowMaLength Период второй LWMA по минимумам (значение по умолчанию 75).
TriggerEmaLength Период быстрой EMA по закрытиям (значение по умолчанию 5).
MacdFastLength Период быстрой EMA в расчёте MACD (значение по умолчанию 15).
MacdSlowLength Период медленной EMA в расчёте MACD (значение по умолчанию 26).
StopLossPips Расстояние стоп-лосса в пунктах; 0 отключает защиту (значение по умолчанию 15).
TakeProfitPips Расстояние тейк-профита в пунктах; 0 отключает (значение по умолчанию 15).
CandleType Тип свечей/таймфрейм для расчётов (по умолчанию 1 час).

Особенности реализации

  • Размер пункта вычисляется из Security.PriceStep. Для инструментов с 3 и 5 знаками после запятой шаг умножается на 10, что соответствует логике MT5.
  • История MACD соответствует эксперту: первое валидное значение сохраняется и используется как база для следующей свечи перед проверкой условий.
  • Флаги _readyForLong и _readyForShort повторяют переменные startb и starts, требуя выхода EMA за границы LWMA перед каждым новым входом.
  • На график выводятся свечи, обе LWMA, EMA-триггер и отдельная область с MACD для визуальной проверки.

Соответствие элементов MT5

Элемент MT5 Эквивалент в StockSharp
iMA по минимумам/закрытиям WeightedMovingAverage (для минимумов) и ExponentialMovingAverage (для закрытий)
iMACD (основная линия) MovingAverageConvergenceDivergence
Подсчёт позиций buy/sell Проверка знака Position и объёма при BuyMarket / SellMarket
Magic number, проскальзывание Не требуются в высокоуровневом API
Стоп-лосс / тейк-профит в пунктах StartProtection с абсолютными смещениями цены, рассчитанными из размера пункта

Конвертация сохраняет торговую логику оригинала и использует инфраструктуру StockSharp для индикаторов, подписок и управления рисками.

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>
/// MAMACD trend-following strategy converted from the original MetaTrader 5 expert advisor.
/// Combines two low-price LWMA filters, a fast EMA trigger, and a MACD confirmation filter.
/// </summary>
public class MamAcdStrategy : Strategy
{
	private readonly StrategyParam<int> _firstLowMaLength;
	private readonly StrategyParam<int> _secondLowMaLength;
	private readonly StrategyParam<int> _triggerEmaLength;
	private readonly StrategyParam<int> _macdFastLength;
	private readonly StrategyParam<int> _macdSlowLength;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<DataType> _candleType;

	private WeightedMovingAverage _firstLowMa = null!;
	private WeightedMovingAverage _secondLowMa = null!;
	private ExponentialMovingAverage _triggerEma = null!;
	private MovingAverageConvergenceDivergence _macd = null!;

	private decimal? _previousMacd;
	private bool _readyForLong;
	private bool _readyForShort;
	private decimal _pipSize;

	/// <summary>
	/// Period of the first LWMA calculated on low prices.
	/// </summary>
	public int FirstLowMaLength
	{
		get => _firstLowMaLength.Value;
		set => _firstLowMaLength.Value = value;
	}

	/// <summary>
	/// Period of the second LWMA calculated on low prices.
	/// </summary>
	public int SecondLowMaLength
	{
		get => _secondLowMaLength.Value;
		set => _secondLowMaLength.Value = value;
	}

	/// <summary>
	/// Period of the fast EMA calculated on close prices.
	/// </summary>
	public int TriggerEmaLength
	{
		get => _triggerEmaLength.Value;
		set => _triggerEmaLength.Value = value;
	}

	/// <summary>
	/// Fast EMA period of the MACD filter.
	/// </summary>
	public int MacdFastLength
	{
		get => _macdFastLength.Value;
		set => _macdFastLength.Value = value;
	}

	/// <summary>
	/// Slow EMA period of the MACD filter.
	/// </summary>
	public int MacdSlowLength
	{
		get => _macdSlowLength.Value;
		set => _macdSlowLength.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips. Set to zero to disable protective stop.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips. Set to zero to disable take-profit.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

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

	/// <summary>
	/// Initializes <see cref="MamAcdStrategy"/> with default parameters.
	/// </summary>
	public MamAcdStrategy()
	{
		_firstLowMaLength = Param(nameof(FirstLowMaLength), 85)
		.SetGreaterThanZero()
		.SetDisplay("LWMA #1", "Length of the first LWMA on lows", "Indicators")
		;

		_secondLowMaLength = Param(nameof(SecondLowMaLength), 75)
		.SetGreaterThanZero()
		.SetDisplay("LWMA #2", "Length of the second LWMA on lows", "Indicators")
		;

		_triggerEmaLength = Param(nameof(TriggerEmaLength), 5)
		.SetGreaterThanZero()
		.SetDisplay("Trigger EMA", "Length of the EMA on closes", "Indicators")
		;

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

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

		_stopLossPips = Param(nameof(StopLossPips), 500)
		.SetNotNegative()
		.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk management");

		_takeProfitPips = Param(nameof(TakeProfitPips), 500)
		.SetNotNegative()
		.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
	}

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

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

		_previousMacd = null;
		_readyForLong = false;
		_readyForShort = false;
		_pipSize = 0m;
	}

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

		_firstLowMa = new WeightedMovingAverage { Length = FirstLowMaLength };
		_secondLowMa = new WeightedMovingAverage { Length = SecondLowMaLength };
		_triggerEma = new EMA { Length = TriggerEmaLength };
		_macd = new MovingAverageConvergenceDivergence
		{
			ShortMa = { Length = MacdFastLength },
			LongMa = { Length = MacdSlowLength }
		};

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

		_pipSize = CalculatePipSize();

		var takeProfit = TakeProfitPips > 0 ? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute) : new Unit();
		var stopLoss = StopLossPips > 0 ? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute) : new Unit();
		StartProtection(takeProfit, stopLoss);

		var priceArea = CreateChartArea();
		if (priceArea != null)
		{
			DrawCandles(priceArea, subscription);
			DrawIndicator(priceArea, _firstLowMa);
			DrawIndicator(priceArea, _secondLowMa);
			DrawIndicator(priceArea, _triggerEma);
			DrawOwnTrades(priceArea);

			var macdArea = CreateChartArea();
			if (macdArea != null)
			{
				macdArea.Title = "MACD";
				DrawIndicator(macdArea, _macd);
			}
		}
	}

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

		// Feed indicator chain: LWMAs work on low prices, EMA and MACD on closes.
		var firstLowValue = _firstLowMa.Process(new DecimalIndicatorValue(_firstLowMa, candle.LowPrice, candle.OpenTime) { IsFinal = true });
		var secondLowValue = _secondLowMa.Process(new DecimalIndicatorValue(_secondLowMa, candle.LowPrice, candle.OpenTime) { IsFinal = true });
		var triggerValue = _triggerEma.Process(new DecimalIndicatorValue(_triggerEma, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var macdValue = _macd.Process(new DecimalIndicatorValue(_macd, candle.ClosePrice, candle.OpenTime) { IsFinal = true });

		// Wait for all indicators to collect enough history.
		if (!_firstLowMa.IsFormed || !_secondLowMa.IsFormed || !_triggerEma.IsFormed || !_macd.IsFormed)
		{
			if (_macd.IsFormed)
			_previousMacd = macdValue.ToDecimal();
			return;
		}

		// indicators already checked above

		var ma1 = firstLowValue.ToDecimal();
		var ma2 = secondLowValue.ToDecimal();
		var ma3 = triggerValue.ToDecimal();
		var macd = macdValue.ToDecimal();

		// Store the first complete MACD observation before evaluating signals.
		if (_previousMacd is null)
		{
			_previousMacd = macd;
			return;
		}

		// Skip calculations when MACD lacks momentum confirmation just like the original EA.
		if (macd == 0m || _previousMacd.Value == 0m)
		{
			_previousMacd = macd;
			return;
		}

		// Track reset flags: EMA must dip below both LWMAs to prepare for a new long, and rise above them for shorts.
		if (ma3 < ma1 && ma3 < ma2)
		_readyForLong = true;

		if (ma3 > ma1 && ma3 > ma2)
		_readyForShort = true;

		var macdImproving = macd > _previousMacd.Value;
		var longSignal = ma3 > ma1 && ma3 > ma2 && _readyForLong && (macd > 0m || macdImproving);
		var shortSignal = ma3 < ma1 && ma3 < ma2 && _readyForShort && (macd < 0m || !macdImproving);

		if (longSignal && Position <= 0)
		{
			var volume = Volume + (Position < 0 ? -Position : 0m);
			if (volume > 0)
			{
				BuyMarket();
				_readyForLong = false;
			}
		}
		else if (shortSignal && Position >= 0)
		{
			var volume = Volume + (Position > 0 ? Position : 0m);
			if (volume > 0)
			{
				SellMarket();
				_readyForShort = false;
			}
		}

		_previousMacd = macd;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;

		if (step <= 0m)
		return 1m;

		var decimals = CountDecimalPlaces(step);

		return decimals == 3 || decimals == 5 ? step * 10m : step;
	}

	private static int CountDecimalPlaces(decimal value)
	{
		value = Math.Abs(value);

		var count = 0;
		while (value != Math.Truncate(value) && count < 10)
		{
			value *= 10m;
			count++;
		}

		return count;
	}
}