零滞后 MACD 交叉策略
该策略复刻自 MetaTrader 5 的 ZeroLagEA-AIP 算法。它使用基于两条零滞后指数移动平均线的零滞后 MACD。当当前 MACD 值高于前一根柱时开空仓,低于前一根柱时开多仓。如果在持仓期间出现反向信号,当前仓位将被平仓,下一根柱再开新仓。
逻辑
- 计算两个可配置周期的 ZLEMA。
- 两者差值乘以 10 得到零滞后 MACD。
- 仅在 MACD 在相邻两根柱之间改变方向时触发交易(可选)。
- 仅在设定的开始和结束时间内交易,超出时间或在指定的星期和小时强制平仓。
参数
- Volume – 订单数量。
- Fast EMA – 快速 ZLEMA 周期。
- Slow EMA – 慢速 ZLEMA 周期。
- Use Fresh Signal – 启用后仅在 MACD 方向变化时交易。
- Start Hour / End Hour – 以 UTC 表示的交易时间窗口。
- Kill Day / Kill Hour – 指定的星期和小时用于强制平仓。
- Candle Type – 用于计算的K线类型。
说明
策略使用 StockSharp 高级 API,通过 SubscribeCandles 和 Bind 获取指标数值,并使用市价单平仓。
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()