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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import AverageTrueRange
class fractal_zig_zag_strategy(Strategy):
def __init__(self):
super(fractal_zig_zag_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(8))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._level = self.Param("Level", 2) \
.SetDisplay("Fractal Depth", "Candles on each side to confirm fractal", "Signals")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period for stops", "Indicators")
self._window = []
self._trend = 0 # 1=bearish (last was high), 2=bullish (last was low)
self._prev_trend = 0
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def Level(self):
return self._level.Value
@property
def AtrLength(self):
return self._atr_length.Value
def OnStarted2(self, time):
super(fractal_zig_zag_strategy, self).OnStarted2(time)
self._window = []
self._trend = 0
self._prev_trend = 0
self._entry_price = 0.0
self._atr = AverageTrueRange()
self._atr.Length = self.AtrLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._atr, self.ProcessCandle).Start()
def ProcessCandle(self, candle, atr_val):
if candle.State != CandleStates.Finished:
return
av = float(atr_val)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
depth = max(1, self.Level)
window_size = depth * 2 + 1
self._window.append((high, low))
while len(self._window) > window_size:
self._window.pop(0)
# Evaluate fractals
if len(self._window) >= window_size:
center_index = len(self._window) - 1 - depth
center = self._window[center_index]
is_high = True
is_low = True
for i in range(len(self._window)):
if i == center_index:
continue
if self._window[i][0] >= center[0]:
is_high = False
if self._window[i][1] <= center[1]:
is_low = False
if not is_high and not is_low:
break
if is_high:
self._trend = 1
if is_low:
self._trend = 2
if av <= 0 or self._trend == 0:
self._prev_trend = self._trend
return
# Exit management
if self.Position > 0:
if close <= self._entry_price - av * 2.0 or close >= self._entry_price + av * 3.0 or self._trend == 1:
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
if close >= self._entry_price + av * 2.0 or close <= self._entry_price - av * 3.0 or self._trend == 2:
self.BuyMarket()
self._entry_price = 0.0
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_trend = self._trend
return
# Entry on trend change
if self.Position == 0 and self._prev_trend != 0 and self._trend != self._prev_trend:
if self._trend == 2:
self._entry_price = close
self.BuyMarket()
elif self._trend == 1:
self._entry_price = close
self.SellMarket()
self._prev_trend = self._trend
def OnReseted(self):
super(fractal_zig_zag_strategy, self).OnReseted()
self._window = []
self._trend = 0
self._prev_trend = 0
self._entry_price = 0.0
def CreateClone(self):
return fractal_zig_zag_strategy()