MACD Zero Filtered Cross
Overview
MACD Zero Filtered Cross is a C# port of the MetaTrader 4 expert advisor Robot_MACD_12.26.9. The original robot watches for
crossovers between the MACD line and its signal line, but filters new trades so that long entries only occur while both lines
remain below the zero axis and short entries only occur while both lines remain above the axis. The StockSharp version keeps the
same crossover logic, adds risk controls that integrate with the framework (portfolio balance filtering and unified take-profit
management), and exposes every configurable value through strategy parameters that support optimization.
The strategy relies on finished candles from a configurable timeframe. Indicator values are supplied by the built-in
MovingAverageConvergenceDivergenceSignal indicator, ensuring that the strategy stays compatible with the high-level API and
respects the BindEx usage guidelines.
Strategy logic
Indicator calculation
- MACD line – difference between a fast and slow exponential moving average (default lengths: 12 and 26).
- Signal line – exponential moving average applied to the MACD line (default length: 9).
- Zero filter – the sign of both lines relative to zero determines whether a crossover can trigger a position entry.
Entry rules
- Long setup
- The MACD line must cross above the signal line (
MACD[t-1] < Signal[t-1] and MACD[t] > Signal[t]).
- Both the MACD line and the signal line must be below zero after the crossover.
- The current net position must be flat or short; existing shorts are closed immediately before attempting a long.
- An optional balance filter requires the portfolio value to exceed a configurable minimum before a new order is sent.
- Short setup
- The MACD line must cross below the signal line (
MACD[t-1] > Signal[t-1] and MACD[t] < Signal[t]).
- Both indicator lines must be above zero after the crossover.
- The current net position must be flat or long; existing longs are flattened before a new short is sent.
- The balance filter is applied symmetrically to short entries.
Exit rules
- Crossover exit – when the MACD line crosses back through the signal line against the current position, the strategy closes
the open trade at market. This mirrors the original EA, which always flattened the position on an opposing crossover before
looking for new opportunities.
- Fixed take-profit – a unit-based take-profit (expressed in price points) is applied via
StartProtection. The level matches
the MQL parameter TakeProfit and uses the instrument’s point value.
Risk and capital management
- Volume handling – the
LotVolume parameter mirrors the MT4 lot size. The strategy submits that exact volume for each entry.
- Balance filter – the
MinimumBalancePerVolume parameter multiplies the requested volume to determine the minimal portfolio
value required before new entries are allowed. If the balance check fails the strategy logs a message and skips the trade,
matching the original free-margin safeguard.
- Data integrity – signals are processed only on finished candles and after
IsFormedAndOnlineAndAllowTrading() confirms that
both the connection and indicators are ready.
Parameters
| Parameter |
Description |
FastPeriod |
EMA length of the fast MACD component. |
SlowPeriod |
EMA length of the slow MACD component. |
SignalPeriod |
EMA length of the MACD signal line. |
TakeProfitPoints |
Distance to the protective take-profit in price points. Set to 0 to disable. |
LotVolume |
Base order volume, equivalent to the “Lots” input of the MT4 version. |
MinimumBalancePerVolume |
Minimum portfolio value required per traded volume unit before opening a position. Set to 0 to skip the filter. |
CandleType |
Timeframe used to build candles and feed the indicator chain. |
Additional notes
- The strategy uses the
BindEx overload so that the MACD indicator can supply both the MACD and signal values in a single
callback without manual calls to GetValue.
- All comments inside the C# code are written in English, matching the project guidelines.
- There is no Python translation for this strategy; only the C# implementation is provided in the API package.
- To replicate the original MT4 behaviour most closely, select a candle timeframe that matches the chart where the EA used to run
and keep the volume parameter consistent with the lot size previously traded.
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, UnitTypes, Unit
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergenceSignal
class macd_zero_filtered_cross_strategy(Strategy):
def __init__(self):
super(macd_zero_filtered_cross_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 12) \
.SetDisplay("Fast Period", "Short EMA period for MACD", "MACD")
self._slow_period = self.Param("SlowPeriod", 26) \
.SetDisplay("Slow Period", "Long EMA period for MACD", "MACD")
self._signal_period = self.Param("SignalPeriod", 9) \
.SetDisplay("Signal Period", "Signal line length for MACD", "MACD")
self._take_profit_points = self.Param("TakeProfitPoints", 300.0) \
.SetDisplay("Take Profit (points)", "Fixed take-profit distance in price points", "Risk Management")
self._lot_volume = self.Param("LotVolume", 1.0) \
.SetDisplay("Lot Volume", "Trading volume per order", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe that drives MACD calculations", "General")
self._previous_macd = None
self._previous_signal = None
@property
def FastPeriod(self):
return self._fast_period.Value
@property
def SlowPeriod(self):
return self._slow_period.Value
@property
def SignalPeriod(self):
return self._signal_period.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def LotVolume(self):
return self._lot_volume.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(macd_zero_filtered_cross_strategy, self).OnStarted2(time)
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = self.FastPeriod
macd.Macd.LongMa.Length = self.SlowPeriod
macd.SignalMa.Length = self.SignalPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(macd, self.ProcessCandle).Start()
tp = float(self.TakeProfitPoints)
if tp > 0:
self.StartProtection(Unit(tp, UnitTypes.Absolute), None)
def ProcessCandle(self, candle, macd_value):
if candle.State != CandleStates.Finished:
return
macd_line = macd_value.Macd
signal_line = macd_value.Signal
if macd_line is None or signal_line is None:
return
macd_line = float(macd_line)
signal_line = float(signal_line)
if self._previous_macd is not None and self._previous_signal is not None:
cross_up = self._previous_macd < self._previous_signal and macd_line > signal_line
cross_down = self._previous_macd > self._previous_signal and macd_line < signal_line
if cross_down and self.Position > 0:
self.SellMarket(self.Position)
self._previous_macd = macd_line
self._previous_signal = signal_line
return
if cross_up and self.Position < 0:
self.BuyMarket(abs(self.Position))
self._previous_macd = macd_line
self._previous_signal = signal_line
return
if cross_up and macd_line < 0 and signal_line < 0 and self.Position <= 0:
self.BuyMarket(float(self.LotVolume))
elif cross_down and macd_line > 0 and signal_line > 0 and self.Position >= 0:
self.SellMarket(float(self.LotVolume))
self._previous_macd = macd_line
self._previous_signal = signal_line
def OnReseted(self):
super(macd_zero_filtered_cross_strategy, self).OnReseted()
self._previous_macd = None
self._previous_signal = None
def CreateClone(self):
return macd_zero_filtered_cross_strategy()