Открыть на GitHub

MACD с фильтром по нулевой линии

Обзор

Стратегия «MACD с фильтром по нулевой линии» представляет собой порт MetaTrader 4-советника Robot_MACD_12.26.9. Исходный робот отслеживает пересечения линии MACD и сигнальной линии, но разрешает открывать длинные позиции только тогда, когда обе линии остаются ниже нуля, а короткие позиции — только при нахождении обеих линий выше нуля. Реализация на StockSharp сохраняет эти фильтры, добавляет встроенную систему управления риском (контроль баланса портфеля и фиксированный тейк-профит в пунктах) и вынесена в параметры, пригодные для оптимизации.

Стратегия работает с завершёнными свечами выбранного таймфрейма. Индикатор MovingAverageConvergenceDivergenceSignal подключается через BindEx, что позволяет получить значения MACD и сигнальной линии в одном колбэке без ручного обращения к GetValue.

Логика стратегии

Расчёт индикаторов

  • Линия MACD — разница между быстрой и медленной EMA (по умолчанию 12 и 26 периодов).
  • Сигнальная линия — EMA от MACD (по умолчанию 9 периодов).
  • Фильтр по нулю — проверяет знак обоих значений и решает, допускается ли вход по текущему пересечению.

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

  • Покупка
    • MACD пересекает сигнал снизу вверх (MACD[t-1] < Signal[t-1], MACD[t] > Signal[t]).
    • После пересечения обе линии остаются ниже нуля.
    • Текущая позиция должна быть пустой или короткой; при наличии шорта он закрывается и только затем допускается новый вход.
    • Дополнительно проверяется баланс портфеля: CurrentValue должен быть не меньше MinimumBalancePerVolume * LotVolume.
  • Продажа
    • MACD пересекает сигнал сверху вниз (MACD[t-1] > Signal[t-1], MACD[t] < Signal[t]).
    • Обе линии выше нулевой отметки.
    • Позиция должна быть пустой или длинной; лонги закрываются до отправки новой продажи.
    • Балансовый фильтр действует симметрично.

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

  • Пересечение в противоположную сторону — закрывает текущую позицию рыночной заявкой, полностью повторяя поведение оригинала.
  • Фиксированный тейк-профит — задаётся в пунктах и запускается через StartProtection, аналог параметра TakeProfit в МТ4.

Управление риском

  • Объём сделкиLotVolume соответствует параметру Lots в МТ4 и используется для каждой заявки.
  • Фильтр балансаMinimumBalancePerVolume задаёт минимальное значение портфеля на единицу объёма. При недостатке средств стратегия выводит диагностическое сообщение и пропускает сигнал.
  • Контроль данных — обработка ведётся только по закрытым свечам и после того, как IsFormedAndOnlineAndAllowTrading() подтверждает готовность соединения и индикаторов.

Параметры

Параметр Описание
FastPeriod Период быстрой EMA в расчёте MACD.
SlowPeriod Период медленной EMA в расчёте MACD.
SignalPeriod Период сглаживания сигнальной линии.
TakeProfitPoints Расстояние до тейк-профита в пунктах (0 — отключить).
LotVolume Объём заявки, эквивалентный лотам в MT4.
MinimumBalancePerVolume Требуемый баланс портфеля на единицу объёма (0 — фильтр отключён).
CandleType Таймфрейм свечей, используемых для расчётов.

Дополнительно

  • Все комментарии в исходном коде написаны на английском языке согласно общим требованиям.
  • Python-версии стратегии не существует, реализована только C#-модификация.
  • Для максимального сходства с MT4 рекомендуется выбрать тот же таймфрейм и объём, что использовались в исходном советнике.
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>
/// Port of the MetaTrader 4 expert advisor Robot_MACD_12.26.9.
/// Trades MACD signal-line crossovers, but only enters longs while both lines stay below zero and shorts while they stay above zero.
/// Includes an optional balance filter and a fixed take-profit expressed in instrument points.
/// </summary>
public class MacdZeroFilteredCrossStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _signalPeriod;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _lotVolume;
	private readonly StrategyParam<decimal> _minimumBalancePerVolume;
	private readonly StrategyParam<DataType> _candleType;

	private MovingAverageConvergenceDivergenceSignal _macd = null!;
	private decimal? _previousMacd;
	private decimal? _previousSignal;

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

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

	/// <summary>
	/// Signal line smoothing length for MACD.
	/// </summary>
	public int SignalPeriod
	{
		get => _signalPeriod.Value;
		set => _signalPeriod.Value = value;
	}

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

	/// <summary>
	/// Base trading volume that mirrors the "Lots" setting in the original robot.
	/// </summary>
	public decimal LotVolume
	{
		get => _lotVolume.Value;
		set => _lotVolume.Value = value;
	}

	/// <summary>
	/// Minimum account value required per traded volume unit before opening new positions.
	/// </summary>
	public decimal MinimumBalancePerVolume
	{
		get => _minimumBalancePerVolume.Value;
		set => _minimumBalancePerVolume.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the strategy.
	/// </summary>
	public MacdZeroFilteredCrossStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 12)
			.SetGreaterThanZero()
			.SetDisplay("Fast Period", "Short EMA period for MACD", "MACD")
			
			.SetOptimize(6, 18, 1);

		_slowPeriod = Param(nameof(SlowPeriod), 26)
			.SetGreaterThanZero()
			.SetDisplay("Slow Period", "Long EMA period for MACD", "MACD")
			
			.SetOptimize(20, 40, 2);

		_signalPeriod = Param(nameof(SignalPeriod), 9)
			.SetGreaterThanZero()
			.SetDisplay("Signal Period", "Signal line length for MACD", "MACD")
			
			.SetOptimize(6, 12, 1);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 300m)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Fixed take-profit distance in price points", "Risk Management");

		_lotVolume = Param(nameof(LotVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Lot Volume", "Trading volume per order", "Trading")
			.SetOptimize(1m, 5m, 1m);

		_minimumBalancePerVolume = Param(nameof(MinimumBalancePerVolume), 1000m)
			.SetNotNegative()
			.SetDisplay("Balance per Volume", "Required balance per volume unit before opening trades", "Risk Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe that drives MACD calculations", "General");
	}

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

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

		_previousMacd = null;
		_previousSignal = null;
	}

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

		_macd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = FastPeriod },
				LongMa = { Length = SlowPeriod },
			},
			SignalMa = { Length = SignalPeriod }
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_macd, ProcessCandle)
			.Start();

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

		if (TakeProfitPoints > 0m)
		{
			StartProtection(new Unit(TakeProfitPoints, UnitTypes.Absolute), null);
		}

		base.OnStarted2(time);
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdValue)
	{
		// Work only with completed candles to avoid premature signals.
		if (candle.State != CandleStates.Finished)
			return;

		// Skip processing when the strategy is not ready or trading is disabled.
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var typed = (MovingAverageConvergenceDivergenceSignalValue)macdValue;

		// Ensure both MACD and signal components are available before calculating.
		if (typed.Macd is not decimal macdLine || typed.Signal is not decimal signalLine)
			return;

		if (_previousMacd is decimal prevMacd && _previousSignal is decimal prevSignal)
		{
			var crossUp = prevMacd < prevSignal && macdLine > signalLine;
			var crossDown = prevMacd > prevSignal && macdLine < signalLine;

			// Close existing long position when MACD crosses below the signal line.
			if (crossDown && Position > 0m)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				_previousMacd = macdLine;
				_previousSignal = signalLine;
				return;
			}

			// Close existing short position when MACD crosses above the signal line.
			if (crossUp && Position < 0m)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				_previousMacd = macdLine;
				_previousSignal = signalLine;
				return;
			}

			// Enter long only when the crossover happens below zero (momentum still negative).
			if (crossUp && macdLine < 0m && signalLine < 0m && Position <= 0m && HasRequiredBalance())
			{
				var volume = LotVolume;
				BuyMarket(volume);
			}

			// Enter short only when the crossover happens above zero (momentum still positive).
			else if (crossDown && macdLine > 0m && signalLine > 0m && Position >= 0m && HasRequiredBalance())
			{
				var volume = LotVolume;
				SellMarket(volume);
			}
		}

		_previousMacd = macdLine;
		_previousSignal = signalLine;
	}

	private bool HasRequiredBalance()
	{
		// If portfolio information is not available, assume requirements are met.
		var balance = Portfolio?.CurrentValue;
		if (balance is null)
			return true;

		var required = MinimumBalancePerVolume * LotVolume;
		if (required <= 0m)
			return true;

		if (balance.Value >= required)
			return true;

		return false;
	}
}