MACD-Divergenz (MACD Divergence)
Die MACD-Divergenz sucht nach Widersprüchen zwischen der Kursbewegung und dem MACD-Indikator. Höhere Hochs im Kurs, aber niedrigere Hochs im MACD deuten auf nachlassendes Momentum hin (bärische Divergenz), während niedrigere Tiefs im Kurs und höhere MACD-Tiefs auf eine bullische Umkehr hinweisen.
Tests zeigen eine durchschnittliche jährliche Rendite von etwa 70 %. Die Strategie eignet sich am besten für den Aktienmarkt.
Nach der Erkennung einer Divergenz wartet das System darauf, dass der MACD seine Signallinie kreuzt, bevor es einsteigt. Der Trade wird geschlossen, wenn der MACD in die entgegengesetzte Richtung kreuzt oder der Stop-Loss ausgelöst wird.
Details
- Einstiegskriterien: Bullische oder bärische Divergenz plus MACD-Kreuzung der Signallinie.
- Long/Short: Beide Richtungen.
- Ausstiegskriterien: MACD kreuzt in entgegengesetzte Richtung oder Stop.
- Stops: Ja.
- Standardwerte:
FastMacdPeriod= 12SlowMacdPeriod= 26SignalPeriod= 9DivergencePeriod= 5CandleType= TimeSpan.FromMinutes(15)StopLossPercent= 2.0m
- Filter:
- Kategorie: Divergenz
- Richtung: Beide
- Indikatoren: MACD
- Stops: Ja
- Komplexität: Mittel
- Zeitrahmen: Intraday
- Saisonalität: Nein
- Neuronale Netze: Nein
- Divergenz: Ja
- Risikolevel: Mittel
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Compares consecutive confirmed price pivots with their MACD-line values.
/// Enters only on a later MACD/signal cross and fully exits on the reverse cross or protection.
/// </summary>
public class MacdDivergenceStrategy : Strategy
{
private readonly StrategyParam<int> _fastMacdPeriod;
private readonly StrategyParam<int> _slowMacdPeriod;
private readonly StrategyParam<int> _signalPeriod;
private readonly StrategyParam<int> _divergencePeriod;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly List<(decimal High, decimal Low, decimal Macd)> _window = new();
private (decimal Price, decimal Macd)? _lastLow;
private (decimal Price, decimal Macd)? _lastHigh;
private decimal? _previousMacd;
private decimal? _previousSignal;
private int _bar;
private int _bullishUntil;
private int _bearishUntil;
private Order _pendingOrder;
public int FastMacdPeriod { get => _fastMacdPeriod.Value; set => _fastMacdPeriod.Value = value; }
public int SlowMacdPeriod { get => _slowMacdPeriod.Value; set => _slowMacdPeriod.Value = value; }
public int SignalPeriod { get => _signalPeriod.Value; set => _signalPeriod.Value = value; }
public int DivergencePeriod { get => _divergencePeriod.Value; set => _divergencePeriod.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public decimal StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }
public MacdDivergenceStrategy()
{
_fastMacdPeriod = Param(nameof(FastMacdPeriod), 12).SetGreaterThanZero()
.SetDisplay("Fast MACD Period", "Fast close EMA length", "Indicators");
_slowMacdPeriod = Param(nameof(SlowMacdPeriod), 26).SetGreaterThanZero()
.SetDisplay("Slow MACD Period", "Slow close EMA length", "Indicators");
_signalPeriod = Param(nameof(SignalPeriod), 9).SetGreaterThanZero()
.SetDisplay("Signal Period", "MACD signal EMA length", "Indicators");
_divergencePeriod = Param(nameof(DivergencePeriod), 5).SetRange(3, 31)
.SetDisplay("Divergence Period", "Odd-width confirmed pivot window and maximum bars to wait for a cross", "Pattern");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "MACD and pivot timeframe", "General");
_stopLossPercent = Param(nameof(StopLossPercent), 2m).SetNotNegative()
.SetDisplay("Stop Loss (%)", "Actual-fill percent stop; zero disables it.", "Protection");
OrderRegistering += order =>
{
_pendingOrder = order;
if (Position != 0m && order.Side == (Position > 0m ? Sides.Sell : Sides.Buy))
_bullishUntil = _bearishUntil = 0;
};
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
ClearState();
}
private void ClearState()
{
_window.Clear();
_lastLow = _lastHigh = null;
_previousMacd = _previousSignal = null;
_bar = _bullishUntil = _bearishUntil = 0;
_pendingOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (FastMacdPeriod >= SlowMacdPeriod || DivergencePeriod % 2 != 1)
throw new InvalidOperationException("FastMacdPeriod must be below SlowMacdPeriod and DivergencePeriod must be odd.");
ClearState();
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = FastMacdPeriod },
LongMa = { Length = SlowMacdPeriod },
},
SignalMa = { Length = SignalPeriod }
};
var candles = SubscribeCandles(CandleType);
candles.BindEx(macd, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, candles);
DrawIndicator(area, macd);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native actual-fill protection evaluates executable quotes between signal candles.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue output)
{
if (candle.State != CandleStates.Finished || !output.Indicator.IsFormed ||
output is not MovingAverageConvergenceDivergenceSignalValue value ||
value.Macd is not decimal line || value.Signal is not decimal signal)
return;
_bar++;
var oldLine = _previousMacd;
var oldSignal = _previousSignal;
var upCross = oldLine is decimal oldUpLine && oldSignal is decimal oldUpSignal &&
oldUpLine <= oldUpSignal && line > signal;
var downCross = oldLine is decimal oldDownLine && oldSignal is decimal oldDownSignal &&
oldDownLine >= oldDownSignal && line < signal;
var bullishReady = _bullishUntil >= _bar;
var bearishReady = _bearishUntil >= _bar;
_previousMacd = line;
_previousSignal = signal;
_window.Add((candle.HighPrice, candle.LowPrice, line));
if (_window.Count == DivergencePeriod)
{
var centerIndex = DivergencePeriod / 2;
var center = _window[centerIndex];
if (_window.Where((_, index) => index != centerIndex).All(bar => center.Low < bar.Low))
{
var divergence = _lastLow is { } previousLow &&
center.Low < previousLow.Price && center.Macd > previousLow.Macd;
_lastLow = (center.Low, center.Macd);
_bullishUntil = divergence ? _bar + DivergencePeriod : 0;
}
if (_window.Where((_, index) => index != centerIndex).All(bar => center.High > bar.High))
{
var divergence = _lastHigh is { } previousHigh &&
center.High > previousHigh.Price && center.Macd < previousHigh.Macd;
_lastHigh = (center.High, center.Macd);
_bearishUntil = divergence ? _bar + DivergencePeriod : 0;
}
_window.RemoveAt(0);
}
if (!IsFormedAndOnlineAndAllowTrading() ||
_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position > 0m && downCross)
{
SellMarket(Position);
_bullishUntil = _bearishUntil = 0;
}
else if (Position < 0m && upCross)
{
BuyMarket(Math.Abs(Position));
_bullishUntil = _bearishUntil = 0;
}
else if (Position == 0m)
{
if (bullishReady && upCross)
{
BuyMarket(Volume);
_bullishUntil = _bearishUntil = 0;
}
else if (bearishReady && downCross)
{
SellMarket(Volume);
_bullishUntil = _bearishUntil = 0;
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes, OrderStates, Level1Fields, Sides
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
class macd_divergence_strategy(Strategy):
"""
Compares confirmed price pivots with their MACD-line values.
Enters only on a later MACD/signal cross; fully exits on reverse cross or stop.
"""
def __init__(self):
super(macd_divergence_strategy, self).__init__()
self._fast_macd_period = self.Param("FastMacdPeriod", 12).SetGreaterThanZero().SetDisplay("Fast MACD Period", "Fast close EMA length", "Indicators")
self._slow_macd_period = self.Param("SlowMacdPeriod", 26).SetGreaterThanZero().SetDisplay("Slow MACD Period", "Slow close EMA length", "Indicators")
self._signal_period = self.Param("SignalPeriod", 9).SetGreaterThanZero().SetDisplay("Signal Period", "MACD signal EMA length", "Indicators")
self._divergence_period = self.Param("DivergencePeriod", 5).SetRange(3, 31).SetDisplay("Divergence Period", "Odd-width confirmed pivot window and maximum bars to wait for a cross", "Pattern")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay("Candle Type", "MACD and pivot timeframe", "General")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop Loss (%)", "Actual-fill percent stop; zero disables it.", "Protection")
self._clear_state()
self.OrderRegistering += self._track_pending
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
if self.Position != 0 and order.Side == (Sides.Sell if self.Position > 0 else Sides.Buy):
self._bullish_until = self._bearish_until = 0
def _clear_state(self):
self._window = []
self._last_low = None
self._last_high = None
self._previous_macd = None
self._previous_signal = None
self._bar = 0
self._bullish_until = 0
self._bearish_until = 0
self._pending_order = None
def OnReseted(self):
super(macd_divergence_strategy, self).OnReseted()
self._clear_state()
def OnStarted2(self, time):
super(macd_divergence_strategy, self).OnStarted2(time)
if self._fast_macd_period.Value >= self._slow_macd_period.Value or self._divergence_period.Value % 2 != 1:
raise ValueError("FastMacdPeriod must be below SlowMacdPeriod and DivergencePeriod must be odd.")
self._clear_state()
self.StartProtection(Unit(), Unit(Decimal(self._stop_loss_percent.Value), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = self._fast_macd_period.Value
macd.Macd.LongMa.Length = self._slow_macd_period.Value
macd.SignalMa.Length = self._signal_period.Value
candles = self.SubscribeCandles(self.candle_type)
candles.BindEx(macd, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candles)
self.DrawIndicator(area, macd)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native actual-fill protection evaluates executable quotes between signal candles.
pass
def _process_candle(self, candle, output):
if candle.State != CandleStates.Finished or not output.Indicator.IsFormed:
return
line = output.Macd
signal = output.Signal
if line is None or signal is None:
return
self._bar += 1
up_cross = (self._previous_macd is not None and self._previous_signal is not None and
self._previous_macd <= self._previous_signal and line > signal)
down_cross = (self._previous_macd is not None and self._previous_signal is not None and
self._previous_macd >= self._previous_signal and line < signal)
bullish_ready = self._bullish_until >= self._bar
bearish_ready = self._bearish_until >= self._bar
self._previous_macd = line
self._previous_signal = signal
self._window.append((candle.HighPrice, candle.LowPrice, line))
width = self._divergence_period.Value
if len(self._window) == width:
center_index = width // 2
center = self._window[center_index]
if all(center[1] < bar[1] for i, bar in enumerate(self._window) if i != center_index):
divergence = self._last_low is not None and center[1] < self._last_low[0] and center[2] > self._last_low[1]
self._last_low = (center[1], center[2])
self._bullish_until = self._bar + width if divergence else 0
if all(center[0] > bar[0] for i, bar in enumerate(self._window) if i != center_index):
divergence = self._last_high is not None and center[0] > self._last_high[0] and center[2] < self._last_high[1]
self._last_high = (center[0], center[2])
self._bearish_until = self._bar + width if divergence else 0
self._window.pop(0)
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
if self.Position > 0 and down_cross:
self.SellMarket(self.Position)
self._bullish_until = self._bearish_until = 0
elif self.Position < 0 and up_cross:
self.BuyMarket(Math.Abs(self.Position))
self._bullish_until = self._bearish_until = 0
elif self.Position == 0:
if bullish_ready and up_cross:
self.BuyMarket(self.Volume)
self._bullish_until = self._bearish_until = 0
elif bearish_ready and down_cross:
self.SellMarket(self.Volume)
self._bullish_until = self._bearish_until = 0
def CreateClone(self):
return macd_divergence_strategy()