Ver no GitHub

Fractal ZigZag Strategy

This strategy is a direct port of the MetaTrader 4 expert advisor Fractal ZigZag Expert.mq4. It rebuilds the Bill Williams fractal sequence and interprets the most recent confirmed extremum as the active market leg. When the latest valid fractal is a swing low, the system opens a long position; when a swing high is confirmed, it opens a short. The implementation keeps the original parameters — fractal depth, take profit, initial stop and trailing stop distances — while adapting the order routing to the high-level StockSharp API.

The strategy is best suited for H1 candles, replicating the default chart used in the MetaTrader version. Nevertheless, the CandleType parameter allows switching to any other timeframe supported by the data feed. All distances are expressed in price points (instrument price steps), which mirrors the way MetaTrader uses the Point constant.

Trading rules

  • Signal detection
    • The algorithm scans each finished candle and builds a rolling window with 2 * Level + 1 elements.
    • A high fractal is confirmed when the middle candle has the highest high inside that window; a low fractal requires the lowest low.
    • Only the latest confirmed fractal controls the direction: a low sets the internal trend to 2 (bullish), a high sets it to 1 (bearish).
  • Entries
    • When the internal trend equals 2 and there is no open position, a market buy is sent using the Lots volume.
    • When the trend equals 1 with no position, a market sell is sent.
    • The strategy will re-enter in the same direction after a position closes if the trend has not flipped.
  • Exits & risk management
    • Every entry receives an initial stop loss and a fixed take profit defined in points. A stop value of 0 disables the respective protection.
    • Optional trailing stop (also in points) activates once price moves by the configured distance. The stop is then moved to maintain the same offset from the closing price, never crossing the initial protective stop.
    • Protective orders are emulated by monitoring candle highs/lows to approximate intrabar touches, closely matching the original MQL4 logic.

Default parameters

Parameter Default Description
Level 2 Number of candles on each side required to confirm a fractal.
TakeProfitPoints 25 Distance to the take profit target in price points.
InitialStopPoints 20 Distance to the initial stop loss in price points.
TrailingStopPoints 10 Trailing stop distance in price points (set to 0 to disable).
Lots 1 Order volume used for market entries.
CandleType H1 Timeframe of candles processed by the strategy.

Notes

  • The strategy calls StartProtection() once at startup so that StockSharp can manage emergency position liquidation if needed.
  • All logs and comments are provided in English, while descriptions follow the language of each README variant as required by the conversion guidelines.
  • The implementation avoids indicator buffers and mimics the MetaTrader approach by keeping only the minimal rolling window necessary to evaluate a fractal.
using System;
using System.Collections.Generic;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Fractal ZigZag: Confirms Bill Williams fractals then trades
/// in the direction of the last confirmed extremum.
/// Bullish after low fractal, bearish after high fractal.
/// </summary>
public class FractalZigZagStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _level;
	private readonly StrategyParam<int> _atrLength;

	private readonly List<(decimal high, decimal low, DateTimeOffset time)> _window = new();
	private int _trend; // 1=bearish (last was high), 2=bullish (last was low)
	private int _prevTrend;
	private decimal _entryPrice;

	public FractalZigZagStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_level = Param(nameof(Level), 2)
			.SetDisplay("Fractal Depth", "Candles on each side to confirm fractal.", "Signals");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period for stops.", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int Level
	{
		get => _level.Value;
		set => _level.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_window.Clear();
		_trend = 0;
		_prevTrend = 0;
		_entryPrice = 0;
	}

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

		var atr = new AverageTrueRange { Length = AtrLength };

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

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

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

		// Update fractal window
		var depth = Math.Max(1, Level);
		var windowSize = depth * 2 + 1;

		_window.Add((candle.HighPrice, candle.LowPrice, candle.OpenTime));
		while (_window.Count > windowSize)
			_window.RemoveAt(0);

		// Evaluate fractals
		if (_window.Count >= windowSize)
		{
			var centerIndex = _window.Count - 1 - depth;
			var center = _window[centerIndex];
			var isHigh = true;
			var isLow = true;

			for (var i = 0; i < _window.Count; i++)
			{
				if (i == centerIndex)
					continue;

				if (_window[i].high >= center.high)
					isHigh = false;
				if (_window[i].low <= center.low)
					isLow = false;

				if (!isHigh && !isLow)
					break;
			}

			if (isHigh)
				_trend = 1; // bearish: last fractal was a high
			if (isLow)
				_trend = 2; // bullish: last fractal was a low
		}

		if (atrVal <= 0 || _trend == 0)
		{
			_prevTrend = _trend;
			return;
		}

		var close = candle.ClosePrice;

		// Exit management
		if (Position > 0)
		{
			if (close <= _entryPrice - atrVal * 2m || close >= _entryPrice + atrVal * 3m || _trend == 1)
			{
				SellMarket();
				_entryPrice = 0;
			}
		}
		else if (Position < 0)
		{
			if (close >= _entryPrice + atrVal * 2m || close <= _entryPrice - atrVal * 3m || _trend == 2)
			{
				BuyMarket();
				_entryPrice = 0;
			}
		}

		// Entry on trend change
		if (Position == 0 && _prevTrend != 0 && _trend != _prevTrend)
		{
			if (_trend == 2)
			{
				_entryPrice = close;
				BuyMarket();
			}
			else if (_trend == 1)
			{
				_entryPrice = close;
				SellMarket();
			}
		}

		_prevTrend = _trend;
	}
}