Auf GitHub ansehen

Expert-ZZLWA-Strategie

Übersicht

Diese Strategie ist ein StockSharp-High-Level-Port des ursprünglichen ExpertZZLWA MetaTrader-5-Expert-Advisors. Der EA bot drei verschiedene Betriebsmodi und optional Martingale-Positionsgrößen. Der Port behält die Struktur des ursprünglichen Experten bei und passt ihn an StockSharp-Kerzen und -Indikatoren an:

  1. Original-Modus – wechselt auf jedem abgeschlossenen Balken zwischen Long- und Short-Trades, solange keine offene Position vorhanden ist.
  2. ZigZag-Addition-Modus – recreiert das Verhalten des benutzerdefinierten Indikators „ZigZag LW Addition", indem er neue Swing-Hochs und -Tiefs durch rollende Höchst-/Tiefstwerte verfolgt.
  3. Moving-Average-Test-Modus – spiegelt die geglättete MA (150) vs. einfache MA (10) Crossover-Logik aus dem MQL-Code wider.

Alle Modi verwenden konfigurierbare schützende Stop-Loss- und Take-Profit-Offsets in Preispunkten. Die Strategie unterstützt optional Martingale-Sizing, bei dem ein neuer Trade nach einem realisierten Verlust um einen Multiplikator erhöht wird, begrenzt durch ein maximales Volumen.

Handelslogik

Original-Modus

  • Arbeitet nur mit fertigen Kerzen.
  • Wenn keine Position offen ist, wechselt die Strategie auf jedem neuen Balken zwischen Long- und Short-Marktorders.
  • Stop-Loss und Take-Profit werden über den eingebauten StartProtection-Helper registriert.
  • Sobald ein Trade schließt (entweder am Stop oder am Ziel), wird die entgegengesetzte Richtung für den nächsten Balken aktiv.

ZigZag-Addition-Modus

  • Abonniert die ausgewählte Kerzenserie und pflegt rollende Highest- und Lowest-Indikatoren.
  • Erkennt ein Swing-Hoch, wenn das Kerzenhoch den aktuellen Höchstwert berührt, während die vorherige Swing-Richtung nicht aufwärts war. Dies recreiert die Buy/Sell-Puffer-Signale von „ZigZag LW Addition".
  • Erkennt ein Swing-Tief, wenn das Kerzentief den rollenden Tiefstwert in der entgegengesetzten Weise berührt.
  • Generiert unmittelbar nach dem Kerzenschluss eine Marktorder in der signalisierten Richtung.

Moving-Average-Test-Modus

  • Erstellt einen geglätteten gleitenden Durchschnitt mit Länge 150 und einen einfachen gleitenden Durchschnitt mit Länge 10 (entspricht der MQL-Implementierung).
  • Erzeugt ein Long-Signal, wenn der geglättete MA vom vorherigen Balken zum aktuellen Balken über den einfachen MA kreuzt.
  • Erzeugt ein Short-Signal, wenn der geglättete MA unter den einfachen MA kreuzt.
  • Signale werden nur auf geschlossenen Kerzen verarbeitet.

Martingale-Behandlung

  • Nach jedem eigenen Trade verfolgt die Strategie die Nettoposition und den durchschnittlichen Einstiegspreis.
  • Wenn eine Position vollständig geschlossen wird, wird der realisierte Gewinn des letzten Trades aufgezeichnet.
  • Wenn der Trade mit Verlust schloss und Martingale aktiviert ist, wird das nächste Ordervolumen zu letztes_Volumen * MartingaleMultiplier (begrenzt durch MaximumVolume).
  • Wenn der Trade mit Gewinn schloss oder Martingale deaktiviert ist, fällt die Strategie auf das Basisvolumen zurück.

Parameter

Parameter Standard Beschreibung
StopLossPoints 600 Abstand zum Schutz-Stop in Preispunkten.
TakeProfitPoints 700 Abstand zum Take-Profit in Preispunkten.
BaseVolume 0.01 Standard-Ordergröße, wenn Martingale nicht angewendet wird.
UseMartingale false Aktiviert Martingale-Sizing, wenn auf true gesetzt.
MartingaleMultiplier 2 Multiplikator, der nach einem Verlust auf das letzte Trade-Volumen angewendet wird.
MaximumVolume 10 Maximal zulässiges Volumen für Martingale-Sizing.
Mode Original Betriebsmodus: Original, ZigZagAddition oder MovingAverageTest.
ZigZagTerm LongTerm Empfindlichkeits-Preset für den ZigZag-Modus (ShortTerm, MediumTerm, LongTerm).
SlowMaPeriod 150 Periode des geglätteten MA im MA-Test-Modus.
FastMaPeriod 10 Periode des einfachen MA im MA-Test-Modus.
CandleType 15-Minuten-Zeitrahmen Für die Verarbeitung abonnierter Kerzentyp.

Hinweise

  • Stop/Take-Offsets werden mit dem Instrument-PriceStep multipliziert, was dem _Point-Verhalten von MetaTrader entspricht.
  • Die Strategie verwendet ausschließlich die StockSharp-High-Level-API (SubscribeCandles + Indikator-Bindung).
  • Die ZigZag-Empfindlichkeits-Presets entsprechen Highest/Lowest-Längen von 12 (Kurz), 24 (Mittel) und 48 (Lang). Passen Sie diese an, wenn eine andere Swing-Breite erforderlich ist.
  • Der Martingale-Tracker basiert auf eigenen Trade-Benachrichtigungen; stellen Sie sicher, dass die Strategie in einer Umgebung läuft, in der Fills korrekt gemeldet werden.

Konversionsunterschiede vs. MQL

  • Die MQL-Version interagierte mit einem kompilierten ZigZag LW Addition-Indikator. In StockSharp approximieren wir die Puffer mit rollenden Hochs/Tiefs, was ähnliche Signale ohne externe Binärdateien liefert.
  • Die Orderplatzierung basiert auf BuyMarket / SellMarket und dem verwalteten Schutz-Helper anstelle von manuellen Order-Tickets.
  • Die historische Lot-Berechnung im ursprünglichen Experten verwendete die Terminal-Deal-History. Der Port repliziert dieses Verhalten durch Analyse eigener Trades in Echtzeit und Speicherung des letzten geschlossenen Trade-Volumens und -Gewinns.
  • Slip- und Magic-Number-Eingaben aus MQL werden weggelassen, da StockSharp sie für Marktorders in diesem Kontext nicht benötigt.
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 ExpertZZLWA MetaTrader strategy with three operation modes.
/// </summary>
public class ExpertZzlwaStrategy : Strategy
{
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<bool> _useMartingale;
	private readonly StrategyParam<decimal> _martingaleMultiplier;
	private readonly StrategyParam<decimal> _maximumVolume;
	private readonly StrategyParam<StrategyModes> _mode;
	private readonly StrategyParam<TermLevels> _termLevel;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private Highest _highest;
	private Lowest _lowest;
	private SmoothedMovingAverage _slowMa;
	private SimpleMovingAverage _fastMa;

	private bool _pendingBuySignal;
	private bool _pendingSellSignal;
	private bool _originalBuyReady;
	private bool _originalSellReady;
	private int _zigZagDirection;
	private decimal _prevSlow;
	private decimal _prevFast;

	private decimal _trackedPosition;
	private decimal _averageEntryPrice;
	private decimal _lastClosedVolume;
	private bool _lastTradeLoss;

	/// <summary>
	/// Operation modes reproduced from the original expert.
	/// </summary>
	public enum StrategyModes
	{
		Original,
		ZigZagAddition,
		MovingAverageTest,
	}

	/// <summary>
	/// ZigZag sensitivity presets available in addition mode.
	/// </summary>
	public enum TermLevels
	{
		ShortTerm,
		MediumTerm,
		LongTerm,
	}

	/// <summary>
	/// Protective stop size in price points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Profit target size in price points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Base order volume used by the strategy.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Enable martingale style position sizing.
	/// </summary>
	public bool UseMartingale
	{
		get => _useMartingale.Value;
		set => _useMartingale.Value = value;
	}

	/// <summary>
	/// Multiplier applied after a losing trade when martingale is active.
	/// </summary>
	public decimal MartingaleMultiplier
	{
		get => _martingaleMultiplier.Value;
		set => _martingaleMultiplier.Value = value;
	}

	/// <summary>
	/// Maximum allowed order volume.
	/// </summary>
	public decimal MaximumVolume
	{
		get => _maximumVolume.Value;
		set => _maximumVolume.Value = value;
	}

	/// <summary>
	/// Selected trading mode.
	/// </summary>
	public StrategyModes Mode
	{
		get => _mode.Value;
		set => _mode.Value = value;
	}

	/// <summary>
	/// ZigZag term preset for addition mode.
	/// </summary>
	public TermLevels ZigZagTerm
	{
		get => _termLevel.Value;
		set => _termLevel.Value = value;
	}

	/// <summary>
	/// Period of the slow smoothed moving average used in MA test mode.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <summary>
	/// Period of the fast simple moving average used in MA test mode.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="ExpertZzlwaStrategy"/> class.
	/// </summary>
	public ExpertZzlwaStrategy()
	{
		_stopLossPoints = Param(nameof(StopLossPoints), 600)
		.SetGreaterThanZero()
		.SetDisplay("Stop Loss (points)", "Protective stop in points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 700)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit (points)", "Profit target in points", "Risk");

		_baseVolume = Param(nameof(BaseVolume), 0.01m)
		.SetGreaterThanZero()
		.SetDisplay("Base Volume", "Default order volume", "Trading");

		_useMartingale = Param(nameof(UseMartingale), false)
		.SetDisplay("Use Martingale", "Enable martingale sizing", "Trading");

		_martingaleMultiplier = Param(nameof(MartingaleMultiplier), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Martingale Multiplier", "Multiplier applied after a loss", "Trading");

		_maximumVolume = Param(nameof(MaximumVolume), 10m)
		.SetGreaterThanZero()
		.SetDisplay("Maximum Volume", "Upper cap for order size", "Trading");

		_mode = Param(nameof(Mode), StrategyModes.MovingAverageTest)
		.SetDisplay("Mode", "Operating mode", "General");

		_termLevel = Param(nameof(ZigZagTerm), TermLevels.LongTerm)
		.SetDisplay("ZigZag Term", "Sensitivity preset for ZigZag", "Indicators");

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 150)
		.SetGreaterThanZero()
		.SetDisplay("Slow MA Period", "Smoothed MA length", "Indicators");

		_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
		.SetGreaterThanZero()
		.SetDisplay("Fast MA Period", "Simple MA length", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Time frame to analyse", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_highest = null;
		_lowest = null;
		_slowMa = null;
		_fastMa = null;
		_pendingBuySignal = false;
		_pendingSellSignal = false;
		_originalBuyReady = true;
		_originalSellReady = true;
		_zigZagDirection = 0;
		_prevSlow = 0m;
		_prevFast = 0m;
		_trackedPosition = 0m;
		_averageEntryPrice = 0m;
		_lastClosedVolume = BaseVolume;
		_lastTradeLoss = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		StartProtection(
		stopLoss: new Unit(StopLossPoints * GetPriceStep(), UnitTypes.Absolute),
		takeProfit: new Unit(TakeProfitPoints * GetPriceStep(), UnitTypes.Absolute));

		_originalBuyReady = true;
		_originalSellReady = true;
		_pendingBuySignal = false;
		_pendingSellSignal = false;
		_trackedPosition = 0m;
		_averageEntryPrice = 0m;
		_lastClosedVolume = BaseVolume;
		_lastTradeLoss = false;

		var subscription = SubscribeCandles(CandleType);

		switch (Mode)
		{
			case StrategyModes.Original:
				subscription.Bind(ProcessOriginalCandle).Start();
				break;

			case StrategyModes.ZigZagAddition:
				_highest = new Highest { Length = GetZigZagDepth(ZigZagTerm) };
				_lowest = new Lowest { Length = GetZigZagDepth(ZigZagTerm) };
				subscription.Bind(_highest, _lowest, ProcessAdditionCandle).Start();
				break;

			case StrategyModes.MovingAverageTest:
				_slowMa = new SmoothedMovingAverage { Length = SlowMaPeriod };
				_fastMa = new SimpleMovingAverage { Length = FastMaPeriod };
				subscription.Bind(_slowMa, _fastMa, ProcessMovingAverageCandle).Start();
				break;

			default:
				throw new NotSupportedException($"Unsupported mode {Mode}.");
			}

			var area = CreateChartArea();
			if (area != null)
			{
				DrawCandles(area, subscription);

				switch (Mode)
				{
					case StrategyModes.ZigZagAddition:
						DrawIndicator(area, _highest);
						DrawIndicator(area, _lowest);
						break;
					case StrategyModes.MovingAverageTest:
						DrawIndicator(area, _slowMa);
						DrawIndicator(area, _fastMa);
						break;
				}

				DrawOwnTrades(area);
			}
		}

		private void ProcessOriginalCandle(ICandleMessage candle)
		{
			if (candle.State != CandleStates.Finished)
			return;

			if (Position == 0)
			{
				if (_originalBuyReady)
				{
					ExecuteTrade(Sides.Buy);
					_originalBuyReady = false;
					_originalSellReady = true;
				}
				else if (_originalSellReady)
				{
					ExecuteTrade(Sides.Sell);
					_originalSellReady = false;
					_originalBuyReady = true;
				}
			}
		}

		private void ProcessAdditionCandle(ICandleMessage candle, decimal highest, decimal lowest)
		{
			if (candle.State != CandleStates.Finished)
			return;

			if (!_highest.IsFormed || !_lowest.IsFormed)
			return;

			// Detect fresh ZigZag pivots similar to the original indicator buffers.
			if (candle.HighPrice >= highest && _zigZagDirection != 1)
			{
				_pendingSellSignal = true;
				_pendingBuySignal = false;
				_zigZagDirection = 1;
			}
			else if (candle.LowPrice <= lowest && _zigZagDirection != -1)
			{
				_pendingBuySignal = true;
				_pendingSellSignal = false;
				_zigZagDirection = -1;
			}

			DispatchSignals();
		}

		private void ProcessMovingAverageCandle(ICandleMessage candle, decimal slow, decimal fast)
		{
			if (candle.State != CandleStates.Finished)
			return;

			if (!_slowMa.IsFormed || !_fastMa.IsFormed)
			return;

			// Reproduce cross checks from the MQL version.
			var crossDown = _prevSlow > _prevFast && slow < fast;
			var crossUp = _prevSlow < _prevFast && slow > fast;

			_prevSlow = slow;
			_prevFast = fast;

			if (crossUp)
			{
				_pendingBuySignal = true;
				_pendingSellSignal = false;
			}
			else if (crossDown)
			{
				_pendingSellSignal = true;
				_pendingBuySignal = false;
			}

			DispatchSignals();
		}

		private void DispatchSignals()
		{
			if (_pendingBuySignal)
			{
				ExecuteTrade(Sides.Buy);
				_pendingBuySignal = false;
				_pendingSellSignal = false;
			}
			else if (_pendingSellSignal)
			{
				ExecuteTrade(Sides.Sell);
				_pendingSellSignal = false;
				_pendingBuySignal = false;
			}
		}

		private void ExecuteTrade(Sides side)
		{
			var volume = GetOrderVolume();
			if (volume <= 0)
			return;

			if (side == Sides.Buy)
			BuyMarket(volume);
			else
			SellMarket(volume);
		}

		private decimal GetOrderVolume()
		{
			if (!UseMartingale)
			return BaseVolume;

			if (!_lastTradeLoss)
			return BaseVolume;

			var nextVolume = _lastClosedVolume * MartingaleMultiplier;
			return nextVolume > MaximumVolume ? BaseVolume : nextVolume;
		}

		private int GetZigZagDepth(TermLevels level)
		{
			return level switch
			{
				TermLevels.ShortTerm => 12,
				TermLevels.MediumTerm => 24,
				_ => 48,
			};
		}

		private decimal GetPriceStep()
		{
			return Security?.PriceStep ?? 1m;
		}

		/// <inheritdoc />
		protected override void OnOwnTradeReceived(MyTrade trade)
		{
			if (trade?.Order == null)
			return;

			var side = trade.Order.Side;
			var volume = trade.Trade.Volume;
			var price = trade.Trade.Price;

			var previousPosition = _trackedPosition;

			if (side == Sides.Buy)
			{
				if (previousPosition >= 0)
				{
					// Building or creating a long position.
					var newPosition = previousPosition + volume;
					_averageEntryPrice = newPosition == 0m
					? 0m
					: (_averageEntryPrice * previousPosition + price * volume) / newPosition;
					_trackedPosition = newPosition;
				}
				else
				{
					// Closing part or all of a short position.
					var closingVolume = Math.Min(volume, Math.Abs(previousPosition));
					var profit = (_averageEntryPrice - price) * closingVolume;
					var remaining = previousPosition + volume;

					if (remaining >= 0m)
					{
						RegisterClosedTrade(closingVolume, profit);
						if (remaining > 0m)
						{
							// Flip into a new long position with leftover quantity.
							_trackedPosition = remaining;
							_averageEntryPrice = price;
						}
						else
						{
							_trackedPosition = 0m;
							_averageEntryPrice = 0m;
						}
					}
					else
					{
						_trackedPosition = remaining;
						// Average price of the remaining short stays unchanged.
					}
				}
			}
			else
			{
				if (previousPosition <= 0)
				{
					// Building or creating a short position.
					var newPosition = previousPosition - volume;
					var absPrev = Math.Abs(previousPosition);
					var absNew = Math.Abs(newPosition);
					_averageEntryPrice = absNew == 0m
					? 0m
					: (_averageEntryPrice * absPrev + price * volume) / absNew;
					_trackedPosition = newPosition;
				}
				else
				{
					// Closing part or all of a long position.
					var closingVolume = Math.Min(volume, previousPosition);
					var profit = (price - _averageEntryPrice) * closingVolume;
					var remaining = previousPosition - volume;

					if (remaining <= 0m)
					{
						RegisterClosedTrade(closingVolume, profit);
						if (remaining < 0m)
						{
							_trackedPosition = remaining;
							_averageEntryPrice = price;
						}
						else
						{
							_trackedPosition = 0m;
							_averageEntryPrice = 0m;
						}
					}
					else
					{
						_trackedPosition = remaining;
						// Average entry price is preserved for the reduced long position.
					}
				}
			}
		}

		private void RegisterClosedTrade(decimal volume, decimal profit)
		{
			_lastClosedVolume = volume;
			_lastTradeLoss = profit < 0m;
		}
	}