View on GitHub

ZigAndZag Scalpel Strategy

Overview

ZigAndZagScalpelStrategy is a StockSharp port of the MetaTrader 4 "ZigAndZag" toolkit (folder 8304). The original package combines a custom indicator and an expert advisor. Two ZigZag windows are used:

  • KeelOver – a long lookback swing detector that marks the dominant trend.
  • Slalom – a short lookback swing detector that defines actionable breakouts.

When the long-term ZigZag flips upward the strategy looks for the next Slalom low and waits for price to rise a configurable number of points above that pivot. A buy market order is issued once the breakout distance is met. A symmetrical rule opens a short position when the KeelOver trend turns down, the Slalom prints a fresh high, and price drops below it. Positions can optionally be closed as soon as the opposite Slalom pivot is confirmed, mimicking the indicator's limit-arrow removal.

The implementation keeps the daily trade limiter from the expert advisor. Only a configurable number of trades is allowed per trading day, resetting automatically at midnight (exchange time). This reproduces the "new day" flag from the original code.

How it works

  1. Subscribe to the primary candle stream defined by CandleType.
  2. Feed two ZigZagIndicator instances:
    • Depth = KeelOverLength for the trend detector.
    • Depth = SlalomLength for entry signals.
  3. Track the most recent KeelOver pivot to determine whether the trend is up (last pivot is a low) or down (last pivot is a high).
  4. When the Slalom indicator publishes a new pivot, arm a breakout in that direction.
  5. Calculate the weighted price (5×Close + 2×Open + High + Low) / 9. If price moves more than BreakoutDistancePoints (converted into price units) away from the pivot while the trend supports the move, execute a market order.
  6. Close existing positions when the global trend flips or the opposite Slalom pivot appears and CloseOnOppositePivot is enabled.
  7. Reset the daily trade counter at every calendar day change.

The parameters DeviationPoints and Backstep are shared between both ZigZag instances so the swing structure matches the MetaTrader indicator buffers.

Parameters

Name Default Description
CandleType 15m Primary timeframe used to build both ZigZag ladders.
KeelOverLength 55 Long-term ZigZag lookback that defines the trend (original KeelOver).
SlalomLength 17 Short-term ZigZag lookback used for entries (original Slalom).
DeviationPoints 5 Minimum swing size in points before a new ZigZag pivot is confirmed.
Backstep 3 Required bar distance between consecutive pivots.
BreakoutDistancePoints 2 Distance from a pivot (in points) before firing an order.
MaxTradesPerDay 1 Maximum number of entries per calendar day. Mirrors the original newday flag.
CloseOnOppositePivot true Close open positions when the Slalom ZigZag produces the opposite swing.

All point-based parameters are converted to price units using Security.PriceStep. If the instrument has no price step configured, a value of 1 is used to keep the strategy functional during testing.

Usage notes

  • The strategy operates with market orders (BuyMarket / SellMarket). Attach your own risk rules or stop-loss helpers if tighter risk management is required.
  • Because both ZigZag indicators share the same candle stream, make sure the chosen CandleType is supported by your data adapter.
  • MaxTradesPerDay = 1 reproduces the "one trade per day" behaviour. Increase the value if you need multiple entries during the same session.
  • Set CloseOnOppositePivot = false to keep positions open until the global trend reverses instead of reacting to every short-term swing.

Differences vs. the MT4 expert advisor

  • The MetaTrader version placed pending limit arrows. The StockSharp port executes breakouts with immediate market orders to stay within the high-level API.
  • Risk management, lot sizing and partial closes are intentionally omitted. Use StockSharp position sizing helpers if you need advanced capital control.
  • Indicator buffers 4/5/6 are replaced by direct strategy logic and chart annotations via DrawIndicator and DrawOwnTrades.
  • Add stop-loss and take-profit parameters tied to ATR or recent ZigZag swings.
  • Overlay the original indicator with BreakoutDistancePoints = 0 to visualize the raw pivot ladder.
  • Combine with a session filter (IsFormedAndOnlineAndAllowTrading) to limit trading hours.
namespace StockSharp.Samples.Strategies;

using System;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// ZigAndZagScalpel translation that trades on breakouts from short-term pivots confirmed by a long-term ZigZag trend.
/// </summary>
public class ZigAndZagScalpelStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maxTradesPerDay;
	private readonly StrategyParam<bool> _closeOnOppositePivot;

	private decimal _previousMajorPivot;
	private decimal _lastMajorPivot;
	private decimal _previousMinorPivot;
	private decimal _lastMinorPivot;
	private DateTime _currentDay = DateTime.MinValue;
	private int _tradesToday;
	private bool _trendUp;
	private PivotTypes _lastMinorPivotType = PivotTypes.None;
	private bool _minorPivotUsed;

	/// <summary>
	/// Initializes a new instance of the <see cref="ZigAndZagScalpelStrategy"/> class.
	/// </summary>
	public ZigAndZagScalpelStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for all calculations", "General");

		_maxTradesPerDay = Param(nameof(MaxTradesPerDay), 1)
			.SetDisplay("Max Trades Per Day", "Daily limit matching the original expert advisor", "Trading");

		_closeOnOppositePivot = Param(nameof(CloseOnOppositePivot), true)
			.SetDisplay("Close On Opposite Pivot", "Exit when the entry ZigZag prints the opposite swing", "Risk");
	}

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

	/// <summary>
	/// Maximum number of trades allowed per trading day.
	/// </summary>
	public int MaxTradesPerDay
	{
		get => _maxTradesPerDay.Value;
		set => _maxTradesPerDay.Value = value;
	}

	/// <summary>
	/// Determines whether open positions should be closed on the opposite entry pivot.
	/// </summary>
	public bool CloseOnOppositePivot
	{
		get => _closeOnOppositePivot.Value;
		set => _closeOnOppositePivot.Value = value;
	}

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

		_previousMajorPivot = 0m;
		_lastMajorPivot = 0m;
		_previousMinorPivot = 0m;
		_lastMinorPivot = 0m;
		_currentDay = DateTime.MinValue;
		_tradesToday = 0;
		_trendUp = false;
		_lastMinorPivotType = PivotTypes.None;
		_minorPivotUsed = false;
	}

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

		var majorZigZag = new ZigZag { Deviation = 0.02m };
		var minorZigZag = new ZigZag { Deviation = 0.005m };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindWithEmpty(majorZigZag, minorZigZag, ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal? majorValue, decimal? minorValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		UpdateDailyCounter(candle.OpenTime);

		if (majorValue is not null)
			UpdateMajorTrend(majorValue.Value);

		if (minorValue is not null)
			UpdateMinorPivot(minorValue.Value);

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		ManageExistingPosition();

		if (Position != 0)
			return;

		if (_minorPivotUsed)
			return;

		if (_lastMinorPivotType == PivotTypes.None)
			return;

		if (_tradesToday >= MaxTradesPerDay)
			return;

		var navel = CalculateNavel(candle);

		if (_lastMinorPivotType == PivotTypes.Low && _trendUp)
		{
			if (navel > _lastMinorPivot)
			{
				BuyMarket();
				_minorPivotUsed = true;
				_tradesToday++;
			}
		}
		else if (_lastMinorPivotType == PivotTypes.High && !_trendUp)
		{
			if (navel < _lastMinorPivot)
			{
				SellMarket();
				_minorPivotUsed = true;
				_tradesToday++;
			}
		}
	}

	private void UpdateDailyCounter(DateTime time)
	{
		var date = time.Date;
		if (date == _currentDay)
			return;

		_currentDay = date;
		_tradesToday = 0;
	}

	private void UpdateMajorTrend(decimal majorValue)
	{
		if (_lastMajorPivot == 0m)
		{
			_lastMajorPivot = majorValue;
			_previousMajorPivot = majorValue;
			return;
		}

		if (majorValue == _lastMajorPivot)
			return;

		_previousMajorPivot = _lastMajorPivot;
		_lastMajorPivot = majorValue;
		_trendUp = _lastMajorPivot < _previousMajorPivot;
	}

	private void UpdateMinorPivot(decimal minorValue)
	{
		if (_lastMinorPivot == 0m)
		{
			_lastMinorPivot = minorValue;
			_previousMinorPivot = minorValue;
			_lastMinorPivotType = PivotTypes.Low;
			_minorPivotUsed = false;
			return;
		}

		if (minorValue == _lastMinorPivot)
			return;

		_previousMinorPivot = _lastMinorPivot;
		_lastMinorPivot = minorValue;
		_lastMinorPivotType = _lastMinorPivot < _previousMinorPivot ? PivotTypes.Low : PivotTypes.High;
		_minorPivotUsed = false;
	}

	private void ManageExistingPosition()
	{
		if (Position > 0)
		{
			if (!_trendUp || (CloseOnOppositePivot && _lastMinorPivotType == PivotTypes.High))
				SellMarket(Position);
		}
		else if (Position < 0)
		{
			if (_trendUp || (CloseOnOppositePivot && _lastMinorPivotType == PivotTypes.Low))
				BuyMarket(Position.Abs());
		}
	}

	private static decimal CalculateNavel(ICandleMessage candle)
	{
		return (5m * candle.ClosePrice + 2m * candle.OpenPrice + candle.HighPrice + candle.LowPrice) / 9m;
	}

	private enum PivotTypes
	{
		None,
		Low,
		High
	}
}