Ver no GitHub

Estratégia Expert ZZLWA

Visão geral

Esta estratégia é um port de alto nível do StockSharp do assessor especialista original ExpertZZLWA do MetaTrader 5. O EA oferecia três modos de operação distintos e dimensionamento de posição martingale opcional. O port mantém a estrutura do especialista original enquanto o adapta a velas e indicadores do StockSharp:

  1. Modo Original – alterna entre operações compradas e vendidas em cada barra concluída enquanto não há posição aberta.
  2. Modo ZigZag Addition – recria o comportamento do indicador personalizado "ZigZag LW Addition" rastreando novos swing highs e lows por meio de valores máximos/mínimos móveis.
  3. Modo Moving Average Test – espelha a lógica de cruzamento de MA suavizada (150) vs MA simples (10) do código MQL.

Todos os modos usam offsets de stop loss e take profit de proteção configuráveis expressos em pontos de preço. A estratégia suporta dimensionamento martingale opcional onde uma nova operação é aumentada por um multiplicador após uma perda realizada, limitada por um volume máximo.

Lógica de trading

Modo Original

  • Trabalha apenas com velas finalizadas.
  • Quando não há posição aberta, a estratégia alterna entre ordens de mercado compradas e vendidas em cada nova barra.
  • Stop loss e take profit são registrados através do helper integrado StartProtection.
  • Uma vez que uma operação fecha (no stop ou no alvo), a direção oposta se torna ativa para a próxima barra.

Modo ZigZag Addition

  • Assina a série de velas selecionada e mantém indicadores Highest e Lowest móveis.
  • Detecta um swing high quando o máximo da vela toca o valor mais alto atual enquanto a direção do swing anterior não era de alta. Isso recria os sinais de buffer de compra/venda do "ZigZag LW Addition".
  • Detecta um swing low quando o mínimo da vela toca o valor mais baixo móvel de maneira oposta.
  • Gera uma ordem de mercado na direção sinalizada imediatamente após o fechamento da vela.

Modo Moving Average Test

  • Constrói uma média móvel suavizada com comprimento 150 e uma média móvel simples com comprimento 10 (correspondendo à implementação MQL).
  • Produz um sinal comprado quando a MA suavizada cruza acima da MA simples da barra anterior para a barra atual.
  • Produz um sinal vendido quando a MA suavizada cruza abaixo da MA simples.
  • Os sinais são processados apenas em velas fechadas.

Tratamento do Martingale

  • Após cada operação própria recebida, a estratégia rastreia a posição líquida e o preço médio de entrada.
  • Quando uma posição é totalmente fechada, o lucro realizado da última operação é registrado.
  • Se a operação fechou com perda e o martingale está habilitado, o volume do próximo pedido se torna último_volume * MartingaleMultiplier (limitado por MaximumVolume).
  • Se a operação fechou com lucro ou o martingale está desabilitado, a estratégia volta ao volume base.

Parâmetros

Parâmetro Padrão Descrição
StopLossPoints 600 Distância ao stop de proteção em pontos de preço.
TakeProfitPoints 700 Distância ao take profit em pontos de preço.
BaseVolume 0.01 Tamanho de ordem padrão quando o martingale não é aplicado.
UseMartingale false Habilita o dimensionamento martingale quando definido como true.
MartingaleMultiplier 2 Multiplicador aplicado ao último volume de operação após uma perda.
MaximumVolume 10 Volume máximo permitido para dimensionamento martingale.
Mode Original Modo de operação: Original, ZigZagAddition ou MovingAverageTest.
ZigZagTerm LongTerm Preset de sensibilidade para o modo ZigZag (ShortTerm, MediumTerm, LongTerm).
SlowMaPeriod 150 Período da MA suavizada usada no modo MA Test.
FastMaPeriod 10 Período da MA simples usada no modo MA Test.
CandleType Período de 15 minutos Tipo de vela assinada para processamento.

Notas

  • Os offsets de stop/take são multiplicados pelo PriceStep do instrumento, correspondendo ao comportamento _Point do MetaTrader.
  • A estratégia usa exclusivamente a API de alto nível do StockSharp (SubscribeCandles + vinculação de indicadores).
  • Os presets de sensibilidade ZigZag mapeiam para comprimentos de Highest/Lowest de 12 (Curto), 24 (Médio) e 48 (Longo). Ajuste-os se uma largura de swing diferente for necessária.
  • O rastreador martingale depende de notificações de operações próprias; certifique-se de que a estratégia rode em um ambiente onde os preenchimentos são relatados corretamente.

Diferenças de conversão vs MQL

  • A versão MQL interagia com um indicador compilado ZigZag LW Addition. No StockSharp aproximamos os buffers usando máximos/mínimos móveis, o que entrega sinais similares sem binários externos.
  • A colocação de ordens se baseia em BuyMarket / SellMarket e no helper de proteção gerenciado em vez de tickets de ordens manuais.
  • O cálculo histórico de lotes no especialista original usava o histórico de deals do terminal. O port replica esse comportamento analisando operações próprias em tempo real e armazenando o último volume de operação fechado e o lucro.
  • As entradas de slippage e número mágico do MQL são omitidas porque o StockSharp não as precisa para ordens de mercado neste contexto.
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;
		}
	}