Ver no GitHub

ZigZag EvgeTrofi Strategy

The ZigZag EvgeTrofi strategy ports the classic MetaTrader expert advisor into the StockSharp high level API. It watches the most recent swing detected by a ZigZag-style process and reacts quickly while the pivot is still fresh.

Concept

  • The original advisor analyses the first non-zero point of the ZigZag buffer and decides whether the last confirmed swing was a high or a low.
  • A swing high generates a long entry by default. Activating SignalReverse inverts the logic.
  • Positions are opened only while the new pivot is considered recent. The Urgency parameter limits the number of bars after a pivot when trades can be initiated.
  • Existing positions in the opposite direction are flattened immediately before new orders are placed. The strategy can scale into the same direction on consecutive bars while the urgency window is open.

This port keeps the behaviour contrarian: new highs trigger long trades whereas fresh lows trigger shorts, mimicking the original setup.

How it Works

  1. Two rolling indicators (Highest and Lowest) approximate the MetaTrader ZigZag depth logic.
  2. Whenever price prints a new extreme above/below those bands and the move exceeds Deviation (in price steps), a pivot is recorded.
  3. The algorithm tracks how many bars passed since the pivot. Once the counter exceeds Urgency the signal expires.
  4. On every closed candle during the active window the strategy enters using VolumePerTrade. Opposite exposure is closed first, so flip trades happen cleanly.

Parameters

Parameter Default Description
Depth 17 Window in bars to look back for swing highs/lows. Mirrors the ZigZag depth input.
Deviation 7 Minimum price displacement in points (multiplied by the symbol price step) required to accept a new pivot.
Backstep 5 Bars that must elapse before the indicator may switch to the opposite pivot direction.
Urgency 2 Maximum number of bars after the pivot when entries are allowed.
SignalReverse false Flips the mapping of highs/lows to long/short signals.
CandleType 5 minute candles Timeframe used for the analysis. Adjust to the chart you want to mirror.
VolumePerTrade 0.10 Order size submitted on every entry. Matches the original lot input.

Trading Notes

  • The logic does not include stops or targets. Risk control must be added via overlays or portfolio settings if required.
  • Because the system can add to a position every bar within the urgency window, position size may grow quickly on strong trends.
  • Use higher depths on volatile symbols to avoid excessive pivots. Lower depths make the strategy more reactive but noisier.
  • When SignalReverse is true the behaviour becomes breakout-following: swing highs trigger shorts and swing lows trigger longs.

Files

  • CS/ZigZagEvgeTrofiStrategy.cs – C# implementation of the strategy.
  • Python version is intentionally not provided.
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;
using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;



/// <summary>
/// ZigZag pivot strategy based on the original ZigZagEvgeTrofi expert advisor.
/// Reacts to the most recent zigzag swing and enters within a limited number of bars.
/// </summary>
public class ZigZagEvgeTrofiStrategy : Strategy
{
	private enum PivotTypes
	{
		None,
		High,
		Low
	}

	private readonly StrategyParam<int> _depth;
	private readonly StrategyParam<decimal> _deviation;
	private readonly StrategyParam<int> _backstep;
	private readonly StrategyParam<int> _urgency;
	private readonly StrategyParam<bool> _signalReverse;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _volume;

	private Highest _highest;
	private Lowest _lowest;
	private PivotTypes _pivotType;
	private decimal _pivotPrice;
	private int _barsSincePivot;
	private decimal _priceStep;

	/// <summary>
	/// ZigZag depth parameter controlling the swing detection window.
	/// </summary>
	public int Depth
	{
		get => _depth.Value;
		set => _depth.Value = value;
	}

	/// <summary>
	/// Minimum deviation in price steps required to confirm a new pivot.
	/// </summary>
	public decimal Deviation
	{
		get => _deviation.Value;
		set => _deviation.Value = value;
	}

	/// <summary>
	/// Minimum number of bars between opposite pivot updates.
	/// </summary>
	public int Backstep
	{
		get => _backstep.Value;
		set => _backstep.Value = value;
	}

	/// <summary>
	/// Maximum number of bars after a pivot when entries are allowed.
	/// </summary>
	public int Urgency
	{
		get => _urgency.Value;
		set => _urgency.Value = value;
	}

	/// <summary>
	/// Reverses the direction of the generated signals.
	/// </summary>
	public bool SignalReverse
	{
		get => _signalReverse.Value;
		set => _signalReverse.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Trading volume submitted on every entry.
	/// </summary>
	public decimal VolumePerTrade
	{
		get => _volume.Value;
		set => _volume.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="ZigZagEvgeTrofiStrategy"/> class.
	/// </summary>
	public ZigZagEvgeTrofiStrategy()
	{
		_depth = Param(nameof(Depth), 17)
			.SetGreaterThanZero()
			.SetDisplay("Depth", "ZigZag depth parameter", "ZigZag")
			
			.SetOptimize(5, 40, 1);

		_deviation = Param(nameof(Deviation), 7m)
			.SetGreaterThanZero()
			.SetDisplay("Deviation", "Minimum price movement in points", "ZigZag")
			
			.SetOptimize(1m, 20m, 1m);

		_backstep = Param(nameof(Backstep), 5)
			.SetGreaterThanZero()
			.SetDisplay("Backstep", "Bars to lock a pivot before switching", "ZigZag")
			
			.SetOptimize(1, 15, 1);

		_urgency = Param(nameof(Urgency), 2)
			.SetNotNegative()
			.SetDisplay("Urgency", "Maximum bars to use the latest signal", "Trading")
			
			.SetOptimize(0, 5, 1);

		_signalReverse = Param(nameof(SignalReverse), false)
			.SetDisplay("Signal Reverse", "Flip long and short entries", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for analysis", "General");

		_volume = Param(nameof(VolumePerTrade), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume per trade", "Trading");
	}

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

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

		_highest = null;
		_lowest = null;
		_pivotType = PivotTypes.None;
		_pivotPrice = 0m;
		_barsSincePivot = int.MaxValue;
		_priceStep = 0m;
	}

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

		_priceStep = GetEffectivePriceStep();
		_highest = new Highest { Length = Depth };
		_lowest = new Lowest { Length = Depth };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_highest, _lowest, ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal highestValue, decimal lowestValue)
	{
		// Skip unfinished candles to ensure decisions are made on closed bars only.
		if (candle.State != CandleStates.Finished)
			return;

		// Wait until both indicators are fully formed before reacting.
		if (_highest == null || _lowest == null || !_highest.IsFormed || !_lowest.IsFormed)
			return;

		// Increment the bar counter that measures freshness of the latest pivot.
		if (_pivotType != PivotTypes.None && _barsSincePivot < int.MaxValue)
			_barsSincePivot++;

		var deviationPrice = Math.Max(GetDeviationInPrice(), _priceStep);
		var canSwitch = _pivotType == PivotTypes.None || _barsSincePivot >= Backstep;

		// Detect a fresh swing high if price pushes above the tracked maximum.
		if (candle.HighPrice >= highestValue && highestValue > 0m)
		{
			var difference = candle.HighPrice - _pivotPrice;
			if ((_pivotType != PivotTypes.High && canSwitch) || (_pivotType == PivotTypes.High && difference >= deviationPrice))
				SetPivot(PivotTypes.High, candle.HighPrice);
		}
		// Detect a fresh swing low when price dips under the tracked minimum.
		else if (candle.LowPrice <= lowestValue && lowestValue > 0m)
		{
			var difference = _pivotPrice - candle.LowPrice;
			if ((_pivotType != PivotTypes.Low && canSwitch) || (_pivotType == PivotTypes.Low && difference >= deviationPrice))
				SetPivot(PivotTypes.Low, candle.LowPrice);
		}

		if (_pivotType == PivotTypes.None)
			return;

		var isBuySignal = _pivotType == PivotTypes.High ? !SignalReverse : SignalReverse;

		// Close opposite exposure before entering in the new direction.
		if (isBuySignal)
		{
			if (Position < 0)
			{
				var closeVolume = Math.Abs(Position);
				if (closeVolume > 0m)
					BuyMarket(closeVolume);
			}
		}
		else
		{
			if (Position > 0)
			{
				var closeVolume = Math.Abs(Position);
				if (closeVolume > 0m)
					SellMarket(closeVolume);
			}
		}

		// Enter the market while the pivot is still considered fresh.
		if (_barsSincePivot > Urgency)
			return;

		var volume = VolumePerTrade;
		if (volume <= 0m)
			return;

		if (isBuySignal)
			BuyMarket(volume);
		else
			SellMarket(volume);
	}

	// Update the stored pivot information when a new swing is confirmed.
	private void SetPivot(PivotTypes type, decimal price)
	{
		_pivotType = type;
		_pivotPrice = price;
		_barsSincePivot = 0;
	}

	// Convert the deviation input expressed in points to a price value.
	private decimal GetDeviationInPrice()
	{
		return Deviation * _priceStep;
	}

	// Determine the effective price step for translating point-based parameters.
	private decimal GetEffectivePriceStep()
	{
		if (Security?.PriceStep is > 0m)
			return Security.PriceStep.Value;

		return 1m;
	}
}