Zero Lag MACD Crossover Strategy
This strategy replicates the ZeroLagEA-AIP algorithm from MetaTrader 5. It uses a zero lag MACD constructed from two zero lag exponential moving averages. The system opens a short position when the MACD value increases compared to the previous bar and opens a long position when the MACD decreases. If an opposite signal appears while a position is open, the current position is closed and a new one is opened on the following bar.
Logic
- Two zero lag EMAs with configurable periods are calculated.
- Their difference multiplied by 10 forms the zero lag MACD value.
- A trade is executed only when the MACD direction changes between two consecutive bars (optional).
- Trading is allowed only between the configured start and end hours. All positions are force closed outside this window or on the specified weekday and hour.
Parameters
- Volume – order volume.
- Fast EMA – period of the fast zero lag EMA.
- Slow EMA – period of the slow zero lag EMA.
- Use Fresh Signal – if enabled, trades only on a new MACD direction change.
- Start Hour / End Hour – trading session boundaries in UTC.
- Kill Day / Kill Hour – day of week and hour when all positions are closed.
- Candle Type – candle data used for calculations.
Notes
The strategy uses high-level StockSharp API with SubscribeCandles and Bind to receive indicator values. Positions are closed using market orders.
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>
/// Zero lag MACD direction change strategy.
/// Buys when the zero lag MACD decreases and sells when it increases.
/// Trades only during the configured time window and closes positions outside it.
/// </summary>
public class ZeroLagMacdCrossoverStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<bool> _useFreshSignal;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<int> _killDay;
private readonly StrategyParam<int> _killHour;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _fastZlema = null!;
private ExponentialMovingAverage _slowZlema = null!;
private decimal _prevMacd;
private decimal _prevPrevMacd;
private bool _hasPrev;
private bool _hasPrevPrev;
/// <summary>
/// Fast EMA period.
/// </summary>
public int FastLength { get => _fastLength.Value; set => _fastLength.Value = value; }
/// <summary>
/// Slow EMA period.
/// </summary>
public int SlowLength { get => _slowLength.Value; set => _slowLength.Value = value; }
/// <summary>
/// Require fresh MACD direction change before trading.
/// </summary>
public bool UseFreshSignal { get => _useFreshSignal.Value; set => _useFreshSignal.Value = value; }
/// <summary>
/// Trading window start hour (inclusive, UTC).
/// </summary>
public int StartHour { get => _startHour.Value; set => _startHour.Value = value; }
/// <summary>
/// Trading window end hour (exclusive, UTC).
/// </summary>
public int EndHour { get => _endHour.Value; set => _endHour.Value = value; }
/// <summary>
/// Day of week when all positions are force closed.
/// 0 - Sunday, 6 - Saturday.
/// </summary>
public int KillDay { get => _killDay.Value; set => _killDay.Value = value; }
/// <summary>
/// Hour of the day when positions are force closed on <see cref="KillDay"/>.
/// </summary>
public int KillHour { get => _killHour.Value; set => _killHour.Value = value; }
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <summary>
/// Initializes a new instance of <see cref="ZeroLagMacdCrossoverStrategy"/>.
/// </summary>
public ZeroLagMacdCrossoverStrategy()
{
_fastLength = Param(nameof(FastLength), 5)
.SetGreaterThanZero()
.SetDisplay("Fast EMA", "Fast EMA period", "MACD")
.SetOptimize(2, 10, 1);
_slowLength = Param(nameof(SlowLength), 55)
.SetGreaterThanZero()
.SetDisplay("Slow EMA", "Slow EMA period", "MACD")
.SetOptimize(20, 60, 2);
_useFreshSignal = Param(nameof(UseFreshSignal), true)
.SetDisplay("Use Fresh Signal", "Trade only on MACD direction change", "MACD");
_startHour = Param(nameof(StartHour), 9)
.SetDisplay("Start Hour", "Trading start hour (UTC)", "Time")
.SetOptimize(0, 23, 1);
_endHour = Param(nameof(EndHour), 15)
.SetDisplay("End Hour", "Trading end hour (UTC)", "Time")
.SetOptimize(1, 24, 1);
_killDay = Param(nameof(KillDay), 5)
.SetDisplay("Kill Day", "Week day for forced close", "Time")
.SetOptimize(0, 6, 1);
_killHour = Param(nameof(KillHour), 21)
.SetDisplay("Kill Hour", "Hour for forced close", "Time")
.SetOptimize(0, 23, 1);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevMacd = 0m;
_prevPrevMacd = 0m;
_hasPrev = false;
_hasPrevPrev = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastZlema = new ExponentialMovingAverage { Length = FastLength };
_slowZlema = new ExponentialMovingAverage { Length = SlowLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_fastZlema, _slowZlema, ProcessCandle)
.Start();
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow)
{
if (candle.State != CandleStates.Finished)
return;
var time = candle.OpenTime;
if (time.Hour < StartHour || time.Hour >= EndHour || ((int)time.DayOfWeek == KillDay && time.Hour == KillHour))
{
if (Position != 0)
{
if (Position > 0)
SellMarket(Position);
else
BuyMarket(-Position);
}
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
var macd = 10m * (fast - slow);
if (!_hasPrev)
{
_prevMacd = macd;
_hasPrev = true;
return;
}
if (!_hasPrevPrev)
{
_prevPrevMacd = _prevMacd;
_prevMacd = macd;
_hasPrevPrev = true;
return;
}
var fresh = !UseFreshSignal || ((_prevMacd > _prevPrevMacd && macd < _prevMacd) || (_prevMacd < _prevPrevMacd && macd > _prevMacd));
if (!fresh)
{
_prevPrevMacd = _prevMacd;
_prevMacd = macd;
return;
}
if (macd > _prevMacd)
{
if (Position > 0)
{
SellMarket(Position);
_prevPrevMacd = _prevMacd;
_prevMacd = macd;
return;
}
if (Position == 0)
SellMarket(Volume);
}
else if (macd < _prevMacd)
{
if (Position < 0)
{
BuyMarket(-Position);
_prevPrevMacd = _prevMacd;
_prevMacd = macd;
return;
}
if (Position == 0)
BuyMarket(Volume);
}
_prevPrevMacd = _prevMacd;
_prevMacd = macd;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class zero_lag_macd_crossover_strategy(Strategy):
def __init__(self):
super(zero_lag_macd_crossover_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 5)
self._slow_length = self.Param("SlowLength", 55)
self._use_fresh_signal = self.Param("UseFreshSignal", True)
self._start_hour = self.Param("StartHour", 9)
self._end_hour = self.Param("EndHour", 15)
self._kill_day = self.Param("KillDay", 5)
self._kill_hour = self.Param("KillHour", 21)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._prev_macd = 0.0
self._prev_prev_macd = 0.0
self._has_prev = False
self._has_prev_prev = False
@property
def FastLength(self):
return self._fast_length.Value
@FastLength.setter
def FastLength(self, value):
self._fast_length.Value = value
@property
def SlowLength(self):
return self._slow_length.Value
@SlowLength.setter
def SlowLength(self, value):
self._slow_length.Value = value
@property
def UseFreshSignal(self):
return self._use_fresh_signal.Value
@UseFreshSignal.setter
def UseFreshSignal(self, value):
self._use_fresh_signal.Value = value
@property
def StartHour(self):
return self._start_hour.Value
@StartHour.setter
def StartHour(self, value):
self._start_hour.Value = value
@property
def EndHour(self):
return self._end_hour.Value
@EndHour.setter
def EndHour(self, value):
self._end_hour.Value = value
@property
def KillDay(self):
return self._kill_day.Value
@KillDay.setter
def KillDay(self, value):
self._kill_day.Value = value
@property
def KillHour(self):
return self._kill_hour.Value
@KillHour.setter
def KillHour(self, value):
self._kill_hour.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(zero_lag_macd_crossover_strategy, self).OnStarted2(time)
self._prev_macd = 0.0
self._prev_prev_macd = 0.0
self._has_prev = False
self._has_prev_prev = False
fast_ema = ExponentialMovingAverage()
fast_ema.Length = self.FastLength
slow_ema = ExponentialMovingAverage()
slow_ema.Length = self.SlowLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(fast_ema, slow_ema, self.ProcessCandle).Start()
self.StartProtection(None, None)
def ProcessCandle(self, candle, fast_val, slow_val):
if candle.State != CandleStates.Finished:
return
fast = float(fast_val)
slow = float(slow_val)
t = candle.OpenTime
start_h = int(self.StartHour)
end_h = int(self.EndHour)
kill_d = int(self.KillDay)
kill_h = int(self.KillHour)
if t.Hour < start_h or t.Hour >= end_h or (int(t.DayOfWeek) == kill_d and t.Hour == kill_h):
pos = float(self.Position)
if pos != 0:
if pos > 0:
self.SellMarket(pos)
else:
self.BuyMarket(-pos)
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
macd = 10.0 * (fast - slow)
if not self._has_prev:
self._prev_macd = macd
self._has_prev = True
return
if not self._has_prev_prev:
self._prev_prev_macd = self._prev_macd
self._prev_macd = macd
self._has_prev_prev = True
return
if self.UseFreshSignal:
fresh = (self._prev_macd > self._prev_prev_macd and macd < self._prev_macd) or (self._prev_macd < self._prev_prev_macd and macd > self._prev_macd)
else:
fresh = True
if not fresh:
self._prev_prev_macd = self._prev_macd
self._prev_macd = macd
return
pos = float(self.Position)
vol = float(self.Volume)
if macd > self._prev_macd:
if pos > 0:
self.SellMarket(pos)
self._prev_prev_macd = self._prev_macd
self._prev_macd = macd
return
if float(self.Position) == 0:
self.SellMarket(vol)
elif macd < self._prev_macd:
if pos < 0:
self.BuyMarket(-pos)
self._prev_prev_macd = self._prev_macd
self._prev_macd = macd
return
if float(self.Position) == 0:
self.BuyMarket(vol)
self._prev_prev_macd = self._prev_macd
self._prev_macd = macd
def OnReseted(self):
super(zero_lag_macd_crossover_strategy, self).OnReseted()
self._prev_macd = 0.0
self._prev_prev_macd = 0.0
self._has_prev = False
self._has_prev_prev = False
def CreateClone(self):
return zero_lag_macd_crossover_strategy()