Ver no GitHub

MACD Zero Filter Take Profit Strategy

Overview

This strategy replicates the original MetaTrader 5 expert "Robot_MACD" that trades MACD signal line crossovers with additional zero-line filters. It operates on a single instrument and looks for momentum reversals confirmed by the position of the MACD line relative to zero. A fixed-distance take profit is attached to every order, mirroring the point-based profit target from the original implementation.

Data and Indicators

  • Primary data: single candle subscription (default 5-minute timeframe). The timeframe can be changed with the CandleType parameter to suit the traded market.
  • Indicators:
    • MovingAverageConvergenceDivergenceSignal (MACD + signal + histogram). The defaults are 12/26 EMAs with a 9-period signal line, matching the MQL parameters.

Trading Logic

  1. Wait for the MACD calculation to provide both current and previous values of the MACD and signal lines.
  2. Identify bullish and bearish crossovers:
    • Bullish crossover: previous MACD ≤ previous signal and current MACD > current signal.
    • Bearish crossover: previous MACD ≥ previous signal and current MACD < current signal.
  3. Position management:
    • Close a long position when a bearish crossover appears.
    • Close a short position when a bullish crossover appears.
  4. Entry conditions (only when no position is open and there is sufficient capital):
    • Enter long on a bullish crossover while both MACD and signal remain below zero.
    • Enter short on a bearish crossover while both MACD and signal remain above zero.
  5. Attach a fixed take profit at order registration time by calling StartProtection with an absolute distance measured in price points. The distance equals the configured point value multiplied by the security's price step.

Risk Management

  • Every order has an attached take profit defined by TakeProfitPoints. There is no stop-loss in the base logic, preserving parity with the source EA.
  • The strategy checks whether the portfolio value is at least MinimumCapitalPerVolume * VolumePerTrade before placing a new order. This emulates the free-margin guard (FreeMargin() < 1000 * Lots) from the MQL version.

Parameters

Parameter Description Default
MacdFast Fast EMA period for MACD. 12
MacdSlow Slow EMA period for MACD. 26
MacdSignal Signal line smoothing period. 9
TakeProfitPoints Take profit distance expressed in price points. 300
VolumePerTrade Trading volume (lots) used for each entry. 1
MinimumCapitalPerVolume Minimum portfolio value required per traded lot. 1000
CandleType Candle type (timeframe) used to feed the MACD indicator. 5-minute candles

Implementation Notes

  • Orders are executed with BuyMarket/SellMarket, identical to the EA that used market orders via CTrade.
  • The zero-line filters guard against entering trades in the opposite half of the MACD histogram, just as in the MQL script.
  • The portfolio value check relies on Portfolio.CurrentValue. If the trading environment does not supply this value the guard automatically passes, which keeps the strategy usable for simulated accounts.
  • The chart drawing section plots candles, the MACD indicator, and trade markers when a chart area is available in the host application.
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>
/// MACD cross strategy with zero line filter and fixed take profit.
/// </summary>
public class MacdZeroFilterTakeProfitStrategy : Strategy
{
	private readonly StrategyParam<int> _macdFast;
	private readonly StrategyParam<int> _macdSlow;
	private readonly StrategyParam<int> _macdSignal;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<decimal> _volumePerTrade;
	private readonly StrategyParam<decimal> _minimumCapitalPerVolume;
	private readonly StrategyParam<DataType> _candleType;

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

	/// <summary>
	/// Initializes a new instance of the <see cref="MacdZeroFilterTakeProfitStrategy"/> class.
	/// </summary>
	public MacdZeroFilterTakeProfitStrategy()
	{
		_macdFast = Param(nameof(MacdFast), 12)
			.SetGreaterThanZero()
			.SetDisplay("MACD Fast Period", "Fast EMA period used by MACD", "Indicators")
			
			.SetOptimize(8, 20, 2);

		_macdSlow = Param(nameof(MacdSlow), 26)
			.SetGreaterThanZero()
			.SetDisplay("MACD Slow Period", "Slow EMA period used by MACD", "Indicators")
			
			.SetOptimize(20, 40, 2);

		_macdSignal = Param(nameof(MacdSignal), 9)
			.SetGreaterThanZero()
			.SetDisplay("MACD Signal Period", "Signal smoothing period for MACD", "Indicators")
			
			.SetOptimize(6, 15, 1);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 300)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (points)", "Take profit distance expressed in price points", "Risk Management")
			
			.SetOptimize(100, 600, 50);

		_volumePerTrade = Param(nameof(VolumePerTrade), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Number of lots to trade on each entry", "General")
			
			.SetOptimize(0.5m, 5m, 0.5m);

		_minimumCapitalPerVolume = Param(nameof(MinimumCapitalPerVolume), 1000m)
			.SetGreaterThanZero()
			.SetDisplay("Capital per Volume", "Minimum portfolio value required per traded lot", "Risk Management")
			
			.SetOptimize(500m, 5000m, 500m);

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

	/// <summary>
	/// Fast EMA period used by MACD.
	/// </summary>
	public int MacdFast
	{
		get => _macdFast.Value;
		set => _macdFast.Value = value;
	}

	/// <summary>
	/// Slow EMA period used by MACD.
	/// </summary>
	public int MacdSlow
	{
		get => _macdSlow.Value;
		set => _macdSlow.Value = value;
	}

	/// <summary>
	/// Signal smoothing period for MACD.
	/// </summary>
	public int MacdSignal
	{
		get => _macdSignal.Value;
		set => _macdSignal.Value = value;
	}

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

	/// <summary>
	/// Number of lots to trade on each entry.
	/// </summary>
	public decimal VolumePerTrade
	{
		get => _volumePerTrade.Value;
		set => _volumePerTrade.Value = value;
	}

	/// <summary>
	/// Minimum portfolio value required per traded lot.
	/// </summary>
	public decimal MinimumCapitalPerVolume
	{
		get => _minimumCapitalPerVolume.Value;
		set => _minimumCapitalPerVolume.Value = value;
	}

	/// <summary>
	/// Timeframe for MACD calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <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)
	{
		base.OnStarted2(time);

		Volume = VolumePerTrade;

		_macd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = MacdFast },
				LongMa = { Length = MacdSlow },
			},
			SignalMa = { Length = MacdSignal }
		};

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

		var step = Security?.PriceStep ?? 1m;
		StartProtection(
			takeProfit: new Unit(TakeProfitPoints * step, UnitTypes.Absolute),
			stopLoss: new Unit(0m),
			useMarketOrders: true);

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

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

		// Ensure the strategy is ready to trade and has a working connector.
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (macdValue is not MovingAverageConvergenceDivergenceSignalValue macdTyped)
			return;

		if (macdTyped.Macd is not decimal macd || macdTyped.Signal is not decimal signal)
			return;

		if (_previousMacd is null || _previousSignal is null)
		{
			_previousMacd = macd;
			_previousSignal = signal;
			return;
		}

		var previousMacd = _previousMacd.Value;
		var previousSignal = _previousSignal.Value;

		var crossedUp = previousMacd <= previousSignal && macd > signal;
		var crossedDown = previousMacd >= previousSignal && macd < signal;

		if (Position > 0 && crossedDown)
		{
			// Close long position on bearish crossover.
			SellMarket();
		}
		else if (Position < 0 && crossedUp)
		{
			// Close short position on bullish crossover.
			BuyMarket();
		}

		if (Position == 0)
		{
			var requiredCapital = MinimumCapitalPerVolume * VolumePerTrade;
			if (HasEnoughCapital(requiredCapital))
			{
				if (crossedUp && macd < 0m && signal < 0m)
				{
					// Enter long when MACD crosses above signal under the zero line.
					BuyMarket();
				}
				else if (crossedDown && macd > 0m && signal > 0m)
				{
					// Enter short when MACD crosses below signal above the zero line.
					SellMarket();
				}
			}
		}

		_previousMacd = macd;
		_previousSignal = signal;
	}

	private bool HasEnoughCapital(decimal requiredCapital)
	{
		var currentValue = Portfolio?.CurrentValue;

		if (currentValue is null)
			return true;

		if (currentValue.Value >= requiredCapital)
			return true;

		this.LogInfo($"Insufficient capital: available {currentValue.Value:F2}, required {requiredCapital:F2}.");
		return false;
	}
}